@guanlin4924/devflow-pi 0.1.38 → 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.
Files changed (19) hide show
  1. package/package.json +1 -1
  2. package/plugins/devflow-core/extensions/devflow-core.ts +178 -8
  3. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +283 -1
  4. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +31 -0
  5. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/cross_track_consistency_check.rb +94 -0
  6. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +1 -1
  7. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_contract.rb +64 -0
  8. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_result.rb +40 -0
  9. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +157 -0
  10. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +3 -0
  11. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +112 -9
  12. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +222 -0
  13. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_coverage.rb +173 -0
  14. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +14 -8
  15. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +131 -5
  16. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +148 -0
  17. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +29 -0
  18. package/src/constants.ts +1 -1
  19. package/src/runtime.ts +8 -2
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Validates the existing workflow topology as a GateContract source. This is
5
+ # intentionally declarative and read-only: it does not advance STATE or alter
6
+ # failure-branch behavior.
7
+ require "json"
8
+ require "optparse"
9
+ require "yaml"
10
+ require_relative "checker_registry"
11
+
12
+ module WorkflowGateContract
13
+ CHECKPOINT_FIELDS = %w[gate_after producing_step required_outputs required_meta_output required_meta_fields prompt confirm_options skip_policy resume_behavior].freeze
14
+
15
+ module_function
16
+
17
+ def validate(workflow)
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"] }
24
+ checkpoints = workflow["checkpoints"]
25
+ unless checkpoints.is_a?(Hash)
26
+ return [error("CHECKPOINTS_SCHEMA_INVALID", "workflow checkpoints must be a mapping")]
27
+ end
28
+ checkpoints.each do |id, checkpoint|
29
+ unless checkpoint.is_a?(Hash)
30
+ errors << error("CHECKPOINT_SCHEMA_INVALID", "checkpoint must be a mapping", checkpoint: id)
31
+ next
32
+ end
33
+ CHECKPOINT_FIELDS.each do |field|
34
+ errors << error("CHECKPOINT_FIELD_MISSING", "checkpoint field is required", checkpoint: id, field: field) unless checkpoint.key?(field)
35
+ end
36
+ errors << error("CHECKPOINT_PRODUCER_MISMATCH", "gate_after must equal producing_step", checkpoint: id) if checkpoint["gate_after"].to_s != checkpoint["producing_step"].to_s
37
+ errors << error("CHECKPOINT_OUTPUTS_INVALID", "required_outputs must be an array", checkpoint: id) unless checkpoint["required_outputs"].is_a?(Array)
38
+ errors << error("CHECKPOINT_META_FIELDS_INVALID", "required_meta_fields must be an array", checkpoint: id) unless checkpoint["required_meta_fields"].is_a?(Array)
39
+ errors << error("CHECKPOINT_OPTIONS_INVALID", "confirm_options must include approve and needs_fix", checkpoint: id) unless Array(checkpoint["confirm_options"]).sort == %w[approve needs_fix]
40
+ end
41
+
42
+ Array(workflow.dig("governance", "enforced_checkpoints")).each do |id|
43
+ errors << error("CHECKPOINT_REF_NOT_FOUND", "enforced checkpoint is not defined", checkpoint: id) unless checkpoints.key?(id)
44
+ end
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
+
56
+ Array(workflow["parallel_windows"]).each do |window|
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)
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)
114
+ end
115
+
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)
119
+ resume = branch.is_a?(Hash) && branch["resume_after_success"]
120
+ unless resume.is_a?(Hash) && %w[flow step status checkpoint].all? { |field| resume.key?(field) }
121
+ errors << error("FAILURE_RETURN_TO_INVALID", "failure branch must declare flow, step, status and checkpoint", source_step: source_step)
122
+ next
123
+ end
124
+ errors << error("FAILURE_RETURN_CHECKPOINT_INVALID", "failure branch return checkpoint is not defined", source_step: source_step, checkpoint: resume["checkpoint"]) unless checkpoints.key?(resume["checkpoint"])
125
+ end
126
+ errors
127
+ end
128
+
129
+ def error(code, message, **extra)
130
+ { "code" => code, "message" => message }.merge(extra)
131
+ end
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
@@ -17,12 +17,15 @@ phase_scope:
17
17
  # files without governance_started_revision remain legacy_unattested and are
