elanous 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1394-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
  6. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-9da4678f8a310d18.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-e49b9b76c565a0b1.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-a0ea8f5a2341ee6b.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6ddd2109b7fc3a80.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-f672536f14f0969f.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
  56. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
  57. package/apps/pwa/out/approvals/index.html +1 -1
  58. package/apps/pwa/out/approvals/index.txt +14 -14
  59. package/apps/pwa/out/autopilot/index.html +1 -1
  60. package/apps/pwa/out/autopilot/index.txt +14 -14
  61. package/apps/pwa/out/board/index.html +1 -1
  62. package/apps/pwa/out/board/index.txt +14 -14
  63. package/apps/pwa/out/botlab/index.html +1 -1
  64. package/apps/pwa/out/botlab/index.txt +14 -14
  65. package/apps/pwa/out/bots/index.html +1 -1
  66. package/apps/pwa/out/bots/index.txt +14 -14
  67. package/apps/pwa/out/chat/index.html +1 -1
  68. package/apps/pwa/out/chat/index.txt +14 -14
  69. package/apps/pwa/out/consult/index.html +1 -1
  70. package/apps/pwa/out/consult/index.txt +14 -14
  71. package/apps/pwa/out/control/index.html +1 -1
  72. package/apps/pwa/out/control/index.txt +14 -14
  73. package/apps/pwa/out/design-check/index.html +1 -1
  74. package/apps/pwa/out/design-check/index.txt +14 -14
  75. package/apps/pwa/out/editor/index.html +1 -1
  76. package/apps/pwa/out/editor/index.txt +14 -14
  77. package/apps/pwa/out/exec/index.html +1 -1
  78. package/apps/pwa/out/exec/index.txt +14 -14
  79. package/apps/pwa/out/field/index.html +1 -1
  80. package/apps/pwa/out/field/index.txt +14 -14
  81. package/apps/pwa/out/healthz +1 -1
  82. package/apps/pwa/out/index.html +1 -1
  83. package/apps/pwa/out/index.txt +14 -14
  84. package/apps/pwa/out/inside/index.html +1 -1
  85. package/apps/pwa/out/inside/index.txt +14 -14
  86. package/apps/pwa/out/intake/index.html +1 -1
  87. package/apps/pwa/out/intake/index.txt +14 -14
  88. package/apps/pwa/out/live/index.html +1 -1
  89. package/apps/pwa/out/live/index.txt +14 -14
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +14 -14
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +14 -14
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +14 -14
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +14 -14
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +14 -14
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +14 -14
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +15 -15
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +14 -14
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +14 -14
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +14 -14
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +14 -14
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +14 -14
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +14 -14
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +14 -14
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +14 -14
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +14 -14
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +14 -14
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +14 -14
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +14 -14
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +14 -14
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +14 -14
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +14 -14
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +14 -14
  142. package/examples/teams/personas/backend.yaml +4 -0
  143. package/examples/teams/personas/pm.yaml +4 -0
  144. package/examples/teams/personas/qa.yaml +4 -0
  145. package/examples/teams/small-product.yaml +20 -0
  146. package/graphs/context/nightly-condense.yaml +21 -0
  147. package/graphs/context/recipes.yaml +12 -0
  148. package/graphs/demo/inside-seed.yaml +20 -0
  149. package/graphs/demo/recipes.yaml +12 -0
  150. package/graphs/lecture/lecture-note.yaml +28 -0
  151. package/graphs/lecture/recipes.yaml +17 -0
  152. package/package.json +2 -1
  153. package/plugins/card-followup/examples/sample-card.png +0 -0
  154. package/scripts/context-hooks/emit.ts +36 -14
  155. package/scripts/coord-channel-watch.sh +1 -0
  156. package/scripts/discord-seat-probe.ts +144 -0
  157. package/scripts/hq/hq-standby.sh +65 -0
  158. package/scripts/hq/standby-promote.ts +173 -0
  159. package/scripts/hq/standby-role.ts +59 -0
  160. package/scripts/hq/standby-snapshot.ts +226 -0
  161. package/scripts/hq/standby-verify.ts +219 -0
  162. package/scripts/lecture-note/node.ts +256 -0
  163. package/scripts/lib/telegram-inject-args.ts +31 -0
  164. package/scripts/measure-pod-memory-by-goal.ts +130 -0
  165. package/scripts/release-loop/auto-approve-node.ts +1 -1
  166. package/scripts/release-loop/checklist-gate-node.ts +2 -1
  167. package/scripts/release-loop/gate-node.ts +61 -17
  168. package/scripts/release-loop/ops-upgrade-node.ts +59 -6
  169. package/scripts/release-loop/publish-node.ts +4 -1
  170. package/scripts/release-loop/version-node.ts +13 -3
  171. package/scripts/telegram-inject.ts +14 -6
  172. package/scripts/test-deterministic.ts +53 -3
  173. package/scripts/test-diet/remeasure-list.ts +112 -0
  174. package/scripts/ux/demo-rehearsal.ts +17 -13
  175. package/scripts/ux/demo-seed.ts +108 -0
  176. package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
  177. package/skills/omni-crawl/SKILL.md +16 -16
  178. package/skills/omni-crawl/package-lock.json +1 -544
  179. package/skills/omni-crawl/package.json +3 -4
  180. package/skills/omni-crawl/scripts/main.ts +13 -13
  181. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  182. package/skills/omni-crawl/src/apify.ts +13 -2
  183. package/skills/omni-crawl/src/capture.ts +6 -0
  184. package/skills/omni-crawl/src/registry.ts +2 -2
  185. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  186. package/skills/omni-digest/SKILL.md +10 -11
  187. package/skills/omni-digest/package-lock.json +0 -545
  188. package/skills/omni-digest/package.json +2 -5
  189. package/skills/omni-digest/scripts/main.ts +3 -3
  190. package/skills/omni-digest/src/delegate.ts +5 -5
  191. package/skills/omni-digest/src/fetch.ts +2 -2
  192. package/skills/omni-digest/src/media.ts +25 -11
  193. package/skills/omni-digest/src/visual.ts +3 -2
  194. package/skills/omni-market/scripts/main.ts +1 -1
  195. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  196. package/src/agent/identity-env.ts +11 -0
  197. package/src/agent-mission/driver.ts +150 -23
  198. package/src/agent-mission/duo.ts +94 -0
  199. package/src/agent-mission/pty-decision.ts +2 -2
  200. package/src/autopilot/pty-control-loop.ts +4 -1
  201. package/src/autopilot/track-agent-shadow.ts +2 -1
  202. package/src/autopilot/track-agent.ts +4 -1
  203. package/src/boot/daemon-prompt-request.ts +3 -0
  204. package/src/boot/daemon-prompt-turn.ts +6 -2
  205. package/src/boot/daemon-runtime.ts +48 -16
  206. package/src/chat/index.ts +1 -0
  207. package/src/cli/decisions-cli.ts +51 -19
  208. package/src/cli/outputs-cli.ts +92 -0
  209. package/src/cli/persona-cli.ts +45 -13
  210. package/src/cli/pod-cli.ts +27 -3
  211. package/src/cli/pod-memory-advice.ts +35 -0
  212. package/src/cli/pr-cli.ts +15 -1
  213. package/src/cli/release-cli.ts +135 -15
  214. package/src/context-bus/condense.ts +194 -0
  215. package/src/context-bus/context-day.ts +82 -0
  216. package/src/context-bus/context-now-surfaces.ts +32 -2
  217. package/src/context-bus/context-now.ts +2 -2
  218. package/src/context-bus/coord-events.ts +2 -2
  219. package/src/context-bus/long-term-memory.ts +118 -22
  220. package/src/context-bus/nightly-condense.ts +153 -0
  221. package/src/context-bus/session-events.ts +35 -0
  222. package/src/dashboard/dashboard-status-lines.ts +2 -0
  223. package/src/dashboard/index.ts +8 -0
  224. package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
  225. package/src/decisions/decision-cards.ts +13 -7
  226. package/src/decisions/decision-ledger.ts +43 -17
  227. package/src/domains/outbound-alert.ts +99 -31
  228. package/src/domains/outbound-log-probe.ts +8 -0
  229. package/src/execution-loop/launch-gate.ts +17 -8
  230. package/src/field-note/telegram.ts +142 -0
  231. package/src/graph-kinds/registry.ts +2 -2
  232. package/src/graph-runner/graph-approval-card.ts +2 -2
  233. package/src/graph-runner/graph-grow-llm.ts +23 -13
  234. package/src/graph-runner/graph-grow.ts +69 -13
  235. package/src/graph-runner/graph-tick.ts +9 -2
  236. package/src/graph-runner/runner.ts +92 -24
  237. package/src/harness/harness-cli-command.ts +165 -17
  238. package/src/harness/harness-queue-child.ts +37 -0
  239. package/src/harness/harness-queue.ts +306 -0
  240. package/src/hooks/consult-intake.ts +153 -0
  241. package/src/hooks/github-shadow.ts +72 -2
  242. package/src/hooks/receiver.ts +82 -3
  243. package/src/hq/hq.ts +318 -0
  244. package/src/hq/lease.ts +210 -0
  245. package/src/hq/ledger-write-target.ts +41 -0
  246. package/src/index.ts +232 -16
  247. package/src/intake-plane/discord-seat-work.ts +36 -3
  248. package/src/intake-plane/linear-wish.ts +44 -0
  249. package/src/intake-plane/persona-answer.ts +5 -0
  250. package/src/intake-plane/telegram-seat-work.ts +13 -6
  251. package/src/intake-plane/wish-card.ts +33 -0
  252. package/src/llm/difficulty.ts +1 -1
  253. package/src/loops/checker.ts +178 -0
  254. package/src/loops/loop-cli.ts +37 -3
  255. package/src/loops/observe.ts +49 -0
  256. package/src/loops/orchestrator/overlap.ts +141 -0
  257. package/src/loops/orchestrator/traffic.ts +150 -0
  258. package/src/loops/package/verify.ts +125 -0
  259. package/src/loops/verdict.ts +22 -0
  260. package/src/maturity/feature-maturity.ts +5 -4
  261. package/src/memory.ts +37 -4
  262. package/src/nexus/api/card-followup-route.ts +118 -0
  263. package/src/nexus/api/decisions-route.ts +68 -0
  264. package/src/nexus/api/http-server.ts +78 -8
  265. package/src/nexus/api/outbound-report.ts +40 -7
  266. package/src/nexus/api/personas.ts +97 -54
  267. package/src/nexus/api/skill-exec-route.ts +97 -0
  268. package/src/nexus/api/task-cards-api.ts +19 -0
  269. package/src/nexus/api/workflow-history.ts +88 -0
  270. package/src/nexus/api/workflows.ts +52 -0
  271. package/src/nexus/outbound/delivery-ledger.ts +17 -8
  272. package/src/nexus/outbound/router.ts +8 -3
  273. package/src/persona/loader.ts +18 -0
  274. package/src/persona/persona-todo.ts +14 -9
  275. package/src/persona/presets.ts +10 -2
  276. package/src/persona/team.ts +105 -0
  277. package/src/persona/types.ts +4 -0
  278. package/src/plugins/maker/market-bundle.ts +2 -1
  279. package/src/pod-lease/host-lease.ts +130 -20
  280. package/src/release-loop/checklist.ts +15 -3
  281. package/src/release-loop/feature-store.ts +24 -10
  282. package/src/release-loop/landed-but-yellow.ts +60 -0
  283. package/src/seat-dispatch/seat-ask.ts +57 -12
  284. package/src/seat-loop/seat-loop.ts +253 -18
  285. package/src/seat-loop/seat-pack.ts +127 -0
  286. package/src/self-dev/ask-launch-flow.ts +3 -0
  287. package/src/self-dev/draft-sweep.ts +44 -10
  288. package/src/self-dev/draft-triage-rules.ts +54 -2
  289. package/src/self-implement/budget-gate.ts +29 -12
  290. package/src/self-implement/gate-candidate-ranking.ts +60 -0
  291. package/src/self-implement/gate-scope.ts +9 -8
  292. package/src/self-implement/headless-elanous-driver.ts +7 -2
  293. package/src/self-implement/l8-merge-queue.ts +157 -1
  294. package/src/self-implement/orchestrator.ts +15 -0
  295. package/src/self-implement/test-seams.ts +1 -0
  296. package/src/steward/launch.ts +5 -1
  297. package/src/steward/readiness.ts +125 -0
  298. package/src/steward/rescue.ts +75 -0
  299. package/src/steward/triage.ts +48 -9
  300. package/src/task-cards/card-cli.ts +78 -0
  301. package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
  302. package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
  303. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  304. package/src/task-orchestrator/surfaces/self-implement-pod.ts +46 -8
  305. package/src/telegram-commands.ts +21 -0
  306. package/src/telegram-dispatch-forward.ts +1 -1
  307. package/src/telegram-fabric-plan.ts +90 -0
  308. package/src/telegram-project-command.ts +84 -4
  309. package/src/telegram.ts +37 -5
  310. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  311. package/src/user-config.ts +101 -13
  312. package/src/version/packed-revision.json +1 -1
  313. package/src/workflow-runtime/executor.ts +4 -0
  314. package/src/workflow-runtime/node-catalog.ts +13 -0
  315. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  316. package/src/workflow-runtime/schema.ts +17 -1
  317. package/src/workflow-runtime/types.ts +13 -1
  318. package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
  340. package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
  341. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
  342. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
