@guanlin4924/devflow-pi 0.1.85 → 0.1.88

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/README.md +4 -0
  2. package/package.json +1 -1
  3. package/plugins/devflow-core/extensions/devflow-core.ts +211 -19
  4. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +15 -0
  5. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +49 -3
  6. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_adapter.rb +83 -0
  7. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +19 -0
  8. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checkpoint_meta.rb +46 -0
  9. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +10 -1
  10. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +32 -0
  11. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +44 -41
  12. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +5 -4
  13. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +15 -4
  14. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +15 -9
  15. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_preflight.py +238 -0
  16. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_run.py +234 -32
  17. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +63 -11
  18. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +5 -1
  19. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +40 -31
  20. package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +11 -5
  21. package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +6 -6
  22. package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +16 -1
  23. package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +3 -1
  24. package/plugins/devflow-quality/agents/code-reviewer.md +7 -1
  25. package/plugins/devflow-quality/skills/code-review/SKILL.md +10 -0
  26. package/plugins/devflow-quality/skills/workflow-gen-test-guide/SKILL.md +37 -0
  27. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/SKILL.md +327 -0
  28. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/assets/output-template.md +213 -0
  29. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/bug-rules.md +9 -0
  30. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/coverage-checklist.md +63 -0
  31. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/exception-checklist.md +37 -0
  32. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/api-test.md +58 -0
  33. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/boundary-test.md +151 -0
  34. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/common-test.md +49 -0
  35. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/compatibility-test.md +105 -0
  36. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/data-test.md +67 -0
  37. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/exception-test.md +109 -0
  38. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/function-test.md +65 -0
  39. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/integration-test.md +36 -0
  40. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/performance-test.md +100 -0
  41. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/scenario-test.md +63 -0
  42. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/security-test.md +55 -0
  43. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/ui-test.md +49 -0
  44. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/usability-test.md +46 -0
  45. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/writing-guide.md +111 -0
  46. package/src/constants.ts +1 -1
@@ -11,6 +11,15 @@ module CheckerRegistry
11
11
  business-cross-model-check business-invalidation-check
12
12
  ].freeze
13
13
  EMBEDDED = %w[requirement-coverage-check source-authority-check traceability-check].freeze
14
+ GRAPH_BACKED = %w[
15
+ requirement-graph-check slice-independence-check contract-coverage-check
16
+ change-impact-check acceptance-coverage-check test-coverage-check test-obligation-check
17
+ ].freeze
18
+ ADAPTERS = %w[
19
+ api-test-schema-check api-test-matrix-check acceptance-test-results-check
20
+ test-task-binding-check tdd-evidence-check cross-track-consistency-check
21
+ business-cross-model-check business-claim-reference-check
22
+ ].freeze
14
23
 
15
24
  module_function
16
25
 
@@ -24,6 +33,16 @@ module CheckerRegistry
24
33
  nil
25
34
  end
26
35
 
36
+ def executable?(id)
37
+ GRAPH_BACKED.include?(id.to_s) || ADAPTERS.include?(id.to_s)
38
+ end
39
+
40
+ def validate_executable!(ids)
41
+ unsupported = Array(ids).map(&:to_s).reject { |id| executable?(id) }
42
+ raise ArgumentError, "checker has no Core execution adapter: #{unsupported.join(', ')}" unless unsupported.empty?
43
+ true
44
+ end
45
+
27
46
  def validate!(ids)
28
47
  unknown = Array(ids).map(&:to_s).reject { |id| known?(id) }
