elanous 0.2.15 → 0.2.17

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 (309) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1307-33ffcc49ab45123b.js → 1307-aac7332576c4d6fc.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-0e2c57fe6add4b2a.js → page-3143b07a817d1da9.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-17ba086ef82d339b.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-6e5c3a867557298b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-9a41d65355fc25b9.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/control/{page-1d4901eb7c2d8822.js → page-41afec0b255ed2bb.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-4187666132fc98c4.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-09328d499162bae4.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-29ff3a8f84f03b6a.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-80ec7401bc83f511.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-fc7d86467291ae38.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-de43a186e5ba5683.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-043a9111e74f6b24.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-23a4601ceb7f452e.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-10d958605a77b3bd.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-a04260b8ccc968d4.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-12916918988ac97e.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-5a64cb5208c6a8fd.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-1407ec83ea64c1f9.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-7f49fe645d5132f7.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/trace/{page-73735b64bfe1ae67.js → page-424659bacfb8cdd7.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-fc2df261f8a2c7df.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-5d4fef9ccd7d406b.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +1 -0
  55. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  56. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  57. package/apps/pwa/out/approvals/index.html +1 -1
  58. package/apps/pwa/out/approvals/index.txt +15 -15
  59. package/apps/pwa/out/autopilot/index.html +1 -1
  60. package/apps/pwa/out/autopilot/index.txt +15 -15
  61. package/apps/pwa/out/board/index.html +1 -1
  62. package/apps/pwa/out/board/index.txt +15 -15
  63. package/apps/pwa/out/botlab/index.html +1 -1
  64. package/apps/pwa/out/botlab/index.txt +15 -15
  65. package/apps/pwa/out/bots/index.html +1 -1
  66. package/apps/pwa/out/bots/index.txt +15 -15
  67. package/apps/pwa/out/ceo/index.html +1 -1
  68. package/apps/pwa/out/ceo/index.txt +15 -15
  69. package/apps/pwa/out/chat/index.html +1 -1
  70. package/apps/pwa/out/chat/index.txt +15 -15
  71. package/apps/pwa/out/consult/index.html +1 -1
  72. package/apps/pwa/out/consult/index.txt +15 -15
  73. package/apps/pwa/out/control/index.html +1 -1
  74. package/apps/pwa/out/control/index.txt +15 -15
  75. package/apps/pwa/out/design-check/index.html +1 -1
  76. package/apps/pwa/out/design-check/index.txt +15 -15
  77. package/apps/pwa/out/editor/index.html +1 -1
  78. package/apps/pwa/out/editor/index.txt +15 -15
  79. package/apps/pwa/out/exec/index.html +1 -1
  80. package/apps/pwa/out/exec/index.txt +15 -15
  81. package/apps/pwa/out/field/index.html +1 -1
  82. package/apps/pwa/out/field/index.txt +15 -15
  83. package/apps/pwa/out/healthz +1 -1
  84. package/apps/pwa/out/index.html +1 -1
  85. package/apps/pwa/out/index.txt +15 -15
  86. package/apps/pwa/out/inside/index.html +1 -1
  87. package/apps/pwa/out/inside/index.txt +15 -15
  88. package/apps/pwa/out/intake/index.html +1 -1
  89. package/apps/pwa/out/intake/index.txt +15 -15
  90. package/apps/pwa/out/live/index.html +1 -1
  91. package/apps/pwa/out/live/index.txt +15 -15
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +15 -15
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +15 -15
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +15 -15
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +15 -15
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +15 -15
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +15 -15
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +15 -15
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +16 -16
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +15 -15
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +15 -15
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +15 -15
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +15 -15
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +15 -15
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +15 -15
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +15 -15
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +15 -15
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +15 -15
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +15 -15
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +15 -15
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +15 -15
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +15 -15
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +15 -15
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +15 -15
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +15 -15
  146. package/graphs/doc-gen/doc-gen.yaml +24 -0
  147. package/graphs/doc-gen/recipes.yaml +3 -0
  148. package/graphs/intake/intake-daily.yaml +9 -2
  149. package/graphs/intake/news-sources.yaml +4 -0
  150. package/graphs/intake/recipes.yaml +6 -1
  151. package/graphs/orchestrator/orchestrator.yaml +7 -0
  152. package/graphs/orchestrator/recipes.yaml +3 -0
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/__fixtures__/intake-news.xml +7 -0
  156. package/scripts/ci-public-export-import-gate.ts +154 -0
  157. package/scripts/ci-test-interference-gate.ts +5 -1
  158. package/scripts/ci-typecheck-changed.ts +34 -15
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/goals/goal-census.ts +343 -0
  163. package/scripts/intake-news.ts +169 -0
  164. package/scripts/measure/author-section-contribution.ts +84 -0
  165. package/scripts/release-loop/cut-branch.ts +43 -13
  166. package/scripts/release-loop/docs-land-node.ts +69 -10
  167. package/scripts/release-loop/gate-node.ts +3 -1
  168. package/scripts/release-loop/publish-node.ts +2 -2
  169. package/scripts/release-loop/release-docs.ts +134 -0
  170. package/scripts/release-loop/vault-note-node.ts +19 -2
  171. package/scripts/release-loop/version-node.ts +7 -4
  172. package/scripts/release-story/draft.ts +42 -14
  173. package/scripts/rhythm/monthly.ts +97 -0
  174. package/src/acp/client.ts +17 -2
  175. package/src/agent-mission/driver.ts +104 -11
  176. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  177. package/src/agent-mission/supervisor-metrics.ts +218 -0
  178. package/src/autopilot/track-agent-shadow.ts +2 -1
  179. package/src/autopilot/track-agent.ts +5 -3
  180. package/src/briefing/brief-items.ts +191 -36
  181. package/src/budget/usage-outcomes.ts +204 -0
  182. package/src/cli/autopilot-cli.ts +66 -0
  183. package/src/cli/brief-cli.ts +73 -0
  184. package/src/cli/decisions-cli.ts +4 -1
  185. package/src/cli/intake-cli.ts +1 -1
  186. package/src/cli/know-cli.ts +20 -0
  187. package/src/cli/log-event-names.ts +1 -0
  188. package/src/cli/logs-cli.ts +43 -1
  189. package/src/cli/pr-cli.ts +92 -13
  190. package/src/cli/provider-cli.ts +14 -0
  191. package/src/cli/release-cli.ts +11 -8
  192. package/src/cli/self-update-canary.ts +46 -0
  193. package/src/cli/self-update.ts +399 -1
  194. package/src/cli/usage-cli.ts +26 -0
  195. package/src/context-card/index.ts +45 -0
  196. package/src/context-card/task.ts +76 -0
  197. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  198. package/src/decisions/decision-ledger.ts +10 -1
  199. package/src/discord-self-message.ts +9 -0
  200. package/src/flow-loop/split.ts +24 -0
  201. package/src/harness/harness-cli-command.ts +178 -12
  202. package/src/harness/harness-pod-dispatch.ts +36 -55
  203. package/src/harness/harness-queue.ts +86 -7
  204. package/src/harness/harness-seams.ts +13 -3
  205. package/src/harness/helper-repair.ts +139 -8
  206. package/src/harness/helper-scan.ts +13 -2
  207. package/src/harness/nested-elanous-policy.ts +133 -0
  208. package/src/hq/fence-wrapper.ts +123 -0
  209. package/src/hq/hq.ts +16 -2
  210. package/src/hq/lease.ts +5 -3
  211. package/src/hq/transfer.ts +185 -0
  212. package/src/index.ts +168 -13
  213. package/src/intake-plane/digest.ts +85 -28
  214. package/src/knowledge/know-find.ts +105 -0
  215. package/src/llm.ts +75 -19
  216. package/src/loops/orchestrator/finish-rate.ts +14 -3
  217. package/src/loops/orchestrator/tick.ts +96 -25
  218. package/src/maturity/bot-slash-catalog.ts +11 -2
  219. package/src/maturity/feature-maturity.ts +2 -2
  220. package/src/maturity/tui-slash-availability.ts +9 -0
  221. package/src/nexus/api/grid.ts +61 -0
  222. package/src/nexus/api/http-server.ts +19 -1
  223. package/src/nexus/api/task-cards-api.ts +19 -0
  224. package/src/oauth/codex.ts +82 -2
  225. package/src/oauth/overload-failover-launch.ts +75 -0
  226. package/src/oauth/overload-failover.ts +137 -0
  227. package/src/release-loop/checklist.ts +10 -5
  228. package/src/release-loop/feature-store.ts +10 -9
  229. package/src/release-loop/landing-freeze.ts +18 -10
  230. package/src/release-loop/placement.ts +17 -8
  231. package/src/seat-dispatch/seat-ask.ts +33 -6
  232. package/src/seat-loop/seat-loop.ts +172 -21
  233. package/src/self-dev/ask-launch-flow.ts +48 -8
  234. package/src/self-dev/authored-goal-trace.ts +64 -0
  235. package/src/self-dev/dev-cli.ts +19 -1
  236. package/src/self-dev/dev-pipeline.ts +6 -5
  237. package/src/self-dev/entrance-registry.ts +10 -6
  238. package/src/self-dev/old-door.ts +73 -0
  239. package/src/self-dev/run-store.ts +5 -1
  240. package/src/self-dev/run-supervisor.ts +7 -1
  241. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  242. package/src/self-implement/app-api-budget.ts +166 -0
  243. package/src/self-implement/budget-gate.ts +7 -5
  244. package/src/self-implement/follow-up-goals.ts +289 -0
  245. package/src/self-implement/frozen-merges.ts +25 -20
  246. package/src/self-implement/goal-author-clarification.ts +81 -0
  247. package/src/self-implement/goal-author-cli.ts +33 -5
  248. package/src/self-implement/goal-author.ts +107 -25
  249. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  250. package/src/self-implement/goal-document-write.ts +28 -0
  251. package/src/self-implement/heal-intake.ts +4 -0
  252. package/src/self-implement/heal-triage.ts +44 -0
  253. package/src/self-implement/host-regate.ts +3 -1
  254. package/src/self-implement/orchestrator.ts +157 -8
  255. package/src/self-implement/run-ledger.ts +4 -0
  256. package/src/self-implement/seams.ts +44 -15
  257. package/src/session-runtime/index.ts +2 -0
  258. package/src/session-runtime/retry-api.ts +87 -0
  259. package/src/session-runtime/retry-policy.ts +107 -6
  260. package/src/steward/working-backwards.ts +16 -1
  261. package/src/task-agent/actions.ts +118 -0
  262. package/src/task-agent/judge.ts +96 -0
  263. package/src/task-cards/card-cli.ts +25 -0
  264. package/src/task-cards/card-store.ts +9 -4
  265. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
  266. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  267. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  268. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  269. package/src/task-orchestrator/surfaces/pod-pool.ts +95 -18
  270. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  271. package/src/task-orchestrator/surfaces/self-implement-pod.ts +168 -37
  272. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  273. package/src/telegram-commands.ts +6 -1
  274. package/src/telegram-loops-command.ts +21 -14
  275. package/src/telegram.ts +47 -0
  276. package/src/user-config.ts +59 -4
  277. package/src/version/packed-revision.json +1 -1
  278. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/term/page-f5597e894c02cd33.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  305. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  306. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  307. package/src/launch-head/seat-goal-queue.ts +0 -198
  308. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
  309. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { join } from 'node:path';
4
- import { effectiveInstanceRoot } from '../instance/resolve.js';
4
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
5
5
 
6
6
  export interface LandingFreeze {
7
7
  reason: string;
@@ -60,15 +60,23 @@ function mergesDir(root: string): string { return join(root, 'landing-merges');
60
60
 
61
61
  export interface LandingMergeGuard { frozen: LandingFreeze | null; end: () => void }
62
62
 
63
- export function beginLandingMerge(root = effectiveInstanceRoot(), now = new Date()): LandingMergeGuard {
64
- const dir = mergesDir(root);
65
- mkdirSync(dir, { recursive: true });
66
- const marker = join(dir, `${process.pid}-${randomUUID()}.json`);
67
- writeFileSync(marker, `${JSON.stringify({ pid: process.pid, startedAt: now.toISOString() })}\n`, { mode: 0o600 });
68
- const end = () => { rmSync(marker, { force: true }); };
63
+ /** Mark both authorities before reading either freeze: the host and the child each drain their own in-flight merges. */
64
+ export function beginLandingMerge(root = effectiveInstanceRoot(), now = new Date(), prodFreezeRoot = prodInstanceRoot(), forceReason?: string): LandingMergeGuard {
65
+ const markers: string[] = [];
66
+ const end = () => { for (const marker of markers) rmSync(marker, { force: true }); };
67
+ try {
68
+ for (const authority of new Set([prodFreezeRoot, root])) {
69
+ const dir = mergesDir(authority);
70
+ mkdirSync(dir, { recursive: true });
71
+ const marker = join(dir, `${process.pid}-${randomUUID()}.json`);
72
+ markers.push(marker);
73
+ writeFileSync(marker, `${JSON.stringify({ pid: process.pid, startedAt: now.toISOString() })}\n`, { mode: 0o600 });
74
+ }
75
+ } catch (error) { end(); throw error; }
69
76
  let frozen: LandingFreeze | null;
70
- try { frozen = readLandingFreeze(root, now); } catch (error) { end(); throw error; }
71
- if (frozen) { end(); return { frozen, end: () => {} }; }
77
+ try { frozen = readLandingFreeze(prodFreezeRoot, now) ?? (root === prodFreezeRoot ? null : readLandingFreeze(root, now)); }
78
+ catch (error) { end(); throw error; }
79
+ if (frozen && !forceReason?.trim()) { end(); return { frozen, end: () => {} }; }
72
80
  return { frozen: null, end };
73
81
  }
74
82
 
@@ -10,7 +10,7 @@ import { move, releasedVersion } from './feature-store.js';
10
10
  import { listSchedules, type ReleaseSchedule } from './release-schedule.js';
11
11
 
12
12
  export type PlacementPriority = 'P0' | 'P1' | 'P2';
13
- export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }
13
+ export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }
14
14
  export interface PlacementDecision { id: string; from: string | null; version: string; reason: string; displaced: Array<{ id: string; from: string; to: string; reason: string }> }
15
15
  export interface RebalanceResult {
16
16
  decisions: PlacementDecision[];
@@ -122,6 +122,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
122
122
  validateCeoLoad(input);
123
123
  const seat = parseOwner(input.owner).seat;
124
124
  if (!['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
125
+ if (input.accelerator !== undefined && input.accelerator !== true && input.accelerator !== false) throw new CliUserError('가속 등급은 true 이거나 생략한다');
125
126
  const now = (deps.now ?? new Date()).getTime();
126
127
  const schedules = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
127
128
  const released = deps.released ?? releasedVersion();
@@ -142,6 +143,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
142
143
  if (from && released && versionOrder(from, released) <= 0) throw new CliUserError(`이미 발행된 판의 칸은 배치하지 않는다: ${input.id}`);
143
144
  if (existing?.status === 'done') throw new CliUserError(`끝난 칸은 배치하지 않는다: ${input.id}`);
144
145
  if (existing && (existing.title !== input.title || existing.owner !== input.owner)) throw new CliUserError(`기존 칸 제목·담당 불일치: ${input.id}`);
146
+ const accelerator = input.accelerator === true || (input.accelerator === undefined && existing?.accelerator === true);
145
147
  const load = { id: input.id, ceoMinutes: input.ceoMinutes ?? existing?.ceoMinutes, ceoDate: input.ceoDate ?? existing?.ceoDate };
146
148
  const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
147
149
  const predecessors = input.predecessors.map((id) => {
@@ -170,6 +172,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
170
172
  let chosen: ReleaseSchedule | undefined;
171
173
  let loadIssue: string | null = null;
172
174
  const feasible = candidates.filter((row) => !dependencyViolation(snapshots, locations, new Map([[input.id, row.version]]), input));
175
+ // 판은 «마감 안 가장 이른 판»부터 본다(기존 불변식). 가속 등급은 그 판이 찼을 때 같은 우선순위 비가속 칸을 밀 수 있게만 한다.
173
176
  for (const row of feasible) {
174
177
  const placement = new Map([[input.id, row.version]]);
175
178
  const items = occupied(row.version);
@@ -180,11 +183,15 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
180
183
  chosen = row;
181
184
  break;
182
185
  }
183
- if (input.priority !== 'P1' || from === row.version) { loadIssue ??= directIssue; continue; }
186
+ const mayDisplace = input.priority === 'P1' ? from !== row.version : accelerator && from !== row.version;
187
+ if (!mayDisplace) { loadIssue ??= directIssue; continue; }
184
188
  const next = open.find((later) => versionOrder(later.version, row.version) > 0);
185
189
  if (!next || items.length - 1 >= capacity(row)) { loadIssue ??= directIssue; continue; }
190
+ const victimOk = (priority: PlacementPriority | undefined) => (input.priority === 'P1' && priority === 'P2')
191
+ || (accelerator && priority === input.priority);
186
192
  const p2 = [...items].reverse().find((item) => {
187
- if (item.priority !== 'P2' || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input)) return false;
193
+ if (item.accelerator === true) return false;
194
+ if (!victimOk(item.priority) || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input)) return false;
188
195
  if (item.deadlineVersion && versionOrder(next.version, item.deadlineVersion) > 0) return false;
189
196
  if (items.length < capacity(row) && !(seatItems.length >= seatLimit && ownerMatches(item.owner, seat))) return false;
190
197
  if (!item.owner) return false;
@@ -202,22 +209,24 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
202
209
  return true;
203
210
  });
204
211
  if (p2) {
205
- displaced.push({ id: p2.id, from: row.version, to: next.version, reason: `P1 ${input.id} 마감 판 ${row.version} 용량 확보를 위해 P2 이월` });
212
+ displaced.push({ id: p2.id, from: row.version, to: next.version, reason: accelerator
213
+ ? `가속 등급 ${input.id} 를 같은 우선순위 안에서 먼저 두기 위해 ${p2.priority ?? 'P2'} ${p2.id} 이월`
214
+ : `P1 ${input.id} 마감 판 ${row.version} 용량 확보를 위해 P2 이월` });
206
215
  chosen = row;
207
216
  break;
208
217
  }
209
218
  loadIssue ??= directIssue;
210
219
  }
211
220
  if (!chosen) throw new CliUserError(loadIssue ?? `${input.id} 배치할 판이 없다 — 용량·선행·마감·동결을 확인하라`);
212
- const reason = `${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
221
+ const reason = `${accelerator ? '가속 등급 · ' : ''}${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'}${accelerator ? ' · 같은 우선순위 안에서 가속 등급을 먼저' : ''} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
213
222
  const decision = { id: input.id, from, version: chosen.version, reason, displaced };
214
223
  if (!deps.dryRun) {
215
224
  const by = deps.by ?? 'OP';
216
225
  for (const shifted of displaced) move(shifted.id, shifted.from, shifted.to, by, undefined, undefined, shifted.reason);
217
226
  if (from && from !== chosen.version) move(input.id, from, chosen.version, by, undefined, undefined, reason);
218
- if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion) || (input.ceoMinutes !== undefined && existing.ceoMinutes !== input.ceoMinutes) || (input.ceoDate !== undefined && existing.ceoDate !== input.ceoDate)))
219
- setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}) }, by);
220
- if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion, ...(load.ceoMinutes !== undefined ? { ceoMinutes: load.ceoMinutes } : {}), ...(load.ceoDate !== undefined ? { ceoDate: load.ceoDate } : {}) });
227
+ if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion) || (input.ceoMinutes !== undefined && existing.ceoMinutes !== input.ceoMinutes) || (input.ceoDate !== undefined && existing.ceoDate !== input.ceoDate) || (input.accelerator !== undefined && (existing?.accelerator === true) !== (input.accelerator === true))))
228
+ setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator !== undefined ? { accelerator: input.accelerator === true ? true : null } : {}) }, by);
229
+ if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion, ...(load.ceoMinutes !== undefined ? { ceoMinutes: load.ceoMinutes } : {}), ...(load.ceoDate !== undefined ? { ceoDate: load.ceoDate } : {}), ...(accelerator ? { accelerator: true } : {}) });
221
230
  }
