elanous 0.2.18 → 0.2.20

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-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
  14. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
  65. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  66. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  67. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -1
  87. package/apps/pwa/out/decisions/index.txt +14 -14
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +16 -15
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +16 -15
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +24 -23
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/demo/editor-recipes.yaml +22 -0
  160. package/graphs/ops/guardian.yaml +26 -0
  161. package/graphs/ops/recipes.yaml +3 -0
  162. package/graphs/retro/daily.yaml +25 -0
  163. package/graphs/retro/recipes.yaml +24 -0
  164. package/package.json +1 -1
  165. package/scripts/ask-marker-check.ts +3 -1
  166. package/scripts/coord-channel-watch.sh +2 -2
  167. package/scripts/discord-seat-probe.ts +3 -1
  168. package/scripts/land-global-checks.ts +89 -0
  169. package/scripts/lib/pch-regress-map.ts +24 -0
  170. package/scripts/morning-report.ts +18 -4
  171. package/scripts/pch-regress.ts +88 -0
  172. package/scripts/release-loop/gate-diff.ts +18 -0
  173. package/scripts/release-loop/gate-node.ts +628 -65
  174. package/scripts/release-loop/gate-partial.ts +106 -0
  175. package/scripts/release-loop/gate-timing.ts +229 -0
  176. package/scripts/release-loop/preflight.ts +6 -4
  177. package/scripts/release-loop/publish-window.ts +141 -0
  178. package/scripts/release-loop/release-docs.ts +1 -4
  179. package/scripts/release-loop/release-universe.ts +38 -0
  180. package/scripts/release-loop/resume-release.ts +67 -5
  181. package/scripts/release-loop/shard-plan.ts +65 -0
  182. package/scripts/release-loop/unattended-release.ts +12 -2
  183. package/scripts/release-story/draft.ts +5 -1
  184. package/scripts/retro/shadow-compare.ts +71 -0
  185. package/scripts/rhythm/daily.ts +6 -1
  186. package/scripts/video/scene-table.ts +159 -0
  187. package/src/acp/core-turn-bridge.ts +39 -2
  188. package/src/agent-mission/driver.ts +34 -6
  189. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  190. package/src/autopilot/build/merge-intent.ts +135 -7
  191. package/src/away/presence.ts +40 -0
  192. package/src/away/release-watch.ts +272 -0
  193. package/src/chat/index.ts +2 -1
  194. package/src/chat/short-question.ts +3 -40
  195. package/src/cli/away-cli.ts +68 -0
  196. package/src/cli/config-drift.ts +513 -0
  197. package/src/cli/guardian-cli.ts +49 -0
  198. package/src/cli/intake-cli.ts +13 -7
  199. package/src/cli/log-event-names.ts +5 -0
  200. package/src/cli/pod-cli.ts +5 -1
  201. package/src/cli/pr-cli.ts +20 -0
  202. package/src/cli/pty-drive-cli.ts +14 -3
  203. package/src/cli/release-cli.ts +52 -12
  204. package/src/cli/retro-cli.ts +78 -0
  205. package/src/cli/setup-cli.ts +31 -1
  206. package/src/cli/tasks-cli.ts +193 -14
  207. package/src/context-bus/context-now-public.ts +1 -1
  208. package/src/context-bus/context-now-surfaces.ts +16 -6
  209. package/src/context-bus/context-now.ts +97 -3
  210. package/src/core-turn/short-question.ts +100 -0
  211. package/src/dashboard/dashboard-status-lines.ts +24 -4
  212. package/src/dashboard/index.ts +50 -5
  213. package/src/dashboard/input/slash-executor.ts +6 -2
  214. package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
  215. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
  217. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  218. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  219. package/src/domains/guardian-tick.ts +138 -0
  220. package/src/domains/outbound-alert.ts +44 -13
  221. package/src/domains/outbound-log-probe.ts +6 -1
  222. package/src/domains/schedule-registry.ts +23 -0
  223. package/src/domains/telegram-kind-route.ts +39 -0
  224. package/src/domains/watch-default-scheduler.ts +95 -0
  225. package/src/esc-abort-gate.ts +44 -0
  226. package/src/graph-runner/graph-run-api.ts +205 -0
  227. package/src/graph-runner/runner.ts +36 -6
  228. package/src/harness/harness-cli-command.ts +420 -51
  229. package/src/harness/harness-queue.ts +315 -25
  230. package/src/harness/harness-salvage-cli.ts +140 -0
  231. package/src/harness/harness-worktree-add.ts +68 -0
  232. package/src/harness/ledger-write-guard.ts +6 -16
  233. package/src/hq/fence-outcomes.ts +63 -0
  234. package/src/hq/fence-wrapper.ts +33 -0
  235. package/src/hq/hq.ts +17 -2
  236. package/src/index.ts +118 -6
  237. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  238. package/src/instance/test-write-guard.ts +200 -0
  239. package/src/intake-plane/digest.ts +39 -15
  240. package/src/intake-plane/harness-target.ts +4 -3
  241. package/src/intake-plane/intake-sources.ts +1 -0
  242. package/src/intake-plane/items.ts +1 -0
  243. package/src/loops/orchestrator/degradation.ts +57 -0
  244. package/src/loops/orchestrator/finish-rate.ts +33 -3
  245. package/src/loops/orchestrator/tick.ts +166 -45
  246. package/src/maturity/feature-maturity.ts +6 -4
  247. package/src/maturity/tui-slash-maturity.ts +1 -0
  248. package/src/mss/logging/log-store.ts +3 -0
  249. package/src/nexus/api/graph-kinds.ts +10 -4
  250. package/src/nexus/api/graphs-api.ts +178 -6
  251. package/src/nexus/api/http-server.ts +54 -8
  252. package/src/nexus/api/loop-edges.ts +292 -17
  253. package/src/nexus/api/ops-api.ts +112 -29
  254. package/src/nexus/api/ops-board-route.ts +48 -0
  255. package/src/nexus/index.ts +7 -0
  256. package/src/onboarding/entry-hints.ts +5 -0
  257. package/src/onboarding.ts +24 -6
  258. package/src/release-loop/checklist.ts +2 -0
  259. package/src/release-loop/feature-store.ts +68 -18
  260. package/src/release-loop/gate-shards.ts +158 -0
  261. package/src/release-loop/light-rc.ts +173 -0
  262. package/src/release-loop/placement.ts +51 -17
  263. package/src/release-loop/release-schedule.ts +6 -2
  264. package/src/release-loop/rubric.ts +57 -1
  265. package/src/retro/daily.ts +372 -0
  266. package/src/seat-loop/op-decide.ts +173 -0
  267. package/src/seat-loop/seat-loop.ts +60 -3
  268. package/src/seat-loop/seat-report.ts +58 -3
  269. package/src/self-dev/ask-launch-flow.ts +5 -4
  270. package/src/self-dev/ask-launch-io.ts +49 -1
  271. package/src/self-dev/ask-path-candidates.ts +40 -2
  272. package/src/self-dev/dev-cli.ts +36 -0
  273. package/src/self-dev/draft-sweep.ts +371 -16
  274. package/src/self-dev/draft-triage-rules.ts +19 -4
  275. package/src/self-dev/orchestrate.ts +1 -0
  276. package/src/self-dev/pr-preview.ts +247 -0
  277. package/src/self-dev/shard-run.ts +23 -7
  278. package/src/self-implement/blocked-draft-policy.ts +101 -0
  279. package/src/self-implement/execution-clarification.ts +11 -0
  280. package/src/self-implement/gate-cli.ts +29 -8
  281. package/src/self-implement/gate-env-retry.ts +83 -0
  282. package/src/self-implement/goal-author-clarification.ts +88 -13
  283. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  284. package/src/self-implement/orchestrator.ts +220 -6
  285. package/src/self-implement/run-stop.ts +123 -0
  286. package/src/self-implement/run-ttl.ts +258 -0
  287. package/src/self-implement/salvage-branches.ts +254 -0
  288. package/src/self-implement/salvage-retention.ts +123 -0
  289. package/src/self-implement/seams.ts +4 -0
  290. package/src/self-implement/self-implement-cli.ts +2 -0
  291. package/src/self-implement/sibling-resync.ts +223 -0
  292. package/src/self-implement/stop-autoheal.ts +204 -0
  293. package/src/task-agent/actions.ts +63 -2
  294. package/src/task-agent/board.ts +132 -0
  295. package/src/task-agent/card-evidence.ts +292 -0
  296. package/src/task-agent/completion-evidence.ts +126 -0
  297. package/src/task-agent/judge.ts +25 -0
  298. package/src/task-agent/live-moves.ts +276 -0
  299. package/src/task-agent/mission.ts +33 -7
  300. package/src/task-agent/shadow.ts +192 -15
  301. package/src/task-agent/task-hand.ts +124 -17
  302. package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
  303. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  304. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
  308. package/src/telegram-away-command.ts +38 -0
  309. package/src/telegram-commands.ts +52 -0
  310. package/src/telegram-report-log-probe.ts +21 -0
  311. package/src/telegram-report.ts +27 -3
  312. package/src/testing/real-cli-spawn.ts +106 -0
  313. package/src/tui-client/dashboard-session.ts +1 -0
  314. package/src/user-config.ts +190 -5
  315. package/src/version/packed-revision.json +1 -1
  316. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  324. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
  349. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
  350. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -0,0 +1,13 @@
