@guanlin4924/devflow-pi 0.1.105 → 0.1.107

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 (66) hide show
  1. package/README.md +10 -8
  2. package/package.json +1 -1
  3. package/plugins/devflow-core/extensions/devflow-core.ts +287 -394
  4. package/plugins/devflow-core/runtime/hooks/context_sync_gate.py +3 -1
  5. package/plugins/devflow-core/runtime/hooks/hook_common.py +6 -5
  6. package/plugins/devflow-core/runtime/hooks/index_gate.py +19 -26
  7. package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +2 -0
  8. package/plugins/devflow-core/skills/wireframe-render/SKILL.md +2 -0
  9. package/plugins/devflow-core/skills/wireframe-review/SKILL.md +2 -0
  10. package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +2 -0
  11. package/plugins/devflow-core/skills/workflow-dynamic/SKILL.md +41 -0
  12. package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +37 -50
  13. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +46 -191
  14. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/artifact_specs.rb +12 -0
  15. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dynamic_plan.rb +504 -0
  16. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +38 -1
  17. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_policy.rb +11 -0
  18. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_result.rb +12 -0
  19. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +4 -9
  20. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +19 -86
  21. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +24 -13
  22. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/repair_control_plane.rb +4 -4
  23. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +7 -1
  24. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +17 -39
  25. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +39 -3
  26. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +2 -3
  27. package/plugins/devflow-core/skills/workflow-orchestrator/templates/regression-acceptance.yaml +0 -1
  28. package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +13 -5
  29. package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +1 -1
  30. package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +2 -0
  31. package/plugins/devflow-design/skills/workflow-api-call-guide/SKILL.md +2 -0
  32. package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +3 -1
  33. package/plugins/devflow-design/skills/workflow-api-supplement/SKILL.md +2 -0
  34. package/plugins/devflow-design/skills/workflow-architecture-feasibility/SKILL.md +2 -0
  35. package/plugins/devflow-design/skills/workflow-backend-tech-spec/SKILL.md +6 -4
  36. package/plugins/devflow-design/skills/workflow-design-rule-pack/SKILL.md +2 -0
  37. package/plugins/devflow-design/skills/workflow-feature-breakdown/SKILL.md +2 -0
  38. package/plugins/devflow-design/skills/workflow-frontend-tech-spec/SKILL.md +6 -4
  39. package/plugins/devflow-design/skills/workflow-integration-verify/SKILL.md +3 -1
  40. package/plugins/devflow-design/skills/workflow-ui-spec/SKILL.md +4 -2
  41. package/plugins/devflow-implementation/agents/implementation-worker.md +2 -8
  42. package/plugins/devflow-implementation/skills/workflow-bug-fix/SKILL.md +5 -0
  43. package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +3 -1
  44. package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +6 -1
  45. package/plugins/devflow-intel/skills/code-index-query/SKILL.md +2 -0
  46. package/plugins/devflow-intel/skills/workflow-code-index/SKILL.md +2 -0
  47. package/plugins/devflow-intel/skills/workflow-context-sync/SKILL.md +2 -0
  48. package/plugins/devflow-intel/skills/workflow-update-navigation/SKILL.md +3 -1
  49. package/plugins/devflow-quality/agents/code-reviewer.md +2 -0
  50. package/plugins/devflow-quality/skills/code-review/SKILL.md +7 -2
  51. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +3 -1
  52. package/plugins/devflow-quality/skills/workflow-gen-test-guide/SKILL.md +2 -0
  53. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +2 -0
  54. package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +2 -0
  55. package/plugins/devflow-quality/skills/workflow-regression-test-design/SKILL.md +2 -0
  56. package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +7 -5
  57. package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +11 -6
  58. package/plugins/devflow-requirements/skills/requirement-feasibility-review/SKILL.md +2 -0
  59. package/plugins/devflow-requirements/skills/workflow-demand-clarify/SKILL.md +2 -0
  60. package/plugins/devflow-requirements/skills/workflow-domain-modeling/SKILL.md +2 -0
  61. package/plugins/devflow-requirements/skills/workflow-requirement-baseline/SKILL.md +6 -2
  62. package/plugins/devflow-requirements/skills/workflow-requirement-triage/SKILL.md +5 -0
  63. package/plugins/devflow-requirements/skills/workflow-wayfinder/SKILL.md +2 -0
  64. package/src/constants.ts +1 -1
  65. package/src/dynamic-workflow.ts +31 -0
  66. package/templates/omp/AGENTS.md +8 -3