222
231
  return decision;
223
232
  }
@@ -20,7 +20,7 @@ function createTable(store: MsgStore): void {
20
20
  store.db.exec(`CREATE TABLE IF NOT EXISTS seat_asks (
21
21
  id TEXT PRIMARY KEY, seat TEXT NOT NULL, origin TEXT NOT NULL,
22
22
  deadline INTEGER NOT NULL, timeout_minutes INTEGER NOT NULL DEFAULT 120, status TEXT NOT NULL DEFAULT 'pending',
23
- answer TEXT, answered_at INTEGER
23
+ answer TEXT, answered_at INTEGER, asked_at INTEGER
24
24
  ); CREATE TABLE IF NOT EXISTS seat_ask_outbox (
25
25
  id TEXT PRIMARY KEY, origin TEXT NOT NULL, text TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
26
26
  claim_until INTEGER NOT NULL DEFAULT 0, claim_token TEXT
@@ -28,6 +28,7 @@ function createTable(store: MsgStore): void {
28
28
  const askColumns = store.db.query('PRAGMA table_info(seat_asks)').all() as Array<{ name: string }>;
29
29
  if (!askColumns.some(({ name }) => name === 'answer')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answer TEXT');
30
30
  if (!askColumns.some(({ name }) => name === 'answered_at')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answered_at INTEGER');
31
+ if (!askColumns.some(({ name }) => name === 'asked_at')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN asked_at INTEGER');
31
32
  const columns = store.db.query('PRAGMA table_info(seat_ask_outbox)').all() as Array<{ name: string }>;
32
33
  if (!columns.some(({ name }) => name === 'claim_until')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_until INTEGER NOT NULL DEFAULT 0');
33
34
  if (!columns.some(({ name }) => name === 'claim_token')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_token TEXT');
@@ -87,8 +88,9 @@ export async function askSeat(text: string, origin: AskOrigin, command: CeoComma
87
88
  const store = (deps.open ?? openMsgStore)();
88
89
  try {
89
90
  createTable(store);
90
- store.db.query('INSERT INTO seat_asks (id, seat, origin, deadline, timeout_minutes, status) VALUES (?, ?, ?, ?, ?, ?)')
91
- .run(id, 'TC', JSON.stringify(origin), (deps.now ?? Date.now)() + timeout, minutes, 'pending');
91
+ const askedAt = (deps.now ?? Date.now)();
92
+ store.db.query('INSERT INTO seat_asks (id, seat, origin, deadline, timeout_minutes, status, asked_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
93
+ .run(id, 'TC', JSON.stringify(origin), askedAt + timeout, minutes, 'pending', askedAt);
92
94
  } finally { store.close(); }
93
95
  try {
94
96
  const result = await (deps.dispatch ?? dispatchCeoTask)('TC', `질문: ${body}\n답장 요청: ${id}\n회신: elanous seat answer ${id} "<답>"`, command,
@@ -119,12 +121,25 @@ export function answerSeatAsk(id: string, text: string, deps: Pick<SeatAskDeps,
119
121
  } finally { store.close(); }
120
122
  }
121
123
 
124
+ /** Return only the recorded reply's return address and measured ask time. */
125
+ export function seatAskReplyInfo(id: string, open: () => MsgStore = openMsgStore): { origin: AskOrigin; askedAt: number | null; answeredAt: number } {
126
+ const store = open();
127
+ try {
128
+ createTable(store);
129
+ const row = store.db.query('SELECT origin, asked_at, answered_at FROM seat_asks WHERE id = ? AND answer IS NOT NULL')
130
+ .get(id) as { origin: string; asked_at: number | null; answered_at: number } | null;
131
+ if (!row) throw new Error(`seat ask answer not recorded: ${id}`);
132
+ return { origin: JSON.parse(row.origin) as AskOrigin, askedAt: row.asked_at, answeredAt: row.answered_at };
133
+ } finally { store.close(); }
134
+ }
135
+
122
136
  /** Deliver recorded seat answers or expiration notices via the original surface. */
123
- export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
137
+ export async function deliverSeatAnswers(deps: SeatAskDeps & { askId?: string }): Promise<void> {
124
138
  const store = (deps.open ?? openMsgStore)();
125
139
  try {
126
140
  createTable(store);
127
- const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE status = 'pending'").all() as AskRow[];
141
+ const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE status = 'pending' AND (? IS NULL OR id = ?)")
142
+ .all(deps.askId ?? null, deps.askId ?? null) as AskRow[];
128
143
  const decide = store.db.transaction((id: string, now: number): 'answered' | 'expired' | null => {
129
144
  const ask = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE id = ? AND status = 'pending'").get(id) as AskRow | null;
130
145
  if (!ask) return null;
@@ -150,7 +165,8 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
150
165
  const status = decide(ask.id, (deps.now ?? Date.now)());
151
166
  if (status) debug.log('seat.ask', status, { seat: ask.seat, id: ask.id, via: origin.channel });
152
167
  }
153
- const outbox = store.db.query("SELECT id, origin, text FROM seat_ask_outbox WHERE status = 'pending'").all() as Array<{ id: string; origin: string; text: string }>;
168
+ const outbox = store.db.query("SELECT id, origin, text FROM seat_ask_outbox WHERE status = 'pending' AND (? IS NULL OR id = ?)")
169
+ .all(deps.askId ?? null, deps.askId ?? null) as Array<{ id: string; origin: string; text: string }>;
154
170
  for (const item of outbox) {
155
171
  const origin = JSON.parse(item.origin) as AskOrigin;
156
172
  if (deps.channel && origin.channel !== deps.channel) continue;
@@ -177,6 +193,17 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
177
193
  } finally { store.close(); }
178
194
  }
179
195
 
196
+ /** A pending outbox with a live claim is being delivered by another worker. */
197
+ export function seatAskDeliveryInProgress(id: string, open: () => MsgStore = openMsgStore): boolean {
198
+ const store = open();
199
+ try {
200
+ createTable(store);
201
+ const row = store.db.query("SELECT claim_until FROM seat_ask_outbox WHERE id = ? AND status = 'pending' AND claim_token IS NOT NULL")
202
+ .get(id) as { claim_until: number } | null;
203
+ return row !== null && row.claim_until > Date.now();
204
+ } finally { store.close(); }
205
+ }
206
+
180
207
  /** Confirm only this browser's displayed answers; polling alone never removes them. */
181
208
  export function acknowledgeSeatAnswers(clientId: string, ids: string[], open: () => MsgStore = openMsgStore): void {
182
209
  const store = open();
@@ -10,7 +10,7 @@ import { surfaceEventsDbPath } from '../domains/surface-events.js';
10
10
  import { judgeNeighbor } from '../loops/neighbors.js';
11
11
  import { addHarnessQueue, harnessQueueIdForKey, harnessQueueOutcome, listHarnessQueue } from '../harness/harness-queue.js';
12
12
  import { isRescueAllowedAction } from '../steward/rescue.js';
13
- import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
13
+ import { DecisionLedger, type DecisionEntry, type DecisionOption, type Recommendation } from '../decisions/decision-ledger.js';
14
14
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
15
15
  import { loopEvent } from '../loops/observe.js';
16
16
  import { getUserConfig, type PersonaLoopConfig, type SeatLoopConfig, type UserConfig } from '../user-config.js';
@@ -43,7 +43,7 @@ export type TcCandidate =
43
43
  | { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
44
44
  | { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
45
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 };
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 };
46
+ export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'held' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'resolved' | '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
47
  export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off'; modeDowngradeReason?: string };
48
48
 
49
49
  export function seatLoopTickLine(result: SeatLoopResult): string {
@@ -53,6 +53,8 @@ export function seatLoopTickLine(result: SeatLoopResult): string {
53
53
  }
54
54
  export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
55
55
  export type SeatDeps = {
56
+ /** Version stamp for decision cards the seat loop raises. Default = the ledger's resolver (`git reflog` on the repo, up to 10 s) — tests inject a fixed one. */
57
+ resolveDecisionVersion?: (at: string) => { released: string | null; dev: string | null; codename: string | null };
56
58
  personaConfig?: PersonaLoopConfig;
57
59
  personaRegistry?: PersonaRegistry;
58
60
  personaDir?: string;
@@ -256,6 +258,59 @@ function queueKey(seat: string, item: SeatItem): string {
256
258
  return `seat-loop:${seat}:${createHash('sha256').update(handledItemKey(item)).digest('hex')}`;
257
259
  }
258
260
 
261
+ /** A decision ledger that raises cards — forwards the injected version resolver so tests never run the repo's `git reflog`. */
262
+ function cardLedger(root: string, deps: SeatDeps, now?: Date): DecisionLedger {
263
+ return new DecisionLedger({ stateDir: root, ...(now ? { now: () => now } : {}), ...(deps.resolveDecisionVersion ? { resolveVersion: deps.resolveDecisionVersion } : {}) });
264
+ }
265
+
266
+ const DEFAULT_REPEAT_STOP = 3;
267
+
268
+ function repeatStopLimit(config: SeatLoopConfig): number {
269
+ const value = config.repeatStop;
270
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : DEFAULT_REPEAT_STOP;
271
+ }
272
+
273
+ /** A finished attempt that never landed (no merged-PR evidence). `attempting` is the intent row of the same launch. A `refused` row whose reason is the AUTOQ retry note is the reconcile of a queued row, not a second try. */
274
+ function unlandedAttempt(entry: SeatEntry): boolean {
275
+ if (!entry.item || entry.action === 'seat-question' || entry.action === 'seat-answer' || entry.inquiry) return false;
276
+ if (entry.status === 'launched' || entry.status === 'hitl' || entry.status === 'held' || entry.status === 'attempting') return false;
277
+ if (entry.status === 'refused' && entry.reason === 'AUTOQ failed or cancelled; eligible for retry') return false;
278
+ return entry.status === 'queued' || entry.status === 'refused' || entry.status === 'outcome-unknown';
279
+ }
280
+
281
+ function repeatStopReason(attempts: number): string {
282
+ return `같은 칸 ${attempts}회 착지 0 — 사람 판단`;
283
+ }
284
+
285
+ function personReopened(ledger: readonly SeatEntry[], seat: string, item: SeatItem, root: string): boolean {
286
+ const key = handledItemKey(item);
287
+ const lastHold = [...ledger].reverse().find((entry) => entry.seat === seat && entry.item && handledItemKey(entry.item) === key && entry.status === 'held');
288
+ if (!lastHold) return false;
289
+ const heldAt = Date.parse(lastHold.at || lastHold.ts || '');
290
+ if (!Number.isFinite(heldAt)) return false;
291
+ const ref = decisionRef(seat, item);
292
+ return new DecisionLedger({ stateDir: root }).list({ status: 'all' }).some((card) => {
293
+ if (!card.refs?.includes(ref) || card.status !== 'decided' || !card.decidedAt) return false;
294
+ const decidedAt = Date.parse(card.decidedAt);
295
+ return Number.isFinite(decidedAt) && decidedAt > heldAt;
296
+ });
297
+ }
298
+
299
+ /** Prior attempts of this seat and handled item that finished without a landing. A queued row whose queue is still retryable has not finished. Queue rows with the same key count when the ledger has no receipt. */
300
+ function unlandedAttemptCount(seat: string, item: SeatItem, ledger: readonly SeatEntry[], root: string, deps: SeatDeps): number {
301
+ const key = handledItemKey(item);
302
+ const idem = queueKey(seat, item);
303
+ const outcome = deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot }));
304
+ const finished = (entry: SeatEntry) => unlandedAttempt(entry) && !(entry.status === 'queued' && entry.queueId && outcome(entry.queueId, root) === 'retryable');
305
+ const fromLedger = ledger.filter((entry) => entry.seat === seat && entry.item && handledItemKey(entry.item) === key && finished(entry));
306
+ const seen = new Set(fromLedger.flatMap((entry) => entry.queueId ? [entry.queueId] : []));
307
+ let rows: ReturnType<typeof listHarnessQueue> = [];
308
+ try { rows = (deps.queueItems ?? ((stateRoot: string) => listHarnessQueue({ root: stateRoot })))(root); }
309
+ catch (error) { observe('repeat-stop-queue-unreadable', { seat, item: item.id, error: String(error).slice(0, 200) }); }
310
+ const fromQueue = rows.filter((row) => row.seat === seat && row.idempotencyKey === idem && row.status === 'finished' && !seen.has(row.id));
311
+ return fromLedger.length + fromQueue.length;
312
+ }
313
+
259
314
  /** A crash (or a failed write) between AUTOQ enqueue and the `queued` row leaves an `attempting`/`outcome-unknown` row with no queue id. Find its queue row by the
260
315
  * seat-loop idempotency key so the outcome below (retry · launched · still queued) applies to it too (LOOP-LIVE1 review must-fix). */
261
316
  function recoverAttemptingQueueIds(ledger: readonly SeatEntry[], root: string, deps: SeatDeps): SeatEntry[] {
@@ -320,6 +375,23 @@ function decisionSummary(text: string): string {
320
375
  return Array.from(sentences).slice(0, 240).join('').trim();
321
376
  }
322
377
 
378
+ function decisionCardMaterial(input: {
379
+ what: string; soFar: string; current: string; question: string; answer: string;
380
+ options: DecisionOption[]; recommendation: Recommendation; ifUntouched: string; evidence: string; confidence: string;
381
+ }): { scqa: { s: string; c: string; q: string; a: string }; pendingQuestion: string } {
382
+ const short = (value: string) => decisionSummary(value) || '관측되지 않음';
383
+ return {
384
+ scqa: { s: short(input.what), c: short(input.current), q: short(input.question), a: short(input.answer) },
385
+ pendingQuestion: [
386
+ `무엇: ${input.what}`, `지금까지: ${input.soFar}`, `지금 상태 재측: ${input.current}`,
387
+ ...input.options.map((option) => `선택지 ${option.key} (${option.label}) 결과: ${option.consequence}`),
388
+ 'skipped' in input.recommendation ? `권고 비움: ${input.recommendation.reason}` : `권고: ${input.recommendation.option} — ${input.recommendation.why}`,
389
+ `확신: ${input.confidence}`, `그냥 두면: ${input.ifUntouched}`, `근거: ${input.evidence}`,
390
+ `Q: ${input.question}`, `A: ${input.answer}`,
391
+ ].join('\n'),
392
+ };
393
+ }
394
+
323
395
  function decisionSourceRef(seat: string, item: SeatItem): string {
324
396
  return item.source === 'request' && item.id.startsWith('coord:') ? item.id : decisionRef(seat, item);
325
397
  }
@@ -333,12 +405,12 @@ function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEn
333
405
  function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string, liveSafe = false): Set<string> {
334
406
  const inquiryRow = (entry: SeatEntry) => entry.action === 'seat-question' || entry.action === 'seat-answer' || !!entry.inquiry;
335
407
  return new Set(ledger.filter((entry, index) => entry.status === 'asked' || entry.status === 'answered'
336
- || entry.status === 'resolved-by-neighbor'
408
+ || entry.status === 'resolved-by-neighbor' || entry.status === 'resolved'
337
409
  // RM3a: only TC's other-seat defect ask-shadows are deduplicated here — an ordinary inquiry recorded in shadow must still be delivered once questions turn on.
338
410
  || (entry.status === 'shadow' && entry.candidate?.kind === 'other-seat-cell-defect')
339
411
  || (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
340
412
  || (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
341
- || (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown'))
413
+ || (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown' || (entry.status === 'held' && !personReopened(ledger, entry.seat, entry.item!, root ?? ''))))
342
414
  || (!inquiryRow(entry) && entry.status === 'attempting' && !(entry.item?.source === 'checklist' && ledger.slice(index + 1).some((later) =>
343
415
  later.seat === entry.seat && later.status === 'launched' && later.item?.source === 'checklist'
344
416
  && later.item.version === entry.item?.version && later.item.id === entry.item?.id
@@ -798,7 +870,7 @@ const SEAT_ACTION_RULES: ReadonlyArray<{ kind: 'wait' | 'harness' | 'decision';
798
870
  { kind: 'wait', signal: /실측\s*대기|(?:사람|실제)\s*기기(?!\s*(?:(?:실측|측정|확인)\s*)?완료)(?:\s*(?:실측|측정|확인|대기))?|실물\s*측정(?!\s*완료)/iu, source: 'content', resolvedBy: /(?:실측|측정|(?:사람|실제)\s*기기|실물\s*측정)\s*완료/iu },
799
871
  { kind: 'wait', signal: /\u{1F451}\s*확인\s*대기/iu, source: 'content', resolvedBy: /\u{1F451}\s*확인\s*완료/iu },
800
872
  { kind: 'decision', signal: TRACK_AGENT_FORBIDDEN_ACTION_REGEX, source: 'task' },
801
- { kind: 'decision', signal: /(?:마켓|레지스트리)(?:에|로)?\s*(?:게시|등록|발행)|사이트\s*운영\s*반영|SNS(?:에|로)?\s*(?:게시|발행|업로드)|공개\s*발행/iu, source: 'content' },
873
+ { kind: 'decision', signal: /(?:마켓|레지스트리)(?:에|로)?\s*(?:게시|등록|발행)|사이트\s*운영\s*반영|SNS(?:에|로)?\s*(?:게시|발행|업로드)|공개\s*발행/iu, source: 'task' },
802
874
  { kind: 'harness', signal: /(?:마켓|레지스트리|사이트|SNS|공개)?(?:에|로)?\s*(?:게시|발행|배포|등록|운영\s*반영)(?:를?\s*위한)?\s*(?:준비|초안|원고|자료|구현|점검|테스트)|(?:마켓|레지스트리|사이트|SNS|공개)?(?:에|로)?\s*(?:게시|발행|배포|등록)(?:용|를?\s*위한)\s*(?:초안|원고|자료|코드|기능)/iu, source: 'task' },
803
875
  ];
804
876
 
@@ -806,9 +878,9 @@ export function planAction(item: SeatItem, seat?: string): { kind: 'decision' |
806
878
  const task = `${item.id}\n${item.title}\n${item.text}`;
807
879
  const content = `${task}\n${item.evidence ?? ''}`;
808
880
  const preparation = SEAT_ACTION_RULES.find(({ kind }) => kind === 'harness')!;
809
- const prepared = [...content.matchAll(new RegExp(preparation.signal.source, `${preparation.signal.flags.replace('g', '')}g`))];
881
+ const prepared = [...item.text.matchAll(new RegExp(preparation.signal.source, `${preparation.signal.flags.replace('g', '')}g`))];
810
882
  const rule = SEAT_ACTION_RULES.map(({ kind, signal, source, resolvedBy }) => {
811
- const text = source === 'content' ? content : task;
883
+ const text = kind === 'decision' ? item.text : source === 'content' ? content : task;
812
884
  if (resolvedBy?.test(item.evidence ?? '')) return null;
813
885
  const hits = [...text.matchAll(new RegExp(signal.source, `${signal.flags.replace('g', '')}g`))];
814
886
  const hit = kind === 'decision' ? hits.find((match) => !prepared.some((prep) =>
@@ -881,13 +953,16 @@ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps
881
953
  if (judgment.state === 'absent') {
882
954
  if (config.mode === 'live-safe' && judgment.action?.action === 'escalate' && isRescueAllowedAction('decision-card')) {
883
955
  const ref = `seat-loop:neighbor:${seat}:${neighbor.id}:${lastSeenAt}`;
884
- new DecisionLedger({ stateDir: root, now: () => now }).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
885
- scqa: { s: `${seat} 이웃 ${neighbor.id}의 마지막 맥락 버스 사건: ${lastSeenAt}.`,
886
- c: `설정한 heartbeat ${neighbor.heartbeat.everyMinutes}분 × ${neighbor.heartbeat.missedTicks}회 동안 새 사건이 없다.`,
887
- q: '결측 이웃의 업무를 어떻게 처리할까?' },
888
- options: [{ key: 'a', label: '사람 판단 후 별도 조치', consequence: '자리 루프는 대행하지 않는다' },
889
- { key: 'b', label: '현상 유지', consequence: '결측 상태를 계속 관측한다' }],
890
- recommendation: { skipped: true, reason: '결측만으로 대행이나 보류를 자동 결정하지 않는다' },
956
+ const options = [{ key: 'a', label: '사람 판단 후 별도 조치', consequence: '자리 루프는 대행하지 않는다' },
957
+ { key: 'b', label: '현상 유지', consequence: '결측 상태를 계속 관측한다' }];
958
+ const recommendation = { option: 'b', why: '새 사건을 확인하기 전에는 이웃 업무를 대행하지 않는다' };
959
+ cardLedger(root, deps, now).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
960
+ ...decisionCardMaterial({ what: `${seat} 이웃 ${neighbor.id} 결측`, soFar: '마지막 맥락 버스 사건 이후 새 ACK 없음',
961
+ current: `현재 시각 ${now.toISOString()}; 마지막 사건 ${lastSeenAt}; heartbeat ${neighbor.heartbeat.everyMinutes}분 × ${neighbor.heartbeat.missedTicks}회 결측`,
962
+ question: '결측 이웃의 업무를 어떻게 처리할까?', answer: '새 ACK까지 현상 유지하고 이웃 업무는 대행하지 않는다',
963
+ options, recommendation, confidence: '중간 — 사건의 부재만 관측됨',
964
+ ifUntouched: '결측 상태를 계속 관측하며 대행은 하지 않는다', evidence: `맥락 버스 마지막 사건 ${lastSeenAt}; ${ref}` }),
965
+ options, recommendation,
891
966
  raisedBy: { agent: 'seat-loop' }, refs: [ref], raisedAt: now.toISOString(),
892
967
  crossCheckSkipped: '이웃 결측으로 교차 확인 불가' }, ref);
893
968
  action = 'decision-card';
@@ -957,10 +1032,17 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
957
1032
  const question = proposal.inquiry?.question ?? proposal.item!.title;
958
1033
  const note = `${proposal.xcheckNote ?? ''} — 해결 가능하다고 했으나 ${waited}분 안 해소 안 됨`.trim();
959
1034
  if (!decisionReceipt(stateRoot, seat, proposal.item!)) {
1035
+ const options = [{ key: 'a', label: '승인', consequence: '사람 판단으로 진행' }, { key: 'b', label: '보류', consequence: '진행하지 않음' }];
1036
+ const recommendation = { option: 'b', why: `기한 ${waited}분 뒤에도 ${cell?.status ?? '질문 미해결'}이므로 착지 확인 전에는 진행하지 않는다` };
1037
+ const material = decisionCardMaterial({ what: `${seat} 질문 · ${proposal.item!.title}`, soFar: `이웃 ${neighbor} 해결 제안: ${proposal.xcheckNote ?? '기록 없음'}`,
1038
+ current: `기한 ${waited}분 경과 · 칸 상태 ${cell?.status ?? '질문 미해결'}`, question, answer: '재측상 미해결이므로 보류를 권고한다',
1039
+ options, recommendation, confidence: '중간 — 기한 경과와 미해결을 재측함', ifUntouched: '질문/칸은 미해결로 남는다',
1040
+ evidence: `${proposal.item!.evidence ?? '원천 근거 미기재'} · ${note} · ${decisionRef(seat, proposal.item!)}` });
960
1041
  await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${question}`, '--category', 'other',
961
- '--s', `${seat} 질문 · ${proposal.item!.title}`, '--c', '이웃 자리 해결 제안이 기한 안에 해소되지 않았다',
1042
+ '--s', material.scqa.s, '--c', material.scqa.c, '--q', material.scqa.q, '--a', material.scqa.a,
1043
+ '--pending-question', material.pendingQuestion,
962
1044
  '--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
963
- '--skip-recommend', '근거가 부족하다', '--xcheck', `${neighbor}:${note}`,
1045
+ '--recommend', recommendation.option, '--why', recommendation.why, '--xcheck', `${neighbor}:${note}`,
964
1046
  '--agent', 'seat-loop', '--ref', decisionRef(seat, proposal.item!), '--json']);
