elanous 0.2.10 → 0.2.12

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 (615) 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/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1452.93700483b9d4fc61.js +9 -0
  8. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 1829-b453a1b22d3999bb.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/390.c373e31b3b9d0c41.js +5 -0
  18. package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +25 -0
  19. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4685-c3c1c625b71accd1.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +2 -0
  24. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{8067-87ce253ec2f23697.js → 5845-e6c003ce8d5fd36a.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 5cd7e3a9-404fd6b8d0ecd44c.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7334.a6679d9168f37aa8.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/79f8be9e-957d68913b590b08.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8039-df92e08fede5de8f.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8421-1c4acfb1395a0edf.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 9269.d2a4795bc6812717.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/9656-c1540088f5859c80.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-54b32bd27726588d.js → page-5c05b0da4bc1392c.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-793fa536b3a6ef37.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/term/page-e297866051595d81.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → f48170cc-d05d28bf87a43bf4.js} +1 -1
  99. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +1 -0
  105. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +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 -0
  134. package/apps/pwa/out/inside/index.txt +31 -0
  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/market/index.html +1 -1
  140. package/apps/pwa/out/market/index.txt +22 -22
  141. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  142. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  143. package/apps/pwa/out/morning/index.html +1 -1
  144. package/apps/pwa/out/morning/index.txt +23 -23
  145. package/apps/pwa/out/observatory/index.html +1 -1
  146. package/apps/pwa/out/observatory/index.txt +23 -23
  147. package/apps/pwa/out/ops/checklist/index.html +1 -1
  148. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  149. package/apps/pwa/out/ops/release/index.html +1 -1
  150. package/apps/pwa/out/ops/release/index.txt +22 -22
  151. package/apps/pwa/out/ops/seats/index.html +1 -1
  152. package/apps/pwa/out/ops/seats/index.txt +22 -22
  153. package/apps/pwa/out/outputs/index.html +1 -0
  154. package/apps/pwa/out/outputs/index.txt +30 -0
  155. package/apps/pwa/out/reflection/index.html +1 -1
  156. package/apps/pwa/out/reflection/index.txt +23 -23
  157. package/apps/pwa/out/scheduler/index.html +1 -1
  158. package/apps/pwa/out/scheduler/index.txt +23 -23
  159. package/apps/pwa/out/sessions/index.html +1 -1
  160. package/apps/pwa/out/sessions/index.txt +22 -22
  161. package/apps/pwa/out/settings/devices/index.html +1 -1
  162. package/apps/pwa/out/settings/devices/index.txt +23 -23
  163. package/apps/pwa/out/settings/index.html +1 -1
  164. package/apps/pwa/out/settings/index.txt +23 -23
  165. package/apps/pwa/out/setup/done/index.html +1 -1
  166. package/apps/pwa/out/setup/done/index.txt +23 -23
  167. package/apps/pwa/out/setup/index.html +1 -1
  168. package/apps/pwa/out/setup/index.txt +23 -23
  169. package/apps/pwa/out/share/index.html +1 -1
  170. package/apps/pwa/out/share/index.txt +23 -23
  171. package/apps/pwa/out/showroom/index.html +1 -1
  172. package/apps/pwa/out/showroom/index.txt +22 -22
  173. package/apps/pwa/out/tasks/index.html +1 -1
  174. package/apps/pwa/out/tasks/index.txt +23 -23
  175. package/apps/pwa/out/term/index.html +1 -1
  176. package/apps/pwa/out/term/index.txt +23 -24
  177. package/apps/pwa/out/today/index.html +1 -1
  178. package/apps/pwa/out/today/index.txt +23 -23
  179. package/apps/pwa/out/trace/index.html +1 -1
  180. package/apps/pwa/out/trace/index.txt +23 -23
  181. package/apps/pwa/out/vault/index.html +1 -1
  182. package/apps/pwa/out/vault/index.txt +23 -23
  183. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  184. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  185. package/apps/pwa/out/workflows/index.html +1 -1
  186. package/apps/pwa/out/workflows/index.txt +23 -23
  187. package/apps/pwa/out/workspace/index.html +1 -1
  188. package/apps/pwa/out/workspace/index.txt +23 -24
  189. package/apps/pwa/out/worktrees/index.html +1 -1
  190. package/apps/pwa/out/worktrees/index.txt +23 -23
  191. package/graphs/context/nightly-condense.yaml +21 -0
  192. package/graphs/context/recipes.yaml +12 -0
  193. package/graphs/demo/inside-seed.yaml +20 -0
  194. package/graphs/demo/recipes.yaml +12 -0
  195. package/graphs/drafts/draft-cleanup.yaml +19 -0
  196. package/graphs/drafts/recipes.yaml +23 -0
  197. package/graphs/intake/intake-daily.yaml +2 -1
  198. package/graphs/ops/recipes.yaml +3 -0
  199. package/graphs/ops/secret-exposure.yaml +24 -0
  200. package/graphs/release/recipes.yaml +3 -0
  201. package/graphs/release/release-loop.yaml +4 -1
  202. package/graphs/video/explainer-line.yaml +2 -2
  203. package/package.json +2 -1
  204. package/plugins/card-followup/examples/sample-card.png +0 -0
  205. package/plugins/elanous-hwp/README.md +25 -0
  206. package/plugins/elanous-hwp/cli.ts +22 -0
  207. package/plugins/elanous-hwp/converter.ts +237 -0
  208. package/plugins/elanous-hwp/fixtures/fixed-controls.hwpx +0 -0
  209. package/plugins/elanous-hwp/fixtures/fixed-table-controls.hwpx +0 -0
  210. package/plugins/elanous-hwp/fixtures/fixed.hwpx +0 -0
  211. package/plugins/elanous-hwp/plugin.json +16 -0
  212. package/plugins/elanous-hwp/plugin.ts +40 -0
  213. package/scripts/ci-public-export-leak-gate.ts +75 -14
  214. package/scripts/ci-public-leak-gate.ts +44 -9
  215. package/scripts/context-hooks/claude-stop.sh +4 -0
  216. package/scripts/context-hooks/emit.ts +24 -0
  217. package/scripts/context-hooks/post-tool-use.sh +4 -0
  218. package/scripts/coord-channel-watch.sh +12 -0
  219. package/scripts/coord-requests-bridge.ts +143 -0
  220. package/scripts/field-feed/lib.ts +4 -0
  221. package/scripts/fleet/node-setup.sh +20 -3
  222. package/scripts/homebrew-formula-bump.ts +69 -0
  223. package/scripts/lib/telegram-inject-args.ts +31 -0
  224. package/scripts/marketing/measure-site-numbers.ts +169 -0
  225. package/scripts/measure-gate-shards.ts +309 -0
  226. package/scripts/measure-pod-memory-by-goal.ts +130 -0
  227. package/scripts/mission-request-judge.ts +31 -1
  228. package/scripts/morning-report.ts +2 -2
  229. package/scripts/opportunity-scan.ts +1 -1
  230. package/scripts/release-loop/auto-approve-node.ts +21 -4
  231. package/scripts/release-loop/checklist-gate-node.ts +2 -1
  232. package/scripts/release-loop/cut-branch.ts +84 -0
  233. package/scripts/release-loop/gate-node.ts +22 -15
  234. package/scripts/release-loop/mac-smoke-node.ts +229 -0
  235. package/scripts/release-loop/node-verdict.ts +3 -3
  236. package/scripts/release-loop/ops-upgrade-node.ts +47 -6
  237. package/scripts/release-loop/publish-node.ts +4 -1
  238. package/scripts/release-loop/tui-node-checks.ts +42 -0
  239. package/scripts/release-loop/tui-sim-node.ts +32 -8
  240. package/scripts/release-loop/unattended-release.ts +5 -3
  241. package/scripts/release-loop/version-node.ts +74 -13
  242. package/scripts/signal-digest-cycle.ts +2 -7
  243. package/scripts/signal-digest-photo.ts +11 -0
  244. package/scripts/tech1-public-measure.ts +94 -0
  245. package/scripts/telegram-inject.ts +14 -6
  246. package/scripts/test-diet/node.ts +1 -1
  247. package/scripts/test-diet/remeasure-list.ts +112 -0
  248. package/scripts/tui-regress.ts +2 -2
  249. package/scripts/ux/demo-rehearsal.ts +247 -0
  250. package/scripts/ux/demo-seed.ts +108 -0
  251. package/scripts/ux/lib/rehearsal-verdict.ts +80 -0
  252. package/skills/diagram-master/SKILL.md +366 -0
  253. package/skills/diagram-master/references/engines/excalidraw/color-palette.md +67 -0
  254. package/skills/diagram-master/references/engines/excalidraw/element-templates.md +182 -0
  255. package/skills/diagram-master/references/engines/excalidraw/json-schema.md +71 -0
  256. package/skills/diagram-master/references/engines/excalidraw/render_excalidraw.py +189 -0
  257. package/skills/diagram-master/references/engines/excalidraw/render_template.html +57 -0
  258. package/skills/diagram-master/references/engines/matplotlib/matplotlib-patterns.md +235 -0
  259. package/skills/diagram-master/references/engines/matplotlib/render_matplotlib.py +184 -0
  260. package/skills/diagram-master/references/engines/mermaid/domain-templates.md +435 -0
  261. package/skills/diagram-master/references/engines/mermaid/render_mermaid.py +116 -0
  262. package/skills/diagram-master/references/engines/mermaid/syntax-reference.md +2247 -0
  263. package/skills/diagram-master/references/engines/nanobannana/render_nanobannana2.py +168 -0
  264. package/skills/diagram-master/references/engines/svg/color-palette.md +188 -0
  265. package/skills/diagram-master/references/engines/svg/html-base-template.html +332 -0
  266. package/skills/diagram-master/references/engines/svg/interactivity-patterns.md +435 -0
  267. package/skills/diagram-master/references/engines/svg/render_interactive_svg.py +212 -0
  268. package/skills/diagram-master/references/engines/svg/svg-element-templates.md +349 -0
  269. package/skills/diagram-master/references/examples/README.md +69 -0
  270. package/skills/diagram-master/references/examples/ref_grid_architecture.png +0 -0
  271. package/skills/diagram-master/references/examples/ref_layered_flow.png +0 -0
  272. package/skills/diagram-master/references/pyproject.toml +16 -0
  273. package/skills/diagram-master/references/quality-checklist.md +33 -0
  274. package/skills/diagram-master/references/routing-guide.md +164 -0
  275. package/skills/diagram-master/references/uv.lock +1356 -0
  276. package/skills/google-workspace/SKILL.md +33 -0
  277. package/skills/google-workspace/scripts/gws-safe.sh +177 -0
  278. package/skills/grill-me/SKILL.md +1 -1
  279. package/skills/lecture-note-digitizer/SKILL.md +926 -0
  280. package/skills/lecture-note-digitizer/references/convert_to_pdf.py +30 -0
  281. package/skills/lecture-note-digitizer/references/extract_slide_diagram.py +73 -0
  282. package/skills/lecture-note-digitizer/references/html-template.md +167 -0
  283. package/skills/lecture-note-digitizer/references/katex-style-guide.md +165 -0
  284. package/skills/omni-crawl/SKILL.md +21 -11
  285. package/skills/omni-crawl/scripts/main.ts +6 -6
  286. package/skills/omni-crawl/src/router.ts +3 -3
  287. package/skills/omni-digest/SKILL.md +17 -9
  288. package/skills/omni-digest/digest.md +2 -2
  289. package/skills/omni-digest/src/fetch.ts +30 -3
  290. package/skills/pitch-deck/SKILL.md +2 -2
  291. package/skills/pitch-deck/template.html +2 -2
  292. package/skills/youtube-master/SKILL.md +163 -0
  293. package/skills/youtube-master/package-lock.json +558 -0
  294. package/skills/youtube-master/package.json +13 -0
  295. package/skills/youtube-master/references/study-note-template.md +124 -0
  296. package/skills/youtube-master/scripts/main.ts +435 -0
  297. package/skills/youtube-master/scripts/transcribe-local.ts +24 -0
  298. package/skills/youtube-master/src/audio.ts +63 -0
  299. package/skills/youtube-master/src/domain-staging.ts +50 -0
  300. package/skills/youtube-master/src/env.ts +36 -0
  301. package/skills/youtube-master/src/obsidian.ts +155 -0
  302. package/skills/youtube-master/src/router.ts +110 -0
  303. package/skills/youtube-master/src/stt.ts +199 -0
  304. package/skills/youtube-master/src/study-note.ts +175 -0
  305. package/skills/youtube-master/src/summarize.ts +358 -0
  306. package/skills/youtube-master/src/transcript.ts +76 -0
  307. package/skills/youtube-master/src/types.ts +91 -0
  308. package/skills/youtube-master/src/util.ts +93 -0
  309. package/skills/youtube-master/src/youtube.ts +70 -0
  310. package/skills/youtube-master/tsconfig.json +13 -0
  311. package/src/acp/server.ts +5 -2
  312. package/src/agent-mission/driver.ts +215 -23
  313. package/src/agent-mission/pty-decision.ts +71 -0
  314. package/src/agent-mission/review-loop.ts +30 -0
  315. package/src/autopilot/build/llm-conflict-merge.ts +16 -3
  316. package/src/autopilot/pty-control-loop.ts +4 -1
  317. package/src/autopilot/tool-registry.ts +2 -0
  318. package/src/boot/daemon-prompt-turn.ts +5 -0
  319. package/src/boot/daemon-runtime.ts +9 -3
  320. package/src/boot/daemon-tools/web-search.ts +3 -0
  321. package/src/card-followup/core.ts +109 -0
  322. package/src/chat/index.ts +2 -0
  323. package/src/cli/drive-input-jail.ts +101 -0
  324. package/src/cli/hooks-cli.ts +17 -3
  325. package/src/cli/make-cli.ts +43 -0
  326. package/src/cli/persona-cli.ts +98 -9
  327. package/src/cli/plugin-cli.ts +31 -22
  328. package/src/cli/pod-cli.ts +36 -9
  329. package/src/cli/pod-memory-advice.ts +35 -0
  330. package/src/cli/pr-cli.ts +51 -15
  331. package/src/cli/pty-drive-cli.ts +41 -7
  332. package/src/cli/pty-drive-verdict.ts +40 -0
  333. package/src/cli/release-cli.ts +63 -10
  334. package/src/cli/setup-cli.ts +43 -1
  335. package/src/cli/skills-cli.ts +17 -1
  336. package/src/context-bus/claude-hooks-install.ts +10 -0
  337. package/src/context-bus/context-now-surfaces.ts +65 -0
  338. package/src/context-bus/context-now.ts +104 -0
  339. package/src/context-bus/coord-events.ts +2 -2
  340. package/src/context-bus/external-events.ts +52 -0
  341. package/src/context-bus/long-term-memory.ts +108 -0
  342. package/src/context-bus/nightly-condense.ts +153 -0
  343. package/src/context-bus/session-events.ts +35 -0
  344. package/src/dashboard/dashboard-status-lines.ts +2 -0
  345. package/src/dashboard/index.ts +17 -4
  346. package/src/dashboard/slash-runtime/dashboard-handlers.ts +31 -1
  347. package/src/decisions/decision-cards.ts +10 -3
  348. package/src/decisions/decision-ledger.ts +13 -5
  349. package/src/discord/sprint21-runtime.ts +5 -1
  350. package/src/discord-self-message.ts +32 -0
  351. package/src/discord-slash-wire.ts +5 -1
  352. package/src/discord.ts +17 -1
  353. package/src/domains/core-tools.ts +3 -2
  354. package/src/domains/outbound-alert.ts +105 -32
  355. package/src/domains/outbound-log-probe.ts +8 -0
  356. package/src/domains/telegram-kind-route.ts +51 -13
  357. package/src/exec-requests/default-outputs.ts +25 -0
  358. package/src/exec-requests/planner.ts +10 -2
  359. package/src/exec-requests/runner.ts +20 -6
  360. package/src/exec-requests/store.ts +3 -0
  361. package/src/execution-loop/launch-gate.ts +109 -0
  362. package/src/field/field-reel.ts +5 -1
  363. package/src/field-note/index.ts +201 -0
  364. package/src/field-note/telegram.ts +142 -0
  365. package/src/field-note/transcribe.ts +248 -0
  366. package/src/graph-kinds/registry.ts +3 -3
  367. package/src/graph-runner/graph-cli.ts +61 -1
  368. package/src/graph-runner/graph-grow-llm.ts +47 -0
  369. package/src/graph-runner/graph-grow.ts +50 -0
  370. package/src/graph-runner/runner.ts +329 -33
  371. package/src/grok/agent-search.ts +8 -2
  372. package/src/guardian/secret-exposure-scan.ts +101 -0
  373. package/src/harness/harness-cli-command.ts +375 -13
  374. package/src/harness/harness-pod-dispatch.ts +43 -23
  375. package/src/harness/harness-seams.ts +38 -4
  376. package/src/harness/harness-stop.ts +23 -4
  377. package/src/harness/launch-registry.ts +91 -0
  378. package/src/harness/process-garbage.ts +5 -3
  379. package/src/hooks/consult-intake.ts +153 -0
  380. package/src/hooks/dispatch.ts +82 -0
  381. package/src/hooks/expose.ts +254 -0
  382. package/src/hooks/github-shadow.ts +108 -0
  383. package/src/hooks/providers.ts +71 -5
  384. package/src/hooks/queue.ts +1 -1
  385. package/src/hooks/receiver.ts +94 -16
  386. package/src/index.ts +76 -14
  387. package/src/intake-plane/categorize.ts +18 -46
  388. package/src/intake-plane/discord-seat-work.ts +38 -2
  389. package/src/intake-plane/front-route-classifier.ts +33 -39
  390. package/src/intake-plane/persona-answer.ts +30 -0
  391. package/src/intake-plane/runtime-callables.ts +8 -1
  392. package/src/intake-plane/telegram-seat-work.ts +36 -2
  393. package/src/intake-plane/wish-card.ts +33 -0
  394. package/src/intake-plane/wish-folder.ts +67 -0
  395. package/src/intent-gate/gate.ts +30 -10
  396. package/src/live/detail-switch.ts +5 -0
  397. package/src/llm/difficulty.ts +57 -0
  398. package/src/llm/judge-layer.ts +101 -0
  399. package/src/llm/mission-router.ts +33 -19
  400. package/src/loops/contract/examples/cmo-seat.loop.yaml +51 -0
  401. package/src/loops/contract/examples/orchestrator.loop.yaml +57 -0
  402. package/src/loops/contract/examples/steward.loop.yaml +50 -0
  403. package/src/loops/contract/loop-agent.schema.json +138 -0
  404. package/src/loops/contract/validate.ts +64 -0
  405. package/src/loops/registry.ts +20 -6
  406. package/src/market/signed-index.ts +4 -0
  407. package/src/maturity/bot-command-maturity.ts +45 -0
  408. package/src/maturity/feature-maturity.ts +27 -4
  409. package/src/model-tier/index.ts +2 -2
  410. package/src/model-tier/tier-resolver.ts +20 -20
  411. package/src/model-tier/types.ts +6 -6
  412. package/src/mss/logging/log-store.ts +15 -0
  413. package/src/nexus/api/config-model-tier.ts +5 -4
  414. package/src/nexus/api/fabric-plans.ts +53 -0
  415. package/src/nexus/api/harness-api.ts +5 -1
  416. package/src/nexus/api/health.ts +2 -0
  417. package/src/nexus/api/http-server.ts +67 -8
  418. package/src/nexus/api/inside-events.ts +226 -0
  419. package/src/nexus/api/meta-api.ts +21 -8
  420. package/src/nexus/api/missions.ts +6 -1
  421. package/src/nexus/api/ops-seats.ts +33 -0
  422. package/src/nexus/api/outbound-report.ts +40 -7
  423. package/src/nexus/api/outputs-route.ts +44 -0
  424. package/src/nexus/api/personas.ts +95 -53
  425. package/src/nexus/api/seat-requests.ts +44 -2
  426. package/src/nexus/api/sessions-store.ts +17 -38
  427. package/src/nexus/api/sub-seat-charters.ts +34 -0
  428. package/src/nexus/api/tabs-mutations.ts +3 -1
  429. package/src/nexus/api/task-cards-api.ts +19 -0
  430. package/src/nexus/api/trace.ts +18 -6
  431. package/src/nexus/api/workflow-history.ts +88 -0
  432. package/src/nexus/api/workflow-run-event-bridge.ts +1 -0
  433. package/src/nexus/api/workflows.ts +132 -6
  434. package/src/nexus/index.ts +2 -0
  435. package/src/nexus/kinds/webterm.ts +2 -1
  436. package/src/nexus/outbound/delivery-ledger.ts +17 -8
  437. package/src/nexus/outbound/router.ts +8 -3
  438. package/src/nexus/webterm/pty.ts +6 -2
  439. package/src/onboarding/cli-auth-status.ts +80 -0
  440. package/src/onboarding/voice-ai.ts +3 -3
  441. package/src/org/seat-budget.ts +48 -0
  442. package/src/org/stall-escalation.ts +63 -0
  443. package/src/outputs/ledger.ts +120 -0
  444. package/src/persona/persona-todo.ts +79 -0
  445. package/src/persona/presets.ts +123 -2
  446. package/src/persona/team-set.ts +155 -0
  447. package/src/plugins/install/plugin-install.ts +14 -0
  448. package/src/plugins/maker/market-bundle.ts +110 -0
  449. package/src/plugins/maker/plugin-maker.ts +254 -11
  450. package/src/plugins/maker/wizard-generate.ts +103 -0
  451. package/src/pod-lease/admission.ts +150 -0
  452. package/src/pod-lease/dependency-state.ts +85 -0
  453. package/src/pod-lease/host-lease.ts +127 -0
  454. package/src/project/project-suggestion.ts +16 -0
  455. package/src/project/session-context.ts +24 -0
  456. package/src/release-loop/checklist.ts +17 -5
  457. package/src/release-loop/feature-store.ts +29 -13
  458. package/src/release-loop/landed-but-yellow.ts +48 -0
  459. package/src/release-loop/next-md-merge.ts +58 -0
  460. package/src/release-loop/release-note.ts +59 -4
  461. package/src/seat-dispatch/seat-ask.ts +130 -0
  462. package/src/seat-dispatch/seat-questions.ts +100 -0
  463. package/src/seat-loop/seat-loop.ts +709 -29
  464. package/src/seat-loop/seat-report.ts +37 -2
  465. package/src/self-dev/dev-cli.ts +4 -1
  466. package/src/self-dev/dev-pipeline.ts +37 -1
  467. package/src/self-dev/draft-sweep.ts +79 -40
  468. package/src/self-dev/draft-triage-rules.ts +62 -7
  469. package/src/self-dev/fabric-plan-core.ts +297 -0
  470. package/src/self-dev/orchestrate.ts +36 -4
  471. package/src/self-dev/release-path-guard.ts +62 -0
  472. package/src/self-dev/shard-aggregate.ts +230 -0
  473. package/src/self-dev/shard-placement.ts +60 -0
  474. package/src/self-dev/shard-plan.ts +48 -0
  475. package/src/self-dev/shard-pod-run.ts +77 -0
  476. package/src/self-dev/shard-run.ts +105 -0
  477. package/src/self-implement/budget-gate.ts +29 -12
  478. package/src/self-implement/gate-baseline.ts +1 -1
  479. package/src/self-implement/gate-candidate-ranking.ts +60 -0
  480. package/src/self-implement/gate-cli.ts +40 -1
  481. package/src/self-implement/gate-scope.ts +44 -13
  482. package/src/self-implement/graph-variant.ts +171 -0
  483. package/src/self-implement/harness-policy-gates.ts +1 -1
  484. package/src/self-implement/headless-elanous-driver.ts +27 -3
  485. package/src/self-implement/host-regate.ts +22 -1
  486. package/src/self-implement/importer-test-index.ts +2 -1
  487. package/src/self-implement/l8-merge-queue.ts +130 -0
  488. package/src/self-implement/orchestrator.ts +257 -9
  489. package/src/self-implement/route-consumer-test-index.ts +122 -0
  490. package/src/self-implement/seams.ts +72 -11
  491. package/src/self-implement/test-seams.ts +2 -0
  492. package/src/self-implement/uncommitted-work.ts +59 -0
  493. package/src/session/index.ts +36 -13
  494. package/src/skills/gift-install.ts +215 -0
  495. package/src/skills/tools/web-search.ts +4 -1
  496. package/src/steward/launch.ts +42 -26
  497. package/src/steward/steward-cards.ts +17 -9
  498. package/src/steward/triage.ts +46 -14
  499. package/src/task-cards/card-cli.ts +25 -1
  500. package/src/task-orchestrator/mission.ts +5 -0
  501. package/src/task-orchestrator/store.ts +12 -3
  502. package/src/task-orchestrator/surfaces/pod-command-job.ts +43 -17
  503. package/src/task-orchestrator/surfaces/pod-lease.ts +156 -19
  504. package/src/task-orchestrator/surfaces/pod-pool.ts +127 -26
  505. package/src/task-orchestrator/surfaces/self-implement-pod.ts +222 -40
  506. package/src/task-orchestrator/surfaces/self-implement.ts +15 -2
  507. package/src/task-orchestrator/types.ts +2 -0
  508. package/src/telegram-card-followup.ts +16 -96
  509. package/src/telegram-commands.ts +31 -1
  510. package/src/telegram-project-command.ts +145 -0
  511. package/src/telegram-report.ts +21 -17
  512. package/src/telegram.ts +61 -8
  513. package/src/tool-runtime/self-cognition-runtimes.ts +13 -3
  514. package/src/user-config.ts +110 -24
  515. package/src/version/packed-revision.json +1 -1
  516. package/src/web-search/grok.ts +7 -1
  517. package/src/web-search/index.ts +28 -1
  518. package/src/web-search/provider.ts +4 -0
  519. package/src/web-search/x-intent.ts +5 -0
  520. package/src/workflow-runtime/executor.ts +277 -47
  521. package/src/workflow-runtime/index.ts +1 -0
  522. package/src/workflow-runtime/nodes/subworkflow.ts +60 -0
  523. package/src/workflow-runtime/plugin-kind-node.ts +1 -1
  524. package/src/workflow-runtime/schema.ts +79 -4
  525. package/src/workflow-runtime/types.ts +35 -4
  526. package/apps/pwa/out/_next/static/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +0 -1
  527. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  528. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  529. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  530. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  531. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  532. package/apps/pwa/out/_next/static/chunks/2375-cb6debea56cce4fb.js +0 -1
  533. package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +0 -1
  534. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  535. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  536. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  537. package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +0 -1
  538. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  539. package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +0 -1
  540. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  541. package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +0 -1
  542. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  543. package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +0 -1
  544. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
  545. package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +0 -1
  546. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  547. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  548. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  549. package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +0 -1
  550. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  551. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  552. package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +0 -1
  553. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  554. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
  555. package/apps/pwa/out/_next/static/chunks/8195-6fde3d7a39859206.js +0 -10
  556. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  557. package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +0 -1
  558. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  559. package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +0 -1
  560. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  561. package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +0 -1
  562. package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +0 -1
  563. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  564. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  565. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +0 -1
  566. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +0 -1
  567. package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +0 -1
  568. package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +0 -1
  569. package/apps/pwa/out/_next/static/chunks/app/chat/page-f8116559c7424da7.js +0 -1
  570. package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +0 -1
  571. package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +0 -1
  572. package/apps/pwa/out/_next/static/chunks/app/design-check/page-54e1f966ab60a4dc.js +0 -1
  573. package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +0 -1
  574. package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +0 -1
  575. package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +0 -1
  576. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  577. package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +0 -1
  578. package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +0 -1
  579. package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +0 -1
  580. package/apps/pwa/out/_next/static/chunks/app/market/page-b31610deda76337b.js +0 -1
  581. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6cb0e1bbefe5fb3d.js +0 -1
  582. package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +0 -1
  583. package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +0 -1
  584. package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +0 -1
  585. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +0 -1
  586. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +0 -1
  587. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +0 -1
  588. package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +0 -1
  589. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +0 -1
  590. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +0 -1
  591. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +0 -1
  592. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e168fa1030af0548.js +0 -1
  593. package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +0 -1
  594. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +0 -1
  595. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  596. package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +0 -1
  597. package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +0 -1
  598. package/apps/pwa/out/_next/static/chunks/app/showroom/page-4fddc90b23feb215.js +0 -1
  599. package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +0 -1
  600. package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +0 -1
  601. package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +0 -1
  602. package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +0 -1
  603. package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +0 -1
  604. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +0 -1
  605. package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +0 -1
  606. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +0 -1
  607. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +0 -1
  608. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  609. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  610. package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +0 -1
  611. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  612. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  613. package/apps/pwa/out/_next/static/chunks/webpack-e5bea36f183b25e6.js +0 -1
  614. package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +0 -1
  615. /package/apps/pwa/out/_next/static/{2DsiDfDCks44g1cPUMhiW → -6XrAoD3eU7NyMHAE4xPS}/_ssgManifest.js +0 -0
