@kontourai/flow-agents 3.12.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 (46) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/build/src/builder-gate-action-envelope.d.ts +2 -1
  3. package/build/src/builder-gate-action-envelope.js +4 -3
  4. package/build/src/continuation-validation.js +31 -1
  5. package/dist/base/build/package.json +1 -1
  6. package/dist/base/build/src/builder-gate-action-envelope.d.ts +2 -1
  7. package/dist/base/build/src/builder-gate-action-envelope.js +4 -3
  8. package/dist/base/build/src/continuation-validation.js +31 -1
  9. package/dist/base/docs/migrations.md +5 -0
  10. package/dist/base/install.sh +1 -1
  11. package/dist/claude-code/build/package.json +1 -1
  12. package/dist/claude-code/build/src/builder-gate-action-envelope.d.ts +2 -1
  13. package/dist/claude-code/build/src/builder-gate-action-envelope.js +4 -3
  14. package/dist/claude-code/build/src/continuation-validation.js +31 -1
  15. package/dist/claude-code/docs/migrations.md +5 -0
  16. package/dist/claude-code/install.sh +1 -1
  17. package/dist/codex/build/package.json +1 -1
  18. package/dist/codex/build/src/builder-gate-action-envelope.d.ts +2 -1
  19. package/dist/codex/build/src/builder-gate-action-envelope.js +4 -3
  20. package/dist/codex/build/src/continuation-validation.js +31 -1
  21. package/dist/codex/docs/migrations.md +5 -0
  22. package/dist/codex/install.sh +1 -1
  23. package/dist/kiro/build/package.json +1 -1
  24. package/dist/kiro/build/src/builder-gate-action-envelope.d.ts +2 -1
  25. package/dist/kiro/build/src/builder-gate-action-envelope.js +4 -3
  26. package/dist/kiro/build/src/continuation-validation.js +31 -1
  27. package/dist/kiro/docs/migrations.md +5 -0
  28. package/dist/kiro/install.sh +1 -1
  29. package/dist/opencode/build/package.json +1 -1
  30. package/dist/opencode/build/src/builder-gate-action-envelope.d.ts +2 -1
  31. package/dist/opencode/build/src/builder-gate-action-envelope.js +4 -3
  32. package/dist/opencode/build/src/continuation-validation.js +31 -1
  33. package/dist/opencode/docs/migrations.md +5 -0
  34. package/dist/opencode/install.sh +1 -1
  35. package/dist/pi/build/package.json +1 -1
  36. package/dist/pi/build/src/builder-gate-action-envelope.d.ts +2 -1
  37. package/dist/pi/build/src/builder-gate-action-envelope.js +4 -3
  38. package/dist/pi/build/src/continuation-validation.js +31 -1
  39. package/dist/pi/docs/migrations.md +5 -0
  40. package/dist/pi/install.sh +1 -1
  41. package/docs/migrations.md +5 -0
  42. package/package.json +1 -1
  43. package/src/builder-gate-action-envelope.ts +6 -4
  44. package/src/cli/builder-flow-runtime.test.mjs +4 -2
  45. package/src/cli/continuation-driver.test.mjs +16 -1
  46. package/src/continuation-validation.ts +31 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.12.1](https://github.com/kontourai/flow-agents/compare/v3.12.0...v3.12.1) (2026-07-13)
4
+
5
+
6
+ ### Fixes
7
+
8
+ * bind gate-action requirements to gates ([#608](https://github.com/kontourai/flow-agents/issues/608)) ([b1e9ad1](https://github.com/kontourai/flow-agents/commit/b1e9ad1a03ba1b88e09d6ace156e33c010bc6848))
9
+
3
10
  ## [3.12.0](https://github.com/kontourai/flow-agents/compare/v3.11.0...v3.12.0) (2026-07-13)
4
11
 
5
12
 
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
35
35
  };
36
36
  };
37
37
  export type GateActionEnvelope = {
38
- schema_version: "1.0";
38
+ schema_version: "2.0";
39
39
  flow: {
40
40
  run_id: string;
41
41
  definition_id: string;
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
47
47
  gate: {
48
48
  requirements: Array<{
49
49
  id: string;
50
+ gate_id: string;
50
51
  required: boolean;
51
52
  description: string;
52
53
  claim_type: string;
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
70
70
  const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
71
71
  .flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
72
72
  return expectationsForGate(gate, input.run.config)
73
- .map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
73
+ .map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
74
74
  });
75
75
  if (requirements.length > MAX_REQUIREMENTS)
76
76
  throw new Error("Builder gate-action envelope requirements exceed the supported bound");
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
91
91
  .filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
92
92
  .map((binding) => binding.artifact);
93
93
  const envelope = {
94
- schema_version: "1.0",
94
+ schema_version: "2.0",
95
95
  flow: {
96
96
  run_id: input.run.runId,
97
97
  definition_id: input.run.definitionId,
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
215
215
  ],
216
216
  };
217
217
  }
218
- function requirementFromExpectation(expectation, satisfied, acceptedException) {
218
+ function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
219
219
  const record = expectation;
220
220
  const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
221
221
  if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
223
223
  }
224
224
  return {
225
225
  id: record.id,
226
+ gate_id: gateId,
226
227
  required: record.required === true,
227
228
  description: record.description,
228
229
  claim_type: claim.claimType,
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
18
18
  return structuredClone(value);
19
19
  }
20
20
  function validateGateActionEnvelope(value) {
21
- if (!value || typeof value !== "object" || value.schema_version !== "1.0" || !value.flow || typeof value.flow.current_step !== "string"
21
+ if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
22
22
  || !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
23
23
  throw new Error("continuation snapshot gate-action envelope is malformed");
24
+ if (value.gate !== undefined)
25
+ validateGateRequirementBindings(value);
24
26
  if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
25
27
  throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
26
28
  }
29
+ function validateGateRequirementBindings(value) {
30
+ if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
31
+ throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
32
+ }
33
+ const activeGates = new Set(value.flow.gate_ids);
34
+ const acceptedGates = new Set();
35
+ for (const exception of value.gate.accepted_exceptions) {
36
+ if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
37
+ || !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
38
+ throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
39
+ }
40
+ acceptedGates.add(exception.gate_id);
41
+ }
42
+ const gatesWithRequirements = new Set();
43
+ for (const requirement of value.gate.requirements) {
44
+ if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
45
+ throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
46
+ }
47
+ gatesWithRequirements.add(requirement.gate_id);
48
+ if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
49
+ && requirement.status !== "satisfied") {
50
+ throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
51
+ }
52
+ }
53
+ if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
54
+ throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
55
+ }
56
+ }
27
57
  export function validateTurnResult(value) {
28
58
  if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
29
59
  throw new Error("continuation adapter must return status completed or wait");
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "type": "module"
5
5
  }
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
35
35
  };