965
1047
  }
966
1048
  const entry: SeatEntry = { ...proposal, ts: now.toISOString(), at: now.toISOString(), status: 'hitl' };
@@ -1258,10 +1340,18 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1258
1340
  const check = answer ? ['--xcheck', `${requestedNeighbor}:${answer.note}`,
1259
1341
  ...(answer.agree ? [] : ['--dissent', `${requestedNeighbor}: ${answer.note}`])]
1260
1342
  : ['--no-xcheck', `이웃 ${requestedNeighbor} 무응답 ${entry.xcheckWaitMinutes}분`];
1343
+ const options = [{ key: 'a', label: '승인', consequence: '사람 판단으로 진행' }, { key: 'b', label: '보류', consequence: '진행하지 않음' }];
1344
+ const recommendation = { option: 'b', why: '교차 확인에서 해소 근거가 확보되지 않았다' };
1345
+ const material = decisionCardMaterial({ what: `${seat} 질문 · ${item.title}`, soFar: `이웃 ${requestedNeighbor}에 교차 확인 요청`,
1346
+ current: answer ? `이웃 응답: ${answer.note}` : `이웃 ${requestedNeighbor} 무응답 ${entry.xcheckWaitMinutes}분`,
1347
+ question: ask.question, answer: '해소 근거 확인 전까지 보류를 권고한다', options, recommendation,
1348
+ confidence: answer ? '중간 — 이웃 응답을 확인함' : '낮음 — 기한 내 이웃 응답 없음',
1349
+ ifUntouched: '사람 판단 전 집행하지 않는다', evidence: `${item.evidence ?? '원천 근거 미기재'} · ${decisionRef(seat, item)}` });
1261
1350
  await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