@@ -1,35 +1,79 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
2
3
  import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
3
4
  import { Database } from 'bun:sqlite';
4
5
  import { dirname, join, resolve } from 'node:path';
5
6
  import { promisify } from 'node:util';
6
7
  import { TRACK_AGENT_FORBIDDEN_ACTION_REGEX, universeLaunch } from '../autopilot/track-agent.js';
7
8
  import { debug } from '../debug/log.js';
9
+ import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
8
10
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
9
- import { getUserConfig, type SeatLoopConfig } from '../user-config.js';
10
- import { listChecklist } from '../release-loop/checklist.js';
11
- import { readSchedules } from '../release-loop/feature-store.js';
11
+ import { getUserConfig, type PersonaLoopConfig, type SeatLoopConfig, type UserConfig } from '../user-config.js';
12
+ import { checkSeatBudget, type RunningSeatPod } from '../org/seat-budget.js';
13
+ import { DEFAULT_STALL_PARENTS, findStalls } from '../org/stall-escalation.js';
14
+ import { openMsgStore, type MessageEnvelope } from '../msg/msg-store.js';
15
+ import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
16
+ import { nextPersonaTodo, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
17
+ import { PersonaRegistry } from '../persona/registry.js';
18
+ import { answerSeat, askSeat, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatQuestions, type SeatId } from '../seat-dispatch/seat-questions.js';
19
+ import { queryRunningRuns } from '../self-implement/running-runs.js';
20
+ import { checklistHistory, listChecklist, type Checklist } from '../release-loop/checklist.js';
21
+ import { readSchedules, releasedVersion } from '../release-loop/feature-store.js';
12
22
 
13
23
  // `kind`/`createdAt` are the V3 shadow-compare keys (내부 문서 `METHOD-v3-shadow-compare-2026-10-02` · MK 10-02 18:54).
14
- export type SeatItem = { source: 'request' | 'checklist'; kind?: 'request' | 'cell'; id: string; title: string; text: string; version?: string; queuedAt?: string; createdAt?: string };
24
+ 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 };
15
25
  export type SeatInputs = { requests: SeatItem[]; checklist: SeatItem[]; role: string };
