elanous 0.2.27 → 0.2.28

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 (285) 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/{1454-a9c338f5bb556f42.js → 1454-29d1d773d5996a49.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-07110da72c45e68f.js → page-24453f6a302816b4.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/{page-d2d76d4a7eae91bd.js → page-6c131d30bb1cf871.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/page-fa46d4588e8442c2.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-e5a9302c105725a3.js → page-dc0772b11861af51.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-6a16a48d1aa6e91f.js → page-256ffd365d32e100.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e0de78ae7b99f01c.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/exec/{page-39f82acf619cca23.js → page-4c741eb760f2f77c.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/{page-7c2c8f81b185f9c0.js → page-e9ba98f5f1440a21.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/inside/{page-ae7aff07e32676bb.js → page-dafc901bb1b72b5f.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-482c60c1bc540d0a.js → page-2f6f303fa114ec24.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/{layout-7cb996c1aa08b6f5.js → layout-1634081c5272006c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-623e08fe4477d606.js → page-ced0bcfc6542b3fc.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-db460cd8aecb8490.js → page-bb1f05cd51c38944.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-ff27902d0b11ef4f.js → page-646b50f63b6fe732.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-1917ea87ea71ab6b.js → page-f736324afa2117c1.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-25a302273b054fd7.js → page-6f131b51193a36f9.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/{not-found-670510eda8b9443f.js → not-found-435f5e2faf02bc01.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-497e0eedf20e7890.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a8d28fdf995e16ea.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4bb7556e2e97a750.js → page-8966452228753c5e.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/{page-4eb96fc7241c2456.js → page-66a67eb19ebd976b.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-0d64ff212dc55d03.js → page-c71267e7681da982.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-7f096248993fc2d4.js → page-a21d1e1797172692.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-e47fc72049149a38.js → page-cb249e81ed339318.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-f7d808e31e005895.js → page-781cfc5114e097da.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-5c2b24b5496dad73.js → page-478157214db6a023.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-76cdb531668b5191.js → page-86141f12cf090b7f.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/page-7256c73bb1c36751.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/today/{page-dab163b338e3b966.js → page-979d318a534e92da.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-0260ffc7eda50db1.js → page-21050928a697ed7a.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-99c4e6c7345a9156.js → page-601fb77340e63c9a.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-11bf9e5725a72c22.js +1 -0
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +13 -13
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +13 -13
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +13 -13
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +13 -13
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +13 -13
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/decisions/index.html +1 -1
  72. package/apps/pwa/out/decisions/index.txt +13 -13
  73. package/apps/pwa/out/design-check/index.html +1 -1
  74. package/apps/pwa/out/design-check/index.txt +13 -13
  75. package/apps/pwa/out/editor/index.html +1 -1
  76. package/apps/pwa/out/editor/index.txt +13 -13
  77. package/apps/pwa/out/exec/index.html +1 -1
  78. package/apps/pwa/out/exec/index.txt +13 -13
  79. package/apps/pwa/out/field/index.html +1 -1
  80. package/apps/pwa/out/field/index.txt +13 -13
  81. package/apps/pwa/out/healthz +1 -1
  82. package/apps/pwa/out/index.html +1 -1
  83. package/apps/pwa/out/index.txt +13 -13
  84. package/apps/pwa/out/inside/index.html +1 -1
  85. package/apps/pwa/out/inside/index.txt +13 -13
  86. package/apps/pwa/out/intake/index.html +1 -1
  87. package/apps/pwa/out/intake/index.txt +13 -13
  88. package/apps/pwa/out/live/index.html +1 -1
  89. package/apps/pwa/out/live/index.txt +13 -13
  90. package/apps/pwa/out/live-run/index.html +1 -1
  91. package/apps/pwa/out/live-run/index.txt +13 -13
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +13 -13
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +13 -13
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +13 -13
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +13 -13
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +13 -13
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +13 -13
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +13 -13
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +14 -14
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +13 -13
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +13 -13
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +13 -13
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +13 -13
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +13 -13
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +13 -13
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +13 -13
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +13 -13
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +13 -13
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +13 -13
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +13 -13
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +13 -13
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +13 -13
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +13 -13
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +13 -13
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +13 -13
  146. package/graphs/implement-loop.yaml +38 -1
  147. package/graphs/steward/recipes.yaml +6 -0
  148. package/graphs/steward/release-day.yaml +23 -0
  149. package/graphs/steward/steward.yaml +9 -1
  150. package/graphs/work/code-pr.yaml +23 -0
  151. package/package.json +1 -1
  152. package/scripts/browser-observe-cycle.ts +76 -2
  153. package/scripts/ci-typecheck-changed.ts +50 -17
  154. package/scripts/detect-test-interference.ts +31 -1
  155. package/scripts/release-loop/docs-node.ts +51 -22
  156. package/scripts/release-loop/gate-node.ts +37 -4
  157. package/scripts/release-loop/node-verdict.ts +3 -1
  158. package/scripts/release-loop/publish-node.ts +3 -0
  159. package/scripts/release-loop/tui-node-checks.ts +5 -2
  160. package/scripts/skills/public-check.ts +5 -2
  161. package/scripts/test-deterministic.ts +520 -37
  162. package/scripts/ux-sim.ts +7 -1
  163. package/scripts/verify-kpack6-live.ts +230 -0
  164. package/src/acp/codex-plugins.ts +17 -0
  165. package/src/agent/task-notification.ts +2 -1
  166. package/src/agent-mission/in-agent-plugin.ts +8 -3
  167. package/src/agent-substrate/self-review-cli.ts +34 -10
  168. package/src/briefing/brief-items.ts +151 -35
  169. package/src/budget/llm-cost.ts +25 -2
  170. package/src/budget/usage-outcomes.ts +32 -0
  171. package/src/claims/claims-ledger.ts +23 -0
  172. package/src/claims/claims-render.ts +22 -2
  173. package/src/cli/brief-cli.ts +131 -10
  174. package/src/cli/claims-cli.ts +24 -4
  175. package/src/cli/msg-cli.ts +165 -9
  176. package/src/cli/plugin-cli.ts +11 -3
  177. package/src/cli/pr-cli.ts +80 -18
  178. package/src/cli/pr-land-refusal.ts +20 -0
  179. package/src/cli/release-cli.ts +2 -0
  180. package/src/cli/rfc-cli.ts +24 -0
  181. package/src/cli/tailscale-serve.ts +5 -8
  182. package/src/cli/tasks-cli.ts +393 -26
  183. package/src/cli/usage-cli.ts +9 -4
  184. package/src/dashboard/first-screen-band.ts +63 -13
  185. package/src/dashboard/index.ts +8 -3
  186. package/src/dashboard/skill-detection-runtime.ts +24 -4
  187. package/src/domains/morning-report.ts +3 -1
  188. package/src/github/pr-labels.ts +10 -4
  189. package/src/graph-runner/runner.ts +101 -14
  190. package/src/harness/harness-clean.ts +21 -3
  191. package/src/harness/harness-cli-command.ts +22 -6
  192. package/src/harness/harness-pod-dispatch.ts +11 -4
  193. package/src/hq/seats.ts +17 -3
  194. package/src/index.ts +34 -5
  195. package/src/intelligence-map/model-bench-scores.ts +102 -0
  196. package/src/loops/orchestrator/inbox.ts +73 -0
  197. package/src/loops/orchestrator/resync-shadow.ts +100 -12
  198. package/src/loops/orchestrator/tick.ts +73 -2
  199. package/src/maturity/feature-maturity.ts +1 -1
  200. package/src/msg/msg-sse-server.ts +141 -0
  201. package/src/msg/msg-store.ts +84 -14
  202. package/src/patents/ledger.ts +128 -0
  203. package/src/plugins/install/market-fetch.ts +32 -1
  204. package/src/pod-lease/admission.ts +116 -41
  205. package/src/release-loop/checklist.ts +11 -0
  206. package/src/release-loop/merged-pr-checklist.ts +24 -5
  207. package/src/release-loop/rubric.ts +70 -0
  208. package/src/rfc-registry/registry.ts +88 -0
  209. package/src/seat-dispatch/seat-request-ledger.ts +4 -0
  210. package/src/seat-loop/knowhow-lint.ts +233 -0
  211. package/src/seat-loop/seat-pack.ts +16 -4
  212. package/src/seat-loop/traffic-signal.ts +7 -0
  213. package/src/self-dev/ask-launch-flow.ts +48 -8
  214. package/src/self-dev/ask-path-candidates.ts +69 -14
  215. package/src/self-dev/decompose-proposal.ts +43 -1
  216. package/src/self-dev/graph-journey-nodes.ts +2 -0
  217. package/src/self-dev/launch-preflight-text.ts +5 -1
  218. package/src/self-dev/launch-preflight.ts +143 -10
  219. package/src/self-implement/gate-cli.ts +6 -2
  220. package/src/self-implement/goal-author.ts +2 -1
  221. package/src/self-implement/goal-new-paths.ts +4 -5
  222. package/src/self-implement/graph-authority.ts +17 -3
  223. package/src/self-implement/graph-yaml.ts +15 -7
  224. package/src/self-implement/headless-elanous-driver.ts +16 -1
  225. package/src/self-implement/host-regate.ts +44 -9
  226. package/src/self-implement/orchestrator.ts +3 -10
  227. package/src/self-implement/rework-policy.ts +80 -2
  228. package/src/self-implement/speed-claims.ts +145 -6
  229. package/src/skills/tools/persistent-grounding.ts +59 -4
  230. package/src/skills/tools/shell.ts +79 -1
  231. package/src/steward/rejudge-tick.ts +12 -1
  232. package/src/steward/release-day.ts +158 -0
  233. package/src/steward/resync-tick.ts +123 -0
  234. package/src/task-agent/actions.ts +1 -0
  235. package/src/task-agent/adopt-pr.ts +140 -0
  236. package/src/task-agent/board.ts +11 -1
  237. package/src/task-agent/card-evidence.ts +37 -5
  238. package/src/task-agent/cover.ts +84 -17
  239. package/src/task-agent/failure-notify.ts +61 -0
  240. package/src/task-agent/green-sweep.ts +170 -0
  241. package/src/task-agent/land-worktree.ts +52 -5
  242. package/src/task-agent/live-move-history.ts +1 -0
  243. package/src/task-agent/live-moves.ts +43 -12
  244. package/src/task-agent/parent-host.ts +1 -1
  245. package/src/task-agent/postland-card.ts +139 -0
  246. package/src/task-agent/rejudge-on-head.ts +106 -7
  247. package/src/task-agent/remote-rejudge-bind.ts +62 -0
  248. package/src/task-agent/repair-branch-cas.ts +182 -0
  249. package/src/task-agent/repair-cycle.ts +275 -0
  250. package/src/task-agent/repair-next-move.ts +31 -0
  251. package/src/task-agent/repair-review.ts +24 -0
  252. package/src/task-agent/repair-worker.ts +87 -0
  253. package/src/task-agent/review-result-capture.ts +2 -1
  254. package/src/task-agent/shadow.ts +13 -7
  255. package/src/task-agent/task-hand.ts +115 -6
  256. package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
  257. package/src/task-orchestrator/surfaces/self-implement-pod.ts +70 -12
  258. package/src/telegram.ts +30 -1
  259. package/src/user-config.ts +8 -1
  260. package/src/ux-sim/fault-sqlite-busy.ts +50 -0
  261. package/src/version/packed-revision.json +1 -1
  262. package/apps/pwa/out/_next/static/chunks/app/approvals/page-a1ce306364f08da9.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/consult/page-20662900d6b4b455.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/control/page-f707a509515ac50c.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/market/page-8397a5c6ca569bf4.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/outputs/page-15c0a4095baddfba.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d376e0c83edc041a.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/trace/page-08019efef111ed2e.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-980ce6fe36ad95eb.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/workspace/page-0ab42e0f05823013.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +0 -1
  284. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
  285. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
@@ -628,17 +628,74 @@ function sameProviderStep(child: ExplicitChildForEscalation): EscalateTarget | n
628
628
  return null;
629
629
  }
630
630
 
631
+ /** escalateChild 매핑 해석 근원 — `env` 가 `config` 를 이기고, 둘 다 없으면 같은 provider 사다리(`ladder`). */
632
+ export type EscalateTargetSource = 'config' | 'env' | 'ladder';
633
+
634
+ /** `self-dev.rework`/`escalate-target` 관측 한 건. 관측 전용 — 판정·분기는 이 값을 읽지 않는다. */
635
+ export interface EscalateTargetObservation {
636
+ readonly category: 'self-dev.rework';
637
+ readonly event: 'escalate-target';
638
+ readonly data: {
639
+ readonly source: EscalateTargetSource;
640
+ readonly from: string;
641
+ readonly to: string;
642
+ readonly applied: boolean;
643
+ /** `applied=false` 의 이유 — provider 경계 또는 불완전한 모델 ID. */
644
+ readonly reason?: 'cross-provider' | 'invalid-target';
645
+ };
646
+ }
647
+
648
+ /** 명시 자식 승급 해석 옵션. `escalateChild`(`tools.selfImplement.escalateChild` from→to 맵)는 호출부가
649
+ * config 에서 읽어 전달한다 — ⚠️ Pod 안엔 운영 config 가 없다(POD-CONFIG-SNAPSHOT 전) → Pod 자식은
650
+ * `ELANOUS_SELFDEV_ESCALATE_CHILD` env 로만 받는다. 관측 싱크도 호출부가 주입한다(정책은 순수 유지). */
651
+ export interface ExplicitChildEscalateOptions {
652
+ readonly escalateChild?: Readonly<Record<string, string>>;
653
+ readonly observe?: (observation: EscalateTargetObservation) => void;
654
+ }
655
+
656
+ /** 모델 id(`openrouter/z-ai/glm-5.3` 등)의 provider 세그먼트 — 첫 `/` 앞. */
657
+ function escalateModelProvider(model: string): string {
658
+ return (model.split('/')[0] ?? '').trim();
659
+ }
660
+
661
+ /** 명시 매핑은 provider/model, OpenRouter 는 openrouter/vendor/model 형태의 완전한 ID 여야 한다. */
662
+ function validEscalateModelId(model: string): boolean {
663
+ const segments = model.split('/');
664
+ const required = segments[0] === 'openrouter' ? 3 : 2;
665
+ return segments.length === required && segments.every((segment) => segment.length > 0 && !/\s/.test(segment));
666
+ }
667
+
668
+ /** `ELANOUS_SELFDEV_ESCALATE_CHILD=<from>=<to>` 해석 — 첫 `=` 기준. 빈 조각·구문 파손은 매핑 없음. */
669
+ function parseEscalateChildEnv(raw: string | undefined): { from: string; to: string } | undefined {
670
+ const value = raw?.trim();
671
+ if (!value) return undefined;
672
+ const separator = value.indexOf('=');
673
+ if (separator <= 0) return undefined;
674
+ const from = value.slice(0, separator).trim();
675
+ const to = value.slice(separator + 1).trim();
676
+ return from && to ? { from, to } : undefined;
677
+ }
678
+
631
679
  /** 명시 자식의 승급 타깃 — 같은 provider 안에서만(위 규칙). `ELANOUS_SELFDEV_SOL_*` 는 운영자의 명시
632
680
  * 덮어쓰기라 그대로 우선한다. ⛔ `effort` 가 빈 문자열이면 «안 보낸다»는 뜻이다(OpenRouter 는 노력을
633
- * 보내면 덜 생각했다 — 2026-09-23 실측). */
681
+ * 보내면 덜 생각했다 — 2026-09-23 실측).
682
+ * escalateChild 짝지기(env 가 config 를 이긴다)는 사다리(`sameProviderStep`)보다 «먼저» —
683
+ * 같은 provider 타깃이면 그 타깃으로, provider 가 다르면 무시 ⊕ 관측하고 사다리로 돌아간다.
684
+ * 둘 다 없으면 종전 그대로(바이트 불변). */
634
685
  export function resolveExplicitChildEscalateTarget(
635
686
  tier: EscalateTier,
636
687
  env: Record<string, string | undefined> = process.env,
637
688
  child?: ExplicitChildForEscalation,
689
+ options?: ExplicitChildEscalateOptions,
638
690
  ): EscalateTarget | null {
639
691
  if (tier === 'none') return null;
692
+ const sink = options?.observe;
693
+ const emit = sink
694
+ ? (data: EscalateTargetObservation['data']): void => sink({ category: 'self-dev.rework', event: 'escalate-target', data })
695
+ : undefined;
640
696
  const overrideModel = env.ELANOUS_SELFDEV_SOL_MODEL?.trim();
641
697
  if (overrideModel) {
698
+ if (child) emit?.({ source: 'env', from: child.model, to: overrideModel, applied: true });
642
699
  return {
643
700
  model: overrideModel,
644
701
  provider: env.ELANOUS_SELFDEV_SOL_PROVIDER?.trim() || child?.provider || 'openai-codex',
@@ -646,9 +703,30 @@ export function resolveExplicitChildEscalateTarget(
646
703
  };
647
704
  }
648
705
  if (child === undefined) return null;
706
+ const effortOverride = env.ELANOUS_SELFDEV_SOL_EFFORT?.trim();
707
+ // env → config 순서로 이 자식 모델에 대한 매핑을 모은다(없는 층은 건너뛴다).
708
+ const pairings: Array<{ source: 'env' | 'config'; to: string }> = [];
709
+ const envPairing = parseEscalateChildEnv(env.ELANOUS_SELFDEV_ESCALATE_CHILD);
710
+ if (envPairing && envPairing.from === child.model) pairings.push({ source: 'env', to: envPairing.to });
711
+ const configMap = options?.escalateChild;
712
+ const configTo = configMap && Object.hasOwn(configMap, child.model) ? configMap[child.model]?.trim() : undefined;
713
+ if (configTo) pairings.push({ source: 'config', to: configTo });
714
+ for (const pairing of pairings) {
715
+ if (!validEscalateModelId(pairing.to)) {
716
+ emit?.({ source: pairing.source, from: child.model, to: pairing.to, applied: false, reason: 'invalid-target' });
717
+ continue;
718
+ }
719
+ if (escalateModelProvider(pairing.to) === child.provider) {
720
+ emit?.({ source: pairing.source, from: child.model, to: pairing.to, applied: true });
721
+ return effortOverride
722
+ ? { model: pairing.to, provider: child.provider, effort: effortOverride }
723
+ : { model: pairing.to, provider: child.provider, effort: '' };
724
+ }
725
+ emit?.({ source: pairing.source, from: child.model, to: pairing.to, applied: false, reason: 'cross-provider' });
726
+ }
649
727
  const step = sameProviderStep(child);
650
728
  if (step === null) return null;
651
- const effortOverride = env.ELANOUS_SELFDEV_SOL_EFFORT?.trim();
729
+ emit?.({ source: 'ladder', from: child.model, to: step.model, applied: true });
652
730
  return effortOverride ? { ...step, effort: effortOverride } : step;
653
731
  }
654
732
 
@@ -2,7 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import { readFileSync, readdirSync } from 'node:fs';
3
3
  import { dirname, isAbsolute, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { prodInstanceRoot } from '../instance/resolve.js';
5
+ import { prodInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
6
6
  import { seatOfTree } from '../loops/orchestrator/traffic.js';
7
7
  import { getUserConfig, ORCHESTRATOR_DEFAULTS } from '../user-config.js';
8
8
  import { isRunStatus } from './run-status-mapping.js';
@@ -30,6 +30,7 @@ export interface SpeedClaimsMeasurement {
30
30
  rows: SpeedClaimsRow[];
31
31
  note: string;
32
32
  parent: ParentSpeedClaimsMeasurement;
33
+ daily: SpeedClaimsDailyMeasurement;
33
34
  }
34
35
  export interface ParentSpeedClaimsEvent {
35
36
  ts: string;
@@ -58,6 +59,32 @@ export interface ParentSpeedClaimsMeasurement {
58
59
  note: string;
59
60
  }
60
61
 
62
+ export interface SpeedClaimsReleaseEvent {
63
+ ts: string;
64
+ version: string;
65
+ }
66
+
67
+ export interface SpeedClaimsDailyRow {
68
+ date: string;
69
+ landedPrCount: number | null;
70
+ releasedCount: number | null;
71
+ }
72
+
73
+ export interface SpeedClaimsDailySourceStatus {
74
+ status: 'measured' | 'unmeasured';
75
+ undatedCount: number;
76
+ }
77
+
78
+ export interface SpeedClaimsDailyMeasurement {
79
+ scope: 'last-30-days';
80
+ windowStart: string;
81
+ windowEnd: string;
82
+ landedPrs: SpeedClaimsDailySourceStatus;
83
+ releases: SpeedClaimsDailySourceStatus;
84
+ rows: SpeedClaimsDailyRow[];
85
+ note: string;
86
+ }
87
+
61
88
  export interface SpeedClaimsOptions {
62
89
  dir?: string;
63
90
  list?: (path: string) => string[];
@@ -65,6 +92,8 @@ export interface SpeedClaimsOptions {
65
92
  readSeat?: (runId: string, ledgerDirectory: string, entries: readonly RunLedgerEntry[]) => SpeedClaimsSeat;
66
93
  readParentEvents?: () => readonly ParentSpeedClaimsEvent[];
67
94
  readParentQueue?: () => unknown;
95
+ now?: number;
96
+ readReleaseEvents?: () => readonly SpeedClaimsReleaseEvent[];
68
97
  }
69
98
 
70
99
  const NOTE = 'Child-ledger elapsed minutes from the first child event (or checkpoint creation when unavailable) to the recorded child end, including retries and resumes; this is not the parent launch-to-done interval. Only finished runs enter quartiles (linear interpolation at ranks (n-1)*p). In-progress runs (including parked) and missing, unreadable or invalid timestamps are excluded, not assigned zero. Seats come from the same state root checkpoint, launch queue or recorded launch tree; unresolved seats are counted separately. A completed run without a recorded merge is not a merge.';
@@ -192,6 +221,98 @@ function readParentQueueFromState(): unknown {
192
221
  return JSON.parse(readFileSync(join(prodInstanceRoot(), 'harness', 'queue.json'), 'utf8')) as unknown;
193
222
  }
194
223
 
224
+ const DAILY_WINDOW_DAYS = 30;
225
+ const DAY_MS = 86_400_000;
226
+ const DAILY_NOTE = 'Daily rows cover the last 30 UTC calendar days ending on the measurement day. Landed PRs are child run-ledger merged events (merged=true), identified by PR number with the run id as fallback when no number is recorded, de-duplicated across runs, retries and resumes, and attributed to the earliest recorded landing timestamp. Issued releases use the release ledger publishedAt, never a tag target commit date, and are de-duplicated by version. Identities without any valid timestamp are counted as undated, never as a day. A count of zero means the day was queried and had no events; unmeasured means the source could not be queried, and the JSON value is null. Start-to-land and external-change-to-reflection timings stay with the parent and child measurements above.';
227
+ const VERSION_TAG = /^v?\d+(?:\.\d+)+(?:-[\w.]+)?$/;
228
+
229
+ interface DailyObservation {
230
+ key: string;
231
+ ts: number | null;
232
+ }
233
+
234
+ function utcDayOf(ts: number): string {
235
+ return new Date(ts).toISOString().slice(0, 10);
236
+ }
237
+
238
+ /** Landed-PR identity: the recorded PR number, else the run id (one run lands at most one PR). */
239
+ function landedPrIdentity(data: unknown, runId: string): string {
240
+ const number = object(data)?.number;
241
+ if (typeof number === 'number' && Number.isInteger(number) && number > 0) return `pr-${number}`;
242
+ if (typeof number === 'string' && /^\d+$/.test(number.trim()) && Number(number.trim()) > 0) return `pr-${Number(number.trim())}`;
243
+ return runId;
244
+ }
245
+
246
+ /** De-duplicate repeated identities by their earliest valid timestamp; identities without any valid timestamp are reported as undated, never dropped silently. */
247
+ function dailyBuckets(observations: readonly DailyObservation[]): { counts: Map<string, number>; undated: number } {
248
+ const earliest = new Map<string, number>();
249
+ const undated = new Set<string>();
250
+ for (const { key, ts } of observations) {
251
+ if (key.length === 0) continue;
252
+ if (ts === null) { undated.add(key); continue; }
253
+ const known = earliest.get(key);
254
+ if (known === undefined || ts < known) earliest.set(key, ts);
255
+ }
256
+ const counts = new Map<string, number>();
257
+ for (const ts of earliest.values()) {
258
+ const day = utcDayOf(ts);
259
+ counts.set(day, (counts.get(day) ?? 0) + 1);
260
+ }
261
+ let undatedCount = 0;
262
+ for (const key of undated) if (!earliest.has(key)) undatedCount++;
263
+ return { counts, undated: undatedCount };
264
+ }
265
+
266
+ /** Read-only daily census of landed PRs and issued releases over the last 30 UTC days. */
267
+ function measureDailySpeedClaims(
268
+ landed: readonly DailyObservation[],
269
+ landedMeasured: boolean,
270
+ releases: readonly SpeedClaimsReleaseEvent[] | null,
271
+ now: number,
272
+ ): SpeedClaimsDailyMeasurement {
273
+ const windowEnd = utcDayOf(now);
274
+ const startMs = Date.parse(`${windowEnd}T00:00:00.000Z`) - (DAILY_WINDOW_DAYS - 1) * DAY_MS;
275
+ const landedBuckets = dailyBuckets(landed);
276
+ const releaseBuckets = releases === null
277
+ ? null
278
+ : dailyBuckets(releases.map((event) => ({ key: event.version.trim().replace(/^v(?=\d)/, ''), ts: parseTime(event.ts) })));
279
+ const landedComplete = landedMeasured && landedBuckets.undated === 0;
280
+ const releasesComplete = releaseBuckets !== null && releaseBuckets.undated === 0;
281
+ return {
282
+ scope: 'last-30-days', windowStart: utcDayOf(startMs), windowEnd,
283
+ landedPrs: { status: landedComplete ? 'measured' : 'unmeasured', undatedCount: landedBuckets.undated },
284
+ releases: { status: releasesComplete ? 'measured' : 'unmeasured', undatedCount: releaseBuckets?.undated ?? 0 },
285
+ rows: Array.from({ length: DAILY_WINDOW_DAYS }, (_, index) => {
286
+ const date = utcDayOf(startMs + index * DAY_MS);
287
+ return {
288
+ date,
289
+ landedPrCount: landedComplete ? (landedBuckets.counts.get(date) ?? 0) : null,
290
+ releasedCount: releasesComplete ? (releaseBuckets!.counts.get(date) ?? 0) : null,
291
+ };
292
+ }),
293
+ note: DAILY_NOTE,
294
+ };
295
+ }
296
+
297
+ /** Release manifests record the actual publication time; lightweight tags have no creation timestamp. */
298
+ function readReleaseEventsFromLedger(): SpeedClaimsReleaseEvent[] {
299
+ const root = join(releaseLedgerRoot(), 'release');
300
+ const versions = readdirSync(root).filter((version) => VERSION_TAG.test(version));
301
+ const events: SpeedClaimsReleaseEvent[] = [];
302
+ for (const version of versions) {
303
+ // Absence and corruption are both unmeasured; only a readable manifest proves preparation.
304
+ const value: unknown = JSON.parse(readFileSync(join(root, version, 'release.json'), 'utf8')) as unknown;
305
+ const record = object(value);
306
+ if (record?.version !== version) throw new Error(`release manifest version mismatch: ${version}`);
307
+ if (!Object.hasOwn(record, 'publishedAt')) continue; // Prepared, not yet published.
308
+ if (typeof record.publishedAt !== 'string' || parseTime(record.publishedAt) === null) throw new Error(`invalid release publication time: ${version}`);
309
+ events.push({ version, ts: record.publishedAt });
310
+ }
311
+ // A checkout without tags or a release history cannot establish a queried zero.
312
+ if (versions.length === 0) throw new Error('release publication history unavailable');
313
+ return events;
314
+ }
315
+
195
316
  /** Parent launch-to-done census; no child ledger id is used as a join key. */
196
317
  export function measureParentSpeedClaims(
197
318
  events: readonly ParentSpeedClaimsEvent[], queue?: unknown,
@@ -262,7 +383,13 @@ export function measureParentSpeedClaims(
262
383
 
263
384
  /** Read-only elapsed-time census of canonical local run ledgers, including unsuccessful runs. */
264
385
  export function measureSpeedClaims(options: SpeedClaimsOptions = {}): SpeedClaimsMeasurement {
386
+ const now = options.now ?? Date.now();
265
387
  const ledgerDirectory = resolve(options.dir ?? runLedgerDir());
388
+ let releaseEvents: readonly SpeedClaimsReleaseEvent[] | null = null;
389
+ try {
390
+ const events = (options.readReleaseEvents ?? readReleaseEventsFromLedger)();
391
+ releaseEvents = Array.isArray(events) ? events : null;
392
+ } catch { /* An unreadable release source marks releases unmeasured; it does not erase landed-PR observations. */ }
266
393
  let parent: ParentSpeedClaimsMeasurement;
267
394
  try {
268
395
  const events = (options.readParentEvents ?? readParentEventsFromLogs)();
@@ -272,21 +399,27 @@ export function measureSpeedClaims(options: SpeedClaimsOptions = {}): SpeedClaim
272
399
  } catch {
273
400
  parent = { ...measureParentSpeedClaims([]), status: 'cannot-measure' };
274
401
  }
275
- const result: SpeedClaimsMeasurement = {
402
+ const result: Omit<SpeedClaimsMeasurement, 'daily'> = {
276
403
  scope: 'self-implement-run-ledger', ledgerDirectory, status: 'measured',
277
404
  completedCount: 0, inProgressCount: 0, cannotMeasureCount: 0, unknownSeatCount: 0, rows: [], note: NOTE, parent,
278
405
  };
279
406
  let files: string[];
280
407
  try { files = (options.list ?? readdirSync)(ledgerDirectory); }
281
- catch { return { ...result, status: 'cannot-measure' }; }
408
+ catch { return { ...result, status: 'cannot-measure', daily: measureDailySpeedClaims([], false, releaseEvents, now) }; }
282
409
 
410
+ const landed: DailyObservation[] = [];
411
+ let ledgerUnreadable = false;
283
412
  const groups = new Map<string, { seat: SpeedClaimsSeat; outcome: SpeedClaimsOutcome; values: number[] }>();
284
413
  for (const file of files.sort()) {
285
414
  const runId = file.endsWith('.jsonl') ? file.slice(0, -'.jsonl'.length) : '';
286
415
  if (!CANONICAL_RUN_ID.test(runId)) continue;
287
416
  try {
288
417
  const entries = loadRunLedger(runId, ledgerDirectory, options.read);
289
- if (!entries || entries.length === 0) { result.cannotMeasureCount++; continue; }
418
+ if (!entries || entries.length === 0) { ledgerUnreadable = true; result.cannotMeasureCount++; continue; }
419
+ const merges = entries.filter((entry) => entry.event === 'merged' && entry.data.merged === true);
420
+ const numbered = new Set(merges.map((entry) => landedPrIdentity(entry.data, runId)).filter((key) => key !== runId));
421
+ if (numbered.size > 1) ledgerUnreadable = true; // Conflicting PR identities cannot establish a daily count.
422
+ else for (const entry of merges) landed.push({ key: numbered.size === 1 ? [...numbered][0]! : runId, ts: parseTime(entry.timestamp) });
290
423
  const end = endOfRun(entries);
291
424
  const first = time(entries[0]);
292
425
  const createdAt = checkpointFor(runId, ledgerDirectory)?.createdAt;
@@ -306,14 +439,14 @@ export function measureSpeedClaims(options: SpeedClaimsOptions = {}): SpeedClaim
306
439
  group.values.push((endTime! - start) / 60_000);
307
440
  groups.set(key, group);
308
441
  result.completedCount++;
309
- } catch { result.cannotMeasureCount++; }
442
+ } catch { result.cannotMeasureCount++; ledgerUnreadable = true; }
310
443
  }
311
444
  result.rows = [...groups.values()].map(({ seat, outcome, values }) => {
312
445
  values.sort((a, b) => a - b);
313
446
  return { seat, outcome, count: values.length, underOneMinuteCount: values.filter((minutes) => minutes < 1).length, medianMinutes: percentile(values, 0.5), q1Minutes: percentile(values, 0.25), q3Minutes: percentile(values, 0.75) };
314
447
  }).sort((a, b) => SEATS.indexOf(a.seat) - SEATS.indexOf(b.seat) || a.outcome.localeCompare(b.outcome));
315
448
  if (result.cannotMeasureCount > 0) result.status = 'cannot-measure';
316
- return result;
449
+ return { ...result, daily: measureDailySpeedClaims(landed, !ledgerUnreadable, releaseEvents, now) };
317
450
  }
318
451
 
319
452
  export function renderSpeedClaims(result: SpeedClaimsMeasurement): string {
@@ -334,5 +467,11 @@ export function renderSpeedClaims(result: SpeedClaimsMeasurement): string {
334
467
  `parent cannot measure (excluded): ${result.parent.cannotMeasureCount}`,
335
468
  ...result.parent.rows.map((row) => `seat=${row.seat} stage=${row.stage} merged=${row.merged} count=${row.count} median=${row.medianMinutes}m q1=${row.q1Minutes}m q3=${row.q3Minutes}m`),
336
469
  `parent note: ${result.parent.note}`,
470
+ 'daily landed PRs / issued releases (last 30 days, UTC):',
471
+ `daily window: ${result.daily.windowStart}..${result.daily.windowEnd}`,
472
+ `landed PRs status: ${result.daily.landedPrs.status} (undated: ${result.daily.landedPrs.undatedCount})`,
473
+ `issued releases status: ${result.daily.releases.status} (undated: ${result.daily.releases.undatedCount})`,
474
+ ...result.daily.rows.map((row) => `date=${row.date} landed=${row.landedPrCount ?? 'unmeasured'} released=${row.releasedCount ?? 'unmeasured'}`),
475
+ `daily note: ${result.daily.note}`,
337
476
  ].join('\n');
338
477
  }
@@ -212,6 +212,26 @@ function groundingMessages(goal: string, terms: readonly string[]): LLMMessage[]
212
212
  }];
213
213
  }
214
214
 
215
+ function unreadEvidencePaths(evidence: string, readFiles: ReadonlySet<string>): string[] {
216
+ return Array.from(new Set(evidence.split(/\r?\n/)
217
+ .flatMap(repositoryRelativeCodePaths)
218
+ .filter((path) => isCodeRootPath(path) && !readFiles.has(path)))).slice(0, 8);
219
+ }
220
+
221
+ function rejectedEvidenceFeedback(evidence: string, readFiles: ReadonlySet<string>): LLMMessage {
222
+ const unread = unreadEvidencePaths(evidence, readFiles);
223
+ const read = Array.from(readFiles).slice(0, 8);
224
+ return {
225
+ role: 'user',
226
+ content: [
227
+ 'Your completion evidence was rejected because it cited a repository path that Read did not verify.',
228
+ `Unverified paths: ${unread.length ? unread.join(', ') : '(none in allowed code roots)'}`,
229
+ `Paths actually Read: ${read.length ? read.join(', ') : '(none)'}`,
230
+ 'Try once more. Read each relevant unverified path before citing it, or remove it from the evidence. Cite only repository-relative code paths that this grounding run actually Read, each with a concrete reason. If no path is relevant, do not invent evidence.',
231
+ ].join('\n'),
232
+ };
233
+ }
234
+
215
235
  /** Bounded, shared, read-only grounding loop. Failure or no candidates returns null. */
216
236
  export async function groundPersistently(goal: string, cwd: string, deps: PersistentGroundingDeps = {}): Promise<PersistentGroundingResult | null> {
217
237
  const startedAt = performance.now();
@@ -250,20 +270,53 @@ export async function groundPersistently(goal: string, cwd: string, deps: Persis
250
270
  },
251
271
  };
252
272
  const options: GoalLoopOptions = { maxIterations, objective: `Persistent repository grounding: ${goal.slice(0, 200)}` };
253
- const result = await loop(ctx, options);
254
- const verified = result.goalComplete && result.stopReason === 'goal_complete'
273
+ const first = await loop(ctx, options);
274
+ const firstVerified = first.goalComplete && first.stopReason === 'goal_complete'
255
275
  ? verifiedEvidenceFromCompletion(completionEvidence, readFiles)
256
276
  : {
257
277
  files: [],
258
278
  evidence: [],
259
279
  rejection: { noPath: 0, outsideRoot: 0, notRead: 0, noMeaningfulText: 0, samples: [] },
260
280
  };
281
+ let result = first;
282
+ let verified = firstVerified;
283
+ let feedbackRetry = false;
284
+ let retryIterations = 0;
285
+ if (firstVerified.rejection.notRead > 0) {
286
+ feedbackRetry = true;
287
+ const feedback = rejectedEvidenceFeedback(completionEvidence, readFiles);
288
+ completionEvidence = '';
289
+ try {
290
+ const retry = await loop({ ...ctx, sessionId: `${sessionId}-evidence-retry`, messages: [...ctx.messages, feedback] },
291
+ { ...options, maxIterations: 2 });
292
+ retryIterations = retry.iterations;
293
+ const retryVerified = retry.goalComplete && retry.stopReason === 'goal_complete'
294
+ ? verifiedEvidenceFromCompletion(completionEvidence, readFiles)
295
+ : null;
296
+ if (retryVerified) {
297
+ verified = {
298
+ files: Array.from(new Set([...firstVerified.files, ...retryVerified.files])),
299
+ evidence: Array.from(new Set([...firstVerified.evidence, ...retryVerified.evidence])),
300
+ rejection: retryVerified.rejection,
301
+ };
302
+ result = retry;
303
+ }
304
+ } catch (error) {
305
+ try {
306
+ debug.log('grounding.persistent', 'evidence-retry-failed', { sessionId,
307
+ reason: error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200) }, { level: 'warn' });
308
+ } catch {
309
+ // A failed retry and its telemetry cannot erase evidence already verified by Read.
310
+ }
311
+ }
312
+ }
313
+ const totalIterations = first.iterations + retryIterations;
261
314
  try {
262
315
  debug.log('grounding.persistent', 'finished', {
263
316
  sessionId,
264
317
  maxIterations,
265
318
  durationMs: durationMs(),
266
- iterations: result.iterations,
319
+ iterations: totalIterations,
267
320
  stopReason: result.stopReason,
268
321
  // ⛔ 「끝났다」가 두 뜻이다 — ⓐ 스스로 목표를 끝냈다 ⓑ 턴이 «그냥» 끝났다.
269
322
  // 📏 2026-08-11 72차 전수(400건): 후보 0인 finished 가 5건이고 ***다섯 다 end_turn*** 이었다.
@@ -273,6 +326,8 @@ export async function groundPersistently(goal: string, cwd: string, deps: Persis
273
326
  candidateCount: verified.files.length,
274
327
  evidenceCount: verified.evidence.length,
275
328
  evidenceRejection: verified.rejection,
329
+ feedbackRetry,
330
+ ...(feedbackRetry ? { firstEvidenceRejection: firstVerified.rejection } : {}),
276
331
  toolCalls,
277
332
  });
278
333
  } catch {
@@ -281,7 +336,7 @@ export async function groundPersistently(goal: string, cwd: string, deps: Persis
281
336
  return {
282
337
  files: verified.files,
283
338
  evidence: verified.evidence,
284
- iterations: result.iterations,
339
+ iterations: totalIterations,
285
340
  stopReason: result.stopReason,
286
341
  };
287
342
  } catch (error) {
@@ -31,9 +31,13 @@ import {
31
31
  runShell as runnerDispatch,
32
32
  getShellRunnerDeps,
33
33
  } from '../../shell-runner/dispatch.js';
34
- import type { ShellResult as RunnerResult } from '../../shell-runner/types.js';
34
+ import type { ShellHandle, ShellResult as RunnerResult } from '../../shell-runner/types.js';
35
35
  import { debug } from '../../debug/log.js';
36
36
  import { getSessionCwd } from '../../session/working-dir.js';
37
+ import {
38
+ globalTaskNotificationQueue,
39
+ type TaskNotificationQueue,
40
+ } from '../../agent/task-notification.js';
37
41
 
38
42
  /** LLMToolSpec — the shape surfaced to the model. Kept narrow so
39
43
  * accidental misuse is hard: no env override, no cwd outside the
@@ -178,6 +182,62 @@ function logShellDispatch(event: string, data: Record<string, unknown>): void {
178
182
  debug.log('shell.dispatch', event, data);
179
183
  }
180
184
 
185
+ // ── bg completion → turn notification queue (TUI-BG-SHELL) ───────────
186
+ // When a bg-mode shell handle settles, its result is no longer awaited
187
+ // by anyone — the spawn already returned the `outcome=started` stub.
188
+ // To surface the eventual result we enqueue one task-notification into
189
+ // the existing turn notification queue (injected at turn-start by
190
+ // chat/skill-runner). One notice per handle; opening an idle turn from
191
+ // the wake signal is deliberately NOT wired here (TUI-EVENT-TURN-QUEUE).
192
+
193
+ export interface ShellCompletionNoticeInput {
194
+ handle: ShellHandle;
195
+ ok: boolean;
196
+ exitCode: number | null;
197
+ durationMs: number;
198
+ /** Set when ok === false — the runner's failure reason. */
199
+ errorMessage?: string;
200
+ /** Caller-supplied free-form label (RunShell's `description` arg). */
201
+ label?: string;
202
+ }
203
+
204
+ /** Per-handle dedup guard — module-local, keyed by the handle OBJECT.
205
+ * A registry replacement may mint a NEW handle that reuses an id; the
206
+ * shell layer must not swallow that fresh completion as a duplicate
207
+ * settle (an id-keyed record here would). */
208
+ const notifiedHandles = new WeakSet<ShellHandle>();
209
+ let nextShellCompletionNoticeId = 0;
210
+
211
+ /** Enqueue one bg shell-handle completion notice. Returns true when this
212
+ * handle had not notified before (the notice was handed to `queue`),
213
+ * false on a duplicate settle of the same handle. The queue's own
214
+ * The shared queue deduplicates by taskId even after drain, so each
215
+ * completed handle gets its own notification id. The original handle id
216
+ * remains in the output for ShellPoll and human follow-up.
217
+ * `queue` defaults to the global turn notification queue; injectable
218
+ * so tests can isolate. */
219
+ export function enqueueShellCompletionNotice(
220
+ input: ShellCompletionNoticeInput,
221
+ queue: TaskNotificationQueue = globalTaskNotificationQueue,
222
+ ): boolean {
223
+ if (notifiedHandles.has(input.handle)) return false;
224
+ notifiedHandles.add(input.handle);
225
+ const header = `RunShell bg handle=${input.handle.id} ${input.ok ? 'completed' : 'failed'}`;
226
+ const output = `${header}${input.ok || input.exitCode !== null ? ` exitCode=${input.exitCode ?? 'null'}` : ''} elapsed=${input.durationMs}ms${input.errorMessage ? ` error=${input.errorMessage}` : ''}`;
227
+ queue.enqueue({
228
+ taskId: `shell:${input.handle.id}:${++nextShellCompletionNoticeId}`,
229
+ agentName: 'RunShell',
230
+ ...(input.label ? { label: input.label } : {}),
231
+ state: input.ok ? 'done' : 'error',
232
+ output,
233
+ truncated: false,
234
+ ...(input.errorMessage ? { errorMessage: input.errorMessage } : {}),
235
+ durationMs: input.durationMs,
236
+ finishedAt: Date.now(),
237
+ });
238
+ return true;
239
+ }
240
+
181
241
  export async function dispatchRunShell(
182
242
  rawArgs: Record<string, unknown>,
183
243
  opts: { signal?: AbortSignal } = {},
@@ -269,6 +329,16 @@ async function dispatchViaRunner(args: {
269
329
  exitCode: result.exitCode ?? null,
270
330
  durationMs: result.durationMs,
271
331
  });
332
+ const ok = result.outcome === 'exit' && result.exitCode === 0;
333
+ enqueueShellCompletionNotice({
334
+ handle,
335
+ ok,
336
+ exitCode: result.exitCode ?? null,
337
+ durationMs: result.durationMs,
338
+ ...(!ok ? { errorMessage: result.exitCode !== undefined
339
+ ? `exitCode=${result.exitCode}` : `outcome=${result.outcome}` } : {}),
340
+ ...(args.description !== undefined ? { label: args.description } : {}),
341
+ });
272
342
  },
273
343
  (err: unknown) => {
274
344
  debug.log('skill-tool.shell', 'bg-finished', {
@@ -277,6 +347,14 @@ async function dispatchViaRunner(args: {
277
347
  durationMs: Date.now() - bgStartedAt,
278
348
  error: err instanceof Error ? err.message : String(err),
279
349
  });
350
+ enqueueShellCompletionNotice({
351
+ handle,
352
+ ok: false,
353
+ exitCode: null,
354
+ durationMs: Date.now() - bgStartedAt,
355
+ errorMessage: err instanceof Error ? err.message : String(err),
356
+ ...(args.description !== undefined ? { label: args.description } : {}),
357
+ });
280
358
  },
281
359
  );
282
360
  return {
@@ -19,6 +19,7 @@ import { debug } from '../debug/log.js';
19
19
  import { effectiveInstanceRoot } from '../instance/resolve.js';
20
20
  import { spawnElanous } from '../instance/spawn-elanous.js';
21
21
  import { getUserConfig } from '../user-config.js';
22
+ import { resyncPendingPath } from './resync-tick.js';
22
23
 
23
24
  export const REJUDGE_TICK_EVENT = 'rejudge-tick';
24
25
  export const REJUDGE_DEFAULT_MAX_LOAD = 60;
@@ -49,6 +50,7 @@ export interface RejudgeTickDeps {
49
50
  config?: () => { rejudgeEveryMinutes?: number; rejudgeMaxLoad?: number } | undefined;
50
51
  load?: () => number;
51
52
  stampPath?: string;
53
+ pendingPath?: string;
52
54
  run?: () => Promise<RejudgeRun>;
53
55
  log?: (event: string, data: Record<string, unknown>) => void;
54
56
  }
@@ -110,7 +112,9 @@ export async function rejudgeTick(deps: RejudgeTickDeps = {}): Promise<RejudgeTi
110
112
  const now = (deps.now ?? (() => new Date()))();
111
113
  const stampPath = deps.stampPath ?? defaultStampPath();
112
114
  const lastStartedAt = readStamp(stampPath);
113
- if (lastStartedAt && now.getTime() - Date.parse(lastStartedAt) < everyMinutes * 60_000 - REJUDGE_JITTER_MS) {
115
+ const pendingAt = readPending(deps.pendingPath ?? resyncPendingPath());
116
+ const savedSinceLastRun = pendingAt !== null && (!lastStartedAt || Date.parse(pendingAt) > Date.parse(lastStartedAt));
117
+ if (!savedSinceLastRun && lastStartedAt && now.getTime() - Date.parse(lastStartedAt) < everyMinutes * 60_000 - REJUDGE_JITTER_MS) {
114
118
  return emit({ decision: 'not-due', everyMinutes, lastStartedAt });
115
119
  }
116
120
  const maxLoad = positive(cfg?.rejudgeMaxLoad) ?? REJUDGE_DEFAULT_MAX_LOAD;
@@ -133,6 +137,13 @@ export async function rejudgeTick(deps: RejudgeTickDeps = {}): Promise<RejudgeTi
133
137
  }
134
138
  }
135
139
 
140
+ function readPending(path: string): string | null {
141
+ try {
142
+ const value = (JSON.parse(readFileSync(path, 'utf8')) as { savedAt?: unknown }).savedAt;
143
+ return typeof value === 'string' && Number.isFinite(Date.parse(value)) ? value : null;
144
+ } catch { return null; }
145
+ }
146
+
136
147
  if (import.meta.main) {
137
148
  try { await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('steward'); }
138
149
  catch (error) { console.error(`steward rejudge: log sink registration failed — ${error instanceof Error ? error.message : String(error)}`); }