@guanlin4924/devflow-pi 0.1.95 → 0.1.97

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 (49) hide show
  1. package/README.md +42 -2
  2. package/package.json +2 -1
  3. package/plugins/devflow-core/extensions/devflow-core.ts +679 -91
  4. package/plugins/devflow-core/runtime/hooks/hook_common.py +245 -0
  5. package/plugins/devflow-core/runtime/hooks/index_gate.py +18 -0
  6. package/plugins/devflow-core/skills/wireframe-render/SKILL.md +1 -1
  7. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +98 -2
  8. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +6 -1
  9. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +64 -8
  10. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_coverage.py +3 -0
  11. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_draft.py +53 -0
  12. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +52 -2
  13. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +2 -0
  14. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/explore_preflight.py +58 -0
  15. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +1 -0
  16. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/locator_map.py +156 -0
  17. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +1 -0
  18. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/playwright_adapter.py +1 -1
  19. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +50 -15
  20. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +60 -0
  21. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/startup_preflight.py +167 -0
  22. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +5 -0
  23. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_render.py +262 -56
  24. package/plugins/devflow-core/skills/workflow-orchestrator/templates/regression-acceptance.yaml +126 -0
  25. package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +2 -0
  26. package/plugins/devflow-intel/extensions/devflow-intel.ts +1 -1
  27. package/plugins/devflow-quality/runtime/playwright/devflow-test-dsl-v1.schema.json +103 -88
  28. package/plugins/devflow-quality/runtime/playwright/explore-host.mjs +879 -0
  29. package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +297 -15
  30. package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +3 -2
  31. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +4 -0
  32. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +10 -2
  33. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +6 -2
  34. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/project-dsl-contract.md +3 -1
  35. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/ui-mainflow-rules.md +4 -2
  36. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +98 -15
  37. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/locator_report.py +73 -0
  38. package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +3 -1
  39. package/plugins/devflow-quality/skills/workflow-regression-test-design/SKILL.md +69 -0
  40. package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +1 -1
  41. package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +4 -2
  42. package/src/cli.ts +10 -0
  43. package/src/constants.ts +1 -1
  44. package/src/doctor.ts +9 -0
  45. package/src/mcp/auth.ts +103 -0
  46. package/src/mcp/core.ts +102 -0
  47. package/src/mcp/server.ts +193 -0
  48. package/src/mcp/types.ts +5 -0
  49. package/src/startup-briefing.ts +109 -0
@@ -25,6 +25,7 @@ parser = OptionParser.new do |opts|
25
25
  opts.on("--source-session-id ID", "Source OMP session id") { |v| options[:source_session_id] = v }
26
26
  opts.on("--target-session-id ID", "Target OMP session id") { |v| options[:target_session_id] = v }
27
27
  opts.on("--reason TEXT", "Auditable rollover reason") { |v| options[:reason] = v }
28
+ opts.on("--carry-file PATH", "JSON carry text supplied by the Runtime") { |v| options[:carry_file] = v }
28
29
  opts.on("--lease-seconds N", Integer, "Claim lease duration") { |v| options[:lease_seconds] = v }
29
30
  end
30
31
  action = ARGV.shift
@@ -68,8 +69,64 @@ def lease_expiry(seconds)
68
69
  [[seconds.to_i, 5].max, 3600].min.then { |duration| (Time.now.utc + duration).iso8601(3) }
69
70
  end
70
71
 
72
+ def truncate_text(value, limit)
73
+ return "" unless value.is_a?(String)
74
+ return value if value.length <= limit
75
+
76
+ value[0, limit - 1] + "…"
77
+ end
78
+
79
+ def load_carry(path)
80
+ parsed = JSON.parse(File.read(path))
81
+ raise JSON::ParserError, "carry must be a JSON object" unless parsed.is_a?(Hash)
82
+
83
+ notes = parsed["user_notes"].is_a?(Array) ? parsed["user_notes"].select { |item| item.is_a?(String) } : []
84
+ {
85
+ "user_notes" => notes.last(10).map { |item| truncate_text(item, 500) },
86
+ "agent_report" => truncate_text(parsed["agent_report"], 4000)
87
+ }
88
+ end
89
+
90
+ def previous_step_payload(root, state)
91
+ step_id = state["completed_steps"].is_a?(Array) ? state["completed_steps"].last : nil
92
+ return nil unless step_id.is_a?(String) && !step_id.empty?
93
+
94
+ workflow_path, = DevflowControlIO.resolve_control_path(root, state["workflow_config"], :workflow)
95
+ workflow = YAML.safe_load(File.read(workflow_path), permitted_classes: [], aliases: false)
96
+ step = workflow.fetch("steps", []).find { |item| item.is_a?(Hash) && item["id"] == step_id }
97
+ return nil unless step
98
+
99
+ render = lambda do |value|
100
+ value.gsub("{feature}", state["current_feature"].to_s).gsub("{phase}", state["current_phase"].to_s)
101
+ end
102
+ paths = step.fetch("outputs", []).each_with_object([]) do |item, result|
103
+ result << item["path"] if item.is_a?(Hash) && item["required"] == true && item["path"].is_a?(String)
104
+ end
105
+ meta = step["meta_output"]
106
+ paths << meta["path"] if meta.is_a?(Hash) && meta["path"].is_a?(String)
107
+ outputs = paths.each_with_object([]) do |raw_path, result|
108
+ relative = render.call(raw_path)
109
+ absolute = File.expand_path(relative, root)
110
+ next unless absolute.start_with?("#{File.realpath(root)}#{File::SEPARATOR}") && File.exist?(absolute) && File.lstat(absolute).file?
111
+
112
+ result << { "path" => relative, "sha256" => Digest::SHA256.hexdigest(File.binread(absolute)) }
113
+ end
114
+ { "id" => step_id, "label" => step["label"].to_s, "outputs" => outputs }
115
+ rescue StandardError
116
+ nil
117
+ end
118
+
71
119
  root = Dir.pwd