29
48
  raise ArgumentError, "unknown checker id(s): #{unknown.join(", ")}" unless unknown.empty?
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+
6
+ module CheckpointMeta
7
+ module_function
8
+
9
+ def validate(root:, state:, checkpoint:)
10
+ return [] unless checkpoint.is_a?(Hash)
11
+
12
+ raw_path = checkpoint["required_meta_output"]
13
+ # Legacy/custom checkpoints may not declare checkpoint metadata. Only
14
+ # enforce the contract when a metadata path is explicitly declared.
15
+ return [] if raw_path.to_s.empty?
16
+
17
+ relative = raw_path.to_s
18
+ .gsub("{feature}", state.fetch("current_feature", "").to_s)
19
+ .gsub("{phase}", state.fetch("current_phase", "").to_s)
20
+ absolute = File.expand_path(relative, root)
21
+ unless File.file?(absolute)
22
+ return [{ "code" => "missing_checkpoint_meta_output", "message" => "checkpoint meta output is missing", "path" => relative }]
23
+ end
24
+
25
+ data = JSON.parse(File.read(absolute))
26
+ errors = []
27
+ unless data.is_a?(Hash)
28
+ errors << { "code" => "invalid_checkpoint_meta_shape", "message" => "checkpoint meta output must be a JSON object", "path" => relative }
29
+ return errors
30
+ end
31
+
32
+ Array(checkpoint["required_meta_fields"]).each do |field|
33
+ next if data.key?(field.to_s) && !data[field.to_s].nil?
34
+
35
+ errors << {
36
+ "code" => "missing_checkpoint_meta_field",
37
+ "message" => "checkpoint meta output is missing a required field",
38
+ "path" => relative,
39
+ "field" => field
40
+ }
41
+ end
42
+ errors
43
+ rescue JSON::ParserError => error
44
+ [{ "code" => "invalid_checkpoint_meta_json", "message" => error.message, "path" => relative }]
45
+ end
46
+ end
@@ -9,6 +9,7 @@ require "optparse"
9
9
  require "yaml"
10
10
  require_relative "control_io"
11
11
  require_relative "requirement_graph"
12
+ require_relative "checker_registry"
12
13
 
