elanous 0.2.4 → 0.2.6

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 (455) hide show
  1. package/README.md +7 -6
  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/1169-696263ac7af8c4d4.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 2984-977cfa339fcd65a5.js} +3 -3
  9. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/{8142-011e1f474efb8b33.js → 3190-e9282753e7ebde85.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/3995-bee512a7063036ce.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 4240-7153881b0b377306.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/{3403.4bc97b4066945a94.js → 5329.8b7e4bca684b6eec.js} +2 -2
  19. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 763-7d0e1913b5074e98.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7633.0654fe6ea259c1ee.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 91ff6dda-3b33d42d20657429.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-37b8bf78a3c6eec5.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_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 +11 -1
  172. package/graphs/release/release-loop.yaml +9 -2
  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/card-followup/README.md +33 -0
  177. package/plugins/card-followup/examples/card-sample.json +7 -0
  178. package/plugins/card-followup/graphs/card-followup.yaml +16 -0
  179. package/plugins/card-followup/graphs/recipes.yaml +12 -0
  180. package/plugins/card-followup/graphs/run-step.ts +172 -0
  181. package/plugins/card-followup/plugin.json +25 -0
  182. package/plugins/geo-check/README.md +13 -0
  183. package/plugins/geo-check/examples/brand-sample.json +8 -0
  184. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  185. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  186. package/plugins/geo-check/graphs/run-step.ts +177 -0
  187. package/plugins/geo-check/plugin.json +24 -0
  188. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  189. package/plugins/job-coach/README.md +42 -0
  190. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  191. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  192. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  193. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  194. package/plugins/job-coach/graphs/run-step.ts +211 -17
  195. package/plugins/job-coach/plugin.json +16 -3
  196. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  197. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  198. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  199. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  200. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  201. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  202. package/scripts/backup/elanous-backup.sh +4 -1
  203. package/scripts/build-docs-site.ts +4 -3
  204. package/scripts/coord-channel-watch.sh +23 -4
  205. package/scripts/coord-post.sh +49 -12
  206. package/scripts/coord-tracks.json +5 -5
  207. package/scripts/daemon-port-baseline.txt +1 -2
  208. package/scripts/eval-bot-vs-grok.ts +79 -0
  209. package/scripts/eval-grok-live-baseline.ts +36 -0
  210. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  211. package/scripts/fleet/node-setup.sh +46 -7
  212. package/scripts/landing-heal/must-fix-match.ts +26 -0
  213. package/scripts/landing-heal/run-mission.ts +114 -20
  214. package/scripts/lib/android-test-device.ts +32 -0
  215. package/scripts/lib/front-route-measurement.ts +131 -0
  216. package/scripts/lib/tui-sim-keys.ts +42 -0
  217. package/scripts/measure-front-route-classifier.ts +56 -0
  218. package/scripts/media/ocr-leak-scan.sh +21 -0
  219. package/scripts/public-export.ts +2 -3
  220. package/scripts/public-import-pr.ts +80 -6
  221. package/scripts/publish-internal-dist.ts +2 -3
  222. package/scripts/red-sweep-diff.ts +1 -1
  223. package/scripts/release-loop/auto-approve-node.ts +72 -0
  224. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  225. package/scripts/release-loop/gate-diff.ts +29 -0
  226. package/scripts/release-loop/gate-node.ts +366 -45
  227. package/scripts/release-loop/publish-node.ts +7 -3
  228. package/scripts/release-loop/release-docs.ts +20 -13
  229. package/scripts/release-loop/shard-plan.ts +47 -0
  230. package/scripts/release-loop/test-durations.ts +63 -0
  231. package/scripts/release-loop/upgrade-node.ts +3 -2
  232. package/scripts/test-audit-coverage-per-file.sh +16 -0
  233. package/scripts/test-audit-meaningless.ts +88 -0
  234. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  235. package/scripts/test-deterministic.ts +27 -5
  236. package/scripts/tui-sim.ts +59 -14
  237. package/scripts/ux/bump-installed.sh +20 -0
  238. package/scripts/ux/lib/cdp.ts +72 -0
  239. package/scripts/ux/record-page.ts +53 -0
  240. package/skills/explainer-video/SKILL.md +117 -0
  241. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  242. package/skills/explainer-video/engine/build.mjs +431 -0
  243. package/skills/explainer-video/engine/reel.mjs +128 -0
  244. package/skills/explainer-video/engine/reel.sh +8 -0
  245. package/skills/explainer-video/engine/vo.ts +79 -0
  246. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  247. package/src/agent/nested-agent-env.ts +1 -0
  248. package/src/agent-mission/driver.ts +95 -16
  249. package/src/agent-mission/mission-cli.ts +6 -0
  250. package/src/agent-mission/resource-ladder.ts +73 -25
  251. package/src/auth/acp-token.ts +16 -0
  252. package/src/auth/github-app-token.ts +112 -0
  253. package/src/autopilot/gh-rest-fallback.ts +177 -0
  254. package/src/autopilot/mission-origin.ts +20 -4
  255. package/src/autopilot/mission-phase-track.ts +11 -1
  256. package/src/autopilot/pr-manager.ts +51 -4
  257. package/src/autopilot/track-agent.ts +3 -3
  258. package/src/autopilot/track-courier.ts +2 -2
  259. package/src/boot/acp-server.ts +2 -16
  260. package/src/boot/daemon-tools/web-search.ts +2 -1
  261. package/src/boot/ws-bridge.ts +11 -0
  262. package/src/chat/index.ts +37 -8
  263. package/src/chat/log-scroll-key.ts +12 -0
  264. package/src/chat/submit-trace.ts +30 -0
  265. package/src/cli/agent-cli.ts +7 -2
  266. package/src/cli/decisions-cli.ts +94 -0
  267. package/src/cli/doctor-cli.ts +40 -54
  268. package/src/cli/doctor-readiness.ts +16 -24
  269. package/src/cli/intake-cli.ts +1 -1
  270. package/src/cli/mcp-call.ts +9 -0
  271. package/src/cli/mcp-gateway-cli.ts +7 -3
  272. package/src/cli/nexus-ios-bind.ts +25 -8
  273. package/src/cli/phone-link.ts +117 -0
  274. package/src/cli/plugin-cli.ts +136 -11
  275. package/src/cli/pr-lineage.ts +2 -1
  276. package/src/cli/private-ledger-commands.ts +31 -0
  277. package/src/cli/pwa-start.ts +3 -0
  278. package/src/cli/release-cli.ts +97 -7
  279. package/src/cli/self-update.ts +7 -4
  280. package/src/cli/start-cli.ts +33 -14
  281. package/src/dashboard/acp-boot.ts +3 -0
  282. package/src/dashboard/index.ts +62 -13
  283. package/src/dashboard/input/no-provider-reply.ts +13 -0
  284. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  285. package/src/dashboard/stale-install-check.ts +77 -0
  286. package/src/decisions/decision-ledger.ts +404 -0
  287. package/src/domains/outbound-alert.ts +13 -7
  288. package/src/domains/telegram-kind-route.ts +21 -0
  289. package/src/execution-loop/dispatch-task.ts +139 -0
  290. package/src/execution-loop/gate-decision-summary.ts +29 -0
  291. package/src/execution-loop/gates.ts +197 -0
  292. package/src/field/field-media.ts +245 -0
  293. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  294. package/src/graph-kinds/plugin-nodes.ts +10 -2
  295. package/src/graph-runner/runner.ts +35 -5
  296. package/src/grounding/token.ts +12 -6
  297. package/src/harness/harness-cli-command.ts +68 -9
  298. package/src/harness/harness-pod-dispatch.ts +8 -2
  299. package/src/harness/harness-stop.ts +147 -18
  300. package/src/harness/self-send-decide.ts +26 -17
  301. package/src/harness/self-send-target.ts +22 -1
  302. package/src/harness/worktree-branch-prefix.ts +4 -3
  303. package/src/index.ts +50 -13
  304. package/src/instance/resolve.ts +6 -0
  305. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  306. package/src/intake-plane/digest.ts +11 -2
  307. package/src/live/detail-switch.ts +8 -3
  308. package/src/llm/usable-llm.ts +32 -0
  309. package/src/loops/registry.ts +27 -5
  310. package/src/market/publish.ts +59 -3
  311. package/src/native-tool-catalog.ts +2 -2
  312. package/src/nexus/api/field-uploads.ts +143 -0
  313. package/src/nexus/api/graph-approvals.ts +174 -0
  314. package/src/nexus/api/harness-api.ts +219 -5
  315. package/src/nexus/api/http-server.ts +69 -6
  316. package/src/nexus/api/log-fabric.ts +10 -1
  317. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  318. package/src/nexus/api/merge-approvals.ts +2 -1
  319. package/src/nexus/api/meta-api.ts +14 -0
  320. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  321. package/src/nexus/api/plugins-market.ts +87 -3
  322. package/src/nexus/api/pod-credential-api.ts +68 -14
  323. package/src/nexus/api/public-routes.ts +7 -0
  324. package/src/nexus/api/trace.ts +57 -6
  325. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  326. package/src/nexus/index.ts +33 -32
  327. package/src/nexus/outbound/router.ts +1 -1
  328. package/src/notify/llm-budget-alert.ts +119 -0
  329. package/src/onboarding.ts +25 -2
  330. package/src/plugins/core/host.ts +14 -0
  331. package/src/plugins/install/market-fetch.ts +4 -3
  332. package/src/plugins/install/plugin-credentials.ts +106 -0
  333. package/src/plugins/install/plugin-install.ts +163 -32
  334. package/src/plugins/maker/plugin-maker.ts +208 -0
  335. package/src/prompt-library/universal-preamble.ts +8 -2
  336. package/src/relay/channel.ts +184 -0
  337. package/src/relay/mux.ts +182 -0
  338. package/src/relay/test-vectors.json +29 -0
  339. package/src/release-loop/checklist.ts +217 -0
  340. package/src/release-loop/release-note.ts +6 -3
  341. package/src/seat-address/seat-address.ts +34 -0
  342. package/src/self-dev/dev-cli.ts +40 -20
  343. package/src/self-dev/dev-pipeline.ts +77 -1
  344. package/src/self-implement/gate-baseline.ts +3 -1
  345. package/src/self-implement/goal-author.ts +18 -1
  346. package/src/self-implement/host-regate.ts +6 -2
  347. package/src/self-implement/orchestrator.ts +7 -2
  348. package/src/self-implement/run-ledger-compact.ts +1 -1
  349. package/src/self-implement/seams.ts +4 -2
  350. package/src/session/index.ts +23 -10
  351. package/src/skills/tools/dev-harness.ts +42 -0
  352. package/src/steward/steward-cards.ts +45 -0
  353. package/src/steward/triage.ts +50 -14
  354. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  355. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  356. package/src/task-orchestrator/surfaces/pod-command-job.ts +36 -6
  357. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  358. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  359. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  360. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  361. package/src/telegram-report.ts +12 -6
  362. package/src/telegram.ts +108 -1
  363. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  364. package/src/tui.ts +3 -2
  365. package/src/user-config.ts +32 -2
  366. package/src/util/spawn-sync-output.ts +15 -0
  367. package/src/version/code-revision.ts +1 -1
  368. package/src/version/packed-revision.json +1 -1
  369. package/src/video-pipeline/recipes/explainer.ts +213 -0
  370. package/src/video-pipeline/walk-line.ts +3 -2
  371. package/src/web-search/free.ts +77 -0
  372. package/src/web-search/index.ts +2 -0
  373. package/src/workflow-runtime/deps-bridge.ts +4 -3
  374. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  375. package/src/workflow-runtime/types.ts +2 -1
  376. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  388. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/botlab/page-9fa879ebf5959096.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/control/page-4472ddf1e2458e69.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/morning/page-112f7e477bda77fd.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  454. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  455. /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ import { CLI_HARNESS_DOGFOOD_ENTRANCE, CLI_HARNESS_ORCHESTRATE_ENTRANCE, describ
26
26
  import { collectCommandEntrances, renderCommandEntrances } from './self-dev/entrance-inventory.js';
27
27
  import { isGoalAuthorFileName } from './self-implement/goal-document.js';
28
28
  import { DEV_PIPELINE_SINK_SURFACE } from './self-implement/self-cli-sink-surface.js';
29
+ import { registerPrivateLedgerCommands } from './cli/private-ledger-commands.js';
29
30
  import { applyTestStateDirFlagFromArgv } from './cli/test-state-dir-flag.js';
30
31
  import { applyTestFlagFromArgv, observeTestFlagOwnership, uncoveredTestFlagPaths, staleTestFlagPaths } from './cli/test-flag.js';
31
32
  import { registerPtyTakeoverCommands } from './cli/pty-takeover-cli.js';
@@ -115,7 +116,7 @@ import { DEFAULT_REVIEW_BACKEND } from './agent-substrate/acp-reviewer.js';
115
116
  import type { ReviewResult } from './agent-substrate/pr-reviewer.js';
116
117
  import type { ReviewVerdict } from './agent-mission/review-loop.js';
117
118
  import type { SyncMode } from './types.js';
118
- import { runOnboarding, runOnboardingStep, runOnboardingNonInteractive, needsOnboarding, type OnboardingStepId } from './onboarding.js';
119
+ import { runOnboarding, runOnboardingStep, runOnboardingNonInteractive, needsOnboarding, handleOnboardingRefusal, type OnboardingStepId } from './onboarding.js';
119
120
  import {
120
121
  getUserConfig, reloadUserConfig, userConfigPath, saveUserConfig,
121
122
  backupUserConfig, backupConfigPath, addRotationEntry, type RotationEntry,
@@ -2088,13 +2089,14 @@ harnessCmd
2088
2089
  });
2089
2090
  harnessCmd
2090
2091
  .command('stop <runId>')
2091
- .description('런 하나를 멈춘다 — 오케스트레이터 프로세스(pid.json 의 시작 시각이 맞을 때만) ⊕ 라벨 elanous.run=<runId> Pod Job')
2092
+ .description('런 하나를 멈춘다 — pid.json 시작 시각 대조 ⊕ 런 발사 프로세스 표 조회 ⊕ 라벨 elanous.run=<runId> Pod Job')
2092
2093
  .option('--context <ctx...>', 'kube 문맥(여러 번 · 기본 = 알려진 문맥 전부)')
2093
2094
  .option('--json', 'JSON 출력')
2094
- .action(async (runId: string, opts: { context?: string[]; json?: boolean }) => {
2095
+ .option('--dry-run', '프로세스 후보만 보고 신호·Pod Job 삭제는 하지 않는다')
2096
+ .action(async (runId: string, opts: { context?: string[]; json?: boolean; dryRun?: boolean }) => {
2095
2097
  const { stopHarnessRun, defaultHarnessStopDeps, formatHarnessStop } = await import('./harness/harness-stop.js');
2096
2098
  try {
2097
- const result = await stopHarnessRun(runId, defaultHarnessStopDeps(), opts.context);
2099
+ const result = await stopHarnessRun(runId, defaultHarnessStopDeps(), opts.context, opts.dryRun === true);
2098
2100
  process.stdout.write(`${opts.json ? JSON.stringify(result) : formatHarnessStop(result)}\n`);
2099
2101
  } catch (error) {
2100
2102
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
@@ -4238,8 +4240,9 @@ const selfOrchestrateCmd = selfCmd
4238
4240
  if (!opts.json) ui.info(`[pod] 이미지 다시 굽기 — ${image.reason} (~1분)`);
4239
4241
  const { spawnSync } = await import('node:child_process');
4240
4242
  const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).stdout.trim() || process.cwd();
4241
- const b = spawnSync('bash', [`${top}/docker/harness/build.sh`], { encoding: 'utf8', timeout: 900_000 });
4242
- if (b.status !== 0) { ui.error(`--substrate pod: 이미지 굽기 실패 rc=${b.status}: ${(b.stdout + b.stderr).slice(-400)}`); process.exit(2); }
4243
+ // bun 1.4 applies the 1 MiB default maxBuffer: a docker build log past it kills the child (ENOBUFS · status null).
4244
+ const b = spawnSync('bash', [`${top}/docker/harness/build.sh`], { encoding: 'utf8', timeout: 900_000, maxBuffer: 64 * 1024 * 1024 });
4245
+ if (b.status !== 0) { ui.error(`--substrate pod: 이미지 굽기 실패 rc=${b.status}${b.error ? ` (${(b.error as NodeJS.ErrnoException).code ?? b.error.message})` : ''}: ${(b.stdout + b.stderr).slice(-400)}`); process.exit(2); }
4243
4246
  image = podImageFreshness();
4244
4247
  }
4245
4248
  }
@@ -4410,7 +4413,7 @@ const selfOrchestrateCmd = selfCmd
4410
4413
  ...results.map((r) => {
4411
4414
  const icon = r.status === 'done' ? '✅' : r.status === 'cancelled' ? '⛔' : '❌';
4412
4415
  const disp = r.merged ? ` → merged ${r.prUrl}` : r.prUrl ? ` → PR ${r.prUrl}` : r.stage ? ` [${r.stage}]` : '';
4413
- return ` ${icon} ${r.status} · ${r.feature.slice(0, 56)}${disp}${r.error ? ` — ${r.error.code}` : ''}`;
4416
+ return ` ${icon} ${r.status} · ${r.feature.slice(0, 56)}${disp}${r.error ? ` — ${r.error.code}${r.error.code === 'pod-job-failed' ? `: ${r.error.message}` : ''}` : ''}`;
4414
4417
  }),
4415
4418
  ].join('\n'));
4416
4419
  await reportRun(results);
@@ -4608,7 +4611,7 @@ selfCmd
4608
4611
  .action(async (space: string | undefined, opts: { stop?: boolean; memo?: string; run?: string; includeStale?: boolean; readWait?: string }) => {
4609
4612
  const { classifyRunScreenMissing, queryRunScreenKey, listRunLedgers } = await import('./self-implement/run-ledger.js');
4610
4613
  const { getPtyManifest, listPtyManifestRows } = await import('./pty-shell/pty-manifest.js');
4611
- const { readPodFragment, podFragmentFinished, dispatchPodSelfSend } = await import('./harness/self-send-target.js');
4614
+ const { readPodFragment, podFragmentFinished, listPodFragmentsForRun, dispatchPodSelfSend } = await import('./harness/self-send-target.js');
4612
4615
  const { listHarnessScreens, readHarnessHeartbeat } = await import('./harness/harness-screen.js');
4613
4616
  const decision = decideSelfSend({ space, opts, now: Date.now() }, {
4614
4617
  resolveRunScreen: (runId) => {
@@ -4616,6 +4619,7 @@ selfCmd
4616
4619
  return { ...resolved, missingStatus: resolved.screenKey ? undefined : classifyRunScreenMissing(resolved.lastEvent) };
4617
4620
  },
4618
4621
  resolveTarget: (target) => resolveSelfSendTarget(target, { getPtyManifest, listPtyManifestRows }),
4622
+ podFragmentsForRun: (runId) => listPodFragmentsForRun(runId),
4619
4623
  isPodFragment: (spaceId) => Boolean(readPodFragment(spaceId) || podFragmentFinished(spaceId)),
4620
4624
  screens: () => listHarnessScreens().map((screen): SelfSendCandidate => {
4621
4625
  const heartbeat = readHarnessHeartbeat(screen.spaceId);
@@ -5426,6 +5430,7 @@ program.command('ad [input...]')
5426
5430
  });
5427
5431
 
5428
5432
  registerDocsCommands(program);
5433
+ registerPrivateLedgerCommands(program);
5429
5434
  registerFleetCommands(program);
5430
5435
 
5431
5436
  // ── ops (운영 관측 — 지금 뭐 도나·이상 없나·상태 전이) ──
@@ -5592,6 +5597,7 @@ agentCmd
5592
5597
  .option('--backend <id>', `외부 에이전트 backend (${agentBackendHelpList}). 각 CLI auto-approve 모드로 PTY 구동. 미등록 지정 시 명시 에러(조용한 codex 폴백 없음·애그노스틱)`)
5593
5598
  .option('--chain <backends>', '한 미션의 backend 사슬 (예: codex,claude,elanous)')
5594
5599
  .option('--plugin <name@market>', '에이전트 화면 안에서 플러그인을 설치하고 그 스킬로 미션 실행 (codex/claude)')
5600
+ .addOption(new Option('--resources <mode>', '미션 전 자원 판단 사다리 (on|off, 기본 on)').choices(['on', 'off']).default('on'))
5595
5601
  .option('--headless', 'Claude 구독 CLI를 비대화형 stream-json 모드로 실행 (claude backend 전용)')
5596
5602
  .action(async (textParts: string[], opts: Record<string, any>) => {
5597
5603
  // 구독 모드 보장 — 브레인(streamLLM)·codex 둘 다 ChatGPT 구독으로 (API 과금 회피).
@@ -7815,6 +7821,16 @@ sessionCmd
7815
7821
  console.log(res.formatted ?? '(no context)');
7816
7822
  });
7817
7823
 
7824
+ async function runOnboardingForCli(): Promise<boolean> {
7825
+ try {
7826
+ await runOnboarding();
7827
+ return true;
7828
+ } catch (error) {
7829
+ if (handleOnboardingRefusal(error, 'other')) return false;
7830
+ throw error;
7831
+ }
7832
+ }
7833
+
7818
7834
  function announceChatToolsCompatibility(): void {
7819
7835
  process.stderr.write('`chat --tools` is a compatibility entrypoint; use `elanous agent` for tool-loop calls.\n');
7820
7836
  debug.log('chat.tools-compatibility', 'invoked', { toolLoopEnabled: true });
@@ -7841,7 +7857,7 @@ program
7841
7857
  const cfg = getUserConfig();
7842
7858
  if (needsOnboarding(cfg)) {
7843
7859
  ui.info('No config yet — launching setup wizard first.');
7844
- await runOnboarding();
7860
+ if (!await runOnboardingForCli()) return;
7845
7861
  }
7846
7862
  const refreshed = reloadUserConfig();
7847
7863
  // ★ 관측갭 수리(2026-07-21·제1원칙·트랙A) — self-implement 자식 goal-loop(`chat --goal-loop`)은 데몬과
@@ -7885,7 +7901,7 @@ program
7885
7901
  .option('--new', 'Force a new session at boot instead of resuming the active one')
7886
7902
  .option('--session <id>', 'Resume an explicit session (id or unique prefix)')
7887
7903
  .option('--scenario <path>', 'Run a YAML multi-turn scenario before handing back to interactive (or exit)')
7888
- .option('--replay <session-id>', 'Re-execute the user prompts from a previous session in a fresh REPL run (BACKLOG #5). User prompts are extracted in order and fed through the same dispatcher as --scenario; assistant/tool messages and attachments are dropped. Mutually exclusive with --scenario.')
7904
+ .option('--replay <session-id>', 'Re-execute the user prompts from a previous session in a fresh REPL run. User prompts are extracted in order and fed through the same dispatcher as --scenario; assistant/tool messages and attachments are dropped. Mutually exclusive with --scenario.')
7889
7905
  .option('--exit-after-scenario', 'Exit after the scenario / replay completes (default true when stdin is not a TTY)')
7890
7906
  .option('--no-exit-after-scenario', 'Stay in the interactive prompt after the scenario / replay completes (TTY default)')
7891
7907
  .option('--json', 'Emit one JSON line per turn (sessionId/provider/model/reply/...) instead of streaming text')
@@ -7908,7 +7924,7 @@ program
7908
7924
  const cfg = getUserConfig();
7909
7925
  if (needsOnboarding(cfg)) {
7910
7926
  ui.info('No config yet — launching setup wizard first.');
7911
- await runOnboarding();
7927
+ if (!await runOnboardingForCli()) return;
7912
7928
  }
7913
7929
  const refreshed = reloadUserConfig();
7914
7930
  const { runRepl } = await import('./repl/index.js');
@@ -8471,7 +8487,7 @@ program
8471
8487
  'ACP client for the running elanous daemon. Three modes: handshake-only (default), one-shot (--message), interactive REPL (--interactive). Local (unix socket) or remote (--host / --url) over Tailscale.',
8472
8488
  )
8473
8489
  .option('--socket <path>', 'Override the unix socket path (default: ~/.elanous/elanous.sock)')
8474
- .option('--host <hostport>', 'Remote daemon host:port (e.g. mbp.tailnet:31415). Coerced to ws://<hostport>/v1/acp.')
8490
+ .option('--host <hostport>', 'Remote daemon host:port (e.g. laptop.tailnet:31415). Coerced to ws://<hostport>/v1/acp.')
8475
8491
  .option('--url <wsurl>', 'Remote daemon WS URL (e.g. ws://host:31415/v1/acp). Overrides --host.')
8476
8492
  .option('-r, --remote [name]', 'Bookmark name (`-r` alone = default). Fills host/token-file; explicit --url/--host/--token/--token-file win.')
8477
8493
  .option('--token <token>', 'Bearer token for remote auth. Overrides --token-file and ELANOUS_TOKEN.')
@@ -8945,6 +8961,7 @@ const nexusRunCmd = nexusCmd
8945
8961
  .option('--watch', 'Static-mode: fs.watch loop inside daemon — source edits auto-rebuild apps/pwa. Default on for `nexus run` (no args), off for `--test`.')
8946
8962
  .option('--no-watch', 'Disable the static-mode fs.watch loop (canonical entry default-on).')
8947
8963
  .option('--no-mcp', 'Skip the entire MCP-client boot wire (every `mcp.servers[]` in user-config). Use when a misbehaving server (e.g. xcrun mcpbridge tools/list hang) drags every startup through its 8s timeout.')
8964
+ .option('--no-auth', 'Explicitly disable Nexus HTTP and ACP websocket bearer authentication (unsafe).')
8948
8965
  .option('--dispatch', '§5-③ — enable the autonomous idle-continuation scheduler for this run (drives an active auto-mode goal on idle). Same as user-config `dispatch.enabled`. Default off — ignites self-firing turns, so opt-in.')
8949
8966
  .option('--force', 'Take the lock even if another nexus instance is recorded as running.')
8950
8967
  .option('--status', 'Alias for `elanous nexus status` (or test-mode status when combined with --test).')
@@ -8980,6 +8997,7 @@ const nexusRunCmd = nexusCmd
8980
8997
  autoRestart?: boolean;
8981
8998
  watch?: boolean;
8982
8999
  mcp?: boolean;
9000
+ auth?: boolean;
8983
9001
  fresh?: boolean;
8984
9002
  force?: boolean;
8985
9003
  dispatch?: boolean;
@@ -9129,6 +9147,7 @@ const nexusRunCmd = nexusCmd
9129
9147
  preflightStaleServe: true,
9130
9148
  verifyListen: true,
9131
9149
  ...(opts.mcp === false ? { mcpEnabled: false } : {}),
9150
+ ...(opts.auth === false ? { noAuth: true } : {}),
9132
9151
  ...(opts.https ? { https: true } : {}),
9133
9152
  ...(resolvedPort !== undefined ? { httpPort: resolvedPort } : {}),
9134
9153
  ...(opts.force ? { force: true } : {}),
@@ -9156,6 +9175,7 @@ const nexusRunCmd = nexusCmd
9156
9175
  // it — service-manager production posture stays watch-off.
9157
9176
  ...((bgChild ? opts.watch === true : watchOn) ? { pwaWatch: true } : {}),
9158
9177
  ...(opts.mcp === false ? { mcpEnabled: false } : {}),
9178
+ ...(opts.auth === false ? { noAuth: true } : {}),
9159
9179
  ...(opts.force ? { force: true } : {}),
9160
9180
  ...(opts.dispatch ? { dispatch: true } : {}),
9161
9181
  ...(opts.toolCwd ? { toolCwd: opts.toolCwd } : {}),
@@ -9187,6 +9207,7 @@ const nexusRunCmd = nexusCmd
9187
9207
  ...(opts.rebuild ? { rebuild: true } : {}),
9188
9208
  ...(watchOnTty ? { watch: true } : {}),
9189
9209
  ...(opts.mcp === false ? { mcpEnabled: false } : {}),
9210
+ ...(opts.auth === false ? { noAuth: true } : {}),
9190
9211
  });
9191
9212
  process.exit(result.exitCode);
9192
9213
  });
@@ -9892,6 +9913,22 @@ nexusCmd
9892
9913
  }
