@guanlin4924/devflow-pi 0.1.15 → 0.1.17

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 (30) hide show
  1. package/package.json +1 -1
  2. package/plugins/devflow-core/extensions/devflow-core.ts +425 -54
  3. package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +3 -2
  4. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_assessment.rb +458 -0
  5. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_brief.rb +256 -0
  6. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_constraints.rb +155 -0
  7. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_feasibility.rb +176 -0
  8. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/control_io.rb +46 -0
  9. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +94 -0
  10. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +2 -1
  11. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +112 -0
  12. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +646 -0
  13. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/runtime_context_collector.rb +210 -0
  14. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/stale_check.rb +56 -0
  15. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/static_runtime_context_collector.rb +518 -0
  16. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +140 -0
  17. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workload_constraint.rb +57 -0
  18. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +88 -2
  19. package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +3 -2
  20. package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +17 -1
  21. package/plugins/devflow-design/skills/workflow-architecture-feasibility/SKILL.md +37 -0
  22. package/plugins/devflow-design/skills/workflow-architecture-feasibility/agents/openai.yaml +4 -0
  23. package/plugins/devflow-design/skills/workflow-backend-tech-spec/SKILL.md +7 -2
  24. package/plugins/devflow-design/skills/workflow-frontend-tech-spec/SKILL.md +6 -1
  25. package/plugins/devflow-requirements/skills/requirement-feasibility-review/SKILL.md +4 -0
  26. package/plugins/devflow-requirements/skills/workflow-domain-modeling/SKILL.md +4 -0
  27. package/plugins/devflow-requirements/skills/workflow-requirement-baseline/SKILL.md +4 -0
  28. package/plugins/devflow-requirements/skills/workflow-requirement-triage/SKILL.md +4 -0
  29. package/src/cli.ts +2 -1
  30. package/src/runtime.ts +2 -1
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # ExecutionContract v2 is synthesized from current Core truth. There is no
5
+ # CLI input for agents, paths, checkers, or budgets: those are fixed policy.
6
+ require "digest"
7
+ require "json"
8
+ require "optparse"
9
+ require "yaml"
10
+ require_relative "control_io"
11
+ require_relative "requirement_graph"
12
+
13
+ POLICY = {
14
+ "requirement-baseline" => { "checkers" => %w[requirement-graph-check slice-independence-check acceptance-coverage-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[requirement rule boundary slice acceptance] },
15
+ "api-contract" => { "checkers" => %w[requirement-graph-check contract-coverage-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[api] },
16
+ "architecture-feasibility" => { "checkers" => %w[requirement-graph-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[workload_shape workload_constraint decision risk] },
17
+ "integration-verify" => { "checkers" => %w[requirement-graph-check slice-independence-check contract-coverage-check change-impact-check], "agents" => ["verification-runner"], "budget" => 1, "allowed_graph_delta" => %w[requirement risk decision] },
18
+ "change-plan" => { "checkers" => %w[requirement-graph-check slice-independence-check contract-coverage-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[acceptance] },
19
+ "execute" => { "checkers" => %w[change-impact-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[requirement rule boundary behavior] },
20
+ "verify" => { "checkers" => %w[acceptance-coverage-check change-impact-check], "agents" => ["verification-runner"], "budget" => 1, "allowed_graph_delta" => %w[acceptance] }
21
+ }.freeze
22
+
23
+ options = { feature: nil, step: nil, expected_state_revision: nil, expected_graph_revision: nil, write: false, json: false }
24
+ command = ARGV.shift
25
+ OptionParser.new do |o|
26
+ o.on("--feature NAME") { |v| options[:feature] = v }
27
+ o.on("--step ID") { |v| options[:step] = v }
28
+ o.on("--expected-state-revision N", Integer) { |v| options[:expected_state_revision] = v }
29
+ o.on("--expected-graph-revision N", Integer) { |v| options[:expected_graph_revision] = v }
30
+ o.on("--write") { options[:write] = true }
31
+ o.on("--json") { options[:json] = true }
32
+ end.parse!
33
+
34
+ def load_state(root)
35
+ document = File.read(File.join(root, ".planning/STATE.md"))
36
+ front = document.match(/\A---\n(.*?)\n---/m)&.[](1)
37
+ raise "STATE.md has no YAML front matter" unless front
38
+ YAML.safe_load(front, permitted_classes: [], aliases: false)
39
+ end
40
+
41
+ def interpolate(value, state)
42
+ value.gsub("{feature}", state.fetch("current_feature").to_s).gsub("{phase}", state.fetch("current_phase").to_s)
43
+ end
44
+
45
+ root = Dir.pwd
46
+ lock = %w[create inspect].include?(command) ? DevflowControlIO.acquire_project_lock(root) : nil
47
+ begin
48
+ raise ArgumentError, "command must be create or inspect" unless %w[create inspect].include?(command)
49
+ raise ArgumentError, "invalid feature" unless RequirementGraph.valid_feature?(options[:feature])
50
+ state = load_state(root)
51
+ raise ArgumentError, "state feature/step mismatch" unless state["current_feature"] == options[:feature] && state["current_step"] == options[:step]
52
+ workflow_relative = state.fetch("workflow_config")
53
+ workflow_path, = DevflowControlIO.resolve_control_path(root, workflow_relative, :workflow)
54
+ workflow_raw = File.read(workflow_path)
55
+ workflow = YAML.safe_load(workflow_raw, permitted_classes: [], aliases: false)
56
+ step = Array(workflow["steps"]).find { |item| item["id"] == options[:step] }
57
+ raise ArgumentError, "unknown workflow step" unless step
58
+ graph, = RequirementGraph.load_graph(root, options[:feature])
59
+ if command == "create"
60
+ raise ArgumentError, "stale state revision: expected #{options[:expected_state_revision]}, actual #{state["revision"]}" if options[:expected_state_revision] && state["revision"] != options[:expected_state_revision]
61
+ raise ArgumentError, "stale graph revision: expected #{options[:expected_graph_revision]}, actual #{graph["revision"]}" if options[:expected_graph_revision] && graph["revision"] != options[:expected_graph_revision]
62
+ end
63
+ policy = POLICY.fetch(options[:step], { "checkers" => [], "agents" => [], "budget" => 0 })
64
+ paths = (Array(step["outputs"]).map { |output| output["path"] } + [step.dig("meta_output", "path")]).compact.map { |value| interpolate(value, state) }.uniq
65
+ contract = {
66
+ "schema_version" => 2, "origin" => "core_execution_contract", "feature" => options[:feature], "step" => options[:step],
67
+ "state_revision" => state["revision"], "graph_revision" => graph["revision"], "workflow_path" => workflow_relative,
68
+ "workflow_fingerprint" => Digest::SHA256.hexdigest(workflow_raw), "manifest_id" => "devflow-core-execution-policy-v2",
69
+ "allowed_paths" => paths, "allowed_agents" => policy["agents"], "checker_ids" => policy["checkers"],
70
+ "allowed_tools" => %w[read grep find glob lsp search write edit apply_patch bash shell exec_command task],
71
+ "tool_budget" => policy["budget"], "acceptance" => Array(graph["nodes"]).select { |node| node["kind"] == "acceptance" }.map { |node| node["id"] },
72
+ "allowed_graph_delta" => Array(policy["allowed_graph_delta"]),
73
+ "on_unknown" => "block_and_escalate"
74
+ }
75
+ frozen = contract.merge("frozen_hash" => Digest::SHA256.hexdigest(JSON.generate(contract)))
76
+ contract_id = "EC-#{frozen["frozen_hash"][0, 16]}"
77
+ frozen["contract_id"] = contract_id
78
+ if command == "inspect"
79
+ result = { "ok" => true, "contract" => frozen }
80
+ else
81
+ raise ArgumentError, "--write is required" unless options[:write]
82
+ destination = File.join(root, ".planning/contracts", options[:feature], "#{contract_id}.json")
83
+ DevflowControlIO.atomic_write(destination, JSON.pretty_generate(frozen) + "\n")
84
+ audit = File.join(root, ".planning/contracts", options[:feature], "control-events.jsonl")
85
+ existing = File.exist?(audit) ? File.read(audit) : ""
86
+ DevflowControlIO.atomic_write(audit, existing + JSON.generate({ "event" => "execution_contract_frozen", "contract_id" => contract_id, "state_revision" => state["revision"], "graph_revision" => graph["revision"], "frozen_hash" => frozen["frozen_hash"] }) + "\n")
87
+ result = { "ok" => true, "path" => destination.sub("#{root}/", ""), "contract" => frozen }
88
+ end
89
+ rescue StandardError => error
90
+ result = { "ok" => false, "reason" => "execution_contract_failed", "message" => error.message }
91
+ ensure
92
+ DevflowControlIO.release_lock(lock)
93
+ end
94
+ puts JSON.pretty_generate(result)
@@ -94,7 +94,8 @@ pending_steps = if File.exist?(workflow_path)
94
94
  current_step = options[:step] || pending_steps.first || "requirement-triage"
95
95
 
96
96
  state = {
97
- "state_schema_version" => 1,
97
+ "state_schema_version" => 2,
98
+ "governance_started_revision" => 0,
98
99
  "revision" => 0,
99
100
  "state_updated_at" => Date.today.to_s,
100
101
  "current_flow" => options[:flow],
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Evidence Pack v2 is deliberately assembled from the current STATE/workflow
5
+ # and canonical graph. Agent text is never used to decide completion.
6
+ require "digest"
7
+ require "json"
8
+ require "optparse"
9
+ require "yaml"
10
+ require_relative "control_io"
11
+ require_relative "requirement_graph"
12
+
13
+ options = { feature: nil, step: nil, contract: nil, write: false, json: false }
14
+ OptionParser.new do |o|
15
+ o.on("--feature NAME") { |v| options[:feature] = v }
16
+ o.on("--step ID") { |v| options[:step] = v }
17
+ o.on("--contract PATH") { |v| options[:contract] = v }
18
+ o.on("--write") { options[:write] = true }
19
+ o.on("--json") { options[:json] = true }
20
+ end.parse!
21
+ abort "feature, step and --write are required" unless options[:feature] && options[:step] && options[:write]
22
+ abort "invalid feature" unless RequirementGraph.valid_feature?(options[:feature])
23
+
24
+ def state_front_matter(root)
25
+ document = File.read(File.join(root, ".planning/STATE.md"))
26
+ front = document.match(/\A---\n(.*?)\n---/m)&.[](1)
27
+ raise "STATE.md has no YAML front matter" unless front
28
+ YAML.safe_load(front, permitted_classes: [], aliases: false)
29
+ end
30
+
31
+ def interpolate(path, state)
32
+ path.gsub("{feature}", state.fetch("current_feature", "").to_s).gsub("{phase}", state.fetch("current_phase", "").to_s)
33
+ end
34
+
35
+ def configured_checkers(workflow, step_id)
36
+ explicit = workflow.dig("governance", "step_checkers", step_id)
37
+ return Array(explicit) if explicit
38
+ case step_id
39
+ when "requirement-baseline" then %w[requirement-graph-check slice-independence-check acceptance-coverage-check change-impact-check]
40
+ when "api-contract" then %w[requirement-graph-check contract-coverage-check change-impact-check]
41
+ when "architecture-feasibility" then %w[requirement-graph-check change-impact-check]
42
+ when "integration-verify", "change-plan" then %w[requirement-graph-check slice-independence-check contract-coverage-check change-impact-check]
43
+ when "verify" then %w[acceptance-coverage-check change-impact-check]
44
+ else []
45
+ end
46
+ end
47
+
48
+ root = Dir.pwd
49
+ lock = DevflowControlIO.acquire_project_lock(root)
50
+ begin
51
+ state = state_front_matter(root)
52
+ unless state["current_feature"] == options[:feature] && state["current_step"] == options[:step]
53
+ result = { "ok" => false, "reason" => "state_step_mismatch", "state_feature" => state["current_feature"], "state_step" => state["current_step"] }
54
+ puts JSON.pretty_generate(result)
55
+ exit 0
56
+ end
57
+ workflow_path, = DevflowControlIO.resolve_control_path(root, state.fetch("workflow_config"), :workflow)
58
+ workflow_raw = File.read(workflow_path)
59
+ workflow = YAML.safe_load(workflow_raw, permitted_classes: [], aliases: false)
60
+ step = Array(workflow["steps"]).find { |item| item["id"] == options[:step] }
61
+ raise "unknown workflow step" unless step
62
+
63
+ output_specs = Array(step["outputs"]).select { |item| item["required"] }
64
+ output_specs << step["meta_output"] if step["meta_output"].is_a?(Hash)
65
+ files = output_specs.map do |spec|
66
+ path = spec["path"]
67
+ next unless path
68
+ relative = interpolate(path, state)
69
+ absolute, = DevflowControlIO.resolve_control_path(root, relative, :artifact)
70
+ exists = File.file?(absolute)
71
+ { "path" => relative, "required" => true, "exists" => exists, "bytes" => exists ? File.size(absolute) : 0,
72
+ "sha256" => exists ? Digest::SHA256.file(absolute).hexdigest : nil }
73
+ end.compact
74
+
75
+ graph, = RequirementGraph.load_graph(root, options[:feature])
76
+ checkers = RequirementGraph.checker_results(graph)
77
+ selected = configured_checkers(workflow, options[:step])
78
+ gate = workflow.dig("governance", "requirement_graph_gate").to_s
79
+ enforced = gate == "enforced"
80
+ checker_results = selected.map do |id|
81
+ issues = checkers.fetch(id, ["unknown fixed checker: #{id}"])
82
+ { "id" => id, "origin" => "core_fixed_checker", "exit_code" => issues.empty? ? 0 : 1, "passed" => issues.empty?, "issues" => issues,
83
+ "counters" => { "issue_count" => issues.length } }
84
+ end
85
+ graph_missing = graph["revision"].zero? && !File.exist?(RequirementGraph.graph_path(root, options[:feature]).first)
86
+ checker_passed = !enforced || (!graph_missing && checker_results.all? { |item| item["passed"] })
87
+ contract_hash = nil
88
+ if options[:contract]
89
+ contract_path = File.expand_path(options[:contract], root)
90
+ raise "contract path escapes project" unless contract_path.start_with?("#{File.realpath(root)}#{File::SEPARATOR}") && File.file?(contract_path)
91
+ contract_hash = Digest::SHA256.file(contract_path).hexdigest
92
+ end
93
+ node_counts = Array(graph["nodes"]).group_by { |node| node["kind"] }.transform_values(&:length)
94
+ pack = {
95
+ "schema_version" => 2, "origin" => "core_machine", "feature" => options[:feature], "step" => options[:step],
96
+ "state_revision" => state["revision"], "graph_revision" => graph["revision"], "workflow_fingerprint" => Digest::SHA256.hexdigest(workflow_raw),
97
+ "contract_hash" => contract_hash, "files" => files, "checkers" => checker_results,
98
+ "counters" => { "required_file_count" => files.length, "existing_file_count" => files.count { |item| item["exists"] }, "nodes_by_kind" => node_counts,
99
+ "stale_node_count" => Array(graph["stale"]).length, "stale_view_count" => Hash(graph["views"]).count { |_name, view| view["needs_recheck"] } },
100
+ "view_fingerprints" => Hash(graph["views"]).transform_values { |view| view.select { |key, _value| %w[fingerprint graph_revision state needs_recheck stale_fingerprint].include?(key) } },
101
+ "complete" => files.all? { |item| item["exists"] } && checker_passed,
102
+ "gate_mode" => gate.empty? ? "observe" : gate
103
+ }.compact
104
+ destination = File.join(root, ".planning/evidence", options[:feature], "#{options[:step]}.machine.json")
105
+ DevflowControlIO.atomic_write(destination, JSON.pretty_generate(pack) + "\n")
106
+ result = { "ok" => true, "path" => destination.sub("#{root}/", ""), "complete" => pack["complete"], "state_revision" => pack["state_revision"], "graph_revision" => pack["graph_revision"], "checkers" => checker_results }
107
+ rescue StandardError => error
108
+ result = { "ok" => false, "reason" => "machine_evidence_failed", "message" => error.message }
109
+ ensure
110
+ DevflowControlIO.release_lock(lock)
111
+ end
112
+ puts JSON.pretty_generate(result)