elanous 0.2.21 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/1307-08b716476f1d50af.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{2489-ff55a35b202c0f94.js → 2489-d238d3e2eb5b2bf5.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/3857-5985236d9307f676.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/5633-d6852385000738ef.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/5977-5b9e8a45a5b0c823.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/8857-aa1cc4ac90af4d40.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/9247-55bcfbb0e8057f59.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/approvals/page-88459e0903da2803.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-d781be3e3a9cfd74.js → page-d85c43f46f58716b.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/board/{page-b0a80997903852d8.js → page-4e645600579ae952.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-2b1263165630d9c8.js → page-87edbfd19117823b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/bots/page-7f0c2b7801ac6a54.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/ceo/page-bf1f030a9d1adffd.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-e2d6bed48ac20120.js → page-cbaf66b1e1804adf.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-111cabe4d1fd69ae.js → page-80ed511e02c81412.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/{page-544dd4bd4847eeb7.js → page-9f5610caaa06178c.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-3ee191319bf97d87.js → page-ec8f3575e339de74.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/design-check/page-86d753e460f1a989.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/editor/page-4166a7d51cf0a57d.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/exec/{page-a449f1ed472b81c2.js → page-2a19c5cd3b562580.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/field/{page-9045094d6d3b5cd0.js → page-b6a09cde2e07521a.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/inside/{page-c06673596849ef45.js → page-f7eae1518ca0c353.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/intake/page-456a8573ad811381.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/{layout-9f81233f29dcd409.js → layout-8462d6a543cbb2d9.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/live/page-2974cdfb87c0f9c9.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/live-run/page-f927ffa25f3c86d1.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/loops/page-abd86cd832aaa011.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/market/page-79daf68f6256adaa.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8c3f168fc3f91939.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/morning/{page-7a12f61be4a579d1.js → page-786fb4504d0f2fa9.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/{not-found-c85b0589591a99c9.js → not-found-75209f14597dcb8f.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-725a4da02dd129eb.js → page-bf30affe3a85af7e.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1187a44b7515434b.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-3d0910185a66ed54.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-f401ee1dd5a26e25.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/outputs/page-5dfe7604fd842784.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/{page-21bec4afeb2f0917.js → page-76cf838050a8d681.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-dc39ee7a36d26385.js → page-7ec8e289b4c14df6.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-84ae759f76053672.js → page-8c8ac17d52be2a79.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-3db58ddb61fe2be9.js → page-d359ab048d63a9d9.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-c5315e30f3528dc4.js → page-b8b345c880bf78d1.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/settings/{page-2fe368efb6de3b6a.js → page-f151cc10ad026940.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-9875f103057da433.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/setup/page-9789328af9a9aa62.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/share/{page-c69762987b751174.js → page-bf495a1e284f4e00.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a263d94bca4be22a.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/tasks/page-3962eab8ec58d16e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/term/page-9abcfac07e02a542.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/today/page-bee49f927c333abb.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/trace/page-bac2d56dcb02d955.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/vault/page-937cbcc13176295c.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d09495594f7f480d.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-7f8d7fcdf058a845.js → page-69cb93d1a2e57e0e.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-22163556cd38bc88.js → page-72ec543928d67c3e.js} +1 -1
  57. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-82906eeda0186a7b.js +1 -0
  58. package/apps/pwa/out/_next/static/css/f2bd5257e6af88d8.css +1 -0
  59. package/apps/pwa/out/approvals/index.html +1 -1
  60. package/apps/pwa/out/approvals/index.txt +14 -14
  61. package/apps/pwa/out/autopilot/index.html +1 -1
  62. package/apps/pwa/out/autopilot/index.txt +14 -14
  63. package/apps/pwa/out/board/index.html +1 -1
  64. package/apps/pwa/out/board/index.txt +14 -14
  65. package/apps/pwa/out/botlab/index.html +1 -1
  66. package/apps/pwa/out/botlab/index.txt +14 -14
  67. package/apps/pwa/out/bots/index.html +1 -1
  68. package/apps/pwa/out/bots/index.txt +14 -14
  69. package/apps/pwa/out/ceo/index.html +1 -1
  70. package/apps/pwa/out/ceo/index.txt +14 -14
  71. package/apps/pwa/out/chat/index.html +1 -1
  72. package/apps/pwa/out/chat/index.txt +14 -14
  73. package/apps/pwa/out/consult/index.html +1 -1
  74. package/apps/pwa/out/consult/index.txt +14 -14
  75. package/apps/pwa/out/control/index.html +1 -1
  76. package/apps/pwa/out/control/index.txt +14 -14
  77. package/apps/pwa/out/decisions/index.html +1 -1
  78. package/apps/pwa/out/decisions/index.txt +14 -14
  79. package/apps/pwa/out/design-check/index.html +1 -1
  80. package/apps/pwa/out/design-check/index.txt +14 -14
  81. package/apps/pwa/out/editor/index.html +1 -1
  82. package/apps/pwa/out/editor/index.txt +14 -14
  83. package/apps/pwa/out/exec/index.html +1 -1
  84. package/apps/pwa/out/exec/index.txt +14 -14
  85. package/apps/pwa/out/field/index.html +1 -1
  86. package/apps/pwa/out/field/index.txt +14 -14
  87. package/apps/pwa/out/healthz +1 -1
  88. package/apps/pwa/out/index.html +1 -1
  89. package/apps/pwa/out/index.txt +14 -14
  90. package/apps/pwa/out/inside/index.html +1 -1
  91. package/apps/pwa/out/inside/index.txt +14 -14
  92. package/apps/pwa/out/intake/index.html +1 -1
  93. package/apps/pwa/out/intake/index.txt +14 -14
  94. package/apps/pwa/out/live/index.html +1 -1
  95. package/apps/pwa/out/live/index.txt +14 -14
  96. package/apps/pwa/out/live-run/index.html +1 -1
  97. package/apps/pwa/out/live-run/index.txt +14 -14
  98. package/apps/pwa/out/loops/index.html +1 -1
  99. package/apps/pwa/out/loops/index.txt +14 -14
  100. package/apps/pwa/out/market/index.html +1 -1
  101. package/apps/pwa/out/market/index.txt +14 -14
  102. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  103. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  104. package/apps/pwa/out/morning/index.html +1 -1
  105. package/apps/pwa/out/morning/index.txt +14 -14
  106. package/apps/pwa/out/observatory/index.html +1 -1
  107. package/apps/pwa/out/observatory/index.txt +14 -14
  108. package/apps/pwa/out/ops/checklist/index.html +1 -1
  109. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  110. package/apps/pwa/out/ops/release/index.html +1 -1
  111. package/apps/pwa/out/ops/release/index.txt +14 -14
  112. package/apps/pwa/out/ops/seats/index.html +1 -1
  113. package/apps/pwa/out/ops/seats/index.txt +14 -14
  114. package/apps/pwa/out/outputs/index.html +1 -1
  115. package/apps/pwa/out/outputs/index.txt +14 -14
  116. package/apps/pwa/out/reflection/index.html +1 -1
  117. package/apps/pwa/out/reflection/index.txt +15 -15
  118. package/apps/pwa/out/scheduler/index.html +1 -1
  119. package/apps/pwa/out/scheduler/index.txt +14 -14
  120. package/apps/pwa/out/sessions/index.html +1 -1
  121. package/apps/pwa/out/sessions/index.txt +14 -14
  122. package/apps/pwa/out/settings/devices/index.html +1 -1
  123. package/apps/pwa/out/settings/devices/index.txt +14 -14
  124. package/apps/pwa/out/settings/index.html +1 -1
  125. package/apps/pwa/out/settings/index.txt +14 -14
  126. package/apps/pwa/out/setup/done/index.html +1 -1
  127. package/apps/pwa/out/setup/done/index.txt +14 -14
  128. package/apps/pwa/out/setup/index.html +1 -1
  129. package/apps/pwa/out/setup/index.txt +14 -14
  130. package/apps/pwa/out/share/index.html +1 -1
  131. package/apps/pwa/out/share/index.txt +14 -14
  132. package/apps/pwa/out/showroom/index.html +1 -1
  133. package/apps/pwa/out/showroom/index.txt +14 -14
  134. package/apps/pwa/out/tasks/index.html +1 -1
  135. package/apps/pwa/out/tasks/index.txt +14 -14
  136. package/apps/pwa/out/term/index.html +1 -1
  137. package/apps/pwa/out/term/index.txt +14 -14
  138. package/apps/pwa/out/today/index.html +1 -1
  139. package/apps/pwa/out/today/index.txt +14 -14
  140. package/apps/pwa/out/trace/index.html +1 -1
  141. package/apps/pwa/out/trace/index.txt +14 -14
  142. package/apps/pwa/out/vault/index.html +1 -1
  143. package/apps/pwa/out/vault/index.txt +14 -14
  144. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  145. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  146. package/apps/pwa/out/workflows/index.html +1 -1
  147. package/apps/pwa/out/workflows/index.txt +14 -14
  148. package/apps/pwa/out/workspace/index.html +1 -1
  149. package/apps/pwa/out/workspace/index.txt +14 -14
  150. package/apps/pwa/out/worktrees/index.html +1 -1
  151. package/apps/pwa/out/worktrees/index.txt +14 -14
  152. package/graphs/catalog/catalog.yaml +1 -1
  153. package/graphs/drafts/recipes.yaml +2 -2
  154. package/graphs/test-diet/recipes.yaml +3 -0
  155. package/graphs/test-diet/test-diet.yaml +3 -1
  156. package/package.json +1 -1
  157. package/scripts/f-session-monitor.ts +69 -0
  158. package/scripts/hq/standby-snapshot.ts +7 -3
  159. package/scripts/knowledge-compress.ts +11 -0
  160. package/scripts/nightly-docops.ts +90 -3
  161. package/scripts/research-coo-admin-dates.ts +39 -0
  162. package/scripts/skills/public-check.ts +7 -2
  163. package/scripts/smart-merge/resync-shadow.ts +5 -3
  164. package/scripts/test-diet/lib.ts +81 -1
  165. package/scripts/test-diet/node.ts +48 -2
  166. package/scripts/verify-coo-admin-evidence.ts +83 -0
  167. package/src/acp/client.ts +4 -2
  168. package/src/agent/run-context.ts +4 -0
  169. package/src/agent-env/profile.ts +25 -5
  170. package/src/agent-mission/driver.ts +26 -12
  171. package/src/agent-mission/supervisor-metrics.ts +17 -2
  172. package/src/agent-substrate/acp-reviewer.ts +12 -5
  173. package/src/agent-substrate/pr-reviewer.ts +11 -4
  174. package/src/agent-substrate/review-intent.ts +102 -3
  175. package/src/autopilot/build/llm-conflict-merge.ts +22 -8
  176. package/src/autopilot/pr-manager.ts +18 -8
  177. package/src/away/release-watch.ts +47 -8
  178. package/src/briefing/brief-items.ts +104 -11
  179. package/src/chat/index.ts +1 -0
  180. package/src/cli/brief-cli.ts +20 -2
  181. package/src/cli/config-classify.ts +83 -0
  182. package/src/cli/control-cli.ts +14 -0
  183. package/src/cli/decisions-cli.ts +26 -16
  184. package/src/cli/logs-model-input.ts +1 -1
  185. package/src/cli/pr-cli.ts +65 -8
  186. package/src/cli/release-cli.ts +21 -4
  187. package/src/cli/tasks-cli.ts +108 -1
  188. package/src/connectors/types.ts +6 -6
  189. package/src/context-bus/context-now-surfaces.ts +18 -0
  190. package/src/context-bus/coord-events.ts +32 -4
  191. package/src/context-bus/long-term-memory.ts +23 -6
  192. package/src/context-bus/nightly-condense.ts +169 -21
  193. package/src/control/control-status.ts +87 -0
  194. package/src/control-plane/light-member.ts +5 -3
  195. package/src/control-plane/member.ts +172 -7
  196. package/src/core-turn/run-goal-loop.ts +22 -1
  197. package/src/dashboard/slash-runtime/dashboard-handlers.ts +9 -0
  198. package/src/decisions/decision-cards.ts +20 -1
  199. package/src/decisions/decision-ledger.ts +11 -8
  200. package/src/decisions/web-push-decision-cards.ts +19 -3
  201. package/src/discord-self-message.ts +2 -2
  202. package/src/domains/coo-admin-dates.evidence.json +876 -0
  203. package/src/domains/coo-admin-tool.ts +48 -14
  204. package/src/domains/ops-status.ts +53 -0
  205. package/src/domains/outbound-alert.ts +24 -1
  206. package/src/exec-requests/planner.ts +6 -0
  207. package/src/graph-kinds/registry.ts +6 -6
  208. package/src/graph-runner/graph-cli.ts +34 -2
  209. package/src/graph-runner/graph-run-api.ts +7 -0
  210. package/src/graph-runner/graph-tick.ts +73 -12
  211. package/src/graph-runner/runner.ts +43 -11
  212. package/src/graph-unify/wf-to-graph.ts +79 -0
  213. package/src/graph-wizard/generate.ts +34 -2
  214. package/src/graph-wizard/steps.ts +13 -0
  215. package/src/harness/harness-cli-command.ts +121 -23
  216. package/src/harness/harness-queue.ts +58 -14
  217. package/src/harness/launch-advice.ts +23 -0
  218. package/src/index.ts +36 -7
  219. package/src/knowledge/conclusion-compress.ts +93 -0
  220. package/src/knowledge/know-find.ts +9 -2
  221. package/src/knowledge/query.ts +20 -0
  222. package/src/llm.ts +26 -16
  223. package/src/loops/neighbor-runtime.ts +88 -0
  224. package/src/loops/orchestrator/author-depth.ts +29 -11
  225. package/src/loops/orchestrator/overlap.ts +7 -1
  226. package/src/loops/orchestrator/resync-shadow.ts +1 -1
  227. package/src/loops/orchestrator/tick.ts +35 -2
  228. package/src/loops/orchestrator/traffic.ts +2 -0
  229. package/src/loops/registry.ts +5 -0
  230. package/src/market/publish.ts +17 -1
  231. package/src/maturity/feature-maturity.ts +2 -1
  232. package/src/mss/logging/redaction.ts +17 -0
  233. package/src/nexus/api/draft-metrics.ts +22 -8
  234. package/src/nexus/api/graph-peer-edit.ts +77 -0
  235. package/src/nexus/api/graph-wizard-api.ts +9 -1
  236. package/src/nexus/api/graphs-api.ts +145 -7
  237. package/src/nexus/api/http-server.ts +93 -8
  238. package/src/nexus/api/plugins-market.ts +24 -0
  239. package/src/nexus/index.ts +3 -2
  240. package/src/oauth/claude-pty-maturity.ts +24 -0
  241. package/src/oauth/fallback-chain.ts +27 -14
  242. package/src/plugins/maker/market-bundle.ts +9 -0
  243. package/src/plugins/maker/plugin-maker.ts +9 -3
  244. package/src/plugins/maker/wizard-generate.ts +73 -9
  245. package/src/policy/llm-policy.ts +11 -6
  246. package/src/release-loop/checklist-board.ts +27 -0
  247. package/src/release-loop/checklist.ts +38 -0
  248. package/src/seat-address/seat-address.ts +1 -0
  249. package/src/seat-loop/seat-loop.ts +174 -20
  250. package/src/seat-loop/seat-report.ts +29 -1
  251. package/src/self-dev/dev-cli.ts +90 -8
  252. package/src/self-dev/dev-pipeline.ts +20 -1
  253. package/src/self-dev/draft-sweep.ts +45 -8
  254. package/src/self-dev/orchestrate.ts +3 -0
  255. package/src/self-dev/review-provider-fallback.ts +83 -0
  256. package/src/self-dev/run-supervisor.ts +5 -1
  257. package/src/self-implement/harness-policy-gates.ts +7 -5
  258. package/src/self-implement/host-regate.ts +28 -14
  259. package/src/self-implement/l8-merge-queue.ts +80 -8
  260. package/src/self-implement/orchestrator.ts +68 -27
  261. package/src/self-implement/seams.ts +5 -3
  262. package/src/self-implement/self-implement-cli.ts +4 -0
  263. package/src/shell-env-bootstrap.ts +19 -4
  264. package/src/steward/triage.ts +25 -8
  265. package/src/storage/file-lock.ts +33 -4
  266. package/src/task-agent/completion-evidence.ts +31 -3
  267. package/src/task-agent/cover.ts +61 -0
  268. package/src/task-agent/live-moves.ts +126 -3
  269. package/src/task-agent/shadow.ts +19 -4
  270. package/src/task-agent/task-hand.ts +2 -0
  271. package/src/task-orchestrator/surfaces/pod-pool.ts +138 -15
  272. package/src/task-orchestrator/surfaces/self-implement.ts +7 -0
  273. package/src/telegram-commands.ts +8 -0
  274. package/src/telegram-project-command.ts +6 -0
  275. package/src/telegram.ts +2 -2
  276. package/src/typecheck-ratchet.ts +2 -1
  277. package/src/user-config.ts +35 -4
  278. package/src/version/packed-revision.json +1 -1
  279. package/src/workflow-runtime/executor.ts +24 -2
  280. package/src/workflow-runtime/triggers/registry.ts +27 -0
  281. package/src/workflow-runtime/triggers/schedule-source.ts +30 -3
  282. package/apps/pwa/out/_next/static/chunks/1307-c97eba9fb72a9a5e.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/5977-59fedea50d13c762.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/8857-f1762140ef9a96b1.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/9247-7581825d511a64c0.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/9497-b0e758d1cd4ae8d7.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/approvals/page-937c92292a4ab4f0.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/bots/page-ea998f7f5d156bea.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/ceo/page-33c328b64ca508f8.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e4bcb2c503dab04b.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/editor/page-300897dcbbd5ed76.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/intake/page-103fa3191c1b2e08.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/live/page-be8b4615c919e36c.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/live-run/page-c685b104e0dfa609.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/loops/page-8d693331cfbb6add.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/market/page-cef4f30a4c22fae1.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66ea1b54fc83fa15.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-8cc707bd09a497d9.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-90b55be1f15ce0e8.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cb16606e08be65db.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/outputs/page-16dc64b74ed6a40a.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-8d04132dd0ac5e10.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/setup/page-f23484de467c9ea3.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/app/showroom/page-5456692bda39aafb.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d75f7cd65845ce98.js +0 -1
  307. package/apps/pwa/out/_next/static/chunks/app/term/page-5603f5e568b25ce3.js +0 -1
  308. package/apps/pwa/out/_next/static/chunks/app/today/page-d2cc0f4e89f42028.js +0 -1
  309. package/apps/pwa/out/_next/static/chunks/app/trace/page-a6eb73b8a4840f1c.js +0 -1
  310. package/apps/pwa/out/_next/static/chunks/app/vault/page-c0a3f4d10edb83fa.js +0 -1
  311. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6c0b88068c143f15.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-141436fd26dd56fc.js +0 -1
  313. package/apps/pwa/out/_next/static/css/e81df11490b4413b.css +0 -1
  314. /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_buildManifest.js +0 -0
  315. /package/apps/pwa/out/_next/static/{MObU8ujkMciAKr6ljAV05 → aBi6EyMtLsrl7J8eH4kF7}/_ssgManifest.js +0 -0
@@ -0,0 +1,88 @@
1
+ import { debug } from '../debug/log.js';
2
+ import { openMsgStore } from '../msg/msg-store.js';
3
+ import { judgeNeighbor, type Neighbor } from './neighbors.js';
4
+ import { join } from 'node:path';
5
+
6
+ export type LoopHealth = 'healthy' | 'degraded';
7
+ export type NeighborIncident = { observer: string; neighbor: string; state: 'absent'; reason: string; action: Neighbor['onAbsent']['action']; to: string; at: string; lastSeenAt: string | null };
8
+
9
+ export const SEAT_NEIGHBORS: Record<string, Neighbor[]> = Object.fromEntries(['OP', 'TC', 'MK', 'UX'].map(seat => [seat,
10
+ [
11
+ ...['OP', 'TC', 'MK', 'UX'].filter(peer => peer !== seat).map(peer => ({
12
+ id: peer === 'MK' ? 'cmo-seat' : `${peer.toLowerCase()}-seat`, exchange: ['status'] as Neighbor['exchange'],
13
+ heartbeat: { everyMinutes: 15, missedTicks: 2 },
14
+ onAbsent: peer === 'OP' ? { action: 'delegate' as const, delegateTo: 'steward' }
15
+ : peer === 'TC' ? { action: 'escalate' as const, delegateTo: 'orchestrator' }
16
+ : { action: 'defer' as const, delegateTo: 'orchestrator' },
17
+ })),
18
+ { id: 'orchestrator', exchange: ['status'], heartbeat: { everyMinutes: 120, missedTicks: 2 },
19
+ onAbsent: { action: 'escalate', delegateTo: 'human' } },
20
+ ],
21
+ ]));
22
+
23
+ type Signal = { at: string; health: LoopHealth; ends_at: string | null };
24
+
25
+ function withStore<T>(root: string, fn: (store: ReturnType<typeof openMsgStore>) => T): T {
26
+ const store = openMsgStore(join(root, 'msg', 'messages.db'));
27
+ try {
28
+ store.db.exec(`CREATE TABLE IF NOT EXISTS loop_signals (loop_id TEXT PRIMARY KEY, at TEXT NOT NULL, health TEXT NOT NULL, ends_at TEXT);
29
+ CREATE TABLE IF NOT EXISTS loop_watches (observer TEXT NOT NULL, neighbor TEXT NOT NULL, first_at TEXT NOT NULL, PRIMARY KEY(observer, neighbor));
30
+ CREATE TABLE IF NOT EXISTS loop_incidents (observer TEXT NOT NULL, neighbor TEXT NOT NULL, signal_at TEXT NOT NULL,
31
+ reason TEXT NOT NULL, action TEXT NOT NULL, recipient TEXT NOT NULL, at TEXT NOT NULL,
32
+ PRIMARY KEY(observer, neighbor, signal_at, reason));`);
33
+ return fn(store);
34
+ } finally { store.close(); }
35
+ }
36
+
37
+ /** Durable signal, independent of chat sessions or the seat work-item ledger. */
38
+ export function emitLoopHeartbeat(root: string, loopId: string, health: LoopHealth, now: Date, endsAt?: string): void {
39
+ if (!/^[a-z][a-z0-9-]*$/.test(loopId) || !Number.isFinite(now.getTime())) throw new Error('invalid loop heartbeat');
40
+ if (endsAt && !Number.isFinite(Date.parse(endsAt))) throw new Error('invalid heartbeat end');
41
+ withStore(root, store => {
42
+ store.db.query(`INSERT INTO loop_signals(loop_id, at, health, ends_at) VALUES(?, ?, ?, ?)
43
+ ON CONFLICT(loop_id) DO UPDATE SET at=excluded.at, health=excluded.health, ends_at=excluded.ends_at
44
+ WHERE loop_signals.at <= excluded.at`).run(loopId, now.toISOString(), health, endsAt ?? null);
45
+ });
46
+ debug.log(`loop.${loopId}`, 'heartbeat', { loopId, at: now.toISOString(), health, endsAt: endsAt ?? null });
47
+ }
48
+
49
+ /** First watch starts the missing-signal clock. Action receipt and message commit atomically. */
50
+ export function checkLoopNeighbors(root: string, observer: string, neighbors: readonly Neighbor[], now: Date): NeighborIncident[] {
51
+ return withStore(root, store => {
52
+ const incidents: NeighborIncident[] = [];
53
+ for (const neighbor of neighbors) {
54
+ if (neighbor.id === observer) continue;
55
+ try {
56
+ store.db.exec('BEGIN IMMEDIATE');
57
+ store.db.query('INSERT OR IGNORE INTO loop_watches(observer, neighbor, first_at) VALUES(?, ?, ?)').run(observer, neighbor.id, now.toISOString());
58
+ const signal = store.db.query('SELECT at, health, ends_at FROM loop_signals WHERE loop_id=?').get(neighbor.id) as Signal | null;
59
+ const watch = store.db.query('SELECT first_at FROM loop_watches WHERE observer=? AND neighbor=?').get(observer, neighbor.id) as { first_at: string };
60
+ const judgment = judgeNeighbor({ neighbor, lastSeenAt: signal?.at ?? watch.first_at, now });
61
+ const expired = !!signal?.ends_at && Date.parse(signal.ends_at) <= now.getTime();
62
+ // A declared lease (ends_at in the future) outranks the heartbeat interval: a scheduled loop idle between
63
+ // windows is not absent until its own declared end passes.
64
+ const leased = !!signal?.ends_at && !expired;
65
+ const absent = expired || signal?.health === 'degraded' || (!leased && judgment.state === 'absent');
66
+ if (!absent) { store.db.exec('COMMIT'); continue; }
67
+ const reason = expired ? 'end-expired' : signal?.health === 'degraded' ? 'degraded' : judgment.reason;
68
+ const signalAt = signal?.at ?? watch.first_at;
69
+ const { action, delegateTo: to } = neighbor.onAbsent;
70
+ const inserted = store.db.query(`INSERT OR IGNORE INTO loop_incidents(observer, neighbor, signal_at, reason, action, recipient, at)
71
+ VALUES(?, ?, ?, ?, ?, ?, ?)`).run(observer, neighbor.id, signalAt, reason, action, to, now.toISOString());
72
+ if (inserted.changes) {
73
+ const incident: NeighborIncident = { observer, neighbor: neighbor.id, state: 'absent', reason, action, to,
74
+ at: now.toISOString(), lastSeenAt: signal?.at ?? null };
75
+ store.append({ from: observer, to, kind: `neighbor-${action}`,
76
+ body: `[${observer} → ${to}] ${neighbor.id} ${reason}: ${action}` });
77
+ incidents.push(incident);
78
+ }
79
+ store.db.exec('COMMIT');
80
+ } catch (error) {
81
+ try { store.db.exec('ROLLBACK'); } catch { /* no open transaction */ }
82
+ debug.log('loop.neighbors', 'check-failed', { observer, neighbor: neighbor.id, error: String(error) }, { level: 'warn' });
83
+ }
84
+ }
85
+ for (const incident of incidents) debug.log('loop.neighbors', 'absent', incident, { level: 'warn' });
86
+ return incidents;
87
+ });
88
+ }
@@ -12,7 +12,7 @@ import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorSeat } from '../
12
12
  import { checkAuthorInput } from './author-input-check.js';
13
13
  import { AuthorLedger } from './author-ledger.js';
14
14
  import { collectWork, findOverlaps, type WorkItem } from './overlap.js';
15
- import { seatOfTree, trafficTick, TRAFFIC_SEATS } from './traffic.js';
15
+ import { isTrafficLaunch, seatOfTree, trafficTick, TRAFFIC_SEATS } from './traffic.js';
16
16
 
17
17
  export type AuthorCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'> & { version: string };
18
18
  export type AuthorQueueRow = Pick<QueueItem, 'seat' | 'status'> & Partial<Pick<QueueItem, 'input' | 'kind'>>;
@@ -101,8 +101,12 @@ export interface CollectAuthorDepthDeps {
101
101
  versions?: () => readonly [string, string];
102
102
  ledger?: Pick<AuthorLedger, 'request'>;
103
103
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
104
+ listProcesses?: typeof defaultListHarnessProcesses;
104
105
  }
105
106
 
107
+ /** Budget for the shadow's process read (it runs inside the queue lock, whose waiters give up after 30 s). */
108
+ export const AUTHOR_DEPTH_PROCESS_DEADLINE_MS = 10_000;
109
+
106
110
  function defaultVersions(now: Date, root = releaseLedgerRoot()): readonly [string, string] {
107
111
  if (!existsSync(join(root, 'release', 'features.sqlite'))) throw new Error('release schedule ledger unavailable');
108
112
  const upcoming = listSchedules(root).filter(row => row.cutAt && Date.parse(row.cutAt) > now.getTime())
@@ -122,15 +126,6 @@ export function collectAuthorDepth(deps: CollectAuthorDepthDeps = {}): AuthorDep
122
126
  };
123
127
  const cfg = deps.caps && deps.running ? null : observe('config', () => getUserConfig().loops?.orchestrator ?? ORCHESTRATOR_DEFAULTS);
124
128
  const caps = deps.caps ? observe('caps', deps.caps) : cfg === null ? null : observe('caps', () => cfg.seatCaps);
125
- const running = deps.running ? observe('running', deps.running) : cfg === null ? null : observe('running', () => {
126
- const observation = defaultListHarnessProcesses();
127
- if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
128
- const processes = observation.records.map(process => ({ ...process,
129
- seat: seatOfTree(process.cwdStatus === 'unknown' ? undefined : process.cwd, cfg) }));
130
- const measured = trafficTick({ processes, now, caps: cfg.seatCaps, openCells: [] });
131
- if (measured.unassigned) throw new Error(`${measured.unassigned} unassigned harness launch process(es)`);
132
- return Object.fromEntries(measured.seats.map(row => [row.seat, row.running]));
133
- });
134
129
  const queued = observe('queue', deps.queued ?? (() => listHarnessQueue({ root: deps.root })));
135
130
  const releaseRoot = deps.root ?? releaseLedgerRoot();
136
131
  const versions = observe('versions', deps.versions ?? (() => defaultVersions(now, releaseRoot)));
@@ -139,7 +134,30 @@ export function collectAuthorDepth(deps: CollectAuthorDepthDeps = {}): AuthorDep
139
134
  throw new Error('checklist ledger unavailable');
140
135
  return deps.cells?.(version) ?? listChecklist(version, deps.root).items.map(item => ({ ...item, version }));
141
136
  }));
142
- const overlaps = observe('overlap', deps.overlaps ?? (() => {
137
+ // Without release cells there is no candidate to compare, and the host reads below are the expensive ones (every launch's
138
+ // cwd · open PRs over the network) — a fresh --test universe has no release ledger, so it skips them (10-08 tick hang).
139
+ const noCells = cells === null && !deps.cells;
140
+ if (noCells && !deps.running && cfg !== null) unreadable.push({ source: 'running', reason: 'skipped: no release cells to compare' });
141
+ const running = deps.running ? observe('running', deps.running) : cfg === null || noCells ? null : observe('running', () => {
142
+ // Only launch processes are counted, so only they need a cwd; the whole read is bounded because the queue tick runs
143
+ // this shadow inside its lock (10-08: ~490 host processes × lsof kept a --test tick for minutes).
144
+ const started = Date.now();
145
+ const observation = (deps.listProcesses ?? defaultListHarnessProcesses)({ include: record => isTrafficLaunch(record.command),
146
+ cwdOnly: true, deadlineMs: AUTHOR_DEPTH_PROCESS_DEADLINE_MS });
147
+ if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
148
+ const late = observation.records.filter(process => process.cwdFailureReason?.startsWith('observation deadline')).length;
149
+ (deps.log ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data)))(
150
+ 'loop.orchestrator', 'author-depth-processes', { launches: observation.records.length, late, ms: Date.now() - started,
151
+ deadlineMs: AUTHOR_DEPTH_PROCESS_DEADLINE_MS });
152
+ if (late) throw new Error(`process observation deadline ${AUTHOR_DEPTH_PROCESS_DEADLINE_MS}ms: ${late} launch cwd(s) unread`);
153
+ const processes = observation.records.map(process => ({ ...process,
154
+ seat: seatOfTree(process.cwdStatus === 'unknown' ? undefined : process.cwd, cfg) }));
155
+ const measured = trafficTick({ processes, now, caps: cfg.seatCaps, openCells: [] });
156
+ if (measured.unassigned) throw new Error(`${measured.unassigned} unassigned harness launch process(es)`);
157
+ return Object.fromEntries(measured.seats.map(row => [row.seat, row.running]));
158
+ });
159
+ if (noCells && !deps.overlaps) unreadable.push({ source: 'overlap', reason: 'skipped: no release cells to compare' });
160
+ const overlaps = noCells && !deps.overlaps ? null : observe('overlap', deps.overlaps ?? (() => {
143
161
  const observed = collectWork();
144
162
  if (observed.unreadableTrees || observed.unreadableGoals || observed.work.some(item => item.unreadable))
145
163
  throw new Error(`incomplete work observation: trees=${observed.unreadableTrees} goals=${observed.unreadableGoals}`);
@@ -19,6 +19,9 @@ export interface CollectWorkDeps {
19
19
  config?: Pick<OrchestratorLoopConfig, 'seatTrees'>;
20
20
  }
21
21
 
22
+ /** Budget for collectWork's per-process cwd reads. */
23
+ export const COLLECT_WORK_PROCESS_DEADLINE_MS = 10_000;
24
+
22
25
  const repoRoot = resolve(import.meta.dir, '../../..');
23
26
  const defaultRunGh = (args: string[]): string => execFileSync('bun', ['bin/elanous.mjs', '--test', 'gh', ...args], {
24
27
  cwd: repoRoot, encoding: 'utf8', timeout: 60_000, maxBuffer: 20 * 1024 * 1024,
@@ -63,7 +66,10 @@ function goalFiles(text: string): string[] {
63
66
  export function collectWork(deps: CollectWorkDeps = {}): CollectWorkResult {
64
67
  const prs = JSON.parse((deps.runGh ?? defaultRunGh)(['pr', 'list', '--state', 'open', '--limit', '1000', '--json', 'number,title,body,headRefName,files,isDraft,labels'])) as OpenPr[];
65
68
  if (!Array.isArray(prs)) throw new Error('PR observation is not a list');
66
- const observation = (deps.listProcesses ?? defaultListHarnessProcesses)();
69
+ // Only goal-carrying launches become work, so only they need a cwd; bounded so a host with hundreds of harness processes
70
+ // cannot hold the queue lock for minutes (10-08). Records past the budget stay cwd-unknown → their goal reads unreadable.
71
+ const observation = deps.listProcesses ? deps.listProcesses()
72
+ : defaultListHarnessProcesses({ include: record => goalPath(record.command) !== null, cwdOnly: true, deadlineMs: COLLECT_WORK_PROCESS_DEADLINE_MS });
67
73
  if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
68
74
  const cfg = deps.config ?? getUserConfig().loops?.orchestrator ?? ORCHESTRATOR_DEFAULTS;
69
75
  const excluded = { stalled: 0, superseded: 0, draft: 0 };
@@ -171,7 +171,7 @@ export async function runResyncShadow(max = 3, deps: ResyncShadowDeps = {}): Pro
171
171
  entry.resolvedFiles = outcome.resolvedFiles ?? [];
172
172
  if (outcome.sizeCollapse?.length || outcome.providerFailure?.length || outcome.testDeclarationLoss?.length) {
173
173
  entry.verdict = 'guard-tripped';
174
- } else if (outcome.status === 'merged' || outcome.status === 'llm-resolved') {
174
+ } else if (outcome.status === 'merged' || outcome.status === 'llm-resolved' || outcome.status === 'deterministic-resolved') {
175
175
  if (entry.testFiles.length === 0) entry.verdict = 'merge-ok-no-tests';
176
176
  else {
177
177
  const test = (deps.runTests ?? defaultRunTests)(tmp, entry.testFiles);
@@ -8,6 +8,7 @@ import { effectiveInstanceRoot } from '../../instance/resolve.js';
8
8
  import { getUserConfig, type OrchestratorLoopConfig } from '../../user-config.js';
9
9
  import { rubricPriority } from '../../release-loop/rubric.js';
10
10
  import { debug } from '../../debug/log.js';
11
+ import { emitLoopHeartbeat } from '../neighbor-runtime.js';
11
12
  import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../self-dev/run-store.js';
12
13
  import { planAction, runSeatLoopOnce, type SeatDeps, type SeatItem } from '../../seat-loop/seat-loop.js';
13
14
  import { addHarnessQueue, harnessQueueOutcome, setQueuedPoolHint } from '../../harness/harness-queue.js';
@@ -48,7 +49,8 @@ export type PlacementInput = { id: string; title: string; owner: string; priorit
48
49
  export type PlaceAdapter = (cell: PlacementInput, deps?: { dryRun?: boolean; now?: Date }) => Promise<{ version?: string } | null> | { version?: string } | null;
49
50
  export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown[]; blocked: Array<{ id: string; reason: string }> }> | { decisions: unknown[]; blocked: Array<{ id: string; reason: string }> };
50
51
  export interface TickDeps {
51
- root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
52
+ root?: string; now?: Date | (() => Date); mode?: Mode; runId?: string; window?: Window;
53
+ heartbeatIntervalMs?: number;
52
54
  cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
53
55
  degradation?: () => { llm: boolean; board: boolean };
54
56
  gridHosts?: readonly GridHost[];
@@ -101,6 +103,19 @@ export function kstWindow(now: Date): Window {
101
103
  const hour = Number(new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Seoul', hour: '2-digit', hourCycle: 'h23' }).format(now));
102
104
  return hour < 12 ? '08' : hour < 18 ? '12' : '18';
103
105
  }
106
+ /** KST schedule of graphs/orchestrator/orchestrator.yaml (`0 8,12,18 * * *`). */
107
+ const ORCHESTRATOR_HOURS_KST = [8, 12, 18] as const;
108
+ /** Grace past the next scheduled window before neighbors may call the orchestrator absent. */
109
+ export const ORCHESTRATOR_LEASE_GRACE_MS = 60 * 60_000;
110
+ /** The next scheduled window strictly after `now` — the orchestrator is idle, not absent, until then. */
111
+ export function nextOrchestratorWindowAt(now: Date): Date {
112
+ const kst = new Date(now.getTime() + 9 * 3_600_000);
113
+ for (let day = 0; day < 2; day++) for (const hour of ORCHESTRATOR_HOURS_KST) {
114
+ const at = Date.UTC(kst.getUTCFullYear(), kst.getUTCMonth(), kst.getUTCDate() + day, hour) - 9 * 3_600_000;
115
+ if (at > now.getTime()) return new Date(at);
116
+ }
117
+ throw new Error('unreachable: no orchestrator window within two days');
118
+ }
104
119
  function kstDay(now: Date): string {
105
120
  return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
106
121
  }
@@ -371,7 +386,8 @@ async function optionalAdapter<T>(path: string, name: string): Promise<T | undef
371
386
 
372
387
  /** A graph command invokes one node; only the per-run file carries data across processes. */
373
388
  export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Promise<TickState> {
374
- const now = deps.now ?? new Date();
389
+ const currentTime = () => typeof deps.now === 'function' ? deps.now() : deps.now ?? new Date();
390
+ const now = currentTime();
375
391
  const window = deps.window ?? kstWindow(now);
376
392
  const root = deps.root ?? effectiveInstanceRoot();
377
393
  const runId = deps.runId ?? graphRunId() ?? `${kstDay(now)}-${window}`;
@@ -379,6 +395,16 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
379
395
  const orchestratorConfig = getUserConfig().loops?.orchestrator;
380
396
  const configured = deps.mode ?? orchestratorConfig?.mode;
381
397
  const mode: Mode = configured === 'off' || configured === 'live' ? configured : 'shadow';
398
+ const pulse = (health: 'healthy' | 'degraded', at: Date): void => {
399
+ try { emitLoopHeartbeat(root, 'orchestrator', health, at,
400
+ new Date(nextOrchestratorWindowAt(at).getTime() + ORCHESTRATOR_LEASE_GRACE_MS).toISOString()); }
401
+ catch (error) { debug.log('loop.orchestrator', 'heartbeat-failed', { error: String(error).slice(0, 200) }, { level: 'warn' }); }
402
+ };
403
+ if (mode !== 'off') pulse('healthy', now);
404
+ const heartbeatTimer = mode === 'off' ? undefined : setInterval(() => pulse('healthy', currentTime()), deps.heartbeatIntervalMs ?? 60 * 60_000);
405
+ heartbeatTimer?.unref();
406
+ let nodeError: unknown;
407
+ try {
382
408
  const state: TickState = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as TickState : initial(runId, window, mode, kstDay(now));
383
409
  state.steps ??= [];
384
410
  if (state.window !== window || state.mode !== mode) throw new Error('orchestrator run window/mode changed');
@@ -739,6 +765,13 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
739
765
  mkdirSync(dirname(path), { recursive: true });
740
766
  writeFileSync(path, JSON.stringify(state));
741
767
  return state;
768
+ } catch (error) {
769
+ nodeError = error;
770
+ throw error;
771
+ } finally {
772
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
773
+ if (mode !== 'off') pulse(nodeError === undefined ? 'healthy' : 'degraded', currentTime());
774
+ }
742
775
  }
743
776
 
744
777
  /** COORD-HA — the production supply of `TickDeps.degradation` (config `loops.orchestrator.degradationSignal`). Shadow (the
@@ -50,6 +50,8 @@ function realOrResolved(path: string): string {
50
50
  }
51
51
 
52
52
  const IS_LAUNCH = /(?:^|\s)(?:\S*\/)?elanous\.mjs\s+(?:(?:--test|--config-dir\s+\S+)\s+)?harness\s+(?:ask|say)(?=\s|$)/;
53
+ /** A `harness ask|say` launch command line — the only processes `trafficTick` counts. */
54
+ export const isTrafficLaunch = (command: string): boolean => IS_LAUNCH.test(command);
53
55
 
54
56
  /** Process-to-seat resolution occurs at the boundary; this decision does no IO. */
55
57
  export function trafficTick({ processes, now, caps, lastLaunchAt, idleSince, openCells, nextRound, totalSlots, idleMinutes = 30 }: {
@@ -685,6 +685,11 @@ export function unregisteredCronLoops(entries: AllLoopEntry[], opts: LoopRegistr
685
685
  return warnings;
686
686
  }
687
687
 
688
+ /** Newest-first run history of one loop without re-listing the registry (health reads a longer window than `loop status`). */
689
+ export function loopRecentRuns(id: string, opts: LoopRegistryOptions = {}, count = 5): LoopRun[] {
690
+ return recentRuns(id, opts.stateRoot ?? effectiveInstanceRoot(), count);
691
+ }
692
+
688
693
  export function loopStatus(id: string, opts: LoopRegistryOptions = {}): LoopEntry & { recentRuns: LoopRun[] } {
689
694
  const loop = listLoops(opts).find(entry => entry.id === id);
690
695
  if (!loop) throw new Error(`loop not found: ${id}`);
@@ -91,6 +91,8 @@ export interface PublishResult {
91
91
  ok: boolean;
92
92
  sequence: number;
93
93
  published: Array<{ name: string; version: string; sha256: string; bundled: string[] }>;
94
+ /** With keepExisting: previous index entries whose name has no folder under pluginsDir, carried over unchanged. */
95
+ kept: Array<{ name: string; version: string; sha256: string }>;
94
96
  skipped: Array<{ dir: string; reason: string; hits?: InternalRefHit[] }>;
95
97
  /** Published anyway, but a reviewer should look (e.g. a third-party graph relying on core recipes). */
96
98
  warnings: Array<{ dir: string; graph: string; reason: 'third-party-core-recipe'; recipes: string[] }>;
@@ -103,6 +105,8 @@ export function publishMarket(input: {
103
105
  market: { name: string; displayName: string };
104
106
  key: { keyId: string; privateKeyPem: string };
105
107
  source?: { repoUrl: string; sha: string; basePath: string };
108
+ /** Carry over previous entries whose name is not a plugin folder under pluginsDir (entry, archive and unpacked files untouched). */
109
+ keepExisting?: boolean;
106
110
  now?: Date;
107
111
  }): PublishResult {
108
112
  let sequence = 1;
@@ -307,6 +311,18 @@ export function publishMarket(input: {
307
311
  published.push({ name: manifest.name, version: manifest.version, sha256, bundled });
308
312
  debug.log('market.publish', 'bundled', { name: manifest.name, version: manifest.version, skills: bundled });
309
313
  }
314
+ const kept: PublishResult['kept'] = [];
315
+ if (input.keepExisting && previous) {
316
+ // Folders under pluginsDir own their names (even when skipped); every other previous entry stays as it was.
317
+ const present = new Set(readdirSync(input.pluginsDir).filter(dir => lstatSync(join(input.pluginsDir, dir)).isDirectory()));
318
+ for (const item of previous.plugins) {
319
+ if (present.has(item.name)) continue;
320
+ plugins.push(item);
321
+ kept.push({ name: item.name, version: item.version, sha256: item.artifact.sha256 });
322
+ debug.log('market.publish', 'kept', { name: item.name, version: item.version });
323
+ }
324
+ plugins.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
325
+ }
310
326
  const index: MarketplaceIndex = { name: input.market.name, interface: { displayName: input.market.displayName },
311
327
  sequence, generatedAt: (input.now ?? new Date()).toISOString(), plugins };
312
328
  const bytes = Buffer.from(JSON.stringify(index, null, 2) + '\n');
@@ -345,7 +361,7 @@ export function publishMarket(input: {
345
361
  signatureText: readFileSync(join(input.outDir, 'index.sig'), 'utf8'), trustedKeys });
346
362
  if (!verified.ok) throw new Error(`index verification failed: ${verified.reason}: ${verified.detail}`);
347
363
  for (const item of published) debug.log('market.publish', 'published', { name: item.name, version: item.version, sequence });
348
- return { ok: !skipped.some(item => item.reason === 'internal-reference'), sequence, published, skipped, warnings };
364
+ return { ok: !skipped.some(item => item.reason === 'internal-reference'), sequence, published, kept, skipped, warnings };
349
365
  } catch (error) {
350
366
  const rawReason = error instanceof Error ? error.message : 'unknown-error';
351
367
  const reason = input.key.privateKeyPem && rawReason.includes(input.key.privateKeyPem) ? 'invalid signing key' : rawReason;
@@ -117,6 +117,7 @@ export const FEATURE_MATURITY = {
117
117
  widget: 'tool',
118
118
  log: 'tool',
119
119
  loops: 'beta',
120
+ board: 'beta',
120
121
  persona: 'beta', // G3 b — 대화별 페르소나(GOAL-BENCH B7 #23836)
121
122
  memory: 'beta',
122
123
  export: 'stable',
@@ -161,7 +162,7 @@ export const FEATURE_MATURITY = {
161
162
  ping: 'stable', provider: 'stable', sessions: 'stable', fork: 'stable', resume: 'stable',
162
163
  decisions: 'stable', work: 'stable', cancel: 'stable',
163
164
  skills: 'beta', skill: 'beta', digest: 'beta', intake: 'beta', ad: 'beta',
164
- taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', loops: 'beta', runs: 'beta', release: 'beta', away: 'beta', project: 'beta', wish: 'beta',
165
+ taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', loops: 'beta', runs: 'beta', board: 'beta', release: 'beta', away: 'beta', project: 'beta', wish: 'beta',
165
166
  brain: 'tool', cc: 'tool', cdx: 'tool', gem: 'tool', local: 'tool',
166
167
  harness: 'ops',
167
168
  // Existing commands outside the CXO four-grade menu list are owner-only.
@@ -93,3 +93,20 @@ export function redactLogRecord(rec: LogRecord, opts: RedactOpts = {}): LogRecor
93
93
  const mask = opts.mask ?? defaultMask;
94
94
  return { ...rec, data: redactValue(rec.data, keyBlock, mask, new WeakSet()) };
95
95
  }
96
+
97
+ /** Pre-bundle knowledge-pack DLP. Reasons are fixed labels, never source text or secret values.
98
+ * This is separate from the opt-in log masking path above. */
99
+ const KNOWLEDGE_PACK_DLP_RULES: ReadonlyArray<{ reason: string; pattern: RegExp }> = [
100
+ { reason: 'secret-value', pattern: /-----BEGIN (?:[A-Z0-9_-]+ )?PRIVATE KEY(?: BLOCK)?-----|\bBearer\s+[A-Za-z0-9._~+/-]{12,}={0,2}\b|\b(?:sk-[A-Za-z0-9_-]{16,}|xai-[A-Za-z0-9_-]{20,}|gh[pousr]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|(?:AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16}|AIza[A-Za-z0-9_-]{35})\b|\b(?:api[_-]?key|secret[_-]?key|access[_-]?token|private[_-]?key|token)['"]?\s*[=:]\s*['"]?[A-Za-z0-9_./+~-]{16,}/iu },
101
+ // A key that names a password/secret makes ANY assigned value secret: short or punctuated values count too.
102
+ // Schema flags (`"secret": true`) are not values.
103
+ { reason: 'secret-value', pattern: /\b(?:password|passwd|pwd|passphrase|client[_-]?secret|secret)['"]?\s*[=:]\s*(?!(?:true|false|null)\b)(?:"[^"\n]+"|'[^'\n]+'|[^\s'",;{}\[\]]{4,})/iu },
104
+ { reason: 'personal-information', pattern: /\b01[016789]-?\d{3,4}-?\d{4}\b|\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b|\b\d{6}-[1-8]\d{6}\b/iu },
105
+ { reason: 'internal-identifier', pattern: /(?:사번|사내\s*식별자|내부\s*식별자|employee[_ -]?id|internal[_ -]?id)\s*(?:[:=]|\s)\s*[A-Za-z0-9][A-Za-z0-9_-]{3,}/iu },
106
+ { reason: 'sales-confidential', pattern: /영업\s*기밀|(?:sales\s+confidential|confidential\s+sales|영업\s*비밀)/iu },
107
+ ];
108
+
109
+ export function scanKnowledgePackDlp(text: string): string[] {
110
+ const reasons = KNOWLEDGE_PACK_DLP_RULES.filter(({ pattern }) => pattern.test(text)).map(({ reason }) => reason);
111
+ return [...new Set(reasons)];
112
+ }
@@ -1,6 +1,8 @@
1
1
  import { join } from 'node:path';
2
2
  import { debug } from '../../debug/log.js';
3
- import type { DraftMetrics } from '../../self-dev/draft-sweep.js';
3
+ import type { DraftMetrics, OverlapMetrics } from '../../self-dev/draft-sweep.js';
4
+
5
+ export type CeoDraftMetrics = DraftMetrics & { readonly overlap: OverlapMetrics | null };
4
6
 
5
7
  /**
6
8
  * DRAFT-METRIC — CEO view source. Opening the page must never trigger a GitHub read:
@@ -14,7 +16,7 @@ export type DraftMetricsState = 'measuring' | 'ready' | 'unavailable';
14
16
  export interface DraftMetricsSnapshot {
15
17
  /** measuring = no value yet and a collection is running · unavailable = no value and the last collection failed. */
16
18
  readonly state: DraftMetricsState;
17
- readonly metrics: DraftMetrics | null;
19
+ readonly metrics: CeoDraftMetrics | null;
18
20
  /** When the served value was collected (ISO). null without a value. */
19
21
  readonly measuredAt: string | null;
20
22
  /** A background collection is in flight. */
@@ -24,7 +26,7 @@ export interface DraftMetricsSnapshot {
24
26
  }
25
27
 
26
28
  export interface DraftMetricsSourceOptions {
27
- readonly collect?: () => Promise<DraftMetrics>;
29
+ readonly collect?: () => Promise<CeoDraftMetrics>;
28
30
  readonly ttlMs?: number;
29
31
  readonly failureRetryMs?: number;
30
32
  readonly now?: () => number;
@@ -43,17 +45,29 @@ function isMetrics(value: unknown): value is DraftMetrics {
43
45
  && nullableNumber('oldestAgeHours') && nullableNumber('conversion48h');
44
46
  }
45
47
 
46
- /** Parses the CLI's single JSON line; anything else is a failed collection, never a zero. */
47
- export function parseDraftMetricsOutput(stdout: string): DraftMetrics {
48
+ function isOverlapMetrics(value: unknown): value is OverlapMetrics {
49
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
50
+ const row = value as Record<string, unknown>;
51
+ const count = (key: string) => row[key] === null || (Number.isInteger(row[key]) && (row[key] as number) >= 0);
52
+ const nullableNumber = (key: string) => row[key] === null || (typeof row[key] === 'number' && Number.isFinite(row[key]) && (row[key] as number) >= 0);
53
+ return ['launches24h', 'launched', 'unmeasured', 'linked', 'autoLanded', 'salvaged', 'salvageUnmeasured'].every(count)
54
+ && typeof row.sourceIncomplete === 'boolean' && nullableNumber('secondSiblingMedianHours')
55
+ && (row.autoRate === null || (typeof row.autoRate === 'number' && Number.isFinite(row.autoRate) && row.autoRate >= 0 && row.autoRate <= 1));
56
+ }
57
+
58
+ /** Parses the CLI's single JSON line; malformed draft values fail, malformed overlap is unmeasured. */
59
+ export function parseDraftMetricsOutput(stdout: string): CeoDraftMetrics {
48
60
  const line = stdout.trim().split('\n').reverse().find((text) => text.trim().startsWith('{'));
49
61
  if (!line) throw new Error('draft metrics: no JSON output');
50
62
  const parsed: unknown = JSON.parse(line);
51
63
  if (!isMetrics(parsed)) throw new Error('draft metrics: malformed output');
52
64
  const { inventory, oldestAgeHours, needsOwner, converted48h, cohort48h, conversion48h } = parsed;
53
- return { inventory, oldestAgeHours, needsOwner, converted48h, cohort48h, conversion48h };
65
+ const candidate = (parsed as DraftMetrics & { overlap?: unknown }).overlap;
66
+ return { inventory, oldestAgeHours, needsOwner, converted48h, cohort48h, conversion48h,
67
+ overlap: isOverlapMetrics(candidate) ? candidate : null };
54
68
  }
55
69
 
56
- async function collectViaCli(): Promise<DraftMetrics> {
70
+ async function collectViaCli(): Promise<CeoDraftMetrics> {
57
71
  const root = join(import.meta.dir, '../../..');
58
72
  const proc = Bun.spawn(['bun', join(root, 'bin/elanous.mjs'), 'harness', 'drafts', 'metrics', '--json'], {
59
73
  cwd: root, stdout: 'pipe', stderr: 'pipe',
@@ -75,7 +89,7 @@ export function createDraftMetricsSource(options: DraftMetricsSourceOptions = {}
75
89
  const ttl = options.ttlMs ?? DRAFT_METRICS_TTL_MS;
76
90
  const retry = options.failureRetryMs ?? DRAFT_METRICS_FAILURE_RETRY_MS;
77
91
  const now = options.now ?? Date.now;
78
- let value: { metrics: DraftMetrics; at: number } | null = null;
92
+ let value: { metrics: CeoDraftMetrics; at: number } | null = null;
79
93
  let failure: { reason: string; at: number } | null = null;
80
94
  let inFlight: Promise<void> | null = null;
81
95
 
@@ -0,0 +1,77 @@
1
+ // W9c run guard (TC security review of #25123) — a peer «edit» grant lets another token rewrite a «mine» graph.
2
+ // Running that graph would run the peer's change with this machine's privileges, so a peer save leaves a
3
+ // marker next to the graph's access grants (`<mine>/.access/<id>.peer-edit.json`) and every run entry for a
4
+ // «mine» graph refuses it until the owner approves that exact version (or re-saves it themselves).
5
+ // Fail closed: a marker that exists but cannot be read or parsed refuses the run.
6
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { randomUUID } from 'node:crypto';
9
+
10
+ export const PEER_EDIT_REFUSAL = '상대가 바꾼 그래프 — 변경을 확인하고 승인해야 실행할 수 있다';
11
+
12
+ export interface PeerEditRecord {
13
+ /** `peer:<first 8 hex of the peer token's sha256>` — never the token itself. */
14
+ editedBy: string;
15
+ /** The version id the peer save produced (`pending` while the save is in flight). */
16
+ version: string;
17
+ at: string;
18
+ approved?: { version: string; at: string };
19
+ }
20
+
21
+ export type PeerEditGate =
22
+ | { ok: true }
23
+ | { ok: false; error: 'peer-edit-unapproved'; record: PeerEditRecord }
24
+ | { ok: false; error: 'peer-edit-unreadable'; reason: string };
25
+
26
+ export function peerEditFile(mineDir: string, id: string): string {
27
+ return join(mineDir, '.access', `${id}.peer-edit.json`);
28
+ }
29
+
30
+ /** null = no peer ever saved this graph. Throws when the marker exists but is unreadable or malformed. */
31
+ export function readPeerEdit(mineDir: string, id: string): PeerEditRecord | null {
32
+ let text: string;
33
+ try { text = readFileSync(peerEditFile(mineDir, id), 'utf8'); }
34
+ catch (error) {
35
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
36
+ throw error;
37
+ }
38
+ const parsed: unknown = JSON.parse(text);
39
+ const record = parsed as PeerEditRecord;
40
+ if (!parsed || typeof parsed !== 'object' || typeof record.editedBy !== 'string' || typeof record.version !== 'string'
41
+ || typeof record.at !== 'string'
42
+ || (record.approved !== undefined && (typeof record.approved !== 'object' || record.approved === null
43
+ || typeof record.approved.version !== 'string' || typeof record.approved.at !== 'string'))) {
44
+ throw new Error('invalid peer-edit marker');
45
+ }
46
+ return record;
47
+ }
48
+
49
+ export function writePeerEdit(mineDir: string, id: string, record: PeerEditRecord): void {
50
+ mkdirSync(join(mineDir, '.access'), { recursive: true, mode: 0o700 });
51
+ const target = peerEditFile(mineDir, id);
52
+ const temp = `${target}.${process.pid}.${randomUUID()}.tmp`;
53
+ try {
54
+ writeFileSync(temp, JSON.stringify(record), { mode: 0o600 });
55
+ renameSync(temp, target);
56
+ } finally { rmSync(temp, { force: true }); }
57
+ }
58
+
59
+ /** An owner-authorized save replaces the peer's bytes with bytes the owner chose. */
60
+ export function clearPeerEdit(mineDir: string, id: string): void {
61
+ rmSync(peerEditFile(mineDir, id), { force: true });
62
+ }
63
+
64
+ export function peerEditRunGate(mineDir: string, id: string): PeerEditGate {
65
+ let record: PeerEditRecord | null;
66
+ try { record = readPeerEdit(mineDir, id); }
67
+ catch (error) { return { ok: false, error: 'peer-edit-unreadable', reason: String(error).slice(0, 200) }; }
68
+ if (!record || record.approved?.version === record.version) return { ok: true };
69
+ return { ok: false, error: 'peer-edit-unapproved', record };
70
+ }
71
+
72
+ /** The 409 body every refusing run entry returns — `reason` is what the editor shows. */
73
+ export function peerEditRefusalBody(id: string, gate: Exclude<PeerEditGate, { ok: true }>): Record<string, unknown> {
74
+ return gate.error === 'peer-edit-unapproved'
75
+ ? { error: gate.error, id, reason: PEER_EDIT_REFUSAL, editedBy: gate.record.editedBy, version: gate.record.version, editedAt: gate.record.at }
76
+ : { error: gate.error, id, reason: PEER_EDIT_REFUSAL, detail: gate.reason };
77
+ }
@@ -1,7 +1,11 @@
1
1
  import { debug } from '../../debug/log.js';
2
- import { GraphWizardInputError, generateGraphFromPrompt, type GraphWizardDeps, type GraphWizardTurn } from '../../graph-wizard/generate.js';
2
+ import { GraphWizardInputError, generateGraphFromPrompt, wizardInstalledPacks, type GraphWizardDeps, type GraphWizardTurn } from '../../graph-wizard/generate.js';
3
3
  import { jsonResponse } from './json-response.js';
4
4
 
5
+ export function handleGraphWizardPacksGet(deps: GraphWizardDeps = {}): Response {
6
+ return jsonResponse({ packs: wizardInstalledPacks(deps) });
7
+ }
8
+
5
9
  const MAX_PROMPT = 4000;
6
10
  const MAX_YAML = 200_000;
7
11
 
@@ -28,6 +32,9 @@ export async function handleGraphWizardPost(req: Request, deps: GraphWizardDeps
28
32
  if (body.currentYaml !== undefined && (typeof body.currentYaml !== 'string' || body.currentYaml.length > MAX_YAML)) {
29
33
  return jsonResponse({ error: 'bad_request', reason: 'currentYaml must be a string' }, 400);
30
34
  }
35
+ if (body.packId !== undefined && typeof body.packId !== 'string') {
36
+ return jsonResponse({ error: 'bad_request', reason: 'packId must be a string' }, 400);
37
+ }
31
38
  let history: GraphWizardTurn[] | undefined;
32
39
  if (body.history !== undefined) {
33
40
  if (!Array.isArray(body.history) || !body.history.every((t) => t && typeof t === 'object' &&
@@ -42,6 +49,7 @@ export async function handleGraphWizardPost(req: Request, deps: GraphWizardDeps
42
49
  ...(body.kind ? { kind: body.kind as 'harness' | 'workflow' } : {}),
43
50
  ...(typeof body.currentYaml === 'string' && body.currentYaml.trim() ? { currentYaml: body.currentYaml } : {}),
44
51
  ...(history ? { history } : {}),
52
+ ...(typeof body.packId === 'string' ? { packId: body.packId } : {}),
45
53
  }, deps);
46
54
  return jsonResponse({ ...result, base: result.base ?? null }, result.ok ? 200 : 422);
47
55
  } catch (error) {