36
36
  };
37
37
  export type GateActionEnvelope = {
38
- schema_version: "1.0";
38
+ schema_version: "2.0";
39
39
  flow: {
40
40
  run_id: string;
41
41
  definition_id: string;
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
47
47
  gate: {
48
48
  requirements: Array<{
49
49
  id: string;
50
+ gate_id: string;
50
51
  required: boolean;
51
52
  description: string;
52
53
  claim_type: string;
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
70
70
  const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
71
71
  .flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
72
72
  return expectationsForGate(gate, input.run.config)
73
- .map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
73
+ .map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
74
74
  });
75
75
  if (requirements.length > MAX_REQUIREMENTS)
76
76
  throw new Error("Builder gate-action envelope requirements exceed the supported bound");
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
91
91
  .filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
92
92
  .map((binding) => binding.artifact);
93
93
  const envelope = {
94
- schema_version: "1.0",
94
+ schema_version: "2.0",
95
95
  flow: {
96
96
  run_id: input.run.runId,
97
97
  definition_id: input.run.definitionId,
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
215
215
  ],
216
216
  };
217
217
  }
218
- function requirementFromExpectation(expectation, satisfied, acceptedException) {
218
+ function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
219
219
  const record = expectation;
220
220
  const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
221
221
  if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
223
223
  }
224
224
  return {
225
225
  id: record.id,
226
+ gate_id: gateId,
226
227
  required: record.required === true,
227
228
  description: record.description,
228
229
  claim_type: claim.claimType,
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
18
18
  return structuredClone(value);
19
19
  }
20
20
  function validateGateActionEnvelope(value) {
21
- if (!value || typeof value !== "object" || value.schema_version !== "1.0" || !value.flow || typeof value.flow.current_step !== "string"
21
+ if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
22
22
  || !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
23
23
  throw new Error("continuation snapshot gate-action envelope is malformed");
24
+ if (value.gate !== undefined)
25
+ validateGateRequirementBindings(value);
24
26
  if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
25
27
  throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
26
28
  }
29
+ function validateGateRequirementBindings(value) {
30
+ if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
31
+ throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
32
+ }
33
+ const activeGates = new Set(value.flow.gate_ids);
34
+ const acceptedGates = new Set();
35
+ for (const exception of value.gate.accepted_exceptions) {
36
+ if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
37
+ || !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
38
+ throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
39
+ }
40
+ acceptedGates.add(exception.gate_id);
41
+ }
42
+ const gatesWithRequirements = new Set();
43
+ for (const requirement of value.gate.requirements) {
44
+ if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
45
+ throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
46
+ }
47
+ gatesWithRequirements.add(requirement.gate_id);
48
+ if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
49
+ && requirement.status !== "satisfied") {
50
+ throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
51
+ }
52
+ }
53
+ if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
54
+ throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
55
+ }
56
+ }
27
57
  export function validateTurnResult(value) {
28
58
  if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
29
59
  throw new Error("continuation adapter must return status completed or wait");
@@ -6,6 +6,11 @@ title: Migrations
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ - Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
10
+ `gate.requirements` includes its originating `gate_id`, allowing consumers to
11
+ verify accepted exceptions without private Flow-definition knowledge. There
12
+ is no legacy envelope fallback; adapters that validate or project the public
13
+ envelope must require version 2.0 and the new binding.
9
14
  - Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
10
15
  of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
11
16
  Move any local session directories, sidecars, or `current.json` pointers you
@@ -72,7 +72,7 @@ if [[ ! -e "$DEST/AGENTS.md" && ! -L "$DEST/AGENTS.md" ]]; then
72
72
  rm -f "$instruction_tmp"
73
73
  fi
74
74
  if command -v node >/dev/null 2>&1; then
75
- node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.0" --install-record "$DEST/.flow-agents/install.json" --runtime "base" || true
75
+ node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "base" || true
76
76
  fi
77
77
  if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
78
78
  bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "type": "module"
5
5
  }
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
35
35
  };