9893
9914
  });
9894
9915
 
9916
+ // 휴대폰 앱 온보딩 — 링크 하나(⊕ QR)로 이 Mac 의 넥서스에 붙인다(대표 2026-09-30 · 아이폰 첫 화면 단순화).
9917
+ program
9918
+ .command('phone')
9919
+ .description('Connect the phone app to this Mac')
9920
+ .command('link')
9921
+ .description('Print a one-tap connect link (and a QR code) for the Elanous phone app')
9922
+ .option('--simulator', 'Use the loopback address (the simulator on this Mac) instead of the tailnet address — `--local` is taken by the global remote flag')
9923
+ .option('--android [serial]', 'Hand the link straight to a USB-connected Android phone (adb reverse + am start · the token is never printed)')
9924
+ .option('--temp', 'Use a short-lived token instead of the owner token')
9925
+ .option('--ttl <duration>', 'Lifetime for --temp (max 24h)', '24h')
9926
+ .option('--no-qr', 'Do not print a QR code')
9927
+ .action(async (opts: { simulator?: boolean; android?: boolean | string; temp?: boolean; ttl: string; qr: boolean }) => {
9928
+ const { runPhoneLink } = await import('./cli/phone-link.js');
9929
+ process.exitCode = await runPhoneLink({ local: opts.simulator, android: opts.android, temp: opts.temp, ttl: opts.ttl, qr: opts.qr });
9930
+ });
9931
+
9895
9932
  // ── ACP — Agent Client Protocol (claude-code, codex, ...) ──
