elanous 0.2.15 → 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 (254) 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-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  51. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  52. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +15 -15
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +15 -15
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +15 -15
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +15 -15
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +15 -15
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +15 -15
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +15 -15
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +15 -15
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +15 -15
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +15 -15
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +15 -15
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +15 -15
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +15 -15
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +15 -15
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +15 -15
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +15 -15
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +15 -15
  88. package/apps/pwa/out/loops/index.html +1 -1
  89. package/apps/pwa/out/loops/index.txt +15 -15
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +15 -15
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +15 -15
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +15 -15
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +15 -15
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +15 -15
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +15 -15
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +16 -16
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +15 -15
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +15 -15
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +15 -15
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +15 -15
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +15 -15
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +15 -15
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +15 -15
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +15 -15
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +15 -15
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +15 -15
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +15 -15
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +15 -15
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +15 -15
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +15 -15
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +15 -15
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +15 -15
  142. package/graphs/doc-gen/doc-gen.yaml +24 -0
  143. package/graphs/doc-gen/recipes.yaml +3 -0
  144. package/graphs/orchestrator/orchestrator.yaml +7 -0
  145. package/graphs/orchestrator/recipes.yaml +3 -0
  146. package/graphs/steward/recipes.yaml +2 -1
  147. package/package.json +1 -1
  148. package/scripts/ci-public-export-import-gate.ts +154 -0
  149. package/scripts/ci-test-interference-gate.ts +5 -1
  150. package/scripts/ci-typecheck-changed.ts +34 -15
  151. package/scripts/docs/doc-rot.ts +164 -0
  152. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  153. package/scripts/docs-cli-check.ts +4 -4
  154. package/scripts/goals/goal-census.ts +343 -0
  155. package/scripts/measure/author-section-contribution.ts +84 -0
  156. package/scripts/release-loop/cut-branch.ts +43 -13
  157. package/scripts/release-loop/gate-node.ts +3 -1
  158. package/scripts/release-loop/vault-note-node.ts +19 -2
  159. package/scripts/rhythm/monthly.ts +97 -0
  160. package/src/acp/client.ts +17 -2
  161. package/src/agent-mission/driver.ts +51 -1
  162. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  163. package/src/agent-mission/supervisor-metrics.ts +218 -0
  164. package/src/autopilot/track-agent-shadow.ts +2 -1
  165. package/src/briefing/brief-items.ts +191 -36
  166. package/src/budget/usage-outcomes.ts +204 -0
  167. package/src/cli/brief-cli.ts +73 -0
  168. package/src/cli/know-cli.ts +20 -0
  169. package/src/cli/logs-cli.ts +43 -1
  170. package/src/cli/pr-cli.ts +80 -12
  171. package/src/cli/release-cli.ts +11 -8
  172. package/src/cli/self-update.ts +200 -1
  173. package/src/cli/usage-cli.ts +26 -0
  174. package/src/discord-self-message.ts +9 -0
  175. package/src/flow-loop/split.ts +24 -0
  176. package/src/harness/harness-cli-command.ts +36 -2
  177. package/src/harness/harness-pod-dispatch.ts +25 -55
  178. package/src/harness/harness-seams.ts +13 -3
  179. package/src/harness/helper-repair.ts +139 -8
  180. package/src/harness/helper-scan.ts +13 -2
  181. package/src/harness/nested-elanous-policy.ts +114 -0
  182. package/src/hq/hq.ts +8 -2
  183. package/src/hq/lease.ts +5 -3
  184. package/src/hq/transfer.ts +185 -0
  185. package/src/index.ts +112 -6
  186. package/src/intake-plane/digest.ts +48 -26
  187. package/src/knowledge/know-find.ts +105 -0
  188. package/src/llm.ts +75 -19
  189. package/src/loops/orchestrator/finish-rate.ts +14 -3
  190. package/src/loops/orchestrator/tick.ts +96 -25
  191. package/src/maturity/bot-slash-catalog.ts +11 -2
  192. package/src/maturity/feature-maturity.ts +2 -2
  193. package/src/maturity/tui-slash-availability.ts +9 -0
  194. package/src/oauth/overload-failover-launch.ts +75 -0
  195. package/src/oauth/overload-failover.ts +137 -0
  196. package/src/release-loop/checklist.ts +10 -5
  197. package/src/release-loop/feature-store.ts +10 -9
  198. package/src/release-loop/placement.ts +17 -8
  199. package/src/seat-dispatch/seat-ask.ts +33 -6
  200. package/src/seat-loop/seat-loop.ts +90 -3
  201. package/src/self-dev/ask-launch-flow.ts +22 -1
  202. package/src/self-dev/authored-goal-trace.ts +64 -0
  203. package/src/self-dev/dev-cli.ts +19 -1
  204. package/src/self-dev/dev-pipeline.ts +6 -5
  205. package/src/self-dev/entrance-registry.ts +10 -6
  206. package/src/self-dev/old-door.ts +73 -0
  207. package/src/self-implement/app-api-budget.ts +166 -0
  208. package/src/self-implement/budget-gate.ts +7 -5
  209. package/src/self-implement/follow-up-goals.ts +289 -0
  210. package/src/self-implement/goal-author.ts +19 -13
  211. package/src/self-implement/heal-triage.ts +44 -0
  212. package/src/self-implement/orchestrator.ts +138 -7
  213. package/src/self-implement/run-ledger.ts +4 -0
  214. package/src/self-implement/seams.ts +44 -15
  215. package/src/session-runtime/index.ts +2 -0
  216. package/src/session-runtime/retry-api.ts +87 -0
  217. package/src/session-runtime/retry-policy.ts +107 -6
  218. package/src/steward/working-backwards.ts +16 -1
  219. package/src/task-cards/card-cli.ts +25 -0
  220. package/src/task-cards/card-store.ts +5 -3
  221. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  222. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  223. package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
  224. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  225. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
  226. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  227. package/src/telegram-commands.ts +5 -0
  228. package/src/telegram.ts +47 -0
  229. package/src/user-config.ts +36 -3
  230. package/src/version/packed-revision.json +1 -1
  231. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  244. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  245. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  246. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  247. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
  248. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  249. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  250. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  251. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  252. package/src/launch-head/seat-goal-queue.ts +0 -198
  253. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
  254. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -6,7 +6,8 @@ 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; impact?: { fact: string; current: string; action: 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,27 +41,32 @@ 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'));
46
57
  const out = intakeOutboxDir(root);
47
58
  const relevant = new Map<string, NonNullable<DigestEntry['impact']>>();
48
- for (const [folder, action] of [
49
- ['goals', '없는 기능의 골 후보를 세운다'],
50
- ['manual', '문서·약속을 현재 동작에 맞게 보강한다'],
51
- ['review', '대조 근거를 추가로 확인한다'],
52
- ['release', '이미 착지한 기능의 바깥 맥락을 릴리스 노트에 반영한다'],
53
- ] as const) {
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) {
54
61
  for (const row of readJsonl(join(out, folder, `${day}.jsonl`))) {
55
62
  if (typeof row.id !== 'string' || relevant.has(row.id)) continue;
56
- const fact = folder === 'release' ? row.title : row.fact;
57
- if (typeof fact !== 'string' || !fact.trim()) continue;
58
- relevant.set(row.id, {
59
- fact,
60
- current: typeof (folder === 'release' ? row.summary : row.current) === 'string'
61
- ? String(folder === 'release' ? row.summary : row.current) : '대조 근거 없음',
62
- action,
63
- });
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 });
64
70
  }