36
36
  };
37
37
  export type GateActionEnvelope = {
38
- schema_version: "1.0";
38
+ schema_version: "2.0";
39
39
  flow: {
40
40
  run_id: string;
41
41
  definition_id: string;
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
47
47
  gate: {
48
48
  requirements: Array<{
49
49
  id: string;
50
+ gate_id: string;
50
51
  required: boolean;
51
52
  description: string;
52
53
  claim_type: string;
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
70
70
  const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
71
71
  .flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
72
72
  return expectationsForGate(gate, input.run.config)
73
- .map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
73
+ .map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
74
74
  });
75
75
  if (requirements.length > MAX_REQUIREMENTS)
76
76
  throw new Error("Builder gate-action envelope requirements exceed the supported bound");
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
91
91
  .filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
92
92
  .map((binding) => binding.artifact);
93
93
  const envelope = {
94
- schema_version: "1.0",
94
+ schema_version: "2.0",
95
95
  flow: {
96
96
  run_id: input.run.runId,
97
97
  definition_id: input.run.definitionId,
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
215
215
  ],
216
216
  };
217
217
  }
218
- function requirementFromExpectation(expectation, satisfied, acceptedException) {
218
+ function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
219
219
  const record = expectation;
220
220
  const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
221
221
  if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
223
223
  }
224
224
  return {
225
225
  id: record.id,
226
+ gate_id: gateId,
226
227
  required: record.required === true,
227
228
  description: record.description,
228
229
  claim_type: claim.claimType,
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
18
18
  return structuredClone(value);
19
19
  }
20
20
  function validateGateActionEnvelope(value) {
21
- if (!value || typeof value !== "object" || value.schema_version !== "1.0" || !value.flow || typeof value.flow.current_step !== "string"
21
+ if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
22
22
  || !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
23
23
  throw new Error("continuation snapshot gate-action envelope is malformed");
24
+ if (value.gate !== undefined)
25
+ validateGateRequirementBindings(value);
24
26
  if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
25
27
  throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
26
28
  }
29
+ function validateGateRequirementBindings(value) {
30
+ if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
31
+ throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
32
+ }
33
+ const activeGates = new Set(value.flow.gate_ids);
34
+ const acceptedGates = new Set();
35
+ for (const exception of value.gate.accepted_exceptions) {
36
+ if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
37
+ || !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
38
+ throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
39
+ }
40
+ acceptedGates.add(exception.gate_id);
41
+ }
42
+ const gatesWithRequirements = new Set();
43
+ for (const requirement of value.gate.requirements) {
44
+ if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
45
+ throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
46
+ }
47
+ gatesWithRequirements.add(requirement.gate_id);
48
+ if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
49
+ && requirement.status !== "satisfied") {
50
+ throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
51
+ }
52
+ }
53
+ if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
54
+ throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
55
+ }
56
+ }
27
57
  export function validateTurnResult(value) {
28
58
  if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
29
59
  throw new Error("continuation adapter must return status completed or wait");
@@ -6,6 +6,11 @@ title: Migrations
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ - Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
10
+ `gate.requirements` includes its originating `gate_id`, allowing consumers to
11
+ verify accepted exceptions without private Flow-definition knowledge. There
12
+ is no legacy envelope fallback; adapters that validate or project the public
13
+ envelope must require version 2.0 and the new binding.
9
14
  - Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
10
15
  of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
11
16
  Move any local session directories, sidecars, or `current.json` pointers you
@@ -72,7 +72,7 @@ if [[ ! -e "$DEST/CLAUDE.md" && ! -L "$DEST/CLAUDE.md" ]]; then
72
72
  rm -f "$instruction_tmp"
73
73
  fi
74
74
  if command -v node >/dev/null 2>&1; then
75
- node "$DEST/scripts/install-merge.js" --config "$DEST/.claude/settings.json" --managed-hooks "$SRC/.claude/settings.json" --version "3.12.0" --install-record "$DEST/.flow-agents/install.json" --runtime "claude-code" || true
75
+ node "$DEST/scripts/install-merge.js" --config "$DEST/.claude/settings.json" --managed-hooks "$SRC/.claude/settings.json" --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "claude-code" || true
76
76
  fi
77
77
  if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
78
78
  bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "type": "module"
5
5
  }
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
35
35
  };
36
36
  };
