elanous 0.2.4 → 0.2.5

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 (412) hide show
  1. package/README.md +3 -3
  2. package/apps/pwa/out/404/index.html +1 -1
  3. package/apps/pwa/out/404.html +1 -1
  4. package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 1751.50b96ea9c37b49b6.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +25 -0
  12. package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +5 -0
  20. package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 4daa2798-1363d725c79f3593.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 5348-bbf838e119661eee.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/5391-8793f88b03b30161.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 8617.465ca22a319c95b3.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8918-482b30ce2a8ebf4c.js} +3 -3
  43. package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 9704-3221c102623c8d73.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-9fa879ebf5959096.js → page-0ac5d053f0444d21.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/{page-4472ddf1e2458e69.js → page-72bd384898d88955.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/{page-112f7e477bda77fd.js → page-bccce0babceae30f.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6f3850d-7f324c6566002113.js} +1 -1
  85. package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-2bf59c720fe9542c.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_buildManifest.js +1 -1
  93. package/apps/pwa/out/approvals/index.html +1 -1
  94. package/apps/pwa/out/approvals/index.txt +29 -27
  95. package/apps/pwa/out/autopilot/index.html +1 -1
  96. package/apps/pwa/out/autopilot/index.txt +29 -27
  97. package/apps/pwa/out/board/index.html +1 -1
  98. package/apps/pwa/out/board/index.txt +29 -27
  99. package/apps/pwa/out/botlab/index.html +1 -1
  100. package/apps/pwa/out/botlab/index.txt +26 -24
  101. package/apps/pwa/out/bots/index.html +1 -1
  102. package/apps/pwa/out/bots/index.txt +26 -24
  103. package/apps/pwa/out/chat/index.html +1 -1
  104. package/apps/pwa/out/chat/index.txt +26 -24
  105. package/apps/pwa/out/control/index.html +1 -1
  106. package/apps/pwa/out/control/index.txt +29 -27
  107. package/apps/pwa/out/design-check/index.html +1 -1
  108. package/apps/pwa/out/design-check/index.txt +29 -27
  109. package/apps/pwa/out/editor/index.html +1 -1
  110. package/apps/pwa/out/editor/index.txt +29 -27
  111. package/apps/pwa/out/healthz +1 -1
  112. package/apps/pwa/out/icons/elanous-16.png +0 -0
  113. package/apps/pwa/out/icons/elanous-180.png +0 -0
  114. package/apps/pwa/out/icons/elanous-192.png +0 -0
  115. package/apps/pwa/out/icons/elanous-32.png +0 -0
  116. package/apps/pwa/out/icons/elanous-512.png +0 -0
  117. package/apps/pwa/out/index.html +1 -1
  118. package/apps/pwa/out/index.txt +26 -24
  119. package/apps/pwa/out/intake/index.html +1 -1
  120. package/apps/pwa/out/intake/index.txt +29 -27
  121. package/apps/pwa/out/live/index.html +1 -1
  122. package/apps/pwa/out/live/index.txt +29 -27
  123. package/apps/pwa/out/manifest.webmanifest +5 -2
  124. package/apps/pwa/out/market/index.html +1 -1
  125. package/apps/pwa/out/market/index.txt +26 -24
  126. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  127. package/apps/pwa/out/missions/placeholder/index.txt +26 -24
  128. package/apps/pwa/out/morning/index.html +1 -1
  129. package/apps/pwa/out/morning/index.txt +29 -27
  130. package/apps/pwa/out/observatory/index.html +1 -1
  131. package/apps/pwa/out/observatory/index.txt +29 -27
  132. package/apps/pwa/out/reflection/index.html +1 -1
  133. package/apps/pwa/out/reflection/index.txt +27 -25
  134. package/apps/pwa/out/scheduler/index.html +1 -1
  135. package/apps/pwa/out/scheduler/index.txt +29 -27
  136. package/apps/pwa/out/sessions/index.html +1 -1
  137. package/apps/pwa/out/sessions/index.txt +26 -24
  138. package/apps/pwa/out/settings/devices/index.html +1 -1
  139. package/apps/pwa/out/settings/devices/index.txt +29 -27
  140. package/apps/pwa/out/settings/index.html +1 -1
  141. package/apps/pwa/out/settings/index.txt +29 -27
  142. package/apps/pwa/out/setup/done/index.html +1 -1
  143. package/apps/pwa/out/setup/done/index.txt +29 -27
  144. package/apps/pwa/out/setup/index.html +1 -1
  145. package/apps/pwa/out/setup/index.txt +29 -27
  146. package/apps/pwa/out/share/index.html +1 -1
  147. package/apps/pwa/out/share/index.txt +29 -27
  148. package/apps/pwa/out/showroom/index.html +1 -1
  149. package/apps/pwa/out/showroom/index.txt +26 -24
  150. package/apps/pwa/out/sw-strategies.js +44 -0
  151. package/apps/pwa/out/sw.js +6 -44
  152. package/apps/pwa/out/tasks/index.html +1 -1
  153. package/apps/pwa/out/tasks/index.txt +29 -27
  154. package/apps/pwa/out/term/index.html +1 -1
  155. package/apps/pwa/out/term/index.txt +29 -27
  156. package/apps/pwa/out/trace/index.html +1 -1
  157. package/apps/pwa/out/trace/index.txt +29 -27
  158. package/apps/pwa/out/vault/index.html +1 -1
  159. package/apps/pwa/out/vault/index.txt +29 -27
  160. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  161. package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
  162. package/apps/pwa/out/workflows/index.html +1 -1
  163. package/apps/pwa/out/workflows/index.txt +29 -27
  164. package/apps/pwa/out/workspace/index.html +1 -1
  165. package/apps/pwa/out/workspace/index.txt +29 -27
  166. package/apps/pwa/out/worktrees/index.html +1 -1
  167. package/apps/pwa/out/worktrees/index.txt +29 -27
  168. package/catalog/resources.yaml +6 -6
  169. package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
  170. package/graphs/landing/landing-heal.yaml +1 -1
  171. package/graphs/release/recipes.yaml +8 -1
  172. package/graphs/release/release-loop.yaml +6 -1
  173. package/graphs/video/README.md +1 -0
  174. package/graphs/video/explainer-line.yaml +58 -0
  175. package/package.json +3 -1
  176. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  177. package/plugins/job-coach/README.md +12 -0
  178. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  179. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  180. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  181. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  182. package/plugins/job-coach/graphs/run-step.ts +211 -17
  183. package/plugins/job-coach/plugin.json +16 -3
  184. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  185. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  186. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  187. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  188. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  189. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  190. package/scripts/backup/elanous-backup.sh +4 -1
  191. package/scripts/build-docs-site.ts +4 -3
  192. package/scripts/coord-channel-watch.sh +23 -4
  193. package/scripts/coord-post.sh +49 -12
  194. package/scripts/coord-tracks.json +5 -5
  195. package/scripts/eval-bot-vs-grok.ts +79 -0
  196. package/scripts/eval-grok-live-baseline.ts +36 -0
  197. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  198. package/scripts/fleet/node-setup.sh +46 -7
  199. package/scripts/landing-heal/must-fix-match.ts +26 -0
  200. package/scripts/landing-heal/run-mission.ts +114 -20
  201. package/scripts/lib/front-route-measurement.ts +131 -0
  202. package/scripts/lib/tui-sim-keys.ts +42 -0
  203. package/scripts/measure-front-route-classifier.ts +56 -0
  204. package/scripts/media/ocr-leak-scan.sh +21 -0
  205. package/scripts/public-export.ts +2 -3
  206. package/scripts/public-import-pr.ts +80 -6
  207. package/scripts/publish-internal-dist.ts +2 -3
  208. package/scripts/red-sweep-diff.ts +1 -1
  209. package/scripts/release-loop/auto-approve-node.ts +72 -0
  210. package/scripts/release-loop/gate-node.ts +332 -44
  211. package/scripts/release-loop/publish-node.ts +7 -3
  212. package/scripts/release-loop/release-docs.ts +20 -13
  213. package/scripts/release-loop/test-durations.ts +63 -0
  214. package/scripts/release-loop/upgrade-node.ts +3 -2
  215. package/scripts/test-audit-coverage-per-file.sh +16 -0
  216. package/scripts/test-audit-meaningless.ts +88 -0
  217. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  218. package/scripts/test-deterministic.ts +27 -5
  219. package/scripts/tui-sim.ts +59 -14
  220. package/scripts/ux/bump-installed.sh +20 -0
  221. package/scripts/ux/lib/cdp.ts +72 -0
  222. package/scripts/ux/record-page.ts +50 -0
  223. package/skills/explainer-video/SKILL.md +99 -0
  224. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  225. package/skills/explainer-video/engine/build.mjs +431 -0
  226. package/skills/explainer-video/engine/vo.ts +37 -0
  227. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  228. package/src/agent/nested-agent-env.ts +1 -0
  229. package/src/agent-mission/driver.ts +95 -16
  230. package/src/agent-mission/mission-cli.ts +6 -0
  231. package/src/agent-mission/resource-ladder.ts +73 -25
  232. package/src/auth/acp-token.ts +16 -0
  233. package/src/auth/github-app-token.ts +112 -0
  234. package/src/autopilot/gh-rest-fallback.ts +177 -0
  235. package/src/autopilot/pr-manager.ts +51 -4
  236. package/src/boot/acp-server.ts +2 -16
  237. package/src/boot/daemon-tools/web-search.ts +2 -1
  238. package/src/boot/ws-bridge.ts +11 -0
  239. package/src/chat/index.ts +37 -8
  240. package/src/chat/log-scroll-key.ts +12 -0
  241. package/src/chat/submit-trace.ts +30 -0
  242. package/src/cli/agent-cli.ts +7 -2
  243. package/src/cli/decisions-cli.ts +94 -0
  244. package/src/cli/doctor-cli.ts +40 -54
  245. package/src/cli/doctor-readiness.ts +16 -24
  246. package/src/cli/intake-cli.ts +1 -1
  247. package/src/cli/mcp-call.ts +9 -0
  248. package/src/cli/mcp-gateway-cli.ts +7 -3
  249. package/src/cli/plugin-cli.ts +109 -11
  250. package/src/cli/pr-lineage.ts +2 -1
  251. package/src/cli/pwa-start.ts +3 -0
  252. package/src/cli/release-cli.ts +94 -7
  253. package/src/cli/self-update.ts +7 -4
  254. package/src/cli/start-cli.ts +33 -14
  255. package/src/dashboard/acp-boot.ts +3 -0
  256. package/src/dashboard/index.ts +62 -13
  257. package/src/dashboard/input/no-provider-reply.ts +13 -0
  258. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  259. package/src/dashboard/stale-install-check.ts +77 -0
  260. package/src/decisions/decision-ledger.ts +404 -0
  261. package/src/domains/outbound-alert.ts +8 -2
  262. package/src/domains/telegram-kind-route.ts +21 -0
  263. package/src/execution-loop/dispatch-task.ts +139 -0
  264. package/src/execution-loop/gate-decision-summary.ts +29 -0
  265. package/src/execution-loop/gates.ts +197 -0
  266. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  267. package/src/graph-kinds/plugin-nodes.ts +10 -2
  268. package/src/graph-runner/runner.ts +35 -5
  269. package/src/grounding/token.ts +12 -6
  270. package/src/harness/harness-cli-command.ts +68 -9
  271. package/src/harness/harness-pod-dispatch.ts +8 -2
  272. package/src/harness/harness-stop.ts +147 -18
  273. package/src/harness/self-send-decide.ts +26 -17
  274. package/src/harness/self-send-target.ts +22 -1
  275. package/src/harness/worktree-branch-prefix.ts +4 -3
  276. package/src/index.ts +56 -13
  277. package/src/instance/resolve.ts +6 -0
  278. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  279. package/src/intake-plane/digest.ts +11 -2
  280. package/src/live/detail-switch.ts +8 -3
  281. package/src/llm/usable-llm.ts +32 -0
  282. package/src/loops/registry.ts +27 -5
  283. package/src/market/publish.ts +59 -3
  284. package/src/native-tool-catalog.ts +2 -2
  285. package/src/nexus/api/graph-approvals.ts +125 -0
  286. package/src/nexus/api/http-server.ts +60 -6
  287. package/src/nexus/api/log-fabric.ts +10 -1
  288. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  289. package/src/nexus/api/merge-approvals.ts +2 -1
  290. package/src/nexus/api/meta-api.ts +14 -0
  291. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  292. package/src/nexus/api/plugins-market.ts +87 -3
  293. package/src/nexus/api/pod-credential-api.ts +68 -14
  294. package/src/nexus/api/public-routes.ts +7 -0
  295. package/src/nexus/api/trace.ts +57 -6
  296. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  297. package/src/nexus/index.ts +33 -32
  298. package/src/nexus/outbound/router.ts +1 -1
  299. package/src/onboarding.ts +25 -2
  300. package/src/plugins/core/host.ts +14 -0
  301. package/src/plugins/install/market-fetch.ts +4 -3
  302. package/src/plugins/install/plugin-credentials.ts +106 -0
  303. package/src/plugins/install/plugin-install.ts +163 -32
  304. package/src/prompt-library/universal-preamble.ts +8 -2
  305. package/src/release-loop/checklist.ts +191 -0
  306. package/src/release-loop/release-note.ts +6 -3
  307. package/src/self-dev/dev-cli.ts +40 -20
  308. package/src/self-dev/dev-pipeline.ts +77 -1
  309. package/src/self-implement/gate-baseline.ts +3 -1
  310. package/src/self-implement/goal-author.ts +18 -1
  311. package/src/self-implement/host-regate.ts +6 -2
  312. package/src/self-implement/orchestrator.ts +7 -2
  313. package/src/self-implement/seams.ts +4 -2
  314. package/src/skills/tools/dev-harness.ts +42 -0
  315. package/src/steward/steward-cards.ts +45 -0
  316. package/src/steward/triage.ts +50 -14
  317. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  318. package/src/task-orchestrator/surfaces/pod-command-job.ts +27 -6
  319. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  320. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  321. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  322. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  323. package/src/telegram-report.ts +12 -6
  324. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  325. package/src/tui.ts +3 -2
  326. package/src/user-config.ts +28 -2
  327. package/src/util/spawn-sync-output.ts +15 -0
  328. package/src/version/code-revision.ts +1 -1
  329. package/src/version/packed-revision.json +1 -1
  330. package/src/web-search/free.ts +77 -0
  331. package/src/web-search/index.ts +2 -0
  332. package/src/workflow-runtime/deps-bridge.ts +4 -3
  333. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  334. package/src/workflow-runtime/types.ts +2 -1
  335. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  346. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  348. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/8142-011e1f474efb8b33.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  411. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  412. /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_ssgManifest.js +0 -0
@@ -3,10 +3,11 @@
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
4
  import { basename, join, relative } from 'node:path';
5
5
  import { loadIntakeLedger, type IntakeItem } from './items.js';
6
+ import { readSavedCursorAt } from './collect-telegram-saved.js';
6
7
  import { intakeOutboxDir, kstDay } from './route.js';
7
8
 
8
9
  export interface DigestEntry { id: string; sources: string[]; url?: string; note?: string; noteName?: string; oneLiner?: string; axis: string }
9
- export interface IntakeDigest { day: string; absorbed: DigestEntry[]; goals: { fact: string; url?: string }[]; review?: { fact: string; note?: string }[]; grounding: number; release: number; manual: number }
10
+ export interface IntakeDigest { day: string; absorbed: DigestEntry[]; goals: { fact: string; url?: string }[]; review?: { fact: string; note?: string }[]; grounding: number; release: number; manual: number; savedSilence?: { days: number; lastNewAt: string } }
10
11
 
11
12
  const AXIS_LABEL: Record<string, string> = {
12
13
  video_automation: '영상 자동화', agent_basics: '에이전트 기본', agent_applied: '에이전트 응용', unrelated: '그 밖',
@@ -36,7 +37,10 @@ function readJsonl(file: string): Record<string, unknown>[] {
36
37
  }
37
38
 
38
39
  /** `day`(KST YYYY-MM-DD)에 흡수·갈래가 끝난 항목과 그날의 산출 큐를 모은다. */
39
- export function buildIntakeDigest(root: string, day: string, readFile: (p: string) => string | undefined = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : undefined)): IntakeDigest {
40
+ /** Saved Messages silent for more than two days → the digest says so (a quiet 0 must not look like «nothing to do»). */
41
+ const SAVED_SILENCE_MS = 48 * 3600_000;
42
+
43
+ export function buildIntakeDigest(root: string, day: string, readFile: (p: string) => string | undefined = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : undefined), now: Date = new Date()): IntakeDigest {
40
44
  const items = [...loadIntakeLedger(root).items.values()]
41
45
  .filter((i) => (i.status === 'absorbed' || i.status === 'routed') && kstDay(i.lastSeenAt) === day && i.outputs.some((o) => o.kind === 'note'));
42
46
  const absorbed = items.map((i: IntakeItem): DigestEntry => {
@@ -53,16 +57,20 @@ export function buildIntakeDigest(root: string, day: string, readFile: (p: strin
53
57
  const goals = readJsonl(join(out, 'goals', `${day}.jsonl`)).map((g) => ({ fact: String(g.fact ?? ''), ...(g.url ? { url: String(g.url) } : {}) }));
54
58
  const review = readJsonl(join(out, 'review', `${day}.jsonl`)).map((r) => ({ fact: String(r.fact ?? ''), ...(r.note ? { note: String(r.note) } : {}) }));
55
59
  const grounding = readJsonl(join(out, 'grounding.jsonl')).filter((g) => typeof g.at === 'string' && kstDay(g.at) === day).length;
60
+ const lastNewAt = readSavedCursorAt(root);
61
+ const silentMs = lastNewAt ? now.getTime() - Date.parse(lastNewAt) : NaN;
56
62
  return {
57
63
  day, absorbed, goals, review, grounding,
58
64
  release: readJsonl(join(out, 'release', `${day}.jsonl`)).length,
59
65
  manual: readJsonl(join(out, 'manual', `${day}.jsonl`)).length,
66
+ ...(lastNewAt && silentMs > SAVED_SILENCE_MS ? { savedSilence: { days: Math.floor(silentMs / 86_400_000), lastNewAt } } : {}),
60
67
  };
61
68
  }
62
69
 
63
70
  /** 노트 절 — 축별 묶음 · 노트 링크 · 한 줄 요약 · 골 후보. */
64
71
  export function renderDigestMarkdown(d: IntakeDigest): string {
65
72
  const L: string[] = [`## 📰 오늘의 흡수 요약 (${d.absorbed.length})`, ''];
73
+ if (d.savedSilence) L.push(`> ⚠️ 텔레그램 «저장된 메시지»에서 새 글을 ${d.savedSilence.days}일째 못 받았다(마지막 새 글 수집 ${kstDay(d.savedSilence.lastNewAt)}) — 저장했는데 안 들어왔다면 저장한 곳(본인 «저장된 메시지»인지)을 확인한다.`, '');
66
74
  if (!d.absorbed.length) { L.push('오늘 흡수한 것이 없다.', ''); return L.join('\n'); }
67
75
  const byAxis = new Map<string, DigestEntry[]>();
68
76
  for (const e of d.absorbed) (byAxis.get(e.axis) ?? byAxis.set(e.axis, []).get(e.axis)!).push(e);
@@ -89,6 +97,7 @@ export function renderDigestMarkdown(d: IntakeDigest): string {
89
97
  /** 텔레그램 짧은 판 — 수 · 한 줄 요약 다섯 · 골 후보 · 노트 자리. */
90
98
  export function renderDigestTelegram(d: IntakeDigest, opts: { vaultRoot?: string; notePath?: string } = {}): string {
91
99
  const L = [`📰 *${d.day} 흡수 ${d.absorbed.length}편*`];
100
+ if (d.savedSilence) L.push(`⚠️ 저장된 메시지 새 글 ${d.savedSilence.days}일째 0 — 저장한 곳 확인`);
92
101
  for (const e of d.absorbed.slice(0, 5)) L.push(`• ${e.oneLiner ?? e.noteName ?? e.url ?? e.id}`);
93
102
  if (d.absorbed.length > 5) L.push(`… 외 ${d.absorbed.length - 5}편`);
94
103
  if (d.goals.length) L.push('', `🔴 엘라누스에 없는 것 ${d.goals.length}건 — 골 후보`);
@@ -7,7 +7,7 @@
7
7
  // 상태 = `<state>/live/detail.json` `{ scope: 'all' | '<runId>', until: <epoch ms>, by?, since }`.
8
8
  // - 넥서스(`/v1/live/detail`)가 쓰고, 하니스·런(다른 프로세스)은 파일을 읽는다 — 프로세스 경계를 넘는 가장 단순한 관.
9
9
  // - `until` 이 지나면 꺼진 것이다(자동 꺼짐 · 파일을 지우는 사람이 없어도 된다).
10
- // ⛔ 읽기 실패·깨진 파일은 «꺼짐» — 상세 관측은 비용이라 fail-closed.
10
+ // ⛔ 읽기 실패·깨진 파일은 기본 «꺼짐» — 단 유효한 미래 ELANOUS_LIVE_DETAIL_UNTIL 은 전체 범위를 켠다.
11
11
 
12
12
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
13
13
  import { dirname, join } from 'node:path';
@@ -107,10 +107,15 @@ export function selectLiveDetail(opts: DetailReadOptions = {}): LiveDetailSelect
107
107
  return { ...selection, state: parsed && parsed.until > now ? parsed : null };
108
108
  }
109
109
 
110
- /** 이 런에 상세 관측이 켜져 있나 — 두 뿌리에 공통인 5초 캐시. */
110
+ /** 이 런에 상세 관측이 켜져 있나 — 파일은 5초 캐시, env 만료는 매번 확인한다. */
111
111
  export function isLiveDetailOn(runId?: string, opts: DetailReadOptions = {}): boolean {
112
112
  const now = opts.now ?? Date.now();
113
- return matches(selectLiveDetail({ ...opts, now }).state, runId, now);
113
+ const state = selectLiveDetail({ ...opts, now }).state;
114
+ if (state) return matches(state, runId, now);
115
+ const rawUntil = process.env.ELANOUS_LIVE_DETAIL_UNTIL;
116
+ if (!rawUntil || !/^\d+$/.test(rawUntil)) return false;
117
+ const until = Number(rawUntil);
118
+ return Number.isSafeInteger(until) && until > now;
114
119
  }
115
120
 
116
121
  function matches(state: LiveDetailState | null, runId: string | undefined, now: number): boolean {
@@ -0,0 +1,32 @@
1
+ import { decideProviderForConfig, type ProviderDecision } from '../llm.js';
2
+ import { getUserConfig, type UserConfig } from '../user-config.js';
3
+
4
+ export interface UsableLlm {
5
+ usable: boolean;
6
+ provider?: string;
7
+ via: 'login' | 'key' | 'local-server' | 'none';
8
+ why: string;
9
+ }
10
+
11
+ export interface UsableLlmDeps {
12
+ config?: UserConfig;
13
+ /** The same selector used to construct a runtime LLM; injectable for isolated tests. */
14
+ decide?: (config: UserConfig) => ProviderDecision;
15
+ }
16
+
17
+ /** Evaluate the selected route, not the union of discovered credentials or servers. */
18
+ export function resolveUsableLlm(deps: UsableLlmDeps = {}): UsableLlm {
19
+ const decision = (deps.decide ?? decideProviderForConfig)(deps.config ?? getUserConfig());
20
+ const provider = decision.provider.replace(/^auto:/, '');
21
+ // Never echo an untrusted configured provider or a model/URL/credential into output.
22
+ const known = ['openai-codex', 'openai', 'anthropic', 'grok', 'gemini', 'openrouter', 'local'];
23
+ if (provider === 'auto' || decision.auth === 'none') {
24
+ // An explicitly selected provider stays named so callers can offer that provider's fix,
25
+ // not the Codex login (the auto route's default remedy).
26
+ const explicit = !decision.provider.startsWith('auto') && known.includes(provider);
27
+ return { usable: false, ...(explicit ? { provider } : {}), via: 'none', why: 'no usable LLM route selected' };
28
+ }
29
+ if (!known.includes(provider)) return { usable: false, via: 'none', why: 'no usable LLM route selected' };
30
+ const via = decision.auth === 'oauth' ? 'login' : decision.auth === 'apikey' ? 'key' : 'local-server';
31
+ return { usable: true, provider, via, why: `LLM via ${via} (${provider})` };
32
+ }
@@ -48,9 +48,13 @@ function graphFiles(dir: string): string[] {
48
48
  return entries.flatMap(e => e.isDirectory() ? graphFiles(join(dir, e.name)) : e.isFile() && /\.ya?ml$/.test(e.name) ? [join(dir, e.name)] : []).sort();
49
49
  }
50
50
 
51
+ /** A wrapper cron that runs the graph itself declares its loop with a trailing `# elanous-loop=<graph_id>` comment. */
52
+ const LOOP_MARKER = /#\s*elanous-loop=([A-Za-z0-9][\w.-]*)\s*$/;
53
+
51
54
  /** Match only the actual graph CLI invocation, not a filename mentioned in a shell argument. */
52
- export function graphJobMatches(command: string | null, file: string, root: string): boolean {
55
+ export function graphJobMatches(command: string | null, file: string, root: string, graphId?: string): boolean {
53
56
  if (!command) return false;
57
+ if (graphId && command.match(LOOP_MARKER)?.[1] === graphId) return true;
54
58
  const invocation = /(?:^|&&\s*|;\s*)(?:(?:\S*\/)?(?:bun|node)\s+(?:\S*\/)?bin\/elanous\.mjs|(?:\S*\/)?elanous)\s+(?:--test(?:=\S+|\s+\S+)\s+)?graph\s+run\s+(['"]?)([^\s'";&|]+)\1(?=\s|$|[;&|])/g;
55
59
  for (const match of command.matchAll(invocation)) {
56
60
  const arg = match[2]!;
@@ -100,8 +104,17 @@ function schedules(opts: LoopRegistryOptions): ScheduleRow[] {
100
104
  } finally { db.close(); }
101
105
  }
102
106
 
107
+ /** Where loop graphs live and where their cron line `cd`s — the same rule as the other elanous crons:
108
+ * the installed package when there is one, else this checkout. Never the caller's cwd: a loop started
109
+ * from a disposable worktree would schedule a cron into a directory that is about to disappear (🅣 09-29). */
110
+ export function defaultLoopRoot(): string {
111
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
112
+ const { installedCronRoot, repoRoot } = require('../domains/schedule-registry.js') as typeof import('../domains/schedule-registry.js');
113
+ return installedCronRoot() ?? repoRoot();
114
+ }
115
+
103
116
  export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
104
- const root = resolve(opts.root ?? process.cwd());
117
+ const root = resolve(opts.root ?? defaultLoopRoot());
105
118
  const stateRoot = opts.stateRoot ?? effectiveInstanceRoot();
106
119
  const rows = schedules(opts);
107
120
  const loops: LoopEntry[] = [];
@@ -117,7 +130,7 @@ export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
117
130
  const trigger = header?.trigger && typeof header.trigger === 'object' && !Array.isArray(header.trigger) ? header.trigger as Record<string, unknown> : null;
118
131
  const declaredCron = typeof trigger?.cron === 'string' ? trigger.cron : null;
119
132
  const events = Array.isArray(trigger?.events) ? trigger.events.filter((e): e is string => typeof e === 'string') : [];
120
- const matched = rows.filter(row => graphJobMatches(row.command, file, root) && row.source === 'crontab' && row.run_via === 'crontab' && row.disabled_reason !== 'vanished');
133
+ const matched = rows.filter(row => graphJobMatches(row.command, file, root, doc.graph_id as string) && row.source === 'crontab' && row.run_via === 'crontab' && row.disabled_reason !== 'vanished');
121
134
  if (!declaredCron && !events.length && !matched.length) continue;
122
135
  if (!/^[a-zA-Z0-9][\w.-]*$/.test(doc.graph_id)) throw new Error(`unsafe loop graph_id: ${doc.graph_id}`);
123
136
  if (loops.some(loop => loop.id === doc.graph_id)) throw new Error(`duplicate loop graph_id: ${doc.graph_id}`);
@@ -142,6 +155,15 @@ export function loopStatus(id: string, opts: LoopRegistryOptions = {}): LoopEntr
142
155
  return { ...loop, recentRuns: recent };
143
156
  }
144
157
 
158
+ /** The crontab line a loop runs — same shape as the other elanous crons: cron starts in $HOME with a thin PATH,
159
+ * so a bare `elanous graph run <relative file>` fails silently every tick (09-29 steward). */
160
+ export function loopCronCommand(id: string, file: string, root: string, bun = process.execPath): string {
161
+ // Quote only when needed: the other crons (and `graphJobMatches`) use bare absolute paths.
162
+ const q = (value: string) => /^[\w./@+-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
163
+ const log = `/tmp/elanous-loop-${id.replace(/[^\w.-]/g, '_')}.log`;
164
+ return `cd ${q(root)} && ${q(bun)} bin/elanous.mjs graph run ${q(file)} >> ${log} 2>&1`;
165
+ }
166
+
145
167
  export async function setLoopEnabled(id: string, enabled: boolean, yes = false, opts: LoopRegistryOptions = {}): Promise<unknown> {
146
168
  const loop = loopStatus(id, opts);
147
169
  const action = enabled ? 'start' : 'stop';
@@ -149,7 +171,7 @@ export async function setLoopEnabled(id: string, enabled: boolean, yes = false,
149
171
  const pending = loop.jobs.filter(job => job.enabled !== enabled);
150
172
  if (!pending.length && loop.jobs.length) return { action, id, changed: false, enabled: loop.enabled };
151
173
  const changes = loop.jobs.length ? pending.map(job => ({ action: enabled ? 'enable' : 'disable', id: job.id })) :
152
- [{ action: 'create', cron: loop.trigger.cron, command: `elanous graph run ${loop.file}` }];
174
+ [{ action: 'create', cron: loop.trigger.cron, command: loopCronCommand(id, loop.file, resolve(opts.root ?? defaultLoopRoot())) }];
153
175
  if (!yes) return { action, id, dryRun: true, changes, note: 'Apply with --yes (crontab backup).' };
154
176
  const dispatch = opts.scheduleAction ?? (async (kind: 'create' | 'enable' | 'disable', args: Record<string, unknown>) => {
155
177
  const { dispatchScheduleManage } = await import('../domains/schedule-manage-tool.js');
@@ -170,5 +192,5 @@ export async function setLoopEnabled(id: string, enabled: boolean, yes = false,
170
192
  export async function runLoop(id: string, dryRun = false, opts: LoopRegistryOptions = {}): Promise<GraphRunState> {
171
193
  const loop = loopStatus(id, opts);
172
194
  debug.log('loops.registry', 'run', { id, dryRun });
173
- return runGraph(join(resolve(opts.root ?? process.cwd()), loop.file), { dryRun, deps: { root: opts.stateRoot ?? effectiveInstanceRoot() } });
195
+ return runGraph(join(resolve(opts.root ?? defaultLoopRoot()), loop.file), { dryRun, deps: { root: opts.stateRoot ?? effectiveInstanceRoot() } });
174
196
  }
@@ -7,6 +7,41 @@ import { debug } from '../debug/log.js';
7
7
  import { packDirDeterministic } from './tgz.js';
8
8
  import { signIndex, verifyIndex, type MarketplaceIndex } from './signed-index.js';
9
9
 
10
+ type IndexConnector = MarketplaceIndex['plugins'][number]['ai.elanous']['connectors'][number];
11
+
12
+ function normalizeConnectors(connectors: unknown): { ok: true; value: IndexConnector[] } | { ok: false } {
13
+ if (!Array.isArray(connectors)) return { ok: false };
14
+ const value: IndexConnector[] = [];
15
+ const record = (item: unknown): item is Record<string, unknown> =>
16
+ item !== null && typeof item === 'object' && !Array.isArray(item);
17
+ for (const connector of connectors) {
18
+ if (!record(connector) || typeof connector.id !== 'string' || !connector.id) return { ok: false };
19
+ if (Array.isArray(connector.userConfig)) {
20
+ if (typeof connector.kind !== 'string' || !connector.userConfig.every((field: unknown) =>
21
+ record(field) && typeof field.key === 'string' && field.key.trim().length > 0 && typeof field.label === 'string' &&
22
+ typeof field.secret === 'boolean' && (field.env === undefined || typeof field.env === 'string'))) {
23
+ return { ok: false };
24
+ }
25
+ value.push(connector as IndexConnector);
26
+ } else if (Array.isArray(connector.fields)) {
27
+ if ((connector.kind !== undefined && typeof connector.kind !== 'string') || !connector.fields.every((field: unknown) =>
28
+ record(field) && typeof field.name === 'string' && field.name.trim().length > 0 && typeof field.secret === 'boolean' &&
29
+ (field.label === undefined || typeof field.label === 'string') &&
30
+ (field.env === undefined || typeof field.env === 'string'))) {
31
+ return { ok: false };
32
+ }
33
+ value.push({ id: connector.id, kind: (connector.kind ?? 'credentials') as string,
34
+ userConfig: connector.fields.map((field: Record<string, unknown>) => ({
35
+ key: field.name as string, label: (field.label ?? field.name) as string, secret: field.secret === true,
36
+ ...(field.env ? { env: field.env as string } : {}),
37
+ })) });
38
+ } else {
39
+ return { ok: false };
40
+ }
41
+ }
42
+ return { ok: true, value };
43
+ }
44
+
10
45
  function unpackLocal(archive: Uint8Array, destination: string): void {
11
46
  const tar = gunzipSync(archive);
12
47
  const field = (part: Buffer) => part.toString('utf8').replace(/\0.*$/, '');
@@ -113,13 +148,15 @@ export function publishMarket(input: {
113
148
  skip('invalid-plugin.json'); continue;
114
149
  }
115
150
  const { category, bundle, ...metadata } = ai as MarketplaceIndex['plugins'][number]['ai.elanous'] & { category?: string; bundle?: unknown };
116
- if (!Array.isArray(metadata.capabilities) || !Array.isArray(metadata.connectors) ||
151
+ if (!Array.isArray(metadata.capabilities) ||
117
152
  (category !== undefined && typeof category !== 'string')) {
118
153
  skip('invalid-plugin.json'); continue;
119
154
  }
120
155
  if (metadata.pricing !== undefined && metadata.pricing?.model !== 'free') {
121
156
  skip('paid-not-allowed-in-M0'); continue;
122
157
  }
158
+ const normalized = normalizeConnectors(metadata.connectors);
159
+ if (!normalized.ok) { skip('invalid-connectors'); continue; }
123
160
  const bundleItem = (item: unknown): item is string | { from: string; as: string } => typeof item === 'string' ||
124
161
  (!!item && typeof item === 'object' && !Array.isArray(item) && typeof (item as { from?: unknown }).from === 'string' &&
125
162
  typeof (item as { as?: unknown }).as === 'string');
@@ -208,7 +245,26 @@ export function publishMarket(input: {
208
245
  if (message.startsWith('bundle-conflict')) { skip('bundle-conflict'); continue; }
209
246
  throw error;
210
247
  }
211
- const sha256 = createHash('sha256').update(archive).digest('hex');
248
+ let sha256 = createHash('sha256').update(archive).digest('hex');
249
+ const recorded = recordedHashes.get(JSON.stringify([manifest.name, manifest.version]));
250
+ if (recorded && recorded !== sha256) {
251
+ const existingPath = join(input.outDir, manifest.name, manifest.version, `${recorded}.tgz`);
252
+ let sameContent = false;
253
+ if (existsSync(existingPath)) {
254
+ try {
255
+ const existing = readFileSync(existingPath);
256
+ sameContent = createHash('sha256').update(existing).digest('hex') === recorded &&
257
+ gunzipSync(existing).equals(gunzipSync(archive));
258
+ if (sameContent) {
259
+ archive = existing;
260
+ sha256 = recorded;
261
+ }
262
+ } catch { /* An unreadable archive cannot prove immutability. */ }
263
+ }
264
+ if (!sameContent) throw new Error(`version-immutable: ${manifest.name}@${manifest.version}`);
265
+ debug.log('market.publish', 'reused-artifact', { name: manifest.name, version: manifest.version,
266
+ reason: 'same-content-different-gzip' });
267
+ }
212
268
  remember(manifest.name, manifest.version, sha256);
213
269
  const key = `${manifest.name}/${manifest.version}/${sha256}.tgz`;
214
270
  const source: MarketplaceIndex['plugins'][number]['source'] = input.source
@@ -218,7 +274,7 @@ export function publishMarket(input: {
218
274
  policy: { installation: 'AVAILABLE', authentication: 'ON_USE' },
219
275
  category: category ?? 'Productivity', version: manifest.version,
220
276
  description: manifest.description, artifact: { sha256, bytes: archive.length, key },
221
- 'ai.elanous': { ...metadata, pricing: metadata.pricing ?? { model: 'free' } },
277
+ 'ai.elanous': { ...metadata, connectors: normalized.value, pricing: metadata.pricing ?? { model: 'free' } },
222
278
  });
223
279
  archives.push({ key, data: archive, name: dir });
224
280
  published.push({ name: manifest.name, version: manifest.version, sha256, bundled });
@@ -1142,8 +1142,8 @@ export const nativeToolCatalog: NativeToolCatalogEntry[] = [
1142
1142
  kind: 'web',
1143
1143
  aliases: ['WebSearch', 'web_search'],
1144
1144
  displayName: 'WebSearch',
1145
- description: 'Search the public web via a configured provider (Grok live-search / Firecrawl / MCP / custom).',
1146
- promptSummary: '`WebSearch` (provider-backed web search — Grok live-search by default; pair with WebFetch for full bodies)',
1145
+ description: 'Search the public web via a configured provider (Grok live-search / Firecrawl / MCP / custom); when no key is available, fall back to keyless DuckDuckGo.',
1146
+ promptSummary: '`WebSearch` (provider-backed web search — Grok live-search by default; when no key is available, fall back to keyless DuckDuckGo; pair with WebFetch for full bodies)',
1147
1147
  host: ['skill'],
1148
1148
  safety: ['network', 'read-only'],
1149
1149
  supportsParallel: true,
@@ -0,0 +1,125 @@
1
+ import { existsSync, readFileSync, readdirSync, type Dirent } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { debug } from '../../debug/log.js';
4
+ import { decideGraphApproval, lastJsonObject, type GraphRunState } from '../../graph-runner/runner.js';
5
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
6
+ import { jsonResponse } from './json-response.js';
7
+
8
+ export const GRAPH_APPROVALS_PATH = '/v1/graph-approvals';
9
+ const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
10
+
11
+ interface GraphApprovalItem {
12
+ graphId: string;
13
+ runId: string;
14
+ nodeId: string;
15
+ message: string;
16
+ since: string;
17
+ path: string[];
18
+ recent: Array<{ nodeId: string; ok: boolean; outcome?: string; summary?: string }>;
19
+ }
20
+
21
+ interface GraphApprovalsDeps {
22
+ authorize?: (request: Request) => boolean;
23
+ /** Credential the request carries, checked independently of same-origin (`bearerCredential`).
24
+ * A decision needs a bearer token: same-origin alone is not trusted because behind
25
+ * `tailscale serve` every tailnet peer (Pod included) arrives from loopback. */
26
+ authReason?: (request: Request) => string | undefined;
27
+ root?: string;
28
+ }
29
+
30
+ const DECISION_AUTH_REASONS = new Set(['bearer-match', 'temp-token', 'noauth']);
31
+
32
+ function validSegment(value: string): boolean {
33
+ return SEGMENT.test(value) && value !== '.' && value !== '..';
34
+ }
35
+
36
+ function readRun(root: string, graphId: string, runId: string): GraphRunState | null {
37
+ if (!validSegment(graphId) || !validSegment(runId)) return null;
38
+ try {
39
+ const state = JSON.parse(readFileSync(join(root, 'graph-runs', graphId, `${runId}.json`), 'utf8')) as GraphRunState;
40
+ return state.graphId === graphId && state.runId === runId ? state : null;
41
+ } catch (error) {
42
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ function undecided(state: GraphRunState, root: string): boolean {
48
+ return state.status === 'awaiting-approval' && !!state.pending && !state.pending.decision
49
+ && Array.isArray(state.path) && !existsSync(join(root, 'graph-runs', state.graphId, `${state.runId}.json.${state.path.length}.decision.json`));
50
+ }
51
+
52
+ function list(root: string): GraphApprovalItem[] {
53
+ const dir = join(root, 'graph-runs');
54
+ let graphs: Dirent[];
55
+ try { graphs = readdirSync(dir, { withFileTypes: true }); }
56
+ catch (error) {
57
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
58
+ throw error;
59
+ }
60
+ const items: GraphApprovalItem[] = [];
61
+ for (const graph of graphs) {
62
+ if (!graph.isDirectory() || !validSegment(graph.name)) continue;
63
+ for (const file of readdirSync(join(dir, graph.name), { withFileTypes: true })) {
64
+ if (!file.isFile() || !file.name.endsWith('.json') || file.name.endsWith('.decision.json')) continue;
65
+ const runId = file.name.slice(0, -5);
66
+ const state = readRun(root, graph.name, runId);
67
+ if (!state || !undecided(state, root) || !Array.isArray(state.nodes)) continue;
68
+ const pending = state.pending!;
69
+ items.push({
70
+ graphId: state.graphId, runId: state.runId, nodeId: pending.nodeId, message: pending.message,
71
+ since: pending.since, path: state.path,
72
+ recent: state.nodes.slice(-5).map((node) => {
73
+ const output = lastJsonObject(node.output);
74
+ return {
75
+ nodeId: node.nodeId, ok: node.ok,
76
+ ...(typeof output?.outcome === 'string' ? { outcome: output.outcome } : {}),
77
+ ...(typeof output?.summary === 'string' ? { summary: output.summary } : {}),
78
+ };
79
+ }),
80
+ });
81
+ }
82
+ }
83
+ return items.sort((a, b) => b.since.localeCompare(a.since));
84
+ }
85
+
86
+ export async function handleGraphApprovals(req: Request, deps: GraphApprovalsDeps = {}): Promise<Response> {
87
+ const pathname = new URL(req.url).pathname;
88
+ const match = /^\/v1\/graph-approvals\/([^/]+)\/([^/]+)$/.exec(pathname);
89
+ if (pathname !== GRAPH_APPROVALS_PATH && !match) return jsonResponse({ error: 'not-found' }, 404);
90
+ if (!deps.authorize?.(req)) return jsonResponse({ error: 'unauthorized' }, 401);
91
+ if (pathname === GRAPH_APPROVALS_PATH) {
92
+ if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
93
+ return jsonResponse({ items: list(deps.root ?? effectiveInstanceRoot()) });
94
+ }
95
+ if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
96
+ const authReason = deps.authReason?.(req);
97
+ if (!authReason || !DECISION_AUTH_REASONS.has(authReason)) {
98
+ debug.log('approvals.graph', 'refused', { reason: 'pairing-required', auth: authReason ?? null });
99
+ return jsonResponse({ error: 'pairing-required' }, 403);
100
+ }
101
+ let graphId: string;
102
+ let runId: string;
103
+ try { graphId = decodeURIComponent(match![1]!); runId = decodeURIComponent(match![2]!); }
104
+ catch { return jsonResponse({ error: 'not-found' }, 404); }
105
+ if (!validSegment(graphId) || !validSegment(runId)) return jsonResponse({ error: 'not-found' }, 404);
106
+ let body: unknown;
107
+ try { body = await req.json(); }
108
+ catch { return jsonResponse({ error: 'bad_request' }, 400); }
109
+ const decision = body && typeof body === 'object' && !Array.isArray(body) && 'decision' in body ? body.decision : undefined;
110
+ if (decision !== 'approved' && decision !== 'rejected') return jsonResponse({ error: 'bad_request' }, 400);
111
+ const root = deps.root ?? effectiveInstanceRoot();
112
+ const state = readRun(root, graphId, runId);
113
+ if (!state) return jsonResponse({ error: 'not-found' }, 404);
114
+ if (!undecided(state, root)) return jsonResponse({ error: 'already-decided' }, 409);
115
+ const nodeId = state.pending!.nodeId;
116
+ try {
117
+ decideGraphApproval(graphId, runId, decision, `pwa:${authReason}`, root);
118
+ } catch (error) {
119
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return jsonResponse({ error: 'not-found' }, 404);
120
+ if (error instanceof Error && error.message.startsWith('run is not awaiting an undecided approval:')) return jsonResponse({ error: 'already-decided' }, 409);
121
+ throw error;
122
+ }
123
+ debug.log('approvals.graph', 'decided', { graphId, runId, nodeId, decision, by: `pwa:${authReason}` });
124
+ return jsonResponse({ graphId, runId, decision });
125
+ }
@@ -17,6 +17,8 @@
17
17
 
18
18
  import type { TelegramEvent } from '../../workflow-runtime/triggers/telegram-source.js';
19
19
  import { spawnSync } from 'node:child_process';
20
+ import { compareTokenConstTime } from '../../acp/transport/auth.js';
21
+ import { ensureAuthToken } from '../../auth/acp-token.js';
20
22
  import type { NexusState } from '../state/state.js';
21
23
  import { DEFAULT_NEXUS_HTTP_PORT } from '../default-port.js';
22
24
  export { DEFAULT_NEXUS_HTTP_PORT } from '../default-port.js';
@@ -195,6 +197,7 @@ import { handleMcpResourceGet, MCP_RESOURCE_ROUTE_PATH } from './mcp-resource-ro
195
197
  // ⛔ 라우트 상수를 «디스패처»가 안 쓰고 문자열로 베끼고 있었다(16차 실측) — 잎에서 읽는다.
196
198
  import { APPROVALS_MERGES_PATH, DESIGN_DIRECTION_PATH, DESIGN_PREVIEW_PATH_PREFIX, DESIGN_PREVIEWS_PATH, DESIGN_SYSTEM_PATH, IPA_PATH_PREFIX, MANIFEST_PATH } from './rest-route-paths.js';
197
199
  import { handleMergeApprovals } from './merge-approvals.js';
200
+ import { GRAPH_APPROVALS_PATH, handleGraphApprovals } from './graph-approvals.js';
198
201
  import { handleLiveDetail, LIVE_DETAIL_PATH } from './live-detail.js';
199
202
  import { handleLiveShipped, LIVE_SHIPPED_PATH } from './live-shipped.js';
200
203
  import { handleMcpWidgetCall, MCP_WIDGET_CALL_ROUTE_PATH, persistWidgetTurnToSessionStore } from './mcp-widget-call-route.js';
@@ -300,6 +303,7 @@ import {
300
303
  handleVoiceCost,
301
304
  handleVoiceTranscribe,
302
305
  checkAuth,
306
+ bearerCredential,
303
307
  registerAuthPeerAddress,
304
308
  type MetaApiOpts,
305
309
  } from './meta-api.js';
@@ -332,7 +336,10 @@ import { handleTriggersSnapshot } from './triggers.js';
332
336
  import { handleWorkflowTemplatesList } from './workflow-templates.js';
333
337
  import { handleGraphsGet, handleGraphsMutation } from './graphs-api.js';
334
338
  import { handleGraphKindsGet, handleGraphsValidatePost } from './graph-kinds.js';
335
- import { handlePluginsIndexGet, handlePluginsGet } from './plugins-market.js';
339
+ import { syncInstalledPluginNodes } from '../../graph-kinds/installed-plugin-nodes.js';
340
+ import { elanousStateRoot } from '../../autopilot/state-paths.js';
341
+ import { handlePluginsIndexGet, handlePluginsGet, handlePluginsMarketRefresh, handlePluginsInstall, handlePluginsRemove } from './plugins-market.js';
342
+ import { handlePluginsCredentialsGet, handlePluginsCredentialsPut } from './plugins-credentials-api.js';
336
343
  import {
337
344
  handleWorkflowsList,
338
345
  } from './workflows.js';
@@ -369,7 +376,7 @@ import {
369
376
  parseAgentCliSessionPath,
370
377
  } from './agent-cli.js';
371
378
  import { handleSessionTurnControl, SESSION_TURN_CONTROL_PATH } from '../../session/session-turn-control.js';
372
- import { authenticatePodCredential, handlePodGrokCredential, POD_CREDENTIAL_GROK_PATH } from './pod-credential-api.js';
379
+ import { authenticatePodCredential, handlePodGrokCredential, handlePodGithubCredential, POD_CREDENTIAL_GROK_PATH, POD_CREDENTIAL_GITHUB_PATH } from './pod-credential-api.js';
373
380
  import { handleOutboundReport } from './outbound-report.js';
374
381
  import { handleSelfEvent } from './self-event.js';
375
382
  /** 매매 «실행»은 애드온이다(대표 09-20 결정 ③ · 별도 상용 저장소 · release/trading-export.yaml) — 공개 코어엔 없다.
@@ -436,6 +443,8 @@ export function probeNexusHttpPort(
436
443
  export type NexusWsBridgeInit = Omit<WsBridgeOpts, 'hostname' | 'port'>;
437
444
 
438
445
  export interface NexusHttpServerOpts {
446
+ /** Optional installer state root for isolated API consumers and tests. */
447
+ pluginStateRoot?: string;
439
448
  state: NexusState;
440
449
  registry: TabRegistry;
441
450
  eventBus: NexusEventBus;
@@ -822,19 +831,27 @@ export async function routeRequest(
822
831
  // Per-route checkAuth calls below stay in place.
823
832
  // OPTIONS is a CORS preflight and cannot carry Authorization; existing
824
833
  // handlers answer it themselves, so the gate does not swallow it.
834
+ // A runtime-less tab mutation still needs authentication before the
835
+ // dispatcher can return its 503. Do not enable this fallback for other paths.
836
+ const runtimeLessTabMutation = !opts.metaApi && method !== 'GET' && method !== 'OPTIONS'
837
+ && (pathname === '/v1/nexus/tabs' || pathname.startsWith('/v1/nexus/tabs/'));
825
838
  if (
826
839
  method !== 'OPTIONS'
827
840
  && pathname.startsWith('/v1/')
828
841
  && !isPublicRoute(method, pathname, { setupMode: false })
829
842
  ) {
830
- if (!opts.metaApi || !checkAuth(req, opts.metaApi)) {
843
+ const fallbackToken = runtimeLessTabMutation ? ensureAuthToken().token : undefined;
844
+ const offered = req.headers.get('authorization');
845
+ const fallbackAuthorized = fallbackToken !== undefined && offered?.startsWith('Bearer ') === true
846
+ && compareTokenConstTime(offered.slice('Bearer '.length).trim(), fallbackToken);
847
+ if (!(opts.metaApi ? checkAuth(req, opts.metaApi) : fallbackAuthorized)) {
831
848
  const name = method === 'POST' && pathname === '/v1/tasks' && opts.metaApi
832
849
  ? matchIngestAuthorization(req)?.name
833
850
  : undefined;
834
851
  if (name === undefined) {
835
852
  debug.log('nexus.auth', 'default-deny', { method, pathname });
836
853
  // MCP 클라이언트(Claude Code)에게는 «할 일»을 말한다 — 안 그러면 OAuth 로 가서 /register 405 를 만난다.
837
- if (pathname === '/v1/mcp') return mcpUnauthorizedResponse();
854
+ if (pathname === '/v1/mcp') return mcpUnauthorizedResponse(new URL(req.url).origin);
838
855
  return jsonResponse({ error: 'unauthorized' }, 401);
839
856
  }
840
857
  debug.log('nexus.auth', 'ingest-token', { name, pathname });
@@ -843,7 +860,7 @@ export async function routeRequest(
843
860
  // OAuth 동적 클라이언트 등록(RFC 7591) — Elanous 는 OAuth 를 쓰지 않는다. 405 대신 «이렇게 등록하라»를 돌려준다.
844
861
  if (method === 'POST' && pathname === '/register') {
845
862
  debug.log('nexus.auth', 'oauth-register-rejected', { pathname });
846
- return oauthRegistrationRejected();
863
+ return oauthRegistrationRejected(new URL(req.url).origin);
847
864
  }
848
865
 
849
866
  // FU2 (2026-05-12) — root-level browser conveniences. The PWA serves
@@ -899,6 +916,12 @@ export async function routeRequest(
899
916
  });
900
917
  }
901
918
 
919
+ if (pathname === GRAPH_APPROVALS_PATH || pathname.startsWith(`${GRAPH_APPROVALS_PATH}/`)) {
920
+ return handleGraphApprovals(req, {
921
+ authorize: (request) => !!opts.metaApi && checkAuth(request, opts.metaApi),
922
+ authReason: (request) => (opts.metaApi ? bearerCredential(request, opts.metaApi) : undefined),
923
+ });
924
+ }
902
925
  if (pathname === APPROVALS_MERGES_PATH || pathname.startsWith(`${APPROVALS_MERGES_PATH}/`)) {
903
926
  return handleMergeApprovals(req, {
904
927
  authorize: (request) => !!opts.metaApi && checkAuth(request, opts.metaApi),
@@ -1189,6 +1212,25 @@ export async function routeRequest(
1189
1212
  // Templates POST handled here too (PR κ).
1190
1213
  // Config / secrets PUT/POST/DELETE here too (PR μ).
1191
1214
  if (method !== 'GET') {
1215
+ if (method === 'POST' && pathname === '/v1/plugins/install') {
1216
+ if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
1217
+ return handlePluginsInstall(req, opts.metaApi, opts.pluginStateRoot);
1218
+ }
1219
+ const marketRefresh = /^\/v1\/plugins\/markets\/([^/]+)\/refresh$/.exec(pathname);
1220
+ if (method === 'POST' && marketRefresh) {
1221
+ if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
1222
+ return handlePluginsMarketRefresh(req, marketRefresh[1]!, opts.metaApi, { root: opts.pluginStateRoot });
1223
+ }
1224
+ const pluginCredentials = /^\/v1\/plugins\/([^/]+)\/credentials$/.exec(pathname);
1225
+ if (method === 'PUT' && pluginCredentials) {
1226
+ if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
1227
+ return handlePluginsCredentialsPut(req, pluginCredentials[1]!, opts.metaApi, opts.pluginStateRoot, opts.reloadMcpClients);
1228
+ }
1229
+ const pluginRemove = /^\/v1\/plugins\/([^/]+)$/.exec(pathname);
1230
+ if (method === 'DELETE' && pluginRemove) {
1231
+ if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
1232
+ return handlePluginsRemove(req, pluginRemove[1]!, opts.metaApi, opts.pluginStateRoot);
1233
+ }
1192
1234
  if ((method === 'PUT' || method === 'POST') && pathname.startsWith('/v1/graphs/')) {
1193
1235
  if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
1194
1236
  if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
@@ -1215,6 +1257,11 @@ export async function routeRequest(
1215
1257
  if (!gate.ok) return gate.response;
1216
1258
  return handlePodGrokCredential(req);
1217
1259
  }
1260
+ if (pathname === POD_CREDENTIAL_GITHUB_PATH && method === 'POST') {
1261
+ const gate = await authenticatePodCredential(req, {}, 'gh-credential');
1262
+ if (!gate.ok) return gate.response;
1263
+ return handlePodGithubCredential(req);
1264
+ }
1218
1265
  if (pathname === '/v1/harness/ask' && method === 'POST') {
1219
1266
  if (!opts.metaApi) return jsonResponse({ error: 'meta-api-runtime-not-wired' }, 503);
1220
1267
  if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
@@ -2431,12 +2478,19 @@ export async function routeRequest(
2431
2478
  // surface (`/v1/workflows` · `~/.elanous/workflows-runs/`) covers the same
2432
2479
  // user need.
2433
2480
 
2481
+ const pluginCredentialsGet = /^\/v1\/plugins\/([^/]+)\/credentials$/.exec(pathname);
2482
+ if (method === 'GET' && pluginCredentialsGet) {
2483
+ if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
2484
+ return handlePluginsCredentialsGet(req, pluginCredentialsGet[1]!, opts.metaApi, opts.pluginStateRoot);
2485
+ }
2434
2486
  if (method === 'GET' && (pathname === '/v1/plugins/index' || pathname === '/v1/plugins')) {
2435
2487
  if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
2436
- return pathname === '/v1/plugins/index' ? handlePluginsIndexGet(req, opts.metaApi) : handlePluginsGet(req, opts.metaApi);
2488
+ return pathname === '/v1/plugins/index' ? handlePluginsIndexGet(req, opts.metaApi, opts.pluginStateRoot) : handlePluginsGet(req, opts.metaApi, opts.pluginStateRoot);
2437
2489
  }
2438
2490
  if (method === 'GET' && pathname === '/v1/graph/kinds') {
2439
2491
  if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
2492
+ try { syncInstalledPluginNodes(opts.pluginStateRoot ?? elanousStateRoot()); }
2493
+ catch (error) { debug.log('graph.kinds', 'installed-sync-failed', { reason: String(error) }); }
2440
2494
  return handleGraphKindsGet(req, opts.metaApi);
2441
2495
  }
2442
2496
  // F-M1 read + P-F1 raw YAML. Writes live in the mutation block and never touch core graphs/.
@@ -301,6 +301,15 @@ export function activeStoreNames(deps: LogFabricDeps, nowMs = Date.now()): strin
301
301
  return names;
302
302
  }
303
303
 
304
+ /** 등록된 로그 저장소 수(이 인스턴스 ⊕ logs.db 가 있는 우주) — `@active` 가 연 수의 «분모». 못 세면 `null`(0 으로 접지 않는다). */
305
+ export function registeredLogStoreCount(deps: Pick<LogFabricDeps, 'instances'> = {}): number | null {
306
+ try {
307
+ const names = new Set((deps.instances ?? readLogInstances)().filter((view) => view.dbExists).map((view) => view.name));
308
+ names.add(resolveLogInstanceName());
309
+ return names.size;
310
+ } catch { return null; }
311
+ }
312
+
304
313
  export function handleLogsQuery(req: Request, opts: MetaApiOpts, deps: LogFabricDeps = {}): Response {
305
314
  if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
306
315
  const url = new URL(req.url);
@@ -374,7 +383,7 @@ export function handleLogsQuery(req: Request, opts: MetaApiOpts, deps: LogFabric
374
383
  ts: new Date().toISOString(),
375
384
  ...(failedStores.length ? { failedStores } : {}),
376
385
  ...(selection.storeLimitReached ? { storeLimitReached: true } : {}),
377
- ...(activeStores ? { stores: activeStores } : {}),
386
+ ...(activeStores ? { stores: activeStores, registeredStores: registeredLogStoreCount(deps) } : {}),
378
387
  }, 200);
379
388
  }
380
389