13
14
  POLICY = {
14
15
  "domain-modeling" => { "checkers" => %w[requirement-graph-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[term object behavior rule boundary] },
@@ -26,6 +27,7 @@ POLICY = {
26
27
  "change-plan" => { "checkers" => %w[requirement-graph-check slice-independence-check contract-coverage-check api-test-matrix-check test-coverage-check test-obligation-check test-task-binding-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[acceptance] },
27
28
  "execute" => { "checkers" => %w[tdd-evidence-check change-impact-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[requirement rule boundary behavior] },
28
29
  "run-acceptance-tests" => { "checkers" => %w[api-test-matrix-check test-coverage-check acceptance-test-results-check change-impact-check], "agents" => ["verification-runner"], "budget" => 1, "allowed_graph_delta" => [] },
30
+ "code-review" => { "checkers" => %w[], "agents" => ["devflow-code-reviewer"], "budget" => 3, "allowed_graph_delta" => [] },
29
31
  "verify" => { "checkers" => %w[acceptance-coverage-check acceptance-test-results-check change-impact-check], "agents" => ["verification-runner"], "budget" => 1, "allowed_graph_delta" => %w[acceptance] }
30
32
  }.freeze
31
33
 
@@ -91,12 +93,19 @@ begin
91
93
  raise ArgumentError, "stale graph revision: expected #{options[:expected_graph_revision]}, actual #{graph["revision"]}" if options[:expected_graph_revision] && graph["revision"] != options[:expected_graph_revision]
92
94
  end
93
95
  policy = POLICY.fetch(options[:step], { "checkers" => [], "agents" => [], "budget" => 0 })
96
+ # The active workflow is the source of truth for gate checkers. POLICY
97
+ # retains execution permissions and budgets, but must not maintain a second
98
+ # checker list that can drift from workflow governance.
99
+ workflow_checkers = workflow.dig("governance", "step_checkers", options[:step])
100
+ checker_ids = workflow_checkers.nil? ? Array(policy["checkers"]) : Array(workflow_checkers).map(&:to_s)
101
+ CheckerRegistry.validate!(checker_ids)
102
+ CheckerRegistry.validate_executable!(checker_ids)
94
103
  paths = (Array(step["outputs"]).map { |output| output["path"] } + [step.dig("meta_output", "path")]).compact.map { |value| interpolate(value, state) }.uniq
95
104
  contract = {
96
105
  "schema_version" => 2, "origin" => "core_execution_contract", "feature" => options[:feature], "step" => options[:step],
97
106
  "state_revision" => state["revision"], "graph_revision" => graph["revision"], "workflow_path" => workflow_relative,
98
107
  "workflow_fingerprint" => Digest::SHA256.hexdigest(workflow_raw), "manifest_id" => "devflow-core-execution-policy-v2",
99
- "allowed_paths" => paths, "allowed_agents" => policy["agents"], "checker_ids" => policy["checkers"],
108
+ "allowed_paths" => paths, "allowed_agents" => policy["agents"], "checker_ids" => checker_ids,
100
109
  # Core read/write tools required by the architecture feasibility skill are
101
110
  # part of the frozen contract. The tool layer still applies per-tool
102
111
  # mutation ownership and the step's allowed paths.
@@ -2,12 +2,14 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "json"
5
+ require "open3"
5
6
  require_relative "gate_result"
6
7
  require_relative "gate_contract"
7
8
  require_relative "checker_registry"
8
9
  require_relative "parallel_run"
9
10
  require_relative "business_model_gate"
10
11
  require_relative "requirement_graph"
12
+ require_relative "checkpoint_meta"
11
13
 
12
14
  module GateRuntime
13
15
  KNOWN_VALIDATIONS = %w[static-runtime-context requirement-graph].freeze
@@ -69,8 +71,32 @@ module GateRuntime
69
71
  missing_outputs = required_paths(contract.dig("outputs", "required"), root, workflow_state)
70
72
  missing_outputs.concat(required_paths(contract.dig("outputs", "required_inputs"), root, workflow_state, purpose: :completion))
71
73
  return GateResult.block(**base, code: "REQUIRED_OUTPUT_MISSING", message: "required step output is missing", details: { "missing" => missing_outputs }) unless missing_outputs.empty?
74
+ checkpoint = contract.dig("topology", "checkpoint")
75
+ if checkpoint.is_a?(Hash) && checkpoint["producing_step"].to_s == step_id.to_s
76
+ meta_errors = CheckpointMeta.validate(root: root, state: workflow_state, checkpoint: checkpoint)
77
+ unless meta_errors.empty?
78
+ return GateResult.block(**base, code: "CHECKPOINT_META_INVALID", message: "checkpoint meta output is missing or invalid", details: { "checkpoint" => checkpoint["id"], "errors" => meta_errors })
79
+ end
80
+ end
72
81
  invalid_outputs = validate_declared_inputs(contract.dig("outputs", "required_inputs"), root, workflow_state, purpose: :completion)
73
82
  return GateResult.block(**base, code: "REQUIRED_OUTPUT_INVALID", message: "required step input/output fact is invalid", details: { "invalid" => invalid_outputs }) unless invalid_outputs.empty?
83
+ if step_id == "code-review"
84
+ contract_view = File.join(root, ".planning/execution-contract.md")
85
+ contract_id = if File.file?(contract_view) && !File.symlink?(contract_view)
86
+ File.read(contract_view).match(/^- Contract ID: `(EC-[0-9a-f]{16})`$/)&.[](1)
87
+ end
88
+ unless contract_id
89
+ return GateResult.block(**base, code: "REVIEW_GATE_NOT_PASSED", message: "current Core review contract identity is unavailable")
90
+ end
91
+ script = File.join(__dir__, "review_run.py")
92
+ stdout, stderr, process = Open3.capture3("python3", script, File.realpath(root), "--gate", workflow_state.fetch("current_feature").to_s, contract_id)
93
+ review = JSON.parse(stdout) rescue nil
94
+ unless process.success? && review.is_a?(Hash) && review["status"] == "pass"
95
+ return GateResult.block(**base, code: "REVIEW_GATE_NOT_PASSED", message: "independent Core review has not passed", details: {
96
+ "review" => review, "error" => stderr.to_s.empty? ? nil : stderr.to_s[0, 1000]
97
+ })
98
+ end
99
+ end
74
100
  end
75
101
  end
76
102
  end
@@ -84,6 +110,12 @@ module GateRuntime
84
110
  unless workflow_state["completed_steps"].to_a.include?(checkpoint["producing_step"].to_s)
85
111
  return GateResult.block(**base, code: "CHECKPOINT_PRODUCER_INCOMPLETE", message: "checkpoint producing step is not completed", details: { "checkpoint" => checkpoint_id })
86
112
  end
113
+ if root
114
+ meta_errors = CheckpointMeta.validate(root: root, state: workflow_state, checkpoint: checkpoint)
115
+ unless meta_errors.empty?
116
+ return GateResult.block(**base, code: "CHECKPOINT_META_INVALID", message: "checkpoint meta output is missing or invalid", details: { "checkpoint" => checkpoint_id, "errors" => meta_errors })
117
+ end
118
+ end
87
119
  end
88
120
  GateResult.allow(**base)
89
121
  rescue KeyError, ArgumentError => e
@@ -60,47 +60,50 @@ unless PhaseOwnership.valid_slug?(phase)
60
60
  end
61
61
  workflow_config = ".planning/workflows/#{options[:flow]}.yaml"
62
62
  workflow_path = File.expand_path(workflow_config, root)
63
- fallback_pending_steps = [
64
- "requirement-triage",
65
- "requirement-wayfinder",
66
- "domain-modeling",
67
- "requirement-feasibility-review",
68
- "requirement-baseline",
69
- "api-contract",
70
- "backend-tech-spec",
71
- "api-call-guide",
72
- "ui-spec",
73
- "feature-breakdown",
74
- "frontend-tech-spec",
75
- "api-supplement",
76
- "integration-verify",
77
- "change-plan",
78
- "phase-structure",
79
- "execute",
80
- "code-index-refresh",
81
- "code-review",
82
- "generate-mainflow-cases",
83
- "verify",
84
- "audit",
85
- "update-navigation",
86
- "archive"
87
- ]
88
- pending_steps = if File.exist?(workflow_path)
89
- workflow = YAML.safe_load(File.read(workflow_path), permitted_classes: [], aliases: false)
90
- # The template is authored in readable sections, so physical
91
- # YAML position is not a scheduling contract. A step with an
92
- # explicit order must initialize ahead of later ordered
93
- # steps even when it was inserted lower in the document.
94
- Array(workflow["steps"])
95
- .each_with_index
96
- .sort_by do |(step, index)|
97
- order = step["order"]
98
- [order.is_a?(Integer) ? 0 : 1, order.is_a?(Integer) ? order : index, index]
99
- end
100
- .map { |(step, _index)| step.fetch("id") }
101
- else
102
- fallback_pending_steps
103
- end
63
+ unless options[:flow].is_a?(String) && options[:flow].match?(/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/)
64
+ puts JSON.pretty_generate({
65
+ "ok" => false,
66
+ "reason" => "invalid_flow",
67
+ "message" => "flow must be a safe workflow id"
68
+ })
69
+ exit 0
70
+ end
71
+ unless File.file?(workflow_path)
72
+ puts JSON.pretty_generate({
73
+ "ok" => false,
74
+ "reason" => "workflow_not_found",
75
+ "message" => "Workflow configuration does not exist: #{workflow_config}. Run init_workflow.rb first or choose a registered flow."
76
+ })
77
+ exit 0
78
+ end
79
+
80
+ begin
81
+ workflow = YAML.safe_load(File.read(workflow_path), permitted_classes: [], aliases: false)
82
+ steps = workflow.is_a?(Hash) ? workflow["steps"] : nil
83
+ unless steps.is_a?(Array) && steps.any? && steps.all? { |step| step.is_a?(Hash) && step["id"].is_a?(String) && !step["id"].empty? }
84
+ raise ArgumentError, "workflow steps must be a non-empty list with string ids"
85
+ end
86
+ step_ids = steps.map { |step| step["id"] }
87
+ raise ArgumentError, "workflow step ids must be unique" unless step_ids.uniq.length == step_ids.length
88
+ rescue Psych::Exception, ArgumentError => error
89
+ puts JSON.pretty_generate({
90
+ "ok" => false,
91
+ "reason" => "invalid_workflow",
92
+ "message" => "Invalid workflow configuration #{workflow_config}: #{error.message}"
93
+ })
94
+ exit 0
95
+ end
96
+
97
+ # The template is authored in readable sections, so physical YAML position is
98
+ # not a scheduling contract. A step with an explicit order must initialize
99
+ # ahead of later ordered steps even when it was inserted lower in the document.
100
+ pending_steps = steps
101
+ .each_with_index
102
+ .sort_by do |(step, index)|
103
+ order = step["order"]
104
+ [order.is_a?(Integer) ? 0 : 1, order.is_a?(Integer) ? order : index, index]
105
+ end
106
+ .map { |(step, _index)| step.fetch("id") }
104
107
  current_step = options[:step] || pending_steps.first || "requirement-triage"
105
108
 
106
109
  state = {
@@ -81,7 +81,7 @@ changes = targets.map do |template_name, target_path|
81
81
  same_as_template = exists && File.read(target) == File.read(source)
82
82
  state_mismatch = blocked_target_paths.include?(target_path)
83
83
  action = if exists && !options[:force]
84
- same_as_template ? "skip_existing" : "skip_existing_changed"
84
+ same_as_template ? "skip_existing" : "blocked_template_drift"
85
85
  elsif state_mismatch && !options[:allow_state_mismatch]
86
86
  "blocked_state_mismatch"
87
87
  elsif blocked_target_paths.any?
@@ -104,14 +104,15 @@ changes = targets.map do |template_name, target_path|
104
104
  }
105
105
  end
106
106
 
107
- blocked = changes.any? { |change| change["action"] == "blocked_state_mismatch" }
107
+ blocked = changes.any? { |change| %w[blocked_state_mismatch blocked_template_drift].include?(change["action"]) }
108
+ template_drift = changes.any? { |change| change["action"] == "blocked_template_drift" }
108
109
  result = {
109
110
  "ok" => !blocked,
110
111
  "written" => options[:write] && !blocked,
111
112
  "force" => options[:force],
112
113
  "allow_state_mismatch" => options[:allow_state_mismatch],
113
- "upgrade_hint" => changes.any? { |change| change["action"] == "skip_existing_changed" } ? "Existing generated workflow differs from the bundled template. Use --force only when intentionally upgrading generated workflow files." : nil,
114
- "blocked_reason" => blocked ? "active_state_step_mismatch" : nil,
114
+ "upgrade_hint" => template_drift ? "Existing generated workflow differs from the bundled template. Review the diff, then rerun with --force to intentionally upgrade generated workflow files." : nil,
115
+ "blocked_reason" => template_drift ? "template_drift" : (blocked ? "active_state_step_mismatch" : nil),
115
116
  "changes" => changes
116
117
  }.reject { |_, value| value.nil? }
117
118
  ensure
@@ -5,6 +5,7 @@ require "json"
5
5
  require "optparse"
6
6
  require "yaml"
7
7
  require_relative "validate_state"
8
+ require_relative "checkpoint_meta"
8
9
 
9
10
  options = { json: false, state: nil }
10
11
  OptionParser.new do |opts|
@@ -34,11 +35,19 @@ def load_yaml_file(path)
34
35
  end
35
36
 
36
37
  def interpolate_path(path, state)
37
- path
38
+ path.to_s
38
39
  .gsub("{feature}", state.fetch("current_feature", "").to_s)
39
40
  .gsub("{phase}", state.fetch("current_phase", "").to_s)
40
41
  end
41
42
 
43
+ def input_payload(input, state)
44
+ if input.is_a?(Hash)
45
+ input.merge("path" => interpolate_path(input.fetch("path"), state))
46
+ else
47
+ interpolate_path(input, state)
48
+ end
49
+ end
50
+
42
51
  def load_artifact_registry(root, workflow)
43
52
  registry_config = workflow["artifact_registry"]
44
53
  registry_path = registry_config && registry_config["source"]
@@ -121,7 +130,7 @@ def step_payload(step, state, root, artifact_by_path)
121
130
  "label" => step["label"],
122
131
  "skill" => step["skill"],
123
132
  "action" => step["action"],
124
- "inputs" => Array(step["inputs"]).map { |path| interpolate_path(path, state) },
133
+ "inputs" => Array(step["inputs"]).map { |input| input_payload(input, state) },
125
134
  "outputs" => Array(step["outputs"]).map { |output| output_payload(output, state, root, artifact_by_path) }
126
135
  }
127
136
  end
@@ -575,7 +584,7 @@ result =
575
584
  label: next_step["label"],
576
585
  skill: next_step["skill"],
577
586
  action: next_step["action"],
578
- inputs: Array(next_step["inputs"]).map { |path| interpolate_path(path, state) },
587
+ inputs: Array(next_step["inputs"]).map { |input| input_payload(input, state) },
579
588
  outputs: Array(next_step["outputs"]).map do |output|
580
589
  resolved = interpolate_path(output.fetch("path"), state)
581
590
  artifact = artifact_by_path[resolved]
@@ -624,11 +633,13 @@ result =
624
633
  missing_outputs = current_outputs
625
634
  .select { |output| output["required"] && !output["exists"] }
626
635
  .map { |output| output["resolved_path"] }
627
-
636
+ checkpoint_meta_errors = CheckpointMeta.validate(root: root, state: state, checkpoint: checkpoint)
637
+ missing_outputs.concat(checkpoint_meta_errors.map { |entry| entry["path"] }.compact)
628
638
  base.merge(
629
639
  action: missing_outputs.empty? ? "await_user_confirmation" : "blocked",
630
640
  reason: missing_outputs.empty? ? nil : "missing_checkpoint_outputs",
631
641
  missing_outputs: missing_outputs,
642
+ checkpoint_meta_errors: checkpoint_meta_errors,
632
643
  resume_behavior: checkpoint && checkpoint["resume_behavior"],
633
644
  next_intent: ".planning/.next-intent"
634
645
  )
@@ -11,6 +11,8 @@ require "yaml"
11
11
  require_relative "control_io"
12
12
  require_relative "requirement_graph"
13
13
  require_relative "checker_registry"
14
+ require_relative "checker_adapter"
15
+ require_relative "checkpoint_meta"
14
16
 
15
17
  options = { feature: nil, step: nil, contract: nil, write: false, json: false, branch_id: nil, attempt: nil, branch_revision: nil, lock_held: false }
16
18
  OptionParser.new do |o|
@@ -199,6 +201,11 @@ begin
199
201
 
200
202
  output_specs = Array(step["outputs"]).select { |item| item["required"] }
201
203
  output_specs << step["meta_output"] if step["meta_output"].is_a?(Hash)
204
+ checkpoint = workflow.dig("checkpoints", step["checkpoint"])
205
+ checkpoint_meta_errors = CheckpointMeta.validate(root: root, state: state, checkpoint: checkpoint)
206
+ if checkpoint.is_a?(Hash) && checkpoint["required_meta_output"]
207
+ output_specs << { "path" => checkpoint["required_meta_output"], "required" => true }
208
+ end
202
209
  files = output_specs.map do |spec|
203
210
  path = spec["path"]
204
211
  next unless path
@@ -208,21 +215,20 @@ begin
208
215
  { "path" => relative, "required" => true, "exists" => exists, "bytes" => exists ? File.size(absolute) : 0,
209
216
  "sha256" => exists ? Digest::SHA256.file(absolute).hexdigest : nil }
210
217
  end.compact
211
- input_files = Array(step["inputs"]).map { |input| file_evidence(root, interpolate(input.to_s, state)) }
218
+ input_files = Array(step["inputs"]).map do |input|
219
+ raw_path = input.is_a?(Hash) ? input.fetch("path") : input
220
+ file_evidence(root, interpolate(raw_path.to_s, state))
221
+ end
212
222
 
213
223
  graph, = RequirementGraph.load_graph(root, options[:feature])
214
224
  checkers = RequirementGraph.checker_results(graph)
215
225
  selected = configured_checkers(workflow, options[:step])
216
226
  CheckerRegistry.validate!(selected)
227
+ CheckerRegistry.validate_executable!(selected)
217
228
  gate = workflow.dig("governance", "requirement_graph_gate").to_s
218
229
  enforced = gate == "enforced"
219
230
  checker_results = selected.map do |id|
220
- supplemental = supplemental_checker(root, options[:feature], id, options[:step])
221
- next supplemental if supplemental
222
-
223
- issues = checkers.fetch(id, ["unknown fixed checker: #{id}"])
224
- { "id" => id, "origin" => "core_fixed_checker", "exit_code" => issues.empty? ? 0 : 1, "passed" => issues.empty?, "issues" => issues,
225
- "counters" => { "issue_count" => issues.length } }
231
+ CheckerAdapter.execute(root: root, feature: options[:feature], step: options[:step], id: id, graph_results: checkers)
226
232
  end
227
233
  graph_missing = graph["revision"].zero? && !File.exist?(RequirementGraph.graph_path(root, options[:feature]).first)
228
234
  checker_passed = !enforced || (!graph_missing && checker_results.all? { |item| item["passed"] })
@@ -248,14 +254,14 @@ begin
248
254
  "counters" => { "required_file_count" => files.length, "existing_file_count" => files.count { |item| item["exists"] }, "nodes_by_kind" => node_counts,
249
255
  "stale_node_count" => Array(graph["stale"]).length, "stale_view_count" => Hash(graph["views"]).count { |_name, view| view["needs_recheck"] } },
250
256
  "view_fingerprints" => Hash(graph["views"]).transform_values { |view| view.select { |key, _value| %w[fingerprint graph_revision state needs_recheck stale_fingerprint].include?(key) } },
251
- "complete" => files.all? { |item| item["exists"] } && checker_passed && evidence_complete,
257
+ "complete" => files.all? { |item| item["exists"] } && checkpoint_meta_errors.empty? && checker_passed && evidence_complete,
252
258
  "evidence_complete" => evidence_complete,
253
259
  "gate_mode" => gate.empty? ? "observe" : gate
254
260
  }.compact
255
261
  suffix = binding ? ".attempt-#{binding["attempt"]}.branch-#{Digest::SHA256.hexdigest(binding["branch_id"].to_s)[0, 12]}" : ""
256
262
  destination = File.join(root, ".planning/evidence", options[:feature], "#{options[:step]}#{suffix}.machine.json")
257
263
  DevflowControlIO.atomic_write(destination, JSON.pretty_generate(pack) + "\n")
258
- result = { "ok" => true, "path" => destination.sub("#{root}/", ""), "complete" => pack["complete"], "state_revision" => pack["state_revision"], "graph_revision" => pack["graph_revision"], "checkers" => checker_results, "parallel_binding" => binding }
264
+ result = { "ok" => true, "path" => destination.sub("#{root}/", ""), "complete" => pack["complete"], "state_revision" => pack["state_revision"], "graph_revision" => pack["graph_revision"], "checkers" => checker_results, "checkpoint_meta_errors" => checkpoint_meta_errors, "parallel_binding" => binding }
259
265
  rescue StandardError => error
260
266
  result = { "ok" => false, "reason" => "machine_evidence_failed", "message" => error.message }
261
267
  ensure