16
- // V3 ledger row: `ts` · `item.id` · `item.kind` · `item.createdAt` · `action` (door or skipped-*) · `reason` (forbidden word for a decision).
17
- export type SeatAction = 'decision' | 'harness' | 'skipped-budget' | 'skipped-empty';
18
- export type SeatEntry = { seat: string; ts?: string; at: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'launched' | 'hitl' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; action?: SeatAction; reason?: string; runId?: string };
26
+ // V3 ledger row: `ts` · `item.id` · `item.kind` · `item.createdAt` · `action` (door, wait or skipped-*) · `reason` (matched rule or forbidden word).
27
+ export type SeatAction = 'decision' | 'harness' | 'wait' | 'seat-question' | 'seat-answer' | 'skipped-budget' | 'skipped-empty';
28
+ export type SeatInquiry = { to: SeatId | 'CEO'; question: string };
29
+ export type SeatReply = { answer: string } | { to: 'CEO'; question: string };
30
+ export type OpCandidate =
31
+ | { kind: 'release-readiness'; version: string | null; verdict: 'ready' | 'not-ready' | 'no-open-release'; cutAt?: string; landBy?: string | null; red: string[]; undecided: string[]; blocked: string[] }
32
+ | { kind: 'unassigned-cell'; version: string | null; id: string | null; title?: string; status?: string; verdict: 'assign' | 'none' }
33
+ | { kind: 'decision-delegation'; id: string | null; title?: string; category?: DecisionEntry['category']; dueAt?: string; verdict: 'review-delegation' | 'none' };
34
+ export type TcCandidate =
35
+ | { kind: 'merged-pr-yellow-cell'; version: string; id: string; title: string; pr: number; mergedAt: string }
36
+ | { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] };
37
+ 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 };
38
+ 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 };
19
39
  export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off' };