1
+ // Child fixture for test-write-guard.test.ts: a bare spy installed BEFORE outbound-alert is imported must not
2
+ // make the captured «real» curl look like a fake. Prints {refused, curlCalls}.
3
+ import { spyOn } from 'bun:test';
4
+ import * as childProcess from 'node:child_process';
5
+
6
+ const bare = spyOn(childProcess, 'execFileSync');
7
+ const { sendTelegramDirect } = await import('../domains/outbound-alert.js');
8
+ const { getUserConfig } = await import('../user-config.js');
9
+ const base = getUserConfig();
10
+ const config = { ...base, telegram: { ...base.telegram, channels: undefined, botToken: '2:fake', homeChannel: 2 } } as typeof base;
11
+ let refused = '';
12
+ try { sendTelegramDirect('private-alert-body', 'ops-alert', { config }); } catch (e) { refused = e instanceof Error ? e.name : String(e); }
13
+ console.log(JSON.stringify({ refused, curlCalls: bare.mock.calls.length }));
@@ -0,0 +1,200 @@
1
+ // Last line of defense: a test process never writes ops state or sends real alerts, even without the test preload.
2
+ // 10-07 incident: the public-export tree ran `bun test` without bunfig.toml (#24629), so tests wrote the ops deferred
3
+ // queue, ops logs.db and sent real Telegram messages. This guard does not depend on the preload or on HOME.
4
+ import { lstatSync, readFileSync, readlinkSync } from 'node:fs';
5
+ import { homedir, userInfo } from 'node:os';
6
+ import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
7
+ import { debug } from '../debug/log.js';
8
+
9
+ export class TestOpsWriteRefusedError extends Error {
10
+ constructor(what: string, path: string, signal: string) {
11
+ super(`[test-guard] refused: a test process (${signal}) tried to ${what} in the operational root (${path}). `
12
+ + 'Point the test at an isolated root (ELANOUS_STATE_DIR / --config-dir tmp) or inject a fake sender.');
13
+ this.name = 'TestOpsWriteRefusedError';
14
+ }
15
+ }
16
+
17
+ const TEST_FILE = /\.(test|spec)\.[cm]?[jt]sx?$/;
18
+
19
+ /** Which signal marks this process as a test, or null for a real run.
20
+ * Measured under bun 1.4.2: `bun test` sets NODE_ENV=test unless the caller set another value, and `Bun.main` is the
21
+ * running test file (argv carries no «test» word). ELANOUS_TEST_HOME is set by the deterministic runner and inherited
22
+ * by children it spawns. A real CLI/daemon has none of the three. */
23
+ export function testProcessSignal(env: NodeJS.ProcessEnv = process.env, main: string | undefined = bunMain()): string | null {
24
+ if (env.NODE_ENV === 'test') return 'NODE_ENV=test';
25
+ if (env.ELANOUS_TEST_HOME?.trim()) return 'ELANOUS_TEST_HOME';
26
+ if (main && TEST_FILE.test(main)) return 'Bun.main=test-file';
27
+ return null;
28
+ }
29
+
30
+ function isTestProcess(): boolean { return testProcessSignal() !== null; }
31
+
32
+ function bunMain(): string | undefined {
33
+ try { return (globalThis as { Bun?: { main?: string } }).Bun?.main; } catch { return undefined; }
34
+ }
35
+
36
+ /** Real path of `path` with the kernel's lookup order: components left to right, each symlink (live or dangling)
37
+ * expanded before a later `..` applies — `alias/../x` with `alias → ops/child` is `ops/x`. Missing tails are kept. */
38
+ export function physicalPath(path: string): string {
39
+ const pending = (isAbsolute(path) ? path : `${process.cwd()}/${path}`).split('/').filter(Boolean);
40
+ let current = '/';
41
+ let links = 0;
42
+ while (pending.length) {
43
+ const part = pending.shift()!;
44
+ if (part === '.') continue;
45
+ if (part === '..') { current = dirname(current); continue; }
46
+ const next = current === '/' ? `/${part}` : `${current}/${part}`;
47
+ let target: string | null = null;
48
+ try { if (lstatSync(next).isSymbolicLink()) target = readlinkSync(next); } catch { /* missing: keep the name */ }
49
+ if (target !== null && links++ < 40) {
50
+ pending.unshift(...target.split('/').filter(Boolean));
51
+ if (isAbsolute(target)) current = '/';
52
+ continue;
53
+ }
54
+ current = next;
55
+ }
56
+ return current;
57
+ }
58
+
59
+ type SpawnSync = (cmd: string[]) => { exitCode: number; stdout: { toString(): string } };
60
+
61
+ /** The OS account's home from the account database, not HOME — test runners redirect HOME. Null when unresolvable. */
62
+ function lookupAccountHome(): string | null {
63
+ try {
64
+ const spawnSync = (globalThis as { Bun?: { spawnSync: SpawnSync } }).Bun?.spawnSync;
65
+ const user = userInfo().username;
66
+ const uid = process.getuid?.();
67
+ const valid = (home: string | undefined): string | null => (home && isAbsolute(home) ? home : null);
68
+ if (process.platform === 'darwin' && spawnSync) {
69
+ const r = spawnSync(['/usr/bin/dscl', '.', '-read', `/Users/${user}`, 'NFSHomeDirectory']);
70
+ const home = valid(r.exitCode === 0 ? r.stdout.toString().trim().split(/\s+/).at(-1) : undefined);
71
+ if (home) return home;
72
+ }
73
+ if (process.platform !== 'darwin' && uid !== undefined) {
74
+ try {
75
+ const home = valid(readFileSync('/etc/passwd', 'utf8').split('\n')
76
+ .map((line) => line.split(':')).find((f) => Number(f[2]) === uid)?.[5]);
77
+ if (home) return home;
78
+ } catch { /* try getent */ }
79
+ if (spawnSync) {
80
+ const r = spawnSync(['getent', 'passwd', String(uid)]);
81
+ const home = valid(r.exitCode === 0 ? r.stdout.toString().trim().split(':')[5] : undefined);
82
+ if (home) return home;
83
+ }
84
+ }
85
+ } catch { /* unresolvable */ }
86
+ return null;
87
+ }
88
+
89
+ let accountHomeLookup: () => string | null = lookupAccountHome;
90
+ let opsRootsOverride: string[] | null = null; // added roots, never a replacement
91
+ let cachedOpsRoots: string[] | null = null;
92
+
93
+ /** Tests add «ops-like» roots to prove the refusal; the real account root stays protected. null clears them. */
94
+ export function setOpsRootsForTesting(roots: string[] | null): void { opsRootsOverride = roots ? roots.map(physicalPath) : null; }
95
+
96
+ /** Tests inject a failing account lookup to prove the fallback; null restores the real one. Clears the cache. */
97
+ export function setAccountHomeLookupForTesting(lookup: (() => string | null) | null): void {
98
+ accountHomeLookup = lookup ?? lookupAccountHome;
99
+ cachedOpsRoots = null;
100
+ accountHomeUnresolved = false;
101
+ }
102
+
103
+ /** The operational instance root(s) the guard protects — the account's real `~/.elanous`.
104
+ * When the account database cannot be read, fail toward protection: every conventional home of this user plus HOME. */
105
+ export function opsRoots(): string[] {
106
+ // Widening only: injected roots and the env list add to the real account root; nothing can remove it.
107
+ const extra = (process.env[EXTRA_OPS_ROOTS_ENV] ?? '').split(delimiter).filter((p) => isAbsolute(p)).map(physicalPath);
108
+ const added = [...(opsRootsOverride ?? []), ...extra];
109
+ return added.length ? [...new Set([...accountOpsRoots(), ...added])] : accountOpsRoots();
110
+ }
111
+
112
+ /** Extra roots the guard protects in this process (path-delimited). Used to prove the guard in a real child entry. */
113
+ export const EXTRA_OPS_ROOTS_ENV = 'ELANOUS_TEST_GUARD_EXTRA_OPS_ROOTS';
114
+
115
+ function accountOpsRoots(): string[] {
116
+ if (cachedOpsRoots) return cachedOpsRoots;
117
+ const home = accountHomeLookup();
118
+ let homes: string[];
119
+ if (home) homes = [home];
120
+ else {
121
+ let user = '';
122
+ try { user = userInfo().username; } catch { /* unknown user */ }
123
+ homes = [...(user ? [`/Users/${user}`, `/home/${user}`] : []), ...(process.getuid?.() === 0 ? ['/root'] : []), homedir()];
124
+ try { debug.log('test-guard', 'account-home-unresolved', { protected: homes.length }); } catch { /* observation only */ }
125
+ }
126
+ accountHomeUnresolved = !home;
127
+ cachedOpsRoots = [...new Set(homes.map((h) => physicalPath(join(h, '.elanous'))))];
128
+ return cachedOpsRoots;
129
+ }
130
+
131
+ let accountHomeUnresolved = false;
132
+
133
+ export function isInsideOpsRoot(path: string, roots: string[] = opsRoots()): boolean {
134
+ const target = physicalPath(path);
135
+ // Unknown account home (non-standard, unreadable): any `<dir>/.elanous` tree may be the ops root — protect them all.
136
+ // Isolated test roots are temp dirs or `<root>/state`, never a directory named `.elanous` under a home.
137
+ // Both spellings: a `.elanous` that is itself a symlink to an oddly named directory is still that tree.
138
+ if (accountHomeUnresolved && [resolve(path), target].some((p) => p.split(sep).includes('.elanous'))) return true;
139
+ return roots.some((root) => {
140
+ const rel = relative(root, target);
141
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
142
+ });
143
+ }
144
+
145
+ function refuse(what: string, path: string, signal: string): never {
146
+ try { debug.log('test-guard', 'ops-write-refused', { what, path, signal }); } catch { /* the refusal stands */ }
147
+ throw new TestOpsWriteRefusedError(what, path, signal);
148
+ }
149
+
150
+ /** Throw when a test process is about to write `path` inside the operational root. No-op in real runs. */
151
+ export function assertNotTestWritingOps(path: string, what: string): void {
152
+ const signal = testProcessSignal();
153
+ if (!signal) return;
154
+ if (path === ':memory:' || !isInsideOpsRoot(path)) return;
155
+ refuse(what, path, signal);
156
+ }
157
+
158
+ /** Throw when a test process is about to perform a network send it must not make.
159
+ * - Routing (config dir · state root · conatus dir · acp-token dir) inside the ops root: always refused, faked or
160
+ * not — a test that resolved the ops universe is the leak itself, whatever its transport does.
161
+ * - `external` (a third-party bot API — Telegram, Discord): refused unless the transport is a test double that
162
+ * replaces the real one (`transportFaked`), whatever root the credentials came from. A double that deliberately
163
+ * calls the original is an explicit choice to send and is out of this guard's reach. */
164
+ export function assertNoRealSendFromTest(what: string, opts: {
165
+ transportFaked: boolean; routingRoots: string[]; external?: boolean;
166
+ }): void {
167
+ const signal = testProcessSignal();
168
+ if (!signal) return;
169
+ const opsRoot = opts.routingRoots.find((root) => root && isInsideOpsRoot(root));
170
+ if (opsRoot) refuse(what, opsRoot, signal);
171
+ if (opts.external && !opts.transportFaked) refuse(what, 'a real third-party bot API', signal);
172
+ }
173
+
174
+ /** True when `fn` is a bun:test/jest double whose behaviour replaces `real`. A bare `spyOn` keeps calling the
175
+ * original (its implementation IS `real`), so it is not a fake. */
176
+ export function isTestDouble(fn: unknown, real?: unknown): boolean {
177
+ if (!isMockFn(fn)) return false;
178
+ const impl = fn.getMockImplementation?.();
179
+ return real === undefined || impl !== unwrapMock(real);
180
+ }
181
+
182
+ type MockFn = { mock: object; getMockImplementation?: () => unknown };
183
+ function isMockFn(fn: unknown): fn is MockFn {
184
+ return typeof fn === 'function' && typeof (fn as { mock?: unknown }).mock === 'object' && (fn as { mock?: unknown }).mock !== null;
185
+ }
186
+
187
+ /** The function under a chain of spies — a «real» captured after a bare spy was installed is the spy itself.
188
+ * Unwrapping a fake yields the fake, so a capture taken under a fake refuses rather than allows. */
189
+ function unwrapMock(fn: unknown): unknown {
190
+ let current = fn;
191
+ for (let i = 0; i < 8 && isMockFn(current); i++) {
192
+ const impl: unknown = current.getMockImplementation?.();
193
+ if (typeof impl !== 'function' || impl === (current as unknown)) break;
194
+ current = impl;
195
+ }
196
+ return current;
197
+ }
198
+
199
+ // Resolve the account home while no test has mocked anything yet; real runs never pay the lookup.
200
+ try { if (typeof process !== 'undefined' && isTestProcess()) opsRoots(); } catch { /* resolved lazily */ }
@@ -1,14 +1,15 @@
1
1
  // 흡수 하루 다이제스트 — 그날 흡수한 것을 사람이 «발견»하는 자리. 노트 절(마크다운) ⊕ 텔레그램 짧은 판.
