elanous 0.2.21 → 0.2.22

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 (315) 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-08b716476f1d50af.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{2489-ff55a35b202c0f94.js → 2489-d238d3e2eb5b2bf5.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/3857-5985236d9307f676.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/5633-d6852385000738ef.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/5977-5b9e8a45a5b0c823.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/8857-aa1cc4ac90af4d40.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/9247-55bcfbb0e8057f59.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/approvals/page-88459e0903da2803.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-d781be3e3a9cfd74.js → page-d85c43f46f58716b.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/board/{page-b0a80997903852d8.js → page-4e645600579ae952.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-2b1263165630d9c8.js → page-87edbfd19117823b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/bots/page-7f0c2b7801ac6a54.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/ceo/page-bf1f030a9d1adffd.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-e2d6bed48ac20120.js → page-cbaf66b1e1804adf.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-111cabe4d1fd69ae.js → page-80ed511e02c81412.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/{page-544dd4bd4847eeb7.js → page-9f5610caaa06178c.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-3ee191319bf97d87.js → page-ec8f3575e339de74.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/design-check/page-86d753e460f1a989.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/editor/page-4166a7d51cf0a57d.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/exec/{page-a449f1ed472b81c2.js → page-2a19c5cd3b562580.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/field/{page-9045094d6d3b5cd0.js → page-b6a09cde2e07521a.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/inside/{page-c06673596849ef45.js → page-f7eae1518ca0c353.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/intake/page-456a8573ad811381.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/{layout-9f81233f29dcd409.js → layout-8462d6a543cbb2d9.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/live/page-2974cdfb87c0f9c9.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/live-run/page-f927ffa25f3c86d1.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/loops/page-abd86cd832aaa011.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/market/page-79daf68f6256adaa.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8c3f168fc3f91939.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/morning/{page-7a12f61be4a579d1.js → page-786fb4504d0f2fa9.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/{not-found-c85b0589591a99c9.js → not-found-75209f14597dcb8f.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-725a4da02dd129eb.js → page-bf30affe3a85af7e.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1187a44b7515434b.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-3d0910185a66ed54.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-f401ee1dd5a26e25.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/outputs/page-5dfe7604fd842784.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/{page-21bec4afeb2f0917.js → page-76cf838050a8d681.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-dc39ee7a36d26385.js → page-7ec8e289b4c14df6.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-84ae759f76053672.js → page-8c8ac17d52be2a79.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-3db58ddb61fe2be9.js → page-d359ab048d63a9d9.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-c5315e30f3528dc4.js → page-b8b345c880bf78d1.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/settings/{page-2fe368efb6de3b6a.js → page-f151cc10ad026940.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-9875f103057da433.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/setup/page-9789328af9a9aa62.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/share/{page-c69762987b751174.js → page-bf495a1e284f4e00.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a263d94bca4be22a.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/tasks/page-3962eab8ec58d16e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/term/page-9abcfac07e02a542.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/today/page-bee49f927c333abb.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/trace/page-bac2d56dcb02d955.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/vault/page-937cbcc13176295c.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d09495594f7f480d.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-7f8d7fcdf058a845.js → page-69cb93d1a2e57e0e.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-22163556cd38bc88.js → page-72ec543928d67c3e.js} +1 -1
  57. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-82906eeda0186a7b.js +1 -0
  58. package/apps/pwa/out/_next/static/css/f2bd5257e6af88d8.css +1 -0
  59. package/apps/pwa/out/approvals/index.html +1 -1
  60. package/apps/pwa/out/approvals/index.txt +14 -14
  61. package/apps/pwa/out/autopilot/index.html +1 -1
  62. package/apps/pwa/out/autopilot/index.txt +14 -14
  63. package/apps/pwa/out/board/index.html +1 -1
  64. package/apps/pwa/out/board/index.txt +14 -14
  65. package/apps/pwa/out/botlab/index.html +1 -1
  66. package/apps/pwa/out/botlab/index.txt +14 -14
  67. package/apps/pwa/out/bots/index.html +1 -1
  68. package/apps/pwa/out/bots/index.txt +14 -14
  69. package/apps/pwa/out/ceo/index.html +1 -1
  70. package/apps/pwa/out/ceo/index.txt +14 -14
  71. package/apps/pwa/out/chat/index.html +1 -1
  72. package/apps/pwa/out/chat/index.txt +14 -14
  73. package/apps/pwa/out/consult/index.html +1 -1
  74. package/apps/pwa/out/consult/index.txt +14 -14
  75. package/apps/pwa/out/control/index.html +1 -1
  76. package/apps/pwa/out/control/index.txt +14 -14
  77. package/apps/pwa/out/decisions/index.html +1 -1
  78. package/apps/pwa/out/decisions/index.txt +14 -14
  79. package/apps/pwa/out/design-check/index.html +1 -1
  80. package/apps/pwa/out/design-check/index.txt +14 -14
  81. package/apps/pwa/out/editor/index.html +1 -1
  82. package/apps/pwa/out/editor/index.txt +14 -14
  83. package/apps/pwa/out/exec/index.html +1 -1
  84. package/apps/pwa/out/exec/index.txt +14 -14
  85. package/apps/pwa/out/field/index.html +1 -1
  86. package/apps/pwa/out/field/index.txt +14 -14
  87. package/apps/pwa/out/healthz +1 -1
  88. package/apps/pwa/out/index.html +1 -1
  89. package/apps/pwa/out/index.txt +14 -14
  90. package/apps/pwa/out/inside/index.html +1 -1
  91. package/apps/pwa/out/inside/index.txt +14 -14
  92. package/apps/pwa/out/intake/index.html +1 -1
  93. package/apps/pwa/out/intake/index.txt +14 -14
  94. package/apps/pwa/out/live/index.html +1 -1
  95. package/apps/pwa/out/live/index.txt +14 -14
  96. package/apps/pwa/out/live-run/index.html +1 -1
  97. package/apps/pwa/out/live-run/index.txt +14 -14
  98. package/apps/pwa/out/loops/index.html +1 -1
  99. package/apps/pwa/out/loops/index.txt +14 -14
  100. package/apps/pwa/out/market/index.html +1 -1
  101. package/apps/pwa/out/market/index.txt +14 -14
  102. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  103. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  104. package/apps/pwa/out/morning/index.html +1 -1
  105. package/apps/pwa/out/morning/index.txt +14 -14
  106. package/apps/pwa/out/observatory/index.html +1 -1
  107. package/apps/pwa/out/observatory/index.txt +14 -14
  108. package/apps/pwa/out/ops/checklist/index.html +1 -1
  109. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  110. package/apps/pwa/out/ops/release/index.html +1 -1
  111. package/apps/pwa/out/ops/release/index.txt +14 -14
  112. package/apps/pwa/out/ops/seats/index.html +1 -1
  113. package/apps/pwa/out/ops/seats/index.txt +14 -14
  114. package/apps/pwa/out/outputs/index.html +1 -1
  115. package/apps/pwa/out/outputs/index.txt +14 -14
  116. package/apps/pwa/out/reflection/index.html +1 -1
  117. package/apps/pwa/out/reflection/index.txt +15 -15
  118. package/apps/pwa/out/scheduler/index.html +1 -1
  119. package/apps/pwa/out/scheduler/index.txt +14 -14
  120. package/apps/pwa/out/sessions/index.html +1 -1
  121. package/apps/pwa/out/sessions/index.txt +14 -14
  122. package/apps/pwa/out/settings/devices/index.html +1 -1
  123. package/apps/pwa/out/settings/devices/index.txt +14 -14
  124. package/apps/pwa/out/settings/index.html +1 -1
  125. package/apps/pwa/out/settings/index.txt +14 -14
  126. package/apps/pwa/out/setup/done/index.html +1 -1
  127. package/apps/pwa/out/setup/done/index.txt +14 -14
  128. package/apps/pwa/out/setup/index.html +1 -1
  129. package/apps/pwa/out/setup/index.txt +14 -14
  130. package/apps/pwa/out/share/index.html +1 -1
  131. package/apps/pwa/out/share/index.txt +14 -14
  132. package/apps/pwa/out/showroom/index.html +1 -1
  133. package/apps/pwa/out/showroom/index.txt +14 -14
  134. package/apps/pwa/out/tasks/index.html +1 -1
  135. package/apps/pwa/out/tasks/index.txt +14 -14
  136. package/apps/pwa/out/term/index.html +1 -1
  137. package/apps/pwa/out/term/index.txt +14 -14
  138. package/apps/pwa/out/today/index.html +1 -1
  139. package/apps/pwa/out/today/index.txt +14 -14
  140. package/apps/pwa/out/trace/index.html +1 -1
  141. package/apps/pwa/out/trace/index.txt +14 -14
  142. package/apps/pwa/out/vault/index.html +1 -1
  143. package/apps/pwa/out/vault/index.txt +14 -14
  144. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  145. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  146. package/apps/pwa/out/workflows/index.html +1 -1
  147. package/apps/pwa/out/workflows/index.txt +14 -14
  148. package/apps/pwa/out/workspace/index.html +1 -1
  149. package/apps/pwa/out/workspace/index.txt +14 -14
  150. package/apps/pwa/out/worktrees/index.html +1 -1
  151. package/apps/pwa/out/worktrees/index.txt +14 -14
  152. package/graphs/catalog/catalog.yaml +1 -1
  153. package/graphs/drafts/recipes.yaml +2 -2
  154. package/graphs/test-diet/recipes.yaml +3 -0
  155. package/graphs/test-diet/test-diet.yaml +3 -1
  156. package/package.json +1 -1
  157. package/scripts/f-session-monitor.ts +69 -0
  158. package/scripts/hq/standby-snapshot.ts +7 -3
  159. package/scripts/knowledge-compress.ts +11 -0
  160. package/scripts/nightly-docops.ts +90 -3
  161. package/scripts/research-coo-admin-dates.ts +39 -0
  162. package/scripts/skills/public-check.ts +7 -2
  163. package/scripts/smart-merge/resync-shadow.ts +5 -3
  164. package/scripts/test-diet/lib.ts +81 -1
  165. package/scripts/test-diet/node.ts +48 -2
  166. package/scripts/verify-coo-admin-evidence.ts +83 -0
  167. package/src/acp/client.ts +4 -2
  168. package/src/agent/run-context.ts +4 -0
  169. package/src/agent-env/profile.ts +25 -5
  170. package/src/agent-mission/driver.ts +26 -12
  171. package/src/agent-mission/supervisor-metrics.ts +17 -2
  172. package/src/agent-substrate/acp-reviewer.ts +12 -5
  173. package/src/agent-substrate/pr-reviewer.ts +11 -4
  174. package/src/agent-substrate/review-intent.ts +102 -3
  175. package/src/autopilot/build/llm-conflict-merge.ts +22 -8
  176. package/src/autopilot/pr-manager.ts +18 -8
  177. package/src/away/release-watch.ts +47 -8
  178. package/src/briefing/brief-items.ts +104 -11
  179. package/src/chat/index.ts +1 -0
  180. package/src/cli/brief-cli.ts +20 -2
  181. package/src/cli/config-classify.ts +83 -0
  182. package/src/cli/control-cli.ts +14 -0
  183. package/src/cli/decisions-cli.ts +26 -16
  184. package/src/cli/logs-model-input.ts +1 -1
  185. package/src/cli/pr-cli.ts +65 -8
  186. package/src/cli/release-cli.ts +21 -4
  187. package/src/cli/tasks-cli.ts +108 -1
  188. package/src/connectors/types.ts +6 -6
  189. package/src/context-bus/context-now-surfaces.ts +18 -0
  190. package/src/context-bus/coord-events.ts +32 -4
  191. package/src/context-bus/long-term-memory.ts +23 -6
  192. package/src/context-bus/nightly-condense.ts +169 -21
  193. package/src/control/control-status.ts +87 -0
  194. package/src/control-plane/light-member.ts +5 -3
  195. package/src/control-plane/member.ts +172 -7
  196. package/src/core-turn/run-goal-loop.ts +22 -1
  197. package/src/dashboard/slash-runtime/dashboard-handlers.ts +9 -0
  198. package/src/decisions/decision-cards.ts +20 -1
  199. package/src/decisions/decision-ledger.ts +11 -8
  200. package/src/decisions/web-push-decision-cards.ts +19 -3
  201. package/src/discord-self-message.ts +2 -2
  202. package/src/domains/coo-admin-dates.evidence.json +876 -0
  203. package/src/domains/coo-admin-tool.ts +48 -14
  204. package/src/domains/ops-status.ts +53 -0
  205. package/src/domains/outbound-alert.ts +24 -1
  206. package/src/exec-requests/planner.ts +6 -0
  207. package/src/graph-kinds/registry.ts +6 -6
  208. package/src/graph-runner/graph-cli.ts +34 -2
  209. package/src/graph-runner/graph-run-api.ts +7 -0
  210. package/src/graph-runner/graph-tick.ts +73 -12
  211. package/src/graph-runner/runner.ts +43 -11
  212. package/src/graph-unify/wf-to-graph.ts +79 -0
  213. package/src/graph-wizard/generate.ts +34 -2
  214. package/src/graph-wizard/steps.ts +13 -0
  215. package/src/harness/harness-cli-command.ts +121 -23
  216. package/src/harness/harness-queue.ts +58 -14
  217. package/src/harness/launch-advice.ts +23 -0
  218. package/src/index.ts +36 -7
  219. package/src/knowledge/conclusion-compress.ts +93 -0
  220. package/src/knowledge/know-find.ts +9 -2
  221. package/src/knowledge/query.ts +20 -0
  222. package/src/llm.ts +26 -16
  223. package/src/loops/neighbor-runtime.ts +88 -0
  224. package/src/loops/orchestrator/author-depth.ts +29 -11
  225. package/src/loops/orchestrator/overlap.ts +7 -1
  226. package/src/loops/orchestrator/resync-shadow.ts +1 -1
  227. package/src/loops/orchestrator/tick.ts +35 -2
  228. package/src/loops/orchestrator/traffic.ts +2 -0
  229. package/src/loops/registry.ts +5 -0
  230. package/src/market/publish.ts +17 -1
  231. package/src/maturity/feature-maturity.ts +2 -1
  232. package/src/mss/logging/redaction.ts +17 -0
  233. package/src/nexus/api/draft-metrics.ts +22 -8
  234. package/src/nexus/api/graph-peer-edit.ts +77 -0
  235. package/src/nexus/api/graph-wizard-api.ts +9 -1
  236. package/src/nexus/api/graphs-api.ts +145 -7
  237. package/src/nexus/api/http-server.ts +93 -8
  238. package/src/nexus/api/plugins-market.ts +24 -0
  239. package/src/nexus/index.ts +3 -2
  240. package/src/oauth/claude-pty-maturity.ts +24 -0
  241. package/src/oauth/fallback-chain.ts +27 -14
  242. package/src/plugins/maker/market-bundle.ts +9 -0
  243. package/src/plugins/maker/plugin-maker.ts +9 -3
  244. package/src/plugins/maker/wizard-generate.ts +73 -9
  245. package/src/policy/llm-policy.ts +11 -6
  246. package/src/release-loop/checklist-board.ts +27 -0
  247. package/src/release-loop/checklist.ts +38 -0
  248. package/src/seat-address/seat-address.ts +1 -0
  249. package/src/seat-loop/seat-loop.ts +174 -20
  250. package/src/seat-loop/seat-report.ts +29 -1
  251. package/src/self-dev/dev-cli.ts +90 -8
  252. package/src/self-dev/dev-pipeline.ts +20 -1
  253. package/src/self-dev/draft-sweep.ts +45 -8
  254. package/src/self-dev/orchestrate.ts +3 -0
  255. package/src/self-dev/review-provider-fallback.ts +83 -0
  256. package/src/self-dev/run-supervisor.ts +5 -1
  257. package/src/self-implement/harness-policy-gates.ts +7 -5
  258. package/src/self-implement/host-regate.ts +28 -14
  259. package/src/self-implement/l8-merge-queue.ts +80 -8
  260. package/src/self-implement/orchestrator.ts +68 -27
  261. package/src/self-implement/seams.ts +5 -3
  262. package/src/self-implement/self-implement-cli.ts +4 -0
  263. package/src/shell-env-bootstrap.ts +19 -4
  264. package/src/steward/triage.ts +25 -8
  265. package/src/storage/file-lock.ts +33 -4
  266. package/src/task-agent/completion-evidence.ts +31 -3
  267. package/src/task-agent/cover.ts +61 -0
  268. package/src/task-agent/live-moves.ts +126 -3
  269. package/src/task-agent/shadow.ts +19 -4
  270. package/src/task-agent/task-hand.ts +2 -0
  271. package/src/task-orchestrator/surfaces/pod-pool.ts +138 -15
  272. package/src/task-orchestrator/surfaces/self-implement.ts +7 -0
  273. package/src/telegram-commands.ts +8 -0
  274. package/src/telegram-project-command.ts +6 -0
  275. package/src/telegram.ts +2 -2
  276. package/src/typecheck-ratchet.ts +2 -1
  277. package/src/user-config.ts +35 -4
  278. package/src/version/packed-revision.json +1 -1
  279. package/src/workflow-runtime/executor.ts +24 -2
  280. package/src/workflow-runtime/triggers/registry.ts +27 -0
  281. package/src/workflow-runtime/triggers/schedule-source.ts +30 -3
  282. package/apps/pwa/out/_next/static/chunks/1307-c97eba9fb72a9a5e.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/5977-59fedea50d13c762.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/8857-f1762140ef9a96b1.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/9247-7581825d511a64c0.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/9497-b0e758d1cd4ae8d7.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/approvals/page-937c92292a4ab4f0.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/bots/page-ea998f7f5d156bea.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/ceo/page-33c328b64ca508f8.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e4bcb2c503dab04b.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/editor/page-300897dcbbd5ed76.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/intake/page-103fa3191c1b2e08.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/live/page-be8b4615c919e36c.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/live-run/page-c685b104e0dfa609.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/loops/page-8d693331cfbb6add.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/market/page-cef4f30a4c22fae1.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66ea1b54fc83fa15.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-8cc707bd09a497d9.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-90b55be1f15ce0e8.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cb16606e08be65db.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/outputs/page-16dc64b74ed6a40a.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-8d04132dd0ac5e10.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/setup/page-f23484de467c9ea3.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/app/showroom/page-5456692bda39aafb.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d75f7cd65845ce98.js +0 -1
  307. package/apps/pwa/out/_next/static/chunks/app/term/page-5603f5e568b25ce3.js +0 -1
  308. package/apps/pwa/out/_next/static/chunks/app/today/page-d2cc0f4e89f42028.js +0 -1
  309. package/apps/pwa/out/_next/static/chunks/app/trace/page-a6eb73b8a4840f1c.js +0 -1
  310. package/apps/pwa/out/_next/static/chunks/app/vault/page-c0a3f4d10edb83fa.js +0 -1
  311. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6c0b88068c143f15.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-141436fd26dd56fc.js +0 -1
  313. package/apps/pwa/out/_next/static/css/e81df11490b4413b.css +0 -1
  314. /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_buildManifest.js +0 -0
  315. /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_ssgManifest.js +0 -0
@@ -1,5 +1,5 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
2
+ import { join, resolve, sep } from 'node:path';
3
3
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
4
4
 
5
5
  /** Research is descriptive. Credential values are never an input to the generator. */
@@ -16,7 +16,7 @@ const SLUG = /^[a-z0-9][a-z0-9-]{1,31}$/;
16
16
  const FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
17
17
 
18
18
  /** Materialize a research draft; its graph steps fail until the requested processing is implemented. */
19
- export function generateWizardFiles(dir: string, slug: string, draft: WizardResearchDraft): string[] {
19
+ export function generateWizardFiles(dir: string, slug: string, draft: WizardResearchDraft, options: { regenerate?: boolean } = {}): string[] {
20
20
  if (!SLUG.test(slug)) throw new Error(`invalid wizard plugin name: ${slug}`);
21
21
  if (!draft || typeof draft.description !== 'string' || !draft.description.trim()) throw new Error('wizard description is required');
22
22
  if (draft.connectors !== undefined && !Array.isArray(draft.connectors)) throw new Error('invalid wizard connectors');
@@ -48,21 +48,30 @@ export function generateWizardFiles(dir: string, slug: string, draft: WizardRese
48
48
  }
49
49
  const manifestPath = join(dir, 'plugin.json');
50
50
  const existing = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, 'utf8')) as {
51
- name: string; extensions: { 'ai.elanous': { connectors?: unknown[]; graphs?: string[]; researchDraft?: boolean } };
51
+ name: string; description?: string; extensions: { 'ai.elanous': { connectors?: unknown[]; graphs?: string[]; researchDraft?: boolean } };
52
52
  } : undefined;
53
53
  if (existing && (existing.name !== slug || !existing.extensions?.['ai.elanous'])) {
54
54
  throw new Error('wizard scaffold name or extension mismatch');
55
55
  }
56
- if (existsSync(join(dir, `skills/${slug}/SKILL.md`))) {
56
+ const skillPath = join(dir, `skills/${slug}/SKILL.md`);
57
+ const readmePath = join(dir, 'README.md');
58
+ const wizardReadme = options.regenerate && existsSync(readmePath) &&
59
+ readFileSync(readmePath, 'utf8').includes('Research draft only. Connector declarations contain credential names, not values.');
60
+ if (options.regenerate) {
61
+ if (!existing || existing.extensions['ai.elanous'].researchDraft !== true || !existsSync(skillPath)) {
62
+ throw new Error('wizard regeneration requires an existing research draft');
63
+ }
64
+ } else if (existsSync(skillPath)) {
57
65
  throw new Error(`wizard file already exists: skills/${slug}/SKILL.md`);
58
66
  }
59
67
  const scaffoldConnectors = existing?.extensions['ai.elanous'].connectors;
60
- if (scaffoldConnectors !== undefined && (!Array.isArray(scaffoldConnectors) || scaffoldConnectors.length > 0)) {
68
+ if (!options.regenerate && scaffoldConnectors !== undefined && (!Array.isArray(scaffoldConnectors) || scaffoldConnectors.length > 0)) {
61
69
  throw new Error('wizard cannot replace existing scaffold connectors');
62
70
  }
63
71
  const manifest = existing ?? { name: slug, version: '0.1.0', description: draft.description,
64
72
  extensions: { 'ai.elanous': { graphs: [`./graphs/${slug}.yaml`],
65
73
  capabilities: ['fs:workdir', 'proc:bun', 'proc:elanous'], connectors: [] as unknown[], researchDraft: true } } };
74
+ if (options.regenerate) manifest.description = draft.description;
66
75
  manifest.extensions['ai.elanous'].connectors = connectors;
67
76
  manifest.extensions['ai.elanous'].researchDraft = true;
68
77
 
@@ -90,14 +99,69 @@ process.exitCode = 1;
90
99
  ['README.md', `# ${slug}\n\n${draft.description}\n\nResearch draft only. Connector declarations contain credential names, not values. The graph cannot perform the requested processing yet: implement and validate its steps, then remove extensions["ai.elanous"].researchDraft from plugin.json before installing or running it. No messages are sent.\n`],
91
100
  [`skills/${slug}/SKILL.md`, `---\n${stringifyYaml({ name: slug, description: draft.skill?.description ?? draft.description, requires: [...new Set(requires)] })}---\n\n${draft.skill?.instructions ?? draft.description}\n`],
92
101
  ];
93
- const generated = existing ? files.filter(([path]) => path === 'plugin.json' || path.startsWith('skills/')) : files;
102
+ const generated = existing ? files.filter(([path]) => path === 'plugin.json' || path.startsWith('skills/') ||
103
+ (options.regenerate && wizardReadme && path === 'README.md')) : files;
94
104
  for (const [path] of generated) {
95
- if (path !== 'plugin.json' && existsSync(join(dir, path))) throw new Error(`wizard file already exists: ${path}`);
105
+ if (path !== 'plugin.json' && !(options.regenerate && (path === 'README.md' || path.startsWith('skills/'))) && existsSync(join(dir, path))) {
106
+ throw new Error(`wizard file already exists: ${path}`);
107
+ }
96
108
  }
97
109
  for (const [path, content] of generated) {
98
110
  const target = join(dir, path);
99
111
  mkdirSync(join(target, '..'), { recursive: true });
100
- writeFileSync(target, content, { flag: path === 'plugin.json' && existing ? 'w' : 'wx' });
112
+ writeFileSync(target, content, { flag: existing && (path === 'plugin.json' || options.regenerate) ? 'w' : 'wx' });
101
113
  }
102
114
  return generated.map(([path]) => path);
103
115
  }
116
+
117
+ /** Browser wizard drafts are stored outside the install ledger. Only wizard-owned directories are listed. */
118
+ export function listWizardDrafts(parent: string): Array<{ name: string; description: string; draft: WizardResearchDraft }> {
119
+ if (!existsSync(parent)) return [];
120
+ return readdirSync(parent, { withFileTypes: true }).filter(entry => entry.isDirectory() && SLUG.test(entry.name)).flatMap(entry => {
121
+ const dir = join(parent, entry.name);
122
+ try {
123
+ const manifestPath = join(dir, 'plugin.json');
124
+ const skillDir = join(dir, 'skills', entry.name);
125
+ const skillPath = join(skillDir, 'SKILL.md');
126
+ if (lstatSync(manifestPath).isSymbolicLink() || lstatSync(skillDir).isSymbolicLink() || lstatSync(skillPath).isSymbolicLink()) return [];
127
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
128
+ name?: string; description?: string; extensions?: { 'ai.elanous'?: { researchDraft?: boolean; connectors?: Array<{ id: string; fields: Array<{ name: string }> }> } } };
129
+ if (manifest.name !== entry.name || manifest.extensions?.['ai.elanous']?.researchDraft !== true) return [];
130
+ const skill = readFileSync(skillPath, 'utf8');
131
+ const parts = /^---\n([\s\S]*?)---\n\n([\s\S]*)$/.exec(skill);
132
+ if (!parts || typeof manifest.description !== 'string') return [];
133
+ const meta = parseYaml(parts[1]!) as { description?: string; requires?: string[] };
134
+ return [{ name: entry.name, description: manifest.description, draft: {
135
+ description: manifest.description,
136
+ connectors: manifest.extensions['ai.elanous'].connectors?.map(connector => ({ id: connector.id, credentials: connector.fields.map(field => ({ name: field.name })) })) ?? [],
137
+ skill: { description: meta.description ?? manifest.description, instructions: parts[2]!.trimEnd(), requires: meta.requires ?? [] },
138
+ } }];
139
+ } catch { return []; }
140
+ }).sort((a, b) => a.name.localeCompare(b.name));
141
+ }
142
+
143
+ export function saveWizardDraft(parent: string, slug: string, draft: WizardResearchDraft, regenerate = false): string[] {
144
+ if (!SLUG.test(slug)) throw new Error(`invalid wizard plugin name: ${slug}`);
145
+ const requested = resolve(parent);
146
+ if (existsSync(requested) && lstatSync(requested).isSymbolicLink()) throw new Error('wizard parent cannot be a symlink');
147
+ // Ancestors may be symlinks (macOS /var -> /private/var); compare the draft directory against the real parent.
148
+ const base = existsSync(requested) ? realpathSync(requested) : requested;
149
+ const dir = resolve(base, slug);
150
+ if (!dir.startsWith(`${base}${sep}`)) throw new Error('invalid wizard directory');
151
+ if (existsSync(dir) && (lstatSync(dir).isSymbolicLink() || !lstatSync(dir).isDirectory() || realpathSync(dir) !== dir)) {
152
+ throw new Error('wizard directory must be a local directory');
153
+ }
154
+ if (regenerate && existsSync(join(dir, 'skills')) && lstatSync(join(dir, 'skills')).isSymbolicLink()) {
155
+ throw new Error('wizard skills cannot be a symlink');
156
+ }
157
+ if (!regenerate && existsSync(dir)) throw new Error('wizard directory already exists');
158
+ if (regenerate && existsSync(join(dir, 'skills', slug)) && lstatSync(join(dir, 'skills', slug)).isSymbolicLink()) {
159
+ throw new Error('wizard skills cannot be a symlink');
160
+ }
161
+ if (regenerate && ['plugin.json', 'README.md', `skills/${slug}/SKILL.md`].some(file => {
162
+ const path = join(dir, file);
163
+ return existsSync(path) && lstatSync(path).isSymbolicLink();
164
+ })) throw new Error('wizard regeneration cannot follow symlinks');
165
+ if (!existsSync(base) && !regenerate) mkdirSync(base, { recursive: true });
166
+ return generateWizardFiles(dir, slug, draft, { regenerate });
167
+ }
@@ -4,7 +4,7 @@ import { isScalar, parseDocument } from 'yaml';
4
4
  import { getElanousConfigDir } from '../elanous-config-dir.js';
5
5
  import { DEFAULT_FALLBACK_CHAIN, isFallbackStep, type FallbackStep } from '../oauth/fallback-chain.js';
6
6
  import { isCodexQuotaPolicy } from '../oauth/codex-quota-policy.js';
7
- import { DEFAULT_BUDGET_GATE_MAX_USED_PERCENT, MODEL_ROLES, RUNTIME_LLM_PROVIDER_NAMES, parseRoleLlmEntry, type RoleLlmConfig } from '../user-config.js';
7
+ import { DEFAULT_BUDGET_GATE_MAX_USED_PERCENT, MODEL_ROLES, RUNTIME_LLM_PROVIDER_NAMES, parseRoleLlmEntry, parseSubscriptionReviewerSpec, type RoleLlmConfig } from '../user-config.js';
8
8
 
9
9
  export interface LlmPolicy {
10
10
  version: 1;
@@ -22,7 +22,7 @@ export interface LlmPolicy {
22
22
  grants: Array<{ account: string; amount: number; expires: string; source: string }>;
23
23
  pace?: { targetPerDay: number; until: string; why?: string };
24
24
  };
25
- caps: { codex: { harness: number; pod: number; tox: number; intake: number; headroom: number }; grok: { weekly: number; tox: number; intake: number } };
25
+ caps: { codex: { harness: number; pod: number; tox: number; intake: number; headroom: number }; grok: { weekly: number; tox: number; intake: number }; claude: { harness: number } };
26
26
  resetCredits: { redeem: 'human' };
27
27
  alerts: { lowRemainingPercent: number; resetCreditExpiryDays: number; creditsStep: number };
28
28
  }
@@ -71,6 +71,7 @@ export function defaultLlmPolicy(): LlmPolicy {
71
71
  caps: {
72
72
  codex: { harness: DEFAULT_BUDGET_GATE_MAX_USED_PERCENT['openai-codex']!, pod: 95, tox: 60, intake: 80, headroom: 15 },
73
73
  grok: { weekly: DEFAULT_BUDGET_GATE_MAX_USED_PERCENT.grok!, tox: 50, intake: 48 },
74
+ claude: { harness: 60 },
74
75
  },
75
76
  resetCredits: { redeem: 'human' },
76
77
  alerts: { lowRemainingPercent: 10, resetCreditExpiryDays: 7, creditsStep: 5000 },
@@ -103,8 +104,10 @@ export function validateLlmPolicy(input: unknown): LlmPolicyValidation {
103
104
  const grants = (v: unknown) => Array.isArray(v) && v.every(g => object(g)
104
105
  && Object.keys(g).every(k => ['account', 'amount', 'expires', 'source'].includes(k))
105
106
  && nonempty(g.account) && positive(g.amount) && date(g.expires) && nonempty(g.source));
106
- const roles = (v: unknown) => object(v) && Object.entries(v).every(([role, spec]) => MODEL_ROLES.some(r => r === role)
107
- && object(spec) && Object.keys(spec).every(k => ['provider', 'tier', 'model'].includes(k)) && parseRoleLlmEntry(spec).ok);
107
+ const roles = (v: unknown) => object(v) && Object.entries(v).every(([role, spec]) => role === 'reviewer'
108
+ ? !!parseSubscriptionReviewerSpec(spec)
109
+ : MODEL_ROLES.some(r => r === role) && object(spec)
110
+ && Object.keys(spec).every(k => ['provider', 'tier', 'model'].includes(k)) && parseRoleLlmEntry(spec).ok);
108
111
  check(input, 'policy', {
109
112
  version: (v: unknown) => v === 1,
110
113
  default: { provider: (v: unknown) => nonempty(v) && RUNTIME_LLM_PROVIDER_NAMES.some(p => p === v), model: nonempty },
@@ -122,7 +125,7 @@ export function validateLlmPolicy(input: unknown): LlmPolicyValidation {
122
125
  && Object.keys(v).every(k => ['targetPerDay', 'until', 'why'].includes(k)),
123
126
  },
124
127
  caps: { codex: { harness: percent, pod: percent, tox: percent, intake: percent, headroom: percent },
125
- grok: { weekly: percent, tox: percent, intake: percent } },
128
+ grok: { weekly: percent, tox: percent, intake: percent }, claude: { harness: percent } },
126
129
  resetCredits: { redeem: (v: unknown) => v === 'human' },
127
130
  alerts: { lowRemainingPercent: percent, resetCreditExpiryDays: positive, creditsStep: positive },
128
131
  });
@@ -191,7 +194,9 @@ export function loadLlmPolicy(opts: LoadLlmPolicyOptions = {}): LoadedLlmPolicy
191
194
  add('llm.provider', llm.provider, base, 'provider', v => nonempty(v) && RUNTIME_LLM_PROVIDER_NAMES.some(p => p === v));
192
195
  add('llm.model', llm.model, base, 'model', nonempty);
193
196
  if (Object.keys(base).length) overlay.default = base;
194
- add('roleLlm', legacy.roleLlm, overlay, 'roles', v => object(v) && Object.entries(v).every(([r, spec]) => MODEL_ROLES.some(k => k === r) && parseRoleLlmEntry(spec).ok));
197
+ add('roleLlm', legacy.roleLlm, overlay, 'roles', v => object(v) && Object.entries(v).every(([r, spec]) => r === 'reviewer'
198
+ ? !!parseSubscriptionReviewerSpec(spec)
199
+ : MODEL_ROLES.some(k => k === r) && parseRoleLlmEntry(spec).ok));
195
200
  add('llm.fallbackChain', llm.fallbackChain, overlay, 'fallback', v => Array.isArray(v) && v.length > 0 && v.every(isFallbackStep) && new Set(v).size === v.length);
196
201
  const codex: Obj = {};
197
202
  add('llm.codexAccountOrder', llm.codexAccountOrder, codex, 'order', v => Array.isArray(v) && v.every(nonempty));
@@ -0,0 +1,27 @@
1
+ import { debug } from '../debug/log.js';
2
+ import { checklistDevVersion, listChecklist, summarizeChecklist, type Checklist } from './checklist.js';
3
+
4
+ /** One text board shared by the Telegram and TUI slash surfaces. */
5
+ export function renderChecklistBoard(checklist: Checklist): string {
6
+ const { green, yellow, red, done, blocked } = summarizeChecklist(checklist);
7
+ const { bySeat } = summarizeChecklist({
8
+ ...checklist,
9
+ items: checklist.items.filter((item) => item.status === 'yellow'),
10
+ });
11
+ return [
12
+ `판 ${checklist.version} · 초록 ${green} · 노랑 ${yellow} · 빨강 ${red} · 끝 ${done}`,
13
+ `빨강 칸: ${blocked.join(', ') || '-'}`,
14
+ `자리별 노랑: ${Object.entries(bySeat).sort(([a], [b]) => a.localeCompare(b)).map(([seat, count]) => `${seat} ${count}`).join(' · ') || '-'}`,
15
+ ].join('\n');
16
+ }
17
+
18
+ /** Read and render the selected board without turning failures into zero counts. */
19
+ export function checklistBoardSlash(args: readonly string[], read: (version: string) => Checklist = listChecklist): string {
20
+ if (args.length > 1) return '사용법: /board [판]';
21
+ try {
22
+ return renderChecklistBoard(read(args[0] ?? checklistDevVersion()));
23
+ } catch (error) {
24
+ debug.log('release.checklist', 'board-read-failed', { version: args[0] ?? null, error: error instanceof Error ? error.message : String(error) });
25
+ return '판 체크리스트를 못 읽었다 — 「없다」가 아니다';
26
+ }
27
+ }
@@ -175,6 +175,44 @@ export function cellsReferencingDoc(doc: string, checklists: readonly Checklist[
175
175
  return rows;
176
176
  }
177
177
 
178
+ /** 문서 절과 판 칸의 `#절` 인용을 양방향으로 대조한다. 원장은 전달받은 스냅샷만 읽는다. */
179
+ export function lintDocRefs(docPath: string, docText: string, checklists: readonly Checklist[]): {
180
+ sections: string[];
181
+ uncoveredSections: string[];
182
+ danglingRefs: Array<{ version: string; id: string; ref: string }>;
183
+ } {
184
+ const path = splitRef(docPath.trim()).path;
185
+ const sections: string[] = [];
186
+ let fence: { marker: string; length: number } | null = null;
187
+ for (const line of docText.split(/\r?\n/)) {
188
+ const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
189
+ if (fence) {
190
+ if (marker?.[0] === fence.marker && marker.length >= fence.length && /^\s*$/.test(line.slice(line.indexOf(marker) + marker.length))) fence = null;
191
+ continue;
192
+ }
193
+ if (marker) { fence = { marker: marker[0]!, length: marker.length }; continue; }
194
+ const heading = /^ {0,3}#{2,4}[ \t]+(\S+)/.exec(line);
195
+ if (heading) {
196
+ const section = heading[1]!.replace(/[.:]$/, '');
197
+ if (!sections.includes(section)) sections.push(section);
198
+ }
199
+ }
200
+ const known = new Set(sections);
201
+ const cited = new Set<string>();
202
+ const danglingRefs: Array<{ version: string; id: string; ref: string }> = [];
203
+ for (const data of checklists) {
204
+ for (const item of data.items) {
205
+ for (const ref of item.refs ?? []) {
206
+ const got = splitRef(ref);
207
+ if (got.path !== path || !got.section) continue;
208
+ if (known.has(got.section)) cited.add(got.section);
209
+ else danglingRefs.push({ version: data.version, id: item.id, ref });
210
+ }
211
+ }
212
+ }
213
+ return { sections, uncoveredSections: sections.filter((section) => !cited.has(section)), danglingRefs };
214
+ }
215
+
178
216
  /** `| 판 | 칸 | 상태 | 절 | 제목 |` 마크다운 표 — 행이 없으면 머리줄 둘만. */
179
217
  export function renderRefsStatus(rows: readonly DocRefRow[]): string {
180
218
  const cell = (value: string) => value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
@@ -9,6 +9,7 @@ export type SeatEntry = {
9
9
  export type SeatAddress = { seats: string[]; body: string };
10
10
 
11
11
  const registry: readonly SeatEntry[] = trackData.tracks.filter((entry) => 'title' in entry);
12
+ export function registeredSeats(): readonly SeatEntry[] { return registry; }
12
13
  const addressLine = /^@([A-Za-z][A-Za-z0-9_-]*(?:,[A-Za-z][A-Za-z0-9_-]*)*)(?:[ \t]+|\r?\n|(?![\s\S]))/gm;
13
14
 
14
15
  /** Parse an address only when its @ begins a line; leave the instruction text intact. */
@@ -1,13 +1,14 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
- import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
4
4
  import { Database } from 'bun:sqlite';
5
- import { dirname, join, resolve } from 'node:path';
5
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
6
  import { promisify } from 'node:util';
7
7
  import { TRACK_AGENT_FORBIDDEN_ACTION_REGEX, universeLaunch } from '../autopilot/track-agent.js';
8
8
  import { debug } from '../debug/log.js';
9
9
  import { openSurfaceEventsDb, recordEvent, surfaceEventsDbPath } from '../domains/surface-events.js';
10
10
  import { judgeNeighbor } from '../loops/neighbors.js';
11
+ import { checkLoopNeighbors, emitLoopHeartbeat, SEAT_NEIGHBORS } from '../loops/neighbor-runtime.js';
11
12
  import { addHarnessQueue, harnessQueueIdForKey, harnessQueueOutcome, listHarnessQueue } from '../harness/harness-queue.js';
12
13
  import { isRescueAllowedAction } from '../steward/rescue.js';
13
14
  import { DecisionLedger, type DecisionEntry, type DecisionOption, type Recommendation } from '../decisions/decision-ledger.js';
@@ -20,6 +21,7 @@ import { openMsgStore, type MessageEnvelope } from '../msg/msg-store.js';
20
21
  import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
21
22
  import { orderedPersonaTodos, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
22
23
  import { PersonaRegistry } from '../persona/registry.js';
24
+ import { registeredSeats, resolveSeat } from '../seat-address/seat-address.js';
23
25
  import { recordSeatAskShadow } from '../seat-dispatch/seat-ask.js';
24
26
  import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../seat-dispatch/seat-request-ledger.js';
25
27
  import { answerCrossCheck, answerSeat, askCrossCheck, askSeat, crossCheckAnswer, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatCrossChecks, seatQuestions, type CrossCheckJudgment, type SeatId } from '../seat-dispatch/seat-questions.js';
@@ -29,7 +31,7 @@ import { readSchedules, releasedVersion } from '../release-loop/feature-store.js
29
31
  import { runOpDecide, type OpDecideCandidate } from './op-decide.js';
30
32
 
31
33
  // `kind`/`createdAt` are the V3 shadow-compare keys (내부 문서 `METHOD-v3-shadow-compare-2026-10-02` · MK 10-02 18:54).
32
- export type SeatItem = { source: 'request' | 'checklist' | 'hook' | 'seat-question'; kind?: 'request' | 'cell' | 'hook-task' | 'seat-question'; id: string; title: string; text: string; evidence?: string; status?: string; version?: string; seat?: string; evidenceHash?: string; asOf?: string; queuedAt?: string; createdAt?: string; from?: SeatId };
34
+ export type SeatItem = { source: 'request' | 'checklist' | 'hook' | 'seat-question' | 'idle'; kind?: 'request' | 'cell' | 'hook-task' | 'seat-question'; id: string; title: string; text: string; evidence?: string; status?: string; version?: string; seat?: string; evidenceHash?: string; asOf?: string; queuedAt?: string; createdAt?: string; from?: SeatId; idleRung?: 1 | 2 | 3 | 4 | 5 };
33
35
  export type SeatInputs = { requests: SeatItem[]; checklist: SeatItem[]; role: string };
34
36
  // V3 ledger row: `ts` · `item.id` · `item.kind` · `item.createdAt` · `action` (door, wait or skipped-*) · `reason` (matched rule or forbidden word).
35
37
  export type SeatAction = 'decision' | 'harness' | 'wait' | 'seat-question' | 'seat-answer' | 'skipped-budget' | 'skipped-empty';
@@ -44,7 +46,7 @@ export type TcCandidate =
44
46
  | { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
45
47
  | { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
46
48
  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 };
47
- 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 | OpDecideCandidate; 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 };
49
+ 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 | OpDecideCandidate; 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; emptyQueueMinutes?: number };
48
50
  export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off'; modeDowngradeReason?: string };
49
51
 
50
52
  export function seatLoopTickLine(result: SeatLoopResult): string {
@@ -93,6 +95,8 @@ export type SeatDeps = {
93
95
  pullRequests?: () => Promise<TcPullRequest[]>;
94
96
  ledgerFiles?: (directory: string) => string[];
95
97
  lockContended?: () => void;
98
+ /** Override the heartbeat timer in isolated long-turn tests; production uses the seat contract's interval. */
99
+ heartbeatIntervalMs?: number;
96
100
  /** Supply a question only when this seat lacks evidence for the selected work item. */
97
101
  inquire?: (seat: SeatId, item: SeatItem) => Promise<SeatInquiry | null> | SeatInquiry | null;
98
102
  /** Supply the receiving seat's answer; absent evidence must not invent a reply. */
@@ -570,8 +574,8 @@ function opDecidePass(config: SeatLoopConfig, deps: SeatDeps, now: Date, path: s
570
574
  mode,
571
575
  ledger: cardLedger(stateRoot, deps, now),
572
576
  runOwner: deps.runOwner ?? ((runId) => {
573
- owners ??= new Map((['OP', 'UX', 'MK', 'TC'] as const).flatMap((owner) =>
574
- readSeatLedger(owner, deps, now).filter((row) => row.runId).map((row) => [row.runId!, owner] as const)));
577
+ owners ??= new Map(registeredSeats().filter(({ id }) => isSeatId(id)).flatMap(({ id: owner }) =>
578
+ readSeatLedger(owner, deps, now).filter((row) => row.runId).map((row) => [row.runId!, owner as SeatId] as const)));
575
579
  return owners.get(runId);
576
580
  }),
577
581
  routeToSeat: (to, card, reason) => {
@@ -691,9 +695,11 @@ function readTcChecklist(root: string): Array<{ version: string; id: string; tit
691
695
  if (!existsSync(path)) return [];
692
696
  const db = new Database(path, { readonly: true, strict: true });
693
697
  try {
698
+ const seats = registeredSeats().filter(({ id }) => isSeatId(id)).map(({ id }) => id);
699
+ if (!seats.length) return [];
694
700
  return db.query(`SELECT a.version, f.id, COALESCE(a.title_override, f.title) AS title, a.status, a.owner
695
- FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.status IN ('yellow', 'red') AND a.owner IN ('OP', 'TC', 'MK', 'UX')
696
- ORDER BY a.version, f.id`).all() as Array<{ version: string; id: string; title: string; status: string; owner: string | null }>;
701
+ FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.status IN ('yellow', 'red') AND a.owner IN (${seats.map(() => '?').join(', ')})
702
+ ORDER BY a.version, f.id`).all(...seats) as Array<{ version: string; id: string; title: string; status: string; owner: string | null }>;
697
703
  } finally { db.close(); }
698
704
  }
699
705
 
@@ -946,13 +952,48 @@ export function planAction(item: SeatItem, seat?: string): { kind: 'decision' |
946
952
  const kind = rule?.kind ?? 'harness';
947
953
  const reason = rule?.reason;
948
954
  observe('plan', { kind, reason: reason ?? '' });
949
- return { kind, text: seat && kind === 'harness' ? seatTaskText(seat, item) : `${item.id} ${item.title}`, ...(reason ? { reason } : {}) };
955
+ const text = seat && kind === 'harness' ? seatTaskText(seat, item) : `${item.id} ${item.title}`;
956
+ // Observed once per plan (not per queue comparison): how many target paths the launched ask carries.
957
+ if (seat && kind === 'harness' && text.startsWith('대상 경로: ')) observe('target-paths', { item: item.id, count: text.split('\n', 1)[0]!.split(' · ').length });
958
+ return { kind, text, ...(reason ? { reason } : {}) };
959
+ }
960
+
961
+ /** A path counts only when it resolves to a regular file whose real location stays inside `root` (symlinks out are refused). */
962
+ export function repoFileExists(root: string = repoRoot): (path: string) => boolean {
963
+ return (path) => {
964
+ try {
965
+ const absolute = resolve(root, path);
966
+ const real = relative(realpathSync(root), realpathSync(absolute));
967
+ return !!real && real !== '..' && !real.startsWith(`..${sep}`) && !isAbsolute(real) && statSync(absolute).isFile();
968
+ } catch { return false; }
969
+ };
970
+ }
971
+
972
+ // Repository paths already written in a checklist item's title or evidence (dedup · first-seen order · at most six).
973
+ export function seatTargetPaths(item: SeatItem, isFile: (path: string) => boolean = repoFileExists()): string[] {
974
+ const paths: string[] = [];
975
+ const text = `${item.title}\n${item.evidence ?? ''}`;
976
+ const matches = text.matchAll(/(?:^|[^A-Za-z0-9_./-])((?:src|apps|scripts|test|docs|bin|graphs|packs|plugins)\/[A-Za-z0-9_./-]+)/g);
977
+ for (const match of matches) {
978
+ const path = match[1]!.replace(/[./]+$/, '');
979
+ if (path.split('/').includes('..')) continue;
980
+ const fromRoot = relative(repoRoot, resolve(repoRoot, path));
981
+ if (!fromRoot || fromRoot === '..' || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot) || !isFile(path)) continue;
982
+ if (!paths.includes(path)) paths.push(path);
983
+ if (paths.length === 6) break;
984
+ }
985
+ return paths;
950
986
  }
951
987
 
952
988
  // The launched sentence names the seat, the source and the release so same-named items stay distinct.
953
- export function seatTaskText(seat: string, item: SeatItem): string {
954
- const where = item.source === 'checklist' ? `${item.version ?? '?'} 체크리스트 칸 ${item.id}` : item.source === 'hook' ? `웹훅 작업 카드 ${item.id}` : `자리 요청 ${item.id}`;
955
- return `[${seat} 자리 · ${where} · 역할 docs/roles/${seat}.md] ${item.title.replace(/[\r\n]+/g, ' ')}`;
989
+ // Checklist items whose title/evidence already name repository files get a leading `대상 경로:` line
990
+ // (the label parseAskTargetPathHints reads); without paths the text is byte-identical to the old one-liner.
991
+ export function seatTaskText(seat: string, item: SeatItem, isFile?: (path: string) => boolean): string {
992
+ const where = item.source === 'checklist' ? `${item.version ?? '?'} 체크리스트 칸 ${item.id}` : item.source === 'idle' ? `쉬는 시간 사다리 ${item.id} · ${item.idleRung}단계${item.version ? ` · ${item.version}` : ''}` : item.source === 'hook' ? `웹훅 작업 카드 ${item.id}` : `자리 요청 ${item.id}`;
993
+ const text = `[${seat} 자리 · ${where} · 역할 docs/roles/${seat}.md] ${item.title.replace(/[\r\n]+/g, ' ')}`;
994
+ if (item.source !== 'checklist') return text;
995
+ const paths = seatTargetPaths(item, isFile);
996
+ return paths.length ? `대상 경로: ${paths.join(' · ')}\n${text}` : text;
956
997
  }
957
998
 
958
999
  function xcheckEvent(event: string, seat: string, neighbor: string, item: string, outcome: string): void {
@@ -986,8 +1027,10 @@ function neighborLastSeen(root: string, isolated: boolean, now: Date, wanted: Re
986
1027
  }
987
1028
 
988
1029
  function neighborSeat(id: string): string | null {
989
- const match = /^(op|tc|mk|ux)-seat$/i.exec(id);
990
- return match ? match[1]!.toUpperCase() : null;
1030
+ const match = /^([A-Za-z][A-Za-z0-9_-]*)-seat$/i.exec(id);
1031
+ if (!match) return null;
1032
+ const resolved = resolveSeat(match[1]!)?.id;
1033
+ return resolved && isSeatId(resolved) ? resolved : null;
991
1034
  }
992
1035
 
993
1036
  function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps, root: string, now: Date): void {
@@ -1034,8 +1077,89 @@ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps
1034
1077
  }
1035
1078
  }
1036
1079
 
1080
+ const IDLE_WORK: Record<SeatId, string> = {
1081
+ MK: '문서 최적화: 현재 문서의 탐색·명료성·검색 유입을 실물 근거로 개선',
1082
+ UX: '멀티 서피스: 실제 채널별 사용자 흐름을 대조하고 결함을 개선',
1083
+ TC: '하니스 자가 치유: 실패 원장을 대조하여 재현 가능한 결함을 수리',
1084
+ OP: '보충 저작: 판 체크리스트의 빠진 칸을 근거와 반증 기준으로 보완',
1085
+ };
1086
+
1087
+ function idleCell(seat: string, rung: 1 | 2 | 3 | 4 | 5, id: string, title: string, now: Date, version?: string, owner?: string, evidence?: string): SeatItem {
1088
+ return { source: 'idle', id, title, text: title, idleRung: rung, seat: owner ?? seat,
1089
+ ...(version ? { version } : {}), ...(evidence ? { evidence } : {}), asOf: now.toISOString() };
1090
+ }
1091
+
1092
+ function emptyQueueMinutes(ledger: readonly SeatEntry[], now: Date): number {
1093
+ const previousIndex = [...ledger].reverse().findIndex((row) => row.emptyQueueMinutes !== undefined);
1094
+ const workIndex = [...ledger].reverse().findIndex((row) => row.item && row.item.source !== 'idle' && row.status !== 'skipped-empty');
1095
+ const previousPosition = previousIndex < 0 ? -1 : ledger.length - 1 - previousIndex;
1096
+ const workPosition = workIndex < 0 ? -1 : ledger.length - 1 - workIndex;
1097
+ const previous = ledger[previousPosition];
1098
+ const lastWork = ledger[workPosition];
1099
+ const previousAt = previous ? Date.parse(previous.at) : NaN;
1100
+ const workAt = lastWork ? Date.parse(lastWork.at) : NaN;
1101
+ const start = Number.isFinite(previousAt) && Number.isFinite(workAt) ? Math.max(previousAt, workAt)
1102
+ : Number.isFinite(previousAt) ? previousAt : workAt;
1103
+ if (!Number.isFinite(start)) return 0;
1104
+ const prior = previousPosition > workPosition ? previous!.emptyQueueMinutes! : 0;
1105
+ return prior + Math.max(0, (now.getTime() - start) / 60_000);
1106
+ }
1107
+
1108
+ function pickIdleWork(seat: SeatId, deps: SeatDeps, ledger: readonly SeatEntry[], now: Date, root: string, shadow: boolean): SeatItem | null {
1109
+ const releaseRoot = deps.root ?? releaseLedgerRoot();
1110
+ const schedules = (deps.schedules ?? (() => readSchedules(releaseRoot)))()
1111
+ .filter((row) => /^\d+\.\d+\.\d+$/.test(row.version) && Number.isFinite(Date.parse(row.cutAt)))
1112
+ .sort((a, b) => versionOrder(a.version, b.version));
1113
+ const shipped = releasedVersion(releaseRoot);
1114
+ const current = schedules.find((row) => Date.parse(row.cutAt) > now.getTime() && (!shipped || versionOrder(row.version, shipped) > 0));
1115
+ const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version, releaseRoot).items);
1116
+ const handled = handledKeys(ledger, shadow, root, !shadow);
1117
+ const available = (item: SeatItem) => eligibleItem(item, ledger, handled, shadow);
1118
+ // The normal queue handles open owned cells first; a landed yellow cell is revisited here for physical rebuttal.
1119
+ for (const row of schedules.filter((row) => row.version === current?.version || Date.parse(row.cutAt) <= now.getTime() || (shipped && versionOrder(row.version, shipped) <= 0))) {
1120
+ for (const cell of itemsOf(row.version).filter((cell) => cell.owner === seat && cell.status === 'yellow' && /착지|merged|병합/i.test(cell.evidence ?? '')).sort((a, b) => a.id.localeCompare(b.id))) {
1121
+ const item = idleCell(seat, 2, cell.id, `착지 후 노랑 실물 반증: ${cell.id} · ${cell.title}`, now, row.version, seat, cell.evidence);
1122
+ item.evidenceHash = createHash('sha256').update(cell.evidence ?? '').digest('hex');
1123
+ if (available(item)) return item;
1124
+ }
1125
+ }
1126
+ const next = schedules.find((row) => current && versionOrder(row.version, current.version) > 0);
1127
+ if (next) {
1128
+ const title = `${next.version} 다음 판 칸 저작: ${seat} 자리의 자율성·효율성·동시성을 높일 칸을 근거와 실물 반증부터 작성`;
1129
+ const item = idleCell(seat, 3, `author:${seat}`, title, now, next.version);
1130
+ if (available(item)) return item;
1131
+ }
1132
+ const recurring = idleCell(seat, 4, `recurring:${seat}:${seatDay(now)}`, IDLE_WORK[seat], now);
1133
+ if (available(recurring)) return recurring;
1134
+ const queue = (deps.queueItems ?? ((stateRoot: string) => listHarnessQueue({ root: stateRoot })))(root);
1135
+ const candidates: Array<{ owner: SeatId; item: SeatItem; pressure: number }> = [];
1136
+ for (const owner of ['OP', 'MK', 'UX', 'TC'] as const) {
1137
+ if (owner === seat) continue;
1138
+ const ownerLedger = readSeatLedger(owner, deps, now);
1139
+ const busy = queue.filter((row) => row.seat === owner && ['queued', 'launching', 'launched'].includes(row.status)).length;
1140
+ for (const row of schedules.filter((row) => Date.parse(row.cutAt) > now.getTime() && (!shipped || versionOrder(row.version, shipped) > 0))) {
1141
+ for (const cell of itemsOf(row.version).filter((cell) => cell.owner === owner && cell.status === 'yellow')) {
1142
+ const evidence = cell.evidence ?? '';
1143
+ const runIds = [...evidence.matchAll(RUN_REFERENCE)].map(([id]) => id.toLowerCase());
1144
+ if (runIds.length) {
1145
+ const observed = (deps.cutoffRuns ?? ((ids: readonly string[]) => queryRunningRuns({ runIds: ids, caller: 'seat-loop.idle' })))(runIds);
1146
+ if (observed.completeness !== 'complete' || observed.pty.unreadable.length || observed.entries.some((entry) => entry.status !== 'ended-unclosed')) continue;
1147
+ }
1148
+ if (ownerLedger.some((entry) => entry.item?.source === 'checklist' && entry.item.id === cell.id && entry.item.version === row.version
1149
+ && ['queued', 'attempting', 'outcome-unknown', 'launched'].includes(entry.status))) continue;
1150
+ if (queue.some((entry) => entry.seat === owner && ['queued', 'launching', 'launched'].includes(entry.status)
1151
+ && entry.input.includes(`체크리스트 칸 ${cell.id} ·`))) continue;
1152
+ const item = idleCell(seat, 5, `borrow:${owner}:${cell.id}`, `${row.version} ${cell.id} · ${cell.title} — ${seat} 지원 (주인 ${owner} 유지)`, now, row.version, owner, evidence);
1153
+ item.evidenceHash = createHash('sha256').update(evidence).digest('hex');
1154
+ if (available(item)) candidates.push({ owner, item, pressure: busy + itemsOf(row.version).filter((entry) => entry.owner === owner && entry.status === 'yellow').length });
1155
+ }
1156
+ }
1157
+ }
1158
+ return candidates.sort((a, b) => b.pressure - a.pressure || a.owner.localeCompare(b.owner) || a.item.id.localeCompare(b.item.id))[0]?.item ?? null;
1159
+ }
1160
+
1037
1161
  export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
1038
- if (!/^(?:MK|OP|TC|UX)$/.test(seat)) throw new Error(`unknown seat: ${seat}`);
1162
+ if (!isSeatId(seat) || !registeredSeats().some(({ id }) => id === seat)) throw new Error(`unknown seat: ${seat}`);
1039
1163
  const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'shadow' };
1040
1164
  if (config.mode === 'off') {
1041
1165
  observe('skipped-off', { seat });
@@ -1060,6 +1184,25 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1060
1184
  } catch (error) { observe('heartbeat-write-failed', { seat, error: String(error).slice(0, 200) }); }
1061
1185
  const recorded = readSeatLedger(seat, deps, now);
1062
1186
  const ledger = config.mode === 'live-safe' ? reconciledQueueLedger(recorded, stateRoot, deps) : recorded;
1187
+ const loopId = seat === 'MK' ? 'cmo-seat' : `${seat.toLowerCase()}-seat`;
1188
+ const heartbeatEveryMs = (SEAT_NEIGHBORS[seat]?.find((peer) => peer.id !== 'orchestrator')?.heartbeat.everyMinutes ?? 15) * 60_000;
1189
+ const leaseMs = heartbeatEveryMs * 2;
1190
+ const pulse = (health: 'healthy' | 'degraded', at: Date): void => {
1191
+ try { emitLoopHeartbeat(stateRoot, loopId, health, at, new Date(at.getTime() + leaseMs).toISOString()); }
1192
+ catch (error) { observe('neighbor-runtime-failed', { seat, error: String(error).slice(0, 200) }); }
1193
+ };
1194
+ pulse('healthy', (deps.now ?? (() => new Date()))());
1195
+ const watchNeighbors = (at: Date): void => {
1196
+ try { checkLoopNeighbors(stateRoot, loopId, SEAT_NEIGHBORS[seat] ?? [], at); }
1197
+ catch (error) { observe('neighbor-runtime-failed', { seat, error: String(error).slice(0, 200) }); }
1198
+ };
1199
+ // A long turn keeps both duties periodic: renew our lease and watch neighbors (incidents are idempotent).
1200
+ const heartbeatTimer = setInterval(() => {
1201
+ const at = (deps.now ?? (() => new Date()))();
1202
+ pulse('healthy', at);
1203
+ watchNeighbors(at);
1204
+ }, deps.heartbeatIntervalMs ?? heartbeatEveryMs / 2);
1205
+ heartbeatTimer.unref();
1063
1206
  if (seat === 'OP') opDecidePass(config, deps, now, path, stateRoot, ledger);
1064
1207
  let turnError: unknown;
1065
1208
  try {
@@ -1259,8 +1402,14 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1259
1402
  checklist: inputs.checklist.filter((candidate) => !ledger.some((row) => row.status === 'awaiting-resolution'
1260
1403
  && row.item && itemKey(row.item) === itemKey(candidate) && itemSnapshot(row.item) === itemSnapshot(candidate))) }, ledger,
1261
1404
  { shadow: config.mode === 'shadow', liveSafe: config.mode === 'live-safe', root: stateRoot, history: deps.checklistHistory ?? checklistHistory });
1262
- const item = pendingQuestion ?? ordinary;
1263
- const append = deps.append ?? defaultAppend;
1405
+ const ownQueueEmpty = !pendingQuestion && !ordinary;
1406
+ const idleMinutes = ownQueueEmpty ? emptyQueueMinutes(ledger, now) : undefined;
1407
+ // IDLE-LADDER: off unless loops.seat.idleLadder is set; 'shadow' records the idle pick only, 'live' lets live-safe/on launch it.
1408
+ const idleShadow = config.mode === 'shadow' || config.idleLadder !== 'live';
1409
+ const item = pendingQuestion ?? ordinary ?? (ownQueueEmpty && !lastDecisionFailure && (config.idleLadder === 'shadow' || config.idleLadder === 'live')
1410
+ ? pickIdleWork(seat as SeatId, deps, ledger, now, stateRoot, idleShadow) : null);
1411
+ const appendBase = deps.append ?? defaultAppend;
1412
+ const append: typeof appendBase = (file, row) => appendBase(file, idleMinutes === undefined ? row : { ...row, emptyQueueMinutes: idleMinutes });
1264
1413
  const revisedProposal = item?.source === 'checklist' && ledger.some((row) => row.status === 'awaiting-resolution'
1265
1414
  && row.item && itemKey(row.item) === itemKey(item) && itemSnapshot(row.item) !== itemSnapshot(item));
1266
1415
  if (config.mode === 'live-safe' && !pendingQuestion) {
@@ -1448,11 +1597,12 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1448
1597
  }
1449
1598
  if (!item && config.mode === 'live-safe' && tcDowngrade) return tcDowngrade;
1450
1599
  const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: item ? 'shadow' : 'skipped-empty',
1600
+ ...(idleMinutes === undefined ? {} : { emptyQueueMinutes: idleMinutes }),
1451
1601
  ...(item ? { item, action: planned!.kind, ...(planned!.reason ? { reason: planned!.reason } : {}) } : { action: 'skipped-empty' as const }) };
1452
- if (config.mode === 'shadow' || !item) {
1602
+ if (config.mode === 'shadow' || !item || (item.source === 'idle' && idleShadow)) {
1453
1603
  if (!item && lastDecisionFailure) return lastDecisionFailure;
1454
1604
  append(path, entry);
1455
- if (config.mode === 'shadow') observe('shadow', { seat, item, status: entry.status });
1605
+ if (config.mode === 'shadow' || item?.source === 'idle') observe('shadow', { seat, item, status: entry.status });
1456
1606
  return entry;
1457
1607
  }
1458
1608
  if (planned!.kind === 'wait') {
@@ -1669,8 +1819,12 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1669
1819
  turnError = error;
1670
1820
  throw error;
1671
1821
  } finally {
1822
+ clearInterval(heartbeatTimer);
1672
1823
  // A turn ends even when there is no picked item or a launch fails.
1673
- try { judgeSeatNeighbors(seat, config, deps, stateRoot, (deps.now ?? (() => new Date()))()); }
1824
+ const endedAt = (deps.now ?? (() => new Date()))();
1825
+ pulse(turnError === undefined ? 'healthy' : 'degraded', endedAt);
1826
+ watchNeighbors(endedAt);
1827
+ try { judgeSeatNeighbors(seat, config, deps, stateRoot, endedAt); }
1674
1828
  catch (error) {
1675
1829
  observe('neighbor-judgment-failed', { seat, error: String(error).slice(0, 200) });
1676
1830
  const neighbors = config.neighbors?.[seat as keyof NonNullable<SeatLoopConfig['neighbors']>] ?? [];