18
18
  # only reported until their next newly-started governed step.
19
19
  governance:
20
+ gate_contract_version: 3
20
21
  requirement_graph_gate: enforced
22
+ requirement_coverage_gate: enforced
21
23
  enforced_checkpoints:
22
24
  - CP-1
23
25
  - CP-API
24
26
  - CP-2
25
27
  - CP-3
28
+ - CP-4
26
29
  step_checkers:
27
30
  requirement-baseline:
28
31
  - requirement-graph-check
@@ -42,6 +45,7 @@ governance:
42
45
  - slice-independence-check
43
46
  - contract-coverage-check
44
47
  - change-impact-check
48
+ - cross-track-consistency-check
45
49
  acceptance-test-design:
46
50
  - requirement-graph-check
47
51
  - contract-coverage-check
@@ -120,6 +124,10 @@ parallel_windows:
120
124
  checkpoint: CP-2
121
125
  execution_policy:
122
126
  mode: parallel_when_supported
127
+ scheduler:
128
+ one_agent_per_step: true
129
+ max_concurrency: 2
130
+ track_order: declared
123
131
  serial_fallback_order:
124
132
  - backend-tech-spec
125
133
  - api-call-guide
@@ -311,6 +319,7 @@ checkpoints:
311
319
  producing_step: run-acceptance-tests
312
320
  required_outputs:
313
321
  - ".planning/test-runs/{feature}/acceptance-results.json"
322
+ required_meta_output: ".planning/test-runs/{feature}/acceptance-results.meta.json"
314
323
  required_meta_fields:
315
324
  - acceptance_tc_passed
316
325
  - acceptance_tc_failed
@@ -365,6 +374,7 @@ steps:
365
374
  order: 6
366
375
  label: Requirement wayfinder
367
376
  skill: workflow-wayfinder
377
+ engine_evidence_gate: enforced
368
378
  depends_on:
369
379
  - requirement-triage
370
380
  inputs:
@@ -392,6 +402,7 @@ steps:
392
402
  order: 7
393
403
  label: Domain modeling
394
404
  skill: workflow-domain-modeling
405
+ engine_evidence_gate: enforced
395
406
  depends_on:
396
407
  - requirement-wayfinder
397
408
  inputs:
@@ -417,6 +428,7 @@ steps:
417
428
  order: 8
418
429
  label: Requirement feasibility review
419
430
  skill: requirement-feasibility-review
431
+ engine_evidence_gate: enforced
420
432
  depends_on:
421
433
  - domain-modeling
422
434
  inputs:
@@ -443,6 +455,7 @@ steps:
443
455
  order: 10
444
456
  label: Requirement baseline
445
457
  skill: workflow-requirement-baseline
458
+ engine_evidence_gate: enforced
446
459
  depends_on:
447
460
  - requirement-feasibility-review
448
461
  inputs:
@@ -478,6 +491,7 @@ steps:
478
491
  order: 15
479
492
  label: API contract
480
493
  skill: workflow-api-contract
494
+ engine_evidence_gate: enforced
481
495
  depends_on:
482
496
  - requirement-baseline
483
497
  inputs:
@@ -504,6 +518,7 @@ steps:
504
518
  order: 18
505
519
  label: Architecture feasibility
506
520
  skill: workflow-architecture-feasibility
521
+ engine_evidence_gate: enforced
507
522
  depends_on:
508
523
  - api-contract
509
524
  inputs:
@@ -535,6 +550,7 @@ steps:
535
550
  order: 20
536
551
  label: UI spec
537
552
  skill: workflow-ui-spec
