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
@@ -11,6 +11,7 @@ import { findGrokModel } from '../grok/models.js';
11
11
  import { getUserConfig } from '../user-config.js';
12
12
  import { BUILTIN_CATALOG } from '../intelligence-map/model-catalog.js';
13
13
  import { getCatalog } from '../registry/loader.js';
14
+ import { byokCostFor, type ByokActualCost, type ByokPartialCost } from '../llm/openrouter-byok-cost.js';
14
15
 
15
16
  export interface LlmPricingUsd {
16
17
  inputPerM: number;
@@ -23,6 +24,13 @@ export interface LlmPricingUsd {
23
24
  longContext?: { thresholdTokens: number; inputPerM: number; outputPerM: number; cacheReadPerM?: number; cacheWritePerM?: number };
24
25
  }
25
26
 
27
+ /** AWS Bedrock Global Standard prices, USD per million tokens (AWS pricing API, ap-northeast-2). */
28
+ export const BEDROCK_PRICING_USD: Readonly<Record<string, LlmPricingUsd>> = {
29
+ 'claude-haiku-5-5': { inputPerM: 0.1, outputPerM: 0.5, cacheWritePerM: 0.125, cacheReadPerM: 0.01 },
30
+ 'claude-sonnet-5-5': { inputPerM: 2, outputPerM: 10, cacheWritePerM: 2.5, cacheReadPerM: 0.1 },
31
+ 'claude-opus-5-5': { inputPerM: 4, outputPerM: 20, cacheWritePerM: 5, cacheReadPerM: 0.2 },
32
+ };
33
+
26
34
  export interface LlmCostUsage {
27
35
  model: string;
28
36
  inputTokens?: number;
@@ -46,7 +54,7 @@ export type LlmCostEstimate =
46
54
  kind: 'known';
47
55
  model: string;
48
56
  usd: number;
49
- source: 'catalog' | 'config' | 'registry';
57
+ source: 'catalog' | 'bedrock-catalog' | 'config' | 'registry';
50
58
  cacheReadPricedAt: 'cache-read' | 'input-rate';
51
59
  cacheWritePricedAt: 'cache-write' | 'input-rate';
52
60
  /** 장문맥 단가로 매겼으면 `'long-context'` — 없으면 기본 단가. */
@@ -56,7 +64,7 @@ export type LlmCostEstimate =
56
64
  kind: 'partial';
57
65
  model: string;
58
66
  usd: number;
59
- source: 'catalog' | 'config' | 'registry';
67
+ source: 'catalog' | 'bedrock-catalog' | 'config' | 'registry';
60
68
  cacheReadPricedAt: 'cache-read' | 'input-rate';
61
69
  cacheWritePricedAt: 'cache-write' | 'input-rate';
62
70
  pricedTier?: 'long-context';
@@ -147,6 +155,21 @@ export function normalizePricingModelId(model: string): string {
147
155
  return m;
148
156
  }
149
157
 
158
+ /** Normalize a Bedrock Anthropic model id to its bare Claude pricing id. */
159
+ export function bedrockPricingModelId(model: string): string {
160
+ return model.trim()
161
+ .replace(/^bedrock\//, '')
162
+ .replace(/^[a-z0-9-]+\.anthropic\./i, '')
163
+ .replace(/^anthropic\./, '')
164
+ .replace(/-v1:0$/, '');
165
+ }
166
+
167
+ function isBedrockPricingModel(model: string): boolean {
168
+ const trimmed = model.trim();
169
+ return /^(?:bedrock\/|[a-z0-9-]+\.anthropic\.|anthropic\.)/i.test(trimmed)
170
+ && bedrockPricingModelId(trimmed).startsWith('claude-');
171
+ }
172
+
150
173
  /** 저장소의 모델 정본(intelligence-map BUILTIN_CATALOG) — 손 카탈로그 넷에 없는 모델(Claude 5 계열 등)의 입력·출력 단가.
151
174
  * 캐시 단가는 거기 없다 → Anthropic 모델은 공식 규칙(읽기 0.1× · 쓰기 5분 1.25×)으로 파생한다. 다른 벤더는 입력 단가로 매기고
152
175
  * `cacheReadPricedAt: 'input-rate'` 로 그 사실을 드러낸다(추측하지 않는다). */
@@ -174,8 +197,8 @@ function registryPricing(model: string): LlmPricingUsd | undefined {
174
197
  function resolvePricing(
175
198
  model: string,
176
199
  opts?: EstimateLlmCostOptions,
177
- ): { pricing: LlmPricingUsd; source: 'catalog' | 'config' | 'registry' } | undefined {
178
- // ⭐ 순서: 손 카탈로그(정확) → 정규화 id → config 오버레이 → 저장소 정본 → 레지스트리. 앞이 이긴다.
200
+ ): { pricing: LlmPricingUsd; source: 'catalog' | 'bedrock-catalog' | 'config' | 'registry' } | undefined {
201
+ // ⭐ 순서: 손 카탈로그(정확) → 정규화 id → config 오버레이 → Bedrock 표 → 저장소 정본 → 레지스트리. 앞이 이긴다.
179
202
  const normalized = normalizePricingModelId(model);
180
203
  for (const id of normalized === model ? [model] : [model, normalized]) {
181
204
  const catalog = catalogPricing(id);
@@ -185,6 +208,10 @@ function resolvePricing(
185
208
  const overlay = opts?.configPricing?.[id];
186
209
  if (validPricing(overlay)) return { pricing: overlay, source: 'config' };
187
210
  }
211
+ if (isBedrockPricingModel(model)) {
212
+ const bedrock = BEDROCK_PRICING_USD[bedrockPricingModelId(model)];
213
+ if (validPricing(bedrock)) return { pricing: bedrock, source: 'bedrock-catalog' };
214
+ }
188
215
  const builtin = builtinCatalogPricing(normalized);
189
216
  if (validPricing(builtin)) return { pricing: builtin, source: 'catalog' };
190
217
  const registry = registryPricing(model);
@@ -297,11 +324,16 @@ export function llmUsageCostFields(
297
324
  usage: Omit<LlmCostUsage, 'model'>,
298
325
  opts?: EstimateLlmCostOptions,
299
326
  billing?: BillingRoute,
300
- ): { cost: LlmCostEstimate | IncludedLlmCost | ActualLlmCost } {
327
+ ): { cost: LlmCostEstimate | IncludedLlmCost | ActualLlmCost | ByokActualCost | ByokPartialCost } {
301
328
  const resolved = opts ?? { configPricing: readLlmConfigPricing() };
302
329
  const estimate = estimateLlmCost({ model, ...usage }, resolved);
303
330
  // ⛔⭐ 구독을 API 단가로 «known» 이라 적지 않는다 — 09-25 벤치에서 codex 구독 팔이 `$7.81 known` 으로 보였다.
304
331
  // ⭐ BACKLOG C7 — provider 가 실어 보낸 «실제 청구액»이 있으면 그것이 비용이다(추정은 곁에 남긴다).
332
+ // OR-BYOK-COST — BYOK 면 «OR 청구 + 상류 청구»가 비용이다(상류를 모르면 partial · 0 으로 가정하지 않는다).
333
+ if (billing !== 'subscription' && billing !== 'local') {
334
+ const byok = byokCostFor(model, usage as Parameters<typeof byokCostFor>[1], estimate.kind === 'known' || estimate.kind === 'partial' ? estimate.usd : undefined);
335
+ if (byok) return { cost: byok };
336
+ }
305
337
  const reported = (usage as { reportedCostUsd?: unknown }).reportedCostUsd;
306
338
  if (typeof reported === 'number' && Number.isFinite(reported) && billing !== 'subscription' && billing !== 'local') {
307
339
  return { cost: { kind: 'actual', model, usd: reported, source: 'provider-reported', ...(estimate.kind === 'known' || estimate.kind === 'partial' ? { estimateUsd: estimate.usd } : {}) } };
@@ -158,18 +158,24 @@ export type ResetCreditExpiryAxis =
158
158
  | { readonly status: 'expired'; readonly expiresAt: string; readonly hasUnknownExpiry: boolean };
159
159
 
160
160
  export interface AccountUsageRow {
161
- readonly provider: 'codex' | 'grok' | 'openrouter';
161
+ /** `zai` = GLM Coding Plan(ZAI-CODING-PLAN-PROVIDER) — `ZAI_API_KEY` 가 있을 때만 행이 생긴다. */
162
+ readonly provider: 'codex' | 'grok' | 'openrouter' | 'zai';
162
163
  readonly accountName: string;
163
164
  readonly accountCount: number;
164
165
  readonly soleAccount: boolean;
165
166
  readonly credits: CreditAxis;
166
167
  readonly subscription: SubscriptionAxis;
167
168
  readonly resetCredits: ResetCreditExpiryAxis;
169
+ /** 행 끝에 붙는 한 줄(출처·실수치) — 없으면 종전 문면과 바이트 동일. */
170
+ readonly note?: string;
168
171
  }
169
172
 
170
173
  export interface UnifiedUsageReport {
171
174
  readonly rows: readonly AccountUsageRow[];
172
- readonly accountCounts: Readonly<Record<'codex' | 'grok' | 'openrouter', number>>;
175
+ /** `zai` 는 키가 있을 때만 실린다(없으면 종전 JSON 과 같다). */
176
+ readonly accountCounts: Readonly<Record<'codex' | 'grok' | 'openrouter', number>> & { readonly zai?: number };
177
+ /** GLM Coding Plan 상세(창 · 우리 적산 · 잔액 사용 · 마지막 한도 코드) — 키가 있을 때만. */
178
+ readonly zaiCodingPlan?: import('../llm/zai-coding-plan.js').ZaiUsageView;
173
179
  }
174
180
 
175
181
  export interface TurnSummary {
@@ -9,6 +9,7 @@ import { createCodexFetcher, type CodexFetcherOpts } from './fetchers/codex.js';
9
9
  import {
10
10
  fetchGrokUsage,
11
11
  GROK_SUBSCRIPTION_AXIS,
12
+ grokUsageToSubscriptionAxis,
12
13
  grokUsageToCreditAxis,
13
14
  type GrokUsageResult,
14
15
  } from '../grok/usage.js';
@@ -20,6 +21,10 @@ import { envLiteral } from '../platform/env-literal.js';
20
21
  import { describeResetCreditExpiry } from './codex-reset-credit-state.js';
21
22
  import { visibleWidth } from '../tui.js';
22
23
  import { listCodexResetCredits, type CodexResetCreditsResult } from './codex-reset-credits.js';
24
+ import {
25
+ buildZaiUsageView, fetchZaiQuota, getZaiApiKey, readZaiLedger,
26
+ type ZaiLedgerRecord, type ZaiQuotaResult, type ZaiUsageView,
27
+ } from '../llm/zai-coding-plan.js';
23
28
  import type {
24
29
  AccountUsageRow,
25
30
  CreditAxis,
@@ -50,24 +55,93 @@ export interface UnifiedUsageDeps {
50
55
  /** 결정 2026-09-23 — OpenRouter 선불 크레딧 조회 seam. 없으면 실키로 `/api/v1/credits`. */
51
56
  readonly openRouterKey?: () => string | undefined;
52
57
  readonly fetchOpenRouterImpl?: typeof fetch;
58
+ /** ZAI-CODING-PLAN-PROVIDER seam — 키 «있음» 판정(값은 안 쓴다) · 공식 조회 · 원장. */
59
+ readonly zaiKey?: () => string | undefined;
60
+ readonly fetchZaiQuota?: () => Promise<ZaiQuotaResult>;
61
+ readonly readZaiLedger?: () => ZaiLedgerRecord[];
53
62
  }
54
63
 
55
64
  export async function collectUnifiedUsage(deps: UnifiedUsageDeps = {}): Promise<UnifiedUsageReport> {
56
- const [codexRows, grokRows, openRouterRows] = await Promise.all([
65
+ const [codexRows, grokRows, openRouterRows, zai] = await Promise.all([
57
66
  collectCodexRows(deps),
58
67
  collectGrokRows(deps),
59
68
  collectOpenRouterRows(deps),
69
+ collectZaiRows(deps),
60
70
  ]);
61
71
  return {
62
- rows: [...codexRows, ...grokRows, ...openRouterRows],
72
+ rows: [...codexRows, ...grokRows, ...openRouterRows, ...(zai?.rows ?? [])],
63
73
  accountCounts: {
64
74
  codex: codexRows.length,
65
75
  grok: grokRows.length,
66
76
  openrouter: openRouterRows.length,
77
+ // ⛔ 키가 없으면 칸 자체를 안 싣는다 — 종전 JSON·문면과 바이트 동일.
78
+ ...(zai ? { zai: 1 } : {}),
67
79
  },
80
+ ...(zai ? { zaiCodingPlan: zai.view } : {}),
68
81
  };
69
82
  }
70
83
 
84
+ /** ZAI-CODING-PLAN-PROVIDER — GLM Coding Plan 의 5h·주 창 ⊕ 잔액(extra usage) 사용.
85
+ * ⓒ 공식 조회(`/api/monitor/usage/quota/limit`)가 되면 그 값 · 안 되면 ⓐ 우리 적산 ⊕ ⓑ 429 리셋 시각으로 «추정».
86
+ * ⛔ 키가 없으면 행을 «안» 만든다. ⛔ 시험 런타임은 seam 이 없으면 «키 없음»(키 캐시는 파일이라 스크럽을 통과한다). */
87
+ async function collectZaiRows(deps: UnifiedUsageDeps): Promise<{ rows: AccountUsageRow[]; view: ZaiUsageView } | null> {
88
+ // ⛔ 시험 런타임(NODE_ENV=test)은 seam 이 없으면 «env 의 ZAI_API_KEY 만» 본다 — 키 캐시 «파일»은 env 스크럽을 통과해
89
+ // seam 을 모르는 옛 usage 시험이 실제 Z.ai 를 치게 되기 때문이다. env 로 키를 주면 test 에서도 행이 선다.
90
+ const key = (deps.zaiKey ?? (process.env.NODE_ENV === 'test' ? () => process.env.ZAI_API_KEY?.trim() || undefined : () => getZaiApiKey()))();
91
+ if (!key) return null;
92
+ const nowMs = (deps.now ?? Date.now)();
93
+ // ⛔ 조회가 «던져도» usage 전체를 죽이지 않는다 — 실패는 결과값으로 바꿔 추정 표로 간다.
94
+ const quota: ZaiQuotaResult = await (deps.fetchZaiQuota ?? (() => fetchZaiQuota()))()
95
+ .catch((error: unknown) => ({ status: 'error' as const, detail: error instanceof Error ? error.message : String(error) }));
96
+ const view = buildZaiUsageView(quota, (deps.readZaiLedger ?? (() => readZaiLedger()))(), nowMs);
97
+ const base = { provider: 'zai' as const, accountCount: 1, soleAccount: true, resetCredits: { status: 'not-applicable' } as const };
98
+ const sourceLabel = { official: '공식', next_flush_time: 'next_flush_time', estimated: '추정' } as const;
99
+ const rows: AccountUsageRow[] = view.windows.map((w) => {
100
+ const windowMs = w.window === '5h' ? 5 * 3_600_000 : 7 * 86_400_000;
101
+ const usedPercent = w.limit > 0 ? Math.round((w.used / w.limit) * 1000) / 10 : null;
102
+ const end = w.resetAt !== null && isValidEpochMs(w.resetAt) ? new Date(w.resetAt) : null;
103
+ const kind: WindowKind = w.window === '5h' ? 'session' : 'weekly';
104
+ return {
105
+ ...base,
106
+ accountName: w.window === '5h' ? 'plan-5h' : 'plan-week',
107
+ credits: {
108
+ status: 'ok',
109
+ usedPercent,
110
+ periodType: kind,
111
+ periodStart: end ? new Date(end.getTime() - windowMs).toISOString() : null,
112
+ periodEnd: end?.toISOString() ?? null,
113
+ monthlyLimit: null,
114
+ used: w.used,
115
+ onDemandCap: null,
116
+ onDemandUsed: null,
117
+ prepaidBalance: null,
118
+ balance: null,
119
+ hasCredits: null,
120
+ unlimited: false,
121
+ },
122
+ // 리셋 시각을 모르면 «available» 로 NaN 을 흘리지 않는다.
123
+ subscription: usedPercent === null || !end
124
+ ? { status: 'unavailable', reason: 'query-does-not-supply' }
125
+ : { status: 'available', remainingPercent: Math.round((100 - usedPercent) * 10) / 10, resetsAt: end.getTime(), windowKind: kind },
126
+ // ⓐ 우리 적산을 공식 값 옆에 같이 둔다 — 공식은 정수 크레딧으로 내려 작은 호출이 0 으로 보인다(10-11 실측).
127
+ note: `${w.used}/${w.limit} 크레딧 · 출처=${sourceLabel[w.source]}${w.resetAt === null ? ' · 리셋 시각 모름' : ''}`
128
+ + (w.source === 'official' ? ` · elanous 적산 ${w.window === '5h' ? view.local.fiveHour.used : view.local.weekly.used}` : ''),
129
+ };
130
+ });
131
+ rows.push({
132
+ ...base,
133
+ accountName: 'extra-usage',
134
+ credits: {
135
+ status: 'ok', usedPercent: null, periodType: 'extra-usage', periodStart: null, periodEnd: null,
136
+ monthlyLimit: null, used: view.balanceCredits, onDemandCap: null, onDemandUsed: null,
137
+ prepaidBalance: null, balance: null, hasCredits: null, unlimited: false,
138
+ },
139
+ subscription: { status: 'unavailable', reason: 'not-a-subscription' },
140
+ note: `잔액 사용 ${view.balanceCredits} 크레딧(추정 · 최근 7일)${view.lastLimit ? ` · 마지막 한도 코드 ${view.lastLimit.code}` : ''}`,
141
+ });
142
+ return { rows, view };
143
+ }
144
+
71
145
  async function collectCodexRows(deps: UnifiedUsageDeps): Promise<AccountUsageRow[]> {
72
146
  const listed = (deps.listCodexAccounts ?? listCodexAccountsInStore)();
73
147
  const accounts = listed;
@@ -138,7 +212,11 @@ async function collectGrokRows(deps: UnifiedUsageDeps): Promise<AccountUsageRow[
138
212
  resetCredits: { status: 'not-applicable' } as const,
139
213
  };
140
214
  if (result.status === 'ok') {
141
- return [{ ...base, credits: grokUsageToCreditAxis(result.usage) }];
215
+ return [{
216
+ ...base,
217
+ subscription: grokUsageToSubscriptionAxis(result.usage),
218
+ credits: grokUsageToCreditAxis(result.usage),
219
+ }];
142
220
  }
143
221
  if (result.status === 'no-subscription') {
144
222
  return [{ ...base, credits: { status: 'no-subscription' } }];
@@ -284,7 +362,7 @@ export function formatUsageCompact(report: UnifiedUsageReport, options: { width:
284
362
  const reset = subscription.status === 'available' ? subscription.resetsAt
285
363
  : credits.status === 'ok' && credits.periodEnd ? Date.parse(credits.periodEnd) : Number.NaN;
286
364
  const periodLabel = period ? `${formatCompactPeriod(period)} ` : '';
287
- const when = period === 'prepaid-usd' ? '' : !isValidEpochMs(reset)
365
+ const when = period === 'prepaid-usd' || period === 'extra-usage' ? '' : !isValidEpochMs(reset)
288
366
  ? ` · ${periodLabel}시각 모름`
289
367
  : reset <= options.nowMs
290
368
  ? ` · ${periodLabel}재조회 필요`
@@ -351,6 +429,8 @@ export function formatUnifiedUsage(report: UnifiedUsageReport): string {
351
429
  const count = report.accountCounts[provider];
352
430
  lines.push(` ${provider} accounts=${count}${count === 1 ? ' (하나뿐)' : count === 0 ? ' (없음)' : ''}`);
353
431
  }
432
+ // ⛔ 키가 없으면 줄을 안 낸다 — 종전 문면과 바이트 동일.
433
+ if (report.accountCounts.zai !== undefined) lines.push(` zai accounts=${report.accountCounts.zai} (GLM Coding Plan)`);
354
434
  lines.push(' provider account accounts credits.usedPercent credits.window window-resets-in reset-credit-expiry subscription');
355
435
  for (const row of report.rows) {
356
436
  lines.push(formatRow(row));
@@ -364,7 +444,7 @@ function formatRow(row: AccountUsageRow): string {
364
444
  const resetsIn = formatWindowResetsIn(row.credits);
365
445
  const resetCredits = formatResetCreditExpiryCell(row.resetCredits);
366
446
  const subscription = formatSubscriptionCell(row.subscription);
367
- return ` ${row.provider.padEnd(10)} ${row.accountName.padEnd(11)} ${String(row.accountCount).padEnd(8)} ${credits.padEnd(20)} ${window.padEnd(26)} ${resetsIn.padEnd(22)} ${resetCredits.padEnd(36)} ${subscription}`;
447
+ return ` ${row.provider.padEnd(10)} ${row.accountName.padEnd(11)} ${String(row.accountCount).padEnd(8)} ${credits.padEnd(20)} ${window.padEnd(26)} ${resetsIn.padEnd(22)} ${resetCredits.padEnd(36)} ${subscription}${row.note ? ` — ${row.note}` : ''}`;
368
448
  }
369
449
 
370
450
  function formatResetCreditExpiryCell(expiry: ResetCreditExpiryAxis): string {
@@ -381,6 +461,7 @@ function formatResetCreditExpiryCell(expiry: ResetCreditExpiryAxis): string {
381
461
 
382
462
  function formatCreditsCell(credits: CreditAxis): string {
383
463
  if (credits.status !== 'ok') return credits.status;
464
+ if (credits.periodType === 'extra-usage') return `${credits.used ?? 0} cr`;
384
465
  if (credits.usedPercent === null) return 'unknown';
385
466
  // 선불 달러 — 퍼센트만으로는 «얼마 남았나»를 못 읽는다.
386
467
  if (credits.periodType === 'prepaid-usd' && credits.balance !== null) return `${credits.usedPercent}% ($${credits.balance} left)`;
@@ -390,6 +471,7 @@ function formatCreditsCell(credits: CreditAxis): string {
390
471
  function formatCreditsWindow(credits: CreditAxis): string {
391
472
  if (credits.status !== 'ok') return '—';
392
473
  if (credits.periodType === 'prepaid-usd') return 'prepaid (창 없음)';
474
+ if (credits.periodType === 'extra-usage') return 'extra-usage (잔액)';
393
475
  const type = credits.periodType ?? '—';
394
476
  if (credits.periodStart && credits.periodEnd) {
395
477
  return `${type} ${credits.periodStart.slice(0, 10)}~${credits.periodEnd.slice(0, 10)}`;
@@ -407,7 +489,7 @@ function formatCreditsWindow(credits: CreditAxis): string {
407
489
  */
408
490
  export function formatWindowResetsIn(credits: CreditAxis, nowMs: number = Date.now()): string {
409
491
  if (credits.status !== 'ok') return '—';
410
- if (credits.periodType === 'prepaid-usd') return 'n/a (리셋 없음)';
492
+ if (credits.periodType === 'prepaid-usd' || credits.periodType === 'extra-usage') return 'n/a (리셋 없음)';
411
493
  const end = credits.periodEnd ? Date.parse(credits.periodEnd) : Number.NaN;
412
494
  if (!Number.isFinite(end)) return 'unknown';
413
495
  const remainMs = end - nowMs;
@@ -0,0 +1,34 @@
1
+ /** BENCH-ISOLATED-ARMS ④ — `elanous bench ls <id>`: 벤치 원장(`<config dir>/bench/<id>.jsonl`)을 읽는 한 자리. */
2
+ import type { Command } from 'commander';
3
+ import { benchLedgerPath, parseBenchSpec, readBenchLedger, renderBenchLs } from '../bench/bench.js';
4
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
5
+
6
+ export interface BenchCliDeps {
7
+ readonly configDir?: () => string;
8
+ readonly write?: (line: string) => void;
9
+ }
10
+
11
+ export function registerBenchCommands(program: Command, deps: BenchCliDeps = {}): Command {
12
+ const bench = program.command('bench').description('벤치 팔(같은 골 · 모델만 다른 발사) 원장 — harness ask/say --bench 가 쓴다');
13
+ bench.command('ls <id>')
14
+ .description('벤치 원장 줄(bench id · base sha · 이미지 pin · 자식 모델 · 라운드)을 읽는다')
15
+ .option('--json', 'JSON 배열로')
16
+ .action((id: string, opts: { json?: boolean }) => {
17
+ const write = deps.write ?? ((line: string) => console.log(line));
18
+ let benchId: string;
19
+ try { benchId = parseBenchSpec(id).id; }
20
+ catch (error) {
21
+ console.error(`❌ ${error instanceof Error ? error.message : String(error)}`);
22
+ process.exitCode = 2;
23
+ return;
24
+ }
25
+ const configDir = (deps.configDir ?? getElanousConfigDir)();
26
+ const rows = readBenchLedger(benchId, configDir);
27
+ if (opts.json) write(JSON.stringify(rows));
28
+ else {
29
+ write(renderBenchLs(benchId, rows));
30
+ if (rows.length === 0) write(`원장 경로: ${benchLedgerPath(benchId, configDir)}`);
31
+ }
32
+ });
33
+ return bench;
34
+ }
@@ -27,7 +27,32 @@ export function registerGuardianCommands(program: Command): void {
27
27
  const cfg = getUserConfig();
28
28
  // Live activation (and crontab retirement) is a separate operational change.
29
29
  if (cfg.guardian?.mode === 'live') throw new Error('guardian live activation is outside GUARD-ONE-a');
30
- const result = await guardianTick(new Date(), plan, { config: cfg });
30
+ // The shadow observation is for the tick's entry time, however long the GC's evidence collection takes.
31
+ const tickAt = new Date();
32
+ // Local orphan GC is separate from the guardian's shadow-only schedule execution.
33
+ const orphanGc = cfg.guardian?.orphanGc ?? 'shadow';
34
+ try {
35
+ // off never loads the collector, so no ps snapshot is taken.
36
+ if (orphanGc !== 'off') {
37
+ // Loading the collector is inside the guard too: a GC that cannot even load only observes.
38
+ const { runMachineHeartbeat } = await import('../ops/machine-heartbeat.js');
39
+ await runMachineHeartbeat(plan.jobs.filter((job): job is typeof job & { command: string; cron: string } =>
40
+ job.source === 'crontab' && job.run_via === 'crontab' && !!job.enabled
41
+ && typeof job.command === 'string' && typeof job.cron === 'string')
42
+ .map(job => ({ command: job.command, cron: job.cron })), { timeZone: plan.timeZone, mode: orphanGc });
43
+ } else {
44
+ // Still one decision line per tick, so «switched off» is distinguishable from «tick never ran».
45
+ const { debug } = await import('../debug/log.js');
46
+ debug.log('ops.orphan-cli', 'decision', { mode: 'off', candidates: 0, collected: 0, alreadyEvidenced: 0, killed: 0 });
47
+ }
48
+ } catch (error) {
49
+ // The logger import is inside the guard as well: if it is the failing module, the shadow tick still runs.
50
+ try {
51
+ const { debug } = await import('../debug/log.js');
52
+ debug.log('ops.orphan-cli', 'observation-failed', { error: String(error) });
53
+ } catch { /* guardian shadow still runs */ }
54
+ }
55
+ const result = await guardianTick(tickAt, plan, { config: cfg });
31
56
  await writeStdoutJson(JSON.stringify(result, null, 2) + '\n');
32
57
  } finally { db.close(); }
33
58
  });
@@ -0,0 +1,101 @@
1
+ import type { Command } from 'commander';
2
+ import { debug } from '../debug/log.js';
3
+ import {
4
+ appendImageLedgerEvent, baselinePinEvent, foldImageLedger, imageLedgerPath, readImageLedger, readRegistryTags,
5
+ type ImageLedgerRow, type RegistryRead,
6
+ } from '../images/image-ledger.js';
7
+ import { isPodImageCommit, podImageRegistry } from '../task-orchestrator/surfaces/pod-image-source.js';
8
+
9
+ const SSH_HOST = /^[A-Za-z0-9][A-Za-z0-9._@-]*$/u;
10
+
11
+ export interface ImageLsDeps {
12
+ readonly ledgerPath?: string;
13
+ readonly readRegistry?: (opts: { port: string; sshHost?: string }) => RegistryRead;
14
+ readonly env?: NodeJS.ProcessEnv;
15
+ readonly out?: (line: string) => void;
16
+ }
17
+
18
+ export interface ImageLsResult {
19
+ readonly ledger: { readonly path: string; readonly exists: boolean; readonly events: number; readonly unreadableLines: number };
20
+ readonly registry: { readonly read: boolean; readonly where: string; readonly tags: number | null; readonly reason?: string };
21
+ readonly rows: readonly ImageLedgerRow[];
22
+ readonly counts: { readonly baseline: number; readonly transient: number | null; readonly ledgered: number };
23
+ }
24
+
25
+ /** `image ls` — read only: ledger rows ⊕ registry tags without a ledger row (= transient). */
26
+ export function imageLs(opts: { json?: boolean; ssh?: string; registry?: boolean }, deps: ImageLsDeps = {}): ImageLsResult {
27
+ const out = deps.out ?? ((line: string) => console.log(line));
28
+ if (opts.ssh !== undefined && !SSH_HOST.test(opts.ssh)) throw new Error(`invalid --ssh host: ${opts.ssh}`);
29
+ const ledger = readImageLedger(deps.ledgerPath ?? imageLedgerPath());
30
+ const registryAddr = podImageRegistry(deps.env ?? process.env);
31
+ const port = registryAddr.slice(registryAddr.lastIndexOf(':') + 1);
32
+ const where = `${opts.ssh ?? 'this machine'} localhost:${port}`;
33
+ let registry: RegistryRead | null = null;
34
+ if (opts.registry !== false) {
35
+ registry = /^[1-9][0-9]{0,4}$/u.test(port) && Number(port) <= 65535
36
+ ? (deps.readRegistry ?? readRegistryTags)({ port, ...(opts.ssh ? { sshHost: opts.ssh } : {}) })
37
+ : { ok: false, reason: `invalid registry port: ${registryAddr}` };
38
+ }
39
+ const tags = registry?.ok ? registry.tags : null;
40
+ const rows = foldImageLedger(ledger.events, tags);
41
+ const result: ImageLsResult = {
42
+ ledger: { path: ledger.path, exists: ledger.exists, events: ledger.events.length, unreadableLines: ledger.unreadableLines },
43
+ registry: registry === null
44
+ ? { read: false, where, tags: null, reason: 'skipped (--no-registry)' }
45
+ : registry.ok ? { read: true, where, tags: registry.tags.length } : { read: false, where, tags: null, reason: registry.reason },
46
+ rows,
47
+ counts: {
48
+ baseline: rows.filter((r) => r.current).length,
49
+ transient: tags === null ? null : rows.filter((r) => r.unledgered).length,
50
+ ledgered: rows.filter((r) => !r.unledgered).length,
51
+ },
52
+ };
53
+ try { debug.log('image.ledger', 'ls', { ledgerEvents: result.ledger.events, unreadableLines: result.ledger.unreadableLines, registryRead: result.registry.read, registryTags: result.registry.tags, transient: result.counts.transient }); } catch { /* log sink optional */ }
54
+ if (opts.json) { out(JSON.stringify(result, null, 2)); return result; }
55
+ out(`image ledger · ${ledger.path}${ledger.exists ? '' : ' (no ledger yet)'} · events ${ledger.events.length}${ledger.unreadableLines ? ` · ⚠️ unreadable lines ${ledger.unreadableLines}` : ''}`);
56
+ out(result.registry.read
57
+ ? `registry · ${where} · tags ${result.registry.tags}`
58
+ : `registry · ${where} · ⚠️ not read — ${result.registry.reason} (transient count unknown, not 0)`);
59
+ for (const r of rows.filter((x) => !x.unledgered)) {
60
+ const reg = r.inRegistry === null ? '?' : r.inRegistry ? 'reg' : '⚠️ not-in-registry';
61
+ out(` ${r.tag} ${r.role}${r.role === 'baseline' ? (r.current ? '*' : ' (previous)') : ''} keep=${r.keep} ${r.origin ? `${r.origin.reason}@${r.origin.host}` : '-'} ${r.lastTs} ${reg}`);
62
+ }
63
+ const transient = rows.filter((x) => x.unledgered);
64
+ if (transient.length > 0) out(` transient (registry tags without a ledger row) · ${transient.length} · ${transient.slice(0, 8).map((r) => r.tag).join(' ')}${transient.length > 8 ? ' …' : ''}`);
65
+ out(`baseline ${result.counts.baseline} · ledgered ${result.counts.ledgered} · transient ${result.counts.transient ?? 'unknown'}`);
66
+ return result;
67
+ }
68
+
69
+ /**
70
+ * Recording site: `config set harness.imageCommit <commit>` (the rail pins the baseline through it).
71
+ * Appends a `baseline ⊕ pin` event (none when that tag is already the current baseline). A failed ledger write never fails the config write; it warns
72
+ * and leaves an observation.
73
+ */
74
+ export function recordImagePinFromConfigSet(path: string, value: unknown, deps: { ledgerPath?: string; warn?: (msg: string) => void; now?: () => Date; host?: string } = {}): 'recorded' | 'unchanged' | 'skipped' | 'failed' {
75
+ if (path !== 'harness.imageCommit' || !isPodImageCommit(value)) return 'skipped';
76
+ const ledgerPath = deps.ledgerPath ?? imageLedgerPath();
77
+ try {
78
+ // The rail re-pins the same commit on quiet cycles — one event per baseline change, not per call.
79
+ const current = foldImageLedger(readImageLedger(ledgerPath).events, null).find((r) => r.current);
80
+ if (current && current.tag === value.trim().toLowerCase().slice(0, 12)) return 'unchanged';
81
+ const event = baselinePinEvent(value, { ...(deps.now ? { now: deps.now } : {}), ...(deps.host ? { host: deps.host } : {}) });
82
+ appendImageLedgerEvent(event, ledgerPath);
83
+ try { debug.log('image.ledger', 'baseline-pinned', { tag: event.tag, commit: event.commit, reason: event.origin.reason, host: event.origin.host, ledgerPath }); } catch { /* */ }
84
+ return 'recorded';
85
+ } catch (e) {
86
+ const message = e instanceof Error ? e.message : String(e);
87
+ try { debug.log('image.ledger', 'baseline-pin-record-failed', { error: message, ledgerPath }); } catch { /* */ }
88
+ (deps.warn ?? ((m: string) => console.error(m)))(`⚠️ image ledger: baseline pin was set in config but not recorded in ${ledgerPath} — ${message}`);
89
+ return 'failed';
90
+ }
91
+ }
92
+
93
+ export function registerImageCommands(program: Command): void {
94
+ const image = program.command('image').description('하니스 이미지 원장 — 신분 표지(역할·보존·출신) · 원장 없는 레지스트리 태그 = transient');
95
+ image.command('ls')
96
+ .description('원장 행 ⊕ 레지스트리 태그(원장에 없으면 transient) — 읽기 전용')
97
+ .option('--json', 'JSON 출력')
98
+ .option('--ssh <host>', '레지스트리가 있는 노드(그 노드의 localhost 포트로 조회)')
99
+ .option('--no-registry', '레지스트리를 읽지 않는다(원장만)')
100
+ .action((opts: { json?: boolean; ssh?: string; registry?: boolean }) => { imageLs(opts); });
101
+ }
@@ -121,7 +121,9 @@ export const KNOWN_LOG_EVENT_NAMES: ReadonlySet<string> = new Set([
121
121
  "background-drain",
122
122
  "background-finish",
123
123
  "barge.in",
124
+ "base-ensured",
124
125
  "base-preexisting",
126
+ "base-refused",
125
127
  "base-selection",
126
128
  "base.default-branch",
127
129
  "base.remote-unreachable",
@@ -736,6 +738,7 @@ export const KNOWN_LOG_EVENT_NAMES: ReadonlySet<string> = new Set([
736
738
  "launch-overrides",
737
739
  "layered-load",
738
740
  "ledger",
741
+ "ledger-unwritten",
739
742
  "ledger-write-failed",
740
743
  "legacy-codex-invoked",
741
744
  "legacy-finalize",
@@ -773,6 +776,7 @@ export const KNOWN_LOG_EVENT_NAMES: ReadonlySet<string> = new Set([
773
776
  "luna-failed",
774
777
  "luna-no-allowlist",
775
778
  "made-public",
779
+ "main-sync-skipped",
776
780
  "main-tree-reject",
777
781
  "malformed-tail",
778
782
  "manifest-frame-fail",
package/src/cli/pr-cli.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, statSync } from 'node:fs';
2
- import { basename, join, relative, resolve } from 'node:path';
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { basename, dirname, join, relative, resolve } from 'node:path';
3
3
  import { createInterface } from 'node:readline';
4
4
  import type { Command } from 'commander';
5
5
  import {
@@ -57,7 +57,8 @@ import { branchLineageSlug, findSiblingPrs } from './pr-lineage.js';
57
57
  import { parseReleaseNoteSection } from '../release-loop/release-note.js';
58
58
  import { landingFreezeMessage } from '../release-loop/landing-freeze.js';
59
59
  import { admitLandingMerge } from '../self-implement/frozen-merges.js';
60
- import { verifyOverlapEvidence } from './pr-land-overlap-evidence.js';
60
+ import { verifyAncestorAutoPass, verifyOverlapEvidence } from './pr-land-overlap-evidence.js';
61
+ import { taskAgentStatePath } from '../task-agent/task-hand.js';
61
62
  import { enqueueL8ShadowQueue, runL8ShadowQueue, shadowPrNumber, statusL8ShadowQueue, type L8ShadowQueueDeps } from '../self-implement/l8-merge-queue.js';
62
63
 
63
64
  export { branchLineageSlug, findSiblingPrs };
@@ -107,6 +108,8 @@ export interface PrLandDeps {
107
108
  resolveBase?: (run: CmdRunner, cwd: string, explicit?: string) => string | undefined;
108
109
  currentBranch?: (cwd: string) => string | undefined;
109
110
  readFile?: (path: string) => string;
111
+ /** 증거 파일 없는 already-ancestor 자동 판정이 리뷰 결과를 찾는 자리(기본 `<TA 상태 디렉터리>/review-results`). */
112
+ reviewResultsDir?: string;
110
113
  listUnfinishedRuns?: () => FederatedUnfinishedRunLedgerQuery | readonly FederatedUnfinishedRunLedgerEntry[];
111
114
  queryRunningRuns?: () => RunningRunsResult;
112
115
  listOpenPrs?: () => readonly { number: number; headRefName: string }[] | null;
@@ -1038,6 +1041,8 @@ const SOURCE_SPLIT_PR_LIST_LIMIT = 200;
1038
1041
  */
1039
1042
  const MERGED_PR_LOOKUP_RUNAWAY_CAP = 1000;
1040
1043
  const SOURCE_LOOKUP_FAILED_LINE = '⚠ source: 브랜치 조회에 실패해 갈래를 내지 못했습니다.';
1044
+ /** BENCH-ISOLATED-ARMS — 벤치 팔 PR 라벨(착지 금지). */
1045
+ export const BENCH_LABEL = 'elanous:bench';
1041
1046
 
1042
1047
  type MergedPrHeadRefsLookup = {
1043
1048
  map: Map<number, string>;
@@ -1616,9 +1621,23 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1616
1621
  if (verdict.ok) {
1617
1622
  out.log(verdict.mode === 'already-ancestor'
1618
1623
  ? `✓ overlap-evidence: already-ancestor — 겹친 최근 착지 ${overlappingLandings.length}개가 전부 head ${opts.expectedHead!.slice(0, 12)} 의 조상 · review pass must-fix 0 (${verdict.reviewResult})`
1619
- : `✓ overlap-evidence: ${ref.remote}/${ref.branch} ${verdict.baseTip.slice(0, 12)} ⊂ head ${opts.expectedHead!.slice(0, 12)} · gate passed on that base · review pass must-fix 0 (${verdict.reviewResult})`);
1624
+ : verdict.mode === 'base-advanced-disjoint'
1625
+ ? `✓ overlap-evidence: base-advanced-disjoint — gate base ${verdict.gateBase.slice(0, 12)} ⊂ head ${opts.expectedHead!.slice(0, 12)} · ${ref.remote}/${ref.branch} ${verdict.baseTip.slice(0, 12)} 까지 ${verdict.windowCommits}개 커밋이 PR 파일을 안 건드린다 · review pass|warn must-fix 0 (${verdict.reviewResult})`
1626
+ : `✓ overlap-evidence: ${ref.remote}/${ref.branch} ${verdict.baseTip.slice(0, 12)} ⊂ head ${opts.expectedHead!.slice(0, 12)} · gate passed on that base · review pass must-fix 0 (${verdict.reviewResult})`);
1620
1627
  }
1621
1628
  else out.error(`✗ overlap-evidence: ${verdict.reason}`);
1629
+ } else if (pinned && overlapDecision?.outcome === 'unavailable') {
1630
+ // 증거 파일이 없을 때 — 겹친 착지가 전부 머리의 조상이고 표준 자리의 리뷰가 pass|warn ⊕ must-fix 0 이면 통과(아니면 종전 거부).
1631
+ const verdict = verifyAncestorAutoPass({
1632
+ pr: Number(opts.pr), expectedHead: opts.expectedHead!, cwd, run, readFile, overlappingLandings,
1633
+ reviewResultsDir: deps.reviewResultsDir ?? join(dirname(taskAgentStatePath()), 'review-results'),
1634
+ listDir: (dir) => readdirSync(dir),
1635
+ });
1636
+ overlapEvidenceAccepted = verdict.ok;
1637
+ if (verdict.ok) {
1638
+ out.log(`✓ overlap-evidence: already-ancestor-auto — 겹친 최근 착지 ${overlappingLandings.length}개가 전부 head ${opts.expectedHead!.slice(0, 12)} 의 조상 · review pass|warn must-fix 0 (${verdict.reviewResult})`);
1639
+ }
1640
+ else out.error(`✗ ancestor-auto: ${verdict.reason}`);
1622
1641
  }
1623
1642
  if (pinned && overlapDecision && overlapDecision.outcome !== 'user-merge' && !overlapEvidenceAccepted) {
1624
1643
  out.error('✗ expected-head: overlap merge approval unavailable for the reviewed head');
@@ -1695,6 +1714,37 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1695
1714
  }
1696
1715
  }
1697
1716
 
1717
+ // LAND-BASE-GUARD-ORDER — 재사용할 PR 의 base·라벨은 upsert «전에» 본다(10-11 #26150: upsert 가 base 를 main 으로
1718
+ // 바꾼 뒤에 읽어 #26117 base-guard 가 무력 · 거부돼도 PR 이 바뀌어 남았다). 거부는 PR 을 건드리지 않는다.
1719
+ // LAND-BASE-MAIN-GUARD — 재사용한 PR 의 base 가 기본 가지가 아니면 병합 «전에» 거부한다(10-11 #26022 가 bench/base-night-* 로 병합됨).
1720
+ // 명시 --base 를 준 경우만 통과한다. 못 읽은 base 는 종전 흐름대로 둔다(병합 뒤 되읽기).
1721
+ // BENCH-ISOLATED-ARMS — `elanous:bench` 라벨 PR 은 벤치 팔 산출이라 착지하지 않는다(승자는 주인이 main 위로 따로 수확).
1722
+ // 명시 --base 로도 통과하지 않는다. 못 읽은 라벨은 base-guard 와 같이 종전 흐름대로 둔다.
1723
+ const refuseReusedPr = (url: string): number | undefined => {
1724
+ const reusedNumber = /\/(\d+)$/.exec(url)?.[1] ?? url;
1725
+ if (!opts.base?.trim()) {
1726
+ const reusedBase = run('gh', ['pr', 'view', url, '--json', 'baseRefName', '--jq', '.baseRefName'], { cwd });
1727
+ const reusedBaseName = reusedBase.ok ? reusedBase.out.trim() : '';
1728
+ if (reusedBaseName && reusedBaseName !== baseBranchName) {
1729
+ record('base-guard', false, { url, prBase: reusedBaseName, expected: baseBranchName });
1730
+ out.error(`✗ base-guard: base=${reusedBaseName} ≠ ${baseBranchName} · gh pr edit ${reusedNumber} --base ${baseBranchName} 뒤 다시`);
1731
+ return 1;
1732
+ }
1733
+ }
1734
+ const reusedLabels = run('gh', ['pr', 'view', url, '--json', 'labels', '--jq', '.labels[].name'], { cwd });
1735
+ if (reusedLabels.ok && reusedLabels.out.split('\n').some((name) => name.trim() === BENCH_LABEL)) {
1736
+ record('bench-guard', false, { url, label: BENCH_LABEL });
1737
+ out.error(`✗ bench-guard: #${reusedNumber} 은 ${BENCH_LABEL} 벤치 팔 PR 이라 착지하지 않는다 · 승자 diff 를 main 위 새 가지로 옮겨 착지`);
1738
+ return 1;
1739
+ }
1740
+ return undefined;
1741
+ };
1742
+ const guardedBeforeUpsert = !hold && !!existing.url;
1743
+ if (guardedBeforeUpsert) {
1744
+ const refused = refuseReusedPr(existing.url!);
1745
+ if (refused !== undefined) return refused;
1746
+ }
1747
+
1698
1748
  // A pinned, already-reviewed commit must not go through upsertPr (git add/commit/force-push/PR edit).
1699
1749
  const upsert = pinned
1700
1750
  ? existing.status === 'ok OUTPUT' && existing.url === pinnedUrl
@@ -1703,7 +1753,8 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1703
1753
  : manager.upsertPr({
1704
1754
  branch,
1705
1755
  worktreePath: cwd,
1706
- base,
1756
+ // 기존 PR 재사용이면 명시 --base 일 때만 넘긴다 — 추론한 base 는 PR 을 main 으로 재타기팅한다(#26150).
1757
+ ...(opts.base?.trim() || !existing.url ? { base } : {}),
1707
1758
  draft: hold,
1708
1759
  title: title ?? `Land ${branch}`,
1709
1760
  // ⛔⭐ 본문 기본값도 «내용이 있는» 쪽으로 — 종전 34자 플레이스홀더는 ***아무에게도 쓸모가 없었고***
@@ -1741,6 +1792,12 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1741
1792
  return 0;
1742
1793
  }
1743
1794
 
1795
+ // 착지 전 PR 을 못 찾았는데(조회 실패) upsert 가 기존 PR 을 재사용했다면 여기서 한 번 더 본다.
1796
+ if (upsert.reused && !guardedBeforeUpsert) {
1797
+ const refused = refuseReusedPr(upsert.url);
1798
+ if (refused !== undefined) return refused;
1799
+ }
1800
+
1744
1801
  const prNumber = Number(/\/(\d+)$/.exec(upsert.url)?.[1]);
1745
1802
  const landedHead = run('git', ['rev-parse', 'HEAD'], { cwd });
1746
1803
  const headCommit = landedHead.ok && /^[a-f0-9]{40}$/i.test(landedHead.out.trim()) ? landedHead.out.trim() : undefined;