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
@@ -23,28 +23,22 @@
23
23
  // state: ~/.elanous/conatus/codex_quota_alert_state.json
24
24
 
25
25
  import { sendOutbound } from '../src/domains/outbound-alert.js';
26
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
26
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
27
27
  import { execFileSync } from 'node:child_process';
28
28
  import { homedir } from 'node:os';
29
29
  import { dirname, join } from 'node:path';
30
30
  import { ensureCronNodePath } from '../src/domains/cron-path.js';
31
31
  import { registerStandaloneLogSink } from '../src/domains/standalone-log-sink.js';
32
32
  import { getUserConfig } from '../src/user-config.js';
33
- import { creditsSummary, formatCreditsSummary } from './codex-quota-alert-format.js';
33
+ import { buildLlmBudgetAlert, type LlmBudgetSnapshot } from '../src/notify/llm-budget-alert.js';
34
+ import { creditPaceStatus, recordCreditBalances } from '../src/budget/codex-credit-pace.js';
35
+ import { loadLlmPolicy } from '../src/policy/llm-policy.js';
36
+ import { DEFAULT_FALLBACK_CHAIN } from '../src/oauth/fallback-chain.js';
34
37
  import {
35
- CODEX_QUOTA_POLICY_LABEL, codexPolicyAllowsCredits, codexPolicyAllowsFallback, resolveCodexQuotaPolicy,
38
+ codexPolicyAllowsCredits, codexPolicyAllowsFallback, resolveCodexQuotaPolicy,
36
39
  } from '../src/oauth/codex-quota-policy.js';
37
40
 
38
41
  const STATE = join(homedir(), '.elanous/conatus/codex_quota_alert_state.json');