9896
9933
  const tokenCmd = program
9897
9934
  .command('token')
@@ -10560,7 +10597,7 @@ async function main(): Promise<void> {
10560
10597
  // gates on config completeness — no entry-mode branch.)
10561
10598
  const cfg = getUserConfig();
10562
10599
  if (needsOnboarding(cfg)) {
10563
- await runOnboarding();
10600
+ if (!await runOnboardingForCli()) return;
10564
10601
  }
10565
10602
 
10566
10603
  // Active-provider banner so users know WHICH model/auth is about to
@@ -32,6 +32,12 @@ export function prodInstanceRoot(): string {
32
32
  return normRoot(join(homedir(), '.elanous'));
33
33
  }
34
34
 
35
+ /** Release records are machine-wide except in an explicitly selected config universe. */
36
+ export function releaseLedgerRoot(): string {
37
+ const { getElanousConfigDirOverride } = require('../elanous-config-dir.js') as typeof import('../elanous-config-dir.js');
38
+ return getElanousConfigDirOverride() ? effectiveInstanceRoot() : prodInstanceRoot();
39
+ }
40
+
35
41
  export function resolveInstance(deps: ResolveDeps = {}): InstanceResolution {
36
42
  const prodRoot = normRoot(deps.prodRoot ?? prodInstanceRoot());
37
43
  if (deps.explicitFlagRoot?.trim()) {
@@ -27,6 +27,11 @@ export function readSavedCursor(root: string): number {
27
27
  try { return Number(JSON.parse(readFileSync(cursorFile(root), 'utf8')).lastId) || 0; } catch { return 0; }
28
28
  }
29
29
 
30
+ /** When the cursor last moved (= a run last found a new saved message). Undefined before the first message. */
31
+ export function readSavedCursorAt(root: string): string | undefined {
32
+ try { const at = JSON.parse(readFileSync(cursorFile(root), 'utf8')).at; return typeof at === 'string' ? at : undefined; } catch { return undefined; }
33
+ }
34
+
30
35
  function writeSavedCursor(root: string, lastId: number): void {
31
36
  mkdirSync(intakeLedgerDir(root), { recursive: true });
32
37
  writeFileSync(cursorFile(root), JSON.stringify({ lastId, at: new Date().toISOString() }) + '\n');
@@ -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,