cowork-flow 0.0.26 → 0.0.28
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 +16 -8
- package/package.json +1 -1
- package/src/lib/copy-template.js +2 -2
- package/template/.agents/skills/before-dev/SKILL.md +1 -0
- package/template/.agents/skills/check/SKILL.md +1 -0
- package/template/.agents/skills/party-mode-v2/SKILL.md +75 -0
- package/template/.agents/skills/tdd/SKILL.md +61 -0
- package/template/.agents/skills/writing-plans/SKILL.md +1 -0
- package/template/.claude/agents/cowork-implement.md +153 -0
- package/template/.claude/hooks/inject-workflow-state.py +17 -8
- package/template/.claude/skills/before-dev/SKILL.md +1 -0
- package/template/.claude/skills/check/SKILL.md +1 -0
- package/template/.claude/skills/party-mode-v2/SKILL.md +75 -0
- package/template/.claude/skills/tdd/SKILL.md +61 -0
- package/template/.claude/skills/writing-plans/SKILL.md +1 -0
- package/template/.codex/agents/cowork-check.toml +2 -0
- package/template/.codex/agents/cowork-implement.toml +2 -0
- package/template/.codex/hooks/inject-workflow-state.py +17 -8
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/config.yaml +16 -0
- package/template/.cowork-flow/scripts/common/archive_utils.py +1 -1
- package/template/.cowork-flow/scripts/common/coding_standards.py +209 -0
- package/template/.cowork-flow/scripts/common/config.py +93 -0
- package/template/.cowork-flow/scripts/common/gates.py +66 -0
- package/template/.cowork-flow/scripts/common/git_snapshot.py +83 -0
- package/template/.cowork-flow/scripts/common/state_machine.py +57 -0
- package/template/.cowork-flow/scripts/common/tdd_evidence.py +301 -0
- package/template/.cowork-flow/scripts/common/test_intent.py +265 -0
- package/template/.cowork-flow/scripts/common/validate_coding_standards.py +181 -0
- package/template/.cowork-flow/scripts/common/validate_implementation.py +243 -0
- package/template/.cowork-flow/scripts/common/validate_rules.py +465 -0
- package/template/.cowork-flow/scripts/doctor.py +18 -36
- package/template/.cowork-flow/scripts/party_mode_v2.py +1162 -0
- package/template/.cowork-flow/scripts/run.py +3 -0
- package/template/.cowork-flow/scripts/task.py +190 -0
- package/template/.cowork-flow/spec/contracts/index.md +12 -0
- package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +68 -0
- package/template/.cowork-flow/spec/{subagent-dispatch.md → contracts/subagent-dispatch.md} +2 -0
- package/template/.cowork-flow/spec/{workflow-state-templates.md → contracts/workflow-state-templates.md} +1 -1
- package/template/.cowork-flow/spec/index.md +15 -0
- package/template/.cowork-flow/spec/{registry.json → runtime/contract-registry.json} +17 -4
- package/template/.cowork-flow/spec/runtime/index.md +10 -0
- package/template/.cowork-flow/spec/runtime/rules.json +173 -0
- package/template/.cowork-flow/spec/schemas/index.md +10 -0
- package/template/.cowork-flow/spec/schemas/party-mode-v2-actions.schema.json +90 -0
- package/template/.cowork-flow/spec/schemas/rules.schema.json +80 -0
- package/template/.cowork-flow/workflow.md +13 -7
- package/template/.opencode/agents/cowork-implement.md +153 -0
- package/template/.opencode/commands/party-mode-v2.md +26 -0
- package/template/.opencode/plugins/cowork-flow.js +18 -7
- package/template/AGENTS.md +2 -6
- package/template/.cowork-flow/scripts/common/entry_classifier.py +0 -122
- /package/template/.cowork-flow/spec/{capabilities.md → contracts/capabilities.md} +0 -0
- /package/template/.cowork-flow/spec/{entry-contract.md → contracts/entry-contract.md} +0 -0
- /package/template/.cowork-flow/spec/{adapter.schema.json → schemas/adapter.schema.json} +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Coding standards helper.
|
|
4
|
+
|
|
5
|
+
Summarizes relevant standards and validates changed files for lifecycle gates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .coding_standards import validate_changed_files
|
|
16
|
+
from .git_snapshot import collect_changed_files, collect_changed_paths
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_coding_standards_summary(
|
|
20
|
+
repo_root: Path,
|
|
21
|
+
task_dir: Path,
|
|
22
|
+
) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Get coding standards summary based on changed files.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
repo_root: Repository root directory
|
|
28
|
+
task_dir: Task directory
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Summary string of relevant coding standards
|
|
32
|
+
"""
|
|
33
|
+
modified_files = collect_changed_paths(repo_root)
|
|
34
|
+
if not modified_files:
|
|
35
|
+
return ""
|
|
36
|
+
|
|
37
|
+
# Classify files
|
|
38
|
+
backend_files = [f for f in modified_files if _is_backend_file(f)]
|
|
39
|
+
frontend_files = [f for f in modified_files if _is_frontend_file(f)]
|
|
40
|
+
|
|
41
|
+
summaries = []
|
|
42
|
+
|
|
43
|
+
# Get backend standards summary
|
|
44
|
+
if backend_files:
|
|
45
|
+
backend_summary = _get_standards_summary(repo_root, "backend")
|
|
46
|
+
if backend_summary:
|
|
47
|
+
summaries.append(f"=== Backend Coding Standards ===\n{backend_summary}")
|
|
48
|
+
|
|
49
|
+
# Get frontend standards summary
|
|
50
|
+
if frontend_files:
|
|
51
|
+
frontend_summary = _get_standards_summary(repo_root, "frontend")
|
|
52
|
+
if frontend_summary:
|
|
53
|
+
summaries.append(f"=== Frontend Coding Standards ===\n{frontend_summary}")
|
|
54
|
+
|
|
55
|
+
return "\n\n".join(summaries)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate_coding_standards(
|
|
59
|
+
repo_root: Path,
|
|
60
|
+
task_dir: Path | None = None,
|
|
61
|
+
) -> list[dict]:
|
|
62
|
+
"""Validate coding standards for changed files."""
|
|
63
|
+
return validate_changed_files(repo_root, collect_changed_files(repo_root))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_backend_file(file_path: str) -> bool:
|
|
67
|
+
"""Check if file is a backend file"""
|
|
68
|
+
backend_patterns = [
|
|
69
|
+
r"\.py$",
|
|
70
|
+
r"\.java$",
|
|
71
|
+
r"\.go$",
|
|
72
|
+
r"\.rs$",
|
|
73
|
+
r"\.rb$",
|
|
74
|
+
]
|
|
75
|
+
return any(re.search(p, file_path) for p in backend_patterns)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _is_frontend_file(file_path: str) -> bool:
|
|
79
|
+
"""Check if file is a frontend file"""
|
|
80
|
+
frontend_patterns = [
|
|
81
|
+
r"\.tsx$",
|
|
82
|
+
r"\.jsx$",
|
|
83
|
+
r"\.vue$",
|
|
84
|
+
r"\.svelte$",
|
|
85
|
+
]
|
|
86
|
+
return any(re.search(p, file_path) for p in frontend_patterns)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_standards_summary(repo_root: Path, category: str) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Get summary of coding standards from markdown files.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
repo_root: Repository root directory
|
|
95
|
+
category: 'backend' or 'frontend'
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Summary string
|
|
99
|
+
"""
|
|
100
|
+
spec_dir = repo_root / ".cowork-flow" / "spec" / category
|
|
101
|
+
if not spec_dir.exists():
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
summaries = []
|
|
105
|
+
|
|
106
|
+
for md_file in sorted(spec_dir.glob("*.md")):
|
|
107
|
+
if md_file.name == "index.md":
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
content = md_file.read_text(encoding="utf-8")
|
|
111
|
+
rules = _extract_key_rules(content)
|
|
112
|
+
|
|
113
|
+
if rules:
|
|
114
|
+
summaries.append(f"File: {md_file.name}")
|
|
115
|
+
for rule in rules[:5]: # Limit to top 5 rules per file
|
|
116
|
+
summaries.append(f" - {rule}")
|
|
117
|
+
summaries.append("")
|
|
118
|
+
|
|
119
|
+
return "\n".join(summaries)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _extract_key_rules(content: str) -> list[str]:
|
|
123
|
+
"""Extract key rules from markdown content"""
|
|
124
|
+
rules = []
|
|
125
|
+
|
|
126
|
+
# Extract forbidden patterns (Must not, Should not, Do not, etc.)
|
|
127
|
+
# Also match Chinese patterns
|
|
128
|
+
forbidden_patterns = [
|
|
129
|
+
r"[-*]\s*(Must not|Should not|Do not|Never|Avoid)\s+(.+)",
|
|
130
|
+
r"[-*]\s*(禁止|不得|不能|不应|避免|不要|不可)\s*(.+)",
|
|
131
|
+
r"[-*]\s*(.+不得|.+不能|.+不应|.+避免|.+不要)\s*(.+)",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
for line in content.split("\n"):
|
|
135
|
+
line = line.strip()
|
|
136
|
+
if not line or line.startswith("#"):
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
for pattern in forbidden_patterns:
|
|
140
|
+
match = re.search(pattern, line, re.IGNORECASE)
|
|
141
|
+
if match:
|
|
142
|
+
# Extract the rule text
|
|
143
|
+
rule_text = line.lstrip("-* ").strip()
|
|
144
|
+
if len(rule_text) > 5: # Skip very short rules
|
|
145
|
+
rules.append(rule_text[:80]) # Limit length
|
|
146
|
+
break # Only match one pattern per line
|
|
147
|
+
|
|
148
|
+
return rules[:10] # Return top 10 rules
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def main():
|
|
152
|
+
"""Main entry point"""
|
|
153
|
+
import argparse
|
|
154
|
+
|
|
155
|
+
parser = argparse.ArgumentParser(description="Get coding standards summary")
|
|
156
|
+
parser.add_argument("--task-dir", required=True, help="Task directory path")
|
|
157
|
+
parser.add_argument("--repo-root", default=".", help="Repository root path")
|
|
158
|
+
parser.add_argument("--validate", action="store_true", help="Fail on coding standards violations")
|
|
159
|
+
|
|
160
|
+
args = parser.parse_args()
|
|
161
|
+
|
|
162
|
+
repo_root = Path(args.repo_root).resolve()
|
|
163
|
+
task_dir = Path(args.task_dir).resolve()
|
|
164
|
+
|
|
165
|
+
if args.validate:
|
|
166
|
+
violations = validate_coding_standards(repo_root, task_dir)
|
|
167
|
+
if violations:
|
|
168
|
+
for violation in violations:
|
|
169
|
+
print(json.dumps(violation, ensure_ascii=False))
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
sys.exit(0)
|
|
172
|
+
|
|
173
|
+
summary = get_coding_standards_summary(repo_root, task_dir)
|
|
174
|
+
if summary:
|
|
175
|
+
print(summary)
|
|
176
|
+
else:
|
|
177
|
+
print("No relevant coding standards found for changed files.")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
main()
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Implementation validation script.
|
|
4
|
+
|
|
5
|
+
Validates code changes against forbidden action rules after task implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from .validate_rules import (
|
|
18
|
+
config_violation,
|
|
19
|
+
load_rule_index,
|
|
20
|
+
rule_violation,
|
|
21
|
+
runtime_rules_path,
|
|
22
|
+
)
|
|
23
|
+
except ImportError: # pragma: no cover - direct script execution
|
|
24
|
+
from validate_rules import ( # type: ignore[no-redef]
|
|
25
|
+
config_violation,
|
|
26
|
+
load_rule_index,
|
|
27
|
+
rule_violation,
|
|
28
|
+
runtime_rules_path,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate_implementation(
|
|
33
|
+
repo_root: Path,
|
|
34
|
+
task_dir: Path,
|
|
35
|
+
allow_spec_file_modifications: bool = False,
|
|
36
|
+
) -> list[dict]:
|
|
37
|
+
"""
|
|
38
|
+
Validate code changes against implementation rules.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
repo_root: Repository root directory
|
|
42
|
+
task_dir: Task directory
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
List of violations
|
|
46
|
+
"""
|
|
47
|
+
violations = []
|
|
48
|
+
|
|
49
|
+
modified_files = _get_modified_files(repo_root)
|
|
50
|
+
if not modified_files:
|
|
51
|
+
return violations
|
|
52
|
+
|
|
53
|
+
rule_index, config_violations = load_rule_index(repo_root)
|
|
54
|
+
if config_violations:
|
|
55
|
+
return config_violations
|
|
56
|
+
|
|
57
|
+
diff_output = _get_git_diff(repo_root, modified_files)
|
|
58
|
+
|
|
59
|
+
# Check forbidden action rules
|
|
60
|
+
violations.extend(
|
|
61
|
+
_check_spec_file_modifications(
|
|
62
|
+
repo_root,
|
|
63
|
+
modified_files,
|
|
64
|
+
rule_index,
|
|
65
|
+
allow_spec_file_modifications=allow_spec_file_modifications,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
violations.extend(_check_premature_abstraction(repo_root, diff_output, rule_index))
|
|
69
|
+
violations.extend(_check_unrequested_features(repo_root, diff_output, task_dir, rule_index))
|
|
70
|
+
|
|
71
|
+
return violations
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _missing_rule(repo_root: Path, rule_id: str) -> dict:
|
|
75
|
+
return config_violation(
|
|
76
|
+
"RULES-CONFIG-004",
|
|
77
|
+
f"Runtime workflow rule metadata is missing: {rule_id}",
|
|
78
|
+
runtime_rules_path(repo_root),
|
|
79
|
+
f"Add {rule_id} to .cowork-flow/spec/runtime/rules.json.",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _get_modified_files(repo_root: Path) -> list[str]:
|
|
84
|
+
"""Get git-tracked modified files under repo_root."""
|
|
85
|
+
try:
|
|
86
|
+
result = subprocess.run(
|
|
87
|
+
["git", "diff", "--name-only", "--", "."],
|
|
88
|
+
cwd=repo_root,
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
encoding="utf-8",
|
|
92
|
+
errors="replace",
|
|
93
|
+
)
|
|
94
|
+
if result.returncode == 0:
|
|
95
|
+
return [file_path for file_path in result.stdout.strip().split("\n") if file_path]
|
|
96
|
+
return []
|
|
97
|
+
except Exception:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _get_git_diff(repo_root: Path, files: list[str]) -> str:
|
|
102
|
+
"""Get git diff output"""
|
|
103
|
+
if not files:
|
|
104
|
+
return ""
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
diff_result = subprocess.run(
|
|
108
|
+
["git", "diff", "--", *files],
|
|
109
|
+
cwd=repo_root,
|
|
110
|
+
capture_output=True,
|
|
111
|
+
text=True,
|
|
112
|
+
encoding="utf-8",
|
|
113
|
+
errors="replace",
|
|
114
|
+
)
|
|
115
|
+
return diff_result.stdout
|
|
116
|
+
except Exception:
|
|
117
|
+
return ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _check_spec_file_modifications(
|
|
121
|
+
repo_root: Path,
|
|
122
|
+
modified_files: list[str],
|
|
123
|
+
rule_index: dict[str, dict],
|
|
124
|
+
*,
|
|
125
|
+
allow_spec_file_modifications: bool,
|
|
126
|
+
) -> list[dict]:
|
|
127
|
+
"""R-AG-002: Check if spec files were modified"""
|
|
128
|
+
violations = []
|
|
129
|
+
|
|
130
|
+
if allow_spec_file_modifications:
|
|
131
|
+
return violations
|
|
132
|
+
|
|
133
|
+
# Spec file path patterns
|
|
134
|
+
spec_patterns = [
|
|
135
|
+
r"(^|/)\.cowork-flow/spec/",
|
|
136
|
+
r"(^|/)\.cowork-flow/workflow\.md$",
|
|
137
|
+
r"(^|/)AGENTS\.md$",
|
|
138
|
+
r"(^|/)CLAUDE\.md$",
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
for file_path in modified_files:
|
|
142
|
+
normalized_path = file_path.replace("\\", "/")
|
|
143
|
+
if any(re.search(pattern, normalized_path) for pattern in spec_patterns):
|
|
144
|
+
rule = rule_index.get("R-AG-002")
|
|
145
|
+
violations.append(
|
|
146
|
+
rule_violation(rule, file_path) if rule else _missing_rule(repo_root, "R-AG-002")
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return violations
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _check_premature_abstraction(
|
|
153
|
+
repo_root: Path,
|
|
154
|
+
diff_output: str,
|
|
155
|
+
rule_index: dict[str, dict],
|
|
156
|
+
) -> list[dict]:
|
|
157
|
+
"""R-AG-006: Check if premature abstraction was used"""
|
|
158
|
+
violations = []
|
|
159
|
+
|
|
160
|
+
# Detection patterns for abstraction
|
|
161
|
+
abstraction_patterns = [
|
|
162
|
+
r"def\s+base_", # base_ prefixed functions
|
|
163
|
+
r"class\s+Abstract", # Abstract prefixed classes
|
|
164
|
+
r"class\s+Base", # Base prefixed classes
|
|
165
|
+
r"class\s+Generic", # Generic prefixed classes
|
|
166
|
+
r"def\s+create_factory", # Factory pattern
|
|
167
|
+
r"def\s+get_instance", # Singleton pattern
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
for pattern in abstraction_patterns:
|
|
171
|
+
matches = re.findall(pattern, diff_output)
|
|
172
|
+
if matches:
|
|
173
|
+
rule = rule_index.get("R-AG-006")
|
|
174
|
+
violations.append(
|
|
175
|
+
rule_violation(rule, "", detail=f"detected pattern: {pattern}")
|
|
176
|
+
if rule
|
|
177
|
+
else _missing_rule(repo_root, "R-AG-006")
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return violations
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _check_unrequested_features(
|
|
184
|
+
repo_root: Path,
|
|
185
|
+
diff_output: str,
|
|
186
|
+
task_dir: Path,
|
|
187
|
+
rule_index: dict[str, dict],
|
|
188
|
+
) -> list[dict]:
|
|
189
|
+
"""R-AG-005: Check if unrequested features were introduced"""
|
|
190
|
+
violations = []
|
|
191
|
+
|
|
192
|
+
# Read PRD
|
|
193
|
+
prd_path = task_dir / "prd.md"
|
|
194
|
+
if not prd_path.exists():
|
|
195
|
+
return violations
|
|
196
|
+
|
|
197
|
+
prd_content = prd_path.read_text(encoding="utf-8")
|
|
198
|
+
|
|
199
|
+
# Check for obvious extra features
|
|
200
|
+
# This is a simple heuristic check, actual implementation may need more complex NLP analysis
|
|
201
|
+
feature_indicators = [
|
|
202
|
+
r"Added.*feature",
|
|
203
|
+
r"New.*functionality",
|
|
204
|
+
r"Implemented.*module",
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
for pattern in feature_indicators:
|
|
208
|
+
if re.search(pattern, diff_output) and not re.search(pattern, prd_content):
|
|
209
|
+
rule = rule_index.get("R-AG-005")
|
|
210
|
+
violations.append(
|
|
211
|
+
rule_violation(rule, "", detail=f"detected pattern: {pattern}")
|
|
212
|
+
if rule
|
|
213
|
+
else _missing_rule(repo_root, "R-AG-005")
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return violations
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main():
|
|
220
|
+
"""Main entry point"""
|
|
221
|
+
import argparse
|
|
222
|
+
|
|
223
|
+
parser = argparse.ArgumentParser(description="Validate implementation against spec")
|
|
224
|
+
parser.add_argument("--task-dir", required=True, help="Task directory path")
|
|
225
|
+
parser.add_argument("--repo-root", default=".", help="Repository root path")
|
|
226
|
+
|
|
227
|
+
args = parser.parse_args()
|
|
228
|
+
|
|
229
|
+
repo_root = Path(args.repo_root).resolve()
|
|
230
|
+
task_dir = Path(args.task_dir).resolve()
|
|
231
|
+
|
|
232
|
+
violations = validate_implementation(repo_root, task_dir)
|
|
233
|
+
|
|
234
|
+
if violations:
|
|
235
|
+
for v in violations:
|
|
236
|
+
print(json.dumps(v, ensure_ascii=False))
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
sys.exit(0)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
main()
|