37
37
  export type GateActionEnvelope = {
38
- schema_version: "1.0";
38
+ schema_version: "2.0";
39
39
  flow: {
40
40
  run_id: string;
41
41
  definition_id: string;
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
47
47
  gate: {
48
48
  requirements: Array<{
49
49
  id: string;
50
+ gate_id: string;
50
51
  required: boolean;
51
52
  description: string;
52
53
  claim_type: string;
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
70
70
  const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
71
71
  .flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
72
72
  return expectationsForGate(gate, input.run.config)
73
- .map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
73
+ .map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
74
74
  });
75
75
  if (requirements.length > MAX_REQUIREMENTS)
76
76
  throw new Error("Builder gate-action envelope requirements exceed the supported bound");
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
91
91
  .filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
92
92
  .map((binding) => binding.artifact);
93
93
  const envelope = {
94
- schema_version: "1.0",
94
+ schema_version: "2.0",
95
95
  flow: {
96
96
  run_id: input.run.runId,
97
97
  definition_id: input.run.definitionId,
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
215
215
  ],
216
216
  };
217
217
  }
218
- function requirementFromExpectation(expectation, satisfied, acceptedException) {
218
+ function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
219
219
  const record = expectation;
220
220
  const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
221
221
  if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
223
223
  }
224
224
  return {
225
225
  id: record.id,
226
+ gate_id: gateId,
226
227
  required: record.required === true,
227
228
  description: record.description,
228
229
  claim_type: claim.claimType,
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
18
18
  return structuredClone(value);
19
19
  }
20
20
  function validateGateActionEnvelope(value) {
21
- if (!value || typeof value !== "object" || value.schema_version !== "1.0" || !value.flow || typeof value.flow.current_step !== "string"
21
+ if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
22
22
  || !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
23
23
  throw new Error("continuation snapshot gate-action envelope is malformed");
24
+ if (value.gate !== undefined)
25
+ validateGateRequirementBindings(value);
24
26
  if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
25
27
  throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
26
28
  }
29
+ function validateGateRequirementBindings(value) {
30
+ if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
31
+ throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
32
+ }
33
+ const activeGates = new Set(value.flow.gate_ids);
34
+ const acceptedGates = new Set();
35
+ for (const exception of value.gate.accepted_exceptions) {
36
+ if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
37
+ || !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
38
+ throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
39
+ }
40
+ acceptedGates.add(exception.gate_id);
41
+ }
42
+ const gatesWithRequirements = new Set();
43
+ for (const requirement of value.gate.requirements) {
44
+ if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
45
+ throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
46
+ }
47
+ gatesWithRequirements.add(requirement.gate_id);
48
+ if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
49
+ && requirement.status !== "satisfied") {
50
+ throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
51
+ }
52
+ }
53
+ if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
54
+ throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
55
+ }
56
+ }
27
57
  export function validateTurnResult(value) {
28
58
  if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
29
59
  throw new Error("continuation adapter must return status completed or wait");
@@ -6,6 +6,11 @@ title: Migrations
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ - Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
10
+ `gate.requirements` includes its originating `gate_id`, allowing consumers to
11
+ verify accepted exceptions without private Flow-definition knowledge. There
12
+ is no legacy envelope fallback; adapters that validate or project the public
13
+ envelope must require version 2.0 and the new binding.
9
14
  - Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
10
15
  of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
11
16
  Move any local session directories, sidecars, or `current.json` pointers you
@@ -60,7 +60,7 @@ SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
60
60
  mkdir -p "$DEST"
61
61
  rsync -a --exclude='/AGENTS.md' --exclude='/CLAUDE.md' --exclude=".codex/hooks.json" "$SRC"/ "$DEST"/
62
62
  if command -v node >/dev/null 2>&1; then
63
- node "$DEST/scripts/install-merge.js" --config "$DEST/.codex/hooks.json" --managed-hooks "$SRC/.codex/hooks.json" --version "3.12.0" --install-record "$DEST/.flow-agents/install.json" --runtime "codex" || true
63
+ node "$DEST/scripts/install-merge.js" --config "$DEST/.codex/hooks.json" --managed-hooks "$SRC/.codex/hooks.json" --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "codex" || true
64
64
  fi
65
65
  if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
66
66
  bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "type": "module"
5
5
  }
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
35
35
  };
36
36
  };
