@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.
@@ -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
- errors << error("PARALLEL_WINDOW_SCHEMA_INVALID", "parallel window must define join_step and branches", window: window["id"]) unless window.is_a?(Hash) && window["join_step"] && window["branches"].is_a?(Hash)
38
- next unless window.is_a?(Hash)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.39";
2
+ export const PRODUCT_VERSION = "0.1.40";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5
 
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": 1,
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