553
+ engine_evidence_gate: enforced
538
554
  depends_on:
539
555
  - api-contract
540
556
  - architecture-feasibility
@@ -551,6 +567,7 @@ steps:
551
567
  order: 30
552
568
  label: Feature breakdown
553
569
  skill: workflow-feature-breakdown
570
+ engine_evidence_gate: enforced
554
571
  depends_on:
555
572
  - ui-spec
556
573
  inputs:
@@ -565,6 +582,7 @@ steps:
565
582
  order: 40
566
583
  label: Frontend tech spec
567
584
  skill: workflow-frontend-tech-spec
585
+ engine_evidence_gate: enforced
568
586
  depends_on:
569
587
  - feature-breakdown
570
588
  - api-contract
@@ -598,6 +616,7 @@ steps:
598
616
  order: 50
599
617
  label: Backend tech spec
600
618
  skill: workflow-backend-tech-spec
619
+ engine_evidence_gate: enforced
601
620
  depends_on:
602
621
  - api-contract
603
622
  - architecture-feasibility
@@ -625,6 +644,7 @@ steps:
625
644
  order: 55
626
645
  label: API call guide
627
646
  skill: workflow-api-call-guide
647
+ engine_evidence_gate: enforced
628
648
  depends_on:
629
649
  - api-contract
630
650
  - backend-tech-spec
@@ -649,6 +669,7 @@ steps:
649
669
  order: 56
650
670
  label: API supplement
651
671
  skill: workflow-api-supplement
672
+ engine_evidence_gate: enforced
652
673
  depends_on:
653
674
  - frontend-tech-spec
654
675
  - api-contract
@@ -672,6 +693,7 @@ steps:
672
693
  order: 58
673
694
  label: Integration verify
674
695
  skill: workflow-integration-verify
696
+ engine_evidence_gate: enforced
675
697
  depends_on:
676
698
  - api-call-guide
677
699
  - frontend-tech-spec
@@ -702,6 +724,7 @@ steps:
702
724
  order: 60
703
725
  label: Change plan
704
726
  skill: workflow-change-plan
727
+ engine_evidence_gate: enforced
705
728
  depends_on:
706
729
  - acceptance-test-design
707
730
  inputs:
@@ -735,6 +758,7 @@ steps:
735
758
  order: 59
736
759
  label: Acceptance test design
737
760
  skill: workflow-acceptance-test-design
761
+ engine_evidence_gate: enforced
738
762
  depends_on:
739
763
  - integration-verify
740
764
  inputs:
@@ -774,6 +798,7 @@ steps:
774
798
  order: 70
775
799
  label: Phase structure
776
800
  action: initialize-phase-structure
801
+ engine_evidence_gate: enforced
777
802
  depends_on:
778
803
  - change-plan
779
804
  inputs:
@@ -791,6 +816,7 @@ steps:
791
816
  order: 80
792
817
  label: Execute
793
818
  skill: workflow-execute-phase
819
+ engine_evidence_gate: enforced
794
820
  depends_on:
795
821
  - phase-structure
796
822
  inputs:
@@ -835,6 +861,7 @@ steps:
835
861
  order: 85
836
862
  label: Code Review
837
863
  skill: code-review
864
+ engine_evidence_gate: enforced
838
865
  depends_on:
839
866
  - code-index-refresh
840
867
  inputs:
@@ -849,6 +876,7 @@ steps:
849
876
  order: 86
850
877
  label: Generate mainflow cases
851
878
  skill: workflow-generate-mainflow-cases
879
+ engine_evidence_gate: enforced
852
880
  depends_on:
853
881
  - code-index-refresh
854
882
  inputs:
@@ -887,6 +915,7 @@ steps:
887
915
  order: 88
888
916
  label: Run frozen acceptance tests
889
917
  skill: workflow-run-acceptance-tests
918
+ engine_evidence_gate: enforced
890
919
  depends_on:
891
920
  - code-review
892
921
  inputs:
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.38";
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