cowork-flow 0.0.26 → 0.0.27

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 (26) hide show
  1. package/README.md +4 -2
  2. package/package.json +1 -1
  3. package/template/.agents/skills/party-mode-v2/SKILL.md +73 -0
  4. package/template/.claude/agents/cowork-implement.md +163 -4
  5. package/template/.claude/skills/party-mode-v2/SKILL.md +73 -0
  6. package/template/.cowork-flow/.version +1 -1
  7. package/template/.cowork-flow/config.yaml +16 -0
  8. package/template/.cowork-flow/scripts/common/archive_utils.py +1 -1
  9. package/template/.cowork-flow/scripts/common/config.py +93 -0
  10. package/template/.cowork-flow/scripts/common/errors.py +107 -0
  11. package/template/.cowork-flow/scripts/common/validate_coding_standards.py +179 -0
  12. package/template/.cowork-flow/scripts/common/validate_implementation.py +195 -0
  13. package/template/.cowork-flow/scripts/common/validate_rules.py +302 -0
  14. package/template/.cowork-flow/scripts/party_mode_v2.py +646 -0
  15. package/template/.cowork-flow/scripts/run.py +3 -0
  16. package/template/.cowork-flow/scripts/task.py +66 -0
  17. package/template/.cowork-flow/spec/party-mode-v2-actions.schema.json +43 -0
  18. package/template/.cowork-flow/spec/party-mode-v2-board.md +43 -0
  19. package/template/.cowork-flow/spec/registry.json +13 -0
  20. package/template/.cowork-flow/spec/rules.json +159 -0
  21. package/template/.cowork-flow/spec/rules.schema.json +67 -0
  22. package/template/.cowork-flow/spec/subagent-dispatch.md +2 -0
  23. package/template/.cowork-flow/workflow.md +6 -0
  24. package/template/.opencode/agents/cowork-implement.md +153 -0
  25. package/template/.opencode/commands/party-mode-v2.md +21 -0
  26. package/template/AGENTS.md +2 -6
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Coding standards reminder script.
4
+
5
+ Reads coding standards from markdown files and outputs a summary.
6
+ Does NOT perform validation - only provides guidance for Agent review.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ import subprocess
13
+ from pathlib import Path
14
+
15
+
16
+ def get_coding_standards_summary(
17
+ repo_root: Path,
18
+ task_dir: Path,
19
+ ) -> str:
20
+ """
21
+ Get coding standards summary based on changed files.
22
+
23
+ Args:
24
+ repo_root: Repository root directory
25
+ task_dir: Task directory
26
+
27
+ Returns:
28
+ Summary string of relevant coding standards
29
+ """
30
+ # Get modified files
31
+ modified_files = _get_modified_files(repo_root)
32
+ if not modified_files:
33
+ return ""
34
+
35
+ # Classify files
36
+ backend_files = [f for f in modified_files if _is_backend_file(f)]
37
+ frontend_files = [f for f in modified_files if _is_frontend_file(f)]
38
+
39
+ summaries = []
40
+
41
+ # Get backend standards summary
42
+ if backend_files:
43
+ backend_summary = _get_standards_summary(repo_root, "backend")
44
+ if backend_summary:
45
+ summaries.append(f"=== Backend Coding Standards ===\n{backend_summary}")
46
+
47
+ # Get frontend standards summary
48
+ if frontend_files:
49
+ frontend_summary = _get_standards_summary(repo_root, "frontend")
50
+ if frontend_summary:
51
+ summaries.append(f"=== Frontend Coding Standards ===\n{frontend_summary}")
52
+
53
+ return "\n\n".join(summaries)
54
+
55
+
56
+ def _get_modified_files(repo_root: Path) -> list[str]:
57
+ """Get list of modified files"""
58
+ try:
59
+ result = subprocess.run(
60
+ ["git", "diff", "--name-only"],
61
+ cwd=repo_root,
62
+ capture_output=True,
63
+ text=True,
64
+ )
65
+ if result.returncode == 0:
66
+ return [f for f in result.stdout.strip().split("\n") if f]
67
+ return []
68
+ except Exception:
69
+ return []
70
+
71
+
72
+ def _is_backend_file(file_path: str) -> bool:
73
+ """Check if file is a backend file"""
74
+ backend_patterns = [
75
+ r"\.py$",
76
+ r"\.java$",
77
+ r"\.go$",
78
+ r"\.rs$",
79
+ r"\.rb$",
80
+ ]
81
+ return any(re.search(p, file_path) for p in backend_patterns)
82
+
83
+
84
+ def _is_frontend_file(file_path: str) -> bool:
85
+ """Check if file is a frontend file"""
86
+ frontend_patterns = [
87
+ r"\.tsx$",
88
+ r"\.jsx$",
89
+ r"\.vue$",
90
+ r"\.svelte$",
91
+ ]
92
+ return any(re.search(p, file_path) for p in frontend_patterns)
93
+
94
+
95
+ def _get_standards_summary(repo_root: Path, category: str) -> str:
96
+ """
97
+ Get summary of coding standards from markdown files.
98
+
99
+ Args:
100
+ repo_root: Repository root directory
101
+ category: 'backend' or 'frontend'
102
+
103
+ Returns:
104
+ Summary string
105
+ """
106
+ spec_dir = repo_root / ".cowork-flow" / "spec" / category
107
+ if not spec_dir.exists():
108
+ return ""
109
+
110
+ summaries = []
111
+
112
+ for md_file in sorted(spec_dir.glob("*.md")):
113
+ if md_file.name == "index.md":
114
+ continue
115
+
116
+ content = md_file.read_text(encoding="utf-8")
117
+ rules = _extract_key_rules(content)
118
+
119
+ if rules:
120
+ summaries.append(f"File: {md_file.name}")
121
+ for rule in rules[:5]: # Limit to top 5 rules per file
122
+ summaries.append(f" - {rule}")
123
+ summaries.append("")
124
+
125
+ return "\n".join(summaries)
126
+
127
+
128
+ def _extract_key_rules(content: str) -> list[str]:
129
+ """Extract key rules from markdown content"""
130
+ rules = []
131
+
132
+ # Extract forbidden patterns (Must not, Should not, Do not, etc.)
133
+ # Also match Chinese patterns
134
+ forbidden_patterns = [
135
+ r"[-*]\s*(Must not|Should not|Do not|Never|Avoid)\s+(.+)",
136
+ r"[-*]\s*(禁止|不得|不能|不应|避免|不要|不可)\s*(.+)",
137
+ r"[-*]\s*(.+不得|.+不能|.+不应|.+避免|.+不要)\s*(.+)",
138
+ ]
139
+
140
+ for line in content.split("\n"):
141
+ line = line.strip()
142
+ if not line or line.startswith("#"):
143
+ continue
144
+
145
+ for pattern in forbidden_patterns:
146
+ match = re.search(pattern, line, re.IGNORECASE)
147
+ if match:
148
+ # Extract the rule text
149
+ rule_text = line.lstrip("-* ").strip()
150
+ if len(rule_text) > 5: # Skip very short rules
151
+ rules.append(rule_text[:80]) # Limit length
152
+ break # Only match one pattern per line
153
+
154
+ return rules[:10] # Return top 10 rules
155
+
156
+
157
+ def main():
158
+ """Main entry point"""
159
+ import argparse
160
+
161
+ parser = argparse.ArgumentParser(description="Get coding standards summary")
162
+ parser.add_argument("--task-dir", required=True, help="Task directory path")
163
+ parser.add_argument("--repo-root", default=".", help="Repository root path")
164
+
165
+ args = parser.parse_args()
166
+
167
+ repo_root = Path(args.repo_root).resolve()
168
+ task_dir = Path(args.task_dir).resolve()
169
+
170
+ summary = get_coding_standards_summary(repo_root, task_dir)
171
+
172
+ if summary:
173
+ print(summary)
174
+ else:
175
+ print("No relevant coding standards found for changed files.")
176
+
177
+
178
+ if __name__ == "__main__":
179
+ main()
@@ -0,0 +1,195 @@
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
+
17
+ def validate_implementation(
18
+ repo_root: Path,
19
+ task_dir: Path,
20
+ ) -> list[dict]:
21
+ """
22
+ Validate code changes against implementation rules.
23
+
24
+ Args:
25
+ repo_root: Repository root directory
26
+ task_dir: Task directory
27
+
28
+ Returns:
29
+ List of violations
30
+ """
31
+ violations = []
32
+
33
+ # Get git diff
34
+ diff_output = _get_git_diff(repo_root)
35
+ if not diff_output:
36
+ return violations
37
+
38
+ # Check forbidden action rules
39
+ violations.extend(_check_spec_file_modifications(diff_output, repo_root))
40
+ violations.extend(_check_premature_abstraction(diff_output))
41
+ violations.extend(_check_unrequested_features(diff_output, task_dir))
42
+
43
+ return violations
44
+
45
+
46
+ def _get_git_diff(repo_root: Path) -> str:
47
+ """Get git diff output"""
48
+ try:
49
+ result = subprocess.run(
50
+ ["git", "diff", "--name-only"],
51
+ cwd=repo_root,
52
+ capture_output=True,
53
+ text=True,
54
+ )
55
+ if result.returncode != 0:
56
+ return ""
57
+
58
+ # Get actual diff content
59
+ files = result.stdout.strip().split("\n")
60
+ if not files or files == [""]:
61
+ return ""
62
+
63
+ diff_result = subprocess.run(
64
+ ["git", "diff"] + files,
65
+ cwd=repo_root,
66
+ capture_output=True,
67
+ text=True,
68
+ )
69
+ return diff_result.stdout
70
+ except Exception:
71
+ return ""
72
+
73
+
74
+ def _check_spec_file_modifications(
75
+ diff_output: str,
76
+ repo_root: Path,
77
+ ) -> list[dict]:
78
+ """R-AG-002: Check if spec files were modified"""
79
+ violations = []
80
+
81
+ # Spec file path patterns
82
+ spec_patterns = [
83
+ r"\.cowork-flow/spec/",
84
+ r"\.cowork-flow/workflow\.md",
85
+ r"AGENTS\.md",
86
+ r"CLAUDE\.md",
87
+ ]
88
+
89
+ for pattern in spec_patterns:
90
+ if re.search(pattern, diff_output):
91
+ violations.append({
92
+ "rule_id": "R-AG-002",
93
+ "type": "forbidden_action",
94
+ "severity": "block",
95
+ "passed": False,
96
+ "message": "Subagent attempted to modify spec files",
97
+ "file": pattern,
98
+ "fix_hint": "Spec files can only be modified by main session",
99
+ })
100
+
101
+ return violations
102
+
103
+
104
+ def _check_premature_abstraction(diff_output: str) -> list[dict]:
105
+ """R-AG-006: Check if premature abstraction was used"""
106
+ violations = []
107
+
108
+ # Detection patterns for abstraction
109
+ abstraction_patterns = [
110
+ r"def\s+base_", # base_ prefixed functions
111
+ r"class\s+Abstract", # Abstract prefixed classes
112
+ r"class\s+Base", # Base prefixed classes
113
+ r"class\s+Generic", # Generic prefixed classes
114
+ r"def\s+create_factory", # Factory pattern
115
+ r"def\s+get_instance", # Singleton pattern
116
+ ]
117
+
118
+ for pattern in abstraction_patterns:
119
+ matches = re.findall(pattern, diff_output)
120
+ if matches:
121
+ violations.append({
122
+ "rule_id": "R-AG-006",
123
+ "type": "forbidden_action",
124
+ "severity": "warn",
125
+ "passed": False,
126
+ "message": f"Premature abstraction detected: {pattern}",
127
+ "file": "",
128
+ "fix_hint": "Keep code simple and direct, only abstract when reuse is confirmed",
129
+ })
130
+
131
+ return violations
132
+
133
+
134
+ def _check_unrequested_features(
135
+ diff_output: str,
136
+ task_dir: Path,
137
+ ) -> list[dict]:
138
+ """R-AG-005: Check if unrequested features were introduced"""
139
+ violations = []
140
+
141
+ # Read PRD
142
+ prd_path = task_dir / "prd.md"
143
+ if not prd_path.exists():
144
+ return violations
145
+
146
+ prd_content = prd_path.read_text(encoding="utf-8")
147
+
148
+ # Check for obvious extra features
149
+ # This is a simple heuristic check, actual implementation may need more complex NLP analysis
150
+ feature_indicators = [
151
+ r"Added.*feature",
152
+ r"New.*functionality",
153
+ r"Implemented.*module",
154
+ ]
155
+
156
+ for pattern in feature_indicators:
157
+ if re.search(pattern, diff_output) and not re.search(pattern, prd_content):
158
+ violations.append({
159
+ "rule_id": "R-AG-005",
160
+ "type": "forbidden_action",
161
+ "severity": "block",
162
+ "passed": False,
163
+ "message": "Unrequested feature introduced",
164
+ "file": "",
165
+ "fix_hint": "Only implement features explicitly required by the PRD",
166
+ })
167
+
168
+ return violations
169
+
170
+
171
+ def main():
172
+ """Main entry point"""
173
+ import argparse
174
+
175
+ parser = argparse.ArgumentParser(description="Validate implementation against spec")
176
+ parser.add_argument("--task-dir", required=True, help="Task directory path")
177
+ parser.add_argument("--repo-root", default=".", help="Repository root path")
178
+
179
+ args = parser.parse_args()
180
+
181
+ repo_root = Path(args.repo_root).resolve()
182
+ task_dir = Path(args.task_dir).resolve()
183
+
184
+ violations = validate_implementation(repo_root, task_dir)
185
+
186
+ if violations:
187
+ for v in violations:
188
+ print(json.dumps(v, ensure_ascii=False))
189
+ sys.exit(1)
190
+
191
+ sys.exit(0)
192
+
193
+
194
+ if __name__ == "__main__":
195
+ main()
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Rule validation script for cowork-flow workflow.
4
+
5
+ Validates rules at task start/complete checkpoints.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+
15
+
16
+ def validate_rules(
17
+ repo_root: Path,
18
+ scope: str,
19
+ task_dir: Path | None = None,
20
+ ) -> list[dict]:
21
+ """
22
+ Validate rules for given scope.
23
+
24
+ Args:
25
+ repo_root: Repository root path
26
+ scope: Validation scope (task_start, task_complete, check, implement)
27
+ task_dir: Optional task directory path
28
+
29
+ Returns:
30
+ List of violations (empty if all rules pass)
31
+ """
32
+ violations = []
33
+
34
+ # Load rules
35
+ rules_path = repo_root / ".cowork-flow" / "spec" / "rules.json"
36
+ if not rules_path.exists():
37
+ return violations
38
+
39
+ with open(rules_path) as f:
40
+ rules_data = json.load(f)
41
+
42
+ rules = rules_data.get("rules", [])
43
+
44
+ # Filter rules by scope
45
+ applicable_rules = [
46
+ r for r in rules
47
+ if r["scope"] == scope or r["scope"] == "all"
48
+ ]
49
+
50
+ # Validate each rule
51
+ for rule in applicable_rules:
52
+ violation = _validate_rule(rule, repo_root, task_dir)
53
+ if violation:
54
+ violations.append(violation)
55
+
56
+ return violations
57
+
58
+
59
+ def _validate_rule(
60
+ rule: dict,
61
+ repo_root: Path,
62
+ task_dir: Path | None,
63
+ ) -> dict | None:
64
+ """Validate a single rule. Returns violation dict if rule fails."""
65
+ rule_type = rule["type"]
66
+
67
+ if rule_type == "phase_gate":
68
+ return _validate_phase_gate(rule, repo_root, task_dir)
69
+ elif rule_type == "forbidden_action":
70
+ # Forbidden actions are validated at runtime, not at checkpoints
71
+ return None
72
+
73
+ return None
74
+
75
+
76
+ def _validate_phase_gate(
77
+ rule: dict,
78
+ repo_root: Path,
79
+ task_dir: Path | None,
80
+ ) -> dict | None:
81
+ """Validate a phase_gate rule."""
82
+ rule_id = rule["id"]
83
+
84
+ # R-WF-001 to R-WF-005: L2 readiness checks
85
+ if rule_id in ["R-WF-001", "R-WF-002", "R-WF-003", "R-WF-004", "R-WF-005"]:
86
+ if task_dir is None:
87
+ return None
88
+
89
+ # Check if task has L2 change
90
+ change_dir = _find_linked_change(repo_root, task_dir)
91
+ if change_dir is None:
92
+ return None
93
+
94
+ # Validate specific file requirements
95
+ if rule_id == "R-WF-001":
96
+ return _check_file_exists(change_dir / "proposal.md", rule, "proposal.md")
97
+ elif rule_id == "R-WF-002":
98
+ return _check_file_exists(change_dir / "spec.md", rule, "spec.md")
99
+ elif rule_id == "R-WF-003":
100
+ return _check_file_exists(change_dir / "design.md", rule, "design.md")
101
+ elif rule_id == "R-WF-004":
102
+ return _check_plan_link(repo_root, change_dir, rule)
103
+ elif rule_id == "R-WF-005":
104
+ return _check_task_link(change_dir, rule)
105
+
106
+ # R-WF-006: Implementation without failing test
107
+ if rule_id == "R-WF-006":
108
+ # This is a warn-level rule, just log
109
+ return None
110
+
111
+ # R-WF-007: Task completion without check
112
+ if rule_id == "R-WF-007":
113
+ if scope == "task_complete" and task_dir:
114
+ task_json = task_dir / "task.json"
115
+ if task_json.exists():
116
+ with open(task_json) as f:
117
+ task_data = json.load(f)
118
+ if task_data.get("status") != "review":
119
+ return {
120
+ "rule_id": rule_id,
121
+ "type": rule["type"],
122
+ "severity": rule["severity"],
123
+ "passed": False,
124
+ "message": rule["message"],
125
+ "file": str(task_json),
126
+ "fix_hint": rule["fix_hint"],
127
+ }
128
+
129
+ return None
130
+
131
+
132
+ def _find_linked_change(repo_root: Path, task_dir: Path) -> Path | None:
133
+ """Find the change directory linked to a task."""
134
+ changes_dir = repo_root / ".cowork-flow" / "changes"
135
+ if not changes_dir.exists():
136
+ return None
137
+
138
+ task_name = task_dir.name
139
+ for change_dir in changes_dir.iterdir():
140
+ if change_dir.is_dir() and change_dir.name != "archive":
141
+ change_yaml = change_dir / "change.yaml"
142
+ if change_yaml.exists():
143
+ with open(change_yaml) as f:
144
+ content = f.read()
145
+ if task_name in content:
146
+ return change_dir
147
+
148
+ return None
149
+
150
+
151
+ def _check_file_exists(
152
+ file_path: Path,
153
+ rule: dict,
154
+ filename: str,
155
+ ) -> dict | None:
156
+ """Check if a required file exists and is not empty."""
157
+ if not file_path.exists():
158
+ return {
159
+ "rule_id": rule["id"],
160
+ "type": rule["type"],
161
+ "severity": rule["severity"],
162
+ "passed": False,
163
+ "message": f"L2 readiness: {filename} is missing",
164
+ "file": str(file_path),
165
+ "fix_hint": rule["fix_hint"],
166
+ }
167
+
168
+ if file_path.stat().st_size == 0:
169
+ return {
170
+ "rule_id": rule["id"],
171
+ "type": rule["type"],
172
+ "severity": rule["severity"],
173
+ "passed": False,
174
+ "message": f"L2 readiness: {filename} is empty",
175
+ "file": str(file_path),
176
+ "fix_hint": rule["fix_hint"],
177
+ }
178
+
179
+ return None
180
+
181
+
182
+ def _check_plan_link(
183
+ repo_root: Path,
184
+ change_dir: Path,
185
+ rule: dict,
186
+ ) -> dict | None:
187
+ """Check if change has a plan link."""
188
+ change_yaml = change_dir / "change.yaml"
189
+ if not change_yaml.exists():
190
+ return {
191
+ "rule_id": rule["id"],
192
+ "type": rule["type"],
193
+ "severity": rule["severity"],
194
+ "passed": False,
195
+ "message": "L2 readiness: change.yaml is missing",
196
+ "file": str(change_yaml),
197
+ "fix_hint": rule["fix_hint"],
198
+ }
199
+
200
+ with open(change_yaml) as f:
201
+ content = f.read()
202
+
203
+ if "plan:" not in content or "plan: null" in content:
204
+ return {
205
+ "rule_id": rule["id"],
206
+ "type": rule["type"],
207
+ "severity": rule["severity"],
208
+ "passed": False,
209
+ "message": "L2 readiness: plan link is missing",
210
+ "file": str(change_yaml),
211
+ "fix_hint": rule["fix_hint"],
212
+ }
213
+
214
+ return None
215
+
216
+
217
+ def _check_task_link(
218
+ change_dir: Path,
219
+ rule: dict,
220
+ ) -> dict | None:
221
+ """Check if change has a task link."""
222
+ change_yaml = change_dir / "change.yaml"
223
+ if not change_yaml.exists():
224
+ return {
225
+ "rule_id": rule["id"],
226
+ "type": rule["type"],
227
+ "severity": rule["severity"],
228
+ "passed": False,
229
+ "message": "L2 readiness: change.yaml is missing",
230
+ "file": str(change_yaml),
231
+ "fix_hint": rule["fix_hint"],
232
+ }
233
+
234
+ with open(change_yaml) as f:
235
+ content = f.read()
236
+
237
+ if "tasks:" not in content:
238
+ return {
239
+ "rule_id": rule["id"],
240
+ "type": rule["type"],
241
+ "severity": rule["severity"],
242
+ "passed": False,
243
+ "message": "L2 readiness: task link is missing",
244
+ "file": str(change_yaml),
245
+ "fix_hint": rule["fix_hint"],
246
+ }
247
+
248
+ return None
249
+
250
+
251
+ def log_violations(
252
+ violations: list[dict],
253
+ scope: str,
254
+ task_dir: Path | None,
255
+ repo_root: Path,
256
+ ) -> None:
257
+ """Log violations to rule-events.jsonl."""
258
+ if not violations:
259
+ return
260
+
261
+ log_dir = repo_root / ".cowork-flow" / "logs"
262
+ log_dir.mkdir(parents=True, exist_ok=True)
263
+ log_path = log_dir / "rule-events.jsonl"
264
+
265
+ log_entry = {
266
+ "timestamp": datetime.now(timezone.utc).isoformat(),
267
+ "scope": scope,
268
+ "task_dir": str(task_dir) if task_dir else None,
269
+ "violations": violations,
270
+ }
271
+
272
+ with open(log_path, "a") as f:
273
+ f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
274
+
275
+
276
+ def main():
277
+ """Main entry point for CLI usage."""
278
+ import argparse
279
+
280
+ parser = argparse.ArgumentParser(description="Validate workflow rules")
281
+ parser.add_argument("scope", choices=["task_start", "task_complete", "check", "implement"])
282
+ parser.add_argument("--task-dir", help="Task directory path")
283
+ parser.add_argument("--repo-root", default=".", help="Repository root path")
284
+
285
+ args = parser.parse_args()
286
+
287
+ repo_root = Path(args.repo_root).resolve()
288
+ task_dir = Path(args.task_dir).resolve() if args.task_dir else None
289
+
290
+ violations = validate_rules(repo_root, args.scope, task_dir)
291
+
292
+ if violations:
293
+ log_violations(violations, args.scope, task_dir, repo_root)
294
+ for v in violations:
295
+ print(json.dumps(v, ensure_ascii=False))
296
+ sys.exit(1)
297
+
298
+ sys.exit(0)
299
+
300
+
301
+ if __name__ == "__main__":
302
+ main()