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.
Files changed (65) hide show
  1. package/package.json +1 -1
  2. package/template/.claude/agents/cowork-check.md +1 -1
  3. package/template/.claude/agents/cowork-implement.md +2 -2
  4. package/template/.cowork-flow/.version +1 -1
  5. package/template/.cowork-flow/scripts/commands/task.py +75 -6
  6. package/template/.cowork-flow/scripts/common/core/execution_context.py +2 -2
  7. package/template/.cowork-flow/scripts/common/core/files.py +8 -0
  8. package/template/.cowork-flow/scripts/common/gates/anti_rationalization.py +55 -0
  9. package/template/.cowork-flow/scripts/common/gates/gates.py +5 -0
  10. package/template/.cowork-flow/scripts/common/gates/spec_validator.py +53 -0
  11. package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +10 -8
  12. package/template/.cowork-flow/scripts/common/gates/test_intent.py +1 -5
  13. package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +170 -131
  14. package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +40 -15
  15. package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +32 -0
  16. package/template/.cowork-flow/scripts/common/gates/validate_rules.py +1 -12
  17. package/template/.cowork-flow/scripts/common/git/git_context.py +4 -4
  18. package/template/.cowork-flow/scripts/common/task/batch_mode.py +145 -0
  19. package/template/.cowork-flow/scripts/common/task/readiness.py +29 -11
  20. package/template/.cowork-flow/spec/backend/quality-guidelines.md +2 -0
  21. package/template/.cowork-flow/spec/contracts/decision-anchor.md +68 -0
  22. package/template/.cowork-flow/spec/contracts/error-output-as-data.md +54 -0
  23. package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
  24. package/template/.cowork-flow/spec/frontend/quality-guidelines.md +2 -0
  25. package/template/.cowork-flow/spec/index.md +16 -0
  26. package/template/.cowork-flow/spec/references/definition-of-done.md +45 -0
  27. package/template/.cowork-flow/spec/references/index.md +20 -0
  28. package/template/.cowork-flow/spec/references/orchestration-patterns.md +139 -0
  29. package/template/.cowork-flow/spec/references/security-checklist.md +51 -0
  30. package/template/.cowork-flow/spec/references/testing-checklist.md +45 -0
  31. package/template/.cowork-flow/spec/runtime/contract-registry.json +57 -0
  32. package/template/.cowork-flow/spec/runtime/rules.json +60 -0
  33. package/template/.cowork-flow/spec/schemas/rules.schema.json +1 -1
  34. package/template/.cowork-flow/workflow.md +9 -4
  35. package/template/.opencode/agents/cowork-check.md +1 -1
  36. package/template/.opencode/agents/cowork-implement.md +2 -2
  37. package/template/.opencode/agents/cowork-research.md +1 -1
  38. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  39. package/template/.zcode/hooks/runtime/scripts/commands/task.py +47 -5
  40. package/template/.zcode/hooks/runtime/scripts/common/core/execution_context.py +2 -2
  41. package/template/.zcode/hooks/runtime/scripts/common/core/files.py +8 -0
  42. package/template/.zcode/hooks/runtime/scripts/common/gates/anti_rationalization.py +55 -0
  43. package/template/.zcode/hooks/runtime/scripts/common/gates/gates.py +5 -0
  44. package/template/.zcode/hooks/runtime/scripts/common/gates/tdd_evidence.py +10 -8
  45. package/template/.zcode/hooks/runtime/scripts/common/gates/test_intent.py +1 -5
  46. package/template/.zcode/hooks/runtime/scripts/common/gates/validate_coding_standards.py +294 -133
  47. package/template/.zcode/hooks/runtime/scripts/common/gates/validate_implementation.py +40 -15
  48. package/template/.zcode/hooks/runtime/scripts/common/gates/validate_rules.py +2 -13
  49. package/template/.zcode/hooks/runtime/scripts/common/git/git_context.py +4 -4
  50. package/template/.zcode/hooks/runtime/scripts/common/task/readiness.py +11 -11
  51. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +68 -0
  52. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
  53. package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +60 -0
  54. package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +1 -1
  55. package/template/.zcode/scaffold/.cowork-flow/workflow.md +9 -4
  56. package/template/AGENTS.md +9 -0
  57. package/template/skills/batch-mode/SKILL.md +117 -0
  58. package/template/skills/before-dev/SKILL.md +11 -3
  59. package/template/skills/break-loop/SKILL.md +53 -0
  60. package/template/skills/check/SKILL.md +63 -0
  61. package/template/skills/continue/SKILL.md +1 -1
  62. package/template/skills/doubt-review/SKILL.md +118 -0
  63. package/template/skills/tdd/SKILL.md +13 -0
  64. package/template/skills/writing-plans/SKILL.md +23 -0
  65. 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
- """Spec-driven coding standards — gates honor user-edited spec files.
2
+ """Coding-standard gates — context-injection model.
3
3
 
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.
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 Callable, Iterable, Sequence
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 — VALIDATED during check gate ===")
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 — VALIDATED during check gate ===")
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 all coding-standard checks — hardcoded UTF-8 + spec-activated NL rules.
152
+ """Run hard machine-checkable coding-standard gates.
219
153
 
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.
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
- violations: list[dict] = []
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
- # 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
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
- activated: set[str] = set()
234
- for rule in spec_rules:
235
- activated.update(rule.get("validators", []))
236
- if not activated:
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
- for changed_file in collect_changed_files(repo_root):
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
- 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
-
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="Spec-driven coding gate")
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", help="Run spec validators vs changed files")
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 features were introduced"""
189
+ """R-AG-005: Check if unrequested files were modified (diff files must be in implement.jsonl)."""
190
190
  violations = []
191
191
 
192
- # Read PRD
193
- prd_path = task_dir / "prd.md"
194
- if not prd_path.exists():
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
- prd_content = prd_path.read_text(encoding="utf-8")
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
- # 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
- ]
219
+ if not allowed_files:
220
+ return violations
206
221
 
207
- for pattern in feature_indicators:
208
- if re.search(pattern, diff_output) and not re.search(pattern, prd_content):
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(rule, "", detail=f"detected pattern: {pattern}")
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
- rule_type = rule["type"]
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 / "prd.md").is_file(),
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}/prd.md")
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}/prd.md"
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 prd.md - read it for task details")
365
+ lines.append("[!] This task has decision-anchor.md - read it for task details")
366
366
  lines.append("")
367
367
 
368
368