cowork-flow 0.0.31 → 0.0.32
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/package.json +1 -1
- package/template/.codex/agents/cowork-check.toml +7 -0
- package/template/.codex/agents/cowork-implement.toml +6 -0
- package/template/.codex/hooks/inject-workflow-state.py +0 -0
- package/template/.codex/hooks.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +254 -132
- package/template/.cowork-flow/scripts/common/gates/validate_rules.py +13 -5
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/runtime/run +2 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_coding_standaries.py +316 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_rules.py +13 -5
- package/template/AGENTS.md +22 -12
package/package.json
CHANGED
|
@@ -38,6 +38,13 @@ Rules:
|
|
|
38
38
|
- MUST NOT commit, archive, or mutate cowork-flow task state.
|
|
39
39
|
- Report findings, changed files, and exact verification commands.
|
|
40
40
|
- Include test intent findings that map tests back to PRD acceptance or regression behavior.
|
|
41
|
+
- BEFORE declaring PRD acceptance passed, run the spec-driven coding gate and
|
|
42
|
+
treat a nonzero exit as a hard blocker:
|
|
43
|
+
./.cowork-flow/run python .cowork-flow/scripts/common/gates/validate_coding_standaries.py --validate --repo-root .
|
|
44
|
+
This naturally enforces every rule the user added to
|
|
45
|
+
.cowork-flow/spec/backend/*.md and spec/frontend/*.md — they are not just
|
|
46
|
+
documentation but active validators. On violation, either fix the code or
|
|
47
|
+
explicitly discard the rule from the spec; do not skip the gate.
|
|
41
48
|
"""
|
|
42
49
|
|
|
43
50
|
[features]
|
|
@@ -40,6 +40,12 @@ Rules:
|
|
|
40
40
|
- Keep edits inside requested scope.
|
|
41
41
|
- Do not treat shallow tests as TDD evidence; red-green-refactor proof must include redCommand, redExitCode, greenCommand, greenExitCode, and PRD acceptance ID mapping.
|
|
42
42
|
- Report changed files and exact verification commands.
|
|
43
|
+
- BEFORE editing any source file, run the spec-driven coding gate and treat a
|
|
44
|
+
nonzero exit as a hard blocker:
|
|
45
|
+
./.cowork-flow/run python .cowork-flow/scripts/common/gates/validate_coding_standaries.py --validate --repo-root .
|
|
46
|
+
Parse every *.md under .cowork-flow/spec/backend and spec/frontend; rules
|
|
47
|
+
declared there activate validators dynamically. On violation, fix the code
|
|
48
|
+
(do not disable the rule) and re-run until exit code 0.
|
|
43
49
|
"""
|
|
44
50
|
|
|
45
51
|
[features]
|
|
File without changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.32
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Coding standards helper.
|
|
2
|
+
"""Spec-driven coding standards — gates honor user-edited spec files.
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
Files under ``.cowork-flow/spec/backend/``, ``spec/frontend/`` and
|
|
5
|
+
``spec/guides/`` are parsed at runtime into review-checklist rules and matched
|
|
6
|
+
against a small registry of registered validators. Anything a user adds to
|
|
7
|
+
those files is activated at the next gate run — the gate re-reads the
|
|
8
|
+
markdown every time.
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
11
|
from __future__ import annotations
|
|
@@ -11,170 +14,289 @@ import json
|
|
|
11
14
|
import re
|
|
12
15
|
import sys
|
|
13
16
|
from pathlib import Path
|
|
17
|
+
from typing import Callable, Iterable, Sequence
|
|
14
18
|
|
|
15
|
-
from .coding_standards import validate_changed_files
|
|
19
|
+
from common.gates.coding_standards import validate_changed_files
|
|
16
20
|
from common.git.git_snapshot import collect_changed_files, collect_changed_paths
|
|
17
21
|
|
|
22
|
+
# Source files our spec validators inspect.
|
|
23
|
+
_EXTENSIONS = {
|
|
24
|
+
".py", ".pyi",
|
|
25
|
+
".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx",
|
|
26
|
+
".java", ".go", ".rs", ".rb", ".cs", ".kt", ".php",
|
|
27
|
+
".yaml", ".yml", ".json", ".toml",
|
|
28
|
+
}
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
task_dir: Path,
|
|
22
|
-
) -> str:
|
|
23
|
-
"""
|
|
24
|
-
Get coding standards summary based on changed files.
|
|
30
|
+
_BACKEND_SUFFIXES = re.compile(r"\.(py|java|go|rs|rb|cs|kt|php)$")
|
|
31
|
+
_FRONTEND_SUFFIXES = re.compile(r"\.(tsx|jsx|vue|svelte|ts|js|mjs|cjs|css|scss)$")
|
|
25
32
|
|
|
26
|
-
Args:
|
|
27
|
-
repo_root: Repository root directory
|
|
28
|
-
task_dir: Task directory
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"""
|
|
33
|
-
modified_files = collect_changed_paths(repo_root)
|
|
34
|
-
if not modified_files:
|
|
35
|
-
return ""
|
|
34
|
+
def _is_backend_file(rel_path: str) -> bool:
|
|
35
|
+
return bool(_BACKEND_SUFFIXES.search(rel_path))
|
|
36
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
37
|
|
|
41
|
-
|
|
38
|
+
def _is_frontend_file(rel_path: str) -> bool:
|
|
39
|
+
return bool(_FRONTEND_SUFFIXES.search(rel_path))
|
|
42
40
|
|
|
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
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if frontend_summary:
|
|
53
|
-
summaries.append(f"=== Frontend Coding Standards ===\n{frontend_summary}")
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Registered NL-backed validators
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
54
45
|
|
|
55
|
-
|
|
46
|
+
# Each entry: (compiled-pattern, category, validator-fn).
|
|
47
|
+
# ``validator(content, rel_path)`` yields ``(lineno, message, fix_hint)``.
|
|
48
|
+
_NL_VALIDATORS: list[tuple[re.Pattern, str, Callable[[str, str], Iterable[tuple[int, str, str]]]]] = []
|
|
56
49
|
|
|
57
50
|
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
return
|
|
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.
|
|
51
|
+
def register_nl_validator(pattern: str) -> Callable[[Callable], Callable]:
|
|
52
|
+
def decorator(fn: Callable) -> Callable:
|
|
53
|
+
category = getattr(fn, "_nl_category", "backend")
|
|
54
|
+
_NL_VALIDATORS.append((re.compile(pattern, re.IGNORECASE), category, fn))
|
|
55
|
+
return fn
|
|
56
|
+
return decorator
|
|
92
57
|
|
|
93
|
-
Args:
|
|
94
|
-
repo_root: Repository root directory
|
|
95
|
-
category: 'backend' or 'frontend'
|
|
96
58
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
59
|
+
def nl_category(category: str) -> Callable[[Callable], Callable]:
|
|
60
|
+
def decorator(fn: Callable) -> Callable:
|
|
61
|
+
fn._nl_category = category
|
|
62
|
+
return fn
|
|
63
|
+
return decorator
|
|
64
|
+
|
|
103
65
|
|
|
104
|
-
|
|
66
|
+
@register_nl_validator(r"(硬编码|hardcode|hard.code|secret|password|api[_\s]?key|token)")
|
|
67
|
+
@nl_category("backend")
|
|
68
|
+
def _no_hardcoded_secrets(content: str, rel_path: str) -> Iterable[tuple[int, str, str]]:
|
|
69
|
+
pattern = re.compile(
|
|
70
|
+
r"""(?ix)
|
|
71
|
+
(?:password|passwd|secret|api[_\s]?key|token|access[_\s]?key|aws_access_key_id|aws_secret_access_key)
|
|
72
|
+
\s*[:=]\s*["'][^"'}{)($]{3,}["']
|
|
73
|
+
"""
|
|
74
|
+
)
|
|
75
|
+
for lineno, raw in enumerate(content.splitlines(), start=1):
|
|
76
|
+
if pattern.search(raw) and "process.env" not in raw and "os.environ" not in raw:
|
|
77
|
+
yield (lineno,
|
|
78
|
+
"Possible hard-coded secret in source; spec forbids it.",
|
|
79
|
+
"Read from configuration or environment variables.")
|
|
105
80
|
|
|
106
|
-
|
|
107
|
-
|
|
81
|
+
|
|
82
|
+
@register_nl_validator(r"(print\b|console\.(log|info|debug)|日志|logger|logging|结构化日志)")
|
|
83
|
+
@nl_category("backend")
|
|
84
|
+
def _no_debug_prints(content: str, rel_path: str) -> Iterable[tuple[int, str, str]]:
|
|
85
|
+
if rel_path.endswith(".py"):
|
|
86
|
+
pat = re.compile(r"(?<!\w)print\s*\(")
|
|
87
|
+
elif rel_path.endswith((".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx")):
|
|
88
|
+
pat = re.compile(r"\bconsole\.(log|info|debug)\s*\(")
|
|
89
|
+
else:
|
|
90
|
+
return
|
|
91
|
+
for lineno, raw in enumerate(content.splitlines(), start=1):
|
|
92
|
+
if pat.search(raw):
|
|
93
|
+
yield (lineno, "Debug print found; spec prefers structured logger.",
|
|
94
|
+
"Use a structured logger instead.")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@register_nl_validator(r"(吞|swallow|静默|except|空\s*except)")
|
|
98
|
+
@nl_category("backend")
|
|
99
|
+
def _no_silent_except(content: str, rel_path: str) -> Iterable[tuple[int, str, str]]:
|
|
100
|
+
if not rel_path.endswith(".py"):
|
|
101
|
+
return
|
|
102
|
+
lines = content.splitlines()
|
|
103
|
+
for lineno, raw in enumerate(lines, start=1):
|
|
104
|
+
if re.match(r"^\s*except\b\s*.*:\s*(#.*)?$", raw):
|
|
105
|
+
nxt = lines[lineno].strip() if lineno < len(lines) else ""
|
|
106
|
+
if nxt in {"pass", "...", ""} or nxt.startswith(("pass ", "...")):
|
|
107
|
+
yield (lineno, "Empty exception handler silently swallows errors.",
|
|
108
|
+
"Log or re-raise the exception, or narrow the except types.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@register_nl_validator(r"(硬编码|hardcode|明文|magic\s+number)")
|
|
112
|
+
@nl_category("frontend")
|
|
113
|
+
def _no_magic_values_frontend(content: str, rel_path: str) -> Iterable[tuple[int, str, str]]:
|
|
114
|
+
pat = re.compile(r"""(?ix)(?:magic\s+number|hardcode|硬编码)""")
|
|
115
|
+
for lineno, raw in enumerate(content.splitlines(), start=1):
|
|
116
|
+
if pat.search(raw):
|
|
117
|
+
yield (lineno, "Hard-coded magic value in frontend source; spec forbids it.")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Spec parser
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
_RULE_PATTERNS = [
|
|
125
|
+
re.compile(r"(?i)(?:^|\s)(?:must\s+not|should\s+not|do\s+not|never|avoid)\s+(.+)$"),
|
|
126
|
+
re.compile(r"(?:^|\s)(?:禁止|不得|不能|不应|避免|不要|不可|不许)\s*(.*)$"),
|
|
127
|
+
re.compile(r"(?i)(?:^|\s)(?:must|should|shall|always|应当|应该|需要)\s+(.+)$"),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _rule_text(line: str) -> str | None:
|
|
132
|
+
line = line.strip().lstrip("-* ").strip()
|
|
133
|
+
if not line or line.startswith("#"):
|
|
134
|
+
return None
|
|
135
|
+
for pat in _RULE_PATTERNS:
|
|
136
|
+
m = pat.search(line)
|
|
137
|
+
if m:
|
|
138
|
+
text = m.group(m.lastindex).strip(" .。")
|
|
139
|
+
if len(text) > 3:
|
|
140
|
+
return text
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def load_spec_rules(
|
|
145
|
+
repo_root,
|
|
146
|
+
categories: tuple[str, ...] = ("backend", "frontend"),
|
|
147
|
+
) -> list[dict]:
|
|
148
|
+
"""Return every natural-language rule declared under ``spec/<category>/``.
|
|
149
|
+
|
|
150
|
+
Re-reads markdown on every call, so user edits take effect without restart.
|
|
151
|
+
"""
|
|
152
|
+
out: list[dict] = []
|
|
153
|
+
repo_root = Path(repo_root)
|
|
154
|
+
for category in categories:
|
|
155
|
+
spec_dir = repo_root / ".cowork-flow" / "spec" / category
|
|
156
|
+
if not spec_dir.is_dir():
|
|
108
157
|
continue
|
|
158
|
+
for md in sorted(spec_dir.glob("*.md")):
|
|
159
|
+
if md.name == "index.md":
|
|
160
|
+
continue
|
|
161
|
+
text = md.read_text(encoding="utf-8")
|
|
162
|
+
for lineno, raw in enumerate(text.splitlines(), start=1):
|
|
163
|
+
rule = _rule_text(raw)
|
|
164
|
+
if rule is None:
|
|
165
|
+
continue
|
|
166
|
+
out.append({
|
|
167
|
+
"category": category,
|
|
168
|
+
"source": f".cowork-flow/spec/{category}/{md.name}",
|
|
169
|
+
"line": lineno,
|
|
170
|
+
"text": rule,
|
|
171
|
+
"validators": [
|
|
172
|
+
pat.pattern for pat, cat, _ in _NL_VALIDATORS
|
|
173
|
+
if cat in (category, "backend") and re.search(pat, rule)
|
|
174
|
+
],
|
|
175
|
+
})
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Public API
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
109
182
|
|
|
110
|
-
content = md_file.read_text(encoding="utf-8")
|
|
111
|
-
rules = _extract_key_rules(content)
|
|
112
183
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
184
|
+
def get_coding_standards_summary(
|
|
185
|
+
repo_root: Path,
|
|
186
|
+
task_dir: Path,
|
|
187
|
+
) -> str:
|
|
188
|
+
"""Render the active user-authored rules as a checklist (read-only)."""
|
|
189
|
+
changed = collect_changed_paths(repo_root)
|
|
190
|
+
if not changed:
|
|
191
|
+
return ""
|
|
118
192
|
|
|
119
|
-
|
|
193
|
+
backend = [p for p in changed if _is_backend_file(p)]
|
|
194
|
+
frontend = [p for p in changed if _is_frontend_file(p)]
|
|
195
|
+
if not backend and not frontend:
|
|
196
|
+
return ""
|
|
120
197
|
|
|
198
|
+
out: list[str] = []
|
|
199
|
+
if backend:
|
|
200
|
+
rules = load_spec_rules(repo_root, ("backend",))
|
|
201
|
+
if rules:
|
|
202
|
+
out.append("=== Backend spec rules — VALIDATED during check gate ===")
|
|
203
|
+
for r in rules:
|
|
204
|
+
out.append(f"- [{r['source']}:{r['line']}] {r['text']}")
|
|
205
|
+
if frontend:
|
|
206
|
+
rules = load_spec_rules(repo_root, ("frontend",))
|
|
207
|
+
if rules:
|
|
208
|
+
out.append("=== Frontend spec rules — VALIDATED during check gate ===")
|
|
209
|
+
for r in rules:
|
|
210
|
+
out.append(f"- [{r['source']}:{r['line']}] {r['text']}")
|
|
211
|
+
return "\n".join(out)
|
|
121
212
|
|
|
122
|
-
def _extract_key_rules(content: str) -> list[str]:
|
|
123
|
-
"""Extract key rules from markdown content"""
|
|
124
|
-
rules = []
|
|
125
213
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
r"[-*]\s*(.+不得|.+不能|.+不应|.+避免|.+不要)\s*(.+)",
|
|
132
|
-
]
|
|
214
|
+
def validate_coding_standards(
|
|
215
|
+
repo_root: Path,
|
|
216
|
+
task_dir: Path | None = None,
|
|
217
|
+
) -> list[dict]:
|
|
218
|
+
"""Run all coding-standard checks — hardcoded UTF-8 + spec-activated NL rules.
|
|
133
219
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
220
|
+
The NL rules only run when the user's spec files declare a matching rule
|
|
221
|
+
text, making spec files genuinely editable quality gates.
|
|
222
|
+
"""
|
|
223
|
+
violations: list[dict] = []
|
|
138
224
|
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
225
|
+
# Phase 1: hardcoded UTF-8 / IO checks always run.
|
|
226
|
+
violations.extend(validate_changed_files(repo_root, collect_changed_files(repo_root)))
|
|
147
227
|
|
|
148
|
-
|
|
228
|
+
# Phase 2: spec-driven validators — activated by user-edited spec text.
|
|
229
|
+
spec_rules = load_spec_rules(repo_root)
|
|
230
|
+
if not spec_rules:
|
|
231
|
+
return violations
|
|
149
232
|
|
|
233
|
+
activated: set[str] = set()
|
|
234
|
+
for rule in spec_rules:
|
|
235
|
+
activated.update(rule.get("validators", []))
|
|
236
|
+
if not activated:
|
|
237
|
+
return violations
|
|
150
238
|
|
|
151
|
-
|
|
152
|
-
|
|
239
|
+
for changed_file in collect_changed_files(repo_root):
|
|
240
|
+
file_path = repo_root / changed_file.path
|
|
241
|
+
if not file_path.is_file():
|
|
242
|
+
continue
|
|
243
|
+
suffix = file_path.suffix.lower()
|
|
244
|
+
if suffix not in _EXTENSIONS:
|
|
245
|
+
continue
|
|
246
|
+
try:
|
|
247
|
+
content = file_path.read_text(encoding="utf-8")
|
|
248
|
+
except (OSError, UnicodeDecodeError):
|
|
249
|
+
continue
|
|
250
|
+
for pat, _category, validator in _NL_VALIDATORS:
|
|
251
|
+
if pat.pattern not in activated:
|
|
252
|
+
continue
|
|
253
|
+
try:
|
|
254
|
+
results = validator(content, changed_file.path)
|
|
255
|
+
except Exception: # noqa: BLE001 — defensively isolate parsing
|
|
256
|
+
continue
|
|
257
|
+
for lineno, msg, *rest in results:
|
|
258
|
+
fix_hint = rest[0] if rest else ""
|
|
259
|
+
violations.append({
|
|
260
|
+
"rule_id": f"SPEC-{validator.__name__}",
|
|
261
|
+
"type": "spec_coding_standard",
|
|
262
|
+
"severity": "block",
|
|
263
|
+
"passed": False,
|
|
264
|
+
"message": msg,
|
|
265
|
+
"file": changed_file.path,
|
|
266
|
+
"line": lineno,
|
|
267
|
+
"fix_hint": fix_hint,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
return violations
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def main() -> None:
|
|
153
274
|
import argparse
|
|
154
275
|
|
|
155
|
-
parser = argparse.ArgumentParser(description="
|
|
156
|
-
parser.add_argument("--task-dir",
|
|
157
|
-
parser.add_argument("--repo-root", default=".", help="
|
|
158
|
-
parser.add_argument("--
|
|
276
|
+
parser = argparse.ArgumentParser(description="Spec-driven coding gate")
|
|
277
|
+
parser.add_argument("--task-dir", help="Task directory")
|
|
278
|
+
parser.add_argument("--repo-root", default=".", help="Repo root path")
|
|
279
|
+
parser.add_argument("--summarize", action="store_true")
|
|
280
|
+
parser.add_argument("--list", action="store_true", help="List parsed spec rules")
|
|
281
|
+
parser.add_argument("--validate", action="store_true", help="Run spec validators vs changed files")
|
|
159
282
|
|
|
160
283
|
args = parser.parse_args()
|
|
161
|
-
|
|
162
284
|
repo_root = Path(args.repo_root).resolve()
|
|
163
|
-
task_dir = Path(args.task_dir).resolve()
|
|
164
|
-
|
|
285
|
+
task_dir = Path(args.task_dir).resolve() if args.task_dir else repo_root
|
|
286
|
+
|
|
287
|
+
if args.list:
|
|
288
|
+
for r in load_spec_rules(repo_root):
|
|
289
|
+
print(json.dumps(r, ensure_ascii=False))
|
|
290
|
+
return
|
|
291
|
+
if args.summarize:
|
|
292
|
+
print(get_coding_standards_summary(repo_root, task_dir) or "No spec rules activated.")
|
|
293
|
+
return
|
|
165
294
|
if args.validate:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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.")
|
|
295
|
+
v = validate_coding_standards(repo_root, task_dir)
|
|
296
|
+
for x in v:
|
|
297
|
+
print(json.dumps(x, ensure_ascii=False))
|
|
298
|
+
sys.exit(1 if v else 0)
|
|
299
|
+
print(get_coding_standards_summary(repo_root, task_dir) or "No spec rules activated.")
|
|
178
300
|
|
|
179
301
|
|
|
180
302
|
if __name__ == "__main__":
|
|
@@ -306,7 +306,7 @@ def _validate_phase_gate(
|
|
|
306
306
|
elif rule_id == "R-WF-004":
|
|
307
307
|
return _check_plan_link(repo_root, change_dir, rule)
|
|
308
308
|
elif rule_id == "R-WF-005":
|
|
309
|
-
return _check_task_link(change_dir, rule)
|
|
309
|
+
return _check_task_link(change_dir, task_dir, rule)
|
|
310
310
|
|
|
311
311
|
# R-WF-006: Implementation without failing test
|
|
312
312
|
if rule_id == "R-WF-006":
|
|
@@ -395,9 +395,11 @@ def _check_plan_link(
|
|
|
395
395
|
|
|
396
396
|
def _check_task_link(
|
|
397
397
|
change_dir: Path,
|
|
398
|
+
task_dir: Path,
|
|
398
399
|
rule: dict,
|
|
399
400
|
) -> dict | None:
|
|
400
|
-
"""Check if change
|
|
401
|
+
"""Check if change references ``task_dir`` via ``task:``/``tasks:`` or any
|
|
402
|
+
list-item match in ``change.yaml``."""
|
|
401
403
|
change_yaml = change_dir / "change.yaml"
|
|
402
404
|
if not change_yaml.exists():
|
|
403
405
|
return rule_violation(rule, change_yaml, detail="change.yaml is missing")
|
|
@@ -405,10 +407,16 @@ def _check_task_link(
|
|
|
405
407
|
with open(change_yaml, encoding="utf-8") as f:
|
|
406
408
|
content = f.read()
|
|
407
409
|
|
|
408
|
-
if
|
|
409
|
-
return
|
|
410
|
+
if _metadata_has_link(content, ("task", "tasks")):
|
|
411
|
+
return None
|
|
410
412
|
|
|
411
|
-
|
|
413
|
+
task_name = task_dir.name
|
|
414
|
+
for raw in content.splitlines():
|
|
415
|
+
stripped = raw.strip().lstrip("-").strip().strip("#").strip().strip('"').strip("'").strip(",")
|
|
416
|
+
if stripped == task_name:
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
return rule_violation(rule, change_yaml, detail="task link is missing")
|
|
412
420
|
|
|
413
421
|
|
|
414
422
|
def log_violations(
|
|
@@ -47,6 +47,8 @@ select_python() {
|
|
|
47
47
|
|
|
48
48
|
select_python
|
|
49
49
|
|
|
50
|
+
SELECTED_PYTHON_ARG=${SELECTED_PYTHON_ARG:-}
|
|
51
|
+
|
|
50
52
|
if [ -n "$SELECTED_PYTHON_ARG" ]; then
|
|
51
53
|
export PYTHONPATH="$RUNTIME_SCRIPTS${PYTHONPATH:+:$PYTHONPATH}"
|
|
52
54
|
exec "$SELECTED_PYTHON" "$SELECTED_PYTHON_ARG" "$RUNNER_SCRIPT" "$@"
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Spec-driven coding standards (zcode hooks runtime).
|
|
3
|
+
|
|
4
|
+
ZCode-flavored copy: relative imports (``.coding_standaries``), same CLI
|
|
5
|
+
interface as the original (--task-dir required). Behaves identically to
|
|
6
|
+
``template/.cowork-flow/scripts/common/gates/validate_coding_standaries.py``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Callable, Iterable, Sequence
|
|
16
|
+
|
|
17
|
+
# Standalone-CLI import fallback: when loaded directly (not as a
|
|
18
|
+
# package), the relative imports cannot resolve. Patch ``__package__``
|
|
19
|
+
# and re-add this directory to ``sys.path`` so the absolute-style
|
|
20
|
+
# fallbacks work identically to the relative ones.
|
|
21
|
+
if __name__ == "__main__" and __package__ in (None, ""):
|
|
22
|
+
import sys as _sys
|
|
23
|
+
_dir = Path(__file__).resolve().parent
|
|
24
|
+
_parent = _dir.parent # common/gates -> common
|
|
25
|
+
_grand = _parent.parent # common
|
|
26
|
+
if str(_grand) not in _sys.path:
|
|
27
|
+
_sys.path.insert(0, str(_grand))
|
|
28
|
+
if str(_parent) not in _sys.path:
|
|
29
|
+
_sys.path.insert(0, str(_parent))
|
|
30
|
+
__package__ = "common.gates"
|
|
31
|
+
|
|
32
|
+
from .coding_standards import validate_changed_files
|
|
33
|
+
from common.git.git_snapshot import collect_changed_files, collect_changed_paths
|
|
34
|
+
|
|
35
|
+
# Source files our spec validators inspect.
|
|
36
|
+
_EXTENSIONS = {
|
|
37
|
+
".py", ".pyi",
|
|
38
|
+
".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx",
|
|
39
|
+
".java", ".go", ".rs", ".rb", ".cs", ".kt", ".php",
|
|
40
|
+
".yaml", ".yml", ".json", ".toml",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_BACKEND_SUFFIXES = re.compile(r"\.(py|java|go|rs|rb|cs|kt|php)$")
|
|
44
|
+
_FRONTEND_SUFFIXES = re.compile(r"\.(tsx|jsx|vue|svelte|ts|js|mjs|cjs|css|scss)$")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_backend_file(rel_path: str) -> bool:
|
|
48
|
+
return bool(_BACKEND_SUFFIXES.search(rel_path))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_frontend_file(rel_path: str) -> bool:
|
|
52
|
+
return bool(_FRONTEND_SUFFIXES.search(rel_path))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Registered NL-backed validators
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
# Each entry: (compiled-pattern, category, validator-fn).
|
|
60
|
+
# ``validator(content, rel_path)`` yields ``(lineno, message, fix_hint)``.
|
|
61
|
+
_NL_VALIDATORS: list[tuple[re.Pattern, str, Callable[[str, str], Iterable[tuple[int, str, str]]]]] = []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def register_nl_validator(pattern: str) -> Callable[[Callable], Callable]:
|
|
65
|
+
def decorator(fn: Callable) -> Callable:
|
|
66
|
+
category = getattr(fn, "_nl_category", "backend")
|
|
67
|
+
_NL_VALIDATORS.append((re.compile(pattern, re.IGNORECASE), category, fn))
|
|
68
|
+
return fn
|
|
69
|
+
return decorator
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def nl_category(category: str) -> Callable[[Callable], Callable]:
|
|
73
|
+
def decorator(fn: Callable) -> Callable:
|
|
74
|
+
fn._nl_category = category
|
|
75
|
+
return fn
|
|
76
|
+
return decorator
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@register_nl_validator(r"(硬编码|hardcode|hard.code|secret|password|api[_\s]?key|token)")
|
|
80
|
+
@nl_category("backend")
|
|
81
|
+
def _no_hardcoded_secrets(content: str, rel_path: str) -> Iterable[tuple[int, str, str]]:
|
|
82
|
+
pattern = re.compile(
|
|
83
|
+
r"""(?ix)
|
|
84
|
+
(?:password|passwd|secret|api[_\s]?key|token|access[_\s]?key|aws_access_key_id|aws_secret_access_key)
|
|
85
|
+
\s*[:=]\s*["'][^"'}{)($]{3,}["']
|
|
86
|
+
"""
|
|
87
|
+
)
|
|
88
|
+
for lineno, raw in enumerate(content.splitlines(), start=1):
|
|
89
|
+
if pattern.search(raw) and "process.env" not in raw and "os.environ" not in raw:
|
|
90
|
+
yield (lineno,
|
|
91
|
+
"Possible hard-coded secret in source; spec forbids it.",
|
|
92
|
+
"Read from configuration or environment variables.")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@register_nl_validator(r"(print\b|console\.(log|info|debug)|日志|logger|logging|结构化日志)")
|
|
96
|
+
@nl_category("backend")
|
|
97
|
+
def _no_debug_prints(content: str, rel_path: str) -> Iterable[tuple[int, str, str]]:
|
|
98
|
+
if rel_path.endswith(".py"):
|
|
99
|
+
pat = re.compile(r"(?<!\w)print\s*\(")
|
|
100
|
+
elif rel_path.endswith((".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx")):
|
|
101
|
+
pat = re.compile(r"\bconsole\.(log|info|debug)\s*\(")
|
|
102
|
+
else:
|
|
103
|
+
return
|
|
104
|
+
for lineno, raw in enumerate(content.splitlines(), start=1):
|
|
105
|
+
if pat.search(raw):
|
|
106
|
+
yield (lineno, "Debug print found; spec prefers structured logger.",
|
|
107
|
+
"Use a structured logger instead.")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@register_nl_validator(r"(吞|swallow|静默|except|空\s*except)")
|
|
111
|
+
@nl_category("backend")
|
|
112
|
+
def _no_silent_except(content: str, rel_path: str) -> Iterable[tuple[int, str, str]]:
|
|
113
|
+
if not rel_path.endswith(".py"):
|
|
114
|
+
return
|
|
115
|
+
lines = content.splitlines()
|
|
116
|
+
for lineno, raw in enumerate(lines, start=1):
|
|
117
|
+
if re.match(r"^\s*except\b\s*.*:\s*(#.*)?$", raw):
|
|
118
|
+
nxt = lines[lineno].strip() if lineno < len(lines) else ""
|
|
119
|
+
if nxt in {"pass", "...", ""} or nxt.startswith(("pass ", "...")):
|
|
120
|
+
yield (lineno, "Empty exception handler silently swallows errors.",
|
|
121
|
+
"Log or re-raise the exception, or narrow the except types.")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@register_nl_validator(r"(硬编码|hardcode|明文|magic\s+number)")
|
|
125
|
+
@nl_category("frontend")
|
|
126
|
+
def _no_magic_values_frontend(content: str, rel_path: str) -> Iterable[tuple[int, str, str]]:
|
|
127
|
+
pat = re.compile(r"""(?ix)(?:magic\s+number|hardcode|硬编码)""")
|
|
128
|
+
for lineno, raw in enumerate(content.splitlines(), start=1):
|
|
129
|
+
if pat.search(raw):
|
|
130
|
+
yield (lineno, "Hard-coded magic value in frontend source; spec forbids it.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# Spec parser
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
_RULE_PATTERNS = [
|
|
138
|
+
re.compile(r"(?i)(?:^|\s)(?:must\s+not|should\s+not|do\s+not|never|avoid)\s+(.+)$"),
|
|
139
|
+
re.compile(r"(?:^|\s)(?:禁止|不得|不能|不应|避免|不要|不可|不许)\s*(.*)$"),
|
|
140
|
+
re.compile(r"(?i)(?:^|\s)(?:must|should|shall|always|应当|应该|需要)\s+(.+)$"),
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _rule_text(line: str) -> str | None:
|
|
145
|
+
line = line.strip().lstrip("-* ").strip()
|
|
146
|
+
if not line or line.startswith("#"):
|
|
147
|
+
return None
|
|
148
|
+
for pat in _RULE_PATTERNS:
|
|
149
|
+
m = pat.search(line)
|
|
150
|
+
if m:
|
|
151
|
+
text = m.group(m.lastindex).strip(" .。")
|
|
152
|
+
if len(text) > 3:
|
|
153
|
+
return text
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def load_spec_rules(
|
|
158
|
+
repo_root,
|
|
159
|
+
categories: tuple[str, ...] = ("backend", "frontend"),
|
|
160
|
+
) -> list[dict]:
|
|
161
|
+
"""Return every natural-language rule declared under ``spec/<category>/``.
|
|
162
|
+
|
|
163
|
+
Re-reads markdown on every call, so user edits take effect without restart.
|
|
164
|
+
"""
|
|
165
|
+
out: list[dict] = []
|
|
166
|
+
repo_root = Path(repo_root)
|
|
167
|
+
for category in categories:
|
|
168
|
+
spec_dir = repo_root / ".cowork-flow" / "spec" / category
|
|
169
|
+
if not spec_dir.is_dir():
|
|
170
|
+
continue
|
|
171
|
+
for md in sorted(spec_dir.glob("*.md")):
|
|
172
|
+
if md.name == "index.md":
|
|
173
|
+
continue
|
|
174
|
+
text = md.read_text(encoding="utf-8")
|
|
175
|
+
for lineno, raw in enumerate(text.splitlines(), start=1):
|
|
176
|
+
rule = _rule_text(raw)
|
|
177
|
+
if rule is None:
|
|
178
|
+
continue
|
|
179
|
+
out.append({
|
|
180
|
+
"category": category,
|
|
181
|
+
"source": f".cowork-flow/spec/{category}/{md.name}",
|
|
182
|
+
"line": lineno,
|
|
183
|
+
"text": rule,
|
|
184
|
+
"validators": [
|
|
185
|
+
pat.pattern for pat, cat, _ in _NL_VALIDATORS
|
|
186
|
+
if cat in (category, "backend") and re.search(pat, rule)
|
|
187
|
+
],
|
|
188
|
+
})
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Public API
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_coding_standaries_summary(
|
|
198
|
+
repo_root: Path,
|
|
199
|
+
task_dir: Path,
|
|
200
|
+
) -> str:
|
|
201
|
+
"""Render the active user-authored rules as a checklist (read-only)."""
|
|
202
|
+
changed = collect_changed_paths(repo_root)
|
|
203
|
+
if not changed:
|
|
204
|
+
return ""
|
|
205
|
+
|
|
206
|
+
backend = [p for p in changed if _is_backend_file(p)]
|
|
207
|
+
frontend = [p for p in changed if _is_frontend_file(p)]
|
|
208
|
+
if not backend and not frontend:
|
|
209
|
+
return ""
|
|
210
|
+
|
|
211
|
+
out: list[str] = []
|
|
212
|
+
if backend:
|
|
213
|
+
rules = load_spec_rules(repo_root, ("backend",))
|
|
214
|
+
if rules:
|
|
215
|
+
out.append("=== Backend spec rules — VALIDATED during check gate ===")
|
|
216
|
+
for r in rules:
|
|
217
|
+
out.append(f"- [{r['source']}:{r['line']}] {r['text']}")
|
|
218
|
+
if frontend:
|
|
219
|
+
rules = load_spec_rules(repo_root, ("frontend",))
|
|
220
|
+
if rules:
|
|
221
|
+
out.append("=== Frontend spec rules — VALIDATED during check gate ===")
|
|
222
|
+
for r in rules:
|
|
223
|
+
out.append(f"- [{r['source']}:{r['line']}] {r['text']}")
|
|
224
|
+
return "\n".join(out)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def validate_coding_standaries(
|
|
228
|
+
repo_root: Path,
|
|
229
|
+
task_dir: Path,
|
|
230
|
+
) -> list[dict]:
|
|
231
|
+
"""Run all coding-standard checks — hardcoded UTF-8 + spec-activated NL rules.
|
|
232
|
+
|
|
233
|
+
The NL rules only run when the user's spec files declare a matching rule
|
|
234
|
+
text, making spec files genuinely editable quality gates.
|
|
235
|
+
"""
|
|
236
|
+
violations: list[dict] = []
|
|
237
|
+
|
|
238
|
+
# Phase 1: hardcoded UTF-8 / IO checks always run.
|
|
239
|
+
violations.extend(validate_changed_files(repo_root, collect_changed_files(repo_root)))
|
|
240
|
+
|
|
241
|
+
# Phase 2: spec-driven validators — activated by user-edited spec text.
|
|
242
|
+
spec_rules = load_spec_rules(repo_root)
|
|
243
|
+
if not spec_rules:
|
|
244
|
+
return violations
|
|
245
|
+
|
|
246
|
+
activated: set[str] = set()
|
|
247
|
+
for rule in spec_rules:
|
|
248
|
+
activated.update(rule.get("validators", []))
|
|
249
|
+
if not activated:
|
|
250
|
+
return violations
|
|
251
|
+
|
|
252
|
+
for changed_file in collect_changed_files(repo_root):
|
|
253
|
+
file_path = repo_root / changed_file.path
|
|
254
|
+
if not file_path.is_file():
|
|
255
|
+
continue
|
|
256
|
+
suffix = file_path.suffix.lower()
|
|
257
|
+
if suffix not in _EXTENSIONS:
|
|
258
|
+
continue
|
|
259
|
+
try:
|
|
260
|
+
content = file_path.read_text(encoding="utf-8")
|
|
261
|
+
except (OSError, UnicodeDecodeError):
|
|
262
|
+
continue
|
|
263
|
+
for pat, _category, validator in _NL_VALIDATORS:
|
|
264
|
+
if pat.pattern not in activated:
|
|
265
|
+
continue
|
|
266
|
+
try:
|
|
267
|
+
results = validator(content, changed_file.path)
|
|
268
|
+
except Exception: # noqa: BLE001 — defensively isolate parsing
|
|
269
|
+
continue
|
|
270
|
+
for lineno, msg, *rest in results:
|
|
271
|
+
fix_hint = rest[0] if rest else ""
|
|
272
|
+
violations.append({
|
|
273
|
+
"rule_id": f"SPEC-{validator.__name__}",
|
|
274
|
+
"type": "spec_coding_standard",
|
|
275
|
+
"severity": "block",
|
|
276
|
+
"passed": False,
|
|
277
|
+
"message": msg,
|
|
278
|
+
"file": changed_file.path,
|
|
279
|
+
"line": lineno,
|
|
280
|
+
"fix_hint": fix_hint,
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
return violations
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def main() -> None:
|
|
287
|
+
import argparse
|
|
288
|
+
|
|
289
|
+
parser = argparse.ArgumentParser(description="Spec-driven coding gate (zcode)")
|
|
290
|
+
parser.add_argument("--task-dir", required=True, help="Task directory path")
|
|
291
|
+
parser.add_argument("--repo-root", default=".", help="Repository root path")
|
|
292
|
+
parser.add_argument("--summarize", action="store_true")
|
|
293
|
+
parser.add_argument("--list", action="store_true", help="List parsed spec rules")
|
|
294
|
+
parser.add_argument("--validate", action="store_true")
|
|
295
|
+
|
|
296
|
+
args = parser.parse_args()
|
|
297
|
+
repo_root = Path(args.repo_root).resolve()
|
|
298
|
+
task_dir = Path(args.task_dir).resolve()
|
|
299
|
+
|
|
300
|
+
if args.list:
|
|
301
|
+
for r in load_spec_rules(repo_root):
|
|
302
|
+
print(json.dumps(r, ensure_ascii=False))
|
|
303
|
+
return
|
|
304
|
+
if args.summarize:
|
|
305
|
+
print(get_coding_standaries_summary(repo_root, task_dir) or "No spec rules activated.")
|
|
306
|
+
return
|
|
307
|
+
if args.validate:
|
|
308
|
+
v = validate_coding_standaries(repo_root, task_dir)
|
|
309
|
+
for x in v:
|
|
310
|
+
print(json.dumps(x, ensure_ascii=False))
|
|
311
|
+
sys.exit(1 if v else 0)
|
|
312
|
+
print(get_coding_standaries_summary(repo_root, task_dir) or "No spec rules activated.")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if __name__ == "__main__":
|
|
316
|
+
main()
|
|
@@ -306,7 +306,7 @@ def _validate_phase_gate(
|
|
|
306
306
|
elif rule_id == "R-WF-004":
|
|
307
307
|
return _check_plan_link(repo_root, change_dir, rule)
|
|
308
308
|
elif rule_id == "R-WF-005":
|
|
309
|
-
return _check_task_link(change_dir, rule)
|
|
309
|
+
return _check_task_link(change_dir, task_dir, rule)
|
|
310
310
|
|
|
311
311
|
# R-WF-006: Implementation without failing test
|
|
312
312
|
if rule_id == "R-WF-006":
|
|
@@ -395,9 +395,11 @@ def _check_plan_link(
|
|
|
395
395
|
|
|
396
396
|
def _check_task_link(
|
|
397
397
|
change_dir: Path,
|
|
398
|
+
task_dir: Path,
|
|
398
399
|
rule: dict,
|
|
399
400
|
) -> dict | None:
|
|
400
|
-
"""Check if change
|
|
401
|
+
"""Check if change references ``task_dir`` via ``task:``/``tasks:`` or any
|
|
402
|
+
list-item match in ``change.yaml`` (supports the documented YAML list form)."""
|
|
401
403
|
change_yaml = change_dir / "change.yaml"
|
|
402
404
|
if not change_yaml.exists():
|
|
403
405
|
return rule_violation(rule, change_yaml, detail="change.yaml is missing")
|
|
@@ -405,10 +407,16 @@ def _check_task_link(
|
|
|
405
407
|
with open(change_yaml, encoding="utf-8") as f:
|
|
406
408
|
content = f.read()
|
|
407
409
|
|
|
408
|
-
if
|
|
409
|
-
return
|
|
410
|
+
if _metadata_has_link(content, ("task", "tasks")):
|
|
411
|
+
return None
|
|
410
412
|
|
|
411
|
-
|
|
413
|
+
task_name = task_dir.name
|
|
414
|
+
for raw in content.splitlines():
|
|
415
|
+
stripped = raw.strip().lstrip("-").strip().strip("#").strip().strip('"').strip("'").strip(",")
|
|
416
|
+
if stripped == task_name:
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
return rule_violation(rule, change_yaml, detail="task link is missing")
|
|
412
420
|
|
|
413
421
|
|
|
414
422
|
def log_violations(
|
package/template/AGENTS.md
CHANGED
|
@@ -16,24 +16,34 @@
|
|
|
16
16
|
|
|
17
17
|
## 0.1 编码前强制门禁
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
**编辑文件、变更代码、派发子代理之前,先确定当前工作流状态,再决定放行、阻断还是路由。**
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
按以下顺序判定当前状态:
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
23
|
+
1. **系统提示已包含 `<workflow-state>` 块(hook 注入)**: 直接按该块
|
|
24
|
+
`Status:` 行路由。
|
|
25
|
+
2. **否则运行一次导航器**:
|
|
26
|
+
```bash
|
|
27
|
+
./.cowork-flow/run task next [task-dir-or-name]
|
|
28
|
+
```
|
|
29
|
+
按返回的 `task.json.status` 路由,缺失活动任务时先创建再启动。
|
|
30
|
+
|
|
31
|
+
状态路由:
|
|
32
|
+
|
|
33
|
+
- `no_task` → **阻断**。拒绝写代码;引导 brainstorming → plan → task create → task start。
|
|
34
|
+
- `delegated_subtask` → 按子代理规则执行,派发方核验绑定。
|
|
35
|
+
- `planning` → **阻断**。PRD 和 implement.jsonl 就绪前不允许实现或派发 cowork-implement。
|
|
36
|
+
- `in_progress` / `review` → 放行;按当前阶段推进。
|
|
37
|
+
- `completed` → **阻断**。创建新任务或归档会话。
|
|
28
38
|
|
|
29
39
|
豁免:
|
|
30
40
|
|
|
31
|
-
-
|
|
32
|
-
- 纯查询命令(`git status`、`task next`、`task current
|
|
33
|
-
- 用户明确说"跳过流程"
|
|
34
|
-
- `
|
|
41
|
+
- 只读问答。
|
|
42
|
+
- 纯查询命令(`git status`、`task next`、`task current`)。
|
|
43
|
+
- 用户明确说"跳过流程"。
|
|
44
|
+
- `task next` / `task validate` 本身。
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
不确定是否需要写代码时,先运行 `task next`。
|
|
37
47
|
|
|
38
48
|
## 1. 先思考
|
|
39
49
|
|