@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.
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +425 -54
- package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +3 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_assessment.rb +458 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_brief.rb +256 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_constraints.rb +155 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_feasibility.rb +176 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/control_io.rb +46 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +94 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +2 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +112 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +646 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/runtime_context_collector.rb +210 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/stale_check.rb +56 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/static_runtime_context_collector.rb +518 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +140 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workload_constraint.rb +57 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +88 -2
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +3 -2
- package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +17 -1
- package/plugins/devflow-design/skills/workflow-architecture-feasibility/SKILL.md +37 -0
- package/plugins/devflow-design/skills/workflow-architecture-feasibility/agents/openai.yaml +4 -0
- package/plugins/devflow-design/skills/workflow-backend-tech-spec/SKILL.md +7 -2
- package/plugins/devflow-design/skills/workflow-frontend-tech-spec/SKILL.md +6 -1
- package/plugins/devflow-requirements/skills/requirement-feasibility-review/SKILL.md +4 -0
- package/plugins/devflow-requirements/skills/workflow-domain-modeling/SKILL.md +4 -0
- package/plugins/devflow-requirements/skills/workflow-requirement-baseline/SKILL.md +4 -0
- package/plugins/devflow-requirements/skills/workflow-requirement-triage/SKILL.md +4 -0
- package/src/cli.ts +2 -1
- 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" =>
|
|
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)
|