elanous 0.2.6 → 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 (450) hide show
  1. package/.env.example +10 -7
  2. package/README.md +4 -0
  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/{3995-bee512a7063036ce.js → 2375-cb6debea56cce4fb.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
  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/{8d0d9faf-97887ff99713b84e.js → 2ea96958-e8c7e112de50b067.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/{5329.8b7e4bca684b6eec.js → 3403.4bc97b4066945a94.js} +2 -2
  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/{7633.0654fe6ea259c1ee.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/{2984-977cfa339fcd65a5.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/{763-7d0e1913b5074e98.js → 5791-bb82743fa6299562.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/{4240-7153881b0b377306.js → 6062-73e68343a9814f6a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +1 -0
  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/{91ff6dda-3b33d42d20657429.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-10ed24f413ed66e9.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/page-624f7a2f89b60c8f.js +1 -0
  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-55d50e9b0006a91d.js +1 -0
  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-37b8bf78a3c6eec5.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/index.html +1 -1
  123. package/apps/pwa/out/index.txt +21 -21
  124. package/apps/pwa/out/intake/index.html +1 -1
  125. package/apps/pwa/out/intake/index.txt +22 -22
  126. package/apps/pwa/out/live/index.html +1 -1
  127. package/apps/pwa/out/live/index.txt +22 -22
  128. package/apps/pwa/out/market/index.html +1 -1
  129. package/apps/pwa/out/market/index.txt +21 -21
  130. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  131. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  132. package/apps/pwa/out/morning/index.html +1 -1
  133. package/apps/pwa/out/morning/index.txt +22 -22
  134. package/apps/pwa/out/observatory/index.html +1 -1
  135. package/apps/pwa/out/observatory/index.txt +22 -22
  136. package/apps/pwa/out/reflection/index.html +1 -1
  137. package/apps/pwa/out/reflection/index.txt +22 -22
  138. package/apps/pwa/out/scheduler/index.html +1 -1
  139. package/apps/pwa/out/scheduler/index.txt +22 -22
  140. package/apps/pwa/out/sessions/index.html +1 -1
  141. package/apps/pwa/out/sessions/index.txt +21 -21
  142. package/apps/pwa/out/settings/devices/index.html +1 -1
  143. package/apps/pwa/out/settings/devices/index.txt +22 -22
  144. package/apps/pwa/out/settings/index.html +1 -1
  145. package/apps/pwa/out/settings/index.txt +22 -22
  146. package/apps/pwa/out/setup/done/index.html +1 -1
  147. package/apps/pwa/out/setup/done/index.txt +22 -22
  148. package/apps/pwa/out/setup/index.html +1 -1
  149. package/apps/pwa/out/setup/index.txt +22 -22
  150. package/apps/pwa/out/share/index.html +1 -1
  151. package/apps/pwa/out/share/index.txt +22 -22
  152. package/apps/pwa/out/showroom/index.html +1 -1
  153. package/apps/pwa/out/showroom/index.txt +21 -21
  154. package/apps/pwa/out/tasks/index.html +1 -1
  155. package/apps/pwa/out/tasks/index.txt +22 -22
  156. package/apps/pwa/out/term/index.html +1 -1
  157. package/apps/pwa/out/term/index.txt +22 -22
  158. package/apps/pwa/out/trace/index.html +1 -1
  159. package/apps/pwa/out/trace/index.txt +22 -22
  160. package/apps/pwa/out/vault/index.html +1 -1
  161. package/apps/pwa/out/vault/index.txt +22 -22
  162. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  163. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  164. package/apps/pwa/out/workflows/index.html +1 -1
  165. package/apps/pwa/out/workflows/index.txt +22 -22
  166. package/apps/pwa/out/workspace/index.html +1 -1
  167. package/apps/pwa/out/workspace/index.txt +22 -22
  168. package/apps/pwa/out/worktrees/index.html +1 -1
  169. package/apps/pwa/out/worktrees/index.txt +22 -22
  170. package/bin/elanous.cjs +107 -0
  171. package/graphs/field/field-feed.yaml +24 -0
  172. package/graphs/field/recipes.yaml +12 -0
  173. package/graphs/landing/landing-heal.yaml +3 -1
  174. package/graphs/landing/recipes.yaml +3 -0
  175. package/graphs/release/recipes.yaml +10 -0
  176. package/graphs/release/release-loop.yaml +10 -3
  177. package/package.json +5 -5
  178. package/plugins/card-followup/README.md +13 -4
  179. package/plugins/card-followup/graphs/card-followup.yaml +3 -1
  180. package/plugins/card-followup/graphs/recipes.yaml +5 -1
  181. package/plugins/card-followup/graphs/run-step.ts +122 -18
  182. package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
  183. package/plugins/card-followup/plugin.json +1 -1
  184. package/plugins/doc-draft/README.md +13 -0
  185. package/plugins/doc-draft/examples/brief-sample.json +8 -0
  186. package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
  187. package/plugins/doc-draft/graphs/recipes.yaml +9 -0
  188. package/plugins/doc-draft/graphs/run-step.ts +113 -0
  189. package/plugins/doc-draft/plugin.json +24 -0
  190. package/plugins/geo-check/README.md +3 -1
  191. package/plugins/geo-check/graphs/run-step.ts +24 -3
  192. package/plugins/geo-check/plugin.json +1 -1
  193. package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
  194. package/plugins/job-coach/README.md +1 -1
  195. package/plugins/job-coach/graphs/run-step.ts +2 -2
  196. package/plugins/job-coach/plugin.json +1 -1
  197. package/plugins/job-coach/plugin.ts +1 -1
  198. package/scripts/affinity/endcard.sh +3 -2
  199. package/scripts/affinity/reframe.sh +3 -1
  200. package/scripts/ci-public-export-leak-gate.ts +48 -0
  201. package/scripts/codex-quota-alert.ts +122 -258
  202. package/scripts/coord-tracks.json +4 -4
  203. package/scripts/field-feed/deliver-node.ts +52 -0
  204. package/scripts/field-feed/feed-node.ts +67 -0
  205. package/scripts/field-feed/lib.ts +162 -0
  206. package/scripts/field-feed/reel-node.ts +24 -0
  207. package/scripts/field-feed/start.ts +62 -0
  208. package/scripts/fixtures/vflow-sample.ndjson +6 -0
  209. package/scripts/install.sh +92 -13
  210. package/scripts/landing-heal/clean-checkout.ts +114 -0
  211. package/scripts/landing-heal/run-mission.ts +38 -3
  212. package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
  213. package/scripts/public-export.ts +8 -4
  214. package/scripts/release-loop/baseline-from-logs.ts +92 -0
  215. package/scripts/release-loop/env-known-failures.ts +31 -0
  216. package/scripts/release-loop/gate-node.ts +81 -19
  217. package/scripts/release-loop/known-issues-node.ts +159 -0
  218. package/scripts/release-loop/npm-publish-node.ts +118 -0
  219. package/scripts/release-loop/ops-upgrade-node.ts +87 -0
  220. package/scripts/release-loop/release-readiness.ts +58 -0
  221. package/scripts/release-loop/unattended-release.ts +93 -0
  222. package/scripts/ux/measure-first-text.ts +102 -0
  223. package/scripts/vflow-collect.ts +20 -7
  224. package/scripts/vflow-search.ts +73 -0
  225. package/scripts/video-free-line-test-walker.ts +36 -0
  226. package/scripts/video-pipeline.ts +2 -2
  227. package/skills/explainer-video/SKILL.md +12 -9
  228. package/skills/explainer-video/engine/build.mjs +1 -1
  229. package/skills/explainer-video/engine/reel.mjs +138 -31
  230. package/skills/explainer-video/engine/reel.sh +2 -1
  231. package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
  232. package/skills/grill-me/SKILL.md +1 -1
  233. package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
  234. package/src/a2a/server.ts +2 -0
  235. package/src/acp/elanous-aware-peers.ts +30 -0
  236. package/src/acp/server.ts +91 -15
  237. package/src/agent/monad-agent-turn.ts +25 -6
  238. package/src/agent/shared-app-tools.ts +5 -3
  239. package/src/agent-substrate/review-intent.ts +6 -2
  240. package/src/agent-substrate/shard-boundary-intent.ts +70 -0
  241. package/src/auth/github-app-token.ts +4 -1
  242. package/src/autopilot/track-agent-shadow.ts +151 -0
  243. package/src/autopilot/track-agent.ts +23 -3
  244. package/src/boot/acp-server-help.ts +16 -0
  245. package/src/boot/daemon-prompt-turn.ts +31 -10
  246. package/src/boot/daemon-tools/index.ts +7 -1
  247. package/src/budget/codex-credit-pace.ts +84 -0
  248. package/src/budget/codex-credit-plan.ts +154 -0
  249. package/src/budget/codex-reset-credit-state.ts +2 -0
  250. package/src/chat/chat-cli.ts +4 -1
  251. package/src/chat/short-question.ts +40 -0
  252. package/src/cli/agent-cli.ts +24 -15
  253. package/src/cli/ask-bare.ts +42 -0
  254. package/src/cli/cli-user-error.ts +25 -0
  255. package/src/cli/codex-credit-plan-view.ts +25 -0
  256. package/src/cli/dependence-gauge.ts +237 -0
  257. package/src/cli/doctor-cli.ts +5 -0
  258. package/src/cli/doctor-modes.ts +48 -0
  259. package/src/cli/intake-cli.ts +12 -2
  260. package/src/cli/intake-source-cli.ts +58 -0
  261. package/src/cli/machine-cli.ts +23 -0
  262. package/src/cli/msg-cli.ts +137 -0
  263. package/src/cli/plugin-cli.ts +25 -0
  264. package/src/cli/pr-cli.ts +36 -0
  265. package/src/cli/provider-cli.ts +5 -0
  266. package/src/cli/release-cli.ts +42 -1
  267. package/src/cli/release-run-if-ready.ts +48 -0
  268. package/src/cli/setup-cli.ts +27 -6
  269. package/src/cli/setup-recommend.ts +8 -0
  270. package/src/cli/usage-cli.ts +11 -1
  271. package/src/codex/setup.ts +27 -8
  272. package/src/dashboard/index.ts +1 -1
  273. package/src/discord.ts +56 -6
  274. package/src/domains/core-tools.ts +37 -14
  275. package/src/domains/outbound-alert.ts +50 -11
  276. package/src/exec-requests/planner.ts +140 -0
  277. package/src/exec-requests/runner.ts +338 -0
  278. package/src/exec-requests/store.ts +76 -0
  279. package/src/field/field-media.ts +8 -3
  280. package/src/field/field-reel.ts +196 -0
  281. package/src/grok/credential.ts +7 -5
  282. package/src/harness/aside-run-status.ts +33 -0
  283. package/src/harness/browser-verify.ts +48 -17
  284. package/src/harness/harness-cli-command.ts +25 -3
  285. package/src/harness/image-load-state.ts +21 -0
  286. package/src/harness/process-garbage.ts +45 -0
  287. package/src/index.ts +86 -14
  288. package/src/instance/leader.ts +13 -1
  289. package/src/intake-plane/collect-github.ts +5 -3
  290. package/src/intake-plane/collect-telegram-saved.ts +15 -3
  291. package/src/intake-plane/discord-seat-work.ts +56 -0
  292. package/src/intake-plane/intake-sources.ts +202 -0
  293. package/src/intake-plane/items.ts +28 -12
  294. package/src/intake-plane/seat-doc-route.ts +207 -0
  295. package/src/intake-plane/submit-intake-work.ts +9 -1
  296. package/src/intake-plane/telegram-seat-work.ts +72 -0
  297. package/src/llm/model-defaults.ts +24 -1
  298. package/src/llm.ts +5 -4
  299. package/src/machines/machine-ledger.ts +127 -0
  300. package/src/market/pack-internal-refs.ts +28 -0
  301. package/src/market/publish.ts +31 -4
  302. package/src/maturity/feature-maturity.ts +65 -0
  303. package/src/model-tier/turn-tier.ts +24 -0
  304. package/src/msg/msg-store.ts +149 -0
  305. package/src/native-tool-catalog.ts +2 -1
  306. package/src/nexus/api/chat-fast-path-config.ts +28 -0
  307. package/src/nexus/api/consult-requests.ts +117 -0
  308. package/src/nexus/api/discord-trigger-bot.ts +3 -0
  309. package/src/nexus/api/exec-requests.ts +51 -0
  310. package/src/nexus/api/field-uploads.ts +40 -3
  311. package/src/nexus/api/graph-approvals-feed.ts +140 -0
  312. package/src/nexus/api/graph-approvals.ts +71 -4
  313. package/src/nexus/api/http-server.ts +61 -7
  314. package/src/nexus/api/seat-requests.ts +254 -0
  315. package/src/nexus/api/trace-run-scan.ts +37 -0
  316. package/src/nexus/api/trace.ts +19 -3
  317. package/src/nexus/index.ts +3 -0
  318. package/src/nexus/setup-status.ts +4 -5
  319. package/src/notify/llm-budget-alert.ts +16 -10
  320. package/src/oauth/codex-account-rotation.ts +26 -4
  321. package/src/oauth/codex-account-store.ts +20 -2
  322. package/src/oauth/codex-browser-login.ts +173 -0
  323. package/src/oauth/codex.ts +4 -1
  324. package/src/oauth/fallback-chain.ts +5 -5
  325. package/src/oauth/store.ts +42 -5
  326. package/src/onboarding/continue-here.ts +29 -0
  327. package/src/onboarding/entry-hints.ts +8 -0
  328. package/src/onboarding/full-screen-io.ts +35 -3
  329. package/src/onboarding/help/skills.md +1 -1
  330. package/src/onboarding/non-interactive.ts +1 -0
  331. package/src/onboarding/subscription-hint.ts +27 -0
  332. package/src/onboarding/web-first.ts +83 -0
  333. package/src/onboarding.ts +230 -23
  334. package/src/plugins/maker/node-install.ts +92 -0
  335. package/src/plugins/maker/node-maker.ts +139 -0
  336. package/src/plugins/maker/plugin-maker.ts +19 -7
  337. package/src/policy/llm-policy.ts +268 -0
  338. package/src/prompt-library/session-guidance.ts +7 -1
  339. package/src/prompt-library/universal-preamble.ts +2 -2
  340. package/src/relay/relay-cli.ts +20 -0
  341. package/src/relay/relay-server.ts +220 -0
  342. package/src/release-loop/checklist.ts +9 -8
  343. package/src/self-dev/decompose.ts +1 -0
  344. package/src/self-dev/dev-pipeline.ts +6 -3
  345. package/src/self-dev/run-record-mirror.ts +44 -0
  346. package/src/self-dev/run-store.ts +12 -3
  347. package/src/self-implement/harness-policy-gates.ts +4 -1
  348. package/src/self-implement/report-origin.ts +12 -1
  349. package/src/session/chat.ts +17 -5
  350. package/src/skills/shared-agent-skill-roots.ts +25 -0
  351. package/src/steward/failure-streak.ts +78 -0
  352. package/src/steward/steward-cards.ts +24 -0
  353. package/src/steward/triage-budget.ts +127 -0
  354. package/src/steward/triage-plan.ts +75 -0
  355. package/src/steward/triage.ts +244 -42
  356. package/src/task-cards/card-store.ts +1 -1
  357. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  358. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
  359. package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
  360. package/src/telegram.ts +38 -2
  361. package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
  362. package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
  363. package/src/user-config.ts +80 -4
  364. package/src/version/packed-revision.json +1 -1
  365. package/src/video-pipeline/recipes/character.ts +42 -5
  366. package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
  367. package/src/video-pipeline/recipes/free-line.ts +16 -3
  368. package/src/video-pipeline/recipes/remote.ts +7 -1
  369. package/src/video-pipeline/vflow-reference.ts +109 -0
  370. package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
  379. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
  448. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
  449. package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
  450. /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
@@ -0,0 +1,268 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { isScalar, parseDocument } from 'yaml';
4
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
5
+ import { DEFAULT_FALLBACK_CHAIN, isFallbackStep, type FallbackStep } from '../oauth/fallback-chain.js';
6
+ import { isCodexQuotaPolicy } from '../oauth/codex-quota-policy.js';
7
+ import { DEFAULT_BUDGET_GATE_MAX_USED_PERCENT, MODEL_ROLES, RUNTIME_LLM_PROVIDER_NAMES, parseRoleLlmEntry, type RoleLlmConfig } from '../user-config.js';
8
+
9
+ export interface LlmPolicy {
10
+ version: 1;
11
+ default: { provider: string; model?: string };
12
+ roles: RoleLlmConfig;
13
+ fallback: FallbackStep[];
14
+ accounts: { codex: {
15
+ order: string[];
16
+ rotateAtPercent: number;
17
+ perAccount: Record<string, { rotateAtPercent: number; until?: string; why?: string }>;
18
+ rotationEnabled: boolean;
19
+ } };
20
+ credits: {
21
+ codex: 'use' | 'fallback' | 'never';
22
+ grants: Array<{ account: string; amount: number; expires: string; source: string }>;
23
+ pace?: { targetPerDay: number; until: string; why?: string };
24
+ };
25
+ caps: { codex: { harness: number; pod: number; tox: number; intake: number; headroom: number }; grok: { weekly: number; tox: number; intake: number } };
26
+ resetCredits: { redeem: 'human' };
27
+ alerts: { lowRemainingPercent: number; resetCreditExpiryDays: number; creditsStep: number };
28
+ }
29
+
30
+ export type LlmPolicySource = 'default' | 'legacy-config' | 'file';
31
+ export interface LlmPolicyValidation {
32
+ valid: boolean;
33
+ errors: string[];
34
+ warnings: string[];
35
+ }
36
+ export interface LoadedLlmPolicy extends LlmPolicyValidation {
37
+ policy: LlmPolicy;
38
+ sources: Record<string, LlmPolicySource>;
39
+ path: string;
40
+ filePresent: boolean;
41
+ }
42
+ export interface LoadLlmPolicyOptions {
43
+ /** All file IO remains scoped to this directory; tests can pass a temporary directory. */
44
+ configDir?: string;
45
+ path?: string;
46
+ /** An already-read legacy config; when omitted the sibling config.json is read directly. */
47
+ legacyConfig?: unknown;
48
+ now?: Date;
49
+ }
50
+
51
+ type Obj = Record<string, unknown>;
52
+ const object = (v: unknown): v is Obj => v !== null && typeof v === 'object' && !Array.isArray(v);
53
+ const percent = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v) && v >= 0 && v <= 100;
54
+ const nonempty = (v: unknown): v is string => typeof v === 'string' && v.trim().length > 0;
55
+ const positive = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v) && v > 0;
56
+ const date = (v: unknown): v is string => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v) && !Number.isNaN(Date.parse(`${v}T00:00:00Z`)) && new Date(`${v}T00:00:00Z`).toISOString().slice(0, 10) === v;
57
+
58
+ function setOwn(target: Obj, key: string, value: unknown): void {
59
+ Object.defineProperty(target, key, { value, configurable: true, enumerable: true, writable: true });
60
+ }
61
+
62
+ /** Today's code defaults, not a snapshot of one machine's user config. */
63
+ export function defaultLlmPolicy(): LlmPolicy {
64
+ return {
65
+ version: 1,
66
+ default: { provider: 'auto' },
67
+ roles: {},
68
+ fallback: [...DEFAULT_FALLBACK_CHAIN],
69
+ accounts: { codex: { order: [], rotateAtPercent: 95, perAccount: { default: { rotateAtPercent: 60 } }, rotationEnabled: true } },
70
+ credits: { codex: 'fallback', grants: [] },
71
+ caps: {
72
+ codex: { harness: DEFAULT_BUDGET_GATE_MAX_USED_PERCENT['openai-codex']!, pod: 95, tox: 60, intake: 80, headroom: 15 },
73
+ grok: { weekly: DEFAULT_BUDGET_GATE_MAX_USED_PERCENT.grok!, tox: 50, intake: 48 },
74
+ },
75
+ resetCredits: { redeem: 'human' },
76
+ alerts: { lowRemainingPercent: 10, resetCreditExpiryDays: 7, creditsStep: 5000 },
77
+ };
78
+ }
79
+
80
+ function leafPaths(value: unknown, prefix = ''): string[] {
81
+ if (!object(value) || Object.keys(value).length === 0) return prefix ? [prefix] : [];
82
+ return Object.entries(value).flatMap(([key, item]) => leafPaths(item, prefix ? `${prefix}.${key}` : key));
83
+ }
84
+
85
+ /** Validate the partial policy document. Unknown fields and invalid values are errors, not silent overrides. */
86
+ export function validateLlmPolicy(input: unknown): LlmPolicyValidation {
87
+ const errors: string[] = [];
88
+ const warnings: string[] = [];
89
+ if (!object(input)) return { valid: false, errors: ['policy must be a mapping'], warnings };
90
+ const check = (node: unknown, path: string, schema: Obj): void => {
91
+ if (!object(node)) { errors.push(`${path} must be a mapping`); return; }
92
+ for (const [key, value] of Object.entries(node)) {
93
+ if (!Object.hasOwn(schema, key)) { errors.push(`unknown field ${path}.${key}`); continue; }
94
+ const rule = schema[key];
95
+ if (object(rule)) check(value, `${path}.${key}`, rule);
96
+ else if (typeof rule === 'function' && !(rule as (v: unknown) => boolean)(value)) errors.push(`${path}.${key} is invalid`);
97
+ }
98
+ };
99
+ const dateField = (v: unknown) => date(v);
100
+ const perAccount = (v: unknown) => object(v) && Object.entries(v).every(([key, row]) => nonempty(key) && object(row)
101
+ && Object.keys(row).every(k => ['rotateAtPercent', 'until', 'why'].includes(k))
102
+ && percent(row.rotateAtPercent) && (row.until === undefined || date(row.until)) && (row.why === undefined || nonempty(row.why)));
103
+ const grants = (v: unknown) => Array.isArray(v) && v.every(g => object(g)
104
+ && Object.keys(g).every(k => ['account', 'amount', 'expires', 'source'].includes(k))
105
+ && nonempty(g.account) && positive(g.amount) && date(g.expires) && nonempty(g.source));
106
+ const roles = (v: unknown) => object(v) && Object.entries(v).every(([role, spec]) => MODEL_ROLES.some(r => r === role)
107
+ && object(spec) && Object.keys(spec).every(k => ['provider', 'tier', 'model'].includes(k)) && parseRoleLlmEntry(spec).ok);
108
+ check(input, 'policy', {
109
+ version: (v: unknown) => v === 1,
110
+ default: { provider: (v: unknown) => nonempty(v) && RUNTIME_LLM_PROVIDER_NAMES.some(p => p === v), model: nonempty },
111
+ roles,
112
+ fallback: (v: unknown) => Array.isArray(v) && v.length > 0 && v.every(isFallbackStep) && new Set(v).size === v.length,
113
+ accounts: { codex: {
114
+ order: (v: unknown) => Array.isArray(v) && v.every(nonempty) && new Set(v).size === v.length,
115
+ rotateAtPercent: percent, perAccount, rotationEnabled: (v: unknown) => typeof v === 'boolean',
116
+ } },
117
+ credits: {
118
+ codex: (v: unknown) => v === 'use' || v === 'fallback' || v === 'never',
119
+ grants,
120
+ pace: (v: unknown) => object(v) && positive(v.targetPerDay) && dateField(v.until)
121
+ && (v.why === undefined || nonempty(v.why))
122
+ && Object.keys(v).every(k => ['targetPerDay', 'until', 'why'].includes(k)),
123
+ },
124
+ caps: { codex: { harness: percent, pod: percent, tox: percent, intake: percent, headroom: percent },
125
+ grok: { weekly: percent, tox: percent, intake: percent } },
126
+ resetCredits: { redeem: (v: unknown) => v === 'human' },
127
+ alerts: { lowRemainingPercent: percent, resetCreditExpiryDays: positive, creditsStep: positive },
128
+ });
129
+ if (input.version !== undefined && input.version !== 1) warnings.push('unsupported policy version');
130
+ return { valid: errors.length === 0, errors, warnings };
131
+ }
132
+
133
+ function merge(base: Obj, overlay: Obj, sources: Record<string, LlmPolicySource>, source: LlmPolicySource, prefix = ''): void {
134
+ for (const [key, value] of Object.entries(overlay)) {
135
+ const path = prefix ? `${prefix}.${key}` : key;
136
+ if (object(value) && Object.hasOwn(base, key) && object(base[key])) merge(base[key] as Obj, value, sources, source, path);
137
+ else {
138
+ setOwn(base, key, value !== null && typeof value === 'object' ? structuredClone(value) : value);
139
+ for (const leaf of leafPaths(value, path)) setOwn(sources, leaf, source);
140
+ }
141
+ }
142
+ }
143
+
144
+ /** The date is inclusive: a rule with until=2026-10-29 remains active through that UTC day. */
145
+ function removeExpired(input: Obj, now: Date, warnings: string[], prefix = ''): void {
146
+ for (const [key, value] of Object.entries(input)) {
147
+ const path = prefix ? `${prefix}.${key}` : key;
148
+ if (path === 'credits.grants' && Array.isArray(value)) {
149
+ input[key] = value.filter(grant => {
150
+ if ((grant as { expires: string }).expires >= now.toISOString().slice(0, 10)) return true;
151
+ warnings.push(`${path} ${String((grant as { account: string }).account)} expired on ${String((grant as { expires: string }).expires)}; ignored`);
152
+ return false;
153
+ });
154
+ continue;
155
+ }
156
+ if (!object(value)) continue;
157
+ if (typeof value.until === 'string' && date(value.until) && value.until < now.toISOString().slice(0, 10)) {
158
+ delete input[key];
159
+ warnings.push(`${path} expired on ${value.until}; ignored`);
160
+ } else removeExpired(value, now, warnings, path);
161
+ }
162
+ }
163
+
164
+ export function loadLlmPolicy(opts: LoadLlmPolicyOptions = {}): LoadedLlmPolicy {
165
+ const configDir = opts.configDir ?? getElanousConfigDir();
166
+ const path = opts.path ?? join(configDir, 'policy', 'llm.yaml');
167
+ const policy = defaultLlmPolicy();
168
+ const sources: Record<string, LlmPolicySource> = Object.fromEntries(leafPaths(policy).map(p => [p, 'default']));
169
+ const warnings: string[] = [];
170
+ const errors: string[] = [];
171
+ let legacy: unknown = opts.legacyConfig;
172
+ if (legacy === undefined) {
173
+ const legacyPath = join(configDir, 'config.json');
174
+ try { legacy = JSON.parse(readFileSync(legacyPath, 'utf8')); }
175
+ catch (error) {
176
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
177
+ warnings.push(`legacy config ${legacyPath} could not be loaded: ${error instanceof Error ? error.message : String(error)}`);
178
+ }
179
+ legacy = {};
180
+ }
181
+ }
182
+ if (object(legacy)) {
183
+ const llm = object(legacy.llm) ? legacy.llm : {};
184
+ const overlay: Obj = {};
185
+ const add = (key: string, value: unknown, target: Obj, targetKey: string, valid: (v: unknown) => boolean): void => {
186
+ if (value === undefined) return;
187
+ if (valid(value)) { setOwn(target, targetKey, value); warnings.push(`legacy config ${key} applies; migrate to policy/llm.yaml`); }
188
+ else warnings.push(`legacy config ${key} is invalid; ignored`);
189
+ };
190
+ const base: Obj = {};
191
+ add('llm.provider', llm.provider, base, 'provider', v => nonempty(v) && RUNTIME_LLM_PROVIDER_NAMES.some(p => p === v));
192
+ add('llm.model', llm.model, base, 'model', nonempty);
193
+ if (Object.keys(base).length) overlay.default = base;
194
+ add('roleLlm', legacy.roleLlm, overlay, 'roles', v => object(v) && Object.entries(v).every(([r, spec]) => MODEL_ROLES.some(k => k === r) && parseRoleLlmEntry(spec).ok));
195
+ add('llm.fallbackChain', llm.fallbackChain, overlay, 'fallback', v => Array.isArray(v) && v.length > 0 && v.every(isFallbackStep) && new Set(v).size === v.length);
196
+ const codex: Obj = {};
197
+ add('llm.codexAccountOrder', llm.codexAccountOrder, codex, 'order', v => Array.isArray(v) && v.every(nonempty));
198
+ add('llm.codexAccountRotation', llm.codexAccountRotation, codex, 'rotationEnabled', v => typeof v === 'boolean');
199
+ add('llm.codexAccountRotationThresholdPercent', llm.codexAccountRotationThresholdPercent, codex, 'rotateAtPercent', percent);
200
+ if (object(llm.codexAccountRotationThresholdPercentByAccount)) {
201
+ const rows: Obj = {};
202
+ for (const [account, value] of Object.entries(llm.codexAccountRotationThresholdPercentByAccount)) {
203
+ add(`llm.codexAccountRotationThresholdPercentByAccount.${account}`, value, rows, account, percent);
204
+ if (Object.hasOwn(rows, account)) setOwn(rows, account, { rotateAtPercent: rows[account] });
205
+ }
206
+ if (Object.keys(rows).length) codex.perAccount = rows;
207
+ }
208
+ if (Object.keys(codex).length) overlay.accounts = { codex };
209
+ const credits: Obj = {};
210
+ if (llm.codexQuotaPolicy !== undefined) {
211
+ if (isCodexQuotaPolicy(llm.codexQuotaPolicy)) {
212
+ credits.codex = { credits: 'use', fallback: 'fallback', 'within-quota': 'never' }[llm.codexQuotaPolicy];
213
+ warnings.push('legacy config llm.codexQuotaPolicy applies; migrate to policy/llm.yaml');
214
+ } else warnings.push('legacy config llm.codexQuotaPolicy is invalid; ignored');
215
+ }
216
+ if (credits.codex === undefined && llm.codexCreditsAllowed === true) {
217
+ credits.codex = 'use';
218
+ warnings.push('legacy config llm.codexCreditsAllowed applies; migrate to policy/llm.yaml');
219
+ }
220
+ if (Object.keys(credits).length) overlay.credits = credits;
221
+ const gate = object(legacy.harness) && object(legacy.harness.budgetGate) ? legacy.harness.budgetGate : {};
222
+ const caps: Obj = {};
223
+ const harness: Obj = {};
224
+ add('harness.budgetGate.minHeadroomPercent', gate.minHeadroomPercent, harness, 'headroom', percent);
225
+ if (object(gate.maxUsedPercent)) {
226
+ add('harness.budgetGate.maxUsedPercent.openai-codex', gate.maxUsedPercent['openai-codex'], harness, 'harness', percent);
227
+ const grok: Obj = {};
228
+ add('harness.budgetGate.maxUsedPercent.grok', gate.maxUsedPercent.grok, grok, 'weekly', percent);
229
+ if (Object.keys(grok).length) caps.grok = grok;
230
+ }
231
+ if (Object.keys(harness).length) caps.codex = harness;
232
+ if (Object.keys(caps).length) overlay.caps = caps;
233
+ merge(policy as unknown as Obj, overlay, sources, 'legacy-config');
234
+ }
235
+ // Read directly: `existsSync` is also false when the folder cannot be searched (EACCES), which would
236
+ // report an unreadable policy as «no file · valid». Only ENOENT/ENOTDIR mean the file is absent.
237
+ let raw: string | undefined;
238
+ let filePresent = false;
239
+ try {
240
+ raw = readFileSync(path, 'utf8');
241
+ filePresent = true;
242
+ } catch (error) {
243
+ const code = (error as NodeJS.ErrnoException).code;
244
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
245
+ filePresent = true;
246
+ errors.push(`cannot read policy file ${path}: ${code ?? (error instanceof Error ? error.message : String(error))}`);
247
+ }
248
+ }
249
+ if (raw !== undefined) {
250
+ try {
251
+ const doc = parseDocument(raw, { uniqueKeys: true });
252
+ if (doc.errors.length) throw new Error(doc.errors.map(e => e.message).join('; '));
253
+ const emptyDocument = doc.contents === null || (isScalar(doc.contents) && doc.contents.type === 'PLAIN' && doc.contents.source === '');
254
+ const parsed: unknown = emptyDocument ? {} : doc.toJS();
255
+ const validation = validateLlmPolicy(parsed);
256
+ errors.push(...validation.errors);
257
+ warnings.push(...validation.warnings);
258
+ if (validation.valid && object(parsed)) {
259
+ const filtered = structuredClone(parsed);
260
+ removeExpired(filtered, opts.now ?? new Date(), warnings);
261
+ merge(policy as unknown as Obj, filtered, sources, 'file');
262
+ }
263
+ } catch (error) {
264
+ errors.push(`invalid policy file ${path}: ${error instanceof Error ? error.message : String(error)}`);
265
+ }
266
+ }
267
+ return { policy, sources, path, filePresent, valid: errors.length === 0, errors, warnings };
268
+ }
@@ -115,9 +115,15 @@ export function buildSessionGuidanceAddendum(
115
115
  }
116
116
 
117
117
  if (items.length === 0) return [];
118
+ // B4 — 파일 수 규칙. 명령을 실제로 돌릴 도구(Bash·PtyShellStart·WebTerminal*)가 있을 때만 `git ls-files` 로 세라고 하고,
119
+ // Read 만 켜진 세션에선 셀 수 없으니 추측하지 말고 확인 불가라고 답하게 한다(EVAL q09 «959» vs 실측 778 · 리뷰 3라운드 must-fix).
120
+ const canRunCommands = enabledTools.some(name => /^(Bash|PtyShellStart|WebTerminal)/.test(name));
121
+ const fileCountRule = canRunCommands
122
+ ? '저장소의 파일 수·목록은 저장소 루트에서 `git ls-files <경로>`로 추적 파일 기준으로 센다 — 하위 디렉터리에서 실행 중이면 저장소 루트로 이동해서 세고, 다른 기준이면 그 기준을 답에 적는다.'
123
+ : '이 세션에는 명령을 실행할 도구가 없다 — 저장소의 파일 수는 추측하지 말고 «이 모드에서는 셀 수 없다»고 답한다(도구로 센 목록이 대화에 있으면 그 기준을 밝히고 쓴다).';
118
124
  return [{
119
125
  role: 'system',
120
- content: '# Session-specific guidance\n\n' +
126
+ content: `${fileCountRule}\n\n` + '# Session-specific guidance\n\n' +
121
127
  items.map(s => `- ${s}`).join('\n'),
122
128
  }];
123
129
  }
@@ -60,8 +60,8 @@ export const PROJECT_TREE_MAX_CHARS = 8 * 1024;
60
60
  export const PROJECT_TREE_DIR_ENTRY_LIMIT = 20;
61
61
 
62
62
  const NO_TOOLS_VERIFICATION_GUIDANCE = `## Chat without tools — verify rather than guess
63
- This conversation has tools disabled. Do not guess repository or real-time facts that require verification, such as file counts, file contents, commits, or the current time. Say you cannot check them here. The project tree above is for navigation, not evidence for counts.
64
- To check these facts, enable tools with \`elanous agent …\` or \`elanous chat --tools …\`.`;
63
+ This conversation has tools disabled. Do not guess repository or real-time facts that require verification, such as file counts, file contents, commits, or the current date or time. When asked for facts you cannot verify here (including repository state, files, and dates), explicitly say you cannot check them in this mode. The project tree above is for navigation, not evidence for counts.
64
+ Give one line explaining how to enable tools: \`elanous agent "<your question>"\`.`;
65
65
 
66
66
  // P4 (2026-05-03) — Family-agnostic coding-agent lifecycle. Maps the
67
67
  // three user-facing pipelines to an explicit tool-batch sequence so every
@@ -0,0 +1,20 @@
1
+ import type { Command } from 'commander';
2
+
3
+ /** `elanous relay serve` — 릴레이 서버(R2 · 내부 문서 `RFC-mobile-relay-without-tailscale-2026-09-30` A5).
4
+ * index.ts 원문을 잘라 보는 시험(첫 `.command('serve')` = mcp)을 흔들지 않게 등록을 여기 둔다. */
5
+ export function registerRelayCommand(program: Command): void {
6
+ const relayCmd = program.command('relay').description('Encrypted WebSocket relay');
7
+ relayCmd.command('serve')
8
+ .description('Serve paired relay connections using operator-provisioned host signing keys')
9
+ .requiredOption('--trusted-host-keys <file>', 'JSON object of X25519-derived serverId to Ed25519 public key (base64url)')
10
+ .option('--port <port>', 'Listening port', '8765')
11
+ .action(async (opts: { trustedHostKeys: string; port: string }) => {
12
+ const port = Number(opts.port);
13
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('invalid relay port');
14
+ const { loadTrustedHostKeys, startRelayServer } = await import('./relay-server.js');
15
+ const relay = startRelayServer({ port, trustedHostKeys: loadTrustedHostKeys(opts.trustedHostKeys) });
16
+ console.log(`relay listening on 127.0.0.1:${relay.port}`);
17
+ process.once('SIGINT', () => { relay.stop(); });
18
+ process.once('SIGTERM', () => { relay.stop(); });
19
+ });
20
+ }
@@ -0,0 +1,220 @@
1
+ import { createPublicKey, randomBytes, verify } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import type { ServerWebSocket } from 'bun';
4
+ import { MAX_FRAME_BYTES } from './channel.js';
5
+
6
+ const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
7
+ const MAX_CLIENTS_PER_SERVER = 8;
8
+ const BYTES_PER_SECOND = 1024 * 1024;
9
+ const BURST_BYTES = 2 * BYTES_PER_SECOND;
10
+ const PING_INTERVAL_MS = 30_000;
11
+ const DEAD_PEER_MS = 90_000;
12
+ const AUTH_TIMEOUT_MS = 10_000;
13
+
14
+ type SocketData = { role: 'host' | 'client'; serverId: string; clientId?: number; challenge?: string; authenticated: boolean; lastSeen: number; tokens: number; refillAt: number; authTimer?: ReturnType<typeof setTimeout> };
15
+ type Socket = ServerWebSocket<SocketData>;
16
+ type Room = { host?: Socket; clients: Map<number, Socket>; nextId: number; bytes: number; failures: Record<string, number> };
17
+
18
+ export interface RelayServerOptions {
19
+ port?: number;
20
+ hostname?: string;
21
+ /** Ed25519 signing public keys (base64url raw 32 bytes), pinned by X25519-derived serverId. */
22
+ trustedHostKeys?: ReadonlyMap<string, string>;
23
+ }
24
+
25
+ /** Load operator-provisioned pins, never keys claimed by connecting hosts. */
26
+ export function loadTrustedHostKeys(file: string): ReadonlyMap<string, string> {
27
+ const raw: unknown = JSON.parse(readFileSync(file, 'utf8'));
28
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('invalid relay trusted host keys');
29
+ const result = new Map<string, string>();
30
+ for (const [id, key] of Object.entries(raw)) {
31
+ if (!/^[a-f0-9]{64}$/.test(id) || typeof key !== 'string' || !decodeKey(key)) {
32
+ throw new Error('invalid relay trusted host key entry');
33
+ }
34
+ result.set(id, key);
35
+ }
36
+ return result;
37
+ }
38
+
39
+ export function challengeMessage(serverId: string, nonce: string): Uint8Array {
40
+ return Buffer.from(`elanous-relay-v1:${serverId}:${nonce}`, 'utf8');
41
+ }
42
+
43
+ function decodeKey(value: string): Buffer | undefined {
44
+ if (!/^[A-Za-z0-9_-]{43}$/.test(value)) return undefined;
45
+ const decoded = Buffer.from(value, 'base64url');
46
+ return decoded.length === 32 && decoded.toString('base64url') === value ? decoded : undefined;
47
+ }
48
+
49
+ function encodeClientId(id: number, payload: Uint8Array): Uint8Array {
50
+ const result = new Uint8Array(payload.length + 4);
51
+ new DataView(result.buffer).setUint32(0, id, false);
52
+ result.set(payload, 4);
53
+ return result;
54
+ }
55
+
56
+ /** The relay never parses or logs encrypted channel payloads. Host binary frames carry a
57
+ * four-byte big-endian clientId prefix; phone binary frames are raw ciphertext. */
58
+ export function startRelayServer(options: RelayServerOptions = {}) {
59
+ if (!options.trustedHostKeys?.size) throw new Error('relay trusted host keys required');
60
+ const trustedHostKeys = new Map(options.trustedHostKeys);
61
+ for (const [id, key] of trustedHostKeys) {
62
+ if (!/^[a-f0-9]{64}$/.test(id) || !decodeKey(key)) throw new Error('invalid relay trusted host key entry');
63
+ }
64
+ const rooms = new Map<string, Room>();
65
+ const sockets = new Set<Socket>();
66
+ const roomFor = (id: string): Room => {
67
+ let room = rooms.get(id);
68
+ if (!room) { room = { clients: new Map(), nextId: 1, bytes: 0, failures: {} }; rooms.set(id, room); }
69
+ return room;
70
+ };
71
+ const fail = (room: Room, reason: string) => { room.failures[reason] = (room.failures[reason] ?? 0) + 1; };
72
+ const sendControl = (ws: Socket, value: object) => { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(value)); };
73
+ const drop = (ws: Socket) => {
74
+ const data = ws.data;
75
+ if (data.authTimer) clearTimeout(data.authTimer);
76
+ sockets.delete(ws);
77
+ const room = rooms.get(data.serverId);
78
+ if (!room) return;
79
+ if (!data.authenticated) {
80
+ if (!room.host && room.clients.size === 0 &&
81
+ !Array.from(sockets).some(peer => peer.data.serverId === data.serverId)) rooms.delete(data.serverId);
82
+ return;
83
+ }
84
+ if (data.role === 'host' && room.host === ws) {
85
+ room.host = undefined;
86
+ const clients = [...room.clients.values()];
87
+ room.clients.clear();
88
+ for (const client of clients) client.close(1012, 'host disconnected');
89
+ } else if (data.role === 'client' && data.clientId !== undefined && room.clients.get(data.clientId) === ws) {
90
+ room.clients.delete(data.clientId);
91
+ if (room.host) sendControl(room.host, { type: 'client-disconnected', clientId: data.clientId });
92
+ }
93
+ if (!room.host && room.clients.size === 0 &&
94
+ !Array.from(sockets).some(peer => peer.data.serverId === data.serverId)) rooms.delete(data.serverId);
95
+ };
96
+ const capacity = (ws: Socket, length: number): boolean => {
97
+ const data = ws.data;
98
+ const now = Date.now();
99
+ data.tokens = Math.min(BURST_BYTES, data.tokens + (now - data.refillAt) * BYTES_PER_SECOND / 1000);
100
+ data.refillAt = now;
101
+ if (length > data.tokens) return false;
102
+ data.tokens -= length;
103
+ return true;
104
+ };
105
+ const server = Bun.serve<SocketData>({
106
+ hostname: options.hostname ?? '127.0.0.1', port: options.port ?? 0,
107
+ fetch(req, server) {
108
+ const url = new URL(req.url);
109
+ if (url.pathname !== '/host' && url.pathname !== '/client') return new Response('not found', { status: 404 });
110
+ const serverId = url.searchParams.get('serverId');
111
+ if (!serverId || !/^[a-f0-9]{64}$/.test(serverId)) return new Response('invalid serverId', { status: 400 });
112
+ if (!trustedHostKeys.has(serverId)) return new Response('unknown serverId', { status: 403 });
113
+ if (req.headers.get('upgrade')?.toLowerCase() !== 'websocket') return new Response('upgrade required', { status: 426 });
114
+ const role = url.pathname === '/host' ? 'host' : 'client';
115
+ const now = Date.now();
116
+ if (!server.upgrade(req, { data: { role, serverId, authenticated: false, lastSeen: now, tokens: BURST_BYTES, refillAt: now } })) {
117
+ return new Response('upgrade required', { status: 426 });
118
+ }
119
+ return undefined;
120
+ },
121
+ websocket: {
122
+ maxPayloadLength: MAX_FRAME_BYTES + 4,
123
+ open(ws) {
124
+ sockets.add(ws);
125
+ const data = ws.data;
126
+ const room = roomFor(data.serverId);
127
+ if (data.role === 'host') {
128
+ if (room.host) { fail(room, 'duplicate-host'); ws.close(1008, 'host already connected'); return; }
129
+ data.challenge = randomBytes(32).toString('base64url');
130
+ sendControl(ws, { type: 'challenge', nonce: data.challenge });
131
+ } else {
132
+ if (!room.host || room.clients.size >= MAX_CLIENTS_PER_SERVER) {
133
+ fail(room, !room.host ? 'host-offline' : 'client-limit');
134
+ ws.close(1008, !room.host ? 'host offline' : 'client limit reached');
135
+ return;
136
+ }
137
+ const id = room.nextId++;
138
+ data.clientId = id;
139
+ data.authenticated = true;
140
+ room.clients.set(id, ws);
141
+ sendControl(ws, { type: 'connected', clientId: id });
142
+ sendControl(room.host, { type: 'client-connected', clientId: id });
143
+ }
144
+ if (!data.authenticated) data.authTimer = setTimeout(() => ws.close(1008, 'authentication timeout'), AUTH_TIMEOUT_MS);
145
+ },
146
+ message(ws, message) {
147
+ const data = ws.data;
148
+ const room = rooms.get(data.serverId);
149
+ if (!room) { ws.close(1008, 'room missing'); return; }
150
+ data.lastSeen = Date.now();
151
+ if (!data.authenticated) {
152
+ if (data.role !== 'host' || typeof message !== 'string') { fail(room, 'invalid-auth'); ws.close(1008, 'authentication required'); return; }
153
+ let auth: unknown;
154
+ try { auth = JSON.parse(message); } catch { /* reject below */ }
155
+ const value = auth as { type?: unknown; publicKey?: unknown; signature?: unknown } | undefined;
156
+ const key = typeof value?.publicKey === 'string' ? decodeKey(value.publicKey) : undefined;
157
+ const signature = typeof value?.signature === 'string' && /^[A-Za-z0-9_-]{86}$/.test(value.signature)
158
+ ? Buffer.from(value.signature, 'base64url') : undefined;
159
+ const expected = trustedHostKeys.get(data.serverId);
160
+ let valid = false;
161
+ if (value?.type === 'auth' && key && signature?.length === 64 && data.challenge &&
162
+ value.publicKey === expected) {
163
+ try {
164
+ valid = verify(null, challengeMessage(data.serverId, data.challenge),
165
+ createPublicKey({ key: Buffer.concat([ED25519_SPKI_PREFIX, key]), format: 'der', type: 'spki' }), signature);
166
+ } catch { /* invalid public key */ }
167
+ }
168
+ if (!valid || (room.host && room.host !== ws)) { fail(room, 'invalid-auth'); ws.close(1008, 'invalid host signature'); return; }
169
+ data.authenticated = true;
170
+ data.challenge = undefined;
171
+ if (data.authTimer) clearTimeout(data.authTimer);
172
+ room.host = ws;
173
+ sendControl(ws, { type: 'authenticated' });
174
+ return;
175
+ }
176
+ if (typeof message === 'string') { fail(room, 'plaintext-data'); ws.close(1003, 'binary ciphertext required'); return; }
177
+ const bytes = new Uint8Array(message);
178
+ if (bytes.length > MAX_FRAME_BYTES + (data.role === 'host' ? 4 : 0) || bytes.length < (data.role === 'host' ? 5 : 1)) {
179
+ fail(room, 'invalid-frame'); ws.close(1009, 'invalid frame size'); return;
180
+ }
181
+ if (!capacity(ws, bytes.length)) { fail(room, 'rate-limit'); ws.close(1008, 'rate limit exceeded'); return; }
182
+ if (data.role === 'client') {
183
+ if (!room.host || data.clientId === undefined) { ws.close(1012, 'host offline'); return; }
184
+ room.bytes += bytes.length;
185
+ room.host.send(encodeClientId(data.clientId, bytes));
186
+ } else {
187
+ if (room.host !== ws) { ws.close(1008, 'host replaced'); return; }
188
+ const clientId = new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, false);
189
+ const client = room.clients.get(clientId);
190
+ if (!client) { fail(room, 'unknown-client'); return; }
191
+ room.bytes += bytes.length - 4;
192
+ client.send(bytes.subarray(4));
193
+ }
194
+ },
195
+ pong(ws) { ws.data.lastSeen = Date.now(); },
196
+ close(ws) { drop(ws); },
197
+ },
198
+ });
199
+ const heartbeat = setInterval(() => {
200
+ for (const ws of sockets) {
201
+ if (Date.now() - ws.data.lastSeen > DEAD_PEER_MS) ws.close(1001, 'heartbeat timeout');
202
+ else ws.ping();
203
+ }
204
+ }, PING_INTERVAL_MS);
205
+ heartbeat.unref();
206
+ return {
207
+ server,
208
+ get port() { return server.port; },
209
+ stats(serverId: string) {
210
+ const room = rooms.get(serverId);
211
+ return room ? { host: !!room.host, clients: room.clients.size, bytes: room.bytes, failures: { ...room.failures } } :
212
+ { host: false, clients: 0, bytes: 0, failures: {} as Record<string, number> };
213
+ },
214
+ stop() {
215
+ clearInterval(heartbeat);
216
+ for (const ws of sockets) ws.close(1001, 'relay stopping');
217
+ server.stop(true);
218
+ },
219
+ };
220
+ }
@@ -4,6 +4,7 @@ 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';
9
10
  export type ChecklistDisposition = 'move' | 'known-issue' | 'block';