@@ -27,6 +27,10 @@ export interface DraftSweepAdapters {
27
27
  closeDraft(repository: string, number: number, comment: string): Promise<void>;
28
28
  /** Resolve the latest claim comment owner; a failed lookup must not authorize a close. */
29
29
  getClaimOwner?(repository: string, number: number): Promise<string | undefined>;
30
+ /** Whether the current owned run has a self-implement.result final observation, even if its worktree remains. */
31
+ hasFinalRunResult?(draft: SweepDraft, repository: string): Promise<boolean | undefined>;
32
+ /** Per-file last change on the draft branch; undefined means file coverage is unverified. */
33
+ getLatestFileChanges?(draft: SweepDraft, repository: string): Promise<Readonly<Record<string, string>> | undefined>;
30
34
  }
31
35
 
32
36
  export interface DraftSweepOptions {
@@ -120,9 +124,23 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
120
124
  return finish();
121
125
  }
122
126
  let statuses: Map<number, string | undefined>;
127
+ let finality: Map<number, boolean | undefined>;
128
+ let latestFileChanges: Map<number, Readonly<Record<string, string>> | undefined>;
123
129
  try {
124
130
  statuses = new Map<number, string | undefined>();
125
- for (const draft of drafts) statuses.set(draft.number, await adapters.getRunStatus(draft, repository));
131
+ finality = new Map<number, boolean | undefined>();
132
+ latestFileChanges = new Map<number, Readonly<Record<string, string>> | undefined>();
133
+ for (const draft of drafts) {
134
+ const status = await adapters.getRunStatus(draft, repository);
135
+ // A prior final cannot terminate a new active run on the same draft.
136
+ const active = status === 'running' || status === 'probable-running';
137
+ const final = active ? false : await adapters.hasFinalRunResult?.(draft, repository);
138
+ finality.set(draft.number, final);
139
+ statuses.set(draft.number, final ? 'self-implement.result final' : status);
140
+ if (draft.changedFiles?.length && adapters.getLatestFileChanges) {
141
+ latestFileChanges.set(draft.number, await adapters.getLatestFileChanges(draft, repository));
142
+ }
143
+ }
126
144
  } catch (error) {
127
145
  result.error = String(error);
128
146
  return finish();
@@ -132,8 +150,10 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
132
150
  let unobserved = 0;
133
151
  let claimed = 0;
134
152
  let claimExpired = 0;
135
- for (const draft of drafts) {
153
+ for (const listedDraft of drafts) {
154
+ const draft = { ...listedDraft, latestFileChanges: latestFileChanges.get(listedDraft.number) };
136
155
  const runStatus = statuses.get(draft.number);
156
+ const finalRunResult = finality.get(draft.number);
137
157
  const states = stateLabels.filter((state) => draft.labels.includes(state.name));
138
158
  const contradiction = states.length > 1 || draft.labels.includes(approvalLabel ?? '');
139
159
  if (draft.labels.includes(releaseHoldLabel ?? '') || (!contradiction && draft.labels.includes(keepLabel ?? ''))) {
@@ -147,8 +167,14 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
147
167
  ? (now.getTime() - Date.parse(draft.updatedAt)) / 3_600_000 : NaN;
148
168
  // An unobserved claim may still have a merged twin: liveness/holds retain their priority,
149
169
  // but a superseded draft must not be hidden by the six-hour claim window.
150
- const mergedDecision = isClaimed ? decideDraft({ draft, runStatus, mergedTwins: merged, liveBranches, ageHours: idleHours }) : undefined;
151
- const supersededClaim = mergedDecision?.reason.startsWith('superseded-by #') === true;
170
+ const mergedDecision = isClaimed ? decideDraft({ draft, runStatus, mergedTwins: merged, openDrafts: drafts, liveBranches,
171
+ finalRunResult, ageHours: idleHours }) : undefined;
172
+ const supersededClaim = mergedDecision?.reason.startsWith('superseded-by #') === true
173
+ || mergedDecision?.reason.startsWith('duplicate-of-open #') === true;
174
+ if (!contradiction && mergedDecision?.reason === 'branch-finality-unobserved') {
175
+ record({ number: draft.number, action: 'keep', reason: mergedDecision.reason, applied: false });
176
+ continue;
177
+ }
152
178
  // Without an observed update time, neither expiration nor a fallback to creation time can authorize mutation —
153
179
  // but a merged twin does not need the update time, so superseded keeps its priority over the unobserved claim.
154
180
  if (isClaimed && !supersededClaim && (typeof draft.updatedAt !== 'string' || !Number.isFinite(Date.parse(draft.updatedAt)))) {
@@ -162,13 +188,21 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
162
188
  }
163
189
  if (isClaimed && !supersededClaim && Number.isFinite(idleHours) && idleHours >= CLAIM_IDLE_HOURS) {
164
190
  claimExpired += 1;
165
- if (runStatus === 'running' || runStatus === 'probable-running' || liveBranches.has(draft.branch)) {
191
+ if (liveBranches.has(draft.branch) && finalRunResult === undefined
192
+ && runStatus !== 'running' && runStatus !== 'probable-running') {
193
+ record({ number: draft.number, action: 'keep', reason: 'branch-finality-unobserved', applied: false });
194
+ continue;
195
+ }
196
+ if (runStatus === 'running' || runStatus === 'probable-running' ||
197
+ (runStatus !== 'self-implement.result final' && liveBranches.has(draft.branch))) {
166
198
  record({ number: draft.number, action: 'keep', reason: 'claim-expired-but-live', applied: false });
167
199
  continue;
168
200
  }
169
201
  const action = closes >= DRAFT_SWEEP_CLOSE_CAP ? 'keep' : 'close';
170
202
  if (action === 'close') closes += 1;
171
- const entry: DraftSweepEntry = { number: draft.number, action, reason: action === 'close' ? 'claim-expired' : 'close-cap',
203
+ const entry: DraftSweepEntry = { number: draft.number, action,
204
+ reason: action === 'close' ? runStatus === 'self-implement.result final'
205
+ ? 'claim-expired (self-implement.result final; worktree is not live)' : 'claim-expired' : 'close-cap',
172
206
  ...(action === 'close' ? { statusLabel: stalledLabel } : {}), applied: false };
173
207
  record(entry);
174
208
  if (!apply || action !== 'close') continue;
@@ -178,7 +212,7 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
178
212
  await adapters.setLabels(repository, draft.number, { add: stalledLabel, remove: [runningLabel] });
179
213
  labelsChanged = true;
180
214
  await adapters.closeDraft(repository, draft.number,
181
- `Draft sweep: 처리 중 표식이 ${CLAIM_IDLE_HOURS}시간 갱신 없음 — 런·워크트리 없음 · 주인 ${owner}. Closed as stalled. Branch preserved; reopen to restore.`);
215
+ `Draft sweep: 처리 중 표식이 ${CLAIM_IDLE_HOURS}시간 갱신 없음 — ${runStatus === 'self-implement.result final' ? 'self-implement.result final · 남은 워크트리는 live 아님' : '런·워크트리 없음'} · 주인 ${owner}. Closed as stalled. Branch preserved; reopen to restore.`);
182
216
  entry.applied = true;
183
217
  } catch (error) {
184
218
  if (labelsChanged) entry.partialApplied = true;
@@ -191,8 +225,8 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
191
225
  record({ number: draft.number, action: 'keep', reason: 'outside-harness', applied: false });
192
226
  continue;
193
227
  }
194
- let decision = supersededClaim ? mergedDecision! : decideDraft({ draft, runStatus, mergedTwins: merged, liveBranches,
195
- ageHours: runStatus ? ageHours : idleHours });
228
+ let decision = supersededClaim ? mergedDecision! : decideDraft({ draft, runStatus, mergedTwins: merged, openDrafts: drafts, liveBranches,
229
+ finalRunResult, ageHours: runStatus ? ageHours : idleHours });
196
230
  if (!runStatus && (decision.reason === 'unobserved' || decision.reason === 'stale-unobserved')) unobserved += 1;
197
231
  if (!contradiction && decision.action === 'close') {
198
232
  if (closes >= DRAFT_SWEEP_CLOSE_CAP) decision = { action: 'keep', reason: 'close-cap' };
@@ -204,7 +238,7 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
204
238
  : decision.reason;
205
239
  const isLive = decision.reason === 'live';
206
240
  const target = action === 'report' ? undefined : action === 'close'
207
- ? decision.reason.startsWith('superseded-by #') ? supersededLabel : stalledLabel
241
+ ? decision.reason.startsWith('superseded-by #') || decision.reason.startsWith('duplicate-of-open #') ? supersededLabel : stalledLabel
208
242
  : isLive ? (states.length ? runningLabel : undefined) : runStatus && (states[0]?.name === runningLabel || states.length === 0) ? stalledLabel : undefined;
209
243
  const entry: DraftSweepEntry = { number: draft.number, action, reason, ...(target ? { statusLabel: target } : {}), applied: false };
210
244
  record(entry);
@@ -6,14 +6,25 @@ export interface DraftTriagePr {
6
6
  title?: string;
7
7
  branch: string;
8
8
  labels?: readonly string[];
9
+ body?: string;
10
+ mergeCommitMessage?: string;
11
+ landingVerifiedComments?: readonly string[];
12
+ createdAt?: string;
13
+ mergedAt?: string;
14
+ changedFiles?: readonly string[];
15
+ /** Latest change to each draft file, from the draft branch's commit history. Missing is not creation time. */
16
+ latestFileChanges?: Readonly<Record<string, string>>;
9
17
  }
10
18
 
11
19
  export interface DraftTriageInput {
12
20
  draft: DraftTriagePr;
13
21
  runStatus: string | undefined;
14
22
  mergedTwins: readonly DraftTriagePr[];
23
+ openDrafts?: readonly DraftTriagePr[];
15
24
  ageHours: number;
16
25
  liveBranches: ReadonlySet<string>;
26
+ /** false only when the current run's final result has actually been checked and ruled out. */
27
+ finalRunResult?: boolean;
17
28
  }
18
29
 
19
30
  export type DraftDecision = { action: 'close' | 'keep'; reason: string };
@@ -25,11 +36,22 @@ const PROTECTED_LABELS: ReadonlySet<string> = new Set<string>([...PR_LABELS
25
36
  .map((label) => label.name), RELEASE_PATH_LABEL]);
26
37
 
27
38
  const goalId = (branch: string): string | undefined => /(?:^|[-/])goalid-([a-f0-9]+)(?=-|\/|$)/i.exec(branch)?.[1]?.toLowerCase();
39
+ export const branchStem = (branch: string): string | undefined => /^self-impl\/(.+)-[a-f0-9]{8}-r[a-f0-9]+$/i.exec(branch)?.[1];
40
+ const slot = (body?: string): string | undefined => /^칸:\s*(.+?)\s*$/m.exec(body ?? '')?.[1];
41
+ const referencesDraft = (text: string, number: number): boolean =>
42
+ new RegExp(`(?:\\(수확\\s*#${number}\\s*\\)|\\bsuperseded\\s*#${number}(?!\\d))`, 'i').test(text);
43
+ const landingCommentReferencesDraft = (text: string, number: number): boolean =>
44
+ new RegExp(`\\blanding-verified\\s*:\\s*(?:draft|superseded|수확)\\s*#${number}(?!\\d)\\b`, 'i').test(text);
28
45
 
29
- export function decideDraft({ draft, runStatus, mergedTwins, ageHours, liveBranches }: DraftTriageInput): DraftDecision {
46
+ export function decideDraft({ draft, runStatus, mergedTwins, openDrafts, ageHours, liveBranches, finalRunResult }: DraftTriageInput): DraftDecision {
30
47
  const held = draft.labels?.find((label) => PROTECTED_LABELS.has(label));
31
48
  if (held) return { action: 'keep', reason: `label:${held}` };
32
- if (runStatus === 'running' || runStatus === 'probable-running' || (draft.branch && liveBranches.has(draft.branch))) {
49
+ if (runStatus !== 'self-implement.result final' && draft.branch && liveBranches.has(draft.branch)
50
+ && finalRunResult === undefined && runStatus !== 'running' && runStatus !== 'probable-running') {
51
+ return { action: 'keep', reason: 'branch-finality-unobserved' };
52
+ }
53
+ if (runStatus === 'running' || runStatus === 'probable-running' ||
54
+ (runStatus !== 'self-implement.result final' && draft.branch && liveBranches.has(draft.branch))) {
33
55
  return { action: 'keep', reason: 'live' };
34
56
  }
35
57
  const lineage = goalId(draft.branch);
@@ -37,9 +59,39 @@ export function decideDraft({ draft, runStatus, mergedTwins, ageHours, liveBranc
37
59
  (draft.title && pr.title === draft.title) || (lineage && goalId(pr.branch) === lineage)
38
60
  ));
39
61
  if (twin) return { action: 'close', reason: `superseded-by #${twin.number}` };
62
+ const stem = branchStem(draft.branch);
63
+ const createdAt = Date.parse(draft.createdAt ?? '');
64
+ const newer = stem && draft.title && Number.isFinite(createdAt) ? openDrafts
65
+ ?.filter((pr) => pr.number !== draft.number && pr.title === draft.title && branchStem(pr.branch) === stem
66
+ && Number.isFinite(Date.parse(pr.createdAt ?? '')) && Date.parse(pr.createdAt!) > createdAt)
67
+ .sort((a, b) => Date.parse(b.createdAt!) - Date.parse(a.createdAt!) || b.number - a.number)[0] : undefined;
68
+ if (newer) return { action: 'close', reason: `duplicate-of-open #${newer.number}` };
69
+ const harvest = mergedTwins.find((pr) => pr.number !== draft.number && (
70
+ [pr.mergeCommitMessage, pr.body].some((text) => text && referencesDraft(text, draft.number))
71
+ || pr.landingVerifiedComments?.some((text) => landingCommentReferencesDraft(text, draft.number))
72
+ ));
73
+ if (harvest) return { action: 'close', reason: `superseded-by #${harvest.number} (harvest #${draft.number})` };
74
+ const created = Date.parse(draft.createdAt ?? '');
75
+ const files = draft.changedFiles;
76
+ const draftSlot = slot(draft.body);
77
+ const later = mergedTwins.filter((pr) => pr.number !== draft.number && Number.isFinite(created)
78
+ && Number.isFinite(Date.parse(pr.mergedAt ?? '')) && Date.parse(pr.mergedAt!) > created
79
+ && ((lineage && goalId(pr.branch) === lineage) || (draftSlot && slot(pr.body) === draftSlot)));
80
+ if (files?.length && files.every((file) => {
81
+ const changed = Date.parse(draft.latestFileChanges?.[file] ?? '');
82
+ return Number.isFinite(changed) && changed >= created && later.some((pr) =>
83
+ pr.changedFiles?.includes(file) && Date.parse(pr.mergedAt!) > changed);
84
+ })) {
85
+ const covering = later.filter((pr) => pr.changedFiles?.some((file) => files.includes(file)
86
+ && Date.parse(pr.mergedAt!) > Date.parse(draft.latestFileChanges![file]!)));
87
+ return { action: 'close', reason: `superseded-by #${covering.map((pr) => pr.number).join(', #')} (all-files-landed)` };
88
+ }
40
89
  if (!runStatus) return Number.isFinite(ageHours) && ageHours >= STALLED_DRAFT_HOURS
41
90
  ? { action: 'close', reason: 'stale-unobserved' }
42
91
  : { action: 'keep', reason: 'unobserved' };
92
+ if (runStatus === 'self-implement.result final') return Number.isFinite(ageHours) && ageHours >= STALLED_DRAFT_HOURS
93
+ ? { action: 'close', reason: 'stale-ended-run (self-implement.result final; worktree is not live)' }
94
+ : { action: 'keep', reason: 'recent (self-implement.result final; worktree is not live)' };
43
95
  if (Number.isFinite(ageHours) && ageHours >= STALLED_DRAFT_HOURS) return { action: 'close', reason: 'stale-ended-run' };
44
96
  return { action: 'keep', reason: 'recent' };
45
97
  }
@@ -101,21 +101,27 @@ function codexCandidateUsable(candidate: BudgetCodexCandidate, cap: number | und
101
101
  function slotUsable(
102
102
  provider: string,
103
103
  input: DecideBudgetInput,
104
- ): { usable: boolean; reason: string } {
104
+ allowUnmeasured = false,
105
+ ): { usable: boolean; reason: string; unmeasured?: boolean } {
105
106
  if (provider === CODEX_PROVIDER) {
106
107
  const cap = capFor(input.maxUsedPercent, CODEX_PROVIDER);
107
- const usable = input.codexCandidates.some((candidate) => codexCandidateUsable(candidate, cap));
108
- return { usable, reason: formatCodexReason(input.codexCandidates, cap) };
108
+ const measuredUsable = input.codexCandidates.some((candidate) => codexCandidateUsable(candidate, cap));
109
+ const unmeasured = allowUnmeasured && !measuredUsable && (input.codexCandidates.length === 0
110
+ || input.codexCandidates.some((candidate) => candidate.reached !== true
111
+ && (typeof candidate.usedPercent !== 'number' || !Number.isFinite(candidate.usedPercent))));
112
+ return { usable: measuredUsable || unmeasured, reason: formatCodexReason(input.codexCandidates, cap), ...(unmeasured ? { unmeasured: true } : {}) };
109
113
  }
110
114
  if (provider === GROK_PROVIDER) {
111
115
  const cap = capFor(input.maxUsedPercent, GROK_PROVIDER);
112
116
  const known = typeof input.grokUsedPercent === 'number' && Number.isFinite(input.grokUsedPercent);
113
- const usable = cap === undefined
117
+ const unmeasured = allowUnmeasured && !known;
118
+ const usable = unmeasured || (cap === undefined
114
119
  ? known
115
- : known && (input.grokUsedPercent as number) < cap;
120
+ : known && (input.grokUsedPercent as number) < cap);
116
121
  return {
117
122
  usable,
118
123
  reason: formatGrokReason(input.grokUsedPercent, cap),
124
+ ...(unmeasured ? { unmeasured: true } : {}),
119
125
  };
120
126
  }
121
127
  return { usable: false, reason: `${provider}: 예산 판정 없음` };
@@ -127,6 +133,15 @@ function slotUsable(
127
133
  * 아무것도 못 쓰면 onShortfall 이 wait-reset 일 때만 wait-reset, 아니면 stop.
128
134
  */
129
135
  export function decideBudget(input: DecideBudgetInput): BudgetDecision {
136
+ return decideBudgetWithPolicy(input, false).decision;
137
+ }
138
+
139
+ /** Only the harness launch gate treats an unmeasured provider as launchable. */
140
+ export function decideLaunchBudget(input: DecideBudgetInput): { decision: BudgetDecision; unmeasuredProvider?: string } {
141
+ return decideBudgetWithPolicy(input, true);
142
+ }
143
+
144
+ function decideBudgetWithPolicy(input: DecideBudgetInput, allowUnmeasured: boolean): { decision: BudgetDecision; unmeasuredProvider?: string } {
130
145
  const reasons: string[] = [];
131
146
  if (input.preference.chain.length === 0) {
132
147
  reasons.push('chain: 비었음 — tools.selfImplement.childLlm.chain 도 llm.fallbackChain 도 없다');
@@ -134,7 +149,7 @@ export function decideBudget(input: DecideBudgetInput): BudgetDecision {
134
149
  let firstUnusable = false;
135
150
  for (let index = 0; index < input.preference.chain.length; index++) {
136
151
  const slot = input.preference.chain[index]!;
137
- const judged = slotUsable(slot.provider, input);
152
+ const judged = slotUsable(slot.provider, input, allowUnmeasured);
138
153
  reasons.push(judged.reason);
139
154
  if (!judged.usable) {
140
155
  if (index === 0) firstUnusable = true;
@@ -142,16 +157,18 @@ export function decideBudget(input: DecideBudgetInput): BudgetDecision {
142
157
  }
143
158
  const action: BudgetAction = index === 0 || !firstUnusable ? 'proceed' : 'next-provider';
144
159
  return {
145
- action,
146
- provider: slot.provider,
147
- ...(slot.model !== undefined ? { model: slot.model } : {}),
148
- reasons,
160
+ decision: {
161
+ action,
162
+ provider: slot.provider,
163
+ ...(slot.model !== undefined ? { model: slot.model } : {}),
164
+ reasons,
165
+ },
166
+ ...(judged.unmeasured ? { unmeasuredProvider: slot.provider } : {}),
149
167
  };
150
168
  }
151
169
  const onShortfall = input.preference.budgetGate.onShortfall;
152
170
  return {
153
- action: onShortfall === 'wait-reset' ? 'wait-reset' : 'stop',
154
- reasons,
171
+ decision: { action: onShortfall === 'wait-reset' ? 'wait-reset' : 'stop', reasons },
155
172
  };
156
173
  }
157
174
 
@@ -0,0 +1,60 @@
1
+ import type { GateCallerReason } from './gate-scope.js';
2
+
3
+ export interface GateCallerCandidate {
4
+ readonly file: string;
5
+ readonly reasons: readonly GateCallerReason[];
6
+ }
7
+
8
+ /** Path segments between `file` and the nearest changed path's directory (0 = same directory). */
9
+ function directoryDistance(file: string, changedPaths: readonly string[]): number {
10
+ const dir = file.split('/').slice(0, -1);
11
+ let best = Infinity;
12
+ for (const changed of changedPaths) {
13
+ const other = changed.split('/').slice(0, -1);
14
+ let shared = 0;
15
+ while (shared < dir.length && shared < other.length && dir[shared] === other[shared]) shared += 1;
16
+ best = Math.min(best, dir.length - shared + other.length - shared);
17
+ }
18
+ return best;
19
+ }
20
+
21
+ /**
22
+ * Rank gate caller tests by closeness to the change, nearest first (GATE-CALLERS2 — the cap keeps the head).
23
+ * A `route` candidate names a route on a changed diff line, so it ranks with touched tests (distance 0);
24
+ * otherwise the distance is the shortest reverse-import path from a changed file (direct importer = 1,
25
+ * unreachable = after every reachable candidate). Equal distances prefer the test nearest a changed file
26
+ * in the directory tree, then numeric-aware path order. Inputs are not modified.
27
+ * `importersByFile` maps each imported file to its immediate importers (tests or sources).
28
+ */
29
+ export function rankGateCallerCandidates<T extends GateCallerCandidate>(
30
+ changedPaths: readonly string[],
31
+ candidates: readonly T[],
32
+ importersByFile: ReadonlyMap<string, readonly string[]>,
33
+ ): T[] {
34
+ const distances = new Map<string, number>();
35
+ const queue: string[] = [];
36
+ for (const path of changedPaths) {
37
+ if (distances.has(path)) continue;
38
+ distances.set(path, 0);
39
+ queue.push(path);
40
+ }
41
+ for (let index = 0; index < queue.length; index += 1) {
42
+ const source = queue[index]!;
43
+ const nextDistance = distances.get(source)! + 1;
44
+ for (const importer of importersByFile.get(source) ?? []) {
45
+ if (distances.has(importer)) continue;
46
+ distances.set(importer, nextDistance);
47
+ queue.push(importer);
48
+ }
49
+ }
50
+ const keys = new Map(candidates.map((candidate) => [candidate, {
51
+ distance: candidate.reasons.includes('route') ? 0 : distances.get(candidate.file) ?? Infinity,
52
+ directory: directoryDistance(candidate.file, changedPaths),
53
+ }]));
54
+ return [...candidates].sort((left, right) => {
55
+ const a = keys.get(left)!;
56
+ const b = keys.get(right)!;
57
+ return (a.distance - b.distance || 0) || (a.directory - b.directory || 0)
58
+ || left.file.localeCompare(right.file, undefined, { numeric: true });
59
+ });
60
+ }
@@ -30,6 +30,7 @@
30
30
 
31
31
  import { importerTestsNotInRunSet, type ImporterTestIndex } from './importer-test-index.js';
32
32
  import type { RouteConsumerTestIndex } from './route-consumer-test-index.js';
33
+ import { rankGateCallerCandidates } from './gate-candidate-ranking.js';
33
34
 
34
35
  export const GATE_CALLER_TEST_LIMIT = 30;
35
36
  export type GateCallerReason = 'import' | 'route';
@@ -249,11 +250,7 @@ interface GateScopeDecision {
249
250
  readonly unverified: readonly string[];
250
251
  /** elanous 자체가 남긴 런타임 산출물. 미검증 사용자 변경에서 제외하되 경로를 보존한다. */
251
252
  readonly elanousRuntimeArtifacts: readonly string[];
252
- /** ⭐ **편집되지 않았는데 함께 끌어와 돌린** 연관 테스트(`changed-tests` 분기 전용).
253
- * ⚠️ 직전 판본은 이 자리에 `unrunRelatedTests`(=실행집합에 없는 연관 테스트)를 뒀는데,
254
- * 실행집합이 연관 테스트를 **전부 포함**하도록 바뀌었으므로 그 값은 **정의상 항상 빈 배열**이었다.
255
- * 빈 배열을 보고 *"안 돈 게 없다"* 고 읽게 되지만 그건 사실 확인이 아니라 계산의 결과였다
256
- * (관측이 사실 아닌 값을 나르는 형태). ⇒ **실제로 일어난 일**을 센다: 합집합이 구제한 파일들. */
253
+ /** ⭐ 편집되지 않았는데 함께 끌어와 돌린 연관 테스트(`changed-tests` 분기) — 합집합이 실제로 구제한 파일들. */
257
254
  readonly pulledInRelatedTests: readonly string[];
258
255
  /** 실행 집합에 없는, 변경 소스를 상대 import 하는 테스트. 색인이 주입되지 않으면 null. */
259
256
  readonly importerTestsNotRun: {
@@ -352,8 +349,9 @@ export function resolveGateScope(
352
349
  }
353
350
  }
354
351
  }
355
- const candidates = [...callers].sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))
356
- .map(([file, reasons]) => ({ file, reasons: [...reasons] }));
352
+ // GATE-CALLERS2 — the cap keeps the candidates closest to the change, not the alphabetically first.
353
+ const candidates = rankGateCallerCandidates(userChanged, [...callers].map(([file, reasons]) => ({ file, reasons: [...reasons] })),
354
+ importerTestIndex?.testsBySource ?? new Map());
357
355
  const callerTests = candidates.slice(0, GATE_CALLER_TEST_LIMIT);
358
356
  const callerTestsOverflow = candidates.slice(GATE_CALLER_TEST_LIMIT);
359
357
  const runWithCallers = (tests: readonly string[]) => [...new Set([...tests, ...callerTests.map(({ file }) => file)])];
@@ -364,11 +362,14 @@ export function resolveGateScope(
364
362
  });
365
363
 
366
364
  if (testFiles.length > 0) {
365
+ // Edited tests ⊕ every related test of the changed sources ⊕ capped callers (restored 10-04: #23467 narrowed this
366
+ // to edited tests only, so a co-located sibling of a changed source stopped running — 0.2.12 gate introduced).
367
367
  const allRelatedTests = conventional;
368
368
  const runSet = runWithCallers([...testFiles, ...allRelatedTests]);
369
369
  // 편집된 테스트만 돌렸다면 빠졌을 것들 — 이 목록이 비어있지 않다는 것은 합집합이 실제로 구제했다는 뜻이다.
370
370
  const pulledInRelatedTests = allRelatedTests.filter((f) => !testFiles.includes(f));
371
- return { ...base, ...withDocumentNonContribution(runSet), testArgs: runSet, unverified: unverifiedFor(runSet), pulledInRelatedTests, importerTestsNotRun: observationFor(runSet), skipTestStep: false, reason: 'changed-tests' };
371
+ return { ...base, ...withDocumentNonContribution(runSet), testArgs: runSet, unverified: unverifiedFor(runSet), pulledInRelatedTests,
372
+ importerTestsNotRun: observationFor(runSet), skipTestStep: false, reason: 'changed-tests' };
372
373
  }
373
374
  if (userChanged.length === 0) {
374
375
  // postsync 빈 범위는 기존 no-changes(깨끗함)와 다른 값이다. 일반 호출자는 계속 no-changes를 읽는다.
@@ -1170,11 +1170,16 @@ export interface HeadlessGoalLoopPtyResult {
1170
1170
 
1171
1171
  export function pickImplementRoleForGoal(document: string): ReturnType<typeof resolveRoleLlm> | undefined {
1172
1172
  try {
1173
- const role = resolveRoleLlm('implement');
1173
+ const config = getUserConfig();
1174
+ const role = resolveRoleLlm('implement', { config });
1175
+ if (config.harness?.difficultyPlacement !== true || role.source !== 'default') return role;
1174
1176
  const signals = goalDifficultySignals(document);
1175
1177
  const level = classifyGoalDifficulty(signals);
1176
1178
  const picked = resolveImplementDifficulty(role, level);
1177
- try { debug.log('llm.difficulty', 'pick', { level, tier: picked.tier ?? null, signals }); } catch { /* observation is fail-soft */ }
1179
+ try {
1180
+ debug.log('llm.difficulty', 'pick', { level, tier: picked.tier ?? null, signals });
1181
+ debug.log('harness.role-llm', 'difficulty-placement', { difficulty: level, tier: picked.tier, source: role.source });
1182
+ } catch { /* observation is fail-soft */ }
1178
1183
  return picked;
1179
1184
  } catch { return undefined; }
1180
1185
  }
@@ -1,8 +1,9 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { NEXT_MD_PATH, resolveNextMdConflict } from '../release-loop/next-md-merge.js';
6
+ import { getDefaultLogStore } from '../mss/logging/log-store.js';
6
7
 
7
8
  type Command = (bin: string, args: readonly string[], cwd: string) => { status: number | null; stdout: string; stderr: string };
8
9
  export type L8MergeQueueInput = { number: number; cwd: string; matchHeadCommit: string };
@@ -13,6 +14,161 @@ export type L8MergeQueueDeps = {
13
14
  acquire?: (cwd: string) => Promise<() => void>;
14
15
  };
15
16
 
17
+ export type L8ShadowVerdict = { number: number; head: string; base?: string; verdict: 'pass' | 'conflict' | 'fail'; detail?: string; tests: string[]; at: string };
18
+ export type L8ShadowQueueState = { pending: { number: number; head: string }[]; verdicts: L8ShadowVerdict[] };
19
+ export type L8ShadowQueueDeps = L8MergeQueueDeps & { observe?: (verdict: L8ShadowVerdict) => void | Promise<void> };
20
+
21
+ function shadowCommand(execute: Command, bin: string, args: string[], cwd: string): string {
22
+ const result = execute(bin, args, cwd);
23
+ if (result.status !== 0) throw new Error(`${bin} ${args.join(' ')}: ${(result.stderr || result.stdout || `exit ${result.status}`).trim().slice(0, 500)}`);
24
+ return args.includes('-z') ? result.stdout : result.stdout.trim();
25
+ }
26
+
27
+ function shadowQueuePath(cwd: string, execute: Command): string {
28
+ const dir = shadowCommand(execute, 'git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);
29
+ if (!dir) throw new Error('merge queue common git directory unavailable');
30
+ return join(dir, 'elanous-l8-shadow-queue.json');
31
+ }
32
+
33
+ function readShadowQueue(path: string): L8ShadowQueueState {
34
+ if (!existsSync(path)) return { pending: [], verdicts: [] };
35
+ const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
36
+ if (!value || typeof value !== 'object' || !('pending' in value) || !Array.isArray(value.pending)
37
+ || !value.pending.every((p: { number?: number; head?: string }) => Number.isSafeInteger(p?.number) && p.number! > 0 && typeof p.head === 'string' && /^[0-9a-f]{40}$/i.test(p.head))
38
+ || !('verdicts' in value) || !Array.isArray(value.verdicts)) throw new Error('invalid shadow queue ledger');
39
+ return value as L8ShadowQueueState;
40
+ }
41
+
42
+ function writeShadowQueue(path: string, state: L8ShadowQueueState): void {
43
+ const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
44
+ try {
45
+ writeFileSync(temp, JSON.stringify(state, null, 2) + '\n', { flag: 'wx' });
46
+ renameSync(temp, path);
47
+ } finally { rmSync(temp, { force: true }); }
48
+ }
49
+
50
+ async function withShadowQueue<T>(cwd: string, deps: L8ShadowQueueDeps, fn: (state: L8ShadowQueueState, path: string) => Promise<T>): Promise<T> {
51
+ const release = await (deps.acquire ?? acquireL8MergeQueue)(cwd);
52
+ try {
53
+ const path = shadowQueuePath(cwd, deps.command ?? command);
54
+ return await fn(readShadowQueue(path), path);
55
+ } finally { release(); }
56
+ }
57
+
58
+ export function shadowPrNumber(raw: string): number {
59
+ const text = raw.replace(/^#/, '');
60
+ const number = Number(text);
61
+ if (!/^[1-9]\d*$/.test(text) || !Number.isSafeInteger(number)) throw new Error('expected a positive PR number');
62
+ return number;
63
+ }
64
+
65
+ export async function statusL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowQueueState> {
66
+ return withShadowQueue(cwd, deps, async (state) => state);
67
+ }
68
+
69
+ export async function enqueueL8ShadowQueue(cwd: string, number: number, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowQueueState> {
70
+ if (!Number.isSafeInteger(number) || number <= 0) throw new Error('expected a positive PR number');
71
+ return withShadowQueue(cwd, deps, async (state, path) => {
72
+ const execute = deps.command ?? command;
73
+ const view = JSON.parse(shadowCommand(execute, 'gh', ['pr', 'view', String(number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'], cwd)) as {
74
+ headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
75
+ };
76
+ if (!view.headRefOid || !/^[0-9a-f]{40}$/i.test(view.headRefOid) || view.baseRefName !== 'main'
77
+ || view.state !== 'OPEN' || view.isDraft !== false || view.isCrossRepository !== false) throw new Error('PR is not an open, ready, same-repository main PR');
78
+ if (state.pending.some((p) => p.number === number)) throw new Error(`PR #${number} already queued`);
79
+ state.pending.push({ number, head: view.headRefOid });
80
+ writeShadowQueue(path, state);
81
+ return state;
82
+ });
83
+ }
84
+
85
+ /** Evaluate one pinned queue head against freshly fetched main. Never push or call a merge API. */
86
+ export async function runL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowVerdict | null> {
87
+ return withShadowQueue(cwd, deps, async (state, path) => {
88
+ const item = state.pending[0];
89
+ if (!item) return null;
90
+ const execute = deps.command ?? command;
91
+ const run = (bin: string, args: string[], where = cwd) => shadowCommand(execute, bin, args, where);
92
+ const verdict: L8ShadowVerdict = { number: item.number, head: item.head, verdict: 'fail', tests: [], at: new Date().toISOString() };
93
+ const view = JSON.parse(run('gh', ['pr', 'view', String(item.number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'])) as {
94
+ headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
95
+ };
96
+ if (view.headRefOid !== item.head || view.baseRefName !== 'main' || view.state !== 'OPEN'
97
+ || view.isDraft !== false || view.isCrossRepository !== false) throw new Error('queued PR changed or is not ready');
98
+ run('git', ['fetch', 'origin', 'refs/heads/main']);
99
+ const base = run('git', ['rev-parse', 'FETCH_HEAD']);
100
+ if (!/^[0-9a-f]{40}$/i.test(base)) throw new Error('main commit unavailable');
101
+ verdict.base = base;
102
+ run('git', ['fetch', 'origin', `refs/pull/${item.number}/head`]);
103
+ if (run('git', ['rev-parse', 'FETCH_HEAD']) !== item.head) throw new Error('fetched PR head changed');
104
+ let temp: string | undefined;
105
+ let attached = false;
106
+ try {
107
+ temp = mkdtempSync(join(tmpdir(), 'elanous-l8-shadow-'));
108
+ run('git', ['worktree', 'add', '--detach', temp, base]);
109
+ attached = true;
110
+ const fork = run('git', ['merge-base', base, item.head], temp);
111
+ const changed = run('git', ['diff', '--name-only', '-z', fork, item.head], temp).split('\0').filter(Boolean);
112
+ if (!changed.length) throw new Error('PR changed-file paths unavailable');
113
+ const diff = run('git', ['diff', '--name-only', '-z', '--diff-filter=ACMRT', fork, item.head], temp);
114
+ const files = diff.split('\0').filter(Boolean);
115
+ if (changed.some((file) => file.startsWith('-') || file.startsWith('/') || file.split('/').includes('..') || file.includes('\\'))) throw new Error('unsafe changed-file path');
116
+ verdict.tests = files.filter((file) => /(?:^|\/)[^/]+\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/.test(file));
117
+ const merge = execute('git', ['-c', 'user.name=elanous shadow', '-c', 'user.email=shadow@localhost', 'merge', '--no-ff', '--no-edit', item.head], temp);
118
+ if (merge.status !== 0) {
119
+ const unresolved = run('git', ['diff', '--name-only', '--diff-filter=U'], temp);
120
+ if (!unresolved) throw new Error(`git merge failed: ${merge.stderr.trim() || `exit ${merge.status}`}`);
121
+ verdict.verdict = 'conflict';
122
+ verdict.detail = unresolved;
123
+ } else {
124
+ if (verdict.tests.length) {
125
+ if (verdict.tests.some((file) => !existsSync(join(temp!, file)))) throw new Error('changed test path unavailable');
126
+ run('bun', ['install', '--frozen-lockfile'], temp);
127
+ const result = execute('bun', ['run', 'test:deterministic', ...verdict.tests], temp);
128
+ const output = `${result.stdout}\n${result.stderr}`;
129
+ const counts = /\b(\d+) pass\b[\s\S]*?\b(\d+) fail\b[\s\S]*?Ran ([1-9]\d*) tests? across ([1-9]\d*) files?/.exec(output);
130
+ if (!counts || Number(counts[1]) + Number(counts[2]) !== Number(counts[3])
131
+ || Number(counts[4]) !== verdict.tests.length || (result.status !== 0 && Number(counts[2]) === 0)) {
132
+ throw new Error(`changed tests unmeasured: ${output.slice(-400)}`);
133
+ }
134
+ if (Number(counts[2]) > 0 || Number(counts[1]) === 0) {
135
+ verdict.detail = `changed tests failed: ${output.slice(-400)}`;
136
+ } else {
137
+ verdict.verdict = 'pass';
138
+ }
139
+ } else {
140
+ verdict.verdict = 'pass';
141
+ }
142
+ }
143
+ } finally {
144
+ try {
145
+ if (attached && temp) run('git', ['worktree', 'remove', '--force', temp]);
146
+ } finally {
147
+ if (temp) rmSync(temp, { recursive: true, force: true });
148
+ }
149
+ }
150
+ if (verdict.verdict === 'pass') {
151
+ run('git', ['fetch', 'origin', 'refs/heads/main']);
152
+ if (run('git', ['rev-parse', 'FETCH_HEAD']) !== verdict.base) throw new Error('main advanced during shadow verification');
153
+ const current = JSON.parse(run('gh', ['pr', 'view', String(item.number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'])) as {
154
+ headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
155
+ };
156
+ if (current.headRefOid !== item.head || current.baseRefName !== 'main' || current.state !== 'OPEN'
157
+ || current.isDraft !== false || current.isCrossRepository !== false) throw new Error('PR changed during shadow verification');
158
+ }
159
+ // A rejected insert leaves the pinned item queued for a later evaluation.
160
+ await (deps.observe ?? ((entry) => {
161
+ const store = getDefaultLogStore();
162
+ if (!store) throw new Error('merge-queue observation store unavailable');
163
+ store.insertBatch([{ rec: { ts: new Date().toISOString(), category: 'merge-queue', event: 'shadow-verdict', data: entry }, surface: 'merge-queue' }]);
164
+ }))(verdict);
165
+ state.pending.shift();
166
+ state.verdicts.push(verdict);
167
+ writeShadowQueue(path, state);
168
+ return verdict;
169
+ });
170
+ }
171
+
16
172
  const command: Command = (bin, args, cwd) => {
17
173
  const r = spawnSync(bin, [...args], { cwd, encoding: 'utf8', timeout: 600_000, maxBuffer: 32 * 1024 * 1024 });
18
174
  return { status: r.status, stdout: r.stdout ?? '', stderr: (r.stderr ?? '') + (r.error?.message ?? '') };
@@ -63,6 +63,7 @@ import { GoalRunStore, insertGoalRunRecord, type GoalPriorRuns } from './goal-ru
63
63
  import { writeGoalRunRecordFragment } from './goal-execution-records.js';
64
64
  import { getUserConfig } from '../user-config.js';
65
65
  import { appendRunLedgerEntry, loadRunLedger, parseRunShardIdentity, queryRunChain, type RunChainShardSibling, type RunLedgerEntry, type RunLedgerWriter, type RunOriginData, type RunShardIdentity } from './run-ledger.js';
66
+ import { emitSessionEvent, type SessionEventInput } from '../context-bus/session-events.js';
66
67
  import { decideLineageSupersede, lineageSupersedeCloseComment, type LineageSupersedeOpenDraft } from './lineage-supersede.js';
67
68
  import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
68
69
  import { enqueueControlMemo, readSoftStopRequestStatus, type SoftStopRequestRead } from '../harness/control-inbox.js';
@@ -874,6 +875,8 @@ export type ImplementTerminalStatus = {
874
875
  export interface SelfImplementSeams {
875
876
  /** Run-observer ledger sink. Tests inject an in-memory sink to avoid touching user state. */
876
877
  writeRunLedger?: RunLedgerWriter;
878
+ /** Optional context-journal sink for isolated runs. */
879
+ emitContextEvent?: (input: SessionEventInput) => void;
877
880
  /** Registers this run as an ephemeral loop-agent asset; failures are observed but never affect execution. */
878
881
  registerLoopAgent?: (input: LoopAgentInput) => void;
879
882
  /** Wraps implementation and gate promises with their effective wall-clock timeout; tests observe the actual stage budgets. */
@@ -3384,6 +3387,16 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
3384
3387
  detail: 'observe-only: SelfImplement execution skipped before child boot',
3385
3388
  };
3386
3389
  }
3390
+ const emitContext = (kind: 'task-claimed' | 'task-done', text: string, pr?: number): void => {
3391
+ try {
3392
+ (opts.seams.emitContextEvent ?? emitSessionEvent)({ kind, seat: 'TC', text,
3393
+ ref: `runId=${runId}${pr === undefined ? '' : ` PR=#${pr}`}` });
3394
+ } catch (error) {
3395
+ try { logRunAwareFailSoft(runId, 'context-event-failed', { kind, error: safeErrorDescription(error) }); }
3396
+ catch { /* context publication never changes the run result */ }
3397
+ }
3398
+ };
3399
+ emitContext('task-claimed', `Self-implement claimed: ${opts.feature.split(/\r?\n/, 1)[0]}`);
3387
3400
  const parentHarnessSpaceId = getHarnessSpace()?.id || undefined;
3388
3401
  const configRoot = getElanousConfigDir() || undefined;
3389
3402
  const stateRoot = elanousStateRoot() || undefined;
@@ -4091,6 +4104,8 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
4091
4104
  }, { level: 'warn' });
4092
4105
  } catch { /* fail-soft — 종결 기록 실패가 원래 예외를 덮으면 안 된다 */ }
4093
4106
  }
4107
+ emitContext('task-done', `Self-implement ${terminalResult?.stage ?? 'crashed'}: ${opts.feature.split(/\r?\n/, 1)[0]}`,
4108
+ terminalResult?.prNumber ?? capturedPrNumber);
4094
4109
  finalizeTraversal();
4095
4110
  try { unregisterProviderErrorSink(); } catch { /* fail-soft — cleanup must not change the terminal result */ }
4096
4111
  }