37
37
  export type GateActionEnvelope = {
38
- schema_version: "1.0";
38
+ schema_version: "2.0";
39
39
  flow: {
40
40
  run_id: string;
41
41
  definition_id: string;
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
47
47
  gate: {
48
48
  requirements: Array<{
49
49
  id: string;
50
+ gate_id: string;
50
51
  required: boolean;
51
52
  description: string;
52
53
  claim_type: string;
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
70
70
  const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
71
71
  .flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
72
72
  return expectationsForGate(gate, input.run.config)
73
- .map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
73
+ .map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
74
74
  });
75
75
  if (requirements.length > MAX_REQUIREMENTS)
76
76
  throw new Error("Builder gate-action envelope requirements exceed the supported bound");
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
91
91
  .filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
92
92
  .map((binding) => binding.artifact);
93
93
  const envelope = {
94
- schema_version: "1.0",
94
+ schema_version: "2.0",
95
95
  flow: {
96
96
  run_id: input.run.runId,
97
97
  definition_id: input.run.definitionId,
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
215
215
  ],
216
216
  };
217
217
  }
218
- function requirementFromExpectation(expectation, satisfied, acceptedException) {
218
+ function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
219
219
  const record = expectation;
220
220
  const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
221
221
  if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
223
223
  }
224
224
  return {
225
225
  id: record.id,
226
+ gate_id: gateId,
226
227
  required: record.required === true,
227
228
  description: record.description,
228
229
  claim_type: claim.claimType,
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
18
18
  return structuredClone(value);
19
19
  }
20
20
  function validateGateActionEnvelope(value) {
21
- if (!value || typeof value !== "object" || value.schema_version !== "1.0" || !value.flow || typeof value.flow.current_step !== "string"
21
+ if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
22
22
  || !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
23
23
  throw new Error("continuation snapshot gate-action envelope is malformed");
24
+ if (value.gate !== undefined)
25
+ validateGateRequirementBindings(value);
24
26
  if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
25
27
  throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
26
28
  }
29
+ function validateGateRequirementBindings(value) {
30
+ if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
31
+ throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
32
+ }
33
+ const activeGates = new Set(value.flow.gate_ids);
34
+ const acceptedGates = new Set();
35
+ for (const exception of value.gate.accepted_exceptions) {
36
+ if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
37
+ || !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
38
+ throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
39
+ }
40
+ acceptedGates.add(exception.gate_id);
41
+ }
42
+ const gatesWithRequirements = new Set();
43
+ for (const requirement of value.gate.requirements) {
44
+ if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
45
+ throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
46
+ }
47
+ gatesWithRequirements.add(requirement.gate_id);
48
+ if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
49
+ && requirement.status !== "satisfied") {
50
+ throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
51
+ }
52
+ }
53
+ if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
54
+ throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
55
+ }
56
+ }
27
57
  export function validateTurnResult(value) {
28
58
  if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
29
59
  throw new Error("continuation adapter must return status completed or wait");
@@ -6,6 +6,11 @@ title: Migrations
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ - Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
10
+ `gate.requirements` includes its originating `gate_id`, allowing consumers to
11
+ verify accepted exceptions without private Flow-definition knowledge. There
12
+ is no legacy envelope fallback; adapters that validate or project the public
13
+ envelope must require version 2.0 and the new binding.
9
14
  - Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
10
15
  of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
11
16
  Move any local session directories, sidecars, or `current.json` pointers you
@@ -73,7 +73,7 @@ fi
73
73
  export DEST
74
74
  find "$DEST" \( -path "$DEST/AGENTS.md" -o -path "$DEST/CLAUDE.md" \) -prune -o -type f \( -name '*.json' -o -name '*.md' -o -name '*.sh' -o -name '*.js' -o -name '*.ts' -o -name '*.yaml' -o -name '*.yml' \) -print0 | xargs -0 perl -0pi -e 's#__KIRO_PACKAGE_ROOT__#$ENV{DEST}#g'
75
75
  if command -v node >/dev/null 2>&1; then
76
- node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.0" --install-record "$DEST/.flow-agents/install.json" --runtime "kiro" || true
76
+ node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "kiro" || true
77
77
  fi
78
78
  if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
79
79
  bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "type": "module"
5
5
  }
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
35
35
  };
36
36
  };
37
37
  export type GateActionEnvelope = {
38
- schema_version: "1.0";
38
+ schema_version: "2.0";
39
39
  flow: {
40
40
  run_id: string;
41
41
  definition_id: string;
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
47
47
  gate: {
48
48
  requirements: Array<{
49
49
  id: string;
50
+ gate_id: string;
50
51
  required: boolean;
51
52
  description: string;
52
53
  claim_type: string;
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
70
70
  const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
71
71
  .flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
72
72
  return expectationsForGate(gate, input.run.config)
73
- .map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
73
+ .map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
74
74
  });
75
75
  if (requirements.length > MAX_REQUIREMENTS)
76
76
  throw new Error("Builder gate-action envelope requirements exceed the supported bound");
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
91
91
  .filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
92
92
  .map((binding) => binding.artifact);
93
93
  const envelope = {
94
- schema_version: "1.0",
94
+ schema_version: "2.0",
95
95
  flow: {
96
96
  run_id: input.run.runId,
97
97
  definition_id: input.run.definitionId,
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
215
215
  ],
216
216
  };
217
217
  }
218
- function requirementFromExpectation(expectation, satisfied, acceptedException) {
218
+ function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
219
219
  const record = expectation;
220
220
  const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
221
221
  if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
223
223
  }
224
224
  return {
225
225
  id: record.id,
226
+ gate_id: gateId,
226
227
  required: record.required === true,
227
228
  description: record.description,
228
229
  claim_type: claim.claimType,
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
18
18
  return structuredClone(value);
19
19
  }
20
20
  function validateGateActionEnvelope(value) {
21
- if (!value || typeof value !== "object" || value.schema_version !== "1.0" || !value.flow || typeof value.flow.current_step !== "string"
21
+ if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
22
22
  || !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
23
23
  throw new Error("continuation snapshot gate-action envelope is malformed");
24
+ if (value.gate !== undefined)
25
+ validateGateRequirementBindings(value);
24
26
  if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
25
27
  throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
26
28
  }
29
+ function validateGateRequirementBindings(value) {
30
+ if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
31
+ throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
32
+ }
33
+ const activeGates = new Set(value.flow.gate_ids);
34
+ const acceptedGates = new Set();
35
+ for (const exception of value.gate.accepted_exceptions) {
36
+ if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
37
+ || !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
38
+ throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
39
+ }
40
+ acceptedGates.add(exception.gate_id);
41
+ }
42
+ const gatesWithRequirements = new Set();
43
+ for (const requirement of value.gate.requirements) {
44
+ if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
45
+ throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
46
+ }
47
+ gatesWithRequirements.add(requirement.gate_id);
48
+ if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
49
+ && requirement.status !== "satisfied") {
50
+ throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
51
+ }
52
+ }
53
+ if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
54
+ throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
55
+ }
56
+ }
27
57
  export function validateTurnResult(value) {
28
58
  if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
29
59
  throw new Error("continuation adapter must return status completed or wait");
@@ -6,6 +6,11 @@ title: Migrations
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ - Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
10
+ `gate.requirements` includes its originating `gate_id`, allowing consumers to
11
+ verify accepted exceptions without private Flow-definition knowledge. There
12
+ is no legacy envelope fallback; adapters that validate or project the public
13
+ envelope must require version 2.0 and the new binding.
9
14
  - Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
10
15
  of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
11
16
  Move any local session directories, sidecars, or `current.json` pointers you
@@ -72,7 +72,7 @@ if [[ ! -e "$DEST/AGENTS.md" && ! -L "$DEST/AGENTS.md" ]]; then
72
72
  rm -f "$instruction_tmp"
73
73
  fi
74
74
  if command -v node >/dev/null 2>&1; then
75
- node "$DEST/scripts/install-merge.js" --config "$DEST/opencode.json" --managed-hooks "$SRC/opencode.json" --version "3.12.0" --install-record "$DEST/.flow-agents/install.json" --runtime "opencode" || true
75
+ node "$DEST/scripts/install-merge.js" --config "$DEST/opencode.json" --managed-hooks "$SRC/opencode.json" --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "opencode" || true
76
76
  fi
77
77
  if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
78
78
  bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "type": "module"
5
5
  }
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
35
35
  };
