open-codev-workflow 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.
- codev_workflow/__init__.py +5 -0
- codev_workflow/__main__.py +4 -0
- codev_workflow/bundle/.agents/skills/build-change/SKILL.md +96 -0
- codev_workflow/bundle/.agents/skills/build-change/agents/openai.yaml +4 -0
- codev_workflow/bundle/.agents/skills/build-change/assets/implementation-plan.template.md +51 -0
- codev_workflow/bundle/.agents/skills/define-product/SKILL.md +79 -0
- codev_workflow/bundle/.agents/skills/define-product/agents/openai.yaml +4 -0
- codev_workflow/bundle/.agents/skills/define-product/assets/brief.template.md +50 -0
- codev_workflow/bundle/.agents/skills/design-solution/SKILL.md +75 -0
- codev_workflow/bundle/.agents/skills/design-solution/agents/openai.yaml +4 -0
- codev_workflow/bundle/.agents/skills/design-solution/assets/decision.template.md +26 -0
- codev_workflow/bundle/.agents/skills/design-solution/assets/design.template.md +76 -0
- codev_workflow/bundle/.agents/skills/launch-product/SKILL.md +66 -0
- codev_workflow/bundle/.agents/skills/launch-product/agents/openai.yaml +4 -0
- codev_workflow/bundle/.agents/skills/launch-product/assets/launch-plan.template.md +48 -0
- codev_workflow/bundle/.agents/skills/plan-delivery/SKILL.md +140 -0
- codev_workflow/bundle/.agents/skills/plan-delivery/agents/openai.yaml +4 -0
- codev_workflow/bundle/.agents/skills/plan-delivery/assets/delivery-plan.template.md +41 -0
- codev_workflow/bundle/.agents/skills/review-change/SKILL.md +48 -0
- codev_workflow/bundle/.agents/skills/review-change/agents/openai.yaml +4 -0
- codev_workflow/bundle/.agents/skills/specify-project/SKILL.md +205 -0
- codev_workflow/bundle/.agents/skills/specify-project/agents/openai.yaml +4 -0
- codev_workflow/bundle/.agents/skills/specify-project/assets/specification.template.md +151 -0
- codev_workflow/bundle/.agents/skills/specify-project/references/interview-coverage.md +303 -0
- codev_workflow/bundle/.agents/skills/specify-project/scripts/validate_specification.py +143 -0
- codev_workflow/bundle/.opencode/agents/builder.md +54 -0
- codev_workflow/bundle/.opencode/agents/orchestrator.md +72 -0
- codev_workflow/bundle/.opencode/agents/reviewer.md +35 -0
- codev_workflow/bundle/AGENTS.md +23 -0
- codev_workflow/bundle/docs/AI-WORKFLOW-PROMPTS.md +318 -0
- codev_workflow/bundle/docs/WORKFLOW-COOKBOOK.md +419 -0
- codev_workflow/bundle/docs/WORKFLOW-HUMAN.md +212 -0
- codev_workflow/bundle/docs/for-ai/WORKFLOW-AGENTS.md +171 -0
- codev_workflow/bundle/docs/handbooks/IDEA-TO-PRODUCTION-HANDBOOK.md +1190 -0
- codev_workflow/bundle/docs/handbooks/LANGUAGE-AGNOSTIC-PROJECT-HANDBOOK.md +745 -0
- codev_workflow/bundle/docs/handbooks/PYTHON-PROJECT-HANDBOOK.md +960 -0
- codev_workflow/bundle/evals/development-workflow/scenarios.json +132 -0
- codev_workflow/bundle/scripts/evaluate-development-workflow.py +352 -0
- codev_workflow/bundle/scripts/validate-development-workflow.py +213 -0
- codev_workflow/cli.py +140 -0
- codev_workflow/installer.py +891 -0
- open_codev_workflow-0.1.0.dist-info/METADATA +150 -0
- open_codev_workflow-0.1.0.dist-info/RECORD +47 -0
- open_codev_workflow-0.1.0.dist-info/WHEEL +5 -0
- open_codev_workflow-0.1.0.dist-info/entry_points.txt +2 -0
- open_codev_workflow-0.1.0.dist-info/licenses/LICENSE +28 -0
- open_codev_workflow-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate the repository's human-AI development workflow without dependencies."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
EXPECTED_SKILLS = {
|
|
15
|
+
"specify-project": [
|
|
16
|
+
"assets/specification.template.md",
|
|
17
|
+
"references/interview-coverage.md",
|
|
18
|
+
"scripts/validate_specification.py",
|
|
19
|
+
],
|
|
20
|
+
"define-product": ["assets/brief.template.md"],
|
|
21
|
+
"design-solution": [
|
|
22
|
+
"assets/design.template.md",
|
|
23
|
+
"assets/decision.template.md",
|
|
24
|
+
],
|
|
25
|
+
"plan-delivery": ["assets/delivery-plan.template.md"],
|
|
26
|
+
"build-change": ["assets/implementation-plan.template.md"],
|
|
27
|
+
"review-change": [],
|
|
28
|
+
"launch-product": ["assets/launch-plan.template.md"],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
EXPECTED_HANDBOOKS = [
|
|
32
|
+
"PYTHON-PROJECT-HANDBOOK.md",
|
|
33
|
+
"LANGUAGE-AGNOSTIC-PROJECT-HANDBOOK.md",
|
|
34
|
+
"IDEA-TO-PRODUCTION-HANDBOOK.md",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
EXPECTED_GUIDES = [
|
|
38
|
+
"AGENTS.md",
|
|
39
|
+
"docs/WORKFLOW-HUMAN.md",
|
|
40
|
+
"docs/for-ai/WORKFLOW-AGENTS.md",
|
|
41
|
+
"docs/WORKFLOW-COOKBOOK.md",
|
|
42
|
+
"docs/AI-WORKFLOW-PROMPTS.md",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
EVALUATION_SCRIPT = "scripts/evaluate-development-workflow.py"
|
|
46
|
+
EVALUATION_CATALOG = "evals/development-workflow/scenarios.json"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_frontmatter(text: str, path: Path, errors: list[str]) -> dict[str, str]:
|
|
50
|
+
lines = text.splitlines()
|
|
51
|
+
if not lines or lines[0] != "---":
|
|
52
|
+
errors.append(f"{path}: missing opening YAML delimiter")
|
|
53
|
+
return {}
|
|
54
|
+
try:
|
|
55
|
+
end = lines.index("---", 1)
|
|
56
|
+
except ValueError:
|
|
57
|
+
errors.append(f"{path}: missing closing YAML delimiter")
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
result: dict[str, str] = {}
|
|
61
|
+
for line in lines[1:end]:
|
|
62
|
+
match = re.fullmatch(r"([a-zA-Z0-9_-]+):\s*(.+)", line)
|
|
63
|
+
if not match:
|
|
64
|
+
errors.append(f"{path}: unsupported frontmatter line: {line!r}")
|
|
65
|
+
continue
|
|
66
|
+
result[match.group(1)] = match.group(2).strip().strip('"')
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_skill(root: Path, name: str, assets: list[str], errors: list[str]) -> None:
|
|
71
|
+
skill_dir = root / name
|
|
72
|
+
skill_file = skill_dir / "SKILL.md"
|
|
73
|
+
if not skill_file.is_file():
|
|
74
|
+
errors.append(f"{skill_file}: missing")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
text = skill_file.read_text(encoding="utf-8")
|
|
78
|
+
metadata = parse_frontmatter(text, skill_file, errors)
|
|
79
|
+
if set(metadata) != {"name", "description"}:
|
|
80
|
+
errors.append(f"{skill_file}: frontmatter must contain only name and description")
|
|
81
|
+
if metadata.get("name") != name:
|
|
82
|
+
errors.append(f"{skill_file}: name must match directory {name!r}")
|
|
83
|
+
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name) or len(name) > 64:
|
|
84
|
+
errors.append(f"{skill_file}: name must be hyphen-case and at most 64 characters")
|
|
85
|
+
description = metadata.get("description", "")
|
|
86
|
+
if len(description) < 80:
|
|
87
|
+
errors.append(f"{skill_file}: description is too short to trigger reliably")
|
|
88
|
+
if len(description) > 1024 or "<" in description or ">" in description:
|
|
89
|
+
errors.append(f"{skill_file}: description violates skill metadata limits")
|
|
90
|
+
if len(text.splitlines()) > 500:
|
|
91
|
+
errors.append(f"{skill_file}: exceeds the 500-line skill budget")
|
|
92
|
+
if re.search(r"\b(TODO|TBD)\b", text, re.IGNORECASE):
|
|
93
|
+
errors.append(f"{skill_file}: contains an unfinished placeholder")
|
|
94
|
+
|
|
95
|
+
ui_file = skill_dir / "agents" / "openai.yaml"
|
|
96
|
+
if not ui_file.is_file():
|
|
97
|
+
errors.append(f"{ui_file}: missing")
|
|
98
|
+
else:
|
|
99
|
+
ui = ui_file.read_text(encoding="utf-8")
|
|
100
|
+
for field in ("display_name", "short_description", "default_prompt"):
|
|
101
|
+
if not re.search(rf"^\s*{field}:\s*\".+\"\s*$", ui, re.MULTILINE):
|
|
102
|
+
errors.append(f"{ui_file}: missing quoted {field}")
|
|
103
|
+
if f"${name}" not in ui:
|
|
104
|
+
errors.append(f"{ui_file}: default_prompt must mention ${name}")
|
|
105
|
+
|
|
106
|
+
for relative in assets:
|
|
107
|
+
asset = skill_dir / relative
|
|
108
|
+
if not asset.is_file() or asset.stat().st_size == 0:
|
|
109
|
+
errors.append(f"{asset}: missing or empty")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def validate_guides(repo: Path, errors: list[str]) -> None:
|
|
113
|
+
guides = [repo / relative for relative in EXPECTED_GUIDES]
|
|
114
|
+
for guide in guides:
|
|
115
|
+
if not guide.is_file():
|
|
116
|
+
errors.append(f"{guide}: missing")
|
|
117
|
+
continue
|
|
118
|
+
text = guide.read_text(encoding="utf-8")
|
|
119
|
+
for skill in EXPECTED_SKILLS:
|
|
120
|
+
if skill not in text:
|
|
121
|
+
errors.append(f"{guide}: does not reference {skill}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def validate_handbooks(repo: Path, errors: list[str]) -> None:
|
|
125
|
+
handbook_root = repo / "docs" / "handbooks"
|
|
126
|
+
for name in EXPECTED_HANDBOOKS:
|
|
127
|
+
handbook = handbook_root / name
|
|
128
|
+
if not handbook.is_file():
|
|
129
|
+
errors.append(f"{handbook}: missing")
|
|
130
|
+
continue
|
|
131
|
+
text = handbook.read_text(encoding="utf-8")
|
|
132
|
+
if len(text.splitlines()) < 100:
|
|
133
|
+
errors.append(f"{handbook}: unexpectedly short")
|
|
134
|
+
if re.search(r"\b(TODO|TBD)\b", text, re.IGNORECASE):
|
|
135
|
+
errors.append(f"{handbook}: contains an unfinished placeholder")
|
|
136
|
+
for skill in EXPECTED_SKILLS:
|
|
137
|
+
if skill not in text:
|
|
138
|
+
errors.append(f"{handbook}: does not reference {skill}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def validate_evaluations(repo: Path, errors: list[str]) -> int:
|
|
142
|
+
script = repo / EVALUATION_SCRIPT
|
|
143
|
+
catalog = repo / EVALUATION_CATALOG
|
|
144
|
+
if not script.is_file():
|
|
145
|
+
errors.append(f"{script}: missing")
|
|
146
|
+
return 0
|
|
147
|
+
if not catalog.is_file():
|
|
148
|
+
errors.append(f"{catalog}: missing")
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
for label, extra_args in (
|
|
152
|
+
("catalog", []),
|
|
153
|
+
("scorer self-test", ["--self-test"]),
|
|
154
|
+
):
|
|
155
|
+
try:
|
|
156
|
+
completed = subprocess.run(
|
|
157
|
+
[sys.executable, str(script), "--repo", str(repo), *extra_args],
|
|
158
|
+
check=False,
|
|
159
|
+
capture_output=True,
|
|
160
|
+
text=True,
|
|
161
|
+
timeout=10,
|
|
162
|
+
)
|
|
163
|
+
except subprocess.TimeoutExpired:
|
|
164
|
+
errors.append(f"behavioral evaluation {label} timed out")
|
|
165
|
+
return 0
|
|
166
|
+
if completed.returncode != 0:
|
|
167
|
+
detail = (completed.stdout + completed.stderr).strip()
|
|
168
|
+
errors.append(f"behavioral evaluation {label} is invalid: {detail}")
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
data = json.loads(catalog.read_text(encoding="utf-8"))
|
|
173
|
+
return len(data.get("scenarios", []))
|
|
174
|
+
except (OSError, ValueError, TypeError) as error:
|
|
175
|
+
errors.append(f"{catalog}: cannot count scenarios: {error}")
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def main() -> int:
|
|
180
|
+
parser = argparse.ArgumentParser()
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
"--repo",
|
|
183
|
+
type=Path,
|
|
184
|
+
default=Path(__file__).resolve().parents[1],
|
|
185
|
+
help="Repository root (defaults to the script's repository)",
|
|
186
|
+
)
|
|
187
|
+
args = parser.parse_args()
|
|
188
|
+
repo = args.repo.resolve()
|
|
189
|
+
skill_root = repo / ".agents" / "skills"
|
|
190
|
+
errors: list[str] = []
|
|
191
|
+
|
|
192
|
+
for skill, assets in EXPECTED_SKILLS.items():
|
|
193
|
+
validate_skill(skill_root, skill, assets, errors)
|
|
194
|
+
validate_guides(repo, errors)
|
|
195
|
+
validate_handbooks(repo, errors)
|
|
196
|
+
scenario_count = validate_evaluations(repo, errors)
|
|
197
|
+
|
|
198
|
+
if errors:
|
|
199
|
+
print("Workflow validation failed:")
|
|
200
|
+
for error in errors:
|
|
201
|
+
print(f"- {error}")
|
|
202
|
+
return 1
|
|
203
|
+
|
|
204
|
+
print(
|
|
205
|
+
"Workflow validation passed: "
|
|
206
|
+
f"{len(EXPECTED_SKILLS)} skills, {len(EXPECTED_GUIDES)} guides, and "
|
|
207
|
+
f"{len(EXPECTED_HANDBOOKS)} handbooks, plus {scenario_count} behavioral scenarios"
|
|
208
|
+
)
|
|
209
|
+
return 0
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
sys.exit(main())
|
codev_workflow/cli.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Command-line interface for CoDev."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import platform
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from codev_workflow import __version__
|
|
12
|
+
from codev_workflow.installer import (
|
|
13
|
+
CoDevError,
|
|
14
|
+
apply_plan,
|
|
15
|
+
check_project,
|
|
16
|
+
format_plan,
|
|
17
|
+
plan_init,
|
|
18
|
+
plan_remove,
|
|
19
|
+
plan_update,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _target(value: str) -> Path:
|
|
24
|
+
return Path(value).expanduser()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parser() -> argparse.ArgumentParser:
|
|
28
|
+
parser = argparse.ArgumentParser(
|
|
29
|
+
prog="codev",
|
|
30
|
+
description="Install and maintain human-guided AI delivery workflows.",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument("--version", action="version", version=f"CoDev {__version__}")
|
|
33
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
34
|
+
|
|
35
|
+
init = commands.add_parser("init", help="install CoDev into a repository")
|
|
36
|
+
init.add_argument("--target", type=_target, default=Path.cwd())
|
|
37
|
+
init.add_argument(
|
|
38
|
+
"--platform",
|
|
39
|
+
action="append",
|
|
40
|
+
choices=("all", "codex", "opencode"),
|
|
41
|
+
default=None,
|
|
42
|
+
help="target adapter; repeat to select several (default: all)",
|
|
43
|
+
)
|
|
44
|
+
init.add_argument("--dry-run", action="store_true", help="show the plan only")
|
|
45
|
+
|
|
46
|
+
check = commands.add_parser("check", help="verify an installed bundle")
|
|
47
|
+
check.add_argument("--target", type=_target, default=Path.cwd())
|
|
48
|
+
|
|
49
|
+
doctor = commands.add_parser("doctor", help="show environment and bundle health")
|
|
50
|
+
doctor.add_argument("--target", type=_target, default=Path.cwd())
|
|
51
|
+
|
|
52
|
+
diff = commands.add_parser("diff", help="preview update changes")
|
|
53
|
+
diff.add_argument("--target", type=_target, default=Path.cwd())
|
|
54
|
+
|
|
55
|
+
update = commands.add_parser("update", help="apply a conflict-free bundle update")
|
|
56
|
+
update.add_argument("--target", type=_target, default=Path.cwd())
|
|
57
|
+
|
|
58
|
+
remove = commands.add_parser(
|
|
59
|
+
"remove", help="remove an unchanged CoDev installation"
|
|
60
|
+
)
|
|
61
|
+
remove.add_argument("--target", type=_target, default=Path.cwd())
|
|
62
|
+
remove.add_argument("--dry-run", action="store_true", help="show the plan only")
|
|
63
|
+
return parser
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _print_check(target: Path) -> int:
|
|
67
|
+
result = check_project(target)
|
|
68
|
+
if result.ok:
|
|
69
|
+
print(
|
|
70
|
+
f"CoDev {result.version} is healthy: "
|
|
71
|
+
f"{result.managed_files} managed files, no drift."
|
|
72
|
+
)
|
|
73
|
+
return 0
|
|
74
|
+
print(f"CoDev check found {len(result.issues)} issue(s):")
|
|
75
|
+
for issue in result.issues:
|
|
76
|
+
print(f"- {issue}")
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
81
|
+
args = _parser().parse_args(argv)
|
|
82
|
+
try:
|
|
83
|
+
if args.command == "init":
|
|
84
|
+
target = args.target.resolve()
|
|
85
|
+
platforms = args.platform or ["all"]
|
|
86
|
+
plan = plan_init(target, platforms)
|
|
87
|
+
print(format_plan(plan))
|
|
88
|
+
if plan.conflicts:
|
|
89
|
+
print(f"Installation stopped: {len(plan.conflicts)} conflict(s).")
|
|
90
|
+
return 2
|
|
91
|
+
if args.dry_run:
|
|
92
|
+
print("Dry run complete; no files were written.")
|
|
93
|
+
return 0
|
|
94
|
+
apply_plan(target, plan)
|
|
95
|
+
print(f"Installed CoDev {__version__} into {target}")
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
if args.command == "check":
|
|
99
|
+
return _print_check(args.target.resolve())
|
|
100
|
+
|
|
101
|
+
if args.command == "doctor":
|
|
102
|
+
print(f"CoDev: {__version__}")
|
|
103
|
+
print(f"Python: {platform.python_version()} ({platform.system()})")
|
|
104
|
+
print(f"Target: {args.target.resolve()}")
|
|
105
|
+
return _print_check(args.target.resolve())
|
|
106
|
+
|
|
107
|
+
if args.command in {"diff", "update"}:
|
|
108
|
+
target = args.target.resolve()
|
|
109
|
+
plan = plan_update(target)
|
|
110
|
+
print(format_plan(plan))
|
|
111
|
+
if plan.conflicts:
|
|
112
|
+
print(f"Update stopped: {len(plan.conflicts)} conflict(s).")
|
|
113
|
+
return 2
|
|
114
|
+
if args.command == "diff":
|
|
115
|
+
print("Preview complete; no files were written.")
|
|
116
|
+
return 0
|
|
117
|
+
apply_plan(target, plan)
|
|
118
|
+
print(f"Updated CoDev bundle to {__version__} in {target}")
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
if args.command == "remove":
|
|
122
|
+
target = args.target.resolve()
|
|
123
|
+
plan = plan_remove(target)
|
|
124
|
+
print(format_plan(plan))
|
|
125
|
+
if plan.conflicts:
|
|
126
|
+
print(f"Removal stopped: {len(plan.conflicts)} conflict(s).")
|
|
127
|
+
return 2
|
|
128
|
+
if args.dry_run:
|
|
129
|
+
print("Dry run complete; no files were removed.")
|
|
130
|
+
return 0
|
|
131
|
+
apply_plan(target, plan)
|
|
132
|
+
print(f"Removed CoDev from {target}")
|
|
133
|
+
return 0
|
|
134
|
+
except CoDevError as error:
|
|
135
|
+
print(f"codev: {error}", file=sys.stderr)
|
|
136
|
+
return 2
|
|
137
|
+
except OSError as error:
|
|
138
|
+
print(f"codev: filesystem error: {error}", file=sys.stderr)
|
|
139
|
+
return 2
|
|
140
|
+
return 2
|