elanous 0.2.14 → 0.2.16

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 (350) 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/{1307-6e7e0377c1db0bdc.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-482c5dbde699947c.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-8c2d9526164f5d09.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/control/{page-e6b84ab07d3567d5.js → page-a0e2a6d1bfb52fc5.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c53598cfce6156f2.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-13a63953f53c6a23.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-5d3c8429d04f3831.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-3193bdc0bbab047a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-6107ca8ddd5e3bdd.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-dce686e582c23937.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-160089123a40dd91.js → page-1a61c7ca1ceb3ae7.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-51f811fa119308b6.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-4167348049166852.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-de52e8fdf9095f7a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-ca3952cd78a83ca2.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-e7e10cd3386222e5.js → page-5b042736aa113756.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-1ebfe049c640e171.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-57924393e3e4e539.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-0f5cd57ca928f42b.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-d28573d6792b4275.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-58043db78ba519ae.js → page-59f03fda76c03d22.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-357750dfb91bf960.js → page-d751de5f8062ff5e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-fe1759f94003b32b.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-9afdee66b820c76e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-f8da5dc6f3752276.js → page-1ea64acadd704ace.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-3544f154c87b43ee.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  53. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  54. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  55. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +1 -0
  56. package/apps/pwa/out/approvals/index.html +1 -1
  57. package/apps/pwa/out/approvals/index.txt +15 -15
  58. package/apps/pwa/out/autopilot/index.html +1 -1
  59. package/apps/pwa/out/autopilot/index.txt +15 -15
  60. package/apps/pwa/out/board/index.html +1 -1
  61. package/apps/pwa/out/board/index.txt +15 -15
  62. package/apps/pwa/out/botlab/index.html +1 -1
  63. package/apps/pwa/out/botlab/index.txt +15 -15
  64. package/apps/pwa/out/bots/index.html +1 -1
  65. package/apps/pwa/out/bots/index.txt +15 -15
  66. package/apps/pwa/out/ceo/index.html +1 -0
  67. package/apps/pwa/out/ceo/index.txt +33 -0
  68. package/apps/pwa/out/chat/index.html +1 -1
  69. package/apps/pwa/out/chat/index.txt +15 -15
  70. package/apps/pwa/out/consult/index.html +1 -1
  71. package/apps/pwa/out/consult/index.txt +15 -15
  72. package/apps/pwa/out/control/index.html +1 -1
  73. package/apps/pwa/out/control/index.txt +15 -15
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +15 -15
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +15 -15
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +15 -15
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +15 -15
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +15 -15
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +15 -15
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +15 -15
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +15 -15
  91. package/apps/pwa/out/loops/index.html +1 -1
  92. package/apps/pwa/out/loops/index.txt +15 -15
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +15 -15
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +15 -15
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +15 -15
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +15 -15
  105. package/apps/pwa/out/ops/seats/index.html +1 -1
  106. package/apps/pwa/out/ops/seats/index.txt +15 -15
  107. package/apps/pwa/out/outputs/index.html +1 -1
  108. package/apps/pwa/out/outputs/index.txt +15 -15
  109. package/apps/pwa/out/reflection/index.html +1 -1
  110. package/apps/pwa/out/reflection/index.txt +16 -16
  111. package/apps/pwa/out/scheduler/index.html +1 -1
  112. package/apps/pwa/out/scheduler/index.txt +15 -15
  113. package/apps/pwa/out/sessions/index.html +1 -1
  114. package/apps/pwa/out/sessions/index.txt +15 -15
  115. package/apps/pwa/out/settings/devices/index.html +1 -1
  116. package/apps/pwa/out/settings/devices/index.txt +15 -15
  117. package/apps/pwa/out/settings/index.html +1 -1
  118. package/apps/pwa/out/settings/index.txt +15 -15
  119. package/apps/pwa/out/setup/done/index.html +1 -1
  120. package/apps/pwa/out/setup/done/index.txt +15 -15
  121. package/apps/pwa/out/setup/index.html +1 -1
  122. package/apps/pwa/out/setup/index.txt +15 -15
  123. package/apps/pwa/out/share/index.html +1 -1
  124. package/apps/pwa/out/share/index.txt +15 -15
  125. package/apps/pwa/out/showroom/index.html +1 -1
  126. package/apps/pwa/out/showroom/index.txt +15 -15
  127. package/apps/pwa/out/tasks/index.html +1 -1
  128. package/apps/pwa/out/tasks/index.txt +15 -15
  129. package/apps/pwa/out/term/index.html +1 -1
  130. package/apps/pwa/out/term/index.txt +15 -15
  131. package/apps/pwa/out/today/index.html +1 -1
  132. package/apps/pwa/out/today/index.txt +15 -15
  133. package/apps/pwa/out/trace/index.html +1 -1
  134. package/apps/pwa/out/trace/index.txt +15 -15
  135. package/apps/pwa/out/vault/index.html +1 -1
  136. package/apps/pwa/out/vault/index.txt +15 -15
  137. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  138. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  139. package/apps/pwa/out/workflows/index.html +1 -1
  140. package/apps/pwa/out/workflows/index.txt +15 -15
  141. package/apps/pwa/out/workspace/index.html +1 -1
  142. package/apps/pwa/out/workspace/index.txt +15 -15
  143. package/apps/pwa/out/worktrees/index.html +1 -1
  144. package/apps/pwa/out/worktrees/index.txt +15 -15
  145. package/graphs/doc-gen/doc-gen.yaml +24 -0
  146. package/graphs/doc-gen/recipes.yaml +3 -0
  147. package/graphs/heal/heal-loop.yaml +1 -1
  148. package/graphs/implement-loop.yaml +1 -1
  149. package/graphs/orchestrator/orchestrator.yaml +7 -0
  150. package/graphs/orchestrator/recipes.yaml +3 -0
  151. package/graphs/release/recipes.yaml +3 -0
  152. package/graphs/release/release-loop.yaml +4 -2
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/brand/check.ts +7 -5
  156. package/scripts/ci-public-export-import-gate.ts +154 -0
  157. package/scripts/ci-test-interference-gate.ts +5 -1
  158. package/scripts/ci-typecheck-changed.ts +82 -31
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +437 -0
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/expose-rubric-suggest.ts +18 -12
  163. package/scripts/goals/goal-census.ts +343 -0
  164. package/scripts/lecture-note/node.ts +31 -11
  165. package/scripts/lessons/handbook-page.ts +30 -3
  166. package/scripts/marketing/generate-ax-brief.ts +140 -0
  167. package/scripts/marketing/generate-feature-map.ts +118 -0
  168. package/scripts/measure/author-section-contribution.ts +84 -0
  169. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  170. package/scripts/release-loop/cut-branch.ts +43 -13
  171. package/scripts/release-loop/gate-node.ts +82 -14
  172. package/scripts/release-loop/vault-note-node.ts +257 -8
  173. package/scripts/release-story/draft.ts +57 -9
  174. package/scripts/rhythm/daily.ts +263 -76
  175. package/scripts/rhythm/monthly.ts +97 -0
  176. package/scripts/rhythm/weekly.ts +30 -9
  177. package/scripts/smart-merge/resync-shadow.ts +147 -0
  178. package/src/acp/client.ts +17 -2
  179. package/src/agent-env/profile.ts +161 -0
  180. package/src/agent-mission/driver.ts +51 -1
  181. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  182. package/src/agent-mission/supervisor-metrics.ts +218 -0
  183. package/src/agent-substrate/pr-reviewer.ts +2 -0
  184. package/src/ask-user-question/pending-questions.ts +5 -0
  185. package/src/auth/github-app-token.ts +112 -13
  186. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  187. package/src/autopilot/track-agent-shadow.ts +2 -1
  188. package/src/briefing/brief-items.ts +276 -0
  189. package/src/budget/usage-outcomes.ts +204 -0
  190. package/src/claims/claims-render.ts +14 -5
  191. package/src/cli/agent-env-cli.ts +21 -0
  192. package/src/cli/brief-cli.ts +73 -0
  193. package/src/cli/decisions-cli.ts +9 -0
  194. package/src/cli/doctor-cli.ts +23 -1
  195. package/src/cli/doctor-node-modules.ts +147 -0
  196. package/src/cli/intake-cli.ts +46 -17
  197. package/src/cli/know-cli.ts +20 -0
  198. package/src/cli/logs-cli.ts +43 -1
  199. package/src/cli/pod-cli.ts +2 -0
  200. package/src/cli/pr-cli.ts +80 -12
  201. package/src/cli/release-cli.ts +66 -29
  202. package/src/cli/seat-requests-cli.ts +52 -0
  203. package/src/cli/self-update.ts +200 -1
  204. package/src/cli/usage-cli.ts +26 -0
  205. package/src/context-bus/context-now-public.ts +16 -6
  206. package/src/context-bus/context-now.ts +3 -1
  207. package/src/context-bus/coord-events.ts +6 -4
  208. package/src/decisions/decision-cards.ts +33 -7
  209. package/src/decisions/proact-meter.ts +286 -0
  210. package/src/decisions/proact1-lite.ts +86 -0
  211. package/src/decisions/web-push-decision-cards.ts +1 -1
  212. package/src/discord-self-message.ts +9 -0
  213. package/src/domains/alert-audit.ts +271 -0
  214. package/src/domains/core-tools.ts +4 -3
  215. package/src/domains/ops-facts-tool.ts +31 -1
  216. package/src/flow-loop/split.ts +24 -0
  217. package/src/harness/harness-cli-command.ts +147 -17
  218. package/src/harness/harness-incidents.ts +2 -0
  219. package/src/harness/harness-pod-dispatch.ts +67 -8
  220. package/src/harness/harness-queue.ts +380 -50
  221. package/src/harness/harness-seams.ts +13 -3
  222. package/src/harness/helper-repair.ts +218 -0
  223. package/src/harness/helper-scan.ts +27 -4
  224. package/src/harness/launch-stamp.ts +236 -0
  225. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  226. package/src/harness/ledger-write-guard.ts +35 -0
  227. package/src/harness/nested-elanous-policy.ts +114 -0
  228. package/src/harness/pod-memory-policy.ts +15 -0
  229. package/src/harness/repository-name.ts +55 -0
  230. package/src/hq/fence-audit.ts +90 -0
  231. package/src/hq/hq.ts +70 -6
  232. package/src/hq/lease.ts +9 -7
  233. package/src/hq/move-plan.ts +126 -0
  234. package/src/hq/promote.ts +31 -0
  235. package/src/hq/transfer.ts +185 -0
  236. package/src/index.ts +189 -13
  237. package/src/intake-plane/check.ts +63 -0
  238. package/src/intake-plane/digest.ts +65 -21
  239. package/src/intake-plane/front-classifier.ts +6 -0
  240. package/src/intake-plane/items.ts +3 -2
  241. package/src/intake-plane/lens.ts +177 -0
  242. package/src/intake-plane/route.ts +145 -1
  243. package/src/knowledge/know-find.ts +105 -0
  244. package/src/llm.ts +75 -19
  245. package/src/loops/budget.ts +36 -5
  246. package/src/loops/checker.ts +2 -2
  247. package/src/loops/orchestrator/author-depth.ts +65 -9
  248. package/src/loops/orchestrator/author-input-check.ts +8 -3
  249. package/src/loops/orchestrator/author-ledger.ts +3 -1
  250. package/src/loops/orchestrator/finish-rate.ts +14 -3
  251. package/src/loops/orchestrator/seat-share.ts +56 -0
  252. package/src/loops/orchestrator/tick.ts +107 -29
  253. package/src/loops/orchestrator/traffic.ts +117 -11
  254. package/src/loops/registry.ts +142 -61
  255. package/src/maturity/bot-slash-catalog.ts +11 -2
  256. package/src/maturity/feature-maturity.ts +4 -3
  257. package/src/maturity/tui-slash-availability.ts +40 -0
  258. package/src/mss/logging/log-store.ts +35 -4
  259. package/src/nexus/api/http-server.ts +28 -14
  260. package/src/nexus/api/loop-edges.ts +173 -0
  261. package/src/nexus/api/schedules-read.ts +13 -0
  262. package/src/nexus/api/seat-requests.ts +79 -56
  263. package/src/oauth/overload-failover-launch.ts +75 -0
  264. package/src/oauth/overload-failover.ts +137 -0
  265. package/src/prompt-enhance/enhance.ts +6 -1
  266. package/src/release-loop/auto-start.ts +6 -3
  267. package/src/release-loop/checklist.ts +19 -5
  268. package/src/release-loop/feature-store.ts +59 -31
  269. package/src/release-loop/merged-pr-checklist.ts +125 -0
  270. package/src/release-loop/placement.ts +98 -29
  271. package/src/seat-dispatch/seat-ask.ts +33 -6
  272. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  273. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  274. package/src/seat-loop/seat-loop.ts +215 -12
  275. package/src/self-dev/ask-launch-flow.ts +22 -1
  276. package/src/self-dev/authored-goal-trace.ts +64 -0
  277. package/src/self-dev/dev-cli.ts +19 -1
  278. package/src/self-dev/dev-pipeline.ts +14 -9
  279. package/src/self-dev/draft-sweep.ts +90 -10
  280. package/src/self-dev/draft-triage-rules.ts +25 -8
  281. package/src/self-dev/entrance-registry.ts +10 -6
  282. package/src/self-dev/old-door.ts +73 -0
  283. package/src/self-dev/orchestrate.ts +15 -1
  284. package/src/self-dev/review-provider-fallback.ts +6 -1
  285. package/src/self-dev/run-supervisor.ts +49 -2
  286. package/src/self-implement/app-api-budget.ts +166 -0
  287. package/src/self-implement/budget-gate.ts +7 -5
  288. package/src/self-implement/follow-up-goals.ts +289 -0
  289. package/src/self-implement/frozen-merges.ts +3 -2
  290. package/src/self-implement/goal-author.ts +50 -6
  291. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  292. package/src/self-implement/heal-intake.ts +120 -2
  293. package/src/self-implement/heal-triage.ts +44 -0
  294. package/src/self-implement/host-regate.ts +9 -2
  295. package/src/self-implement/l8-merge-queue.ts +132 -0
  296. package/src/self-implement/orchestrator.ts +325 -47
  297. package/src/self-implement/pipeline-shape.ts +1 -1
  298. package/src/self-implement/revert-guard.ts +42 -7
  299. package/src/self-implement/run-ledger.ts +6 -0
  300. package/src/self-implement/run-status-mapping.ts +2 -0
  301. package/src/self-implement/seams.ts +45 -16
  302. package/src/self-implement/self-implement-cli.ts +1 -0
  303. package/src/self-implement/speed-claims.ts +338 -0
  304. package/src/session-runtime/index.ts +2 -0
  305. package/src/session-runtime/retry-api.ts +87 -0
  306. package/src/session-runtime/retry-policy.ts +107 -6
  307. package/src/steward/triage.ts +74 -3
  308. package/src/steward/working-backwards.ts +20 -2
  309. package/src/task-cards/card-cli.ts +25 -0
  310. package/src/task-cards/card-store.ts +5 -3
  311. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  312. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  313. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  315. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  316. package/src/task-orchestrator/surfaces/pod-pool.ts +70 -9
  317. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  318. package/src/task-orchestrator/surfaces/self-implement-pod.ts +166 -46
  319. package/src/task-orchestrator/surfaces/self-implement.ts +7 -1
  320. package/src/telegram-commands.ts +5 -0
  321. package/src/telegram.ts +72 -4
  322. package/src/user-config.ts +64 -7
  323. package/src/version/packed-revision.json +1 -1
  324. package/src/workflow-runtime/nodes/bash.ts +9 -1
  325. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  349. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  350. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -61,8 +61,22 @@ export interface IntakeCheckEvidence {
61
61
  readonly contrary?: boolean;
62
62
  }
63
63
 
64
+ export interface IntakeReplacementCandidate {
65
+ readonly tool: string;
66
+ readonly paidDependency: string;
67
+ readonly status: '현장 시험 필요';
68
+ /** A comparison claim is not evidence that any of these checks have passed. */
69
+ readonly trial: {
70
+ readonly installation: '미검증';
71
+ readonly sampleRun: '미검증';
72
+ readonly license: '미검증';
73
+ readonly maintenanceStatus: '미검증';
74
+ };
75
+ }
76
+
64
77
  export interface IntakeCheckItem {
65
78
  readonly fact: string;
79
+ readonly replacementCandidate?: IntakeReplacementCandidate;
66
80
  /** Source identity preserved from the supplied fact for card-backed goal evidence. */
67
81
  readonly sourceRef?: string;
68
82
  readonly originalClaims?: readonly string[];
@@ -1053,6 +1067,40 @@ function factKey(text: string): string {
1053
1067
  return text.replace(/\s+/g, ' ').trim().toLowerCase();
1054
1068
  }
1055
1069
 
1070
+ /** A stated possibility is a trial candidate, not proof of a working replacement. */
1071
+ function replacementCandidate(fact: string): IntakeReplacementCandidate | undefined {
1072
+ if (!/(?:대체(?:하|한|할| 가능| 후보|재)|replace|replacement|alternative to)/i.test(fact)
1073
+ || /(?:대체하지\s*(?:는|도|조차|를)?\s*(?:않|못)|대체\s*못|대체할\s*수\s*(?:는|도|가)?\s*없|대체(?:가|는| 후보가)?\s*(?:아니|불가)|cannot replace|can't replace|does not replace|not a replacement)/i.test(fact)) return undefined;
1074
+ const paid = fact.match(/(?:유료|paid)\s*(?:(?:서비스|의존성|도구|툴|API|dependency|service|tool)\s*)?`?([a-z][\w.-]+)`?/i);
1075
+ // firecrawl is a named paid-dependency example even when the claim omits the word "paid".
1076
+ const paidDependency = paid?.[1] ?? fact.match(/(?:^|[^\w])`?(firecrawl)`?(?=$|[^\w])/i)?.[1];
1077
+ if (!paidDependency) return undefined;
1078
+ const names = [...fact.matchAll(/`([a-z][\w.-]+)`/gi)].map((match) => match[1]!);
1079
+ const explicit = names.filter((name) => name.toLowerCase() !== paidDependency.toLowerCase());
1080
+ const plain = fact.match(/^\s*([a-z][\w.-]+)\s*(?:가|이|는|은|으로|로|can\b|could\b|may\b)/i)?.[1];
1081
+ const tool = explicit.length === 1 ? explicit[0] : explicit.length === 0 ? plain : undefined;
1082
+ if (!tool || tool.toLowerCase() === paidDependency.toLowerCase() || tool.toLowerCase() === 'elanous') return undefined;
1083
+ // Names appearing together do not establish the direction of replacement.
1084
+ const clause = fact.replace(/`/g, '').replace(/(?:유료|paid)\s*(?:(?:서비스|의존성|도구|툴|API|dependency|service|tool)\s*)?/gi, '');
1085
+ const escape = (name: string): string => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1086
+ const name = (value: string): string => `(?<![\\w.-])${escape(value)}(?![\\w.-])`;
1087
+ const toolName = name(tool);
1088
+ const dependencyName = name(paidDependency);
1089
+ const replaces = (subject: string, object: string): boolean => [
1090
+ new RegExp(`${subject}\\s*(?:이|가|은|는)?\\s+${object}\\s*(?:을|를)\\s*대체(?:하|한|할| 가능| 후보|재)`, 'i'),
1091
+ new RegExp(`${object}\\s*(?:을|를)\\s+${subject}\\s*(?:이|가|은|는)\\s*대체(?:하|한|할| 가능| 후보|재)`, 'i'),
1092
+ new RegExp(`${subject}\\s+(?:can |could |may )?replace(?:s|d)?\\s+${object}`, 'i'),
1093
+ new RegExp(`${subject}\\s+(?:is |could be |may be )?(?:an? )?(?:replacement|alternative)\\s+to\\s+${object}`, 'i'),
1094
+ ].some((pattern) => pattern.test(clause));
1095
+ if (!replaces(toolName, dependencyName) || replaces(dependencyName, toolName)) return undefined;
1096
+ return {
1097
+ tool,
1098
+ paidDependency,
1099
+ status: '현장 시험 필요',
1100
+ trial: { installation: '미검증', sampleRun: '미검증', license: '미검증', maintenanceStatus: '미검증' },
1101
+ };
1102
+ }
1103
+
1056
1104
  function isLens(value: string): value is PreprocessLens {
1057
1105
  return (PREPROCESS_LENSES as readonly string[]).includes(value);
1058
1106
  }
@@ -1414,8 +1462,17 @@ export function runIntakeCheck(
1414
1462
  }
1415
1463
  // src/index.ts intake check .action → runIntakeCheck / runIntakeCheckDocument → runIntakeCheck → judgeFact.
1416
1464
  const judged = judgeFact(fact, ruler, deps);
1465
+ const candidate = replacementCandidate(fact.text);
1466
+ const verdict = candidate && judged.verdict !== '못 쟀다' ? '판단 필요' : judged.verdict;
1467
+ const current = candidate && judged.verdict !== '못 쟀다'
1468
+ ? `${judged.current} · 대체 후보 ${candidate.tool} → ${candidate.paidDependency}: 설치·샘플 실행·라이선스·유지보수 상태 현장 시험 필요`
1469
+ : judged.current;
1417
1470
  const item: IntakeCheckItem = {
1418
1471
  ...judged,
1472
+ verdict,
1473
+ current,
1474
+ line: `${fact.text} → ${current} → ${verdict}`,
1475
+ ...(candidate ? { replacementCandidate: candidate } : {}),
1419
1476
  quotes: [quote],
1420
1477
  ...(fact.sourceRef ? { sourceRef: fact.sourceRef } : {}),
1421
1478
  ...(fact.originalClaim ? { originalClaims: [fact.originalClaim] } : {}),
@@ -1711,6 +1768,11 @@ export function renderIntakeCheckReport(report: IntakeCheckReport): string {
1711
1768
  for (const item of report.items) {
1712
1769
  lines.push(item.line);
1713
1770
  lines.push(` 판정: ${item.verdict}`);
1771
+ if (item.replacementCandidate) {
1772
+ const candidate = item.replacementCandidate;
1773
+ lines.push(` 대체 후보: ${candidate.tool} → 유료 의존성 ${candidate.paidDependency} (${candidate.status})`);
1774
+ lines.push(' 현장 시험: 설치 미검증 · 샘플 실행 미검증 · 라이선스 미검증 · 유지보수 상태 미검증');
1775
+ }
1714
1776
  if (item.goalDraftPath) lines.push(` 골 초안: ${item.goalDraftPath}`);
1715
1777
  for (const quote of item.quotes) lines.push(` 인용: ${quote}`);
1716
1778
  for (const original of item.originalClaims ?? []) lines.push(` 원 주장: ${original}`);
@@ -1746,6 +1808,7 @@ export function intakeCheckReportJson(report: IntakeCheckReport): Record<string,
1746
1808
  current: item.current,
1747
1809
  verdict: item.verdict,
1748
1810
  line: item.line,
1811
+ ...(item.replacementCandidate ? { replacementCandidate: item.replacementCandidate } : {}),
1749
1812
  quotes: item.quotes,
1750
1813
  evidence: item.evidence,
1751
1814
  patterns: item.patterns,
@@ -1,12 +1,13 @@
1
1
  // 흡수 하루 다이제스트 — 그날 흡수한 것을 사람이 «발견»하는 자리. 노트 절(마크다운) ⊕ 텔레그램 짧은 판.
2
2
  // 결정론만: 요약 한 줄은 각 노트가 이미 쓴 「한 줄 결론」을 뽑는다(LLM 을 다시 부르지 않는다).
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
- import { basename, join, relative } from 'node:path';
4
+ import { basename, join } from 'node:path';
5
5
  import { loadIntakeLedger, type IntakeItem } from './items.js';
6
6
  import { readSavedCursorAt } from './collect-telegram-saved.js';
7
7
  import { intakeOutboxDir, kstDay } from './route.js';
8
8
 
9
- export interface DigestEntry { id: string; sources: string[]; url?: string; note?: string; noteName?: string; oneLiner?: string; axis: string }
9
+ export type DigestLensVerdict = '대체 후보' | '보강' | '경쟁 대조';
10
+ export interface DigestEntry { id: string; sources: string[]; url?: string; note?: string; noteName?: string; oneLiner?: string; axis: string; impact?: { verdict: DigestLensVerdict; why: string; target: string } }
10
11
  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 } }
11
12
 
12
13
  const AXIS_LABEL: Record<string, string> = {
@@ -40,20 +41,46 @@ function readJsonl(file: string): Record<string, unknown>[] {
40
41
  /** Saved Messages silent for more than two days → the digest says so (a quiet 0 must not look like «nothing to do»). */
41
42
  const SAVED_SILENCE_MS = 48 * 3600_000;
42
43
 
44
+ /** A «why» that only names paths (optionally behind a label like «경로:») is evidence, not a reason for us. */
45
+ export function pathOnly(why: string): boolean {
46
+ const rest = why
47
+ .replace(/`?(?:[\w.-]+\/)*[\w.-]+\.[A-Za-z0-9]{1,6}(?::\d+)?`?/g, ' ')
48
+ .replace(/`?(?:[\w.-]+\/)+[\w.-]*`?/g, ' ')
49
+ .replace(/(?:경로|파일|근거|path|file|evidence)\s*[::]?/gi, ' ')
50
+ .replace(/[\s,·:;()[\]\-–—]+/g, '');
51
+ return rest.length < 4;
52
+ }
53
+
43
54
  export function buildIntakeDigest(root: string, day: string, readFile: (p: string) => string | undefined = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : undefined), now: Date = new Date()): IntakeDigest {
44
55
  const items = [...loadIntakeLedger(root).items.values()]
45
56
  .filter((i) => (i.status === 'absorbed' || i.status === 'routed') && kstDay(i.lastSeenAt) === day && i.outputs.some((o) => o.kind === 'note'));
57
+ const out = intakeOutboxDir(root);
58
+ const relevant = new Map<string, NonNullable<DigestEntry['impact']>>();
59
+ // The lens (src/intake-plane/lens.ts) writes its verdicts to outbox/lens/<day>.jsonl; route folders carry none.
60
+ for (const folder of ['lens'] as const) {
61
+ for (const row of readJsonl(join(out, folder, `${day}.jsonl`))) {
62
+ if (typeof row.id !== 'string' || relevant.has(row.id)) continue;
63
+ const verdict = row.lensVerdict;
64
+ if (verdict !== '대체 후보' && verdict !== '보강' && verdict !== '경쟁 대조') continue;
65
+ const why = typeof row.why === 'string' ? row.why.trim() : '';
66
+ const target = typeof row.target === 'string' ? row.target.trim() : '';
67
+ // A check's fact/current or repo path is evidence, not a lens judgement or a reason for us.
68
+ if (!why || !target || pathOnly(why)) continue;
69
+ relevant.set(row.id, { verdict, why, target });
70
+ }
71
+ }
46
72
  const absorbed = items.map((i: IntakeItem): DigestEntry => {
47
73
  const note = [...i.outputs].reverse().find((o) => o.kind === 'note')?.ref;
48
74
  const md = note ? readFile(note) : undefined;
75
+ const impact = relevant.get(i.id);
49
76
  return {
50
77
  id: i.id, sources: i.sources, ...(i.url ? { url: i.url } : {}),
51
78
  ...(note ? { note, noteName: basename(note, '.md') } : {}),
52
79
  ...(md ? { oneLiner: noteOneLiner(md) } : {}),
53
80
  axis: AXIS_LABEL[i.judgement?.axis ?? i.axis ?? ''] ?? (i.sources.includes('telegram-saved') ? '내가 저장한 것' : '그 밖'),
81
+ ...(impact ? { impact } : {}),
54
82
  };
55
83
  });
56
- const out = intakeOutboxDir(root);
57
84
  const goals = readJsonl(join(out, 'goals', `${day}.jsonl`)).map((g) => ({ fact: String(g.fact ?? ''), ...(g.url ? { url: String(g.url) } : {}) }));
58
85
  const review = readJsonl(join(out, 'review', `${day}.jsonl`)).map((r) => ({ fact: String(r.fact ?? ''), ...(r.note ? { note: String(r.note) } : {}) }));
59
86
  const grounding = readJsonl(join(out, 'grounding.jsonl')).filter((g) => typeof g.at === 'string' && kstDay(g.at) === day).length;
@@ -84,28 +111,45 @@ export function renderDigestMarkdown(d: IntakeDigest): string {
84
111
  for (const g of d.goals.slice(0, 10)) L.push(`- ${g.fact}${g.url ? ` · [원본](${g.url})` : ''}`);
85
112
  L.push('');
86
113
  }
87
- if (d.review?.length) {
88
- L.push(`### 🟡 사람이 가를 것 — 판단 필요 (${d.review.length})`, '', '> 근거가 실행 코드에 닿지만 «같은 것인가»는 결정론으로 못 가른다 — 노트의 🧭 절에서 판단한다.', '');
89
- for (const r of d.review.slice(0, 10)) L.push(`- ${r.fact}${r.note ? ` · [[${basename(r.note, '.md')}]]` : ''}`);
90
- if (d.review.length > 10) L.push(`- … 외 ${d.review.length - 10}건`);
91
- L.push('');
92
- }
114
+ const undecided = d.absorbed.filter((e) => !e.impact).length;
115
+ if (undecided) L.push(`렌즈 판정 못 함 ${undecided} — 원장 \`elanous intake items\``, '');
93
116
  L.push(`> 매뉴얼 후보 ${d.manual} · 릴리스 노트 맥락 ${d.release} · 그라운딩 후보 ${d.grounding} — 흡수 원장 \`elanous intake items\` · 큐 \`<인스턴스>/intake/outbox/\``, '');
94
117
  return L.join('\n');
95
118
  }
96
119
 
97
- /** 텔레그램 짧은 판 — 수 · 한 줄 요약 다섯 · 골 후보 · 노트 자리. */
98
- export function renderDigestTelegram(d: IntakeDigest, opts: { vaultRoot?: string; notePath?: string } = {}): string {
99
- const L = [`📰 *${d.day} 흡수 ${d.absorbed.length}편*`];
100
- if (d.savedSilence) L.push(`⚠️ 저장된 메시지 새 글 ${d.savedSilence.days}일째 0 — 저장한 곳 확인`);
101
- for (const e of d.absorbed.slice(0, 5)) L.push(`• ${e.oneLiner ?? e.noteName ?? e.url ?? e.id}`);
102
- if (d.absorbed.length > 5) L.push(`… 외 ${d.absorbed.length - 5}편`);
103
- if (d.goals.length) L.push('', `🔴 엘라누스에 없는 것 ${d.goals.length}건 — 골 후보`);
104
- if (d.review?.length) L.push(`${d.goals.length ? '' : '\n'}🟡 사람이 가를 것 ${d.review.length}건 — 노트 🧭 절`);
105
- if (opts.vaultRoot && opts.notePath) {
106
- const vault = basename(opts.vaultRoot);
107
- const file = relative(opts.vaultRoot, opts.notePath).replace(/\.md$/, '');
108
- L.push('', `📂 obsidian://open?vault=${encodeURIComponent(vault)}&file=${encodeURIComponent(file)}`);
120
+ /** 텔레그램 — 흡수 렌즈가 대조한 항목만 SCQA 짧은 판으로 낸다. */
121
+ export function renderDigestTelegram(d: IntakeDigest, _opts: { vaultRoot?: string; notePath?: string } = {}): string {
122
+ // «Touching» is decided by the lens verdict alone; a missing note summary only changes how S reads (ACP must-fix).
123
+ const touching = d.absorbed.filter((e) => e.impact);
124
+ const L = [`흡수 ${d.absorbed.length} → 우리에게 닿는 것 ${touching.length}`];
125
+ if (!touching.length) return L[0];
126
+ // At most three items are shown, each as its own S·C·A·link block so context and source stay paired;
127
+ // a later item sharing (verdict, target) says «위와 같은 행동» instead of repeating the A sentence (ACP must-fix).
128
+ // S must be the note's own summary: items without one still count as touching but are not shown (ACP must-fix).
129
+ const shownItems = touching.filter((e) => e.oneLiner?.trim()).slice(0, 3);
130
+ const actionSeen = new Set<string>();
131
+ const actionsUsed = new Set<string>();
132
+ for (const e of shownItems) {
133
+ const impact = e.impact!;
134
+ const key = `${impact.verdict}\u0000${impact.target}`;
135
+ const base = impact.verdict === '대체 후보' ? `${impact.target} lite Pod 실증 제안`
136
+ : impact.verdict === '보강' ? `칸 ${impact.target} 에 근거 추가`
137
+ : `${impact.target} 비교표 갱신`;
138
+ // Every item keeps its own concrete action; a repeat of (verdict, target) names which item it is, so no two A lines match.
139
+ const label = (e.oneLiner?.trim() || impact.why).slice(0, 24);
140
+ let action = actionSeen.has(key) ? `${base} — «${label}» 근거로` : base;
141
+ // Two identical labels still differ by their position in the list.
142
+ if (actionsUsed.has(action)) action = `${action} (${shownItems.indexOf(e) + 1})`;
143
+ actionsUsed.add(action);
144
+ actionSeen.add(key);
145
+ L.push('',
146
+ `S 무엇: ${e.oneLiner?.trim() || '노트 한 줄 요약 없음'}`,
147
+ `C 우리에게 왜: ${impact.why}`,
148
+ `A 그래서 무엇을 하나: ${action}`,
149
+ `🔗 원문 링크: ${e.url?.trim() || '링크 없음'}`,
150
+ );
109
151
  }
152
+ if (touching.length > shownItems.length) L.push('', `닿는 것 ${touching.length - shownItems.length}건 더(요약 없는 것 포함) · 원장 \`elanous intake items\``);
153
+ if (d.absorbed.length > touching.length) L.push('', `그 밖 ${d.absorbed.length - touching.length}건 · 참고`);
110
154
  return L.join('\n');
111
155
  }
@@ -18,6 +18,12 @@ export interface FrontClassifierDecision extends IntakeFrontRouteDecision {
18
18
  urlRoute: UrlRouteDecision | null;
19
19
  }
20
20
 
21
+ /** Only Telegram links explicitly routed to saved/absorbed work enter the event lane. */
22
+ export function telegramAbsorbUrls(input: FrontClassifierInput, decision: FrontClassifierDecision): string[] {
23
+ if (input.surface !== 'telegram' || decision.track !== 'absorb' || !decision.urlRoute) return [];
24
+ return decision.urlRoute.urls;
25
+ }
26
+
21
27
  const QUESTION_WORD_RE = new RegExp(
22
28
  `(?:${INTAKE_FRONT_QUESTION_WORDS.map((word) => /^[a-z]+$/i.test(word) ? `(?<![\\p{L}\\p{N}_])${word}(?![\\p{L}\\p{N}_])` : word).join('|')})[.!?。?!]*$`,
23
29
  'iu',
@@ -5,6 +5,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { debug } from '../debug/log.js';
7
7
  import { findTrack } from '../autopilot/mission-phase-track.js';
8
+ import { wasIntakeEventScheduled } from './route.js';
8
9
 
9
10
  export const INTAKE_SOURCES = ['x', 'youtube', 'github', 'telegram-saved', 'telegram-bot', 'memo', 'pwa'] as const;
10
11
  export type RegisteredIntakeSourceKind = 'rss' | 'command';
@@ -243,13 +244,13 @@ export function parseRawIntakeJsonl(text: string): { raws: RawIntakeItem[]; bad:
243
244
  */
244
245
  const STALE_QUEUED_MS = 24 * 3600_000;
245
246
 
246
- export function pickAbsorbQueue(instanceRoot: string, opts: { max: number; laneMax?: number; kind?: IntakeKind; dryRun?: boolean }, now = new Date().toISOString()): IntakeItem[] {
247
+ export function pickAbsorbQueue(instanceRoot: string, opts: { max: number; laneMax?: number; kind?: IntakeKind; dryRun?: boolean }, now = new Date().toISOString(), taskStatus?: Parameters<typeof wasIntakeEventScheduled>[3]): IntakeItem[] {
247
248
  const userLeft = (i: IntakeItem) => i.sources.some((s) => s === 'telegram-saved' || s === 'memo');
248
249
  const score = (i: IntakeItem) => Math.max(0, ...Object.entries(i.signals).filter(([k]) => k.endsWith('.score')).map(([, v]) => v));
249
250
  const eligible = [...loadIntakeLedger(instanceRoot).items.values()]
250
251
  // queued 로 옮긴 뒤 흡수·표시가 없이 하루가 지난 것(흡수 도중 죽은 판)은 다시 고른다.
251
252
  .filter((i) => (i.status === 'new' || (i.status === 'queued' && Date.parse(now) - Date.parse(i.lastSeenAt) > STALE_QUEUED_MS))
252
- && !!i.url && (!opts.kind || i.kind === opts.kind))
253
+ && !!i.url && !wasIntakeEventScheduled(instanceRoot, i.url, now, taskStatus) && (!opts.kind || i.kind === opts.kind))
253
254
  .sort((a, b) => Number(userLeft(b)) - Number(userLeft(a)) || b.sources.length - a.sources.length || score(b) - score(a) || b.lastSeenAt.localeCompare(a.lastSeenAt));
254
255
  const laneMax = opts.laneMax ?? 30;
255
256
  const lane = eligible.filter((i) => i.sources.includes('telegram-saved')).slice(0, Math.max(0, laneMax));
@@ -0,0 +1,177 @@
1
+ // 흡수 렌즈 — outbox 항목마다 «우리에게 무엇인가»를 판정해 outbox/lens/<day>.jsonl 에만 남긴다.
2
+ // 원 갈래(goals·manual·review·release)는 읽기만 한다. LLM 은 judge 한 번(id 당).
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { debug } from '../debug/log.js';
6
+ import { judge } from '../llm/judge-layer.js';
7
+ import { intakeOutboxDir } from './route.js';
8
+
9
+ export const LENS_VERDICTS = ['대체 후보', '보강', '경쟁 대조', '참고'] as const;
10
+ export type LensVerdict = (typeof LENS_VERDICTS)[number];
11
+
12
+ export const LENS_BRANCHES = ['goals', 'manual', 'review', 'release'] as const;
13
+ export const LENS_DAILY_CAP = 20;
14
+ export const LENS_FACT_CAP = 12;
15
+
16
+ export interface LensRecord {
17
+ id: string;
18
+ at: string;
19
+ lensVerdict: LensVerdict;
20
+ why: string;
21
+ target: string;
22
+ }
23
+
24
+ export interface LensJudgeInput {
25
+ id: string;
26
+ facts: { fact: string; current: string }[];
27
+ url?: string;
28
+ }
29
+
30
+ export type LensJudge = (input: LensJudgeInput) => Promise<unknown>;
31
+
32
+ export interface AnnotateIntakeLensDeps {
33
+ judge?: LensJudge;
34
+ now?: () => string;
35
+ log?: (event: string, data: Record<string, unknown>) => void;
36
+ }
37
+
38
+ export interface AnnotateIntakeLensResult {
39
+ judged: number;
40
+ skipped: number;
41
+ failed: number;
42
+ capped: boolean;
43
+ }
44
+
45
+ interface GroupedItem {
46
+ id: string;
47
+ facts: { fact: string; current: string }[];
48
+ url?: string;
49
+ }
50
+
51
+ function readJsonl(file: string): Record<string, unknown>[] {
52
+ if (!existsSync(file)) return [];
53
+ return readFileSync(file, 'utf8').split('\n').filter(Boolean).flatMap((line) => {
54
+ try { const row = JSON.parse(line) as unknown; return row && typeof row === 'object' && !Array.isArray(row) ? [row as Record<string, unknown>] : []; }
55
+ catch { return []; }
56
+ });
57
+ }
58
+
59
+ function isTestProcess(): boolean {
60
+ return process.env.NODE_ENV === 'test' || Boolean(process.env.ELANOUS_TEST_HOME);
61
+ }
62
+
63
+ function isLensVerdict(value: unknown): value is LensVerdict {
64
+ return typeof value === 'string' && (LENS_VERDICTS as readonly string[]).includes(value);
65
+ }
66
+
67
+ /** 스키마 밖이면 null — 기록하지 않는다. why 는 한 문장이다. */
68
+ export function coerceLensVerdict(raw: unknown): Omit<LensRecord, 'id' | 'at'> | null {
69
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
70
+ const row = raw as Record<string, unknown>;
71
+ const why = typeof row.why === 'string' ? row.why.trim() : '';
72
+ const target = typeof row.target === 'string' ? row.target.trim() : '';
73
+ if (!isLensVerdict(row.lensVerdict) || !why || !target) return null;
74
+ if (/[\r\n]/.test(why)) return null;
75
+ return { lensVerdict: row.lensVerdict, why, target };
76
+ }
77
+
78
+ function groupOutbox(root: string, day: string): GroupedItem[] {
79
+ const out = intakeOutboxDir(root);
80
+ const order: string[] = [];
81
+ const groups = new Map<string, GroupedItem>();
82
+ for (const branch of LENS_BRANCHES) {
83
+ for (const row of readJsonl(join(out, branch, `${day}.jsonl`))) {
84
+ const id = typeof row.id === 'string' ? row.id.trim() : '';
85
+ const fact = typeof row.fact === 'string' ? row.fact.trim() : '';
86
+ const current = typeof row.current === 'string' ? row.current.trim() : '';
87
+ if (!id || !fact) continue;
88
+ let group = groups.get(id);
89
+ if (!group) {
90
+ group = { id, facts: [] };
91
+ groups.set(id, group);
92
+ order.push(id);
93
+ }
94
+ if (!group.url && typeof row.url === 'string' && row.url.trim()) group.url = row.url.trim();
95
+ if (group.facts.length < LENS_FACT_CAP) group.facts.push({ fact, current });
96
+ }
97
+ }
98
+ return order.map((id) => groups.get(id)!);
99
+ }
100
+
101
+ function judgedIds(file: string): Set<string> {
102
+ return new Set(readJsonl(file).flatMap((row) => typeof row.id === 'string' && row.id ? [row.id] : []));
103
+ }
104
+
105
+ function lensPrompt(item: GroupedItem): string {
106
+ const lines = item.facts.map((row, i) => `${i + 1}. fact: ${row.fact}\n current: ${row.current || '(없음)'}`);
107
+ return [
108
+ 'SYSTEM:',
109
+ '흡수 항목이 우리에게 무엇인가를 한 번만 판정한다. 경로를 나열하지 말고, 왜 우리에게 그 판정인지를 한 문장으로 말한다.',
110
+ 'lensVerdict 는 다음 넷 중 하나다: 대체 후보 · 보강 · 경쟁 대조 · 참고.',
111
+ 'target 은 닿는 우리 쪽 이름 하나(기능·칸·파일)다.',
112
+ 'JSON 만 답한다: {"lensVerdict":"...","why":"...","target":"..."}',
113
+ '',
114
+ `id: ${item.id}`,
115
+ ...(item.url ? [`url: ${item.url}`] : []),
116
+ ...lines,
117
+ ].join('\n');
118
+ }
119
+
120
+ async function defaultJudge(item: GroupedItem): Promise<unknown> {
121
+ const decision = await judge({
122
+ site: 'intake.lens',
123
+ prompt: lensPrompt(item),
124
+ schema: (value) => coerceLensVerdict(value),
125
+ });
126
+ if (!decision.ok) {
127
+ if (decision.reason === 'schema') return null;
128
+ throw decision.error instanceof Error ? decision.error : new Error(String(decision.error));
129
+ }
130
+ return decision.value;
131
+ }
132
+
133
+ /** 그날 outbox 를 id 로 묶어 아직 없는 판정만 남긴다. 원 갈래 파일은 쓰지 않는다. */
134
+ export async function annotateIntakeLens(
135
+ root: string,
136
+ day: string,
137
+ deps: AnnotateIntakeLensDeps = {},
138
+ ): Promise<AnnotateIntakeLensResult> {
139
+ const log = deps.log ?? ((event, data) => { debug.log('intake.lens', event, data); });
140
+ const empty = { judged: 0, skipped: 0, failed: 0, capped: false };
141
+ if (!deps.judge && isTestProcess()) {
142
+ log('skipped-test', {});
143
+ return empty;
144
+ }
145
+ const items = groupOutbox(root, day);
146
+ const file = join(intakeOutboxDir(root), 'lens', `${day}.jsonl`);
147
+ const done = judgedIds(file);
148
+ const pending = items.filter((item) => !done.has(item.id));
149
+ const capped = pending.length > LENS_DAILY_CAP;
150
+ const batch = pending.slice(0, LENS_DAILY_CAP);
151
+ if (capped) log('capped', { day, total: pending.length, judged: batch.length });
152
+ const judgeOne = deps.judge ?? ((input: LensJudgeInput) => defaultJudge(input));
153
+ const now = deps.now ?? (() => new Date().toISOString());
154
+ let judged = 0;
155
+ let failed = 0;
156
+ for (const item of batch) {
157
+ let raw: unknown;
158
+ try {
159
+ raw = await judgeOne({ id: item.id, facts: item.facts, ...(item.url ? { url: item.url } : {}) });
160
+ } catch (error) {
161
+ failed++;
162
+ log('judge-failed', { id: item.id, reason: error instanceof Error ? error.message : String(error) });
163
+ continue;
164
+ }
165
+ const verdict = coerceLensVerdict(raw);
166
+ if (!verdict) {
167
+ failed++;
168
+ log('judge-failed', { id: item.id, reason: 'schema' });
169
+ continue;
170
+ }
171
+ mkdirSync(join(intakeOutboxDir(root), 'lens'), { recursive: true });
172
+ const record: LensRecord = { id: item.id, at: now(), ...verdict };
173
+ appendFileSync(file, `${JSON.stringify(record)}\n`);
174
+ judged++;
175
+ }
176
+ return { judged, skipped: items.length - pending.length, failed, capped };
177
+ }
@@ -3,9 +3,13 @@
3
3
  // ⛔ 원장의 `text`(개인 메모일 수 있다)는 큐에 싣지 않는다 — 대조가 낸 사실·근거와 노트 경로만.
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
6
+ import { Database } from 'bun:sqlite';
6
7
  import { join } from 'node:path';
7
8
  import { debug } from '../debug/log.js';
8
- import { intakeLedgerDir, loadIntakeLedger, markIntakeItem, type IntakeItem } from './items.js';
9
+ import { getToxRuntimeDeps } from '../task-orchestrator/runtime-deps.js';
10
+ import type { TaskStore } from '../task-orchestrator/store.js';
11
+ import type { TaskStatus } from '../task-orchestrator/types.js';
12
+ import { canonicalUrl, ingestIntakeItems, intakeItemId, intakeLedgerDir, loadIntakeLedger, markIntakeItem, type IntakeItem } from './items.js';
9
13
 
10
14
  /** `elanous intake check --json` 산출 중 갈래가 읽는 칸. */
11
15
  export interface IntakeCheckJson {
@@ -29,6 +33,146 @@ export const gitLastCommitOf: LastCommitOf = (tree, path) => {
29
33
 
30
34
  export function intakeOutboxDir(root: string): string { return join(intakeLedgerDir(root), 'outbox'); }
31
35
 
36
+ export const INTAKE_EVENT_DEFERRED_REPLY = '일일 즉시 흡수 상한을 넘어 다음 일일 판으로 미뤘습니다.';
37
+ export type IntakeEventResult = { status: 'scheduled' | 'deferred' | 'duplicate'; reason: string; url: string; reply?: string };
38
+
39
+ // A lost claim must become eligible at the next daily pass; live tasks remain excluded.
40
+ const INTAKE_EVENT_LEASE_MS = 23 * 3600_000;
41
+
42
+ // A non-terminal task that has not been touched for this long is treated as a dead worker.
43
+ const INTAKE_EVENT_TASK_STALE_MS = 2 * 3600_000;
44
+
45
+ /** A probe may return the bare status, or the status with the task's last update (epoch ms). */
46
+ export type IntakeEventTaskProbe = (id: string) => TaskStatus | { status: TaskStatus; updatedAt?: number } | undefined;
47
+
48
+ function eventTaskStatus(taskId: string): { status: TaskStatus; updatedAt?: number } | undefined {
49
+ const deps = getToxRuntimeDeps();
50
+ const graphTask = deps.getGraph()?.getTask(taskId) ?? (deps.getStore?.() as TaskStore | null | undefined)?.getTask(taskId);
51
+ return graphTask ? { status: graphTask.status, updatedAt: graphTask.updatedAt } : undefined;
52
+ }
53
+
54
+ function probeStatus(probe: IntakeEventTaskProbe, id: string): { status?: TaskStatus; updatedAt?: number } {
55
+ const value = probe(id);
56
+ return typeof value === 'string' ? { status: value } : value ?? {};
57
+ }
58
+
59
+ type EventClaim = { status: string; task_id: string | null; claimed_at: string | null; day: string };
60
+
61
+ function activeEventClaim(row: EventClaim, now: string, taskStatus: IntakeEventTaskProbe): boolean {
62
+ if (row.status !== 'scheduled' && row.status !== 'pending') return false;
63
+ const { status, updatedAt } = row.task_id ? probeStatus(taskStatus, row.task_id) : {};
64
+ if (status === 'done') return true;
65
+ if (status === 'failed' || status === 'cancelled' || status === 'superseded') return false;
66
+ const claimedAt = Date.parse(row.claimed_at ?? `${row.day}T00:00:00.000+09:00`);
67
+ const withinLease = Number.isFinite(claimedAt) && Date.parse(now) - claimedAt < INTAKE_EVENT_LEASE_MS;
68
+ if (!status) return withinLease;
69
+ // A status alone is not liveness: a crashed worker leaves «running» behind, so require a recent update.
70
+ if (updatedAt !== undefined && Number.isFinite(updatedAt)) return Date.parse(now) - updatedAt < INTAKE_EVENT_TASK_STALE_MS;
71
+ return withinLease;
72
+ }
73
+
74
+ /** Claim an event slot before posting. The unique URL and KST-day quota share one SQLite transaction. */
75
+ export async function scheduleIntakeEvent(
76
+ root: string,
77
+ url: string,
78
+ opts: {
79
+ eventMax?: number;
80
+ now?: string;
81
+ post: (input: { url: string }) => Promise<{ taskId: string }>;
82
+ },
83
+ ): Promise<IntakeEventResult> {
84
+ const normalized = canonicalUrl(url);
85
+ if (!normalized) throw new Error('intake event requires an HTTP(S) URL');
86
+ const now = opts.now ?? new Date().toISOString();
87
+ const day = kstDay(now);
88
+ const id = intakeItemId('telegram-bot', { url: normalized });
89
+ const max = opts.eventMax ?? 10;
90
+ if (!Number.isSafeInteger(max) || max < 0) throw new RangeError('intake.eventMax must be a non-negative integer');
91
+ const existing = loadIntakeLedger(root).items.get(id);
92
+ if (existing && ['queued', 'absorbed', 'checked', 'routed', 'discarded'].includes(existing.status)) {
93
+ return { status: 'duplicate', url: normalized, reason: 'already-in-ledger' };
94
+ }
95
+ mkdirSync(intakeLedgerDir(root), { recursive: true });
96
+ const db = new Database(join(intakeLedgerDir(root), 'events.sqlite'), { create: true });
97
+ db.exec('PRAGMA busy_timeout = 5000');
98
+ try {
99
+ db.exec(`CREATE TABLE IF NOT EXISTS intake_events (
100
+ url TEXT PRIMARY KEY, day TEXT NOT NULL, status TEXT NOT NULL, task_id TEXT, claimed_at TEXT
101
+ )`);
102
+ const columns = db.query('PRAGMA table_info(intake_events)').all() as { name: string }[];
103
+ if (!columns.some((column) => column.name === 'claimed_at')) db.exec('ALTER TABLE intake_events ADD COLUMN claimed_at TEXT');
104
+ // One row per claim that reached the task queue — the KST-day quota counts these, not current statuses.
105
+ db.exec('CREATE TABLE IF NOT EXISTS intake_event_claims (url TEXT NOT NULL, day TEXT NOT NULL, claimed_at TEXT NOT NULL, PRIMARY KEY (url, claimed_at))');
106
+ const claim = db.transaction((): 'scheduled' | 'deferred' | 'duplicate' => {
107
+ const previous = db.query('SELECT status, task_id, claimed_at, day FROM intake_events WHERE url = ?').get(normalized) as EventClaim | null;
108
+ if (previous && activeEventClaim(previous, now, eventTaskStatus)) {
109
+ if (previous.task_id && eventTaskStatus(previous.task_id)?.status === 'done') {
110
+ db.query("UPDATE intake_events SET status = 'completed' WHERE url = ?").run(normalized);
111
+ }
112
+ return 'duplicate';
113
+ }
114
+ if (previous && previous.status !== 'failed' && previous.status !== 'expired'
115
+ && previous.status !== 'pending' && previous.status !== 'scheduled') {
116
+ return previous.status === 'deferred' ? 'deferred' : 'duplicate';
117
+ }
118
+ const count = db.query('SELECT count(*) AS n FROM intake_event_claims WHERE day = ?').get(day) as { n: number };
119
+ const status = count.n >= max ? 'deferred' : 'pending';
120
+ db.query(`INSERT INTO intake_events (url, day, status, claimed_at) VALUES (?, ?, ?, ?)
121
+ ON CONFLICT(url) DO UPDATE SET day = excluded.day, status = excluded.status, claimed_at = excluded.claimed_at, task_id = NULL`)
122
+ .run(normalized, day, status, now);
123
+ if (status === 'pending') db.query('INSERT OR IGNORE INTO intake_event_claims (url, day, claimed_at) VALUES (?, ?, ?)').run(normalized, day, now);
124
+ return status === 'pending' ? 'scheduled' : 'deferred';
125
+ })();
126
+ if (claim === 'duplicate') return { status: 'duplicate', url: normalized, reason: 'already-claimed' };
127
+ if (claim === 'deferred') {
128
+ ingestIntakeItems(root, 'telegram-bot', [{ url: normalized }], now);
129
+ debug.log('intake.event', 'deferred', { url: normalized, reason: 'event-max' });
130
+ return { status: 'deferred', url: normalized, reason: 'event-max', reply: INTAKE_EVENT_DEFERRED_REPLY };
131
+ }
132
+ let posted: { taskId: string };
133
+ try {
134
+ ingestIntakeItems(root, 'telegram-bot', [{ url: normalized }], now);
135
+ posted = await opts.post({ url: normalized });
136
+ if (!posted.taskId) throw new Error('intake event post returned no taskId');
137
+ } catch (error) {
138
+ db.query('DELETE FROM intake_events WHERE url = ? AND status = ? AND claimed_at = ?').run(normalized, 'pending', now);
139
+ db.query('DELETE FROM intake_event_claims WHERE url = ? AND claimed_at = ?').run(normalized, now);
140
+ throw error;
141
+ }
142
+ db.query("UPDATE intake_events SET status = 'scheduled', task_id = ? WHERE url = ? AND status = 'pending' AND claimed_at = ?")
143
+ .run(posted.taskId, normalized, now);
144
+ debug.log('intake.event', 'scheduled', { url: normalized, reason: 'telegram-absorb' });
145
+ return { status: 'scheduled', url: normalized, reason: 'telegram-absorb' };
146
+ } finally {
147
+ db.close();
148
+ }
149
+ }
150
+
151
+ /** Only an active task or a fresh claim excludes a URL from the daily queue. */
152
+ export function wasIntakeEventScheduled(root: string, url: string, now = new Date().toISOString(), taskStatus: IntakeEventTaskProbe = eventTaskStatus): boolean {
153
+ const file = join(intakeLedgerDir(root), 'events.sqlite');
154
+ if (!existsSync(file)) return false;
155
+ const normalized = canonicalUrl(url);
156
+ if (!normalized) return false;
157
+ const db = new Database(file);
158
+ try {
159
+ const row = db.query('SELECT status, task_id, claimed_at, day FROM intake_events WHERE url = ?').get(normalized) as EventClaim | null;
160
+ if (!row || (row.status !== 'scheduled' && row.status !== 'pending')) return row?.status === 'completed';
161
+ if (activeEventClaim(row, now, taskStatus)) {
162
+ if (row.task_id && probeStatus(taskStatus, row.task_id).status === 'done') {
163
+ db.query("UPDATE intake_events SET status = 'completed' WHERE url = ?").run(normalized);
164
+ }
165
+ return true;
166
+ }
167
+ const status = row.task_id ? probeStatus(taskStatus, row.task_id).status : undefined;
168
+ db.query("UPDATE intake_events SET status = ? WHERE url = ? AND status IN ('pending', 'scheduled')")
169
+ .run(status === 'failed' || status === 'cancelled' || status === 'superseded' ? 'failed' : 'expired', normalized);
170
+ return false;
171
+ } finally {
172
+ db.close();
173
+ }
174
+ }
175
+
32
176
  function appendJsonl(file: string, rows: unknown[]): void {
33
177
  if (!rows.length) return;
34
178
  mkdirSync(join(file, '..'), { recursive: true });