@@ -9,10 +9,18 @@ require "open3"
9
9
  require "optparse"
10
10
  require "yaml"
11
11
  require_relative "control_io"
12
+ require_relative "artifact_specs"
13
+ require_relative "gate_policy"
12
14
  require_relative "validate_state"
13
15
  require_relative "phase_ownership"
14
16
  require_relative "requirement_graph"
15
17
  require_relative "gate_runtime"
18
+ require_relative "dynamic_plan"
19
+
20
+ if DynamicPlan.read_state(Dir.pwd)&.dig("current_flow") == "dynamic"
21
+ puts JSON.generate({ "ok" => false, "reason" => "dynamic_plan_requires_evidence", "message" => "Use devflow_plan; legacy complete-step cannot complete dynamic tasks." })
22
+ exit 1
23
+ end
16
24
 
17
25
  options = {
18
26
  state: nil,
@@ -84,9 +92,7 @@ def gate_contract_enabled?(workflow)
84
92
  workflow.dig("governance", "gate_contract_version").to_i >= 3
85
93
  end
86
94
 
87
- def core_hard_gate_mode(workflow)
88
- workflow.dig("governance", "hard_gate_mode").to_s.empty? ? "chain" : workflow.dig("governance", "hard_gate_mode").to_s
89
- end
95
+
90
96
 
91
97
  # Build the runtime view from the existing workflow declaration. This keeps
92
98
  # governance.step_checkers as the single checker namespace and avoids creating
@@ -125,7 +131,6 @@ def transition_gate_contract(workflow, step)
125
131
  "required_inputs" => Array(step["inputs"]).select { |input| input.is_a?(Hash) && input["required_at_completion"] == true }
126
132
  },
127
133
  "quality" => { "checker_ids" => Array(workflow.dig("governance", "step_checkers", step["id"])) },
128
- "hard_gate_mode" => core_hard_gate_mode(workflow),
129
134
  "concurrency" => { "project_lock" => "required", "optimistic_revision" => "required", "registry_view" => "candidate_overlay" },
130
135
  "recovery" => { "allowed" => ["sync_artifacts", "repair_outputs"], "max_attempts" => 2 }
131
136
  }
@@ -419,19 +424,7 @@ def track_allowed_steps(track)
419
424
  end
420
425
 
421
426
  def next_pending_step_id(state, pending_ids, step_by_id, completed_ids)
422
- track = active_track(state)
423
- unless track
424
- return pending_ids.find do |step_id|
425
- step_by_id[step_id] && dependencies_satisfied?(step_by_id[step_id], completed_ids)
426
- end
427
- end
428
-
429
- allowed = track_allowed_steps(track)
430
- pending_ids.find do |step_id|
431
- allowed.include?(step_id) &&
432
- step_by_id[step_id] &&
433
- dependencies_satisfied?(step_by_id[step_id], completed_ids)
434
- end
427
+ pending_ids.find { |id| step_by_id[id] && dependencies_satisfied?(step_by_id[id], completed_ids) } || pending_ids.find { |id| step_by_id[id] }
435
428
  end
436
429
 
437
430
  def conditional_skip_policies(workflow, checkpoint_id)
@@ -535,7 +528,11 @@ def join_parallel_window(root, state, workflow, current_revision)
535
528
  raise ArgumentError, "parallel run not found" unless record
536
529
  branches = Array(record["branches"])
537
530
  incomplete = branches.reject { |branch| branch["status"].to_s == "succeeded" }
