elanous 0.2.12 → 0.2.13

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 (289) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1394-ee3795b112907122.js → 1394-931c9827067b2276.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{7758-0b018a9e4fc56200.js → 7758-85c2b53f731da024.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/{8605-0259994816604c49.js → 8605-9da4678f8a310d18.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-cb169620945d1952.js → page-e5b4e1b256f41076.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-cefe8169e36cf62a.js → page-e49b9b76c565a0b1.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-813c4a52c9d05ca8.js → page-4c1dbb21861beab2.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-4731f2c215358937.js → page-c85b816e3b269dbb.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/{page-a022b257950f39e6.js → page-f6bc66b919b2ea7d.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-a2ffc717e04b8b0a.js → page-60603c1e056c9b25.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b59ee4e70fc18fcf.js → page-a0ea8f5a2341ee6b.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/field/{page-c172dabd796eb7a3.js → page-36c6c4183353ac9e.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/inside/{page-1efd0cb9e0ed9d13.js → page-cb2eb0fd809980da.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/intake/{page-d5dc7ff69a37b4cf.js → page-31012264da8a341f.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/market/{page-dfca9b7d2de0b135.js → page-0696ee3fac9fe56c.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-66c27608f651b866.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/morning/{page-6dd3bfbbde2a5c2c.js → page-6ddd2109b7fc3a80.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-77447356baad75d2.js → page-7a2bf830038a2264.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-6d604227e1664b71.js → page-f672536f14f0969f.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-4048543b4b7ad5e0.js → page-e7a6c80f288b1948.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4c3a3126abed551a.js → page-29a709616df9b050.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-64b0aecb1ca75e75.js → page-fee842d7c2151129.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/{page-9547b5cec7cf76bf.js → page-d92e32d51ab5157e.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-2cddeb9e2aeedc55.js → page-a1833cd43d832601.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-63d14a4d8fe1e1c1.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/today/{page-baba2e047872fb0f.js → page-2a511d320f762e06.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/trace/{page-e895da4ec9cac345.js → page-1c3e03f88dac1e21.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2e9e2fb01c9da1c9.js → page-2b256feed331546f.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-d62ceb13411ebba4.js → page-505bb96d69a8622c.js} +1 -1
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +13 -13
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +13 -13
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +13 -13
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +13 -13
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +13 -13
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +13 -13
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +13 -13
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +13 -13
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +13 -13
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +13 -13
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +13 -13
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +13 -13
  88. package/apps/pwa/out/market/index.html +1 -1
  89. package/apps/pwa/out/market/index.txt +13 -13
  90. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  91. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  92. package/apps/pwa/out/morning/index.html +1 -1
  93. package/apps/pwa/out/morning/index.txt +13 -13
  94. package/apps/pwa/out/observatory/index.html +1 -1
  95. package/apps/pwa/out/observatory/index.txt +13 -13
  96. package/apps/pwa/out/ops/checklist/index.html +1 -1
  97. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  98. package/apps/pwa/out/ops/release/index.html +1 -1
  99. package/apps/pwa/out/ops/release/index.txt +13 -13
  100. package/apps/pwa/out/ops/seats/index.html +1 -1
  101. package/apps/pwa/out/ops/seats/index.txt +13 -13
  102. package/apps/pwa/out/outputs/index.html +1 -1
  103. package/apps/pwa/out/outputs/index.txt +13 -13
  104. package/apps/pwa/out/reflection/index.html +1 -1
  105. package/apps/pwa/out/reflection/index.txt +14 -14
  106. package/apps/pwa/out/scheduler/index.html +1 -1
  107. package/apps/pwa/out/scheduler/index.txt +13 -13
  108. package/apps/pwa/out/sessions/index.html +1 -1
  109. package/apps/pwa/out/sessions/index.txt +13 -13
  110. package/apps/pwa/out/settings/devices/index.html +1 -1
  111. package/apps/pwa/out/settings/devices/index.txt +13 -13
  112. package/apps/pwa/out/settings/index.html +1 -1
  113. package/apps/pwa/out/settings/index.txt +13 -13
  114. package/apps/pwa/out/setup/done/index.html +1 -1
  115. package/apps/pwa/out/setup/done/index.txt +13 -13
  116. package/apps/pwa/out/setup/index.html +1 -1
  117. package/apps/pwa/out/setup/index.txt +13 -13
  118. package/apps/pwa/out/share/index.html +1 -1
  119. package/apps/pwa/out/share/index.txt +13 -13
  120. package/apps/pwa/out/showroom/index.html +1 -1
  121. package/apps/pwa/out/showroom/index.txt +13 -13
  122. package/apps/pwa/out/tasks/index.html +1 -1
  123. package/apps/pwa/out/tasks/index.txt +13 -13
  124. package/apps/pwa/out/term/index.html +1 -1
  125. package/apps/pwa/out/term/index.txt +13 -13
  126. package/apps/pwa/out/today/index.html +1 -1
  127. package/apps/pwa/out/today/index.txt +13 -13
  128. package/apps/pwa/out/trace/index.html +1 -1
  129. package/apps/pwa/out/trace/index.txt +13 -13
  130. package/apps/pwa/out/vault/index.html +1 -1
  131. package/apps/pwa/out/vault/index.txt +13 -13
  132. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  133. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  134. package/apps/pwa/out/workflows/index.html +1 -1
  135. package/apps/pwa/out/workflows/index.txt +13 -13
  136. package/apps/pwa/out/workspace/index.html +1 -1
  137. package/apps/pwa/out/workspace/index.txt +13 -13
  138. package/apps/pwa/out/worktrees/index.html +1 -1
  139. package/apps/pwa/out/worktrees/index.txt +13 -13
  140. package/examples/teams/personas/backend.yaml +4 -0
  141. package/examples/teams/personas/pm.yaml +4 -0
  142. package/examples/teams/personas/qa.yaml +4 -0
  143. package/examples/teams/small-product.yaml +20 -0
  144. package/graphs/lecture/lecture-note.yaml +28 -0
  145. package/graphs/lecture/recipes.yaml +17 -0
  146. package/package.json +2 -1
  147. package/scripts/context-hooks/emit.ts +36 -14
  148. package/scripts/coord-channel-watch.sh +1 -0
  149. package/scripts/discord-seat-probe.ts +144 -0
  150. package/scripts/hq/hq-standby.sh +65 -0
  151. package/scripts/hq/standby-promote.ts +173 -0
  152. package/scripts/hq/standby-role.ts +59 -0
  153. package/scripts/hq/standby-snapshot.ts +226 -0
  154. package/scripts/hq/standby-verify.ts +219 -0
  155. package/scripts/lecture-note/node.ts +256 -0
  156. package/scripts/release-loop/gate-node.ts +53 -15
  157. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  158. package/scripts/test-deterministic.ts +53 -3
  159. package/skills/omni-crawl/SKILL.md +16 -16
  160. package/skills/omni-crawl/package-lock.json +1 -544
  161. package/skills/omni-crawl/package.json +3 -4
  162. package/skills/omni-crawl/scripts/main.ts +13 -13
  163. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  164. package/skills/omni-crawl/src/apify.ts +13 -2
  165. package/skills/omni-crawl/src/capture.ts +6 -0
  166. package/skills/omni-crawl/src/registry.ts +2 -2
  167. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  168. package/skills/omni-digest/SKILL.md +10 -11
  169. package/skills/omni-digest/package-lock.json +0 -545
  170. package/skills/omni-digest/package.json +2 -5
  171. package/skills/omni-digest/scripts/main.ts +3 -3
  172. package/skills/omni-digest/src/delegate.ts +5 -5
  173. package/skills/omni-digest/src/fetch.ts +2 -2
  174. package/skills/omni-digest/src/media.ts +25 -11
  175. package/skills/omni-digest/src/visual.ts +3 -2
  176. package/skills/omni-market/scripts/main.ts +1 -1
  177. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  178. package/src/agent/identity-env.ts +11 -0
  179. package/src/agent-mission/driver.ts +12 -7
  180. package/src/agent-mission/duo.ts +94 -0
  181. package/src/agent-mission/pty-decision.ts +1 -1
  182. package/src/autopilot/track-agent-shadow.ts +2 -1
  183. package/src/autopilot/track-agent.ts +4 -1
  184. package/src/boot/daemon-prompt-request.ts +3 -0
  185. package/src/boot/daemon-prompt-turn.ts +6 -2
  186. package/src/boot/daemon-runtime.ts +48 -16
  187. package/src/cli/decisions-cli.ts +51 -19
  188. package/src/cli/outputs-cli.ts +92 -0
  189. package/src/cli/persona-cli.ts +29 -0
  190. package/src/cli/pod-cli.ts +10 -3
  191. package/src/cli/pr-cli.ts +15 -1
  192. package/src/cli/release-cli.ts +108 -28
  193. package/src/context-bus/condense.ts +194 -0
  194. package/src/context-bus/context-day.ts +82 -0
  195. package/src/context-bus/context-now-surfaces.ts +15 -2
  196. package/src/context-bus/long-term-memory.ts +118 -22
  197. package/src/context-bus/nightly-condense.ts +1 -1
  198. package/src/decisions/decision-cards.ts +13 -7
  199. package/src/decisions/decision-ledger.ts +43 -17
  200. package/src/graph-kinds/registry.ts +2 -2
  201. package/src/graph-runner/graph-approval-card.ts +2 -2
  202. package/src/graph-runner/graph-grow.ts +64 -11
  203. package/src/graph-runner/graph-tick.ts +9 -2
  204. package/src/graph-runner/runner.ts +81 -17
  205. package/src/harness/harness-cli-command.ts +27 -1
  206. package/src/harness/harness-queue-child.ts +37 -0
  207. package/src/harness/harness-queue.ts +306 -0
  208. package/src/hooks/github-shadow.ts +72 -2
  209. package/src/hooks/receiver.ts +4 -2
  210. package/src/hq/hq.ts +318 -0
  211. package/src/hq/lease.ts +210 -0
  212. package/src/hq/ledger-write-target.ts +41 -0
  213. package/src/index.ts +199 -7
  214. package/src/intake-plane/discord-seat-work.ts +13 -6
  215. package/src/intake-plane/linear-wish.ts +44 -0
  216. package/src/intake-plane/persona-answer.ts +5 -0
  217. package/src/intake-plane/telegram-seat-work.ts +13 -6
  218. package/src/intake-plane/wish-card.ts +1 -1
  219. package/src/loops/checker.ts +178 -0
  220. package/src/loops/loop-cli.ts +37 -3
  221. package/src/loops/observe.ts +49 -0
  222. package/src/loops/orchestrator/overlap.ts +141 -0
  223. package/src/loops/orchestrator/traffic.ts +150 -0
  224. package/src/loops/package/verify.ts +125 -0
  225. package/src/loops/verdict.ts +22 -0
  226. package/src/maturity/feature-maturity.ts +3 -3
  227. package/src/memory.ts +37 -4
  228. package/src/nexus/api/card-followup-route.ts +118 -0
  229. package/src/nexus/api/decisions-route.ts +68 -0
  230. package/src/nexus/api/http-server.ts +47 -0
  231. package/src/nexus/api/personas.ts +2 -1
  232. package/src/nexus/api/skill-exec-route.ts +97 -0
  233. package/src/persona/loader.ts +18 -0
  234. package/src/persona/persona-todo.ts +14 -9
  235. package/src/persona/presets.ts +2 -1
  236. package/src/persona/team.ts +105 -0
  237. package/src/persona/types.ts +4 -0
  238. package/src/pod-lease/host-lease.ts +130 -20
  239. package/src/release-loop/landed-but-yellow.ts +12 -0
  240. package/src/seat-dispatch/seat-ask.ts +57 -12
  241. package/src/seat-loop/seat-loop.ts +90 -16
  242. package/src/seat-loop/seat-pack.ts +127 -0
  243. package/src/self-dev/ask-launch-flow.ts +3 -0
  244. package/src/self-implement/l8-merge-queue.ts +157 -1
  245. package/src/steward/launch.ts +5 -1
  246. package/src/steward/readiness.ts +125 -0
  247. package/src/steward/rescue.ts +75 -0
  248. package/src/steward/triage.ts +48 -9
  249. package/src/task-cards/card-cli.ts +78 -0
  250. package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
  251. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  252. package/src/task-orchestrator/surfaces/self-implement-pod.ts +38 -8
  253. package/src/telegram-dispatch-forward.ts +1 -1
  254. package/src/telegram-fabric-plan.ts +90 -0
  255. package/src/telegram-project-command.ts +10 -4
  256. package/src/telegram.ts +5 -0
  257. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  258. package/src/user-config.ts +94 -10
  259. package/src/version/packed-revision.json +1 -1
  260. package/src/workflow-runtime/executor.ts +4 -0
  261. package/src/workflow-runtime/node-catalog.ts +13 -0
  262. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  263. package/src/workflow-runtime/schema.ts +17 -1
  264. package/src/workflow-runtime/types.ts +13 -1
  265. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  288. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
  289. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
@@ -1,5 +1,5 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { createHash } from 'node:crypto';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
4
4
  import { Database } from 'bun:sqlite';
5
5
  import { dirname, join, resolve } from 'node:path';
@@ -8,13 +8,15 @@ import { TRACK_AGENT_FORBIDDEN_ACTION_REGEX, universeLaunch } from '../autopilot
8
8
  import { debug } from '../debug/log.js';
9
9
  import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
10
10
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
11
+ import { loopEvent } from '../loops/observe.js';
11
12
  import { getUserConfig, type PersonaLoopConfig, type SeatLoopConfig, type UserConfig } from '../user-config.js';
12
13
  import { checkSeatBudget, type RunningSeatPod } from '../org/seat-budget.js';
13
14
  import { DEFAULT_STALL_PARENTS, findStalls } from '../org/stall-escalation.js';
14
15
  import { openMsgStore, type MessageEnvelope } from '../msg/msg-store.js';
15
16
  import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
16
- import { nextPersonaTodo, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
17
+ import { orderedPersonaTodos, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
17
18
  import { PersonaRegistry } from '../persona/registry.js';
19
+ import { recordSeatAskShadow } from '../seat-dispatch/seat-ask.js';
18
20
  import { answerSeat, askSeat, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatQuestions, type SeatId } from '../seat-dispatch/seat-questions.js';
19
21
  import { queryRunningRuns } from '../self-implement/running-runs.js';
20
22
  import { checklistHistory, listChecklist, type Checklist } from '../release-loop/checklist.js';
@@ -33,11 +35,12 @@ export type OpCandidate =
33
35
  | { kind: 'decision-delegation'; id: string | null; title?: string; category?: DecisionEntry['category']; dueAt?: string; verdict: 'review-delegation' | 'none' };
34
36
  export type TcCandidate =
35
37
  | { 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[] };
38
+ | { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
39
+ | { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
37
40
  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
41
  export type SeatEntry = { seat: string; ts?: string; at: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean };
39
42
  export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off' };
40
- export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null };
43
+ export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
41
44
  export type SeatDeps = {
42
45
  personaConfig?: PersonaLoopConfig;
43
46
  personaRegistry?: PersonaRegistry;
@@ -134,8 +137,17 @@ export function personaShadowLedgerPath(personaId: string, root: string, now: Da
134
137
  return join(root, 'persona-loop', personaId, `${seatDay(now)}.jsonl`);
135
138
  }
136
139
 
140
+ export async function runPersonaLoopOnce(name: string, deps: SeatDeps = {}): Promise<PersonaShadowEntry> {
141
+ const entries = await pickPersonaShadows(deps, name);
142
+ return entries[0]!;
143
+ }
144
+
137
145
  export async function runPersonaShadowOnce(deps: SeatDeps = {}): Promise<PersonaShadowEntry[]> {
138
146
  if ((deps.personaConfig ?? getUserConfig().loops?.persona)?.enabled !== true) return [];
147
+ return pickPersonaShadows(deps);
148
+ }
149
+
150
+ async function pickPersonaShadows(deps: SeatDeps, name?: string): Promise<PersonaShadowEntry[]> {
139
151
  const root = deps.root ?? effectiveInstanceRoot();
140
152
  const dir = deps.personaDir ?? (deps.root ? join(root, 'personas') : resolveStatePersonaDir());
141
153
  const registry = deps.personaRegistry ?? new PersonaRegistry();
@@ -145,19 +157,31 @@ export async function runPersonaShadowOnce(deps: SeatDeps = {}): Promise<Persona
145
157
  }
146
158
  const now = (deps.now ?? (() => new Date()))();
147
159
  const entries: PersonaShadowEntry[] = [];
148
- for (const profile of registry.list().sort((a, b) => a.personaId.localeCompare(b.personaId))) {
160
+ const profiles = registry.list().sort((a, b) => a.personaId.localeCompare(b.personaId));
161
+ const selected = name === undefined ? profiles : profiles.filter((profile) => profile.personaId.toLowerCase() === name.trim().toLowerCase()
162
+ || profile.displayName.toLowerCase() === name.trim().toLowerCase());
163
+ if (name !== undefined && selected.length !== 1) throw new Error(selected.length ? `ambiguous persona name: ${name}` : `persona not found: ${name}`);
164
+ for (const profile of selected) {
149
165
  const personaId = profile.personaId;
150
- const { todos, errors } = readPersonaTodos(dir, personaId);
166
+ const todoFile = profile.todo ?? `${personaId}.todo.jsonl`;
167
+ const { todos, errors } = readPersonaTodos(dir, todoFile.slice(0, -'.todo.jsonl'.length));
151
168
  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 };
169
+ const candidates = orderedPersonaTodos(todos);
170
+ const picked = pickNext({ requests: candidates.map((candidate) => ({
171
+ source: 'request', id: candidate.id, title: candidate.title, text: candidate.title, createdAt: candidate.createdAt,
172
+ })), checklist: [], role: '' }, [], { shadow: true });
173
+ const todo = picked ? candidates.find((candidate) => candidate.id === picked.id) ?? null : null;
174
+ const planned = name !== undefined && todo && picked ? planAction(picked, profile.seat) : undefined;
175
+ const entry: PersonaShadowEntry = { personaId, ts: now.toISOString(), status: todo ? 'shadow' : 'skipped-empty', todo,
176
+ ...(planned ? { action: planned.kind, what: planned.text } : {}) };
154
177
  const path = personaShadowLedgerPath(personaId, root, now);
155
178
  if (deps.appendPersona) deps.appendPersona(path, entry);
156
179
  else {
157
180
  mkdirSync(dirname(path), { recursive: true });
158
181
  appendFileSync(path, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
159
182
  }
160
- observe('persona-shadow', { personaId, status: entry.status, todoId: todo?.id ?? null });
183
+ try { debug.log('persona.loop', 'picked', { personaId, status: entry.status, todoId: todo?.id ?? null, title: todo?.title ?? null, action: entry.action ?? null }); }
184
+ catch { /* observation cannot alter the shadow ledger */ }
161
185
  entries.push(entry);
162
186
  }
163
187
  return entries;
@@ -167,6 +191,25 @@ export async function runSeatLoopTurn(seat: string, deps: SeatDeps = {}): Promis
167
191
  const result = await runSeatLoopOnce(seat, deps);
168
192
  try { await runPersonaShadowOnce(deps); }
169
193
  catch (error) { observe('persona-shadow-error', { error: String(error).slice(0, 200) }); }
194
+ try {
195
+ const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'off' };
196
+ const profile = result.status === 'skipped-off' ? 'off' : config.mode;
197
+ const context = { runId: 'runId' in result && result.runId ? result.runId : `seat-turn-${randomUUID()}`,
198
+ profile, sourceRef: `docs/roles/${seat}.md` };
199
+ const loopId = `${seat.toLowerCase()}-seat`;
200
+ loopEvent(loopId, 'tick', { ...context, outcome: result.status, reason: result.status });
201
+ if ('inquiry' in result && result.inquiry && (result.status === 'asked' || result.status === 'hitl')) {
202
+ loopEvent(loopId, 'exchange', { ...context, outcome: result.status, reason: 'delegation',
203
+ from: seat, to: result.inquiry.to, itemId: result.item?.id });
204
+ }
205
+ if ('escalated' in result && result.escalated && result.status === 'skipped-budget') {
206
+ loopEvent(loopId, 'exchange', { ...context, outcome: result.status, reason: 'delegation',
207
+ from: seat, to: (config.stall?.parents ?? {})[seat] ?? DEFAULT_STALL_PARENTS[seat], itemId: result.item?.id });
208
+ } else if (result.status === 'skipped-budget') {
209
+ loopEvent(loopId, 'exchange', { ...context, outcome: result.status, reason: 'requeue',
210
+ itemId: result.item?.id });
211
+ }
212
+ } catch { /* observation cannot change a completed seat turn */ }
170
213
  return result;
171
214
  }
172
215
 
@@ -198,6 +241,8 @@ function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEn
198
241
  function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string): Set<string> {
199
242
  const inquiryRow = (entry: SeatEntry) => entry.action === 'seat-question' || entry.action === 'seat-answer' || !!entry.inquiry;
200
243
  return new Set(ledger.filter((entry, index) => entry.status === 'asked' || entry.status === 'answered'
244
+ // RM3a: only TC's other-seat defect ask-shadows are deduplicated here — an ordinary inquiry recorded in shadow must still be delivered once questions turn on.
245
+ || (entry.status === 'shadow' && entry.candidate?.kind === 'other-seat-cell-defect')
201
246
  || (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
202
247
  || (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
203
248
  || (!inquiryRow(entry) && entry.status === 'outcome-unknown')
@@ -303,9 +348,10 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
303
348
  debug.log('seat.loop', 'schedule-unreadable', { seat, error: String(error).slice(0, 200) });
304
349
  }
305
350
  const checklist: SeatItem[] = [];
351
+ const closed: string[] = [];
306
352
  for (const version of open === null ? [] : versions().sort(versionOrder)) {
307
353
  if (!open!.has(version)) {
308
- debug.log('seat.loop', 'skip-closed-version', { seat, version });
354
+ closed.push(version);
309
355
  continue;
310
356
  }
311
357
  let items: ReturnType<typeof itemsOf>;
@@ -325,6 +371,9 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
325
371
  }
326
372
  }
327
373
  }
374
+ if (closed.length > 0) {
375
+ debug.log('seat.loop', 'skip-closed-versions', { seat, count: closed.length, newest: closed.at(-1)!, oldest: closed[0]! });
376
+ }
328
377
  checklist.sort((a, b) => versionOrder(a.version!, b.version!) || a.id.localeCompare(b.id));
329
378
  return { requests: pending, checklist, role: read(join(deps.repo ?? repoRoot, 'docs', 'roles', `${seat}.md`)).slice(0, 4_000) };
330
379
  }
@@ -375,7 +424,7 @@ function readTcChecklist(root: string): Array<{ version: string; id: string; tit
375
424
  const db = new Database(path, { readonly: true, strict: true });
376
425
  try {
377
426
  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'
427
+ FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.status IN ('yellow', 'red') AND a.owner IN ('OP', 'TC', 'MK', 'UX')
379
428
  ORDER BY a.version, f.id`).all() as Array<{ version: string; id: string; title: string; status: string; owner: string | null }>;
380
429
  } finally { db.close(); }
381
430
  }
@@ -461,16 +510,27 @@ async function tcCandidates(deps: SeatDeps, now: Date): Promise<TcCandidate[]> {
461
510
  const candidates: TcCandidate[] = [];
462
511
  const releaseRoot = deps.root ?? releaseLedgerRoot();
463
512
  try {
464
- const cells = merged.length === 0 ? [] : deps.checklistItems && deps.schedules
513
+ const cells = deps.checklistItems && deps.schedules
465
514
  ? deps.schedules().flatMap(({ version }) => deps.checklistItems!(version).map((cell) => ({ ...cell, version })))
466
515
  : readTcChecklist(releaseRoot);
516
+ let shipped: string | undefined;
517
+ try { shipped = releasedVersion(releaseRoot); }
518
+ catch (error) { observe('tc-release-unreadable', { seat: 'TC', error: String(error).slice(0, 200) }); }
519
+ const openVersions = deps.schedules ? new Set(deps.schedules().filter(({ cutAt }) => Number.isFinite(Date.parse(cutAt))
520
+ && Date.parse(cutAt) > now.getTime()).map(({ version }) => version)) : null;
467
521
  for (const cell of cells) {
468
- if (cell.owner !== 'TC' || cell.status !== 'yellow') continue;
522
+ if (cell.owner !== 'TC' && ((shipped && versionOrder(cell.version, shipped) <= 0) || (openVersions && !openVersions.has(cell.version)))) continue;
523
+ if (cell.status !== 'yellow' && cell.status !== 'red') continue;
524
+ if (!cell.owner || !isSeatId(cell.owner)) continue;
525
+ if (cell.owner !== 'TC') {
526
+ if (cell.status === 'red') candidates.push({ kind: 'other-seat-cell-defect', version: cell.version, id: cell.id, title: cell.title, to: cell.owner });
527
+ continue;
528
+ }
469
529
  for (const pr of merged) {
470
530
  const id = cell.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
471
531
  const reference = new RegExp(`(?<![A-Za-z0-9])${id}(?![A-Za-z0-9])`);
472
532
  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! });
533
+ if (cell.owner === 'TC' && cell.status === 'yellow') candidates.push({ kind: 'merged-pr-yellow-cell', version: cell.version, id: cell.id, title: cell.title, pr: pr.number, mergedAt: pr.mergedAt! });
474
534
  }
475
535
  }
476
536
  }
@@ -624,6 +684,20 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
624
684
  if (seat === 'TC') {
625
685
  const candidates = await tcCandidates(deps, now);
626
686
  for (const candidate of candidates) {
687
+ if (candidate.kind === 'other-seat-cell-defect') {
688
+ const question = `${candidate.version} 체크리스트 ${candidate.id} · ${candidate.title}: ${candidate.to} 칸 결함을 확인해 주세요.`;
689
+ const recorded = recordSeatAskShadow(stateRoot, { from: 'TC', to: candidate.to, itemId: candidate.id, version: candidate.version, question }, now.getTime());
690
+ const inLedger = ledger.some((row) => row.status === 'shadow' && row.candidate?.kind === 'other-seat-cell-defect'
691
+ && row.candidate.version === candidate.version && row.candidate.id === candidate.id && row.candidate.to === candidate.to);
692
+ if (!inLedger) {
693
+ const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
694
+ item: { source: 'checklist', kind: 'cell', id: candidate.id, title: candidate.title, text: candidate.title, version: candidate.version, seat: candidate.to },
695
+ action: 'seat-question', inquiry: { to: candidate.to, question } };
696
+ (deps.append ?? defaultAppend)(path, entry);
697
+ }
698
+ if (!inLedger || recorded) observe('ask-shadow', { from: seat, to: candidate.to, itemId: candidate.id });
699
+ continue;
700
+ }
627
701
  if (ledger.some((row) => row.status === 'shadow' && JSON.stringify(row.candidate) === JSON.stringify(candidate))) continue;
628
702
  const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate };
629
703
  (deps.append ?? defaultAppend)(path, entry);
@@ -761,7 +835,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
761
835
  await run(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
762
836
  '--s', `${seat} 질문 · ${item.title}`, '--c', '판단 근거 부족 — 사람의 결정이 필요하다',
763
837
  '--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
764
- '--skip-recommend', '근거가 부족하다', '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
838
+ '--skip-recommend', '근거가 부족하다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②', '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
765
839
  }
766
840
  if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
767
841
  entry.status = 'hitl';
@@ -866,7 +940,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
866
940
  const raised = await run(['decisions', 'raise', '--title', title, '--category', 'other',
867
941
  '--s', `${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')}\n역할: ${inputs.role}`, '--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
868
942
  '--option', 'a=사람 승인:승인 후 별도로 집행', '--option', 'b=보류:집행하지 않음',
869
- '--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--agent', 'seat-loop', '--json']);
943
+ '--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②', '--agent', 'seat-loop', '--json']);
870
944
  const decision: unknown = JSON.parse(raised.trim());
871
945
  if (!decision || typeof decision !== 'object' || typeof (decision as { id?: unknown }).id !== 'string') {
872
946
  throw new Error('decisions raise returned no decision id');
@@ -0,0 +1,127 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, relative, resolve, sep } from 'node:path';
4
+ import { userInfo } from 'node:os';
5
+ import { DEFAULT_CONCURRENT_PODS, DEFAULT_DAILY_GOALS } from '../org/seat-budget.js';
6
+
7
+ export interface SeatPackManifest {
8
+ name: string;
9
+ version: string;
10
+ files: Array<{ path: string; sha256: string }>;
11
+ }
12
+
13
+ export class SeatPackLeakError extends Error {
14
+ constructor(file: string, line: number, marker: string) {
15
+ super(`${file}:${line}: public leak (${marker})`);
16
+ }
17
+ }
18
+
19
+ function regularFile(path: string): boolean {
20
+ return lstatSync(path).isFile();
21
+ }
22
+
23
+ function walk(directory: string, root: string): string[] {
24
+ if (!existsSync(directory)) return [];
25
+ if (!lstatSync(directory).isDirectory()) throw new Error(`not a directory: ${relative(root, directory)}`);
26
+ return readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))
27
+ .flatMap((entry) => {
28
+ const path = join(directory, entry.name);
29
+ if (entry.isSymbolicLink()) throw new Error(`symlink in seat pack: ${relative(root, path)}`);
30
+ return entry.isDirectory() ? walk(path, root) : entry.isFile() ? [relative(root, path)] : [];
31
+ });
32
+ }
33
+
34
+ function scan(file: string, body: string): void {
35
+ const usernames = new Set([userInfo().username, process.env.ELANOUS_LEAK_HOME_USER].filter((name): name is string => Boolean(name)));
36
+ const accountPatterns = [...usernames].map((name) => new RegExp(`(?<![A-Za-z0-9._-])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9._-])`, 'iu'));
37
+ const patterns: Array<[string, RegExp]> = [
38
+ ['royal marker', /\u{1F451}/u],
39
+ ['track marker', /(?:🅞|🅣|🅢|🅕|\*\*\[(?:OP|TC|MK|UX|S|T|O|F)\]\*\*|\[(?:OP|TC|MK|UX|S|T|O|F)\])/u],
40
+ ['absolute user path', /\/Users\/[A-Za-z0-9._-]+/u],
41
+ ...accountPatterns.map((pattern): [string, RegExp] => ['account name', pattern]),
42
+ ];
43
+ for (const [index, line] of body.split(/\r?\n/u).entries()) {
44
+ for (const [marker, pattern] of patterns) {
45
+ if (pattern.test(line)) throw new SeatPackLeakError(file, index + 1, marker);
46
+ }
47
+ }
48
+ }
49
+
50
+ /** Build from charters, explicitly referenced seat graphs and the seat's own `graphs/<seat>/` folder; never copy an entire shared graph directory by default. */
51
+ export function exportSeatPack(seat: string, out: string, root = resolve(import.meta.dir, '../..')): SeatPackManifest {
52
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/u.test(seat)) throw new Error(`invalid seat: ${seat}`);
53
+ const repo = resolve(root);
54
+ const output = resolve(out);
55
+ const roles = join(repo, 'docs', 'roles');
56
+ const charter = join(roles, `${seat}.md`);
57
+ if (!existsSync(charter) || !regularFile(charter)) throw new Error(`seat charter not found: docs/roles/${seat}.md`);
58
+ const roleFiles = [`docs/roles/${seat}.md`, ...walk(join(roles, seat), repo).filter((file) => file.endsWith('.md'))];
59
+ const graphFiles = new Set<string>();
60
+ const contents = new Map<string, Buffer>();
61
+ for (const file of roleFiles) {
62
+ const body = readFileSync(join(repo, file));
63
+ contents.set(file, body);
64
+ for (const match of body.toString('utf8').matchAll(/graphs\/([A-Za-z0-9_./-]+(?:\.ya?ml|\/\*\*))/gu)) {
65
+ const ref = `graphs/${match[1]!}`;
66
+ if (ref.split('/').includes('..')) throw new Error(`invalid seat graph: ${ref}`);
67
+ if (ref.endsWith('/**')) {
68
+ if (!existsSync(join(repo, ref.slice(0, -3)))) throw new Error(`seat graph not found: ${ref}`);
69
+ for (const graph of walk(join(repo, ref.slice(0, -3)), repo)) {
70
+ if (/\.ya?ml$/u.test(graph)) graphFiles.add(graph);
71
+ }
72
+ } else graphFiles.add(ref);
73
+ }
74
+ }
75
+ // A seat's own graph folder (`graphs/<seat in lower case>/`) belongs to the seat even when the
76
+ // charter no longer mentions it — inclusion must not hinge on a text reference that can be edited away.
77
+ const ownGraphs = join(repo, 'graphs', seat.toLowerCase());
78
+ if (existsSync(ownGraphs)) {
79
+ for (const graph of walk(ownGraphs, repo)) if (/\.ya?ml$/u.test(graph)) graphFiles.add(graph);
80
+ }
81
+ for (const file of graphFiles) {
82
+ const path = join(repo, file);
83
+ if (!existsSync(path) || !regularFile(path)) throw new Error(`seat graph not found: ${file}`);
84
+ contents.set(file, readFileSync(path));
85
+ }
86
+ const verdicts = roleFiles.flatMap((file) => contents.get(file)!.toString('utf8').split(/\r?\n/u)
87
+ .filter((line) => /\|\s*판정선\s*\|/u.test(line)).map((line) => ({ source: file, text: line.split('|').slice(2, -1).join('|').trim() })));
88
+ for (const file of roleFiles) {
89
+ const charter = contents.get(file)!.toString('utf8');
90
+ for (const match of charter.matchAll(/(?:^|\n)\s*[-*]\s*\*\*(?:관문|판정선)\*\*\s*[—:-]\s*([^\n]+)/gu)) {
91
+ verdicts.push({ source: file, text: match[1]!.trim() });
92
+ }
93
+ let inGateSection = false;
94
+ for (const line of charter.split(/\r?\n/u)) {
95
+ if (/^##\s+\d+\.\s+관문(?:\s|\(|$)/u.test(line)) { inGateSection = true; continue; }
96
+ if (/^##\s/u.test(line)) inGateSection = false;
97
+ if (!inGateSection) continue;
98
+ const bullet = line.match(/^\s*[-*]\s+(.+)$/u);
99
+ if (bullet) verdicts.push({ source: file, text: bullet[1]!.trim() });
100
+ }
101
+ }
102
+ if (verdicts.length === 0) throw new Error(`seat verdict not found for ${seat}`);
103
+ contents.set('verdicts.json', Buffer.from(`${JSON.stringify(verdicts, null, 2)}\n`));
104
+ contents.set('budget.json', Buffer.from(`${JSON.stringify({ dailyGoals: DEFAULT_DAILY_GOALS, concurrentPods: DEFAULT_CONCURRENT_PODS }, null, 2)}\n`));
105
+ const version = (JSON.parse(readFileSync(join(repo, 'package.json'), 'utf8')) as { version: string }).version;
106
+ const files = [...contents].sort(([a], [b]) => a.localeCompare(b)).map(([path, body]) => ({
107
+ path, sha256: createHash('sha256').update(body).digest('hex'),
108
+ }));
109
+ const manifest: SeatPackManifest = { name: seat, version, files };
110
+ const manifestBody = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
111
+ for (const [file, body] of [...contents, ['manifest.json', manifestBody] as [string, Buffer]]) scan(file, body.toString('utf8'));
112
+ if (existsSync(output)) {
113
+ if (!lstatSync(output).isDirectory() || readdirSync(output).length > 0) throw new Error(`output must be an empty directory: ${output}`);
114
+ }
115
+ const sources = [roles, join(repo, 'graphs')];
116
+ if (output === repo || sources.some((source) => output === source || source.startsWith(`${output}${sep}`) || output.startsWith(`${source}${sep}`))) {
117
+ throw new Error('output overlaps seat pack sources');
118
+ }
119
+ mkdirSync(output, { recursive: true });
120
+ for (const [file, body] of contents) {
121
+ const dest = join(output, file);
122
+ mkdirSync(dirname(dest), { recursive: true });
123
+ writeFileSync(dest, body);
124
+ }
125
+ writeFileSync(join(output, 'manifest.json'), manifestBody);
126
+ return manifest;
127
+ }
@@ -158,6 +158,8 @@ export interface AskLaunchFlowDeps {
158
158
  /** ⛔ 못 얻으면 `null` — 같은 이유. */
159
159
  recentAuthoringSamples(): RecentAuthoringSample[] | null;
160
160
  authorGoal(args: readonly string[], options: Record<string, unknown>): Promise<AskAuthoredGoal>;
161
+ /** Pod launches reserve a host slot before goal authoring begins. */
162
+ beforeAuthoring?(): Promise<void>;
161
163
  /** 발사 전 권고용 분해 seam. 생략하면 전용 관측명을 준 기존 분해기를 쓴다. */
162
164
  decomposeGoal?(goal: string): Promise<SelfDevDecomposition>;
163
165
  /**
@@ -825,6 +827,7 @@ export async function runAskLaunchFlow(
825
827
  const launchPreflight = await runPrePreflight(input, deps);
826
828
  if (launchPreflight === false) return { kind: 'stopped-before-authoring' };
827
829
 
830
+ await deps.beforeAuthoring?.();
828
831
  const authorAndPreflight = async (): Promise<{
829
832
  readonly goalFile: string;
830
833
  readonly decision: ReturnType<typeof decideAskPreflight>;
@@ -1,8 +1,9 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { NEXT_MD_PATH, resolveNextMdConflict } from '../release-loop/next-md-merge.js';
6
+ import { getDefaultLogStore } from '../mss/logging/log-store.js';
6
7
 
7
8
  type Command = (bin: string, args: readonly string[], cwd: string) => { status: number | null; stdout: string; stderr: string };
8
9
  export type L8MergeQueueInput = { number: number; cwd: string; matchHeadCommit: string };
@@ -13,6 +14,161 @@ export type L8MergeQueueDeps = {
13
14
  acquire?: (cwd: string) => Promise<() => void>;
14
15
  };
15
16
 
17
+ export type L8ShadowVerdict = { number: number; head: string; base?: string; verdict: 'pass' | 'conflict' | 'fail'; detail?: string; tests: string[]; at: string };
18
+ export type L8ShadowQueueState = { pending: { number: number; head: string }[]; verdicts: L8ShadowVerdict[] };
19
+ export type L8ShadowQueueDeps = L8MergeQueueDeps & { observe?: (verdict: L8ShadowVerdict) => void | Promise<void> };
20
+
21
+ function shadowCommand(execute: Command, bin: string, args: string[], cwd: string): string {
22
+ const result = execute(bin, args, cwd);
23
+ if (result.status !== 0) throw new Error(`${bin} ${args.join(' ')}: ${(result.stderr || result.stdout || `exit ${result.status}`).trim().slice(0, 500)}`);
24
+ return args.includes('-z') ? result.stdout : result.stdout.trim();
25
+ }
26
+
27
+ function shadowQueuePath(cwd: string, execute: Command): string {
28
+ const dir = shadowCommand(execute, 'git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);
29
+ if (!dir) throw new Error('merge queue common git directory unavailable');
30
+ return join(dir, 'elanous-l8-shadow-queue.json');
31
+ }
32
+
33
+ function readShadowQueue(path: string): L8ShadowQueueState {
34
+ if (!existsSync(path)) return { pending: [], verdicts: [] };
35
+ const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
36
+ if (!value || typeof value !== 'object' || !('pending' in value) || !Array.isArray(value.pending)
37
+ || !value.pending.every((p: { number?: number; head?: string }) => Number.isSafeInteger(p?.number) && p.number! > 0 && typeof p.head === 'string' && /^[0-9a-f]{40}$/i.test(p.head))
38
+ || !('verdicts' in value) || !Array.isArray(value.verdicts)) throw new Error('invalid shadow queue ledger');
39
+ return value as L8ShadowQueueState;
40
+ }
41
+
42
+ function writeShadowQueue(path: string, state: L8ShadowQueueState): void {
43
+ const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
44
+ try {
45
+ writeFileSync(temp, JSON.stringify(state, null, 2) + '\n', { flag: 'wx' });
46
+ renameSync(temp, path);
47
+ } finally { rmSync(temp, { force: true }); }
48
+ }
49
+
50
+ async function withShadowQueue<T>(cwd: string, deps: L8ShadowQueueDeps, fn: (state: L8ShadowQueueState, path: string) => Promise<T>): Promise<T> {
51
+ const release = await (deps.acquire ?? acquireL8MergeQueue)(cwd);
52
+ try {
53
+ const path = shadowQueuePath(cwd, deps.command ?? command);
54
+ return await fn(readShadowQueue(path), path);
55
+ } finally { release(); }
56
+ }
57
+
58
+ export function shadowPrNumber(raw: string): number {
59
+ const text = raw.replace(/^#/, '');
60
+ const number = Number(text);
61
+ if (!/^[1-9]\d*$/.test(text) || !Number.isSafeInteger(number)) throw new Error('expected a positive PR number');
62
+ return number;
63
+ }
64
+
65
+ export async function statusL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowQueueState> {
66
+ return withShadowQueue(cwd, deps, async (state) => state);
67
+ }
68
+
69
+ export async function enqueueL8ShadowQueue(cwd: string, number: number, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowQueueState> {
70
+ if (!Number.isSafeInteger(number) || number <= 0) throw new Error('expected a positive PR number');
71
+ return withShadowQueue(cwd, deps, async (state, path) => {
72
+ const execute = deps.command ?? command;
73
+ const view = JSON.parse(shadowCommand(execute, 'gh', ['pr', 'view', String(number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'], cwd)) as {
74
+ headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
75
+ };
76
+ if (!view.headRefOid || !/^[0-9a-f]{40}$/i.test(view.headRefOid) || view.baseRefName !== 'main'
77
+ || view.state !== 'OPEN' || view.isDraft !== false || view.isCrossRepository !== false) throw new Error('PR is not an open, ready, same-repository main PR');
78
+ if (state.pending.some((p) => p.number === number)) throw new Error(`PR #${number} already queued`);
79
+ state.pending.push({ number, head: view.headRefOid });
80
+ writeShadowQueue(path, state);
81
+ return state;
82
+ });
83
+ }
84
+
85
+ /** Evaluate one pinned queue head against freshly fetched main. Never push or call a merge API. */
86
+ export async function runL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowVerdict | null> {
87
+ return withShadowQueue(cwd, deps, async (state, path) => {
88
+ const item = state.pending[0];
89
+ if (!item) return null;
90
+ const execute = deps.command ?? command;
91
+ const run = (bin: string, args: string[], where = cwd) => shadowCommand(execute, bin, args, where);
92
+ const verdict: L8ShadowVerdict = { number: item.number, head: item.head, verdict: 'fail', tests: [], at: new Date().toISOString() };
93
+ const view = JSON.parse(run('gh', ['pr', 'view', String(item.number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'])) as {
94
+ headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
95
+ };
96
+ if (view.headRefOid !== item.head || view.baseRefName !== 'main' || view.state !== 'OPEN'
97
+ || view.isDraft !== false || view.isCrossRepository !== false) throw new Error('queued PR changed or is not ready');
98
+ run('git', ['fetch', 'origin', 'refs/heads/main']);
99
+ const base = run('git', ['rev-parse', 'FETCH_HEAD']);
100
+ if (!/^[0-9a-f]{40}$/i.test(base)) throw new Error('main commit unavailable');
101
+ verdict.base = base;
102
+ run('git', ['fetch', 'origin', `refs/pull/${item.number}/head`]);
103
+ if (run('git', ['rev-parse', 'FETCH_HEAD']) !== item.head) throw new Error('fetched PR head changed');
104
+ let temp: string | undefined;
105
+ let attached = false;
106
+ try {
107
+ temp = mkdtempSync(join(tmpdir(), 'elanous-l8-shadow-'));
108
+ run('git', ['worktree', 'add', '--detach', temp, base]);
109
+ attached = true;
110
+ const fork = run('git', ['merge-base', base, item.head], temp);
111
+ const changed = run('git', ['diff', '--name-only', '-z', fork, item.head], temp).split('\0').filter(Boolean);
112
+ if (!changed.length) throw new Error('PR changed-file paths unavailable');
113
+ const diff = run('git', ['diff', '--name-only', '-z', '--diff-filter=ACMRT', fork, item.head], temp);
114
+ const files = diff.split('\0').filter(Boolean);
115
+ if (changed.some((file) => file.startsWith('-') || file.startsWith('/') || file.split('/').includes('..') || file.includes('\\'))) throw new Error('unsafe changed-file path');
116
+ verdict.tests = files.filter((file) => /(?:^|\/)[^/]+\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/.test(file));
117
+ const merge = execute('git', ['-c', 'user.name=elanous shadow', '-c', 'user.email=shadow@localhost', 'merge', '--no-ff', '--no-edit', item.head], temp);
118
+ if (merge.status !== 0) {
119
+ const unresolved = run('git', ['diff', '--name-only', '--diff-filter=U'], temp);
120
+ if (!unresolved) throw new Error(`git merge failed: ${merge.stderr.trim() || `exit ${merge.status}`}`);
121
+ verdict.verdict = 'conflict';
122
+ verdict.detail = unresolved;
123
+ } else {
124
+ if (verdict.tests.length) {
125
+ if (verdict.tests.some((file) => !existsSync(join(temp!, file)))) throw new Error('changed test path unavailable');
126
+ run('bun', ['install', '--frozen-lockfile'], temp);
127
+ const result = execute('bun', ['run', 'test:deterministic', ...verdict.tests], temp);
128
+ const output = `${result.stdout}\n${result.stderr}`;
129
+ const counts = /\b(\d+) pass\b[\s\S]*?\b(\d+) fail\b[\s\S]*?Ran ([1-9]\d*) tests? across ([1-9]\d*) files?/.exec(output);
130
+ if (!counts || Number(counts[1]) + Number(counts[2]) !== Number(counts[3])
131
+ || Number(counts[4]) !== verdict.tests.length || (result.status !== 0 && Number(counts[2]) === 0)) {
132
+ throw new Error(`changed tests unmeasured: ${output.slice(-400)}`);
133
+ }
134
+ if (Number(counts[2]) > 0 || Number(counts[1]) === 0) {
135
+ verdict.detail = `changed tests failed: ${output.slice(-400)}`;
136
+ } else {
137
+ verdict.verdict = 'pass';
138
+ }
139
+ } else {
140
+ verdict.verdict = 'pass';
141
+ }
142
+ }
143
+ } finally {
144
+ try {
145
+ if (attached && temp) run('git', ['worktree', 'remove', '--force', temp]);
146
+ } finally {
147
+ if (temp) rmSync(temp, { recursive: true, force: true });
148
+ }
149
+ }
150
+ if (verdict.verdict === 'pass') {
151
+ run('git', ['fetch', 'origin', 'refs/heads/main']);
152
+ if (run('git', ['rev-parse', 'FETCH_HEAD']) !== verdict.base) throw new Error('main advanced during shadow verification');
153
+ const current = JSON.parse(run('gh', ['pr', 'view', String(item.number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'])) as {
154
+ headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
155
+ };
156
+ if (current.headRefOid !== item.head || current.baseRefName !== 'main' || current.state !== 'OPEN'
157
+ || current.isDraft !== false || current.isCrossRepository !== false) throw new Error('PR changed during shadow verification');
158
+ }
159
+ // A rejected insert leaves the pinned item queued for a later evaluation.
160
+ await (deps.observe ?? ((entry) => {
161
+ const store = getDefaultLogStore();
162
+ if (!store) throw new Error('merge-queue observation store unavailable');
163
+ store.insertBatch([{ rec: { ts: new Date().toISOString(), category: 'merge-queue', event: 'shadow-verdict', data: entry }, surface: 'merge-queue' }]);
164
+ }))(verdict);
165
+ state.pending.shift();
166
+ state.verdicts.push(verdict);
167
+ writeShadowQueue(path, state);
168
+ return verdict;
169
+ });
170
+ }
171
+
16
172
  const command: Command = (bin, args, cwd) => {
17
173
  const r = spawnSync(bin, [...args], { cwd, encoding: 'utf8', timeout: 600_000, maxBuffer: 32 * 1024 * 1024 });
18
174
  return { status: r.status, stdout: r.stdout ?? '', stderr: (r.stderr ?? '') + (r.error?.message ?? '') };
@@ -7,6 +7,7 @@ import { redactSecrets } from '../task-cards/card-store.js';
7
7
  import { preLaunchGate, type PreLaunchGateDecision } from '../execution-loop/launch-gate.js';
8
8
  import type { BudgetDecision } from '../self-implement/budget-gate.js';
9
9
  import type { ScheduledDecision, StewardSettings, TriageIssue } from './triage.js';
10
+ import { launchModeOf, rescueAllows } from './rescue.js';
10
11
 
11
12
  /** `completed` = the run finished without a merge (a non-code artifact, or nothing to deliver) — never reported as a failure. */
12
13
  export type LaunchStatus = 'shadow' | 'launched' | 'skipped-budget' | 'blocked-duplicate' | 'blocked-location' | 'failed' | 'running' | 'merged' | 'completed';
@@ -50,7 +51,7 @@ const RUN_ID = /\brun-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{1
50
51
  const DEFAULT_POOL = 'pool-node-b@node-b:8';
51
52
  // `harness budget --json` outcomes: proceed | next-provider | wait-reset | stop. next-provider still runs (the harness switches provider itself).
52
53
  const BUDGET_PROCEED: readonly string[] = ['proceed', 'next-provider'];
53
- const launchMode = (settings: StewardSettings): 'off' | 'shadow' | 'live' => settings.mode ?? settings.launch ?? 'shadow';
54
+ const launchMode = (settings: StewardSettings): 'off' | 'shadow' | 'live' => launchModeOf(settings.mode ?? settings.launch ?? 'shadow');
54
55
  const cli = (args: string[]) => ['bun', 'bin/elanous.mjs', ...(process.env.NODE_ENV === 'test' ? ['--test'] : []), ...args];
55
56
  const sourceOf = (issue: TriageIssue): string => /^출처: (telegram|pwa|tui|cli)$/m.exec(issue.body)?.[1] ?? `linear:${issue.identifier}`;
56
57
  const promptOf = (issue: TriageIssue): string => `${issue.title}\n${issue.body.slice(0, 1500)}`;
@@ -125,6 +126,7 @@ export function launch(item: LaunchItem, deps: LaunchDeps): LaunchEntry {
125
126
  const { ledger, settings, root } = deps;
126
127
  if (item.row.rung !== 4 || item.row.hitlReason || item.row.disposition !== 'now' || (ledger.launches[item.issue.identifier] && !['skipped-budget', 'blocked-duplicate', 'blocked-location'].includes(ledger.launches[item.issue.identifier].status) && !(launchMode(settings) === 'live' && ledger.launches[item.issue.identifier].status === 'shadow'))) throw new Error('steward launch is not eligible');
127
128
  if (launchMode(settings) === 'off') throw new Error('steward launch is off');
129
+ if (!rescueAllows(settings.mode, 'launch', { issue: item.issue.identifier })) throw new Error('steward rescue refused launch');
128
130
  const key = item.issue.identifier;
129
131
  const entry: LaunchEntry = { issue: key, title: redactSecrets(item.issue.title), source: item.source, command: redactSecrets(item.command), status: 'shadow' };
130
132
  if (launchMode(settings) !== 'live') {
@@ -187,6 +189,7 @@ export function launch(item: LaunchItem, deps: LaunchDeps): LaunchEntry {
187
189
  export function raiseHitl(issue: TriageIssue, row: ScheduledDecision, deps: LaunchDeps): HitlEntry {
188
190
  const old = deps.ledger.hitl[issue.identifier];
189
191
  if (old && (old.attempted || launchMode(deps.settings) !== 'live')) return old;
192
+ if (!rescueAllows(deps.settings.mode, 'decision-card', { issue: issue.identifier })) return old ?? { issue: issue.identifier, reason: redactSecrets(row.hitlReason ?? row.why), raised: false };
190
193
  const entry: HitlEntry = old ?? { issue: issue.identifier, reason: redactSecrets(row.hitlReason ?? row.why), raised: false };
191
194
  deps.ledger.hitl[issue.identifier] = entry;
192
195
  if (launchMode(deps.settings) !== 'live') {
@@ -215,6 +218,7 @@ function alive(pid: number | undefined): boolean {
215
218
 
216
219
  export function collectOutcomes(ledger: LaunchLedger, deps: LaunchDeps): LaunchEntry[] {
217
220
  if (launchMode(deps.settings) !== 'live') return [];
221
+ if (!rescueAllows(deps.settings.mode, 'harvest')) return [];
218
222
  const done: LaunchEntry[] = [];
219
223
  for (const entry of Object.values(ledger.launches)) {
220
224
  if (entry.reported || entry.status === 'shadow' || entry.status === 'skipped-budget' || entry.status === 'blocked-duplicate' || entry.status === 'blocked-location') continue;