36
36
  };
37
37
  export type GateActionEnvelope = {
38
- schema_version: "1.0";
38
+ schema_version: "2.0";
39
39
  flow: {
40
40
  run_id: string;
41
41
  definition_id: string;
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
47
47
  gate: {
48
48
  requirements: Array<{
49
49
  id: string;
50
+ gate_id: string;
50
51
  required: boolean;
51
52
  description: string;
52
53
  claim_type: string;
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
70
70
  const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
71
71
  .flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
72
72
  return expectationsForGate(gate, input.run.config)
73
- .map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
73
+ .map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
74
74
  });
75
75
  if (requirements.length > MAX_REQUIREMENTS)
76
76
  throw new Error("Builder gate-action envelope requirements exceed the supported bound");
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
91
91
  .filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
92
92
  .map((binding) => binding.artifact);
93
93
  const envelope = {
94
- schema_version: "1.0",
94
+ schema_version: "2.0",
95
95
  flow: {
96
96
  run_id: input.run.runId,
97
97
  definition_id: input.run.definitionId,
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
215
215
  ],
216
216
  };
217
217
  }
218
- function requirementFromExpectation(expectation, satisfied, acceptedException) {
218
+ function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
219
219
  const record = expectation;
220
220
  const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
221
221
  if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
223
223
  }
224
224
  return {
225
225
  id: record.id,
226
+ gate_id: gateId,
226
227
  required: record.required === true,
227
228
  description: record.description,
228
229
  claim_type: claim.claimType,
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
18
18
  return structuredClone(value);
19
19
  }
20
20
  function validateGateActionEnvelope(value) {
21
- if (!value || typeof value !== "object" || value.schema_version !== "1.0" || !value.flow || typeof value.flow.current_step !== "string"
21
+ if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
22
22
  || !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
23
23
  throw new Error("continuation snapshot gate-action envelope is malformed");
24
+ if (value.gate !== undefined)
25
+ validateGateRequirementBindings(value);
24
26
  if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
25
27
  throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
26
28
  }
29
+ function validateGateRequirementBindings(value) {
30
+ if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
31
+ throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
32
+ }
33
+ const activeGates = new Set(value.flow.gate_ids);
34
+ const acceptedGates = new Set();
35
+ for (const exception of value.gate.accepted_exceptions) {
36
+ if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
37
+ || !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
38
+ throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
39
+ }
40
+ acceptedGates.add(exception.gate_id);
41
+ }
42
+ const gatesWithRequirements = new Set();
43
+ for (const requirement of value.gate.requirements) {
44
+ if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
45
+ throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
46
+ }
47
+ gatesWithRequirements.add(requirement.gate_id);
48
+ if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
49
+ && requirement.status !== "satisfied") {
50
+ throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
51
+ }
52
+ }
53
+ if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
54
+ throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
55
+ }
56
+ }
27
57
  export function validateTurnResult(value) {
28
58
  if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
29
59
  throw new Error("continuation adapter must return status completed or wait");
@@ -6,6 +6,11 @@ title: Migrations
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ - Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
10
+ `gate.requirements` includes its originating `gate_id`, allowing consumers to
11
+ verify accepted exceptions without private Flow-definition knowledge. There
12
+ is no legacy envelope fallback; adapters that validate or project the public
13
+ envelope must require version 2.0 and the new binding.
9
14
  - Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
10
15
  of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
11
16
  Move any local session directories, sidecars, or `current.json` pointers you
@@ -72,7 +72,7 @@ if [[ ! -e "$DEST/AGENTS.md" && ! -L "$DEST/AGENTS.md" ]]; then
72
72
  rm -f "$instruction_tmp"
73
73
  fi
74
74
  if command -v node >/dev/null 2>&1; then
75
- node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.0" --install-record "$DEST/.flow-agents/install.json" --runtime "pi" || true
75
+ node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "pi" || true
76
76
  fi
77
77
  if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
78
78
  bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
@@ -6,6 +6,11 @@ title: Migrations
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ - Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
10
+ `gate.requirements` includes its originating `gate_id`, allowing consumers to
11
+ verify accepted exceptions without private Flow-definition knowledge. There
12
+ is no legacy envelope fallback; adapters that validate or project the public
13
+ envelope must require version 2.0 and the new binding.
9
14
  - Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
10
15
  of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
11
16
  Move any local session directories, sidecars, or `current.json` pointers you
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
5
5
  "keywords": [
6
6
  "agents",
@@ -53,7 +53,7 @@ export type GateActionPublicMutation =
53
53
  };
54
54
 
55
55
  export type GateActionEnvelope = {
56
- schema_version: "1.0";
56
+ schema_version: "2.0";
57
57
  flow: {
58
58
  run_id: string;
59
59
  definition_id: string;
@@ -65,6 +65,7 @@ export type GateActionEnvelope = {
65
65
  gate: {
66
66
  requirements: Array<{
67
67
  id: string;
68
+ gate_id: string;
68
69
  required: boolean;
69
70
  description: string;
70
71
  claim_type: string;
@@ -222,7 +223,7 @@ function deriveFlowRequirements(input: BuilderGateActionEnvelopeInput, action: K
222
223
  const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
223
224
  .flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
224
225
  return (expectationsForGate(gate, input.run.config) as FlowExpectation[])
225
- .map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
226
+ .map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
226
227
  });
227
228
  if (requirements.length > MAX_REQUIREMENTS) throw new Error("Builder gate-action envelope requirements exceed the supported bound");
228
229
  if (!sameSet(action.expectation_ids, requirements.map((requirement) => requirement.id))) {
@@ -243,7 +244,7 @@ function assembleGateActionEnvelope(input: BuilderGateActionEnvelopeInput, loade
243
244
  .filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
244
245
  .map((binding) => binding.artifact);
245
246
  const envelope: GateActionEnvelope = {
246
- schema_version: "1.0",
247
+ schema_version: "2.0",
247
248
  flow: {
248
249
  run_id: input.run.runId,
249
250
  definition_id: input.run.definitionId,
@@ -379,7 +380,7 @@ function workflowEvidenceMutation(expectationId: string, sessionArgument: string
379
380
  };
380
381
  }
381
382
 
382
- function requirementFromExpectation(expectation: FlowExpectation, satisfied: Set<string>, acceptedException: boolean): GateActionEnvelope["gate"]["requirements"][number] {
383
+ function requirementFromExpectation(gateId: string, expectation: FlowExpectation, satisfied: Set<string>, acceptedException: boolean): GateActionEnvelope["gate"]["requirements"][number] {
383
384
  const record = expectation as unknown as AnyRecord;
384
385
  const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
385
386
  if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
@@ -387,6 +388,7 @@ function requirementFromExpectation(expectation: FlowExpectation, satisfied: Set
387
388
  }
388
389
  return {
389
390
  id: record.id,
391
+ gate_id: gateId,
390
392
  required: record.required === true,
391
393
  description: record.description,
392
394
  claim_type: claim.claimType,
@@ -372,7 +372,8 @@ test("small-model client can start and advance from projected actions without ch
372
372
  assert.ok(started.projection.next_action.command.includes(`'workflow' 'status' '--session-dir' '.kontourai/flow-agents/${session.slug}' '--json'`));
373
373
  const envelope = started.gateActionEnvelope;
374
374
  assert.equal(Object.hasOwn(started.projection.next_action, "gate_action_envelope"), false, "durable state has no duplicate envelope");
375
- assert.equal(envelope.schema_version, "1.0");
375
+ assert.equal(envelope.schema_version, "2.0");
376
+ assert.equal(envelope.gate.requirements[0].gate_id, "pull-work-gate");
376
377
  assert.equal(envelope.action.implementation_allowed, false, "implementation is forbidden before builder.build/execute");
377
378
  assert.deepEqual(envelope.action.declared_evidence, ["selected-work"]);
378
379
  assert.deepEqual(envelope.action.declared_artifacts, [`<slug>--pull-work.md`, "trust.bundle#selected-work"]);
@@ -481,6 +482,7 @@ test("accepted Flow exceptions explicitly waive the current envelope requirement
481
482
  });
482
483
  const recovered = await recoverBuilderFlowSession({ sessionDir: session.sessionDir });
483
484
  assert.deepEqual(recovered.gateActionEnvelope.gate.accepted_exceptions, [{ gate_id: "pull-work-gate", exception_id: exception.id }]);
485
+ assert.equal(recovered.gateActionEnvelope.gate.requirements[0].gate_id, "pull-work-gate");
484
486
  assert.equal(recovered.gateActionEnvelope.gate.requirements[0].status, "accepted_exception");
485
487
  assert.deepEqual(recovered.gateActionEnvelope.stop_condition.required.unresolved_evidence_ids, []);
486
488
  assert.deepEqual(recovered.gateActionEnvelope.stop_condition.required.artifact_refs, []);
@@ -761,7 +763,7 @@ test("public workflow drive signs adapter evidence with a consumed one-time key"
761
763
  assert.deepEqual(payload.adapter_turns.map((turn) => turn.request), observedRequests);
762
764
  assert.equal(payload.adapter_turns[0].request.schema_version, "1.0");
763
765
  assert.equal(payload.adapter_turns[0].request.next_action.status, "continue");
764
- assert.equal(payload.adapter_turns[0].request.gate_action_envelope.schema_version, "1.0");
766
+ assert.equal(payload.adapter_turns[0].request.gate_action_envelope.schema_version, "2.0");
765
767
  assert.deepEqual(payload.adapter_turns.map((turn) => turn.request), observedRequests, "the signed payload binds the unchanged envelope bytes observed by the adapter");
766
768
  assert.deepEqual(payload.adapter_turns[0].result.evidence.usage, { input_tokens: 10, output_tokens: 2 });
767
769
  const tampered = Buffer.from(JSON.stringify({ ...payload, max_turns: 2 }));
@@ -9,6 +9,7 @@ import { createHash, generateKeyPairSync, randomBytes, sign } from "node:crypto"
9
9
 
10
10
  import { MAX_CONTINUATION_ADAPTER_EVIDENCE_BYTES, MAX_CONTINUATION_TURN_RESULT_BYTES, ContinuationAdapterTimeoutError, createFileContinuationStore, runContinuationDriver, withContinuationDriverLock } from "../../build/src/continuation-driver.js";
11
11
  import { executeContinuationAdapter, executeLoadedContinuationAdapter, loadContinuationAdapterCommand, waitForContinuationBarrier } from "../../build/src/cli/continuation-adapter.js";
12
+ import { validateSnapshot } from "../../build/src/continuation-validation.js";
12
13
 
13
14
  const require = createRequire(import.meta.url);
14
15
  const activeTurnAuthority = require("../../scripts/hooks/lib/continuation-turn-authority.js");
@@ -38,7 +39,7 @@ function snapshot(step, status = "active") {
38
39
  function envelopeSnapshot(step, { evidence = [], artifacts = [], implementationAllowed = false, status = "active" } = {}) {
39
40
  const value = snapshot(step, status);
40
41
  value.gate_action_envelope = {
41
- schema_version: "1.0",
42
+ schema_version: "2.0",
42
43
  flow: { current_step: step, status },
43
44
  action: { implementation_allowed: implementationAllowed },
44
45
  stop_condition: { kind: "one_turn", adapter_evidence_is_gate_evidence: false },
@@ -47,6 +48,20 @@ function envelopeSnapshot(step, { evidence = [], artifacts = [], implementationA
47
48
  return value;
48
49
  }
49
50
 
51
+ test("gate-action envelope rejects partial accepted-exception bindings", () => {
52
+ const partial = envelopeSnapshot("execute");
53
+ partial.gate_action_envelope.flow.gate_ids = ["execute-gate"];
54
+ partial.gate_action_envelope.gate = {
55
+ requirements: [
56
+ { id: "implemented", gate_id: "execute-gate", required: true, status: "accepted_exception" },
57
+ { id: "verified", gate_id: "execute-gate", required: true, status: "unresolved" },
58
+ ],
59
+ unresolved_requirement_ids: ["verified"],
60
+ accepted_exceptions: [{ gate_id: "execute-gate", exception_id: "exception-1" }],
61
+ };
62
+ assert.throws(() => validateSnapshot(partial), /inconsistent exception bindings/);
63
+ });
64
+
50
65
  function terminalProgressSnapshot(status, { step = "learn", evidence = [], artifacts = [] } = {}) {
51
66
  const value = snapshot(step, status);
52
67
  value.progress_snapshot = {
@@ -16,11 +16,41 @@ export function validateSnapshot(value: ContinuationSnapshot): ContinuationSnaps
16
16
  }
17
17
 
18
18
  function validateGateActionEnvelope(value: GateActionEnvelope): void {
19
- if (!value || typeof value !== "object" || value.schema_version !== "1.0" || !value.flow || typeof value.flow.current_step !== "string"
19
+ if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
20
20
  || !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts)) throw new Error("continuation snapshot gate-action envelope is malformed");
21
+ if (value.gate !== undefined) validateGateRequirementBindings(value);
21
22
  if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536) throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
22
23
  }
23
24
 
25
+ function validateGateRequirementBindings(value: GateActionEnvelope): void {
26
+ if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
27
+ throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
28
+ }
29
+ const activeGates = new Set(value.flow.gate_ids);
30
+ const acceptedGates = new Set<string>();
31
+ for (const exception of value.gate.accepted_exceptions) {
32
+ if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
33
+ || !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
34
+ throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
35
+ }
36
+ acceptedGates.add(exception.gate_id);
37
+ }
38
+ const gatesWithRequirements = new Set<string>();
39
+ for (const requirement of value.gate.requirements) {
40
+ if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
41
+ throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
42
+ }
43
+ gatesWithRequirements.add(requirement.gate_id);
44
+ if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
45
+ && requirement.status !== "satisfied") {
46
+ throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
47
+ }
48
+ }
49
+ if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
50
+ throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
51
+ }
52
+ }
53
+
24
54
  export function validateTurnResult(value: ContinuationTurnResult): ContinuationTurnResult {
25
55
  if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait")) throw new Error("continuation adapter must return status completed or wait");
26
56
  if (value.status === "wait") validateBarrier(value.barrier);