2
2
  // 결정론만: 요약 한 줄은 각 노트가 이미 쓴 「한 줄 결론」을 뽑는다(LLM 을 다시 부르지 않는다).
3
- import { existsSync, readFileSync } from 'node:fs';
4
- import { basename, join } from 'node:path';
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
4
+ import { basename, dirname, join } from 'node:path';
5
5
  import { loadIntakeLedger, type IntakeItem } from './items.js';
6
+ import { canonicalSeat } from './intake-sources.js';
6
7
  import { readSavedCursorAt } from './collect-telegram-saved.js';
7
8
  import { intakeOutboxDir, kstDay } from './route.js';
8
9
 
9
10
  export type DigestLensVerdict = '대체 후보' | '보강' | '경쟁 대조';
10
11
  export interface DigestEntry { id: string; sources: string[]; url?: string; note?: string; noteName?: string; oneLiner?: string; axis: string; impact?: { verdict: DigestLensVerdict; why: string; target: string } }
11
- export interface IntakeDigest { day: string; absorbed: DigestEntry[]; goals: { fact: string; url?: string }[]; news?: { title: string; url: string; summary: string[]; implication: string[] }[]; shadowSuggestions?: { fact: string; url?: string; verdict: string }[]; review?: { fact: string; note?: string }[]; grounding: number; release: number; manual: number; savedSilence?: { days: number; lastNewAt: string } }
12
+ export interface IntakeDigest { day: string; seat?: string; absorbed: DigestEntry[]; goals: { fact: string; url?: string }[]; news?: { title: string; url: string; summary: string[]; implication: string[] }[]; shadowSuggestions?: { fact: string; url?: string; verdict: string }[]; review?: { fact: string; note?: string }[]; grounding: number; release: number; manual: number; savedSilence?: { days: number; lastNewAt: string } }
12
13
 