40
+ export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null };
20
41
  export type SeatDeps = {
42
+ personaConfig?: PersonaLoopConfig;
43
+ personaRegistry?: PersonaRegistry;
44
+ personaDir?: string;
45
+ appendPersona?: (path: string, entry: PersonaShadowEntry) => void;
21
46
  root?: string;
22
47
  repo?: string;
23
48
  config?: SeatLoopConfig;
49
+ budgetConfig?: Pick<UserConfig, 'org'>;
50
+ running?: () => readonly RunningSeatPod[];
51
+ escalate?: (parent: string, line: string) => Promise<void>;
24
52
  now?: () => Date;
25
53
  read?: (path: string) => string;
26
54
  run?: (args: string[]) => Promise<string>;
27
55
  append?: (path: string, entry: SeatEntry) => void;
28
56
  versions?: () => string[];
57
+ /** 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). */
58
+ schedules?: () => Array<{ version: string; cutAt: string; landBy?: string | null }>;
29
59
  /** Checklist items of one version — default reads the release ledger DB (checklist.json is only a legacy import source since REL5b). */
30
- checklistItems?: (version: string) => Array<{ id: string; title: string; status: string; owner?: string | null }>;
60
+ checklistItems?: (version: string) => Array<{ id: string; title: string; status: string; owner?: string | null; disposition?: string; evidence?: string }>;
61
+ /** Chronological release-ledger events for a checklist id; injected for isolated release fixtures. */
62
+ checklistHistory?: (id: string) => ReturnType<typeof checklistHistory>;
63
+ /** Full checklist snapshot including history, for stall detection. */
64
+ stallChecklist?: (version: string) => Checklist;
65
+ /** Inject a mailbox write for isolated failure tests; production uses the seat message store. */
66
+ stallDelivery?: (message: MessageEnvelope, key: string, root: string) => boolean;
67
+ /** Open cards from the decision ledger; injected only for isolated ledger tests. */
68
+ pendingDecisions?: () => Array<Pick<DecisionEntry, 'id' | 'title' | 'category' | 'dueAt'>>;
69
+ /** Read-only PR snapshot for TC judgment; defaults to GitHub CLI reads. */
70
+ pullRequests?: () => Promise<TcPullRequest[]>;
31
71
  ledgerFiles?: (directory: string) => string[];
32
72
  lockContended?: () => void;
73
+ /** Supply a question only when this seat lacks evidence for the selected work item. */
74
+ inquire?: (seat: SeatId, item: SeatItem) => Promise<SeatInquiry | null> | SeatInquiry | null;
75
+ /** Supply the receiving seat's answer; absent evidence must not invent a reply. */
76
+ reply?: (seat: SeatId, question: string, from: SeatId) => Promise<SeatReply | null> | SeatReply | null;
33
77
  };
34
78
 
35
79
  const repoRoot = resolve(import.meta.dir, '../..');
@@ -84,6 +128,48 @@ export function seatLedgerPath(seat: string, root: string, now: Date): string {
84
128
  return join(root, 'seat-loop', seat, `${seatDay(now)}.jsonl`);
85
129
  }
86
130
 
131
+ export function personaShadowLedgerPath(personaId: string, root: string, now: Date): string {
132
+ // Use the todo module's ID guard for this second per-persona filesystem path too.
133
+ personaTodoPath(root, personaId);
134
+ return join(root, 'persona-loop', personaId, `${seatDay(now)}.jsonl`);
135
+ }
136
+
137
+ export async function runPersonaShadowOnce(deps: SeatDeps = {}): Promise<PersonaShadowEntry[]> {
138
+ if ((deps.personaConfig ?? getUserConfig().loops?.persona)?.enabled !== true) return [];
139
+ const root = deps.root ?? effectiveInstanceRoot();
140
+ const dir = deps.personaDir ?? (deps.root ? join(root, 'personas') : resolveStatePersonaDir());
141
+ const registry = deps.personaRegistry ?? new PersonaRegistry();
142
+ if (!deps.personaRegistry) {
143
+ const loaded = await loadLayeredPersonaDirs(registry, deps.root ? [dir] : [dir, resolveRepositoryPersonaDir()]);
144
+ for (const error of loaded.errors) observe('persona-load-error', { error });
145
+ }
146
+ const now = (deps.now ?? (() => new Date()))();
147
+ const entries: PersonaShadowEntry[] = [];
148
+ for (const profile of registry.list().sort((a, b) => a.personaId.localeCompare(b.personaId))) {
149
+ const personaId = profile.personaId;
150
+ const { todos, errors } = readPersonaTodos(dir, personaId);
151
+ for (const error of errors) observe('persona-todo-error', { personaId, error });
152
+ const todo = nextPersonaTodo(todos);
153
+ const entry: PersonaShadowEntry = { personaId, ts: now.toISOString(), status: todo ? 'shadow' : 'skipped-empty', todo };
154
+ const path = personaShadowLedgerPath(personaId, root, now);
155
+ if (deps.appendPersona) deps.appendPersona(path, entry);
156
+ else {
157
+ mkdirSync(dirname(path), { recursive: true });
158
+ appendFileSync(path, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
159
+ }
160
+ observe('persona-shadow', { personaId, status: entry.status, todoId: todo?.id ?? null });
161
+ entries.push(entry);
162
+ }
163
+ return entries;
164
+ }
165
+
166
+ export async function runSeatLoopTurn(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
167
+ const result = await runSeatLoopOnce(seat, deps);
168
+ try { await runPersonaShadowOnce(deps); }
169
+ catch (error) { observe('persona-shadow-error', { error: String(error).slice(0, 200) }); }
170
+ return result;
171
+ }
172
+
87
173
  function rows(text: string): Record<string, unknown>[] {
88
174
  return text.split('\n').filter(Boolean).map((line) => JSON.parse(line) as Record<string, unknown>);
89
175
  }
@@ -93,17 +179,104 @@ function versionOrder(a: string, b: string): number {
93
179
  return (aa[0]! - bb[0]!) || (aa[1]! - bb[1]!) || (aa[2]! - bb[2]!);
94
180
  }
95
181
 
96
- export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}): Promise<SeatInputs> {
182
+ function itemKey(item: SeatItem): string { return `${item.source}:${item.version ?? ''}:${item.id}`; }
183
+
184
+ function itemSnapshot(item: SeatItem): string {
185
+ return JSON.stringify([item.title, item.text, item.evidence ?? null, item.status ?? null]);
186
+ }
187
+
188
+ function decisionRef(seat: string, item: SeatItem): string {
189
+ return `seat-loop:${seat}:${encodeURIComponent(itemKey(item))}`;
190
+ }
191
+
192
+ function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEntry | undefined {
193
+ const ref = decisionRef(seat, item);
194
+ return new DecisionLedger({ stateDir: root }).list({ status: 'all' }).find((card) => card.refs?.includes(ref));
195
+ }
196
+
197
+ // RM3 (asked/answered · decision-card receipts for seat inquiries) ⊕ main (checklist cells re-picked by evidence, not by a shadow row).
198
+ function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string): Set<string> {
199
+ const inquiryRow = (entry: SeatEntry) => entry.action === 'seat-question' || entry.action === 'seat-answer' || !!entry.inquiry;
200
+ return new Set(ledger.filter((entry, index) => entry.status === 'asked' || entry.status === 'answered'
201
+ || (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
202
+ || (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
203
+ || (!inquiryRow(entry) && entry.status === 'outcome-unknown')
204
+ || (!inquiryRow(entry) && entry.status === 'attempting' && !(entry.item?.source === 'checklist' && ledger.slice(index + 1).some((later) =>
205
+ later.seat === entry.seat && later.status === 'launched' && later.item?.source === 'checklist'
206
+ && later.item.version === entry.item?.version && later.item.id === entry.item?.id
207
+ && later.item.title === entry.item?.title && later.item.evidenceHash === entry.item?.evidenceHash)))
208
+ || (shadow && entry.status === 'shadow' && entry.action !== 'wait' && entry.item?.source !== 'checklist')
209
+ || (entry.status === 'launched' && entry.item?.source !== 'checklist'))
210
+ .filter((entry): entry is SeatEntry & { item: SeatItem } => !!entry.item && !!entry.item.source && typeof entry.item.id === 'string')
211
+ .map((entry) => itemKey(entry.item)));
212
+ }
213
+
214
+ function eligibleItem(item: SeatItem, ledger: readonly SeatEntry[], handled: ReadonlySet<string>, shadow: boolean): boolean {
215
+ const key = itemKey(item);
216
+ if (handled.has(key)) return false;
217
+ const lastWait = [...ledger].reverse().find((entry) => entry.item && itemKey(entry.item) === key
218
+ && (entry.status === 'wait' || (shadow && entry.status === 'shadow' && entry.action === 'wait')));
219
+ return !lastWait?.item || itemSnapshot(lastWait.item) !== itemSnapshot(item);
220
+ }
221
+
222
+ const CHECKLIST_HANDLED_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
223
+
224
+ // Same-version snapshots are handled here; carried work requires an explicit release-ledger move.
225
+ export function alreadyHandled(item: SeatItem, ledger: readonly SeatEntry[]): boolean {
226
+ if (item.source !== 'checklist' || !item.seat || !item.asOf || item.evidenceHash === undefined) return false;
227
+ const now = Date.parse(item.asOf);
228
+ if (!Number.isFinite(now)) return false;
229
+ return ledger.some((entry) => {
230
+ const at = Date.parse(entry.at || entry.ts || '');
231
+ return entry.seat === item.seat && at >= now - CHECKLIST_HANDLED_WINDOW_MS && at <= now
232
+ // A shadowed seat question was never delivered — it must be picked again once delivery is on (RM3 · TC merge 10-03).
233
+ && (entry.status === 'launched' || (entry.status === 'shadow' && !entry.inquiry))
234
+ && entry.item?.source === 'checklist' && entry.item.id === item.id && entry.item.version === item.version
235
+ && entry.item.title === item.title && entry.item.evidenceHash === item.evidenceHash;
236
+ });
237
+ }
238
+
239
+ function readSeatLedger(seat: string, deps: SeatDeps, now: Date): SeatEntry[] {
240
+ const directory = dirname(seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now));
241
+ const files = deps.ledgerFiles?.(directory) ?? (existsSync(directory) ? readdirSync(directory) : []);
242
+ return files.filter((name) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(name) && name <= `${seatDay(now)}.jsonl`)
243
+ .flatMap((name) => rows((deps.read ?? defaultRead)(join(directory, name))) as SeatEntry[]);
244
+ }
245
+
246
+ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger?: readonly SeatEntry[]): Promise<SeatInputs> {
97
247
  const read = deps.read ?? defaultRead;
98
248
  const root = deps.root ?? effectiveInstanceRoot();
249
+ const activeLedger = ledger ?? readSeatLedger(seat, deps, (deps.now ?? (() => new Date()))());
250
+ const consumed = handledKeys(activeLedger, (deps.config ?? getUserConfig().loops?.seat)?.mode === 'shadow', root);
99
251
  const requests = new Map<string, Record<string, unknown>>();
100
252
  for (const row of rows(read(join(root, 'seat-requests', 'requests.jsonl')))) {
101
253
  if (typeof row.key === 'string') requests.set(row.key, row);
102
254
  }
103
- const pending = [...requests.values()]
255
+ const pending: SeatItem[] = seatQuestions(root, seat as SeatId)
256
+ .filter((message) => !hasSeatAnswer(root, message) && !consumed.has(`seat-question::${message.id}`))
257
+ .map((message) => ({ source: 'seat-question', kind: 'seat-question', id: String(message.id), title: message.body, text: message.body,
258
+ from: message.from as SeatId, queuedAt: message.createdAt, createdAt: message.createdAt }));
259
+ const ordinary: SeatItem[] = [...requests.values()]
104
260
  .filter((row) => row.seat === seat && (row.status === 'pending' || row.status === 'queued') && typeof row.text === 'string')
105
- .map((row) => ({ source: 'request' as const, kind: 'request' as const, id: String(row.key), title: String(row.text), text: String(row.text), queuedAt: String(row.queuedAt ?? ''), createdAt: String(row.queuedAt ?? '') }))
261
+ .map((row) => ({ source: 'request' as const, kind: 'request' as const, id: String(row.key), title: String(row.text), text: String(row.text), ...(typeof row.evidence === 'string' ? { evidence: row.evidence } : {}), queuedAt: String(row.queuedAt ?? ''), createdAt: String(row.queuedAt ?? '') }))
106
262
  .sort((a, b) => a.queuedAt.localeCompare(b.queuedAt) || a.id.localeCompare(b.id));
263
+ const store = openMsgStore(join(root, 'msg', 'messages.db'));
264
+ const hookMessages: ReturnType<typeof store.listByRecipient> = [];
265
+ try {
266
+ let cursor = 0;
267
+ while (true) {
268
+ const batch = store.listByRecipient(seat, cursor, 1000);
269
+ hookMessages.push(...batch.filter(message => message.kind === 'hook-task'));
270
+ if (batch.length < 1000) break;
271
+ cursor = batch[batch.length - 1]!.id;
272
+ }
273
+ } finally { store.close(); }
274
+ for (const message of hookMessages) {
275
+ const item: SeatItem = { source: 'hook', kind: 'hook-task', id: String(message.id), title: message.body, text: message.body, queuedAt: message.createdAt, createdAt: message.createdAt };
276
+ if (eligibleItem(item, activeLedger, consumed, (deps.config ?? getUserConfig().loops?.seat)?.mode === 'shadow')) ordinary.push(item);
277
+ }
278
+ ordinary.sort((a, b) => (a.queuedAt ?? '').localeCompare(b.queuedAt ?? '') || a.id.localeCompare(b.id));
279
+ pending.push(...ordinary);
107
280
  const releaseRoot = deps.root ?? releaseLedgerRoot();
108
281
  const versions = deps.versions ?? (() => {
109
282
  const dir = join(releaseRoot, 'release');
@@ -113,16 +286,42 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}): Promi
113
286
  return [...new Set([...fromDirs, ...fromSchedule])].filter((v) => /^\d+\.\d+\.\d+$/.test(v));
114
287
  });