538
- raise ArgumentError, "parallel join requires every branch to succeed" unless branches.any? && incomplete.empty?
531
+ raise ArgumentError, "parallel run has no branches" if branches.empty?
532
+ if incomplete.any?
533
+ $devflow_advisories << { "reason" => "parallel_branches_not_succeeded", "branches" => incomplete.map { |branch| branch.slice("branch_id", "status") } }
534
+ record["completion_with_warnings"] = true
535
+ end
539
536
 
540
537
  branch_steps = branches.map { |branch| branch.fetch("workflow_step").to_s }
541
538
  state["completed_steps"] = (Array(state["completed_steps"]) + branch_steps).uniq
@@ -597,45 +594,19 @@ end
597
594
  # a small safety floor for malformed control-plane state and unsafe writes, but
598
595
  # Core-generated test/traceability/merge findings no longer strand the serial
599
596
  # workflow before the AI review can decide what to do.
600
- SOFT_GATE_MODE = ENV.fetch("DEVFLOW_SOFT_GATES", "1") != "0"
601
- HARD_FAILURE_REASONS = %w[
602
- missing_state_file
603
- missing_state_front_matter
604
- stale_revision
605
- invalid_control_path
606
- unknown_step
607
- gate_target_step_unknown
608
- gate_current_step_mismatch
609
- parallel_transition_requires_write
610
- feature_not_active
611
- cancel_reason_required
612
- session_rollover_in_progress
613
- failure_branch_incomplete
614
- parallel_machine_evidence_failed
615
- parallel_join_failed
616
- parallel_branch_transition_failed
617
- conditional_step_skip_failed
618
- cross_track_consistency_failed
619
- business_cross_model_check_failed
620
- gate_boundary_rejected
621
- acceptance_results_not_passed
622
- execution_surface_mismatch
623
- approval_invalid
624
- human_result_unapproved
625
- dsl_not_approved
626
- environment_profile_not_approved
627
- runtime_integrity_error
628
- acceptance_test_suite_freeze_failed
629
- acceptance_test_suite_freeze_attestation_invalid
630
- acceptance_test_suite_freeze_attestation_failed
631
- acceptance_test_suite_candidate_binding_failed
632
- acceptance_test_suite_freeze_recovery_failed
597
+ OPERATION_FAILURE_REASONS = %w[
598
+ missing_state_file missing_state_front_matter stale_revision invalid_control_path
599
+ unknown_step gate_target_step_unknown gate_current_step_mismatch
600
+ parallel_transition_requires_write feature_not_active cancel_reason_required
601
+ session_rollover_in_progress parallel_branch_not_found gate_boundary_rejected
602
+ parallel_join_failed parallel_branch_transition_failed state_validation_failed
603
+ phase_ownership_claim_failed phase_ownership_completion_failed not_waiting_checkpoint no_pending_step
633
604
  ].freeze
634
605
  $devflow_advisories = []
635
606
 
636
607
  def fail_result(reason, details = {})
637
608
  failure = { "ok" => false, "reason" => reason, "details" => details }
638
- if SOFT_GATE_MODE && !HARD_FAILURE_REASONS.include?(reason.to_s)
609
+ if !OPERATION_FAILURE_REASONS.include?(reason.to_s)
639
610
  $devflow_advisories << failure
640
611
  return nil
641
612
  end
@@ -660,8 +631,8 @@ rescue JSON::ParserError => error
660
631
  { "ok" => false, "reason" => "test_task_binding_#{stage}_check_failed", "errors" => ["checker returned invalid JSON: #{error.message}"] }
661
632
  end
662
633
 
663
- # Acceptance results are a hard gate even in the default soft mode: Core itself
664
- # re-validates the frozen-suite results instead of trusting a checker summary.
634
+ # Always re-check acceptance results. Soft mode retains failures as advisories
635
+ # without changing the canonical test verdict.
665
636
  ACCEPTANCE_RESULT_GATED_STEPS = %w[run-acceptance-tests release-closeout].freeze
666
637
 
667
638
  def acceptance_results_check(root, feature)
@@ -956,7 +927,9 @@ result =
956
927
  !%w[prepared claimed].include?(state.dig("rollover", "status")) &&
957
928
  (options[:complete_step] || options[:confirm_checkpoint] || options[:enter_failure_branch] || options[:exit_failure_branch] || options[:parallel_action])
958
929
  topology_errors = WorkflowGateContract.validate(workflow)
959
- unless topology_errors.empty?
930
+ if !topology_errors.empty?
931
+ $devflow_advisories.concat(topology_errors)
932
+ elsif !topology_errors.empty?
960
933
  result = fail_result("gate_boundary_rejected", "gate" => {
961
934
  "verdict" => "deny",
962
935
  "reason" => "workflow_gate_contract_invalid",
@@ -1014,7 +987,8 @@ result =
1014
987
  transition_kind: transition_kind,
1015
988
  checkpoint_authorized: options[:runtime_confirmed]
1016
989
  )
1017
- if gate_result && gate_result["verdict"] != "allow"
990
+ $devflow_advisories.concat(Array(gate_result && gate_result["warnings"]))
991
+ if gate_result && !gate_result["ok"]
1018
992
  result = fail_result("gate_boundary_rejected", "gate" => gate_result)
1019
993
  end
1020
994
  end
@@ -1129,24 +1103,22 @@ result =
1129
1103
  state.delete("checkpoint_decision")
1130
1104
 
1131
1105
  if step["checkpoint"]
1132
- state["current_step"] = step_id
1133
- state["status"] = "waiting_checkpoint"
1134
- state["checkpoint"] = step["checkpoint"]
1135
- state["checkpoint_label"] = checkpoint_label(workflow, step["checkpoint"])
1136
- action_description = "Step completed: #{step["label"]} (#{step_id}). Now waiting at #{step["checkpoint"]}."
1137
- elsif (parallel_window = parallel_window_for(workflow, step_id))
1106
+ $devflow_advisories << { "reason" => "checkpoint_advisory", "checkpoint" => step["checkpoint"], "approval_recorded" => false }
1107
+ state["checkpoint_decision"] = "continued_with_warning"
1108
+ begin
1109
+ apply_conditional_step_skips!(root, state, workflow, step["checkpoint"], current_revision + 1)
1110
+ pending = Array(state["pending_steps"])
1111
+ completed = Array(state["completed_steps"])
1112
+ rescue JSON::ParserError, KeyError, ArgumentError => error
1113
+ $devflow_advisories << { "reason" => "conditional_step_skip_failed", "message" => error.message }
1114
+ end
1115
+ end
1116
+ if (parallel_window = parallel_window_for(workflow, step_id))
1138
1117
  state["current_step"] = parallel_window.fetch("join_step")
1139
1118
  state["status"] = "in_progress"
1140
1119
  state.delete("checkpoint")
1141
1120
  state.delete("checkpoint_label")
1142
1121
  action_description = "Step completed: #{step["label"]} (#{step_id}). Entered parallel window #{parallel_window["id"]}; awaiting Core prepare."
1143
- elsif track_boundary?(state, pending)
1144
- state["current_step"] = step_id
1145
- state["status"] = "blocked"
1146
- state["block_reason"] = "track_scope_complete"
1147
- state.delete("checkpoint")
1148
- state.delete("checkpoint_label")
1149
- action_description = "Step completed: #{step["label"]} (#{step_id}). Active track output scope is complete."
1150
1122
  elsif (next_id = next_pending_step_id(state, pending, step_by_id, completed))
1151
1123
  state["current_step"] = next_id
1152
1124
  state["status"] = "in_progress"
@@ -1178,125 +1150,6 @@ result =
1178
1150
  elsif Array(state["pending_steps"]).empty?
1179
1151
  result = fail_result("no_pending_step")
1180
1152
  else
1181
- # CP-API turns the shared candidate contract into confirmed graph
1182
- # fact inside the same user checkpoint. Skills cannot self-confirm
1183
- # API nodes and downstream generated TC imports can require a
1184
- # reviewed contract revision.
1185
- if state["checkpoint"] == "CP-API" && steps.any? { |step| step["id"] == "api-contract" }
1186
- feature = state.fetch("current_feature")
1187
- graph, = RequirementGraph.load_graph(root, feature)
1188
- schema_check = api_test_schema_check(root, feature, graph)
1189
- if !schema_check["ok"]
1190
- result = fail_result("api_test_schema_incomplete", "api_test_schema" => schema_check)
1191
- else
1192
- api_confirmation = RequirementGraph.confirm_api_contracts!(root, graph, "CP-API: state revision #{current_revision} approved")
1193
- if api_confirmation["ok"]
1194
- actions << "api_contract_confirmed" unless Array(api_confirmation["confirmed_api_ids"]).empty?
1195
- else
1196
- result = fail_result("api_contract_confirmation_failed", "api_contract" => api_confirmation)
1197
- end
1198
- end
1199
- end
1200
-
1201
- # CP-3 authorizes exactly one immutable TC freeze. It is deliberately
1202
- # inside the checkpoint transition so the user does not have to
1203
- # approve an extra model-issued dialog and a Skill cannot freeze its
1204
- # own candidates early.
1205
- if result.nil? && state["checkpoint"] == "CP-3" && steps.any? { |step| %w[acceptance-test-design regression-test-design].include?(step["id"]) }
1206
- feature = state.fetch("current_feature")
1207
- graph, = RequirementGraph.load_graph(root, feature)
1208
- regression = regression_workflow?(state, steps)
1209
- attestation_path, = test_suite_freeze_attestation_path(root, feature)
1210
-
1211
- if File.exist?(attestation_path)
1212
- frozen_check = if regression
1213
- regression_freeze_attestation_check(graph, feature, attestation_path)
1214
- else
1215
- test_task_binding_check(root, feature, "frozen")
1216
- end
1217
- unless frozen_check["ok"]
1218
- result = fail_result("acceptance_test_suite_freeze_attestation_invalid", "test_suite" => frozen_check)
1219
- else
1220
- actions << "acceptance_test_suite_freeze_attestation_verified"
1221
- end
1222
- elsif cp3_freeze_provenance?(graph, current_revision)
1223
- # A process can die after graph/view writes but before the
1224
- # attestation is published. Recover only if each TC proves that
1225
- # this exact CP-3 already froze it; never use this path to bless
1226
- # a generic confirmed suite.
1227
- recovery_check = if regression
1228
- { "ok" => true, "regression" => true, "candidate_test_suite" => graph.dig("views", "tests") }
1229
- else
1230
- test_task_binding_check(root, feature, "recovery")
1231
- end
1232
- unless recovery_check["ok"]
1233
- result = fail_result("acceptance_test_suite_freeze_recovery_failed", "test_suite" => recovery_check)
1234
- else
1235
- recovered = {
1236
- "ok" => true,
1237
- "tests_view" => graph.dig("views", "tests"),
1238
- "frozen_case_ids" => Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "test_case" }.map { |node| node["id"] }
1239
- }
1240
- begin
1241
- write_test_suite_freeze_attestation(root, feature, current_revision, recovery_check, recovered)
1242
- frozen_check = if regression
1243
- regression_freeze_attestation_check(graph, feature, attestation_path)
1244
- else
1245
- test_task_binding_check(root, feature, "frozen")
1246
- end
1247
- if frozen_check["ok"]
1248
- actions << "acceptance_test_suite_freeze_attestation_recovered"
1249
- else
1250
- result = fail_result("acceptance_test_suite_freeze_attestation_invalid", "test_suite" => frozen_check)
1251
- end
1252
- rescue DevflowControlIO::InvalidControlPath, SystemCallError => error
1253
- result = fail_result("acceptance_test_suite_freeze_attestation_failed", "message" => error.message)
1254
- end
1255
- end
1256
- else
1257
- matrix_check = api_test_matrix_check(root, feature, graph)
1258
- # A new freeze requires every TC to declare a consistent
1259
- # execution_surface; this is a hard failure even in soft mode.
1260
- surface_check = matrix_check["ok"] ? execution_surface_check(root, feature) : nil
1261
- unless matrix_check["ok"]
1262
- result = fail_result("api_test_matrix_incomplete", "api_test_matrix" => matrix_check)
1263
- else
1264
- result = fail_result("execution_surface_mismatch", "execution_surface" => surface_check) unless surface_check["ok"]
1265
- end
1266
- if matrix_check["ok"] && surface_check["ok"]
1267
- candidate_check = if regression
1268
- { "ok" => true, "regression" => true, "candidate_test_suite" => graph.dig("views", "tests") }
1269
- else
1270
- test_task_binding_check(root, feature, "candidate")
1271
- end
1272
- unless candidate_check["ok"]
1273
- result = fail_result("acceptance_test_suite_candidate_binding_failed", "test_suite" => candidate_check)
1274
- else
1275
- frozen = RequirementGraph.freeze_test_cases!(
1276
- root,
1277
- graph,
1278
- "CP-3: state revision #{current_revision} approved",
1279
- confirm_prerequisites: options[:confirm_test_prerequisites] && options[:runtime_confirmed],
1280
- regression: regression,
1281
- persist: false
1282
- )
1283
- unless frozen["ok"]
1284
- result = fail_result("acceptance_test_suite_freeze_failed", "test_suite" => frozen)
1285
- else
1286
- actions << "acceptance_test_prerequisites_confirmed" unless Array(frozen["confirmed_prerequisite_ids"]).empty?
1287
- test_suite_freeze_to_write = {
1288
- "feature" => feature,
1289
- "state_revision" => current_revision,
1290
- "graph" => graph,
1291
- "candidate_check" => candidate_check,
1292
- "frozen" => frozen
1293
- }
1294
- actions << "acceptance_test_suite_frozen"
1295
- end
1296
- end
1297
- end
1298
- end
1299
- end
1300
1153
  if result.nil?
1301
1154
  begin
1302
1155
  applied_skips = apply_conditional_step_skips!(root, state, workflow, state["checkpoint"], current_revision + 1)
@@ -1313,7 +1166,7 @@ result =
1313
1166
  completed = Array(state["completed_steps"])
1314
1167
  next_id = next_pending_step_id(state, pending, step_by_id, completed)
1315
1168
  if next_id.nil? && track_boundary?(state, pending)
1316
- state["checkpoint_decision"] = "approved"
1169
+ state["checkpoint_decision"] = "continued_with_warning"
1317
1170
  state["status"] = "blocked"
1318
1171
  state["block_reason"] = "track_scope_complete"
1319
1172
  state.delete("checkpoint")
@@ -1323,7 +1176,7 @@ result =
1323
1176
  action_description = "Checkpoint confirmed. Active track output scope is complete."
1324
1177
  next_intent_requested = true
1325
1178
  else
1326
- state["checkpoint_decision"] = "approved"
1179
+ state["checkpoint_decision"] = "continued_with_warning"
1327
1180
  state["current_step"] = next_id || pending.first
1328
1181
  state["status"] = "in_progress"
1329
1182
  state.delete("checkpoint")
@@ -1408,7 +1261,9 @@ result =
1408
1261
  phase_owner_override: prospective_owner
1409
1262
  )
1410
1263
 
1411
- if changed && !validation["ok"] && !SOFT_GATE_MODE
1264
+ $devflow_advisories.concat(Array(validation["warnings"]))
1265
+ state["gate_advisories"] = JSON.parse(JSON.generate(Array(state["gate_advisories"]) + $devflow_advisories)) if changed
1266
+ if changed && !validation["ok"]
1412
1267
  result = fail_result("state_validation_failed", "validation" => validation)
1413
1268
  elsif changed && options[:write]
1414
1269
  ownership = PhaseOwnership.claim(
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArtifactSpecs
4
+ module_function
5
+
6
+ def for_step(step)
7
+ outputs = Array(step["outputs"]).select { |entry| entry.is_a?(Hash) }.map(&:dup)
8
+ meta = step["meta_output"]
9
+ outputs << { "type" => "json" }.merge(meta).merge("required" => true) if meta.is_a?(Hash) && meta["path"]
10
+ outputs.uniq { |entry| entry["path"] }
11
+ end
12
+ end