@wrenai/genbi 0.0.1

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 (286) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +141 -0
  3. package/bin/genbi.mjs +31 -0
  4. package/dist/assets/echarts-LTFC-ysd.js +40 -0
  5. package/dist/assets/index-Bo--QMUN.js +441 -0
  6. package/dist/assets/index-Xs4q7pNo.css +1 -0
  7. package/dist/index.html +14 -0
  8. package/dist/wrenai_logo.png +0 -0
  9. package/dist/wrenai_logo_white.png +0 -0
  10. package/dist-server/harness/auth/detect.d.ts +18 -0
  11. package/dist-server/harness/auth/detect.js +37 -0
  12. package/dist-server/harness/auth/index.d.ts +4 -0
  13. package/dist-server/harness/auth/index.js +2 -0
  14. package/dist-server/harness/auth/probe.d.ts +18 -0
  15. package/dist-server/harness/auth/probe.js +52 -0
  16. package/dist-server/harness/auth/types.d.ts +42 -0
  17. package/dist-server/harness/auth/types.js +9 -0
  18. package/dist-server/harness/bundle/loader.d.ts +32 -0
  19. package/dist-server/harness/bundle/loader.js +43 -0
  20. package/dist-server/harness/bundle/schema.d.ts +199 -0
  21. package/dist-server/harness/bundle/schema.js +127 -0
  22. package/dist-server/harness/bundle/version.d.ts +10 -0
  23. package/dist-server/harness/bundle/version.js +43 -0
  24. package/dist-server/harness/capability/gate.d.ts +6 -0
  25. package/dist-server/harness/capability/gate.js +29 -0
  26. package/dist-server/harness/capability/registry.d.ts +6 -0
  27. package/dist-server/harness/capability/registry.js +17 -0
  28. package/dist-server/harness/cli-args.d.ts +123 -0
  29. package/dist-server/harness/cli-args.js +247 -0
  30. package/dist-server/harness/cli.d.ts +2 -0
  31. package/dist-server/harness/cli.js +151 -0
  32. package/dist-server/harness/compile/cache.d.ts +23 -0
  33. package/dist-server/harness/compile/cache.js +109 -0
  34. package/dist-server/harness/compile/compose-profile.d.ts +34 -0
  35. package/dist-server/harness/compile/compose-profile.js +95 -0
  36. package/dist-server/harness/compile/errors.d.ts +22 -0
  37. package/dist-server/harness/compile/errors.js +41 -0
  38. package/dist-server/harness/compile/fingerprint.d.ts +22 -0
  39. package/dist-server/harness/compile/fingerprint.js +84 -0
  40. package/dist-server/harness/compile/index.d.ts +8 -0
  41. package/dist-server/harness/compile/index.js +7 -0
  42. package/dist-server/harness/compile/pipeline.d.ts +66 -0
  43. package/dist-server/harness/compile/pipeline.js +176 -0
  44. package/dist-server/harness/compile/resolve-binary.d.ts +55 -0
  45. package/dist-server/harness/compile/resolve-binary.js +133 -0
  46. package/dist-server/harness/compile/types.d.ts +107 -0
  47. package/dist-server/harness/compile/types.js +1 -0
  48. package/dist-server/harness/compile/warble-identity.d.ts +12 -0
  49. package/dist-server/harness/compile/warble-identity.js +33 -0
  50. package/dist-server/harness/compliance/gate.d.ts +35 -0
  51. package/dist-server/harness/compliance/gate.js +48 -0
  52. package/dist-server/harness/compliance/index.d.ts +2 -0
  53. package/dist-server/harness/compliance/index.js +1 -0
  54. package/dist-server/harness/compliance/types.d.ts +16 -0
  55. package/dist-server/harness/compliance/types.js +1 -0
  56. package/dist-server/harness/events/emitter.d.ts +34 -0
  57. package/dist-server/harness/events/emitter.js +25 -0
  58. package/dist-server/harness/events/index.d.ts +4 -0
  59. package/dist-server/harness/events/index.js +2 -0
  60. package/dist-server/harness/events/sse.d.ts +20 -0
  61. package/dist-server/harness/events/sse.js +48 -0
  62. package/dist-server/harness/events/types.d.ts +131 -0
  63. package/dist-server/harness/events/types.js +1 -0
  64. package/dist-server/harness/exec/errors.d.ts +16 -0
  65. package/dist-server/harness/exec/errors.js +29 -0
  66. package/dist-server/harness/exec/index.d.ts +5 -0
  67. package/dist-server/harness/exec/index.js +2 -0
  68. package/dist-server/harness/exec/local.d.ts +32 -0
  69. package/dist-server/harness/exec/local.js +134 -0
  70. package/dist-server/harness/exec/policy.d.ts +12 -0
  71. package/dist-server/harness/exec/policy.js +1 -0
  72. package/dist-server/harness/exec/types.d.ts +63 -0
  73. package/dist-server/harness/exec/types.js +1 -0
  74. package/dist-server/harness/guardrails/index.d.ts +2 -0
  75. package/dist-server/harness/guardrails/index.js +1 -0
  76. package/dist-server/harness/guardrails/policy.d.ts +45 -0
  77. package/dist-server/harness/guardrails/policy.js +57 -0
  78. package/dist-server/harness/index.d.ts +37 -0
  79. package/dist-server/harness/index.js +19 -0
  80. package/dist-server/harness/loop/errors.d.ts +53 -0
  81. package/dist-server/harness/loop/errors.js +72 -0
  82. package/dist-server/harness/loop/executor.d.ts +123 -0
  83. package/dist-server/harness/loop/executor.js +380 -0
  84. package/dist-server/harness/loop/guard.d.ts +16 -0
  85. package/dist-server/harness/loop/guard.js +1 -0
  86. package/dist-server/harness/loop/index.d.ts +5 -0
  87. package/dist-server/harness/loop/index.js +3 -0
  88. package/dist-server/harness/native-producer/index.d.ts +109 -0
  89. package/dist-server/harness/native-producer/index.js +302 -0
  90. package/dist-server/harness/npm-package-resolve.d.ts +28 -0
  91. package/dist-server/harness/npm-package-resolve.js +47 -0
  92. package/dist-server/harness/providers/adapters/anthropic.d.ts +7 -0
  93. package/dist-server/harness/providers/adapters/anthropic.js +6 -0
  94. package/dist-server/harness/providers/adapters/mock.d.ts +4 -0
  95. package/dist-server/harness/providers/adapters/mock.js +5 -0
  96. package/dist-server/harness/providers/adapters/openai-compatible.d.ts +10 -0
  97. package/dist-server/harness/providers/adapters/openai-compatible.js +10 -0
  98. package/dist-server/harness/providers/binding.d.ts +23 -0
  99. package/dist-server/harness/providers/binding.js +13 -0
  100. package/dist-server/harness/providers/errors.d.ts +6 -0
  101. package/dist-server/harness/providers/errors.js +12 -0
  102. package/dist-server/harness/providers/index.d.ts +18 -0
  103. package/dist-server/harness/providers/index.js +22 -0
  104. package/dist-server/harness/providers/registry.d.ts +16 -0
  105. package/dist-server/harness/providers/registry.js +24 -0
  106. package/dist-server/harness/render/envelope.d.ts +145 -0
  107. package/dist-server/harness/render/envelope.js +428 -0
  108. package/dist-server/harness/render/errors.d.ts +33 -0
  109. package/dist-server/harness/render/errors.js +46 -0
  110. package/dist-server/harness/render/index.d.ts +5 -0
  111. package/dist-server/harness/render/index.js +3 -0
  112. package/dist-server/harness/render/validate.d.ts +13 -0
  113. package/dist-server/harness/render/validate.js +88 -0
  114. package/dist-server/harness/route/adapter-spec.d.ts +40 -0
  115. package/dist-server/harness/route/adapter-spec.js +61 -0
  116. package/dist-server/harness/route/agent-sdk-cli.d.ts +30 -0
  117. package/dist-server/harness/route/agent-sdk-cli.js +86 -0
  118. package/dist-server/harness/route/agent-sdk-manifest.d.ts +31 -0
  119. package/dist-server/harness/route/agent-sdk-manifest.js +56 -0
  120. package/dist-server/harness/route/artifact-content.d.ts +53 -0
  121. package/dist-server/harness/route/artifact-content.js +83 -0
  122. package/dist-server/harness/route/chat-event-mapper.d.ts +95 -0
  123. package/dist-server/harness/route/chat-event-mapper.js +164 -0
  124. package/dist-server/harness/route/codex-ask-events.d.ts +25 -0
  125. package/dist-server/harness/route/codex-ask-events.js +395 -0
  126. package/dist-server/harness/route/codex-ask.d.ts +15 -0
  127. package/dist-server/harness/route/codex-ask.js +242 -0
  128. package/dist-server/harness/route/codex-local-cli.d.ts +20 -0
  129. package/dist-server/harness/route/codex-local-cli.js +78 -0
  130. package/dist-server/harness/route/codex-local-manifest.d.ts +22 -0
  131. package/dist-server/harness/route/codex-local-manifest.js +180 -0
  132. package/dist-server/harness/route/describe.d.ts +54 -0
  133. package/dist-server/harness/route/describe.js +84 -0
  134. package/dist-server/harness/route/dispatched.d.ts +138 -0
  135. package/dist-server/harness/route/dispatched.js +339 -0
  136. package/dist-server/harness/route/in-process.d.ts +43 -0
  137. package/dist-server/harness/route/in-process.js +106 -0
  138. package/dist-server/harness/route/index.d.ts +19 -0
  139. package/dist-server/harness/route/index.js +12 -0
  140. package/dist-server/harness/route/profile-source.d.ts +37 -0
  141. package/dist-server/harness/route/profile-source.js +76 -0
  142. package/dist-server/harness/route/route.d.ts +33 -0
  143. package/dist-server/harness/route/route.js +114 -0
  144. package/dist-server/harness/route/tier-binding.d.ts +36 -0
  145. package/dist-server/harness/route/tier-binding.js +63 -0
  146. package/dist-server/harness/route/types.d.ts +286 -0
  147. package/dist-server/harness/route/types.js +1 -0
  148. package/dist-server/harness/session/errors.d.ts +4 -0
  149. package/dist-server/harness/session/errors.js +7 -0
  150. package/dist-server/harness/session/gate.d.ts +11 -0
  151. package/dist-server/harness/session/gate.js +13 -0
  152. package/dist-server/harness/session/index.d.ts +5 -0
  153. package/dist-server/harness/session/index.js +3 -0
  154. package/dist-server/harness/session/run.d.ts +60 -0
  155. package/dist-server/harness/session/run.js +333 -0
  156. package/dist-server/harness/session/types.d.ts +29 -0
  157. package/dist-server/harness/session/types.js +1 -0
  158. package/dist-server/harness/setup/codex-events.d.ts +12 -0
  159. package/dist-server/harness/setup/codex-events.js +108 -0
  160. package/dist-server/harness/setup/runner.d.ts +497 -0
  161. package/dist-server/harness/setup/runner.js +1314 -0
  162. package/dist-server/harness/tools/errors.d.ts +107 -0
  163. package/dist-server/harness/tools/errors.js +141 -0
  164. package/dist-server/harness/tools/index.d.ts +13 -0
  165. package/dist-server/harness/tools/index.js +8 -0
  166. package/dist-server/harness/tools/mcp.d.ts +26 -0
  167. package/dist-server/harness/tools/mcp.js +15 -0
  168. package/dist-server/harness/tools/native.d.ts +179 -0
  169. package/dist-server/harness/tools/native.js +322 -0
  170. package/dist-server/harness/tools/parse.d.ts +14 -0
  171. package/dist-server/harness/tools/parse.js +18 -0
  172. package/dist-server/harness/tools/resolve-wren-binary.d.ts +12 -0
  173. package/dist-server/harness/tools/resolve-wren-binary.js +21 -0
  174. package/dist-server/harness/tools/resolve.d.ts +51 -0
  175. package/dist-server/harness/tools/resolve.js +79 -0
  176. package/dist-server/harness/tools/setup-native.d.ts +84 -0
  177. package/dist-server/harness/tools/setup-native.js +353 -0
  178. package/dist-server/harness/tools/wiring.d.ts +10 -0
  179. package/dist-server/harness/tools/wiring.js +16 -0
  180. package/dist-server/providers/setup.provider.yaml +31 -0
  181. package/dist-server/providers/wren.provider.yaml +33 -0
  182. package/dist-server/server/adopt.d.ts +87 -0
  183. package/dist-server/server/adopt.js +263 -0
  184. package/dist-server/server/app.d.ts +6 -0
  185. package/dist-server/server/app.js +2521 -0
  186. package/dist-server/server/auth-choice.d.ts +16 -0
  187. package/dist-server/server/auth-choice.js +36 -0
  188. package/dist-server/server/bin.d.ts +2 -0
  189. package/dist-server/server/bin.js +601 -0
  190. package/dist-server/server/bootstrap-project-binding.d.ts +31 -0
  191. package/dist-server/server/bootstrap-project-binding.js +71 -0
  192. package/dist-server/server/clarify.d.ts +10 -0
  193. package/dist-server/server/clarify.js +17 -0
  194. package/dist-server/server/codex-setup-mcp.d.ts +2 -0
  195. package/dist-server/server/codex-setup-mcp.js +147 -0
  196. package/dist-server/server/compose.d.ts +89 -0
  197. package/dist-server/server/compose.js +271 -0
  198. package/dist-server/server/conn-config.d.ts +30 -0
  199. package/dist-server/server/conn-config.js +253 -0
  200. package/dist-server/server/context-files.d.ts +12 -0
  201. package/dist-server/server/context-files.js +117 -0
  202. package/dist-server/server/context-map.d.ts +30 -0
  203. package/dist-server/server/context-map.js +95 -0
  204. package/dist-server/server/context-source.d.ts +68 -0
  205. package/dist-server/server/context-source.js +85 -0
  206. package/dist-server/server/db.d.ts +862 -0
  207. package/dist-server/server/db.js +1839 -0
  208. package/dist-server/server/enrichment-runner.d.ts +184 -0
  209. package/dist-server/server/enrichment-runner.js +213 -0
  210. package/dist-server/server/enrichment-verify.d.ts +73 -0
  211. package/dist-server/server/enrichment-verify.js +290 -0
  212. package/dist-server/server/enrichment.d.ts +232 -0
  213. package/dist-server/server/enrichment.js +199 -0
  214. package/dist-server/server/env-detect.d.ts +8 -0
  215. package/dist-server/server/env-detect.js +17 -0
  216. package/dist-server/server/env-field-hints.d.ts +46 -0
  217. package/dist-server/server/env-field-hints.js +48 -0
  218. package/dist-server/server/fold.d.ts +133 -0
  219. package/dist-server/server/fold.js +407 -0
  220. package/dist-server/server/harness-profile-sources.d.ts +3 -0
  221. package/dist-server/server/harness-profile-sources.js +24 -0
  222. package/dist-server/server/harness.d.ts +19 -0
  223. package/dist-server/server/harness.js +439 -0
  224. package/dist-server/server/impact.d.ts +18 -0
  225. package/dist-server/server/impact.js +85 -0
  226. package/dist-server/server/interactive-terminal.d.ts +175 -0
  227. package/dist-server/server/interactive-terminal.js +336 -0
  228. package/dist-server/server/native-artifacts.d.ts +1004 -0
  229. package/dist-server/server/native-artifacts.js +564 -0
  230. package/dist-server/server/native-dispatch-registry.d.ts +42 -0
  231. package/dist-server/server/native-dispatch-registry.js +45 -0
  232. package/dist-server/server/native-producer-fence.d.ts +14 -0
  233. package/dist-server/server/native-producer-fence.js +48 -0
  234. package/dist-server/server/native-session-resume.d.ts +6 -0
  235. package/dist-server/server/native-session-resume.js +103 -0
  236. package/dist-server/server/native-session-workspace.d.ts +40 -0
  237. package/dist-server/server/native-session-workspace.js +175 -0
  238. package/dist-server/server/native-sessions.d.ts +374 -0
  239. package/dist-server/server/native-sessions.js +1517 -0
  240. package/dist-server/server/native-wren-runtime.d.ts +20 -0
  241. package/dist-server/server/native-wren-runtime.js +259 -0
  242. package/dist-server/server/node-pty-host.d.ts +31 -0
  243. package/dist-server/server/node-pty-host.js +100 -0
  244. package/dist-server/server/route-intent.d.ts +40 -0
  245. package/dist-server/server/route-intent.js +23 -0
  246. package/dist-server/server/runtime-binding.d.ts +52 -0
  247. package/dist-server/server/runtime-binding.js +258 -0
  248. package/dist-server/server/source-catalog.d.ts +84 -0
  249. package/dist-server/server/source-catalog.js +221 -0
  250. package/dist-server/server/source-driver.d.ts +35 -0
  251. package/dist-server/server/source-driver.js +124 -0
  252. package/dist-server/server/spa.d.ts +2 -0
  253. package/dist-server/server/spa.js +51 -0
  254. package/dist-server/server/subscription-model-catalog.d.ts +27 -0
  255. package/dist-server/server/subscription-model-catalog.js +203 -0
  256. package/dist-server/server/turn.d.ts +249 -0
  257. package/dist-server/server/turn.js +1074 -0
  258. package/dist-server/server/wire-types.d.ts +799 -0
  259. package/dist-server/server/wire-types.js +1 -0
  260. package/dist-server/server/wren-home.d.ts +19 -0
  261. package/dist-server/server/wren-home.js +33 -0
  262. package/dist-server/server/wren-profiles.d.ts +43 -0
  263. package/dist-server/server/wren-profiles.js +123 -0
  264. package/package.json +93 -0
  265. package/profiles/genbi-default/context/binding.yml +9 -0
  266. package/profiles/genbi-default/ir.golden.json +506 -0
  267. package/profiles/genbi-default/profile.yml +16 -0
  268. package/profiles/genbi-enrich-context/components/apply_enrichment/component.yml +42 -0
  269. package/profiles/genbi-enrich-context/components/draft_enrichment/component.yml +30 -0
  270. package/profiles/genbi-enrich-context/components/draft_enrichment/steps/draft.md +34 -0
  271. package/profiles/genbi-enrich-context/components/inspect_context/component.yml +32 -0
  272. package/profiles/genbi-enrich-context/components/inspect_context/steps/inspect.md +13 -0
  273. package/profiles/genbi-enrich-context/context/binding.yml +4 -0
  274. package/profiles/genbi-enrich-context/ir.golden.json +302 -0
  275. package/profiles/genbi-enrich-context/profile.yml +15 -0
  276. package/profiles/genbi-monitor/context/binding.yml +7 -0
  277. package/profiles/genbi-monitor/ir.golden.json +188 -0
  278. package/profiles/genbi-monitor/profile.yml +27 -0
  279. package/profiles/genbi-setup/components/build_context/component.yml +44 -0
  280. package/profiles/genbi-setup/components/build_context/steps/build.md +14 -0
  281. package/profiles/genbi-setup/components/connect_source/component.yml +51 -0
  282. package/profiles/genbi-setup/components/connect_source/steps/connect.md +23 -0
  283. package/profiles/genbi-setup/context/binding.yml +8 -0
  284. package/profiles/genbi-setup/ir.golden.json +140 -0
  285. package/profiles/genbi-setup/profile.yml +18 -0
  286. package/profiles/genbi-setup/raw/schema.json +5 -0