115
288
  const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version).items);
289
+ let shipped: string | undefined;
290
+ try { shipped = releasedVersion(releaseRoot); }
291
+ catch (error) {
292
+ console.warn(`seat loop: cannot read published versions; retaining previous order (${String(error).slice(0, 200)})`);
293
+ }
294
+ // UX J2 criterion (10-03): only versions whose cut is still ahead are open; if the schedule can't be read, pick no cell.
295
+ const asOf = (deps.now ?? (() => new Date()))().toISOString();
296
+ const nowMs = Date.parse(asOf);
297
+ let open: Set<string> | null = null;
298
+ try {
299
+ const rows = (deps.schedules ?? (() => readSchedules()))();
300
+ open = new Set(rows.filter((row) => Number.isFinite(Date.parse(row.cutAt)) && Date.parse(row.cutAt) > nowMs).map((row) => row.version));
301
+ } catch (error) {
302
+ console.warn(`seat loop: cannot read release schedules; no checklist cell is picked (${String(error).slice(0, 200)})`);
303
+ debug.log('seat.loop', 'schedule-unreadable', { seat, error: String(error).slice(0, 200) });
304
+ }
116
305
  const checklist: SeatItem[] = [];
117
- for (const version of versions().sort(versionOrder)) {
306
+ for (const version of open === null ? [] : versions().sort(versionOrder)) {
307
+ if (!open!.has(version)) {
308
+ debug.log('seat.loop', 'skip-closed-version', { seat, version });
309
+ continue;
310
+ }
118
311
  let items: ReturnType<typeof itemsOf>;
119
312
  try { items = itemsOf(version); } catch (error) {
120
313
  debug.log('seat.loop', 'checklist-unreadable', { seat, version, error: String(error).slice(0, 200) });
121
314
  continue;
122
315
  }
316
+ if (shipped && versionOrder(version, shipped) <= 0) {
317
+ const cells = items.filter((item) => item.owner === seat && (item.status === 'yellow' || item.status === 'red')).length;
318
+ debug.log('seat.loop', 'skip-shipped-version', { version, cells });
319
+ continue;
320
+ }
123
321
  for (const item of items) {
124
322
  if (item.owner === seat && (item.status === 'yellow' || item.status === 'red')) {
125
- checklist.push({ source: 'checklist', kind: 'cell', version, id: item.id, title: item.title, text: item.title });
323
+ checklist.push({ source: 'checklist', kind: 'cell', version, seat, asOf, id: item.id, title: item.title, text: item.title, status: item.status,
324
+ ...(item.evidence ? { evidence: item.evidence } : {}), evidenceHash: createHash('sha256').update(item.evidence ?? '').digest('hex') });
126
325
  }
127
326
  }
128
327
  }
@@ -130,24 +329,280 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}): Promi
130
329
  return { requests: pending, checklist, role: read(join(deps.repo ?? repoRoot, 'docs', 'roles', `${seat}.md`)).slice(0, 4_000) };
131
330
  }
132
331
 
