elanous 0.2.13 → 0.2.14

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 (445) 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/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1307-6e7e0377c1db0bdc.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
  9. package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
  31. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
  42. package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-7b2b4f378354869a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-f14987d5850a31e0.js → page-1abfe3aec3be6bdc.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-1512d91c7491ed62.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-a627db94ec6904a0.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-fed69f9759d248d0.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-15374909d8f1aebd.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-012a742f81218b01.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/intake/page-99effd9d893a43dc.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/layout-938de57ae0315e46.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/market/page-629e1ede482ec6eb.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-66c27608f651b866.js → page-2ce5471926761fdd.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/morning/page-27e99b4e54a849d4.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-32cde713be47cddb.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e64caff084f7ce21.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-db07eaffba0df7e3.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-78b40a344ed22f24.js → page-e1e47d8d940aba3d.js} +1 -1
  81. package/apps/pwa/out/_next/static/chunks/app/settings/page-86f91034bd5cabe5.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/share/page-ce4d8a1215ce53a3.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/term/{page-63d14a4d8fe1e1c1.js → page-274703bc41b71333.js} +1 -1
  89. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/vault/page-3b7e821dc492bdfd.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2f810b18d89ecba6.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
  97. package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  98. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
  104. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +1 -0
  105. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +1 -0
  106. package/apps/pwa/out/approvals/index.html +1 -1
  107. package/apps/pwa/out/approvals/index.txt +23 -23
  108. package/apps/pwa/out/autopilot/index.html +1 -1
  109. package/apps/pwa/out/autopilot/index.txt +23 -23
  110. package/apps/pwa/out/board/index.html +1 -1
  111. package/apps/pwa/out/board/index.txt +23 -23
  112. package/apps/pwa/out/botlab/index.html +1 -1
  113. package/apps/pwa/out/botlab/index.txt +22 -22
  114. package/apps/pwa/out/bots/index.html +1 -1
  115. package/apps/pwa/out/bots/index.txt +22 -22
  116. package/apps/pwa/out/chat/index.html +1 -1
  117. package/apps/pwa/out/chat/index.txt +22 -22
  118. package/apps/pwa/out/consult/index.html +1 -1
  119. package/apps/pwa/out/consult/index.txt +23 -23
  120. package/apps/pwa/out/control/index.html +1 -1
  121. package/apps/pwa/out/control/index.txt +23 -23
  122. package/apps/pwa/out/design-check/index.html +1 -1
  123. package/apps/pwa/out/design-check/index.txt +23 -23
  124. package/apps/pwa/out/editor/index.html +1 -1
  125. package/apps/pwa/out/editor/index.txt +23 -23
  126. package/apps/pwa/out/exec/index.html +1 -1
  127. package/apps/pwa/out/exec/index.txt +23 -23
  128. package/apps/pwa/out/field/index.html +1 -1
  129. package/apps/pwa/out/field/index.txt +23 -23
  130. package/apps/pwa/out/healthz +1 -1
  131. package/apps/pwa/out/index.html +1 -1
  132. package/apps/pwa/out/index.txt +22 -22
  133. package/apps/pwa/out/inside/index.html +1 -1
  134. package/apps/pwa/out/inside/index.txt +22 -22
  135. package/apps/pwa/out/intake/index.html +1 -1
  136. package/apps/pwa/out/intake/index.txt +23 -23
  137. package/apps/pwa/out/live/index.html +1 -1
  138. package/apps/pwa/out/live/index.txt +23 -23
  139. package/apps/pwa/out/loops/index.html +1 -0
  140. package/apps/pwa/out/loops/index.txt +30 -0
  141. package/apps/pwa/out/market/index.html +1 -1
  142. package/apps/pwa/out/market/index.txt +22 -22
  143. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  144. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  145. package/apps/pwa/out/morning/index.html +1 -1
  146. package/apps/pwa/out/morning/index.txt +23 -23
  147. package/apps/pwa/out/observatory/index.html +1 -1
  148. package/apps/pwa/out/observatory/index.txt +23 -23
  149. package/apps/pwa/out/ops/checklist/index.html +1 -1
  150. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  151. package/apps/pwa/out/ops/release/index.html +1 -1
  152. package/apps/pwa/out/ops/release/index.txt +22 -22
  153. package/apps/pwa/out/ops/seats/index.html +1 -1
  154. package/apps/pwa/out/ops/seats/index.txt +22 -22
  155. package/apps/pwa/out/outputs/index.html +1 -1
  156. package/apps/pwa/out/outputs/index.txt +22 -22
  157. package/apps/pwa/out/reflection/index.html +1 -1
  158. package/apps/pwa/out/reflection/index.txt +23 -23
  159. package/apps/pwa/out/scheduler/index.html +1 -1
  160. package/apps/pwa/out/scheduler/index.txt +23 -23
  161. package/apps/pwa/out/sessions/index.html +1 -1
  162. package/apps/pwa/out/sessions/index.txt +22 -22
  163. package/apps/pwa/out/settings/devices/index.html +1 -1
  164. package/apps/pwa/out/settings/devices/index.txt +23 -23
  165. package/apps/pwa/out/settings/index.html +1 -1
  166. package/apps/pwa/out/settings/index.txt +23 -23
  167. package/apps/pwa/out/setup/done/index.html +1 -1
  168. package/apps/pwa/out/setup/done/index.txt +23 -23
  169. package/apps/pwa/out/setup/index.html +1 -1
  170. package/apps/pwa/out/setup/index.txt +23 -23
  171. package/apps/pwa/out/share/index.html +1 -1
  172. package/apps/pwa/out/share/index.txt +23 -23
  173. package/apps/pwa/out/showroom/index.html +1 -1
  174. package/apps/pwa/out/showroom/index.txt +22 -22
  175. package/apps/pwa/out/tasks/index.html +1 -1
  176. package/apps/pwa/out/tasks/index.txt +23 -23
  177. package/apps/pwa/out/term/index.html +1 -1
  178. package/apps/pwa/out/term/index.txt +23 -23
  179. package/apps/pwa/out/today/index.html +1 -1
  180. package/apps/pwa/out/today/index.txt +23 -23
  181. package/apps/pwa/out/trace/index.html +1 -1
  182. package/apps/pwa/out/trace/index.txt +23 -23
  183. package/apps/pwa/out/vault/index.html +1 -1
  184. package/apps/pwa/out/vault/index.txt +23 -23
  185. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  186. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  187. package/apps/pwa/out/workflows/index.html +1 -1
  188. package/apps/pwa/out/workflows/index.txt +23 -23
  189. package/apps/pwa/out/workspace/index.html +1 -1
  190. package/apps/pwa/out/workspace/index.txt +23 -23
  191. package/apps/pwa/out/worktrees/index.html +1 -1
  192. package/apps/pwa/out/worktrees/index.txt +23 -23
  193. package/graphs/orchestrator/orchestrator.yaml +59 -0
  194. package/graphs/orchestrator/recipes.yaml +18 -0
  195. package/graphs/release/recipes.yaml +3 -0
  196. package/graphs/release/release-loop.yaml +4 -1
  197. package/graphs/rhythm/daily.yaml +38 -0
  198. package/graphs/rhythm/recipes.yaml +18 -0
  199. package/graphs/rhythm/weekly.yaml +39 -0
  200. package/graphs/video/recipes.yaml +3 -0
  201. package/graphs/video/youtube-summary.yaml +17 -0
  202. package/package.json +1 -1
  203. package/scripts/audit-test-state-writes.ts +72 -17
  204. package/scripts/brand/check.ts +186 -0
  205. package/scripts/coord-post.sh +11 -1
  206. package/scripts/docs-cli-check.ts +23 -6
  207. package/scripts/expose-rubric-check.ts +118 -0
  208. package/scripts/expose-rubric-suggest.ts +163 -0
  209. package/scripts/heal/run-role.ts +14 -0
  210. package/scripts/hq/standby-verify.ts +11 -7
  211. package/scripts/lessons/handbook-page.ts +85 -0
  212. package/scripts/release-loop/gate-node.ts +9 -0
  213. package/scripts/release-loop/publish-node.ts +14 -1
  214. package/scripts/release-loop/unattended-release.ts +23 -1
  215. package/scripts/release-loop/vault-note-node.ts +196 -0
  216. package/scripts/release-story/draft.ts +172 -0
  217. package/scripts/rhythm/daily.ts +383 -0
  218. package/scripts/rhythm/weekly.ts +294 -0
  219. package/scripts/skills/public-check.ts +169 -0
  220. package/scripts/test-deterministic.ts +4 -8
  221. package/scripts/test-diet/effectiveness.ts +195 -0
  222. package/scripts/test-diet/lib.ts +16 -8
  223. package/scripts/test-diet/node.ts +77 -9
  224. package/scripts/ux/demo-rehearsal.ts +2 -2
  225. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  226. package/scripts/video/youtube-summary.ts +133 -0
  227. package/skills/diagram-master/SKILL.md +42 -49
  228. package/skills/diagram-master/references/pyproject.toml +4 -1
  229. package/skills/diagram-master/references/routing-guide.md +4 -2
  230. package/skills/grill-me/SKILL.md +1 -0
  231. package/skills/youtube-master/SKILL.md +13 -13
  232. package/skills/youtube-master/package-lock.json +0 -545
  233. package/skills/youtube-master/package.json +2 -5
  234. package/skills/youtube-master/scripts/main.ts +2 -2
  235. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  236. package/skills/youtube-master/src/util.ts +30 -7
  237. package/src/agent-mission/driver.ts +3 -3
  238. package/src/autopilot/build/llm-conflict-merge.ts +45 -8
  239. package/src/autopilot/build/merge-intent.ts +53 -0
  240. package/src/chat/index.ts +2 -0
  241. package/src/claims/claims-ledger.ts +232 -0
  242. package/src/claims/claims-render.ts +157 -0
  243. package/src/cli/claims-cli.ts +94 -0
  244. package/src/cli/decisions-cli.ts +35 -3
  245. package/src/cli/docs-cli.ts +2 -0
  246. package/src/cli/flow-cli.ts +11 -0
  247. package/src/cli/freeze-cli.ts +45 -0
  248. package/src/cli/lesson-cli.ts +108 -0
  249. package/src/cli/logs-cli.ts +104 -2
  250. package/src/cli/logs-model-input.ts +131 -0
  251. package/src/cli/pr-cli.ts +29 -1
  252. package/src/cli/private-ledger-commands.ts +54 -4
  253. package/src/cli/pty-drive-verdict.ts +11 -2
  254. package/src/cli/release-cli.ts +144 -21
  255. package/src/cli/rfc-status.ts +91 -0
  256. package/src/connectors/linear.ts +5 -3
  257. package/src/context-bus/condense.ts +15 -20
  258. package/src/context-bus/context-now-public.ts +86 -0
  259. package/src/context-bus/context-now-surfaces.ts +55 -2
  260. package/src/context-bus/context-now.ts +4 -3
  261. package/src/context-bus/long-term-memory.ts +92 -11
  262. package/src/dashboard/index.ts +75 -22
  263. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  264. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  265. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  266. package/src/decisions/decision-ledger.ts +40 -1
  267. package/src/discord/sprint21-runtime.ts +16 -0
  268. package/src/discord-self-message.ts +8 -0
  269. package/src/discord-slash-wire.ts +31 -2
  270. package/src/domains/outbound-alert.ts +1 -1
  271. package/src/elanous-config-dir.ts +29 -4
  272. package/src/execution-loop/launch-gate.ts +29 -14
  273. package/src/flow/cards.ts +239 -0
  274. package/src/graph-runner/graph-tick.ts +4 -0
  275. package/src/graph-runner/runner.ts +30 -5
  276. package/src/harness/harness-cli-command.ts +152 -27
  277. package/src/harness/harness-incidents.ts +43 -12
  278. package/src/harness/harness-pod-dispatch.ts +2 -0
  279. package/src/harness/harness-queue-child.ts +3 -3
  280. package/src/harness/harness-queue.ts +216 -28
  281. package/src/harness/helper-scan.ts +139 -0
  282. package/src/harness/model-input-observation.ts +37 -0
  283. package/src/hq/hq.ts +1 -1
  284. package/src/hq/promote.ts +301 -0
  285. package/src/index.ts +83 -21
  286. package/src/intake-plane/linear-wish.ts +39 -11
  287. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  288. package/src/intake-plane/wish-card.ts +53 -2
  289. package/src/intake-plane/wish-folder.ts +5 -2
  290. package/src/intake-plane/wish-reply.ts +72 -0
  291. package/src/lessons/lesson-import.ts +106 -0
  292. package/src/lessons/lesson-ledger.ts +206 -0
  293. package/src/loops/activity.ts +203 -0
  294. package/src/loops/budget.ts +32 -0
  295. package/src/loops/checker.ts +5 -3
  296. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  297. package/src/loops/loop-cli.ts +36 -4
  298. package/src/loops/neighbors.ts +40 -0
  299. package/src/loops/observe.ts +10 -0
  300. package/src/loops/orchestrator/author-depth.ts +164 -0
  301. package/src/loops/orchestrator/author-input-check.ts +77 -0
  302. package/src/loops/orchestrator/author-ledger.ts +230 -0
  303. package/src/loops/orchestrator/finish-rate.ts +175 -0
  304. package/src/loops/orchestrator/overlap.ts +51 -12
  305. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  306. package/src/loops/orchestrator/tick.ts +302 -0
  307. package/src/loops/orchestrator/traffic.ts +18 -3
  308. package/src/loops/registry.ts +454 -9
  309. package/src/loops/status-rows.ts +118 -0
  310. package/src/loops/verdict.ts +17 -5
  311. package/src/maturity/bot-slash-catalog.ts +66 -0
  312. package/src/maturity/feature-maturity.ts +7 -4
  313. package/src/nexus/api/dashboard.ts +6 -1
  314. package/src/nexus/api/http-server.ts +18 -2
  315. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  316. package/src/nexus/api/schedules-read.ts +5 -1
  317. package/src/nexus/api/task-cards-api.ts +44 -5
  318. package/src/nexus/api/workflows.ts +2 -1
  319. package/src/release-loop/auto-start.ts +48 -0
  320. package/src/release-loop/checklist.ts +13 -4
  321. package/src/release-loop/feature-store.ts +28 -16
  322. package/src/release-loop/landing-freeze.ts +121 -0
  323. package/src/release-loop/placement.ts +242 -0
  324. package/src/release-loop/release-schedule.ts +3 -1
  325. package/src/seat-dispatch/ceo-commands.ts +2 -2
  326. package/src/seat-dispatch/seat-ask.ts +16 -3
  327. package/src/seat-dispatch/seat-questions.ts +58 -0
  328. package/src/seat-loop/seat-loop.ts +406 -31
  329. package/src/self-dev/dev-pipeline.ts +6 -2
  330. package/src/self-dev/draft-sweep.ts +76 -0
  331. package/src/self-dev/run-store.ts +73 -1
  332. package/src/self-dev/run-supervisor.ts +8 -0
  333. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  334. package/src/self-implement/budget-gate.ts +29 -0
  335. package/src/self-implement/expose-gate.ts +47 -0
  336. package/src/self-implement/frozen-merges.ts +294 -0
  337. package/src/self-implement/gate-cli.ts +10 -4
  338. package/src/self-implement/gate-scope.ts +46 -9
  339. package/src/self-implement/goal-archive.ts +222 -0
  340. package/src/self-implement/goal-lookup.ts +147 -0
  341. package/src/self-implement/heal-intake.ts +79 -0
  342. package/src/self-implement/host-regate.ts +25 -2
  343. package/src/self-implement/orchestrator.ts +114 -10
  344. package/src/self-implement/revert-guard.ts +62 -0
  345. package/src/self-implement/run-ledger.ts +1 -1
  346. package/src/self-implement/seams.ts +28 -8
  347. package/src/session/index.ts +2 -0
  348. package/src/task-cards/card-cli.ts +10 -2
  349. package/src/task-cards/card-store.ts +10 -6
  350. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  351. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  352. package/src/task-orchestrator/surfaces/self-implement-pod.ts +80 -16
  353. package/src/telegram-commands.ts +27 -3
  354. package/src/telegram-loops-command.ts +44 -0
  355. package/src/user-config.ts +120 -16
  356. package/src/version/packed-revision.json +1 -1
  357. package/src/workflow-runtime/storage.ts +31 -13
  358. package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/3992-fe5c06d6b31f8b25.js +0 -25
  369. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  373. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/8605-9da4678f8a310d18.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/setup/page-d92e32d51ab5157e.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  444. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  445. /package/apps/pwa/out/_next/static/{D51l8jUBplJrIpWvENsDK → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
@@ -6,6 +6,10 @@ import { dirname, join, resolve } 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
+ import { surfaceEventsDbPath } from '../domains/surface-events.js';
10
+ import { judgeNeighbor } from '../loops/neighbors.js';
11
+ import { addHarnessQueue, harnessQueueIdForKey, harnessQueueOutcome, listHarnessQueue } from '../harness/harness-queue.js';
12
+ import { isRescueAllowedAction } from '../steward/rescue.js';
9
13
  import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
10
14
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
11
15
  import { loopEvent } from '../loops/observe.js';
@@ -17,7 +21,7 @@ import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePerson
17
21
  import { orderedPersonaTodos, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
18
22
  import { PersonaRegistry } from '../persona/registry.js';
19
23
  import { recordSeatAskShadow } from '../seat-dispatch/seat-ask.js';
20
- import { answerSeat, askSeat, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatQuestions, type SeatId } from '../seat-dispatch/seat-questions.js';
24
+ import { answerCrossCheck, answerSeat, askCrossCheck, askSeat, crossCheckAnswer, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatCrossChecks, seatQuestions, type CrossCheckJudgment, type SeatId } from '../seat-dispatch/seat-questions.js';
21
25
  import { queryRunningRuns } from '../self-implement/running-runs.js';
22
26
  import { checklistHistory, listChecklist, type Checklist } from '../release-loop/checklist.js';
23
27
  import { readSchedules, releasedVersion } from '../release-loop/feature-store.js';
@@ -38,7 +42,7 @@ export type TcCandidate =
38
42
  | { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
39
43
  | { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
40
44
  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 };
41
- export type SeatEntry = { seat: string; ts?: string; at: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean };
45
+ export type SeatEntry = { seat: string; ts?: string; at: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
42
46
  export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off' };
43
47
  export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
44
48
  export type SeatDeps = {
@@ -55,6 +59,10 @@ export type SeatDeps = {
55
59
  now?: () => Date;
56
60
  read?: (path: string) => string;
57
61
  run?: (args: string[]) => Promise<string>;
62
+ enqueue?: (seat: string, text: string, root: string, idempotencyKey: string) => Promise<{ id: string }>;
63
+ queueOutcome?: (id: string, root: string) => ReturnType<typeof harnessQueueOutcome>;
64
+ queueItems?: (root: string) => ReturnType<typeof listHarnessQueue>;
65
+ queueIdForKey?: (key: string, root: string) => string | undefined;
58
66
  append?: (path: string, entry: SeatEntry) => void;
59
67
  versions?: () => string[];
60
68
  /** Release schedules (version · cutAt) — a version is open while its cut is still ahead. Default reads the release ledger; unreadable ⇒ no checklist candidates (fail closed). */
@@ -77,6 +85,9 @@ export type SeatDeps = {
77
85
  inquire?: (seat: SeatId, item: SeatItem) => Promise<SeatInquiry | null> | SeatInquiry | null;
78
86
  /** Supply the receiving seat's answer; absent evidence must not invent a reply. */
79
87
  reply?: (seat: SeatId, question: string, from: SeatId) => Promise<SeatReply | null> | SeatReply | null;
88
+ /** Judge a decision draft against the receiving seat's evidence; null leaves the request pending. */
89
+ crossCheck?: (seat: SeatId, draft: string, from: SeatId) => Promise<CrossCheckJudgment | null> | CrossCheckJudgment | null;
90
+ decisionsConfig?: UserConfig['decisions'];
80
91
  };
81
92
 
82
93
  const repoRoot = resolve(import.meta.dir, '../..');
@@ -224,6 +235,64 @@ function versionOrder(a: string, b: string): number {
224
235
 
225
236
  function itemKey(item: SeatItem): string { return `${item.source}:${item.version ?? ''}:${item.id}`; }
226
237
 
238
+ function handledItemKey(item: SeatItem): string {
239
+ return `${itemKey(item)}:${JSON.stringify([item.evidenceHash ?? null, itemSnapshot(item)])}`;
240
+ }
241
+
242
+ function queueKey(seat: string, item: SeatItem): string {
243
+ return `seat-loop:${seat}:${createHash('sha256').update(handledItemKey(item)).digest('hex')}`;
244
+ }
245
+
246
+ /** 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
247
+ * seat-loop idempotency key so the outcome below (retry · launched · still queued) applies to it too (LOOP-LIVE1 review must-fix). */
248
+ function recoverAttemptingQueueIds(ledger: readonly SeatEntry[], root: string, deps: SeatDeps): SeatEntry[] {
249
+ const unsettled = (entry: SeatEntry) => (entry.status === 'attempting' || entry.status === 'outcome-unknown') && entry.action === 'harness' && !entry.queueId && !!entry.item;
250
+ const attempting = ledger.filter(unsettled);
251
+ if (attempting.length === 0) return [...ledger];
252
+ let rows: ReturnType<typeof listHarnessQueue>;
253
+ try { rows = (deps.queueItems ?? ((stateRoot: string) => listHarnessQueue({ root: stateRoot })))(root); }
254
+ catch (error) { observe('queue-recovery-unreadable', { error: String(error).slice(0, 200) }); return [...ledger]; }
255
+ return ledger.map((entry, index) => {
256
+ const item = entry.item;
257
+ if (!unsettled(entry) || !item) return entry;
258
+ const key = handledItemKey(item);
259
+ // A later row for the same item already settled this attempt.
260
+ if (ledger.slice(index + 1).some((later) => later.seat === entry.seat && later.item && handledItemKey(later.item) === key && later.status !== 'attempting' && later.status !== 'outcome-unknown')) return entry;
261
+ const row = rows.find((candidate) => candidate.seat === entry.seat && candidate.idempotencyKey === queueKey(entry.seat, item)
262
+ && candidate.input === seatTaskText(entry.seat, item));
263
+ // The row may already have left the queue (finished · cancelled); its settled id is kept by key.
264
+ const id = row?.id ?? (deps.queueIdForKey ?? ((key: string, stateRoot: string) => harnessQueueIdForKey(key, { root: stateRoot })))(queueKey(entry.seat, item), root);
265
+ if (!id) return entry;
266
+ observe('queue-recovered', { seat: entry.seat, item: item.id, queueId: id, from: row ? 'queue' : 'key-marker' });
267
+ return { ...entry, status: 'queued' as const, queueId: id };
268
+ });
269
+ }
270
+
271
+ function reconciledQueueLedger(recorded: readonly SeatEntry[], root: string, deps: SeatDeps): SeatEntry[] {
272
+ const ledger = recoverAttemptingQueueIds(recorded, root, deps);
273
+ const outcome = deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot }));
274
+ const retryable = new Set(ledger.filter((entry) => entry.status === 'queued' && entry.queueId
275
+ && outcome(entry.queueId, root) === 'retryable' && entry.item).map((entry) => `${entry.seat}:${handledItemKey(entry.item!)}`));
276
+ const pending = new Set(ledger.filter((entry) => entry.status === 'queued' && entry.queueId
277
+ && ['pending', 'unknown'].includes(outcome(entry.queueId, root)) && entry.item).map((entry) => `${entry.seat}:${handledItemKey(entry.item!)}`));
278
+ const succeeded = new Set(ledger.filter((entry) => entry.status === 'queued' && entry.queueId
279
+ && outcome(entry.queueId, root) === 'succeeded' && entry.item).map((entry) => `${entry.seat}:${handledItemKey(entry.item!)}`));
280
+ return ledger.map((entry) => {
281
+ if (entry.status === 'attempting' && entry.item) {
282
+ const key = `${entry.seat}:${handledItemKey(entry.item)}`;
283
+ if (retryable.has(key) && !pending.has(key) && !succeeded.has(key)) return { ...entry, status: 'refused' as const };
284
+ }
285
+ if (entry.status !== 'queued' || !entry.queueId) return entry;
286
+ const state = outcome(entry.queueId, root);
287
+ if (state === 'retryable') {
288
+ observe('queue-retryable', { seat: entry.seat, item: entry.item?.id, queueId: entry.queueId });
289
+ return { ...entry, status: entry.item && (pending.has(`${entry.seat}:${handledItemKey(entry.item)}`) || succeeded.has(`${entry.seat}:${handledItemKey(entry.item)}`)) ? 'queued' as const : 'refused' as const,
290
+ reason: 'AUTOQ failed or cancelled; eligible for retry' };
291
+ }
292
+ return { ...entry, status: state === 'succeeded' ? 'launched' as const : entry.status };
293
+ });
294
+ }
295
+
227
296
  function itemSnapshot(item: SeatItem): string {
228
297
  return JSON.stringify([item.title, item.text, item.evidence ?? null, item.status ?? null]);
229
298
  }
@@ -238,14 +307,15 @@ function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEn
238
307
  }
239
308
 
240
309
  // RM3 (asked/answered · decision-card receipts for seat inquiries) ⊕ main (checklist cells re-picked by evidence, not by a shadow row).
241
- function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string): Set<string> {
310
+ function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string, liveSafe = false): Set<string> {
242
311
  const inquiryRow = (entry: SeatEntry) => entry.action === 'seat-question' || entry.action === 'seat-answer' || !!entry.inquiry;
243
312
  return new Set(ledger.filter((entry, index) => entry.status === 'asked' || entry.status === 'answered'
313
+ || entry.status === 'resolved-by-neighbor'
244
314
  // 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.
245
315
  || (entry.status === 'shadow' && entry.candidate?.kind === 'other-seat-cell-defect')
246
316
  || (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
247
317
  || (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
248
- || (!inquiryRow(entry) && entry.status === 'outcome-unknown')
318
+ || (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown'))
249
319
  || (!inquiryRow(entry) && entry.status === 'attempting' && !(entry.item?.source === 'checklist' && ledger.slice(index + 1).some((later) =>
250
320
  later.seat === entry.seat && later.status === 'launched' && later.item?.source === 'checklist'
251
321
  && later.item.version === entry.item?.version && later.item.id === entry.item?.id
@@ -253,12 +323,14 @@ function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: strin
253
323
  || (shadow && entry.status === 'shadow' && entry.action !== 'wait' && entry.item?.source !== 'checklist')
254
324
  || (entry.status === 'launched' && entry.item?.source !== 'checklist'))
255
325
  .filter((entry): entry is SeatEntry & { item: SeatItem } => !!entry.item && !!entry.item.source && typeof entry.item.id === 'string')
256
- .map((entry) => itemKey(entry.item)));
326
+ .map((entry) => !liveSafe || inquiryRow(entry) ? itemKey(entry.item) : handledItemKey(entry.item)));
257
327
  }
258
328
 
259
329
  function eligibleItem(item: SeatItem, ledger: readonly SeatEntry[], handled: ReadonlySet<string>, shadow: boolean): boolean {
260
330
  const key = itemKey(item);
261
- if (handled.has(key)) return false;
331
+ if (handled.has(key) || handled.has(handledItemKey(item))) return false;
332
+ const rejected = [...ledger].reverse().find((entry) => entry.item && itemKey(entry.item) === key && entry.status === 'rejected-no-evidence');
333
+ if (rejected?.item && itemSnapshot(rejected.item) === itemSnapshot(item)) return false;
262
334
  const lastWait = [...ledger].reverse().find((entry) => entry.item && itemKey(entry.item) === key
263
335
  && (entry.status === 'wait' || (shadow && entry.status === 'shadow' && entry.action === 'wait')));
264
336
  return !lastWait?.item || itemSnapshot(lastWait.item) !== itemSnapshot(item);
@@ -275,7 +347,7 @@ export function alreadyHandled(item: SeatItem, ledger: readonly SeatEntry[]): bo
275
347
  const at = Date.parse(entry.at || entry.ts || '');
276
348
  return entry.seat === item.seat && at >= now - CHECKLIST_HANDLED_WINDOW_MS && at <= now
277
349
  // A shadowed seat question was never delivered — it must be picked again once delivery is on (RM3 · TC merge 10-03).
278
- && (entry.status === 'launched' || (entry.status === 'shadow' && !entry.inquiry))
350
+ && (entry.status === 'launched' || entry.status === 'queued' || (entry.status === 'shadow' && !entry.inquiry))
279
351
  && entry.item?.source === 'checklist' && entry.item.id === item.id && entry.item.version === item.version
280
352
  && entry.item.title === item.title && entry.item.evidenceHash === item.evidenceHash;
281
353
  });
@@ -292,7 +364,8 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
292
364
  const read = deps.read ?? defaultRead;
293
365
  const root = deps.root ?? effectiveInstanceRoot();
294
366
  const activeLedger = ledger ?? readSeatLedger(seat, deps, (deps.now ?? (() => new Date()))());
295
- const consumed = handledKeys(activeLedger, (deps.config ?? getUserConfig().loops?.seat)?.mode === 'shadow', root);
367
+ const mode = (deps.config ?? getUserConfig().loops?.seat)?.mode;
368
+ const consumed = handledKeys(activeLedger, mode === 'shadow', root, mode === 'live-safe');
296
369
  const requests = new Map<string, Record<string, unknown>>();
297
370
  for (const row of rows(read(join(root, 'seat-requests', 'requests.jsonl')))) {
298
371
  if (typeof row.key === 'string') requests.set(row.key, row);
@@ -330,7 +403,7 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
330
403
  try { fromSchedule = readSchedules().map((row) => row.version); } catch { /* ledger unreadable — directories only */ }
331
404
  return [...new Set([...fromDirs, ...fromSchedule])].filter((v) => /^\d+\.\d+\.\d+$/.test(v));
332
405
  });
333
- const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version).items);
406
+ const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version, releaseRoot).items);
334
407
  let shipped: string | undefined;
335
408
  try { shipped = releasedVersion(releaseRoot); }
336
409
  catch (error) {
@@ -375,6 +448,11 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
375
448
  debug.log('seat.loop', 'skip-closed-versions', { seat, count: closed.length, newest: closed.at(-1)!, oldest: closed[0]! });
376
449
  }
377
450
  checklist.sort((a, b) => versionOrder(a.version!, b.version!) || a.id.localeCompare(b.id));
451
+ const ownedEvidence = checklist.filter((item) => item.evidence?.trim())
452
+ .map((item) => `${item.id} · ${item.title}: ${item.evidence!.trim()}`).join('\n').slice(0, 8_000);
453
+ for (const request of pending) {
454
+ if (request.source === 'seat-question' && ownedEvidence) request.evidence = ownedEvidence;
455
+ }
378
456
  return { requests: pending, checklist, role: read(join(deps.repo ?? repoRoot, 'docs', 'roles', `${seat}.md`)).slice(0, 4_000) };
379
457
  }
