@kontourai/flow-agents 3.11.0 → 3.12.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 (254) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/build/src/builder-flow-run-adapter.d.ts +1 -0
  3. package/build/src/builder-flow-run-adapter.js +2 -0
  4. package/build/src/builder-flow-runtime.d.ts +5 -0
  5. package/build/src/builder-flow-runtime.js +90 -68
  6. package/build/src/builder-gate-action-envelope.d.ts +152 -0
  7. package/build/src/builder-gate-action-envelope.js +368 -0
  8. package/build/src/cli/public-contracts.d.ts +139 -0
  9. package/build/src/cli/public-contracts.js +37 -0
  10. package/build/src/cli/workflow-sidecar.js +9 -13
  11. package/build/src/cli/workflow.d.ts +1 -0
  12. package/build/src/cli/workflow.js +49 -12
  13. package/build/src/continuation-driver.d.ts +40 -3
  14. package/build/src/continuation-driver.js +361 -412
  15. package/build/src/continuation-persistence.d.ts +4 -0
  16. package/build/src/continuation-persistence.js +296 -0
  17. package/build/src/continuation-validation.d.ts +9 -0
  18. package/build/src/continuation-validation.js +169 -0
  19. package/build/src/flow-kit/action-metadata.d.ts +28 -0
  20. package/build/src/flow-kit/action-metadata.js +162 -0
  21. package/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  22. package/build/src/flow-kit/action-repository-validation.js +237 -0
  23. package/build/src/flow-kit/validate.d.ts +2 -14
  24. package/build/src/flow-kit/validate.js +18 -345
  25. package/build/src/index.d.ts +4 -2
  26. package/build/src/index.js +2 -1
  27. package/dist/base/build/package.json +1 -1
  28. package/dist/base/build/src/builder-flow-run-adapter.d.ts +1 -0
  29. package/dist/base/build/src/builder-flow-run-adapter.js +2 -0
  30. package/dist/base/build/src/builder-flow-runtime.d.ts +5 -0
  31. package/dist/base/build/src/builder-flow-runtime.js +90 -68
  32. package/dist/base/build/src/builder-gate-action-envelope.d.ts +152 -0
  33. package/dist/base/build/src/builder-gate-action-envelope.js +368 -0
  34. package/dist/base/build/src/cli/public-contracts.d.ts +139 -0
  35. package/dist/base/build/src/cli/public-contracts.js +37 -0
  36. package/dist/base/build/src/cli/workflow-sidecar.js +9 -13
  37. package/dist/base/build/src/cli/workflow.d.ts +1 -0
  38. package/dist/base/build/src/cli/workflow.js +49 -12
  39. package/dist/base/build/src/continuation-driver.d.ts +40 -3
  40. package/dist/base/build/src/continuation-driver.js +361 -412
  41. package/dist/base/build/src/continuation-persistence.d.ts +4 -0
  42. package/dist/base/build/src/continuation-persistence.js +296 -0
  43. package/dist/base/build/src/continuation-validation.d.ts +9 -0
  44. package/dist/base/build/src/continuation-validation.js +169 -0
  45. package/dist/base/build/src/flow-kit/action-metadata.d.ts +28 -0
  46. package/dist/base/build/src/flow-kit/action-metadata.js +162 -0
  47. package/dist/base/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  48. package/dist/base/build/src/flow-kit/action-repository-validation.js +237 -0
  49. package/dist/base/build/src/flow-kit/validate.d.ts +2 -14
  50. package/dist/base/build/src/flow-kit/validate.js +18 -345
  51. package/dist/base/build/src/index.d.ts +4 -2
  52. package/dist/base/build/src/index.js +2 -1
  53. package/dist/base/docs/migrations.md +5 -0
  54. package/dist/base/docs/public-workflow-cli.md +41 -5
  55. package/dist/base/docs/spec/builder-flow-runtime.md +117 -0
  56. package/dist/base/evals/integration/test_builder_step_producers.sh +60 -21
  57. package/dist/base/evals/integration/test_bundle_install.sh +15 -15
  58. package/dist/base/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  59. package/dist/base/install.sh +1 -1
  60. package/dist/base/kits/builder/kit.json +88 -14
  61. package/dist/claude-code/build/package.json +1 -1
  62. package/dist/claude-code/build/src/builder-flow-run-adapter.d.ts +1 -0
  63. package/dist/claude-code/build/src/builder-flow-run-adapter.js +2 -0
  64. package/dist/claude-code/build/src/builder-flow-runtime.d.ts +5 -0
  65. package/dist/claude-code/build/src/builder-flow-runtime.js +90 -68
  66. package/dist/claude-code/build/src/builder-gate-action-envelope.d.ts +152 -0
  67. package/dist/claude-code/build/src/builder-gate-action-envelope.js +368 -0
  68. package/dist/claude-code/build/src/cli/public-contracts.d.ts +139 -0
  69. package/dist/claude-code/build/src/cli/public-contracts.js +37 -0
  70. package/dist/claude-code/build/src/cli/workflow-sidecar.js +9 -13
  71. package/dist/claude-code/build/src/cli/workflow.d.ts +1 -0
  72. package/dist/claude-code/build/src/cli/workflow.js +49 -12
  73. package/dist/claude-code/build/src/continuation-driver.d.ts +40 -3
  74. package/dist/claude-code/build/src/continuation-driver.js +361 -412
  75. package/dist/claude-code/build/src/continuation-persistence.d.ts +4 -0
  76. package/dist/claude-code/build/src/continuation-persistence.js +296 -0
  77. package/dist/claude-code/build/src/continuation-validation.d.ts +9 -0
  78. package/dist/claude-code/build/src/continuation-validation.js +169 -0
  79. package/dist/claude-code/build/src/flow-kit/action-metadata.d.ts +28 -0
  80. package/dist/claude-code/build/src/flow-kit/action-metadata.js +162 -0
  81. package/dist/claude-code/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  82. package/dist/claude-code/build/src/flow-kit/action-repository-validation.js +237 -0
  83. package/dist/claude-code/build/src/flow-kit/validate.d.ts +2 -14
  84. package/dist/claude-code/build/src/flow-kit/validate.js +18 -345
  85. package/dist/claude-code/build/src/index.d.ts +4 -2
  86. package/dist/claude-code/build/src/index.js +2 -1
  87. package/dist/claude-code/docs/migrations.md +5 -0
  88. package/dist/claude-code/docs/public-workflow-cli.md +41 -5
  89. package/dist/claude-code/docs/spec/builder-flow-runtime.md +117 -0
  90. package/dist/claude-code/evals/integration/test_builder_step_producers.sh +60 -21
  91. package/dist/claude-code/evals/integration/test_bundle_install.sh +15 -15
  92. package/dist/claude-code/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  93. package/dist/claude-code/install.sh +1 -1
  94. package/dist/claude-code/kits/builder/kit.json +88 -14
  95. package/dist/codex/build/package.json +1 -1
  96. package/dist/codex/build/src/builder-flow-run-adapter.d.ts +1 -0
  97. package/dist/codex/build/src/builder-flow-run-adapter.js +2 -0
  98. package/dist/codex/build/src/builder-flow-runtime.d.ts +5 -0
  99. package/dist/codex/build/src/builder-flow-runtime.js +90 -68
  100. package/dist/codex/build/src/builder-gate-action-envelope.d.ts +152 -0
  101. package/dist/codex/build/src/builder-gate-action-envelope.js +368 -0
  102. package/dist/codex/build/src/cli/public-contracts.d.ts +139 -0
  103. package/dist/codex/build/src/cli/public-contracts.js +37 -0
  104. package/dist/codex/build/src/cli/workflow-sidecar.js +9 -13
  105. package/dist/codex/build/src/cli/workflow.d.ts +1 -0
  106. package/dist/codex/build/src/cli/workflow.js +49 -12
  107. package/dist/codex/build/src/continuation-driver.d.ts +40 -3
  108. package/dist/codex/build/src/continuation-driver.js +361 -412
  109. package/dist/codex/build/src/continuation-persistence.d.ts +4 -0
  110. package/dist/codex/build/src/continuation-persistence.js +296 -0
  111. package/dist/codex/build/src/continuation-validation.d.ts +9 -0
  112. package/dist/codex/build/src/continuation-validation.js +169 -0
  113. package/dist/codex/build/src/flow-kit/action-metadata.d.ts +28 -0
  114. package/dist/codex/build/src/flow-kit/action-metadata.js +162 -0
  115. package/dist/codex/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  116. package/dist/codex/build/src/flow-kit/action-repository-validation.js +237 -0
  117. package/dist/codex/build/src/flow-kit/validate.d.ts +2 -14
  118. package/dist/codex/build/src/flow-kit/validate.js +18 -345
  119. package/dist/codex/build/src/index.d.ts +4 -2
  120. package/dist/codex/build/src/index.js +2 -1
  121. package/dist/codex/docs/migrations.md +5 -0
  122. package/dist/codex/docs/public-workflow-cli.md +41 -5
  123. package/dist/codex/docs/spec/builder-flow-runtime.md +117 -0
  124. package/dist/codex/evals/integration/test_builder_step_producers.sh +60 -21
  125. package/dist/codex/evals/integration/test_bundle_install.sh +15 -15
  126. package/dist/codex/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  127. package/dist/codex/install.sh +1 -1
  128. package/dist/codex/kits/builder/kit.json +88 -14
  129. package/dist/kiro/build/package.json +1 -1
  130. package/dist/kiro/build/src/builder-flow-run-adapter.d.ts +1 -0
  131. package/dist/kiro/build/src/builder-flow-run-adapter.js +2 -0
  132. package/dist/kiro/build/src/builder-flow-runtime.d.ts +5 -0
  133. package/dist/kiro/build/src/builder-flow-runtime.js +90 -68
  134. package/dist/kiro/build/src/builder-gate-action-envelope.d.ts +152 -0
  135. package/dist/kiro/build/src/builder-gate-action-envelope.js +368 -0
  136. package/dist/kiro/build/src/cli/public-contracts.d.ts +139 -0
  137. package/dist/kiro/build/src/cli/public-contracts.js +37 -0
  138. package/dist/kiro/build/src/cli/workflow-sidecar.js +9 -13
  139. package/dist/kiro/build/src/cli/workflow.d.ts +1 -0
  140. package/dist/kiro/build/src/cli/workflow.js +49 -12
  141. package/dist/kiro/build/src/continuation-driver.d.ts +40 -3
  142. package/dist/kiro/build/src/continuation-driver.js +361 -412
  143. package/dist/kiro/build/src/continuation-persistence.d.ts +4 -0
  144. package/dist/kiro/build/src/continuation-persistence.js +296 -0
  145. package/dist/kiro/build/src/continuation-validation.d.ts +9 -0
  146. package/dist/kiro/build/src/continuation-validation.js +169 -0
  147. package/dist/kiro/build/src/flow-kit/action-metadata.d.ts +28 -0
  148. package/dist/kiro/build/src/flow-kit/action-metadata.js +162 -0
  149. package/dist/kiro/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  150. package/dist/kiro/build/src/flow-kit/action-repository-validation.js +237 -0
  151. package/dist/kiro/build/src/flow-kit/validate.d.ts +2 -14
  152. package/dist/kiro/build/src/flow-kit/validate.js +18 -345
  153. package/dist/kiro/build/src/index.d.ts +4 -2
  154. package/dist/kiro/build/src/index.js +2 -1
  155. package/dist/kiro/docs/migrations.md +5 -0
  156. package/dist/kiro/docs/public-workflow-cli.md +41 -5
  157. package/dist/kiro/docs/spec/builder-flow-runtime.md +117 -0
  158. package/dist/kiro/evals/integration/test_builder_step_producers.sh +60 -21
  159. package/dist/kiro/evals/integration/test_bundle_install.sh +15 -15
  160. package/dist/kiro/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  161. package/dist/kiro/install.sh +1 -1
  162. package/dist/kiro/kits/builder/kit.json +88 -14
  163. package/dist/opencode/build/package.json +1 -1
  164. package/dist/opencode/build/src/builder-flow-run-adapter.d.ts +1 -0
  165. package/dist/opencode/build/src/builder-flow-run-adapter.js +2 -0
  166. package/dist/opencode/build/src/builder-flow-runtime.d.ts +5 -0
  167. package/dist/opencode/build/src/builder-flow-runtime.js +90 -68
  168. package/dist/opencode/build/src/builder-gate-action-envelope.d.ts +152 -0
  169. package/dist/opencode/build/src/builder-gate-action-envelope.js +368 -0
  170. package/dist/opencode/build/src/cli/public-contracts.d.ts +139 -0
  171. package/dist/opencode/build/src/cli/public-contracts.js +37 -0
  172. package/dist/opencode/build/src/cli/workflow-sidecar.js +9 -13
  173. package/dist/opencode/build/src/cli/workflow.d.ts +1 -0
  174. package/dist/opencode/build/src/cli/workflow.js +49 -12
  175. package/dist/opencode/build/src/continuation-driver.d.ts +40 -3
  176. package/dist/opencode/build/src/continuation-driver.js +361 -412
  177. package/dist/opencode/build/src/continuation-persistence.d.ts +4 -0
  178. package/dist/opencode/build/src/continuation-persistence.js +296 -0
  179. package/dist/opencode/build/src/continuation-validation.d.ts +9 -0
  180. package/dist/opencode/build/src/continuation-validation.js +169 -0
  181. package/dist/opencode/build/src/flow-kit/action-metadata.d.ts +28 -0
  182. package/dist/opencode/build/src/flow-kit/action-metadata.js +162 -0
  183. package/dist/opencode/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  184. package/dist/opencode/build/src/flow-kit/action-repository-validation.js +237 -0
  185. package/dist/opencode/build/src/flow-kit/validate.d.ts +2 -14
  186. package/dist/opencode/build/src/flow-kit/validate.js +18 -345
  187. package/dist/opencode/build/src/index.d.ts +4 -2
  188. package/dist/opencode/build/src/index.js +2 -1
  189. package/dist/opencode/docs/migrations.md +5 -0
  190. package/dist/opencode/docs/public-workflow-cli.md +41 -5
  191. package/dist/opencode/docs/spec/builder-flow-runtime.md +117 -0
  192. package/dist/opencode/evals/integration/test_builder_step_producers.sh +60 -21
  193. package/dist/opencode/evals/integration/test_bundle_install.sh +15 -15
  194. package/dist/opencode/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  195. package/dist/opencode/install.sh +1 -1
  196. package/dist/opencode/kits/builder/kit.json +88 -14
  197. package/dist/pi/build/package.json +1 -1
  198. package/dist/pi/build/src/builder-flow-run-adapter.d.ts +1 -0
  199. package/dist/pi/build/src/builder-flow-run-adapter.js +2 -0
  200. package/dist/pi/build/src/builder-flow-runtime.d.ts +5 -0
  201. package/dist/pi/build/src/builder-flow-runtime.js +90 -68
  202. package/dist/pi/build/src/builder-gate-action-envelope.d.ts +152 -0
  203. package/dist/pi/build/src/builder-gate-action-envelope.js +368 -0
  204. package/dist/pi/build/src/cli/public-contracts.d.ts +139 -0
  205. package/dist/pi/build/src/cli/public-contracts.js +37 -0
  206. package/dist/pi/build/src/cli/workflow-sidecar.js +9 -13
  207. package/dist/pi/build/src/cli/workflow.d.ts +1 -0
  208. package/dist/pi/build/src/cli/workflow.js +49 -12
  209. package/dist/pi/build/src/continuation-driver.d.ts +40 -3
  210. package/dist/pi/build/src/continuation-driver.js +361 -412
  211. package/dist/pi/build/src/continuation-persistence.d.ts +4 -0
  212. package/dist/pi/build/src/continuation-persistence.js +296 -0
  213. package/dist/pi/build/src/continuation-validation.d.ts +9 -0
  214. package/dist/pi/build/src/continuation-validation.js +169 -0
  215. package/dist/pi/build/src/flow-kit/action-metadata.d.ts +28 -0
  216. package/dist/pi/build/src/flow-kit/action-metadata.js +162 -0
  217. package/dist/pi/build/src/flow-kit/action-repository-validation.d.ts +16 -0
  218. package/dist/pi/build/src/flow-kit/action-repository-validation.js +237 -0
  219. package/dist/pi/build/src/flow-kit/validate.d.ts +2 -14
  220. package/dist/pi/build/src/flow-kit/validate.js +18 -345
  221. package/dist/pi/build/src/index.d.ts +4 -2
  222. package/dist/pi/build/src/index.js +2 -1
  223. package/dist/pi/docs/migrations.md +5 -0
  224. package/dist/pi/docs/public-workflow-cli.md +41 -5
  225. package/dist/pi/docs/spec/builder-flow-runtime.md +117 -0
  226. package/dist/pi/evals/integration/test_builder_step_producers.sh +60 -21
  227. package/dist/pi/evals/integration/test_bundle_install.sh +15 -15
  228. package/dist/pi/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  229. package/dist/pi/install.sh +1 -1
  230. package/dist/pi/kits/builder/kit.json +88 -14
  231. package/docs/migrations.md +5 -0
  232. package/docs/public-workflow-cli.md +41 -5
  233. package/docs/spec/builder-flow-runtime.md +117 -0
  234. package/evals/integration/test_builder_step_producers.sh +60 -21
  235. package/evals/integration/test_bundle_install.sh +15 -15
  236. package/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
  237. package/kits/builder/kit.json +88 -14
  238. package/package.json +1 -1
  239. package/src/builder-flow-run-adapter.ts +3 -0
  240. package/src/builder-flow-runtime.ts +73 -46
  241. package/src/builder-gate-action-envelope.ts +531 -0
  242. package/src/cli/builder-flow-runtime.test.mjs +289 -58
  243. package/src/cli/continuation-driver.test.mjs +542 -24
  244. package/src/cli/kit-metadata-security.test.mjs +161 -8
  245. package/src/cli/public-contracts.ts +41 -0
  246. package/src/cli/workflow-sidecar.ts +9 -13
  247. package/src/cli/workflow.ts +48 -12
  248. package/src/continuation-driver.ts +466 -378
  249. package/src/continuation-persistence.ts +228 -0
  250. package/src/continuation-validation.ts +137 -0
  251. package/src/flow-kit/action-metadata.ts +155 -0
  252. package/src/flow-kit/action-repository-validation.ts +192 -0
  253. package/src/flow-kit/validate.ts +19 -326
  254. package/src/index.ts +17 -0