65
71
  }
66
72
  const absorbed = items.map((i: IntakeItem): DigestEntry => {
@@ -105,29 +111,45 @@ export function renderDigestMarkdown(d: IntakeDigest): string {
105
111
  for (const g of d.goals.slice(0, 10)) L.push(`- ${g.fact}${g.url ? ` · [원본](${g.url})` : ''}`);
106
112
  L.push('');
107
113
  }
108
- if (d.review?.length) {
109
- L.push(`### 🟡 사람이 가를 것 — 판단 필요 (${d.review.length})`, '', '> 근거가 실행 코드에 닿지만 «같은 것인가»는 결정론으로 못 가른다 — 노트의 🧭 절에서 판단한다.', '');
110
- for (const r of d.review.slice(0, 10)) L.push(`- ${r.fact}${r.note ? ` · [[${basename(r.note, '.md')}]]` : ''}`);
111
- if (d.review.length > 10) L.push(`- … 외 ${d.review.length - 10}건`);
112
- L.push('');
113
- }
114
+ const undecided = d.absorbed.filter((e) => !e.impact).length;
115
+ if (undecided) L.push(`렌즈 판정 못 함 ${undecided} — 원장 \`elanous intake items\``, '');
114
116
  L.push(`> 매뉴얼 후보 ${d.manual} · 릴리스 노트 맥락 ${d.release} · 그라운딩 후보 ${d.grounding} — 흡수 원장 \`elanous intake items\` · 큐 \`<인스턴스>/intake/outbox/\``, '');
115
117
  return L.join('\n');
116
118
  }
117
119
 
118
120
  /** 텔레그램 — 흡수 렌즈가 대조한 항목만 SCQA 짧은 판으로 낸다. */
119
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).
120
123
  const touching = d.absorbed.filter((e) => e.impact);