380
458
 
@@ -583,6 +661,10 @@ function escalateStalls(seat: string, deps: SeatDeps, config: SeatLoopConfig, no
583
661
  debug.log('org.stall', 'escalate', { ...data, mode: 'shadow' });
584
662
  continue;
585
663
  }
664
+ if (config.mode === 'live-safe' && !isRescueAllowedAction('alert')) {
665
+ observe('refused', { seat, item: item.id, action: 'alert', reason: 'policy denied internal stall alert' });
666
+ continue;
667
+ }
586
668
  const key = JSON.stringify([schedule.version, item.id, from, to, reason, startedAt]);
587
669
  const message: MessageEnvelope = { from, to, kind: 'stall-escalation',
588
670
  body: `[stall:${key}] ${schedule.version} 체크리스트 ${item.id} · ${item.title} · ${reason} · ${stalledMin}분 정체 (${from} → ${to})` };
@@ -601,8 +683,8 @@ function escalateStalls(seat: string, deps: SeatDeps, config: SeatLoopConfig, no
601
683
  }
602
684
 
603
685
  // In shadow mode a shadowed item counts as handled, so a rehearsal day walks the queue like the seat would.
604
- export function pickNext(inputs: SeatInputs, ledger: readonly SeatEntry[], opts: { shadow?: boolean; root?: string; history?: SeatDeps['checklistHistory'] } = {}): SeatItem | null {
605
- const handled = handledKeys(ledger, opts.shadow === true, opts.root);
686
+ export function pickNext(inputs: SeatInputs, ledger: readonly SeatEntry[], opts: { shadow?: boolean; liveSafe?: boolean; root?: string; history?: SeatDeps['checklistHistory'] } = {}): SeatItem | null {
687
+ const handled = handledKeys(ledger, opts.shadow === true, opts.root, opts.liveSafe === true);
606
688
  return [...inputs.requests, ...inputs.checklist].find((item) => {
607
689
  if (alreadyHandled(item, ledger)) {
608
690
  debug.log('seat.loop', 'skip-handled', { id: item.id });
@@ -666,9 +748,85 @@ export function seatTaskText(seat: string, item: SeatItem): string {
666
748
  return `[${seat} 자리 · ${where} · 역할 docs/roles/${seat}.md] ${item.title.replace(/[\r\n]+/g, ' ')}`;
667
749
  }
668
750
 
751
+ function xcheckEvent(event: string, seat: string, neighbor: string, item: string, outcome: string): void {
752
+ try { debug.log('decisions.xcheck', event, { seat, neighbor, item, outcome }); }
753
+ catch { /* Observation does not alter delivery. */ }
754
+ }
755
+
756
+ function neighborLastSeen(root: string, isolated: boolean, now: Date, wanted: ReadonlySet<string>): Map<string, string> {
757
+ const path = isolated ? join(root, 'surface_events.db') : surfaceEventsDbPath();
758
+ const last = new Map<string, string>();
759
+ if (!existsSync(path)) return last;
760
+ const db = new Database(path, { readonly: true });
761
+ try {
762
+ // Same outbound seat events and refs.seat as context day; unlike its 24h display window,
763
+ // look back across the whole journal so a genuinely old ACK can be judged absent.
764
+ // Newest first and stop once every wanted seat has an ACK, so a long journal is not read and sorted in full each turn.
765
+ const statement = db.prepare(`SELECT ts, refs FROM events
766
+ WHERE surface IN ('coord:channel', 'context:session') AND direction='outbound' AND ts<=? ORDER BY ts DESC`);
767
+ try {
768
+ const events = statement.iterate(now.toISOString()) as Iterable<{ ts: string; refs: string | null }>;
769
+ for (const event of events) {
770
+ let seat: unknown;
771
+ try { seat = (JSON.parse(event.refs ?? '{}') as { seat?: unknown }).seat; } catch { continue; }
772
+ if (typeof seat === 'string' && wanted.has(seat) && !last.has(seat)
773
+ && Number.isFinite(Date.parse(event.ts)) && Date.parse(event.ts) <= now.getTime()) last.set(seat, event.ts);
774
+ if (last.size >= wanted.size) break;
775
+ }
776
+ } finally { statement.finalize(); }
777
+ return last;
778
+ } finally { db.close(); }
779
+ }
780
+
781
+ function neighborSeat(id: string): string | null {
782
+ const match = /^(op|tc|mk|ux)-seat$/i.exec(id);
783
+ return match ? match[1]!.toUpperCase() : null;
784
+ }
785
+
786
+ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps, root: string, now: Date): void {
787
+ const neighbors = config.neighbors?.[seat as keyof NonNullable<SeatLoopConfig['neighbors']>] ?? [];
788
+ if (!neighbors.length) return;
789
+ let seen = new Map<string, string>();
790
+ let unreadable = false;
791
+ const wanted = new Set(neighbors.flatMap((neighbor) => { const source = neighborSeat(neighbor.id); return source ? [source] : []; }));
792
+ try { seen = wanted.size ? neighborLastSeen(root, !!deps.root, now, wanted) : seen; }
793
+ catch (error) { unreadable = true; observe('neighbor-source-unreadable', { seat, error: String(error).slice(0, 200) }); }
794
+ for (const neighbor of neighbors) {
795
+ try {
796
+ const source = neighborSeat(neighbor.id);
797
+ const lastSeenAt = source ? seen.get(source) : undefined;
798
+ const judgment = judgeNeighbor({ neighbor, lastSeenAt: unreadable ? null : lastSeenAt, now });
799
+ // action = what this turn actually did; plannedAction = what onAbsent asks for (shadow only records it).
800
+ let action: string | null = null;
801
+ const plannedAction = judgment.state === 'absent' ? judgment.action?.action ?? null : null;
802
+ if (judgment.state === 'absent') {
803
+ if (config.mode === 'live-safe' && judgment.action?.action === 'escalate' && isRescueAllowedAction('decision-card')) {
804
+ const ref = `seat-loop:neighbor:${seat}:${neighbor.id}:${lastSeenAt}`;
805
+ new DecisionLedger({ stateDir: root, now: () => now }).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
806
+ scqa: { s: `${seat} 이웃 ${neighbor.id}의 마지막 맥락 버스 사건: ${lastSeenAt}.`,
807
+ c: `설정한 heartbeat ${neighbor.heartbeat.everyMinutes}분 × ${neighbor.heartbeat.missedTicks}회 동안 새 사건이 없다.`,
808
+ q: '결측 이웃의 업무를 어떻게 처리할까?' },
809
+ options: [{ key: 'a', label: '사람 판단 후 별도 조치', consequence: '자리 루프는 대행하지 않는다' },
810
+ { key: 'b', label: '현상 유지', consequence: '결측 상태를 계속 관측한다' }],
811
+ recommendation: { skipped: true, reason: '결측만으로 대행이나 보류를 자동 결정하지 않는다' },
812
+ raisedBy: { agent: 'seat-loop' }, refs: [ref], raisedAt: now.toISOString(),
813
+ crossCheckSkipped: '이웃 결측으로 교차 확인 불가' }, ref);
814
+ action = 'decision-card';
815
+ }
816
+ }
817
+ try { debug.log('loop.neighbors', 'tick', { seat, neighbor: neighbor.id, state: judgment.state, action, plannedAction, mode: config.mode }); }
818
+ catch { /* Observation cannot change the judgment. */ }
819
+ } catch (error) {
820
+ observe('neighbor-judgment-failed', { seat, neighbor: neighbor.id, error: String(error).slice(0, 200) });
821
+ try { debug.log('loop.neighbors', 'tick', { seat, neighbor: neighbor.id, state: 'unknown', action: null, mode: config.mode }); }
822
+ catch { /* Observation cannot change the turn. */ }
823
+ }
824
+ }
825
+ }
826
+
669
827
  export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
670
828
  if (!/^(?:MK|OP|TC|UX)$/.test(seat)) throw new Error(`unknown seat: ${seat}`);
671
- const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'off' };
829
+ const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'shadow' };
672
830
  if (config.mode === 'off' || !(config.seats ?? ['MK']).includes(seat)) {
673
831
  observe('skipped-off', { seat });
674
832
  return { seat, status: 'skipped-off' };
@@ -677,10 +835,99 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
677
835
  const path = seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now);
