elanous 0.2.23 → 0.2.24

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 (347) 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/{272-b23d2e92161d11fd.js → 272-0eed0fb0aab716b6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{3961-fbef064c213b2b18.js → 3961-aaae04ed1d32622f.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d5341ac96203a983.js → page-3b9fc39362473ae7.js} +1 -1
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f2b62c19ca68fef8.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/board/{page-55c631c032dcc47e.js → page-d7602d489edf5f37.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-ad3bd8084048bee4.js → page-24282bf9f90031a5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/page-8b7c5ab6b5bc8d64.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5c5d0e370f495ef6.js → page-4eb84d454f8c3415.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-c325e21543cd48a8.js → page-b2e494bdad57fcb5.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/{page-6168a7c43e6ef8f5.js → page-c0ab229673a8b0eb.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-3e2b45f3c36dbac4.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/decisions/page-c22aa511b28743ad.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/page-26345242e4049f55.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/editor/{page-25bb425afdb27541.js → page-bf88c0f558d8ef54.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/exec/{page-32d72fdef1afe11a.js → page-52fec523e5b5d3ad.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/page-67723dfdcde92b31.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/inside/{page-2bd9ce9c020d02d4.js → page-22725e2570f1126b.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a34096245e63b9f2.js → page-1891335950d68b70.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/layout-3267af1ebdc73cf5.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-4cc66d22ca4ec502.js → page-c314b75c11234998.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-83b14c729fcea837.js → page-9f9c85415f61782b.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-c1d4276059085587.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/page-833aab587f26ad4c.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-ccdb83ed40c8d724.js → page-8526c17ae38e6a50.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-7ad3de8fa06fdea9.js → page-f998637a8402115c.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-b7bb53828b17b335.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1b1850de0257e46d.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-786111f30b6f0969.js → page-6f23159fedb41f67.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-48fb31810693c357.js → page-1d809a87139dd024.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4e17cce5e2ed0fee.js → page-da9e570234d481fd.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-cb154acadb025414.js → page-b92caddb5c715f30.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-8ce3a38e210b810a.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-c2dd2b19c93f90ab.js → page-57ffeeb6019440fd.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-abbb417bf514630b.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-d3c007c8a0ddd7ee.js → page-2c2b3f29cec1efa4.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7837ce2aacb97b04.js → page-9369279f72fc7b51.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/page-64861f14039499c3.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-ddc4c93efd8ab94b.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-901a1391b81053bc.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/page-68d9bbaac9841a58.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-119be9a0444d2482.js → page-b5c0c996f329371e.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-fa806c018833e332.js → page-89881ccbcf728978.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/page-0e87a803b3d7165e.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/today/{page-d431fbdea6ad93dd.js → page-58d4e5cb30ef4871.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-02ce4c3b5f39e4d2.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/page-e620d1bb8eadcec2.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-847c3de6ca8a4416.js → page-acf868c64d77f950.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-54fa605fa6dba457.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-74e69ba2c8e970d4.js → page-bc79f0833533fa84.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-70038f41ae7cf931.js → page-d5c4398eea03d8f3.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/72becfd42807b50d.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -1
  65. package/apps/pwa/out/ceo/index.txt +14 -14
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/decisions/index.html +1 -1
  73. package/apps/pwa/out/decisions/index.txt +14 -14
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +14 -14
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +14 -14
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +14 -14
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +14 -14
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +14 -14
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +14 -14
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +14 -14
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +14 -14
  91. package/apps/pwa/out/live-run/index.html +1 -1
  92. package/apps/pwa/out/live-run/index.txt +14 -14
  93. package/apps/pwa/out/loops/index.html +1 -1
  94. package/apps/pwa/out/loops/index.txt +14 -14
  95. package/apps/pwa/out/market/index.html +1 -1
  96. package/apps/pwa/out/market/index.txt +14 -14
  97. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  98. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  99. package/apps/pwa/out/morning/index.html +1 -1
  100. package/apps/pwa/out/morning/index.txt +14 -14
  101. package/apps/pwa/out/observatory/index.html +1 -1
  102. package/apps/pwa/out/observatory/index.txt +14 -14
  103. package/apps/pwa/out/ops/checklist/index.html +1 -1
  104. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  105. package/apps/pwa/out/ops/release/index.html +1 -1
  106. package/apps/pwa/out/ops/release/index.txt +14 -14
  107. package/apps/pwa/out/ops/seats/index.html +1 -1
  108. package/apps/pwa/out/ops/seats/index.txt +14 -14
  109. package/apps/pwa/out/outputs/index.html +1 -1
  110. package/apps/pwa/out/outputs/index.txt +14 -14
  111. package/apps/pwa/out/reflection/index.html +1 -1
  112. package/apps/pwa/out/reflection/index.txt +15 -15
  113. package/apps/pwa/out/scheduler/index.html +1 -1
  114. package/apps/pwa/out/scheduler/index.txt +14 -14
  115. package/apps/pwa/out/sessions/index.html +1 -1
  116. package/apps/pwa/out/sessions/index.txt +14 -14
  117. package/apps/pwa/out/settings/devices/index.html +1 -1
  118. package/apps/pwa/out/settings/devices/index.txt +14 -14
  119. package/apps/pwa/out/settings/index.html +1 -1
  120. package/apps/pwa/out/settings/index.txt +14 -14
  121. package/apps/pwa/out/setup/done/index.html +1 -1
  122. package/apps/pwa/out/setup/done/index.txt +14 -14
  123. package/apps/pwa/out/setup/index.html +1 -1
  124. package/apps/pwa/out/setup/index.txt +14 -14
  125. package/apps/pwa/out/share/index.html +1 -1
  126. package/apps/pwa/out/share/index.txt +14 -14
  127. package/apps/pwa/out/showroom/index.html +1 -1
  128. package/apps/pwa/out/showroom/index.txt +14 -14
  129. package/apps/pwa/out/tasks/index.html +1 -1
  130. package/apps/pwa/out/tasks/index.txt +14 -14
  131. package/apps/pwa/out/term/index.html +1 -1
  132. package/apps/pwa/out/term/index.txt +14 -14
  133. package/apps/pwa/out/today/index.html +1 -1
  134. package/apps/pwa/out/today/index.txt +14 -14
  135. package/apps/pwa/out/trace/index.html +1 -1
  136. package/apps/pwa/out/trace/index.txt +14 -14
  137. package/apps/pwa/out/vault/index.html +1 -1
  138. package/apps/pwa/out/vault/index.txt +14 -14
  139. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  140. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  141. package/apps/pwa/out/workflows/index.html +1 -1
  142. package/apps/pwa/out/workflows/index.txt +14 -14
  143. package/apps/pwa/out/workspace/index.html +1 -1
  144. package/apps/pwa/out/workspace/index.txt +14 -14
  145. package/apps/pwa/out/worktrees/index.html +1 -1
  146. package/apps/pwa/out/worktrees/index.txt +14 -14
  147. package/catalog/models/openrouter/deepseek-v4-pro-0813.yaml +39 -0
  148. package/catalog/models/openrouter/deepseek-v4.1-flash.yaml +38 -0
  149. package/catalog/models/openrouter/glm-5.3-flash.yaml +35 -0
  150. package/catalog/providers/openrouter.yaml +2 -0
  151. package/graphs/implement-loop.yaml +31 -0
  152. package/graphs/ops/harness-gc.yaml +17 -0
  153. package/graphs/ops/recipes.yaml +3 -0
  154. package/graphs/overlays/README.md +1 -1
  155. package/graphs/overlays/stop-heal-ladder.yaml +105 -0
  156. package/package.json +1 -1
  157. package/scripts/add-api-key.sh +1 -1
  158. package/scripts/ci-typecheck-changed.ts +71 -14
  159. package/scripts/fleet/bedrock-pod-secret.ts +122 -0
  160. package/scripts/fleet/node-setup-linux.sh +97 -0
  161. package/scripts/fleet/node-setup.sh +11 -0
  162. package/scripts/fleet/zai-pod-secret.ts +86 -0
  163. package/scripts/land-global-checks.ts +3 -0
  164. package/scripts/measure-log-diet-2.ts +213 -0
  165. package/scripts/measure-tsc-affected-roots.ts +289 -0
  166. package/scripts/release-loop/gate-executor.ts +28 -0
  167. package/scripts/release-loop/gate-node.ts +262 -26
  168. package/scripts/release-loop/resume-release.ts +9 -3
  169. package/scripts/release-loop/resume.ts +41 -0
  170. package/scripts/release-loop/unattended-release.ts +22 -7
  171. package/scripts/tsc-baseline-cache.ts +147 -0
  172. package/scripts/usage-rollup.ts +58 -6
  173. package/src/agent/run-context.ts +10 -0
  174. package/src/agent-mission/driver.ts +18 -3
  175. package/src/agent-mission/resource-ladder.ts +8 -3
  176. package/src/agent-mission/review-loop.ts +8 -2
  177. package/src/agent-substrate/acp-implementer.ts +139 -0
  178. package/src/agent-substrate/pr-reviewer.ts +31 -4
  179. package/src/agent-substrate/self-review-cli.ts +48 -9
  180. package/src/autopilot/mission-critique.ts +18 -5
  181. package/src/autopilot/mission-feasibility-negotiate.ts +11 -10
  182. package/src/autopilot/mission-trace.ts +12 -1
  183. package/src/autopilot/system-lookback-run.ts +16 -10
  184. package/src/bench/bench.ts +183 -0
  185. package/src/budget/fetchers/grok.ts +15 -5
  186. package/src/budget/llm-cost.ts +37 -5
  187. package/src/budget/types.ts +8 -2
  188. package/src/budget/unified-usage.ts +88 -6
  189. package/src/cli/bench-cli.ts +34 -0
  190. package/src/cli/guardian-cli.ts +26 -1
  191. package/src/cli/image-cli.ts +101 -0
  192. package/src/cli/log-event-names.ts +4 -0
  193. package/src/cli/pr-cli.ts +62 -5
  194. package/src/cli/pr-land-overlap-evidence.ts +160 -12
  195. package/src/cli/release-cli.ts +18 -9
  196. package/src/cli/rfc-status.ts +52 -2
  197. package/src/cli/self-update.ts +104 -0
  198. package/src/cli/tasks-cli.ts +109 -4
  199. package/src/cli/usage-cli.ts +91 -8
  200. package/src/core-turn/run-goal-loop.ts +5 -4
  201. package/src/dashboard/ad-run-setup-bindings.ts +279 -0
  202. package/src/dashboard/index.ts +18 -276
  203. package/src/dashboard/stale-install-check.ts +11 -0
  204. package/src/debug/log-diet.ts +38 -7
  205. package/src/debug/log.ts +19 -1
  206. package/src/domains/release-tool.ts +39 -0
  207. package/src/error-report/report.ts +21 -4
  208. package/src/github/pr-labels.ts +1 -0
  209. package/src/graph-runner/graph-cli.ts +42 -1
  210. package/src/graph-runner/runner.ts +167 -6
  211. package/src/grok/usage.ts +23 -2
  212. package/src/grounding/ref-index.ts +253 -0
  213. package/src/harness/dispatch-backpressure.ts +10 -2
  214. package/src/harness/harness-clean.ts +199 -78
  215. package/src/harness/harness-cli-command.ts +122 -7
  216. package/src/harness/harness-gc.ts +331 -0
  217. package/src/harness/harness-stop.ts +4 -2
  218. package/src/harness/harness-substrate-default.ts +142 -7
  219. package/src/harness/launch-registry.ts +6 -0
  220. package/src/harness/worktree-branch-prefix.ts +9 -0
  221. package/src/hq/fence.ts +187 -0
  222. package/src/hq/seats.ts +38 -22
  223. package/src/images/image-ledger.ts +179 -0
  224. package/src/index.ts +93 -11
  225. package/src/instance/child-scope.ts +32 -1
  226. package/src/instance/dev-project.ts +133 -0
  227. package/src/instance/resolve.ts +24 -0
  228. package/src/instance/spawn-elanous.ts +45 -0
  229. package/src/intake-plane/ref-prefetch.ts +110 -0
  230. package/src/intelligence-map/model-catalog.ts +72 -0
  231. package/src/llm/bedrock-eventstream.ts +215 -0
  232. package/src/llm/bedrock-provider.ts +114 -41
  233. package/src/llm/bedrock.ts +174 -28
  234. package/src/llm/openrouter-byok-cost.ts +96 -0
  235. package/src/llm/openrouter-deepseek.ts +151 -0
  236. package/src/llm/openrouter-upstream-pins.config.ts +25 -0
  237. package/src/llm/zai-coding-plan.ts +700 -0
  238. package/src/llm.ts +87 -16
  239. package/src/loops/checker.ts +148 -9
  240. package/src/loops/orchestrator/resync-shadow.ts +34 -7
  241. package/src/maturity/feature-maturity.ts +2 -2
  242. package/src/model-tier/llm-tier-map.ts +9 -5
  243. package/src/models/prompts.ts +2 -1
  244. package/src/mss/logging/log-store.ts +58 -11
  245. package/src/nexus/api/missions.ts +73 -2
  246. package/src/nexus/api/ops-api.ts +18 -0
  247. package/src/nexus/index.ts +7 -75
  248. package/src/nexus/tools-kind.ts +78 -0
  249. package/src/oauth/codex-account-rotation.ts +39 -2
  250. package/src/oauth/codex-account-store.ts +323 -24
  251. package/src/oauth/fallback-chain.ts +53 -15
  252. package/src/onboarding/first-run.ts +37 -18
  253. package/src/onboarding/web-first.ts +20 -2
  254. package/src/ops/machine-heartbeat.ts +185 -0
  255. package/src/ops/overview-cache.ts +175 -0
  256. package/src/ops/overview.ts +8 -0
  257. package/src/prompt-cache/openai.ts +3 -0
  258. package/src/prompt-cache/types.ts +5 -0
  259. package/src/prompt-enhance/enhance.ts +23 -0
  260. package/src/release-loop/placement.ts +16 -1
  261. package/src/requirements/requirement-ledger.ts +536 -0
  262. package/src/retro/daily.ts +5 -5
  263. package/src/seat-loop/seat-loop.ts +32 -10
  264. package/src/self-dev/dev-cli.ts +44 -10
  265. package/src/self-dev/dev-pipeline.ts +7 -2
  266. package/src/self-dev/draft-residue.ts +14 -2
  267. package/src/self-dev/draft-sweep.ts +103 -0
  268. package/src/self-dev/graph-journey-route.ts +6 -0
  269. package/src/self-dev/orchestrate-cli.ts +25 -3
  270. package/src/self-dev/orchestrate.ts +41 -6
  271. package/src/self-dev/run-supervisor.ts +188 -15
  272. package/src/self-dev/run-types.ts +13 -0
  273. package/src/self-dev/supervisor-verdict-edges.ts +1 -0
  274. package/src/self-implement/budget-gate.ts +53 -13
  275. package/src/self-implement/child-context-composition.ts +139 -0
  276. package/src/self-implement/child-llm-preference.ts +13 -5
  277. package/src/self-implement/claude-acp-implement.ts +67 -0
  278. package/src/self-implement/flex-signals.ts +59 -0
  279. package/src/self-implement/frozen-merges.ts +8 -3
  280. package/src/self-implement/goal-author-decompose.ts +10 -2
  281. package/src/self-implement/goal-author.ts +24 -9
  282. package/src/self-implement/graph-overlay-condition.ts +28 -0
  283. package/src/self-implement/graph-overlay-yaml.ts +41 -7
  284. package/src/self-implement/graph-templates.ts +33 -1
  285. package/src/self-implement/graph-yaml.ts +56 -0
  286. package/src/self-implement/heal-intake.ts +5 -1
  287. package/src/self-implement/orchestrator.ts +144 -15
  288. package/src/self-implement/running-runs.ts +75 -5
  289. package/src/self-implement/seams.ts +47 -1
  290. package/src/self-implement/self-implement-cli.ts +9 -4
  291. package/src/self-implement/stop-autoheal.ts +69 -2
  292. package/src/session-runtime/retry-policy.ts +2 -1
  293. package/src/steward/launch.ts +5 -4
  294. package/src/steward/triage-budget.ts +62 -0
  295. package/src/steward/triage.ts +83 -22
  296. package/src/task-agent/actions.ts +62 -5
  297. package/src/task-agent/completion-evidence.ts +5 -0
  298. package/src/task-agent/cover.ts +101 -3
  299. package/src/task-agent/judge.ts +16 -0
  300. package/src/task-agent/land-order-ledger.ts +95 -0
  301. package/src/task-agent/live-move-history.ts +87 -1
  302. package/src/task-agent/live-moves.ts +350 -20
  303. package/src/task-agent/rejudge-on-head.ts +75 -20
  304. package/src/task-agent/review-prior-fix.ts +146 -0
  305. package/src/task-agent/run-adopt.ts +136 -0
  306. package/src/task-agent/shadow.ts +53 -9
  307. package/src/task-agent/task-hand.ts +54 -7
  308. package/src/task-orchestrator/surfaces/pod-account-broker.ts +38 -5
  309. package/src/task-orchestrator/surfaces/pod-bedrock-child.ts +74 -0
  310. package/src/task-orchestrator/surfaces/pod-child-default.ts +50 -0
  311. package/src/task-orchestrator/surfaces/pod-child-providers.ts +14 -1
  312. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +79 -6
  313. package/src/task-orchestrator/surfaces/pod-lease.ts +17 -5
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  315. package/src/task-orchestrator/surfaces/pod-pool.ts +20 -4
  316. package/src/task-orchestrator/surfaces/pod-zai-child.ts +101 -0
  317. package/src/task-orchestrator/surfaces/self-implement-pod.ts +213 -23
  318. package/src/task-orchestrator/surfaces/self-implement.ts +19 -1
  319. package/src/typecheck-ratchet.ts +1 -0
  320. package/src/user-config.ts +51 -11
  321. package/src/version/packed-revision.json +1 -1
  322. package/src/workflow-runtime/triggers/registry.ts +59 -0
  323. package/src/workflow-runtime/triggers/webhook-source.ts +35 -5
  324. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-0160ab83c8685a12.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/bots/page-e6784759a2531031.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/control/page-31417d3604154a6d.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/decisions/page-e9606dc0bdec9305.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/design-check/page-a648bc464c9dc754.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/field/page-d27b794c8ba76bbb.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/layout-22170f4f00a15496.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/loops/page-6e2781c523dad761.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/market/page-26dd415c8f13f341.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/not-found-015c02c4fec4023e.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6b0ae5b5c42f6d2f.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/page-16258e956ea93fc2.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f35dd1dd96ae87.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/settings/page-7350ee93f4fe1033.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-00723458575f2b19.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/setup/page-745d9b6c015076f8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/share/page-736d33c7414a8a1a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/term/page-c782041b5f5199ba.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/trace/page-0c056abff82b9891.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/vault/page-242d668e4fbf72c4.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/workflows/page-81dd0d8ae923ed72.js +0 -1
  345. package/apps/pwa/out/_next/static/css/5619d59e87eddf66.css +0 -1
  346. /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_buildManifest.js +0 -0
  347. /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_ssgManifest.js +0 -0
@@ -23,7 +23,7 @@ import { listRunLedgers, loadFederatedRunLedger, loadRunLedger, queryRunScreenKe
23
23
  import { classifyPodExitStop, recordRunStop, runStopRecorded, RUN_STOP_EVENT } from '../self-implement/run-stop.js';
24
24
  import { ensureOpenRouterChildModelLive, resolveChildLlmEffort, resolveImplementationChildModel } from '../self-dev/dev-cli.js';
25
25
  import { resolveHarnessTarget } from '../self-implement/harness-target-options.js';
26
- import { queryRunningRuns } from '../self-implement/running-runs.js';
26
+ import { buildRunProgressRows, queryRunningRuns, renderRunProgressTable } from '../self-implement/running-runs.js';
27
27
  import { DevPipelineError } from '../self-dev/dev-pipeline.js';
28
28
  import { collectDraftMetrics, collectOverlapMetrics, DRAFT_SWEEP_CONCURRENCY, mapBounded, resolveDraftSweepCloseCap, runDraftSweep, sweepFailureReason, type DraftSweepAdapters, type DraftSweepResult, type OverlapMetricAdapters, type SweepDraft, type SweepMergedPr, type SweepReviewGate } from '../self-dev/draft-sweep.js';
29
29
  import { readAskPreflightRowsSince, readSalvagedRowsSince } from '../self-dev/ask-launch-io.js';
@@ -58,6 +58,9 @@ import type { MissionSolveOutcome } from './mission-solve-loop.js';
58
58
  import { formatAxis, formatAxisObservations, inspectAskMarkers, inspectUnpressedDecisionSignals } from '../../scripts/ask-marker-check.js';
59
59
  import { enterJourneyNode, ensureJourneyKey, exitJourneyNode, JOURNEY_KEY_ENV, passJourneyNode, withJourneyNode } from '../self-dev/graph-journey-nodes.js';
60
60
  import { intakeTakesQueue, readJourneyEdge, recordJourneyEdge } from '../self-dev/graph-journey-route.js';
61
+ import { appendBenchLedger, BENCH_LOG_CATEGORY, benchBaseBranch, defaultBenchGitRun, ensureBenchBase, nextBenchRound, parseBenchSpec, readBenchLedger, type BenchBase, type BenchGitRun } from '../bench/bench.js';
62
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
63
+ import { codeRevision } from '../version/code-revision.js';
61
64
 
62
65
  let harnessPlanRfcForTesting: typeof runHarnessPlanRfc | undefined;
63
66
  let harnessAskMarkerInspectorForTesting: ((ask: string) => readonly string[]) | undefined;
@@ -144,6 +147,17 @@ export interface HarnessAskSayChildLlmOptions extends HarnessAskSayOptions {
144
147
  childLlmProvider?: string;
145
148
  childLlmModel?: string;
146
149
  childLlmEffort?: string;
150
+ /** BENCH-ISOLATED-ARMS — `<id>[:<base-sha>]` · base 를 `bench/<id>/base` 로 고정한다(`--base` 와 함께 못 쓴다). */
151
+ bench?: string;
152
+ }
153
+
154
+ /** BENCH-ISOLATED-ARMS — 시험이 실제 원격·운영 config 를 안 건드리게 주입한다. */
155
+ export interface HarnessBenchDeps {
156
+ readonly git?: BenchGitRun;
157
+ readonly configDir?: string;
158
+ readonly imageCommit?: () => string | null;
159
+ /** 부모(이 발사 프로세스)가 도는 코드 커밋 — 기본 `codeRevision()`(코드 자신의 위치 · cwd 무관). */
160
+ readonly codeCommit?: () => string | null;
147
161
  }
148
162
 
149
163
  export interface HarnessPlanOptions extends HarnessAskSayOptions {
@@ -288,7 +302,8 @@ function registerHarnessAskSayOptions(command: Command): Command {
288
302
  .option('--pod-pool <spec>', 'pod: 풀 — 인자 > ELANOUS_POD_POOL > harness.podPool > 기존 pod.pool > 현재 컨텍스트')
289
303
  .addOption(new Option('--pod-memory <tier>', 'pod: 메모리 등급 — lite(2Gi · 문서·조사·네트워크 스킬 골 · OOM 이면 high 로 한 번 재시도) | standard(16Gi) | high(32Gi) · 없으면 골 문면 `Pod 메모리: <등급>` 한 줄 · 그것도 없으면 골이 apps/pwa/ 를 담을 때 high').choices(['lite', 'standard', 'high']))
290
304
  .option('--after <goal-or-pr>', 'pod: 선행 — 이 골 ID(16자) 또는 PR(#N) 이 병합될 때까지 Pod 발사가 큐에서 이유와 함께 기다린다(병합 없이 닫히면 blocked) · 골 문면 `선행: #N` 한 줄과 같다 · 대상 파일만 겹치면 선행이 아니다(경고만)')
291
- .option('--source <spec>', 'pod: 원천 — commit:<40자 sha> | pr:<정수> | worktree:<경로> | files:<경로>[,<경로>…] · `--substrate pod` 와 함께');
305
+ .option('--source <spec>', 'pod: 원천 — commit:<40자 sha> | pr:<정수> | worktree:<경로> | files:<경로>[,<경로>…] · `--substrate pod` 와 함께')
306
+ .option('--bench <id[:sha]>', '벤치 팔 — base 를 bench/<id>/base 로 고정(없으면 sha·생략 시 origin/main 으로 만든다 · 있으면 sha 일치 확인 · 불일치 거부) · 팔 가지 bench/<id>/<arm> · PR 라벨 elanous:bench · PR 직전 main 동기화 생략 · 원장 elanous bench ls <id>');
292
307
  }
293
308
 
294
309
  function registerHarnessPlanOptions(command: Command): Command {
@@ -793,6 +808,36 @@ async function readHarnessLaunchBudget(
793
808
  }
794
809
  }
795
810
 
811
+ type BenchLaunchRow = Parameters<typeof appendBenchLedger>[0];
812
+
813
+ /** BENCH-ISOLATED-ARMS ④ — 원장 «launched» 줄. ⛔ 원장을 못 써도 발사는 막지 않는다(관측 `bench`/`ledger-unwritten`). */
814
+ function recordBenchLaunch(bench: BenchBase, opts: HarnessAskSayChildLlmOptions, substrate: string, entrance: string, deps: HarnessBenchDeps): BenchLaunchRow {
815
+ const configDir = deps.configDir ?? getElanousConfigDir();
816
+ const childLlmModel = opts.childLlmModel?.trim() || null;
817
+ let round = 1;
818
+ try { round = nextBenchRound(readBenchLedger(bench.id, configDir), childLlmModel); } catch { /* 못 읽으면 1 — 줄에 그대로 남는다 */ }
819
+ let imageCommit: string | null = null;
820
+ try { imageCommit = deps.imageCommit ? deps.imageCommit() : getUserConfig().harness?.imageCommit ?? null; } catch { imageCommit = null; }
821
+ // ⭐ base sha 와 «다른 칸»이다 — base 는 자식 작업 가지의 출발점일 뿐, 부모·Pod 발사 코드는 지금 도는 코드다
822
+ // (10-11 사고: 부모를 base 의 옛 트리에서 띄워 옛 발사 코드가 provider 를 조용히 바꿨다).
823
+ let parentCodeCommit: string | null = null;
824
+ try { parentCodeCommit = deps.codeCommit ? deps.codeCommit() : codeRevision() ?? null; } catch { parentCodeCommit = null; }
825
+ const row: BenchLaunchRow = {
826
+ ts: new Date().toISOString(), event: 'launched', benchId: bench.id, baseBranch: bench.branch, baseSha: bench.sha,
827
+ parentCodeCommit, imageCommit, childLlmProvider: opts.childLlmProvider?.trim() || null, childLlmModel, round,
828
+ entrance, substrate, ...(opts.seat ? { seat: opts.seat } : {}),
829
+ };
830
+ try { appendBenchLedger(row, configDir); }
831
+ catch (error) { try { debug.log(BENCH_LOG_CATEGORY, 'ledger-unwritten', { benchId: bench.id, event: 'launched', reason: error instanceof Error ? error.message : String(error) }); } catch { /* fail-soft */ } }
832
+ return row;
833
+ }
834
+
835
+ function recordBenchFinish(launched: BenchLaunchRow, deps: HarnessBenchDeps): void {
836
+ const exitCode = typeof process.exitCode === 'number' ? process.exitCode : Number(process.exitCode ?? 0) || 0;
837
+ try { appendBenchLedger({ ...launched, ts: new Date().toISOString(), event: 'finished', exitCode }, deps.configDir ?? getElanousConfigDir()); }
838
+ catch (error) { try { debug.log(BENCH_LOG_CATEGORY, 'ledger-unwritten', { benchId: launched.benchId, event: 'finished', reason: error instanceof Error ? error.message : String(error) }); } catch { /* fail-soft */ } }
839
+ }
840
+
796
841
  async function dispatchHarnessAskSay(
797
842
  opts: HarnessAskSayChildLlmOptions & HarnessDryRunOpts,
798
843
  dryRunPreview: {
@@ -806,6 +851,7 @@ async function dispatchHarnessAskSay(
806
851
  queueDeps?: HarnessQueueDeps,
807
852
  immediateLaunch?: HarnessQueueDeps['launch'],
808
853
  immediateExit?: () => Promise<number>,
854
+ benchDeps: HarnessBenchDeps = {},
809
855
  ): Promise<void> {
810
856
  // HARNESS-FULL-GRAPH — 여정 조인 키는 이 발사 동안만 환경에 싣는다(자식은 그동안 물려받는다).
811
857
  const journeyPreset = Boolean(process.env[JOURNEY_KEY_ENV]?.trim());
@@ -815,6 +861,29 @@ async function dispatchHarnessAskSay(
815
861
  if (refusePodChildProviderBeforeLaunch(resolveHarnessSubstrate({ flag: opts as HarnessSubstrateOpts, config: getUserConfig(), env: process.env }), opts)) return;
816
862
  if (opts.childLlmModel !== undefined) await ensureOpenRouterChildModelLive(opts.childLlmProvider, opts.childLlmModel, { persist: !isHarnessDryRun(opts) });
817
863
  assertHarnessChildLlmModel(opts);
864
+ // BENCH-ISOLATED-ARMS ① — 기준선 가지를 발사 «전»에 고정한다(꼴이 틀리거나 sha 가 어긋나면 발사 0).
865
+ let bench: BenchBase | undefined;
866
+ if (opts.bench !== undefined) {
867
+ const spec = parseBenchSpec(opts.bench);
868
+ if (opts.base !== undefined) {
869
+ console.error(`❌ --bench 와 --base 는 함께 못 쓴다 — 벤치 base 는 ${benchBaseBranch(spec.id)} 로 정해진다`);
870
+ process.exitCode = 2;
871
+ return;
872
+ }
873
+ if (isHarnessDryRun(opts)) {
874
+ console.log(`[dry-run] bench: ${spec.id} → base ${benchBaseBranch(spec.id)} (${spec.sha ?? '가지가 있으면 그 sha · 없으면 발사 시점 origin/main'}) · 원격 변경 없음`);
875
+ } else {
876
+ try {
877
+ bench = ensureBenchBase(spec, benchDeps.git ?? defaultBenchGitRun(process.cwd()));
878
+ } catch (error) {
879
+ try { debug.log(BENCH_LOG_CATEGORY, 'base-refused', { benchId: spec.id, requestedSha: spec.sha ?? null, reason: error instanceof Error ? error.message : String(error) }); } catch { /* observation is fail-soft */ }
880
+ throw error;
881
+ }
882
+ try { debug.log(BENCH_LOG_CATEGORY, 'base-ensured', { benchId: bench.id, branch: bench.branch, sha: bench.sha, created: bench.created }); } catch { /* observation is fail-soft */ }
883
+ console.error(`🧪 bench ${bench.id} — base ${bench.branch} @ ${bench.sha.slice(0, 12)}${bench.created ? ' (새로 만듦)' : ' (기존 가지 · sha 일치)'}`);
884
+ opts = { ...opts, base: bench.branch };
885
+ }
886
+ }
818
887
  if (isHarnessDryRun(opts)) {
819
888
  resolveLaunchSubstrate(opts);
820
889
  printHarnessLaunchDryRun(dryRunPreview);
@@ -857,7 +926,9 @@ async function dispatchHarnessAskSay(
857
926
  resolveNestedElanousOption(opts);
858
927
  const flags: string[] = [];
859
928
  const values: Array<[string, string | undefined]> = [
860
- ['--base', opts.base], ['--target', opts.target], ['--correlation', opts.correlation],
929
+ // 벤치는 «--bench id:sha» 로 넘긴다 — 대기열 자식이 sha 를 다시 확인하고 원장 줄을 쓴다.
930
+ ['--base', bench ? undefined : opts.base], ['--bench', bench ? `${bench.id}:${bench.sha}` : undefined],
931
+ ['--target', opts.target], ['--correlation', opts.correlation],
861
932
  ['--goal-type', opts.goalType], ...(dryRunPreview.entrance === 'cli-harness-say' ? [['--author-grade', opts.authorGradeSource === 'flag' ? opts.authorGrade : undefined] as [string, string | undefined]] : []), ['--child-llm-provider', opts.childLlmProvider],
862
933
  ['--child-llm-model', opts.childLlmModel], ['--child-llm-effort', opts.childLlmEffort],
863
934
  ['--pod-pool', (opts as HarnessSubstrateOpts).podPool], ['--pod-memory', (opts as HarnessSubstrateOpts).podMemory],
@@ -937,8 +1008,10 @@ async function dispatchHarnessAskSay(
937
1008
  else delete process.env.ELANOUS_HARNESS_SEAT;
938
1009
  // 배치 노드 — 실행 칸(local | pod)이 그 출구 라벨이고, YAML 의 decompose 간선(on: substrate)이 그 값으로 갈린다.
939
1010
  passJourneyNode('dispatch', { ...journey, outcome: resolved.substrate });
1011
+ const benchRow = bench ? recordBenchLaunch(bench, stampedOpts, resolved.substrate, dryRunPreview.entrance, benchDeps) : undefined;
940
1012
  try { await dispatch(resolved, stampedOpts); }
941
1013
  finally {
1014
+ if (benchRow) recordBenchFinish(benchRow, benchDeps);
942
1015
  if (previous === undefined) delete process.env.ELANOUS_HARNESS_SEAT;
943
1016
  else process.env.ELANOUS_HARNESS_SEAT = previous;
944
1017
  }
@@ -2299,8 +2372,16 @@ export type GithubDraftSweepAdapters = DraftSweepAdapters & {
2299
2372
  runLedgerMatches(): readonly RunLedgerMatch[];
2300
2373
  };
2301
2374
 
2375
+ /** DRAFT-LINEAGE-ONE-1 — how the sweep stops a superseded run: `harness stop` (process ⊕ Pod Job by run label). */
2376
+ export type DraftSweepStopRun = (runId: string) => Promise<Pick<import('./harness-stop.js').HarnessStopResult, 'process' | 'jobsDeleted' | 'scanned'>>;
2377
+ const defaultDraftSweepStopRun: DraftSweepStopRun = async (runId) => {
2378
+ const { stopHarnessRun, defaultHarnessStopDeps } = await import('./harness-stop.js');
2379
+ return stopHarnessRun(runId, defaultHarnessStopDeps());
2380
+ };
2381
+
2302
2382
  export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: DraftSweepGitExecute = runGitCommand, cwd = process.cwd(),
2303
- executeAsync: (args: string[]) => Promise<string> = execute === executeGh ? executeGhAsync : async (args) => execute(args)): GithubDraftSweepAdapters {
2383
+ executeAsync: (args: string[]) => Promise<string> = execute === executeGh ? executeGhAsync : async (args) => execute(args),
2384
+ stopRun: DraftSweepStopRun = defaultDraftSweepStopRun): GithubDraftSweepAdapters {
2304
2385
  const inventory = async (repository: string, state: 'open' | 'closed'): Promise<GithubPull[]> => {
2305
2386
  // ⛔ A large repository has tens of thousands of closed PRs (2026-09-28: 21k → «exceeded pagination limit», no sweep at all).
2306
2387
  // Merged twins only matter after the oldest open draft was opened, so the closed listing stops at that time.
@@ -2568,10 +2649,25 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
2568
2649
  commentDraft: async (repository, number, body) => { execute(['pr', 'comment', String(number), '--repo', repository, '--body', body]); },
2569
2650
  removeLabel: async (repository, number, name) => { execute(['pr', 'edit', String(number), '--repo', repository, '--remove-label', name]); },
2570
2651
  getReviewGate: async (draft, repository) => reviewGateForDraft(draft, repository),
2652
+ // DRAFT-LINEAGE-ONE-1 — the sweep calls this only with `--apply`, only for a live run whose newer same-lineage run
2653
+ // is live too. The run id must come from this PR's own ledger owner; a stop that cannot be confirmed rejects, so
2654
+ // the draft is neither labelled nor closed.
2655
+ supersedeRun: async (draft) => {
2656
+ if (!draft.runId) throw new Error(`run id of #${draft.number} unknown (no single ledger owner) — not stopping`);
2657
+ const stopped = await stopRun(draft.runId);
2658
+ // `absent` after a complete process-table scan = nothing left to stop (a dead run whose worktree remains) — confirmed.
2659
+ const confirmed = stopped.process === 'stopped' || stopped.process === 'killed' || stopped.jobsDeleted > 0
2660
+ || (stopped.process === 'absent' && stopped.scanned);
2661
+ if (!confirmed) {
2662
+ throw new Error(`run ${draft.runId} not confirmed stopped (process ${stopped.process} · jobs ${stopped.jobsDeleted} · scanned ${stopped.scanned})`);
2663
+ }
2664
+ },
2571
2665
  };
2572
2666
  }
2573
2667
 
2574
2668
  export interface HarnessDraftSweepDeps {
2669
+ /** DRAFT-LINEAGE-ONE-1 test seam for the real adapter's run stop; omitted ⇒ `harness stop`. */
2670
+ readonly stopRun?: DraftSweepStopRun;
2575
2671
  readonly adapters?: DraftSweepAdapters;
2576
2672
  /** Optional overlap source injection; tests do not read GitHub or logs.db. */
2577
2673
  readonly overlapAdapters?: OverlapMetricAdapters;
@@ -2729,7 +2825,7 @@ function installHarnessDraftSweepCommand(harnessCmd: Command, deps: HarnessDraft
2729
2825
  throw new HarnessCliInputError(`invalid --repo (expected owner/name): ${repository}`);
2730
2826
  }
2731
2827
  // One adapter instance per sweep: its open listing and ledger scan are shared with the RUN-TTL step.
2732
- const github = deps.adapters ? undefined : githubDraftSweepAdapters(deps.execute);
2828
+ const github = deps.adapters ? undefined : githubDraftSweepAdapters(deps.execute, undefined, undefined, undefined, deps.stopRun);
2733
2829
  const configCap = deps.closeCapConfig
2734
2830
  ? deps.closeCapConfig()
2735
2831
  : (() => { try { return getUserConfig().tools?.selfImplement?.draftSweepCloseCap; } catch { return undefined; } })();
@@ -3022,6 +3118,8 @@ export interface HarnessCliCommandDeps {
3022
3118
  say?: HarnessSayHandler;
3023
3119
  /** Observe-only Pod host dispatch seam. */
3024
3120
  podDispatchTask?: (input: DispatchTaskInput, deps?: DispatchTaskDeps) => ReturnType<typeof dispatchTask>;
3121
+ /** BENCH-ISOLATED-ARMS — 벤치 base 고정·원장 주입(시험). */
3122
+ bench?: HarnessBenchDeps;
3025
3123
  launchGate?: PreLaunchGateDeps & { readBudget?: () => Promise<BudgetDecision | 'unknown'> };
3026
3124
  plan?: HarnessPlanHandler;
3027
3125
  /** ⭐ `plan` 주입이 «없을 때» 가는 기본 분기. 시험이 이 자리로 «CLI 의 기본 배선»을 문다
@@ -3029,6 +3127,8 @@ export interface HarnessCliCommandDeps {
3029
3127
  planRfc?: typeof runHarnessPlanRfc;
3030
3128
  mission?: HarnessMissionHandler;
3031
3129
  missionLoop?: HarnessMissionLoopHandler;
3130
+ /** `harness progress` 가 도는 런 관측을 읽는 자리(시험 주입). 기본은 queryRunningRuns. */
3131
+ progress?: { query?: typeof queryRunningRuns; nowMs?: () => number };
3032
3132
  }
3033
3133
 
3034
3134
  export function installHarnessCliCommand(program: Command, deps: HarnessCliCommandDeps): Command {
@@ -3087,6 +3187,21 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
3087
3187
  process.exitCode = 1;
3088
3188
  }
3089
3189
  });
3190
+ harnessCmd.command('progress').description('도는 골마다 진도를 한 표로 읽는다 — 단계 · 진입 시각 · 마지막 진전 · pty (읽기 전용)')
3191
+ .option('--json', '같은 행 배열을 JSON 으로 출력')
3192
+ .action((opts: { json?: boolean }) => {
3193
+ try {
3194
+ const result = (deps.progress?.query ?? queryRunningRuns)({ caller: 'harness.progress' });
3195
+ const nowMs = (deps.progress?.nowMs ?? Date.now)();
3196
+ if (opts.json) {
3197
+ console.log(JSON.stringify(buildRunProgressRows(result, nowMs)));
3198
+ if (result.completeness === 'partial') console.error(renderRunProgressTable(result, nowMs).split('\n')[0]);
3199
+ } else console.log(renderRunProgressTable(result, nowMs));
3200
+ } catch (error) {
3201
+ console.error(humanErrorLine(error));
3202
+ process.exitCode = 1;
3203
+ }
3204
+ });
3090
3205
  installDeliverableVerifyCliCommand(harnessCmd, deps.deliverableVerify);
3091
3206
  installHarnessProcessObservationCommand(harnessCmd, deps.processObservation);
3092
3207
  installHarnessBudgetCommand(harnessCmd);
@@ -3194,7 +3309,7 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
3194
3309
  }
3195
3310
  return resolved.substrate === 'pod' ? onPod(stampedOpts, 'cli-harness-ask', goalPath, resolved.pool!, deps.podDispatchTask) : (announceLocalChildProvider(stampedOpts), ask(goalPath, normalizeHarnessAskSayOptions(stampedOpts)));
3196
3311
  },
3197
- deps.launchGate, queueDeps, immediateLaunch, queueDeps.launch ? undefined : immediateExit,
3312
+ deps.launchGate, queueDeps, immediateLaunch, queueDeps.launch ? undefined : immediateExit, deps.bench,
3198
3313
  );
3199
3314
  });
3200
3315
  }
@@ -3227,7 +3342,7 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
3227
3342
  ? onPod(stampedOpts, 'cli-harness-say', sentence.join(' '), resolved.pool!, deps.podDispatchTask, harnessAuthorOnPod())
3228
3343
  : (announceLocalChildProvider(stampedOpts), say(sentence, normalizeHarnessAskSayOptions(stampedOpts)));
3229
3344
  },
3230
- deps.launchGate, queueDeps, immediateLaunch, queueDeps.launch ? undefined : immediateExit,
3345
+ deps.launchGate, queueDeps, immediateLaunch, queueDeps.launch ? undefined : immediateExit, deps.bench,
3231
3346
  );
3232
3347
  });
3233
3348
  }
@@ -0,0 +1,331 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ import { debug } from '../debug/log.js';
5
+ import { configuredWorktreeRoot } from '../user-config.js';
6
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
7
+ import { acquireLockSync } from '../storage/file-lock.js';
8
+ import { createGhRunner, execHarnessClean, gitStdout, type GitRunner, type GhRunner, type ActiveDirectoryProvider } from './harness-clean.js';
9
+ import type { WorktreeAssessmentInput, WorktreeAssessment } from './harness-worktrees.js';
10
+
11
+ export interface HarnessGcOptions {
12
+ repos?: string[];
13
+ dryRun?: boolean;
14
+ window?: number;
15
+ maxRemove?: number;
16
+ budgetMs?: number;
17
+ }
18
+
19
+ export interface HarnessGcDeps {
20
+ stateRoot?: string;
21
+ worktreeRoot?: string;
22
+ now?: () => number;
23
+ run?: GitRunner;
24
+ runGh?: GhRunner;
25
+ activeDirectoryProvider?: ActiveDirectoryProvider;
26
+ assessWorktree?: (input: WorktreeAssessmentInput) => WorktreeAssessment;
27
+ readWorktreeProvenance?: (path: string, run: GitRunner) => { owner: string; command: string; createdAt: string };
28
+ }
29
+
30
+ export interface HarnessGcRepo {
31
+ gitDir: string;
32
+ offset: number;
33
+ window: number;
34
+ total: number;
35
+ removed: number;
36
+ preserved: number;
37
+ deferred: number;
38
+ failed: number;
39
+ preservedByReason: Record<string, number>;
40
+ orphanScan: 'scanned' | 'unavailable' | 'not-requested';
41
+ unseenOutsideWindow?: number;
42
+ unseenOutsidePrefix?: number;
43
+ }
44
+
45
+ export interface HarnessGcResult {
46
+ outcome: 'ok' | 'skipped' | 'fail';
47
+ reason?: 'overlap' | 'no-target-repo' | 'repo-failed' | 'budget-exhausted';
48
+ repos: number;
49
+ removed: number;
50
+ preserved: number;
51
+ deferred: number;
52
+ failed: number;
53
+ unseenOutsideWindow?: number;
54
+ unseenOutsidePrefix?: number;
55
+ undiscoverable: number;
56
+ orphanScanUnavailable: number;
57
+ budgetMs: number;
58
+ elapsedMs: number;
59
+ repositories: HarnessGcRepo[];
60
+ repoFailures?: Array<{ gitDir: string; error: string }>;
61
+ /** The tick budget ran out inside a repository's queries; that repository was left unverified (cursor kept). */
62
+ budgetExhausted?: boolean;
63
+ }
64
+
65
+ /** Discover current linked worktrees; the tick also retains previously verified automatic targets. */
66
+ export function discoverHarnessGcRepos(root: string, expired: () => boolean = () => false):
67
+ { repos: string[]; undiscoverable: number; stopped?: boolean } {
68
+ const repos = new Set<string>();
69
+ let undiscoverable = 0;
70
+ // The deadline is checked *between* reads (each readdir / .git read); a stopped walk leaves the rest unverified.
71
+ let stopped = false;
72
+ const halt = (): boolean => { if (!stopped && expired()) stopped = true; return stopped; };
73
+ const done = () => ({ repos: [...repos].sort(), undiscoverable, ...(stopped ? { stopped } : {}) });
74
+ const children = (path: string): string[] => readdirSync(path, { withFileTypes: true })
75
+ .filter((entry) => entry.isDirectory()).map((entry) => join(path, entry.name));
76
+ let scopes: string[];
77
+ if (halt()) return done();
78
+ try { scopes = children(root); }
79
+ catch { return { repos: [], undiscoverable: 0 }; }
80
+ for (const scope of scopes) {
81
+ if (halt()) return done();
82
+ let parents: string[];
83
+ try { parents = children(scope); }
84
+ catch { undiscoverable++; continue; }
85
+ for (const parent of parents) {
86
+ if (!parent.endsWith('.worktrees')) continue;
87
+ if (halt()) return done();
88
+ let paths: string[];
89
+ try { paths = children(parent); }
90
+ catch { undiscoverable++; continue; }
91
+ for (const path of paths) {
92
+ if (halt()) return done();
93
+ try {
94
+ const line = readFileSync(join(path, '.git'), 'utf8').trim();
95
+ const match = /^gitdir:\s*(.+?)[/\\]worktrees[/\\][^/\\]+\s*$/.exec(line);
96
+ if (!match) { undiscoverable++; continue; }
97
+ const gitDir = isAbsolute(match[1]!) ? match[1]! : resolve(path, match[1]!);
98
+ repos.add(resolve(gitDir));
99
+ } catch { undiscoverable++; }
100
+ }
101
+ }
102
+ }
103
+ return done();
104
+ }
105
+
106
+ function defaultRun(args: string[]) {
107
+ const result = spawnSync('git', args, { encoding: 'utf8', timeout: 20_000 });
108
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
109
+ }
110
+
111
+ function positive(value: number | undefined, fallback: number): number {
112
+ if (value === undefined) return fallback;
113
+ if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`invalid GC limit: ${value}`);
114
+ return value;
115
+ }
116
+
117
+ /** One lock guards discovery, every repository window, deletion, and cursor writes. */
118
+ export function runHarnessGcTick(opts: HarnessGcOptions = {}, deps: HarnessGcDeps = {}): HarnessGcResult {
119
+ const windowSize = positive(opts.window, 150);
120
+ const maxRemove = positive(opts.maxRemove, 30);
121
+ const budgetMs = positive(opts.budgetMs, 240_000);
122
+ const now = deps.now ?? Date.now;
123
+ const started = now();
124
+ const state = join(deps.stateRoot ?? effectiveInstanceRoot(), 'harness-gc');
125
+ mkdirSync(state, { recursive: true });
126
+ const lockPath = join(state, 'tick.lock');
127
+ let holder = '';
128
+ let release: () => void;
129
+ try {
130
+ release = acquireLockSync(lockPath, { maxTries: 2, retryBusyMs: 1, breakDeadHolder: true,
131
+ staleMs: Math.max(2 * budgetMs, 600_000), onWait: (value) => { holder = value; } });
132
+ } catch (error) {
133
+ if (!String(error).includes('timed out')) throw error;
134
+ debug.log('harness.gc', 'tick-skipped', { reason: 'overlap', holder });
135
+ return { outcome: 'skipped', reason: 'overlap', repos: 0, removed: 0, preserved: 0, deferred: 0, failed: 0,
136
+ undiscoverable: 0, orphanScanUnavailable: 0, budgetMs, elapsedMs: now() - started, repositories: [] };
137
+ }
138
+ try {
139
+ const runner = deps.run ?? defaultRun;
140
+ const cursorPath = join(state, 'cursor.json');
141
+ const targetsPath = join(state, 'targets.json');
142
+ let cursor: Record<string, number> = {};
143
+ let persistedTargets: string[] = [];
144
+ try {
145
+ const parsed: unknown = JSON.parse(readFileSync(cursorPath, 'utf8'));
146
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) cursor = parsed as Record<string, number>;
147
+ } catch { /* No cursor yet, or corrupt state: start at the beginning. */ }
148
+ try {
149
+ const parsed: unknown = JSON.parse(readFileSync(targetsPath, 'utf8'));
150
+ if (Array.isArray(parsed)) persistedTargets = parsed.filter((path): path is string =>
151
+ typeof path === 'string' && isAbsolute(path));
152
+ } catch { /* No persisted automatic targets yet. */ }
153
+ const discovery = opts.repos === undefined
154
+ ? discoverHarnessGcRepos(deps.worktreeRoot ?? configuredWorktreeRoot(), () => now() - started >= budgetMs)
155
+ : { repos: opts.repos, undiscoverable: 0 };
156
+ // A verified automatic target survives removal of its last linked worktree; --repo never enrolls it.
157
+ const candidates = opts.repos === undefined
158
+ ? [...new Set([...discovery.repos, ...persistedTargets])]
159
+ : discovery.repos;
160
+ const targets = new Set<string>();
161
+ const repoFailures: Array<{ gitDir: string; error: string }> = [];
162
+ let failed = 0;
163
+ // A discovery walk cut by the deadline: repositories it did not reach are unverified, not absent.
164
+ let discoveryBudgetExhausted = 'stopped' in discovery && discovery.stopped === true;
165
+ for (const repo of [...candidates].sort()) {
166
+ if (now() - started >= budgetMs) { discoveryBudgetExhausted = true; break; }
167
+ const common = gitStdout(['-C', repo, 'rev-parse', '--path-format=absolute', '--git-common-dir'], runner);
168
+ if (common.ok && common.value && isAbsolute(common.value)) {
169
+ const gitDir = resolve(common.value);
170
+ targets.add(gitDir);
171
+ if (!opts.dryRun && opts.repos === undefined) persistedTargets.push(gitDir);
172
+ } else { failed++; repoFailures.push({ gitDir: repo, error: 'git common directory unavailable' }); }
173
+ }
174
+ const repositories: HarnessGcRepo[] = [];
175
+ let removed = 0;
176
+ let preserved = 0;
177
+ let deferred = 0;
178
+ let unseenOutsideWindow: number | undefined = repoFailures.length || discoveryBudgetExhausted ? undefined : 0;
179
+ let unseenOutsidePrefix: number | undefined = repoFailures.length || discoveryBudgetExhausted ? undefined : 0;
180
+ let orphanScanUnavailable = 0;
181
+ let processedTargets = 0;
182
+ let budgetExhausted = false;
183
+ // maxRemove bounds deletion *attempts* (incl. partial/failed ones), not only successes.
184
+ let attempts = 0;
185
+ const overBudget = (): boolean => now() - started >= budgetMs;
186
+ const baseGh: GhRunner = deps.runGh ?? createGhRunner();
187
+ for (const gitDir of [...targets].sort()) {
188
+ if (overBudget()) { budgetExhausted = true; break; } // remaining repositories unprocessed — record it
189
+ processedTargets++;
190
+ const offset = Number.isSafeInteger(cursor[gitDir]) && cursor[gitDir]! >= 0 ? cursor[gitDir]! : 0;
191
+ // The deadline is enforced inside the clean too: once it passes, every further *read* (git query, PR lookup,
192
+ // safety measurement) fails fast instead of running on past the tick. Deletions are never cut mid-item —
193
+ // `shouldRemove` already refuses to start one after the deadline.
194
+ let budgetHit = false;
195
+ const isDeletion = (args: readonly string[]): boolean =>
196
+ (args.includes('worktree') && args.includes('remove')) || (args.includes('branch') && args.includes('-D'));
197
+ const run: GitRunner = (args) => {
198
+ if (!isDeletion(args) && overBudget()) { budgetHit = true; return { status: 1, stdout: '', stderr: 'harness gc tick budget exhausted' }; }
199
+ return runner(args[0] === '-C' ? args : ['-C', gitDir, ...args]);
200
+ };
201
+ const runGh: GhRunner = (args, options) => {
202
+ if (overBudget()) { budgetHit = true; return { status: 1, stdout: '', stderr: 'harness gc tick budget exhausted', error: 'budget' }; }
203
+ return baseGh(args, options);
204
+ };
205
+ try {
206
+ // gh's {owner}/{repo} placeholders refer to cwd, not this target repository.
207
+ const remote = gitStdout(['config', '--get', 'remote.origin.url'], run);
208
+ const identity = remote.ok ? /(?:github\.com[:/])([^/:]+)\/([^/]+?)(?:\.git)?\/?$/.exec(remote.value) : null;
209
+ if (!identity) throw new Error('target repository has no GitHub origin identity');
210
+ const result = execHarnessClean({ mode: 'merged', dryRun: opts.dryRun, repo: `${identity[1]}/${identity[2]}`,
211
+ window: { offset, size: windowSize }, forceWorktreeRemove: false, run, runGh, expired: overBudget,
212
+ activeDirectoryProvider: deps.activeDirectoryProvider,
213
+ assessWorktree: deps.assessWorktree,
214
+ readWorktreeProvenance: deps.readWorktreeProvenance,
215
+ shouldRemove: () => {
216
+ if (attempts >= maxRemove) return false; // count cap, not the time budget
217
+ if (overBudget()) { budgetExhausted = true; return false; } // deletion deferred by the time budget
218
+ attempts++;
219
+ return true;
220
+ } });
221
+ const plan = result.plan;
222
+ if (result.expired) budgetHit = true;
223
+ if (budgetHit) {
224
+ // Unverified, not failed: nothing is counted as preserved/failed and the cursor is kept for the next tick.
225
+ removed += result.removed.length;
226
+ failed += result.failed.length;
227
+ budgetExhausted = true;
228
+ debug.log('harness.gc', 'repo-budget-exhausted', { gitDir, removed: result.removed.length });
229
+ break;
230
+ }
231
+ if (plan.unavailable) {
232
+ repoFailures.push({ gitDir, error: 'required clean query unavailable' });
233
+ failed++;
234
+ }
235
+ const pending = result.deferred?.length ?? 0;
236
+ const byReason: Record<string, number> = {};
237
+ for (const item of plan.preserve) byReason[item.reason] = (byReason[item.reason] ?? 0) + 1;
238
+ const repo: HarnessGcRepo = {
239
+ gitDir, offset: result.window!.offset, window: result.window!.size, total: result.window!.total,
240
+ removed: result.removed.length, preserved: plan.preserve.length, deferred: pending,
241
+ failed: result.failed.length, preservedByReason: byReason, orphanScan: plan.orphanScan,
242
+ ...(plan.queryStatus?.branches && plan.queryStatus.worktrees
243
+ ? { unseenOutsideWindow: result.window!.unseenOutsideWindow } : {}),
244
+ ...(plan.queryStatus?.branches && plan.queryStatus.worktrees && plan.scope?.registeredWorktrees !== undefined
245
+ ? { unseenOutsidePrefix: Math.max(0, plan.scope.registeredWorktrees - plan.scope.matchedWorktrees) } : {}),
246
+ };
247
+ repositories.push(repo);
248
+ debug.log('harness.gc', 'repo', repo);
249
+ removed += repo.removed;
250
+ preserved += repo.preserved;
251
+ deferred += repo.deferred;
252
+ failed += repo.failed;
253
+ if (repo.unseenOutsideWindow === undefined) unseenOutsideWindow = undefined;
254
+ else if (unseenOutsideWindow !== undefined) unseenOutsideWindow += repo.unseenOutsideWindow;
255
+ if (repo.unseenOutsidePrefix === undefined) unseenOutsidePrefix = undefined;
256
+ else if (unseenOutsidePrefix !== undefined) unseenOutsidePrefix += repo.unseenOutsidePrefix;
257
+ if (plan.orphanScan === 'unavailable') orphanScanUnavailable++;
258
+ if (!opts.dryRun && repo.deferred === 0 && repo.failed === 0 && !plan.unavailable) {
259
+ const next = repo.offset + repo.window - repo.removed;
260
+ cursor[gitDir] = next >= repo.total - repo.removed ? 0 : Math.max(0, next);
261
+ }
262
+ } catch (error) {
263
+ if (budgetHit) {
264
+ budgetExhausted = true;
265
+ debug.log('harness.gc', 'repo-budget-exhausted', { gitDir, error: String(error) });
266
+ break;
267
+ }
268
+ failed++;
269
+ unseenOutsideWindow = undefined;
270
+ unseenOutsidePrefix = undefined;
271
+ repoFailures.push({ gitDir, error: String(error) });
272
+ debug.log('harness.gc', 'repo-failed', { gitDir, error: String(error) }, { level: 'error' });
273
+ }
274
+ }
275
+ if (processedTargets < targets.size || budgetExhausted) {
276
+ unseenOutsideWindow = undefined;
277
+ unseenOutsidePrefix = undefined;
278
+ }
279
+ if (!opts.dryRun && targets.size) {
280
+ const temp = join(state, `cursor.${process.pid}.tmp`);
281
+ writeFileSync(temp, JSON.stringify(cursor));
282
+ renameSync(temp, cursorPath);
283
+ }
284
+ if (!opts.dryRun && opts.repos === undefined && persistedTargets.length) {
285
+ const temp = join(state, `targets.${process.pid}.tmp`);
286
+ writeFileSync(temp, JSON.stringify([...new Set(persistedTargets)].sort()));
287
+ renameSync(temp, targetsPath);
288
+ }
289
+ // A tick whose budget ran out before any repository was verified did not succeed — it is «skipped», never «ok».
290
+ const outcome = (targets.size === 0 && !discoveryBudgetExhausted) || failed > 0 ? 'fail'
291
+ : repositories.length === 0 ? 'skipped' : 'ok';
292
+ const summary: HarnessGcResult = { outcome,
293
+ ...(outcome === 'fail' ? { reason: targets.size === 0 ? 'no-target-repo' as const : 'repo-failed' as const }
294
+ : outcome === 'skipped' ? { reason: 'budget-exhausted' as const } : {}),
295
+ repos: repositories.length, removed, preserved, deferred, failed,
296
+ ...(unseenOutsideWindow === undefined ? {} : { unseenOutsideWindow }),
297
+ ...(unseenOutsidePrefix === undefined ? {} : { unseenOutsidePrefix }),
298
+ undiscoverable: discovery.undiscoverable, orphanScanUnavailable, budgetMs,
299
+ elapsedMs: now() - started, repositories,
300
+ ...(repoFailures.length ? { repoFailures } : {}),
301
+ ...(budgetExhausted || discoveryBudgetExhausted ? { budgetExhausted: true } : {}) };
302
+ debug.log('harness.gc', 'tick', summary);
303
+ return summary;
304
+ } finally { release(); }
305
+ }
306
+
307
+ function cliArgs(args: string[]): HarnessGcOptions {
308
+ const opts: HarnessGcOptions = {};
309
+ for (let i = 0; i < args.length; i++) {
310
+ const arg = args[i];
311
+ if (arg === '--json') continue;
312
+ if (arg === '--dry-run') { opts.dryRun = true; continue; }
313
+ if (arg === '--repo') { (opts.repos ??= []).push(args[++i] ?? ''); continue; }
314
+ const key = arg === '--window' ? 'window' : arg === '--max-remove' ? 'maxRemove' : arg === '--budget-ms' ? 'budgetMs' : null;
315
+ if (!key) throw new Error(`unknown GC option: ${arg}`);
316
+ opts[key] = Number(args[++i]);
317
+ }
318
+ if (opts.repos?.some((repo) => !repo)) throw new Error('--repo needs a path');
319
+ return opts;
320
+ }
321
+
322
+ if (import.meta.main) {
323
+ try {
324
+ const result = runHarnessGcTick(cliArgs(process.argv.slice(2)));
325
+ console.log(JSON.stringify(result));
326
+ if (result.outcome === 'fail') process.exitCode = 1;
327
+ } catch (error) {
328
+ console.log(JSON.stringify({ outcome: 'fail', reason: String(error) }));
329
+ process.exitCode = 1;
330
+ }
331
+ }
@@ -299,8 +299,10 @@ export async function stopHarnessRun(runId: string, deps: HarnessStopDeps, conte
299
299
  const targets = dryRun ? [] : contexts && contexts.length > 0 ? [...contexts] : deps.kubeContexts();
300
300
  let jobsDeleted = 0;
301
301
  for (const context of targets) {
302
- const r = deps.kubectl(['--context', context, '-n', deps.namespace, 'delete', 'job', '-l', `elanous.run=${runId}`, '--wait=false']);
303
- if (r.status === 0) jobsDeleted += r.stdout.split('\n').filter((line) => line.startsWith('job.batch/')).length;
302
+ // `-o name` → «job.batch/<name>» 한 줄씩. 맨 출력은 «job.batch "<name>" deleted» 라 «job.batch/» 로만 세면
303
+ // 지워도 늘 0 이었다(10-11 · 여섯 번 «0개 지움» 뒤 Pod Terminating). 두 꼴을 다 센다.
304
+ const r = deps.kubectl(['--context', context, '-n', deps.namespace, 'delete', 'job', '-l', `elanous.run=${runId}`, '--wait=false', '-o', 'name']);
305
+ if (r.status === 0) jobsDeleted += r.stdout.split('\n').filter((line) => /^job\.batch(?:\/|\s+")/.test(line.trim())).length;
304
306
  else debug.log('harness.stop', 'job-delete-failed', { runId, context, error: r.stderr.slice(0, 200) }, { level: 'warn' });
305
307
  }
306
308
  const result: HarnessStopResult = { runId, ...(given !== runId ? { givenRunId: given } : {}), process: processState, ...(record ? { pid: record.pid } : {}), candidates, scanned: scan.status === 'ok', pidRecordFound: record !== null, dryRun, jobsDeleted, contexts: targets };