policyflow 0.1.0__py3-none-any.whl
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.
- policyflow/__init__.py +36 -0
- policyflow/agent_execution.py +301 -0
- policyflow/api.py +125 -0
- policyflow/assets/agents/architecture-agent.md +50 -0
- policyflow/assets/agents/planning-agent.md +46 -0
- policyflow/assets/agents/qa-agent.md +46 -0
- policyflow/assets/agents/review-agent.md +51 -0
- policyflow/assets/agents/senior-dev-agent.md +49 -0
- policyflow/assets/docs/concepts.md +30 -0
- policyflow/assets/docs/extraction-notes.md +10 -0
- policyflow/assets/docs/getting-started.md +282 -0
- policyflow/assets/docs/governance-enforcement-roadmap.md +58 -0
- policyflow/assets/docs/human-in-the-loop.md +26 -0
- policyflow/assets/docs/overview.md +19 -0
- policyflow/assets/docs/project-context-contract.md +42 -0
- policyflow/assets/docs/public-api.md +87 -0
- policyflow/assets/docs/release-and-upgrade.md +125 -0
- policyflow/assets/docs/runner-contract.md +104 -0
- policyflow/assets/docs/schema-compatibility.md +99 -0
- policyflow/assets/examples/policyflow.github-governed.yml +29 -0
- policyflow/assets/examples/policyflow.minimal.yml +7 -0
- policyflow/assets/examples/project-context.yml +26 -0
- policyflow/assets/github/ISSUE_TEMPLATE/architecture-change.yml +92 -0
- policyflow/assets/github/ISSUE_TEMPLATE/bugfix.yml +97 -0
- policyflow/assets/github/ISSUE_TEMPLATE/feature.yml +97 -0
- policyflow/assets/github/PULL_REQUEST_TEMPLATE.md +57 -0
- policyflow/assets/github/workflows/policyflow-governance.yml +84 -0
- policyflow/assets/prompts/architecture-agent.prompt.md +11 -0
- policyflow/assets/prompts/codex-feature-prompt.template.md +20 -0
- policyflow/assets/prompts/codex-review-prompt.template.md +18 -0
- policyflow/assets/prompts/planning-agent.prompt.md +11 -0
- policyflow/assets/prompts/qa-agent.prompt.md +11 -0
- policyflow/assets/prompts/review-agent.prompt.md +11 -0
- policyflow/assets/prompts/senior-dev-agent.prompt.md +11 -0
- policyflow/assets/rules/agent-handoff-contracts.md +36 -0
- policyflow/assets/rules/confidence-governance.md +25 -0
- policyflow/assets/rules/escalation-rules.md +20 -0
- policyflow/assets/rules/qa-checklist.md +10 -0
- policyflow/assets/rules/review-checklist.md +11 -0
- policyflow/assets/rules/risk-classification.md +41 -0
- policyflow/assets/rules/risk-review-matrix.md +57 -0
- policyflow/assets/workflows/templates/architecture-change-workflow.template.yml +220 -0
- policyflow/assets/workflows/templates/bugfix-workflow.template.yml +194 -0
- policyflow/assets/workflows/templates/feature-workflow.template.yml +263 -0
- policyflow/assets/workflows/templates/governance-fields.example.yml +48 -0
- policyflow/assets/workflows/templates/handoff-fields.example.yml +63 -0
- policyflow/assets/workflows/templates/low-risk-workflow.template.yml +151 -0
- policyflow/bootstrap.py +378 -0
- policyflow/cli.py +382 -0
- policyflow/codex_runner.py +188 -0
- policyflow/consumer_config.py +119 -0
- policyflow/doctor.py +358 -0
- policyflow/exceptions.py +9 -0
- policyflow/github_approval.py +89 -0
- policyflow/models.py +321 -0
- policyflow/reporting.py +192 -0
- policyflow/runtime.py +263 -0
- policyflow/schemas.py +57 -0
- policyflow/sync.py +135 -0
- policyflow/validator.py +897 -0
- policyflow/workflow_generator.py +228 -0
- policyflow-0.1.0.dist-info/METADATA +450 -0
- policyflow-0.1.0.dist-info/RECORD +67 -0
- policyflow-0.1.0.dist-info/WHEEL +5 -0
- policyflow-0.1.0.dist-info/entry_points.txt +2 -0
- policyflow-0.1.0.dist-info/licenses/LICENSE +21 -0
- policyflow-0.1.0.dist-info/top_level.txt +1 -0
policyflow/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""PolicyFlow lightweight governance validator."""
|
|
2
|
+
|
|
3
|
+
from policyflow.api import (
|
|
4
|
+
WorkflowDocument,
|
|
5
|
+
WorkflowValidationError,
|
|
6
|
+
audit_workflows,
|
|
7
|
+
block_workflow_phase,
|
|
8
|
+
complete_workflow_phase,
|
|
9
|
+
get_workflow_status,
|
|
10
|
+
inspect_workflow,
|
|
11
|
+
record_workflow_handoff,
|
|
12
|
+
start_workflow_phase,
|
|
13
|
+
validate_github_approvals,
|
|
14
|
+
validate_pr_body,
|
|
15
|
+
validate_workflow,
|
|
16
|
+
validate_workflow_data,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"WorkflowDocument",
|
|
21
|
+
"WorkflowValidationError",
|
|
22
|
+
"__version__",
|
|
23
|
+
"audit_workflows",
|
|
24
|
+
"block_workflow_phase",
|
|
25
|
+
"complete_workflow_phase",
|
|
26
|
+
"get_workflow_status",
|
|
27
|
+
"inspect_workflow",
|
|
28
|
+
"record_workflow_handoff",
|
|
29
|
+
"start_workflow_phase",
|
|
30
|
+
"validate_github_approvals",
|
|
31
|
+
"validate_pr_body",
|
|
32
|
+
"validate_workflow",
|
|
33
|
+
"validate_workflow_data",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from policyflow.exceptions import WorkflowValidationError
|
|
14
|
+
from policyflow.runtime import (
|
|
15
|
+
PHASE_AGENT_MAP,
|
|
16
|
+
block_phase,
|
|
17
|
+
complete_phase,
|
|
18
|
+
load_workflow_raw,
|
|
19
|
+
record_handoff,
|
|
20
|
+
save_workflow_raw,
|
|
21
|
+
start_phase,
|
|
22
|
+
)
|
|
23
|
+
from policyflow.validator import validate_workflow_data
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
DEFAULT_RUNNER_CONFIG = Path("policyflow.runners.yml")
|
|
27
|
+
PHASE_EVIDENCE_KEYS = {
|
|
28
|
+
"planning": "planning",
|
|
29
|
+
"architecture-check": "architecture-check",
|
|
30
|
+
"review": "review",
|
|
31
|
+
"qa": "qa",
|
|
32
|
+
"approval": "approval",
|
|
33
|
+
}
|
|
34
|
+
PHASE_CONTRACT_KEYS = {
|
|
35
|
+
"planning": "planning",
|
|
36
|
+
"architecture-check": "architecture-check",
|
|
37
|
+
"implementation": "implementation",
|
|
38
|
+
"review": "review",
|
|
39
|
+
"qa": "qa",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_phase_with_runner(
|
|
44
|
+
workflow_path: Path, phase: str, runner_config_path: Path | None = None
|
|
45
|
+
) -> None:
|
|
46
|
+
if phase == "approval":
|
|
47
|
+
raise WorkflowValidationError(
|
|
48
|
+
["Phase 'approval' remains human-driven and cannot be run via agent execution."]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
config_path = runner_config_path or DEFAULT_RUNNER_CONFIG
|
|
52
|
+
runner_config = _load_runner_config(config_path)
|
|
53
|
+
runner_name = str(runner_config.get("default_runner", "")).strip()
|
|
54
|
+
runners = runner_config.get("runners")
|
|
55
|
+
|
|
56
|
+
if not runner_name:
|
|
57
|
+
raise WorkflowValidationError(["Runner config must declare default_runner."])
|
|
58
|
+
if not isinstance(runners, dict) or runner_name not in runners:
|
|
59
|
+
raise WorkflowValidationError(
|
|
60
|
+
[f"Runner config must declare runner settings for '{runner_name}'."]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
runner = runners[runner_name]
|
|
64
|
+
if not isinstance(runner, dict):
|
|
65
|
+
raise WorkflowValidationError(
|
|
66
|
+
[f"Runner definition for '{runner_name}' must be a mapping."]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
start_phase(workflow_path, phase)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
raw = load_workflow_raw(workflow_path)
|
|
73
|
+
payload = _build_execution_payload(raw, workflow_path, phase, runner, config_path)
|
|
74
|
+
result = _execute_runner(runner, payload, workflow_path, config_path)
|
|
75
|
+
_apply_runner_result(workflow_path, phase, result)
|
|
76
|
+
except WorkflowValidationError as exc:
|
|
77
|
+
_block_phase_on_failure(workflow_path, phase, exc.errors[0])
|
|
78
|
+
raise
|
|
79
|
+
except Exception as exc: # pragma: no cover - defensive catch for subprocess/json edge cases
|
|
80
|
+
_block_phase_on_failure(workflow_path, phase, str(exc))
|
|
81
|
+
raise WorkflowValidationError([str(exc)]) from exc
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _load_runner_config(path: Path) -> dict[str, Any]:
|
|
85
|
+
if not path.exists():
|
|
86
|
+
raise WorkflowValidationError([f"Runner config file not found: {path}"])
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
90
|
+
except yaml.YAMLError as exc:
|
|
91
|
+
raise WorkflowValidationError([f"Invalid runner config YAML: {exc}"]) from exc
|
|
92
|
+
|
|
93
|
+
if not isinstance(data, dict):
|
|
94
|
+
raise WorkflowValidationError(
|
|
95
|
+
["Runner config file must contain a top-level YAML mapping."]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return data
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _build_execution_payload(
|
|
102
|
+
raw: dict[str, Any],
|
|
103
|
+
workflow_path: Path,
|
|
104
|
+
phase: str,
|
|
105
|
+
runner: dict[str, Any],
|
|
106
|
+
config_path: Path,
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
handoffs = raw.get("handoffs") or []
|
|
109
|
+
inbound_handoff = None
|
|
110
|
+
for handoff in handoffs:
|
|
111
|
+
if (
|
|
112
|
+
isinstance(handoff, dict)
|
|
113
|
+
and handoff.get("to_phase") == phase
|
|
114
|
+
and handoff.get("status") in {"pending", "completed"}
|
|
115
|
+
):
|
|
116
|
+
inbound_handoff = handoff
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"workflow_path": str(workflow_path),
|
|
121
|
+
"workflow": raw,
|
|
122
|
+
"phase": phase,
|
|
123
|
+
"owner_agent": PHASE_AGENT_MAP.get(phase),
|
|
124
|
+
"prompt_text": _load_phase_asset(runner, "prompt_paths", phase, config_path),
|
|
125
|
+
"agent_text": _load_phase_asset(runner, "agent_paths", phase, config_path),
|
|
126
|
+
"handoff": inbound_handoff,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _load_phase_asset(
|
|
131
|
+
runner: dict[str, Any], config_key: str, phase: str, config_path: Path
|
|
132
|
+
) -> str | None:
|
|
133
|
+
phase_map = runner.get(config_key)
|
|
134
|
+
if not isinstance(phase_map, dict):
|
|
135
|
+
return None
|
|
136
|
+
configured_path = phase_map.get(phase)
|
|
137
|
+
if not isinstance(configured_path, str) or not configured_path.strip():
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
candidate = Path(configured_path)
|
|
141
|
+
if candidate.exists():
|
|
142
|
+
return candidate.read_text(encoding="utf-8")
|
|
143
|
+
|
|
144
|
+
cwd_candidate = Path.cwd() / configured_path
|
|
145
|
+
if cwd_candidate.exists():
|
|
146
|
+
return cwd_candidate.read_text(encoding="utf-8")
|
|
147
|
+
|
|
148
|
+
config_candidate = config_path.parent / configured_path
|
|
149
|
+
if config_candidate.exists():
|
|
150
|
+
return config_candidate.read_text(encoding="utf-8")
|
|
151
|
+
|
|
152
|
+
raise WorkflowValidationError(
|
|
153
|
+
[f"Runner asset for phase '{phase}' not found: {configured_path}"]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _execute_runner(
|
|
158
|
+
runner: dict[str, Any],
|
|
159
|
+
payload: dict[str, Any],
|
|
160
|
+
workflow_path: Path,
|
|
161
|
+
config_path: Path,
|
|
162
|
+
) -> dict[str, Any]:
|
|
163
|
+
command = runner.get("command")
|
|
164
|
+
if not isinstance(command, list) or not command:
|
|
165
|
+
raise WorkflowValidationError(["Runner command must be a non-empty list."])
|
|
166
|
+
|
|
167
|
+
with tempfile.TemporaryDirectory() as temp_dir_name:
|
|
168
|
+
temp_dir = Path(temp_dir_name)
|
|
169
|
+
input_path = temp_dir / "policyflow-agent-input.json"
|
|
170
|
+
output_path = temp_dir / "policyflow-agent-output.json"
|
|
171
|
+
input_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
172
|
+
|
|
173
|
+
substitutions = {
|
|
174
|
+
"workflow_path": str(workflow_path),
|
|
175
|
+
"phase": payload["phase"],
|
|
176
|
+
"input_path": str(input_path),
|
|
177
|
+
"output_path": str(output_path),
|
|
178
|
+
"python_executable": sys.executable,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
resolved_command = [
|
|
182
|
+
_format_command_token(token, substitutions) for token in command
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
completed = subprocess.run(
|
|
186
|
+
resolved_command,
|
|
187
|
+
cwd=Path.cwd(),
|
|
188
|
+
text=True,
|
|
189
|
+
capture_output=True,
|
|
190
|
+
check=False,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if completed.returncode != 0:
|
|
194
|
+
stderr = completed.stderr.strip()
|
|
195
|
+
stdout = completed.stdout.strip()
|
|
196
|
+
message = stderr or stdout or f"runner exited with code {completed.returncode}"
|
|
197
|
+
raise WorkflowValidationError([f"Agent runner failed: {message}"])
|
|
198
|
+
|
|
199
|
+
if not output_path.exists():
|
|
200
|
+
raise WorkflowValidationError(
|
|
201
|
+
[f"Agent runner did not produce output JSON: {output_path}"]
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
result = json.loads(output_path.read_text(encoding="utf-8"))
|
|
206
|
+
except json.JSONDecodeError as exc:
|
|
207
|
+
raise WorkflowValidationError(
|
|
208
|
+
[f"Agent runner produced invalid JSON: {exc}"]
|
|
209
|
+
) from exc
|
|
210
|
+
|
|
211
|
+
if not isinstance(result, dict):
|
|
212
|
+
raise WorkflowValidationError(
|
|
213
|
+
["Agent runner output must be a top-level JSON object."]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _format_command_token(token: Any, substitutions: dict[str, str]) -> str:
|
|
220
|
+
if not isinstance(token, str):
|
|
221
|
+
raise WorkflowValidationError(["Runner command entries must be strings."])
|
|
222
|
+
return token.format(**substitutions)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _apply_runner_result(workflow_path: Path, phase: str, result: dict[str, Any]) -> None:
|
|
226
|
+
output_phase = str(result.get("phase", "")).strip()
|
|
227
|
+
if output_phase != phase:
|
|
228
|
+
raise WorkflowValidationError(
|
|
229
|
+
[f"Agent runner output phase must match requested phase: {phase}"]
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
output_owner = str(result.get("owner_agent", "")).strip()
|
|
233
|
+
expected_owner = PHASE_AGENT_MAP.get(phase)
|
|
234
|
+
if output_owner != expected_owner:
|
|
235
|
+
raise WorkflowValidationError(
|
|
236
|
+
[f"Agent runner output owner_agent must match expected owner: {expected_owner}"]
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
status = str(result.get("status", "")).strip()
|
|
240
|
+
if status not in {"completed", "blocked"}:
|
|
241
|
+
raise WorkflowValidationError(
|
|
242
|
+
["Agent runner output status must be 'completed' or 'blocked'."]
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if status == "blocked":
|
|
246
|
+
blockers = result.get("blockers")
|
|
247
|
+
if isinstance(blockers, list) and blockers:
|
|
248
|
+
reason = str(blockers[0])
|
|
249
|
+
else:
|
|
250
|
+
reason = str(result.get("summary", "")).strip() or "agent runner blocked phase"
|
|
251
|
+
_block_phase_on_failure(workflow_path, phase, reason)
|
|
252
|
+
raise WorkflowValidationError([f"Agent runner blocked phase '{phase}': {reason}"])
|
|
253
|
+
|
|
254
|
+
raw = load_workflow_raw(workflow_path)
|
|
255
|
+
updated = deepcopy(raw)
|
|
256
|
+
_apply_phase_updates(updated, phase, result)
|
|
257
|
+
validate_workflow_data(updated)
|
|
258
|
+
save_workflow_raw(workflow_path, updated)
|
|
259
|
+
|
|
260
|
+
complete_phase(workflow_path, phase)
|
|
261
|
+
|
|
262
|
+
handoff = result.get("handoff")
|
|
263
|
+
if isinstance(handoff, dict):
|
|
264
|
+
to_phase = handoff.get("to_phase")
|
|
265
|
+
required_inputs = handoff.get("required_inputs")
|
|
266
|
+
produced_outputs = handoff.get("produced_outputs")
|
|
267
|
+
if (
|
|
268
|
+
isinstance(to_phase, str)
|
|
269
|
+
and isinstance(required_inputs, list)
|
|
270
|
+
and isinstance(produced_outputs, list)
|
|
271
|
+
):
|
|
272
|
+
record_handoff(
|
|
273
|
+
workflow_path,
|
|
274
|
+
phase,
|
|
275
|
+
to_phase,
|
|
276
|
+
[str(item) for item in required_inputs],
|
|
277
|
+
[str(item) for item in produced_outputs],
|
|
278
|
+
[],
|
|
279
|
+
[],
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _apply_phase_updates(raw: dict[str, Any], phase: str, result: dict[str, Any]) -> None:
|
|
284
|
+
contract_key = PHASE_CONTRACT_KEYS.get(phase)
|
|
285
|
+
contract_updates = result.get("contract_updates")
|
|
286
|
+
if contract_key and isinstance(contract_updates, dict):
|
|
287
|
+
raw.setdefault("contracts", {})
|
|
288
|
+
raw["contracts"][contract_key] = contract_updates
|
|
289
|
+
|
|
290
|
+
evidence_key = PHASE_EVIDENCE_KEYS.get(phase)
|
|
291
|
+
evidence_updates = result.get("evidence_updates")
|
|
292
|
+
if evidence_key and isinstance(evidence_updates, dict):
|
|
293
|
+
raw.setdefault("evidence", {})
|
|
294
|
+
raw["evidence"][evidence_key] = evidence_updates
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _block_phase_on_failure(workflow_path: Path, phase: str, reason: str) -> None:
|
|
298
|
+
try:
|
|
299
|
+
block_phase(workflow_path, phase, reason)
|
|
300
|
+
except WorkflowValidationError:
|
|
301
|
+
pass
|
policyflow/api.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Stable public Python API for PolicyFlow consumers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from policyflow.exceptions import WorkflowValidationError
|
|
9
|
+
from policyflow.github_approval import validate_github_pr_approvals
|
|
10
|
+
from policyflow.models import WorkflowDocument
|
|
11
|
+
from policyflow.reporting import audit_directory, workflow_status
|
|
12
|
+
from policyflow.runtime import (
|
|
13
|
+
block_phase,
|
|
14
|
+
complete_phase,
|
|
15
|
+
record_handoff,
|
|
16
|
+
start_phase,
|
|
17
|
+
)
|
|
18
|
+
from policyflow.validator import (
|
|
19
|
+
inspect_workflow_file,
|
|
20
|
+
validate_pull_request,
|
|
21
|
+
validate_workflow_data as _validate_workflow_data,
|
|
22
|
+
validate_workflow_file,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"WorkflowDocument",
|
|
27
|
+
"WorkflowValidationError",
|
|
28
|
+
"audit_workflows",
|
|
29
|
+
"block_workflow_phase",
|
|
30
|
+
"complete_workflow_phase",
|
|
31
|
+
"get_workflow_status",
|
|
32
|
+
"inspect_workflow",
|
|
33
|
+
"record_workflow_handoff",
|
|
34
|
+
"start_workflow_phase",
|
|
35
|
+
"validate_github_approvals",
|
|
36
|
+
"validate_pr_body",
|
|
37
|
+
"validate_workflow",
|
|
38
|
+
"validate_workflow_data",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def inspect_workflow(path: str | Path) -> tuple[WorkflowDocument, list[str]]:
|
|
43
|
+
"""Validate a workflow file and return the normalized document plus warnings."""
|
|
44
|
+
|
|
45
|
+
return inspect_workflow_file(path)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate_workflow(path: str | Path) -> WorkflowDocument:
|
|
49
|
+
"""Validate a workflow file and return its normalized workflow document."""
|
|
50
|
+
|
|
51
|
+
return validate_workflow_file(path)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_workflow_data(raw_data: dict[str, Any]) -> WorkflowDocument:
|
|
55
|
+
"""Validate an in-memory workflow mapping."""
|
|
56
|
+
|
|
57
|
+
return _validate_workflow_data(raw_data)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def validate_pr_body(
|
|
61
|
+
workflow_path: str | Path, pr_body_path: str | Path
|
|
62
|
+
) -> WorkflowDocument:
|
|
63
|
+
"""Validate a PR body markdown file against a workflow file."""
|
|
64
|
+
|
|
65
|
+
return validate_pull_request(workflow_path, pr_body_path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def validate_github_approvals(
|
|
69
|
+
workflow_path: str | Path, pr_body_path: str | Path, reviews_path: str | Path
|
|
70
|
+
) -> WorkflowDocument:
|
|
71
|
+
"""Validate GitHub review metadata against workflow approval requirements."""
|
|
72
|
+
|
|
73
|
+
return validate_github_pr_approvals(workflow_path, pr_body_path, reviews_path)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_workflow_status(path: str | Path) -> dict[str, Any]:
|
|
77
|
+
"""Return workflow status and merge-readiness data."""
|
|
78
|
+
|
|
79
|
+
return workflow_status(Path(path))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def audit_workflows(directory: str | Path) -> dict[str, Any]:
|
|
83
|
+
"""Return an audit payload for all workflow files under a directory."""
|
|
84
|
+
|
|
85
|
+
return audit_directory(Path(directory))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def start_workflow_phase(path: str | Path, phase: str) -> None:
|
|
89
|
+
"""Start a pending workflow phase and persist runtime state."""
|
|
90
|
+
|
|
91
|
+
start_phase(Path(path), phase)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def complete_workflow_phase(path: str | Path, phase: str) -> None:
|
|
95
|
+
"""Complete an in-progress workflow phase and persist runtime state."""
|
|
96
|
+
|
|
97
|
+
complete_phase(Path(path), phase)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def block_workflow_phase(path: str | Path, phase: str, reason: str) -> None:
|
|
101
|
+
"""Block a workflow phase with a reason and persist runtime state."""
|
|
102
|
+
|
|
103
|
+
block_phase(Path(path), phase, reason)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def record_workflow_handoff(
|
|
107
|
+
path: str | Path,
|
|
108
|
+
from_phase: str,
|
|
109
|
+
to_phase: str,
|
|
110
|
+
required_inputs: list[str],
|
|
111
|
+
produced_outputs: list[str],
|
|
112
|
+
blockers: list[str] | None = None,
|
|
113
|
+
override_refs: list[str] | None = None,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Record a workflow phase handoff with concrete artifacts."""
|
|
116
|
+
|
|
117
|
+
record_handoff(
|
|
118
|
+
Path(path),
|
|
119
|
+
from_phase,
|
|
120
|
+
to_phase,
|
|
121
|
+
required_inputs,
|
|
122
|
+
produced_outputs,
|
|
123
|
+
blockers,
|
|
124
|
+
override_refs,
|
|
125
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Architecture Agent
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Protect architecture boundaries and keep the workflow aligned with the target project's operating model.
|
|
6
|
+
|
|
7
|
+
## Responsibilities
|
|
8
|
+
|
|
9
|
+
- review architecture context and boundaries
|
|
10
|
+
- confirm risk and review depth
|
|
11
|
+
- identify approval needs
|
|
12
|
+
- define implementation constraints
|
|
13
|
+
|
|
14
|
+
## Accepted Inputs
|
|
15
|
+
|
|
16
|
+
- issue brief
|
|
17
|
+
- acceptance criteria
|
|
18
|
+
- non-goals
|
|
19
|
+
- initial risk level
|
|
20
|
+
- protected areas touched
|
|
21
|
+
- confidence summary
|
|
22
|
+
- escalation flags
|
|
23
|
+
|
|
24
|
+
## Required Outputs
|
|
25
|
+
|
|
26
|
+
- architecture assessment
|
|
27
|
+
- approved scope
|
|
28
|
+
- module boundaries
|
|
29
|
+
- contract impact
|
|
30
|
+
- risk review decision
|
|
31
|
+
- required reviews
|
|
32
|
+
- implementation constraints
|
|
33
|
+
|
|
34
|
+
## Forbidden Actions
|
|
35
|
+
|
|
36
|
+
- waive mandatory reviews
|
|
37
|
+
- approve speculative architecture
|
|
38
|
+
- normalize undocumented coupling
|
|
39
|
+
|
|
40
|
+
## Escalation Triggers
|
|
41
|
+
|
|
42
|
+
- contract ownership unclear
|
|
43
|
+
- boundary conflict
|
|
44
|
+
- protected area touched without approval path
|
|
45
|
+
- risk needs upgrade
|
|
46
|
+
|
|
47
|
+
## Handoff Target
|
|
48
|
+
|
|
49
|
+
- senior-dev-agent
|
|
50
|
+
- human approval first when required
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Planning Agent
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Turn an approved request into a bounded workflow with clear scope, non-goals, risk, approvals, and handoffs.
|
|
6
|
+
|
|
7
|
+
## Responsibilities
|
|
8
|
+
|
|
9
|
+
- read target-project context first
|
|
10
|
+
- classify risk
|
|
11
|
+
- choose the smallest workflow that still satisfies governance rules
|
|
12
|
+
- define scope, non-goals, affected modules, and escalation points
|
|
13
|
+
|
|
14
|
+
## Accepted Inputs
|
|
15
|
+
|
|
16
|
+
- approved issue or request
|
|
17
|
+
- target-project context
|
|
18
|
+
- relevant governance rules
|
|
19
|
+
|
|
20
|
+
## Required Outputs
|
|
21
|
+
|
|
22
|
+
- issue brief
|
|
23
|
+
- acceptance criteria
|
|
24
|
+
- non-goals
|
|
25
|
+
- initial risk level
|
|
26
|
+
- protected areas touched
|
|
27
|
+
- confidence summary
|
|
28
|
+
- escalation flags
|
|
29
|
+
|
|
30
|
+
## Forbidden Actions
|
|
31
|
+
|
|
32
|
+
- implement changes directly
|
|
33
|
+
- downgrade risk to avoid review
|
|
34
|
+
- reduce required reviews below the matrix
|
|
35
|
+
|
|
36
|
+
## Escalation Triggers
|
|
37
|
+
|
|
38
|
+
- risk unclear
|
|
39
|
+
- protected area touched
|
|
40
|
+
- conflicting architecture signals
|
|
41
|
+
- scope ambiguity
|
|
42
|
+
|
|
43
|
+
## Handoff Target
|
|
44
|
+
|
|
45
|
+
- architecture-agent
|
|
46
|
+
- senior-dev-agent on LOW-risk fast path only
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# QA Agent
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Validate acceptance criteria, evidence quality, and merge readiness.
|
|
6
|
+
|
|
7
|
+
## Responsibilities
|
|
8
|
+
|
|
9
|
+
- check scope and acceptance criteria against the actual diff
|
|
10
|
+
- confirm validation evidence
|
|
11
|
+
- confirm approval compliance
|
|
12
|
+
- record residual risks and merge readiness
|
|
13
|
+
|
|
14
|
+
## Accepted Inputs
|
|
15
|
+
|
|
16
|
+
- review approval
|
|
17
|
+
- residual risk
|
|
18
|
+
- QA focus areas
|
|
19
|
+
- test expectations
|
|
20
|
+
- approval evidence when required
|
|
21
|
+
|
|
22
|
+
## Required Outputs
|
|
23
|
+
|
|
24
|
+
- QA report
|
|
25
|
+
- quality gate status
|
|
26
|
+
- unresolved risks
|
|
27
|
+
- approval required
|
|
28
|
+
- merge readiness
|
|
29
|
+
|
|
30
|
+
## Forbidden Actions
|
|
31
|
+
|
|
32
|
+
- declare success without evidence
|
|
33
|
+
- bypass approval requirements
|
|
34
|
+
- weaken quality gates to force completion
|
|
35
|
+
|
|
36
|
+
## Escalation Triggers
|
|
37
|
+
|
|
38
|
+
- validation evidence missing
|
|
39
|
+
- approval ambiguity
|
|
40
|
+
- confidence conflicts with evidence
|
|
41
|
+
- unresolved critical risk remains
|
|
42
|
+
|
|
43
|
+
## Handoff Target
|
|
44
|
+
|
|
45
|
+
- human approval when required
|
|
46
|
+
- merge readiness when all gates are satisfied
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Review Agent
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Review for regressions, scope drift, architecture issues, and missing validation.
|
|
6
|
+
|
|
7
|
+
## Responsibilities
|
|
8
|
+
|
|
9
|
+
- compare the diff with issue, workflow, and rules
|
|
10
|
+
- check protected areas and contract safety
|
|
11
|
+
- identify missing tests or documentation
|
|
12
|
+
- decide whether the change returns for fixes or can move to QA
|
|
13
|
+
|
|
14
|
+
## Accepted Inputs
|
|
15
|
+
|
|
16
|
+
- implementation summary
|
|
17
|
+
- changed files
|
|
18
|
+
- test summary
|
|
19
|
+
- docs updates
|
|
20
|
+
- known limitations
|
|
21
|
+
- unresolved questions
|
|
22
|
+
|
|
23
|
+
## Required Outputs
|
|
24
|
+
|
|
25
|
+
- review findings
|
|
26
|
+
- required fixes
|
|
27
|
+
- severity
|
|
28
|
+
- approval status
|
|
29
|
+
- review approval
|
|
30
|
+
- residual risk
|
|
31
|
+
- QA focus areas
|
|
32
|
+
- test expectations
|
|
33
|
+
|
|
34
|
+
## Forbidden Actions
|
|
35
|
+
|
|
36
|
+
- rewrite scope during review
|
|
37
|
+
- waive approvals
|
|
38
|
+
- hand off to QA while blocking findings remain
|
|
39
|
+
|
|
40
|
+
## Escalation Triggers
|
|
41
|
+
|
|
42
|
+
- hidden protected-area impact
|
|
43
|
+
- risk understated
|
|
44
|
+
- evidence incomplete
|
|
45
|
+
- fix loop does not converge
|
|
46
|
+
|
|
47
|
+
## Handoff Target
|
|
48
|
+
|
|
49
|
+
- senior-dev-agent
|
|
50
|
+
- qa-agent
|
|
51
|
+
- human approval if risk or approval path changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Senior Dev Agent
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Implement the smallest approved change while preserving reviewability and evidence.
|
|
6
|
+
|
|
7
|
+
## Responsibilities
|
|
8
|
+
|
|
9
|
+
- follow the approved workflow
|
|
10
|
+
- keep changes local and explicit
|
|
11
|
+
- add or update tests when approved logic changes require them
|
|
12
|
+
- record assumptions and validation
|
|
13
|
+
|
|
14
|
+
## Accepted Inputs
|
|
15
|
+
|
|
16
|
+
- architecture assessment when required
|
|
17
|
+
- approved scope
|
|
18
|
+
- module boundaries
|
|
19
|
+
- contract impact
|
|
20
|
+
- risk review decision
|
|
21
|
+
- required reviews
|
|
22
|
+
- implementation constraints
|
|
23
|
+
|
|
24
|
+
## Required Outputs
|
|
25
|
+
|
|
26
|
+
- implementation summary
|
|
27
|
+
- changed files
|
|
28
|
+
- test summary
|
|
29
|
+
- docs updates
|
|
30
|
+
- known limitations
|
|
31
|
+
- unresolved questions
|
|
32
|
+
|
|
33
|
+
## Forbidden Actions
|
|
34
|
+
|
|
35
|
+
- expand scope
|
|
36
|
+
- bypass approval
|
|
37
|
+
- change protected areas without authorization
|
|
38
|
+
|
|
39
|
+
## Escalation Triggers
|
|
40
|
+
|
|
41
|
+
- scope no longer fits approved workflow
|
|
42
|
+
- protected area becomes implicated
|
|
43
|
+
- validation blocked
|
|
44
|
+
- architecture conflict appears during implementation
|
|
45
|
+
|
|
46
|
+
## Handoff Target
|
|
47
|
+
|
|
48
|
+
- review-agent
|
|
49
|
+
- architecture-agent or human approval if governance conditions change
|