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