133
- function itemKey(item: SeatItem): string { return `${item.source}:${item.version ?? ''}:${item.id}`; }
332
+ function opCandidates(deps: SeatDeps, now: Date): OpCandidate[] {
333
+ const releaseRoot = deps.root ?? releaseLedgerRoot();
334
+ const shipped = releasedVersion(releaseRoot);
335
+ let schedules: ReturnType<NonNullable<SeatDeps['schedules']>>;
336
+ try { schedules = (deps.schedules ?? (() => readSchedules(releaseRoot)))(); }
337
+ catch (error) {
338
+ observe('schedule-unreadable', { seat: 'OP', error: String(error).slice(0, 200) });
339
+ throw error;
340
+ }
341
+ const open = schedules.filter((row) => /^\d+\.\d+\.\d+$/.test(row.version)
342
+ && Number.isFinite(Date.parse(row.cutAt)) && Date.parse(row.cutAt) > now.getTime()
343
+ && (!shipped || versionOrder(row.version, shipped) > 0)).sort((a, b) => versionOrder(a.version, b.version));
344
+ const itemsOf = deps.checklistItems ?? ((version: string) => listChecklist(version).items);
345
+ const releases = open.map((schedule) => ({ schedule, items: itemsOf(schedule.version).sort((a, b) => a.id.localeCompare(b.id)) }));
346
+ const first = releases[0];
347
+ const red = first?.items.filter((item) => item.status === 'red').map((item) => item.id) ?? [];
348
+ const undecided = first?.items.filter((item) => item.status === 'yellow' && !item.disposition).map((item) => item.id) ?? [];
349
+ const blocked = first?.items.filter((item) => item.status === 'yellow' && item.disposition === 'block').map((item) => item.id) ?? [];
350
+ const ready = first !== undefined && first.items.length > 0 && first.items.every((item) =>
351
+ item.status === 'green' || item.status === 'done'
352
+ || (item.status === 'yellow' && (item.disposition === 'move' || item.disposition === 'known-issue')));
353
+ const unassigned = releases.flatMap(({ schedule, items }) => items
354
+ .filter((item) => !item.owner && (item.status === 'yellow' || item.status === 'red'))
355
+ .map((item): OpCandidate => ({ kind: 'unassigned-cell', version: schedule.version, id: item.id,
356
+ title: item.title, status: item.status, verdict: 'assign' })));
357
+ const pending = (deps.pendingDecisions ?? (() => new DecisionLedger({ stateDir: deps.root }).list({ status: 'open' })))()
358
+ .sort((a, b) => (a.dueAt ?? '\uffff').localeCompare(b.dueAt ?? '\uffff') || a.id.localeCompare(b.id))[0];
359
+ return [
360
+ { kind: 'release-readiness', version: first?.schedule.version ?? null,
361
+ verdict: !first ? 'no-open-release' : ready ? 'ready' : 'not-ready',
362
+ ...(first ? { cutAt: first.schedule.cutAt, landBy: first.schedule.landBy ?? null } : {}), red, undecided, blocked },
363
+ ...(unassigned.length ? unassigned : [{ kind: 'unassigned-cell' as const, version: null, id: null, verdict: 'none' as const }]),
364
+ { kind: 'decision-delegation', id: pending?.id ?? null,
365
+ ...(pending ? { title: pending.title, category: pending.category, ...(pending.dueAt ? { dueAt: pending.dueAt } : {}) } : {}),
366
+ verdict: pending ? 'review-delegation' : 'none' },
367
+ ];
368
+ }
369
+
370
+ const TC_APPROVAL_WAIT_MS = 2 * 60 * 60 * 1000;
371
+
372
+ function readTcChecklist(root: string): Array<{ version: string; id: string; title: string; status: string; owner: string | null }> {
373
+ const path = join(root, 'release', 'features.sqlite');
374
+ if (!existsSync(path)) return [];
375
+ const db = new Database(path, { readonly: true, strict: true });
376
+ try {
377
+ return db.query(`SELECT a.version, f.id, COALESCE(a.title_override, f.title) AS title, a.status, a.owner
378
+ FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.status = 'yellow' AND a.owner = 'TC'
379
+ ORDER BY a.version, f.id`).all() as Array<{ version: string; id: string; title: string; status: string; owner: string | null }>;
380
+ } finally { db.close(); }
381
+ }
382
+
383
+ type TcTimelineEvent = { event: string; created_at: string };
384
+
385
+ export function tcApprovalWaitAt(pr: TcPullRequest, timeline: readonly TcTimelineEvent[]): { readyAt: string; approvalWaitAt: string } | undefined {
386
+ if (pr.state !== 'OPEN' || pr.isDraft || pr.reviewDecision !== 'REVIEW_REQUIRED') return undefined;
387
+ const events = timeline.filter((event) => typeof event.created_at === 'string' && Number.isFinite(Date.parse(event.created_at)))
388
+ .sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
389
+ let readyAt: string | undefined = pr.createdAt;
390
+ let waitAt: string | undefined = pr.createdAt;
391
+ for (const event of events) {
392
+ if (event.event === 'converted_to_draft') { readyAt = undefined; waitAt = undefined; }
393
+ if (event.event === 'ready_for_review') { readyAt = event.created_at; waitAt = event.created_at; }
394
+ if (event.event === 'review_requested' && readyAt && Date.parse(event.created_at) >= Date.parse(readyAt)) waitAt = event.created_at;
395
+ }
396
+ return readyAt && waitAt && Number.isFinite(Date.parse(readyAt)) && Date.parse(waitAt) >= Date.parse(readyAt)
397
+ ? { readyAt, approvalWaitAt: waitAt } : undefined;
398
+ }
399
+
400
+ export async function readTcPullRequests(query: (args: string[]) => Promise<string> = async (args) => {
401
+ const { stdout } = await exec('gh', args, { cwd: repoRoot, encoding: 'utf8', timeout: 120_000, maxBuffer: 64 * 1024 * 1024 });
402
+ return stdout;
403
+ }): Promise<TcPullRequest[]> {
404
+ const pages: unknown = JSON.parse(await query(['api', '--paginate', '--slurp', 'repos/{owner}/{repo}/pulls?state=all&per_page=100']));
405
+ if (!Array.isArray(pages) || !pages.every((page) => Array.isArray(page))) throw new Error('PR list is not an array of pages');
406
+ const prs = pages.flatMap((page: unknown[]) => page).filter((pr) => pr && typeof pr === 'object'
407
+ && ((pr as Record<string, unknown>).state === 'open' || typeof (pr as Record<string, unknown>).merged_at === 'string'));
408
+ const results: TcPullRequest[] = [];
409
+ for (let offset = 0; offset < prs.length; offset += 10) {
410
+ results.push(...await Promise.all(prs.slice(offset, offset + 10).map(async (pr): Promise<TcPullRequest> => {
411
+ const listing = pr as Record<string, unknown>;
412
+ if (typeof listing.number !== 'number' || typeof listing.title !== 'string' || typeof listing.created_at !== 'string'
413
+ || typeof listing.draft !== 'boolean' || (listing.state !== 'open' && listing.state !== 'closed')) {
414
+ throw new Error('incomplete PR record');
415
+ }
416
+ const details: unknown = listing.state === 'open'
417
+ ? JSON.parse(await query(['pr', 'view', String(listing.number), '--json', 'number,title,body,state,isDraft,createdAt,mergedAt,files,reviewDecision']))
418
+ : { number: listing.number, title: listing.title, body: listing.body, state: 'MERGED', isDraft: listing.draft,
419
+ createdAt: listing.created_at, mergedAt: listing.merged_at, files: [] };
420
+ if (!details || typeof details !== 'object' || Array.isArray(details)) throw new Error('invalid PR record');
421
+ const row = details as Record<string, unknown>;
422
+ if (typeof row.number !== 'number' || typeof row.title !== 'string' || typeof row.createdAt !== 'string'
423
+ || typeof row.isDraft !== 'boolean' || (row.state !== 'OPEN' && row.state !== 'MERGED' && row.state !== 'CLOSED')) {
424
+ throw new Error('incomplete PR record');
425
+ }
426
+ const paths = Array.isArray(row.files) ? row.files.flatMap((file: unknown) =>
427
+ file && typeof file === 'object' && typeof (file as { path?: unknown }).path === 'string' ? [(file as { path: string }).path] : []) : [];
428
+ const result: TcPullRequest = { number: row.number, title: row.title, body: typeof row.body === 'string' ? row.body : '',
429
+ state: row.state, isDraft: row.isDraft, createdAt: row.createdAt,
430
+ mergedAt: typeof row.mergedAt === 'string' ? row.mergedAt : null, paths,
431
+ reviewDecision: typeof row.reviewDecision === 'string' ? row.reviewDecision : undefined };
432
+ if (result.state === 'OPEN' && !result.isDraft && result.reviewDecision === 'REVIEW_REQUIRED'
433
+ && paths.some((path) => path.startsWith('release/public/') || path.startsWith('scripts/public-export') || path.startsWith('src/market/'))) {
434
+ try {
435
+ const pages: unknown = JSON.parse(await query(['api', '--paginate', '--slurp', `repos/{owner}/{repo}/issues/${result.number}/timeline`]));
436
+ if (!Array.isArray(pages) || !pages.every((page) => Array.isArray(page))) throw new Error('PR timeline is not an array of pages');
437
+ const timeline: unknown[] = pages.flatMap((page: unknown[]) => page);
438
+ const transitions = timeline.filter((event): event is Record<string, unknown> => event !== null && typeof event === 'object'
439
+ && ['converted_to_draft', 'ready_for_review', 'review_requested'].includes((event as Record<string, unknown>).event as string));
440
+ if (!transitions.every((event) => typeof event.created_at === 'string' && Number.isFinite(Date.parse(event.created_at)))) {
441
+ throw new Error('invalid PR timeline transition date');
442
+ }
443
+ Object.assign(result, tcApprovalWaitAt(result, transitions as TcTimelineEvent[]));
444
+ } catch (error) { observe('tc-pr-timeline-unreadable', { pr: result.number, error: String(error).slice(0, 200) }); }
445
+ }
446
+ return result;
447
+ })));
448
+ }
449
+ return results;
450
+ }
451
+
452
+ async function tcCandidates(deps: SeatDeps, now: Date): Promise<TcCandidate[]> {
453
+ let prs: TcPullRequest[];
454
+ // An injected instance root must not fetch unrelated live GitHub state.
455
+ try { prs = await (deps.pullRequests ?? (deps.root ? async () => [] : readTcPullRequests))(); }
456
+ catch (error) {
457
+ observe('tc-pr-unreadable', { seat: 'TC', error: String(error).slice(0, 200) });
458
+ return [];
459
+ }
460
+ const merged = prs.filter((pr) => pr.state === 'MERGED' && pr.mergedAt && Number.isFinite(Date.parse(pr.mergedAt)) && Date.parse(pr.mergedAt) <= now.getTime());
461
+ const candidates: TcCandidate[] = [];
462
+ const releaseRoot = deps.root ?? releaseLedgerRoot();
463
+ try {
464
+ const cells = merged.length === 0 ? [] : deps.checklistItems && deps.schedules
465
+ ? deps.schedules().flatMap(({ version }) => deps.checklistItems!(version).map((cell) => ({ ...cell, version })))
466
+ : readTcChecklist(releaseRoot);
467
+ for (const cell of cells) {
468
+ if (cell.owner !== 'TC' || cell.status !== 'yellow') continue;
469
+ for (const pr of merged) {
470
+ const id = cell.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
471
+ const reference = new RegExp(`(?<![A-Za-z0-9])${id}(?![A-Za-z0-9])`);
472
+ if (reference.test(pr.title) || reference.test(pr.body ?? '')) {
473
+ candidates.push({ kind: 'merged-pr-yellow-cell', version: cell.version, id: cell.id, title: cell.title, pr: pr.number, mergedAt: pr.mergedAt! });
474
+ }
475
+ }
476
+ }
477
+ } catch (error) { observe('tc-checklist-unreadable', { seat: 'TC', error: String(error).slice(0, 200) }); }
478
+ for (const pr of prs) {
479
+ if (pr.state !== 'OPEN' || pr.isDraft || pr.reviewDecision !== 'REVIEW_REQUIRED'
480
+ || !pr.paths?.some((path) => path.startsWith('release/public/') || path.startsWith('scripts/public-export') || path.startsWith('src/market/'))) continue;
481
+ const readyAt = pr.readyAt;
482
+ const age = now.getTime() - Date.parse(pr.approvalWaitAt ?? '');
483
+ if (!readyAt || !Number.isFinite(Date.parse(readyAt)) || Date.parse(readyAt) > now.getTime()
484
+ || !Number.isFinite(age) || age < TC_APPROVAL_WAIT_MS) continue;
485
+ candidates.push({ kind: 'publication-pr-approval-wait', pr: pr.number, title: pr.title, readyAt, approvalWaitAt: pr.approvalWaitAt!, paths: pr.paths });
486
+ }
487
+ return candidates;
488
+ }
489
+
490
+ function deliverStall(message: MessageEnvelope, key: string, root: string): boolean {
491
+ const store = openMsgStore(join(root, 'msg', 'messages.db'));
492
+ try {
493
+ // Persist the exact stall identity alongside its message, atomically across processes.
494
+ store.db.exec('BEGIN IMMEDIATE');
495
+ try {
496
+ store.db.exec('CREATE TABLE IF NOT EXISTS seat_stall_deliveries (key TEXT PRIMARY KEY)');
497
+ const inserted = store.db.query('INSERT OR IGNORE INTO seat_stall_deliveries (key) VALUES (?)').run(key);
498
+ if (inserted.changes === 0) { store.db.exec('COMMIT'); return false; }
499
+ store.append(message);
500
+ store.db.exec('COMMIT');
501
+ return true;
502
+ } catch (error) { store.db.exec('ROLLBACK'); throw error; }
503
+ } finally { store.close(); }
504
+ }
505
+
506
+ function escalateStalls(seat: string, deps: SeatDeps, config: SeatLoopConfig, now: Date): void {
507
+ const root = deps.root ?? releaseLedgerRoot();
508
+ const schedules = (deps.schedules ?? (() => readSchedules(root)))();
509
+ const shipped = releasedVersion(root);
510
+ const failures: Error[] = [];
511
+ for (const schedule of schedules) {
512
+ if (!/^\d+\.\d+\.\d+$/.test(schedule.version)
513
+ || !Number.isFinite(Date.parse(schedule.cutAt)) || Date.parse(schedule.cutAt) <= now.getTime()
514
+ || (shipped && versionOrder(schedule.version, shipped) <= 0)) continue;
515
+ try {
516
+ // Same ledger root as the schedules and shipped version above (TC harvest · review round 3).
517
+ const checklist = (deps.stallChecklist ?? ((version: string) => listChecklist(version, deps.root)))(schedule.version);
518
+ for (const stall of findStalls(checklist, config, now)) {
519
+ if (stall.from !== seat) continue;
520
+ const { item, from, to, stalledMin, startedAt, reason } = stall;
521
+ const data = { item: item.id, from, to, stalledMin, version: schedule.version, reason };
522
+ if (config.mode === 'shadow') {
523
+ debug.log('org.stall', 'escalate', { ...data, mode: 'shadow' });
524
+ continue;
525
+ }
526
+ const key = JSON.stringify([schedule.version, item.id, from, to, reason, startedAt]);
527
+ const message: MessageEnvelope = { from, to, kind: 'stall-escalation',
528
+ body: `[stall:${key}] ${schedule.version} 체크리스트 ${item.id} · ${item.title} · ${reason} · ${stalledMin}분 정체 (${from} → ${to})` };
529
+ try {
530
+ const sent = (deps.stallDelivery ?? deliverStall)(message, key, deps.root ?? effectiveInstanceRoot());
531
+ if (sent) debug.log('org.stall', 'escalate', data);
532
+ } catch (error) {
533
+ failures.push(new Error(`stall escalation ${schedule.version}/${item.id}: ${String(error)}`, { cause: error }));
534
+ }
535
+ }
536
+ } catch (error) {
537
+ failures.push(new Error(`stall checklist ${schedule.version}: ${String(error)}`, { cause: error }));
538
+ }
539
+ }
540
+ if (failures.length) throw new AggregateError(failures, `stall escalation failed for ${seat}: ${failures.map((error) => error.message).join('; ')}`);
541
+ }
134
542
 
135
543
  // In shadow mode a shadowed item counts as handled, so a rehearsal day walks the queue like the seat would.
