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
@@ -0,0 +1,536 @@
1
+ import { closeSync, existsSync, fstatSync, ftruncateSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
4
+ import { debug, redactSecretText } from '../debug/log.js';
5
+
6
+ /** Closed status ladder plus terminal exits. */
7
+ export type RequirementStatus =
8
+ | 'captured'
9
+ | 'placed'
10
+ | 'in-progress'
11
+ | 'landed'
12
+ | 'verified'
13
+ | 'superseded'
14
+ | 'dropped';
15
+
16
+ export type NfrQuality =
17
+ | 'reliability'
18
+ | 'performance'
19
+ | 'cost'
20
+ | 'security'
21
+ | 'observability'
22
+ | 'operability'
23
+ | 'portability';
24
+
25
+ export type AdminArea =
26
+ | 'legal'
27
+ | 'ip'
28
+ | 'account'
29
+ | 'billing'
30
+ | 'hardware'
31
+ | 'partner'
32
+ | 'publish'
33
+ | 'people';
34
+
35
+ export type RequirementKind =
36
+ | 'fr'
37
+ | 'arch'
38
+ | `nfr:${NfrQuality}`
39
+ | `admin:${AdminArea}`;
40
+
41
+ /** Admin evidence is locations / approval ids only — never raw account or payment text. */
42
+ export interface AdminEvidence {
43
+ approvalCardId?: string;
44
+ documentLocation?: string;
45
+ receiptLocation?: string;
46
+ }
47
+
48
+ export type RequirementEvidence = AdminEvidence | Record<string, string>;
49
+
50
+ export interface RequirementSource {
51
+ kind: string;
52
+ at: string;
53
+ /** Forbidden on admin:* kinds — raw utterance must not enter the ledger. */
54
+ excerpt?: string;
55
+ }
56
+
57
+ export interface RequirementEntry {
58
+ id: string;
59
+ statement: string;
60
+ kind: RequirementKind;
61
+ source: RequirementSource;
62
+ status: RequirementStatus;
63
+ rfc?: string[];
64
+ cells?: string[];
65
+ lives?: string[];
66
+ supersedes?: string;
67
+ owner?: string;
68
+ tags?: string[];
69
+ themes?: string[];
70
+ evidence?: RequirementEvidence;
71
+ }
72
+
73
+ export type CaptureInput = Omit<RequirementEntry, 'id' | 'status'> & {
74
+ id?: string;
75
+ status?: RequirementStatus;
76
+ };
77
+
78
+ export interface RequirementLedgerOptions {
79
+ stateDir?: string;
80
+ now?: () => Date;
81
+ }
82
+
83
+ const ALLOWED_KEYS = new Set([
84
+ 'id',
85
+ 'statement',
86
+ 'kind',
87
+ 'source',
88
+ 'status',
89
+ 'rfc',
90
+ 'cells',
91
+ 'lives',
92
+ 'supersedes',
93
+ 'owner',
94
+ 'tags',
95
+ 'themes',
96
+ 'evidence',
97
+ ]);
98
+
99
+ const SOURCE_ALLOWED_KEYS = new Set(['kind', 'at', 'excerpt']);
100
+
101
+ const ADMIN_EVIDENCE_KEYS = new Set(['approvalCardId', 'documentLocation', 'receiptLocation']);
102
+
103
+ /** Raw account / payment field names that must never be accepted. */
104
+ const ACCOUNT_PAYMENT_RAW_KEYS = new Set([
105
+ 'account',
106
+ 'accountNumber',
107
+ 'accountId',
108
+ 'username',
109
+ 'password',
110
+ 'login',
111
+ 'credential',
112
+ 'credentials',
113
+ 'apiKey',
114
+ 'api_key',
115
+ 'secret',
116
+ 'token',
117
+ 'accessToken',
118
+ 'refreshToken',
119
+ 'payment',
120
+ 'paymentMethod',
121
+ 'cardNumber',
122
+ 'creditCard',
123
+ 'cvv',
124
+ 'iban',
125
+ 'routingNumber',
126
+ 'bankAccount',
127
+ 'billingAccount',
128
+ 'receiptText',
129
+ 'receiptBody',
130
+ 'rawReceipt',
131
+ ]);
132
+
133
+ const NFR_QUALITIES: readonly NfrQuality[] = [
134
+ 'reliability',
135
+ 'performance',
136
+ 'cost',
137
+ 'security',
138
+ 'observability',
139
+ 'operability',
140
+ 'portability',
141
+ ];
142
+
143
+ const ADMIN_AREAS: readonly AdminArea[] = [
144
+ 'legal',
145
+ 'ip',
146
+ 'account',
147
+ 'billing',
148
+ 'hardware',
149
+ 'partner',
150
+ 'publish',
151
+ 'people',
152
+ ];
153
+
154
+ const STATUSES: readonly RequirementStatus[] = [
155
+ 'captured',
156
+ 'placed',
157
+ 'in-progress',
158
+ 'landed',
159
+ 'verified',
160
+ 'superseded',
161
+ 'dropped',
162
+ ];
163
+
164
+ const LADDER: readonly RequirementStatus[] = [
165
+ 'captured',
166
+ 'placed',
167
+ 'in-progress',
168
+ 'landed',
169
+ 'verified',
170
+ ];
171
+
172
+ type Event =
173
+ | { type: 'captured'; entry: RequirementEntry }
174
+ | { type: 'transition'; id: string; at: string; status: RequirementStatus; by?: string; note?: string; supersedes?: string };
175
+
176
+ function required(value: string, name: string): string {
177
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} is required`);
178
+ return value.trim();
179
+ }
180
+
181
+ function single(value: string, name: string): string {
182
+ const text = required(value, name);
183
+ if (/[\r\n]/.test(text)) throw new Error(`${name} must be one line`);
184
+ return text;
185
+ }
186
+
187
+ function utc(value: string): string {
188
+ if (typeof value !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z$/.test(value)) {
189
+ throw new Error(`UTC timestamp required: ${value}`);
190
+ }
191
+ const parsed = new Date(value);
192
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 19) !== value.slice(0, 19)) {
193
+ throw new Error(`UTC timestamp required: ${value}`);
194
+ }
195
+ return parsed.toISOString();
196
+ }
197
+
198
+ function assertNoSecret(value: string, name: string): string {
199
+ if (redactSecretText(value) !== value) {
200
+ throw new Error(`secret-shaped value rejected in ${name}`);
201
+ }
202
+ return value;
203
+ }
204
+
205
+ function assertNoSecretDeep(value: unknown, path: string): void {
206
+ if (typeof value === 'string') {
207
+ assertNoSecret(value, path);
208
+ return;
209
+ }
210
+ if (Array.isArray(value)) {
211
+ value.forEach((item, i) => assertNoSecretDeep(item, `${path}[${i}]`));
212
+ return;
213
+ }
214
+ if (value && typeof value === 'object') {
215
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
216
+ assertNoSecretDeep(v, `${path}.${k}`);
217
+ }
218
+ }
219
+ }
220
+
221
+ function isAdminKind(kind: string): kind is `admin:${AdminArea}` {
222
+ return kind.startsWith('admin:') && (ADMIN_AREAS as readonly string[]).includes(kind.slice('admin:'.length));
223
+ }
224
+
225
+ function isNfrKind(kind: string): kind is `nfr:${NfrQuality}` {
226
+ return kind.startsWith('nfr:') && (NFR_QUALITIES as readonly string[]).includes(kind.slice('nfr:'.length));
227
+ }
228
+
229
+ function parseKind(kind: unknown): RequirementKind {
230
+ if (typeof kind !== 'string') throw new Error('kind is required');
231
+ if (kind === 'fr' || kind === 'arch') return kind;
232
+ if (isNfrKind(kind) || isAdminKind(kind)) return kind;
233
+ throw new Error(`invalid kind: ${kind}`);
234
+ }
235
+
236
+ function rejectAccountPaymentKeys(obj: Record<string, unknown>, path: string): void {
237
+ for (const key of Object.keys(obj)) {
238
+ if (ACCOUNT_PAYMENT_RAW_KEYS.has(key)) {
239
+ throw new Error(`account/payment raw-text field rejected: ${path}.${key}`);
240
+ }
241
+ }
242
+ }
243
+
244
+ function validateSource(source: unknown, kind: RequirementKind): RequirementSource {
245
+ if (!source || typeof source !== 'object' || Array.isArray(source)) throw new Error('source is required');
246
+ const raw = source as Record<string, unknown>;
247
+ for (const key of Object.keys(raw)) {
248
+ if (!SOURCE_ALLOWED_KEYS.has(key)) throw new Error(`disallowed source key: ${key}`);
249
+ }
250
+ rejectAccountPaymentKeys(raw, 'source');
251
+ if (isAdminKind(kind) && raw.excerpt !== undefined) {
252
+ throw new Error('admin source.excerpt is forbidden');
253
+ }
254
+ const sourceKind = single(String(raw.kind ?? ''), 'source.kind');
255
+ const at = utc(String(raw.at ?? ''));
256
+ assertNoSecret(sourceKind, 'source.kind');
257
+ const out: RequirementSource = { kind: sourceKind, at };
258
+ if (raw.excerpt !== undefined) {
259
+ out.excerpt = assertNoSecret(single(String(raw.excerpt), 'source.excerpt'), 'source.excerpt');
260
+ }
261
+ return out;
262
+ }
263
+
264
+ function validateEvidence(evidence: unknown, kind: RequirementKind): RequirementEvidence | undefined {
265
+ if (evidence === undefined) return undefined;
266
+ if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence)) {
267
+ throw new Error('evidence must be an object');
268
+ }
269
+ const raw = evidence as Record<string, unknown>;
270
+ rejectAccountPaymentKeys(raw, 'evidence');
271
+ if (isAdminKind(kind)) {
272
+ for (const key of Object.keys(raw)) {
273
+ if (!ADMIN_EVIDENCE_KEYS.has(key)) {
274
+ throw new Error(`admin evidence allows only approvalCardId, documentLocation, receiptLocation; got ${key}`);
275
+ }
276
+ }
277
+ const out: AdminEvidence = {};
278
+ for (const key of ADMIN_EVIDENCE_KEYS) {
279
+ if (raw[key] === undefined) continue;
280
+ out[key as keyof AdminEvidence] = assertNoSecret(single(String(raw[key]), `evidence.${key}`), `evidence.${key}`);
281
+ }
282
+ return out;
283
+ }
284
+ const out: Record<string, string> = {};
285
+ for (const [key, value] of Object.entries(raw)) {
286
+ if (typeof value !== 'string') throw new Error(`evidence.${key} must be a string`);
287
+ out[key] = assertNoSecret(single(value, `evidence.${key}`), `evidence.${key}`);
288
+ }
289
+ return out;
290
+ }
291
+
292
+ function validateCapture(input: CaptureInput): RequirementEntry {
293
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('capture input required');
294
+ const raw = input as Record<string, unknown>;
295
+ for (const key of Object.keys(raw)) {
296
+ if (!ALLOWED_KEYS.has(key)) throw new Error(`disallowed key: ${key}`);
297
+ }
298
+ rejectAccountPaymentKeys(raw, 'root');
299
+
300
+ const kind = parseKind(raw.kind);
301
+ const statement = assertNoSecret(single(String(raw.statement ?? ''), 'statement'), 'statement');
302
+ const source = validateSource(raw.source, kind);
303
+ const status: RequirementStatus = raw.status === undefined ? 'captured' : (raw.status as RequirementStatus);
304
+ if (!(STATUSES as readonly string[]).includes(status)) throw new Error(`invalid status: ${status}`);
305
+ if (status !== 'captured') throw new Error('capture must start as captured');
306
+
307
+ const entry: RequirementEntry = {
308
+ id: raw.id === undefined ? '' : assertNoSecret(single(String(raw.id), 'id'), 'id'),
309
+ statement,
310
+ kind,
311
+ source,
312
+ status,
313
+ };
314
+
315
+ if (raw.rfc !== undefined) {
316
+ if (!Array.isArray(raw.rfc)) throw new Error('rfc must be a list');
317
+ entry.rfc = raw.rfc.map((item, i) => assertNoSecret(single(String(item), `rfc[${i}]`), `rfc[${i}]`));
318
+ }
319
+ if (raw.cells !== undefined) {
320
+ if (!Array.isArray(raw.cells)) throw new Error('cells must be a list');
321
+ entry.cells = raw.cells.map((item, i) => assertNoSecret(single(String(item), `cells[${i}]`), `cells[${i}]`));
322
+ }
323
+ if (raw.lives !== undefined) {
324
+ if (!Array.isArray(raw.lives)) throw new Error('lives must be a list');
325
+ entry.lives = raw.lives.map((item, i) => assertNoSecret(single(String(item), `lives[${i}]`), `lives[${i}]`));
326
+ }
327
+ if (raw.supersedes !== undefined) {
328
+ entry.supersedes = assertNoSecret(single(String(raw.supersedes), 'supersedes'), 'supersedes');
329
+ }
330
+ if (raw.owner !== undefined) {
331
+ entry.owner = assertNoSecret(single(String(raw.owner), 'owner'), 'owner');
332
+ }
333
+ if (raw.tags !== undefined) {
334
+ if (!Array.isArray(raw.tags)) throw new Error('tags must be a list');
335
+ entry.tags = raw.tags.map((item, i) => assertNoSecret(single(String(item), `tags[${i}]`), `tags[${i}]`));
336
+ }
337
+ if (raw.themes !== undefined) {
338
+ if (!Array.isArray(raw.themes)) throw new Error('themes must be a list');
339
+ entry.themes = raw.themes.map((item, i) => assertNoSecret(single(String(item), `themes[${i}]`), `themes[${i}]`));
340
+ }
341
+ const evidence = validateEvidence(raw.evidence, kind);
342
+ if (evidence !== undefined) entry.evidence = evidence;
343
+
344
+ assertNoSecretDeep(entry, 'entry');
345
+ return entry;
346
+ }
347
+
348
+ function canTransition(from: RequirementStatus, to: RequirementStatus): boolean {
349
+ if (from === to) return false;
350
+ if (to === 'superseded' || to === 'dropped') {
351
+ return from !== 'superseded' && from !== 'dropped' && from !== 'verified';
352
+ }
353
+ const fromIdx = LADDER.indexOf(from as typeof LADDER[number]);
354
+ const toIdx = LADDER.indexOf(to as typeof LADDER[number]);
355
+ if (fromIdx < 0 || toIdx < 0) return false;
356
+ return toIdx === fromIdx + 1;
357
+ }
358
+
359
+ export class RequirementLedger {
360
+ readonly path: string;
361
+ private readonly lockPath: string;
362
+ private readonly now: () => Date;
363
+ private readonly stateDir: string;
364
+
365
+ constructor(options: RequirementLedgerOptions = {}) {
366
+ this.stateDir = options.stateDir ?? elanousStateRoot();
367
+ this.path = join(this.stateDir, 'requirements', 'requirements.jsonl');
368
+ this.lockPath = `${this.path}.lock`;
369
+ this.now = options.now ?? (() => new Date());
370
+ }
371
+
372
+ private locked<T>(fn: () => T): T {
373
+ mkdirSync(join(this.path, '..'), { recursive: true });
374
+ const deadline = Date.now() + 10000;
375
+ let fd: number;
376
+ for (;;) {
377
+ try {
378
+ fd = openSync(this.lockPath, 'wx', 0o600);
379
+ break;
380
+ } catch (error) {
381
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
382
+ try {
383
+ const pid = Number(readFileSync(this.lockPath, 'utf8'));
384
+ if (Number.isSafeInteger(pid) && pid > 0 && Date.now() - statSync(this.lockPath).mtimeMs > 30000) {
385
+ try {
386
+ process.kill(pid, 0);
387
+ } catch (e) {
388
+ if ((e as NodeJS.ErrnoException).code === 'ESRCH') {
389
+ unlinkSync(this.lockPath);
390
+ continue;
391
+ }
392
+ }
393
+ }
394
+ } catch (e) {
395
+ if ((e as NodeJS.ErrnoException).code === 'ENOENT') continue;
396
+ }
397
+ if (Date.now() >= deadline) throw new Error('requirement ledger lock timeout');
398
+ Bun.sleepSync(20);
399
+ }
400
+ }
401
+ try {
402
+ writeSync(fd, `${process.pid}`);
403
+ return fn();
404
+ } finally {
405
+ closeSync(fd);
406
+ unlinkSync(this.lockPath);
407
+ }
408
+ }
409
+
410
+ private events(): Event[] {
411
+ if (!existsSync(this.path)) return [];
412
+ const text = readFileSync(this.path, 'utf8');
413
+ if (text && !text.endsWith('\n')) throw new Error('incomplete requirement ledger line');
414
+ return text.split('\n').filter(Boolean).map((line, i) => {
415
+ try {
416
+ return JSON.parse(line) as Event;
417
+ } catch {
418
+ throw new Error(`invalid requirement ledger event on line ${i + 1}`);
419
+ }
420
+ });
421
+ }
422
+
423
+ private append(event: Event): void {
424
+ const fd = openSync(this.path, 'a', 0o600);
425
+ try {
426
+ const bytes = Buffer.from(`${JSON.stringify(event)}\n`, 'utf8');
427
+ const size = fstatSync(fd).size;
428
+ try {
429
+ if (writeSync(fd, bytes) !== bytes.length) throw new Error('short requirement ledger write');
430
+ } catch (error) {
431
+ try {
432
+ ftruncateSync(fd, size);
433
+ } catch {
434
+ /* Keep the original write failure. */
435
+ }
436
+ throw error;
437
+ }
438
+ } finally {
439
+ closeSync(fd);
440
+ }
441
+ }
442
+
443
+ private materialize(): Map<string, RequirementEntry> {
444
+ const map = new Map<string, RequirementEntry>();
445
+ for (const event of this.events()) {
446
+ if (event.type === 'captured') {
447
+ if (map.has(event.entry.id)) throw new Error(`duplicate requirement: ${event.entry.id}`);
448
+ map.set(event.entry.id, { ...event.entry });
449
+ continue;
450
+ }
451
+ const old = map.get(event.id);
452
+ if (!old) throw new Error(`unknown requirement: ${event.id}`);
453
+ if (!canTransition(old.status, event.status)) {
454
+ throw new Error(`invalid requirement transition: ${old.status} → ${event.status}`);
455
+ }
456
+ map.set(event.id, {
457
+ ...old,
458
+ status: event.status,
459
+ ...(event.supersedes ? { supersedes: event.supersedes } : {}),
460
+ });
461
+ }
462
+ return map;
463
+ }
464
+
465
+ private nextId(at: string): string {
466
+ const day = at.slice(2, 10).replaceAll('-', '');
467
+ const prefix = `req-${day}-`;
468
+ let next = 1;
469
+ for (const entry of this.materialize().values()) {
470
+ if (entry.id.startsWith(prefix)) {
471
+ const n = Number(entry.id.slice(prefix.length));
472
+ if (Number.isSafeInteger(n)) next = Math.max(next, n + 1);
473
+ }
474
+ }
475
+ return `${prefix}${next}`;
476
+ }
477
+
478
+ list(filters: { status?: RequirementStatus | 'all'; kind?: RequirementKind } = {}): RequirementEntry[] {
479
+ return [...this.materialize().values()]
480
+ .filter(entry => (filters.status === undefined || filters.status === 'all' || entry.status === filters.status)
481
+ && (filters.kind === undefined || entry.kind === filters.kind))
482
+ .sort((a, b) => a.id.localeCompare(b.id));
483
+ }
484
+
485
+ show(id: string): RequirementEntry {
486
+ const entry = this.materialize().get(id);
487
+ if (!entry) throw new Error(`requirement not found: ${id}`);
488
+ return entry;
489
+ }
490
+
491
+ /** Open a requirement at `captured`. Validates allowlist + secret rejection before any disk write. */
492
+ capture(input: CaptureInput): RequirementEntry {
493
+ const clean = validateCapture(input);
494
+ return this.locked(() => {
495
+ const at = utc(clean.source.at || this.now().toISOString());
496
+ const id = clean.id || this.nextId(at);
497
+ if (this.materialize().has(id)) throw new Error(`duplicate requirement: ${id}`);
498
+ const entry: RequirementEntry = { ...clean, id, status: 'captured', source: { ...clean.source, at } };
499
+ assertNoSecretDeep(entry, 'entry');
500
+ this.append({ type: 'captured', entry });
501
+ debug.log('requirements.ledger', 'captured', { id: entry.id, kind: entry.kind, status: entry.status });
502
+ return entry;
503
+ });
504
+ }
505
+
506
+ /** Advance along captured→placed→in-progress→landed→verified, or exit via superseded/dropped. */
507
+ transition(id: string, status: RequirementStatus, opts: { by?: string; note?: string; supersedes?: string; at?: string } = {}): RequirementEntry {
508
+ if (!(STATUSES as readonly string[]).includes(status)) throw new Error(`invalid status: ${status}`);
509
+ if (opts.by !== undefined) assertNoSecret(single(opts.by, 'by'), 'by');
510
+ if (opts.note !== undefined) assertNoSecret(single(opts.note, 'note'), 'note');
511
+ if (opts.supersedes !== undefined) assertNoSecret(single(opts.supersedes, 'supersedes'), 'supersedes');
512
+ return this.locked(() => {
513
+ const old = this.show(id);
514
+ if (!canTransition(old.status, status)) {
515
+ throw new Error(`invalid requirement transition: ${old.status} → ${status}`);
516
+ }
517
+ const at = utc(opts.at ?? this.now().toISOString());
518
+ this.append({
519
+ type: 'transition',
520
+ id,
521
+ at,
522
+ status,
523
+ ...(opts.by ? { by: opts.by } : {}),
524
+ ...(opts.note ? { note: opts.note } : {}),
525
+ ...(opts.supersedes ? { supersedes: opts.supersedes } : {}),
526
+ });
527
+ debug.log('requirements.ledger', 'transition', { id, from: old.status, to: status });
528
+ return this.show(id);
529
+ });
530
+ }
531
+
532
+ /** Convenience: captured → placed (optionally attaching rfc/cells if provided on a fresh capture). */
533
+ place(id: string, opts: { by?: string; note?: string; at?: string } = {}): RequirementEntry {
534
+ return this.transition(id, 'placed', opts);
535
+ }
536
+ }
@@ -13,10 +13,10 @@
13
13
  * RFC: 내부 문서 `RFC-retro-loop-daily-weekly-insight-2026-10-07` §1 (§0 표가 첫 판의 정답지).
14
14
  */
15
15
  import { createHash } from 'node:crypto';
16
- import { spawnSync } from 'node:child_process';
17
16
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
18
17
  import { dirname, join, resolve } from 'node:path';
19
18
  import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
19
+ import { spawnElanousSync } from '../instance/spawn-elanous.js';
20
20
  import { getElanousConfigDir } from '../elanous-config-dir.js';
21
21
  import { getUserConfig } from '../user-config.js';
22
22
  import { debug } from '../debug/log.js';
@@ -251,10 +251,10 @@ export type RetroDeps = { now?: Date; readFacts?: (since: string, root: string)
251
251
  const repoRoot = resolve(import.meta.dir, '..', '..');
252
252
 
253
253
  function readFactsViaCli(since: string, root: string, configDir: string, repo = repoRoot): unknown {
254
- const args = ['bin/elanous.mjs', ...(root === prodInstanceRoot() ? [] : [`--test=${root}`]), 'retro', 'facts', '--since', since, '--json', '--config-dir', resolve(configDir)];
255
- const result = spawnSync('bun', args, { cwd: repo, encoding: 'utf8', timeout: 570_000, maxBuffer: 64 * 1024 * 1024 });
256
- if (result.error || result.status !== 0) throw new Error(`retro facts failed: ${result.error?.message || result.stderr?.trim() || `exit ${result.status ?? 'unknown'}`}`);
257
- return JSON.parse(result.stdout);
254
+ const args = [...(root === prodInstanceRoot() ? [] : [`--test=${root}`]), 'retro', 'facts', '--since', since, '--json', '--config-dir', resolve(configDir)];
255
+ const result = spawnElanousSync(args, { bin: join(repo, 'bin/elanous.mjs'), stateDir: root, cwd: repo, encoding: 'utf8', timeout: 570_000, maxBuffer: 64 * 1024 * 1024 });
256
+ if (result.error || result.status !== 0) throw new Error(`retro facts failed: ${result.error?.message || String(result.stderr ?? '').trim() || `exit ${result.status ?? 'unknown'}`}`);
257
+ return JSON.parse(String(result.stdout));
258
258
  }
259
259
 
260
260
  /** 최근 판 열 개의 체크리스트에서 green/done 칸을 모은다(예: DRAFT3 0.2.12 · DRAFT-TRIAGE 0.2.18). 못 읽은 판은 건너뛴다. */
@@ -7,6 +7,8 @@ import { promisify } from 'node:util';
7
7
  import { TRACK_AGENT_FORBIDDEN_ACTION_REGEX, universeLaunch } from '../autopilot/track-agent.js';
8
8
  import { debug } from '../debug/log.js';
9
9
  import { openSurfaceEventsDb, recordEvent, surfaceEventsDbPath } from '../domains/surface-events.js';
10
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
11
+ import { installedCliArgv, resolveHqLaunchSeat } from '../hq/seats.js';
10
12
  import { judgeNeighbor } from '../loops/neighbors.js';
11
13
  import { checkLoopNeighbors, emitLoopHeartbeat, SEAT_NEIGHBORS } from '../loops/neighbor-runtime.js';
12
14
  import { addHarnessQueue, harnessQueueIdForKey, harnessQueueOutcome, listHarnessQueue } from '../harness/harness-queue.js';
@@ -72,6 +74,8 @@ export type SeatDeps = {
72
74
  now?: () => Date;
73
75
  read?: (path: string) => string;
74
76
  run?: (args: string[]) => Promise<string>;
77
+ /** Seat-folder resolver for every process this loop starts; production reads the registry-validated `seats/<role>` view, tests inject an isolated folder. */
78
+ launchSeat?: (seat: string) => string;
75
79
  enqueue?: (seat: string, text: string, root: string, idempotencyKey: string, item: SeatItem) => Promise<{ id: string }>;
76
80
  queueOutcome?: (id: string, root: string) => ReturnType<typeof harnessQueueOutcome>;
77
81
  queueItems?: (root: string) => ReturnType<typeof listHarnessQueue>;
@@ -118,10 +122,27 @@ const defaultRead = (path: string): string => {
118
122
  try { return readFileSync(path, 'utf8'); }
119
123
  catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return ''; throw error; }
120
124
  };
121
- const defaultRun = async (args: string[]): Promise<string> => {
122
- const { argv, env } = universeLaunch(args);
123
- const { stdout } = await exec('bun', argv, {
124
- cwd: repoRoot, env, encoding: 'utf8',
125
+ /** The seat's own registry-validated folder (`seats/<role>`, kept fresh by `hq seat --refresh`) — never this module's source tree. */
126
+ export function seatLoopLaunchSeat(seat: string): string {
127
+ return resolveHqLaunchSeat(seat, getElanousConfigDir());
128
+ }
129
+ /** One seat launch boundary: installed-CLI argv (absolute runtime + absolute entry) with `seats/<role>` as cwd. */
130
+ export function seatLoopLaunchCommand(seat: string, args: readonly string[],
131
+ launchSeat: (role: string) => string = seatLoopLaunchSeat): { argv: string[]; cwd: string; env: NodeJS.ProcessEnv } {
132
+ let cwd: string;
133
+ try { cwd = launchSeat(seat); }
134
+ catch (error) {
135
+ // A missing seat folder is a stopped launch, never a fall back to the tree this code was loaded from.
136
+ observe('launch-seat-unavailable', { seat, error: String(error).slice(0, 200) });
137
+ throw new Error(`seat ${seat} has no registry-validated seats/${seat} folder; refusing to launch from the source tree: ${String(error)}`);
138
+ }
139
+ const { argv, env } = universeLaunch([...args]);
140
+ return { argv: installedCliArgv(argv.slice(1)), cwd, env };
141
+ }
142
+ const defaultRun = async (args: string[], seat: string, launchSeat?: (role: string) => string): Promise<string> => {
143
+ const { argv, cwd, env } = seatLoopLaunchCommand(seat, args, launchSeat ?? seatLoopLaunchSeat);
144
+ const { stdout } = await exec(argv[0]!, argv.slice(1), {
145
+ cwd, env, encoding: 'utf8',
125
146
  timeout: args[0] === 'harness' && args[1] === 'say' ? 3_600_000 : 120_000, maxBuffer: 4 * 1024 * 1024,
126
147
  });
127
148
  return stdout;
@@ -1203,6 +1224,8 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1203
1224
  return { seat, status: 'skipped-off',
1204
1225
  ...(config.mode === 'live-safe' ? { modeDowngradeReason: '자리 목록에 없음 (loops.seat.seats)' } : {}) };
1205
1226
  }
1227
+ // Every process this turn starts runs through the installed CLI in the seat's own folder (`seats/<role>`, always fresh).
1228
+ const run = deps.run ?? ((args: string[]): Promise<string> => defaultRun(args, seat, deps.launchSeat));
1206
1229
  const now = (deps.now ?? (() => new Date()))();
1207
1230
  const path = seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now);
1208
1231
  const directory = dirname(path);
@@ -1276,7 +1299,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1276
1299
  current: `기한 ${waited}분 경과 · 칸 상태 ${cell?.status ?? '질문 미해결'}`, question, answer: '재측상 미해결이므로 보류를 권고한다',
1277
1300
  options, recommendation, confidence: '중간 — 기한 경과와 미해결을 재측함', ifUntouched: '질문/칸은 미해결로 남는다',
1278
1301
  evidence: `${proposal.item!.evidence ?? '원천 근거 미기재'} · ${note} · ${decisionRef(seat, proposal.item!)}` });
1279
- await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${question}`, '--category', 'other',
1302
+ await run(['decisions', 'raise', '--title', `${seat}: ${question}`, '--category', 'other',
1280
1303
  '--s', material.scqa.s, '--c', material.scqa.c, '--q', material.scqa.q, '--a', material.scqa.a,
1281
1304
  '--pending-question', material.pendingQuestion,
1282
1305
  '--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
@@ -1313,7 +1336,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1313
1336
  const inputs = receivedInputs!;
1314
1337
  const evidence = judgmentEvidence;
1315
1338
  if (!evidence) throw new Error('cross-check judgment requires receiving-seat evidence');
1316
- const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
1339
+ const output = JSON.parse((await run(['agent', '--json', '--no-tools',
1317
1340
  `[${receiver} 자리 · 역할 docs/roles/${receiver}.md] ${from} 자리의 결정 초안을 교차 확인한다. 아래 실제 근거만 사용하고 모르는 사실은 지어내지 마라. JSON 하나만 반환: {"agree":boolean,"note":"근거를 포함한 메모","resolves":boolean}. 이웃 자리에서 직접 해결할 수 있을 때만 resolves=true. 초안: ${draft}\n역할: ${inputs.role}\n${receiver} 체크리스트 근거:\n${evidence || '(없음)'}`])).trim()) as { reply?: unknown };
1318
1341
  const value: unknown = typeof output.reply === 'string' ? JSON.parse(output.reply) : output;
1319
1342
  return value as CrossCheckJudgment;
@@ -1472,7 +1495,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1472
1495
  const inquiry = item.source === 'seat-question' ? null : revisedProposal && item.source === 'checklist'
1473
1496
  ? ledger.find((row) => row.status === 'awaiting-resolution' && row.item && itemKey(row.item) === itemKey(item))?.inquiry ?? null
1474
1497
  : unfinished?.inquiry ?? await (deps.inquire ?? (async (asking: SeatId, work: SeatItem) => { try {
1475
- const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
1498
+ const output = JSON.parse((await run(['agent', '--json', '--no-tools',
1476
1499
  `[${asking} 자리 · 역할 docs/roles/${asking}.md] 이 항목을 판단할 근거가 부족한 경우에만 다른 자리 또는 사람에게 질문한다. 근거가 충분하면 null. JSON 객체 하나만 반환: null 또는 {"to":"OP|TC|MK|UX|CEO","question":"필요한 근거를 묻는 질문"}. 항목: ${work.id} ${work.title}\n내용: ${work.text}\n역할: ${(deps.read ?? defaultRead)(join(deps.repo ?? repoRoot, 'docs', 'roles', `${asking}.md`)).slice(0, 4_000)}`])).trim()) as { reply?: unknown };
1477
1500
  const value: unknown = typeof output?.reply === 'string' ? JSON.parse(output.reply) : output;
1478
1501
  if (value === null) return null;
@@ -1491,7 +1514,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1491
1514
  ? unfinished?.answer ? { answer: unfinished.answer } : unfinished?.inquiry ? unfinished.inquiry : await (deps.reply ?? (async (receiver: SeatId, question: string, from: SeatId) => {
1492
1515
  const evidence = inputs.checklist.map((candidate) =>
1493
1516
  `${candidate.id} · ${candidate.title} (판 ${candidate.version}, ${candidate.kind})${candidate.evidence?.trim() ? ` · 근거: ${candidate.evidence.trim()}` : ''}`).join('\n').slice(0, 8_000);
1494
- const output = JSON.parse((await (deps.run ?? defaultRun)(['agent', '--json', '--no-tools',
1517
+ const output = JSON.parse((await run(['agent', '--json', '--no-tools',
1495
1518
  `[${receiver} 자리 · 역할 docs/roles/${receiver}.md] ${from} 자리의 질문에 근거를 갖고 답하라. 아래 ${receiver} 자리의 실제 체크리스트 근거에 없는 사실은 지어내지 마라. 모르면 사람 결정이 필요한 질문을 작성하라. JSON 하나만 반환: {"answer":"근거를 포함한 답"} 또는 {"to":"CEO","question":"결정할 질문"}. 질문: ${question}\n역할: ${inputs.role}\n${receiver} 체크리스트 근거:\n${evidence || '(없음)'}`])).trim()) as { reply?: unknown };
1496
1519
  const value: unknown = typeof output.reply === 'string' ? JSON.parse(output.reply) : output;
1497
1520
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid seat reply');
@@ -1592,7 +1615,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1592
1615
  question: ask.question, answer: '해소 근거 확인 전까지 보류를 권고한다', options, recommendation,
1593
1616
  confidence: answer ? '중간 — 이웃 응답을 확인함' : '낮음 — 기한 내 이웃 응답 없음',
1594
1617
  ifUntouched: '사람 판단 전 집행하지 않는다', evidence: `${item.evidence ?? '원천 근거 미기재'} · ${decisionRef(seat, item)}` });
1595
- await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
1618
+ await run(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
1596
1619
  '--s', material.scqa.s, '--c', material.scqa.c, '--q', material.scqa.q, '--a', material.scqa.a,
1597
1620
  '--pending-question', material.pendingQuestion,
1598
1621
  '--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
@@ -1656,7 +1679,6 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
1656
1679
  return entry;
1657
1680
  }
1658
1681
  }
1659
- const run = deps.run ?? defaultRun;
1660
1682
  let outcome: string | undefined;
1661
1683
  try { outcome = (JSON.parse(await run(['harness', 'budget', '--json'])) as { outcome?: string }).outcome; }
1662
1684
  catch { /* failed budget observation is never permission to launch */ }