@@ -54,7 +55,7 @@ function releasedVersion(): string {
54
55
  }
55
56
 
56
57
  function pathFor(v: string): string {
57
- 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}`);
58
59
  return join(releaseLedgerRoot(), 'release', v, 'checklist.json');
59
60
  }
60
61
 
@@ -110,10 +111,10 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
110
111
  function refresh(data: Checklist): void { data.released = releasedVersion(); data.dev = devVersion(); }
111
112
 
112
113
  export function addItem(v: string, input: { id: string; title: string; owner?: string }): Checklist {
113
- if (!input.id.trim()) throw new Error('칸 id 가 비었다');
114
- if (!input.title.trim()) throw new Error('칸 제목이 비었다');
114
+ if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
115
+ if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
115
116
  return mutate(v, (data) => {
116
- 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> 로 고친다');
117
118
  const by = process.env.ELANOUS_TRACK || 'cli';
118
119
  const at = new Date().toISOString();
119
120
  const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), updatedAt: at, updatedBy: by };
@@ -126,9 +127,9 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
126
127
  export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition }, by: string): Checklist {
127
128
  return mutate(v, (data) => {
128
129
  const item = data.items.find((i) => i.id === id);
129
- if (!item) throw new Error(`없는 칸: ${id}`);
130
- if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new Error(`잘못된 상태: ${patch.status}`);
131
- if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new Error(`잘못된 처분: ${patch.disposition}`);
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}`);
132
133
  const fields = (['evidence', 'owner', 'status', 'disposition'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
133
134
  if (fields.length === 0) return false;
134
135
  const at = new Date().toISOString();
@@ -147,7 +148,7 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
147
148
  export function removeItem(v: string, id: string, by: string): Checklist {
148
149
  return mutate(v, (data) => {
149
150
  const index = data.items.findIndex((i) => i.id === id);
150
- if (index < 0) throw new Error(`없는 칸: ${id}`);
151
+ if (index < 0) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
151
152
  const at = new Date().toISOString();
152
153
  const [item] = data.items.splice(index, 1);
153
154
  change(data, id, 'remove', item, null, by, at);
@@ -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
  }