@@ -0,0 +1,1314 @@
1
+ /**
2
+ * Agentic setup/connect flow — BFF wiring.
3
+ *
4
+ * The Setup wizard's CONNECT step onboards a NEW wren project by dispatching
5
+ * warble's `connect_source` component via a dispatched (claude-agent-sdk)
6
+ * subprocess. Design decisions this file encodes (see the ticket for full
7
+ * rationale):
8
+ *
9
+ * - Setup state lives on disk (the scaffolded project + its `.env`), not
10
+ * only in the agent conversation: each setup turn is independently
11
+ * re-verifiable via `parseSetupTerminal`'s on-disk artifact checks below,
12
+ * regardless of which mechanism served the turn. Historically every setup
13
+ * turn was ALSO a fresh dispatched dispatch with no SDK-level continuity
14
+ * across turns (a disk-state-only "Plan B" re-composition — see
15
+ * `server/compose.ts`'s `composeSetupPrompt`'s `resumeFromDisk` option).
16
+ * `SetupStepRunOptions.resumeSessionId`/`SetupStepRunResult.sessionId`
17
+ * below add the alternative "Plan A": resuming the SAME underlying
18
+ * agent-sdk conversation (`DispatchedOptions.resumeSessionId` /
19
+ * `DispatchedResult.sessionId`, `../route/dispatched.js`) when a caller has one to
20
+ * resume from, so a `max_turns_continue` decision (`server/turn.ts`) can
21
+ * give the agent a fresh turn budget without making it re-orient from
22
+ * scratch (re-`ls`, re-`Read`, re-fetch skills). Both remain available:
23
+ * a caller with no session id to resume gets the original disk-state
24
+ * behavior unchanged.
25
+ * - The terminal contract (`SETUP_STATUS: ok|needs_input|error`) lives in
26
+ * the composed prompt, not in warble — `parseSetupTerminal` below parses
27
+ * it back out of the turn's finalText, and independently verifies
28
+ * `<root>/<name>/wren_project.yml` on disk before trusting an `ok`.
29
+ * - `SetupStepRunner` is a backend-abstract seam: `DispatchedSetupRunner`
30
+ * (claude-agent-sdk subprocess, subscription auth) and `InProcessSetupRunner`
31
+ * (in-process vercel loop, api-key/local/gateway auth) both implement it,
32
+ * and a caller picks between them purely on `authChoice.mode` without
33
+ * otherwise caring which one served a given turn.
34
+ */
35
+ import { createHash } from "node:crypto";
36
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
37
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
38
+ import { spawn } from "node:child_process";
39
+ import { createInterface } from "node:readline";
40
+ import os from "node:os";
41
+ import path from "node:path";
42
+ import { fileURLToPath } from "node:url";
43
+ import { loadBundleWithProvenance } from "../bundle/loader.js";
44
+ import { runWarble } from "../compile/pipeline.js";
45
+ import { resolveWarbleBinary } from "../compile/resolve-binary.js";
46
+ import { createAgentEventEmitter } from "../events/index.js";
47
+ import { createLocalExecutionEnv } from "../exec/index.js";
48
+ import { executeAgent, StepBudgetExhaustedError } from "../loop/index.js";
49
+ // Re-exported: `InProcessSetupRunner.run()` throws this (with an enriched,
50
+ // Mode-A-specific `.message` — see its `catch` block below) whenever the
51
+ // Dispatched agent's step budget is what ended the turn, not the model
52
+ // finishing on its own. Callers that want to distinguish that from any
53
+ // other setup-dispatch failure (e.g. via `instanceof`) can import it from
54
+ // here without also reaching into `harness/loop/index.js`.
55
+ export { StepBudgetExhaustedError };
56
+ import { createDefaultProviderRegistry } from "../providers/index.js";
57
+ import { deriveAdapterSpec } from "../route/adapter-spec.js";
58
+ import { resolveCodexLocalCli } from "../route/codex-local-cli.js";
59
+ import { runDispatchedDefault } from "../route/dispatched.js";
60
+ import { buildUniformTierBinding } from "../route/tier-binding.js";
61
+ import { createNativeToolRegistry, createSetupExecutionTool, SETUP_EXECUTION_TOOL_NAME } from "../tools/index.js";
62
+ import { withResolvedTools } from "../tools/wiring.js";
63
+ import { CodexSetupEventMapper } from "./codex-events.js";
64
+ /** The warble component the setup wizard's connect step dispatches. */
65
+ export const CONNECT_SOURCE_AGENT_ID = "connect_source";
66
+ /** The Warble component the setup wizard's context step dispatches (`profiles/genbi-setup/components/build_context`). */
67
+ export const BUILD_CONTEXT_AGENT_ID = "build_context";
68
+ /**
69
+ * Default agent-loop budget for setup turns, forwarded as `warble-agent-sdk
70
+ * chat --max-turns`. Well above the dispatcher's own default (40) because
71
+ * `build_context` agentically generates an MDL — many read/introspect/write/
72
+ * build/validate tool calls — and routinely blows past 40, failing the turn
73
+ * with `error_max_turns`. Overridable via `DispatchedSetupRunnerOptions.maxTurns`
74
+ * (wired to `WREN_HARNESS_SETUP_MAX_TURNS` in `server/bin.ts`).
75
+ */
76
+ export const DEFAULT_SETUP_MAX_TURNS = 120;
77
+ /** Single provider-aware selection seam shared by boot wiring and tests. */
78
+ export function selectSetupRunnerForAuth(authChoice, runners) {
79
+ if (authChoice.mode !== "subscription")
80
+ return runners.nonSubscription;
81
+ return authChoice.provider === "codex" ? runners.codexSubscription : runners.claudeSubscription;
82
+ }
83
+ /**
84
+ * Resolves the `--max-turns` budget for dispatching `agentId`, given an
85
+ * optional explicit override (`DispatchedSetupRunnerOptions.maxTurns`, wired from
86
+ * `WREN_HARNESS_SETUP_MAX_TURNS` in `server/bin.ts`). Shared by
87
+ * `DispatchedSetupRunner.run()` and `DispatchedSetupRunner.effectiveMaxTurns()` so the
88
+ * two can never disagree.
89
+ */
90
+ function resolveEffectiveMaxTurns(configuredMaxTurns, agentId) {
91
+ return configuredMaxTurns ?? (agentId === BUILD_CONTEXT_AGENT_ID ? DEFAULT_SETUP_MAX_TURNS : undefined);
92
+ }
93
+ const SAFE_PROJECT_NAME = /^[a-zA-Z0-9_-]+$/;
94
+ /**
95
+ * Subscription dispatchers have one cwd per turn, while their guarded setup
96
+ * tool accepts a separately bounded working root. Keep the initial scaffold
97
+ * at the workspace root. Immediately before a project-bound dispatch,
98
+ * canonicalize the existing workspace/project dirs and require the latter to
99
+ * be a strict descendant of the former. This is a pre-dispatch guard, not a
100
+ * claim that filesystem paths cannot change after the child process starts.
101
+ */
102
+ function resolveDispatchedUserProject(runOptions, agentId) {
103
+ const stepKey = runOptions.stepKey ?? (agentId === BUILD_CONTEXT_AGENT_ID ? "context" : "connect");
104
+ if (stepKey === "connect")
105
+ return runOptions.workspaceRoot;
106
+ const projectName = runOptions.projectName;
107
+ if (projectName === undefined || !SAFE_PROJECT_NAME.test(projectName)) {
108
+ throw new Error(`dispatched ${stepKey} requires a validated single-segment projectName`);
109
+ }
110
+ const canonicalDirectory = (directory, label) => {
111
+ if (!existsSync(directory))
112
+ throw new Error(`dispatched ${stepKey} ${label} must exist before dispatch`);
113
+ if (!statSync(directory).isDirectory())
114
+ throw new Error(`dispatched ${stepKey} ${label} must be a directory`);
115
+ return realpathSync(directory);
116
+ };
117
+ const canonicalWorkspace = canonicalDirectory(runOptions.workspaceRoot, "workspace root");
118
+ const projectDir = path.resolve(runOptions.workspaceRoot, projectName);
119
+ const canonicalProject = canonicalDirectory(projectDir, "project directory");
120
+ const relative = path.relative(canonicalWorkspace, canonicalProject);
121
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
122
+ throw new Error(`dispatched ${stepKey} project directory must be a strict descendant of the setup workspace root`);
123
+ }
124
+ return canonicalProject;
125
+ }
126
+ /**
127
+ * Dispatches a setup-wizard turn over dispatched, bypassing `route()` entirely
128
+ * (unlike the Ask turn path) so it can perform its own, setup-specific
129
+ * subscription-mode check with a clear, actionable error message rather than
130
+ * `route()`'s generic in-process/B branch. Uses `DispatchedOptions.irPath` to skip
131
+ * `compileProfile` — there is no bound wren project yet during setup, so
132
+ * there is nothing to compile a profile against; the setup dispatch always
133
+ * runs the same fixed, warble-committed IR regardless of which project is
134
+ * being onboarded.
135
+ */
136
+ export class DispatchedSetupRunner {
137
+ options;
138
+ constructor(options) {
139
+ this.options = options;
140
+ }
141
+ effectiveMaxTurns(agentId) {
142
+ return resolveEffectiveMaxTurns(this.options.maxTurns, agentId);
143
+ }
144
+ async run(runOptions) {
145
+ const { authChoice } = runOptions;
146
+ const agentId = runOptions.agentId ?? CONNECT_SOURCE_AGENT_ID;
147
+ // Only build_context needs the raised turn budget — generating an MDL is
148
+ // many tool calls. connect/connect_resume stay on the dispatcher's default
149
+ // (40) so a stuck connect (e.g. bad credentials retrying) still fails fast
150
+ // rather than burning 3x the runway first. An explicit
151
+ // DispatchedSetupRunnerOptions.maxTurns override, when set, applies to any turn.
152
+ // Kept in lockstep with `effectiveMaxTurns()` above via the shared
153
+ // `resolveEffectiveMaxTurns` helper.
154
+ const maxTurns = this.effectiveMaxTurns(agentId);
155
+ if (authChoice.mode !== "subscription") {
156
+ throw new Error(`Agentic setup requires subscription auth mode (dispatched) to dispatch "${agentId}" ` +
157
+ `via the claude-agent-sdk dispatcher — got "${authChoice.mode}". Set WREN_HARNESS_MODE=subscription ` +
158
+ "to enable the setup wizard's connect step.");
159
+ }
160
+ const modelsConfig = this.options.getModelsConfig?.();
161
+ const userProject = resolveDispatchedUserProject(runOptions, agentId);
162
+ const result = await runDispatchedDefault({
163
+ authChoice,
164
+ // Unused: `irPath` below makes `runDispatchedDefault` skip compileProfile
165
+ // entirely (see its short-circuit), so this value is never read. It is
166
+ // set to the same setup IR for clarity, not because it is consulted.
167
+ profileSource: this.options.irPath,
168
+ userProject,
169
+ question: runOptions.prompt,
170
+ agentId,
171
+ irPath: this.options.irPath,
172
+ ...(this.options.warbleBin !== undefined ? { warbleBin: this.options.warbleBin } : {}),
173
+ ...(this.options.agentSdkBin !== undefined ? { agentSdkBin: this.options.agentSdkBin } : {}),
174
+ ...(this.options.outDir !== undefined ? { outDir: this.options.outDir } : {}),
175
+ ...(this.options.workDir !== undefined ? { workDir: this.options.workDir } : {}),
176
+ ...(modelsConfig !== undefined ? { modelsConfig } : {}),
177
+ ...(maxTurns !== undefined ? { maxTurns } : {}),
178
+ ...(runOptions.onEvent !== undefined ? { onEvent: runOptions.onEvent } : {}),
179
+ ...(runOptions.resumeSessionId !== undefined ? { resumeSessionId: runOptions.resumeSessionId } : {}),
180
+ });
181
+ return { finalText: result.finalText, ...(result.sessionId !== undefined ? { sessionId: result.sessionId } : {}) };
182
+ }
183
+ }
184
+ const CODEX_BILLING_ENV_KEYS = new Set([
185
+ "OPENAI_API_KEY",
186
+ "CODEX_API_KEY",
187
+ "AZURE_OPENAI_API_KEY",
188
+ "OPENAI_ORGANIZATION",
189
+ "OPENAI_ORG_ID",
190
+ "OPENAI_PROJECT",
191
+ "OPENAI_PROJECT_ID",
192
+ ]);
193
+ async function resolveCodexSetupProducedField(irPath, agentId) {
194
+ let parsed;
195
+ try {
196
+ parsed = JSON.parse(await readFile(irPath, "utf8"));
197
+ }
198
+ catch {
199
+ throw new Error("Codex setup IR is not valid JSON");
200
+ }
201
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
202
+ throw new Error("Codex setup IR does not contain a valid component contract");
203
+ }
204
+ const components = parsed.components;
205
+ if (!Array.isArray(components)) {
206
+ throw new Error("Codex setup IR does not contain a valid component contract");
207
+ }
208
+ const matches = components.filter((component) => typeof component === "object" && component !== null && component.id === agentId);
209
+ const component = matches[0];
210
+ if (matches.length !== 1 || component === undefined || !Array.isArray(component.llm_calls) || component.llm_calls.length !== 1) {
211
+ throw new Error(`Codex setup component "${agentId}" does not declare exactly one produced field`);
212
+ }
213
+ const produces = component.llm_calls[0]?.produces;
214
+ if (typeof produces !== "string" || !produces.trim()) {
215
+ throw new Error(`Codex setup component "${agentId}" does not declare exactly one produced field`);
216
+ }
217
+ return produces;
218
+ }
219
+ function unwrapCodexSetupEnvelope(rawFinalText, expectedField) {
220
+ let parsed;
221
+ try {
222
+ parsed = JSON.parse(rawFinalText);
223
+ }
224
+ catch {
225
+ throw new Error("Codex setup final response is not valid JSON");
226
+ }
227
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
228
+ throw new Error("Codex setup final response must be a single-field JSON object");
229
+ }
230
+ const fields = Object.keys(parsed);
231
+ if (fields.length !== 1 || fields[0] !== expectedField) {
232
+ throw new Error(`Codex setup final response must contain only the declared "${expectedField}" field`);
233
+ }
234
+ const produced = parsed[expectedField];
235
+ if (typeof produced !== "string") {
236
+ throw new Error(`Codex setup final response field "${expectedField}" must be a string`);
237
+ }
238
+ return produced;
239
+ }
240
+ /**
241
+ * Setup-only Codex subscription runner. It invokes the target's `dispatch`
242
+ * command directly against the committed setup IR and exposes exactly one
243
+ * guarded MCP tool. Normal Ask routing never calls this class.
244
+ */
245
+ export class CodexSetupRunner {
246
+ options;
247
+ constructor(options) {
248
+ this.options = options;
249
+ }
250
+ async run(runOptions) {
251
+ if (runOptions.authChoice.mode !== "subscription" || runOptions.authChoice.provider !== "codex") {
252
+ throw new Error("Codex setup runner requires a Codex subscription auth choice");
253
+ }
254
+ const agentId = runOptions.agentId ?? CONNECT_SOURCE_AGENT_ID;
255
+ if (agentId !== CONNECT_SOURCE_AGENT_ID && agentId !== BUILD_CONTEXT_AGENT_ID) {
256
+ throw new Error(`Codex setup only supports "${CONNECT_SOURCE_AGENT_ID}" and "${BUILD_CONTEXT_AGENT_ID}"`);
257
+ }
258
+ const model = this.options.getStrongModel().trim();
259
+ if (!model)
260
+ throw new Error("Codex setup requires a configured strong-tier model");
261
+ const expectedProducedField = await resolveCodexSetupProducedField(this.options.irPath, agentId);
262
+ const cli = this.options.codexLocalCli ?? (await resolveCodexLocalCli(this.options.codexLocalBin));
263
+ const traceDir = this.options.mcpServer ? undefined : await mkdtemp(path.join(os.tmpdir(), "wren-codex-setup-trace-"));
264
+ const tracePath = traceDir ? path.join(traceDir, "setup-execution.jsonl") : undefined;
265
+ if (tracePath)
266
+ await writeFile(tracePath, "", { encoding: "utf8", mode: 0o600 });
267
+ const mcp = this.options.mcpServer ?? defaultCodexSetupMcpInvocation(tracePath);
268
+ const timeoutMs = this.options.timeoutMs ?? 10 * 60 * 1000;
269
+ const turnRoot = resolveDispatchedUserProject(runOptions, agentId);
270
+ const args = [
271
+ ...cli.prefixArgs,
272
+ "dispatch",
273
+ this.options.irPath,
274
+ runOptions.prompt,
275
+ "--component",
276
+ agentId,
277
+ "--project",
278
+ turnRoot,
279
+ "--model",
280
+ model,
281
+ "--server",
282
+ "setup",
283
+ "--server-command",
284
+ mcp.command,
285
+ ...mcp.prefixArgs.flatMap((arg) => (arg.startsWith("-") ? [`--server-arg=${arg}`] : ["--server-arg", arg])),
286
+ "--server-arg=--workspace-root",
287
+ "--server-arg",
288
+ turnRoot,
289
+ "--source-tool",
290
+ SETUP_EXECUTION_TOOL_NAME,
291
+ "--context-tool",
292
+ SETUP_EXECUTION_TOOL_NAME,
293
+ "--timeout",
294
+ String(timeoutMs),
295
+ ...(this.options.codexBin ? ["--codex-bin", this.options.codexBin] : []),
296
+ "--stream-json",
297
+ ];
298
+ const emitter = createAgentEventEmitter(runOptions.onEvent);
299
+ emitter.emit({ kind: "run.start", mode: "B", agentId });
300
+ const mapper = new CodexSetupEventMapper(tracePath);
301
+ try {
302
+ const rawFinalText = await spawnCodexSetup(cli.command, args, mapper, emitter.emit, timeoutMs + 5_000);
303
+ const finalText = unwrapCodexSetupEnvelope(rawFinalText, expectedProducedField);
304
+ emitter.emit({ kind: "run.finish", status: "answer" });
305
+ return { finalText };
306
+ }
307
+ catch (error) {
308
+ const message = error instanceof Error ? error.message : String(error);
309
+ emitter.emit({ kind: "error", message });
310
+ emitter.emit({ kind: "run.finish", status: "error" });
311
+ throw error;
312
+ }
313
+ finally {
314
+ if (traceDir)
315
+ await rm(traceDir, { recursive: true, force: true });
316
+ }
317
+ }
318
+ }
319
+ function defaultCodexSetupMcpInvocation(tracePath) {
320
+ return {
321
+ command: process.execPath,
322
+ prefixArgs: [fileURLToPath(new URL("../../server/codex-setup-mcp.js", import.meta.url)), "--trace-path", tracePath],
323
+ };
324
+ }
325
+ function sanitizedCodexSetupEnvironment() {
326
+ return Object.fromEntries(Object.entries(process.env).filter(([key, value]) => value !== undefined && !CODEX_BILLING_ENV_KEYS.has(key.toUpperCase())));
327
+ }
328
+ function spawnCodexSetup(command, args, mapper, emit, timeoutMs) {
329
+ return new Promise((resolve, reject) => {
330
+ const child = spawn(command, [...args], {
331
+ env: sanitizedCodexSetupEnvironment(),
332
+ stdio: ["ignore", "pipe", "pipe"],
333
+ detached: process.platform !== "win32",
334
+ });
335
+ let stderr = "";
336
+ let settled = false;
337
+ let timedOut = false;
338
+ let killTimer;
339
+ const terminate = (signal) => {
340
+ if (child.pid !== undefined && process.platform !== "win32") {
341
+ try {
342
+ process.kill(-child.pid, signal);
343
+ return;
344
+ }
345
+ catch {
346
+ // Fall through to the direct child.
347
+ }
348
+ }
349
+ child.kill(signal);
350
+ };
351
+ const timer = setTimeout(() => {
352
+ timedOut = true;
353
+ terminate("SIGTERM");
354
+ killTimer = setTimeout(() => terminate("SIGKILL"), 1_000);
355
+ }, timeoutMs);
356
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
357
+ lines.on("line", (line) => {
358
+ if (settled || !line.trim())
359
+ return;
360
+ try {
361
+ const event = mapper.nextLine(line);
362
+ if (event)
363
+ emit(event);
364
+ }
365
+ catch (error) {
366
+ settled = true;
367
+ clearTimeout(timer);
368
+ terminate("SIGTERM");
369
+ killTimer = setTimeout(() => terminate("SIGKILL"), 1_000);
370
+ reject(error);
371
+ }
372
+ });
373
+ child.stderr.on("data", (chunk) => {
374
+ if (stderr.length < 8_192)
375
+ stderr += chunk.toString("utf8");
376
+ });
377
+ child.on("error", (error) => {
378
+ if (settled)
379
+ return;
380
+ settled = true;
381
+ clearTimeout(timer);
382
+ if (killTimer)
383
+ clearTimeout(killTimer);
384
+ reject(new Error(`warble-codex-local failed to start: ${error.message}`));
385
+ });
386
+ child.on("close", (code, signal) => {
387
+ clearTimeout(timer);
388
+ lines.close();
389
+ // Do not cancel an already-scheduled process-group SIGKILL here. The
390
+ // direct dispatcher can exit on SIGTERM while one of its descendants
391
+ // ignores the signal; the escalation still has to reap that descendant.
392
+ if (settled)
393
+ return;
394
+ settled = true;
395
+ if (timedOut) {
396
+ reject(new Error(`warble-codex-local timed out after ${timeoutMs}ms`));
397
+ }
398
+ else if (code !== 0) {
399
+ reject(new Error(`warble-codex-local exited with ${code ?? signal ?? "unknown"}: ${stderr.trim()}`));
400
+ }
401
+ else {
402
+ try {
403
+ resolve(mapper.result());
404
+ }
405
+ catch (error) {
406
+ reject(error);
407
+ }
408
+ }
409
+ });
410
+ });
411
+ }
412
+ /**
413
+ * This package's bundled setup capability-provider fragment
414
+ * (`providers/setup.provider.yaml`) — the default `--provider` for
415
+ * `InProcessSetupRunner`'s dispatch, mirroring `compileProfile`'s own
416
+ * `DEFAULT_WREN_PROVIDER_PATH` constant (`harness/compile/pipeline.ts`).
417
+ */
418
+ export const DEFAULT_SETUP_PROVIDER_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "providers", "setup.provider.yaml");
419
+ /**
420
+ * Dispatches a setup-wizard turn over in-process: the in-process vercel loop
421
+ * (`harness/loop/executor.ts`'s `executeAgent`), instead of shelling a
422
+ * claude-agent-sdk subprocess. Mirrors `DispatchedSetupRunner`'s own shape
423
+ * (same `SetupStepRunner` interface, same "bypass the per-user compile
424
+ * pipeline and dispatch the fixed, warble-committed setup IR directly"
425
+ * design) but reuses the in-process building blocks `harness/route/in-process.ts`'s
426
+ * `runInProcessDefault` wires together, minus the two pieces that don't apply to
427
+ * setup:
428
+ *
429
+ * - No `resolveWrenBinary()` preflight — the setup components' only tool
430
+ * (`setup_execution`, see `harness/tools/setup-native.ts`) never shells
431
+ * the `wren` CLI directly; it's a general scoped exec/write pair, not a
432
+ * wren-specific one.
433
+ * - No `deriveEnforcement(agent)` (`harness/guardrails/policy.ts`) — that
434
+ * function maps `read_only`/`scoped_write`/threshold guardrails onto an
435
+ * `EnforcementPolicy` and has no branch for the `setup_execution`
436
+ * guardrail shape (`{name: "setup_execution", locked: true, scope: "."}`)
437
+ * the two setup components declare. Rather than widen that shared module
438
+ * for a guardrail only this path uses, this runner builds its own fixed
439
+ * `ExecutionPolicy` below, matching what `setup_execution` actually grants
440
+ * on the dispatched (claude-agent-sdk) side: unrestricted exec (subject only
441
+ * to the native tool's own destructive/redirection denylist, never
442
+ * `readOnly`-gated) and a write scope of the whole workspace root.
443
+ *
444
+ * Unlike `DispatchedSetupRunner`, which forwards its `--max-turns` budget to a
445
+ * subprocess CLI flag, this runner's turn budget is an in-process
446
+ * `ExecuteAgentContext.maxSteps` (an `isStepCount` stop condition on the
447
+ * step's underlying `ToolLoopAgent`) — see `effectiveMaxTurns()` below for
448
+ * how it's resolved, and `StepBudgetExhaustedError` (`harness/loop/errors.ts`)
449
+ * for how a turn that runs out of steps before finishing is reported.
450
+ *
451
+ * Also calls `executeAgent` directly rather than the higher-level
452
+ * `runAgent()` in-process's Ask-turn path uses: `runAgent` asserts every
453
+ * `agent.capabilities[]` entry against a `CapabilityRegistry`, but
454
+ * `connect_source`/`build_context` declare `source_connect`/`context_build`
455
+ * — capabilities outside the fixed `DEFAULT_CAPABILITIES` set a wren-project
456
+ * Ask turn resolves. `executeAgent` (confirmed via `ExecuteAgentContext`'s
457
+ * own shape) has no `capabilityRegistry` concept at all, so it runs the
458
+ * bundle's dataflow with no capability assertion — exactly what dispatching
459
+ * a setup-only capability needs, without inventing a parallel registry for
460
+ * two names nothing else provides.
461
+ */
462
+ export class InProcessSetupRunner {
463
+ options;
464
+ constructor(options) {
465
+ this.options = options;
466
+ }
467
+ /**
468
+ * The `ExecuteAgentContext.maxSteps` budget this runner will actually pass
469
+ * into `executeAgent` for a given `agentId` — the same value `run()`
470
+ * computes internally. This was previously documented as always
471
+ * `undefined` on the theory that "`executeAgent` has no turn-budget
472
+ * concept at all" — that was already false when it was written: every
473
+ * tools-bearing step ran as a `ToolLoopAgent` turn, and `ToolLoopAgent`
474
+ * itself silently defaults to a 20-step budget (`ai`'s
475
+ * `stopWhen: this.settings.stopWhen ?? isStepCount(20)`) whenever nothing
476
+ * overrides it. There was a real, unconfigurable ceiling the whole time;
477
+ * it just had no name and nothing here reported it. Now that `run()` below
478
+ * threads an explicit `maxSteps` through, this reports the real value —
479
+ * mirroring `DispatchedSetupRunner.effectiveMaxTurns` — via the same
480
+ * `resolveEffectiveMaxTurns` helper so the two can never disagree.
481
+ */
482
+ effectiveMaxTurns(agentId) {
483
+ return resolveEffectiveMaxTurns(this.options.maxTurns, agentId);
484
+ }
485
+ async run(runOptions) {
486
+ const { authChoice } = runOptions;
487
+ const agentId = runOptions.agentId ?? CONNECT_SOURCE_AGENT_ID;
488
+ if (authChoice.mode === "subscription") {
489
+ throw new Error(`in-process setup dispatch requires an api-key/local/gateway auth mode to dispatch "${agentId}" ` +
490
+ `via the in-process vercel loop — got "subscription". Use DispatchedSetupRunner ` +
491
+ "(WREN_HARNESS_MODE=subscription) for a subscription auth choice instead.");
492
+ }
493
+ const warbleBin = await resolveWarbleBinary(this.options.warbleBin);
494
+ const providerPath = this.options.providerPath ?? DEFAULT_SETUP_PROVIDER_PATH;
495
+ const callerSuppliedOutDir = this.options.outDir !== undefined;
496
+ const outDir = this.options.outDir ?? (await mkdtemp(path.join(os.tmpdir(), "wren-harness-setup-in-process-")));
497
+ // `executeAgent` emits the UN-stamped `AgentEventInput` shape (no
498
+ // `runId`/`seq` — it doesn't know either); `SetupStepRunOptions.onEvent`
499
+ // is the caller-facing, fully-stamped `AgentEvent` shape. Wrapping via
500
+ // `createAgentEventEmitter` (exactly how `runAgent` itself bridges the
501
+ // same two shapes in `harness/session/run.ts`) is what stamps every
502
+ // event from this turn with one shared `runId` and a monotonic `seq`
503
+ // before it reaches `runOptions.onEvent`. The emitter itself no-ops when
504
+ // `runOptions.onEvent` is undefined, so `emitter.emit` is always safe to
505
+ // pass unconditionally.
506
+ const emitter = createAgentEventEmitter(runOptions.onEvent);
507
+ emitter.emit({ kind: "run.start", mode: "A", agentId });
508
+ try {
509
+ await runWarble(warbleBin, [
510
+ "dispatch",
511
+ "--target",
512
+ "vercel",
513
+ "--provider",
514
+ providerPath,
515
+ this.options.irPath,
516
+ "--out",
517
+ outDir,
518
+ ]);
519
+ const bundlePath = path.join(outDir, "bundle.json");
520
+ const bundle = loadBundleWithProvenance(JSON.parse(await readFile(bundlePath, "utf-8")), {
521
+ warbleBin,
522
+ profileSource: this.options.irPath,
523
+ });
524
+ const agent = bundle.agents.find((candidate) => candidate.id === agentId);
525
+ if (!agent) {
526
+ throw new Error(`compiled setup bundle has no "${agentId}" agent`);
527
+ }
528
+ const adapterSpec = this.options.getStrongAdapterSpec
529
+ ? this.options.getStrongAdapterSpec(authChoice)
530
+ : deriveAdapterSpec(authChoice, this.options.model !== undefined ? { model: this.options.model } : {});
531
+ const binding = buildUniformTierBinding(agent, adapterSpec);
532
+ const registry = createDefaultProviderRegistry();
533
+ // Hardcoded, NOT `deriveEnforcement(agent)` — see this class's doc
534
+ // comment above for why. `artifactWriteScope: "."` resolves against
535
+ // `env`'s `rootDir` below, which IS `workspaceRoot` — so "." means
536
+ // "anywhere under workspaceRoot", matching `setup_execution`'s
537
+ // project-root write scope on the dispatched side.
538
+ const policy = { readOnly: false, artifactWriteScope: "." };
539
+ const env = createLocalExecutionEnv({ rootDir: runOptions.workspaceRoot });
540
+ const nativeTools = createNativeToolRegistry();
541
+ nativeTools.register(SETUP_EXECUTION_TOOL_NAME, () => createSetupExecutionTool({ env, policy, workspaceRoot: runOptions.workspaceRoot }));
542
+ // Kept in lockstep with `effectiveMaxTurns()` above via the shared
543
+ // `resolveEffectiveMaxTurns` helper, exactly like `DispatchedSetupRunner.run()`
544
+ // does for its own `--max-turns` flag.
545
+ const maxSteps = this.effectiveMaxTurns(agentId);
546
+ const artifacts = await withResolvedTools(agent, { nativeTools, enforcementPolicy: policy, executionEnv: env }, (tools) => executeAgent(agent, {
547
+ binding,
548
+ registry,
549
+ tools,
550
+ userInput: runOptions.prompt,
551
+ onEvent: emitter.emit,
552
+ ...(maxSteps !== undefined ? { maxSteps } : {}),
553
+ }));
554
+ // Every setup component has exactly one step (`connect` / `build`), so
555
+ // its `produces` artifact is the turn's final answer — mirroring
556
+ // `DispatchedSetupRunner`'s `result.finalText`, which is likewise "the last
557
+ // thing the dispatched turn said."
558
+ const producedName = agent.steps[0]?.produces;
559
+ const finalText = producedName !== undefined ? String(artifacts.get(producedName) ?? "") : "";
560
+ // `"answer"` is the closest of `RunFinishEvent.status`'s three literals
561
+ // ("answer" | "refusal" | "error") to "a setup turn completed" — setup
562
+ // has no envelope-gated refusal concept of its own (see this class's
563
+ // doc comment), so a completed dispatch is reported the same way
564
+ // `runAgent` reports a completed Ask turn that produced output.
565
+ emitter.emit({ kind: "answer", text: finalText });
566
+ emitter.emit({ kind: "run.finish", status: "answer" });
567
+ // `sessionId: null`, not omitted: in-process has no SDK subprocess
568
+ // conversation to resume (`executeAgent` re-runs the bundle's dataflow
569
+ // from scratch every call) — `null` is `SetupStepRunResult`'s
570
+ // documented "the dispatcher itself reported no session id at all,"
571
+ // which is exactly this case, distinct from a runner that simply never
572
+ // set the field.
573
+ return { finalText, sessionId: null };
574
+ }
575
+ catch (error) {
576
+ // A `StepBudgetExhaustedError` is a distinguishable, honest failure
577
+ // mode, not a generic one — surface that here rather than letting it
578
+ // fall through as a bare "final message did not contain a
579
+ // SETUP_STATUS line" once it reaches `parseSetupTerminal`'s caller
580
+ // (which never even runs `parseSetupTerminal` on a thrown `run()`
581
+ // error — see `executeSetupTurn`, `server/turn.ts` — it reports
582
+ // `error.message` directly). No literal "Continue" checkpoint is
583
+ // offered here (out of scope for this runner: in-process has no SDK
584
+ // session to resume — `sessionId: null` below, always) — just an
585
+ // honest message pointing at where any partial work may have landed.
586
+ if (error instanceof StepBudgetExhaustedError) {
587
+ // Mutate `.message` in place (legal — only the constructor
588
+ // parameters above are `readonly`, not the inherited `Error.message`)
589
+ // rather than wrapping in a fresh `Error`: callers upstream (or
590
+ // future ones) can still `instanceof StepBudgetExhaustedError` this
591
+ // and read `.maxSteps`/`.stepId`/`.stepsCompleted`, while
592
+ // `server/turn.ts`'s generic `failWithError(error.message, ...)`
593
+ // path — which only ever reads `.message` — sees the enriched,
594
+ // Mode-A-specific text instead of the generic one from
595
+ // `harness/loop/errors.ts`.
596
+ error.message =
597
+ `setup step "${agentId}" ran out of steps (${error.maxSteps} max) before finishing — ` +
598
+ `its partial work, if any, may still be on disk under ${runOptions.workspaceRoot}`;
599
+ }
600
+ const message = error instanceof Error ? error.message : String(error);
601
+ // Mirrors `runAgent`'s own catch/emit/rethrow in `harness/session/run.ts`:
602
+ // a thrown failure still gets a `run.finish` bookend (status "error")
603
+ // rather than leaving the event stream hanging with a `run.start` and
604
+ // nothing after it.
605
+ emitter.emit({ kind: "error", message });
606
+ emitter.emit({ kind: "run.finish", status: "error" });
607
+ throw error;
608
+ }
609
+ finally {
610
+ if (!callerSuppliedOutDir) {
611
+ await rm(outDir, { recursive: true, force: true });
612
+ }
613
+ }
614
+ }
615
+ }
616
+ /**
617
+ * Reads a single top-level `field: value` line out of a YAML file using the
618
+ * same lightweight line-scan convention already used elsewhere in this
619
+ * codebase (e.g. `server/compose.ts`'s `countRelationships`/`listModelNames`)
620
+ * rather than pulling in a YAML parser dependency. Matches only an unindented
621
+ * `field:` key (so it can't be fooled by a same-named key nested under
622
+ * another mapping), strips a single layer of surrounding quotes, and returns
623
+ * `undefined` if the file is missing/unreadable or the field isn't present.
624
+ */
625
+ function readProjectYamlField(yamlPath, field) {
626
+ let content;
627
+ try {
628
+ content = readFileSync(yamlPath, "utf-8");
629
+ }
630
+ catch {
631
+ return undefined;
632
+ }
633
+ const pattern = new RegExp(`^${field}:\\s*(.+?)\\s*$`, "m");
634
+ const match = pattern.exec(content);
635
+ if (!match) {
636
+ return undefined;
637
+ }
638
+ const raw = match[1];
639
+ const unquoted = /^"(.*)"$/.exec(raw) ?? /^'(.*)'$/.exec(raw);
640
+ return (unquoted ? unquoted[1] : raw).trim();
641
+ }
642
+ /** Parses the small KEY=value subset of a project .env file used by Wren profiles. */
643
+ function parseDotEnv(text) {
644
+ const values = new Map();
645
+ for (const rawLine of text.split(/\r?\n/)) {
646
+ const line = rawLine.trim();
647
+ if (!line || line.startsWith("#"))
648
+ continue;
649
+ const assignment = line.startsWith("export ") ? line.slice("export ".length) : line;
650
+ const separator = assignment.indexOf("=");
651
+ if (separator === -1)
652
+ continue;
653
+ const key = assignment.slice(0, separator).trim();
654
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
655
+ continue;
656
+ const rawValue = assignment.slice(separator + 1).trim();
657
+ const quoted = /^(".*"|'.*')$/.test(rawValue) ? rawValue.slice(1, -1) : rawValue;
658
+ values.set(key, quoted);
659
+ }
660
+ return values;
661
+ }
662
+ function normalizeConnectionScheme(scheme) {
663
+ const normalized = scheme.replace(/:$/, "").toLowerCase();
664
+ return normalized === "postgresql" ? "postgres" : normalized;
665
+ }
666
+ /**
667
+ * Closed URL-scheme allowlist for the source types accepted by Setup. Keep
668
+ * this local rather than importing `server/app.ts`: harness must not depend
669
+ * on its BFF consumer. A source without an unambiguous host/database URL
670
+ * shape (for example DuckDB's filesystem `url`) deliberately has no entries
671
+ * and therefore cannot retain lifecycle proof through this URL path.
672
+ */
673
+ const URL_SCHEMES_BY_SOURCE = new Map([
674
+ ["postgres", new Set(["postgres"])],
675
+ ["mysql", new Set(["mysql", "mysql+pymysql", "mysql+mysqldb"])],
676
+ ["bigquery", new Set(["bigquery"])],
677
+ ["snowflake", new Set(["snowflake"])],
678
+ ["clickhouse", new Set(["clickhouse", "clickhouse+http", "clickhouse+https"])],
679
+ ["mssql", new Set(["mssql"])],
680
+ ["trino", new Set(["trino", "trino+https"])],
681
+ ["duckdb", new Set()],
682
+ ]);
683
+ /**
684
+ * Only these Setup sources have the host/port/database fields represented by
685
+ * the generic field fallback below. Other supported sources use different
686
+ * identity shapes (for example BigQuery project/dataset and Trino
687
+ * catalog/schema), so accepting generic DB_HOST/DB_DATABASE for them would
688
+ * forge an identity rather than derive the effective connection target.
689
+ */
690
+ const HOST_DATABASE_FIELD_SOURCES = new Set([
691
+ "postgres",
692
+ "mysql",
693
+ "clickhouse",
694
+ "mssql",
695
+ // Added when Setup began offering wren's full connector set. Each of these
696
+ // has host/port/database in its own wren connection model, so the generic
697
+ // fallback derives their identity rather than forging one. Sources whose
698
+ // identity is shaped differently — BigQuery's project/dataset, Trino's
699
+ // catalog/schema, Databricks' serverHostname/httpPath, Athena's S3 staging
700
+ // dir, Spark's bare host/port — are still deliberately absent.
701
+ "oracle",
702
+ "redshift",
703
+ "doris",
704
+ ]);
705
+ function connectionTargetFromUrl(value, sourceType) {
706
+ try {
707
+ const url = new URL(value);
708
+ const database = decodeURIComponent(url.pathname).replace(/^\/+|\/+$/g, "");
709
+ const scheme = normalizeConnectionScheme(url.protocol);
710
+ if (!url.protocol || !url.hostname || !database || !URL_SCHEMES_BY_SOURCE.get(sourceType)?.has(scheme))
711
+ return undefined;
712
+ // Intentionally do not return username, password, query, or fragment.
713
+ return {
714
+ scheme,
715
+ host: url.hostname.toLowerCase(),
716
+ port: url.port,
717
+ database,
718
+ };
719
+ }
720
+ catch {
721
+ return undefined;
722
+ }
723
+ }
724
+ function connectionTargetFromFields(scheme, host, port, database) {
725
+ if (!host || !database)
726
+ return undefined;
727
+ return { scheme: normalizeConnectionScheme(scheme), host: host.toLowerCase(), port: port ?? "", database };
728
+ }
729
+ /**
730
+ * Resolves a single effective value using Wren's shell-over-.env precedence.
731
+ * Multiple aliases that disagree are ambiguous, so retained proof must not be
732
+ * reused. The returned value is never persisted directly.
733
+ */
734
+ function effectiveConnectionValue(dotenv, aliases) {
735
+ const values = new Set();
736
+ for (const key of aliases) {
737
+ const value = process.env[key] ?? dotenv.get(key);
738
+ if (value !== undefined && value.trim())
739
+ values.add(value.trim());
740
+ }
741
+ return values.size === 1 ? [...values][0] : undefined;
742
+ }
743
+ function connectionAliases(sourceType) {
744
+ const source = sourceType.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_");
745
+ const generic = {
746
+ urls: [`${source}_URL`, "DATABASE_URL", "DB_URL"],
747
+ hosts: [`${source}_HOST`, "DB_HOST"],
748
+ ports: [`${source}_PORT`, "DB_PORT"],
749
+ databases: [`${source}_DATABASE`, `${source}_DB`, "DB_DATABASE", "DB_NAME"],
750
+ };
751
+ if (source !== "POSTGRES" && source !== "POSTGRESQL")
752
+ return generic;
753
+ return {
754
+ urls: ["POSTGRES_URL", "POSTGRESQL_URL", "PG_URL", ...generic.urls],
755
+ hosts: ["POSTGRES_HOST", "PGHOST", ...generic.hosts],
756
+ ports: ["POSTGRES_PORT", "PGPORT", ...generic.ports],
757
+ databases: ["POSTGRES_DATABASE", "POSTGRES_DB", "PGDATABASE", "PG_DB", ...generic.databases],
758
+ };
759
+ }
760
+ /**
761
+ * Returns a digest of the effective, non-secret database target. It accepts a
762
+ * URL or host/database fields, but deliberately retains only scheme, host,
763
+ * port, and database; credentials and raw URLs never enter persisted state.
764
+ * Missing or contradictory target facts fail closed.
765
+ */
766
+ function effectiveConnectionTargetDigest(projectDir, sourceType) {
767
+ let dotenv;
768
+ try {
769
+ dotenv = parseDotEnv(readFileSync(path.join(projectDir, ".env"), "utf-8"));
770
+ }
771
+ catch {
772
+ return undefined;
773
+ }
774
+ const aliases = connectionAliases(sourceType);
775
+ const targets = new Set();
776
+ for (const key of aliases.urls) {
777
+ const value = process.env[key] ?? dotenv.get(key);
778
+ if (value === undefined || !value.trim())
779
+ continue;
780
+ const target = connectionTargetFromUrl(value.trim(), sourceType);
781
+ if (target === undefined)
782
+ return undefined;
783
+ targets.add(JSON.stringify(target));
784
+ }
785
+ const fieldTarget = !HOST_DATABASE_FIELD_SOURCES.has(sourceType)
786
+ ? undefined
787
+ : connectionTargetFromFields(sourceType, effectiveConnectionValue(dotenv, aliases.hosts), effectiveConnectionValue(dotenv, aliases.ports), effectiveConnectionValue(dotenv, aliases.databases));
788
+ if (fieldTarget !== undefined)
789
+ targets.add(JSON.stringify(fieldTarget));
790
+ if (targets.size !== 1)
791
+ return undefined;
792
+ return createHash("sha256").update([...targets][0]).digest("hex");
793
+ }
794
+ /**
795
+ * A non-secret, host-derived identity for retained context lifecycle proof.
796
+ * It deliberately binds the canonical on-disk project, the BFF-selected
797
+ * source type, wren's project-level profile declaration, and a non-secret
798
+ * digest of the effective connection target. If any required filesystem fact
799
+ * cannot be read or is ambiguous, callers must fail closed and not reuse
800
+ * evidence from an earlier attempt.
801
+ */
802
+ export function contextLifecycleIdentityFingerprint(root, name, selectedSourceType) {
803
+ try {
804
+ const projectDir = realpathSync(path.join(root, name));
805
+ if (!statSync(projectDir).isDirectory())
806
+ return undefined;
807
+ const projectYml = path.join(projectDir, "wren_project.yml");
808
+ const profile = readProjectYamlField(projectYml, "profile");
809
+ const declaredSourceType = readProjectYamlField(projectYml, "data_source");
810
+ const normalizedSelectedSourceType = selectedSourceType.trim().toLowerCase();
811
+ const normalizedDeclaredSourceType = declaredSourceType?.trim().toLowerCase();
812
+ if (profile === undefined || normalizedDeclaredSourceType === undefined || normalizedDeclaredSourceType !== normalizedSelectedSourceType)
813
+ return undefined;
814
+ const connectionTargetDigest = effectiveConnectionTargetDigest(projectDir, normalizedSelectedSourceType);
815
+ if (connectionTargetDigest === undefined)
816
+ return undefined;
817
+ return createHash("sha256")
818
+ .update(JSON.stringify({ version: 2, projectDir, selectedSourceType: normalizedSelectedSourceType, profile, declaredSourceType: normalizedDeclaredSourceType, connectionTargetDigest }))
819
+ .digest("hex");
820
+ }
821
+ catch {
822
+ return undefined;
823
+ }
824
+ }
825
+ function hostContractFailure(code, message, observed, expectedArtifactContract, failureKind) {
826
+ return {
827
+ status: "error",
828
+ message,
829
+ diagnostic: { kind: "host_contract", code, observed, expectedArtifactContract },
830
+ ...(failureKind !== undefined ? { failureKind } : {}),
831
+ };
832
+ }
833
+ const SETUP_STATUS_LINE = /^SETUP_STATUS:\s*(ok|needs_input|error)\b\s*[-:]?\s*(.*)$/i;
834
+ function defaultMessageFor(status) {
835
+ switch (status) {
836
+ case "ok":
837
+ return "setup completed successfully";
838
+ case "needs_input":
839
+ return "waiting for user input";
840
+ case "error":
841
+ return "setup failed";
842
+ }
843
+ }
844
+ /** Counts the built MDL's models; unreadable/malformed content is deliberately treated as empty rather than trusted. */
845
+ function countMdlModels(mdlPath) {
846
+ try {
847
+ const parsed = JSON.parse(readFileSync(mdlPath, "utf-8"));
848
+ return Array.isArray(parsed.models) ? parsed.models.length : 0;
849
+ }
850
+ catch {
851
+ return 0;
852
+ }
853
+ }
854
+ /**
855
+ * Returns the path of the first artifact `connect`'s prompt promises but
856
+ * that is missing from disk, or `undefined` if all three are present. Checked
857
+ * in the order a real `connect` run would produce them, so the message names
858
+ * the earliest failure (e.g. a missing project dir implies the other two are
859
+ * also missing, but naming the dir is the most useful signal).
860
+ */
861
+ function missingConnectArtifact(context) {
862
+ const projectDir = path.join(context.root, context.name);
863
+ if (!existsSync(projectDir)) {
864
+ return projectDir;
865
+ }
866
+ const projectYml = path.join(projectDir, "wren_project.yml");
867
+ if (!existsSync(projectYml)) {
868
+ return projectYml;
869
+ }
870
+ const envFile = path.join(projectDir, ".env");
871
+ if (!existsSync(envFile)) {
872
+ return envFile;
873
+ }
874
+ return undefined;
875
+ }
876
+ /**
877
+ * Matches in-process's / Codex local's `setup_execution` "exec" call's
878
+ * `ToolStep.detail` — a `summarizeToolOutput`-bounded (200-char)
879
+ * `JSON.stringify` of `{exitCode, stdout, stderr, ...}` (see
880
+ * `harness/tools/setup-native.ts`) — and extracts the exit code. A regex, not
881
+ * `JSON.parse`: once `stdout`/`stderr` push a stringified object past 200
882
+ * characters (routine for a CLI usage/error message), the truncated detail
883
+ * is no longer valid JSON, but in-process serializes `exitCode` first so it
884
+ * always survives.
885
+ *
886
+ * Deliberately Mode-A/Codex-local-only. There used to be a sibling
887
+ * `MODE_B_BASH_EXIT_CODE` regex here matching a literal `"exit code: N"`
888
+ * phrase, on the theory that a dispatched Bash result might carry that text —
889
+ * but no real code path ever writes it: dispatched's `detail` is the command's
890
+ * raw (240-char-truncated) stdout via `summarizeResultContent` in warble's
891
+ * `dispatcher/claude-agent-sdk/src/events.ts`, with no such wrapping. That
892
+ * dead regex made `execSucceeded` return `undefined` for every genuine dispatched
893
+ * discovery/validate/build call, which made the `context` step's discovery
894
+ * gate structurally unsatisfiable in dispatched — see `execSucceeded` for the fix.
895
+ */
896
+ const SETUP_EXEC_EXIT_CODE = /^\{"exitCode":(-?\d+)/;
897
+ function inProcessExecExitCode(detail) {
898
+ if (detail === undefined)
899
+ return undefined;
900
+ const match = SETUP_EXEC_EXIT_CODE.exec(detail);
901
+ return match ? Number(match[1]) : undefined;
902
+ }
903
+ /**
904
+ * In-process registers the scoped execution capability under its policy name,
905
+ * while dispatched's streamed SDK events retain the underlying `Bash` tool name.
906
+ * Both names represent the same setup-only command boundary at this layer.
907
+ */
908
+ const MODE_B_SETUP_EXECUTION_TOOL_NAME = "Bash";
909
+ /** Codex local streams the same allowlisted tool with its MCP server prefix. */
910
+ const CODEX_SETUP_EXECUTION_TOOL_NAME = `setup.${SETUP_EXECUTION_TOOL_NAME}`;
911
+ function isSetupExecutionEntry(entry) {
912
+ return (entry.label === SETUP_EXECUTION_TOOL_NAME ||
913
+ entry.label === MODE_B_SETUP_EXECUTION_TOOL_NAME ||
914
+ entry.label === CODEX_SETUP_EXECUTION_TOOL_NAME);
915
+ }
916
+ /** In-process and Codex local share the same non-throwing `setup_execution` tool — see {@link execSucceeded}. */
917
+ function isInProcessShapedExecEntry(entry) {
918
+ return entry.label === SETUP_EXECUTION_TOOL_NAME || entry.label === CODEX_SETUP_EXECUTION_TOOL_NAME;
919
+ }
920
+ /**
921
+ * Whether a setup-execution worklog entry represents a genuinely successful
922
+ * command run — `true`/`false` when that's known, `undefined` when it isn't
923
+ * recorded either way (e.g. a in-process file-write action, which has no exit
924
+ * code at all).
925
+ *
926
+ * In-process and Codex local share the same non-throwing `setup_execution` tool
927
+ * (`harness/tools/setup-native.ts`): the JS tool call itself only throws on a
928
+ * pre-flight denylist/scope violation (`SetupCommandDeniedError`), never on
929
+ * the shell command's own exit code — a nonexistent CLI subcommand still
930
+ * returns normally as `{exitCode: 2, ...}` structured output. That means
931
+ * `ToolStep.state` is ALWAYS `"done"` for these entries regardless of
932
+ * whether the shell command itself succeeded, so the only trustworthy signal
933
+ * for them is the structured exit code folded into `detail`
934
+ * ({@link inProcessExecExitCode}) — unchanged from before this fix.
935
+ *
936
+ * Dispatched's Bash tool has no such gap: `LiveWorkLog.ingest` (`server/fold.ts`)
937
+ * already sets `ToolStep.state` from the originating `AgentEvent`'s own
938
+ * `status` field ("success" -> "done", "error" -> "error") at fold time, for
939
+ * both a genuine command failure and a blocked/guardrail-denied execution —
940
+ * either way the tool call itself did not succeed, so `state` is sound to
941
+ * trust directly. It is also the ONLY signal available: dispatched's `detail` is
942
+ * the command's raw (240-char-truncated) stdout, never a structured exit
943
+ * code.
944
+ */
945
+ function execSucceeded(entry) {
946
+ if (isInProcessShapedExecEntry(entry)) {
947
+ const exitCode = inProcessExecExitCode(entry.detail);
948
+ return exitCode === undefined ? undefined : exitCode === 0;
949
+ }
950
+ if (entry.state === "done")
951
+ return true;
952
+ if (entry.state === "error")
953
+ return false;
954
+ return undefined;
955
+ }
956
+ /** Best-effort extraction of the `command` an exec-action worklog entry ran, for a readable error message; `undefined` if the input isn't the expected shape. */
957
+ function execCommandOf(entry) {
958
+ const input = entry.input;
959
+ if (typeof input !== "object" || input === null)
960
+ return undefined;
961
+ const command = input.command;
962
+ return typeof command === "string" ? command : undefined;
963
+ }
964
+ /**
965
+ * Finds the first setup execution call in `worklog` whose recorded result was
966
+ * a non-zero exit code — i.e. a command that genuinely ran and failed,
967
+ * as opposed to one whose tool invocation merely reported success. This
968
+ * distinction matters because `ToolStep.state` alone cannot make it: every
969
+ * setup execution call reports `state: "done"` as long as the TOOL MECHANISM
970
+ * itself didn't throw (it ran a shell command and captured
971
+ * whatever came back) — regardless of whether the underlying shell command
972
+ * itself succeeded. A nonexistent CLI subcommand (e.g. `wren generate-mdl` —
973
+ * `generate-mdl` is a skill document, not a command; see
974
+ * `composeSetupPrompt`'s `context` branch) still shows `state: "done"`; only
975
+ * the nested `detail` JSON's `exitCode` carries the real signal. See
976
+ * `parseSetupTerminal`'s use of this for why: an `error` status whose own
977
+ * claimed reason blames the connection/data source is only trustworthy if
978
+ * nothing in the trace shows a command failing outright first.
979
+ *
980
+ * KNOWN LIMITATION (disclosed, not silently assumed away): this flags ANY
981
+ * failed exec among the step's setup execution calls, without judging
982
+ * whether that particular command was expected to fail as part of ordinary
983
+ * exploration (e.g. a `grep` with no match, or a conditional shell test) —
984
+ * doing so reliably would require actually understanding the command's
985
+ * intent, which this module has no basis to do. The returned message is
986
+ * worded as advisory ("does not by itself confirm...") rather than as a
987
+ * counter-assertion for exactly this reason.
988
+ *
989
+ * `exitCode` is optional: in-process/Codex local failures carry a real numeric
990
+ * exit code; a dispatched (Bash) failure — including a blocked/guardrail-denied
991
+ * execution — has none (only the fact that it failed), so callers must
992
+ * degrade their wording gracefully rather than assume a number is present.
993
+ */
994
+ function firstFailedExec(worklog) {
995
+ for (const entry of worklog) {
996
+ if (!isSetupExecutionEntry(entry))
997
+ continue;
998
+ if (execSucceeded(entry) === false) {
999
+ const exitCode = isInProcessShapedExecEntry(entry) ? inProcessExecExitCode(entry.detail) : undefined;
1000
+ return { command: execCommandOf(entry), ...(exitCode !== undefined ? { exitCode } : {}) };
1001
+ }
1002
+ }
1003
+ return undefined;
1004
+ }
1005
+ /**
1006
+ * Whether `command` has one of the schema-introspection shapes currently
1007
+ * documented by the generate-mdl skill. This is deliberately an allowlist:
1008
+ * a Wren SQL query, inline Python using SQLAlchemy or a documented database
1009
+ * driver, or a connector CLI is only counted when its command text ALSO has
1010
+ * recognizable Phase-2 discovery evidence: a schema/catalog query, a table
1011
+ * listing command, or a documented table/column discovery API. In particular,
1012
+ * connectivity probes such as `SELECT 1` and arbitrary data queries do NOT
1013
+ * count; neither do skill fetches or the context build/validate lifecycle
1014
+ * commands.
1015
+ *
1016
+ * KNOWN LIMITATIONS (disclosed, not silently assumed away): a worklog stores
1017
+ * only the shell command, not an executed script's contents or its query
1018
+ * result. Therefore this recognizes an ATTEMPT with recognizable discovery
1019
+ * evidence, not a successful schema read; it cannot recognize opaque script
1020
+ * paths, database-specific CLI metacommands, every future driver/CLI, or
1021
+ * prove that a matched query returned schema rows. Expanding beyond the
1022
+ * evidenced command shapes would turn this from an allowlist into guesswork,
1023
+ * so unmatched commands intentionally do not count.
1024
+ */
1025
+ const SCHEMA_DISCOVERY_EVIDENCE = /\b(?:information_schema|pg_catalog|sqlite_master|system\.tables|show\s+(?:full\s+)?(?:tables|columns)|describe\s+(?:table|schema)|\.tables|get_table_names|get_columns|get_foreign_keys|get_pk_constraint|get_unique_constraints|list_tables|get_table)\b/i;
1026
+ const SCHEMA_INTROSPECTION_COMMANDS = [
1027
+ /\bwren(?:\s+query)?\s+--sql(?:\s|=)/i,
1028
+ /\b(?:python(?:3(?:\.\d+)?)?|uv\s+run\s+python)\b[\s\S]*\b(?:sqlalchemy|psycopg|asyncpg|google\.cloud\.bigquery|snowflake\.connector|clickhouse_driver)\b/i,
1029
+ /\b(?:python(?:3(?:\.\d+)?)?|uv\s+run\s+python)\b[\s\S]*\bwren\.profile\b[\s\S]*\bwren\.connector\b/i,
1030
+ /\bWREN_PYTHON\b[\s\S]*\bcommand\s+-v\s+wren\b[\s\S]*\bwren\.profile\b[\s\S]*\bwren\.connector\b/i,
1031
+ /\b(?:psql|mysql|sqlite3|duckdb|snowsql|clickhouse-client|trino(?:-cli)?)\b[\s\S]*(?:\s(?:-c|--command|--execute|--query)\s|\b(?:information_schema|show\s+(?:tables|columns)|describe\s+(?:table|schema)|\.tables)\b)/i,
1032
+ /\bbq\s+query\b/i,
1033
+ ];
1034
+ /**
1035
+ * The single setup-workflow contract for recognisable schema discovery. A
1036
+ * matching command is evidence only after it completed successfully (see
1037
+ * {@link execSucceeded} for what "successfully" means per mode); command
1038
+ * text alone proves neither execution nor a readable schema. Callers use
1039
+ * this for both terminal acceptance and the no-introspection diagnostic,
1040
+ * rather than reimplementing the allowlist elsewhere.
1041
+ */
1042
+ export function classifyRecordedSchemaDiscovery(worklog) {
1043
+ let failed;
1044
+ for (const entry of worklog) {
1045
+ const command = isSetupExecutionEntry(entry) ? execCommandOf(entry) : undefined;
1046
+ if (command === undefined ||
1047
+ !SCHEMA_DISCOVERY_EVIDENCE.test(command) ||
1048
+ !SCHEMA_INTROSPECTION_COMMANDS.some((pattern) => pattern.test(command))) {
1049
+ continue;
1050
+ }
1051
+ const succeeded = execSucceeded(entry);
1052
+ if (succeeded === true)
1053
+ return { kind: "successful", command };
1054
+ if (succeeded === false && failed === undefined) {
1055
+ const exitCode = isInProcessShapedExecEntry(entry) ? inProcessExecExitCode(entry.detail) : undefined;
1056
+ failed = { kind: "failed", command, ...(exitCode !== undefined ? { exitCode } : {}) };
1057
+ }
1058
+ }
1059
+ return failed ?? { kind: "none" };
1060
+ }
1061
+ const SHELL_ENV_ASSIGNMENT = String.raw `[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s;&|]+)`;
1062
+ const DIRECT_CONTEXT_LIFECYCLE_COMMAND = new RegExp(String.raw `^\s*(?:(?:${SHELL_ENV_ASSIGNMENT}\s+)*|env\s+(?:${SHELL_ENV_ASSIGNMENT}\s+)+)?wren\s+context\s+(validate|build)(?:\s+--path\s+(?:\.|"\."|'\.'))?\s*$`, "i");
1063
+ /**
1064
+ * The mandatory foundation has a native, host-verifiable sequence. A context
1065
+ * success needs schema discovery, then `wren context validate`, then `wren
1066
+ * context build`, each recorded as a successful setup execution. An artifact
1067
+ * alone or merely naming a command in the final message is not evidence.
1068
+ */
1069
+ function contextLifecycleStage(command) {
1070
+ // This evidence must describe the actual lifecycle invocation, rather than
1071
+ // merely mention it. `setup_execution` already supplies the project cwd as
1072
+ // a separate structured field; the only shell prefixes we accept here are
1073
+ // environment assignments (including the standard `env KEY=value` form).
1074
+ // The one accepted flag is `--path .`: setup_execution separately binds the
1075
+ // command cwd to the canonical project, so this is the same project-local
1076
+ // lifecycle operation emitted by some Wren guidance. Arbitrary paths, other
1077
+ // flags, shell sequencing, quoted text, and wrappers remain unrecognized.
1078
+ const match = DIRECT_CONTEXT_LIFECYCLE_COMMAND.exec(command);
1079
+ return match?.[1]?.toLowerCase();
1080
+ }
1081
+ function stageAfter(prefix) {
1082
+ if (prefix === "discovery")
1083
+ return "validate";
1084
+ if (prefix === "validate" || prefix === "build")
1085
+ return "build";
1086
+ return "discovery";
1087
+ }
1088
+ function prefixFor(stage) {
1089
+ if (stage === "validate")
1090
+ return "discovery";
1091
+ return stage === "build" ? "validate" : "none";
1092
+ }
1093
+ function scanRecordedContextLifecycle(worklog, prior) {
1094
+ let stage = stageAfter(prior);
1095
+ let completed = prior === "build";
1096
+ let outOfOrderObserved;
1097
+ for (const entry of worklog) {
1098
+ if (!isSetupExecutionEntry(entry) || execSucceeded(entry) !== true)
1099
+ continue;
1100
+ const command = execCommandOf(entry);
1101
+ if (command === undefined)
1102
+ continue;
1103
+ if (stage === "discovery") {
1104
+ if (classifyRecordedSchemaDiscovery([entry]).kind === "successful") {
1105
+ stage = "validate";
1106
+ continue;
1107
+ }
1108
+ if (contextLifecycleStage(command) !== undefined) {
1109
+ outOfOrderObserved ??= `successful \`${command}\` ran before successful schema discovery`;
1110
+ }
1111
+ continue;
1112
+ }
1113
+ const nativeStage = contextLifecycleStage(command);
1114
+ if (stage === "validate") {
1115
+ if (nativeStage === "validate") {
1116
+ stage = "build";
1117
+ }
1118
+ else if (nativeStage === "build") {
1119
+ outOfOrderObserved ??= `successful \`${command}\` ran before successful \`wren context validate\``;
1120
+ }
1121
+ continue;
1122
+ }
1123
+ if (nativeStage === "build")
1124
+ completed = true;
1125
+ }
1126
+ return { stage, completed, outOfOrderObserved };
1127
+ }
1128
+ /**
1129
+ * Returns the greatest ordered prefix proved by the supplied successful tool
1130
+ * calls. This intentionally retains an earlier valid prefix even if a later
1131
+ * call in the same attempt is out of order, so a corrective attempt can ask
1132
+ * only for the unproven suffix.
1133
+ */
1134
+ export function recordedContextLifecyclePrefix(worklog, prior = "none") {
1135
+ const scan = scanRecordedContextLifecycle(worklog, prior);
1136
+ return scan.completed ? "build" : prefixFor(scan.stage);
1137
+ }
1138
+ export function classifyRecordedContextLifecycle(worklog, prior = "none") {
1139
+ const { stage, completed, outOfOrderObserved } = scanRecordedContextLifecycle(worklog, prior);
1140
+ if (completed)
1141
+ return { kind: "successful" };
1142
+ if (outOfOrderObserved !== undefined)
1143
+ return { kind: "out_of_order", observed: outOfOrderObserved };
1144
+ if (stage === "discovery")
1145
+ return { kind: "missing_discovery" };
1146
+ if (stage === "validate")
1147
+ return { kind: "validate_missing", observed: "no successful `wren context validate` followed successful schema discovery" };
1148
+ return { kind: "build_missing", observed: "no successful `wren context build` followed successful `wren context validate`" };
1149
+ }
1150
+ /** A narrow terminal-message check: only override a context error that explicitly claims a zero-table/model outcome. */
1151
+ function claimsNoTablesOrModels(message) {
1152
+ return /\b(?:no|zero)\s+(?:tables?|models?)\b|\b0\s+(?:tables?|models?)\b/i.test(message);
1153
+ }
1154
+ /**
1155
+ * Parses the LAST `SETUP_STATUS: ok|needs_input|error` line out of a setup
1156
+ * turn's finalText (the terminal contract lives in the composed
1157
+ * prompt, not warble — the agent is instructed to emit this line, and an
1158
+ * agent narrating its plan mid-message could emit misleading earlier
1159
+ * "SETUP_STATUS"-like text, so only the last line counts).
1160
+ *
1161
+ * For `ok`, independently verifies an on-disk artifact before trusting the
1162
+ * agent's self-report, rather than blindly advancing the wizard — see
1163
+ * `SetupTerminalContext.stepKey`'s doc comment for exactly which artifact each
1164
+ * step checks (`wren_project.yml` / `.wren-validated` / `target/mdl.json`
1165
+ * with at least one model).
1166
+ *
1167
+ * The `context` step's `ok` check also requires a successfully completed
1168
+ * recognized schema-discovery command whenever a worklog is available. It
1169
+ * does NOT try to infer fabrication from model names or columns. Two semantic
1170
+ * heuristics were considered and rejected as unsafe:
1171
+ * - Flagging a model literally named "example": a real warehouse can
1172
+ * genuinely have a table named "example" (the composed `context` prompt
1173
+ * itself explicitly tolerates a pre-existing "example" placeholder
1174
+ * surviving on disk), so this would false-positive on a legitimate build.
1175
+ * - Flagging models with zero columns: a fabricating agent writes
1176
+ * plausible-looking column lists (that's what makes the fabrication
1177
+ * convincing), so this wouldn't even have caught the real incident —
1178
+ * it's a weak signal with no compensating safety benefit.
1179
+ * No cheap structural signal reliably distinguishes fabricated MDL from a
1180
+ * real one. The completed-discovery gate is therefore the deterministic
1181
+ * provenance boundary here; the prompt still tells the agent to never
1182
+ * fabricate and to report failed discovery honestly as additional guidance.
1183
+ */
1184
+ export function parseSetupTerminal(finalText, context) {
1185
+ const lines = finalText.split("\n");
1186
+ let matched;
1187
+ for (const line of lines) {
1188
+ const match = SETUP_STATUS_LINE.exec(line.trim());
1189
+ if (match) {
1190
+ const status = match[1].toLowerCase();
1191
+ const message = match[2]?.trim();
1192
+ matched = { status, message: message && message.length > 0 ? message : defaultMessageFor(status) };
1193
+ }
1194
+ }
1195
+ if (!matched) {
1196
+ return {
1197
+ status: "error",
1198
+ message: "the setup agent's final message did not contain a SETUP_STATUS line",
1199
+ failureKind: "missing_terminal_status",
1200
+ };
1201
+ }
1202
+ if (matched.status === "ok") {
1203
+ if (context.stepKey === "context") {
1204
+ if (context.worklog !== undefined) {
1205
+ const lifecycle = classifyRecordedContextLifecycle(context.worklog, context.priorContextLifecycle);
1206
+ if (lifecycle.kind === "missing_discovery") {
1207
+ return hostContractFailure("context_schema_discovery_missing", "The agent never completed recognized schema discovery in its recorded worklog, so its model/build result cannot be accepted. This is an agent-workflow failure, not evidence that the connection or data source lacks tables.", "the recorded worklog has no successful recognized schema-discovery command", [
1208
+ "run a recognized schema-discovery command against the connected source and make sure it exits successfully",
1209
+ "build target/mdl.json only from that discovered schema, with at least one model",
1210
+ ], "no_successful_schema_discovery");
1211
+ }
1212
+ const discovery = classifyRecordedSchemaDiscovery(context.worklog);
1213
+ if (discovery.kind === "failed") {
1214
+ const exitCodeSuffix = discovery.exitCode !== undefined ? ` with exit code ${discovery.exitCode}` : "";
1215
+ return hostContractFailure("context_schema_discovery_failed", `\`${discovery.command}\` failed${exitCodeSuffix} during schema discovery — a model/build result cannot be accepted until that command/tool failure is resolved; this is not evidence that the connection or data source lacks tables.`, discovery.exitCode !== undefined
1216
+ ? `the recorded schema-discovery command exited with code ${discovery.exitCode}`
1217
+ : "the recorded schema-discovery command failed", [
1218
+ "resolve the schema-discovery command failure and run a recognized discovery command successfully",
1219
+ "build target/mdl.json only from that discovered schema, with at least one model",
1220
+ ]);
1221
+ }
1222
+ if (lifecycle.kind === "validate_missing") {
1223
+ return hostContractFailure("context_validate_missing", "The agent did not successfully run `wren context validate` after recognized schema discovery, so its model/build result cannot be accepted.", lifecycle.observed, [
1224
+ "run a recognized schema-discovery command successfully",
1225
+ "run `wren context validate` successfully before `wren context build`",
1226
+ "build target/mdl.json only after validation, with at least one model",
1227
+ ]);
1228
+ }
1229
+ if (lifecycle.kind === "build_missing") {
1230
+ return hostContractFailure("context_build_missing", "The agent did not successfully run `wren context build` after `wren context validate`, so its claimed context success cannot be accepted.", lifecycle.observed, [
1231
+ "run a recognized schema-discovery command successfully",
1232
+ "run `wren context validate` successfully",
1233
+ "run `wren context build` successfully after validation and produce target/mdl.json with at least one model",
1234
+ ]);
1235
+ }
1236
+ if (lifecycle.kind === "out_of_order") {
1237
+ return hostContractFailure("context_lifecycle_out_of_order", "The native context lifecycle ran out of order; Setup requires successful schema discovery followed by `wren context validate` and then `wren context build`.", lifecycle.observed, [
1238
+ "run a recognized schema-discovery command successfully",
1239
+ "run `wren context validate` successfully before `wren context build`",
1240
+ "produce target/mdl.json with at least one schema-derived model",
1241
+ ]);
1242
+ }
1243
+ }
1244
+ const mdlPath = path.join(context.root, context.name, "target", "mdl.json");
1245
+ if (!existsSync(mdlPath)) {
1246
+ return hostContractFailure("context_mdl_missing", `the setup agent reported "ok" but ${mdlPath} does not exist — treating this as a failed context build`, "target/mdl.json is missing after the claimed context build", ["target/mdl.json must exist", "target/mdl.json must contain at least one model"]);
1247
+ }
1248
+ const modelCount = countMdlModels(mdlPath);
1249
+ if (modelCount < 1) {
1250
+ return hostContractFailure("context_mdl_empty", `the setup agent reported "ok" but ${mdlPath} has ${modelCount} models — treating this as a failed context build`, `target/mdl.json has ${modelCount} models after the claimed context build`, ["target/mdl.json must contain at least one model"]);
1251
+ }
1252
+ }
1253
+ else {
1254
+ const markerName = context.stepKey === "connect_resume" ? ".wren-validated" : "wren_project.yml";
1255
+ const marker = path.join(context.root, context.name, markerName);
1256
+ if (!existsSync(marker)) {
1257
+ return hostContractFailure(context.stepKey === "connect_resume" ? "connection_marker_missing" : "connect_artifact_missing", `the setup agent reported "ok" but ${marker} does not exist — treating this as a failed setup`, `${markerName} is missing after the claimed setup success`, context.stepKey === "connect_resume"
1258
+ ? ["create .wren-validated only after the connection profile validates successfully"]
1259
+ : ["create the project directory", "create wren_project.yml", "create the empty .env template"]);
1260
+ }
1261
+ if (context.stepKey === "connect_resume" && context.expectedSourceType !== undefined) {
1262
+ const projectYml = path.join(context.root, context.name, "wren_project.yml");
1263
+ const pinnedProfile = readProjectYamlField(projectYml, "profile");
1264
+ if (pinnedProfile === undefined || pinnedProfile.length === 0) {
1265
+ return hostContractFailure("connection_profile_missing", `the setup agent reported "ok" but ${projectYml} has no "profile:" pin — the connection profile was never actually pinned to this project (check the profile, not .env)`, "wren_project.yml has no non-empty profile pin", ["wren_project.yml must have a non-empty profile pin", "the pinned profile must match the selected data source"]);
1266
+ }
1267
+ const pinnedDataSource = readProjectYamlField(projectYml, "data_source");
1268
+ if ((pinnedDataSource ?? "").trim().toLowerCase() !== context.expectedSourceType.trim().toLowerCase()) {
1269
+ return hostContractFailure("connection_source_mismatch", `the setup agent reported "ok" but ${projectYml}'s "data_source: ${pinnedDataSource ?? "(missing)"}" does not match the selected data source "${context.expectedSourceType}" — the connection profile is for the wrong data source (check the profile, not .env)`, `the pinned data source does not match the selected ${context.expectedSourceType} source`, [
1270
+ "wren_project.yml must have a non-empty profile pin",
1271
+ `the pinned data_source must match the selected ${context.expectedSourceType} source`,
1272
+ "create .wren-validated only after the matching profile validates successfully",
1273
+ ]);
1274
+ }
1275
+ }
1276
+ }
1277
+ }
1278
+ else if (matched.status === "needs_input" && (context.stepKey === undefined || context.stepKey === "connect")) {
1279
+ const missing = missingConnectArtifact(context);
1280
+ if (missing !== undefined) {
1281
+ return {
1282
+ status: "error",
1283
+ message: `the setup agent reported "needs_input" but ${missing} does not exist — treating this as a failed connect`,
1284
+ };
1285
+ }
1286
+ }
1287
+ else if (matched.status === "error" && context.stepKey === "context" && context.worklog !== undefined) {
1288
+ // The agent's own `SETUP_STATUS: error` line for the `context` step routinely
1289
+ // blames the connection or data source (e.g. "schema introspection found no
1290
+ // tables"), but that is only an honest conclusion if an introspection command
1291
+ // actually ran and completed. If the agent instead ran a nonexistent or
1292
+ // otherwise-failing CLI command (its own tool-use mistake, not a fact about
1293
+ // the data source), the "no tables" framing is a false attribution. Before
1294
+ // trusting the agent's message, check the turn's own worklog for a
1295
+ // `setup_execution` call that exited non-zero, and reframe if one is found.
1296
+ const failedExec = firstFailedExec(context.worklog);
1297
+ if (failedExec !== undefined) {
1298
+ const command = failedExec.command !== undefined ? `\`${failedExec.command}\`` : "a command";
1299
+ const exitCodeSuffix = failedExec.exitCode !== undefined ? ` with exit code ${failedExec.exitCode}` : "";
1300
+ return {
1301
+ status: "error",
1302
+ message: `${command} failed${exitCodeSuffix} during this step — a command in this step's own history failed, so this step's error framing can't be trusted until that's ruled out; this is a command/tool failure, not by itself evidence that the connection or data source lacks tables. The agent's own report was: "${matched.message}"`,
1303
+ };
1304
+ }
1305
+ if (claimsNoTablesOrModels(matched.message) && classifyRecordedSchemaDiscovery(context.worklog).kind === "none") {
1306
+ return {
1307
+ status: "error",
1308
+ failureKind: "no_successful_schema_discovery",
1309
+ message: "The agent never attempted schema introspection in its recorded worklog — it reported a zero-table/model outcome without a recognized introspection command. This is an agent-workflow failure, not evidence that the connection or data source lacks tables.",
1310
+ };
1311
+ }
1312
+ }
1313
+ return matched;
1314
+ }