121
124
  const L = [`흡수 ${d.absorbed.length} → 우리에게 닿는 것 ${touching.length}`];
122
125
  if (!touching.length) return L[0];
123
- for (const e of touching.slice(0, 3)) {
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);
124
145
  L.push('',
125
- `S 무엇: ${e.impact!.fact}`,
126
- `C 우리에게 왜: ${e.impact!.current}`,
127
- `A 그래서 무엇을 하나: ${e.impact!.action}`,
146
+ `S 무엇: ${e.oneLiner?.trim() || '노트 한 줄 요약 없음'}`,
147
+ `C 우리에게 왜: ${impact.why}`,
148
+ `A 그래서 무엇을 하나: ${action}`,
128
149
  `🔗 원문 링크: ${e.url?.trim() || '링크 없음'}`,
129
150
  );
130
151
  }
131
- if (d.absorbed.length > touching.length) L.push('', `그 밖 ${d.absorbed.length - touching.length}건 · 닿지 않음`);
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}건 · 참고`);
132
154
  return L.join('\n');
133
155
  }
@@ -0,0 +1,105 @@
1
+ import { createRequire } from 'node:module';
2
+ import { LessonLedger, type LessonRow } from '../lessons/lesson-ledger.js';
3
+ import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
4
+ import { devVersion, listChecklist, type Checklist } from '../release-loop/checklist.js';
5
+ import { debug } from '../debug/log.js';
6
+
7
+ /** Directive row fields this search reads — kept local because `src/directives/directive-index` is excluded from the public export. */
8
+ export interface DirectiveRow { ts: string; text: string; source_file: string; line_no: number }
9
+
10
+ /** Loads the private directive index at run time only, so public builds bundle cleanly and report the directive source as unreadable. */
11
+ function openDirectiveIndex(): { search(q: string): DirectiveRow[]; close(): void } {
12
+ const spec = ['..', 'directives', 'directive-index.js'].join('/');
13
+ const mod = createRequire(import.meta.url)(spec) as { DirectiveIndex: new () => ReturnType<typeof openDirectiveIndex> };
14
+ return new mod.DirectiveIndex();
15
+ }
16
+
17
+ export type KnowSource = 'directive' | 'decision' | 'checklist' | 'lesson';
18
+ export interface KnowHit {
19
+ source: KnowSource;
20
+ id: string;
21
+ title: string;
22
+ status: string;
23
+ at: string;
24
+ current: boolean;
25
+ ref: string;
26
+ }
27
+ export interface KnowResult {
28
+ rows: KnowHit[];
29
+ unavailable: Array<{ source: KnowSource; reason: string }>;
30
+ }
31
+ export interface KnowFindDeps {
32
+ directives: (query: string) => DirectiveRow[];
33
+ decisions: () => DecisionEntry[];
34
+ checklist: (version: string) => Checklist;
35
+ lessons: (query: string) => LessonRow[];
36
+ versions: () => string[];
37
+ now: () => Date;
38
+ }
39
+
40
+ function versions(): string[] {
41
+ const current = devVersion().replace(/-dev\.\d+$/, '');
42
+ const parts = current.split('.').map(Number);
43
+ return [current, `${parts[0]}.${parts[1]}.${parts[2]! + 1}`];
44
+ }
45
+
46
+ const defaults: KnowFindDeps = {
47
+ directives: (query) => {
48
+ const index = openDirectiveIndex();
49
+ try { return index.search(query); } finally { index.close(); }
50
+ },
51
+ decisions: () => new DecisionLedger().list({ status: 'all' }),
52
+ checklist: (version) => listChecklist(version),
53
+ lessons: (query) => new LessonLedger().find(query),
54
+ versions,
55
+ now: () => new Date(),
56
+ };
57
+
58
+ function matches(text: string, words: string[]): boolean {
59
+ const normalized = text.toLocaleLowerCase();
60
+ return words.every((word) => normalized.includes(word));
61
+ }
62
+
63
+ export function knowFind(query: string, deps: Partial<KnowFindDeps> = {}): KnowResult {
64
+ const read = { ...defaults, ...deps };
65
+ const words = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
66
+ if (!words.length) throw new Error('know: 질문이 필요합니다');
67
+ const rows: KnowHit[] = [];
68
+ const unavailable: KnowResult['unavailable'] = [];
69
+ const attempt = (source: KnowSource, work: () => void): void => {
70
+ try { work(); }
71
+ catch (error) { unavailable.push({ source, reason: error instanceof Error ? error.message : String(error) }); }
72
+ };
73
+ const cutoff = read.now().getTime() - 30 * 86400000;
74
+ attempt('directive', () => {
75
+ for (const row of read.directives(query)) rows.push({ source: 'directive', id: `${row.source_file}:${row.line_no}`,
76
+ title: row.text.replace(/\s+/g, ' ').slice(0, 160), status: 'recorded', at: row.ts,
77
+ current: Number.isFinite(Date.parse(row.ts)) && Date.parse(row.ts) >= cutoff, ref: `${row.source_file}:${row.line_no}` });
78
+ });
79
+ attempt('decision', () => {
80
+ for (const row of read.decisions()) {
81
+ if (!matches([row.title, row.scqa.s, row.scqa.c, row.scqa.q, row.scqa.a].filter(Boolean).join(' '), words)) continue;
82
+ rows.push({ source: 'decision', id: row.id, title: row.title, status: row.status,
83
+ at: row.decidedAt ?? row.withdrawnAt ?? row.raisedAt ?? row.importedAt ?? '', current: row.status === 'open',
84
+ ref: row.refs?.[0] ?? `decisions show ${row.id}` });
85
+ }
86
+ });
87
+ attempt('checklist', () => {
88
+ for (const version of new Set(read.versions())) {
89
+ for (const row of read.checklist(version).items) {
90
+ if (!matches(`${row.id} ${row.title}`, words)) continue;
91
+ rows.push({ source: 'checklist', id: row.id, title: row.title, status: row.status,
92
+ at: row.updatedAt, current: row.status === 'yellow', ref: `release checklist status --version ${version}` });
93
+ }
94
+ }
95
+ });
96
+ attempt('lesson', () => {
97
+ for (const row of read.lessons(query)) rows.push({ source: 'lesson', id: row.id, title: row.incident,
98
+ status: row.status, at: row.updated_at, current: row.occurrence_count > 1, ref: `lesson show ${row.id}` });
99
+ });
100
+ rows.sort((a, b) => Number(b.current) - Number(a.current) || (Date.parse(b.at) || 0) - (Date.parse(a.at) || 0)
101
+ || a.source.localeCompare(b.source) || a.id.localeCompare(b.id));
102
+ debug.log('knowledge.know', 'query', { query, counts: Object.fromEntries((['directive', 'decision', 'checklist', 'lesson'] as const)
103
+ .map(source => [source, rows.filter(row => row.source === source).length])), unavailable });
104
+ return { rows, unavailable };
105
+ }
package/src/llm.ts CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  type ProviderFallbackAttempt,
37
37
  type ProviderFallbackTerminalVerdict,
38
38
  } from './session-runtime/retry-policy.js';
39
- import { fetchApiWithRetry } from './session-runtime/retry-api.js';
39
+ import { fetchApiWithRetry, OverloadFailoverError, type FetchApiWithRetryOpts } from './session-runtime/retry-api.js';
40
40
  import { resolveModelAlias } from './intelligence-map/model-alias.js';
41
41
  import { reasoningEffortCeiling as reasoningEffortCeilingOf } from './intelligence-map/model-catalog.js';
42
42
  import { inferProviderFromModel as registryInferProviderFromModel } from './registry/normalize.js';
@@ -407,6 +407,10 @@ export interface LLMOpts {
407
407
  background?: 'transparent' | 'opaque' | 'auto';
408
408
  };
409
409
  };
410
+ /** Unused configured fallbacks. The HTTP retry layer switches provider
411
+ * after k consecutive overload responses before spending those retries.
412
+ * Callers that already pinned a provider omit this (0). */
413
+ remainingFallbacks?: number;
410
414
  }
411
415
 
412
416
  export interface LLMProvider {
@@ -1310,6 +1314,20 @@ export async function* parseOpenAISSELines(
1310
1314
  * the request URL. The OpenAI streamer is shared by openai/grok/local
1311
1315
  * endpoints — all three pass through here. The string lands in the
1312
1316
  * `llm.retry` debug snapshot and the `ApiHttpError.provider` field. */
1317
+ /** Retries still left for this HTTP call. Callers that already know a
1318
+ * fallback remains pass it so overload switches before those retries. */
1319
+ function retryOptsFor(
1320
+ provider: string,
1321
+ errorPrefix: string,
1322
+ remainingFallbacks?: number,
1323
+ ): FetchApiWithRetryOpts {
1324
+ return {
1325
+ provider,
1326
+ errorPrefix,
1327
+ ...(remainingFallbacks !== undefined && remainingFallbacks > 0 ? { remainingFallbacks } : {}),
1328
+ };
1329
+ }
1330
+
1313
1331
  function inferOpenAIProviderName(url: string): string {
1314
1332
  if (url.includes('x.ai')) return 'grok';
1315
1333
  if (url.includes('openai.com')) return 'openai';
@@ -1326,6 +1344,7 @@ async function* streamOpenAIEvents(
1326
1344
  * `x-grok-client-version` 이 빠지면 HTTP 426 으로 거절된다(실측).
1327
1345
  * ⛔ 생략하면 종전과 «바이트 동일** — 기존 호출자 무회귀. */
1328
1346
  extraHeaders?: Record<string, string>,
1347
+ remainingFallbacks?: number,
1329
1348
  ): AsyncGenerator<LLMStreamEvent, void, unknown> {
1330
1349
  const headers: Record<string, string> = { 'Content-Type': 'application/json', ...(extraHeaders ?? {}) };
1331
1350
  if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
@@ -1353,10 +1372,7 @@ async function* streamOpenAIEvents(
1353
1372
  body: JSON.stringify({ ...body, stream: true }),
1354
1373
  signal,
1355
1374
  },
1356
- {
1357
- provider: inferOpenAIProviderName(url),
1358
- errorPrefix: 'LLM API',
1359
- },
1375
+ retryOptsFor(inferOpenAIProviderName(url), 'LLM API', remainingFallbacks),
1360
1376
  );
1361
1377
 
1362
1378
  debug.log('llm.response.status', `${response.status} ${response.statusText || ''}`.trim(), {
@@ -2030,6 +2046,8 @@ export async function* streamCodexResponsesEvents(
2030
2046
  * call. Best-effort; null/undefined if the backend skips the
2031
2047
  * event. */
2032
2048
  onResponseCreated?: (id: string) => void;
2049
+ /** Unused configured fallbacks. Overload k times switches before retries. */
2050
+ remainingFallbacks?: number;
2033
2051
  } = {},
2034
2052
  ): AsyncGenerator<LLMStreamEvent, void, unknown> {
2035
2053
  const startedAt = Date.now();
@@ -2118,10 +2136,7 @@ export async function* streamCodexResponsesEvents(
2118
2136
  }),
2119
2137
  signal,
2120
2138
  },
2121
- {
2122
- provider: 'codex',
2123
- errorPrefix: 'Codex API',
2124
- },
2139
+ retryOptsFor('codex', 'Codex API', extra.remainingFallbacks),
2125
2140
  );
2126
2141
  debug.log('llm.response.status', `codex ${response.status}`, {
2127
2142
  url, status: response.status, elapsedMs: Date.now() - startedAt,
@@ -3103,6 +3118,7 @@ async function* streamAnthropicEvents(
3103
3118
  apiKey: string,
3104
3119
  body: any,
3105
3120
  signal?: AbortSignal,
3121
+ remainingFallbacks?: number,
3106
3122
  ): AsyncGenerator<LLMStreamEvent, void, unknown> {
3107
3123
  // Wave 6 (2026-05-04) — Anthropic beta headers from
3108
3124
  // ref/opencode `provider/provider.ts:181-185` analysis.
@@ -3139,10 +3155,7 @@ async function* streamAnthropicEvents(
3139
3155
  body: JSON.stringify({ ...body, stream: true }),
3140
3156
  signal,
3141
3157
  },
3142
- {
3143
- provider: 'anthropic',
3144
- errorPrefix: 'Anthropic API',
3145
- },
3158
+ retryOptsFor('anthropic', 'Anthropic API', remainingFallbacks),
3146
3159
  );
3147
3160
 
3148
3161
  try {
@@ -4507,7 +4520,11 @@ export async function streamLLM(
4507
4520
  const consumed = await consumeProviderText(
4508
4521
  activeProvider,
4509
4522
  messages,
4510
- { ...opts, model: activeModel },
4523
+ {
4524
+ ...opts,
4525
+ model: activeModel,
4526
+ remainingFallbacks: opts.provider ? 0 : remainingFallbackProviderNames(attemptedProviders, opts).length,
4527
+ },
4511
4528
  onChunk,
4512
4529
  );
4513
4530
  const full = consumed.full;
@@ -4528,6 +4545,22 @@ export async function streamLLM(
4528
4545
  }
4529
4546
 
4530
4547
  const err: any = consumed.error;
4548
+ if (err instanceof OverloadFailoverError && !opts.provider) {
4549
+ const switched = providerByName('grok');
4550
+ if (switched && !attemptedProviders.has(switched.name) && switched.available() && full.length === 0) {
4551
+ debug.log('llm.router', 'overload-failover', {
4552
+ blockedProvider: activeProvider.name,
4553
+ fallbackProvider: switched.name,
4554
+ streak: err.streak,
4555
+ status: err.status,
4556
+ kind: err.kind,
4557
+ }, { level: 'warn' });
4558
+ activeProvider = switched;
4559
+ modelOverride = undefined;
4560
+ attemptedProviders.add(activeProvider.name);
4561
+ continue;
4562
+ }
4563
+ }
4531
4564
  const reason = sanitizeProviderFailureReason(err?.message || String(err));
4532
4565
  debug.log('llm.router.error', 'streamLLM', {
4533
4566
  provider: activeProvider.name,
@@ -8350,9 +8383,11 @@ export async function streamLLMWithTools(
8350
8383
  // `tools: activeTools` (not opts.tools) so mid-loop hydration reaches the
8351
8384
  // provider on the next turn. turn-0 keeps opts.toolChoice; later turns
8352
8385
  // revert to auto (see above).
8353
- const buildTurnOpts = () => turn === 0
8354
- ? { ...opts, model: activeModel, tools: activeTools }
8355
- : { ...opts, model: activeModel, tools: activeTools, toolChoice: undefined };
8386
+ const buildTurnOpts = () => {
8387
+ const remainingFallbacks = opts.provider ? 0 : remainingFallbackProviderNames(attemptedProviders, opts).length;
8388
+ const base = { ...opts, model: activeModel, tools: activeTools, remainingFallbacks };
8389
+ return turn === 0 ? base : { ...base, toolChoice: undefined };
8390
+ };
8356
8391
  let turnOpts = buildTurnOpts();
8357
8392
  // reasoning-heavy family(codex/claude/gemini/grok/local)는 침묵 추론 구간이 길어 idle watchdog 를
8358
8393
  // family-aware 로 상향(2026-07-19 goal-exec → 아크4 전-family). fast/mini(gpt·other)만 45s 유지.
@@ -8390,6 +8425,26 @@ export async function streamLLMWithTools(
8390
8425
  ]);
8391
8426
  } catch (err) {
8392
8427
  if (idleTimer) clearTimeout(idleTimer);
8428
+ if (err instanceof OverloadFailoverError && !opts.provider) {
8429
+ const switched = providerByName('grok');
8430
+ if (switched && !attemptedProviders.has(switched.name) && switched.available() && evCount === 0) {
8431
+ debug.log('llm.router', 'overload-failover', {
8432
+ turn,
8433
+ blockedProvider: activeProvider.name,
8434
+ fallbackProvider: switched.name,
8435
+ streak: err.streak,
8436
+ status: err.status,
8437
+ kind: err.kind,
8438
+ }, { level: 'warn' });
8439
+ const finalized = finalizeStreamingProviderModel(switched, switched.defaultModel);
8440
+ activeProvider = finalized.provider;
8441
+ activeModel = finalized.model;
8442
+ attemptedProviders.add(activeProvider.name);
8443
+ turnOpts = buildTurnOpts();
8444
+ streamIter = undefined;
8445
+ continue;
8446
+ }
8447
+ }
8393
8448
  // 명시 provider 선택만 폴백 대상이 아니다 — `streamLLM` 과 같은 계약으로
8394
8449
  // «원본 오류»를 그대로 올린다. 모델만 전달된 호출은 다음 provider의 기본 모델로 재시도한다.
8395
8450
  if (opts.provider) throw err;
@@ -10958,6 +11013,7 @@ function makeCodexProvider(cfg: UCLLMConfig): LLMProvider {
10958
11013
  // lastResponseId stays undefined and we fall back to full
10959
11014
  // input on the next turn (safe — just no token savings).
10960
11015
  onResponseCreated: useStore ? (id) => { lastResponseId = id; } : undefined,
11016
+ remainingFallbacks: opts.remainingFallbacks,
10961
11017
  });
10962
11018
  // Snapshot full input AFTER the stream completes so the next
10963
11019
  // call's delta computation uses what the backend actually has
@@ -10991,7 +11047,7 @@ function makeCodexProvider(cfg: UCLLMConfig): LLMProvider {
10991
11047
  ...openAiTemperatureField(apiKeyModel, opts.temperature ?? 0.3),
10992
11048
  ...openAiOutputTokenField(apiKeyModel, opts.maxTokens ?? 2048),
10993
11049
  ...(tools ? { tools } : {}),
10994
- }, opts.signal);
11050
+ }, opts.signal, undefined, opts.remainingFallbacks);
10995
11051
  },
10996
11052
  async *chat(messages, opts = {}) { yield* textOnly(this.streamChat!(messages, opts)); },
10997
11053
  };
@@ -11085,7 +11141,7 @@ function makeAnthropicProvider(cfg: UCLLMConfig): LLMProvider {
11085
11141
  ...(effort ? { thinking: { type: 'adaptive' }, output_config: { effort } } : {}),
11086
11142
  ...anthropicTemperatureField(anthropicModel, thinkingActive, opts.temperature),
11087
11143
  ...(tools ? { tools } : {}),
11088
- }, opts.signal);
11144
+ }, opts.signal, opts.remainingFallbacks);
11089
11145
  },
11090
11146
  async *chat(messages, opts = {}) { yield* textOnly(this.streamChat!(messages, opts)); },
11091
11147
  };
@@ -6,6 +6,8 @@ import { loadRunLedger, resolveFederatedRunLedgerTargets } from '../../self-impl
6
6
  import type { LogTarget } from '../../cli/logs-cli.js';
7
7
 
8
8
  const DAY_MS = 24 * 60 * 60 * 1_000;
9
+ /** Host-side records a Pod-dispatched launch leaves when its run starts inside the Pod. */
10
+ const LAUNCH_ONLY_EVENTS = new Set(['launch-quota-policy', 'author-on-pod-receipt']);
9
11
  const GH_LIMIT = 1000;
10
12
 
11
13
  export type FinishMetric = 'launched' | 'landed' | 'landingRate' | 'staleDrafts' | 'conflictRatio' | 'unknownMergeable';
@@ -38,6 +40,7 @@ function defaultListStarts(cutoff: number, targets?: readonly LogTarget[]): Star
38
40
  const childLedgers = new Set<string>();
39
41
  let unreadableDirectories = 0;
40
42
  let unresolvedDirectories = 0;
43
+ const launchOnlyIds = new Set<string>();
41
44
  for (const { ledgerDirectory } of resolveFederatedRunLedgerTargets({ includeTest: true, ...(targets ? { targets } : {}) })) {
42
45
  let dir: string;
43
46
  try {
@@ -68,16 +71,24 @@ function defaultListStarts(cutoff: number, targets?: readonly LogTarget[]): Star
68
71
  continue;
69
72
  }
70
73
  const start = entries.find(entry => entry.event === 'start');
71
- if (!start?.timestamp) {
74
+ // A Pod-dispatched launch leaves only a host launch record (launch-quota-policy · author-on-pod-receipt) — the run
75
+ // itself starts inside the Pod. That is still a launch: its time is the record's timestamp, else the ledger mtime.
76
+ const launchRecord = !start ? entries.find(entry => LAUNCH_ONLY_EVENTS.has(entry.event)) : undefined;
77
+ const startedAt = start?.timestamp ?? (launchRecord ? (launchRecord.timestamp ?? new Date(statSync(path).mtimeMs).toISOString()) : undefined);
78
+ if (!startedAt) {
72
79
  if (!starts.has(runId)) unreadable.set(runId, { runId, reason: entries[0]?.event ?? 'start missing' });
73
80
  continue;
74
81
  }
75
- starts.set(runId, { runId, startedAt: start.timestamp });
82
+ // One runId can leave a host launch record in one directory and a Pod `start` in another: a real `start`
83
+ // always wins and is never overwritten by a launch record, whatever order the directories are read in.
84
+ if (launchRecord && starts.has(runId)) continue;
85
+ if (launchRecord) launchOnlyIds.add(runId); else launchOnlyIds.delete(runId);
86
+ starts.set(runId, { runId, startedAt });
76
87
  unreadable.delete(runId);
77
88
  }
78
89
  }
79
90
  debug.log('loop.orchestrator', 'finish-launch-population', {
80
- directories: directories.size + unresolvedDirectories, launched: starts.size, childLedgers: childLedgers.size, unreadable: unreadable.size,
91
+ directories: directories.size + unresolvedDirectories, launched: starts.size, launchOnly: launchOnlyIds.size, childLedgers: childLedgers.size, unreadable: unreadable.size,
81
92
  });
82
93
  return { starts: [...starts.values()], unreadable: [...unreadable.values()], unreadableDirectories };
83
94
  }