39
- /** 잔여가 이 % 이하로 내려간 계정을 «임박»으로 본다. */
40
- const LOW_REMAINING_PERCENT = 10;
41
- /** 리셋권 만료가 이 일수 안이면 알린다. */
42
- const RESET_CREDIT_EXPIRY_DAYS = 7;
43
- /** 정책 `credits`(크레딧 사용이 «허가된» 상태)에서 잔액 보고의 발송 단위 — 이만큼 줄 때마다 한 번.
44
- * ⛔ 허가된 지출을 매시 경보로 보내면 진짜 경보(리셋 전 고갈 · 정책 위반)가 묻힌다(대표 2026-09-28 «정책과 안 맞는다»).
45
- * 허가 «밖»(within-quota·fallback)에서는 종전대로 줄 때마다 보낸다 — 그때는 줄어드는 것 자체가 사고다. */
46
- const CREDITS_REPORT_STEP = 5000;
47
-
48
42
  interface AccountRow {
49
43
  provider: string; accountName: string;
50
44
  credits?: { usedPercent?: number; periodEnd?: string; balance?: number; hasCredits?: boolean };
@@ -86,283 +80,153 @@ function hoursUntil(ms?: number): number | null {
86
80
  if (typeof ms !== 'number' || !Number.isFinite(ms)) return null;
87
81
  return Math.round(((ms - Date.now()) / 3_600_000) * 10) / 10;
88
82
  }
89
- function daysUntil(iso?: string): number | null {
90
- if (!iso) return null;
91
- const t = Date.parse(iso);
92
- if (Number.isNaN(t)) return null;
93
- return Math.round(((t - Date.now()) / 86_400_000) * 10) / 10;
94
- }
95
-
96
- function loadState(): Record<string, string> {
97
- try { return JSON.parse(readFileSync(STATE, 'utf8')) as Record<string, string>; } catch { return {}; }
83
+ function loadState(path = STATE): Record<string, string> {
84
+ try { return JSON.parse(readFileSync(path, 'utf8')) as Record<string, string>; } catch { return {}; }
98
85
  }
99
- function saveState(s: Record<string, string>): void {
100
- try { mkdirSync(dirname(STATE), { recursive: true }); writeFileSync(STATE, JSON.stringify(s, null, 1)); }
86
+ function saveState(s: Record<string, string>, path = STATE): void {
87
+ try { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, JSON.stringify(s, null, 1)); }
101
88
  catch { /* 상태 저장 실패가 발송을 막지 않는다 */ }
102
89
  }
103
90
 
104
- export async function runCodexQuotaAlert(): Promise<void> {
105
- const usage = readUsage();
91
+ export async function runCodexQuotaAlert(deps: { readUsage?: typeof readUsage; statePath?: string; send?: typeof sendOutbound } = {}): Promise<void> {
92
+ const send = deps.send ?? sendOutbound;
93
+ const usage = (deps.readUsage ?? readUsage)();
94
+ const load = () => loadState(deps.statePath);
95
+ const save = (s: Record<string, string>) => saveState(s, deps.statePath);
106
96
  if ('error' in usage) {
107
97
  // ⛔ 「못 쟀다」도 알린다 — 조회가 죽은 채로 조용하면 한도 경고가 «영영» 안 온다.
108
98
  const key = 'usage-unreadable';
109
- const prev = loadState();
110
- if (prev[key] !== usage.error) {
111
- sendOutbound(`⚠️ **LLM 한도 조회가 실패했습니다** — 경고가 이 상태로는 안 옵니다.\n\n사유: \`${usage.error}\``, 'alert');
112
- saveState({ ...prev, [key]: usage.error });
99
+ const prev = load();
100
+ if (prev[key] !== usage.error && send(`⚠️ **LLM 한도 조회가 실패했습니다** — 경고가 이 상태로는 안 옵니다.\n\n사유: \`${usage.error}\``, 'alert')) {
101
+ save({ ...prev, [key]: usage.error });
113
102
  }
114
103
  process.exit(0);
115
104
  }
116
105
 
117
106
  // 대표 2026-09-28 한도 정책(`llm.codexQuotaPolicy`) — 알림 문구·발송 기준이 «설정된 정책»을 따른다.
118
107
  let quota = resolveCodexQuotaPolicy(undefined);
119
- try { quota = resolveCodexQuotaPolicy(getUserConfig().llm); } catch { /* 못 읽으면 기본(fallback) */ }
108
+ let configuredFallback: string[] | undefined;
109
+ try {
110
+ const llm = getUserConfig().llm;
111
+ quota = resolveCodexQuotaPolicy(llm);
112
+ configuredFallback = llm.fallbackChain;
113
+ } catch { /* 못 읽으면 기본(fallback) */ }
120
114
  const creditsAllowed = codexPolicyAllowsCredits(quota.policy);
121
115
  const fallbackAllowed = codexPolicyAllowsFallback(quota.policy);
122
116
 
123
117
  const codex = usage.rows.filter((r) => r.provider === 'codex');
124
118
  const others = usage.rows.filter((r) => r.provider !== 'codex');
125
-
126
- /**
127
- * 🩸 «못 읽은 계정» — 이 자의 가장 위험한 실패다.
128
- * 📏 2026-09-19 실측(🅕 관측): `usage --json` 이 ***간헐적으로 전부 null 을 낸다***.
129
- * 같은 계정을 몇십 초 차로 두 번 쟀는데 한쪽은 `used=100 · hasCredits=true · 잔액 2965`,
130
- * 다른 쪽은 ***전부 None*** 이었다. 그 틱에 종전 코드는:
131
- * exhausted : (rem ?? 100) <= 0 ⇒ null 이 «100» 이 되어 「소진 아님」
132
- * burning : capped && hasCredits ⇒ capped=false 라 「안 태운다」
133
- * low : typeof rem === 'number' ⇒ 건너뜀
134
- * ⇒ ***세 갈래가 모두 조용해져 「경고 조건 없음」을 낸다 — 자가 눈이 먼 채 「정상」이라 말한다.***
135
- * 🩹 그래서 「못 읽음」을 «값으로» 센다. ⛔ 「안전」으로도 「소진」으로도 접지 않는다.
136
- */
137
- const unreadable = codex.filter((r) => {
138
- const rem = r.subscription?.remainingPercent;
139
- const used = r.credits?.usedPercent;
140
- return typeof rem !== 'number' && typeof used !== 'number';
141
- });
142
-
143
- // ⛔ 「못 읽음」은 소진 판정에서 «뺀다» — 그것은 별도 갈래이고, 섞으면 둘 다 못 읽는다.
144
- const exhausted = codex.filter((r) => !unreadable.includes(r)
145
- && (r.subscription?.remainingPercent ?? 100) <= 0);
146
- const low = codex.filter((r) => {
147
- const rem = r.subscription?.remainingPercent;
148
- return typeof rem === 'number' && rem > 0 && rem <= LOW_REMAINING_PERCENT;
149
- });
150
- // 🔥 «돈이 나가는 중» — 한도를 넘겼는데 요청이 안 죽는 계정. 소진 경고와 «다른 축»이다:
151
- // 소진은 「멈췄다」이고 이것은 ***「안 멈췄다」***이다. 후자가 과금이다.
152
- const burning = codex.filter((r) => {
153
- if (unreadable.includes(r)) return false; // 못 읽은 것은 위 unreadable 이 «따로» 말한다
154
- const rem = r.subscription?.remainingPercent;
155
- const used = r.credits?.usedPercent;
156
- const capped = (typeof rem === 'number' && rem <= 0) || (typeof used === 'number' && used >= 100);
157
- return capped && r.credits?.hasCredits === true;
158
- });
159
-
160
- const expiring = codex.filter((r) => {
161
- const d = daysUntil(r.resetCredits?.expiresAt);
162
- return r.resetCredits?.status === 'available' && d !== null && d <= RESET_CREDIT_EXPIRY_DAYS;
163
- });
119
+ const unreadableCount = codex.filter((r) => typeof r.subscription?.remainingPercent !== 'number'
120
+ || !Number.isFinite(r.subscription.remainingPercent)).length;
121
+ const knownExhausted = codex.filter((r) => typeof r.subscription?.remainingPercent === 'number'
122
+ && Number.isFinite(r.subscription.remainingPercent) && r.subscription.remainingPercent <= 0);
123
+ const knownLow = codex.filter((r) => typeof r.subscription?.remainingPercent === 'number'
124
+ && Number.isFinite(r.subscription.remainingPercent) && r.subscription.remainingPercent > 0 && r.subscription.remainingPercent <= 10);
125
+ const exhausted = codex.length > 0 && unreadableCount === 0 && knownExhausted.length === codex.length;
126
+ const creditAvailable = codex.some((r) => r.credits?.hasCredits === true && typeof r.credits.balance === 'number' && r.credits.balance > 0);
127
+ const fallback = fallbackAllowed ? others.find((r) =>
128
+ (configuredFallback ?? DEFAULT_FALLBACK_CHAIN).some((step) => step === r.provider)
129
+ && typeof r.subscription?.remainingPercent === 'number' && Number.isFinite(r.subscription.remainingPercent)
130
+ && r.subscription.remainingPercent > 0) : undefined;
164
131
 
165
132
  const nowMs = Date.now();
166
- const prevState = loadState();
133
+ const prevState = load();
167
134
 
168
- /**
169
- * 🩸 「지금도 나가는 중인가」는 «임계»로 못 잡는다 — 그 자가 한 번 눈이 멀었다.
170
- * 📏 2026-09-18: 실물 유출이 «분당 -0.33» 이었는데, 종전 서명은 잔액을 50 단위로 접어서
171
- * ***한 시간에 -20 이면 구간을 안 넘어 「상태 동일 — 무발송」*** 을 냈다. 즉 «켜져 있는데 안 보였다».
172
- * 🩹 그래서 임계를 버리고 ***직전 표본 대비 «감소했나»*** 로 바꾼다 — 얼마든 줄면 줄었다고 말한다.
173
- * ⛔ 도배되지 않는 이유는 «감소 자체»가 아니라 `burning` 게이트다(한도 100% ⊕ hasCredits=true).
174
- * 그 상태에서 줄고 있으면 그것은 ***지금 돈이 나가는 중***이고, 매 틱 알리는 것이 옳다.
175
- * ⛔ 직전 표본이 «없으면» 「안 준다」가 아니라 「모른다」다 — 첫 틱은 기준선만 적는다.
176
- */
177
- function drainOf(r: AccountRow): { state: 'draining' | 'flat' | 'unknown'; delta?: number; perHour?: number; windowMin?: number } {
178
- const now = r.credits?.balance;
179
- const before = Number(prevState[`balance:${r.accountName}`]);
180
- const beforeAt = Number(prevState[`balanceAt:${r.accountName}`]);
181
- if (typeof now !== 'number' || !Number.isFinite(before)) return { state: 'unknown' };
182
- const delta = now - before;
183
- if (delta >= 0) return { state: 'flat', delta };
184
- const hours = Number.isFinite(beforeAt) ? (nowMs - beforeAt) / 3_600_000 : NaN;
185
- // ⛔ 속도와 ETA 는 «한 구간의 기울기»다 — 창이 짧으면 버스트 하나가 그 기울기를 지배한다.
186
- // 📏 2026-09-18 실측: 5분 창이 «시간당 -165 · 19시간 뒤 0» 을 냈는데, 직후 187초 실측은
187
- // «시간당 -4.2»(33배 차이)였다. 앞의 수는 틀린 게 아니라 ***그 창에서 참***이었고,
188
- // 읽는 사람이 그것을 「지속 속도」로 읽은 것이 문제다.
189
- // 🩹 ⇒ 임의 임계로 «막지» 않는다(그 상수를 관례로 채울 근거가 없다).
190
- // 대신 ***창의 길이를 값으로 같이 낸다*** — 읽는 쪽이 짧은 창을 스스로 깎게 한다.
191
- return {
192
- state: 'draining', delta,
193
- ...(hours > 0 ? { perHour: delta / hours, windowMin: Math.round(hours * 60) } : {}),
194
- };
195
- }
196
- const drain = new Map((quota.policy === 'credits' ? codex : burning)
197
- .map((r) => [r.accountName, drainOf(r)] as const));
198
- // 🩸 2026-09-28: 머리줄이 «찬 ⊕ 크레딧 있음» 계정 수(3)를 «나가는 중» 으로 셌다 — 실제로 줄던 것은 default 하나.
199
- // ⇒ «줄고 있다»(draining)와 «크레딧은 있지만 안 쓰는 중»을 가른다.
200
- const draining = burning.filter((r) => drain.get(r.accountName)?.state === 'draining');
201
- const creditHolders = quota.policy === 'credits'
202
- ? codex.filter((r) => r.credits?.hasCredits === true)
203
- : [];
204
- const combinedCredits = creditHolders.length >= 2 ? creditsSummary(creditHolders.map((r) => ({
205
- name: r.accountName,
206
- balance: r.credits?.balance,
207
- perHour: drain.get(r.accountName)?.state === 'flat' ? 0 : drain.get(r.accountName)?.perHour,
208
- resetInHours: hoursUntil(r.subscription?.resetsAt) ?? undefined,
209
- }))) : null;
210
-
211
- // ⭐ 상태를 «문자열 하나»로 접어 두고, 바뀔 때만 보낸다(같은 경고 반복 금지).
212
- const signature = JSON.stringify({
213
- ex: exhausted.map((r) => r.accountName).sort(),
214
- low: low.map((r) => `${r.accountName}:${r.subscription?.remainingPercent}`).sort(),
215
- exp: expiring.map((r) => r.accountName).sort(),
216
- // ⛔ 「못 읽음」이 서명에 없으면, 자가 눈먼 상태가 「상태 동일」로 조용해진다.
217
- unread: unreadable.map((r) => r.accountName).sort(),
218
- policy: quota.policy,
219
- ...(combinedCredits ? { beforeReset: combinedCredits.beforeReset } : {}),
220
- // ⛔ 「줄고 있는 중」은 «매 틱» 서명이 달라야 한다 — 돈이 계속 나가는데 한 번만 알리면
221
- // 두 번째 시간부터는 「상태 동일」로 조용해진다. 그래서 draining 이면 원값을 넣는다.
222
- // ⛔ 반대로 멎었으면(flat) 원값을 넣지 «않는다» — 그래야 반복 발송이 멎는다.
223
- burn: burning.map((r) => {
224
- const d = drain.get(r.accountName);
225
- if (d?.state !== 'draining') return `${r.accountName}:${d?.state ?? 'unknown'}`;
226
- const b = r.credits?.balance;
227
- // 허가된 크레딧 지출은 «단위»로 접는다 — 단 «리셋 전에 바닥난다»로 바뀌면 단위와 무관하게 서명이 바뀐다.
228
- const resetH = hoursUntil(r.subscription?.resetsAt);
229
- const etaH = d.perHour && d.perHour < 0 && typeof b === 'number' ? b / -d.perHour : null;
230
- const risk = !combinedCredits && etaH !== null && (resetH === null || etaH < resetH) ? ':before-reset' : '';
231
- return `${r.accountName}:${creditsAllowed ? b === undefined ? 'balance-unknown' : `step${Math.floor(b / CREDITS_REPORT_STEP)}${risk}` : String(b ?? 0)}`;
232
- }).sort(),
233
- });
234
- const prev = prevState;
235
- // ⭐ 잔액은 «경고와 무관하게» 매 틱 적어 둔다 — 안 적으면 델타의 기준선이 영영 안 생긴다.
236
- // ⛔ 다만 signature 는 «발송에 성공했을 때만» 전진한다(그 규율은 아래 그대로다).
237
135
  const balances: Record<string, string> = {};
136
+ const creditBalances: Record<string, number> = {};
238
137
  for (const r of codex) {
239
138
  if (typeof r.credits?.balance === 'number') {
139
+ creditBalances[r.accountName] = r.credits.balance;
240
140
  balances[`balance:${r.accountName}`] = String(r.credits.balance);
241
141
  // ⭐ 값과 «시각»을 같이 남긴다 — 속도(시간당 얼마)는 두 축이 있어야 나온다.
242
142
  balances[`balanceAt:${r.accountName}`] = String(nowMs);
243
143
  }
244
144
  }
245
-
246
- if (prev.signature === signature) {
247
- saveState({ ...prev, ...balances });
248
- console.log(`[codex-quota-alert] 상태 동일 — 무발송 (소진 ${exhausted.length} · 임박 ${low.length})`);
249
- process.exit(0);
250
- }
251
-
252
- if (exhausted.length === 0 && low.length === 0 && expiring.length === 0 && burning.length === 0
253
- && unreadable.length === 0 && !combinedCredits) {
254
- console.log('[codex-quota-alert] 경고 조건 없음');
255
- saveState({ ...prev, ...balances, signature });
256
- process.exit(0);
257
- }
258
-
259
- const lines: string[] = [];
260
- lines.push(`🧭 정책: **${CODEX_QUOTA_POLICY_LABEL[quota.policy]}** (\`llm.codexQuotaPolicy=${quota.policy}\`${quota.source === 'legacy-credits' ? ' · 옛 codexCreditsAllowed 에서' : quota.source === 'default' ? ' · 기본값' : quota.source === 'invalid' ? ' · ⚠️ 모르는 값이라 기본값' : ''})`);
261
- lines.push('');
262
- // ⛔ 「못 읽음」을 맨 위에 둔다 — 아래 판정들이 «그만큼 눈이 먼» 상태임을 먼저 말해야 한다.
263
- if (unreadable.length > 0) {
264
- lines.push(`⚠️ **계정 ${unreadable.length}개를 «못 읽었습니다»** — 아래 판정은 그만큼 눈이 멉니다.`);
265
- for (const r of unreadable) lines.push(` • \`${r.accountName}\` — 사용률·잔여가 «둘 다» 없습니다`);
266
- lines.push('⛔ 이것은 「정상」이 아니라 「모름」입니다. 다음 틱에 값이 돌아오면 그때 판정이 바뀝니다.');
267
- lines.push('');
268
- }
269
- // 🔥 돈이 나가는 축을 «맨 위»에 둔다 — 소진은 멈춘 것이고 이것은 안 멈춘 것이다.
270
- if (burning.length > 0 || combinedCredits) {
271
- if (creditsAllowed) {
272
- lines.push(burning.length > 0
273
- ? `💳 **크레딧으로 진행 중 — 줄고 있는 계정 ${draining.length}개** (크레딧 보유 ${burning.length}개 · 정책상 허가된 지출)`
274
- : '💳 **크레딧 보유 계정**');
275
- if (combinedCredits) lines.push(formatCreditsSummary(combinedCredits));
276
- if (burning.length > 0) lines.push(combinedCredits
277
- ? `잔액이 ${CREDITS_REPORT_STEP} 단위로 줄 때마다 한 번 알립니다. 리셋 전 고갈은 합계로 판정합니다.`
278
- : `잔액이 ${CREDITS_REPORT_STEP} 단위로 줄 때마다 한 번 알립니다. 🚨 표시가 있으면 리셋 전에 바닥납니다.`);
279
- } else {
280
- lines.push(`🚨 **정책 밖 지출 — 한도를 넘겼는데 크레딧에서 나가고 있습니다 (줄고 있는 계정 ${draining.length}개 · 크레딧 보유 ${burning.length}개)**`);
281
- lines.push(`정책이 «${CODEX_QUOTA_POLICY_LABEL[quota.policy]}» 인데 레이트리밋 오류가 안 나 회전·폴백이 안 열립니다. 크레딧을 허가하려면 \`elanous config set llm.codexQuotaPolicy credits\`.`);
282
- }
283
- for (const r of burning) {
284
- const now = r.credits?.balance;
285
- const nowText = typeof now === 'number' ? now.toFixed(2) : '미상';
286
- const d = drain.get(r.accountName);
287
- let tail: string;
288
- if (!d || d.state === 'unknown') tail = ' (직전 관측 없음 — 이번이 첫 표본입니다)';
289
- else if (d.state === 'flat') tail = ' · 크레딧 보유 · 직전 관측 이후 «안 썼습니다»';
290
- else {
291
- // ⛔ 남은 시간은 «속도를 실제로 쟀을 때만» 말한다 — 지어내면 사람이 그 수로 계획을 세운다.
292
- const rate = d.perHour;
293
- // 🩸 그리고 「N시간 뒤 0」만 내면 «겁만 준다» — 한도 창이 «먼저» 리셋되면 고갈은 «안 온다».
294
- // 📏 2026-09-18 실측: 고갈 예상 144시간 vs 리셋 22.5시간 ⇒ 리셋이 먼저다(고갈 안 된다).
295
- // 그 비교 없이 내면 사람이 「145시간 뒤 0」을 읽고 «긴급»으로 오독한다.
296
- // ⇒ 「나가는 중」(늘 말할 값)과 「고갈된다」(진짜 긴급)를 «가른다».
297
- const etaH = rate && rate < 0 && typeof now === 'number' ? now / -rate : null;
298
- const resetH = hoursUntil(r.subscription?.resetsAt);
299
- let eta = '';
300
- if (etaH !== null && !combinedCredits) {
301
- const h = Math.max(0, Math.round(etaH));
302
- if (resetH !== null && resetH > 0 && resetH < etaH) {
303
- // 리셋이 먼저 온다 — 이 창에서 탈 양까지 «수»로 말한다.
304
- const burnLeft = Math.round(-(rate as number) * resetH);
305
- eta = ` · ⏳ 리셋(${resetH}시간 뒤)이 «먼저» 옵니다 — 이 창에서 약 ${burnLeft} 더 타고 고갈은 «안 됩니다»`;
306
- } else if (resetH === null) {
307
- eta = ` · 이 속도면 약 ${h}시간 뒤 0 (⚠️ 리셋 시각을 못 읽어 «어느 쪽이 먼저인지» 모릅니다)`;
308
- } else {
309
- eta = ` · 🚨 이 속도면 약 ${h}시간 뒤 0 — 리셋(${resetH}시간 뒤)«보다 먼저»입니다`;
310
- }
311
- }
312
- // ⭐ 창을 «값으로» 낸다 — 「시간당 -165」가 5분 창의 것인지 한 시간 창의 것인지가 갈린다.
313
- const win = d.windowMin !== undefined ? `${d.windowMin}분 창 · ` : '';
314
- tail = ` · 직전 대비 **${d.delta?.toFixed(2)}**`
315
- + (rate ? ` (${win}시간당 ${rate.toFixed(1)})` : ' (경과를 못 재 속도는 미상)') + eta;
316
- }
317
- lines.push(` • \`${r.accountName}\` — 잔액 ${nowText}${tail}`);
318
- }
319
- lines.push('');
320
- }
321
- if (exhausted.length === codex.length && codex.length > 0) {
322
- lines.push(`${creditsAllowed ? '💳' : '🚨'} **codex 구독 한도가 «전부» 찼습니다 (${codex.length}/${codex.length})**`);
323
- if (creditsAllowed) lines.push('정책 «크레딧까지» — codex 를 크레딧으로 계속 씁니다. 크레딧까지 떨어지면 폴백 체인으로 갑니다.');
324
- else if (fallbackAllowed) lines.push(`정책 «자동 폴백» — 폴백 체인(\`llm.fallbackChain\`)으로 갑니다. 폴백 대상의 한도도 같이 보십시오.`);
325
- else lines.push('정책 «한도 안에서만» — codex 작업은 리셋까지 «멈춥니다»(폴백·크레딧 없음).');
326
- } else if (exhausted.length > 0) {
327
- lines.push(`⚠️ **codex 계정 ${exhausted.length}/${codex.length} 한도 참** — 남은 계정으로 회전합니다`);
328
- }
329
- for (const r of exhausted) {
330
- const h = hoursUntil(r.subscription?.resetsAt);
331
- lines.push(` • \`${r.accountName}\` — 잔여 0% · 리셋 ${h === null ? '시각 미상' : `${h}시간 뒤`}`);
332
- }
333
- for (const r of low) {
334
- const h = hoursUntil(r.subscription?.resetsAt);
335
- lines.push(` • \`${r.accountName}\` — 잔여 ${r.subscription?.remainingPercent}% · 리셋 ${h === null ? '미상' : `${h}시간 뒤`}`);
336
- }
337
- if (expiring.length > 0) {
338
- lines.push('');
339
- lines.push(`💳 **리셋권 만료 임박 (${RESET_CREDIT_EXPIRY_DAYS}일 이내)**`);
340
- for (const r of expiring) lines.push(` • \`${r.accountName}\` — ${daysUntil(r.resetCredits?.expiresAt)}일 뒤`);
341
- }
342
- if (others.length > 0 && fallbackAllowed) {
343
- lines.push('');
344
- lines.push(creditsAllowed ? '폴백 대상(크레딧까지 떨어진 뒤):' : '폴백 대상:');
345
- for (const r of others) {
346
- const rem = r.subscription?.remainingPercent;
347
- const used = r.credits?.usedPercent;
348
- lines.push(` • \`${r.provider}/${r.accountName}\` — ${rem !== undefined ? `잔여 ${rem}%` : used !== undefined ? `사용 ${used}%` : '미상'}`);
349
- }
350
- }
351
- lines.push('');
352
- lines.push('⛔ 잔액 숫자는 provider 가 준 값 그대로입니다 — 토큰을 달러로 바꾸는 자는 아직 없습니다.');
353
-
354
- const msg = lines.join('\n');
355
- console.log(msg);
356
- if (sendOutbound(msg, 'alert')) {
357
- saveState({ ...prev, ...balances, signature });
145
+ try { recordCreditBalances(creditBalances, new Date(nowMs)); }
146
+ catch { /* 날짜 원장 저장 실패는 기존 알림의 발송을 막지 않는다 */ }
147
+
148
+ let policy;
149
+ try { policy = loadLlmPolicy({ now: new Date(nowMs) }); }
150
+ catch { policy = undefined; }
151
+ let pace;
152
+ try {
153
+ if (policy?.valid) pace = creditPaceStatus({ policy: policy.policy, balances: creditBalances, now: new Date(nowMs) });
154
+ } catch { /* 페이스 조회 실패도 잔액 기록·알림 발송을 막지 않는다 */ }
155
+ const resetExpiry = codex.map((r) => r.resetCredits?.status === 'available' ? r.resetCredits.expiresAt : undefined)
156
+ .filter((date): date is string => typeof date === 'string' && Number.isFinite(Date.parse(date)) && Date.parse(date) >= nowMs)
157
+ .sort((a, b) => Date.parse(a) - Date.parse(b))[0];
158
+ const expiresAt = [
159
+ ...(policy?.valid ? policy.policy.credits.grants
160
+ .filter((grant) => grant.expires >= new Date(nowMs).toISOString().slice(0, 10))
161
+ .map((grant) => `${grant.expires}T15:00:00Z`) : []),
162
+ ...(resetExpiry ? [resetExpiry] : []),
163
+ ].sort((a, b) => Date.parse(a) - Date.parse(b))[0];
164
+ const snapshot: LlmBudgetSnapshot = {
165
+ at: new Date(nowMs).toISOString(),
166
+ credits: {
167
+ total: codex.reduce((sum, r) => sum + (typeof r.credits?.balance === 'number' && Number.isFinite(r.credits.balance) ? r.credits.balance : 0), 0),
168
+ usedToday: pace?.todaySpent ?? 0,
169
+ paceTarget: pace?.target ?? 0,
170
+ ...(expiresAt ? { expiresAt } : {}),
171
+ ...(policy?.valid && policy.policy.credits.codex === 'use' && policy.policy.credits.pace?.until
172
+ && nowMs < Date.parse(`${policy.policy.credits.pace.until}T15:00:00Z`) ? { useFirst: true } : {}),
173
+ },
174
+ accounts: codex.map((r) => ({
175
+ name: r.accountName,
176
+ subscriptionRemainingPct: r.subscription?.remainingPercent,
177
+ credits: r.credits?.balance,
178
+ resetInHours: hoursUntil(r.subscription?.resetsAt) ?? undefined,
179
+ })),
180
+ selected: { reason: exhausted
181
+ ? creditsAllowed && creditAvailable ? 'credits' : fallback ? 'fallback' : 'none'
182
+ : 'subscription' },
183
+ ...(fallback && exhausted && !(creditsAllowed && creditAvailable) ? { fallback: {
184
+ provider: fallback.provider,
185
+ remainingPct: fallback.subscription?.remainingPercent,
186
+ } } : {}),
187
+ };
188
+ const knownAlerts = [...knownExhausted, ...knownLow].map((r) =>
189
+ `${r.accountName}:${r.subscription!.remainingPercent}`).sort();
190
+ const unreadableAccounts = codex.filter((r) => typeof r.subscription?.remainingPercent !== 'number'
191
+ || !Number.isFinite(r.subscription.remainingPercent)).map((r) => r.accountName).sort();
192
+ const unreadableKey = knownAlerts.length > 0
193
+ ? `unreadable:${unreadableAccounts.join(',')}:${knownAlerts.join(',')}`
194
+ : unreadableAccounts.length > 1 ? `unreadable:${unreadableAccounts.join(',')}` : 'unreadable';
195
+ const alertKey = unreadableCount > 0 && prevState.nt1Key === unreadableKey ? undefined : prevState.nt1Key;
196
+ const budgetAlert = buildLlmBudgetAlert(snapshot, alertKey);
197
+ // 정책 밖 지출(리뷰 3라운드 must-fix) — 정책이 크레딧을 허가하지 않는데 직전 관측보다 잔액이 줄었다.
198
+ // NT1 모양의 다른 상태보다 앞선다(행동이 필요한 유일한 «사고»). 같은 날이라도 잔액이 한 단계 더 줄면 다시 알린다.
199
+ const offPolicyDraining = creditsAllowed ? [] : codex.filter((r) => {
200
+ const before = Number(prevState[`balance:${r.accountName}`]);
201
+ return typeof r.credits?.balance === 'number' && Number.isFinite(before) && r.credits.balance < before;
202
+ });
203
+ const offPolicyStep = policy?.valid ? policy.policy.alerts.creditsStep : 5000;
204
+ const offPolicyKey = offPolicyDraining.length > 0
205
+ ? `off-policy:${new Date(nowMs + 9 * 3600_000).toISOString().slice(0, 10)}:${Math.floor(snapshot.credits.total / Math.max(1, offPolicyStep))}`
206
+ : undefined;
207
+ const offPolicyAlert = offPolicyKey ? {
208
+ send: prevState.nt1Key !== offPolicyKey,
209
+ key: offPolicyKey,
210
+ text: `🚨 정책 밖 지출 — 한도를 넘겼는데 크레딧에서 나가고 있습니다(줄고 있는 계정 ${offPolicyDraining.length}개).\n남은 크레딧 ${Math.round(snapshot.credits.total).toLocaleString('en-US')}.\n할 일: 허가하려면 \`elanous config set llm.codexQuotaPolicy credits\`, 아니면 크레딧 지출을 멈추세요.`,
211
+ } : undefined;
212
+ const alert = offPolicyAlert ?? (budgetAlert.key !== 'ok' || unreadableCount === 0 ? budgetAlert : {
213
+ send: prevState.nt1Key !== unreadableKey,
214
+ key: unreadableKey,
215
+ text: `⚠️ 일부 codex 계정의 구독 한도를 읽지 못했습니다.${knownExhausted.length > 0 ? ` 확인된 ${knownExhausted.length}개 계정은 한도가 소진됐습니다.` : knownLow.length > 0 ? ` 확인된 ${knownLow.length}개 계정은 잔여가 10% 이하입니다.` : ''}\n할 일: 한도 조회 상태를 확인해 주세요.`,
216
+ });
217
+ const state = { ...prevState, ...balances };
218
+ if (!alert.send) {
219
+ if (alert.key === 'ok') delete state.nt1Key;
220
+ save(state);
221
+ console.log('[codex-quota-alert] NT1 상태 동일 또는 경고 조건 없음 — 무발송');
222
+ return;
223
+ }
224
+ console.log(alert.text);
225
+ if (send(alert.text, 'alert')) {
226
+ save({ ...state, nt1Key: alert.key });
358
227
  console.log('\n✅ 텔레그램 발송');
359
228
  } else {
360
- // ⛔ 발송 실패면 signature 를 «저장하지 않는다» — 다음 틱에 다시 시도해야 한다.
361
- // 🩸 그런데 «잔액은 저장한다». 종전엔 여기서 통째로 안 저장해서, 발송이 막힌 동안
362
- // ***델타의 기준선이 얼어붙고 「직전 대비」가 「마지막 성공 발송 대비」로 «조용히» 둔갑***했다.
363
- // 📏 실측 2026-09-18: 50초 간격 두 번에 -12.41 → -12.55 로 «거의 안 움직였고» 속도는 영영 「미상」이었다.
364
- // ⇒ 이 파일 머리의 규율(「잔액은 경고와 무관하게 매 틱 적는다」)이 «이 갈래에만» 안 걸려 있었다.
365
- saveState({ ...prev, ...balances });
229
+ save(state);
366
230
  console.error('발송 실패(/v1/outbound + 텔레그램 직접 모두 실패) — 잔액 기준선은 갱신했다');
367
231
  process.exit(1);
368
232
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "_comment": "조율 채널 세션 신원의 정본(09-30 역할 재편). id = 2글자 신원(발신 접두 **[TC]**) · title = 직함 · alias = 옛 한 글자(기록 읽기·전환기 발신용) · mark = 옛 표지. 세션을 더하려면 여기 한 줄만 더한다 — coord-post.sh · coord-channel-watch.sh · build-docs-site.ts · mission-phase-track.ts 가 이 파일을 읽는다.",
3
3
  "tracks": [
4
- { "id": "OP", "title": "COO", "alias": "S", "mark": "🅢", "owns": ["판올림", "컷", "체크리스트", "분배", "통합", "로드맵", "기획", "총괄", "전략", "인계", "release cut", "roadmap", "plan", "strategy"] },
5
- { "id": "MK", "title": "CMO", "alias": "T", "mark": "🅣", "owns": ["블로그", "콘텐츠", "발행", "광고", "프로모션", "소구점", "마케팅", "영상", "비디오", "티저", "하이퍼프레임", "렌더", "blog", "content", "marketing", "ads", "video", "teaser", "render", "B-roll", "상표", "변리사", "브랜드", "trademark", "brand", "홈페이지", "웹사이트", "elanous.ai", "elanvital.ai", "website"] },
6
- { "id": "TC", "title": "CTO", "alias": "O", "mark": "🅞", "owns": ["하니스", "게이트", "원장", "리뷰", "관측", "릴리스 루프", "흡수", "Obsidian", "매뉴얼", "문서", "기술", "harness", "gate", "ledger", "review", "observability", "intake", "manual", "docs"] },
7
- { "id": "UX", "title": "CXO", "alias": "F", "mark": "🅕", "owns": ["PWA", "웹", "탭", "화면", "UI", "프론트엔드", "안드로이드", "아이폰", "데스크톱", "텔레그램", "디스코드", "메신저", "봇", "frontend", "web", "tab", "android", "ios", "desktop", "telegram", "discord", "bot"] },
4
+ { "id": "OP", "title": "COO", "alias": "S", "mark": "🅢", "runtime": "claude-code", "owns": ["판올림", "컷", "체크리스트", "분배", "통합", "로드맵", "기획", "총괄", "전략", "인계", "release cut", "roadmap", "plan", "strategy"] },
5
+ { "id": "MK", "title": "CMO", "alias": "T", "mark": "🅣", "runtime": "claude-code", "owns": ["블로그", "콘텐츠", "발행", "광고", "프로모션", "소구점", "마케팅", "영상", "비디오", "티저", "하이퍼프레임", "렌더", "blog", "content", "marketing", "ads", "video", "teaser", "render", "B-roll", "상표", "변리사", "브랜드", "trademark", "brand", "홈페이지", "웹사이트", "elanous.ai", "elanvital.ai", "website"] },
6
+ { "id": "TC", "title": "CTO", "alias": "O", "mark": "🅞", "runtime": "claude-code", "owns": ["하니스", "게이트", "원장", "리뷰", "관측", "릴리스 루프", "흡수", "Obsidian", "매뉴얼", "문서", "기술", "harness", "gate", "ledger", "review", "observability", "intake", "manual", "docs"] },
7
+ { "id": "UX", "title": "CXO", "alias": "F", "mark": "🅕", "runtime": "claude-code", "owns": ["PWA", "웹", "탭", "화면", "UI", "프론트엔드", "안드로이드", "아이폰", "데스크톱", "텔레그램", "디스코드", "메신저", "봇", "frontend", "web", "tab", "android", "ios", "desktop", "telegram", "discord", "bot"] },
8
8
  { "id": "E", "mark": "🅔", "owns": [] }
9
9
  ]
10
10
  }
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bun
2
+ // deliver node — «최종 게시» 승인 «뒤»에만 돈다. 읽는 것은 그 시점의 초안 파일(사람이 고친 최신본)이지 처음 생성본이 아니다.
3
+ // ① 커버 문구·사진별 한 줄이 렌더 당시와 다르면 그 장만 다시 그린다 ② 포함된 장만 순서대로 <folder>/feed/ready/ 에 모은다
4
+ // ③ caption.txt(훅·본문·해시태그·위치) ⊕ 릴스 사본 ④ 텔레그램(/v1/outbound)에 «승인됨 · 게시 준비 완료» 한 줄.
5
+ // ⛔ 인스타그램에 올리지 않는다 — 실제 Meta 게시는 다음 판.
6
+ import { copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { sendOutbound } from '../../src/domains/outbound-alert.js';
9
+ import { coverHtml, readContext, readDraft, result, screenshot, slideHtml, writeDraft } from './lib.js';
10
+
11
+ try {
12
+ const { folder } = readContext();
13
+ const draft = readDraft(folder);
14
+ if (!draft) throw new Error('초안 없음 — feed 노드가 먼저 돌아야 한다');
15
+ const included = draft.slides.filter((s) => s.include);
16
+ if (!included.length) throw new Error('포함된 사진이 0장이다');
17
+
18
+ let redrawn = 0;
19
+ if (draft.cover.text !== draft.cover.renderedText || draft.cover.sub !== draft.cover.renderedSub || draft.cover.renderedSource !== included[0]!.source) {
20
+ screenshot(coverHtml(join(folder, included[0]!.source), draft.cover.text, draft.cover.sub), join(folder, draft.cover.image));
21
+ draft.cover.renderedText = draft.cover.text; draft.cover.renderedSub = draft.cover.sub; draft.cover.renderedSource = included[0]!.source; redrawn++;
22
+ }
23
+ included.forEach((s, i) => {
24
+ // 번호(01 / N)는 포함된 장 기준이라, 빼거나 순서를 바꾸면 글이 같아도 다시 그린다.
25
+ const label = `${i + 1}/${included.length}:${s.caption}`;
26
+ if (s.renderedCaption !== label) {
27
+ screenshot(slideHtml(join(folder, s.source), s.caption, i + 1, included.length), join(folder, s.image));
28
+ s.renderedCaption = label; redrawn++;
29
+ }
30
+ });
31
+ if (redrawn) writeDraft(folder, { ...draft, updatedAt: new Date().toISOString() });
32
+
33
+ const ready = join(folder, 'feed', 'ready');
34
+ rmSync(ready, { recursive: true, force: true });
35
+ mkdirSync(ready, { recursive: true });
36
+ copyFileSync(join(folder, draft.cover.image), join(ready, '00-cover.png'));
37
+ included.forEach((s, i) => copyFileSync(join(folder, s.image), join(ready, `${String(i + 1).padStart(2, '0')}.png`)));
38
+ const text = [draft.caption.hook, '', draft.caption.body, '', draft.hashtags.join(' '), ...(draft.location ? ['', `📍 ${draft.location}`] : [])].join('\n');
39
+ writeFileSync(join(ready, 'caption.txt'), text + '\n');
40
+ const reel = draft.reel && existsSync(join(folder, draft.reel)) ? join(ready, 'reel-9x16.mp4') : null;
41
+ if (reel) copyFileSync(join(folder, draft.reel!), reel);
42
+
43
+ // FIELD_FEED_NOTIFY=0 — 시험·리허설에서 텔레그램으로 나가지 않게.
44
+ const sent = process.env.FIELD_FEED_NOTIFY === '0' ? false : sendOutbound([
45
+ `✅ 승인됨 · 게시 준비 완료 — ${draft.event.title} (${draft.event.date})`,
46
+ `캐러셀 ${included.length + 1}장${reel ? ' ⊕ 릴스 1편' : ''} · 초안 r${draft.revision}${draft.updatedBy === 'human' ? '(사람이 고침)' : ''}`,
47
+ '', text, '', `묶음: ${ready}`, '(인스타그램 게시는 아직 사람이 한다 — 자동 게시는 다음 판)',
48
+ ].join('\n'), 'report');
49
+ result({ ready, slides: included.length + 1, reel: Boolean(reel), redrawn, revision: draft.revision, notified: sent });
50
+ } catch (error) {
51
+ console.log(JSON.stringify({ outcome: 'fail', reason: error instanceof Error ? error.message : String(error) }));
52
+ }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bun
2
+ // feed node — 현장 폴더 → 인스타 피드 초안 한 벌: 4:5(1080×1350) 캐러셀(커버 ⊕ 사진별 한 줄) ⊕ 캡션(훅·본문·해시태그·위치) ⊕ 릴스 경로.
3
+ // 쓰는 곳: <folder>/feed/feed-draft.json(정본) · <folder>/feed/slide-N.png. 사진별 한 줄과 제목은 앞 reel 노드의
4
+ // reel/timeline.json(비전 결과)을 그대로 쓴다 — 같은 사진을 두 번 읽지 않는다. 없으면 «현장 N».
5
+ // 사람이 이미 고친 초안(updatedBy: human)이 있으면 덮지 않는다.
6
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { ask, coverHtml, draftPath, kstDate, lastJson, listPhotos, readContext, readDraft, result, screenshot, slideHtml, writeDraft, type FeedDraft } from './lib.js';
9
+
10
+ const started = Date.now();
11
+ try {
12
+ const { folder, runId } = readContext();
13
+ const existing = readDraft(folder);
14
+ if (existing?.updatedBy === 'human') {
15
+ result({ draftPath: draftPath(folder), slides: existing.slides.length, kept: 'human-edited' });
16
+ process.exit(0);
17
+ }
18
+ let timeline: { title?: string; sub?: string; items?: { file: string; video?: boolean; cap?: string }[] } = {};
19
+ try { timeline = JSON.parse(readFileSync(join(folder, 'reel', 'timeline.json'), 'utf8')); } catch { /* reel 없이도 돈다 */ }
20
+ // 릴스가 쓴 순서(와 reel/v<i>.jpg 번호)를 그대로 따른다 — 같은 사진이 두 순서로 갈리지 않게.
21
+ const reelOrder = (timeline.items ?? []).map((it) => it.file);
22
+ const photos = reelOrder.length ? (timeline.items ?? []).filter((it) => !it.video).map((it) => it.file) : listPhotos(folder);
23
+ if (!photos.length) throw new Error('현장 폴더에 사진이 없다');
24
+ const capOf = new Map((timeline.items ?? []).map((it) => [it.file, it.cap ?? '']));
25
+ const title = timeline.title && timeline.title !== '현장 스케치' ? timeline.title : '현장 스케치';
26
+ const date = timeline.sub || kstDate(photos);
27
+ const shown = photos.slice(0, 9); // 커버 ⊕ 9장 = 인스타 캐러셀 상한 10
28
+ const captions = shown.map((f, i) => capOf.get(f) || `현장 ${i + 1}`);
29
+
30
+ const out = join(folder, 'feed');
31
+ mkdirSync(out, { recursive: true });
32
+ screenshot(coverHtml(join(folder, shown[0]!), title, `${date} · 현장 스케치`), join(out, 'slide-0.png'));
33
+ shown.forEach((f, i) => screenshot(slideHtml(join(folder, f), captions[i]!, i + 1, shown.length), join(out, `slide-${i + 1}.png`)));
34
+ const rendered = Date.now();
35
+
36
+ // 캡션: 행사 제목 ⊕ 사진별 한 줄 ⊕ 사진(앞 셋)을 보고 한 벌. 실패하면 템플릿.
37
+ const prompt = [
38
+ '행사 현장 사진으로 인스타그램 게시글 초안을 쓴다. JSON 객체 하나만 출력하라:',
39
+ '{"hook": 첫 줄(25자 이내 · 이모지 0~1개), "body": 본문 2~3문장(보이는 장면과 아래 한 줄들만 근거 · 사람 이름·외모·감정 추측 금지 · 과장 금지), "hashtags": 6~8개(한국어 위주 · # 포함), "location": 화면 글자에서 읽히는 장소 이름 또는 null}',
40
+ `행사 제목: ${title}`, `날짜: ${date}`, `사진별 한 줄: ${captions.join(' / ')}`,
41
+ ].join('\n');
42
+ const reply = lastJson(await ask(prompt, shown.slice(0, 3).map((f) => join(folder, 'reel', `v${reelOrder.indexOf(f)}.jpg`)).filter(existsSync)));
43
+ const str = (v: unknown, max: number) => (typeof v === 'string' && v.trim() && v.length <= max ? v.trim() : '');
44
+ const tags = Array.isArray(reply?.hashtags) ? (reply!.hashtags as unknown[]).filter((t): t is string => typeof t === 'string' && /^#\S{1,30}$/.test(t)).slice(0, 10) : [];
45
+ const draft: FeedDraft = {
46
+ kind: 'feed-draft', version: 1, revision: (existing?.revision ?? 0) + 1, updatedAt: new Date().toISOString(), updatedBy: 'graph',
47
+ graphId: 'field-feed', runId, folder,
48
+ brand: { name: 'Elanous', handle: 'elanous.ai', avatar: null },
49
+ event: { title, date },
50
+ cover: { text: title, sub: `${date} · 현장 스케치`, image: 'feed/slide-0.png', renderedText: title, renderedSub: `${date} · 현장 스케치`, renderedSource: shown[0]! },
51
+ slides: shown.map((f, i) => ({ image: `feed/slide-${i + 1}.png`, source: f, caption: captions[i]!, include: true, renderedCaption: `${i + 1}/${shown.length}:${captions[i]!}` })),
52
+ caption: {
53
+ hook: str(reply?.hook, 40) || `${title} 현장`,
54
+ body: str(reply?.body, 400) || `${date}, ${title} 현장을 사진으로 담았습니다.`,
55
+ },
56
+ hashtags: tags.length ? tags : ['#현장스케치', `#${title.replace(/\s+/g, '')}`],
57
+ location: str(reply?.location, 40) || null,
58
+ reel: existsSync(join(folder, 'reel', 'reel-9x16.mp4')) ? 'reel/reel-9x16.mp4' : null,
59
+ };
60
+ writeDraft(folder, draft);
61
+ result({
62
+ draftPath: draftPath(folder), slides: draft.slides.length + 1, vision: Boolean(reply),
63
+ renderMs: rendered - started, totalMs: Date.now() - started,
64
+ });
65
+ } catch (error) {
66
+ console.log(JSON.stringify({ outcome: 'fail', reason: error instanceof Error ? error.message : String(error) }));
67
+ }