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
@@ -4,14 +4,17 @@ import { dirname, join } from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { debug } from '../debug/log.js';
6
6
  import { releaseLedgerRoot } from '../instance/resolve.js';
7
+ import { CliUserError } from '../cli/cli-user-error.js';
7
8
 
8
9
  export type ChecklistStatus = 'green' | 'yellow' | 'red' | 'done';
10
+ export type ChecklistDisposition = 'move' | 'known-issue' | 'block';
9
11
  export interface ChecklistItem {
10
12
  id: string;
11
13
  title: string;
12
14
  status: ChecklistStatus;
13
15
  owner?: string;
14
16
  evidence?: string;
17
+ disposition?: ChecklistDisposition;
15
18
  updatedAt: string;
16
19
  updatedBy: string;
17
20
  }
@@ -52,7 +55,7 @@ function releasedVersion(): string {
52
55
  }
53
56
 
54
57
  function pathFor(v: string): string {
55
- if (!/^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/.test(v)) throw new Error(`체크리스트 판이 아니다: ${v}`);
58
+ if (!/^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/.test(v)) throw new CliUserError(`체크리스트 판이 아니다: ${v}`);
56
59
  return join(releaseLedgerRoot(), 'release', v, 'checklist.json');
57
60
  }
58
61
 
@@ -108,10 +111,10 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
108
111
  function refresh(data: Checklist): void { data.released = releasedVersion(); data.dev = devVersion(); }
109
112
 
110
113
  export function addItem(v: string, input: { id: string; title: string; owner?: string }): Checklist {
111
- if (!input.id.trim()) throw new Error('칸 id 가 비었다');
112
- if (!input.title.trim()) throw new Error('칸 제목이 비었다');
114
+ if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
115
+ if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
113
116
  return mutate(v, (data) => {
114
- if (data.items.some((item) => item.id === input.id)) throw new Error(`이미 있는 칸: ${input.id}`);
117
+ if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
115
118
  const by = process.env.ELANOUS_TRACK || 'cli';
116
119
  const at = new Date().toISOString();
117
120
  const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), updatedAt: at, updatedBy: by };
@@ -121,12 +124,13 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
121
124
  });
122
125
  }
123
126
 