1262
- '--s', `${seat} 질문 · ${item.title}`, '--c', '판단 근거 부족 — 사람의 결정이 필요하다',
1351
+ '--s', material.scqa.s, '--c', material.scqa.c, '--q', material.scqa.q, '--a', material.scqa.a,
1352
+ '--pending-question', material.pendingQuestion,
1263
1353
  '--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
1264
- '--skip-recommend', '근거가 부족하다', ...check, '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
1354
+ '--recommend', recommendation.option, '--why', recommendation.why, ...check, '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
1265
1355
  if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
1266
1356
  entry.status = 'hitl';
1267
1357
  xcheckEvent('raised', seat, requestedNeighbor, itemKey(item), answer ? answer.agree ? 'agree' : 'dissent' : 'timed-out');
@@ -1387,11 +1477,27 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1387
1477
  if (action.kind === 'decision') {
1388
1478
  const title = `${seat}: ${decisionSummary(action.text)}`;
1389
1479
  const source = decisionSourceRef(seat, item);
1480
+ const options = [{ key: 'a', label: '사람 승인', consequence: '승인 후 별도로 집행' }, { key: 'b', label: '보류', consequence: '집행하지 않음' }];
1481
+ const recommendation = { option: 'b', why: `요청에서 걸린 문면 «${action.reason}»은 자동 집행 대상이 아니므로 보류한다` };
1482
+ let currentStatus: string | undefined;
1483
+ try {
1484
+ currentStatus = item.source === 'checklist' && item.version
1485
+ ? (deps.checklistItems ?? ((version: string) => listChecklist(version, deps.root ?? releaseLedgerRoot()).items))(item.version)
1486
+ .find((cell) => cell.id === item.id && cell.owner === seat)?.status
1487
+ : item.source === 'request' ? listSeatRequests(stateRoot).find((row) => row.key === item.id && row.seat === seat)?.status
1488
+ : undefined;
1489
+ } catch (error) { observe('decision-source-unreadable', { seat, item: item.id, error: String(error).slice(0, 200) }); }
1490
+ const material = decisionCardMaterial({ what: `${seat} 배정 항목: ${item.title} 역할: ${inputs.role || '역할 근거 없음'}`, soFar: `요청 내용: ${item.text}`,
1491
+ current: `밑바탕 재측: ${currentStatus ?? '상태 미확인'}; 요청에서 금지 문면 «${action.reason}» 발견 — 실행 전 사람 승인 필요`,
1492
+ question: `금지 문면 «${action.reason}»이 걸린 요청을 승인할까?`, answer: '승인 전에는 집행하지 않고 보류한다',
1493
+ options, recommendation, confidence: '높음 — 요청에 금지 문면 직접 일치',
1494
+ ifUntouched: '자동 집행되지 않고 해당 칸은 보류 상태로 남는다',
1495
+ evidence: `${item.evidence?.trim() || '추가 근거 미기재'} · 요청 ${source} · 걸린 낱말 ${action.reason}` });
1390
1496
  const raised = await run(['decisions', 'raise', '--title', title, '--category', 'other',
1391
- '--s', decisionSummary(`${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')} 역할: ${inputs.role}`),
1392
- '--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
1497
+ '--s', material.scqa.s, '--c', material.scqa.c, '--q', material.scqa.q, '--a', material.scqa.a,
1498
+ '--pending-question', material.pendingQuestion,
1393
1499
  '--option', 'a=사람 승인:승인 후 별도로 집행', '--option', 'b=보류:집행하지 않음',
1394
- '--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②',
1500
+ '--recommend', recommendation.option, '--why', recommendation.why, '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②',
1395
1501
  '--agent', 'seat-loop', '--ref', source, '--json']);
1396
1502
  const decision: unknown = JSON.parse(raised.trim());
1397
1503
  if (!decision || typeof decision !== 'object' || typeof (decision as { id?: unknown }).id !== 'string') {
@@ -1403,6 +1509,51 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1403
1509
  return entry;
1404
1510
  }
1405
1511
  if (config.mode === 'live-safe') {
1512
+ const limit = repeatStopLimit(config);
1513
+ const attempts = unlandedAttemptCount(seat, item, ledger, stateRoot, deps);
1514
+ if (attempts >= limit && !personReopened(ledger, seat, item, stateRoot)) {
1515
+ // Re-read the underlying item at the decision boundary, not the picked snapshot.
1516
+ const current = item.source === 'checklist' && item.version
1517
+ ? (deps.checklistItems ?? ((version: string) => listChecklist(version, deps.root ?? releaseLedgerRoot()).items))(item.version)
1518
+ .find((cell) => cell.id === item.id && cell.owner === seat)?.status
1519
+ : item.source === 'request' ? listSeatRequests(stateRoot).find((row) => row.key === item.id && row.seat === seat)?.status
1520
+ : undefined;
1521
+ if (current === 'green' || current === 'done') {
1522
+ entry.status = 'resolved';
1523
+ entry.reason = `밑바탕 재측: ${current} — 반복 정지 카드 불필요`;
1524
+ append(path, entry);
1525
+ observe('repeat-recovered', { seat, item: item.id, status: current });
1526
+ return entry;
1527
+ }
1528
+ const reason = repeatStopReason(attempts);
1529
+ entry.status = 'held';
1530
+ entry.reason = reason;
1531
+ const ref = decisionRef(seat, item);
1532
+ const prior = new DecisionLedger({ stateDir: stateRoot }).list({ status: 'all' }).find((card) => card.refs?.includes(ref) && card.status === 'open');
1533
+ if (!prior) {
1534
+ const options = [
1535
+ { key: 'a', label: '다시 연다', consequence: '결정 뒤에 자리 루프가 한 번 더 시도할 수 있다' },
1536
+ { key: 'b', label: '멈춘다', consequence: '같은 칸은 다시 발사하지 않는다' },
1537
+ ];
1538
+ const recommendation = { option: 'b', why: `착지 없는 ${attempts}회 재시도보다 현재 칸을 멈추는 편이 중복 실행을 피한다` };
1539
+ cardLedger(stateRoot, deps, now).raiseOnce({
1540
+ title: `${seat}: ${item.id} 반복 착지 0`, category: 'other',
1541
+ ...decisionCardMaterial({ what: `${seat} 칸 ${item.id} · ${item.title}`, soFar: `${attempts}회 착지 없이 종료`,
1542
+ current: `발행 직전 밑바탕 재측: ${current ?? '상태 미확인'}; 같은 칸 착지 0`,
1543
+ question: '이 칸을 계속할까, 멈출까?', answer: '중복 발사를 멈추고 사람의 재개 결정을 기다린다',
1544
+ options, recommendation, confidence: current ? '높음 — 밑바탕 상태 재측됨' : '낮음 — 밑바탕 상태 미확인',
1545
+ ifUntouched: '같은 칸은 다시 발사되지 않는다', evidence: `${item.evidence ?? '원천 근거 미기재'} · ${ref} · 완료되지 않은 시도 ${attempts}회` }),
1546
+ options, recommendation,
1547
+ raisedBy: { agent: 'seat-loop' },
1548
+ refs: [ref],
1549
+ raisedAt: now.toISOString(),
1550
+ crossCheckSkipped: '반복 정지는 사람 판단',
1551
+ }, ref);
1552
+ }
1553
+ append(path, entry);
1554
+ observe('repeat-stopped', { seat, item: item.id, attempts, lastQueueId: [...ledger].reverse().find((row) => row.seat === seat && row.item && handledItemKey(row.item) === handledItemKey(item) && row.queueId)?.queueId ?? null });
1555
+ return entry;
1556
+ }
1406
1557
  let queued: { id: string };
1407
1558
  try {
1408
1559
  queued = await (deps.enqueue ?? ((assigned, text, root, idempotencyKey) => addHarnessQueue({ seat: assigned, say: text, idempotencyKey }, { root })))(seat, action.text, stateRoot, queueKey(seat, item));