678
836
  const directory = dirname(path);
679
837
  return withSeatLock(directory, async () => {
680
- const ledger = readSeatLedger(seat, deps, now);
681
838
  const stateRoot = deps.root ?? effectiveInstanceRoot();
839
+ const recorded = readSeatLedger(seat, deps, now);
840
+ const ledger = config.mode === 'live-safe' ? reconciledQueueLedger(recorded, stateRoot, deps) : recorded;
682
841
  let turnError: unknown;
683
842
  try {
843
+ if (config.mode === 'on' && config.questions === 'on') {
844
+ const proposals = ledger.filter((row) => row.status === 'awaiting-resolution' && row.xcheckQuestionId !== undefined
845
+ && ((row.item?.source === 'checklist' && row.item.version) || row.item?.source === 'seat-question'));
846
+ for (const proposal of proposals) {
847
+ if (ledger.some((row) => row.status === 'resolved-by-neighbor' && row.xcheckQuestionId === proposal.xcheckQuestionId)) continue;
848
+ // Review round 3 ②: a seat question has no checklist cell — it is settled when this seat answers it later
849
+ // (new evidence) and otherwise follows the same deadline escalation below.
850
+ const isQuestion = proposal.item!.source === 'seat-question';
851
+ if (isQuestion && ledger.some((row) => row.item && itemKey(row.item) === itemKey(proposal.item!)
852
+ && (row.status === 'answered' || row.status === 'hitl') && Date.parse(row.at) > Date.parse(proposal.at))) continue;
853
+ let cell: { status: string } | undefined;
854
+ if (!isQuestion) try {
855
+ cell = (deps.checklistItems ?? ((version: string) => listChecklist(version, stateRoot).items))(proposal.item!.version!)
856
+ .find((candidate) => candidate.id === proposal.item!.id && candidate.owner === seat);
857
+ } catch (error) {
858
+ observe('xcheck-resolution-verification-failed', { seat, neighbor: proposal.xcheckNeighbor, item: itemKey(proposal.item!), error: String(error).slice(0, 200) });
859
+ }
860
+ if (cell?.status === 'green' || cell?.status === 'done') {
861
+ const entry: SeatEntry = { ...proposal, ts: now.toISOString(), at: now.toISOString(), status: 'resolved-by-neighbor' };
862
+ (deps.append ?? defaultAppend)(path, entry);
863
+ xcheckEvent('resolved-by-neighbor', seat, proposal.xcheckNeighbor ?? '', itemKey(proposal.item!), cell.status);
864
+ return entry;
865
+ }
866
+ // Review round 3 ①: a neighbor's «I can resolve it» has a deadline — if the cell is still open after the
867
+ // cross-check wait, escalate to a CEO card instead of excluding the item forever.
868
+ const waited = proposal.xcheckWaitMinutes ?? 120;
869
+ const proposedAt = Date.parse(proposal.at);
870
+ if ((cell || isQuestion) && Number.isFinite(proposedAt) && now.getTime() - proposedAt >= waited * 60_000) {
871
+ const neighbor = proposal.xcheckNeighbor ?? '';
872
+ const question = proposal.inquiry?.question ?? proposal.item!.title;
873
+ const note = `${proposal.xcheckNote ?? ''} — 해결 가능하다고 했으나 ${waited}분 안 해소 안 됨`.trim();
874
+ if (!decisionReceipt(stateRoot, seat, proposal.item!)) {
875
+ await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${question}`, '--category', 'other',
876
+ '--s', `${seat} 질문 · ${proposal.item!.title}`, '--c', '이웃 자리 해결 제안이 기한 안에 해소되지 않았다',
877
+ '--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
878
+ '--skip-recommend', '근거가 부족하다', '--xcheck', `${neighbor}:${note}`,
879
+ '--agent', 'seat-loop', '--ref', decisionRef(seat, proposal.item!), '--json']);
880
+ }
881
+ const entry: SeatEntry = { ...proposal, ts: now.toISOString(), at: now.toISOString(), status: 'hitl' };
882
+ (deps.append ?? defaultAppend)(path, entry);
883
+ xcheckEvent('raised', seat, neighbor, itemKey(proposal.item!), 'resolution-timed-out');
884
+ return entry;
885
+ }
886
+ }
887
+ const received = seatCrossChecks(stateRoot, seat as SeatId).filter((question) => !crossCheckAnswer(stateRoot, question));
888
+ const receivedInputs = received.length ? await gatherSeatInputs(seat, deps, ledger) : null;
889
+ const judgmentEvidence = receivedInputs?.checklist.filter((item) => item.evidence?.trim())
890
+ .map((item) => `${item.id} · ${item.title}: ${item.evidence!.trim()}`).join('\n').slice(0, 8_000) ?? '';
891
+ const pending = received.find((question) => {
892
+ const senderRows = readSeatLedger(question.from, { root: stateRoot }, now).filter((row) => row.xcheckQuestionId === question.id);
893
+ const latest = senderRows.at(-1);
894
+ if (latest?.status === 'hitl' || latest?.status === 'resolved-by-neighbor' || latest?.status === 'awaiting-resolution') return false;
895
+ const request = senderRows.find((row) => row.status === 'awaiting-xcheck' && row.xcheckRequestedAt);
896
+ const waitMinutes = request?.xcheckWaitMinutes ?? 120;
897
+ if (request?.xcheckRequestedAt && now.getTime() - Date.parse(request.xcheckRequestedAt) >= waitMinutes * 60_000) return false;
898
+ return !ledger.some((row) => row.xcheckQuestionId === question.id && row.status === 'awaiting-xcheck'
899
+ && row.xcheckJudgmentEvidence === judgmentEvidence);
900
+ });
901
+ if (pending) {
902
+ const append = deps.append ?? defaultAppend;
903
+ const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'awaiting-xcheck',
904
+ action: 'seat-answer', xcheckQuestionId: pending.id, xcheckNeighbor: pending.from as SeatId,
905
+ xcheckJudgmentEvidence: judgmentEvidence };
906
+ try {
907
+ const judge = deps.crossCheck ?? (async (receiver: SeatId, draft: string, from: SeatId): Promise<CrossCheckJudgment | null> => {
908
+ const inputs = receivedInputs!;
909
+ const evidence = judgmentEvidence;
910
+ if (!evidence) throw new Error('cross-check judgment requires receiving-seat evidence');
911
+ const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
912
+ `[${receiver} 자리 · 역할 docs/roles/${receiver}.md] ${from} 자리의 결정 초안을 교차 확인한다. 아래 실제 근거만 사용하고 모르는 사실은 지어내지 마라. JSON 하나만 반환: {"agree":boolean,"note":"근거를 포함한 메모","resolves":boolean}. 이웃 자리에서 직접 해결할 수 있을 때만 resolves=true. 초안: ${draft}\n역할: ${inputs.role}\n${receiver} 체크리스트 근거:\n${evidence || '(없음)'}`])).trim()) as { reply?: unknown };
913
+ const value: unknown = typeof output.reply === 'string' ? JSON.parse(output.reply) : output;
914
+ return value as CrossCheckJudgment;
915
+ });
916
+ const answer = await judge(seat as SeatId, pending.body, pending.from as SeatId);
917
+ if (!answer || typeof answer.agree !== 'boolean' || typeof answer.resolves !== 'boolean'
918
+ || typeof answer.note !== 'string' || !answer.note.trim()) throw new Error('invalid cross-check judgment');
919
+ answerCrossCheck(stateRoot, pending, answer);
920
+ entry.status = 'answered';
921
+ entry.xcheckNote = answer.note.trim();
922
+ append(path, entry);
923
+ xcheckEvent('answered', seat, pending.from, String(pending.id), answer.resolves ? 'resolves' : answer.agree ? 'agree' : 'dissent');
924
+ } catch (error) {
925
+ append(path, entry);
926
+ xcheckEvent('answered', seat, pending.from, String(pending.id), `judgment-failed: ${String(error).slice(0, 200)}`);
927
+ }
928
+ return entry;
929
+ }
930
+ }
684
931
  if (seat === 'TC') {
685
932
  const candidates = await tcCandidates(deps, now);
686
933
  for (const candidate of candidates) {
@@ -759,17 +1006,45 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
759
1006
  const inputs = await gatherSeatInputs(seat, deps, ledger);
760
1007
  const questionsMode = config.mode === 'on' ? config.questions ?? 'shadow' : 'shadow';
761
1008
  const questionHandled = handledKeys(ledger, questionsMode !== 'on', stateRoot);
762
- const pendingQuestion = inputs.requests.find((candidate) => candidate.source === 'seat-question' && !questionHandled.has(itemKey(candidate)));
763
- const ordinary = pickNext({ ...inputs, requests: inputs.requests.filter((candidate) => candidate.source !== 'seat-question') }, ledger,
764
- { shadow: config.mode === 'shadow', root: stateRoot, history: deps.checklistHistory ?? checklistHistory });
1009
+ const pendingQuestion = inputs.requests.find((candidate) => candidate.source === 'seat-question'
1010
+ && eligibleItem(candidate, ledger, questionHandled, questionsMode !== 'on')
1011
+ && !ledger.some((row) => row.status === 'awaiting-resolution' && row.item
1012
+ && itemKey(row.item) === itemKey(candidate) && itemSnapshot(row.item) === itemSnapshot(candidate)));
1013
+ const ordinary = pickNext({ ...inputs, requests: inputs.requests.filter((candidate) => candidate.source !== 'seat-question'),
1014
+ checklist: inputs.checklist.filter((candidate) => !ledger.some((row) => row.status === 'awaiting-resolution'
1015
+ && row.item && itemKey(row.item) === itemKey(candidate) && itemSnapshot(row.item) === itemSnapshot(candidate))) }, ledger,
1016
+ { shadow: config.mode === 'shadow', liveSafe: config.mode === 'live-safe', root: stateRoot, history: deps.checklistHistory ?? checklistHistory });
765
1017
  const item = pendingQuestion ?? ordinary;
766
1018
  const append = deps.append ?? defaultAppend;
1019
+ const revisedProposal = item?.source === 'checklist' && ledger.some((row) => row.status === 'awaiting-resolution'
1020
+ && row.item && itemKey(row.item) === itemKey(item) && itemSnapshot(row.item) !== itemSnapshot(item));
1021
+ if (config.mode === 'live-safe' && !pendingQuestion) {
1022
+ const pendingRows = (deps.queueItems ?? ((root: string) => listHarnessQueue({ root })))(stateRoot);
1023
+ for (const candidate of [...inputs.requests, ...inputs.checklist].filter((row) => row.source !== 'seat-question')) {
1024
+ const pending = pendingRows.find((row) => row.seat === seat && row.kind === 'say'
1025
+ && row.idempotencyKey === queueKey(seat, candidate) && row.input === seatTaskText(seat, candidate)
1026
+ && ['queued', 'launching', 'launched'].includes(row.status)
1027
+ && ['pending', 'unknown'].includes((deps.queueOutcome ?? ((id: string, root: string) => harnessQueueOutcome(id, { root })))(row.id, stateRoot)));
1028
+ if (!pending) continue;
1029
+ if (ledger.some((row) => row.queueId === pending.id && row.status === 'queued')) continue;
1030
+ const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'queued', item: candidate, action: 'harness', queueId: pending.id };
1031
+ append(path, entry);
1032
+ observe('queued', { seat, item: candidate, queueId: pending.id, deduplicated: true });
1033
+ return entry;
1034
+ }
1035
+ }
767
1036
  // RM3 review must-fix: `questions` now has a default, so it can't be the trigger — a shadow seat loop never calls the model on its own.
768
- if (item && (item.source === 'seat-question' || deps.inquire || (config.mode === 'on' && config.questions !== undefined))) {
1037
+ if (item && (!revisedProposal || planAction(item, seat).kind === 'decision')
1038
+ && (item.source === 'seat-question' || (config.mode !== 'live-safe' && (deps.inquire || (config.mode === 'on' && config.questions !== undefined))))) {
769
1039
  const root = deps.root ?? effectiveInstanceRoot();
770
- const unfinished = [...ledger].reverse().find((row) => row.item && itemKey(row.item) === itemKey(item)
771
- && (row.status === 'attempting' || row.status === 'outcome-unknown') && (row.action === 'seat-question' || row.action === 'seat-answer'));
772
- const inquiry = item.source === 'seat-question' ? null : unfinished?.inquiry ?? await (deps.inquire ?? (async (asking: SeatId, work: SeatItem) => { try {
1040
+ const unfinished = [...ledger].reverse().find((row) => row.item && itemKey(row.item) === itemKey(item)
1041
+ && !(item.source === 'seat-question' && ledger.some((prior) => prior.status === 'awaiting-resolution'
1042
+ && prior.item && itemKey(prior.item) === itemKey(item) && itemSnapshot(prior.item) !== itemSnapshot(item)))
1043
+ && (row.status === 'attempting' || row.status === 'outcome-unknown' || row.status === 'awaiting-xcheck')
1044
+ && (row.action === 'seat-question' || row.action === 'seat-answer' || row.inquiry?.to === 'CEO'));
1045
+ const inquiry = item.source === 'seat-question' ? null : revisedProposal && item.source === 'checklist'
1046
+ ? ledger.find((row) => row.status === 'awaiting-resolution' && row.item && itemKey(row.item) === itemKey(item))?.inquiry ?? null
1047
+ : unfinished?.inquiry ?? await (deps.inquire ?? (async (asking: SeatId, work: SeatItem) => { try {
773
1048
  const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
774
1049
  `[${asking} 자리 · 역할 docs/roles/${asking}.md] 이 항목을 판단할 근거가 부족한 경우에만 다른 자리 또는 사람에게 질문한다. 근거가 충분하면 null. JSON 객체 하나만 반환: null 또는 {"to":"OP|TC|MK|UX|CEO","question":"필요한 근거를 묻는 질문"}. 항목: ${work.id} ${work.title}\n내용: ${work.text}\n역할: ${(deps.read ?? defaultRead)(join(deps.repo ?? repoRoot, 'docs', 'roles', `${asking}.md`)).slice(0, 4_000)}`])).trim()) as { reply?: unknown };
775
1050
  const value: unknown = typeof output?.reply === 'string' ? JSON.parse(output.reply) : output;
@@ -788,7 +1063,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
788
1063
  const reply = item.source === 'seat-question' && item.from && (questionsMode === 'on' || deps.reply || config.questions !== undefined)
789
1064
  ? unfinished?.answer ? { answer: unfinished.answer } : unfinished?.inquiry ? unfinished.inquiry : await (deps.reply ?? (async (receiver: SeatId, question: string, from: SeatId) => {
790
1065
  const evidence = inputs.checklist.map((candidate) =>
791
- `${candidate.id} · ${candidate.title} (판 ${candidate.version}, ${candidate.kind})`).join('\n').slice(0, 8_000);
1066
+ `${candidate.id} · ${candidate.title} (판 ${candidate.version}, ${candidate.kind})${candidate.evidence?.trim() ? ` · 근거: ${candidate.evidence.trim()}` : ''}`).join('\n').slice(0, 8_000);
792
1067
  const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
793
1068
  `[${receiver} 자리 · 역할 docs/roles/${receiver}.md] ${from} 자리의 질문에 근거를 갖고 답하라. 아래 ${receiver} 자리의 실제 체크리스트 근거에 없는 사실은 지어내지 마라. 모르면 사람 결정이 필요한 질문을 작성하라. JSON 하나만 반환: {"answer":"근거를 포함한 답"} 또는 {"to":"CEO","question":"결정할 질문"}. 질문: ${question}\n역할: ${inputs.role}\n${receiver} 체크리스트 근거:\n${evidence || '(없음)'}`])).trim()) as { reply?: unknown };
794
1069
  const value: unknown = typeof output.reply === 'string' ? JSON.parse(output.reply) : output;
@@ -810,6 +1085,9 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
810
1085
  entry.action = 'decision';
811
1086
  }
812
1087
  if (questionsMode !== 'on') {
1088
+ if (config.mode === 'live-safe' && !isRescueAllowedAction('seat-question')) {
1089
+ observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'question delivery is shadow-only' });
1090
+ }
813
1091
  append(path, entry);
814
1092
  observe('question-shadow', { seat, item, inquiry: entry.inquiry, answered: !!entry.answer });
815
1093
  return entry;
@@ -819,7 +1097,18 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
819
1097
  append(path, entry);
820
1098
  return entry;
821
1099
  }
822
- append(path, { ...entry, status: 'attempting' });
1100
+ if (entry.inquiry?.to === 'CEO' && !item.evidence?.trim() && !decisionReceipt(root, seat, item)
1101
+ && !ledger.some((row) => row.item && itemKey(row.item) === itemKey(item) && row.status === 'awaiting-xcheck')
1102
+ && !deliveredSeatQuestion(root, `xcheck:${seat}:${itemKey(item)}`)) {
1103
+ entry.status = 'rejected-no-evidence';
1104
+ append(path, entry);
1105
+ xcheckEvent('rejected-no-evidence', seat, (deps.decisionsConfig ?? getUserConfig().decisions)?.crossCheckNeighbor?.[seat as SeatId] ?? 'OP', itemKey(item), 'no-evidence');
1106
+ return entry;
1107
+ }
1108
+ if (!revisedProposal && (entry.inquiry?.to !== 'CEO' || (unfinished?.status !== 'awaiting-xcheck' && !ledger.some((row) =>
1109
+ row.item && itemKey(row.item) === itemKey(item) && row.status === 'awaiting-xcheck')))) {
1110
+ append(path, { ...entry, status: 'attempting' });
1111
+ }
823
1112
  try {
824
1113
  if (item.source === 'seat-question' && reply && 'answer' in reply) {
825
1114
  const question = { id: Number(item.id), from: item.from!, to: seat, body: item.text, kind: 'seat-question', createdAt: item.createdAt ?? '' };
@@ -829,16 +1118,55 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
829
1118
  const ask = entry.inquiry!;
830
1119
  if (!ask.question.trim()) throw new Error('empty seat question');
831
1120
  if (ask.to === 'CEO') {
1121
+ const settings = deps.decisionsConfig ?? getUserConfig().decisions!;
1122
+ const neighbor = settings.crossCheckNeighbor?.[seat as SeatId] ?? ({ OP: 'TC', TC: 'OP', MK: 'OP', UX: 'OP' } as const)[seat as SeatId];
1123
+ const waitMinutes = settings.crossCheckWaitMinutes ?? 120;
1124
+ const previous = [...ledger].reverse().find((row) => row.item && itemKey(row.item) === itemKey(item)
1125
+ && row.status === 'awaiting-xcheck' && row.xcheckQuestionId !== undefined);
832
1126
  const prior = decisionReceipt(root, seat, item);
833
- if (!prior) {
834
- const run = deps.run ?? defaultRun;
835
- await run(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
836
- '--s', `${seat} 질문 · ${item.title}`, '--c', '판단 근거 부족 — 사람의 결정이 필요하다',
837
- '--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
838
- '--skip-recommend', '근거가 부족하다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②', '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
1127
+ if (prior) {
1128
+ entry.status = 'hitl';
1129
+ } else {
1130
+ const key = `xcheck:${seat}:${itemKey(item)}`;
1131
+ const delivered = deliveredSeatQuestion(root, key);
1132
+ if (delivered && (delivered.kind !== 'seat-xcheck' || delivered.from !== seat || !isSeatId(delivered.to))) {
1133
+ throw new Error('cross-check retry has an invalid recorded recipient');
1134
+ }
1135
+ const requestedNeighbor = delivered ? delivered.to as SeatId : neighbor;
1136
+ if (!delivered && !item.evidence?.trim()) {
1137
+ entry.status = 'rejected-no-evidence';
1138
+ xcheckEvent('rejected-no-evidence', seat, neighbor, itemKey(item), 'no-evidence');
1139
+ } else {
1140
+ const question = delivered ?? askCrossCheck(root, seat as SeatId, neighbor,
1141
+ `제목: ${seat}: ${ask.question}\nS: ${seat} 질문 · ${item.title}\nC: 판단 근거 부족 — 사람의 결정이 필요하다\n질문: ${ask.question}\n근거: ${item.evidence!.trim()}`, key);
1142
+ const requestedAt = previous?.xcheckRequestedAt ?? (delivered ? delivered.createdAt : now.toISOString());
1143
+ entry.xcheckQuestionId = question.id;
1144
+ entry.xcheckRequestedAt = requestedAt;
1145
+ entry.xcheckNeighbor = requestedNeighbor;
1146
+ entry.xcheckWaitMinutes = previous?.xcheckWaitMinutes ?? waitMinutes;
1147
+ if (!delivered) xcheckEvent('requested', seat, requestedNeighbor, itemKey(item), 'awaiting-xcheck');
1148
+ const answer = delivered ? crossCheckAnswer(root, question) : null;
1149
+ if (!delivered || (!answer && now.getTime() - Date.parse(requestedAt) < entry.xcheckWaitMinutes * 60_000)) {
1150
+ entry.status = 'awaiting-xcheck';
1151
+ } else if (answer?.resolves) {
1152
+ entry.status = 'awaiting-resolution';
1153
+ entry.xcheckNote = answer.note;
1154
+ if (!revisedProposal) xcheckEvent('answered', seat, requestedNeighbor, itemKey(item), 'resolution-proposed');
1155
+ } else {
1156
+ if (!answer) xcheckEvent('timed-out', seat, requestedNeighbor, itemKey(item), `unanswered ${entry.xcheckWaitMinutes}m`);
1157
+ const check = answer ? ['--xcheck', `${requestedNeighbor}:${answer.note}`,
1158
+ ...(answer.agree ? [] : ['--dissent', `${requestedNeighbor}: ${answer.note}`])]
1159
+ : ['--no-xcheck', `이웃 ${requestedNeighbor} 무응답 ${entry.xcheckWaitMinutes}분`];
1160
+ await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
1161
+ '--s', `${seat} 질문 · ${item.title}`, '--c', '판단 근거 부족 — 사람의 결정이 필요하다',
1162
+ '--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
1163
+ '--skip-recommend', '근거가 부족하다', ...check, '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
1164
+ if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
1165
+ entry.status = 'hitl';
1166
+ xcheckEvent('raised', seat, requestedNeighbor, itemKey(item), answer ? answer.agree ? 'agree' : 'dissent' : 'timed-out');
1167
+ }
1168
+ }
839
1169
  }
840
- if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
841
- entry.status = 'hitl';
842
1170
  } else {
843
1171
  if (!isSeatId(ask.to) || ask.to === seat) throw new Error('invalid recipient seat');
844
1172
  const key = `ask:${seat}:${itemKey(item)}`;
@@ -861,6 +1189,9 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
861
1189
  }
862
1190
  if (item) observe('picked', { seat, item, date: seatDay(now) });
863
1191
  const planned = item ? planAction(item, seat) : undefined;
1192
+ if (item && config.mode === 'live-safe' && deps.inquire && !isRescueAllowedAction('seat-question')) {
1193
+ observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'outbound inquiry not in rescue policy' });
1194
+ }
864
1195
  const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: item ? 'shadow' : 'skipped-empty',
865
1196
  ...(item ? { item, action: planned!.kind, ...(planned!.reason ? { reason: planned!.reason } : {}) } : { action: 'skipped-empty' as const }) };
866
1197
  if (config.mode === 'shadow' || !item) {
@@ -873,6 +1204,19 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
873
1204
  append(path, entry);
874
1205
  return entry;
875
1206
  }
1207
+ if (config.mode === 'live-safe') {
1208
+ const requested = planned!.kind === 'harness' ? 'launch' : 'decision-card';
1209
+ if (planned!.kind === 'decision' && !isRescueAllowedAction(planned!.kind)) {
1210
+ observe('refused', { seat, item: item.id, action: planned!.reason ?? 'decision', reason: 'execution forbidden; decision card only' });
1211
+ }
1212
+ if (!isRescueAllowedAction(requested)) {
1213
+ entry.status = 'refused';
1214
+ entry.reason = `action not permitted: ${requested}`;
1215
+ append(path, entry);
1216
+ observe('refused', { seat, item: item.id, action: requested, reason: entry.reason });
1217
+ return entry;
1218
+ }
1219
+ }
876
1220
  const run = deps.run ?? defaultRun;
877
1221
  let outcome: string | undefined;
878
1222
  try { outcome = (JSON.parse(await run(['harness', 'budget', '--json'])) as { outcome?: string }).outcome; }
@@ -912,7 +1256,9 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
912
1256
  observe('skipped-budget', { seat, item, reason: budget.reason });
913
1257
  const parent = (config.stall?.parents ?? {})[seat] ?? DEFAULT_STALL_PARENTS[seat];
914
1258
  // Dedupe only on a DELIVERED escalation, so a failed send is retried on the next turn (TC harvest · review round 1).
915
- if (parent && parent !== seat && !ledger.some((prior) => prior.status === 'skipped-budget' && prior.escalated === true && prior.reason === budget.reason
1259
+ if (parent && parent !== seat && config.mode === 'live-safe' && !isRescueAllowedAction('alert')) {
1260
+ observe('refused', { seat, item: item.id, action: 'alert', reason: 'policy denied budget alert' });
1261
+ } else if (parent && parent !== seat && !ledger.some((prior) => prior.status === 'skipped-budget' && prior.escalated === true && prior.reason === budget.reason
916
1262
  && prior.item && itemKey(prior.item) === itemKey(item) && seatDay(new Date(prior.ts ?? prior.at)) === seatDay(now))) {
917
1263
  const line = `[${seat} → ${parent}] ${item.id}: ${budget.reason}`;
918
1264
  try {
@@ -950,6 +1296,26 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
950
1296
  observe('hitl', { seat, item });
951
1297
  return entry;
952
1298
  }
1299
+ if (config.mode === 'live-safe') {
1300
+ let queued: { id: string };
1301
+ try {
1302
+ queued = await (deps.enqueue ?? ((assigned, text, root, idempotencyKey) => addHarnessQueue({ seat: assigned, say: text, idempotencyKey }, { root })))(seat, action.text, stateRoot, queueKey(seat, item));
1303
+ } catch (error) {
1304
+ // Same key, different task body: never adopt the other work. Refuse this turn; the key frees once that item finishes.
1305
+ if (!/idempotency key collision/.test(String(error))) throw error;
1306
+ entry.status = 'refused';
1307
+ entry.reason = 'AUTOQ idempotency key collision: a queued item with this key carries a different task';
1308
+ append(path, entry);
1309
+ observe('refused', { seat, item: item.id, action: 'launch', reason: entry.reason });
1310
+ return entry;
1311
+ }
1312
+ if (!queued || typeof queued.id !== 'string' || !/^hq-[0-9a-f-]{36}$/i.test(queued.id)) throw new Error('AUTOQ returned no queue id');
1313
+ entry.status = 'queued';
1314
+ entry.queueId = queued.id;
1315
+ append(path, entry);
1316
+ observe('queued', { seat, item, queueId: queued.id });
1317
+ return entry;
1318
+ }
953
1319
  const output = await run(['harness', 'say', action.text, '--substrate', 'pod', '--pod-pool', config.podPool ?? 'pool-node-b@node-b:8', '--base', 'main', '--json']);
954
1320
  const results: unknown = JSON.parse(output.trim());
955
1321
  if (!Array.isArray(results) || results.length === 0 || results.some((result) =>
@@ -974,6 +1340,15 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
974
1340
  throw error;
975
1341
  } finally {
976
1342
  // A turn ends even when there is no picked item or a launch fails.
1343
+ try { judgeSeatNeighbors(seat, config, deps, stateRoot, (deps.now ?? (() => new Date()))()); }
1344
+ catch (error) {
1345
+ observe('neighbor-judgment-failed', { seat, error: String(error).slice(0, 200) });
1346
+ const neighbors = config.neighbors?.[seat as keyof NonNullable<SeatLoopConfig['neighbors']>] ?? [];
1347
+ for (const neighbor of neighbors) {
1348
+ try { debug.log('loop.neighbors', 'tick', { seat, neighbor: neighbor.id, state: 'unknown', action: null, mode: config.mode }); }
1349
+ catch { /* Observation cannot change the turn. */ }
1350
+ }
1351
+ }
977
1352
  try { escalateStalls(seat, deps, config, (deps.now ?? (() => new Date()))()); }
978
1353
  catch (error) {
979
1354
  debug.log('org.stall', 'escalate-error', { seat, error: String(error).slice(0, 200) }, { level: 'warn' });