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
@@ -155,11 +155,16 @@ export function registerGraphCommands(program: Command, runsDeps: { root?: strin
155
155
  .description('Advance one graph run, resuming a pending decision or starting only when requested')
156
156
  .option('--start', 'Start a new run when the graph is idle')
157
157
  .option('--schedule', 'Subscribe graph YAML triggers.schedule to the workflow scheduler and tick on each fire')
158
+ .option('--webhook', 'Subscribe graph YAML triggers.webhook to the workflow webhook router and tick on each request')
159
+ .option('--port <n>', 'Port for --webhook to listen on (default: any free port)')
158
160
  .option('--input <json>', 'JSON object passed to a newly started run')
159
161
  .option('--json', 'Print the tick result as JSON')
160
- .action(async (file: string, opts: { start?: boolean; schedule?: boolean; input?: string; json?: boolean }) => {
162
+ .action(async (file: string, opts: { start?: boolean; schedule?: boolean; webhook?: boolean; port?: string; input?: string; json?: boolean }) => {
161
163
  try {
162
164
  if (opts.schedule && (opts.start || opts.input !== undefined || opts.json)) throw new Error('--schedule cannot be combined with --start, --input or --json');
165
+ if (opts.webhook && (opts.schedule || opts.start || opts.input !== undefined || opts.json)) throw new Error('--webhook cannot be combined with --schedule, --start, --input or --json');
166
+ if (opts.port !== undefined && !opts.webhook) throw new Error('--port requires --webhook');
167
+ if (opts.port !== undefined && (!/^\d+$/.test(opts.port) || Number(opts.port) > 65535)) throw new Error('--port must be an integer from 0 to 65535');
163
168
  if (opts.input !== undefined && !opts.start) throw new Error('--input requires --start');
164
169
  let input: Record<string, unknown> | undefined;
165
170
  if (opts.input !== undefined) {
@@ -171,6 +176,42 @@ export function registerGraphCommands(program: Command, runsDeps: { root?: strin
171
176
  }
172
177
  await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('graph');
173
178
  const { graphTick } = await import('./graph-tick.js');
179
+ if (opts.webhook) {
180
+ const { createWebhookSource } = await import('../workflow-runtime/triggers/webhook-source.js');
181
+ const graphFile = resolve(file);
182
+ const source = createWebhookSource();
183
+ // The CLI tick callback is the graph counterpart of the workflow webhook router's onEmit.
184
+ source.subscribeGraph(graphFile, async () => {
185
+ try {
186
+ const { notifyGraphEvents } = await import('./graph-notify.js');
187
+ const result = await graphTick(graphFile, { startIfIdle: true, deps: { notify: async () => { await notifyGraphEvents(); } } });
188
+ if (result.status === 'refused') return { ok: false, error: `refused: ${result.reason}` };
189
+ if (result.status === 'failed') return { ok: false, runId: result.runId, error: 'failed' };
190
+ return result.runId ? { ok: true, runId: result.runId } : { ok: false, error: `no run (${result.action})` };
191
+ } catch (error) {
192
+ debug.log('graph.runner', 'webhook-tick-failed', { file: graphFile, error: error instanceof Error ? error.message : String(error) });
193
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
194
+ }
195
+ });
196
+ if (!source.subscriptions().length) throw new Error('graph triggers.webhook is missing');
197
+ await source.start();
198
+ const server = Bun.serve({
199
+ hostname: '127.0.0.1',
200
+ port: opts.port === undefined ? 0 : Number(opts.port),
201
+ fetch: async (request) => {
202
+ const response = await source.dispatch({
203
+ method: request.method, path: new URL(request.url).pathname,
204
+ headers: Object.fromEntries(request.headers), body: await request.text(),
205
+ });
206
+ return response ? new Response(response.body, { status: response.status, headers: response.headers }) : new Response('webhook source not started', { status: 503 });
207
+ },
208
+ });
209
+ console.log(`graph webhook: ${source.subscriptions().length} active on http://127.0.0.1:${server.port}`);
210
+ const shutdown = () => { void server.stop(true).then(() => source.stop()).then(() => { process.exitCode = 0; }); };
211
+ process.once('SIGINT', shutdown);
212
+ process.once('SIGTERM', shutdown);
213
+ return;
214
+ }
174
215
  if (opts.schedule) {
175
216
  const { createScheduleSource } = await import('../workflow-runtime/triggers/schedule-source.js');
176
217
  const graphFile = resolve(file);
@@ -2,8 +2,9 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { execFile } from 'node:child_process';
3
3
  import { existsSync, linkSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
4
4
  import { dirname, join, resolve, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
6
- import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
7
+ import { getElanousConfigDir, getElanousConfigDirOverride } from '../elanous-config-dir.js';
7
8
  import { debug } from '../debug/log.js';
8
9
  import { observeModelInputTokens } from '../harness/model-input-observation.js';
9
10
  import { publishInsideEvent } from '../nexus/api/inside-events.js';
@@ -22,7 +23,7 @@ import { psProcessStartMs, START_TOLERANCE_MS } from '../harness/harness-stop.js
22
23
  import { executeBashNode } from '../workflow-runtime/nodes/bash.js';
23
24
  import { interpolate } from '../workflow-runtime/variables.js';
24
25
  import type { BashNode, NodeExecContext, WorkflowDeps } from '../workflow-runtime/types.js';
25
- import { releaseResumeAcceptedRegressions, releaseResumeWaiver, type AcceptedRegression } from '../../scripts/release-loop/resume.js';
26
+ import { releaseResumeAcceptedRegressions, releaseResumeGateExecutor, releaseResumeWaiver, type AcceptedRegression, type GateExecutorOverride } from '../../scripts/release-loop/resume.js';
26
27
 
27
28
  type BashRun = WorkflowDeps['runBash'];
28
29
 
@@ -51,10 +52,14 @@ export interface GraphRunState {
51
52
  graphSnapshot?: { graphSha: string; recipesSha: string };
52
53
  graphPath?: string;
53
54
  variant?: { overlayId: string; goal: string; plan: GraphVariantPlan };
55
+ parentRunId?: string;
56
+ unitCalls?: Array<{ node: string; unit: string; version: number; hash: string; childRunId: string; exit: string; steps: number }>;
54
57
  growth?: GraphGrowth[];
55
58
  growthRejections?: Array<{ from: string; outcome: string; reason: string }>;
56
59
  growthPark?: { from: string; outcome: string; reason: string; decisionId: string; decisionRef: string; growth: GraphGrowth; command?: GrowthCommand; approval?: string };
57
60
  resume?: { from: string; at: string; previousStatus: GraphRunState['status']; graph?: 'snapshot' | 'current' };
61
+ /** GATE-HOST-EXECUTOR — every explicit input change a resume made (what was set and what it replaced), oldest first. */
62
+ inputOverrides?: GateExecutorOverride[];
58
63
  executed: number;
59
64
  dryRun: boolean;
60
65
  statePath: string;
@@ -74,7 +79,11 @@ export interface GraphRunOptions {
74
79
  releaseWaiver?: { nodeId: string; reason: string; requestedRegressions: AcceptedRegression[]; acceptedRegressions: AcceptedRegression[] };
75
80
  /** Release-only: add accepted issues independently of a waiver at a restart boundary. */
76
81
  releaseAcceptedRegressions?: { requested: AcceptedRegression[]; acceptedRegressions: AcceptedRegression[] };
82
+ /** Release-only (GATE-HOST-EXECUTOR): run the restarted gate on this executor — recorded in `inputOverrides`, never a waiver. */
83
+ releaseGateExecutor?: { executor: string; host?: string };
77
84
  useCurrentGraph?: boolean;
85
+ /** Internal unit invocation context, propagated only by runGraph. */
86
+ unitContext?: { parentRunId: string; ancestors: readonly string[]; remainingSteps: number };
78
87
  /**
79
88
  * RELEASE-LEDGER-UNIVERSE — stamp every cmd: child with this run's ledger root (`ELANOUS_STATE_DIR`).
80
89
  * Without it a child launched from a non-leader tree (`bun scripts/...` with cwd = that tree) re-resolves
@@ -389,6 +398,55 @@ function nextNode(edges: readonly GraphEdgeSpec[], nodeId: string, exitOutcome:
389
398
  return edge.map?.[exitOutcome] ?? edge.fallback?.[0]?.node;
390
399
  }
391
400
 
401
+ type ResolvedUnit = { path: string; template: GraphTemplateSpec; hash: string };
402
+
403
+ function resolveUnit(call: string, callerPath: string): ResolvedUnit {
404
+ const [id, requested] = call.split('@');
405
+ const repo = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
406
+ const directories = [join(dirname(callerPath), 'units'), dirname(callerPath), join(repo, 'graphs', 'units'), join(getElanousConfigDir(), 'graphs', 'units')];
407
+ for (const dir of directories) {
408
+ if (!existsSync(dir)) continue;
409
+ const matches = readdirSync(dir).filter(file => /\.ya?ml$/.test(file) && file !== 'recipes.yaml' && file !== 'recipes.yml').flatMap(file => {
410
+ const path = join(dir, file);
411
+ const source = readFileSync(path, 'utf8');
412
+ const header: unknown = parseYaml(source);
413
+ if (!header || typeof header !== 'object' || Array.isArray(header) || (header as Record<string, unknown>).graph_id !== id) return [];
414
+ const parsed = parseGraphTemplateYaml(source, path);
415
+ if (parsed.errors.length) throw new Error(parsed.errors.map(e => `${e.path}: ${e.message}`).join('\n'));
416
+ const template = parsed.template!;
417
+ return template.unit && (requested === String(template.version) || requested === `^${template.version}`)
418
+ ? [{ path, template, hash: createHash('sha256').update(source).digest('hex') }] : [];
419
+ });
420
+ if (matches.length > 1) throw new Error(`ambiguous unit: ${call}`);
421
+ if (matches.length) return matches[0]!;
422
+ }
423
+ throw new Error(`unit not found: ${call}`);
424
+ }
425
+
426
+ function validateUnitCalls(graph: GraphTemplateSpec, path: string, ancestors: readonly string[] = []): Map<string, ResolvedUnit> {
427
+ if (ancestors.includes(graph.graphId)) throw new Error(`unit cycle: ${[...ancestors, graph.graphId].join(' -> ')}`);
428
+ if (ancestors.length > 3) throw new Error('unit nesting exceeds depth 3');
429
+ const result = new Map<string, ResolvedUnit>();
430
+ for (const node of graph.nodes) {
431
+ if (node.kind !== 'subgraph' || node.call === undefined) continue;
432
+ const unit = resolveUnit(node.call!, path);
433
+ const contract = node.contract!;
434
+ const edge = graph.edges.find(e => e.from === node.nodeId);
435
+ if (!unit.template.unit || contract.inputs.length !== unit.template.unit.inputs.length ||
436
+ contract.inputs.some(key => !unit.template.unit!.inputs.includes(key)) ||
437
+ contract.outputs.length !== unit.template.unit.outputs.length ||
438
+ contract.outputs.some(key => !unit.template.unit!.outputs.includes(key)) || edge?.on !== 'outcome' ||
439
+ !edge.map || Object.keys(edge.map).length !== unit.template.unit.exits.length ||
440
+ unit.template.unit.exits.some(exit => !Object.hasOwn(edge.map!, exit))) {
441
+ debug.log('graph.unit', 'contract-mismatch', { graphId: graph.graphId, nodeId: node.nodeId, call: node.call });
442
+ throw new Error(`unit contract mismatch: ${node.nodeId} -> ${node.call}`);
443
+ }
444
+ validateUnitCalls(unit.template, unit.path, [...ancestors, graph.graphId]);
445
+ result.set(node.nodeId, unit);
446
+ }
447
+ return result;
448
+ }
449
+
392
450
  export async function runGraph(path: string, options: GraphRunOptions = {}): Promise<GraphRunState> {
393
451
  if (options.resumeRunId && options.runId) throw new Error('runId and resumeRunId cannot be combined');
394
452
  if (options.fromNodeId && !options.resumeRunId) throw new Error('--from requires --resume');
@@ -396,6 +454,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
396
454
  if (options.variant && options.resumeRunId) throw new Error('variant cannot be combined with --resume');
397
455
  if (options.releaseWaiver && (!options.resumeRunId || options.fromNodeId !== options.releaseWaiver.nodeId)) throw new Error('release waiver requires --resume --from <waived node>');
398
456
  if (options.releaseAcceptedRegressions && (!options.resumeRunId || !options.fromNodeId || options.releaseWaiver)) throw new Error('accepted regressions require a release resume boundary');
457
+ if (options.releaseGateExecutor && (!options.resumeRunId || !options.fromNodeId)) throw new Error('gate executor override requires a release resume boundary');
399
458
  const root = options.deps?.root ?? effectiveInstanceRoot();
400
459
  const matchingRuns = options.resumeRunId && !options.resumeGraphId
401
460
  ? listGraphRuns(root).runs.filter((run) => run.runId === options.resumeRunId && run.graphPath === resolve(path)) : [];
@@ -428,6 +487,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
428
487
  const variant = options.variant ? createGraphVariant({ template: graph, ...options.variant }) : undefined;
429
488
  if (variant && !variant.ok) throw new Error(`graph variant rejected: ${JSON.stringify(variant.rejections)}`);
430
489
  if (variant?.ok) graph = variant.template;
490
+ const units = validateUnitCalls(graph, path, options.unitContext?.ancestors);
431
491
  const variantSource = variant?.ok ? stringifyYaml({ ...document,
432
492
  nodes: graph.nodes.map((node, index) => ({
433
493
  ...((document.nodes as Record<string, unknown>[])[index] ?? {}), node_id: node.nodeId, kind: node.kind,
@@ -506,6 +566,10 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
506
566
  if (options.resumeRunId && state.growth?.length && graphMode === 'current' && state.sourceHash !== approvalSourceHash) {
507
567
  throw new Error('graph or recipes changed since grown run');
508
568
  }
569
+ if (options.unitContext) {
570
+ if (state.parentRunId !== undefined && state.parentRunId !== options.unitContext.parentRunId) throw new Error('unit parent run changed');
571
+ state.parentRunId = options.unitContext.parentRunId;
572
+ }
509
573
  if (state.growth?.length) graph = withGrowth(graph, state.growth);
510
574
  let waivedRecord: GraphRunState['nodes'][number] | undefined;
511
575
  if (options.fromNodeId) {
@@ -577,6 +641,22 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
577
641
  state.input = { ...(state.input as Record<string, unknown>),
578
642
  acceptedRegressions: releaseResumeAcceptedRegressions(state, [], options.fromNodeId) };
579
643
  }
644
+ if (options.releaseGateExecutor) {
645
+ if (graphId !== 'release-loop') throw new Error('gate executor override requires the release-loop graph');
646
+ const override = releaseResumeGateExecutor(state, { from: options.fromNodeId, executor: options.releaseGateExecutor.executor, host: options.releaseGateExecutor.host });
647
+ const next: Record<string, unknown> = { ...(state.input as Record<string, unknown>), ...override.set };
648
+ for (const key of override.unset ?? []) delete next[key];
649
+ state.input = next;
650
+ state.inputOverrides = [...(state.inputOverrides ?? []), override];
651
+ debug.log('graph.runs', 'input-override', { graphId, runId, ...override });
652
+ }
653
+ if (state.unitCalls) {
654
+ const calls = state.unitCalls;
655
+ state.unitCalls = calls.filter(call => {
656
+ const keep = state.path.slice(0, from).filter(nodeId => nodeId === call.node).length;
657
+ return calls.filter(item => item.node === call.node).indexOf(call) < keep;
658
+ });
659
+ }
580
660
  state.path = state.path.slice(0, from);
581
661
  state.nodes = state.nodes.slice(0, from);
582
662
  state.executed = state.nodes.filter((node) => node.executed).length;
@@ -659,7 +739,11 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
659
739
  if (state.status === 'done' || state.status === 'failed' || state.status === 'budget-exceeded') state.finishedAt ??= new Date().toISOString();
660
740
  persistOwnedRun(state, runnerOwner);
661
741
  };
662
- const log = options.deps?.log ?? ((event: string, data: Record<string, unknown>) => debug.log('graph.runner', event, data));
742
+ const baseLog = options.deps?.log ?? ((event: string, data: Record<string, unknown>) => debug.log('graph.runner', event, data));
743
+ // RFC §U: every event of a unit (child) run carries its parentRunId.
744
+ const log = options.unitContext
745
+ ? (event: string, data: Record<string, unknown>) => baseLog(event, { ...data, parentRunId: options.unitContext!.parentRunId })
746
+ : baseLog;
663
747
  const visits = new Map<string, number>();
664
748
  for (const nodeId of state.path) visits.set(nodeId, (visits.get(nodeId) ?? 0) + 1);
665
749
  let current: string | undefined = options.fromNodeId ?? state.pending?.nodeId ?? state.path.at(-1) ?? graph.entryNode;
@@ -685,7 +769,9 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
685
769
  const completed = state.nodes.at(-1);
686
770
  const resumingCompleted = options.resumeRunId !== undefined && !options.fromNodeId && completed?.nodeId === current &&
687
771
  state.path.at(-1) === current && !resumingPending;
688
- if (!resumingPending && !resumingCompleted && (visits.get(current) ?? 0) >= node.maxVisits) {
772
+ const usedSteps = state.path.length + (state.unitCalls ?? []).reduce((sum, call) => sum + call.steps, 0);
773
+ if (!resumingPending && !resumingCompleted && ((visits.get(current) ?? 0) >= node.maxVisits ||
774
+ (options.unitContext && usedSteps >= options.unitContext.remainingSteps))) {
689
775
  state.status = 'budget-exceeded';
690
776
  log('budget-exceeded', { graphId, runId, nodeId: current });
691
777
  persist();
@@ -795,6 +881,81 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
795
881
  error = result.error ? redactCredentials(result.error) : undefined;
796
882
  state.executed++;
797
883
  }
884
+ let unitExecuted = false;
885
+ if (units.has(current) && !waiving && state.dryRun && !resumingCompleted) {
886
+ // Dry run plans the walk only: no child run, no unit event, no unitCalls entry; the first declared exit stands in.
887
+ output = JSON.stringify({ outcome: units.get(current)!.template.unit!.exits[0], dryRun: true });
888
+ } else if (units.has(current) && !waiving && !resumingCompleted) {
889
+ const unit = units.get(current)!;
890
+ const contract = node.contract!;
891
+ const previous: Record<string, unknown> = Object.create(null);
892
+ for (const record of state.nodes) previous[record.nodeId] = lastJsonObject(record.output) ?? record.output;
893
+ const supplied = state.input && typeof state.input === 'object' && !Array.isArray(state.input)
894
+ ? state.input as Record<string, unknown> : {};
895
+ const inputs: Record<string, unknown> = Object.create(null);
896
+ for (const key of contract.inputs) {
897
+ const value = Object.hasOwn(supplied, key) ? supplied[key] : previous[key];
898
+ if (value !== undefined) inputs[key] = value;
899
+ }
900
+ const alreadySpent = state.path.length + (state.unitCalls ?? []).reduce((sum, call) => sum + call.steps, 0);
901
+ const remaining = options.unitContext ? options.unitContext.remainingSteps - alreadySpent : Infinity;
902
+ const childRunId = randomUUID();
903
+ const info = { graphId, runId, nodeId: current, unit: unit.template.graphId, childRunId, parentRunId: runId };
904
+ let entered = false;
905
+ try {
906
+ for (const key of contract.inputs) if (!Object.hasOwn(inputs, key)) throw new Error(`unit input missing: ${current}.${key}`);
907
+ if (createHash('sha256').update(readFileSync(unit.path, 'utf8')).digest('hex') !== unit.hash)
908
+ throw new Error(`unit source changed before call: ${current}`);
909
+ debug.log('graph.unit', 'enter', info);
910
+ log('graph.unit', { ...info, phase: 'enter' });
911
+ entered = true;
912
+ const child = await runGraph(unit.path, { runId: childRunId, input: inputs, dryRun: false,
913
+ pinChildUniverse: options.pinChildUniverse,
914
+ unitContext: { parentRunId: runId, ancestors: [...(options.unitContext?.ancestors ?? []), graphId],
915
+ remainingSteps: Math.min(node.budget!.innerSteps, remaining) }, deps: options.deps });
916
+ const steps = child.path.length + (child.unitCalls ?? []).reduce((sum, call) => sum + call.steps, 0);
917
+ const childExit = child.status === 'done' || child.status === 'failed' ? child.path.at(-1) : undefined;
918
+ // The last successful record carrying every contracted output is the source;
919
+ // intervening nodes (including terminal gates) need not reproduce those fields.
920
+ const values = child.nodes.reduce<Record<string, unknown> | undefined>((found, record) => {
921
+ if (!record.ok || !record.executed) return found;
922
+ const value = lastJsonObject(record.output) ??
923
+ (record.output && typeof record.output === 'object' && !Array.isArray(record.output)
924
+ ? record.output as Record<string, unknown> : undefined);
925
+ return value && contract.outputs.every(key => Object.hasOwn(value, key)) ? value : found;
926
+ }, undefined) ?? {};
927
+ state.unitCalls ??= [];
928
+ state.unitCalls.push({ node: current, unit: unit.template.graphId, version: unit.template.version,
929
+ hash: unit.hash, childRunId, exit: childExit ?? child.status, steps });
930
+ if (child.status === 'awaiting-approval') throw new Error(`unit cannot pause for approval: ${current}`);
931
+ if (child.status === 'budget-exceeded' || steps > node.budget!.innerSteps || steps > remaining) {
932
+ debug.log('graph.unit', 'budget-exhausted', { ...info, steps, innerSteps: node.budget!.innerSteps });
933
+ log('graph.unit', { ...info, phase: 'budget-exhausted', steps });
934
+ throw new Error(`unit inner_steps exceeded: ${current}`);
935
+ }
936
+ if (!childExit || !unit.template.unit!.exits.includes(childExit)) throw new Error(`unit exit missing: ${current}`);
937
+ if (child.status === 'done' && childExit === 'failed') throw new Error(`unit exit mismatch: ${current}`);
938
+ const result: Record<string, unknown> = { outcome: childExit };
939
+ for (const key of contract.outputs) {
940
+ if (!Object.hasOwn(values, key)) throw new Error(`unit output missing: ${current}.${key}`);
941
+ result[key] = values[key];
942
+ }
943
+ output = JSON.stringify(result);
944
+ ok = child.status === 'done';
945
+ debug.log('graph.unit', 'exit', { ...info, exit: childExit, steps });
946
+ log('graph.unit', { ...info, phase: 'exit', exit: childExit, steps });
947
+ } catch (cause) {
948
+ ok = false;
949
+ error = cause instanceof Error ? cause.message : String(cause);
950
+ if (entered) {
951
+ debug.log('graph.unit', 'exit', { ...info, error });
952
+ log('graph.unit', { ...info, phase: 'exit', error });
953
+ }
954
+ }
955
+ // A rejected invocation has no child ledger and did not execute a unit.
956
+ unitExecuted = existsSync(graphRunPath(unit.template.graphId, childRunId, root));
957
+ if (unitExecuted) state.executed++;
958
+ }
798
959
  if (prompt && !waiving && !state.dryRun && !resumingCompleted) {
799
960
  const outputs: Record<string, { ok: boolean; output: unknown; durationMs: number }> = Object.create(null);
800
961
  for (const previous of state.nodes) outputs[previous.nodeId] = { ok: previous.ok, output: lastJsonObject(previous.output) ?? previous.output ?? null, durationMs: 0 };
@@ -833,7 +994,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
833
994
  if (approval && !state.dryRun && !command) {
834
995
  output = JSON.stringify({ outcome: ok ? 'approved' : 'rejected', ...(state.pending?.decidedBy === undefined ? {} : { decidedBy: state.pending.decidedBy }), decidedAt: state.pending?.decidedAt ?? null });
835
996
  }
836
- state.nodes.push({ nodeId: current, ok, exit, executed: !waiving && ((!!command && (!state.dryRun || !!command.dry_run_command)) || (!!prompt && !state.dryRun)), ...((waiving || ((command || prompt || approval) && (!state.dryRun || !!command?.dry_run_command))) && output !== undefined ? { output } : {}), ...(error ? { error } : {}),
997
+ state.nodes.push({ nodeId: current, ok, exit, executed: !waiving && ((!!command && (!state.dryRun || !!command.dry_run_command)) || (!!prompt && !state.dryRun) || unitExecuted), ...((waiving || units.has(current) || ((command || prompt || approval) && (!state.dryRun || !!command?.dry_run_command))) && output !== undefined ? { output } : {}), ...(error ? { error } : {}),
837
998
  ...(waiving ? { waiver: { reason: options.releaseWaiver!.reason, at: nodeEndedAt, original: { exit: waivedRecord!.exit, ...(waivedRecord!.output === undefined ? {} : { output: waivedRecord!.output }), ...(waivedRecord!.error === undefined ? {} : { error: waivedRecord!.error }) } } } : {}),
838
999
  ...(approval && state.pending ? { ...(state.pending.decidedBy === undefined ? {} : { decidedBy: state.pending.decidedBy }), decidedAt: state.pending.decidedAt } : {}),
839
1000
  startedAt: nodeStartedAt, endedAt: nodeEndedAt, seconds });
@@ -842,7 +1003,7 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
842
1003
  delete state.pending;
843
1004
  delete state.approvalSourceHash;
844
1005
  }
845
- const reported = waiving || (prompt && !state.dryRun) || (command && (!state.dryRun || command.dry_run_command)) ? lastJsonObject(resumingCompleted ? completed?.output : output)?.outcome : undefined;
1006
+ const reported = waiving || units.has(current) || (prompt && !state.dryRun) || (command && (!state.dryRun || command.dry_run_command)) ? lastJsonObject(resumingCompleted ? completed?.output : output)?.outcome : undefined;
846
1007
  const namedOutcome = typeof reported === 'string' ? reported : undefined;
847
1008
  console.error(`[graph] ${current} ${ok ? 'ok' : 'fail'} (${seconds.toFixed(2)}s)`);
848
1009
  debug.log('graph.run', 'node', { graphId, runId, nodeId: current, phase: ok ? 'ok' : 'fail', seconds, exit });
package/src/grok/usage.ts CHANGED
@@ -182,13 +182,34 @@ export async function fetchGrokUsage(
182
182
  }
183
183
  }
184
184
 
185
- /** 이 조회는 구독 풀 잔량·리셋을 주지 않는다. 0·빈칸·미상과 다른 값. */
185
+ /** 주간 사용률·리셋이 없거나 유효하지 않은 응답에서 구독 축이 제공되지 않음을 나타낸다. */
186
186
  export const GROK_SUBSCRIPTION_AXIS = {
187
187
  status: 'unavailable',
188
188
  reason: 'query-does-not-supply',
189
189
  } as const;
190
190
 
191
- /** 원본 크레딧 사용률·창·금액류 → 크레딧 축. ⛔ 구독 축으로 전용하지 않는다. */
191
+ /** billing 쿼리가 주간 창과 사용률을 함께 돌려준 경우에만 실제 구독 잔량을 구성한다. */
192
+ export function grokUsageToSubscriptionAxis(usage: GrokUsage):
193
+ | { readonly status: 'available'; readonly remainingPercent: number; readonly resetsAt: number; readonly windowKind: 'weekly' }
194
+ | typeof GROK_SUBSCRIPTION_AXIS {
195
+ const end = usage.periodEnd === null ? Number.NaN : Date.parse(usage.periodEnd);
196
+ if (
197
+ usage.periodType !== 'USAGE_PERIOD_TYPE_WEEKLY'
198
+ || usage.usedPercent === null
199
+ || !Number.isFinite(usage.usedPercent)
200
+ || usage.usedPercent < 0
201
+ || usage.usedPercent > 100
202
+ || !Number.isFinite(end)
203
+ ) return GROK_SUBSCRIPTION_AXIS;
204
+ return {
205
+ status: 'available',
206
+ remainingPercent: 100 - usage.usedPercent,
207
+ resetsAt: end,
208
+ windowKind: 'weekly',
209
+ };
210
+ }
211
+
212
+ /** 원본 크레딧 사용률·창·금액류 → 크레딧 축. ⛔ 크레딧 숫자만으로 구독 잔량을 만들지 않는다. */
192
213
  export function grokUsageToCreditAxis(usage: GrokUsage): {
193
214
  readonly status: 'ok';
194
215
  readonly usedPercent: number | null;
@@ -0,0 +1,253 @@
1
+ // Reference card builder — L0 card + L1 listing for one local-repo / local-docs source.
2
+ //
3
+ // Pure value out: nothing is written. File access goes through the injected
4
+ // `RefIndexFs`, and every read is capped (file count, bytes per file), so a
5
+ // full file body is never loaded. Persistence (`grounding index` → jsonl) and
6
+ // BM25 are deliberately not here — they are the next slice.
7
+
8
+ import { spawnSync } from 'node:child_process';
9
+ import { closeSync, openSync, readSync, readdirSync, statSync } from 'node:fs';
10
+ import { basename, join } from 'node:path';
11
+ import { GroundingSourceError, type GroundingSource } from './sources.js';
12
+
13
+ export interface RefIndexDirEntry {
14
+ name: string;
15
+ isDirectory: boolean;
16
+ }
17
+
18
+ export interface RefIndexFs {
19
+ /** Names directly under `dir`. Unreadable dir → empty list. */
20
+ readDir(dir: string): RefIndexDirEntry[];
21
+ /** At most `maxBytes` from the start of the file. `truncated` when the file is longer. */
22
+ readHead(file: string, maxBytes: number): { text: string; truncated: boolean } | undefined;
23
+ /** Modified time of a path, ms since epoch. */
24
+ mtimeMs(path: string): number | undefined;
25
+ /** Last commit time (ISO) of a git repo, when it has one. */
26
+ lastCommitAt(repoDir: string): string | undefined;
27
+ }
28
+
29
+ export interface RefIndexLimits {
30
+ /** Max markdown/readme/package files opened for one source. */
31
+ maxFiles: number;
32
+ /** Max bytes read from the head of any one file. */
33
+ maxBytesPerFile: number;
34
+ }
35
+
36
+ export const DEFAULT_REF_INDEX_LIMITS: RefIndexLimits = { maxFiles: 200, maxBytesPerFile: 16 * 1024 };
37
+
38
+ const ONE_LINE_MAX = 200;
39
+ const SKIP_DIRS = new Set(['node_modules']);
40
+
41
+ export interface ReferenceTitle {
42
+ /** Path relative to the source root. */
43
+ file: string;
44
+ title: string;
45
+ }
46
+
47
+ export interface ReferenceCard {
48
+ /** L0 — `name` is the repository's own name (package.json `name`, else its directory name), not the registry `id`. */
49
+ id: string;
50
+ kind: GroundingSource['kind'];
51
+ name: string;
52
+ oneLine: string;
53
+ directories: string[];
54
+ receivedAt?: string;
55
+ /** L1 */
56
+ titles: ReferenceTitle[];
57
+ commands: string[];
58
+ /** A file-count or byte limit cut the read short — the lists may be incomplete. */
59
+ truncated: boolean;
60
+ }
61
+
62
+ export function buildReferenceCard(
63
+ source: GroundingSource,
64
+ fs: RefIndexFs,
65
+ limits: Partial<RefIndexLimits> = {},
66
+ ): ReferenceCard {
67
+ if ((source.kind !== 'local-repo' && source.kind !== 'local-docs') || !source.path) {
68
+ throw new GroundingSourceError(`reference card needs a local-repo/local-docs path: ${source.id}`);
69
+ }
70
+ const root = source.path;
71
+ // A missing root would otherwise yield an empty, untruncated card indistinguishable from an empty repo.
72
+ if (fs.mtimeMs(root) === undefined) {
73
+ throw new GroundingSourceError(`reference card source path is not readable: ${source.id} (${root})`);
74
+ }
75
+ const { maxFiles, maxBytesPerFile } = { ...DEFAULT_REF_INDEX_LIMITS, ...limits };
76
+ let opened = 0;
77
+ let truncated = false;
78
+
79
+ const open = (file: string): string | undefined => {
80
+ if (opened >= maxFiles) {
81
+ truncated = true;
82
+ return undefined;
83
+ }
84
+ opened += 1;
85
+ const head = fs.readHead(file, maxBytesPerFile);
86
+ if (!head) return undefined;
87
+ if (head.truncated) truncated = true;
88
+
89
+ return head.text;
90
+ };
91
+
92
+ const top = fs.readDir(root).sort((a, b) => a.name.localeCompare(b.name));
93
+ const directories = top
94
+ .filter((e) => e.isDirectory && !e.name.startsWith('.') && !SKIP_DIRS.has(e.name))
95
+ .map((e) => e.name);
96
+
97
+ const titles: ReferenceTitle[] = [];
98
+ const readme = top.find((e) => !e.isDirectory && /^readme(\.[a-z]+)?$/i.test(e.name));
99
+ let oneLine = '';
100
+ if (readme) {
101
+ const text = open(join(root, readme.name));
102
+ if (text !== undefined) {
103
+ oneLine = firstParagraphLine(text);
104
+ for (const title of headingTitles(text)) titles.push({ file: readme.name, title });
105
+ }
106
+ }
107
+
108
+ const docRoots = source.kind === 'local-docs' ? [''] : directories.includes('docs') ? ['docs'] : [];
109
+ for (const rel of docRoots) {
110
+ for (const file of markdownFiles(fs, root, rel, maxFiles + 2)) {
111
+ if (file === readme?.name) continue;
112
+ const text = open(join(root, file));
113
+ if (text === undefined) continue;
114
+ for (const title of headingTitles(text)) titles.push({ file, title });
115
+ }
116
+ }
117
+
118
+ const commands: string[] = [];
119
+ let packageName: string | undefined;
120
+ if (top.some((e) => !e.isDirectory && e.name === 'package.json')) {
121
+ const raw = open(join(root, 'package.json'));
122
+ if (raw !== undefined) {
123
+ const pkg = parsePackage(raw);
124
+ packageName = pkg.name;
125
+ commands.push(...pkg.commands);
126
+ }
127
+ }
128
+
129
+ const receivedAt = receivedAtOf(source, fs, root);
130
+ return {
131
+ id: source.id,
132
+ kind: source.kind,
133
+ name: packageName ?? basename(root),
134
+ oneLine,
135
+ directories,
136
+ ...(receivedAt ? { receivedAt } : {}),
137
+ titles,
138
+ commands,
139
+ truncated,
140
+ };
141
+ }
142
+
143
+ function receivedAtOf(source: GroundingSource, fs: RefIndexFs, root: string): string | undefined {
144
+ if (source.kind === 'local-repo') {
145
+ const commit = fs.lastCommitAt(root);
146
+ if (commit) return commit;
147
+ }
148
+ const mtime = fs.mtimeMs(root);
149
+ return mtime === undefined ? undefined : new Date(mtime).toISOString();
150
+ }
151
+
152
+ /**
153
+ * Markdown paths (relative to `root`) under `rel`, sorted. Directory listing only — no bodies.
154
+ * The walk stops once `cap` paths are found: past the file budget the caller only needs to know
155
+ * «there was more» (→ truncated), not the whole tree.
156
+ */
157
+ function markdownFiles(fs: RefIndexFs, root: string, rel: string, cap: number): string[] {
158
+ const found: string[] = [];
159
+ const walk = (dirRel: string): void => {
160
+ if (found.length >= cap) return;
161
+ const entries = fs.readDir(dirRel ? join(root, dirRel) : root).sort((a, b) => a.name.localeCompare(b.name));
162
+ for (const e of entries) {
163
+ const childRel = dirRel ? join(dirRel, e.name) : e.name;
164
+ if (found.length >= cap) return;
165
+ if (e.isDirectory) {
166
+ if (!e.name.startsWith('.') && !SKIP_DIRS.has(e.name)) walk(childRel);
167
+ } else if (/\.md$/i.test(e.name)) {
168
+ found.push(childRel);
169
+ }
170
+ }
171
+ };
172
+ walk(rel);
173
+ return found;
174
+ }
175
+
176
+ function firstParagraphLine(text: string): string {
177
+ const lines: string[] = [];
178
+ for (const raw of text.split(/\r?\n/)) {
179
+ const line = raw.trim();
180
+ if (!line) {
181
+ if (lines.length > 0) break;
182
+ continue;
183
+ }
184
+ if (line.startsWith('#') || line.startsWith('![') || line.startsWith('[![') || line.startsWith('<') || line.startsWith('```')) {
185
+ if (lines.length > 0) break;
186
+ continue;
187
+ }
188
+ lines.push(line);
189
+ }
190
+ const joined = lines.join(' ');
191
+ return joined.length > ONE_LINE_MAX ? `${joined.slice(0, ONE_LINE_MAX - 1)}…` : joined;
192
+ }
193
+
194
+ function headingTitles(text: string): string[] {
195
+ const out: string[] = [];
196
+ let fenced = false;
197
+ for (const raw of text.split(/\r?\n/)) {
198
+ if (/^\s*(```|~~~)/.test(raw)) {
199
+ fenced = !fenced;
200
+ continue;
201
+ }
202
+ if (fenced) continue;
203
+ const m = /^#{1,6}\s+(.+?)(?:\s+#+)?\s*$/.exec(raw);
204
+ if (m?.[1]) out.push(m[1]);
205
+ }
206
+ return out;
207
+ }
208
+
209
+ function parsePackage(packageJson: string): { name?: string; commands: string[] } {
210
+ let pkg: unknown;
211
+ try { pkg = JSON.parse(packageJson); } catch { return { commands: [] }; }
212
+ if (!pkg || typeof pkg !== 'object') return { commands: [] };
213
+ const { bin, name } = pkg as { bin?: unknown; name?: unknown };
214
+ const pkgName = typeof name === 'string' && name.trim() ? name.trim() : undefined;
215
+ const base = pkgName ? { name: pkgName } : {};
216
+ if (typeof bin === 'string') return { ...base, commands: pkgName ? [pkgName.replace(/^@[^/]+\//, '')] : [] };
217
+ if (bin && typeof bin === 'object' && !Array.isArray(bin)) return { ...base, commands: Object.keys(bin).sort() };
218
+ return { ...base, commands: [] };
219
+ }
220
+
221
+ /** Real filesystem + local git. `git log` reads local refs only. */
222
+ export const nodeRefIndexFs: RefIndexFs = {
223
+ readDir(dir) {
224
+ try {
225
+ return readdirSync(dir, { withFileTypes: true }).map((e) => ({ name: e.name, isDirectory: e.isDirectory() }));
226
+ } catch {
227
+ return [];
228
+ }
229
+ },
230
+ readHead(file, maxBytes) {
231
+ let fd: number | undefined;
232
+ try {
233
+ const size = statSync(file).size;
234
+ const length = Math.min(size, maxBytes);
235
+ const buf = Buffer.alloc(length);
236
+ fd = openSync(file, 'r');
237
+ readSync(fd, buf, 0, length, 0);
238
+ return { text: buf.toString('utf8'), truncated: size > maxBytes };
239
+ } catch {
240
+ return undefined;
241
+ } finally {
242
+ if (fd !== undefined) closeSync(fd);
243
+ }
244
+ },
245
+ mtimeMs(path) {
246
+ try { return statSync(path).mtimeMs; } catch { return undefined; }
247
+ },
248
+ lastCommitAt(repoDir) {
249
+ const result = spawnSync('git', ['-C', repoDir, 'log', '-1', '--format=%cI'], { encoding: 'utf8', timeout: 10_000 });
250
+ const out = result.status === 0 ? (result.stdout ?? '').trim() : '';
251
+ return out || undefined;
252
+ },
253
+ };
@@ -36,7 +36,9 @@ const constants = { BUILD_STOP, BUILD_SLOW, QUEUE_STOP, QUEUE_SLOW, UNKNOWN_CAP
36
36
  const valid = (value: unknown): BackpressureSignal =>
37
37
  typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 'unknown';
38
38
 
39
- /** Fixed, reviewable thresholds; observations are normalized before any arithmetic. */
39
+ /** Fixed, reviewable thresholds; observations are normalized before any arithmetic.
40
+ * Existing caller: the inline `.action(...)` callback registered by `queue.command('backpressure')` in src/harness/harness-cli-command.ts calls `decideBackpressure(signals, { maxPerCycle: opts.max === undefined ? 6 : Number(opts.max) })`.
41
+ */
40
42
  export function decideBackpressure(input: BackpressureSignals, { maxPerCycle = 6 }: { maxPerCycle?: number } = {}) {
41
43
  const max = Number.isSafeInteger(maxPerCycle) && maxPerCycle >= 0 ? maxPerCycle : 6;
42
44
  const signals: BackpressureSignals = {
@@ -81,7 +83,13 @@ export function decideBackpressure(input: BackpressureSignals, { maxPerCycle = 6
81
83
  budget = Math.min(budget, signals.queueDepth >= QUEUE_STOP ? 0 : Math.floor(max / 2));
82
84
  reasons.push('queue-depth');
83
85
  }
84
- if (signals.staleParents !== 'unknown' && signals.staleParents > 0) reasons.push(`stale-parents:${signals.staleParents}`);
86
+ if (signals.staleParents !== 'unknown' && signals.staleParents > 0) {
87
+ reasons.push(`outdated-version-parents:${signals.staleParents} (legacy observation alias: stale-parents:${signals.staleParents}; 살아 있는 런 · 예산 안 깎음 · 낡은 부모 후보는 eln tasks parents)`);
88
+ // Co-occurrence only: staleParents is local ps, admitWaiting includes remote hosts — not proof that this parent is the one waiting.
89
+ if (signals.admitWaiting !== 'unknown' && signals.admitWaiting > 0) {
90
+ reasons.push('restart-stale-parent:approval-wait');
91
+ }
92
+ }
85
93
  return { launchBudget: Math.max(0, Math.min(max, Math.floor(budget))), reasons, signals, constants };
86
94
  }
87
95