elanous 0.2.12 → 0.2.13

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 (289) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1394-ee3795b112907122.js → 1394-931c9827067b2276.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{7758-0b018a9e4fc56200.js → 7758-85c2b53f731da024.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/{8605-0259994816604c49.js → 8605-9da4678f8a310d18.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-cb169620945d1952.js → page-e5b4e1b256f41076.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-cefe8169e36cf62a.js → page-e49b9b76c565a0b1.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-813c4a52c9d05ca8.js → page-4c1dbb21861beab2.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-4731f2c215358937.js → page-c85b816e3b269dbb.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/{page-a022b257950f39e6.js → page-f6bc66b919b2ea7d.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-a2ffc717e04b8b0a.js → page-60603c1e056c9b25.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b59ee4e70fc18fcf.js → page-a0ea8f5a2341ee6b.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/field/{page-c172dabd796eb7a3.js → page-36c6c4183353ac9e.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/inside/{page-1efd0cb9e0ed9d13.js → page-cb2eb0fd809980da.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/intake/{page-d5dc7ff69a37b4cf.js → page-31012264da8a341f.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/market/{page-dfca9b7d2de0b135.js → page-0696ee3fac9fe56c.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-66c27608f651b866.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/morning/{page-6dd3bfbbde2a5c2c.js → page-6ddd2109b7fc3a80.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-77447356baad75d2.js → page-7a2bf830038a2264.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-6d604227e1664b71.js → page-f672536f14f0969f.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-4048543b4b7ad5e0.js → page-e7a6c80f288b1948.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4c3a3126abed551a.js → page-29a709616df9b050.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-64b0aecb1ca75e75.js → page-fee842d7c2151129.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/{page-9547b5cec7cf76bf.js → page-d92e32d51ab5157e.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-2cddeb9e2aeedc55.js → page-a1833cd43d832601.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-63d14a4d8fe1e1c1.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/today/{page-baba2e047872fb0f.js → page-2a511d320f762e06.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/trace/{page-e895da4ec9cac345.js → page-1c3e03f88dac1e21.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2e9e2fb01c9da1c9.js → page-2b256feed331546f.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-d62ceb13411ebba4.js → page-505bb96d69a8622c.js} +1 -1
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +13 -13
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +13 -13
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +13 -13
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +13 -13
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +13 -13
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +13 -13
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +13 -13
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +13 -13
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +13 -13
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +13 -13
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +13 -13
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +13 -13
  88. package/apps/pwa/out/market/index.html +1 -1
  89. package/apps/pwa/out/market/index.txt +13 -13
  90. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  91. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  92. package/apps/pwa/out/morning/index.html +1 -1
  93. package/apps/pwa/out/morning/index.txt +13 -13
  94. package/apps/pwa/out/observatory/index.html +1 -1
  95. package/apps/pwa/out/observatory/index.txt +13 -13
  96. package/apps/pwa/out/ops/checklist/index.html +1 -1
  97. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  98. package/apps/pwa/out/ops/release/index.html +1 -1
  99. package/apps/pwa/out/ops/release/index.txt +13 -13
  100. package/apps/pwa/out/ops/seats/index.html +1 -1
  101. package/apps/pwa/out/ops/seats/index.txt +13 -13
  102. package/apps/pwa/out/outputs/index.html +1 -1
  103. package/apps/pwa/out/outputs/index.txt +13 -13
  104. package/apps/pwa/out/reflection/index.html +1 -1
  105. package/apps/pwa/out/reflection/index.txt +14 -14
  106. package/apps/pwa/out/scheduler/index.html +1 -1
  107. package/apps/pwa/out/scheduler/index.txt +13 -13
  108. package/apps/pwa/out/sessions/index.html +1 -1
  109. package/apps/pwa/out/sessions/index.txt +13 -13
  110. package/apps/pwa/out/settings/devices/index.html +1 -1
  111. package/apps/pwa/out/settings/devices/index.txt +13 -13
  112. package/apps/pwa/out/settings/index.html +1 -1
  113. package/apps/pwa/out/settings/index.txt +13 -13
  114. package/apps/pwa/out/setup/done/index.html +1 -1
  115. package/apps/pwa/out/setup/done/index.txt +13 -13
  116. package/apps/pwa/out/setup/index.html +1 -1
  117. package/apps/pwa/out/setup/index.txt +13 -13
  118. package/apps/pwa/out/share/index.html +1 -1
  119. package/apps/pwa/out/share/index.txt +13 -13
  120. package/apps/pwa/out/showroom/index.html +1 -1
  121. package/apps/pwa/out/showroom/index.txt +13 -13
  122. package/apps/pwa/out/tasks/index.html +1 -1
  123. package/apps/pwa/out/tasks/index.txt +13 -13
  124. package/apps/pwa/out/term/index.html +1 -1
  125. package/apps/pwa/out/term/index.txt +13 -13
  126. package/apps/pwa/out/today/index.html +1 -1
  127. package/apps/pwa/out/today/index.txt +13 -13
  128. package/apps/pwa/out/trace/index.html +1 -1
  129. package/apps/pwa/out/trace/index.txt +13 -13
  130. package/apps/pwa/out/vault/index.html +1 -1
  131. package/apps/pwa/out/vault/index.txt +13 -13
  132. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  133. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  134. package/apps/pwa/out/workflows/index.html +1 -1
  135. package/apps/pwa/out/workflows/index.txt +13 -13
  136. package/apps/pwa/out/workspace/index.html +1 -1
  137. package/apps/pwa/out/workspace/index.txt +13 -13
  138. package/apps/pwa/out/worktrees/index.html +1 -1
  139. package/apps/pwa/out/worktrees/index.txt +13 -13
  140. package/examples/teams/personas/backend.yaml +4 -0
  141. package/examples/teams/personas/pm.yaml +4 -0
  142. package/examples/teams/personas/qa.yaml +4 -0
  143. package/examples/teams/small-product.yaml +20 -0
  144. package/graphs/lecture/lecture-note.yaml +28 -0
  145. package/graphs/lecture/recipes.yaml +17 -0
  146. package/package.json +2 -1
  147. package/scripts/context-hooks/emit.ts +36 -14
  148. package/scripts/coord-channel-watch.sh +1 -0
  149. package/scripts/discord-seat-probe.ts +144 -0
  150. package/scripts/hq/hq-standby.sh +65 -0
  151. package/scripts/hq/standby-promote.ts +173 -0
  152. package/scripts/hq/standby-role.ts +59 -0
  153. package/scripts/hq/standby-snapshot.ts +226 -0
  154. package/scripts/hq/standby-verify.ts +219 -0
  155. package/scripts/lecture-note/node.ts +256 -0
  156. package/scripts/release-loop/gate-node.ts +53 -15
  157. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  158. package/scripts/test-deterministic.ts +53 -3
  159. package/skills/omni-crawl/SKILL.md +16 -16
  160. package/skills/omni-crawl/package-lock.json +1 -544
  161. package/skills/omni-crawl/package.json +3 -4
  162. package/skills/omni-crawl/scripts/main.ts +13 -13
  163. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  164. package/skills/omni-crawl/src/apify.ts +13 -2
  165. package/skills/omni-crawl/src/capture.ts +6 -0
  166. package/skills/omni-crawl/src/registry.ts +2 -2
  167. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  168. package/skills/omni-digest/SKILL.md +10 -11
  169. package/skills/omni-digest/package-lock.json +0 -545
  170. package/skills/omni-digest/package.json +2 -5
  171. package/skills/omni-digest/scripts/main.ts +3 -3
  172. package/skills/omni-digest/src/delegate.ts +5 -5
  173. package/skills/omni-digest/src/fetch.ts +2 -2
  174. package/skills/omni-digest/src/media.ts +25 -11
  175. package/skills/omni-digest/src/visual.ts +3 -2
  176. package/skills/omni-market/scripts/main.ts +1 -1
  177. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  178. package/src/agent/identity-env.ts +11 -0
  179. package/src/agent-mission/driver.ts +12 -7
  180. package/src/agent-mission/duo.ts +94 -0
  181. package/src/agent-mission/pty-decision.ts +1 -1
  182. package/src/autopilot/track-agent-shadow.ts +2 -1
  183. package/src/autopilot/track-agent.ts +4 -1
  184. package/src/boot/daemon-prompt-request.ts +3 -0
  185. package/src/boot/daemon-prompt-turn.ts +6 -2
  186. package/src/boot/daemon-runtime.ts +48 -16
  187. package/src/cli/decisions-cli.ts +51 -19
  188. package/src/cli/outputs-cli.ts +92 -0
  189. package/src/cli/persona-cli.ts +29 -0
  190. package/src/cli/pod-cli.ts +10 -3
  191. package/src/cli/pr-cli.ts +15 -1
  192. package/src/cli/release-cli.ts +108 -28
  193. package/src/context-bus/condense.ts +194 -0
  194. package/src/context-bus/context-day.ts +82 -0
  195. package/src/context-bus/context-now-surfaces.ts +15 -2
  196. package/src/context-bus/long-term-memory.ts +118 -22
  197. package/src/context-bus/nightly-condense.ts +1 -1
  198. package/src/decisions/decision-cards.ts +13 -7
  199. package/src/decisions/decision-ledger.ts +43 -17
  200. package/src/graph-kinds/registry.ts +2 -2
  201. package/src/graph-runner/graph-approval-card.ts +2 -2
  202. package/src/graph-runner/graph-grow.ts +64 -11
  203. package/src/graph-runner/graph-tick.ts +9 -2
  204. package/src/graph-runner/runner.ts +81 -17
  205. package/src/harness/harness-cli-command.ts +27 -1
  206. package/src/harness/harness-queue-child.ts +37 -0
  207. package/src/harness/harness-queue.ts +306 -0
  208. package/src/hooks/github-shadow.ts +72 -2
  209. package/src/hooks/receiver.ts +4 -2
  210. package/src/hq/hq.ts +318 -0
  211. package/src/hq/lease.ts +210 -0
  212. package/src/hq/ledger-write-target.ts +41 -0
  213. package/src/index.ts +199 -7
  214. package/src/intake-plane/discord-seat-work.ts +13 -6
  215. package/src/intake-plane/linear-wish.ts +44 -0
  216. package/src/intake-plane/persona-answer.ts +5 -0
  217. package/src/intake-plane/telegram-seat-work.ts +13 -6
  218. package/src/intake-plane/wish-card.ts +1 -1
  219. package/src/loops/checker.ts +178 -0
  220. package/src/loops/loop-cli.ts +37 -3
  221. package/src/loops/observe.ts +49 -0
  222. package/src/loops/orchestrator/overlap.ts +141 -0
  223. package/src/loops/orchestrator/traffic.ts +150 -0
  224. package/src/loops/package/verify.ts +125 -0
  225. package/src/loops/verdict.ts +22 -0
  226. package/src/maturity/feature-maturity.ts +3 -3
  227. package/src/memory.ts +37 -4
  228. package/src/nexus/api/card-followup-route.ts +118 -0
  229. package/src/nexus/api/decisions-route.ts +68 -0
  230. package/src/nexus/api/http-server.ts +47 -0
  231. package/src/nexus/api/personas.ts +2 -1
  232. package/src/nexus/api/skill-exec-route.ts +97 -0
  233. package/src/persona/loader.ts +18 -0
  234. package/src/persona/persona-todo.ts +14 -9
  235. package/src/persona/presets.ts +2 -1
  236. package/src/persona/team.ts +105 -0
  237. package/src/persona/types.ts +4 -0
  238. package/src/pod-lease/host-lease.ts +130 -20
  239. package/src/release-loop/landed-but-yellow.ts +12 -0
  240. package/src/seat-dispatch/seat-ask.ts +57 -12
  241. package/src/seat-loop/seat-loop.ts +90 -16
  242. package/src/seat-loop/seat-pack.ts +127 -0
  243. package/src/self-dev/ask-launch-flow.ts +3 -0
  244. package/src/self-implement/l8-merge-queue.ts +157 -1
  245. package/src/steward/launch.ts +5 -1
  246. package/src/steward/readiness.ts +125 -0
  247. package/src/steward/rescue.ts +75 -0
  248. package/src/steward/triage.ts +48 -9
  249. package/src/task-cards/card-cli.ts +78 -0
  250. package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
  251. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  252. package/src/task-orchestrator/surfaces/self-implement-pod.ts +38 -8
  253. package/src/telegram-dispatch-forward.ts +1 -1
  254. package/src/telegram-fabric-plan.ts +90 -0
  255. package/src/telegram-project-command.ts +10 -4
  256. package/src/telegram.ts +5 -0
  257. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  258. package/src/user-config.ts +94 -10
  259. package/src/version/packed-revision.json +1 -1
  260. package/src/workflow-runtime/executor.ts +4 -0
  261. package/src/workflow-runtime/node-catalog.ts +13 -0
  262. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  263. package/src/workflow-runtime/schema.ts +17 -1
  264. package/src/workflow-runtime/types.ts +13 -1
  265. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  288. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
  289. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
@@ -58,6 +58,9 @@ import { envLiteral } from '../../platform/env-literal.js';
58
58
  import { LLM_TIER_MAP_BY_PROVIDER, lookupLlmTierSpec, type LlmTierProvider } from '../../model-tier/llm-tier-map.js';
59
59
  import { parseSelfImplementJson, type SelfImplementJobDone, type SelfImplementJobSpawn } from './self-implement.js';
60
60
  import { codexQuotaPolicyFromConfig } from '../../oauth/codex-account-store.js';
61
+ import { goalTypeOf } from '../../../scripts/measure-pod-memory-by-goal.js';
62
+ import { readPodMemoryAdvice, type PodMemoryAdvice } from '../../cli/pod-memory-advice.js';
63
+ import { getUserConfig } from '../../user-config.js';
61
64
 
62
65
  export type Kubectl = (args: readonly string[], input?: string) => { status: number | null; stdout: string; stderr: string };
63
66
 
@@ -132,6 +135,10 @@ export interface PodSpawnOptions {
132
135
  env?: NodeJS.ProcessEnv;
133
136
  /** Detail switch file (test injection; defaults to the host's local/production selection). */
134
137
  liveDetailFile?: string;
138
+ /** Test seam for the opt-in user configuration; otherwise read the active user config. */
139
+ adviseDefaults?: boolean;
140
+ /** Test seam for the read-only measured advice. */
141
+ memoryAdvice?: () => PodMemoryAdvice;
135
142
  }
136
143
 
137
144
  export function defaultKubectl(args: readonly string[], input?: string): { status: number | null; stdout: string; stderr: string } {
@@ -411,7 +418,7 @@ export const POD_MEMORY_HIGH_DEFAULT = '32Gi';
411
418
  export const POD_MEMORY_LITE_DEFAULT = '2Gi';
412
419
  export const POD_MEMORY_TIERS = ['lite', 'standard', 'high'] as const;
413
420
  export type PodMemoryTier = (typeof POD_MEMORY_TIERS)[number];
414
- export type PodMemorySource = 'option' | 'goal-line' | 'pwa-auto' | 'default' | 'parent-goal-line' | 'parent-pwa-auto';
421
+ export type PodMemorySource = 'option' | 'goal-line' | 'pwa-auto' | 'default' | 'parent-goal-line' | 'parent-pwa-auto' | 'advise';
415
422
  const PWA_PATH = /(?:^|[\s`'"(,·])apps\/pwa\//m;
416
423
  const GOAL_LINE = /^[ \t]*(?:Pod 메모리|pod-memory)[ \t]*:[ \t]*(lite|standard|high)[ \t]*\r?$/im;
417
424
 
@@ -453,14 +460,21 @@ function memoryGi(v: string): number | null {
453
460
  return m[2] === 'Gi' ? Number(m[1]) : Number(m[1]) / 1024;
454
461
  }
455
462
 
456
- export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Readonly<Record<string, string | undefined>>, parentGoal?: string): { limit: string; tier: PodMemoryTier; source: PodMemorySource } {
463
+ export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Readonly<Record<string, string | undefined>>, parentGoal?: string, advice?: PodMemoryAdvice): { limit: string; tier: PodMemoryTier; source: PodMemorySource } {
457
464
  const option = parsePodMemoryTier(env.ELANOUS_POD_MEMORY_TIER);
458
465
  const line = parsePodMemoryTier(GOAL_LINE.exec(feature)?.[1]);
459
- const parentLine = !option && !line && !goalTouchesPwa(feature) ? parsePodMemoryTier(GOAL_LINE.exec(parentGoal ?? '')?.[1]) : null;
466
+ const parentLine = !option && !line ? parsePodMemoryTier(GOAL_LINE.exec(parentGoal ?? '')?.[1]) : null;
467
+ const goalType = advice && !option && !line && !parentLine
468
+ ? (broadPodTestWarning(feature) ? 'test' : goalTypeOf(feature))
469
+ ?? (parentGoal ? (broadPodTestWarning(parentGoal) ? 'test' : goalTypeOf(parentGoal)) : null)
470
+ : null;
471
+ const recommendation = advice?.byGoalType.find((entry) => entry.goalType === goalType && entry.evidence.runs > 0)?.recommended;
460
472
  const [tier, source]: [PodMemoryTier, PodMemorySource] = option ? [option, 'option']
461
473
  : line ? [line, 'goal-line']
474
+ // 조각 자신의 PWA 경로가 부모 줄·실측 권고보다 먼저다 — 부모 «standard» 가 PWA 조각을 16Gi 로 내리면 OOM(POD7 · #23569 순서 회귀)
462
475
  : goalTouchesPwa(feature) ? ['high', 'pwa-auto']
463
476
  : parentLine ? [parentLine, 'parent-goal-line']
477
+ : recommendation ? [recommendation, 'advise']
464
478
  : parentGoal && goalTouchesPwa(parentGoal) ? ['high', 'parent-pwa-auto']
465
479
  : ['standard', 'default'];
466
480
  if (tier === 'lite') return { limit: env.ELANOUS_POD_MEMORY_LITE?.trim() || POD_MEMORY_LITE_DEFAULT, tier, source };
@@ -640,7 +654,6 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
640
654
  const hostSupervised = options.hostSupervised ?? true;
641
655
  return (input) => {
642
656
  const name = podJobName(input.spaceId);
643
- const featureMemory = podMemoryLimitFor(input.feature, env);
644
657
  let parentGoal: string | undefined;
645
658
  let parentGoalUnreadable = false;
646
659
  let inheritedFrom: string | undefined;
@@ -658,7 +671,14 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
658
671
  inheritedFrom = withinRoot;
659
672
  } catch { parentGoalUnreadable = true; }
660
673
  }
661
- const { limit: selectedMemoryLimit, tier: memoryTier, source: memorySource } = parentGoal === undefined ? featureMemory : podMemoryLimitFor(input.feature, env, parentGoal);
674
+ let advice: PodMemoryAdvice | undefined;
675
+ const explicitMemory = Boolean(parsePodMemoryTier(env.ELANOUS_POD_MEMORY_TIER) || GOAL_LINE.test(input.feature) || GOAL_LINE.test(parentGoal ?? ''));
676
+ const useAdvice = options.adviseDefaults ?? getUserConfig().pod?.memory?.adviseDefaults === true;
677
+ if (useAdvice && !explicitMemory) {
678
+ try { advice = (options.memoryAdvice ?? readPodMemoryAdvice)(); }
679
+ catch (error) { debug.log('self-implement.pod', 'memory-advice-unavailable', { spaceId: input.spaceId, reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' }); }
680
+ }
681
+ const { limit: selectedMemoryLimit, tier: memoryTier, source: memorySource } = podMemoryLimitFor(input.feature, env, parentGoal, advice);
662
682
  for (const [where, text] of [['feature', input.feature], ['parent-goal', parentGoal]] as const) {
663
683
  if (text && text.split(/\r?\n/u).some((line) => /Pod 메모리/u.test(line) && !GOAL_LINE.test(line))) {
664
684
  const reason = '한 줄 단독 `Pod 메모리: high|standard` 형식이 아님';
@@ -716,7 +736,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
716
736
  if (recoveryError) return { exitCode: 1, output: '', error: { code: 'pod-oom-recovery', message: recoveryError } };
717
737
  // Each Job attempt has its own host lease; releasing the first after OOM lets the retry
718
738
  // compete with other processes for the newly measured capacity before applying another Job.
719
- let releaseAdmission: ((() => void) & { applied?: () => void }) | undefined;
739
+ let releaseAdmission: ((() => void) & { applied?: (job: string, context: string, namespace: string) => void; observed?: () => void }) | undefined;
720
740
  try {
721
741
  // Every attempt, including an OOM retry, must acquire a pool slot before applying a Job.
722
742
  const acquireSlot = async (): Promise<PodPoolMember | null> => {
@@ -1067,6 +1087,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1067
1087
  const job = podJobManifest({ name, namespace, image: jobImage, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), args, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}), execution: { key: executionKey, attempt: oomRetried ? 2 : 1 }, hostLeaseAdmitted: !!releaseAdmission }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
1068
1088
  const a = kubectl(['apply', '-f', '-'], JSON.stringify(job));
1069
1089
  if (a.status !== 0) { cleanupSecret(); return { exitCode: 1, output: a.stderr, error: { code: 'pod-apply', message: a.stderr.trim() } }; }
1090
+ releaseAdmission?.applied?.(name, context, namespace);
1070
1091
  if (options.source?.kind === 'bundle') {
1071
1092
  const podName = await waitForRunningPod(kubectl, namespace, name, sleep);
1072
1093
  if (!podName) {
@@ -1074,6 +1095,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1074
1095
  cleanupSecret();
1075
1096
  return { exitCode: 1, output: '', error: { code: 'pod-source', message: `Pod for ${name} did not become Running` } };
1076
1097
  }
1098
+ releaseAdmission?.observed?.();
1099
+ if (releaseAdmission) delete releaseAdmission.observed;
1077
1100
  const copied = kubectl(['cp', options.source.bundlePath, `${podName}:/tmp/source.bundle`, '-c', 'child', '-n', namespace]);
1078
1101
  if (copied.status !== 0) {
1079
1102
  debug.log('self-implement.pod', 'source-mismatch', { job: name, kind: 'bundle', headCommit: options.source.headCommit });
@@ -1106,7 +1129,6 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1106
1129
  }
1107
1130
  writePodFragment({ spaceId: input.spaceId, context, namespace, job: name, inboxDir: POD_CONTROL_INBOX_DIR, ...(liveChildRunId === normalizeRunId(liveChildRunId) ? { runId: liveChildRunId } : {}), ...(parentRunId && normalizeRunId(parentRunId) === parentRunId ? { parentRunId } : {}) }, env);
1108
1131
  recorded = true;
1109
- releaseAdmission?.applied?.();
1110
1132
  debug.log('self-implement.pod', 'job-applied', { job: name, namespace, ...(member ? { context: member.context } : {}), image: jobImage, spaceId: input.spaceId, passEnv, extraArgs: options.extraArgs ?? [], ...(options.armEnv?.ELANOUS_ARM_ID ? { armId: options.armEnv.ELANOUS_ARM_ID } : {}) });
1111
1133
  let state: 'complete' | 'failed' | 'aborted' = 'failed';
1112
1134
  let failedReason = '';
@@ -1122,8 +1144,9 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1122
1144
  }
1123
1145
  const g = kubectl(['-n', namespace, 'get', 'job', name, '-o', 'jsonpath={.status.conditions[*].type}']);
1124
1146
  const types = g.stdout;
1125
- if (/Complete|SuccessCriteriaMet/.test(types)) { state = 'complete'; break; }
1147
+ if (/Complete|SuccessCriteriaMet/.test(types)) { releaseAdmission?.observed?.(); state = 'complete'; break; }
1126
1148
  if (/Failed|FailureTarget/.test(types)) {
1149
+ releaseAdmission?.observed?.();
1127
1150
  state = 'failed';
1128
1151
  const reason = kubectl(['-n', namespace, 'get', 'job', name, '-o', 'jsonpath={.status.conditions[?(@.type=="Failed")].reason}']);
1129
1152
  failedReason = reason.status === 0 ? reason.stdout.trim() : '';
@@ -1143,6 +1166,13 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
1143
1166
  debug.log('self-implement.pod', 'container-terminated', { job: name, container: 'child', reason: containerReason, exitCode: containerExitCode, jobReason: failedReason || null, memoryLimit });
1144
1167
  break;
1145
1168
  }
1169
+ if (releaseAdmission?.observed) {
1170
+ const podPhase = kubectl(['-n', namespace, 'get', 'pods', '-l', `job-name=${name}`, '-o', 'jsonpath={.items[*].status.phase}']);
1171
+ if (podPhase.status === 0 && /\b(?:Running|Succeeded|Failed)\b/.test(podPhase.stdout)) {
1172
+ releaseAdmission.observed();
1173
+ delete releaseAdmission.observed;
1174
+ }
1175
+ }
1146
1176
  if (ghTokenExpiresAt !== null && ghTokenExpiresAt - now() <= 10 * 60_000) {
1147
1177
  try {
1148
1178
  const fresh = await issueVerifiedAppCredential();
@@ -42,7 +42,7 @@ export function defaultBaseUrl(deps: { runtime?: () => ReturnType<typeof readNex
42
42
  return `http://${host}:${rt.httpPort}`;
43
43
  }
44
44
 
45
- function defaultToken(): string | null {
45
+ export function defaultToken(): string | null {
46
46
  const p = join(getElanousConfigDir(), 'acp-token');
47
47
  try { return existsSync(p) ? readFileSync(p, 'utf-8').trim() || null : null; } catch { return null; }
48
48
  }
@@ -0,0 +1,90 @@
1
+ import { defaultBaseUrl, defaultToken } from './telegram-dispatch-forward.js';
2
+ import type { FabricPlan } from './self-dev/fabric-plan-core.js';
3
+ import type { TelegramBot, TgIncoming } from './telegram.js';
4
+
5
+ export interface TelegramFabricPlanDeps {
6
+ fetchImpl?: typeof fetch;
7
+ baseUrl?: () => string | null;
8
+ token?: () => string | null;
9
+ }
10
+
11
+ /** Only a successful daemon draft is offered for approval; no execution endpoint is called here. */
12
+ export function attachTelegramFabricPlan(
13
+ bot: Pick<TelegramBot, 'isOwnerAllowed' | 'sendMessage' | 'sendInlineKeyboard' | 'onCallbackQuery' | 'answerCallbackQuery'>,
14
+ deps: TelegramFabricPlanDeps = {},
15
+ ): (ctx: TgIncoming) => Promise<boolean> {
16
+ const fetchImpl = deps.fetchImpl ?? fetch;
17
+ const pending = new Map<string, { chatId: number; userId: number; threadId?: number; messageId: number }>();
18
+ const endpoint = (path: string): { url: string; token: string | null } => {
19
+ const base = (deps.baseUrl ?? defaultBaseUrl)();
20
+ if (!base) throw new Error('데몬에 연결할 수 없습니다');
21
+ return { url: `${base}${path}`, token: (deps.token ?? defaultToken)() };
22
+ };
23
+ const post = async (path: string, body?: unknown): Promise<unknown> => {
24
+ const { url, token } = endpoint(path);
25
+ const response = await fetchImpl(url, {
26
+ method: 'POST',
27
+ headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
28
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
29
+ });
30
+ if (!response.ok) throw new Error(`데몬 응답 ${response.status}`);
31
+ return response.json();
32
+ };
33
+
34
+ bot.onCallbackQuery(async (q) => {
35
+ const match = /^fabric:(approve|edit):([a-f0-9-]{36})$/i.exec(q.data);
36
+ if (!match) return;
37
+ const planId = match[2]!;
38
+ const original = pending.get(planId);
39
+ if (!original || !bot.isOwnerAllowed(q.userId) || q.userId !== original.userId
40
+ || q.chatId !== original.chatId || q.threadId !== original.threadId || q.messageId !== original.messageId) {
41
+ await bot.answerCallbackQuery(q.id, { text: '이 초안의 요청자만 선택할 수 있습니다' });
42
+ return;
43
+ }
44
+ if (match[1] === 'edit') {
45
+ pending.delete(planId);
46
+ await bot.answerCallbackQuery(q.id);
47
+ await bot.sendMessage(original.chatId, '고칠 내용을 반영해 «아크: <복합 문장>»으로 다시 보내세요. 이 초안은 승인되지 않았습니다.', { threadId: original.threadId });
48
+ return;
49
+ }
50
+ try {
51
+ await post(`/v1/fabric/plans/${planId}/approve`);
52
+ pending.delete(planId);
53
+ await bot.answerCallbackQuery(q.id, { text: '승인되었습니다' });
54
+ await bot.sendMessage(original.chatId, `아크 초안 ${planId} 승인됨 — 실행은 별도 단계입니다.`, { threadId: original.threadId });
55
+ } catch (error) {
56
+ await bot.answerCallbackQuery(q.id, { text: `승인 실패: ${error instanceof Error ? error.message : String(error)}` });
57
+ }
58
+ });
59
+
60
+ return async (ctx) => {
61
+ if (ctx.attachments.length || ctx.video || !ctx.text.startsWith('아크:')) return false;
62
+ const request = ctx.text.slice('아크:'.length).trim();
63
+ if (!request || request.includes('\n')) {
64
+ await bot.sendMessage(ctx.chatId, '«아크: <복합 문장>» 한 줄로 보내세요.', { replyTo: ctx.messageId, threadId: ctx.threadId });
65
+ return true;
66
+ }
67
+ try {
68
+ const body = await post('/v1/fabric/decompose', { request }) as { plan?: FabricPlan };
69
+ const plan = body.plan;
70
+ if (!plan || plan.status !== 'draft' || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(plan.id)
71
+ || !Array.isArray(plan.nodes) || plan.nodes.length === 0) throw new Error('초안 형식 오류');
72
+ const steps = plan.nodes.map((node, i) => `${i + 1}. ${node.title}`).join('\n');
73
+ const preview = `아크 초안 · 실행 전 검토\n${steps}`;
74
+ const reply = { replyTo: ctx.messageId, threadId: ctx.threadId };
75
+ for (let offset = 0; offset + 4000 < preview.length; offset += 4000) {
76
+ const sent = await bot.sendMessage(ctx.chatId, preview.slice(offset, offset + 4000), reply);
77
+ if (sent?.messageId === undefined) throw new Error('단계 미리보기를 전부 전달하지 못했습니다 — 승인 불가');
78
+ }
79
+ const finalOffset = Math.floor((preview.length - 1) / 4000) * 4000;
80
+ const sent = await bot.sendInlineKeyboard(ctx.chatId, preview.slice(finalOffset), [[
81
+ { text: '승인', data: `fabric:approve:${plan.id}` },
82
+ { text: '고치기', data: `fabric:edit:${plan.id}` },
83
+ ]], reply);
84
+ if (sent?.messageId !== undefined) pending.set(plan.id, { chatId: ctx.chatId, userId: ctx.userId, threadId: ctx.threadId, messageId: sent.messageId });
85
+ } catch (error) {
86
+ await bot.sendMessage(ctx.chatId, `아크 초안 생성 실패: ${error instanceof Error ? error.message : String(error)}`, { replyTo: ctx.messageId, threadId: ctx.threadId });
87
+ }
88
+ return true;
89
+ };
90
+ }
@@ -35,17 +35,23 @@ export function projectCommand(deps: TelegramProjectCommandDeps = {
35
35
  debug.log('telegram.project', 'listed', { projects: projects.slice(0, 20).map(p => ({ id: p.id, name: p.name })) });
36
36
  if (ctx.isDm && ctx.chatId === ctx.userId
37
37
  && String(ctx.userId) === telegramDecisionOwner(ctxOptions.userConfig) && ctxOptions.sendButtons) {
38
- const buttons = projects.filter(p => {
38
+ const eligible = projects.filter(p => {
39
39
  const bytes = Buffer.byteLength(`prj:${p.id}`, 'utf8');
40
40
  if (bytes <= 64) return true;
41
41
  debug.log('telegram.project', 'button-rejected', { reason: 'callback-data-too-long', projectId: p.id, bytes });
42
42
  return false;
43
- }).slice(0, 8).map(p => ({ text: p.name, data: `prj:${p.id}` }));
44
- buttons.push({ text: '받은 대화로', data: 'prj:-' });
43
+ });
44
+ const visible = eligible.slice(0, 7);
45
+ if (current && eligible.some(p => p.id === current.id) && !visible.some(p => p.id === current.id)) {
46
+ visible[visible.length - 1] = current;
47
+ }
48
+ const buttons = visible.map(p => ({ text: `${p.id === current?.id ? '✓ ' : ''}${p.name}`, data: `prj:${p.id}` }));
49
+ buttons.push({ text: `${session.projectId ? '' : '✓ '}받은 대화로`, data: 'prj:-' });
45
50
  const rows: Array<Array<{ text: string; data: string }>> = [];
46
51
  for (let i = 0; i < buttons.length; i += 2) rows.push(buttons.slice(i, i + 2));
47
52
  try {
48
53
  await ctxOptions.sendButtons('프로젝트를 선택하세요', rows);
54
+ return;
49
55
  } catch (err) {
50
56
  debug.log('telegram.project', 'button-rejected', {
51
57
  reason: 'send-failed', error: err instanceof Error ? err.message : String(err),
@@ -140,6 +146,6 @@ export function attachTelegramProjectButtons(
140
146
  deps.update(session.id, m => { m.projectId = project.id; });
141
147
  debug.log('telegram.project', 'button-moved', { chatId, sessionId: session.id, projectId: project.id });
142
148
  await bot.answerCallbackQuery(q.id);
143
- await bot.sendMessage(chatId, `이 대화를 ${project.name} 프로젝트로 옮겼습니다`);
149
+ await bot.sendMessage(chatId, `지금 프로젝트: ${project.name}`);
144
150
  });
145
151
  }
package/src/telegram.ts CHANGED
@@ -77,6 +77,7 @@ import { handleTelegramFieldNote } from './field-note/telegram.js';
77
77
  import type { TranscriptionEngine, FieldAudio } from './field-note/transcribe.js';
78
78
  import { maybeHandleCardPhoto, type CardPhotoDeps } from './telegram-card-followup.js';
79
79
  import { attachTelegramProjectButtons } from './telegram-project-command.js';
80
+ import { attachTelegramFabricPlan } from './telegram-fabric-plan.js';
80
81
 
81
82
  export type TgAttachmentKind = 'photo' | 'voice' | 'audio' | 'document';
82
83
 
@@ -229,6 +230,7 @@ export interface TelegramBotOpts {
229
230
  fieldNoteDecodeAudio?: (file: string, recordedAt: string) => FieldAudio;
230
231
  /** Test seam for the business-card graph and its private run root. */
231
232
  cardFollowupDeps?: Pick<CardPhotoDeps, 'runGraph' | 'rootDir' | 'ocrText'>;
233
+ fabricPlanDeps?: import('./telegram-fabric-plan.js').TelegramFabricPlanDeps;
232
234
  }
233
235
 
234
236
  /** Surface-unification v2 (FU-2) — Telegram trigger event passed to
@@ -350,6 +352,7 @@ export class TelegramBot {
350
352
  private readonly fieldNoteDecodeAudio?: (file: string, recordedAt: string) => FieldAudio;
351
353
  private readonly cardFollowupDeps: Pick<CardPhotoDeps, 'runGraph' | 'rootDir' | 'ocrText'>;
352
354
  private readonly seatWorkDeps: TelegramSeatWorkDeps;
355
+ private readonly handleFabricPlan: (ctx: TgIncoming) => Promise<boolean>;
353
356
  private seatAskTimer: ReturnType<typeof setInterval> | null = null;
354
357
  /** 앨범 id → `#현장` 행사 · 답장 디바운스. 태그된 앨범만 들어온다. */
355
358
  private readonly fieldGroups = new Map<string, {
@@ -388,6 +391,7 @@ export class TelegramBot {
388
391
  this.fieldNoteEngine = opts.fieldNoteEngine;
389
392
  this.fieldNoteDecodeAudio = opts.fieldNoteDecodeAudio;
390
393
  this.cardFollowupDeps = opts.cardFollowupDeps ?? {};
394
+ this.handleFabricPlan = attachTelegramFabricPlan(this, opts.fabricPlanDeps);
391
395
  this.seatWorkDeps = { ...opts.seatWorkDeps, askDeps: { ...opts.seatWorkDeps?.askDeps,
392
396
  channel: 'telegram', botId: this.botId,
393
397
  send: opts.seatWorkDeps?.askDeps?.send ?? (async (origin, text) => {
@@ -1539,6 +1543,7 @@ export class TelegramBot {
1539
1543
  await this.refuseMessage(ctx);
1540
1544
  return;
1541
1545
  }
1546
+ if (await this.handleFabricPlan(ctx)) return;
1542
1547
  // 현장 업로드 — `#현장` 캡션(또는 태그된 앨범의 나머지)의 사진·영상은 LLM 대신 현장 폴더로.
1543
1548
  if (await this.tryHandleFieldUpload(ctx)) return;
1544
1549
  if (await handleTelegramFieldNote(ctx, {
@@ -3,6 +3,7 @@ import { StringDecoder } from 'node:string_decoder';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { dirname, resolve } from 'node:path';
5
5
  import type { LLMToolSpec } from '../llm.js';
6
+ import { stateDirSourceForChild } from '../agent/identity-env.js';
6
7
  import { debug } from '../debug/log.js';
7
8
  import { canonicalizeForBoundary, isWithinBoundary, resolveHarnessBoundary } from '../harness/harness-write-boundary.js';
8
9
  import { getHarnessSpace } from '../harness/harness-space.js';
@@ -133,7 +134,7 @@ async function spawnHeldElanous(command: string[], cwd: string, instanceRoot: st
133
134
  try {
134
135
  child = deps.spawn(command[0]!, command.slice(1), {
135
136
  cwd,
136
- env: { ...process.env, ELANOUS_STATE_DIR: instanceRoot },
137
+ env: { ...process.env, ELANOUS_STATE_DIR: instanceRoot, ELANOUS_STATE_DIR_SOURCE: stateDirSourceForChild(instanceRoot) },
137
138
  detached: true,
138
139
  stdio: ['ignore', 'pipe', 'pipe'],
139
140
  });
@@ -3475,7 +3475,34 @@ export function resolveRoleModel(role: ModelRole, config: UserConfig = getUserCo
3475
3475
  }
3476
3476
 
3477
3477
 
3478
- export type StewardLoopMode = 'off' | 'shadow' | 'live';
3478
+ export type OrchestratorSeat = 'OP' | 'TC' | 'MK' | 'UX';
3479
+ export interface OrchestratorLoopConfig {
3480
+ seatTrees: Partial<Record<OrchestratorSeat, string[]>>;
3481
+ seatCaps: Record<OrchestratorSeat, number>;
3482
+ trafficMode: 'shadow' | 'live';
3483
+ }
3484
+
3485
+ export const ORCHESTRATOR_DEFAULTS: OrchestratorLoopConfig = {
3486
+ seatTrees: {}, seatCaps: { TC: 8, UX: 6, MK: 6, OP: 4 }, trafficMode: 'shadow',
3487
+ };
3488
+
3489
+ export function parseOrchestratorLoopConfig(raw: unknown): OrchestratorLoopConfig {
3490
+ const value = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
3491
+ const rawTrees = value.seatTrees && typeof value.seatTrees === 'object' && !Array.isArray(value.seatTrees)
3492
+ ? value.seatTrees as Record<string, unknown> : {};
3493
+ const rawCaps = value.seatCaps && typeof value.seatCaps === 'object' && !Array.isArray(value.seatCaps)
3494
+ ? value.seatCaps as Record<string, unknown> : {};
3495
+ const seatTrees: OrchestratorLoopConfig['seatTrees'] = {};
3496
+ const seatCaps = { ...ORCHESTRATOR_DEFAULTS.seatCaps };
3497
+ for (const seat of ['OP', 'TC', 'MK', 'UX'] as const) {
3498
+ if (Array.isArray(rawTrees[seat])) seatTrees[seat] = rawTrees[seat].filter((path): path is string => typeof path === 'string' && isAbsolute(path));
3499
+ const cap = rawCaps[seat];
3500
+ if (typeof cap === 'number' && Number.isSafeInteger(cap) && cap >= 0) seatCaps[seat] = cap;
3501
+ }
3502
+ return { seatTrees, seatCaps, trafficMode: value.trafficMode === 'live' ? 'live' : 'shadow' };
3503
+ }
3504
+
3505
+ export type StewardLoopMode = 'off' | 'shadow' | 'live' | 'rescue';
3479
3506
 
3480
3507
  export type SeatLoopMode = 'off' | 'shadow' | 'on';
3481
3508
 
@@ -3516,25 +3543,51 @@ export function parseEventsConfig(input: unknown): EventsConfig {
3516
3543
  return { mode: events.mode === 'on' ? 'on' : 'shadow', routes, linearAssignees };
3517
3544
  }
3518
3545
 
3546
+ export type HqFenceRole = 'telegram-poller' | 'cron' | 'seat-loop' | 'release-run' | 'conatus' | 'git-push' | 'ledger-cli';
3547
+ export interface HqConfig {
3548
+ /** ssh host that keeps the lease record — homes push to it; it never sshes back (default cloud-vm). */
3549
+ arbiter?: string;
3550
+ /** This host's identity in the lease (default os.hostname()). */
3551
+ hostName?: string;
3552
+ /** Standby host (default node-b) — the quorum's third member; promoted only by the arbiter. */
3553
+ standby?: string;
3554
+ /** Lease TTL in seconds (default 1500 = 25 min). */
3555
+ ttlSeconds?: number;
3556
+ /** Roles that may still run when this host is fenced only for lack of quorum (never when a newer generation exists). Absent = none. */
3557
+ failOpenRoles?: HqFenceRole[];
3558
+ /** Per-host nexus health URL over the tailnet (default https://<host>.<tailnetDomain>:31415/v1/health). */
3559
+ healthUrls?: Record<string, string>;
3560
+ /** MagicDNS suffix (default: read from `tailscale status --json`). */
3561
+ tailnetDomain?: string;
3562
+ /** `tailscale` binary (default tailscale). */
3563
+ tailscaleBin?: string;
3564
+ /** Highest lease generation this host ever saw (default ~/.elanous-hq/seen-generation · 600). */
3565
+ seenGenerationFile?: string;
3566
+ /** Per-probe timeout in seconds (default 5). */
3567
+ probeTimeoutSeconds?: number;
3568
+ }
3569
+
3519
3570
  export interface UserConfig {
3520
3571
  events?: EventsConfig;
3521
3572
  /** Explicit HTTPS destination for doctor diagnostics; absent means no upload. */
3522
3573
  diagnostics?: { uploadUrl?: string };
3574
+ /** HQ lease (본부 임대 · 10-04 HQ-HB/HQ-FENCE): arbiter host over ssh, this host's name, per-role fail-open when the arbiter is unreachable (default: fail closed). */
3575
+ hq?: HqConfig;
3523
3576
  /** Root CLI help audience; absent or invalid means owner. */
3524
3577
  cli?: { helpRole?: 'owner' | 'contributor' | 'general' };
3525
3578
  /** Execution-phase HITL answer deadline; absent means 30 minutes. */
3526
3579
  hitl?: { executionDeadlineMinutes?: number };
3527
3580
  coo?: { linearProject?: string };
3528
- decisions?: { linearProjection: { enabled: boolean } };
3581
+ decisions?: { linearProjection: { enabled: boolean }; requireCrossCheck: boolean };
3529
3582
  /** Per-seat goal and Pod limits; missing or invalid limits use seat-budget defaults. */
3530
3583
  org?: { budget?: Record<string, { dailyGoals?: number; concurrentPods?: number }> };
3531
3584
  /** Steward and seat loops are parsed independently. An absent seat remains off. */
3532
- loops?: { steward?: { mode?: StewardLoopMode; linearTeam?: string; roles?: Record<string, { maxConcurrent?: number }>; budget?: number; tracks?: Record<string, string>; alertAfterFailures?: number; launch?: 'off' | 'shadow' | 'live'; maxParallel?: number; podPool?: string }; seat?: SeatLoopConfig; persona?: PersonaLoopConfig };
3585
+ loops?: { orchestrator?: OrchestratorLoopConfig; steward?: { mode?: StewardLoopMode; linearTeam?: string; roles?: Record<string, { maxConcurrent?: number }>; budget?: number; tracks?: Record<string, string>; alertAfterFailures?: number; launch?: 'off' | 'shadow' | 'live'; maxParallel?: number; podPool?: string }; seat?: SeatLoopConfig; persona?: PersonaLoopConfig };
3533
3586
  /** 🔐 Pod 의 Grok API 키 과금은 명시 opt-in 만. 생략·잘못된 값은 false.
3534
3587
  * budgetGate 와 같은 `harness` 객체다 — 중복 선언이면 뒤 타입이 앞을 지운다. */
3535
- harness?: { pod?: { grokApiKeyOptIn: boolean }; budgetGate?: HarnessBudgetGateConfig; difficultyPlacement?: boolean; defaultRepo?: string; substrate?: 'local' | 'pod'; podPool?: string; worktreeAddTimeoutSec?: number };
3588
+ harness?: { pod?: { grokApiKeyOptIn: boolean }; budgetGate?: HarnessBudgetGateConfig; difficultyPlacement?: boolean; defaultRepo?: string; substrate?: 'local' | 'pod'; podPool?: string; worktreeAddTimeoutSec?: number; queue?: { seatCap?: Partial<Record<'OP' | 'TC' | 'MK' | 'UX', number>> } };
3536
3589
  /** ☸️ Pod 실행 칸 — `pool` = 기존 기본 풀(`컨텍스트[@ssh호스트][:상한][#k3d-레지스트리:포트]` 쉼표 · 앞이 우선). harness 실행은 인자·ELANOUS_POD_POOL·harness.podPool 다음으로 읽는다. */
3537
- pod?: { pool?: string; /** 호스트 Git 미러 디렉터리 — Pod Job 에 읽기 전용으로 마운트한다. */ hostMirror?: string; /** 원격 그라운딩 엔드포인트(호스트 nexus · tailnet 주소) — Pod 가 토큰으로 «질의→인용»만 묻는다(P13). */ groundingUrl?: string; /** 읽기 전용 Pod lease 권장에 쓰는 계정별 동시 수. */ lease?: { perAccount?: number } };
3590
+ pod?: { pool?: string; /** 호스트 Git 미러 디렉터리 — Pod Job 에 읽기 전용으로 마운트한다. */ hostMirror?: string; /** 원격 그라운딩 엔드포인트(호스트 nexus · tailnet 주소) — Pod 가 토큰으로 «질의→인용»만 묻는다(P13). */ groundingUrl?: string; /** 읽기 전용 Pod lease 권장에 쓰는 계정별 동시 수. */ lease?: { perAccount?: number }; /** 실측 권고를 Pod 발사 기본값으로 쓸지 (기본 off). */ memory?: { adviseDefaults?: boolean } };
3538
3591
  skillRouter: SkillRouterConfig;
3539
3592
  llm: LLMConfig;
3540
3593
  skills: SkillsConfig;
@@ -3860,8 +3913,8 @@ function defaultConfig(): UserConfig {
3860
3913
  return {
3861
3914
  cli: { helpRole: 'owner' },
3862
3915
  coo: { linearProject: '외부 행정·큰 일 (COO)' },
3863
- decisions: { linearProjection: { enabled: false } },
3864
- loops: { steward: { mode: 'shadow' }, seat: { mode: 'off', questions: 'shadow', seats: ['MK'], podPool: 'pool-node-b@node-b:8' }, persona: { enabled: false } },
3916
+ decisions: { linearProjection: { enabled: false }, requireCrossCheck: false },
3917
+ loops: { orchestrator: parseOrchestratorLoopConfig(undefined), steward: { mode: 'shadow' }, seat: { mode: 'off', questions: 'shadow', seats: ['MK'], podPool: 'pool-node-b@node-b:8' }, persona: { enabled: false } },
3865
3918
  skillRouter: { ...SR_DEFAULTS },
3866
3919
  llm: { ...LLM_DEFAULTS },
3867
3920
  skills: skillsDefaults(),
@@ -4388,7 +4441,7 @@ function parseStewardLoopsConfig(input: unknown): UserConfig['loops'] {
4388
4441
  }
4389
4442
  }
4390
4443
  return { steward: {
4391
- mode: s.mode === 'off' || s.mode === 'shadow' || s.mode === 'live' ? s.mode :
4444
+ mode: s.mode === 'off' || s.mode === 'shadow' || s.mode === 'live' || s.mode === 'rescue' ? s.mode :
4392
4445
  s.launch === 'off' || s.launch === 'shadow' || s.launch === 'live' ? s.launch : 'shadow',
4393
4446
  // A valid legacy launch setting remains effective when mode is absent; explicit mode always wins.
4394
4447
  ...(s.launch === 'off' || s.launch === 'shadow' || s.launch === 'live' ? { launch: s.launch } : {}),
@@ -4407,6 +4460,25 @@ export function parseCliHelpRole(raw: unknown): NonNullable<UserConfig['cli']>['
4407
4460
  return cli.helpRole === 'general' || cli.helpRole === 'contributor' ? cli.helpRole : 'owner';
4408
4461
  }
4409
4462
 
4463
+ const HQ_FENCE_ROLES: readonly HqFenceRole[] = ['telegram-poller', 'cron', 'seat-loop', 'release-run', 'conatus', 'git-push'];
4464
+ /** `hq` block (HQ-HB · HQ-FENCE): keep only well-typed fields — absent/invalid fields fall back to the hq module defaults. */
4465
+ export function parseHqConfig(raw: unknown): HqConfig | undefined {
4466
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
4467
+ const r = raw as Record<string, unknown>;
4468
+ const str = (v: unknown) => typeof v === 'string' && v.trim() ? v.trim() : undefined;
4469
+ const out: HqConfig = {};
4470
+ for (const key of ['arbiter', 'hostName', 'standby', 'tailnetDomain', 'tailscaleBin', 'seenGenerationFile'] as const) {
4471
+ const v = str(r[key]); if (v) out[key] = v;
4472
+ }
4473
+ if (typeof r.ttlSeconds === 'number' && Number.isFinite(r.ttlSeconds) && r.ttlSeconds > 0) out.ttlSeconds = r.ttlSeconds;
4474
+ if (typeof r.probeTimeoutSeconds === 'number' && Number.isFinite(r.probeTimeoutSeconds) && r.probeTimeoutSeconds > 0) out.probeTimeoutSeconds = r.probeTimeoutSeconds;
4475
+ if (Array.isArray(r.failOpenRoles)) out.failOpenRoles = r.failOpenRoles.filter((v): v is HqFenceRole => HQ_FENCE_ROLES.includes(v as HqFenceRole));
4476
+ if (r.healthUrls && typeof r.healthUrls === 'object' && !Array.isArray(r.healthUrls)) {
4477
+ out.healthUrls = Object.fromEntries(Object.entries(r.healthUrls as Record<string, unknown>).filter((e): e is [string, string] => typeof e[1] === 'string'));
4478
+ }
4479
+ return Object.keys(out).length ? out : undefined;
4480
+ }
4481
+
4410
4482
  export function buildUserConfig(path: string = defaultPath()): UserConfig {
4411
4483
  if (!existsSync(path)) return defaultConfigWithRuntimeProvider();
4412
4484
  let raw: unknown;
@@ -4532,6 +4604,8 @@ export function buildUserConfig(path: string = defaultPath()): UserConfig {
4532
4604
  ...(typeof legacyPod.pool === 'string' && legacyPod.pool.trim() ? { pool: legacyPod.pool.trim() } : {}),
4533
4605
  ...(typeof legacyPod.hostMirror === 'string' && legacyPod.hostMirror.trim() ? { hostMirror: legacyPod.hostMirror.trim() } : {}),
4534
4606
  ...(typeof legacyPod.groundingUrl === 'string' ? { groundingUrl: legacyPod.groundingUrl } : {}),
4607
+ memory: { adviseDefaults: Boolean(legacyPod.memory && typeof legacyPod.memory === 'object' && !Array.isArray(legacyPod.memory)
4608
+ && (legacyPod.memory as Record<string, unknown>).adviseDefaults === true) },
4535
4609
  lease: { perAccount: (() => {
4536
4610
  const lease = legacyPod.lease && typeof legacyPod.lease === 'object' && !Array.isArray(legacyPod.lease)
4537
4611
  ? (legacyPod.lease as Record<string, unknown>) : {};
@@ -4542,6 +4616,12 @@ export function buildUserConfig(path: string = defaultPath()): UserConfig {
4542
4616
  harness: {
4543
4617
  pod: { grokApiKeyOptIn: harnessPod.grokApiKeyOptIn === true },
4544
4618
  difficultyPlacement: harness.difficultyPlacement === true,
4619
+ queue: { seatCap: Object.fromEntries(Object.entries(
4620
+ harness.queue && typeof harness.queue === 'object' && !Array.isArray(harness.queue)
4621
+ && (harness.queue as Record<string, unknown>).seatCap && typeof (harness.queue as Record<string, unknown>).seatCap === 'object'
4622
+ && !Array.isArray((harness.queue as Record<string, unknown>).seatCap)
4623
+ ? (harness.queue as { seatCap: Record<string, unknown> }).seatCap : {},
4624
+ ).filter(([seat, cap]) => /^(OP|TC|MK|UX)$/.test(seat) && typeof cap === 'number' && Number.isSafeInteger(cap) && cap > 0)) },
4545
4625
  ...(harness.substrate === 'local' || harness.substrate === 'pod' ? { substrate: harness.substrate } : {}),
4546
4626
  ...(typeof harness.podPool === 'string' && harness.podPool.trim() ? { podPool: harness.podPool.trim() } : {}),
4547
4627
  ...(typeof harness.worktreeAddTimeoutSec === 'number' && Number.isSafeInteger(harness.worktreeAddTimeoutSec)
@@ -5186,11 +5266,13 @@ export function buildUserConfig(path: string = defaultPath()): UserConfig {
5186
5266
  && typeof (rawObj.hitl as Record<string, unknown>).executionDeadlineMinutes === 'number'
5187
5267
  ? { hitl: { executionDeadlineMinutes: (rawObj.hitl as { executionDeadlineMinutes: number }).executionDeadlineMinutes } }
5188
5268
  : {}),
5269
+ ...(parseHqConfig(rawObj.hq) ? { hq: parseHqConfig(rawObj.hq)! } : {}),
5189
5270
  cli: { helpRole: parseCliHelpRole(rawObj.cli) },
5190
5271
  coo: { linearProject: typeof (rawObj.coo as { linearProject?: unknown } | undefined)?.linearProject === 'string' && (rawObj.coo as { linearProject: string }).linearProject.trim()
5191
5272
  ? (rawObj.coo as { linearProject: string }).linearProject.trim() : '외부 행정·큰 일 (COO)' },
5192
- decisions: { linearProjection: { enabled: ((rawObj.decisions as { linearProjection?: { enabled?: unknown } } | undefined)?.linearProjection?.enabled === true) } },
5193
- loops: { ...(stewardLoops ?? {}), seat: parseSeatLoopsConfig(rawObj.loops),
5273
+ decisions: { linearProjection: { enabled: ((rawObj.decisions as { linearProjection?: { enabled?: unknown } } | undefined)?.linearProjection?.enabled === true) },
5274
+ requireCrossCheck: (rawObj.decisions as { requireCrossCheck?: unknown } | undefined)?.requireCrossCheck === true },
5275
+ loops: { ...(stewardLoops ?? {}), orchestrator: parseOrchestratorLoopConfig((rawObj.loops as { orchestrator?: unknown } | undefined)?.orchestrator), seat: parseSeatLoopsConfig(rawObj.loops),
5194
5276
  persona: { enabled: (rawObj.loops as { persona?: { enabled?: unknown } } | undefined)?.persona?.enabled === true } },
5195
5277
  events: parseEventsConfig(rawObj.events),
5196
5278
  // M1-1: sparse — undefined when the user hasn't set anything, so
@@ -5790,11 +5872,13 @@ export function saveUserConfig(
5790
5872
  defaultRepo: cfg.harness?.defaultRepo ?? rawHarness.defaultRepo,
5791
5873
  substrate: cfg.harness?.substrate,
5792
5874
  podPool: cfg.harness?.podPool,
5875
+ queue: cfg.harness?.queue ?? rawHarness.queue,
5793
5876
  }),
5794
5877
  ...rawRest,
5795
5878
  ...(cfg.loops ? { loops: {
5796
5879
  ...rawLoops,
5797
5880
  ...(cfg.loops.steward ? { steward: { ...rawSteward, mode: cfg.loops.steward.mode ?? cfg.loops.steward.launch ?? 'shadow' } } : {}),
5881
+ ...(cfg.loops.orchestrator ? { orchestrator: cfg.loops.orchestrator } : {}),
5798
5882
  ...(cfg.loops.seat ? { seat: {
5799
5883
  mode: cfg.loops.seat.mode,
5800
5884
  questions: cfg.loops.seat.questions ?? 'shadow',
@@ -1 +1 @@
1
- {"commit":"27045f4612b148543fb0f91b82d37592d1d6c9f2"}
1
+ {"commit":"5116219907580581e7bb40b93018d155deafa5c2"}
@@ -18,6 +18,7 @@ import {
18
18
  isClassifyNode,
19
19
  isExtractNode,
20
20
  isFilterNode,
21
+ isKnowledgeNode,
21
22
  isHttpRequestNode,
22
23
  isIfNode,
23
24
  isIterationNode,
@@ -57,6 +58,7 @@ import { executeExtractNode } from './nodes/extract.js';
57
58
  import { executeSetNode } from './nodes/set.js';
58
59
  import { executeTaskNode } from './nodes/task.js';
59
60
  import { executeFilterNode } from './nodes/filter.js';
61
+ import { executeKnowledgeNode } from './nodes/knowledge.js';
60
62
  import { executeTemplateNode } from './nodes/template.js';
61
63
  import { executeHttpRequestNode } from './nodes/http.js';
62
64
  import { executeShowroomNode } from './nodes/showroom.js';
@@ -706,6 +708,7 @@ async function dispatchNode(
706
708
  if (isExtractNode(node)) return executeExtractNode(node, ctx, deps);
707
709
  if (isSetNode(node)) return executeSetNode(node, ctx, deps);
708
710
  if (isFilterNode(node)) return executeFilterNode(node, ctx, deps);
711
+ if (isKnowledgeNode(node)) return executeKnowledgeNode(node, ctx, deps);
709
712
  if (isTemplateNode(node)) return executeTemplateNode(node, ctx, deps);
710
713
  if (isHttpRequestNode(node)) return executeHttpRequestNode(node, ctx, deps);
711
714
  if (isShowroomNode(node)) return executeShowroomNode(node, ctx, deps);
@@ -748,6 +751,7 @@ function isTriggerVariant(node: DagNode): boolean {
748
751
 
749
752
  function variantOf(node: DagNode): string {
750
753
  if (isPluginKindNode(node)) return node.kind;
754
+ if (isKnowledgeNode(node)) return 'knowledge';
751
755
  const guards: ReadonlyArray<(node: DagNode) => boolean> = [
752
756
  isPromptNode, isBashNode, isSkillNode, isCftNode, isApprovalNode, isIfNode,
753
757
  isSwitchNode, isIterationNode, isClassifyNode, isExtractNode, isSetNode,
@@ -207,6 +207,19 @@ export const NODE_CATALOG: readonly NodeSpec[] = [
207
207
  condition: $item != 'skip'`,
208
208
  related: ['iteration'],
209
209
  },
210
+ {
211
+ kind: 'knowledge',
212
+ category: 'transform',
213
+ summary: '볼트 검색 · 찾은 조각을 다음 노드에',
214
+ yamlKey: 'knowledge',
215
+ required: ['query'],
216
+ optional: ['limit'],
217
+ example: `- id: notes
218
+ knowledge:
219
+ query: $ARGUMENTS
220
+ limit: 3`,
221
+ related: ['prompt'],
222
+ },
210
223
  {
211
224
  kind: 'template',
212
225
  category: 'transform',
@@ -0,0 +1,38 @@
1
+ import { dispatchElanousObsidianSearch } from '../../tool-runtime/elanous-obsidian-search-runtime.js';
2
+ import type { KnowledgeNode, NodeExecContext, NodeOutput, WorkflowDeps } from '../types.js';
3
+ import { interpolate } from '../variables.js';
4
+
5
+ export async function executeKnowledgeNode(
6
+ node: KnowledgeNode,
7
+ ctx: NodeExecContext,
8
+ deps: WorkflowDeps,
9
+ ): Promise<NodeOutput> {
10
+ const startedAt = Date.now();
11
+ try {
12
+ const query = interpolate(node.knowledge.query, {
13
+ arguments: ctx.arguments,
14
+ artifactsDir: ctx.artifactsDir,
15
+ outputs: ctx.outputs,
16
+ }).text;
17
+ const limit = node.knowledge.limit ?? 5;
18
+ const result = await (deps.searchObsidian ?? dispatchElanousObsidianSearch)({ query, limit });
19
+ const matches = result.matches;
20
+ return {
21
+ ok: true,
22
+ output: result.error ? '' : matches.slice(0, limit)
23
+ .map(match => `— ${match.path}:${match.lineNumber}\n${match.snippet.slice(0, 400)}`)
24
+ .join('\n\n'),
25
+ matches,
26
+ ...(result.error ? { error: result.error } : matches.length === 0 ? { error: 'no matches' } : {}),
27
+ durationMs: Date.now() - startedAt,
28
+ };
29
+ } catch (err) {
30
+ return {
31
+ ok: true,
32
+ output: '',
33
+ matches: [],
34
+ error: err instanceof Error ? err.message : String(err),
35
+ durationMs: Date.now() - startedAt,
36
+ };
37
+ }
38
+ }