136
- export function pickNext(inputs: SeatInputs, ledger: readonly SeatEntry[], opts: { shadow?: boolean } = {}): SeatItem | null {
137
- const handled = new Set(ledger.filter((entry) => entry.status === 'launched' || entry.status === 'hitl' || entry.status === 'attempting' || entry.status === 'outcome-unknown' || (opts.shadow === true && entry.status === 'shadow'))
138
- .filter((entry): entry is SeatEntry & { item: SeatItem } => !!entry.item && !!entry.item.source && typeof entry.item.id === 'string').map((entry) => itemKey(entry.item)));
139
- return [...inputs.requests, ...inputs.checklist].find((item) => !handled.has(itemKey(item))) ?? null;
544
+ export function pickNext(inputs: SeatInputs, ledger: readonly SeatEntry[], opts: { shadow?: boolean; root?: string; history?: SeatDeps['checklistHistory'] } = {}): SeatItem | null {
545
+ const handled = handledKeys(ledger, opts.shadow === true, opts.root);
546
+ return [...inputs.requests, ...inputs.checklist].find((item) => {
547
+ if (alreadyHandled(item, ledger)) {
548
+ debug.log('seat.loop', 'skip-handled', { id: item.id });
549
+ return false;
550
+ }
551
+ if (item.source === 'checklist' && item.version && item.seat && item.asOf && item.evidenceHash !== undefined && opts.history
552
+ && ledger.some((entry) => entry.seat === item.seat && entry.status === 'shadow'
553
+ && entry.item?.source === 'checklist' && entry.item.id === item.id && entry.item.version !== item.version)) {
554
+ // Only an actual move into this version identifies a carried cell; unrelated releases may reuse an id.
555
+ const moves = opts.history(item.id).filter((event) => event.field === 'move'
556
+ && typeof event.from === 'string' && typeof event.to === 'string' && Date.parse(event.at) <= Date.parse(item.asOf!));
557
+ const latest = moves.at(-1);
558
+ if (latest?.to === item.version) {
559
+ const processed = [...ledger].reverse().find((entry) => entry.seat === item.seat
560
+ && entry.status === 'shadow' && !entry.inquiry && entry.action !== 'wait'
561
+ && entry.item?.source === 'checklist' && entry.item.id === item.id);
562
+ if (processed && processed.item && processed.item.version === latest.from && processed.item.title === item.title
563
+ && processed.item.evidenceHash === item.evidenceHash
564
+ && Date.parse(processed.at || processed.ts || '') >= Date.parse(item.asOf) - CHECKLIST_HANDLED_WINDOW_MS
565
+ && Date.parse(processed.at || processed.ts || '') <= Date.parse(item.asOf)
566
+ && Date.parse(processed.at || processed.ts || '') <= Date.parse(latest.at)) {
567
+ debug.log('seat.loop', 'skip-carried', { id: item.id, from: latest.from, version: item.version });
568
+ return false;
569
+ }
570
+ }
571
+ }
572
+ return eligibleItem(item, ledger, handled, opts.shadow === true);
573
+ }) ?? null;
140
574
  }
141
575
 
142
- export function planAction(item: SeatItem, seat?: string): { kind: 'decision' | 'harness'; text: string; reason?: string } {
143
- const hit = `${item.id}\n${item.title}\n${item.text}`.match(TRACK_AGENT_FORBIDDEN_ACTION_REGEX);
144
- const kind = hit ? 'decision' : 'harness';
145
- return { kind, text: seat && kind === 'harness' ? seatTaskText(seat, item) : `${item.id} ${item.title}`, ...(hit ? { reason: hit[0] } : {}) };
576
+ const SEAT_ACTION_RULES: ReadonlyArray<{ kind: 'wait' | 'harness' | 'decision'; signal: RegExp; source: 'content' | 'task'; resolvedBy?: RegExp }> = [
577
+ { kind: 'wait', signal: /실측\s*대기|(?:사람|실제)\s*기기(?!\s*(?:(?:실측|측정|확인)\s*)?완료)(?:\s*(?:실측|측정|확인|대기))?|실물\s*측정(?!\s*완료)/iu, source: 'content', resolvedBy: /(?:실측|측정|(?:사람|실제)\s*기기|실물\s*측정)\s*완료/iu },
578
+ { kind: 'wait', signal: /\u{1F451}\s*확인\s*대기/iu, source: 'content', resolvedBy: /\u{1F451}\s*확인\s*완료/iu },
579
+ { kind: 'decision', signal: TRACK_AGENT_FORBIDDEN_ACTION_REGEX, source: 'task' },
580
+ { kind: 'decision', signal: /(?:마켓|레지스트리)(?:에|로)?\s*(?:게시|등록|발행)|사이트\s*운영\s*반영|SNS(?:에|로)?\s*(?:게시|발행|업로드)|공개\s*발행/iu, source: 'content' },
581
+ { kind: 'harness', signal: /(?:마켓|레지스트리|사이트|SNS|공개)?(?:에|로)?\s*(?:게시|발행|배포|등록|운영\s*반영)(?:를?\s*위한)?\s*(?:준비|초안|원고|자료|구현|점검|테스트)|(?:마켓|레지스트리|사이트|SNS|공개)?(?:에|로)?\s*(?:게시|발행|배포|등록)(?:용|를?\s*위한)\s*(?:초안|원고|자료|코드|기능)/iu, source: 'task' },
582
+ ];
583
+
584
+ export function planAction(item: SeatItem, seat?: string): { kind: 'decision' | 'harness' | 'wait'; text: string; reason?: string } {
585
+ const task = `${item.id}\n${item.title}\n${item.text}`;
586
+ const content = `${task}\n${item.evidence ?? ''}`;
587
+ const preparation = SEAT_ACTION_RULES.find(({ kind }) => kind === 'harness')!;
588
+ const prepared = [...content.matchAll(new RegExp(preparation.signal.source, `${preparation.signal.flags.replace('g', '')}g`))];
589
+ const rule = SEAT_ACTION_RULES.map(({ kind, signal, source, resolvedBy }) => {
590
+ const text = source === 'content' ? content : task;
591
+ if (resolvedBy?.test(item.evidence ?? '')) return null;
592
+ const hits = [...text.matchAll(new RegExp(signal.source, `${signal.flags.replace('g', '')}g`))];
593
+ const hit = kind === 'decision' ? hits.find((match) => !prepared.some((prep) =>
594
+ match.index! >= prep.index! && match.index! + match[0].length <= prep.index! + prep[0].length)) : hits[0];
595
+ return hit ? { kind, reason: hit[0] } : null;
596
+ }).find((hit) => hit !== null);
597
+ const kind = rule?.kind ?? 'harness';
598
+ const reason = rule?.reason;
599
+ observe('plan', { kind, reason: reason ?? '' });
600
+ return { kind, text: seat && kind === 'harness' ? seatTaskText(seat, item) : `${item.id} ${item.title}`, ...(reason ? { reason } : {}) };
146
601
  }
147
602
 
148
603
  // The launched sentence names the seat, the source and the release so same-named items stay distinct.
149
604
  export function seatTaskText(seat: string, item: SeatItem): string {
150
- const where = item.source === 'checklist' ? `${item.version ?? '?'} 체크리스트 칸 ${item.id}` : `자리 요청 ${item.id}`;
605
+ const where = item.source === 'checklist' ? `${item.version ?? '?'} 체크리스트 칸 ${item.id}` : item.source === 'hook' ? `웹훅 작업 카드 ${item.id}` : `자리 요청 ${item.id}`;
151
606
  return `[${seat} 자리 · ${where} · 역할 docs/roles/${seat}.md] ${item.title.replace(/[\r\n]+/g, ' ')}`;
152
607
  }
153
608
 
@@ -162,21 +617,188 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
162
617
  const path = seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now);
163
618
  const directory = dirname(path);
164
619
  return withSeatLock(directory, async () => {
165
- const files = deps.ledgerFiles?.(directory) ?? (existsSync(directory) ? readdirSync(directory) : []);
166
- const ledger = files.filter((name) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(name) && name <= `${seatDay(now)}.jsonl`)
167
- .flatMap((name) => rows((deps.read ?? defaultRead)(join(directory, name))) as SeatEntry[]);
168
- const inputs = await gatherSeatInputs(seat, deps);
169
- const item = pickNext(inputs, ledger, { shadow: config.mode === 'shadow' });
620
+ const ledger = readSeatLedger(seat, deps, now);
621
+ const stateRoot = deps.root ?? effectiveInstanceRoot();
622
+ let turnError: unknown;
623
+ try {
624
+ if (seat === 'TC') {
625
+ const candidates = await tcCandidates(deps, now);
626
+ for (const candidate of candidates) {
627
+ if (ledger.some((row) => row.status === 'shadow' && JSON.stringify(row.candidate) === JSON.stringify(candidate))) continue;
628
+ const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate };
629
+ (deps.append ?? defaultAppend)(path, entry);
630
+ observe('tc-shadow-candidate', { seat, candidate });
631
+ }
632
+ }
633
+ if (seat === 'OP' && !seatQuestions(stateRoot, 'OP').some((question) =>
634
+ !hasSeatAnswer(stateRoot, question)
635
+ && !handledKeys(ledger, config.mode !== 'on' || (config.questions ?? 'shadow') !== 'on', stateRoot).has(`seat-question::${question.id}`))) {
636
+ const append = deps.append ?? defaultAppend;
637
+ const candidates = opCandidates(deps, now);
638
+ const assigned = candidates.filter((candidate): candidate is Extract<OpCandidate, { kind: 'unassigned-cell' }> => candidate.kind === 'unassigned-cell' && candidate.verdict === 'assign');
639
+ if (assigned.length) {
640
+ const previous = new Map<string, Extract<OpCandidate, { kind: 'unassigned-cell' }>>();
641
+ for (const row of ledger) {
642
+ const candidate = row.candidate;
643
+ if (candidate?.kind !== 'unassigned-cell') continue;
644
+ if (candidate.verdict === 'none' && !candidate.id) previous.clear();
645
+ else if (candidate.id) previous.set(JSON.stringify([candidate.version, candidate.id]), candidate);
646
+ }
647
+ const active = new Set(assigned.map((candidate) => JSON.stringify([candidate.version, candidate.id])));
648
+ for (const [key, candidate] of previous) {
649
+ if (candidate.verdict === 'assign' && !active.has(key)) candidates.push({ kind: 'unassigned-cell', version: candidate.version, id: candidate.id, verdict: 'none' });
650
+ }
651
+ }
652
+ const previousDecisions = new Set<string>();
653
+ for (const row of ledger) {
654
+ const candidate = row.candidate;
655
+ if (candidate?.kind !== 'decision-delegation') continue;
656
+ if (candidate.verdict === 'none' && !candidate.id) previousDecisions.clear();
657
+ else if (candidate.id) {
658
+ if (candidate.verdict === 'none') previousDecisions.delete(candidate.id);
659
+ else previousDecisions.add(candidate.id);
660
+ }
661
+ }
662
+ const currentDecision = candidates.find((candidate): candidate is Extract<OpCandidate, { kind: 'decision-delegation' }> =>
663
+ candidate.kind === 'decision-delegation' && candidate.verdict === 'review-delegation');
664
+ if (currentDecision) {
665
+ for (const id of previousDecisions) {
666
+ if (id !== currentDecision.id) candidates.push({ kind: 'decision-delegation', id, verdict: 'none' });
667
+ }
668
+ }
669
+ const entries = candidates.map((candidate): SeatEntry => ({ seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate }));
670
+ for (const entry of entries) {
671
+ const candidate = entry.candidate!;
672
+ const previous = [...ledger].reverse().find((row) => row.candidate?.kind === candidate.kind &&
673
+ (candidate.kind === 'release-readiness' || row.candidate?.kind === candidate.kind && (
674
+ candidate.kind === 'unassigned-cell' && row.candidate.kind === 'unassigned-cell'
675
+ ? (candidate.verdict === 'none' && !candidate.id && row.candidate.verdict === 'none' && !row.candidate.id)
676
+ || row.candidate.version === candidate.version && row.candidate.id === candidate.id
677
+ : candidate.kind === 'decision-delegation' && row.candidate.kind === 'decision-delegation'
678
+ && row.candidate.id === candidate.id)));
679
+ if (previous && JSON.stringify(previous.candidate) === JSON.stringify(candidate)) continue;
680
+ append(path, entry);
681
+ observe('op-judgment-shadow', { seat, candidate: entry.candidate, status: entry.status });
682
+ }
683
+ return entries[0]!;
684
+ }
685
+ const inputs = await gatherSeatInputs(seat, deps, ledger);
686
+ const questionsMode = config.mode === 'on' ? config.questions ?? 'shadow' : 'shadow';
687
+ const questionHandled = handledKeys(ledger, questionsMode !== 'on', stateRoot);
688
+ const pendingQuestion = inputs.requests.find((candidate) => candidate.source === 'seat-question' && !questionHandled.has(itemKey(candidate)));
689
+ const ordinary = pickNext({ ...inputs, requests: inputs.requests.filter((candidate) => candidate.source !== 'seat-question') }, ledger,
690
+ { shadow: config.mode === 'shadow', root: stateRoot, history: deps.checklistHistory ?? checklistHistory });
691
+ const item = pendingQuestion ?? ordinary;
692
+ const append = deps.append ?? defaultAppend;
693
+ // 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.
694
+ if (item && (item.source === 'seat-question' || deps.inquire || (config.mode === 'on' && config.questions !== undefined))) {
695
+ const root = deps.root ?? effectiveInstanceRoot();
696
+ const unfinished = [...ledger].reverse().find((row) => row.item && itemKey(row.item) === itemKey(item)
697
+ && (row.status === 'attempting' || row.status === 'outcome-unknown') && (row.action === 'seat-question' || row.action === 'seat-answer'));
698
+ const inquiry = item.source === 'seat-question' ? null : unfinished?.inquiry ?? await (deps.inquire ?? (async (asking: SeatId, work: SeatItem) => { try {
699
+ const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
700
+ `[${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 };
701
+ const value: unknown = typeof output?.reply === 'string' ? JSON.parse(output.reply) : output;
702
+ if (value === null) return null;
703
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid seat inquiry');
704
+ const record = value as Record<string, unknown>;
705
+ if ((record.to === 'CEO' || (typeof record.to === 'string' && isSeatId(record.to) && record.to !== asking))
706
+ && typeof record.question === 'string' && record.question.trim()) return { to: record.to as SeatId | 'CEO', question: record.question };
707
+ throw new Error('invalid seat inquiry');
708
+ } catch (error) {
709
+ // A failed judgment call must not break the record-only path — no question this turn.
710
+ observe('inquire-failed', { seat: asking, item: work.id, error: String(error).slice(0, 200) });
711
+ return null;
712
+ }
713
+ }))(seat as SeatId, item);
714
+ const reply = item.source === 'seat-question' && item.from && (questionsMode === 'on' || deps.reply || config.questions !== undefined)
715
+ ? unfinished?.answer ? { answer: unfinished.answer } : unfinished?.inquiry ? unfinished.inquiry : await (deps.reply ?? (async (receiver: SeatId, question: string, from: SeatId) => {
716
+ const evidence = inputs.checklist.map((candidate) =>
717
+ `${candidate.id} · ${candidate.title} (판 ${candidate.version}, ${candidate.kind})`).join('\n').slice(0, 8_000);
718
+ const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
719
+ `[${receiver} 자리 · 역할 docs/roles/${receiver}.md] ${from} 자리의 질문에 근거를 갖고 답하라. 아래 ${receiver} 자리의 실제 체크리스트 근거에 없는 사실은 지어내지 마라. 모르면 사람 결정이 필요한 질문을 작성하라. JSON 하나만 반환: {"answer":"근거를 포함한 답"} 또는 {"to":"CEO","question":"결정할 질문"}. 질문: ${question}\n역할: ${inputs.role}\n${receiver} 체크리스트 근거:\n${evidence || '(없음)'}`])).trim()) as { reply?: unknown };
720
+ const value: unknown = typeof output.reply === 'string' ? JSON.parse(output.reply) : output;
721
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid seat reply');
722
+ const record = value as Record<string, unknown>;
723
+ if (typeof record.answer === 'string' && record.answer.trim()) return { answer: record.answer };
724
+ if (record.to === 'CEO' && typeof record.question === 'string' && record.question.trim()) return { to: 'CEO' as const, question: record.question };
725
+ throw new Error('invalid seat reply');
726
+ }))(seat as SeatId, item.text, item.from) : null;
727
+ if (item.source === 'seat-question' && reply && 'to' in reply && reply.to !== 'CEO') {
728
+ throw new Error('seat reply may only answer or escalate to CEO');
729
+ }
730
+ if (item.source === 'seat-question' || inquiry) {
731
+ const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), item, status: 'shadow',
732
+ action: item.source === 'seat-question' ? 'seat-answer' : inquiry?.to === 'CEO' ? 'decision' : 'seat-question',
733
+ ...(inquiry ? { inquiry } : {}), ...(reply && 'answer' in reply ? { answer: reply.answer } : {}) };
734
+ if (item.source === 'seat-question' && reply && 'to' in reply) {
735
+ entry.inquiry = reply;
736
+ entry.action = 'decision';
737
+ }
738
+ if (questionsMode !== 'on') {
739
+ append(path, entry);
740
+ observe('question-shadow', { seat, item, inquiry: entry.inquiry, answered: !!entry.answer });
741
+ return entry;
742
+ }
743
+ if (item.source === 'seat-question' && !reply) {
744
+ entry.status = 'awaiting-answer';
745
+ append(path, entry);
746
+ return entry;
747
+ }
748
+ append(path, { ...entry, status: 'attempting' });
749
+ try {
750
+ if (item.source === 'seat-question' && reply && 'answer' in reply) {
751
+ const question = { id: Number(item.id), from: item.from!, to: seat, body: item.text, kind: 'seat-question', createdAt: item.createdAt ?? '' };
752
+ if (!hasSeatAnswer(root, question)) answerSeat(root, question, reply.answer);
753
+ entry.status = 'answered';
754
+ } else {
755
+ const ask = entry.inquiry!;
756
+ if (!ask.question.trim()) throw new Error('empty seat question');
757
+ if (ask.to === 'CEO') {
758
+ const prior = decisionReceipt(root, seat, item);
759
+ if (!prior) {
760
+ const run = deps.run ?? defaultRun;
761
+ await run(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
762
+ '--s', `${seat} 질문 · ${item.title}`, '--c', '판단 근거 부족 — 사람의 결정이 필요하다',
763
+ '--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
764
+ '--skip-recommend', '근거가 부족하다', '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
765
+ }
766
+ if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
767
+ entry.status = 'hitl';
768
+ } else {
769
+ if (!isSeatId(ask.to) || ask.to === seat) throw new Error('invalid recipient seat');
770
+ const key = `ask:${seat}:${itemKey(item)}`;
771
+ const delivered = deliveredSeatQuestion(root, key);
772
+ if (delivered && (delivered.from !== seat || delivered.to !== ask.to || delivered.body !== ask.question.trim())) {
773
+ throw new Error('seat delivery retry changed its recorded question');
774
+ }
775
+ askSeat(root, seat as SeatId, ask.to, ask.question, key);
776
+ entry.status = 'asked';
777
+ }
778
+ }
779
+ append(path, entry);
780
+ observe('question-outcome', { seat, item, status: entry.status });
781
+ return entry;
782
+ } catch (error) {
783
+ append(path, { ...entry, status: 'outcome-unknown' });
784
+ throw error;
785
+ }
786
+ }
787
+ }
170
788
  if (item) observe('picked', { seat, item, date: seatDay(now) });
171
789
  const planned = item ? planAction(item, seat) : undefined;
172
790
  const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: item ? 'shadow' : 'skipped-empty',
173
791
  ...(item ? { item, action: planned!.kind, ...(planned!.reason ? { reason: planned!.reason } : {}) } : { action: 'skipped-empty' as const }) };
174
- const append = deps.append ?? defaultAppend;
175
792
  if (config.mode === 'shadow' || !item) {
176
793
  append(path, entry);
177
794
  if (config.mode === 'shadow') observe('shadow', { seat, item, status: entry.status });
178
795
  return entry;
179
796
  }
797
+ if (planned!.kind === 'wait') {
798
+ entry.status = 'wait';
799
+ append(path, entry);
800
+ return entry;
801
+ }
180
802
  const run = deps.run ?? defaultRun;
181
803
  let outcome: string | undefined;
182
804
  try { outcome = (JSON.parse(await run(['harness', 'budget', '--json'])) as { outcome?: string }).outcome; }
@@ -190,6 +812,52 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
190
812
  return entry;
191
813
  }
192
814
  const action = planned!;
815
+ if (action.kind === 'harness') {
816
+ const launchedRunIds = new Set(ledger.flatMap((row) => row.seat === seat && row.status === 'launched' && row.runId ? [row.runId] : []));
817
+ let running: readonly RunningSeatPod[] = [];
818
+ let observationUnavailable = false;
819
+ try {
820
+ if (deps.running) running = deps.running();
821
+ else if (launchedRunIds.size > 0) {
822
+ const observed = queryRunningRuns({ runIds: [...launchedRunIds], caller: 'seat-loop' });
823
+ if (observed.completeness !== 'complete' || observed.pty.unreadable.length > 0) observationUnavailable = true;
824
+ running = observed.entries
825
+ .filter((row) => launchedRunIds.has(row.runId) && (row.status === 'running' || row.status === 'probable-running' || row.status === 'unknown'))
826
+ // Seat launches always use `--substrate pod` (see the harness say call), so the substrate is known from the launch;
827
+ // only liveness can be uncertain — probable-running/unknown stay «unknown», never counted as confirmed Pods.
828
+ .map((row) => ({ seat, substrate: 'pod', status: row.status === 'running' ? 'running' : 'unknown' }));
829
+ }
830
+ } catch { observationUnavailable = true; }
831
+ const budget = observationUnavailable
832
+ ? { allowed: false as const, reason: `${seat} concurrent Pods budget cannot be verified (running observation unavailable)` }
833
+ : checkSeatBudget({ seat, now, ledger, running, ...(deps.budgetConfig ? { config: deps.budgetConfig } : {}) });
834
+ if (!budget.allowed) {
835
+ entry.status = 'skipped-budget';
836
+ entry.action = 'skipped-budget';
837
+ entry.reason = budget.reason;
838
+ observe('skipped-budget', { seat, item, reason: budget.reason });
839
+ const parent = (config.stall?.parents ?? {})[seat] ?? DEFAULT_STALL_PARENTS[seat];
840
+ // Dedupe only on a DELIVERED escalation, so a failed send is retried on the next turn (TC harvest · review round 1).
841
+ if (parent && parent !== seat && !ledger.some((prior) => prior.status === 'skipped-budget' && prior.escalated === true && prior.reason === budget.reason
842
+ && prior.item && itemKey(prior.item) === itemKey(item) && seatDay(new Date(prior.ts ?? prior.at)) === seatDay(now))) {
843
+ const line = `[${seat} → ${parent}] ${item.id}: ${budget.reason}`;
844
+ try {
845
+ if (deps.escalate) await deps.escalate(parent, line);
846
+ else {
847
+ const store = openMsgStore(join(deps.root ?? effectiveInstanceRoot(), 'msg', 'messages.db'));
848
+ try { store.append({ from: seat, to: parent, body: line, kind: 'seat-budget-escalation' }); }
849
+ finally { store.close(); }
850
+ }
851
+ entry.escalated = true;
852
+ } catch (error) {
853
+ entry.escalated = false;
854
+ observe('budget-escalation-failed', { seat, parent, item: item.id, error: String(error).slice(0, 200) });
855
+ }
856
+ }
857
+ append(path, entry);
858
+ return entry;
859
+ }
860
+ }
193
861
  // Persist the intent before crossing the process boundary: an unreturned call may already have acted.
194
862
  append(path, { ...entry, status: 'attempting' });
195
863
  try {
@@ -227,5 +895,17 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
227
895
  append(path, { ...entry, status: 'outcome-unknown' });
228
896
  throw error;
229
897
  }
898
+ } catch (error) {
899
+ turnError = error;
900
+ throw error;
901
+ } finally {
902
+ // A turn ends even when there is no picked item or a launch fails.
903
+ try { escalateStalls(seat, deps, config, (deps.now ?? (() => new Date()))()); }
904
+ catch (error) {
905
+ debug.log('org.stall', 'escalate-error', { seat, error: String(error).slice(0, 200) }, { level: 'warn' });
906
+ if (turnError !== undefined) throw new AggregateError([turnError, error], `seat turn failed (${String(turnError)}); stall escalation failed (${String(error)}) for ${seat}`);
907
+ throw error;
908
+ }
909
+ }
230
910
  }, deps.lockContended);
231
911
  }