elanous 0.2.5 → 0.2.7

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 (479) hide show
  1. package/.env.example +10 -7
  2. package/README.md +9 -4
  3. package/apps/pwa/out/404/index.html +1 -1
  4. package/apps/pwa/out/404.html +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{5391-8793f88b03b30161.js → 2375-cb6debea56cce4fb.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/{1751.50b96ea9c37b49b6.js → 2415.ed6428ff334c7ce2.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/{4daa2798-1363d725c79f3593.js → 2ea96958-e8c7e112de50b067.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
  18. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/{8617.465ca22a319c95b3.js → 4323.c5dfb6f503304775.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/{8918-482b30ce2a8ebf4c.js → 5080-1613752891fcadb4.js} +3 -3
  24. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{5348-bbf838e119661eee.js → 5791-bb82743fa6299562.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/{9704-3221c102623c8d73.js → 6062-73e68343a9814f6a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/{4440-18bb160d95819f4e.js → 6090-85a3f05f954e5697.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/{f6f3850d-7f324c6566002113.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-f89e9a303515a033.js → page-10ed24f413ed66e9.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/board/{page-4f403cd060442c95.js → page-211810b6c93ab66a.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-0ac5d053f0444d21.js → page-624f7a2f89b60c8f.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-5a5fb385a4df0ffe.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-7457f52683562161.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-b7dcf5d48a854b76.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-51480757e7d68b92.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-07267f1391b96aba.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/morning/{page-bccce0babceae30f.js → page-55d50e9b0006a91d.js} +1 -1
  68. package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/page-a7e37af7d0399cb8.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/sessions/page-15e5a15db84c56aa.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a2e572e4511f2a16.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d422f38c052967e.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/{webpack-2bf59c720fe9542c.js → webpack-e5bea36f183b25e6.js} +1 -1
  95. package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +1 -0
  96. package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +1 -0
  97. package/apps/pwa/out/approvals/index.html +1 -1
  98. package/apps/pwa/out/approvals/index.txt +22 -22
  99. package/apps/pwa/out/autopilot/index.html +1 -1
  100. package/apps/pwa/out/autopilot/index.txt +22 -22
  101. package/apps/pwa/out/board/index.html +1 -1
  102. package/apps/pwa/out/board/index.txt +22 -22
  103. package/apps/pwa/out/botlab/index.html +1 -1
  104. package/apps/pwa/out/botlab/index.txt +21 -21
  105. package/apps/pwa/out/bots/index.html +1 -1
  106. package/apps/pwa/out/bots/index.txt +21 -21
  107. package/apps/pwa/out/chat/index.html +1 -1
  108. package/apps/pwa/out/chat/index.txt +21 -21
  109. package/apps/pwa/out/consult/index.html +1 -0
  110. package/apps/pwa/out/consult/index.txt +32 -0
  111. package/apps/pwa/out/control/index.html +1 -1
  112. package/apps/pwa/out/control/index.txt +22 -22
  113. package/apps/pwa/out/design-check/index.html +1 -1
  114. package/apps/pwa/out/design-check/index.txt +22 -22
  115. package/apps/pwa/out/editor/index.html +1 -1
  116. package/apps/pwa/out/editor/index.txt +22 -22
  117. package/apps/pwa/out/exec/index.html +1 -0
  118. package/apps/pwa/out/exec/index.txt +32 -0
  119. package/apps/pwa/out/field/index.html +1 -0
  120. package/apps/pwa/out/field/index.txt +32 -0
  121. package/apps/pwa/out/healthz +1 -1
  122. package/apps/pwa/out/icons/elanous-16.png +0 -0
  123. package/apps/pwa/out/icons/elanous-180.png +0 -0
  124. package/apps/pwa/out/icons/elanous-192.png +0 -0
  125. package/apps/pwa/out/icons/elanous-32.png +0 -0
  126. package/apps/pwa/out/icons/elanous-512.png +0 -0
  127. package/apps/pwa/out/index.html +1 -1
  128. package/apps/pwa/out/index.txt +21 -21
  129. package/apps/pwa/out/intake/index.html +1 -1
  130. package/apps/pwa/out/intake/index.txt +22 -22
  131. package/apps/pwa/out/live/index.html +1 -1
  132. package/apps/pwa/out/live/index.txt +22 -22
  133. package/apps/pwa/out/market/index.html +1 -1
  134. package/apps/pwa/out/market/index.txt +21 -21
  135. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  136. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  137. package/apps/pwa/out/morning/index.html +1 -1
  138. package/apps/pwa/out/morning/index.txt +22 -22
  139. package/apps/pwa/out/observatory/index.html +1 -1
  140. package/apps/pwa/out/observatory/index.txt +22 -22
  141. package/apps/pwa/out/reflection/index.html +1 -1
  142. package/apps/pwa/out/reflection/index.txt +22 -22
  143. package/apps/pwa/out/scheduler/index.html +1 -1
  144. package/apps/pwa/out/scheduler/index.txt +22 -22
  145. package/apps/pwa/out/sessions/index.html +1 -1
  146. package/apps/pwa/out/sessions/index.txt +21 -21
  147. package/apps/pwa/out/settings/devices/index.html +1 -1
  148. package/apps/pwa/out/settings/devices/index.txt +22 -22
  149. package/apps/pwa/out/settings/index.html +1 -1
  150. package/apps/pwa/out/settings/index.txt +22 -22
  151. package/apps/pwa/out/setup/done/index.html +1 -1
  152. package/apps/pwa/out/setup/done/index.txt +22 -22
  153. package/apps/pwa/out/setup/index.html +1 -1
  154. package/apps/pwa/out/setup/index.txt +22 -22
  155. package/apps/pwa/out/share/index.html +1 -1
  156. package/apps/pwa/out/share/index.txt +22 -22
  157. package/apps/pwa/out/showroom/index.html +1 -1
  158. package/apps/pwa/out/showroom/index.txt +21 -21
  159. package/apps/pwa/out/tasks/index.html +1 -1
  160. package/apps/pwa/out/tasks/index.txt +22 -22
  161. package/apps/pwa/out/term/index.html +1 -1
  162. package/apps/pwa/out/term/index.txt +22 -22
  163. package/apps/pwa/out/trace/index.html +1 -1
  164. package/apps/pwa/out/trace/index.txt +22 -22
  165. package/apps/pwa/out/vault/index.html +1 -1
  166. package/apps/pwa/out/vault/index.txt +22 -22
  167. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  168. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  169. package/apps/pwa/out/workflows/index.html +1 -1
  170. package/apps/pwa/out/workflows/index.txt +22 -22
  171. package/apps/pwa/out/workspace/index.html +1 -1
  172. package/apps/pwa/out/workspace/index.txt +22 -22
  173. package/apps/pwa/out/worktrees/index.html +1 -1
  174. package/apps/pwa/out/worktrees/index.txt +22 -22
  175. package/bin/elanous.cjs +107 -0
  176. package/graphs/field/field-feed.yaml +24 -0
  177. package/graphs/field/recipes.yaml +12 -0
  178. package/graphs/landing/landing-heal.yaml +3 -1
  179. package/graphs/landing/recipes.yaml +3 -0
  180. package/graphs/release/recipes.yaml +13 -0
  181. package/graphs/release/release-loop.yaml +13 -4
  182. package/package.json +5 -5
  183. package/plugins/card-followup/README.md +42 -0
  184. package/plugins/card-followup/examples/card-sample.json +7 -0
  185. package/plugins/card-followup/graphs/card-followup.yaml +18 -0
  186. package/plugins/card-followup/graphs/recipes.yaml +16 -0
  187. package/plugins/card-followup/graphs/run-step.ts +276 -0
  188. package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
  189. package/plugins/card-followup/plugin.json +25 -0
  190. package/plugins/doc-draft/README.md +13 -0
  191. package/plugins/doc-draft/examples/brief-sample.json +8 -0
  192. package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
  193. package/plugins/doc-draft/graphs/recipes.yaml +9 -0
  194. package/plugins/doc-draft/graphs/run-step.ts +113 -0
  195. package/plugins/doc-draft/plugin.json +24 -0
  196. package/plugins/geo-check/README.md +15 -0
  197. package/plugins/geo-check/examples/brand-sample.json +8 -0
  198. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  199. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  200. package/plugins/geo-check/graphs/run-step.ts +198 -0
  201. package/plugins/geo-check/plugin.json +24 -0
  202. package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
  203. package/plugins/job-coach/README.md +31 -1
  204. package/plugins/job-coach/graphs/run-step.ts +2 -2
  205. package/plugins/job-coach/plugin.json +1 -1
  206. package/plugins/job-coach/plugin.ts +1 -1
  207. package/scripts/affinity/endcard.sh +3 -2
  208. package/scripts/affinity/reframe.sh +3 -1
  209. package/scripts/ci-public-export-leak-gate.ts +48 -0
  210. package/scripts/codex-quota-alert.ts +122 -258
  211. package/scripts/coord-tracks.json +4 -4
  212. package/scripts/daemon-port-baseline.txt +1 -2
  213. package/scripts/field-feed/deliver-node.ts +52 -0
  214. package/scripts/field-feed/feed-node.ts +67 -0
  215. package/scripts/field-feed/lib.ts +162 -0
  216. package/scripts/field-feed/reel-node.ts +24 -0
  217. package/scripts/field-feed/start.ts +62 -0
  218. package/scripts/fixtures/vflow-sample.ndjson +6 -0
  219. package/scripts/install.sh +92 -13
  220. package/scripts/landing-heal/clean-checkout.ts +114 -0
  221. package/scripts/landing-heal/run-mission.ts +38 -3
  222. package/scripts/lib/android-test-device.ts +32 -0
  223. package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
  224. package/scripts/public-export.ts +8 -4
  225. package/scripts/release-loop/baseline-from-logs.ts +92 -0
  226. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  227. package/scripts/release-loop/env-known-failures.ts +31 -0
  228. package/scripts/release-loop/gate-diff.ts +29 -0
  229. package/scripts/release-loop/gate-node.ts +124 -29
  230. package/scripts/release-loop/known-issues-node.ts +159 -0
  231. package/scripts/release-loop/npm-publish-node.ts +118 -0
  232. package/scripts/release-loop/ops-upgrade-node.ts +87 -0
  233. package/scripts/release-loop/release-readiness.ts +58 -0
  234. package/scripts/release-loop/shard-plan.ts +47 -0
  235. package/scripts/release-loop/unattended-release.ts +93 -0
  236. package/scripts/ux/measure-first-text.ts +102 -0
  237. package/scripts/ux/record-page.ts +4 -1
  238. package/scripts/vflow-collect.ts +20 -7
  239. package/scripts/vflow-search.ts +73 -0
  240. package/scripts/video-free-line-test-walker.ts +36 -0
  241. package/scripts/video-pipeline.ts +2 -2
  242. package/skills/explainer-video/SKILL.md +24 -3
  243. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -1
  244. package/skills/explainer-video/engine/build.mjs +2 -2
  245. package/skills/explainer-video/engine/reel.mjs +235 -0
  246. package/skills/explainer-video/engine/reel.sh +9 -0
  247. package/skills/explainer-video/engine/vo.ts +58 -16
  248. package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
  249. package/skills/grill-me/SKILL.md +1 -1
  250. package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
  251. package/src/a2a/server.ts +2 -0
  252. package/src/acp/elanous-aware-peers.ts +30 -0
  253. package/src/acp/server.ts +91 -15
  254. package/src/agent/monad-agent-turn.ts +25 -6
  255. package/src/agent/shared-app-tools.ts +5 -3
  256. package/src/agent-substrate/review-intent.ts +6 -2
  257. package/src/agent-substrate/shard-boundary-intent.ts +70 -0
  258. package/src/auth/github-app-token.ts +4 -1
  259. package/src/autopilot/mission-origin.ts +20 -4
  260. package/src/autopilot/mission-phase-track.ts +11 -1
  261. package/src/autopilot/track-agent-shadow.ts +151 -0
  262. package/src/autopilot/track-agent.ts +26 -6
  263. package/src/autopilot/track-courier.ts +2 -2
  264. package/src/boot/acp-server-help.ts +16 -0
  265. package/src/boot/daemon-prompt-turn.ts +31 -10
  266. package/src/boot/daemon-tools/index.ts +7 -1
  267. package/src/budget/codex-credit-pace.ts +84 -0
  268. package/src/budget/codex-credit-plan.ts +154 -0
  269. package/src/budget/codex-reset-credit-state.ts +2 -0
  270. package/src/chat/chat-cli.ts +4 -1
  271. package/src/chat/short-question.ts +40 -0
  272. package/src/cli/agent-cli.ts +24 -15
  273. package/src/cli/ask-bare.ts +42 -0
  274. package/src/cli/cli-user-error.ts +25 -0
  275. package/src/cli/codex-credit-plan-view.ts +25 -0
  276. package/src/cli/dependence-gauge.ts +237 -0
  277. package/src/cli/doctor-cli.ts +5 -0
  278. package/src/cli/doctor-modes.ts +48 -0
  279. package/src/cli/intake-cli.ts +12 -2
  280. package/src/cli/intake-source-cli.ts +58 -0
  281. package/src/cli/machine-cli.ts +23 -0
  282. package/src/cli/msg-cli.ts +137 -0
  283. package/src/cli/nexus-ios-bind.ts +25 -8
  284. package/src/cli/phone-link.ts +117 -0
  285. package/src/cli/plugin-cli.ts +52 -0
  286. package/src/cli/pr-cli.ts +36 -0
  287. package/src/cli/private-ledger-commands.ts +31 -0
  288. package/src/cli/provider-cli.ts +5 -0
  289. package/src/cli/release-cli.ts +56 -12
  290. package/src/cli/release-run-if-ready.ts +48 -0
  291. package/src/cli/setup-cli.ts +27 -6
  292. package/src/cli/setup-recommend.ts +8 -0
  293. package/src/cli/usage-cli.ts +11 -1
  294. package/src/codex/setup.ts +27 -8
  295. package/src/dashboard/index.ts +1 -1
  296. package/src/discord.ts +56 -6
  297. package/src/domains/core-tools.ts +37 -14
  298. package/src/domains/outbound-alert.ts +55 -16
  299. package/src/exec-requests/planner.ts +140 -0
  300. package/src/exec-requests/runner.ts +338 -0
  301. package/src/exec-requests/store.ts +76 -0
  302. package/src/field/field-media.ts +250 -0
  303. package/src/field/field-reel.ts +196 -0
  304. package/src/grok/credential.ts +7 -5
  305. package/src/harness/aside-run-status.ts +33 -0
  306. package/src/harness/browser-verify.ts +48 -17
  307. package/src/harness/harness-cli-command.ts +25 -3
  308. package/src/harness/image-load-state.ts +21 -0
  309. package/src/harness/process-garbage.ts +45 -0
  310. package/src/index.ts +104 -38
  311. package/src/instance/leader.ts +13 -1
  312. package/src/intake-plane/collect-github.ts +5 -3
  313. package/src/intake-plane/collect-telegram-saved.ts +15 -3
  314. package/src/intake-plane/discord-seat-work.ts +56 -0
  315. package/src/intake-plane/intake-sources.ts +202 -0
  316. package/src/intake-plane/items.ts +28 -12
  317. package/src/intake-plane/seat-doc-route.ts +207 -0
  318. package/src/intake-plane/submit-intake-work.ts +9 -1
  319. package/src/intake-plane/telegram-seat-work.ts +72 -0
  320. package/src/llm/model-defaults.ts +24 -1
  321. package/src/llm.ts +5 -4
  322. package/src/machines/machine-ledger.ts +127 -0
  323. package/src/market/pack-internal-refs.ts +28 -0
  324. package/src/market/publish.ts +31 -4
  325. package/src/maturity/feature-maturity.ts +65 -0
  326. package/src/model-tier/turn-tier.ts +24 -0
  327. package/src/msg/msg-store.ts +149 -0
  328. package/src/native-tool-catalog.ts +2 -1
  329. package/src/nexus/api/chat-fast-path-config.ts +28 -0
  330. package/src/nexus/api/consult-requests.ts +117 -0
  331. package/src/nexus/api/discord-trigger-bot.ts +3 -0
  332. package/src/nexus/api/exec-requests.ts +51 -0
  333. package/src/nexus/api/field-uploads.ts +180 -0
  334. package/src/nexus/api/graph-approvals-feed.ts +140 -0
  335. package/src/nexus/api/graph-approvals.ts +120 -4
  336. package/src/nexus/api/harness-api.ts +219 -5
  337. package/src/nexus/api/http-server.ts +68 -5
  338. package/src/nexus/api/seat-requests.ts +254 -0
  339. package/src/nexus/api/trace-run-scan.ts +37 -0
  340. package/src/nexus/api/trace.ts +19 -3
  341. package/src/nexus/index.ts +3 -0
  342. package/src/nexus/setup-status.ts +4 -5
  343. package/src/notify/llm-budget-alert.ts +125 -0
  344. package/src/oauth/codex-account-rotation.ts +26 -4
  345. package/src/oauth/codex-account-store.ts +20 -2
  346. package/src/oauth/codex-browser-login.ts +173 -0
  347. package/src/oauth/codex.ts +4 -1
  348. package/src/oauth/fallback-chain.ts +5 -5
  349. package/src/oauth/store.ts +42 -5
  350. package/src/onboarding/continue-here.ts +29 -0
  351. package/src/onboarding/entry-hints.ts +8 -0
  352. package/src/onboarding/full-screen-io.ts +35 -3
  353. package/src/onboarding/help/skills.md +1 -1
  354. package/src/onboarding/non-interactive.ts +1 -0
  355. package/src/onboarding/subscription-hint.ts +27 -0
  356. package/src/onboarding/web-first.ts +83 -0
  357. package/src/onboarding.ts +230 -23
  358. package/src/plugins/maker/node-install.ts +92 -0
  359. package/src/plugins/maker/node-maker.ts +139 -0
  360. package/src/plugins/maker/plugin-maker.ts +220 -0
  361. package/src/policy/llm-policy.ts +268 -0
  362. package/src/prompt-library/session-guidance.ts +7 -1
  363. package/src/prompt-library/universal-preamble.ts +2 -2
  364. package/src/relay/channel.ts +184 -0
  365. package/src/relay/mux.ts +182 -0
  366. package/src/relay/relay-cli.ts +20 -0
  367. package/src/relay/relay-server.ts +220 -0
  368. package/src/relay/test-vectors.json +29 -0
  369. package/src/release-loop/checklist.ts +36 -9
  370. package/src/seat-address/seat-address.ts +34 -0
  371. package/src/self-dev/decompose.ts +1 -0
  372. package/src/self-dev/dev-pipeline.ts +6 -3
  373. package/src/self-dev/run-record-mirror.ts +44 -0
  374. package/src/self-dev/run-store.ts +12 -3
  375. package/src/self-implement/harness-policy-gates.ts +4 -1
  376. package/src/self-implement/report-origin.ts +12 -1
  377. package/src/self-implement/run-ledger-compact.ts +1 -1
  378. package/src/session/chat.ts +17 -5
  379. package/src/session/index.ts +23 -10
  380. package/src/skills/shared-agent-skill-roots.ts +25 -0
  381. package/src/steward/failure-streak.ts +78 -0
  382. package/src/steward/steward-cards.ts +24 -0
  383. package/src/steward/triage-budget.ts +127 -0
  384. package/src/steward/triage-plan.ts +75 -0
  385. package/src/steward/triage.ts +244 -42
  386. package/src/task-cards/card-store.ts +1 -1
  387. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  388. package/src/task-orchestrator/surfaces/pod-command-job.ts +12 -3
  389. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  390. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
  391. package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
  392. package/src/telegram.ts +145 -2
  393. package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
  394. package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
  395. package/src/user-config.ts +84 -4
  396. package/src/version/packed-revision.json +1 -1
  397. package/src/video-pipeline/recipes/character.ts +42 -5
  398. package/src/video-pipeline/recipes/explainer.ts +213 -0
  399. package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
  400. package/src/video-pipeline/recipes/free-line.ts +16 -3
  401. package/src/video-pipeline/recipes/remote.ts +7 -1
  402. package/src/video-pipeline/vflow-reference.ts +109 -0
  403. package/src/video-pipeline/walk-line.ts +3 -2
  404. package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +0 -25
  411. package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +0 -5
  419. package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/control/page-72bd384898d88955.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +0 -1
  477. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
  478. package/apps/pwa/out/_next/static/ktMqfrBbf8BMGac0jqLrb/_buildManifest.js +0 -1
  479. /package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
@@ -0,0 +1,127 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { debug } from '../debug/log.js';
3
+ import type { TriageDecision, TriageIssue } from './triage.js';
4
+
5
+ export interface HashedTriageDecision extends TriageDecision {
6
+ inputHash: string;
7
+ /** Other issues whose ref/body the judge read while producing this decision. */
8
+ contextInputs?: string[];
9
+ /** Retained from a previous run without a current judgment; never route as a fresh decision. */
10
+ deferred?: true;
11
+ }
12
+
13
+ export function triageInputHash(issue: TriageIssue, issues: TriageIssue[] = [], tracks?: Record<string, string>, contextInputs: string[] = []): string {
14
+ const byId = new Map(issues.map(item => [item.identifier, item]));
15
+ return createHash('sha256').update(JSON.stringify([
16
+ 'context-inputs-v2', issue.identifier, issue.ref, issue.title, issue.body,
17
+ issues.map(item => [item.identifier, item.title]), tracks && Object.keys(tracks).length ? tracks : null,
18
+ contextInputs.map(id => [id, byId.get(id)?.ref ?? null, byId.get(id)?.body ?? null]),
19
+ ])).digest('hex');
20
+ }
21
+
22
+ export async function triageWithinBudget({ issues, previous, judge, tracks, concurrency = 8, perIssueMs = 90_000, deadlineMs = 240_000, now = Date.now, onJudged }: {
23
+ issues: TriageIssue[];
24
+ previous: Array<TriageDecision & { inputHash?: string }>;
25
+ judge: (issue: TriageIssue, observedIssues?: TriageIssue[]) => Promise<TriageDecision>;
26
+ tracks?: Record<string, string>;
27
+ concurrency?: number;
28
+ perIssueMs?: number;
29
+ deadlineMs?: number;
30
+ now?: () => number;
31
+ onJudged?: (decision: HashedTriageDecision) => void;
32
+ }): Promise<{ decisions: HashedTriageDecision[]; stats: { reused: number; judged: number; timedOut: number; deferred: number } }> {
33
+ const end = now() + deadlineMs;
34
+ const slots = Number.isFinite(concurrency) ? Math.max(1, Math.floor(concurrency)) : 4;
35
+ const stats = { reused: 0, judged: 0, timedOut: 0, deferred: 0 };
36
+ const old = new Map(previous.map(decision => [decision.issue, decision]));
37
+ const isTemporary = (decision: TriageDecision): boolean =>
38
+ decision.rung === 'hitl' && decision.hitlReason === 'other' &&
39
+ ['triage deferred — deadline', 'triage judgment timed out — human review',
40
+ 'triage judgment unavailable — human review'].includes(decision.why);
41
+ const decisions: HashedTriageDecision[] = new Array(issues.length);
42
+ const pending: number[] = [];
43
+ for (const [index, issue] of issues.entries()) {
44
+ const earlier = old.get(issue.identifier) as HashedTriageDecision | undefined;
45
+ const hash = triageInputHash(issue, issues, tracks, earlier?.contextInputs ?? []);
46
+ if (earlier?.inputHash === hash && !earlier.deferred && !isTemporary(earlier)) {
47
+ decisions[index] = { ...earlier, inputHash: hash };
48
+ stats.reused++;
49
+ } else pending.push(index);
50
+ }
51
+
52
+ let cursor = 0;
53
+ const activeWork = new Set<Promise<TriageDecision>>();
54
+ const settleSignals = new Set<Promise<boolean>>();
55
+ const timedOut = Symbol('triage-timeout');
56
+ const worker = async (): Promise<void> => {
57
+ while (cursor < pending.length) {
58
+ const remaining = end - now();
59
+ if (remaining <= 0) return;
60
+ if (activeWork.size >= slots) {
61
+ let timer: ReturnType<typeof setTimeout> | undefined;
62
+ const settled = Promise.race([...settleSignals]);
63
+ let deadlineElapsed: boolean;
64
+ try {
65
+ deadlineElapsed = await Promise.race([settled, new Promise<true>(resolve => { timer = setTimeout(() => resolve(true), remaining); })]);
66
+ } finally { if (timer) clearTimeout(timer); }
67
+ if (deadlineElapsed) return;
68
+ continue;
69
+ }
70
+ const index = pending[cursor++]!;
71
+ const issue = issues[index]!;
72
+ const observed = new Set<string>();
73
+ const observedIssues = issues.map(item => new Proxy(item, {
74
+ get(target, property, receiver) {
75
+ if (item.identifier !== issue.identifier && (property === 'ref' || property === 'body')) observed.add(item.identifier);
76
+ return Reflect.get(target, property, receiver);
77
+ },
78
+ }));
79
+ // Check immediately before invoking; a clock that advanced while preparing inputs must not start a new call.
80
+ const issueRemaining = end - now();
81
+ if (issueRemaining <= 0) { cursor--; return; }
82
+ let timer: ReturnType<typeof setTimeout> | undefined;
83
+ let decision: TriageDecision;
84
+ try {
85
+ const work = Promise.resolve(judge(issue, observedIssues));
86
+ // Even a timed-out judge keeps its concurrency slot until the underlying call settles.
87
+ activeWork.add(work);
88
+ const settled = work.then(() => false, () => false);
89
+ void settled.then(() => {
90
+ activeWork.delete(work);
91
+ settleSignals.delete(settled);
92
+ });
93
+ settleSignals.add(settled);
94
+ const timeout = new Promise<never>((_, reject) => {
95
+ timer = setTimeout(() => reject(timedOut), Math.min(perIssueMs, issueRemaining));
96
+ });
97
+ decision = await Promise.race([work, timeout]);
98
+ stats.judged++;
99
+ } catch (error) {
100
+ if (error === timedOut) {
101
+ stats.timedOut++;
102
+ decision = { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0, why: 'triage judgment timed out — human review' };
103
+ } else {
104
+ stats.judged++;
105
+ decision = { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0, why: 'triage judgment unavailable — human review' };
106
+ }
107
+ } finally { if (timer) clearTimeout(timer); }
108
+ const contextInputs = [...observed].sort();
109
+ decisions[index] = { ...decision, inputHash: triageInputHash(issue, issues, tracks, contextInputs),
110
+ ...(contextInputs.length ? { contextInputs } : {}) };
111
+ onJudged?.(decisions[index]!);
112
+ }
113
+ };
114
+ await Promise.all(Array.from({ length: Math.min(slots, pending.length) }, () => worker()));
115
+ for (const index of pending.slice(cursor)) {
116
+ const issue = issues[index]!;
117
+ const earlier = old.get(issue.identifier);
118
+ decisions[index] = earlier
119
+ ? { ...earlier, inputHash: earlier.inputHash ?? '', deferred: true }
120
+ : { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0,
121
+ why: 'triage deferred — deadline', inputHash: triageInputHash(issue, issues, tracks), deferred: true };
122
+ if (!earlier) onJudged?.(decisions[index]!);
123
+ stats.deferred++;
124
+ }
125
+ debug.log('steward.triage', 'budget', stats);
126
+ return { decisions, stats };
127
+ }
@@ -0,0 +1,75 @@
1
+ import { resolveRoleModel } from '../user-config.js';
2
+ import type { HitlReason, Rung, TriageIssue } from './triage.js';
3
+
4
+ export type StewardAsk = (prompt: string, role: 'classify' | 'planning') => Promise<unknown>;
5
+ export interface ClassifiedIssue { issue: string; rung: Rung; why: string; hitlReason?: HitlReason; duplicateOf?: string }
6
+ export interface IssuePlan { issue: string; priority: number; dependsOn: string[]; owner?: string }
7
+
8
+ const unsafe: Array<[HitlReason, RegExp]> = [
9
+ ['money', /돈|유료|결제|구매|판매|송금|환불|크레딧\s*(?:추가|구매)|paid|payment|purchase/i],
10
+ ['public', /공개|발행|외부\s*게시|publish|public\s*release/i],
11
+ ['security', /보안|(?:API|보안|인증|접근)\s*키|(?:키|자격|권한|secret|credential|permission)\s*(?:변경|교체|발급|삭제|공유|노출)|키체인|\b(?:api[ -]?key|secret[ -]?key|access[ -]?token)\b/i],
12
+ ['irreversible', /비가역|강제\s*푸시|force.push|영구\s*삭제|리셋권\s*소비|drop\s*table|초기화/i],
13
+ ];
14
+
15
+ export function unsafeReason(issue: TriageIssue): HitlReason | undefined {
16
+ return unsafe.find(([, pattern]) => pattern.test(`${issue.title}\n${issue.body}`))?.[0];
17
+ }
18
+
19
+ function parsed(raw: unknown): Record<string, unknown> {
20
+ const value: unknown = typeof raw === 'string'
21
+ ? JSON.parse(raw.replace(/^```(?:json)?\s*|\s*```$/g, '').trim()) : raw;
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid steward response');
23
+ return value as Record<string, unknown>;
24
+ }
25
+
26
+ export function ruleJudgment(issue: TriageIssue, issues: TriageIssue[]): ClassifiedIssue | undefined {
27
+ const normalize = (text: string) => text.normalize('NFKC').trim().replace(/\s+/g, ' ').toLocaleLowerCase();
28
+ const title = normalize(issue.title);
29
+ const index = issues.findIndex(item => item.identifier === issue.identifier);
30
+ const prior = index < 0 ? issues.filter(item => item.identifier !== issue.identifier) : issues.slice(0, index);
31
+ // Same title alone is not proof: two different tasks can share a title. Only same title AND same body is a
32
+ // rule duplicate; anything else goes to classification.
33
+ const body = normalize(issue.body ?? '');
34
+ const duplicate = title && prior.find(item => normalize(item.title) === title && normalize(item.body ?? '') === body);
35
+ if (duplicate) return { issue: issue.identifier, rung: 0, duplicateOf: duplicate.identifier, why: `duplicate of ${duplicate.identifier}` };
36
+ return undefined;
37
+ }
38
+
39
+ export async function askSteward(prompt: string, role: 'classify' | 'planning'): Promise<unknown> {
40
+ const { streamLLM } = await import('../llm.js');
41
+ const result = await streamLLM([{ role: 'user', content: prompt }], () => {}, { model: resolveRoleModel(role).model });
42
+ return parsed(result);
43
+ }
44
+
45
+ export async function classifyIssue(issue: TriageIssue, ask: StewardAsk = askSteward): Promise<ClassifiedIssue> {
46
+ const candidate = unsafeReason(issue);
47
+ const response = parsed(await ask(`스튜어드 분류. JSON 객체만: {"rung":0|1|2|3|4|5|"hitl","hitlReason":null,"why":"한 문장"}. 0=이미됨, 1=셸, 2=CLI, 3=조사, 4=하니스 골, 5=외부 에이전트. 돈·공개·보안·비가역은 hitl; 그 외 사람 확인은 other. 확인되지 않은 완료를 추측하지 마라. ${candidate ? `안전 후보 ${candidate}: 이유를 확인하라.` : ''}\n이슈: ${JSON.stringify(issue)}`, 'classify'));
48
+ if (![0, 1, 2, 3, 4, 5, 'hitl'].includes(response.rung as Rung) || typeof response.why !== 'string' || !response.why.trim()) throw new Error(`Invalid steward classification: ${issue.identifier}`);
49
+ const reason = candidate ?? (unsafe.some(([name]) => name === response.hitlReason) ? response.hitlReason as HitlReason : undefined)
50
+ ?? (response.rung === 'hitl' ? 'other' : undefined);
51
+ return { issue: issue.identifier, rung: reason ? 'hitl' : response.rung as Rung,
52
+ ...(reason ? { hitlReason: reason } : {}), why: response.why.trim().replace(/\s+/g, ' ') };
53
+ }
54
+
55
+ /** One request for the current batch, never one request per issue. */
56
+ export async function planIssues(classified: ClassifiedIssue[], ask: StewardAsk = askSteward, tracks?: Record<string, string>, issues: TriageIssue[] = []): Promise<IssuePlan[]> {
57
+ if (!classified.length) return [];
58
+ const keys = Object.keys(tracks ?? {});
59
+ const byId = new Map(issues.map(issue => [issue.identifier, issue]));
60
+ const response = parsed(await ask(`스튜어드 계획. 새로 분류한 이슈마다 priority(숫자), dependsOn(제공된 이슈 키 배열), owner(담당 트랙 키 또는 null)를 정해 JSON {"plans":[{"issue":"키","priority":1,"dependsOn":[],"owner":null}]} 만 출력. 새로 분류한 이슈만 한 번씩 포함하라. 다른 키를 의존으로 넣지 마라. 담당 트랙: ${JSON.stringify(tracks ?? {})}\n제공된 이슈 키: ${JSON.stringify(issues.map(row => ({ key: row.identifier, title: row.title })))}\n새로 분류한 이슈: ${JSON.stringify(classified.map(row => ({ ...row, title: byId.get(row.issue)?.title ?? '', body: byId.get(row.issue)?.body ?? '' })))}`, 'planning'));
61
+ const plans = response.plans;
62
+ if (!Array.isArray(plans) || plans.length !== classified.length) throw new Error('Invalid steward plan');
63
+ const allowed = new Set(issues.length ? issues.map(row => row.identifier) : classified.map(row => row.issue));
64
+ const planned = new Set(classified.map(row => row.issue));
65
+ const seen = new Set<string>();
66
+ return plans.map((raw: unknown) => {
67
+ const row = parsed(raw);
68
+ if (typeof row.issue !== 'string' || !planned.has(row.issue) || seen.has(row.issue) ||
69
+ typeof row.priority !== 'number' || !Number.isFinite(row.priority) ||
70
+ !Array.isArray(row.dependsOn) || !row.dependsOn.every((dep: unknown) => typeof dep === 'string' && allowed.has(dep) && dep !== row.issue)) throw new Error('Invalid steward plan');
71
+ seen.add(row.issue);
72
+ return { issue: row.issue, priority: row.priority, dependsOn: row.dependsOn as string[],
73
+ ...(typeof row.owner === 'string' && keys.includes(row.owner) ? { owner: row.owner } : {}) };
74
+ });
75
+ }
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
1
+ import { closeSync, existsSync, openSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { fetchLinearIssues } from '../connectors/linear.js';
4
4
  import { getSecretAsync } from '../nexus/config/secrets/index.js';
@@ -9,6 +9,9 @@ import { effectiveInstanceRoot } from '../instance/resolve.js';
9
9
  import { redactSecrets } from '../task-cards/card-store.js';
10
10
  import { directiveHash } from './directive.js';
11
11
  import { recordTriageOnCards, type StewardCardDeps } from './steward-cards.js';
12
+ import { triageWithinBudget } from './triage-budget.js';
13
+ import { askSteward, classifyIssue, planIssues, ruleJudgment, unsafeReason, type ClassifiedIssue, type StewardAsk } from './triage-plan.js';
14
+ import { failureAlertText, markStageStart, markStageEnd, readFailureStreak, shouldAlert, writeFailureStreak, type FailureStreak, type StewardStage } from './failure-streak.js';
12
15
 
13
16
  export type Rung = 0 | 1 | 2 | 3 | 4 | 5 | 'hitl';
14
17
  /** `other` = the judge chose human review for a reason outside the four named ones (kept as HITL, never downgraded). */
@@ -19,6 +22,8 @@ export interface TriageDecision {
19
22
  hitlReason?: HitlReason; why: string; role?: string; cost?: number;
20
23
  /** Owner track from `loops.steward.tracks` (absent when no table is configured or the judge picked none of its keys). */
21
24
  owner?: string;
25
+ /** Carried from a previous triage without a current judgment; scheduling must not route it. */
26
+ deferred?: true;
22
27
  }
23
28
  export interface ScheduledDecision extends TriageDecision { disposition: 'now' | 'wait' | 'hitl' }
24
29
  export interface StewardSettings {
@@ -26,6 +31,7 @@ export interface StewardSettings {
26
31
  roles?: Record<string, { maxConcurrent?: number }>;
27
32
  budget?: number;
28
33
  tracks?: Record<string, string>;
34
+ alertAfterFailures?: number;
29
35
  }
30
36
  export interface StewardDeps {
31
37
  fetch?: typeof fetch;
@@ -37,18 +43,15 @@ export interface StewardDeps {
37
43
  root?: string;
38
44
  cardStore?: StewardCardDeps['store'];
39
45
  warn?: (message: string) => void;
46
+ ask?: StewardAsk;
47
+ /** Internal command boundary: the current sync has already claimed this pid. */
48
+ stageOwnedByCommand?: boolean;
40
49
  }
41
50
 
42
51
  export function stewardSettings(): StewardSettings {
43
52
  return getUserConfig().loops?.steward ?? {};
44
53
  }
45
54
 
46
- const unsafe: Array<[HitlReason, RegExp]> = [
47
- ['money', /돈|유료|결제|구매|판매|송금|환불|크레딧\s*(?:추가|구매)|paid|payment|purchase/i],
48
- ['public', /공개|발행|외부\s*게시|publish|public\s*release/i],
49
- ['security', /보안|(?:키|자격|권한|secret|credential|permission)\s*(?:변경|교체|발급|삭제|공유|노출)|키체인/i],
50
- ['irreversible', /비가역|강제\s*푸시|force.push|영구\s*삭제|리셋권\s*소비|drop\s*table|초기화/i],
51
- ];
52
55
 
53
56
  function parseJudgment(issue: TriageIssue, raw: unknown, tracks?: Record<string, string>): TriageDecision {
54
57
  const value = typeof raw === 'string' ? JSON.parse(raw) as unknown : raw;
@@ -59,8 +62,8 @@ function parseJudgment(issue: TriageIssue, raw: unknown, tracks?: Record<string,
59
62
  typeof d.priority !== 'number' || !Number.isFinite(d.priority) || typeof d.why !== 'string' || !d.why.trim()) {
60
63
  throw new Error(`Invalid triage judgment: ${issue.identifier}`);
61
64
  }
62
- const forced = unsafe.find(([, pattern]) => pattern.test(`${issue.title}\n${issue.body}`))?.[0];
63
- const named = forced ?? (unsafe.some(([reason]) => reason === d.hitlReason) ? d.hitlReason as HitlReason : undefined);
65
+ const forced = unsafeReason(issue);
66
+ const named = forced ?? (['money', 'public', 'security', 'irreversible'].includes(d.hitlReason as string) ? d.hitlReason as HitlReason : undefined);
64
67
  // A HITL judgment without one of the four named reasons stays HITL (`other`) — one issue must not stop the stage.
65
68
  const hitlReason = named ?? (d.rung === 'hitl' ? 'other' as const : undefined);
66
69
  return {
@@ -81,30 +84,68 @@ export function trackTablePrompt(tracks?: Record<string, string>): string {
81
84
  return `\n담당 트랙(owner) — 아래 중 하나(${keys.map(k => JSON.stringify(k)).join('|')}) · 이슈가 어느 트랙 일인지:\n${keys.map(k => `- ${k}: ${tracks[k]}`).join('\n')}`;
82
85
  }
83
86
 
84
- async function judgeStewardIssue(issue: TriageIssue, issues: TriageIssue[], tracks?: Record<string, string>): Promise<unknown> {
85
- const ownerField = tracks && Object.keys(tracks).length ? `,"owner":${JSON.stringify(Object.keys(tracks)[0])}` : '';
86
- const prompt = `스튜어드 트리아지. JSON 객체만 출력: {"rung":0|1|2|3|4|5|"hitl","dependsOn":[],"priority":1,"duplicateOf":null,"hitlReason":null,"why":"한 문장","role":"builder","cost":0${ownerField}}.\n0=이미됨/중복, 1=셸, 2=CLI, 3=조사, 4=하니스 골, 5=외부 에이전트. 돈·공개·보안·비가역은 hitl(hitlReason = "money"|"public"|"security"|"irreversible" · 그 밖의 이유로 사람 확인이 필요하면 "other"). 확인되지 않은 완료/중복은 추측하지 마라. 의존은 제공된 이슈 키만 써라.${trackTablePrompt(tracks)}\n이슈 목록: ${JSON.stringify(issues.map(i => ({ key: i.identifier, title: i.title })))}\n판정 대상: ${JSON.stringify(issue)}`;
87
- const { streamLLM } = await import('../llm.js');
88
- const { tierModel } = await import('../llm/model-defaults.js');
89
- const result = await streamLLM([{ role: 'user', content: prompt }], () => {}, { model: tierModel('best') });
90
- return JSON.parse(result.replace(/^```(?:json)?\s*|\s*```$/g, '').trim()) as unknown;
91
- }
92
-
93
- export async function triageIssues(issues: TriageIssue[], judge: NonNullable<StewardDeps['judge']> = judgeStewardIssue, decide: typeof emitDecision = emitDecision, tracks?: Record<string, string>): Promise<TriageDecision[]> {
94
- const decisions: TriageDecision[] = [];
95
- for (const issue of issues) {
96
- let result: TriageDecision;
87
+ export async function triageIssues(issues: TriageIssue[], judge?: NonNullable<StewardDeps['judge']>, decide: typeof emitDecision = emitDecision, tracks?: Record<string, string>, previous: Array<TriageDecision & { inputHash?: string }> = [], ask: StewardAsk = askSteward): Promise<TriageDecision[]> {
88
+ const started = judge ? 0 : Date.now();
89
+ const classified: ClassifiedIssue[] = [];
90
+ const classifiedIds = new Set<string>();
91
+ const counts = { rule: 0, classify: 0, planning: 0 };
92
+ const signal = (result: TriageDecision): void => {
93
+ decide({ kind: result.rung === 'hitl' ? 'ESCALATE' : 'ROUTE', what: result.issue, reason: result.why,
94
+ purpose: 'steward triage', target: result.owner ? `${result.rung} · ${result.owner}` : String(result.rung), refs: { issue: result.issue } });
95
+ };
96
+ const { decisions } = await triageWithinBudget({ issues, previous, tracks, judge: async (issue, observedIssues) => {
97
97
  try {
98
- result = parseJudgment(issue, await judge(issue, issues, tracks), tracks);
98
+ if (judge) return parseJudgment(issue, await judge(issue, observedIssues ?? issues, tracks), tracks);
99
+ const byRule = ruleJudgment(issue, observedIssues ?? issues);
100
+ if (byRule) { counts.rule++; return { ...byRule, dependsOn: [], priority: 0 }; }
101
+ counts.classify++;
102
+ const row = await classifyIssue(issue, ask);
103
+ return { ...row, dependsOn: [], priority: 0 };
99
104
  } catch (error) {
100
- // A malformed or failed judgment for one issue goes to a human; the other issues are still triaged.
101
105
  try { debug.log('steward.stage', 'judgment-invalid', { issue: issue.identifier, reason: redactSecrets(String(error)) }); } catch { /* fail-soft */ }
102
- result = { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0, why: 'triage judgment unavailable — human review' };
106
+ return { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0, why: 'triage judgment unavailable — human review' };
107
+ }
108
+ }, onJudged: result => {
109
+ if (!judge && !result.deferred && !result.duplicateOf &&
110
+ result.why !== 'triage judgment unavailable — human review' &&
111
+ result.why !== 'triage judgment timed out — human review' &&
112
+ result.why !== 'triage deferred — deadline') {
113
+ classified.push({ issue: result.issue, rung: result.rung, why: result.why,
114
+ ...(result.hitlReason ? { hitlReason: result.hitlReason } : {}) });
115
+ classifiedIds.add(result.issue);
116
+ } else signal(result);
117
+ } });
118
+ const planningBatch = classified.slice();
119
+ if (!judge && planningBatch.length) {
120
+ const remaining = 240_000 - (Date.now() - started);
121
+ try {
122
+ if (remaining <= 0) throw new Error('planning deadline');
123
+ counts.planning++;
124
+ let timer: ReturnType<typeof setTimeout> | undefined;
125
+ let plans;
126
+ try {
127
+ plans = await Promise.race([planIssues(planningBatch, ask, tracks, issues), new Promise<never>((_, reject) => {
128
+ timer = setTimeout(() => reject(new Error('planning deadline')), remaining);
129
+ })]);
130
+ } finally { if (timer) clearTimeout(timer); }
131
+ const byIssue = new Map(plans.map(plan => [plan.issue, plan]));
132
+ for (const row of decisions) {
133
+ const plan = byIssue.get(row.issue);
134
+ if (plan && !row.deferred) {
135
+ row.priority = plan.priority;
136
+ row.dependsOn = plan.dependsOn;
137
+ if (plan.owner) row.owner = plan.owner;
138
+ }
139
+ }
140
+ } catch (error) {
141
+ debug.log('steward.stage', 'planning-invalid', { reason: redactSecrets(String(error)) });
142
+ for (const row of decisions) if (classifiedIds.has(row.issue) && !row.deferred && !row.duplicateOf) {
143
+ row.rung = 'hitl'; row.hitlReason = 'other'; row.why = 'triage judgment unavailable — human review';
144
+ }
103
145
  }
104
- decisions.push(result);
105
- decide({ kind: result.rung === 'hitl' ? 'ESCALATE' : 'ROUTE', what: issue.identifier, reason: result.why,
106
- purpose: 'steward triage', target: result.owner ? `${result.rung} · ${result.owner}` : String(result.rung), refs: { issue: issue.identifier } });
146
+ for (const row of decisions) if (classifiedIds.has(row.issue) && !row.deferred) signal(row);
107
147
  }
148
+ debug.log('steward.triage', 'llm-calls', counts);
108
149
  return decisions;
109
150
  }
110
151
 
@@ -128,7 +169,7 @@ export function scheduleTriage(decisions: TriageDecision[], settings: StewardSet
128
169
  const role = item.owner ?? item.role ?? String(item.rung);
129
170
  const max = settings.roles?.[role]?.maxConcurrent ?? Infinity;
130
171
  const blocked = item.dependsOn.some(dep => !completed.has(dep));
131
- const status: ScheduledDecision['disposition'] = item.rung === 'hitl' || item.hitlReason ? 'hitl' :
172
+ const status: ScheduledDecision['disposition'] = item.deferred || item.rung === 'hitl' || item.hitlReason ? 'hitl' :
132
173
  item.rung === 0 || item.duplicateOf || blocked || (slots[role] ?? 0) >= max || (item.cost ?? 0) > remaining ? 'wait' : 'now';
133
174
  if (status === 'now') { slots[role] = (slots[role] ?? 0) + 1; remaining -= item.cost ?? 0; }
134
175
  return { ...item, disposition: status };
@@ -147,6 +188,44 @@ async function linear<T>(key: string, query: string, variables: Record<string, u
147
188
  return body.data;
148
189
  }
149
190
 
191
+ function graphRunId(): string | undefined {
192
+ const path = process.env.ELANOUS_GRAPH_CONTEXT;
193
+ if (!path) return undefined;
194
+ try {
195
+ const context = JSON.parse(readFileSync(path, 'utf8')) as { graphId?: string; runId?: string };
196
+ return context.graphId === 'steward' && /^[a-zA-Z0-9._-]+$/.test(context.runId ?? '') ? context.runId : undefined;
197
+ } catch { return undefined; }
198
+ }
199
+
200
+ function isProcessAlive(pid: number): boolean {
201
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
202
+ try { process.kill(pid, 0); return true; }
203
+ catch (error) { return (error as NodeJS.ErrnoException).code === 'EPERM'; }
204
+ }
205
+
206
+ function skippedPath(root: string): string {
207
+ // A graph run has a stable id across command nodes. Without a graph context,
208
+ // keep the skip until the next sync: stage subprocess parents may differ.
209
+ const runId = graphRunId();
210
+ return join(root, 'steward', runId ? `skipped-${runId}.json` : 'skipped-no-context.json');
211
+ }
212
+
213
+ function currentOpen(root: string, stage: StewardStage): FailureStreak['open'] & { pid?: number; runId?: string } {
214
+ const open = readFailureStreak(root).open;
215
+ return open ?? { stage, startedAt: new Date().toISOString() };
216
+ }
217
+
218
+ function skipOverlappingRun(root: string, open: FailureStreak['open'] & { pid?: number; runId?: string }): void {
219
+ // The skip marker is shared by every stage of one graph run. A loser inside the SAME run must not write it,
220
+ // or the winner's own triage·schedule·report would skip too — it just exits.
221
+ if (open.runId && open.runId === graphRunId()) {
222
+ debug.log('steward.stage', 'skipped-overlap-same-run', { openStage: open.stage, pid: open.pid });
223
+ return;
224
+ }
225
+ debug.log('steward.stage', 'skipped-overlap', { openStage: open.stage, pid: open.pid });
226
+ writeFileSync(skippedPath(root), JSON.stringify({ openStage: open.stage, pid: open.pid }));
227
+ }
228
+
150
229
  function reportPath(root: string): string { return join(root, 'steward', 'observe.json'); }
151
230
  function readState(path: string): { issues: Record<string, string>; digestDay?: string } {
152
231
  try { return JSON.parse(readFileSync(path, 'utf8')) as ReturnType<typeof readState>; }
@@ -157,30 +236,66 @@ function readState(path: string): { issues: Record<string, string>; digestDay?:
157
236
  export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'report', deps: StewardDeps = {}): Promise<void> {
158
237
  const settings = stewardSettings();
159
238
  if ((settings.mode ?? 'observe') !== 'observe') throw new Error('steward act mode is out of scope');
160
- const key = await (deps.getSecret ?? getSecretAsync)('connector.linear.apiKey');
161
- if (!key) throw new Error('connector.linear.apiKey missing');
162
239
  const root = deps.root ?? effectiveInstanceRoot();
240
+ if (stage === 'sync' && !deps.stageOwnedByCommand) {
241
+ return withStageSlot(root, async () => {
242
+ const open = readFailureStreak(root).open as (FailureStreak['open'] & { pid?: number }) | undefined;
243
+ if (open?.pid && isProcessAlive(open.pid)) { skipOverlappingRun(root, open); return; }
244
+ rmSync(skippedPath(root), { force: true });
245
+ await runStewardStage(stage, { ...deps, stageOwnedByCommand: true });
246
+ }, () => { skipOverlappingRun(root, currentOpen(root, stage)); });
247
+ }
163
248
  const dir = join(root, 'steward');
164
249
  mkdirSync(dir, { recursive: true });
165
250
  const snapshot = join(dir, 'issues.json');
166
251
  const judgments = join(dir, 'triage.json');
167
252
  const schedule = join(dir, 'schedule.json');
168
253
  const fetchFn = deps.fetch ?? fetch;
254
+ const skip = skippedPath(root);
255
+ if (stage !== 'sync' && existsSync(skip)) return;
256
+ const key = await (deps.getSecret ?? getSecretAsync)('connector.linear.apiKey');
257
+ if (!key) throw new Error('connector.linear.apiKey missing');
169
258
  if (stage === 'sync') {
170
259
  const events = await fetchLinearIssues({ apiKey: key, teamKey: settings.linearTeam ?? 'ELA', fetch: fetchFn });
171
260
  writeFileSync(snapshot, JSON.stringify(events.map(e => ({ identifier: e.identifier, ref: e.ref, title: e.title, body: e.body }))));
172
261
  } else if (stage === 'triage') {
173
262
  const issues = JSON.parse(readFileSync(snapshot, 'utf8')) as TriageIssue[];
174
- const judge = deps.judge ?? judgeStewardIssue;
175
- writeFileSync(judgments, JSON.stringify(await triageIssues(issues, judge, deps.decide, settings.tracks)));
263
+ const judge = deps.judge;
264
+ let previous: Array<TriageDecision & { inputHash?: string }> = [];
265
+ try {
266
+ const parsed: unknown = JSON.parse(readFileSync(judgments, 'utf8'));
267
+ if (Array.isArray(parsed) && parsed.every((item): item is TriageDecision & { inputHash?: string } =>
268
+ item !== null && typeof item === 'object' && typeof item.issue === 'string' &&
269
+ (item.inputHash === undefined || typeof item.inputHash === 'string') &&
270
+ (item.deferred === undefined || item.deferred === true) &&
271
+ (item.contextInputs === undefined || (Array.isArray(item.contextInputs) &&
272
+ item.contextInputs.every((id: unknown) => typeof id === 'string'))) &&
273
+ [0, 1, 2, 3, 4, 5, 'hitl'].includes(item.rung) &&
274
+ Array.isArray(item.dependsOn) && item.dependsOn.every((dep: unknown) => typeof dep === 'string') &&
275
+ typeof item.priority === 'number' && Number.isFinite(item.priority) && typeof item.why === 'string')) previous = parsed;
276
+ } catch { /* Missing or corrupt previous judgments are an empty cache. */ }
277
+ writeFileSync(judgments, JSON.stringify(await triageIssues(issues, judge, deps.decide, settings.tracks, previous, deps.ask)));
176
278
  } else if (stage === 'schedule') {
177
279
  const decisions = JSON.parse(readFileSync(judgments, 'utf8')) as TriageDecision[];
178
280
  const pending = new Set(decisions.flatMap(decision => decision.dependsOn));
179
281
  const completed = new Set<string>();
180
- for (const identifier of pending) {
181
- const data = await linear<{ issue: { identifier: string; state?: { type: string } } | null }>(key,
182
- 'query($id:String!){issue(id:$id){identifier state{type}}}', { id: identifier }, fetchFn);
183
- if (data.issue?.identifier === identifier && data.issue.state?.type === 'completed') completed.add(identifier);
282
+ if (pending.size) {
283
+ const ids = [...pending];
284
+ for (let offset = 0; offset < ids.length; offset += 250) {
285
+ const batch = ids.slice(offset, offset + 250);
286
+ let cursor: string | null = null;
287
+ do {
288
+ const data: { issues: { nodes: Array<{ identifier: string; state?: { type: string } }>; pageInfo: { hasNextPage: boolean; endCursor: string | null } } } = await linear(key,
289
+ 'query($ids:[String!]!,$after:String){issues(filter:{identifier:{in:$ids}},first:250,after:$after){nodes{identifier state{type}} pageInfo{hasNextPage endCursor}}}',
290
+ { ids: batch, after: cursor }, fetchFn);
291
+ if (!Array.isArray(data.issues?.nodes) || !data.issues.pageInfo ||
292
+ (data.issues.pageInfo.hasNextPage && (!data.issues.pageInfo.endCursor || data.issues.pageInfo.endCursor === cursor))) {
293
+ throw new Error('Linear dependencies incomplete');
294
+ }
295
+ for (const item of data.issues.nodes) if (pending.has(item.identifier) && item.state?.type === 'completed') completed.add(item.identifier);
296
+ cursor = data.issues.pageInfo.hasNextPage ? data.issues.pageInfo.endCursor : null;
297
+ } while (cursor);
298
+ }
184
299
  }
185
300
  writeFileSync(schedule, JSON.stringify(scheduleTriage(decisions, settings, {}, completed)));
186
301
  } else {
@@ -222,17 +337,104 @@ async function sendStewardDigest(text: string): Promise<void> {
222
337
  if (!deliver(text, 'digest')) throw new Error('steward digest delivery failed');
223
338
  }
224
339
 
340
+ export interface StewardCommandDeps extends StewardDeps {
341
+ runStage?: typeof runStewardStage;
342
+ deliverAlert?: (text: string, kind: 'alert') => boolean | Promise<boolean>;
343
+ }
344
+
345
+ // One stage at a time per instance: an exclusive flock(2) on a stable file, taken in-process through libc
346
+ // (as graph-tick does). The kernel drops it when the process dies, so a killed stage never leaves a stale lock.
347
+ // The `flock` command-line tool is Linux-only — on macOS, where the steward cron runs, it does not exist (10-01).
348
+ const STAGE_LOCK_WAIT_MS = 15 * 60_000;
349
+
350
+ async function withStageSlot<T>(root: string, run: () => Promise<T>, onBusy?: () => T): Promise<T> {
351
+ const dir = join(root, 'steward');
352
+ mkdirSync(dir, { recursive: true });
353
+ const library = process.platform === 'darwin' ? '/usr/lib/libSystem.B.dylib' : process.platform === 'linux' ? 'libc.so.6' : null;
354
+ if (!library) throw new Error(`steward stage lock unavailable on ${process.platform}`);
355
+ const { dlopen, FFIType } = await import('bun:ffi');
356
+ const lib = dlopen(library, { flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 } });
357
+ const flock = (fd: number, operation: number): number => lib.symbols.flock(fd, operation) as number;
358
+ const fd = openSync(join(dir, 'stage-slot.lock'), 'a');
359
+ // LOCK_EX|LOCK_NB (2|4), retried on a timer: a blocking FFI call would freeze the event loop, and a holder in the
360
+ // same process could then never release (the lock is per open file, so a second open in one process contends too).
361
+ // With onBusy (sync), one try only: a busy slot means another run is in flight — skip instead of waiting.
362
+ if (onBusy && flock(fd, 2 | 4) !== 0) { closeSync(fd); lib.close(); return onBusy(); }
363
+ const deadline = Date.now() + STAGE_LOCK_WAIT_MS;
364
+ while (flock(fd, 2 | 4) !== 0) {
365
+ if (Date.now() >= deadline) { closeSync(fd); lib.close(); throw new Error('steward stage lock unavailable: another stage is still running'); }
366
+ await new Promise(resolve => setTimeout(resolve, 200));
367
+ }
368
+ try { return await run(); }
369
+ finally { try { flock(fd, 8); } finally { closeSync(fd); lib.close(); } }
370
+ }
371
+
372
+ /** The command-node boundary records both normal exits and an unclosed stage from a killed process. */
373
+ export async function runStewardStageCommand(stage: StewardStage, deps: StewardCommandDeps = {}): Promise<0 | 1> {
374
+ const root = deps.root ?? effectiveInstanceRoot();
375
+ if (stage !== 'sync' && existsSync(skippedPath(root))) return 0;
376
+ return withStageSlot(root, async () => {
377
+ const now = deps.now ?? (() => new Date());
378
+ const threshold = stewardSettings().alertAfterFailures ?? 3;
379
+ const alertIfNeeded = async (state: FailureStreak): Promise<void> => {
380
+ if (!shouldAlert(state, threshold)) return;
381
+ try {
382
+ const send = deps.deliverAlert ?? (async (text: string, kind: 'alert') => {
383
+ const { deliver } = await import('../domains/outbound-alert.js');
384
+ return deliver(text, kind) !== false;
385
+ });
386
+ if (!await send(failureAlertText(state), 'alert')) throw new Error('steward alert delivery failed');
387
+ state.alertedAt = now().toISOString();
388
+ writeFailureStreak(root, state);
389
+ debug.log('steward.streak', 'alerted', { consecutive: state.consecutive, stage: state.lastStage });
390
+ } catch (error) {
391
+ try { debug.log('steward.streak', 'alert-failed', { consecutive: state.consecutive, stage: state.lastStage,
392
+ reason: redactSecretText(error instanceof Error ? error.message : String(error)) }); } catch { /* alert observability must not change the stage result */ }
393
+ }
394
+ };
395
+ const open = readFailureStreak(root).open as (FailureStreak['open'] & { pid?: number; runId?: string }) | undefined;
396
+ if (stage === 'sync') {
397
+ if (open?.pid && isProcessAlive(open.pid)) {
398
+ skipOverlappingRun(root, open);
399
+ return 0;
400
+ }
401
+ rmSync(skippedPath(root), { force: true });
402
+ } else if (existsSync(skippedPath(root))) return 0;
403
+ const hadOpen = open !== undefined;
404
+ const started = markStageStart(root, stage, now());
405
+ if (started.open) {
406
+ Object.assign(started.open, { pid: process.pid, ...(graphRunId() ? { runId: graphRunId() } : {}) });
407
+ writeFailureStreak(root, started);
408
+ }
409
+ if (hadOpen) {
410
+ debug.log('steward.streak', 'recorded', { consecutive: started.consecutive, stage: started.lastStage });
411
+ await alertIfNeeded(started);
412
+ }
413
+ let error: unknown;
414
+ let ok = false;
415
+ try { await (deps.runStage ?? runStewardStage)(stage, { ...deps, stageOwnedByCommand: true }); ok = true; }
416
+ catch (caught) { error = caught; }
417
+ const reason = ok ? '' : redactSecretText(error instanceof Error ? error.message : String(error)).slice(0, 300);
418
+ const state = markStageEnd(root, stage, ok, reason, now());
419
+ debug.log('steward.streak', 'recorded', { consecutive: state.consecutive, stage });
420
+ await alertIfNeeded(state);
421
+ if (!ok) {
422
+ console.error(`steward ${stage}: stage failed — ${reason}`);
423
+ debug.log('steward.stage', 'failed', { stage, reason });
424
+ return 1;
425
+ }
426
+ return 0;
427
+ }, stage === 'sync' ? () => { skipOverlappingRun(root, currentOpen(root, stage)); return 0 as const; } : undefined);
428
+ }
429
+
225
430
  if (import.meta.main) {
226
431
  const stage = process.argv[2];
227
432
  if (!['sync', 'triage', 'schedule', 'report'].includes(stage ?? '')) {
228
433
  console.error('steward: expected sync|triage|schedule|report');
229
434
  process.exitCode = 2;
230
435
  } else {
231
- runStewardStage(stage as 'sync' | 'triage' | 'schedule' | 'report').catch((error: unknown) => {
232
- // Say why (secrets masked) — 09-29 the first resident run failed as a bare «stage failed» on a missing Linear key.
233
- const reason = redactSecretText(error instanceof Error ? error.message : String(error)).slice(0, 300);
234
- console.error(`steward ${stage}: stage failed — ${reason}`);
235
- debug.log('steward.stage', 'failed', { stage, reason });
436
+ runStewardStageCommand(stage as StewardStage).then(code => { process.exitCode = code; }).catch((error: unknown) => {
437
+ console.error(`steward ${stage}: ${redactSecretText(error instanceof Error ? error.message : String(error))}`);
236
438
  process.exitCode = 1;
237
439
  });
238
440
  }
@@ -38,7 +38,7 @@ type CardEvent =
38
38
  * A key is `<section>` or `<section>:<idempotency key>`: the same section can be appended many times over a
39
39
  * task's life (e.g. workspace diff updates), and the same full key is written once. `incidents` is open to any loop. */
40
40
  export const SECTION_OWNERS: Readonly<Record<string, string | null>> = {
41
- intake: 'steward', triage: 'steward',
41
+ intake: 'steward', triage: 'steward', launch: 'steward', outcome: 'steward',
42
42
  gates: 'execution-loop', relations: 'execution-loop', memory: 'execution-loop',
43
43
  workspace: 'executor', run: 'executor',
44
44
  landing: 'landing-loop', release: 'release-loop',
@@ -0,0 +1,18 @@
1
+ export const POD_BUN_CACHE_HOST_PATH = 'POD_BUN_CACHE_HOST_PATH';
2
+
3
+ /** A host-backed Bun install cache; only opt into it when the mounted directory is writable. */
4
+ export function podBunCacheVolume(hostPath: string) {
5
+ return {
6
+ volume: { name: 'bun-cache', hostPath: { path: hostPath, type: 'DirectoryOrCreate' } },
7
+ volumeMount: { name: 'bun-cache', mountPath: '/bun-cache', readOnly: false },
8
+ shellPrefix: 'if [ -d /bun-cache ] && [ -w /bun-cache ]; then export BUN_INSTALL_CACHE_DIR=/bun-cache; fi;',
9
+ };
10
+ }
11
+
12
+ /** First Bun install timing in a combined shard log (including a no-op install). */
13
+ export function parseInstallSeconds(log: string): number | null {
14
+ const match = /(?:\d+ packages? installed|Checked \d+ installs across \d+ packages(?: \(no changes\))?)\s*\[(\d+(?:\.\d+)?)(ms|s)\]/.exec(log);
15
+ if (!match) return null;
16
+ const duration = Number(match[1]);
17
+ return match[2] === 'ms' ? duration / 1000 : duration;
18
+ }