13
14
  const AXIS_LABEL: Record<string, string> = {
14
15
  video_automation: '영상 자동화', agent_basics: '에이전트 기본', agent_applied: '에이전트 응용', unrelated: '그 밖',
@@ -51,9 +52,27 @@ export function pathOnly(why: string): boolean {
51
52
  return rest.length < 4;
52
53
  }
53
54
 
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 {
55
+ /** Seat briefing receipts — `outbox/brief/<seat>.jsonl` rows `{ id, day }`: an item already sent on an earlier day is not «new» again. */
56
+ export function seatBriefFile(root: string, seat: string): string { return join(intakeOutboxDir(root), 'brief', `${seat}.jsonl`); }
57
+
58
+ /** Record the items a seat briefing delivered (called only after a successful send). */
59
+ export function recordSeatBriefed(root: string, seat: string, day: string, ids: readonly string[]): void {
60
+ if (!ids.length) return;
61
+ const file = seatBriefFile(root, seat);
62
+ mkdirSync(dirname(file), { recursive: true });
63
+ appendFileSync(file, ids.map((id) => JSON.stringify({ id, day })).join('\n') + '\n');
64
+ }
65
+
66
+ export function buildIntakeDigest(root: string, day: string, readFile: (p: string) => string | undefined = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : undefined), now: Date = new Date(), seat?: string): IntakeDigest {
67
+ const selectedSeat = seat === undefined ? undefined : canonicalSeat(seat);
68
+ // Re-collecting the same search result moves lastSeenAt to today; without receipts it would be «new» every morning.
69
+ const briefedEarlier = new Set(selectedSeat
70
+ ? readJsonl(seatBriefFile(root, selectedSeat)).filter((r) => typeof r.id === 'string' && r.day !== day).map((r) => r.id as string)
71
+ : []);
55
72
  const items = [...loadIntakeLedger(root).items.values()]
56
- .filter((i) => (i.status === 'absorbed' || i.status === 'routed') && kstDay(i.lastSeenAt) === day && i.outputs.some((o) => o.kind === 'note'));
73
+ .filter((i) => (selectedSeat === undefined || (i.seat === selectedSeat && !briefedEarlier.has(i.id)))
74
+ && (selectedSeat ? i.status !== 'discarded' : (i.status === 'absorbed' || i.status === 'routed') && i.outputs.some((o) => o.kind === 'note'))
75
+ && kstDay(i.lastSeenAt) === day);
57
76
  const out = intakeOutboxDir(root);
58
77
  const relevant = new Map<string, NonNullable<DigestEntry['impact']>>();
59
78
  // The lens (src/intake-plane/lens.ts) writes its verdicts to outbox/lens/<day>.jsonl; route folders carry none.
@@ -76,13 +95,13 @@ export function buildIntakeDigest(root: string, day: string, readFile: (p: strin
76
95
  return {
77
96
  id: i.id, sources: i.sources, ...(i.url ? { url: i.url } : {}),
78
97
  ...(note ? { note, noteName: basename(note, '.md') } : {}),
79
- ...(md ? { oneLiner: noteOneLiner(md) } : {}),
98
+ ...(md ? { oneLiner: noteOneLiner(md) } : selectedSeat && i.title ? { oneLiner: i.title } : {}),
80
99
  axis: AXIS_LABEL[i.judgement?.axis ?? i.axis ?? ''] ?? (i.sources.includes('telegram-saved') ? '내가 저장한 것' : '그 밖'),
81
100
  ...(impact ? { impact } : {}),
82
101
  };
83
102
  });
84
- const goals = readJsonl(join(out, 'goals', `${day}.jsonl`)).map((g) => ({ fact: String(g.fact ?? ''), ...(g.url ? { url: String(g.url) } : {}) }));
85
- const lensRows = readJsonl(join(out, 'lens', `${day}.jsonl`));
103
+ const goals = selectedSeat ? [] : readJsonl(join(out, 'goals', `${day}.jsonl`)).map((g) => ({ fact: String(g.fact ?? ''), ...(g.url ? { url: String(g.url) } : {}) }));
104
+ const lensRows = selectedSeat ? [] : readJsonl(join(out, 'lens', `${day}.jsonl`));
86
105
  const news = lensRows.flatMap((row) =>
87
106
  typeof row.title === 'string' && typeof row.url === 'string' && Array.isArray(row.summary) && Array.isArray(row.implication)
88
107
  ? [{ title: row.title, url: row.url, summary: row.summary.filter((s): s is string => typeof s === 'string'), implication: row.implication.filter((s): s is string => typeof s === 'string') }]
@@ -94,15 +113,15 @@ export function buildIntakeDigest(root: string, day: string, readFile: (p: strin
94
113
  if (suggestion.status !== '제안' || typeof suggestion.fact !== 'string' || !suggestion.fact.trim()) return [];
95
114
  return [{ fact: suggestion.fact, ...(typeof row.url === 'string' ? { url: row.url } : {}), verdict: typeof suggestion.verdict === 'string' ? suggestion.verdict : '판단 필요' }];
96
115
  }) : []);
97
- const review = readJsonl(join(out, 'review', `${day}.jsonl`)).map((r) => ({ fact: String(r.fact ?? ''), ...(r.note ? { note: String(r.note) } : {}) }));
98
- const grounding = readJsonl(join(out, 'grounding.jsonl')).filter((g) => typeof g.at === 'string' && kstDay(g.at) === day).length;
99
- const lastNewAt = readSavedCursorAt(root);
116
+ const review = selectedSeat ? [] : readJsonl(join(out, 'review', `${day}.jsonl`)).map((r) => ({ fact: String(r.fact ?? ''), ...(r.note ? { note: String(r.note) } : {}) }));
117
+ const grounding = selectedSeat ? 0 : readJsonl(join(out, 'grounding.jsonl')).filter((g) => typeof g.at === 'string' && kstDay(g.at) === day).length;
118
+ const lastNewAt = selectedSeat ? undefined : readSavedCursorAt(root);
100
119
  const silentMs = lastNewAt ? now.getTime() - Date.parse(lastNewAt) : NaN;
101
120
  return {
102
- day, absorbed, goals, ...(news.length ? { news } : {}), ...(shadowSuggestions.length ? { shadowSuggestions } : {}), review, grounding,
103
- release: readJsonl(join(out, 'release', `${day}.jsonl`)).length,
104
- manual: readJsonl(join(out, 'manual', `${day}.jsonl`)).length,
105
- ...(lastNewAt && silentMs > SAVED_SILENCE_MS ? { savedSilence: { days: Math.floor(silentMs / 86_400_000), lastNewAt } } : {}),
121
+ day, ...(selectedSeat ? { seat: selectedSeat } : {}), absorbed, goals, ...(news.length ? { news } : {}), ...(shadowSuggestions.length ? { shadowSuggestions } : {}), review, grounding,
122
+ release: selectedSeat ? 0 : readJsonl(join(out, 'release', `${day}.jsonl`)).length,
123
+ manual: selectedSeat ? 0 : readJsonl(join(out, 'manual', `${day}.jsonl`)).length,
124
+ ...(!selectedSeat && lastNewAt && silentMs > SAVED_SILENCE_MS ? { savedSilence: { days: Math.floor(silentMs / 86_400_000), lastNewAt } } : {}),
106
125
  };
107
126
  }
108
127
 
@@ -146,6 +165,11 @@ export function renderDigestMarkdown(d: IntakeDigest): string {
146
165
 
147
166
  /** 텔레그램 — 흡수 렌즈가 대조한 항목만 SCQA 짧은 판으로 낸다. */
148
167
  export function renderDigestTelegram(d: IntakeDigest, _opts: { vaultRoot?: string; notePath?: string } = {}): string {
168
+ if (d.seat && !d.absorbed.length) return '오늘 새 소식 없음';
169
+ if (d.seat) return [
170
+ `흡수 ${d.absorbed.length}`,
171
+ ...d.absorbed.map((item) => `- ${item.oneLiner ?? item.noteName ?? item.id}${item.url ? ` · ${item.url}` : ''}`),
172
+ ].join('\n');
149
173
  // «Touching» is decided by the lens verdict alone; a missing note summary only changes how S reads (ACP must-fix).
150
174
  const touching = d.absorbed.filter((e) => e.impact);
151
175
  const L = [`흡수 ${d.absorbed.length} → 우리에게 닿는 것 ${touching.length}`];
@@ -3,14 +3,15 @@ export type DaemonHarnessTarget =
3
3
  | { ok: false; reason: string };
4
4
 
5
5
  /** Read-only target selection. An explicitly configured path must never fall back to the daemon cwd. */
6
- export function resolveDaemonHarnessTarget({ configured, cwd, isGitRepo }: {
6
+ export function resolveDaemonHarnessTarget({ configured, cwd, isGitRepo, allowNonGit = false }: {
7
7
  configured?: string;
8
8
  cwd: string;
9
9
  isGitRepo: (path: string) => boolean;
10
+ allowNonGit?: boolean;
10
11
  }): DaemonHarnessTarget {
11
12
  if (configured !== undefined) {
12
- if (isGitRepo(configured)) return { ok: true, repo: configured, source: 'config' };
13
- } else if (isGitRepo(cwd)) {
13
+ if (allowNonGit || isGitRepo(configured)) return { ok: true, repo: configured, source: 'config' };
14
+ } else if (allowNonGit || isGitRepo(cwd)) {
14
15
  return { ok: true, repo: cwd, source: 'cwd' };
15
16
  }
16
17
  return {
@@ -20,6 +20,7 @@ export interface RegisteredIntakeSource {
20
20
  }
21
21
 
22
22
  export function canonicalSeat(value: string): string {
23
+ if (value.trim() === 'user') return 'user';
23
24
  const seat = findTrack(value.trim());
24
25
  if (!seat) throw new Error(`알 수 없는 자리: ${value}`);
25
26
  return seat.id;
@@ -84,6 +84,7 @@ export function canonicalUrl(raw: string): string | undefined {
84
84
  function normalizedSeat(seat: unknown): string | undefined {
85
85
  if (seat === undefined) return undefined;
86
86
  if (typeof seat !== 'string') throw new Error(`알 수 없는 자리: ${String(seat)}`);
87
+ if (seat.trim() === 'user') return 'user';
87
88
  const track = findTrack(seat.trim());
88
89
  if (!track) throw new Error(`알 수 없는 자리: ${seat}`);
89
90
  return track.id;
@@ -0,0 +1,57 @@
1
+ // COORD-HA (0.2.19) — the signal the orchestrator tick reads for its degradation ladder (RFC loop map §A8b ④⑤):
2
+ // L1 = no usable LLM (rules only) · L2 = the task board cannot take a write (no new assignment). The tick already acts on
3
+ // `TickDeps.degradation`; until now nothing supplied it in production. Each probe answers «false» only on a measured
4
+ // failure — an unknown is not a degradation.
5
+ import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { randomUUID } from 'node:crypto';
8
+ import { Database } from 'bun:sqlite';
9
+ import { cardIndexPath, taskCardsDir } from '../../task-cards/card-store.js';
10
+ import { resolveUsableLlm } from '../../llm/usable-llm.js';
11
+
12
+ export interface DegradationSignal { llm: boolean; board: boolean; why: string[] }
13
+
14
+ /** The task board takes a write: its folder is a directory a file can be created in (created and removed — a permission
15
+ * bit is not proof · review r1), and its index can take a write lock (busy 2 s · rolled back). */
16
+ export function probeBoardWritable(root: string): { ok: boolean; why?: string } {
17
+ const dir = taskCardsDir(root);
18
+ try {
19
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
20
+ if (!statSync(dir).isDirectory()) return { ok: false, why: 'board dir not writable: not a directory' };
21
+ // A fresh name each time — a leftover from a crashed probe can never make a writable board look full (review r2).
22
+ const probe = join(dir, `.degradation-probe-${randomUUID()}`);
23
+ try { writeFileSync(probe, '', { flag: 'wx', mode: 0o600 }); }
24
+ finally { rmSync(probe, { force: true }); }
25
+ } catch (error) { return { ok: false, why: `board dir not writable: ${(error as NodeJS.ErrnoException).code ?? String(error)}` }; }
26
+ const index = cardIndexPath(root);
27
+ // No index yet: the card store creates it on first open — a writable folder is enough.
28
+ if (!existsSync(index)) return { ok: true };
29
+ let db: Database | undefined;
30
+ try {
31
+ db = new Database(index, { readwrite: true, create: false });
32
+ db.exec('PRAGMA busy_timeout = 2000');
33
+ db.exec('BEGIN IMMEDIATE');
34
+ db.exec('ROLLBACK');
35
+ return { ok: true };
36
+ } catch (error) {
37
+ return { ok: false, why: `board index write lock: ${String(error instanceof Error ? error.message : error).slice(0, 160)}` };
38
+ } finally { try { db?.close(); } catch { /* closing a failed handle is best effort */ } }
39
+ }
40
+
41
+ export interface DegradationDeps {
42
+ usableLlm?: () => { usable: boolean; why: string };
43
+ board?: (root: string) => { ok: boolean; why?: string };
44
+ }
45
+
46
+ export function probeDegradation(root: string, deps: DegradationDeps = {}): DegradationSignal {
47
+ const why: string[] = [];
48
+ let llm = true;
49
+ try {
50
+ const usable = (deps.usableLlm ?? resolveUsableLlm)();
51
+ llm = usable.usable;
52
+ if (!llm) why.push(`llm: ${usable.why}`);
53
+ } catch (error) { why.push(`llm unknown: ${String(error).slice(0, 120)}`); }
54
+ const board = (deps.board ?? probeBoardWritable)(root);
55
+ if (!board.ok) why.push(board.why ?? 'board not writable');
56
+ return { llm, board: board.ok, why };
57
+ }
@@ -15,6 +15,8 @@ export type FinishMetric = 'launched' | 'landed' | 'landingRate' | 'staleDrafts'
15
15
  export type FinishMetrics = {
16
16
  launched: number | null;
17
17
  launchedUnreadable?: number;
18
+ /** Launched Pod runs whose child ledger never came back (`pod-ledger-incomplete`) — counted in `launched`, result unknown. */
19
+ launchedIncomplete?: number;
18
20
  landed: number | null;
19
21
  landingRate: number | null;
20
22
  staleDrafts: number | null;
@@ -97,7 +99,9 @@ export function recordFinishHistory(root: string, metrics: FinishMetrics, now: D
97
99
  }
98
100
 
99
101
  type Start = { runId: string; startedAt: string };
100
- type StartObservation = { starts: readonly Start[]; unreadable: readonly { runId: string; reason: string }[]; unreadableDirectories?: number };
102
+ type StartObservation = { starts: readonly Start[]; unreadable: readonly { runId: string; reason: string }[]; unreadableDirectories?: number;
103
+ /** Runs counted as launched although their child ledger never came back (reason = the host marker's data.reason). */
104
+ incomplete?: readonly { runId: string; reason: string }[] };
101
105
  type Pr = { number: number; headRefName: string; mergedAt?: string; createdAt?: string; mergeable?: string; labels?: { name: string }[] };
102
106
  export interface MeasureFinishDeps {
103
107
  listStarts?: (cutoff: number) => StartObservation;
@@ -113,6 +117,7 @@ function defaultListStarts(cutoff: number, targets?: readonly LogTarget[]): Star
113
117
  let unreadableDirectories = 0;
114
118
  let unresolvedDirectories = 0;
115
119
  const launchOnlyIds = new Set<string>();
120
+ const incomplete = new Map<string, { runId: string; reason: string }>();
116
121
  for (const { ledgerDirectory } of resolveFederatedRunLedgerTargets({ includeTest: true, ...(targets ? { targets } : {}) })) {
117
122
  let dir: string;
118
123
  try {
@@ -143,6 +148,20 @@ function defaultListStarts(cutoff: number, targets?: readonly LogTarget[]): Star
143
148
  continue;
144
149
  }
145
150
  const start = entries.find(entry => entry.event === 'start');
151
+ // ORCH-LIVE-1008 ①: the host writes `pod-ledger-incomplete` on a Pod child run's ledger when the child's own ledger
152
+ // never came back (OOM · logs unavailable · partial collection). The Job did run — it is a launch with an unknown
153
+ // result, not a ledger without a start. Its time is the marker's timestamp, else the ledger mtime.
154
+ const incompleteMarker = !start ? entries.find(entry => entry.event === 'pod-ledger-incomplete') : undefined;
155
+ if (incompleteMarker) {
156
+ // Only a real `start` (from any directory) outranks the marker; a host launch-only record does not — the same
157
+ // run read as launch-only first is still a launch whose result never came back.
158
+ if (starts.has(runId) && !launchOnlyIds.has(runId)) continue;
159
+ if (!starts.has(runId)) starts.set(runId, { runId, startedAt: incompleteMarker.timestamp ?? new Date(statSync(path).mtimeMs).toISOString() });
160
+ const why = (incompleteMarker.data as { reason?: unknown } | undefined)?.reason;
161
+ incomplete.set(runId, { runId, reason: typeof why === 'string' && why ? why : 'unknown' });
162
+ unreadable.delete(runId);
163
+ continue;
164
+ }
146
165
  // A Pod-dispatched launch leaves only a host launch record (launch-quota-policy · author-on-pod-receipt) — the run
147
166
  // itself starts inside the Pod. That is still a launch: its time is the record's timestamp, else the ledger mtime.
148
167
  const launchRecord = !start ? entries.find(entry => LAUNCH_ONLY_EVENTS.has(entry.event)) : undefined;
@@ -157,12 +176,14 @@ function defaultListStarts(cutoff: number, targets?: readonly LogTarget[]): Star
157
176
  if (launchRecord) launchOnlyIds.add(runId); else launchOnlyIds.delete(runId);
158
177
  starts.set(runId, { runId, startedAt });
159
178
  unreadable.delete(runId);
179
+ incomplete.delete(runId);
160
180
  }
161
181
  }
162
182
  debug.log('loop.orchestrator', 'finish-launch-population', {
163
183
  directories: directories.size + unresolvedDirectories, launched: starts.size, launchOnly: launchOnlyIds.size, childLedgers: childLedgers.size, unreadable: unreadable.size,
184
+ ...(incomplete.size > 0 ? { incomplete: incomplete.size } : {}),
164
185
  });
165
- return { starts: [...starts.values()], unreadable: [...unreadable.values()], unreadableDirectories };
186
+ return { starts: [...starts.values()], unreadable: [...unreadable.values()], unreadableDirectories, incomplete: [...incomplete.values()] };
166
187
  }
167
188
 
168
189
  function defaultRunGh(args: string[]): string {
@@ -184,6 +205,7 @@ export function measureFinish(deps: MeasureFinishDeps = {}, now: Date = new Date
184
205
  const cutoff = now.getTime() - DAY_MS;
185
206
  let launched: number | null = null;
186
207
  let launchedUnreadable = 0;
208
+ let launchedIncomplete = 0;
187
209
  let landed: number | null = null;
188
210
  let staleDrafts: number | null = null;
189
211
  let conflictRatio: number | null = null;
@@ -196,6 +218,14 @@ export function measureFinish(deps: MeasureFinishDeps = {}, now: Date = new Date
196
218
  || observation.unreadable.some(entry => !entry || typeof entry.runId !== 'string' || typeof entry.reason !== 'string')) throw new Error('invalid run start observation');
197
219
  launched = new Set(observation.starts.filter(start => Date.parse(start.startedAt) >= cutoff && Date.parse(start.startedAt) <= now.getTime()).map(start => start.runId)).size;
198
220
  launchedUnreadable = observation.unreadable.length;
221
+ const incompleteRuns = (observation.incomplete ?? []).filter(entry => observation.starts.some(start => start.runId === entry.runId
222
+ && Date.parse(start.startedAt) >= cutoff && Date.parse(start.startedAt) <= now.getTime()));
223
+ launchedIncomplete = incompleteRuns.length;
224
+ if (launchedIncomplete > 0) {
225
+ const byReason: Record<string, number> = {};
226
+ for (const entry of incompleteRuns) byReason[entry.reason] = (byReason[entry.reason] ?? 0) + 1;
227
+ debug.log('loop.orchestrator', 'finish-ledger-incomplete', { count: launchedIncomplete, byReason, sample: incompleteRuns.slice(0, 5) });
228
+ }
199
229
  if (launchedUnreadable > 0) {
200
230
  reasons.launched = `${launchedUnreadable}개 원장 시작 줄 없음(하한값)`;
201
231
  debug.log('loop.orchestrator', 'finish-ledger-unreadable', { count: launchedUnreadable, sample: observation.unreadable.slice(0, 5) });
@@ -234,7 +264,7 @@ export function measureFinish(deps: MeasureFinishDeps = {}, now: Date = new Date
234
264
  : 'no harness runs launched in the last 24 hours';
235
265
  else if (landed > launched) reasons.landingRate = `landed exceeds launched — population mismatch (launched=${launched}, landed=${landed})`;
236
266
  else landingRate = landed / launched;
237
- return { launched, ...(launchedUnreadable > 0 ? { launchedUnreadable } : {}), landed, landingRate, staleDrafts, conflictRatio, unknownMergeable, reasons };
267
+ return { launched, ...(launchedUnreadable > 0 ? { launchedUnreadable } : {}), ...(launchedIncomplete > 0 ? { launchedIncomplete } : {}), landed, landingRate, staleDrafts, conflictRatio, unknownMergeable, reasons };
238
268
  }
239
269
 
240
270
  /** Advice only: no launch cap, PR, or config is changed. */