@@ -0,0 +1,192 @@
1
+ import fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { sameStringSet, type KitFlowStepActionEntry } from "./action-metadata.js";
4
+
5
+ type SkillRoleEntry = { skill_id: string; role: string; flow_id?: string; step_ids: string[]; expectation_ids: string[] };
6
+ type FlowExpectation = { id: string; exportKeys: Set<string> };
7
+ type FlowMetadata = { steps: Set<string>; expectationsByStep: Map<string, Map<string, FlowExpectation>>; usesFlowByStep: Map<string, string>; exports: Set<string> };
8
+ type EffectiveFlowStep = { sourceFlowId: string; stepId: string; expectations: Map<string, FlowExpectation>; flowIds: Set<string> };
9
+ const MAX_FLOW_DEFINITION_BYTES = 1_048_576;
10
+
11
+ export function validateActionRepositoryMetadata(input: {
12
+ kitDir: string;
13
+ manifestPath: string;
14
+ manifest: Record<string, unknown>;
15
+ actions: KitFlowStepActionEntry[];
16
+ skillRoles: SkillRoleEntry[];
17
+ }): string[] {
18
+ const errors: string[] = [];
19
+ validateDeclaredSkills(input.manifest, input.skillRoles, input.manifestPath, errors);
20
+ const flows = loadFlowMetadata(input.kitDir, input.manifest, input.manifestPath, errors);
21
+ const resolve = effectiveStepResolver(flows);
22
+ validateRoleReferences(input.skillRoles, flows, resolve, input.manifestPath, errors);
23
+ const owners = seedSkillOwners(input.skillRoles, resolve);
24
+ validateActions(input.actions, input.skillRoles, flows, resolve, owners, input.manifest, input.manifestPath, errors);
25
+ validateOwnerCardinality(flows, owners, input.manifestPath, errors);
26
+ return errors;
27
+ }
28
+
29
+ function validateDeclaredSkills(manifest: Record<string, unknown>, roles: SkillRoleEntry[], manifestPath: string, errors: string[]): void {
30
+ const declared = new Set(Array.isArray(manifest.skills) ? manifest.skills.flatMap((entry) => typeof entry === "object" && entry !== null && typeof (entry as Record<string, unknown>).id === "string" ? [(entry as Record<string, unknown>).id as string] : []) : []);
31
+ const bound = new Set(roles.map((entry) => entry.skill_id));
32
+ for (const id of declared) if (!bound.has(id)) errors.push(`${manifestPath}: skill_roles is missing declared skill '${id}'`);
33
+ for (const id of bound) if (!declared.has(id)) errors.push(`${manifestPath}: skill_roles references undeclared skill '${id}'`);
34
+ }
35
+
36
+ function loadFlowMetadata(kitDir: string, manifest: Record<string, unknown>, manifestPath: string, errors: string[]): Map<string, FlowMetadata> {
37
+ const flows = new Map<string, FlowMetadata>();
38
+ if (!Array.isArray(manifest.flows)) return flows;
39
+ for (const entry of manifest.flows) {
40
+ if (typeof entry !== "object" || entry === null) continue;
41
+ const flow = entry as Record<string, unknown>;
42
+ if (typeof flow.id !== "string" || typeof flow.path !== "string") continue;
43
+ const safe = readSafeFlowDefinition(kitDir, flow.path);
44
+ if (!safe.definition) { errors.push(`${manifestPath}: flows '${flow.id}' ${safe.error}`); continue; }
45
+ const definition = safe.definition;
46
+ const steps = Array.isArray(definition.steps) ? definition.steps.filter(isRecord) : [];
47
+ const stepIds = new Set(steps.flatMap((step) => typeof step.id === "string" ? [step.id] : []));
48
+ const usesFlowByStep = new Map(steps.flatMap((step) => typeof step.id === "string" && typeof step.uses_flow === "string" ? [[step.id, step.uses_flow] as const] : []));
49
+ flows.set(flow.id, { steps: stepIds, usesFlowByStep, expectationsByStep: expectationsByStep(definition), exports: new Set(Array.isArray(definition.exports) ? definition.exports.filter((value): value is string => typeof value === "string" && value.length > 0) : []) });
50
+ }
51
+ return flows;
52
+ }
53
+
54
+ function expectationsByStep(definition: Record<string, unknown>): Map<string, Map<string, FlowExpectation>> {
55
+ const result = new Map<string, Map<string, FlowExpectation>>();
56
+ if (!isRecord(definition.gates)) return result;
57
+ for (const gate of Object.values(definition.gates)) {
58
+ if (!isRecord(gate) || typeof gate.step !== "string" || !Array.isArray(gate.expects)) continue;
59
+ const expectations = result.get(gate.step) ?? new Map<string, FlowExpectation>();
60
+ for (const value of gate.expects) {
61
+ if (!isRecord(value) || typeof value.id !== "string") continue;
62
+ const exportKeys = new Set([value.id]);
63
+ if (isRecord(value.bundle_claim) && typeof value.bundle_claim.claimType === "string") exportKeys.add(value.bundle_claim.claimType);
64
+ expectations.set(value.id, { id: value.id, exportKeys });
65
+ }
66
+ result.set(gate.step, expectations);
67
+ }
68
+ return result;
69
+ }
70
+
71
+ function effectiveStepResolver(flows: Map<string, FlowMetadata>): (flowId: string, stepId: string, seen?: Set<string>) => EffectiveFlowStep | undefined {
72
+ const resolve = (flowId: string, stepId: string, seen = new Set<string>()): EffectiveFlowStep | undefined => {
73
+ const cycleKey = `${flowId}\0${stepId}`;
74
+ if (seen.has(cycleKey)) return undefined;
75
+ seen.add(cycleKey);
76
+ const flow = flows.get(flowId);
77
+ if (!flow) return undefined;
78
+ const direct = flow.expectationsByStep.get(stepId);
79
+ if (direct) return { sourceFlowId: flowId, stepId, expectations: direct, flowIds: new Set([flowId]) };
80
+ const childFlowId = flow.usesFlowByStep.get(stepId);
81
+ if (!childFlowId) return { sourceFlowId: flowId, stepId, expectations: new Map(), flowIds: new Set([flowId]) };
82
+ const child = resolve(childFlowId, stepId, seen);
83
+ const childFlow = flows.get(childFlowId);
84
+ if (!child || !childFlow || [...child.expectations.values()].some((expectation) => ![...expectation.exportKeys].some((key) => childFlow.exports.has(key)))) return undefined;
85
+ child.flowIds.add(flowId);
86
+ return child;
87
+ };
88
+ return resolve;
89
+ }
90
+
91
+ function validateRoleReferences(roles: SkillRoleEntry[], flows: Map<string, FlowMetadata>, resolve: ReturnType<typeof effectiveStepResolver>, manifestPath: string, errors: string[]): void {
92
+ for (const row of roles) {
93
+ if (!row.flow_id) continue;
94
+ const flow = flows.get(row.flow_id);
95
+ if (!flow) { errors.push(`${manifestPath}: skill_roles '${row.skill_id}' references unknown flow '${row.flow_id}'`); continue; }
96
+ for (const stepId of row.step_ids) if (!flow.steps.has(stepId)) errors.push(`${manifestPath}: skill_roles '${row.skill_id}' references unknown step '${row.flow_id}/${stepId}'`);
97
+ const allowed = new Set(row.step_ids.flatMap((stepId) => [...(resolve(row.flow_id!, stepId)?.expectations.keys() ?? [])]));
98
+ for (const expectationId of row.expectation_ids) if (!allowed.has(expectationId)) errors.push(`${manifestPath}: skill_roles '${row.skill_id}' expectation '${expectationId}' is not owned by its bound step(s)`);
99
+ }
100
+ }
101
+
102
+ function seedSkillOwners(roles: SkillRoleEntry[], resolve: ReturnType<typeof effectiveStepResolver>): Map<string, string[]> {
103
+ const owners = new Map<string, string[]>();
104
+ for (const row of roles) {
105
+ if (row.role !== "step" || !row.flow_id) continue;
106
+ for (const stepId of row.step_ids) {
107
+ const effective = resolve(row.flow_id, stepId);
108
+ if (!effective) continue;
109
+ for (const expectationId of row.expectation_ids) if (effective.expectations.has(expectationId)) {
110
+ const key = `${effective.sourceFlowId}\0${effective.stepId}\0${expectationId}`;
111
+ owners.set(key, [...(owners.get(key) ?? []), `skill:${row.skill_id}`]);
112
+ }
113
+ }
114
+ }
115
+ return owners;
116
+ }
117
+
118
+ function validateActions(actions: KitFlowStepActionEntry[], roles: SkillRoleEntry[], flows: Map<string, FlowMetadata>, resolve: ReturnType<typeof effectiveStepResolver>, owners: Map<string, string[]>, manifest: Record<string, unknown>, manifestPath: string, errors: string[]): void {
119
+ const roleByShortId = new Map(roles.map((entry) => [entry.skill_id.replace(`${String(manifest.id)}.`, ""), entry]));
120
+ for (const action of actions) {
121
+ const flow = flows.get(action.flow_id);
122
+ if (!flow || !flow.steps.has(action.step_id)) { errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' references an unknown flow step`); continue; }
123
+ const effective = resolve(action.flow_id, action.step_id);
124
+ if (!effective) { errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' cannot resolve its composed Flow step`); continue; }
125
+ for (const id of action.expectation_ids) if (!effective.expectations.has(id)) errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' operation expectation '${id}' is not owned by its resolved Flow step`);
126
+ if (!sameStringSet(action.expectation_ids, [...effective.expectations.keys()])) errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' expectation_ids must exactly equal its resolved Flow expectation set`);
127
+ for (const skill of action.skills) {
128
+ const row = roleByShortId.get(skill);
129
+ if (!row || row.role !== "step" || !row.flow_id || !effective.flowIds.has(row.flow_id) || !row.step_ids.includes(action.step_id)) errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' skill '${skill}' must match one step-role binding`);
130
+ }
131
+ for (const expectationId of effective.expectations.keys()) {
132
+ const binding = action.expectation_bindings.find((entry) => entry.expectation_id === expectationId);
133
+ if (binding?.interface !== "operation") continue;
134
+ const key = `${effective.sourceFlowId}\0${effective.stepId}\0${expectationId}`;
135
+ owners.set(key, [...(owners.get(key) ?? []), `operation:${action.flow_id}/${action.step_id}`]);
136
+ }
137
+ }
138
+ }
139
+
140
+ function validateOwnerCardinality(flows: Map<string, FlowMetadata>, owners: Map<string, string[]>, manifestPath: string, errors: string[]): void {
141
+ for (const [flowId, flow] of flows) for (const [stepId, expectations] of flow.expectationsByStep) for (const expectationId of expectations.keys()) {
142
+ const found = owners.get(`${flowId}\0${stepId}\0${expectationId}`) ?? [];
143
+ if (found.length !== 1) errors.push(`${manifestPath}: flow expectation '${flowId}/${stepId}/${expectationId}' must have exactly one producer owner; found ${found.length}`);
144
+ }
145
+ }
146
+
147
+ function readSafeFlowDefinition(kitDir: string, relativePath: string): { definition?: Record<string, unknown>; error?: string } {
148
+ const root = path.resolve(kitDir);
149
+ if (path.isAbsolute(relativePath)) return { error: "path must be relative" };
150
+ const lexical = path.resolve(root, relativePath);
151
+ if (lexical === root || !lexical.startsWith(`${root}${path.sep}`)) return { error: "path must stay inside the kit directory" };
152
+ let fd: number | undefined;
153
+ try {
154
+ const realRoot = fs.realpathSync(root);
155
+ const initial = lstatSafePath(realRoot, relativePath);
156
+ if (!initial.file || !initial.stat) return { error: initial.error };
157
+ if (!initial.stat.isFile()) return { error: "path must reference a regular file" };
158
+ if (initial.stat.size > MAX_FLOW_DEFINITION_BYTES) return { error: `file exceeds ${MAX_FLOW_DEFINITION_BYTES} bytes` };
159
+ if (typeof fs.constants.O_NOFOLLOW !== "number") return { error: "O_NOFOLLOW is unavailable" };
160
+ fd = fs.openSync(initial.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
161
+ const opened = fs.fstatSync(fd);
162
+ if (!opened.isFile() || !sameIdentity(initial.stat, opened)) return { error: "flow definition identity changed while opening" };
163
+ const verified = lstatSafePath(realRoot, relativePath);
164
+ if (!verified.stat || !sameIdentity(opened, verified.stat)) return { error: "flow definition identity changed while opening" };
165
+ const bytes = Buffer.alloc(opened.size);
166
+ let offset = 0;
167
+ while (offset < bytes.length) { const read = fs.readSync(fd, bytes, offset, bytes.length - offset, offset); if (read === 0) break; offset += read; }
168
+ const after = fs.fstatSync(fd);
169
+ if (offset !== opened.size || after.size !== opened.size || !sameIdentity(opened, after)) return { error: "file changed while being read" };
170
+ return { definition: JSON.parse(bytes.toString("utf8")) as Record<string, unknown> };
171
+ } catch (error) {
172
+ if ((error as NodeJS.ErrnoException).code === "ELOOP") return { error: "path must not reference a symbolic link" };
173
+ return { error: `path is not readable: ${(error as Error).message}` };
174
+ } finally { if (fd !== undefined) fs.closeSync(fd); }
175
+ }
176
+
177
+ function lstatSafePath(root: string, relativePath: string): { file?: string; stat?: fs.Stats; error?: string } {
178
+ const parts = relativePath.split(path.sep);
179
+ let current = root;
180
+ for (const [index, part] of parts.entries()) {
181
+ if (!part || part === "." || part === "..") return { error: "path must stay inside the kit directory" };
182
+ current = path.join(current, part);
183
+ const stat = fs.lstatSync(current);
184
+ if (stat.isSymbolicLink()) return { error: "path must not traverse a symbolic link" };
185
+ if (index < parts.length - 1 && !stat.isDirectory()) return { error: "path component must be a directory" };
186
+ if (index === parts.length - 1) return { file: current, stat };
187
+ }
188
+ return { error: "path must reference a regular file" };
189
+ }
190
+
191
+ function sameIdentity(left: fs.Stats, right: fs.Stats): boolean { return left.dev === right.dev && left.ino === right.ino; }
192
+ function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
@@ -1,6 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { readJson } from "../lib/fs.js";
4
+ import { parseKitFlowStepActions, workflowTriggerIdentifier } from "./action-metadata.js";
5
+ import { validateActionRepositoryMetadata } from "./action-repository-validation.js";
6
+ export { isObservableBuilderArtifactRef, isSafeBuilderArtifactRef, parseKitFlowStepActions } from "./action-metadata.js";
7
+ export type { KitFlowStepActionEntry, KitFlowStepArtifactBinding, KitFlowStepExpectationBinding } from "./action-metadata.js";
4
8
 
5
9
  // Extension-only asset classes: validated by Flow Agents. Flows are validated by @kontourai/flow.
6
10
  const EXTENSION_ASSET_CLASSES = ["skills", "docs", "adapters", "evals", "assets"] as const;
@@ -44,17 +48,6 @@ export interface KitHookInfluenceExpectationEntry {
44
48
  must_include_actions: string[];
45
49
  }
46
50
 
47
- export interface KitFlowStepActionEntry {
48
- flow_id: string;
49
- step_id: string;
50
- skills: string[];
51
- operations: string[];
52
- /** Gate expectations produced by this action's explicit operation(s). */
53
- expectation_ids?: string[];
54
- /** Durable artifacts produced by an operation-only action. */
55
- artifacts: string[];
56
- }
57
-
58
51
  export type KitSkillRole = "entrypoint" | "profile" | "step" | "shared-primitive" | "extension";
59
52
 
60
53
  export interface KitSkillRoleEntry {
@@ -66,74 +59,6 @@ export interface KitSkillRoleEntry {
66
59
  expectation_ids: string[];
67
60
  }
68
61
 
69
- const MAX_FLOW_DEFINITION_BYTES = 1024 * 1024;
70
-
71
- function sameFileIdentity(left: fs.Stats, right: fs.Stats): boolean {
72
- return left.dev === right.dev && left.ino === right.ino;
73
- }
74
-
75
- function lstatSafePath(root: string, relativePath: string): { file?: string; stat?: fs.Stats; error?: string } {
76
- const parts = relativePath.split(path.sep);
77
- let current = root;
78
- for (const [index, part] of parts.entries()) {
79
- if (!part || part === "." || part === "..") return { error: "path must stay inside the kit directory" };
80
- current = path.join(current, part);
81
- const stat = fs.lstatSync(current);
82
- if (stat.isSymbolicLink()) return { error: "path must not traverse a symbolic link" };
83
- if (index < parts.length - 1 && !stat.isDirectory()) return { error: "path component must be a directory" };
84
- if (index === parts.length - 1) return { file: current, stat };
85
- }
86
- return { error: "path must reference a regular file" };
87
- }
88
-
89
- function readSafeFlowDefinition(kitDir: string, relativePath: string): { definition?: Record<string, unknown>; error?: string } {
90
- const root = path.resolve(kitDir);
91
- if (path.isAbsolute(relativePath)) return { error: "path must be relative" };
92
- const lexicalFile = path.resolve(root, relativePath);
93
- if (lexicalFile === root || !lexicalFile.startsWith(`${root}${path.sep}`)) return { error: "path must stay inside the kit directory" };
94
- let rootDescriptor: number | undefined;
95
- let descriptor: number | undefined;
96
- try {
97
- const realRoot = fs.realpathSync.native(root);
98
- rootDescriptor = fs.openSync(realRoot, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY);
99
- const rootIdentity = fs.fstatSync(rootDescriptor);
100
- if (!rootIdentity.isDirectory() || !sameFileIdentity(rootIdentity, fs.lstatSync(realRoot))) return { error: "kit directory identity changed while opening flow definition" };
101
-
102
- const initialPath = lstatSafePath(realRoot, relativePath);
103
- if (!initialPath.file || !initialPath.stat) return { error: initialPath.error };
104
- if (!initialPath.stat.isFile()) return { error: "path must reference a regular file" };
105
- if (initialPath.stat.size > MAX_FLOW_DEFINITION_BYTES) return { error: `file exceeds ${MAX_FLOW_DEFINITION_BYTES} bytes` };
106
-
107
- // O_NOFOLLOW protects the terminal component. Identity checks before and after
108
- // opening make a race on any intermediate component fail closed on macOS/Node,
109
- // where openat-style traversal from a directory descriptor is not exposed.
110
- descriptor = fs.openSync(initialPath.file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
111
- const stat = fs.fstatSync(descriptor);
112
- if (!stat.isFile()) return { error: "path must reference a regular file" };
113
- if (!sameFileIdentity(initialPath.stat, stat)) return { error: "flow definition identity changed while opening" };
114
- if (stat.size > MAX_FLOW_DEFINITION_BYTES) return { error: `file exceeds ${MAX_FLOW_DEFINITION_BYTES} bytes` };
115
- const verifiedPath = lstatSafePath(realRoot, relativePath);
116
- if (!verifiedPath.stat || !sameFileIdentity(stat, verifiedPath.stat)) return { error: "flow definition identity changed while opening" };
117
-
118
- const buffer = Buffer.alloc(stat.size);
119
- let offset = 0;
120
- while (offset < buffer.length) {
121
- const read = fs.readSync(descriptor, buffer, offset, buffer.length - offset, offset);
122
- if (read === 0) break;
123
- offset += read;
124
- }
125
- const finalStat = fs.fstatSync(descriptor);
126
- if (offset !== stat.size || finalStat.size !== stat.size || !sameFileIdentity(stat, finalStat)) return { error: "file changed while being read" };
127
- return { definition: JSON.parse(buffer.toString("utf8")) as Record<string, unknown> };
128
- } catch (error) {
129
- if ((error as NodeJS.ErrnoException).code === "ELOOP") return { error: "path must not reference a symbolic link" };
130
- return { error: `path is not readable: ${(error as Error).message}` };
131
- } finally {
132
- if (descriptor !== undefined) fs.closeSync(descriptor);
133
- if (rootDescriptor !== undefined) fs.closeSync(rootDescriptor);
134
- }
135
- }
136
-
137
62
  /**
138
63
  * Parse and shape-validate a kit manifest's `dependencies` field (Flow Agents
139
64
  * extension-layer metadata; see docs/adr/0019-kit-dependency-ownership.md).
@@ -193,12 +118,6 @@ function nonEmptyStringList(value: unknown): value is string[] {
193
118
  return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.trim().length > 0);
194
119
  }
195
120
 
196
- const WORKFLOW_TRIGGER_IDENTIFIER_RE = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
197
-
198
- function workflowTriggerIdentifier(value: unknown): value is string {
199
- return typeof value === "string" && WORKFLOW_TRIGGER_IDENTIFIER_RE.test(value);
200
- }
201
-
202
121
  function optionalWorkflowTriggerIdentifier(value: unknown): value is string | undefined {
203
122
  return value === undefined || workflowTriggerIdentifier(value);
204
123
  }
@@ -347,76 +266,6 @@ export function parseKitHookInfluenceExpectations(manifest: Record<string, unkno
347
266
  return { entries, errors };
348
267
  }
349
268
 
350
- export function parseKitFlowStepActions(manifest: Record<string, unknown>, manifestPath: string): { entries: KitFlowStepActionEntry[]; errors: string[] } {
351
- const entries: KitFlowStepActionEntry[] = [];
352
- const errors: string[] = [];
353
- const raw = manifest.flow_step_actions;
354
- if (raw === undefined) return { entries, errors };
355
- if (!Array.isArray(raw)) {
356
- errors.push(`${manifestPath}: .flow_step_actions must be a list`);
357
- return { entries, errors };
358
- }
359
- const seen = new Set<string>();
360
- raw.forEach((entry, index) => {
361
- if (typeof entry !== "object" || entry === null) {
362
- errors.push(`${manifestPath}: flow_step_actions[${index}] must be an object`);
363
- return;
364
- }
365
- const record = entry as Record<string, unknown>;
366
- if (!workflowTriggerIdentifier(record.flow_id)) {
367
- errors.push(`${manifestPath}: flow_step_actions[${index}].flow_id must be a Flow identifier`);
368
- return;
369
- }
370
- if (!workflowTriggerIdentifier(record.step_id)) {
371
- errors.push(`${manifestPath}: flow_step_actions[${index}].step_id must be a step identifier`);
372
- return;
373
- }
374
- const skills = record.skills;
375
- const operations = record.operations ?? [];
376
- const expectationIds = record.expectation_ids;
377
- const artifacts = record.artifacts ?? [];
378
- if (!Array.isArray(skills) || !skills.every(workflowTriggerIdentifier)) {
379
- errors.push(`${manifestPath}: flow_step_actions[${index}].skills must be an identifier list`);
380
- return;
381
- }
382
- if (new Set(skills).size !== skills.length) {
383
- errors.push(`${manifestPath}: flow_step_actions[${index}].skills must not contain duplicates`);
384
- return;
385
- }
386
- if (!Array.isArray(operations) || !operations.every(workflowTriggerIdentifier)) {
387
- errors.push(`${manifestPath}: flow_step_actions[${index}].operations must be an identifier list when present`);
388
- return;
389
- }
390
- if (new Set(operations).size !== operations.length) {
391
- errors.push(`${manifestPath}: flow_step_actions[${index}].operations must not contain duplicates`);
392
- return;
393
- }
394
- if (expectationIds !== undefined && (!Array.isArray(expectationIds) || !expectationIds.every(workflowTriggerIdentifier) || new Set(expectationIds).size !== expectationIds.length)) {
395
- errors.push(`${manifestPath}: flow_step_actions[${index}].expectation_ids must be a unique identifier list when present`);
396
- return;
397
- }
398
- if (!Array.isArray(artifacts) || !artifacts.every((artifact) => typeof artifact === "string" && artifact.trim().length > 0) || new Set(artifacts).size !== artifacts.length) {
399
- errors.push(`${manifestPath}: flow_step_actions[${index}].artifacts must be a unique non-empty string list when present`);
400
- return;
401
- }
402
- const key = `${record.flow_id}/${record.step_id}`;
403
- if (seen.has(key)) {
404
- errors.push(`${manifestPath}: flow_step_actions[${index}] duplicates '${key}'`);
405
- return;
406
- }
407
- seen.add(key);
408
- entries.push({
409
- flow_id: record.flow_id,
410
- step_id: record.step_id,
411
- skills: skills as string[],
412
- operations: operations as string[],
413
- artifacts: artifacts as string[],
414
- ...(Array.isArray(expectationIds) ? { expectation_ids: expectationIds as string[] } : {}),
415
- });
416
- });
417
- return { entries, errors };
418
- }
419
-
420
269
  export function parseKitSkillRoles(manifest: Record<string, unknown>, manifestPath: string): { entries: KitSkillRoleEntry[]; errors: string[] } {
421
270
  const entries: KitSkillRoleEntry[] = [];
422
271
  const errors: string[] = [];
@@ -654,15 +503,15 @@ export async function validateKitRepository(kitDir: string): Promise<string[]> {
654
503
  return errors;
655
504
  }
656
505
 
657
- // Delegate core container validation (schema_version, id, name, flows including file
658
- // existence) to @kontourai/flow — the container contract lives once, in Flow.
659
- // This enforces the degradation invariant: a Flow Agents Kit must remain a valid
660
- // core Flow Kit container when extension fields are stripped.
661
- const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
662
- for (const err of coreErrors) errors.push(err);
506
+ errors.push(...await delegateCoreContainerValidation(kitDir, manifest));
507
+ errors.push(...validateExtensionAssets(kitDir, manifestPath, manifest));
508
+ errors.push(...validateAgentMetadata(kitDir, manifestPath, manifest));
509
+ if (manifest.first_party !== undefined && typeof manifest.first_party !== "boolean") errors.push(`${manifestPath}: .first_party must be a boolean when present`);
510
+ return errors;
511
+ }
663
512
 
664
- // Flow Agents extension validation: skills, docs, adapters, evals, assets.
665
- // Flows are validated above by @kontourai/flow; only extension classes are checked here.
513
+ function validateExtensionAssets(kitDir: string, manifestPath: string, manifest: Record<string, unknown>): string[] {
514
+ const errors: string[] = [];
666
515
  for (const section of EXTENSION_ASSET_CLASSES) {
667
516
  const entries = manifest[section];
668
517
  if (entries === undefined) continue;
@@ -703,9 +552,11 @@ export async function validateKitRepository(kitDir: string): Promise<string[]> {
703
552
  });
704
553
  }
705
554
 
706
- // Flow Agents metadata: cross-kit dependency declarations (extension-layer;
707
- // see docs/adr/0019-kit-dependency-ownership.md). Shape-only here — presence is
708
- // checked separately (non-blocking at install, hard error at activation).
555
+ return errors;
556
+ }
557
+
558
+ function validateAgentMetadata(kitDir: string, manifestPath: string, manifest: Record<string, unknown>): string[] {
559
+ const errors: string[] = [];
709
560
  const depResult = parseKitDependencies(manifest, manifestPath);
710
561
  for (const err of depResult.errors) errors.push(err);
711
562
  const workflowTriggerResult = parseKitWorkflowTriggers(manifest, manifestPath);
@@ -716,167 +567,9 @@ export async function validateKitRepository(kitDir: string): Promise<string[]> {
716
567
  for (const err of flowStepActionResult.errors) errors.push(err);
717
568
  const skillRoleResult = parseKitSkillRoles(manifest, manifestPath);
718
569
  for (const err of skillRoleResult.errors) errors.push(err);
719
- if (manifest.skill_roles !== undefined && skillRoleResult.errors.length === 0) {
720
- const declaredSkillIds = new Set(
721
- Array.isArray(manifest.skills)
722
- ? manifest.skills.flatMap((entry) => typeof entry === "object" && entry !== null && typeof (entry as Record<string, unknown>).id === "string" ? [(entry as Record<string, unknown>).id as string] : [])
723
- : []
724
- );
725
- const roleSkillIds = new Set(skillRoleResult.entries.map((entry) => entry.skill_id));
726
- for (const id of declaredSkillIds) if (!roleSkillIds.has(id)) errors.push(`${manifestPath}: skill_roles is missing declared skill '${id}'`);
727
- for (const id of roleSkillIds) if (!declaredSkillIds.has(id)) errors.push(`${manifestPath}: skill_roles references undeclared skill '${id}'`);
728
-
729
- type FlowExpectation = { id: string; exportKeys: Set<string> };
730
- type FlowMetadata = {
731
- steps: Set<string>;
732
- expectationsByStep: Map<string, Map<string, FlowExpectation>>;
733
- usesFlowByStep: Map<string, string>;
734
- exports: Set<string>;
735
- };
736
- type EffectiveFlowStep = { sourceFlowId: string; stepId: string; expectations: Map<string, FlowExpectation>; flowIds: Set<string> };
737
- const flows = new Map<string, FlowMetadata>();
738
- if (Array.isArray(manifest.flows)) {
739
- for (const entry of manifest.flows) {
740
- if (typeof entry !== "object" || entry === null) continue;
741
- const flow = entry as Record<string, unknown>;
742
- if (typeof flow.id !== "string" || typeof flow.path !== "string") continue;
743
- const safeDefinition = readSafeFlowDefinition(kitDir, flow.path);
744
- if (!safeDefinition.definition) {
745
- errors.push(`${manifestPath}: flows '${flow.id}' ${safeDefinition.error}`);
746
- continue;
747
- }
748
- try {
749
- const definition = safeDefinition.definition;
750
- const steps = new Set(Array.isArray(definition.steps) ? definition.steps.flatMap((step) => typeof step === "object" && step !== null && typeof (step as Record<string, unknown>).id === "string" ? [(step as Record<string, unknown>).id as string] : []) : []);
751
- const usesFlowByStep = new Map<string, string>();
752
- if (Array.isArray(definition.steps)) {
753
- for (const step of definition.steps) {
754
- if (typeof step !== "object" || step === null) continue;
755
- const stepRecord = step as Record<string, unknown>;
756
- if (typeof stepRecord.id === "string" && typeof stepRecord.uses_flow === "string") usesFlowByStep.set(stepRecord.id, stepRecord.uses_flow);
757
- }
758
- }
759
- const expectationsByStep = new Map<string, Map<string, FlowExpectation>>();
760
- if (typeof definition.gates === "object" && definition.gates !== null) {
761
- for (const gate of Object.values(definition.gates as Record<string, unknown>)) {
762
- if (typeof gate !== "object" || gate === null) continue;
763
- const gateRecord = gate as Record<string, unknown>;
764
- if (typeof gateRecord.step !== "string" || !Array.isArray(gateRecord.expects)) continue;
765
- const expectations = expectationsByStep.get(gateRecord.step) ?? new Map<string, FlowExpectation>();
766
- for (const expectation of gateRecord.expects) {
767
- if (typeof expectation !== "object" || expectation === null) continue;
768
- const expectationRecord = expectation as Record<string, unknown>;
769
- if (typeof expectationRecord.id !== "string") continue;
770
- const exportKeys = new Set([expectationRecord.id]);
771
- const claimType = (expectationRecord.bundle_claim as Record<string, unknown> | undefined)?.claimType;
772
- if (typeof claimType === "string") exportKeys.add(claimType);
773
- expectations.set(expectationRecord.id, { id: expectationRecord.id, exportKeys });
774
- }
775
- expectationsByStep.set(gateRecord.step, expectations);
776
- }
777
- }
778
- flows.set(flow.id, {
779
- steps,
780
- expectationsByStep,
781
- usesFlowByStep,
782
- exports: new Set(Array.isArray(definition.exports) ? definition.exports.filter((entry): entry is string => typeof entry === "string" && entry.length > 0) : []),
783
- });
784
- } catch {
785
- // Core Flow validation reports missing or malformed definitions.
786
- }
787
- }
788
- }
789
- const resolveEffectiveFlowStep = (flowId: string, stepId: string, seen = new Set<string>()): EffectiveFlowStep | undefined => {
790
- const cycleKey = `${flowId}\u0000${stepId}`;
791
- if (seen.has(cycleKey)) return undefined;
792
- seen.add(cycleKey);
793
- const flow = flows.get(flowId);
794
- if (!flow) return undefined;
795
- const direct = flow.expectationsByStep.get(stepId);
796
- if (direct) return { sourceFlowId: flowId, stepId, expectations: direct, flowIds: new Set([flowId]) };
797
- const childFlowId = flow.usesFlowByStep.get(stepId);
798
- if (!childFlowId) return { sourceFlowId: flowId, stepId, expectations: new Map(), flowIds: new Set([flowId]) };
799
- const child = resolveEffectiveFlowStep(childFlowId, stepId, seen);
800
- const childFlow = flows.get(childFlowId);
801
- if (!child || !childFlow || [...child.expectations.values()].some((expectation) => ![...expectation.exportKeys].some((key) => childFlow.exports.has(key)))) return undefined;
802
- child.flowIds.add(flowId);
803
- return child;
804
- };
805
-
806
- for (const row of skillRoleResult.entries) {
807
- if (!row.flow_id) continue;
808
- const flow = flows.get(row.flow_id);
809
- if (!flow) {
810
- errors.push(`${manifestPath}: skill_roles '${row.skill_id}' references unknown flow '${row.flow_id}'`);
811
- continue;
812
- }
813
- for (const stepId of row.step_ids) if (!flow.steps.has(stepId)) errors.push(`${manifestPath}: skill_roles '${row.skill_id}' references unknown step '${row.flow_id}/${stepId}'`);
814
- const allowedExpectations = new Set(row.step_ids.flatMap((stepId) => [...(resolveEffectiveFlowStep(row.flow_id!, stepId)?.expectations.keys() ?? [])]));
815
- for (const expectationId of row.expectation_ids) if (!allowedExpectations.has(expectationId)) errors.push(`${manifestPath}: skill_roles '${row.skill_id}' expectation '${expectationId}' is not owned by its bound step(s)`);
816
- }
817
- const roleByShortId = new Map(skillRoleResult.entries.map((entry) => [entry.skill_id.replace(`${String(manifest.id)}.`, ""), entry]));
818
- const producerOwners = new Map<string, string[]>();
819
- for (const row of skillRoleResult.entries) {
820
- if (row.role !== "step" || !row.flow_id) continue;
821
- for (const stepId of row.step_ids) {
822
- const effectiveStep = resolveEffectiveFlowStep(row.flow_id, stepId);
823
- if (!effectiveStep) continue;
824
- for (const expectationId of row.expectation_ids) {
825
- if (!effectiveStep.expectations.has(expectationId)) continue;
826
- const key = `${effectiveStep.sourceFlowId}\u0000${effectiveStep.stepId}\u0000${expectationId}`;
827
- producerOwners.set(key, [...(producerOwners.get(key) ?? []), `skill:${row.skill_id}`]);
828
- }
829
- }
830
- }
831
- for (const action of flowStepActionResult.entries) {
832
- const containingFlow = flows.get(action.flow_id);
833
- if (!containingFlow || !containingFlow.steps.has(action.step_id)) {
834
- errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' references an unknown flow step`);
835
- continue;
836
- }
837
- const effectiveStep = resolveEffectiveFlowStep(action.flow_id, action.step_id);
838
- if (!effectiveStep) {
839
- errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' cannot resolve its composed Flow step`);
840
- continue;
841
- }
842
- const expectedAtStep = effectiveStep.expectations;
843
- for (const expectationId of action.expectation_ids ?? []) {
844
- if (!expectedAtStep.has(expectationId)) errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' operation expectation '${expectationId}' is not owned by its resolved Flow step`);
845
- }
846
- if (action.skills.length === 0 && action.operations.length > 0 && expectedAtStep.size > 0 && !(action.expectation_ids?.length)) {
847
- errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' operation-only action must explicitly declare expectation_ids`);
848
- }
849
- const actionRows: KitSkillRoleEntry[] = [];
850
- for (const skill of action.skills) {
851
- const row = roleByShortId.get(skill);
852
- if (!row || row.role !== "step" || !row.flow_id || !effectiveStep.flowIds.has(row.flow_id) || !row.step_ids.includes(action.step_id)) {
853
- errors.push(`${manifestPath}: flow_step_actions '${action.flow_id}/${action.step_id}' skill '${skill}' must match one step-role binding`);
854
- } else {
855
- actionRows.push(row);
856
- }
857
- }
858
- for (const expectationId of expectedAtStep.keys()) {
859
- const key = `${effectiveStep.sourceFlowId}\u0000${effectiveStep.stepId}\u0000${expectationId}`;
860
- const owners = producerOwners.get(key) ?? [];
861
- if (action.operations.length > 0 && action.expectation_ids?.includes(expectationId)) owners.push(`operation:${action.flow_id}/${action.step_id}`);
862
- producerOwners.set(key, owners);
863
- }
864
- }
865
- for (const [flowId, flow] of flows) {
866
- for (const [stepId, expectations] of flow.expectationsByStep) {
867
- for (const expectationId of expectations.keys()) {
868
- const owners = producerOwners.get(`${flowId}\u0000${stepId}\u0000${expectationId}`) ?? [];
869
- if (owners.length !== 1) {
870
- errors.push(`${manifestPath}: flow expectation '${flowId}/${stepId}/${expectationId}' must have exactly one producer owner; found ${owners.length}`);
871
- }
872
- }
873
- }
874
- }
570
+ if ((manifest.skill_roles !== undefined || manifest.flow_step_actions !== undefined) && skillRoleResult.errors.length === 0) {
571
+ errors.push(...validateActionRepositoryMetadata({ kitDir, manifestPath, manifest, actions: flowStepActionResult.entries, skillRoles: skillRoleResult.entries }));
875
572
  }
876
- if (manifest.first_party !== undefined && typeof manifest.first_party !== "boolean") {
877
- errors.push(`${manifestPath}: .first_party must be a boolean when present`);
878
- }
879
-
880
573
  return errors;
881
574
  }
882
575
 
package/src/index.ts CHANGED
@@ -45,8 +45,24 @@ export {
45
45
  } from "./builder-flow-runtime.js";
46
46
  export type { BuilderFlowAgentLifecycleInput, BuilderFlowAuthorizedLifecycleInput, BuilderFlowSessionInput, BuilderFlowSessionResult } from "./builder-flow-runtime.js";
47
47
 
48
+ export {
49
+ deriveBuilderGateActionEnvelope,
50
+ gateActionProgressSnapshot,
51
+ withGateActionPriorProgress,
52
+ } from "./builder-gate-action-envelope.js";
53
+ export type {
54
+ BuilderGateActionEnvelopeInput,
55
+ GateActionEnvelope,
56
+ GateActionInterfaceParameter,
57
+ GateActionPriorProgress,
58
+ GateActionProgressSnapshot,
59
+ GateActionPublicMutation,
60
+ } from "./builder-gate-action-envelope.js";
61
+
48
62
  export {
49
63
  ContinuationAdapterTimeoutError,
64
+ MAX_CONTINUATION_ADAPTER_EVIDENCE_BYTES,
65
+ MAX_CONTINUATION_TURN_RESULT_BYTES,
50
66
  createFileContinuationStore,
51
67
  driveBuilderFlowSession,
52
68
  runContinuationDriver,
@@ -54,6 +70,7 @@ export {
54
70
  } from "./continuation-driver.js";
55
71
  export type {
56
72
  ContinuationBarrier,
73
+ ContinuationAcceptedTurn,
57
74
  ContinuationDriverEvent,
58
75
  ContinuationDriverLockLease,
59
76
  ContinuationDriverOutcome,