@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
package/plugins/devflow-core/skills/workflow-orchestrator/scripts/runtime_context_collector.rb
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Repository-declared runtime context collector (P0).
|
|
5
|
+
# It reads manifest content from one immutable Git revision rather than the
|
|
6
|
+
# working tree, so every inventory entry and every absence claim is replayable.
|
|
7
|
+
require "digest"
|
|
8
|
+
require "json"
|
|
9
|
+
require "open3"
|
|
10
|
+
require "optparse"
|
|
11
|
+
require "rexml/document"
|
|
12
|
+
require "rexml/xpath"
|
|
13
|
+
require_relative "control_io"
|
|
14
|
+
|
|
15
|
+
module RuntimeContextCollector
|
|
16
|
+
SCHEMA_VERSION = 1
|
|
17
|
+
HISTORY_DIR = File.join(".context", "runtime-context", "revisions")
|
|
18
|
+
POINTER = File.join(".context", "runtime-context.json")
|
|
19
|
+
EXTRACTORS = {
|
|
20
|
+
"package.json" => :extract_package_json,
|
|
21
|
+
"requirements.txt" => :extract_requirements_txt,
|
|
22
|
+
"pom.xml" => :extract_pom_xml
|
|
23
|
+
}.freeze
|
|
24
|
+
EXCLUDED_ROOTS = %w[node_modules vendor dist build .git].freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def safe_version(value)
|
|
29
|
+
text = value.to_s.strip
|
|
30
|
+
# A dependency version is useful evidence, but a direct URL can carry an
|
|
31
|
+
# access token. P0 does not consume versions, so redact unsafe forms.
|
|
32
|
+
return nil if text.empty? || text.match?(%r{[:/@]})
|
|
33
|
+
|
|
34
|
+
text
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def extract_package_json(content)
|
|
38
|
+
parsed = JSON.parse(content)
|
|
39
|
+
deps = parsed["dependencies"].is_a?(Hash) ? parsed["dependencies"] : {}
|
|
40
|
+
deps.map { |name, version| { "subject" => name.to_s.downcase.strip, "version" => safe_version(version) } }
|
|
41
|
+
rescue JSON::ParserError
|
|
42
|
+
raise "parse_error"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def extract_requirements_txt(content)
|
|
46
|
+
content.lines.map(&:strip).reject { |line| line.empty? || line.start_with?("#", "-e", "--", "-r", "-f") }.map do |line|
|
|
47
|
+
name = line.split(/[<>=!\[\s]/, 2).first.to_s.downcase.strip
|
|
48
|
+
version = line.match(/\A[^<>=!\[\s]+\s*(?:==|>=|<=|~=|>|<)\s*([^\s;]+)\z/)&.[](1)
|
|
49
|
+
{ "subject" => name, "version" => safe_version(version) }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def extract_pom_xml(content)
|
|
54
|
+
document = REXML::Document.new(content)
|
|
55
|
+
REXML::XPath.match(document, "//dependency").map do |dependency|
|
|
56
|
+
group = dependency.elements["groupId"]&.text.to_s.strip
|
|
57
|
+
artifact = dependency.elements["artifactId"]&.text.to_s.strip
|
|
58
|
+
next if group.empty? || artifact.empty?
|
|
59
|
+
|
|
60
|
+
{ "subject" => "#{group}:#{artifact}".downcase, "version" => safe_version(dependency.elements["version"]&.text) }
|
|
61
|
+
end.compact
|
|
62
|
+
rescue REXML::ParseException
|
|
63
|
+
raise "parse_error"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def git_capture(root, *args)
|
|
67
|
+
stdout, stderr, status = Open3.capture3("git", "-C", root, *args)
|
|
68
|
+
[stdout, stderr, status.success?]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def git_revision(root)
|
|
72
|
+
output, _error, success = git_capture(root, "rev-parse", "HEAD")
|
|
73
|
+
success ? output.strip : ""
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def excluded_path?(path)
|
|
77
|
+
path.split("/").any? { |segment| EXCLUDED_ROOTS.include?(segment) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def git_file_list(root, revision)
|
|
81
|
+
output, error, success = git_capture(root, "ls-tree", "-r", "--name-only", revision)
|
|
82
|
+
raise "git_tree_unavailable: #{error.strip}" unless success
|
|
83
|
+
|
|
84
|
+
supported = output.lines.map(&:strip).select { |path| EXTRACTORS.key?(File.basename(path)) }.sort
|
|
85
|
+
included, excluded = supported.partition { |path| !excluded_path?(path) }
|
|
86
|
+
[included, excluded]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def git_file_content(root, revision, path)
|
|
90
|
+
output, error, success = git_capture(root, "show", "#{revision}:#{path}")
|
|
91
|
+
raise "git_show_failed: #{error.strip}" unless success
|
|
92
|
+
|
|
93
|
+
output
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Enumerates every supported manifest in the committed tree. Files not
|
|
97
|
+
# matching the explicit P0 allowlist are outside the declared collector
|
|
98
|
+
# scope; every matching file has a scanned or failed entry.
|
|
99
|
+
def discover_and_extract(root, revision)
|
|
100
|
+
files = []
|
|
101
|
+
inventory = {}
|
|
102
|
+
paths, excluded_paths = git_file_list(root, revision)
|
|
103
|
+
excluded_paths.each do |path|
|
|
104
|
+
files << { "path" => path, "status" => "excluded", "reason" => "third_party_or_build_output" }
|
|
105
|
+
end
|
|
106
|
+
paths.each do |path|
|
|
107
|
+
extractor = EXTRACTORS.fetch(File.basename(path))
|
|
108
|
+
begin
|
|
109
|
+
content = git_file_content(root, revision, path)
|
|
110
|
+
entries = send(extractor, content)
|
|
111
|
+
evidence = {
|
|
112
|
+
"path" => path,
|
|
113
|
+
"git_revision" => revision,
|
|
114
|
+
"sha256" => Digest::SHA256.hexdigest(content),
|
|
115
|
+
"extractor" => extractor.to_s
|
|
116
|
+
}
|
|
117
|
+
entries.each do |entry|
|
|
118
|
+
subject = entry["subject"].to_s.downcase.strip
|
|
119
|
+
next if subject.empty?
|
|
120
|
+
|
|
121
|
+
inventory[subject] ||= {
|
|
122
|
+
"kind" => "runtime_dep", "subject" => subject,
|
|
123
|
+
"version" => entry["version"], "evidence" => evidence
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
files << { "path" => path, "status" => "scanned" }
|
|
127
|
+
rescue StandardError => error
|
|
128
|
+
files << { "path" => path, "status" => "failed", "error" => error.message }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
[files, inventory.values]
|
|
132
|
+
rescue StandardError => error
|
|
133
|
+
[[{ "path" => "<repository>", "status" => "failed", "error" => error.message }], []]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def current_revision(root)
|
|
137
|
+
pointer = File.join(root, POINTER)
|
|
138
|
+
return 0 unless File.exist?(pointer)
|
|
139
|
+
|
|
140
|
+
JSON.parse(File.read(pointer))["current_revision"].to_i
|
|
141
|
+
rescue JSON::ParserError
|
|
142
|
+
raise ArgumentError, "runtime-context pointer is invalid JSON: #{pointer}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def collect(root)
|
|
146
|
+
repository_revision = git_revision(root)
|
|
147
|
+
files, inventory =
|
|
148
|
+
if repository_revision.empty?
|
|
149
|
+
[[{ "path" => "<repository>", "status" => "failed", "error" => "git_revision_unavailable" }], []]
|
|
150
|
+
else
|
|
151
|
+
discover_and_extract(root, repository_revision)
|
|
152
|
+
end
|
|
153
|
+
coverage_status = files.any? { |file| file["status"] == "failed" } ? "incomplete" : "complete"
|
|
154
|
+
revision = current_revision(root) + 1
|
|
155
|
+
snapshot = {
|
|
156
|
+
"schema_version" => SCHEMA_VERSION,
|
|
157
|
+
"context_id" => "repo-default",
|
|
158
|
+
"snapshot_revision" => revision,
|
|
159
|
+
"scope" => "repository-default:package.json|requirements.txt|pom.xml",
|
|
160
|
+
"repository_revision" => repository_revision,
|
|
161
|
+
"discovery_hash" => Digest::SHA256.hexdigest(JSON.generate(files)),
|
|
162
|
+
"collector" => {
|
|
163
|
+
"version" => "git-manifest-allowlist-v1",
|
|
164
|
+
"discovery" => { "rules" => EXTRACTORS.keys.sort, "files" => files }
|
|
165
|
+
},
|
|
166
|
+
"inventory_hash" => Digest::SHA256.hexdigest(JSON.generate(inventory)),
|
|
167
|
+
"coverage_status" => coverage_status,
|
|
168
|
+
"inventory" => inventory
|
|
169
|
+
}
|
|
170
|
+
history = File.join(root, HISTORY_DIR, "#{revision}.json")
|
|
171
|
+
DevflowControlIO.atomic_create(history, JSON.pretty_generate(snapshot) + "\n")
|
|
172
|
+
DevflowControlIO.atomic_write(File.join(root, POINTER), JSON.pretty_generate({ "current_revision" => revision, "path" => history.sub("#{root}/", "") }) + "\n")
|
|
173
|
+
DevflowControlIO.append_audit(File.join(root, ".context", "runtime-context", "audit.jsonl"), {
|
|
174
|
+
"event" => "snapshot_collected", "snapshot_revision" => revision,
|
|
175
|
+
"repository_revision" => repository_revision, "coverage_status" => coverage_status
|
|
176
|
+
})
|
|
177
|
+
DevflowControlIO.register_control_artifact(root, {
|
|
178
|
+
"path" => history.sub("#{root}/", ""), "type" => "runtime_context_snapshot",
|
|
179
|
+
"produced_by" => "core", "snapshot_revision" => revision,
|
|
180
|
+
"coverage_status" => coverage_status, "repository_revision" => repository_revision
|
|
181
|
+
})
|
|
182
|
+
snapshot
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if __FILE__ == $PROGRAM_NAME
|
|
187
|
+
options = { json: false }
|
|
188
|
+
command = ARGV.shift
|
|
189
|
+
OptionParser.new do |o|
|
|
190
|
+
o.on("--json") { options[:json] = true }
|
|
191
|
+
end.parse!
|
|
192
|
+
root = Dir.pwd
|
|
193
|
+
lock = command == "collect" ? DevflowControlIO.acquire_project_lock(root) : nil
|
|
194
|
+
begin
|
|
195
|
+
raise ArgumentError, "command must be collect or inspect" unless %w[collect inspect].include?(command)
|
|
196
|
+
result =
|
|
197
|
+
if command == "collect"
|
|
198
|
+
snapshot = RuntimeContextCollector.collect(root)
|
|
199
|
+
{ "ok" => true, "snapshot_revision" => snapshot["snapshot_revision"], "coverage_status" => snapshot["coverage_status"], "inventory_count" => snapshot["inventory"].length, "repository_revision" => snapshot["repository_revision"] }
|
|
200
|
+
else
|
|
201
|
+
pointer = File.join(root, RuntimeContextCollector::POINTER)
|
|
202
|
+
{ "ok" => true, "current_revision" => RuntimeContextCollector.current_revision(root), "pointer_exists" => File.exist?(pointer) }
|
|
203
|
+
end
|
|
204
|
+
rescue StandardError => error
|
|
205
|
+
result = { "ok" => false, "reason" => "runtime_context_collector_failed", "message" => error.message }
|
|
206
|
+
ensure
|
|
207
|
+
DevflowControlIO.release_lock(lock)
|
|
208
|
+
end
|
|
209
|
+
puts JSON.pretty_generate(result)
|
|
210
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Field-level staleness check (P1). A downstream consumer pins the exact
|
|
5
|
+
# evidence paths it consumed plus the snapshot revision it read; this checker
|
|
6
|
+
# compares evidence sha256 across revisions and reports only the changed
|
|
7
|
+
# paths — not a whole-snapshot invalidation.
|
|
8
|
+
require "json"
|
|
9
|
+
require "optparse"
|
|
10
|
+
require_relative "runtime_context_collector"
|
|
11
|
+
|
|
12
|
+
module StaleCheck
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def evidence_map(snapshot)
|
|
16
|
+
map = {}
|
|
17
|
+
Array(snapshot["inventory"]).each do |item|
|
|
18
|
+
evidence = item["evidence"]
|
|
19
|
+
next unless evidence.is_a?(Hash) && evidence["path"]
|
|
20
|
+
map[evidence["path"]] = evidence["sha256"]
|
|
21
|
+
end
|
|
22
|
+
map
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def changed_paths(old_snapshot, new_snapshot, evidence_paths)
|
|
26
|
+
old_map = evidence_map(old_snapshot)
|
|
27
|
+
new_map = evidence_map(new_snapshot)
|
|
28
|
+
Array(evidence_paths).select { |path| old_map[path] != new_map[path] }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if __FILE__ == $PROGRAM_NAME
|
|
33
|
+
options = { consumed_revision: nil, evidence_paths: nil }
|
|
34
|
+
command = ARGV.shift
|
|
35
|
+
OptionParser.new do |o|
|
|
36
|
+
o.on("--consumed-revision N", Integer) { |v| options[:consumed_revision] = v }
|
|
37
|
+
o.on("--evidence-paths JSON") { |v| options[:evidence_paths] = v }
|
|
38
|
+
o.on("--json") { options[:json] = true }
|
|
39
|
+
end.parse!
|
|
40
|
+
root = Dir.pwd
|
|
41
|
+
begin
|
|
42
|
+
raise ArgumentError, "command must be check" unless command == "check"
|
|
43
|
+
raise ArgumentError, "--consumed-revision is required" if options[:consumed_revision].nil?
|
|
44
|
+
current = RuntimeContextCollector.current_revision(root)
|
|
45
|
+
old_path = File.join(root, RuntimeContextCollector::HISTORY_DIR, "#{options[:consumed_revision]}.json")
|
|
46
|
+
new_path = File.join(root, RuntimeContextCollector::HISTORY_DIR, "#{current}.json")
|
|
47
|
+
raise ArgumentError, "consumed snapshot not found" unless File.exist?(old_path)
|
|
48
|
+
old = JSON.parse(File.read(old_path))
|
|
49
|
+
new = JSON.parse(File.read(new_path))
|
|
50
|
+
paths = JSON.parse(options[:evidence_paths].to_s)
|
|
51
|
+
changed = StaleCheck.changed_paths(old, new, paths)
|
|
52
|
+
{ "ok" => true, "consumed_revision" => options[:consumed_revision], "current_revision" => current, "changed_paths" => changed }
|
|
53
|
+
rescue StandardError => error
|
|
54
|
+
{ "ok" => false, "reason" => "stale_check_failed", "message" => error.message }
|
|
55
|
+
end.tap { |r| puts JSON.pretty_generate(r) }
|
|
56
|
+
end
|