@kontourai/flow-agents 3.2.0 → 3.4.0

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 (281) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +173 -0
  3. package/CHANGELOG.md +54 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/assignment-provider.js +10 -1
  11. package/build/src/cli/builder-run.d.ts +1 -0
  12. package/build/src/cli/builder-run.js +27 -0
  13. package/build/src/cli/effective-backlog-settings.js +70 -2
  14. package/build/src/cli/init.d.ts +34 -0
  15. package/build/src/cli/init.js +341 -61
  16. package/build/src/cli/kit.js +55 -12
  17. package/build/src/cli/pull-work-provider.js +346 -5
  18. package/build/src/cli/skill-drift-check.d.ts +1 -0
  19. package/build/src/cli/skill-drift-check.js +165 -0
  20. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  21. package/build/src/cli/telemetry-doctor.js +53 -6
  22. package/build/src/cli/validate-hook-influence.js +37 -7
  23. package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
  24. package/build/src/cli/workflow-sidecar.d.ts +310 -1
  25. package/build/src/cli/workflow-sidecar.js +1914 -126
  26. package/build/src/cli.js +5 -0
  27. package/build/src/flow-kit/validate.d.ts +54 -34
  28. package/build/src/flow-kit/validate.js +237 -26
  29. package/build/src/index.d.ts +2 -0
  30. package/build/src/index.js +1 -0
  31. package/build/src/lib/console-connect-options.d.ts +97 -0
  32. package/build/src/lib/console-connect-options.js +199 -0
  33. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  34. package/build/src/lib/console-telemetry-validate.js +91 -0
  35. package/build/src/lib/flow-resolver.d.ts +54 -1
  36. package/build/src/lib/flow-resolver.js +112 -5
  37. package/build/src/lib/fs.d.ts +17 -0
  38. package/build/src/lib/fs.js +172 -0
  39. package/build/src/lib/local-artifact-root.d.ts +44 -1
  40. package/build/src/lib/local-artifact-root.js +131 -3
  41. package/build/src/runtime-adapters.d.ts +39 -3
  42. package/build/src/runtime-adapters.js +77 -31
  43. package/build/src/tools/build-universal-bundles.js +40 -2
  44. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  45. package/build/src/tools/codex-agent-routing.js +49 -0
  46. package/build/src/tools/generate-context-map.js +1 -0
  47. package/build/src/tools/validate-source-tree.js +30 -3
  48. package/context/contracts/artifact-contract.md +16 -2
  49. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  50. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  51. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  52. package/context/scripts/hooks/workflow-steering.js +194 -22
  53. package/context/scripts/package.json +3 -0
  54. package/context/scripts/telemetry/install-console-config.sh +25 -4
  55. package/context/scripts/telemetry/lib/config.sh +102 -12
  56. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  57. package/context/scripts/telemetry/lib/session.sh +3 -0
  58. package/context/scripts/telemetry/lib/transport.sh +87 -0
  59. package/context/scripts/telemetry/lib/usage.sh +205 -4
  60. package/context/scripts/telemetry/telemetry.conf +6 -0
  61. package/context/scripts/telemetry/telemetry.sh +48 -0
  62. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  63. package/docs/agent-usage-feedback-loop.md +35 -0
  64. package/docs/architecture-engine-and-kits.md +110 -0
  65. package/docs/context-map.md +2 -0
  66. package/docs/coordination-guide.md +370 -0
  67. package/docs/decisions/agent-coordination.md +26 -9
  68. package/docs/decisions/embeddable-engine.md +152 -0
  69. package/docs/decisions/index.md +5 -3
  70. package/docs/decisions/trust-ledger-retention.md +88 -0
  71. package/docs/decisions/trust-reconcile.md +42 -9
  72. package/docs/decisions/workflow-enforcement.md +31 -9
  73. package/docs/fixture-ownership.md +6 -2
  74. package/docs/implementing-trust-reconciliation.md +129 -0
  75. package/docs/index.md +23 -9
  76. package/docs/integrations/flow-agents-console.md +275 -0
  77. package/docs/integrations/index.md +4 -0
  78. package/docs/kit-authoring-guide.md +52 -21
  79. package/docs/spec/builder-flow-runtime.md +80 -0
  80. package/docs/spec/runtime-hook-surface.md +45 -1
  81. package/docs/specs/economics-record-contract.md +270 -0
  82. package/docs/specs/harness-capability-matrix.md +74 -0
  83. package/docs/specs/learning-review-proposals-contract.md +340 -0
  84. package/docs/specs/routing-efficiency-review.md +59 -0
  85. package/docs/verifiable-trust.md +74 -25
  86. package/docs/workflow-artifact-lifecycle.md +38 -1
  87. package/docs/workflow-usage-guide.md +10 -0
  88. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  89. package/evals/ci/antigaming-suite.sh +2 -0
  90. package/evals/ci/run-baseline.sh +78 -4
  91. package/evals/fixtures/economics/acceptance.json +12 -0
  92. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  93. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  94. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  95. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  96. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  97. package/evals/fixtures/economics/critique.json +22 -0
  98. package/evals/fixtures/economics/expected-record.json +71 -0
  99. package/evals/fixtures/economics/session-usage-event.json +1 -0
  100. package/evals/fixtures/economics/state.json +11 -0
  101. package/evals/fixtures/economics/transcript.jsonl +3 -0
  102. package/evals/fixtures/hook-influence/cases.json +7 -7
  103. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  104. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  105. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  106. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  108. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  116. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  117. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  118. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  119. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  120. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  121. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  122. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  123. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  124. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  125. package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
  126. package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
  127. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  128. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  129. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  130. package/evals/integration/test_builder_step_producers.sh +18 -10
  131. package/evals/integration/test_bundle_install.sh +172 -0
  132. package/evals/integration/test_checkpoint_signing.sh +10 -2
  133. package/evals/integration/test_ci_actor_identity.sh +221 -0
  134. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  135. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  136. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  137. package/evals/integration/test_economics_record.sh +674 -0
  138. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  139. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  140. package/evals/integration/test_exemption_usage_review.sh +198 -0
  141. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  142. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  143. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  144. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  145. package/evals/integration/test_gate_lockdown.sh +7 -0
  146. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  147. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  148. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  149. package/evals/integration/test_hook_influence_cases.sh +25 -1
  150. package/evals/integration/test_install_merge.sh +227 -2
  151. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  152. package/evals/integration/test_learning_review_proposals.sh +329 -0
  153. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  154. package/evals/integration/test_liveness_console_relay.sh +166 -0
  155. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  156. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  157. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  158. package/evals/integration/test_publish_delivery.sh +389 -2
  159. package/evals/integration/test_pull_work_board.sh +200 -0
  160. package/evals/integration/test_pull_work_provider.sh +1 -1
  161. package/evals/integration/test_reconcile_preflight.sh +304 -0
  162. package/evals/integration/test_record_check.sh +378 -0
  163. package/evals/integration/test_routing_efficiency.sh +71 -0
  164. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  165. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  166. package/evals/integration/test_skill_drift_check.sh +870 -0
  167. package/evals/integration/test_takeover_protocol.sh +340 -0
  168. package/evals/integration/test_telemetry.sh +445 -0
  169. package/evals/integration/test_telemetry_doctor.sh +66 -0
  170. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  171. package/evals/integration/test_trust_reconcile_negatives.sh +121 -13
  172. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  173. package/evals/integration/test_usage_cost.sh +61 -0
  174. package/evals/integration/test_verify_hold.sh +910 -0
  175. package/evals/integration/test_veritas_governance_kit.sh +257 -0
  176. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
  177. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  178. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  179. package/evals/integration/test_workspace_settings.sh +176 -0
  180. package/evals/lib/env.sh +26 -0
  181. package/evals/lib/node.sh +8 -0
  182. package/evals/run.sh +37 -0
  183. package/evals/static/test_ci_integration_coverage.sh +115 -0
  184. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  185. package/evals/static/test_universal_bundles.sh +34 -0
  186. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  187. package/evals/static/test_workflow_skills.sh +1 -1
  188. package/kits/builder/flows/build.flow.json +9 -18
  189. package/kits/builder/flows/publish-learn.flow.json +5 -1
  190. package/kits/builder/kit.json +120 -0
  191. package/kits/builder/skills/continue-work/SKILL.md +2 -0
  192. package/kits/builder/skills/deliver/SKILL.md +115 -0
  193. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  194. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  195. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  196. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  197. package/kits/builder/skills/pull-work/SKILL.md +33 -2
  198. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  199. package/kits/knowledge/kit.json +9 -0
  200. package/kits/veritas-governance/docs/README.md +113 -7
  201. package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
  202. package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
  203. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  204. package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
  205. package/kits/veritas-governance/kit.json +19 -0
  206. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  207. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  208. package/package.json +2 -2
  209. package/packaging/manifest.json +29 -0
  210. package/schemas/backlog-provider-settings.schema.json +13 -0
  211. package/schemas/workflow-state.schema.json +44 -0
  212. package/scripts/README.md +4 -0
  213. package/scripts/check-content-boundary.cjs +8 -1
  214. package/scripts/ci/trust-reconcile.js +214 -253
  215. package/scripts/hooks/codex-hook-adapter.js +77 -2
  216. package/scripts/hooks/evidence-capture.js +38 -5
  217. package/scripts/hooks/lib/actor-identity.js +82 -0
  218. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  219. package/scripts/hooks/lib/kit-catalog.js +235 -0
  220. package/scripts/hooks/lib/liveness-write.js +28 -1
  221. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  222. package/scripts/hooks/lib/runnable-command.js +177 -0
  223. package/scripts/hooks/lib/skill-drift.js +350 -0
  224. package/scripts/hooks/stop-goal-fit.js +278 -48
  225. package/scripts/hooks/workflow-steering.js +194 -22
  226. package/scripts/install-codex-home.sh +97 -47
  227. package/scripts/install-merge.js +72 -14
  228. package/scripts/install-owned-files.js +178 -0
  229. package/scripts/lib/reconcile-shape.js +381 -0
  230. package/scripts/liveness/relay.sh +84 -0
  231. package/scripts/telemetry/economics-record.schema.json +145 -0
  232. package/scripts/telemetry/economics-record.sh +331 -0
  233. package/scripts/telemetry/install-console-config.sh +25 -4
  234. package/scripts/telemetry/learning-review-decide.sh +124 -0
  235. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  236. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  237. package/scripts/telemetry/lib/config.sh +102 -12
  238. package/scripts/telemetry/lib/pricing.sh +14 -6
  239. package/scripts/telemetry/lib/session.sh +3 -0
  240. package/scripts/telemetry/lib/transport.sh +133 -15
  241. package/scripts/telemetry/lib/usage.sh +121 -28
  242. package/scripts/telemetry/routing-efficiency.sh +0 -0
  243. package/scripts/telemetry/telemetry.conf +6 -0
  244. package/scripts/telemetry/telemetry.sh +48 -0
  245. package/src/builder-flow-run-adapter.ts +357 -0
  246. package/src/builder-flow-runtime.ts +348 -0
  247. package/src/cli/assignment-provider.ts +12 -1
  248. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  249. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  250. package/src/cli/builder-run.ts +28 -0
  251. package/src/cli/codex-agent-routing.test.mjs +44 -0
  252. package/src/cli/codex-exit-code.test.mjs +207 -0
  253. package/src/cli/console-connect-options.test.mjs +329 -0
  254. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  255. package/src/cli/effective-backlog-settings.ts +68 -2
  256. package/src/cli/flow-resolver-composition.test.mjs +72 -0
  257. package/src/cli/init.test.mjs +161 -0
  258. package/src/cli/init.ts +407 -62
  259. package/src/cli/kit-metadata-security.test.mjs +443 -0
  260. package/src/cli/kit.ts +50 -12
  261. package/src/cli/pull-work-provider.ts +377 -3
  262. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  263. package/src/cli/skill-drift-check.ts +196 -0
  264. package/src/cli/telemetry-doctor.test.mjs +53 -0
  265. package/src/cli/telemetry-doctor.ts +50 -7
  266. package/src/cli/validate-hook-influence.ts +37 -6
  267. package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
  268. package/src/cli/workflow-sidecar.ts +1980 -119
  269. package/src/cli.ts +5 -0
  270. package/src/flow-kit/validate.ts +277 -38
  271. package/src/index.ts +19 -0
  272. package/src/lib/console-connect-options.ts +261 -0
  273. package/src/lib/console-telemetry-validate.ts +88 -0
  274. package/src/lib/flow-resolver.ts +117 -4
  275. package/src/lib/fs.ts +160 -0
  276. package/src/lib/local-artifact-root.ts +129 -3
  277. package/src/runtime-adapters.ts +113 -33
  278. package/src/tools/build-universal-bundles.ts +36 -2
  279. package/src/tools/codex-agent-routing.ts +48 -0
  280. package/src/tools/generate-context-map.ts +1 -0
  281. package/src/tools/validate-source-tree.ts +29 -3
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Pure, side-effect-free decision logic for the guided "Connect to Kontour
3
+ * Console?" wizard (install-flow-console-connect PR2). Mirrors the doc-comment
4
+ * convention of `console-telemetry-validate.ts` (this literally is "the future
5
+ * guided console-connect wizard (PR2/PR3)" that file's own docstring names).
6
+ *
7
+ * No `fs`/`child_process`/network imports here — every export is a pure
8
+ * function or a thin orchestration over injected `ask`/`askHidden` callbacks,
9
+ * so `interactiveOptions()` in `src/cli/init.ts` stays a thin caller and this
10
+ * module is fully unit-testable without a TTY.
11
+ *
12
+ * `TelemetrySink` is re-declared as a local type alias (not imported from
13
+ * `init.ts`) to avoid a lib -> cli import direction. Keep this union in sync
14
+ * with `src/cli/init.ts`'s `TelemetrySink` by comment cross-reference:
15
+ * "local-files" | "local-kontour-console" | "kontour-hosted-console" |
16
+ * "user-hosted-console" | "kontour-cloud" | "hosted-kontour-console"
17
+ */
18
+ import { isValidConsoleUrl, isValidConsoleToken, isValidConsoleTenant } from "./console-telemetry-validate.js";
19
+
20
+ export type TelemetrySink =
21
+ | "local-files"
22
+ | "local-kontour-console"
23
+ | "kontour-hosted-console"
24
+ | "user-hosted-console"
25
+ | "kontour-cloud"
26
+ | "hosted-kontour-console";
27
+
28
+ export type ConsoleConnectChoice = "hosted" | "local" | "self-hosted" | "skip";
29
+
30
+ /**
31
+ * Case-insensitive match on the four accepted choice spellings. Blank or
32
+ * unrecognized input falls back to `fallback` (the caller decides the
33
+ * default, typically "hosted").
34
+ */
35
+ export function normalizeConsoleConnectChoice(answer: string, fallback: ConsoleConnectChoice): ConsoleConnectChoice {
36
+ const normalized = answer.trim().toLowerCase();
37
+ if (!normalized) return fallback;
38
+ if (normalized === "hosted" || normalized === "h") return "hosted";
39
+ if (normalized === "local" || normalized === "l") return "local";
40
+ if (normalized === "self-hosted" || normalized === "selfhosted" || normalized === "self" || normalized === "s") return "self-hosted";
41
+ if (normalized === "skip" || normalized === "none" || normalized === "n") return "skip";
42
+ return fallback;
43
+ }
44
+
45
+ /** Pure lookup table: choice -> the exact `telemetrySinks` array to install with. */
46
+ export function mapConsoleConnectChoiceToSinks(choice: ConsoleConnectChoice): TelemetrySink[] {
47
+ if (choice === "hosted") return ["kontour-hosted-console"];
48
+ if (choice === "local") return ["local-kontour-console"];
49
+ if (choice === "self-hosted") return ["user-hosted-console"];
50
+ return ["local-files"];
51
+ }
52
+
53
+ /** Only "self-hosted" requires an inline URL prompt. */
54
+ export function needsConsoleUrlPrompt(choice: ConsoleConnectChoice): boolean {
55
+ return choice === "self-hosted";
56
+ }
57
+
58
+ /**
59
+ * Mirrors `needsConsoleCredentials`'s existing semantics one-for-one
60
+ * (choice-based instead of sinks-based): every choice except "skip" wants
61
+ * token/tenant prompts.
62
+ */
63
+ export function needsConsoleCredentialPrompts(choice: ConsoleConnectChoice): boolean {
64
+ return choice !== "skip";
65
+ }
66
+
67
+ /**
68
+ * The one hard-fail guard this PR adds: self-hosted Console with a blank URL
69
+ * is a downstream `die` in `install-console-config.sh` (the `user-hosted-console`
70
+ * branch requires `--console-url`/`--console-endpoint`). Falls back to
71
+ * `local-files` with a one-line warning instead of letting that crash happen.
72
+ *
73
+ * Only handles the blank case — a non-blank-but-malformed URL (e.g. a typo)
74
+ * is NOT this guard's job; callers separately run `isValidConsoleUrl` for
75
+ * that format-only warn-and-continue case.
76
+ */
77
+ export function resolveSelfHostedUrlOrFallback(url: string): { sinks: TelemetrySink[]; url: string; fallbackWarning?: string } {
78
+ if (url.trim() === "") {
79
+ return {
80
+ sinks: ["local-files"],
81
+ url: "",
82
+ fallbackWarning: "Self-hosted Console requires a URL; no URL was given, so telemetry stays local-only. Re-run init or telemetry-doctor to add a Console URL later.",
83
+ };
84
+ }
85
+ return { sinks: ["user-hosted-console"], url };
86
+ }
87
+
88
+ export type ConsoleConnectIo = {
89
+ ask: (prompt: string) => Promise<string>;
90
+ askHidden: (prompt: string) => Promise<string>;
91
+ };
92
+
93
+ export type ConsoleConnectDefaults = {
94
+ hostedUrl: string;
95
+ localUrl: string;
96
+ };
97
+
98
+ export type ConsoleConnectResult = {
99
+ telemetrySinks: TelemetrySink[];
100
+ consoleUrl?: string;
101
+ consoleTokenValue?: string;
102
+ consoleTenant?: string;
103
+ warnings: string[];
104
+ };
105
+
106
+ // Token provisioning is console-side (CONSOLE_AUTH_TOKENS_JSON), not
107
+ // flow-agents-side. This hint is folded into the token prompt string itself
108
+ // (rather than printed directly via console.log) so the whole orchestration
109
+ // stays testable via the injected `askHidden` stub's captured prompt argument.
110
+ const TOKEN_PROVISIONING_HINT =
111
+ "Tokens are provisioned by your Console admin via CONSOLE_AUTH_TOKENS_JSON -- see docs/integrations/flow-agents-console.md.";
112
+
113
+ /**
114
+ * Full interactive orchestration used by `interactiveOptions()`: prompts the
115
+ * four-way "Connect to Kontour Console?" choice (default Hosted, showing the
116
+ * real `console-presets.sh`-resolved hosted URL), applies the self-hosted
117
+ * blank-URL guard, prompts hidden token + tenant when a console connection is
118
+ * wanted, runs PR1's pure validators (validate-and-warn, not
119
+ * validate-and-block), and returns any format warnings in `warnings` for the
120
+ * caller to print -- rather than printing directly, so this stays testable
121
+ * via return-value assertions, not console-spy assertions.
122
+ */
123
+ export async function runConsoleConnectWizard(
124
+ io: ConsoleConnectIo,
125
+ defaults: ConsoleConnectDefaults,
126
+ ): Promise<ConsoleConnectResult> {
127
+ const warnings: string[] = [];
128
+
129
+ const choiceAnswer = await io.ask(
130
+ `Connect to Kontour Console? [Hosted (${defaults.hostedUrl})/Local (${defaults.localUrl})/Self-hosted/Skip] [Hosted]: `,
131
+ );
132
+ const choice = normalizeConsoleConnectChoice(choiceAnswer, "hosted");
133
+
134
+ let sinks = mapConsoleConnectChoiceToSinks(choice);
135
+ let consoleUrl: string | undefined;
136
+
137
+ if (needsConsoleUrlPrompt(choice)) {
138
+ const urlAnswer = await io.ask("Self-hosted Console URL: ");
139
+ const resolved = resolveSelfHostedUrlOrFallback(urlAnswer);
140
+ sinks = resolved.sinks;
141
+ consoleUrl = resolved.url ? resolved.url : undefined;
142
+ if (resolved.fallbackWarning) {
143
+ warnings.push(resolved.fallbackWarning);
144
+ } else if (consoleUrl && !isValidConsoleUrl(consoleUrl)) {
145
+ warnings.push(
146
+ `Console URL '${consoleUrl}' does not look valid (expected https://, or http:// to localhost/127.0.0.1); continuing anyway.`,
147
+ );
148
+ }
149
+ }
150
+
151
+ let consoleTokenValue: string | undefined;
152
+ let consoleTenant: string | undefined;
153
+ // Only prompt for credentials when a console sink actually survived (the
154
+ // self-hosted blank-URL fallback above may have already downgraded to
155
+ // local-files, in which case there is nothing to authenticate).
156
+ const wantsConsole = needsConsoleCredentialPrompts(choice) && sinks.some((sink) => sink !== "local-files");
157
+ if (wantsConsole) {
158
+ const tokenAnswer = await io.askHidden(`${TOKEN_PROVISIONING_HINT}\nConsole telemetry token (blank to skip): `);
159
+ consoleTokenValue = tokenAnswer.trim() || undefined;
160
+ if (consoleTokenValue && !isValidConsoleToken(consoleTokenValue)) {
161
+ warnings.push(
162
+ "Console token format looks unusual (expected <=4096 chars, charset [A-Za-z0-9._~+/=-]); continuing anyway.",
163
+ );
164
+ }
165
+ const tenantAnswer = await io.ask("Console tenant ID (blank to skip): ");
166
+ consoleTenant = tenantAnswer.trim() || undefined;
167
+ if (consoleTenant && !isValidConsoleTenant(consoleTenant)) {
168
+ warnings.push(
169
+ "Console tenant format looks unusual (expected charset [A-Za-z0-9._:-]); continuing anyway.",
170
+ );
171
+ }
172
+ }
173
+
174
+ return { telemetrySinks: sinks, consoleUrl, consoleTokenValue, consoleTenant, warnings };
175
+ }
176
+
177
+ export type ConsoleStatus = "connected-verified" | "connected-unverified" | "local-only";
178
+
179
+ type DoctorConsoleShape = {
180
+ sink: "local-only" | "console";
181
+ reachability: { checked: boolean; ok: boolean | null; error?: string; statusCode?: number };
182
+ };
183
+
184
+ /**
185
+ * Pure classifier over the exact structural shape `telemetry-doctor.ts`'s
186
+ * `DoctorReport.console` already has. Deliberately accepts a narrow
187
+ * structural type rather than importing `DoctorReport` itself, keeping this
188
+ * module dependency-free (no lib -> cli import).
189
+ */
190
+ // Actionable detail shown when a Console sink's reachability was never
191
+ // attempted (checked:false). In practice this branch is reached ONLY via
192
+ // telemetry-doctor.ts's pre-attempt guard clauses (endpointAllowed() said no,
193
+ // or the endpoint URL was malformed) -- an actually-attempted check that
194
+ // fails always sets checked:true (see checkConsoleReachability's HEAD
195
+ // request path) and is handled by the branch above, which keeps surfacing
196
+ // error/statusCode unchanged. The most common guard-clause reason by far is
197
+ // a self-hosted/BYO HTTPS host that --allow-network was not passed for, so
198
+ // the hint below points at the one flag that unblocks it.
199
+ const NOT_CHECKED_DETAIL =
200
+ "not checked — self-hosted/BYO consoles need `flow-agents telemetry-doctor --allow-network` to verify reachability";
201
+
202
+ export function describeConsoleStatus(doctor: { console: DoctorConsoleShape }): { status: ConsoleStatus; detail?: string } {
203
+ const { sink, reachability } = doctor.console;
204
+ if (sink === "local-only") return { status: "local-only" };
205
+ if (reachability.checked && reachability.ok === true) return { status: "connected-verified" };
206
+ if (reachability.checked && reachability.ok === false) {
207
+ const detail = reachability.error ?? (reachability.statusCode !== undefined ? `HTTP ${reachability.statusCode}` : "reachability check failed");
208
+ return { status: "connected-unverified", detail };
209
+ }
210
+ // reachability.checked === false: never attempted (see NOT_CHECKED_DETAIL's
211
+ // doc comment for why this is always the not-allowed/skipped reason here,
212
+ // never an attempted-and-failed check).
213
+ return { status: "connected-unverified", detail: NOT_CHECKED_DETAIL };
214
+ }
215
+
216
+ export type PostInstallSummaryInput = {
217
+ runtime: string;
218
+ runtimeAutoDetected: boolean;
219
+ dest: string;
220
+ telemetrySinks: string[];
221
+ consoleStatus: { status: ConsoleStatus; detail?: string };
222
+ tokenConfigured: boolean;
223
+ tenantConfigured: boolean;
224
+ nextSteps: string[];
225
+ };
226
+
227
+ function consoleStatusLine(consoleStatus: { status: ConsoleStatus; detail?: string }): string {
228
+ if (consoleStatus.status === "connected-verified") return "✓ Console: connected + verified";
229
+ if (consoleStatus.status === "connected-unverified") {
230
+ return `✗ Console: connected, unverified${consoleStatus.detail ? `: ${consoleStatus.detail}` : ""}`;
231
+ }
232
+ return "- Console: local-only";
233
+ }
234
+
235
+ /**
236
+ * Pure string[]-builder for the post-install summary block: one array entry
237
+ * per line, so callers `console.log` each entry and tests assert on array
238
+ * contents rather than scraping stdout. Never receives or emits the raw
239
+ * console token value -- only the `tokenConfigured`/`tenantConfigured`
240
+ * booleans are shown.
241
+ */
242
+ export function buildPostInstallSummaryLines(input: PostInstallSummaryInput): string[] {
243
+ const lines: string[] = [];
244
+ lines.push("");
245
+ lines.push("Flow Agents install summary:");
246
+ const runtimeLine = input.runtimeAutoDetected ? `${input.runtime} (auto-detected)` : input.runtime;
247
+ lines.push(` ✓ Runtime: ${runtimeLine}`);
248
+ lines.push(` ✓ Destination: ${input.dest}`);
249
+ lines.push(` ✓ Telemetry sink: ${input.telemetrySinks.length ? input.telemetrySinks.join(", ") : "local-files"}`);
250
+ lines.push(` ${consoleStatusLine(input.consoleStatus)}`);
251
+ if (input.consoleStatus.status !== "local-only") {
252
+ lines.push(` Console token: ${input.tokenConfigured ? "configured" : "not configured"}`);
253
+ lines.push(` Console tenant: ${input.tenantConfigured ? "configured" : "not configured"}`);
254
+ }
255
+ if (input.nextSteps.length) {
256
+ lines.push("");
257
+ lines.push("Next steps:");
258
+ for (const step of input.nextSteps) lines.push(` - ${step}`);
259
+ }
260
+ return lines;
261
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Pure, side-effect-free validation predicates mirroring the bash Console
3
+ * telemetry validators exactly, for the future guided console-connect wizard
4
+ * (PR2/PR3) to reuse at prompt time. Deliberately NOT wired into `init.ts`'s
5
+ * interactive flow in this PR — see install-flow-foundations plan, Thread D.
6
+ *
7
+ * Every predicate returns a plain boolean (never throws), matching the
8
+ * "wizard re-prompts on false" use case, unlike the bash `die`-based
9
+ * validators these mirror (which exit non-zero on failure).
10
+ */
11
+
12
+ /**
13
+ * Mirrors `has_control_chars` (scripts/telemetry/install-console-config.sh:43-45).
14
+ * Rejects values containing a newline, carriage return, or tab.
15
+ */
16
+ export function hasControlChars(value: string): boolean {
17
+ return value.includes("\n") || value.includes("\r") || value.includes("\t");
18
+ }
19
+
20
+ /** Shared https/localhost/127.0.0.1 scheme rule used by both URL validators below. */
21
+ function isHttpsOrLocalhostUrl(value: string): boolean {
22
+ if (value.startsWith("https://")) return true;
23
+ if (value === "http://127.0.0.1" || value.startsWith("http://127.0.0.1:") || value.startsWith("http://127.0.0.1/")) return true;
24
+ if (value === "http://localhost" || value.startsWith("http://localhost:") || value.startsWith("http://localhost/")) return true;
25
+ return false;
26
+ }
27
+
28
+ /**
29
+ * Mirrors `validate_url` (scripts/telemetry/install-console-config.sh:47-62).
30
+ * Blank is valid (the field is optional at install time). Otherwise: no
31
+ * control characters, and must be `https://*`, or `http://` to localhost/127.0.0.1.
32
+ */
33
+ export function isValidConsoleUrl(value: string): boolean {
34
+ if (value === "") return true;
35
+ if (hasControlChars(value)) return false;
36
+ return isHttpsOrLocalhostUrl(value);
37
+ }
38
+
39
+ /**
40
+ * Mirrors `console_telemetry_endpoint_allowed` (scripts/telemetry/lib/transport.sh:20-28).
41
+ * Unlike `isValidConsoleUrl`, blank is INVALID (a non-empty endpoint is
42
+ * required at runtime-post time). This check is deliberately narrower than
43
+ * `hasControlChars`: the bash function only rejects `\n`, `\r`, and a
44
+ * literal `"` (the value is later embedded in a curl config file as a
45
+ * quoted string) — it does NOT reject tab, so this must not call
46
+ * `hasControlChars` (which also rejects tab).
47
+ */
48
+ export function isValidConsoleEndpointStrict(value: string): boolean {
49
+ if (value === "") return false;
50
+ if (value.includes("\n") || value.includes("\r") || value.includes('"')) return false;
51
+ return isHttpsOrLocalhostUrl(value);
52
+ }
53
+
54
+ const CONSOLE_TOKEN_CHARSET = /^[A-Za-z0-9._~+/=-]+$/;
55
+
56
+ /**
57
+ * Mirrors `validate_token` (scripts/telemetry/install-console-config.sh:64-69).
58
+ * Blank is valid (optional field). Otherwise: length <= 4096, no control
59
+ * characters, and charset restricted to `[A-Za-z0-9._~+/=-]`.
60
+ */
61
+ export function isValidConsoleToken(value: string): boolean {
62
+ if (value === "") return true;
63
+ if (value.length > 4096) return false;
64
+ if (hasControlChars(value)) return false;
65
+ return CONSOLE_TOKEN_CHARSET.test(value);
66
+ }
67
+
68
+ const CONSOLE_TENANT_CHARSET = /^[A-Za-z0-9._:-]+$/;
69
+
70
+ /**
71
+ * Mirrors `validate_tenant` (scripts/telemetry/install-console-config.sh:82-86).
72
+ * Blank is valid (optional field). Otherwise: charset restricted to
73
+ * `[A-Za-z0-9._:-]` (no length bound at install time).
74
+ */
75
+ export function isValidConsoleTenant(value: string): boolean {
76
+ if (value === "") return true;
77
+ return CONSOLE_TENANT_CHARSET.test(value);
78
+ }
79
+
80
+ /**
81
+ * Mirrors `console_telemetry_safe_tenant` (scripts/telemetry/lib/transport.sh:35-38).
82
+ * Unlike `isValidConsoleTenant`, non-empty is required and length is bounded
83
+ * to 1..128 (the runtime-safety gate, stricter than the install-time validator).
84
+ */
85
+ export function isSafeConsoleTenantForRuntime(value: string): boolean {
86
+ if (value.length < 1 || value.length > 128) return false;
87
+ return CONSOLE_TENANT_CHARSET.test(value);
88
+ }
@@ -73,11 +73,12 @@ export function resolveFlowFilePath(
73
73
  flowName: string,
74
74
  flowId: string,
75
75
  repoRoot: string,
76
+ allowOverride = true,
76
77
  ): string | null {
77
78
  // Primary defense: reject any slug containing traversal chars or non-identifier chars.
78
79
  if (!SLUG_RE.test(kitId) || !SLUG_RE.test(flowName)) return null;
79
80
 
80
- const override = process.env["FLOW_AGENTS_FLOW_DEFS_DIR"];
81
+ const override = allowOverride ? process.env["FLOW_AGENTS_FLOW_DEFS_DIR"] : undefined;
81
82
 
82
83
  let expectedRoot: string;
83
84
  let flowFilePath: string;
@@ -143,6 +144,7 @@ export type ActiveFlowStep = {
143
144
  stepId: string;
144
145
  gateId: string;
145
146
  gateExpects: GateExpectation[];
147
+ routeBackReasons: string[];
146
148
  /** When resolved through a parent step's uses_flow edge, names the child FlowDefinition that owns the gate. */
147
149
  sourceFlowId?: string;
148
150
  };
@@ -183,10 +185,10 @@ function flowIdParts(flowId: string): { kitId: string; flowName: string } | null
183
185
  return { kitId, flowName };
184
186
  }
185
187
 
186
- function readFlowDefinition(flowId: string, repoRoot: string): FlowDefinition | null {
188
+ function readFlowDefinition(flowId: string, repoRoot: string, allowOverride = true): FlowDefinition | null {
187
189
  const parts = flowIdParts(flowId);
188
190
  if (!parts) return null;
189
- const flowFilePath = resolveFlowFilePath(parts.kitId, parts.flowName, flowId, repoRoot);
191
+ const flowFilePath = resolveFlowFilePath(parts.kitId, parts.flowName, flowId, repoRoot, allowOverride);
190
192
  if (!flowFilePath) return null;
191
193
 
192
194
  try {
@@ -213,6 +215,116 @@ export function resolveFlowStep(flowId: string, stepId: string, repoRoot: string
213
215
  return publicStep;
214
216
  }
215
217
 
218
+ /**
219
+ * Compile Flow Agents' `uses_flow` kit extension into one effective definition
220
+ * that the Flow runtime can evaluate without understanding agent-layer composition.
221
+ */
222
+ export function resolveEffectiveFlowDefinition(flowId: string, repoRoot: string, options: { allowOverride?: boolean } = {}): Record<string, unknown> | null {
223
+ return resolveEffectiveFlowDefinitionInternal(flowId, repoRoot, new Set<string>(), options.allowOverride !== false);
224
+ }
225
+
226
+ function resolveEffectiveFlowDefinitionInternal(flowId: string, repoRoot: string, seen: Set<string>, allowOverride: boolean): Record<string, unknown> | null {
227
+ if (seen.has(flowId)) return null;
228
+ const nextSeen = new Set(seen);
229
+ nextSeen.add(flowId);
230
+ const source = readFlowDefinition(flowId, repoRoot, allowOverride);
231
+ if (!source || !Array.isArray(source.steps)) return null;
232
+ const effective = JSON.parse(JSON.stringify(source)) as FlowDefinition;
233
+ effective.gates = { ...(effective.gates ?? {}) };
234
+
235
+ for (let index = 0; index < source.steps.length; index += 1) {
236
+ const sourceStep = source.steps[index]!;
237
+ if (typeof sourceStep.uses_flow !== "string" || !sourceStep.uses_flow.trim()) continue;
238
+ const child = resolveEffectiveFlowDefinitionInternal(sourceStep.uses_flow, repoRoot, nextSeen, allowOverride) as FlowDefinition | null;
239
+ if (!child || !child.gates) return null;
240
+ const childGateEntry = Object.entries(child.gates).find(([, gate]) => gate?.step === sourceStep.id);
241
+ if (!childGateEntry) return null;
242
+ if (Object.values(effective.gates).some((gate) => gate?.step === sourceStep.id)) return null;
243
+ const [childGateId, childGate] = childGateEntry;
244
+ const childExpects = Array.isArray(childGate.expects) ? childGate.expects : [];
245
+ const exported = exportedExpectations(childExpects, child.exports);
246
+ if (!exported) return null;
247
+ effective.gates[`${sourceStep.uses_flow}:${childGateId}`] = {
248
+ ...childGate,
249
+ expects: exported,
250
+ };
251
+ const { uses_flow: _usesFlow, ...compiledStep } = effective.steps![index]!;
252
+ effective.steps![index] = compiledStep;
253
+ }
254
+ const effectiveSteps = effective.steps!;
255
+ const done = effectiveSteps.find((step) => step.id === "done" && step.next === null);
256
+ if (done && !Object.values(effective.gates).some((gate) => gate?.step === "done")) {
257
+ effective.steps = effectiveSteps
258
+ .filter((step) => step.id !== "done")
259
+ .map((step) => step.next === "done" ? { ...step, next: null } : step);
260
+ }
261
+ return effective as unknown as Record<string, unknown>;
262
+ }
263
+
264
+ /**
265
+ * A single (stepId, gateId) → expects[] tuple, as part of the FULL enumeration of every gate in
266
+ * a FlowDefinition (across every step — see resolveAllFlowGateExpects below), not just the
267
+ * currently-active one.
268
+ */
269
+ export type FlowGateExpectsEntry = {
270
+ stepId: string;
271
+ gateId: string;
272
+ gateExpects: GateExpectation[];
273
+ };
274
+
275
+ /**
276
+ * Enumerate the gate expects[] for EVERY step in a FlowDefinition (#270 CRITICAL/HIGH fix) —
277
+ * not just the currently-active step. This is what lets a stamped gate-claim's
278
+ * (expectation_id, claim_type, subject_type, step_id) tuple be validated against the FULL,
279
+ * declared shape of the flow, instead of only against whatever step happens to be active at
280
+ * validation time (which is a DIFFERENT, and usually wrong, question: the stamp names the step
281
+ * it was ORIGINALLY recorded at, which by design may not be the currently-active one — see
282
+ * buildTrustBundle's #270(a)/(c) step_id-freezing comments in workflow-sidecar.ts).
283
+ *
284
+ * Walks flowDef.steps[] (in declaration order) and resolves each step's gate via the same
285
+ * resolveFlowStepInternal used by resolveFlowStep/resolveActiveFlowStep — including the
286
+ * uses_flow composed-step case, so a gate that lives in a child FlowDefinition (e.g.
287
+ * builder.publish-learn's pr-open-gate, composed into builder.build's "pr-open" step) is
288
+ * enumerated too, exports-filtered exactly as a live resolution would be.
289
+ *
290
+ * Pure and synchronous — no throws, fail-open (returns []) on any error, mirroring every other
291
+ * resolver in this module.
292
+ *
293
+ * Callers MUST distinguish the two null-ish outcomes (#270 MEDIUM fix, iteration 3):
294
+ * - `null` → the FlowDefinition could not be LOADED at all (missing file, unreadable, invalid
295
+ * JSON, or no `steps[]` array) — the caller has no basis to validate anything against this
296
+ * flow and must fail closed with a dedicated "cannot be loaded" message, never the "stamp
297
+ * does not match any expects[]" (forged/corrupt) message; those are different failure
298
+ * classes with different remedies (a load failure means "fix/restore the FlowDefinition
299
+ * file or the flowId", not "this stamp was forged").
300
+ * - `[]` → the FlowDefinition LOADED successfully but genuinely declares no steps with
301
+ * matching gates (or no gates at all) — a real, if unusual, flow shape, not an error.
302
+ *
303
+ * @param flowId e.g. "builder.build" — kitId is extracted as the prefix before the first ".".
304
+ * @param repoRoot Absolute path to the repository root (kits/ lives here).
305
+ * Honored only when FLOW_AGENTS_FLOW_DEFS_DIR is not set.
306
+ * @returns Every (stepId, gateId, gateExpects) tuple in the flow; `[]` when the flow loads but
307
+ * declares no matching gates; `null` when the FlowDefinition cannot be loaded/parsed at all.
308
+ */
309
+ export function resolveAllFlowGateExpects(flowId: string, repoRoot: string): FlowGateExpectsEntry[] | null {
310
+ const flowDef = readFlowDefinition(flowId, repoRoot);
311
+ if (!flowDef || typeof flowDef !== "object" || !Array.isArray(flowDef.steps)) return null;
312
+ const out: FlowGateExpectsEntry[] = [];
313
+ const seenGateIds = new Set<string>();
314
+ for (const step of flowDef.steps) {
315
+ if (!step || typeof step.id !== "string" || !step.id) continue;
316
+ const resolved = resolveFlowStepInternal(flowId, step.id, repoRoot, new Set<string>());
317
+ if (!resolved) continue;
318
+ // A gate can be reached by more than one step declaration in degenerate/duplicate step
319
+ // lists; de-dupe by gateId so callers never see the same expects[] entries twice.
320
+ const dedupeKey = `${resolved.gateId}`;
321
+ if (seenGateIds.has(dedupeKey)) continue;
322
+ seenGateIds.add(dedupeKey);
323
+ out.push({ stepId: resolved.stepId, gateId: resolved.gateId, gateExpects: resolved.gateExpects });
324
+ }
325
+ return out;
326
+ }
327
+
216
328
  function expectationExportKeys(expectation: GateExpectation): string[] {
217
329
  const keys: string[] = [];
218
330
  if (typeof expectation.id === "string" && expectation.id) keys.push(expectation.id);
@@ -249,7 +361,7 @@ function resolveFlowStepInternal(flowId: string, stepId: string, repoRoot: strin
249
361
  for (const [gateId, gate] of Object.entries(flowDef.gates)) {
250
362
  if (!gate || gate.step !== stepId) continue;
251
363
  const expects = Array.isArray(gate.expects) ? gate.expects : [];
252
- return { flowId, stepId, gateId, gateExpects: expects, flowExports: flowDef.exports };
364
+ return { flowId, stepId, gateId, gateExpects: expects, routeBackReasons: Object.keys(gate.on_route_back ?? {}), flowExports: flowDef.exports };
253
365
  }
254
366
  }
255
367
 
@@ -266,6 +378,7 @@ function resolveFlowStepInternal(flowId: string, stepId: string, repoRoot: strin
266
378
  stepId,
267
379
  gateId: `${child.flowId}:${child.gateId}`,
268
380
  gateExpects: childGateExpects,
381
+ routeBackReasons: child.routeBackReasons,
269
382
  sourceFlowId: child.flowId,
270
383
  flowExports: flowDef.exports,
271
384
  };