@guanlin4924/devflow-pi 0.1.104 → 0.1.106
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.
- package/README.md +10 -8
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +105 -305
- package/plugins/devflow-core/runtime/hooks/context_sync_gate.py +3 -1
- package/plugins/devflow-core/runtime/hooks/hook_common.py +6 -5
- package/plugins/devflow-core/runtime/hooks/index_gate.py +19 -26
- package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +2 -0
- package/plugins/devflow-core/skills/wireframe-render/SKILL.md +2 -0
- package/plugins/devflow-core/skills/wireframe-review/SKILL.md +2 -0
- package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +2 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +34 -50
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +40 -191
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/artifact_specs.rb +12 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +38 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_policy.rb +11 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_result.rb +12 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +4 -9
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +22 -87
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +24 -13
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/repair_control_plane.rb +4 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +3 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +8 -39
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +39 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +2 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/regression-acceptance.yaml +0 -1
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +10 -5
- package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +1 -1
- package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +2 -0
- package/plugins/devflow-design/skills/workflow-api-call-guide/SKILL.md +2 -0
- package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +3 -1
- package/plugins/devflow-design/skills/workflow-api-supplement/SKILL.md +2 -0
- package/plugins/devflow-design/skills/workflow-architecture-feasibility/SKILL.md +2 -0
- package/plugins/devflow-design/skills/workflow-backend-tech-spec/SKILL.md +6 -4
- package/plugins/devflow-design/skills/workflow-design-rule-pack/SKILL.md +2 -0
- package/plugins/devflow-design/skills/workflow-feature-breakdown/SKILL.md +2 -0
- package/plugins/devflow-design/skills/workflow-frontend-tech-spec/SKILL.md +6 -4
- package/plugins/devflow-design/skills/workflow-integration-verify/SKILL.md +3 -1
- package/plugins/devflow-design/skills/workflow-ui-spec/SKILL.md +4 -2
- package/plugins/devflow-implementation/agents/implementation-worker.md +2 -8
- package/plugins/devflow-implementation/skills/workflow-bug-fix/SKILL.md +2 -0
- package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +3 -1
- package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +3 -1
- package/plugins/devflow-intel/skills/code-index-query/SKILL.md +2 -0
- package/plugins/devflow-intel/skills/workflow-code-index/SKILL.md +2 -0
- package/plugins/devflow-intel/skills/workflow-context-sync/SKILL.md +2 -0
- package/plugins/devflow-intel/skills/workflow-update-navigation/SKILL.md +3 -1
- package/plugins/devflow-quality/skills/code-review/SKILL.md +4 -2
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +3 -1
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/SKILL.md +2 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +2 -0
- package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +2 -0
- package/plugins/devflow-quality/skills/workflow-regression-test-design/SKILL.md +2 -0
- package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +7 -5
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +8 -6
- package/plugins/devflow-requirements/skills/requirement-feasibility-review/SKILL.md +2 -0
- package/plugins/devflow-requirements/skills/workflow-demand-clarify/SKILL.md +2 -0
- package/plugins/devflow-requirements/skills/workflow-domain-modeling/SKILL.md +2 -0
- package/plugins/devflow-requirements/skills/workflow-requirement-baseline/SKILL.md +6 -2
- package/plugins/devflow-requirements/skills/workflow-requirement-triage/SKILL.md +2 -0
- package/plugins/devflow-requirements/skills/workflow-wayfinder/SKILL.md +2 -0
- package/src/constants.ts +1 -1
- package/src/workflow-diagnosis.ts +21 -1
- package/templates/omp/AGENTS.md +6 -2
|
@@ -9,6 +9,8 @@ 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"
|
|
@@ -84,9 +86,7 @@ def gate_contract_enabled?(workflow)
|
|
|
84
86
|
workflow.dig("governance", "gate_contract_version").to_i >= 3
|
|
85
87
|
end
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
workflow.dig("governance", "hard_gate_mode").to_s.empty? ? "chain" : workflow.dig("governance", "hard_gate_mode").to_s
|
|
89
|
-
end
|
|
89
|
+
|
|
90
90
|
|
|
91
91
|
# Build the runtime view from the existing workflow declaration. This keeps
|
|
92
92
|
# governance.step_checkers as the single checker namespace and avoids creating
|
|
@@ -125,7 +125,6 @@ def transition_gate_contract(workflow, step)
|
|
|
125
125
|
"required_inputs" => Array(step["inputs"]).select { |input| input.is_a?(Hash) && input["required_at_completion"] == true }
|
|
126
126
|
},
|
|
127
127
|
"quality" => { "checker_ids" => Array(workflow.dig("governance", "step_checkers", step["id"])) },
|
|
128
|
-
"hard_gate_mode" => core_hard_gate_mode(workflow),
|
|
129
128
|
"concurrency" => { "project_lock" => "required", "optimistic_revision" => "required", "registry_view" => "candidate_overlay" },
|
|
130
129
|
"recovery" => { "allowed" => ["sync_artifacts", "repair_outputs"], "max_attempts" => 2 }
|
|
131
130
|
}
|
|
@@ -419,19 +418,7 @@ def track_allowed_steps(track)
|
|
|
419
418
|
end
|
|
420
419
|
|
|
421
420
|
def next_pending_step_id(state, pending_ids, step_by_id, completed_ids)
|
|
422
|
-
|
|
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
|
|
421
|
+
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
422
|
end
|
|
436
423
|
|
|
437
424
|
def conditional_skip_policies(workflow, checkpoint_id)
|
|
@@ -535,7 +522,11 @@ def join_parallel_window(root, state, workflow, current_revision)
|
|
|
535
522
|
raise ArgumentError, "parallel run not found" unless record
|
|
536
523
|
branches = Array(record["branches"])
|
|
537
524
|
incomplete = branches.reject { |branch| branch["status"].to_s == "succeeded" }
|
|
538
|
-
raise ArgumentError, "parallel
|
|
525
|
+
raise ArgumentError, "parallel run has no branches" if branches.empty?
|
|
526
|
+
if incomplete.any?
|
|
527
|
+
$devflow_advisories << { "reason" => "parallel_branches_not_succeeded", "branches" => incomplete.map { |branch| branch.slice("branch_id", "status") } }
|
|
528
|
+
record["completion_with_warnings"] = true
|
|
529
|
+
end
|
|
539
530
|
|
|
540
531
|
branch_steps = branches.map { |branch| branch.fetch("workflow_step").to_s }
|
|
541
532
|
state["completed_steps"] = (Array(state["completed_steps"]) + branch_steps).uniq
|
|
@@ -597,45 +588,19 @@ end
|
|
|
597
588
|
# a small safety floor for malformed control-plane state and unsafe writes, but
|
|
598
589
|
# Core-generated test/traceability/merge findings no longer strand the serial
|
|
599
590
|
# workflow before the AI review can decide what to do.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
|
591
|
+
OPERATION_FAILURE_REASONS = %w[
|
|
592
|
+
missing_state_file missing_state_front_matter stale_revision invalid_control_path
|
|
593
|
+
unknown_step gate_target_step_unknown gate_current_step_mismatch
|
|
594
|
+
parallel_transition_requires_write feature_not_active cancel_reason_required
|
|
595
|
+
session_rollover_in_progress parallel_branch_not_found gate_boundary_rejected
|
|
596
|
+
parallel_join_failed parallel_branch_transition_failed state_validation_failed
|
|
597
|
+
phase_ownership_claim_failed phase_ownership_completion_failed not_waiting_checkpoint no_pending_step
|
|
633
598
|
].freeze
|
|
634
599
|
$devflow_advisories = []
|
|
635
600
|
|
|
636
601
|
def fail_result(reason, details = {})
|
|
637
602
|
failure = { "ok" => false, "reason" => reason, "details" => details }
|
|
638
|
-
if
|
|
603
|
+
if !OPERATION_FAILURE_REASONS.include?(reason.to_s)
|
|
639
604
|
$devflow_advisories << failure
|
|
640
605
|
return nil
|
|
641
606
|
end
|
|
@@ -660,8 +625,8 @@ rescue JSON::ParserError => error
|
|
|
660
625
|
{ "ok" => false, "reason" => "test_task_binding_#{stage}_check_failed", "errors" => ["checker returned invalid JSON: #{error.message}"] }
|
|
661
626
|
end
|
|
662
627
|
|
|
663
|
-
#
|
|
664
|
-
#
|
|
628
|
+
# Always re-check acceptance results. Soft mode retains failures as advisories
|
|
629
|
+
# without changing the canonical test verdict.
|
|
665
630
|
ACCEPTANCE_RESULT_GATED_STEPS = %w[run-acceptance-tests release-closeout].freeze
|
|
666
631
|
|
|
667
632
|
def acceptance_results_check(root, feature)
|
|
@@ -956,7 +921,9 @@ result =
|
|
|
956
921
|
!%w[prepared claimed].include?(state.dig("rollover", "status")) &&
|
|
957
922
|
(options[:complete_step] || options[:confirm_checkpoint] || options[:enter_failure_branch] || options[:exit_failure_branch] || options[:parallel_action])
|
|
958
923
|
topology_errors = WorkflowGateContract.validate(workflow)
|
|
959
|
-
|
|
924
|
+
if !topology_errors.empty?
|
|
925
|
+
$devflow_advisories.concat(topology_errors)
|
|
926
|
+
elsif !topology_errors.empty?
|
|
960
927
|
result = fail_result("gate_boundary_rejected", "gate" => {
|
|
961
928
|
"verdict" => "deny",
|
|
962
929
|
"reason" => "workflow_gate_contract_invalid",
|
|
@@ -1014,7 +981,8 @@ result =
|
|
|
1014
981
|
transition_kind: transition_kind,
|
|
1015
982
|
checkpoint_authorized: options[:runtime_confirmed]
|
|
1016
983
|
)
|
|
1017
|
-
|
|
984
|
+
$devflow_advisories.concat(Array(gate_result && gate_result["warnings"]))
|
|
985
|
+
if gate_result && !gate_result["ok"]
|
|
1018
986
|
result = fail_result("gate_boundary_rejected", "gate" => gate_result)
|
|
1019
987
|
end
|
|
1020
988
|
end
|
|
@@ -1129,24 +1097,22 @@ result =
|
|
|
1129
1097
|
state.delete("checkpoint_decision")
|
|
1130
1098
|
|
|
1131
1099
|
if step["checkpoint"]
|
|
1132
|
-
|
|
1133
|
-
state["
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1100
|
+
$devflow_advisories << { "reason" => "checkpoint_advisory", "checkpoint" => step["checkpoint"], "approval_recorded" => false }
|
|
1101
|
+
state["checkpoint_decision"] = "continued_with_warning"
|
|
1102
|
+
begin
|
|
1103
|
+
apply_conditional_step_skips!(root, state, workflow, step["checkpoint"], current_revision + 1)
|
|
1104
|
+
pending = Array(state["pending_steps"])
|
|
1105
|
+
completed = Array(state["completed_steps"])
|
|
1106
|
+
rescue JSON::ParserError, KeyError, ArgumentError => error
|
|
1107
|
+
$devflow_advisories << { "reason" => "conditional_step_skip_failed", "message" => error.message }
|
|
1108
|
+
end
|
|
1109
|
+
end
|
|
1110
|
+
if (parallel_window = parallel_window_for(workflow, step_id))
|
|
1138
1111
|
state["current_step"] = parallel_window.fetch("join_step")
|
|
1139
1112
|
state["status"] = "in_progress"
|
|
1140
1113
|
state.delete("checkpoint")
|
|
1141
1114
|
state.delete("checkpoint_label")
|
|
1142
1115
|
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
1116
|
elsif (next_id = next_pending_step_id(state, pending, step_by_id, completed))
|
|
1151
1117
|
state["current_step"] = next_id
|
|
1152
1118
|
state["status"] = "in_progress"
|
|
@@ -1178,125 +1144,6 @@ result =
|
|
|
1178
1144
|
elsif Array(state["pending_steps"]).empty?
|
|
1179
1145
|
result = fail_result("no_pending_step")
|
|
1180
1146
|
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
1147
|
if result.nil?
|
|
1301
1148
|
begin
|
|
1302
1149
|
applied_skips = apply_conditional_step_skips!(root, state, workflow, state["checkpoint"], current_revision + 1)
|
|
@@ -1313,7 +1160,7 @@ result =
|
|
|
1313
1160
|
completed = Array(state["completed_steps"])
|
|
1314
1161
|
next_id = next_pending_step_id(state, pending, step_by_id, completed)
|
|
1315
1162
|
if next_id.nil? && track_boundary?(state, pending)
|
|
1316
|
-
state["checkpoint_decision"] = "
|
|
1163
|
+
state["checkpoint_decision"] = "continued_with_warning"
|
|
1317
1164
|
state["status"] = "blocked"
|
|
1318
1165
|
state["block_reason"] = "track_scope_complete"
|
|
1319
1166
|
state.delete("checkpoint")
|
|
@@ -1323,7 +1170,7 @@ result =
|
|
|
1323
1170
|
action_description = "Checkpoint confirmed. Active track output scope is complete."
|
|
1324
1171
|
next_intent_requested = true
|
|
1325
1172
|
else
|
|
1326
|
-
state["checkpoint_decision"] = "
|
|
1173
|
+
state["checkpoint_decision"] = "continued_with_warning"
|
|
1327
1174
|
state["current_step"] = next_id || pending.first
|
|
1328
1175
|
state["status"] = "in_progress"
|
|
1329
1176
|
state.delete("checkpoint")
|
|
@@ -1408,7 +1255,9 @@ result =
|
|
|
1408
1255
|
phase_owner_override: prospective_owner
|
|
1409
1256
|
)
|
|
1410
1257
|
|
|
1411
|
-
|
|
1258
|
+
$devflow_advisories.concat(Array(validation["warnings"]))
|
|
1259
|
+
state["gate_advisories"] = JSON.parse(JSON.generate(Array(state["gate_advisories"]) + $devflow_advisories)) if changed
|
|
1260
|
+
if changed && !validation["ok"]
|
|
1412
1261
|
result = fail_result("state_validation_failed", "validation" => validation)
|
|
1413
1262
|
elsif changed && options[:write]
|
|
1414
1263
|
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
|
|
@@ -10,6 +10,7 @@ require "yaml"
|
|
|
10
10
|
require_relative "control_io"
|
|
11
11
|
require_relative "requirement_graph"
|
|
12
12
|
require_relative "checker_registry"
|
|
13
|
+
require_relative "parallel_run"
|
|
13
14
|
|
|
14
15
|
POLICY = {
|
|
15
16
|
"domain-modeling" => { "checkers" => %w[requirement-graph-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[term object behavior rule boundary] },
|
|
@@ -134,10 +135,46 @@ begin
|
|
|
134
135
|
devflow_architecture_constraint devflow_architecture_feasibility devflow_architecture_brief
|
|
135
136
|
devflow_approval
|
|
136
137
|
],
|
|
137
|
-
"tool_budget" => policy["budget"], "
|
|
138
|
+
"tool_budget" => policy["budget"], "acceptance" => Array(graph["nodes"]).select { |node| node["kind"] == "acceptance" }.map { |node| node["id"] },
|
|
138
139
|
"allowed_graph_delta" => Array(policy["allowed_graph_delta"]),
|
|
139
140
|
"on_unknown" => "block_and_escalate"
|
|
140
141
|
}
|
|
142
|
+
# While a durable parallel run is attached, current_step is the join
|
|
143
|
+
# anchor, not permission to execute the join before its branches finish.
|
|
144
|
+
if state["parallel_run_ref"]
|
|
145
|
+
run = ParallelRun.load(root, options[:feature])
|
|
146
|
+
raise ArgumentError, "parallel run reference mismatch" unless run && run["run_id"] == state["parallel_run_ref"]
|
|
147
|
+
raise ArgumentError, "parallel run state revision mismatch" unless run["bound_state_revision"] == state["revision"]
|
|
148
|
+
window = Array(workflow["parallel_windows"]).find { |item| item["id"] == run["workflow_window"] }
|
|
149
|
+
raise ArgumentError, "parallel window unavailable" unless window && Array(state["completed_steps"]).include?(window["after_step"])
|
|
150
|
+
declared = window.fetch("branches", {}).values.flatten
|
|
151
|
+
branches = Array(run["branches"])
|
|
152
|
+
raise ArgumentError, "parallel run has no branches" if branches.empty?
|
|
153
|
+
branch_contracts = branches.map do |branch|
|
|
154
|
+
id = branch["workflow_step"]
|
|
155
|
+
raise ArgumentError, "parallel branch outside declared window" unless declared.include?(id)
|
|
156
|
+
definition = Array(workflow["steps"]).find { |item| item["id"] == id }
|
|
157
|
+
branch_policy = POLICY.fetch(id)
|
|
158
|
+
raise ArgumentError, "parallel branch definition unavailable" unless definition
|
|
159
|
+
{
|
|
160
|
+
"branch_id" => branch["branch_id"], "step" => id,
|
|
161
|
+
"allowed_agents" => branch_policy.fetch("agents"),
|
|
162
|
+
"allowed_paths" => (Array(definition["outputs"]).map { |output| output["path"] } + [definition.dig("meta_output", "path")]).compact.map { |value| interpolate(value, state) }.uniq,
|
|
163
|
+
"tool_budget" => branch_policy.fetch("budget"),
|
|
164
|
+
"allowed_graph_delta" => Array(branch_policy["allowed_graph_delta"])
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
contract.merge!(
|
|
168
|
+
"scope" => "parallel_window", "parallel_run_id" => run["run_id"],
|
|
169
|
+
"parallel_window_id" => window["id"], "join_step" => window["join_step"],
|
|
170
|
+
"branch_contracts" => branch_contracts,
|
|
171
|
+
"allowed_paths" => branch_contracts.flat_map { |item| item["allowed_paths"] }.uniq,
|
|
172
|
+
"allowed_agents" => branch_contracts.flat_map { |item| item["allowed_agents"] }.uniq,
|
|
173
|
+
"tool_budget" => branch_contracts.sum { |item| item["tool_budget"] },
|
|
174
|
+
"allowed_graph_delta" => branch_contracts.flat_map { |item| item["allowed_graph_delta"] }.uniq,
|
|
175
|
+
"checker_ids" => []
|
|
176
|
+
)
|
|
177
|
+
end
|
|
141
178
|
frozen = contract.merge("frozen_hash" => Digest::SHA256.hexdigest(JSON.generate(contract)))
|
|
142
179
|
contract_id = "EC-#{frozen["frozen_hash"][0, 16]}"
|
|
143
180
|
frozen["contract_id"] = contract_id
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# One policy for workflow diagnostics. Failed checks retain their evidence;
|
|
4
|
+
# advisory execution never changes a failed test/check into a passing result.
|
|
5
|
+
module GatePolicy
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def advisory(entry)
|
|
9
|
+
entry.merge("severity" => "warning", "soft_gate" => true)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "gate_policy"
|
|
5
|
+
|
|
4
6
|
module GateResult
|
|
5
7
|
VERDICTS = %w[allow allow_with_warning await_user waiting_external_input block_unsafe].freeze
|
|
6
8
|
CONTROL_PLANE_ERROR = "control_plane_error"
|
|
@@ -11,6 +13,16 @@ module GateResult
|
|
|
11
13
|
valid = VERDICTS.include?(verdict.to_s) || verdict.to_s == CONTROL_PLANE_ERROR
|
|
12
14
|
raise ArgumentError, "invalid gate verdict: #{verdict}" unless valid
|
|
13
15
|
|
|
16
|
+
if !%w[allow allow_with_warning].include?(verdict.to_s)
|
|
17
|
+
codes = Array(errors).map { |entry| entry.is_a?(Hash) ? entry["code"].to_s : "" }
|
|
18
|
+
structural = codes.any? { |code| %w[REVISION_CONFLICT BUSINESS_GATE_POLICY_INVALID].include?(code) }
|
|
19
|
+
unless structural
|
|
20
|
+
warnings = Array(warnings) + Array(errors).map { |entry| GatePolicy.advisory(entry.is_a?(Hash) ? entry : { "message" => entry.to_s }) }
|
|
21
|
+
warnings << { "code" => "SOFT_GATE", "original_verdict" => verdict.to_s } if warnings.empty?
|
|
22
|
+
errors = []
|
|
23
|
+
verdict = "allow_with_warning"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
14
26
|
{
|
|
15
27
|
"ok" => %w[allow allow_with_warning].include?(verdict.to_s),
|
|
16
28
|
"verdict" => verdict.to_s,
|
|
@@ -32,14 +32,6 @@ module GateRuntime
|
|
|
32
32
|
gate_id = contract.fetch("gate_id")
|
|
33
33
|
step_id = contract.fetch("step_id")
|
|
34
34
|
base = { gate_id: gate_id, step_id: step_id, evidence: evidence.merge("collector" => source), revisions: { "state" => current_revision } }
|
|
35
|
-
# During the chain-skill migration Core is diagnostic-only. It still
|
|
36
|
-
# validates the contract shape above, but never blocks a transition on
|
|
37
|
-
# graph, artifact, output, checker, review, or topology observations.
|
|
38
|
-
# Checkpoint confirmation remains fail-closed because approval-path outputs
|
|
39
|
-
# do not exist until the user approves the waiting checkpoint.
|
|
40
|
-
if contract.fetch("hard_gate_mode", "core").to_s == "chain" && transition_kind.to_s != "confirm_checkpoint"
|
|
41
|
-
return GateResult.allow(**base.merge(evidence: base.fetch(:evidence).merge("core_hard_gate_mode" => "chain", "core_hard_gate_bypassed" => true)))
|
|
42
|
-
end
|
|
43
35
|
return GateResult.block(**base, code: "REVISION_CONFLICT", message: "current revision does not match expected revision", recovery: { "kind" => "reinspect", "expected_revision" => expected_revision }) unless expected_revision.nil? || current_revision.to_i == expected_revision.to_i
|
|
44
36
|
unless WORKFLOW_STATUSES.include?(workflow_state["status"].to_s)
|
|
45
37
|
return GateResult.block(**base, code: "WORKFLOW_STATUS_INVALID", message: "workflow status is not recognized")
|
|
@@ -191,10 +183,13 @@ module GateRuntime
|
|
|
191
183
|
|
|
192
184
|
def run_checker(contract:, workflow_state:, current_revision:, expected_revision:, checker:, **kwargs)
|
|
193
185
|
boundary = evaluate(contract: contract, workflow_state: workflow_state, current_revision: current_revision, expected_revision: expected_revision, **kwargs)
|
|
194
|
-
return boundary unless boundary["
|
|
186
|
+
return boundary unless boundary["ok"]
|
|
195
187
|
result = checker.call
|
|
196
188
|
raise ArgumentError, "checker must return a GateResult hash" unless result.is_a?(Hash) && result["verdict"]
|
|
189
|
+
warnings = Array(boundary["warnings"]) + Array(result["warnings"])
|
|
197
190
|
result.merge(
|
|
191
|
+
"warnings" => warnings,
|
|
192
|
+
"verdict" => result["verdict"] == "allow" && warnings.any? ? "allow_with_warning" : result["verdict"],
|
|
198
193
|
"gate_id" => result["gate_id"] || boundary["gate_id"],
|
|
199
194
|
"step_id" => result["step_id"] || boundary["step_id"],
|
|
200
195
|
"evidence" => boundary.fetch("evidence", {}).merge(result.fetch("evidence", {})),
|
|
@@ -6,6 +6,7 @@ require "optparse"
|
|
|
6
6
|
require "yaml"
|
|
7
7
|
require_relative "validate_state"
|
|
8
8
|
require_relative "checkpoint_meta"
|
|
9
|
+
require_relative "gate_policy"
|
|
9
10
|
|
|
10
11
|
options = { json: false, state: nil }
|
|
11
12
|
OptionParser.new do |opts|
|
|
@@ -192,19 +193,7 @@ def track_allowed_steps(track)
|
|
|
192
193
|
end
|
|
193
194
|
|
|
194
195
|
def next_pending_step_id(state, pending_ids, step_by_id, completed_ids)
|
|
195
|
-
|
|
196
|
-
unless track
|
|
197
|
-
return pending_ids.find do |step_id|
|
|
198
|
-
step_by_id[step_id] && dependencies_satisfied?(step_by_id[step_id], completed_ids)
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
allowed = track_allowed_steps(track)
|
|
203
|
-
pending_ids.find do |step_id|
|
|
204
|
-
allowed.include?(step_id) &&
|
|
205
|
-
step_by_id[step_id] &&
|
|
206
|
-
dependencies_satisfied?(step_by_id[step_id], completed_ids)
|
|
207
|
-
end
|
|
196
|
+
pending_ids.find { |id| step_by_id[id] && dependencies_satisfied?(step_by_id[id], completed_ids) } || pending_ids.find { |id| step_by_id[id] }
|
|
208
197
|
end
|
|
209
198
|
|
|
210
199
|
def track_boundary(state, pending_ids, completed_ids)
|
|
@@ -366,27 +355,12 @@ result =
|
|
|
366
355
|
|
|
367
356
|
if missing.any?
|
|
368
357
|
fail_result("missing_state_fields", fields: missing)
|
|
369
|
-
elsif project_context_missing?(root)
|
|
370
|
-
project_context_result(
|
|
371
|
-
"incomplete_project_context",
|
|
372
|
-
missing: [
|
|
373
|
-
".devflow/planning/PROJECT.md",
|
|
374
|
-
".devflow/planning/navigation/INDEX.md",
|
|
375
|
-
".devflow/context/spec/index.md"
|
|
376
|
-
]
|
|
377
|
-
)
|
|
378
358
|
else
|
|
379
359
|
pending_template_blockers = Array(state["blockers"]).select do |blocker|
|
|
380
360
|
blocker.is_a?(Hash) && blocker["type"] == "PENDING_TEMPLATE"
|
|
381
361
|
end
|
|
382
362
|
|
|
383
|
-
|
|
384
|
-
fail_result(
|
|
385
|
-
"pending_depth_spec_templates",
|
|
386
|
-
blockers: pending_template_blockers,
|
|
387
|
-
recovery: "add the missing template or mark the topic NOT_APPLICABLE, then rerun workflow-project-context"
|
|
388
|
-
)
|
|
389
|
-
else
|
|
363
|
+
begin
|
|
390
364
|
workflow_path = state["workflow_config"] ||
|
|
391
365
|
File.join(".devflow/planning", "workflows", "#{state["current_flow"]}.yaml")
|
|
392
366
|
abs_workflow_path = File.join(root, workflow_path)
|
|
@@ -413,6 +387,8 @@ result =
|
|
|
413
387
|
strict_artifact_status: true
|
|
414
388
|
)
|
|
415
389
|
|
|
390
|
+
validation["warnings"] << GatePolicy.advisory({ "code" => "incomplete_project_context", "message" => "project context indexes are missing" }) if project_context_missing?(root)
|
|
391
|
+
validation["warnings"].concat(pending_template_blockers.map { |entry| GatePolicy.advisory(entry) })
|
|
416
392
|
if !validation["ok"]
|
|
417
393
|
{
|
|
418
394
|
current_flow: state["current_flow"],
|
|
@@ -429,6 +405,8 @@ result =
|
|
|
429
405
|
},
|
|
430
406
|
next_step: nil,
|
|
431
407
|
workflow_config: workflow_path,
|
|
408
|
+
warnings: Array(state["gate_advisories"]) + Array(validation["warnings"]),
|
|
409
|
+
gate_mode: "soft",
|
|
432
410
|
artifact_registry: artifact_registry_summary(root, artifact_by_path).merge(
|
|
433
411
|
"path" => artifact_registry_path,
|
|
434
412
|
"error" => artifact_registry_error,
|
|
@@ -446,41 +424,6 @@ result =
|
|
|
446
424
|
},
|
|
447
425
|
recovery: "Fix missing/invalid artifacts or call devflow_artifact with action=sync and preserve=false, then call devflow_state with action=inspect."
|
|
448
426
|
}
|
|
449
|
-
elsif (drift = validation["warnings"].find { |issue| issue["code"] == "current_output_registry_drift" })
|
|
450
|
-
{
|
|
451
|
-
current_flow: state["current_flow"],
|
|
452
|
-
current_feature: state["current_feature"],
|
|
453
|
-
current_phase: state["current_phase"],
|
|
454
|
-
status: state["status"],
|
|
455
|
-
completed_steps: completed_ids,
|
|
456
|
-
pending_steps: pending_ids,
|
|
457
|
-
current_step: current_step && {
|
|
458
|
-
id: current_step["id"],
|
|
459
|
-
label: current_step["label"],
|
|
460
|
-
skill: current_step["skill"],
|
|
461
|
-
action: current_step["action"]
|
|
462
|
-
},
|
|
463
|
-
next_step: nil,
|
|
464
|
-
workflow_config: workflow_path,
|
|
465
|
-
artifact_registry: artifact_registry_summary(root, artifact_by_path).merge(
|
|
466
|
-
"path" => artifact_registry_path,
|
|
467
|
-
"error" => artifact_registry_error,
|
|
468
|
-
"current_outputs_registered" => 0
|
|
469
|
-
),
|
|
470
|
-
action: "recover_artifact_registry",
|
|
471
|
-
reason: "current_output_registry_drift",
|
|
472
|
-
drift: drift,
|
|
473
|
-
recovery_plan: {
|
|
474
|
-
classification: "safe_repairable",
|
|
475
|
-
state_transition: "none",
|
|
476
|
-
registry_write: true,
|
|
477
|
-
confirmation_required: true,
|
|
478
|
-
expected_revision: state.fetch("revision", 0),
|
|
479
|
-
diff: [{ path: drift["path"], registry_exists: drift["registry_exists"], filesystem_exists: true,
|
|
480
|
-
registry_content_valid: drift["registry_content_valid"], filesystem_content_valid: true }]
|
|
481
|
-
},
|
|
482
|
-
recovery: "After reviewing this diff, call devflow_recovery(expectedRevision=#{state.fetch("revision", 0)}, confirm=true). It only reconciles registry metadata; explicitly complete the step separately when ready."
|
|
483
|
-
}
|
|
484
427
|
elsif current_step.nil?
|
|
485
428
|
fallback_next_id = next_pending_step_id(state, pending_ids, step_by_id, completed_ids)
|
|
486
429
|
fallback_next_step = fallback_next_id && step_by_id[fallback_next_id]
|
|
@@ -563,6 +506,7 @@ result =
|
|
|
563
506
|
next_step = next_id && step_by_id[next_id]
|
|
564
507
|
checkpoint_id = state["checkpoint"] || current_step["checkpoint"]
|
|
565
508
|
checkpoint = checkpoint_id && workflow.fetch("checkpoints", {})[checkpoint_id]
|
|
509
|
+
checkpoint = checkpoint.merge("id" => checkpoint_id) if checkpoint.is_a?(Hash)
|
|
566
510
|
parallel_window = active_parallel_window(workflow, state, root, step_by_id, completed_ids, pending_ids, artifact_by_path)
|
|
567
511
|
failure_branch = active_failure_branch(workflow, state, current_step, current_outputs, root)
|
|
568
512
|
|
|
@@ -603,6 +547,8 @@ result =
|
|
|
603
547
|
},
|
|
604
548
|
parallel_window: parallel_window,
|
|
605
549
|
workflow_config: workflow_path,
|
|
550
|
+
warnings: Array(state["gate_advisories"]) + Array(validation["warnings"]),
|
|
551
|
+
gate_mode: "soft",
|
|
606
552
|
artifact_registry: artifact_registry_summary(root, artifact_by_path).merge(
|
|
607
553
|
"path" => artifact_registry_path,
|
|
608
554
|
"error" => artifact_registry_error,
|
|
@@ -613,34 +559,23 @@ result =
|
|
|
613
559
|
|
|
614
560
|
boundary = track_boundary(state, pending_ids, completed_ids)
|
|
615
561
|
|
|
616
|
-
if
|
|
617
|
-
base.merge(
|
|
618
|
-
action: "blocked",
|
|
619
|
-
reason: artifact_registry_error,
|
|
620
|
-
recovery: "Call devflow_artifact with action=sync."
|
|
621
|
-
)
|
|
622
|
-
elsif boundary
|
|
623
|
-
base.merge(
|
|
624
|
-
action: "blocked",
|
|
625
|
-
reason: boundary["reason"],
|
|
626
|
-
message: boundary["message"],
|
|
627
|
-
active_track: active_track(state)
|
|
628
|
-
)
|
|
629
|
-
elsif failure_branch
|
|
630
|
-
base.merge(
|
|
631
|
-
action: "route_failure_branch",
|
|
632
|
-
reason: "failure_branch_triggered",
|
|
633
|
-
failure_branch: failure_branch
|
|
634
|
-
)
|
|
635
|
-
elsif state["status"] == "waiting_checkpoint"
|
|
562
|
+
if state["status"] == "waiting_checkpoint"
|
|
636
563
|
missing_outputs = current_outputs
|
|
637
564
|
.select { |output| output["required"] && !output["exists"] }
|
|
638
565
|
.map { |output| output["resolved_path"] }
|
|
639
|
-
|
|
566
|
+
# Approval-path outputs are written by Core only after the user
|
|
567
|
+
# confirms; they must not block entry into the waiting checkpoint.
|
|
568
|
+
meta_path = checkpoint.is_a?(Hash) && checkpoint["required_meta_output"]
|
|
569
|
+
checkpoint_meta_errors = if CheckpointMeta.approval_output?(checkpoint, meta_path)
|
|
570
|
+
[]
|
|
571
|
+
else
|
|
572
|
+
CheckpointMeta.validate(root: root, state: state, checkpoint: checkpoint)
|
|
573
|
+
end
|
|
640
574
|
missing_outputs.concat(checkpoint_meta_errors.map { |entry| entry["path"] }.compact)
|
|
641
575
|
base.merge(
|
|
642
|
-
action:
|
|
643
|
-
reason:
|
|
576
|
+
action: "await_user_confirmation",
|
|
577
|
+
reason: nil,
|
|
578
|
+
warnings: Array(base[:warnings]) + (missing_outputs.empty? ? [] : [{ code: "missing_checkpoint_outputs", paths: missing_outputs, soft_gate: true }]),
|
|
644
579
|
missing_outputs: missing_outputs,
|
|
645
580
|
checkpoint_meta_errors: checkpoint_meta_errors,
|
|
646
581
|
resume_behavior: checkpoint && checkpoint["resume_behavior"],
|