@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.
- package/README.md +42 -2
- package/package.json +2 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +679 -91
- package/plugins/devflow-core/runtime/hooks/hook_common.py +245 -0
- package/plugins/devflow-core/runtime/hooks/index_gate.py +18 -0
- package/plugins/devflow-core/skills/wireframe-render/SKILL.md +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +98 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +6 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +64 -8
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_coverage.py +3 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_draft.py +53 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +52 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +2 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/explore_preflight.py +58 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +1 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/locator_map.py +156 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +1 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/playwright_adapter.py +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +50 -15
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +60 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/startup_preflight.py +167 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +5 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_render.py +262 -56
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/regression-acceptance.yaml +126 -0
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +2 -0
- package/plugins/devflow-intel/extensions/devflow-intel.ts +1 -1
- package/plugins/devflow-quality/runtime/playwright/devflow-test-dsl-v1.schema.json +103 -88
- package/plugins/devflow-quality/runtime/playwright/explore-host.mjs +879 -0
- package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +297 -15
- package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +3 -2
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +4 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +10 -2
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +6 -2
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/project-dsl-contract.md +3 -1
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/ui-mainflow-rules.md +4 -2
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +98 -15
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/locator_report.py +73 -0
- package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +3 -1
- package/plugins/devflow-quality/skills/workflow-regression-test-design/SKILL.md +69 -0
- package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +1 -1
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +4 -2
- package/src/cli.ts +10 -0
- package/src/constants.ts +1 -1
- package/src/doctor.ts +9 -0
- package/src/mcp/auth.ts +103 -0
- package/src/mcp/core.ts +102 -0
- package/src/mcp/server.ts +193 -0
- package/src/mcp/types.ts +5 -0
- 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)
|