@guanlin4924/devflow-pi 0.1.39 → 0.1.40
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/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +125 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +194 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +1 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/cross_track_consistency_check.rb +94 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +71 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +3 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +45 -12
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +222 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +76 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +94 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +5 -0
- package/src/constants.ts +1 -1
- package/src/runtime.ts +8 -2
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
# Validates the existing workflow topology as a GateContract source. This is
|
|
5
5
|
# intentionally declarative and read-only: it does not advance STATE or alter
|
|
6
6
|
# failure-branch behavior.
|
|
7
|
+
require "json"
|
|
8
|
+
require "optparse"
|
|
9
|
+
require "yaml"
|
|
10
|
+
require_relative "checker_registry"
|
|
11
|
+
|
|
7
12
|
module WorkflowGateContract
|
|
8
13
|
CHECKPOINT_FIELDS = %w[gate_after producing_step required_outputs required_meta_output required_meta_fields prompt confirm_options skip_policy resume_behavior].freeze
|
|
9
14
|
|
|
@@ -11,6 +16,11 @@ module WorkflowGateContract
|
|
|
11
16
|
|
|
12
17
|
def validate(workflow)
|
|
13
18
|
errors = []
|
|
19
|
+
unless workflow.is_a?(Hash)
|
|
20
|
+
return [error("WORKFLOW_SCHEMA_INVALID", "workflow must be a mapping")]
|
|
21
|
+
end
|
|
22
|
+
steps = Array(workflow["steps"])
|
|
23
|
+
step_by_id = steps.each_with_object({}) { |step, index| index[step["id"].to_s] = step if step.is_a?(Hash) && step["id"] }
|
|
14
24
|
checkpoints = workflow["checkpoints"]
|
|
15
25
|
unless checkpoints.is_a?(Hash)
|
|
16
26
|
return [error("CHECKPOINTS_SCHEMA_INVALID", "workflow checkpoints must be a mapping")]
|
|
@@ -33,13 +43,79 @@ module WorkflowGateContract
|
|
|
33
43
|
errors << error("CHECKPOINT_REF_NOT_FOUND", "enforced checkpoint is not defined", checkpoint: id) unless checkpoints.key?(id)
|
|
34
44
|
end
|
|
35
45
|
|
|
46
|
+
step_checkers = workflow.dig("governance", "step_checkers")
|
|
47
|
+
if step_checkers.is_a?(Hash)
|
|
48
|
+
step_checkers.each do |step_id, checker_ids|
|
|
49
|
+
errors << error("CHECKER_STEP_REF_NOT_FOUND", "checker declaration references an unknown step", step: step_id) unless step_by_id.key?(step_id.to_s)
|
|
50
|
+
Array(checker_ids).each do |checker_id|
|
|
51
|
+
errors << error("UNKNOWN_CHECKER_ID", "workflow references an unknown checker", step: step_id, checker: checker_id) unless CheckerRegistry.known?(checker_id)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
36
56
|
Array(workflow["parallel_windows"]).each do |window|
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
unless window.is_a?(Hash) && window["join_step"] && window["branches"].is_a?(Hash)
|
|
58
|
+
errors << error("PARALLEL_WINDOW_SCHEMA_INVALID", "parallel window must define join_step and branches", window: window.is_a?(Hash) && window["id"])
|
|
59
|
+
next
|
|
60
|
+
end
|
|
61
|
+
window_id = window["id"]
|
|
62
|
+
errors << error("PARALLEL_AFTER_STEP_NOT_FOUND", "parallel window after_step is not a workflow step", window: window_id, step: window["after_step"]) if window["after_step"] && !step_by_id.key?(window["after_step"].to_s)
|
|
63
|
+
errors << error("PARALLEL_JOIN_STEP_NOT_FOUND", "parallel join_step is not a workflow step", window: window_id, step: window["join_step"]) unless step_by_id.key?(window["join_step"].to_s)
|
|
39
64
|
errors << error("PARALLEL_WINDOW_CHECKPOINT_INVALID", "parallel join checkpoint is not defined", window: window["id"], checkpoint: window["checkpoint"]) if window["checkpoint"] && !checkpoints.key?(window["checkpoint"])
|
|
65
|
+
policy = window["execution_policy"]
|
|
66
|
+
scheduler = policy.is_a?(Hash) && policy["scheduler"]
|
|
67
|
+
unless scheduler.is_a?(Hash)
|
|
68
|
+
errors << error("PARALLEL_SCHEDULER_SCHEMA_INVALID", "parallel window must define execution_policy.scheduler", window: window_id)
|
|
69
|
+
else
|
|
70
|
+
errors << error("PARALLEL_SCHEDULER_FIELD_INVALID", "one_agent_per_step must be boolean", window: window_id, field: "one_agent_per_step") unless [true, false].include?(scheduler["one_agent_per_step"])
|
|
71
|
+
errors << error("PARALLEL_SCHEDULER_FIELD_INVALID", "max_concurrency must be a positive integer", window: window_id, field: "max_concurrency") unless scheduler["max_concurrency"].is_a?(Integer) && scheduler["max_concurrency"] > 0
|
|
72
|
+
errors << error("PARALLEL_SCHEDULER_FIELD_INVALID", "track_order must be declared", window: window_id, field: "track_order") unless scheduler["track_order"] == "declared"
|
|
73
|
+
end
|
|
74
|
+
errors << error("PARALLEL_BRANCH_STEPS_INVALID", "branch_steps must be an array", window: window_id) if window.key?("branch_steps") && !window["branch_steps"].is_a?(Array)
|
|
75
|
+
Array(window["branch_steps"]).each do |step_id|
|
|
76
|
+
errors << error("PARALLEL_BRANCH_STEP_NOT_FOUND", "branch_steps references an unknown step", window: window_id, step: step_id) unless step_by_id.key?(step_id.to_s)
|
|
77
|
+
end
|
|
78
|
+
branch_ids = []
|
|
79
|
+
step_tracks = {}
|
|
80
|
+
window["branches"].each do |track, branch_steps|
|
|
81
|
+
unless branch_steps.is_a?(Array) && !branch_steps.empty?
|
|
82
|
+
errors << error("PARALLEL_TRACK_STEPS_INVALID", "parallel track must contain an ordered step array", window: window_id, track: track)
|
|
83
|
+
next
|
|
84
|
+
end
|
|
85
|
+
positions = branch_steps.each_with_index.to_h { |step_id, position| [step_id.to_s, position] }
|
|
86
|
+
branch_steps.each_with_index do |step_id, position|
|
|
87
|
+
if positions.count { |candidate, _| candidate == step_id.to_s } > 1
|
|
88
|
+
errors << error("PARALLEL_TRACK_DUPLICATE_STEP", "parallel track repeats a step", window: window_id, track: track, step: step_id)
|
|
89
|
+
end
|
|
90
|
+
branch_ids << step_id.to_s
|
|
91
|
+
step_tracks[step_id.to_s] = track.to_s
|
|
92
|
+
errors << error("PARALLEL_BRANCH_STEP_NOT_FOUND", "parallel track references an unknown step", window: window_id, track: track, step: step_id) unless step_by_id.key?(step_id.to_s)
|
|
93
|
+
Array(step_by_id.dig(step_id.to_s, "depends_on")).each do |dependency|
|
|
94
|
+
dependency_position = positions[dependency.to_s]
|
|
95
|
+
next unless dependency_position
|
|
96
|
+
errors << error("PARALLEL_TRACK_ORDER_INVALID", "track dependency must precede dependent step", window: window_id, track: track, step: step_id, dependency: dependency) if dependency_position >= position
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
window["branches"].each do |track, branch_steps|
|
|
101
|
+
Array(branch_steps).each do |step_id|
|
|
102
|
+
Array(step_by_id.dig(step_id.to_s, "depends_on")).each do |dependency|
|
|
103
|
+
dependency_track = step_tracks[dependency.to_s]
|
|
104
|
+
next unless dependency_track && dependency_track != track.to_s
|
|
105
|
+
|
|
106
|
+
errors << error("PARALLEL_CROSS_TRACK_DEPENDENCY", "parallel branch depends on a step in another track", window: window_id, track: track, step: step_id, dependency: dependency, dependency_track: dependency_track)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
counts = branch_ids.each_with_object(Hash.new(0)) { |step_id, result| result[step_id] += 1 }
|
|
111
|
+
duplicate_steps = counts.select { |_, count| count > 1 }.keys
|
|
112
|
+
duplicate_steps.each { |step_id| errors << error("PARALLEL_STEP_DUPLICATED", "step appears in multiple parallel branches", window: window_id, step: step_id) }
|
|
113
|
+
errors << error("PARALLEL_JOIN_IN_BRANCH", "join_step cannot also be a branch step", window: window_id, step: window["join_step"]) if branch_ids.include?(window["join_step"].to_s)
|
|
40
114
|
end
|
|
41
115
|
|
|
42
116
|
(workflow["failure_branches"] || {}).each do |source_step, branch|
|
|
117
|
+
errors << error("FAILURE_SOURCE_STEP_NOT_FOUND", "failure branch source step is not defined", source_step: source_step) unless step_by_id.key?(source_step.to_s)
|
|
118
|
+
errors << error("FAILURE_BRANCH_SCHEMA_INVALID", "failure branch must be a mapping", source_step: source_step) unless branch.is_a?(Hash)
|
|
43
119
|
resume = branch.is_a?(Hash) && branch["resume_after_success"]
|
|
44
120
|
unless resume.is_a?(Hash) && %w[flow step status checkpoint].all? { |field| resume.key?(field) }
|
|
45
121
|
errors << error("FAILURE_RETURN_TO_INVALID", "failure branch must declare flow, step, status and checkpoint", source_step: source_step)
|
|
@@ -54,3 +130,19 @@ module WorkflowGateContract
|
|
|
54
130
|
{ "code" => code, "message" => message }.merge(extra)
|
|
55
131
|
end
|
|
56
132
|
end
|
|
133
|
+
|
|
134
|
+
if $PROGRAM_NAME == __FILE__
|
|
135
|
+
begin
|
|
136
|
+
options = {}
|
|
137
|
+
OptionParser.new do |opts|
|
|
138
|
+
opts.on("--workflow PATH") { |value| options[:workflow] = value }
|
|
139
|
+
opts.on("--json") { options[:json] = true }
|
|
140
|
+
end.parse!
|
|
141
|
+
raise ArgumentError, "--workflow is required" unless options[:workflow]
|
|
142
|
+
result = WorkflowGateContract.validate(YAML.safe_load(File.read(options[:workflow]), permitted_classes: [], aliases: false))
|
|
143
|
+
puts(options[:json] ? JSON.generate(result) : YAML.dump(result))
|
|
144
|
+
rescue StandardError => e
|
|
145
|
+
warn e.message
|
|
146
|
+
exit 1
|
|
147
|
+
end
|
|
148
|
+
end
|
package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml
CHANGED
|
@@ -45,6 +45,7 @@ governance:
|
|
|
45
45
|
- slice-independence-check
|
|
46
46
|
- contract-coverage-check
|
|
47
47
|
- change-impact-check
|
|
48
|
+
- cross-track-consistency-check
|
|
48
49
|
acceptance-test-design:
|
|
49
50
|
- requirement-graph-check
|
|
50
51
|
- contract-coverage-check
|
|
@@ -123,6 +124,10 @@ parallel_windows:
|
|
|
123
124
|
checkpoint: CP-2
|
|
124
125
|
execution_policy:
|
|
125
126
|
mode: parallel_when_supported
|
|
127
|
+
scheduler:
|
|
128
|
+
one_agent_per_step: true
|
|
129
|
+
max_concurrency: 2
|
|
130
|
+
track_order: declared
|
|
126
131
|
serial_fallback_order:
|
|
127
132
|
- backend-tech-spec
|
|
128
133
|
- api-call-guide
|
package/src/constants.ts
CHANGED
package/src/runtime.ts
CHANGED
|
@@ -36,12 +36,18 @@ const MANAGED_SETTINGS: Record<string, unknown> = {
|
|
|
36
36
|
"skills.enableAgentsProject": false,
|
|
37
37
|
// The interactive session is the high-capability Planner. Low-cost agents
|
|
38
38
|
// are dispatched only through explicit Devflow steps, one at a time by
|
|
39
|
-
// default, so background work cannot silently widen a user's request.
|
|
39
|
+
// default, so background work cannot silently widen a user's request. The
|
|
40
|
+
// parallel workflow window is explicitly capped at two workers, matching
|
|
41
|
+
// its two declared tracks; serial workflows are unaffected.
|
|
40
42
|
// Keep ordinary interactive turns on the user's DEFAULT role. In particular,
|
|
41
43
|
// do not inherit SLOW just because a thinking model is configured.
|
|
42
44
|
"defaultThinkingLevel": "low",
|
|
45
|
+
// Parallel Devflow orchestration relies on OMP's AsyncJobManager and
|
|
46
|
+
// subagent lifecycle channel. Keep this explicit instead of inheriting the
|
|
47
|
+
// SDK default so a global OMP config cannot silently disable it.
|
|
48
|
+
"async.enabled": true,
|
|
43
49
|
"task.batch": false,
|
|
44
|
-
"task.maxConcurrency":
|
|
50
|
+
"task.maxConcurrency": 2,
|
|
45
51
|
"task.maxRecursionDepth": 1,
|
|
46
52
|
"task.enableLsp": true,
|
|
47
53
|
// Keep cheap workers bounded by role prompts. OMP's loop guards remain
|