@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,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