72
120
  state_path, = DevflowControlIO.resolve_control_path(root, ".devflow/planning/STATE.md", :state)
121
+ carry = nil
122
+ if options[:carry_file]
123
+ begin
124
+ carry = load_carry(options[:carry_file])
125
+ rescue JSON::ParserError, SystemCallError
126
+ puts JSON.pretty_generate(fail_result("invalid_carry"))
127
+ exit 1
128
+ end
129
+ end
73
130
  result = catch(:transition_done) do
74
131
  lock = DevflowControlIO.acquire_project_lock(root)
75
132
  begin
@@ -149,6 +206,9 @@ result = catch(:transition_done) do
149
206
  "artifact_registry" => ".devflow/planning/artifacts/index.yaml"
150
207
  }
151
208
  }
209
+ previous_step = previous_step_payload(root, state)
210
+ payload["previous_step"] = previous_step if previous_step
211
+ payload["carry"] = carry if options[:carry_file]
152
212
  serialized = JSON.generate(payload) + "\n"
153
213
  digest = Digest::SHA256.hexdigest(serialized)
154
214
  if options[:write]
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env python3
2
+ """Read-only startup preflight shared by every workflow execution entry path."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ import review_preflight
11
+
12
+
13
+ SCOPE = "startup_preflight_only_not_gate_approval"
14
+
15
+
16
+ def render_path(value: str, feature: str, phase: str) -> str:
17
+ return value.replace("{feature}", feature).replace("{phase}", phase)
18
+
19
+
20
+ def read_handoff(root: Path, relative: str, evidence: list[dict], issues: list[dict]) -> dict:
21
+ try:
22
+ handoff_path = review_preflight.within(root, relative)
23
+ payload = json.loads(handoff_path.read_text(encoding="utf-8"))
24
+ if not isinstance(payload, dict):
25
+ raise ValueError("handoff must be a JSON object")
26
+ except (OSError, ValueError, TypeError, json.JSONDecodeError) as error:
27
+ issues.append({"code": "HANDOFF_UNAVAILABLE", "detail": str(error), "path": relative})
28
+ return {}
29
+
30
+ previous = payload.get("previous_step")
31
+ if isinstance(previous, dict):
32
+ outputs = previous.get("outputs", [])
33
+ if isinstance(outputs, list):
34
+ for item in outputs:
35
+ if not isinstance(item, dict) or not isinstance(item.get("path"), str):
36
+ continue
37
+ output_relative = item["path"]
38
+ try:
39
+ output_path = review_preflight.within(root, output_relative)
40
+ except ValueError:
41
+ issues.append({"code": "PREVIOUS_OUTPUT_MISSING", "path": output_relative})
42
+ continue
43
+ if not output_path.is_file():
44
+ issues.append({"code": "PREVIOUS_OUTPUT_MISSING", "path": output_relative})
45
+ elif item.get("sha256") != review_preflight.sha256(output_path):
46
+ issues.append({"code": "PREVIOUS_OUTPUT_CHANGED", "path": output_relative})
47
+ else:
48
+ evidence.append({"kind": "previous_output", "path": output_relative,
49
+ "sha256": item.get("sha256")})
50
+ return {
51
+ **({"previous_step": payload["previous_step"]} if "previous_step" in payload else {}),
52
+ **({"carry": payload["carry"]} if "carry" in payload else {}),
53
+ }
54
+
55
+
56
+ def startup_preflight(root: Path, feature: str, step: str,
57
+ contract: str | None = None, handoff: str | None = None) -> dict:
58
+ root = root.resolve()
59
+ evidence: list[dict] = []
60
+ issues: list[dict] = []
61
+ handoff_result: dict = {}
62
+ contract_status = "ready"
63
+ facts: dict = {"feature": feature}
64
+ try:
65
+ if handoff:
66
+ handoff_result = read_handoff(root, handoff, evidence, issues)
67
+
68
+ if contract:
69
+ contract_path = review_preflight.within(root, contract)
70
+ frozen = json.loads(contract_path.read_text(encoding="utf-8"))
71
+ if not isinstance(frozen, dict):
72
+ raise ValueError("contract must be a JSON object")
73
+ contract_result = review_preflight.preflight(
74
+ root, feature, step, contract, [], None, None, "entry"
75
+ )
76
+ evidence.extend(contract_result.get("evidence", []))
77
+ issues.extend(contract_result.get("issues", []))
78
+ contract_status = contract_result.get("status", "human_confirmation")
79
+ facts["allowed_paths"] = frozen.get("allowed_paths", [])
80
+ facts["contract_id"] = frozen.get("contract_id", "")
81
+
82
+ state = review_preflight.load_state(
83
+ review_preflight.within(root, ".devflow/planning/STATE.md")
84
+ )
85
+ phase = str(state.get("current_phase", ""))
86
+ facts["phase"] = phase
87
+ workflow_relative = state.get("workflow_config")
88
+ if not isinstance(workflow_relative, str):
89
+ raise ValueError("STATE workflow_config is unavailable")
90
+ workflow = review_preflight.load_workflow(review_preflight.within(root, workflow_relative))
91
+ steps = workflow.get("steps", [])
92
+ if not isinstance(steps, list):
93
+ raise ValueError("workflow steps must be an array")
94
+ step_index = next(
95
+ (index for index, item in enumerate(steps)
96
+ if isinstance(item, dict) and item.get("id") == step),
97
+ None,
98
+ )
99
+ if step_index is None:
100
+ issues.append({"code": "STEP_UNAVAILABLE", "detail": f"workflow step {step} is unavailable"})
101
+ else:
102
+ definition = steps[step_index]
103
+ step_fact = {
104
+ "id": step,
105
+ "label": definition.get("label", step),
106
+ "skill": definition.get("skill", ""),
107
+ }
108
+ description = definition.get("description", definition.get("summary"))
109
+ if isinstance(description, str) and description:
110
+ step_fact["description"] = description
111
+ facts["step"] = step_fact
112
+ facts["progress"] = {"index": step_index + 1, "total": len(steps)}
113
+ input_facts = []
114
+ for raw in definition.get("inputs", []):
115
+ if isinstance(raw, str):
116
+ relative, required = raw, True
117
+ elif isinstance(raw, dict) and isinstance(raw.get("path"), str):
118
+ relative = raw["path"]
119
+ required = bool(raw.get("required", True) and raw.get("required_at_entry", True))
120
+ else:
121
+ continue
122
+ rendered = render_path(relative, feature, phase)
123
+ try:
124
+ exists = review_preflight.within(root, rendered).is_file()
125
+ except ValueError:
126
+ exists = False
127
+ input_facts.append({"path": rendered, "required": required, "exists": exists})
128
+ facts["inputs"] = input_facts
129
+ facts["outputs"] = [
130
+ render_path(item["path"], feature, phase)
131
+ for item in definition.get("outputs", [])
132
+ if isinstance(item, dict) and item.get("required") is True
133
+ and isinstance(item.get("path"), str)
134
+ ]
135
+ except Exception as error:
136
+ issues.append({"code": "STARTUP_PREFLIGHT_INPUT_UNAVAILABLE", "detail": str(error)})
137
+
138
+ status = (
139
+ "core_contract_conflict" if contract_status == "core_contract_conflict"
140
+ else "human_confirmation" if issues
141
+ else "ready"
142
+ )
143
+ return {
144
+ "status": status,
145
+ "scope": SCOPE,
146
+ "facts": facts,
147
+ "handoff": handoff_result,
148
+ "evidence": evidence,
149
+ "issues": issues,
150
+ }
151
+
152
+
153
+ def main() -> int:
154
+ parser = argparse.ArgumentParser(description=__doc__)
155
+ parser.add_argument("--root", type=Path, default=Path.cwd())
156
+ parser.add_argument("--feature", required=True)
157
+ parser.add_argument("--step", required=True)
158
+ parser.add_argument("--contract")
159
+ parser.add_argument("--handoff")
160
+ args = parser.parse_args()
161
+ result = startup_preflight(args.root, args.feature, args.step, args.contract, args.handoff)
162
+ print(json.dumps(result, ensure_ascii=False, sort_keys=True))
163
+ return 0 if result["status"] == "ready" else 2
164
+
165
+
166
+ if __name__ == "__main__":
167
+ sys.exit(main())
@@ -55,6 +55,11 @@ module WorkflowStateValidator
55
55
  runtime_integrity_error
56
56
  acceptance_tests_not_passed
57
57
  acceptance_tests_empty
58
+ acceptance_test_suite_freeze_failed
59
+ acceptance_test_suite_freeze_attestation_invalid
60
+ acceptance_test_suite_freeze_attestation_failed
61
+ acceptance_test_suite_candidate_binding_failed
62
+ acceptance_test_suite_freeze_recovery_failed
58
63
  ].freeze
59
64
 
60
65
  def self.hard_validation_code?(code)