cowork-flow 0.0.32 → 0.0.34
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/.claude/agents/cowork-check.md +1 -1
- package/template/.claude/agents/cowork-implement.md +2 -2
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/commands/task.py +75 -6
- package/template/.cowork-flow/scripts/common/core/execution_context.py +2 -2
- package/template/.cowork-flow/scripts/common/core/files.py +8 -0
- package/template/.cowork-flow/scripts/common/gates/anti_rationalization.py +55 -0
- package/template/.cowork-flow/scripts/common/gates/gates.py +5 -0
- package/template/.cowork-flow/scripts/common/gates/spec_validator.py +53 -0
- package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +10 -8
- package/template/.cowork-flow/scripts/common/gates/test_intent.py +1 -5
- package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +170 -131
- package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +40 -15
- package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +32 -0
- package/template/.cowork-flow/scripts/common/gates/validate_rules.py +1 -12
- package/template/.cowork-flow/scripts/common/git/git_context.py +4 -4
- package/template/.cowork-flow/scripts/common/task/batch_mode.py +145 -0
- package/template/.cowork-flow/scripts/common/task/readiness.py +29 -11
- package/template/.cowork-flow/spec/backend/quality-guidelines.md +2 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +68 -0
- package/template/.cowork-flow/spec/contracts/error-output-as-data.md +54 -0
- package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
- package/template/.cowork-flow/spec/frontend/quality-guidelines.md +2 -0
- package/template/.cowork-flow/spec/index.md +16 -0
- package/template/.cowork-flow/spec/references/definition-of-done.md +45 -0
- package/template/.cowork-flow/spec/references/index.md +20 -0
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +139 -0
- package/template/.cowork-flow/spec/references/security-checklist.md +51 -0
- package/template/.cowork-flow/spec/references/testing-checklist.md +45 -0
- package/template/.cowork-flow/spec/runtime/contract-registry.json +57 -0
- package/template/.cowork-flow/spec/runtime/rules.json +60 -0
- package/template/.cowork-flow/spec/schemas/rules.schema.json +1 -1
- package/template/.cowork-flow/workflow.md +9 -4
- package/template/.opencode/agents/cowork-check.md +1 -1
- package/template/.opencode/agents/cowork-implement.md +2 -2
- package/template/.opencode/agents/cowork-research.md +1 -1
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/runtime/scripts/commands/task.py +47 -5
- package/template/.zcode/hooks/runtime/scripts/common/core/execution_context.py +2 -2
- package/template/.zcode/hooks/runtime/scripts/common/core/files.py +8 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/anti_rationalization.py +55 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/gates.py +5 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/tdd_evidence.py +10 -8
- package/template/.zcode/hooks/runtime/scripts/common/gates/test_intent.py +1 -5
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_coding_standards.py +294 -133
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_implementation.py +40 -15
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_rules.py +2 -13
- package/template/.zcode/hooks/runtime/scripts/common/git/git_context.py +4 -4
- package/template/.zcode/hooks/runtime/scripts/common/task/readiness.py +11 -11
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +68 -0
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
- package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +60 -0
- package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +1 -1
- package/template/.zcode/scaffold/.cowork-flow/workflow.md +9 -4
- package/template/AGENTS.md +9 -0
- package/template/skills/batch-mode/SKILL.md +117 -0
- package/template/skills/before-dev/SKILL.md +11 -3
- package/template/skills/break-loop/SKILL.md +53 -0
- package/template/skills/check/SKILL.md +63 -0
- package/template/skills/continue/SKILL.md +1 -1
- package/template/skills/doubt-review/SKILL.md +118 -0
- package/template/skills/tdd/SKILL.md +13 -0
- package/template/skills/writing-plans/SKILL.md +23 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_coding_standaries.py +0 -316
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Coding-standard gates — context-injection model.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
``spec/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
Under Approach A, rules authored under ``.cowork-flow/spec/backend/*.md`` and
|
|
5
|
+
``spec/frontend/*.md`` are machine-parsed into checklist items, then surfaced to
|
|
6
|
+
the LLM via ``get_coding_standards_summary`` (already wired into the check/
|
|
7
|
+
review gates). The LLM is trusted to verify its own code against these items.
|
|
8
|
+
|
|
9
|
+
Only machine-decidable checks run here as hard blocks:
|
|
10
|
+
|
|
11
|
+
* Phase 1 — UTF-8 / IO-encoding (``validate_changed_files``).
|
|
12
|
+
* ``validate_complexity_signals`` — oversize Python functions (warn).
|
|
13
|
+
|
|
14
|
+
A separate ``collect_machine_checks`` helper is exposed for callers that want
|
|
15
|
+
additional regex-style warnings (hardcoded secrets, debug prints, silent
|
|
16
|
+
excepts, trailing comments...) WITHOUT hard-blocking the workflow. These are
|
|
17
|
+
emitted at severity=advisory so they can be rendered for the LLM without being a
|
|
18
|
+
wall the user must satisfy.
|
|
9
19
|
"""
|
|
10
20
|
|
|
11
21
|
from __future__ import annotations
|
|
@@ -14,7 +24,7 @@ import json
|
|
|
14
24
|
import re
|
|
15
25
|
import sys
|
|
16
26
|
from pathlib import Path
|
|
17
|
-
from typing import
|
|
27
|
+
from typing import Sequence
|
|
18
28
|
|
|
19
29
|
from common.gates.coding_standards import validate_changed_files
|
|
20
30
|
from common.git.git_snapshot import collect_changed_files, collect_changed_paths
|
|
@@ -39,84 +49,6 @@ def _is_frontend_file(rel_path: str) -> bool:
|
|
|
39
49
|
return bool(_FRONTEND_SUFFIXES.search(rel_path))
|
|
40
50
|
|
|
41
51
|
|
|
42
|
-
# ---------------------------------------------------------------------------
|
|
43
|
-
# Registered NL-backed validators
|
|
44
|
-
# ---------------------------------------------------------------------------
|
|
45
|
-
|
|
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]]]]] = []
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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.")
|
|
80
|
-
|
|
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
52
|
# ---------------------------------------------------------------------------
|
|
121
53
|
# Spec parser
|
|
122
54
|
# ---------------------------------------------------------------------------
|
|
@@ -148,6 +80,8 @@ def load_spec_rules(
|
|
|
148
80
|
"""Return every natural-language rule declared under ``spec/<category>/``.
|
|
149
81
|
|
|
150
82
|
Re-reads markdown on every call, so user edits take effect without restart.
|
|
83
|
+
``validators`` is preserved as an empty list (legacy field kept so external
|
|
84
|
+
tooling that inspects the rule dict does not break).
|
|
151
85
|
"""
|
|
152
86
|
out: list[dict] = []
|
|
153
87
|
repo_root = Path(repo_root)
|
|
@@ -168,10 +102,7 @@ def load_spec_rules(
|
|
|
168
102
|
"source": f".cowork-flow/spec/{category}/{md.name}",
|
|
169
103
|
"line": lineno,
|
|
170
104
|
"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
|
-
],
|
|
105
|
+
"validators": [],
|
|
175
106
|
})
|
|
176
107
|
return out
|
|
177
108
|
|
|
@@ -180,12 +111,15 @@ def load_spec_rules(
|
|
|
180
111
|
# Public API
|
|
181
112
|
# ---------------------------------------------------------------------------
|
|
182
113
|
|
|
183
|
-
|
|
184
114
|
def get_coding_standards_summary(
|
|
185
115
|
repo_root: Path,
|
|
186
116
|
task_dir: Path,
|
|
187
117
|
) -> str:
|
|
188
|
-
"""Render the active user-authored rules as a checklist (read-only).
|
|
118
|
+
"""Render the active user-authored rules as a review checklist (read-only).
|
|
119
|
+
|
|
120
|
+
Calls whose changed paths cover neither backend nor frontend files return
|
|
121
|
+
an empty string — there is nothing actionable for the LLM to review.
|
|
122
|
+
"""
|
|
189
123
|
changed = collect_changed_paths(repo_root)
|
|
190
124
|
if not changed:
|
|
191
125
|
return ""
|
|
@@ -199,13 +133,13 @@ def get_coding_standards_summary(
|
|
|
199
133
|
if backend:
|
|
200
134
|
rules = load_spec_rules(repo_root, ("backend",))
|
|
201
135
|
if rules:
|
|
202
|
-
out.append("=== Backend spec rules —
|
|
136
|
+
out.append("=== Backend spec rules — LLM review checklist ===")
|
|
203
137
|
for r in rules:
|
|
204
138
|
out.append(f"- [{r['source']}:{r['line']}] {r['text']}")
|
|
205
139
|
if frontend:
|
|
206
140
|
rules = load_spec_rules(repo_root, ("frontend",))
|
|
207
141
|
if rules:
|
|
208
|
-
out.append("=== Frontend spec rules —
|
|
142
|
+
out.append("=== Frontend spec rules — LLM review checklist ===")
|
|
209
143
|
for r in rules:
|
|
210
144
|
out.append(f"- [{r['source']}:{r['line']}] {r['text']}")
|
|
211
145
|
return "\n".join(out)
|
|
@@ -215,28 +149,44 @@ def validate_coding_standards(
|
|
|
215
149
|
repo_root: Path,
|
|
216
150
|
task_dir: Path | None = None,
|
|
217
151
|
) -> list[dict]:
|
|
218
|
-
"""Run
|
|
152
|
+
"""Run hard machine-checkable coding-standard gates.
|
|
219
153
|
|
|
220
|
-
|
|
221
|
-
|
|
154
|
+
Approach A keeps only the UTF-8 / IO-encoding check here. All natural
|
|
155
|
+
language rules authored in the spec tree are surfaced to the LLM via the
|
|
156
|
+
checklist in ``get_coding_standards_summary``; they do not block the
|
|
157
|
+
workflow from this function. Optional advisory warnings live in
|
|
158
|
+
``collect_machine_checks``.
|
|
222
159
|
"""
|
|
223
|
-
|
|
160
|
+
return list(validate_changed_files(repo_root, collect_changed_files(repo_root)))
|
|
224
161
|
|
|
225
|
-
# Phase 1: hardcoded UTF-8 / IO checks always run.
|
|
226
|
-
violations.extend(validate_changed_files(repo_root, collect_changed_files(repo_root)))
|
|
227
162
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
163
|
+
def collect_machine_checks(
|
|
164
|
+
repo_root: Path,
|
|
165
|
+
task_dir: Path | None = None,
|
|
166
|
+
) -> list[dict]:
|
|
167
|
+
"""Emit regex-style advisory signals (severity=advisory).
|
|
232
168
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
169
|
+
The LLM decides how to act on them; the workflow never blocks on a hit.
|
|
170
|
+
Kept separate from ``validate_coding_standards`` so callers can choose
|
|
171
|
+
whether to render them in a gate log, inject them into context, or ignore.
|
|
172
|
+
"""
|
|
173
|
+
violations: list[dict] = []
|
|
174
|
+
try:
|
|
175
|
+
changed_files = collect_changed_files(repo_root)
|
|
176
|
+
except Exception: # noqa: BLE001 — defensively isolate git errors
|
|
237
177
|
return violations
|
|
238
178
|
|
|
239
|
-
|
|
179
|
+
secret_re = re.compile(
|
|
180
|
+
r"""(?ix)
|
|
181
|
+
(?:password|passwd|secret|api[_\s]?key|token|access[_\s]?key)
|
|
182
|
+
\s*[:=]\s*["'][^"'}{)($]{3,}["']
|
|
183
|
+
"""
|
|
184
|
+
)
|
|
185
|
+
silent_except_re = re.compile(r"^\s*except\b\s*.*:\s*(#.*)?$")
|
|
186
|
+
bare_print_re = re.compile(r"(?<!\w)print\s*\(")
|
|
187
|
+
console_re = re.compile(r"\bconsole\.(log|info|debug)\s*\(")
|
|
188
|
+
|
|
189
|
+
for changed_file in changed_files:
|
|
240
190
|
file_path = repo_root / changed_file.path
|
|
241
191
|
if not file_path.is_file():
|
|
242
192
|
continue
|
|
@@ -247,38 +197,125 @@ def validate_coding_standards(
|
|
|
247
197
|
content = file_path.read_text(encoding="utf-8")
|
|
248
198
|
except (OSError, UnicodeDecodeError):
|
|
249
199
|
continue
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
"
|
|
266
|
-
"line"
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
200
|
+
lines = content.splitlines()
|
|
201
|
+
for lineno, raw in enumerate(lines, start=1):
|
|
202
|
+
# Low-signal heuristic: only flag if the line also has an
|
|
203
|
+
# assignment / call so variable-name mentions do not trip it.
|
|
204
|
+
if (secret_re.search(raw)
|
|
205
|
+
and "process.env" not in raw
|
|
206
|
+
and "os.environ" not in raw):
|
|
207
|
+
violations.append(_adv(
|
|
208
|
+
"MACHINE-HARDCODED-SECRET-001",
|
|
209
|
+
"Possible hard-coded secret in source",
|
|
210
|
+
changed_file.path, lineno,
|
|
211
|
+
"Read the value from configuration or environment variables.",
|
|
212
|
+
))
|
|
213
|
+
if suffix == ".py" and bare_print_re.search(raw):
|
|
214
|
+
violations.append(_adv(
|
|
215
|
+
"MACHINE-DEBUG-PRINT-001",
|
|
216
|
+
"Debug print on changed line",
|
|
217
|
+
changed_file.path, lineno,
|
|
218
|
+
"Use a structured logger or remove the print.",
|
|
219
|
+
))
|
|
220
|
+
if suffix in {".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"} and console_re.search(raw):
|
|
221
|
+
violations.append(_adv(
|
|
222
|
+
"MACHINE-CONSOLE-LOG-001",
|
|
223
|
+
"console.{log,info,debug} on changed line",
|
|
224
|
+
changed_file.path, lineno,
|
|
225
|
+
"Use the project logger or remove the call.",
|
|
226
|
+
))
|
|
227
|
+
if (suffix == ".py"
|
|
228
|
+
and silent_except_re.match(raw)
|
|
229
|
+
and lineno < len(lines)
|
|
230
|
+
and lines[lineno].strip() in {"pass", "...", ""}):
|
|
231
|
+
violations.append(_adv(
|
|
232
|
+
"MACHINE-SILENT-EXCEPT-001",
|
|
233
|
+
"Empty exception handler silently swallows errors",
|
|
234
|
+
changed_file.path, lineno,
|
|
235
|
+
"Log or re-raise the exception, or narrow the except types.",
|
|
236
|
+
))
|
|
270
237
|
return violations
|
|
271
238
|
|
|
272
239
|
|
|
240
|
+
def _adv(
|
|
241
|
+
rule_id: str,
|
|
242
|
+
message: str,
|
|
243
|
+
file_path: str,
|
|
244
|
+
line: int,
|
|
245
|
+
fix_hint: str,
|
|
246
|
+
) -> dict:
|
|
247
|
+
return {
|
|
248
|
+
"rule_id": rule_id,
|
|
249
|
+
"type": "machine_check",
|
|
250
|
+
"severity": "advisory",
|
|
251
|
+
"passed": False,
|
|
252
|
+
"message": message,
|
|
253
|
+
"file": file_path,
|
|
254
|
+
"line": line,
|
|
255
|
+
"fix_hint": fix_hint,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def validate_complexity_signals(repo_root: Path, task_dir: Path | None) -> list[dict]:
|
|
260
|
+
"""Detect over-complexity signals in modified files (warn-level)."""
|
|
261
|
+
import ast
|
|
262
|
+
import subprocess
|
|
263
|
+
warnings: list[dict] = []
|
|
264
|
+
try:
|
|
265
|
+
result = subprocess.run(
|
|
266
|
+
["git", "diff", "--name-only", "--diff-filter=ACMR", "HEAD~"],
|
|
267
|
+
cwd=str(repo_root), capture_output=True, text=True,
|
|
268
|
+
encoding="utf-8", timeout=10,
|
|
269
|
+
)
|
|
270
|
+
changed_files = [f for f in result.stdout.splitlines() if f]
|
|
271
|
+
except (subprocess.SubprocessError, OSError):
|
|
272
|
+
return warnings
|
|
273
|
+
|
|
274
|
+
for rel_path in changed_files:
|
|
275
|
+
if not rel_path.endswith(".py"):
|
|
276
|
+
continue
|
|
277
|
+
full_path = repo_root / rel_path
|
|
278
|
+
if not full_path.is_file():
|
|
279
|
+
continue
|
|
280
|
+
try:
|
|
281
|
+
source = full_path.read_text(encoding="utf-8")
|
|
282
|
+
tree = ast.parse(source)
|
|
283
|
+
except (OSError, SyntaxError, UnicodeDecodeError):
|
|
284
|
+
continue
|
|
285
|
+
for node in ast.walk(tree):
|
|
286
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
287
|
+
end = getattr(node, "end_lineno", None) or node.lineno
|
|
288
|
+
func_lines = end - node.lineno
|
|
289
|
+
if func_lines > 50:
|
|
290
|
+
warnings.append({
|
|
291
|
+
"rule_id": "COMPLEX-FUNC-001",
|
|
292
|
+
"type": "coding_standard",
|
|
293
|
+
"severity": "warn",
|
|
294
|
+
"passed": False,
|
|
295
|
+
"message": (
|
|
296
|
+
f"{rel_path}:{node.name} function is "
|
|
297
|
+
f"{func_lines} lines (< 50 recommended)"
|
|
298
|
+
),
|
|
299
|
+
"file": str(rel_path),
|
|
300
|
+
"fix_hint": (
|
|
301
|
+
"Split into multiple named functions "
|
|
302
|
+
"by responsibility."
|
|
303
|
+
),
|
|
304
|
+
})
|
|
305
|
+
return warnings
|
|
306
|
+
|
|
307
|
+
|
|
273
308
|
def main() -> None:
|
|
274
309
|
import argparse
|
|
275
310
|
|
|
276
|
-
parser = argparse.ArgumentParser(description="
|
|
311
|
+
parser = argparse.ArgumentParser(description="Coding-standard gate")
|
|
277
312
|
parser.add_argument("--task-dir", help="Task directory")
|
|
278
313
|
parser.add_argument("--repo-root", default=".", help="Repo root path")
|
|
279
314
|
parser.add_argument("--summarize", action="store_true")
|
|
280
315
|
parser.add_argument("--list", action="store_true", help="List parsed spec rules")
|
|
281
|
-
parser.add_argument("--validate", action="store_true"
|
|
316
|
+
parser.add_argument("--validate", action="store_true")
|
|
317
|
+
parser.add_argument("--machine-checks", action="store_true",
|
|
318
|
+
help="Also emit advisory machine-check warnings")
|
|
282
319
|
|
|
283
320
|
args = parser.parse_args()
|
|
284
321
|
repo_root = Path(args.repo_root).resolve()
|
|
@@ -293,9 +330,11 @@ def main() -> None:
|
|
|
293
330
|
return
|
|
294
331
|
if args.validate:
|
|
295
332
|
v = validate_coding_standards(repo_root, task_dir)
|
|
333
|
+
if args.machine_checks:
|
|
334
|
+
v.extend(collect_machine_checks(repo_root, task_dir))
|
|
296
335
|
for x in v:
|
|
297
336
|
print(json.dumps(x, ensure_ascii=False))
|
|
298
|
-
sys.exit(1 if v else 0)
|
|
337
|
+
sys.exit(1 if any(x["severity"] == "block" for x in v) else 0)
|
|
299
338
|
print(get_coding_standards_summary(repo_root, task_dir) or "No spec rules activated.")
|
|
300
339
|
|
|
301
340
|
|
|
@@ -186,29 +186,54 @@ def _check_unrequested_features(
|
|
|
186
186
|
task_dir: Path,
|
|
187
187
|
rule_index: dict[str, dict],
|
|
188
188
|
) -> list[dict]:
|
|
189
|
-
"""R-AG-005: Check if unrequested
|
|
189
|
+
"""R-AG-005: Check if unrequested files were modified (diff files must be in implement.jsonl)."""
|
|
190
190
|
violations = []
|
|
191
191
|
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
if not
|
|
192
|
+
# Collect allowed files from implement.jsonl
|
|
193
|
+
implement_jsonl = task_dir / "implement.jsonl"
|
|
194
|
+
if not implement_jsonl.exists():
|
|
195
195
|
return violations
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
allowed_files: set[str] = set()
|
|
198
|
+
try:
|
|
199
|
+
for line in implement_jsonl.read_text(encoding="utf-8").splitlines():
|
|
200
|
+
line = line.strip()
|
|
201
|
+
if not line:
|
|
202
|
+
continue
|
|
203
|
+
try:
|
|
204
|
+
entry = json.loads(line)
|
|
205
|
+
except json.JSONDecodeError:
|
|
206
|
+
continue
|
|
207
|
+
file_path = entry.get("file")
|
|
208
|
+
if isinstance(file_path, str) and file_path:
|
|
209
|
+
# Normalize paths (strip repo root prefix if present)
|
|
210
|
+
allowed_files.add(file_path)
|
|
211
|
+
# Also add stripped versions for matching
|
|
212
|
+
if file_path.startswith("./"):
|
|
213
|
+
allowed_files.add(file_path[2:])
|
|
214
|
+
else:
|
|
215
|
+
allowed_files.add(f"./{file_path}")
|
|
216
|
+
except (OSError, UnicodeDecodeError):
|
|
217
|
+
return violations
|
|
198
218
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
feature_indicators = [
|
|
202
|
-
r"Added.*feature",
|
|
203
|
-
r"New.*functionality",
|
|
204
|
-
r"Implemented.*module",
|
|
205
|
-
]
|
|
219
|
+
if not allowed_files:
|
|
220
|
+
return violations
|
|
206
221
|
|
|
207
|
-
|
|
208
|
-
|
|
222
|
+
# Get modified project files (exclude .cowork-flow/ metadata)
|
|
223
|
+
modified_files = _get_modified_files(repo_root)
|
|
224
|
+
for file_path in modified_files:
|
|
225
|
+
# Skip .cowork-flow metadata files and template files
|
|
226
|
+
if ".cowork-flow/" in file_path:
|
|
227
|
+
continue
|
|
228
|
+
normalized = file_path.replace("\\", "/")
|
|
229
|
+
# Check if file is in allowed list
|
|
230
|
+
if normalized not in allowed_files and f"./{normalized}" not in allowed_files:
|
|
209
231
|
rule = rule_index.get("R-AG-005")
|
|
210
232
|
violations.append(
|
|
211
|
-
rule_violation(
|
|
233
|
+
rule_violation(
|
|
234
|
+
rule, file_path,
|
|
235
|
+
detail=f"modified file not in implement.jsonl allowed list"
|
|
236
|
+
)
|
|
212
237
|
if rule
|
|
213
238
|
else _missing_rule(repo_root, "R-AG-005")
|
|
214
239
|
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""JSONL file format validator for cowork-flow context files."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_format(file_path: Path | str) -> tuple[bool, list[str]]:
|
|
11
|
+
"""Validate a JSONL file and return (is_valid, error_messages)."""
|
|
12
|
+
path = Path(file_path)
|
|
13
|
+
errors: list[str] = []
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
content = path.read_text(encoding="utf-8").strip()
|
|
17
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
18
|
+
return False, [f"Cannot read file: {exc}"]
|
|
19
|
+
|
|
20
|
+
if not content:
|
|
21
|
+
return False, ["File is empty"]
|
|
22
|
+
|
|
23
|
+
for line_no, raw_line in enumerate(content.splitlines(), start=1):
|
|
24
|
+
line = raw_line.strip()
|
|
25
|
+
if not line:
|
|
26
|
+
continue
|
|
27
|
+
try:
|
|
28
|
+
json.loads(line)
|
|
29
|
+
except json.JSONDecodeError as exc:
|
|
30
|
+
errors.append(f"Line {line_no}: Invalid JSON: {exc}")
|
|
31
|
+
|
|
32
|
+
return (not errors, errors)
|
|
@@ -266,14 +266,8 @@ def _validate_rule(
|
|
|
266
266
|
scope: str,
|
|
267
267
|
) -> dict | None:
|
|
268
268
|
"""Validate a single rule. Returns violation dict if rule fails."""
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if rule_type == "phase_gate":
|
|
269
|
+
if rule["type"] == "phase_gate":
|
|
272
270
|
return _validate_phase_gate(rule, repo_root, task_dir, scope)
|
|
273
|
-
elif rule_type == "forbidden_action":
|
|
274
|
-
# Forbidden actions are validated at runtime, not at checkpoints
|
|
275
|
-
return None
|
|
276
|
-
|
|
277
271
|
return None
|
|
278
272
|
|
|
279
273
|
|
|
@@ -308,11 +302,6 @@ def _validate_phase_gate(
|
|
|
308
302
|
elif rule_id == "R-WF-005":
|
|
309
303
|
return _check_task_link(change_dir, task_dir, rule)
|
|
310
304
|
|
|
311
|
-
# R-WF-006: Implementation without failing test
|
|
312
|
-
if rule_id == "R-WF-006":
|
|
313
|
-
# This is a warn-level rule, just log
|
|
314
|
-
return None
|
|
315
|
-
|
|
316
305
|
# R-WF-007: Task completion without check
|
|
317
306
|
if rule_id == "R-WF-007":
|
|
318
307
|
if scope == "task_complete" and task_dir:
|
|
@@ -189,7 +189,7 @@ def _get_active_task_snapshot(repo_root: Path) -> ActiveTaskSnapshot:
|
|
|
189
189
|
return ActiveTaskSnapshot(
|
|
190
190
|
path=active_task,
|
|
191
191
|
data=data,
|
|
192
|
-
has_prd=(active_task_dir / "
|
|
192
|
+
has_prd=(active_task_dir / "decision-anchor.md").is_file(),
|
|
193
193
|
)
|
|
194
194
|
|
|
195
195
|
|
|
@@ -271,7 +271,7 @@ def _build_resume_checklist(
|
|
|
271
271
|
commands.append(f"./{DIR_WORKFLOW}/run task list-context {active_task}")
|
|
272
272
|
|
|
273
273
|
if snapshot.has_prd:
|
|
274
|
-
read_files.append(f"{active_task}/
|
|
274
|
+
read_files.append(f"{active_task}/decision-anchor.md")
|
|
275
275
|
|
|
276
276
|
read_files.extend(_task_plan_references(repo_root, snapshot))
|
|
277
277
|
notes.append("Read current plan status only when continuing implementation.")
|
|
@@ -314,7 +314,7 @@ def _append_resume_checklist(
|
|
|
314
314
|
return
|
|
315
315
|
|
|
316
316
|
active_task = snapshot.path
|
|
317
|
-
prd_path = f"{active_task}/
|
|
317
|
+
prd_path = f"{active_task}/decision-anchor.md"
|
|
318
318
|
if prd_path in read_files:
|
|
319
319
|
lines.append(f"- Read active task PRD: {prd_path}")
|
|
320
320
|
else:
|
|
@@ -362,7 +362,7 @@ def _append_active_task(
|
|
|
362
362
|
|
|
363
363
|
if include_prd_hint and snapshot.has_prd:
|
|
364
364
|
lines.append("")
|
|
365
|
-
lines.append("[!] This task has
|
|
365
|
+
lines.append("[!] This task has decision-anchor.md - read it for task details")
|
|
366
366
|
lines.append("")
|
|
367
367
|
|
|
368
368
|
|