124
- export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string }, by: string): Checklist {
127
+ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition }, by: string): Checklist {
125
128
  return mutate(v, (data) => {
126
129
  const item = data.items.find((i) => i.id === id);
127
- if (!item) throw new Error(`없는 칸: ${id}`);
128
- if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new Error(`잘못된 상태: ${patch.status}`);
129
- const fields = (['evidence', 'owner', 'status'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
130
+ if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
131
+ if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new CliUserError(`잘못된 상태: ${patch.status}`);
132
+ if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new CliUserError(`잘못된 처분: ${patch.disposition}`);
133
+ const fields = (['evidence', 'owner', 'status', 'disposition'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
130
134
  if (fields.length === 0) return false;
131
135
  const at = new Date().toISOString();
132
136
  for (const field of fields) {
@@ -144,7 +148,7 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
144
148
  export function removeItem(v: string, id: string, by: string): Checklist {
145
149
  return mutate(v, (data) => {
146
150
  const index = data.items.findIndex((i) => i.id === id);
147
- if (index < 0) throw new Error(`없는 칸: ${id}`);
151
+ if (index < 0) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
148
152
  const at = new Date().toISOString();
149
153
  const [item] = data.items.splice(index, 1);
150
154
  change(data, id, 'remove', item, null, by, at);
@@ -169,6 +173,29 @@ export function summarizeChecklist(data: Checklist): ChecklistSummary {
169
173
  return summary;
170
174
  }
171
175
 
176
+ export interface ChecklistGate {
177
+ ok: boolean;
178
+ red: string[];
179
+ undecided: string[];
180
+ blocked: string[];
181
+ moved: string[];
182
+ knownIssues: Array<{ id: string; title: string; evidence: string }>;
183
+ }
184
+
185
+ export function checklistGate(v: string): ChecklistGate {
186
+ const result: ChecklistGate = { ok: true, red: [], undecided: [], blocked: [], moved: [], knownIssues: [] };
187
+ for (const item of listChecklist(v).items) {
188
+ if (item.status === 'red') result.red.push(item.id);
189
+ if (item.status !== 'yellow') continue;
190
+ if (item.disposition === 'block') result.blocked.push(item.id);
191
+ else if (item.disposition === 'move') result.moved.push(item.id);
192
+ else if (item.disposition === 'known-issue') result.knownIssues.push({ id: item.id, title: item.title, evidence: item.evidence ?? '' });
193
+ else result.undecided.push(item.id);
194
+ }
195
+ result.ok = result.red.length === 0 && result.undecided.length === 0 && result.blocked.length === 0;
196
+ return result;
197
+ }
198
+
172
199
  export function seedFromRoadmap(v: string, markdown: string): Checklist {
173
200
  return mutate(v, (data) => {
174
201
  let added = false;
@@ -0,0 +1,34 @@
1
+ import trackData from '../../scripts/coord-tracks.json';
2
+
3
+ export type SeatEntry = {
4
+ id: string;
5
+ title?: string;
6
+ alias?: string;
7
+ };
8
+
9
+ export type SeatAddress = { seats: string[]; body: string };
10
+
11
+ const registry: readonly SeatEntry[] = trackData.tracks.filter((entry) => 'title' in entry);
12
+ const addressLine = /^@([A-Za-z][A-Za-z0-9_-]*(?:,[A-Za-z][A-Za-z0-9_-]*)*)(?:[ \t]+|\r?\n|(?![\s\S]))/gm;
13
+
14
+ /** Parse an address only when its @ begins a line; leave the instruction text intact. */
15
+ export function parseSeatAddress(text: string): SeatAddress | null {
16
+ const match = addressLine.exec(text);
17
+ addressLine.lastIndex = 0;
18
+ if (!match) return null;
19
+
20
+ const seats = match[1]!.split(',');
21
+ const body = `${text.slice(0, match.index)}${text.slice(match.index + match[0].length)}`;
22
+ return { seats, body };
23
+ }
24
+
25
+ /** Resolve against the seat registry, preferring titles to ids to legacy aliases. */
26
+ export function resolveSeat(address: string, seats: readonly SeatEntry[] = registry): SeatEntry | undefined {
27
+ const needle = address.replace(/^@/, '').toLocaleLowerCase();
28
+ if (!needle) return undefined;
29
+ for (const field of ['title', 'id', 'alias'] as const) {
30
+ const found = seats.find((seat) => seat[field]?.toLocaleLowerCase() === needle);
31
+ if (found) return found;
32
+ }
33
+ return undefined;
34
+ }
@@ -341,6 +341,7 @@ export function buildSelfDevDecomposePrompt(feature: string, maxTasks = DEFAULT_
341
341
  '## Rules',
342
342
  `- Emit at most ${maxTasks} sub-features. Fewer is better — do NOT over-split, except when a sub-feature mixes ${SELF_DEV_CONCERN_SPLIT_THRESHOLD} or more concern classes — ${SELF_DEV_CONCERN_CLASSES.join(' / ')} — split it into sibling sub-features.`,
343
343
  '- SEPARATE DEFINE FROM WIRE: adding a new function/type/export is a SEPARATE task from wiring it into an existing runtime call site. A task that both defines a new symbol AND integrates it across modules tends to leave dead-code (defined but never called) that fails integration gates. Pattern: (a) add the function + its unit test; then (b) a dependent task that wires it into the specific existing call site — name the exact `file.ts:function` to modify.',
344
+ '- Exception to SEPARATE DEFINE FROM WIRE: Keep a definition and its registration/wiring in the same sub-feature — a handler with its route, a command with its CLI registration, a node with its graph/recipe entry.',
344
345
  '- Each sub-feature must be a self-contained, mergeable unit of work.',
345
346
  '- `dependsOn`: list the `id`s of sub-features that MUST land first (e.g. a shared',
346
347
  ' type/interface before its consumers). Independent sub-features share NO dependency',
@@ -1871,7 +1871,10 @@ async function runDevPipelineDispatch(
1871
1871
  observeDevSelection('completion', plan.completion, plan.completionSource, plan.completionSource === 'request' ? spec.completion : undefined, entryRoute, spec.runId, plan.entrance, plan.entranceUnstamped);
1872
1872
  observeDevSelection('autoReview', plan.autoReview, plan.autoReviewSource, plan.autoReviewSource === 'request' ? spec.autoReview : undefined, entryRoute, spec.runId, plan.entrance, plan.entranceUnstamped);
1873
1873
  assertDispatchHonorsOptions(plan); // 미이행 옵션 거부(수락 후 무시 금지)
1874
- emitHumanProgress(completionAnnouncement(plan.completion), spec.humanReadableOutput !== false, deps.progress);
1874
+ // CHAT1 — a plain chat turn (`interactive`) is not a dev run: its completion/base lines are noise to a person.
1875
+ // The selection is still observed above; only the human-facing lines are dev-run-only.
1876
+ const devRunLines = plan.dispatch !== 'interactive';
1877
+ if (devRunLines) emitHumanProgress(completionAnnouncement(plan.completion), spec.humanReadableOutput !== false, deps.progress);
1875
1878
  if (spec.notice) emitHumanProgress(spec.notice, spec.humanReadableOutput !== false, deps.progress);
1876
1879
 
1877
1880
  if (plan.dispatch === 'parallel') {
@@ -1899,8 +1902,8 @@ async function runDevPipelineDispatch(
1899
1902
  rule: plan.baseSelection?.rule ?? 'unspecified',
1900
1903
  evidence: plan.baseSelection?.evidence ?? 'not applicable',
1901
1904
  });
1902
- emitHumanProgress(formatDevBaseSelectionAnnouncement(plan), spec.humanReadableOutput !== false, deps.progress);
1903
- const nonDefaultBaseWarning = formatDevNonDefaultBaseWarning(plan);
1905
+ if (devRunLines) emitHumanProgress(formatDevBaseSelectionAnnouncement(plan), spec.humanReadableOutput !== false, deps.progress);
1906
+ const nonDefaultBaseWarning = devRunLines ? formatDevNonDefaultBaseWarning(plan) : undefined;
1904
1907
  if (nonDefaultBaseWarning) {
1905
1908
  emitHumanProgress(nonDefaultBaseWarning, spec.humanReadableOutput !== false, deps.progress);
1906
1909
  }
@@ -0,0 +1,44 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { debug } from '../debug/log.js';
5
+ import { releaseLedgerRoot } from '../instance/resolve.js';
6
+ import type { SelfDevRunState } from './run-store.js';
7
+
8
+ export function mirrorDir(): string {
9
+ return join(releaseLedgerRoot(), 'self-dev-runs');
10
+ }
11
+
12
+ function mirrorPath(runId: string, dir: string): string {
13
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(runId)) throw new Error(`invalid self-dev run ID: ${runId}`);
14
+ return join(dir, `${runId}.json`);
15
+ }
16
+
17
+ /** Best-effort atomic copy in the machine ledger; never disrupts the primary checkpoint. */
18
+ export function mirrorRunRecord(state: SelfDevRunState, primaryDir: string): void {
19
+ let temp: string | undefined;
20
+ try {
21
+ const dir = mirrorDir();
22
+ if (resolve(primaryDir) === resolve(dir)) return;
23
+ const path = mirrorPath(state.runId, dir);
24
+ mkdirSync(dir, { recursive: true });
25
+ temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
26
+ writeFileSync(temp, JSON.stringify(state, null, 2), { mode: 0o600 });
27
+ renameSync(temp, path);
28
+ } catch (error) {
29
+ if (temp) { try { unlinkSync(temp); } catch { /* no temporary file to remove */ } }
30
+ try { debug.log('self-dev.run-store', 'mirror-failed', { runId: state.runId, reason: String(error) }); } catch { /* keep the primary save fail-soft */ }
31
+ }
32
+ }
33
+
34
+ export function loadMirroredRunRecord(runId: string): SelfDevRunState | null {
35
+ try {
36
+ const record = JSON.parse(readFileSync(mirrorPath(runId, mirrorDir()), 'utf8')) as SelfDevRunState | null;
37
+ return record && record.runId === runId
38
+ && typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
39
+ && typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
40
+ && Array.isArray(record.results) ? record : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
@@ -27,6 +27,7 @@ import type { RunIdSource } from '../harness/harness-space.js';
27
27
  import { withFileLockSync } from '../storage/file-lock.js';
28
28
  import type { SelfDevJobResult, SelfDevGoal } from './orchestrate.js';
29
29
  import type { SupervisorStopReason } from './run-supervisor.js';
30
+ import { loadMirroredRunRecord, mirrorRunRecord } from './run-record-mirror.js';
30
31
 
31
32
  export interface SelfDevRunParticipant {
32
33
  id: string;
@@ -151,18 +152,26 @@ export function saveSelfDevRun(state: SelfDevRunState, dir = selfDevRunsDir()):
151
152
  withFileLockSync(join(dir, `${state.runId}.lock`), () => {
152
153
  const current = loadSelfDevRunFromPath(runPath(state.runId, dir));
153
154
  const participants = mergeParticipants(current?.participants, state.participants);
154
- writeSelfDevRun({
155
+ const saved = {
155
156
  ...state,
156
157
  ...(participants === undefined ? {} : { participants }),
157
158
  ...(current?.parkedResolution && !state.parkedResolution ? { parkedResolution: current.parkedResolution } : {}),
158
- }, dir);
159
+ };
160
+ writeSelfDevRun(saved, dir);
161
+ mirrorRunRecord(saved, dir);
159
162
  });
160
163
  } catch { /* fail-soft — a lost checkpoint just means no resume, not a crash */ }
161
164
  }
162
165
 
163
166
  /** Load a run checkpoint by id, or null if absent/corrupt. */
164
167
  export function loadSelfDevRun(runId: string, dir = selfDevRunsDir()): SelfDevRunState | null {
165
- return loadSelfDevRunFromPath(runPath(runId, dir));
168
+ const primary = loadSelfDevRunFromPath(runPath(runId, dir));
169
+ if (primary) return primary;
170
+ const mirrored = loadMirroredRunRecord(runId);
171
+ if (mirrored) {
172
+ try { debug.log('self-dev.run-store', 'read-from-mirror', { runId }); } catch { /* preserve fail-soft reads */ }
173
+ }
174
+ return mirrored;
166
175
  }
167
176
 
168
177
  /** Add one participant without losing registrations made by another caller for this run. */
@@ -4,8 +4,9 @@ import { runIsolationHardcodeGate } from '../../scripts/ci-isolation-hardcode-ga
4
4
  import { runMockModuleRestoreGate } from '../../scripts/ci-mock-module-restore-gate.js';
5
5
  import { runModelHardcodeGate } from '../../scripts/ci-model-hardcode-gate.js';
6
6
  import { runDaemonPortGate } from '../../scripts/ci-daemon-port-gate.js';
7
+ import { runPublicExportLeakGate } from '../../scripts/ci-public-export-leak-gate.js';
7
8
 
8
- type GateName = 'isolation-gate' | 'mock-module-restore-gate' | 'model-hardcode-gate' | 'daemon-port-gate';
9
+ type GateName = 'isolation-gate' | 'mock-module-restore-gate' | 'model-hardcode-gate' | 'daemon-port-gate' | 'public-export-leak';
9
10
  type GateOutput = { args: string[]; cwd: string; log: (line: string) => void; error: (line: string) => void };
10
11
  type GateRunner = (out: GateOutput) => number;
11
12
 
@@ -27,6 +28,8 @@ export function runHarnessPolicyGates(input: {
27
28
  ['mock-module-restore-gate', input.gates?.['mock-module-restore-gate'] ?? runMockModuleRestoreGate],
28
29
  ['model-hardcode-gate', input.gates?.['model-hardcode-gate'] ?? runModelHardcodeGate],
29
30
  ['daemon-port-gate', input.gates?.['daemon-port-gate'] ?? ((out) => runDaemonPortGate({ log: out.log, error: out.error, cwd: out.cwd, args: [] }))],
31
+ // LEAK1 — the check release prepare fails on, before the harness lands (0.2.7: a mark in a test title).
32
+ ['public-export-leak', input.gates?.['public-export-leak'] ?? runPublicExportLeakGate],
30
33
  ];
31
34
  const failures: Array<{ gate: GateName; lines: string[] }> = [];
32
35
  for (const [gate, run] of runners) {
@@ -2,12 +2,23 @@ import type { MissionOrigin } from '../autopilot/mission-origin.js';
2
2
 
3
3
  export const REPORT_ORIGIN_ENV = 'ELANOUS_REPORT_ORIGIN';
4
4
 
5
- const CHANNELS = new Set<MissionOrigin['channel']>(['telegram', 'pwa', 'voice', 'cli', 'api', 'tui']);
5
+ const CHANNELS = new Set<MissionOrigin['channel']>(['telegram', 'pwa', 'voice', 'cli', 'api', 'tui', 'discord']);
6
+ const SNOWFLAKE = /^\d{1,20}$/;
6
7
 
7
8
  function isReportOrigin(value: unknown): value is MissionOrigin {
8
9
  if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
9
10
  const origin = value as Record<string, unknown>;
10
11
  if (!CHANNELS.has(origin.channel as MissionOrigin['channel'])) return false;
12
+ // Discord carries its own reply target (channel ⊕ optional thread snowflakes) — same shape as MissionOrigin.
13
+ if (origin.channel === 'discord') {
14
+ for (const key of Object.keys(origin)) {
15
+ if (!['channel', 'channelId', 'discordThreadId', 'botId', 'hitlMessageId'].includes(key)) return false;
16
+ }
17
+ return typeof origin.channelId === 'string' && SNOWFLAKE.test(origin.channelId)
18
+ && (origin.discordThreadId === undefined || (typeof origin.discordThreadId === 'string' && SNOWFLAKE.test(origin.discordThreadId)))
19
+ && (origin.botId === undefined || (typeof origin.botId === 'string' && origin.botId.length > 0))
20
+ && (origin.hitlMessageId === undefined || Number.isSafeInteger(origin.hitlMessageId));
21
+ }
11
22
  if (origin.channel === 'telegram' && (!Number.isSafeInteger(origin.chatId) || origin.chatId === 0)) return false;
12
23
  for (const key of Object.keys(origin)) {
13
24
  if (!['channel', 'chatId', 'botId', 'threadId', 'hitlMessageId'].includes(key)) return false;
@@ -280,7 +280,7 @@ function recordedString(entries: readonly RunLedgerEntry[], field: string): stri
280
280
  return undefined;
281
281
  }
282
282
 
283
- function summarizeTerminalLedger(runId: string, entries: readonly RunLedgerEntry[]): RunLedgerSummary {
283
+ export function summarizeTerminalLedger(runId: string, entries: readonly RunLedgerEntry[]): RunLedgerSummary {
284
284
  const last = entries.at(-1)?.timestamp;
285
285
  if (!last || !Number.isFinite(Date.parse(last)) || !hasTerminalRunStatus(entries)) throw new Error('ledger changed since plan');
286
286
  const terminator = [...entries].reverse().find((entry) => entry.event === 'human-stop' || entry.event === 'terminal'
@@ -30,6 +30,7 @@ import {
30
30
  import { buildMemoryInjection, buildMemoryInjectionLLM } from '../memory.js';
31
31
  import { getUserConfig } from '../user-config.js';
32
32
  import { budgetModel } from '../llm/model-defaults.js';
33
+ import { explicitTurnTier } from '../model-tier/turn-tier.js';
33
34
  import { recordTurn } from '../status/metrics.js';
34
35
  import {
35
36
  buildTerminalCapableTurn,
@@ -375,7 +376,18 @@ export async function runTurn(opts: RunTurnOpts): Promise<RunTurnResult> {
375
376
  return trimToBudget(wire, opts.maxTokens ?? DEFAULT_TOKEN_BUDGET);
376
377
  };
377
378
 
378
- const provider = opts.provider ?? getProviderForConfig(opts.userConfig, opts.llmOpts?.model);
379
+ const explicitTier = opts.llmOpts?.model ? undefined : explicitTurnTier(opts.userConfig, opts.sessionId);
380
+ const llmOpts = explicitTier ? {
381
+ ...(opts.llmOpts ?? {}), model: explicitTier.model,
382
+ ...(explicitTier.reasoningEffort && !opts.llmOpts?.reasoningEffort ? { reasoningEffort: explicitTier.reasoningEffort } : {}),
383
+ } : opts.llmOpts;
384
+ if (explicitTier) {
385
+ debug.log('llm.tier', 'turn-tier-applied', {
386
+ sessionId: opts.sessionId, tier: explicitTier.tier, source: explicitTier.source, model: explicitTier.model,
387
+ reasoningEffort: llmOpts?.reasoningEffort ?? null,
388
+ });
389
+ }
390
+ const provider = opts.provider ?? getProviderForConfig(opts.userConfig, llmOpts?.model);
379
391
  let kept: LLMMessage[] = [];
380
392
  let dropped = 0;
381
393
  let used = 0;
@@ -394,7 +406,7 @@ export async function runTurn(opts: RunTurnOpts): Promise<RunTurnResult> {
394
406
  specs: opts.tools,
395
407
  dispatch: rawDispatchTool,
396
408
  systemPromptParts: effectiveSystemPrompt ? [effectiveSystemPrompt] : [],
397
- ...(opts.llmOpts ? { llmOpts: opts.llmOpts } : {}),
409
+ ...(llmOpts ? { llmOpts } : {}),
398
410
  };
399
411
  const terminal = hasPtyShellTool
400
412
  ? isTerminalCapableTurn(terminalBase)
@@ -409,7 +421,7 @@ export async function runTurn(opts: RunTurnOpts): Promise<RunTurnResult> {
409
421
  if (terminal) {
410
422
  effectiveSystemPrompt = terminal.systemPromptParts.join('\n\n');
411
423
  }
412
- const turnLlmOpts = terminal ? terminal.llmOpts : opts.llmOpts;
424
+ const turnLlmOpts = terminal ? terminal.llmOpts : llmOpts;
413
425
  ({ kept, dropped, used } = assembleTurnMessages(effectiveSystemPrompt));
414
426
  // Durable tool telemetry (추적성) — pair each call's args (onToolCall) with its
415
427
  // result (onToolResult) and persist a role:'tool' message so later "어떤 도구
@@ -477,7 +489,7 @@ export async function runTurn(opts: RunTurnOpts): Promise<RunTurnResult> {
477
489
  }
478
490
  } else {
479
491
  ({ kept, dropped, used } = assembleTurnMessages(effectiveSystemPrompt));
480
- const plainOpts = opts.signal ? { ...(opts.llmOpts ?? {}), signal: opts.signal } : (opts.llmOpts ?? {});
492
+ const plainOpts = opts.signal ? { ...(llmOpts ?? {}), signal: opts.signal } : (llmOpts ?? {});
481
493
  const stream = provider.streamChat
482
494
  ? provider.streamChat(kept, plainOpts)
483
495
  : wrapTextOnly(provider.chat(kept, plainOpts));
@@ -529,7 +541,7 @@ export async function runTurn(opts: RunTurnOpts): Promise<RunTurnResult> {
529
541
  // status bar (CTX bar · $ · 🚀) reflects the latest numbers. Uses
530
542
  // rough estimates — providers that expose real `usage` fields can
531
543
  // override via opts.llmOpts in a later phase.
532
- const resolvedModel = opts.llmOpts?.model ?? provider.defaultModel;
544
+ const resolvedModel = llmOpts?.model ?? provider.defaultModel;
533
545
  recordTurn({
534
546
  model: resolvedModel,
535
547
  estimatedPromptText: [...messages, userMsg].map(m =>
@@ -434,12 +434,20 @@ function readIndex(root: string = sessionRoot()): SessionMeta[] {
434
434
  }
435
435
  }
436
436
 
437
- function writeIndex(entries: SessionMeta[], root: string = sessionRoot()): void {
437
+ // 여러 프로세스(`elanous ask` 동시 실행 등)가 같은 목록을 «읽고-고치고-통째로 쓰기» 한다.
438
+ // 쓰기 직전 잠금 안에서 디스크의 최신 목록을 다시 읽어 id 로 합치고, 임시 파일 이름은 프로세스마다 다르게 한다
439
+ // (10-01 MK 재현: 고정 `index.json.tmp` → 한쪽 ENOENT · 덮어쓰기로 남의 세션이 목록에서 사라짐).
440
+ function writeIndex(entries: SessionMeta[], root: string = sessionRoot(), removed: Iterable<string> = []): void {
438
441
  ensureRoots(root);
439
442
  const p = indexPath(root);
440
- const tmp = p + '.tmp';
441
- writeFileSync(tmp, JSON.stringify(entries, null, 2) + '\n', 'utf-8');
442
- renameSync(tmp, p);
443
+ withDirLock(join(root, '.index.lock'), INDEX_LOCK_TIMEOUT_MS, () => {
444
+ const mine = new Set(entries.map((m) => m.id));
445
+ const gone = new Set(removed);
446
+ const peers = readIndex(root).filter((m) => !mine.has(m.id) && !gone.has(m.id));
447
+ const tmp = `${p}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
448
+ writeFileSync(tmp, JSON.stringify([...peers, ...entries], null, 2) + '\n', 'utf-8');
449
+ renameSync(tmp, p);
450
+ });
443
451
  }
444
452
 
445
453
  // ── Tier 1 Phase 3 — listener primitive ──────────────────────────────
@@ -661,7 +669,7 @@ export function rewriteSessionMessages(
661
669
  const body = messages
662
670
  .map(m => JSON.stringify(m.turn_id !== undefined ? m : { ...m, turn_id: mintTurnUri() }))
663
671
  .join('\n');
664
- const tmp = file + '.tmp';
672
+ const tmp = `${file}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
665
673
  writeFileSync(tmp, body ? body + '\n' : '', 'utf-8');
666
674
  renameSync(tmp, file);
667
675
 
@@ -800,8 +808,13 @@ function residentSessionLockPath(root: string): string {
800
808
 
801
809
  function withResidentSessionLock<T>(root: string, action: () => T): T {
802
810
  ensureRoots(root);
803
- const lock = residentSessionLockPath(root);
804
- const deadline = Date.now() + RESIDENT_SESSION_LOCK_TIMEOUT_MS;
811
+ return withDirLock(residentSessionLockPath(root), RESIDENT_SESSION_LOCK_TIMEOUT_MS, action);
812
+ }
813
+
814
+ const INDEX_LOCK_TIMEOUT_MS = 10_000;
815
+
816
+ function withDirLock<T>(lock: string, timeoutMs: number, action: () => T): T {
817
+ const deadline = Date.now() + timeoutMs;
805
818
  for (;;) {
806
819
  try {
807
820
  mkdirSync(lock);
@@ -814,7 +827,7 @@ function withResidentSessionLock<T>(root: string, action: () => T): T {
814
827
  continue;
815
828
  }
816
829
  } catch { continue; }
817
- if (Date.now() >= deadline) throw new Error(`timed out acquiring resident session lock for ${root}`);
830
+ if (Date.now() >= deadline) throw new Error(`timed out acquiring lock ${lock}`);
818
831
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
819
832
  void error;
820
833
  }
@@ -855,7 +868,7 @@ export function deleteSession(id: string, root: string = sessionRoot()): boolean
855
868
  const before = idx.length;
856
869
  const next = idx.filter(m => m.id !== id);
857
870
  if (next.length === before) return false;
858
- writeIndex(next, root);
871
+ writeIndex(next, root, [id]);
859
872
  // Clear active marker if it pointed at the deleted session.
860
873
  if (getActiveSessionId() === id) clearActiveSessionId();
861
874
  return true;
@@ -876,7 +889,7 @@ export function deleteSessions(ids: string[], root: string = sessionRoot()): num
876
889
  const idx = readIndex(root);
877
890
  const next = idx.filter(m => !set.has(m.id));
878
891
  const removed = idx.length - next.length;
879
- if (removed > 0) writeIndex(next, root);
892
+ if (removed > 0) writeIndex(next, root, set);
880
893
  const active = getActiveSessionId();
881
894
  if (active && set.has(active)) clearActiveSessionId();
882
895
  return removed;
@@ -0,0 +1,25 @@
1
+ import { statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ export interface SharedAgentSkillRootsOptions {
6
+ home?: string;
7
+ /** Directory-existence probe; defaults to a read-only filesystem stat. */
8
+ exists?: (dir: string) => boolean;
9
+ includeSharedAgentSkills?: boolean;
10
+ }
11
+
12
+ /** Discover the shared user skill root without creating or modifying it. */
13
+ export function sharedAgentSkillRoots({
14
+ home = homedir(),
15
+ exists = (dir: string) => statSync(dir).isDirectory(),
16
+ includeSharedAgentSkills = true,
17
+ }: SharedAgentSkillRootsOptions): string[] {
18
+ if (!includeSharedAgentSkills) return [];
19
+ const root = join(home, '.agents', 'skills');
20
+ try {
21
+ return exists(root) ? [root] : [];
22
+ } catch {
23
+ return [];
24
+ }
25
+ }
@@ -0,0 +1,78 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { redactSecretText } from '../debug/log.js';
4
+
5
+ export type StewardStage = 'sync' | 'triage' | 'schedule' | 'report';
6
+ export interface FailureStreak {
7
+ consecutive: number;
8
+ lastStage: StewardStage;
9
+ lastReason: string;
10
+ lastAt: string;
11
+ alertedAt?: string;
12
+ open?: { stage: StewardStage; startedAt: string };
13
+ }
14
+
15
+ const stages: readonly string[] = ['sync', 'triage', 'schedule', 'report'];
16
+ const empty = (): FailureStreak => ({ consecutive: 0, lastStage: 'sync', lastReason: '', lastAt: '' });
17
+ const pathFor = (root: string): string => join(root, 'steward', 'streak.json');
18
+
19
+ export function readFailureStreak(root: string): FailureStreak {
20
+ try {
21
+ const value: unknown = JSON.parse(readFileSync(pathFor(root), 'utf8'));
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return empty();
23
+ const s = value as Record<string, unknown>;
24
+ if (!Number.isSafeInteger(s.consecutive) || (s.consecutive as number) < 0 ||
25
+ !stages.includes(s.lastStage as string) || typeof s.lastReason !== 'string' || typeof s.lastAt !== 'string' ||
26
+ (s.alertedAt !== undefined && typeof s.alertedAt !== 'string')) return empty();
27
+ if (s.open !== undefined && (!s.open || typeof s.open !== 'object' || Array.isArray(s.open) ||
28
+ !stages.includes((s.open as Record<string, unknown>).stage as string) ||
29
+ typeof (s.open as Record<string, unknown>).startedAt !== 'string')) return empty();
30
+ return s as unknown as FailureStreak;
31
+ } catch { return empty(); }
32
+ }
33
+
34
+ export function writeFailureStreak(root: string, state: FailureStreak): void {
35
+ const dir = join(root, 'steward');
36
+ mkdirSync(dir, { recursive: true });
37
+ const path = pathFor(root);
38
+ const tmp = `${path}.${process.pid}.tmp`;
39
+ writeFileSync(tmp, JSON.stringify(state));
40
+ renameSync(tmp, path);
41
+ }
42
+
43
+ export function markStageStart(root: string, stage: StewardStage, now: Date = new Date()): FailureStreak {
44
+ const state = readFailureStreak(root);
45
+ if (state.open) {
46
+ state.consecutive += 1;
47
+ state.lastStage = state.open.stage;
48
+ state.lastReason = `killed or timed out: ${state.open.stage}`;
49
+ state.lastAt = now.toISOString();
50
+ }
51
+ state.open = { stage, startedAt: now.toISOString() };
52
+ writeFailureStreak(root, state);
53
+ return state;
54
+ }
55
+
56
+ export function markStageEnd(root: string, stage: StewardStage, ok: boolean, reason = '', now: Date = new Date()): FailureStreak {
57
+ const state = readFailureStreak(root);
58
+ delete state.open;
59
+ if (ok) {
60
+ state.consecutive = 0;
61
+ delete state.alertedAt;
62
+ } else {
63
+ state.consecutive += 1;
64
+ }
65
+ state.lastStage = stage;
66
+ state.lastReason = ok ? '' : redactSecretText(reason);
67
+ state.lastAt = now.toISOString();
68
+ writeFailureStreak(root, state);
69
+ return state;
70
+ }
71
+
72
+ export function shouldAlert(state: FailureStreak, threshold: number): boolean {
73
+ return state.consecutive >= threshold && !state.alertedAt;
74
+ }
75
+
76
+ export function failureAlertText(state: FailureStreak): string {
77
+ return `스튜어드 연속 실패 ${state.consecutive}회 · 마지막 ${state.lastStage} · ${redactSecretText(state.lastReason).slice(0, 120)} · 로그 /tmp/elanous-loop-steward.log`;
78
+ }
@@ -43,3 +43,27 @@ export function recordTriageOnCards(decisions: ScheduledDecision[], issues: Tria
43
43
  if (!deps.store) store.close();
44
44
  }
45
45
  }
46
+
47
+ function recordOnCard(section: 'launch' | 'outcome', issue: Pick<TriageIssue, 'identifier' | 'title'>, value: object, deps: StewardCardDeps): void {
48
+ const store = deps.store ?? new CardStore(deps.root);
49
+ try {
50
+ const content = redactSecrets(JSON.stringify(value));
51
+ const key = `${section}:${createHash('sha256').update(content).digest('hex')}`;
52
+ const card = store.createCard({ goalId: `linear:${issue.identifier}`, title: redactSecrets(issue.title) });
53
+ if (!card.sections.some(item => item.key === key)) {
54
+ store.appendSection(card.id, { key, owner: 'steward', content });
55
+ }
56
+ } finally {
57
+ if (!deps.store) store.close();
58
+ }
59
+ }
60
+
61
+ /** Append a launch revision to the issue card; an identical retry keeps its original section. */
62
+ export function recordLaunchOnCard(issue: Pick<TriageIssue, 'identifier' | 'title'>, launch: object, deps: StewardCardDeps = {}): void {
63
+ recordOnCard('launch', issue, launch, deps);
64
+ }
65
+
66
+ /** Append an outcome revision to the issue card without replacing earlier sections. */
67
+ export function recordOutcomeOnCard(issue: Pick<TriageIssue, 'identifier' | 'title'>, outcome: object, deps: StewardCardDeps = {}): void {
68
+ recordOnCard('outcome', issue, outcome, deps);
69
+ }