cowork-flow 0.0.31 → 0.0.33

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 (48) hide show
  1. package/package.json +1 -1
  2. package/template/.codex/agents/cowork-check.toml +7 -0
  3. package/template/.codex/agents/cowork-implement.toml +6 -0
  4. package/template/.codex/hooks/inject-workflow-state.py +0 -0
  5. package/template/.codex/hooks.json +1 -1
  6. package/template/.cowork-flow/.version +1 -1
  7. package/template/.cowork-flow/scripts/commands/task.py +28 -1
  8. package/template/.cowork-flow/scripts/common/core/files.py +8 -0
  9. package/template/.cowork-flow/scripts/common/gates/anti_rationalization.py +55 -0
  10. package/template/.cowork-flow/scripts/common/gates/gates.py +5 -0
  11. package/template/.cowork-flow/scripts/common/gates/spec_validator.py +53 -0
  12. package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +4 -5
  13. package/template/.cowork-flow/scripts/common/gates/test_intent.py +1 -5
  14. package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +294 -133
  15. package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +32 -0
  16. package/template/.cowork-flow/scripts/common/gates/validate_rules.py +14 -17
  17. package/template/.cowork-flow/scripts/common/task/batch_mode.py +145 -0
  18. package/template/.cowork-flow/scripts/common/task/readiness.py +22 -4
  19. package/template/.cowork-flow/spec/backend/quality-guidelines.md +2 -0
  20. package/template/.cowork-flow/spec/contracts/error-output-as-data.md +54 -0
  21. package/template/.cowork-flow/spec/frontend/quality-guidelines.md +2 -0
  22. package/template/.cowork-flow/spec/index.md +16 -0
  23. package/template/.cowork-flow/spec/references/definition-of-done.md +45 -0
  24. package/template/.cowork-flow/spec/references/index.md +20 -0
  25. package/template/.cowork-flow/spec/references/orchestration-patterns.md +139 -0
  26. package/template/.cowork-flow/spec/references/security-checklist.md +51 -0
  27. package/template/.cowork-flow/spec/references/testing-checklist.md +45 -0
  28. package/template/.cowork-flow/spec/runtime/contract-registry.json +57 -0
  29. package/template/.cowork-flow/spec/runtime/rules.json +60 -0
  30. package/template/.cowork-flow/workflow.md +5 -0
  31. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  32. package/template/.zcode/hooks/runtime/run +2 -0
  33. package/template/.zcode/hooks/runtime/scripts/common/core/files.py +8 -0
  34. package/template/.zcode/hooks/runtime/scripts/common/gates/anti_rationalization.py +55 -0
  35. package/template/.zcode/hooks/runtime/scripts/common/gates/gates.py +5 -0
  36. package/template/.zcode/hooks/runtime/scripts/common/gates/tdd_evidence.py +4 -5
  37. package/template/.zcode/hooks/runtime/scripts/common/gates/test_intent.py +1 -5
  38. package/template/.zcode/hooks/runtime/scripts/common/gates/validate_coding_standards.py +294 -133
  39. package/template/.zcode/hooks/runtime/scripts/common/gates/validate_rules.py +14 -17
  40. package/template/.zcode/hooks/runtime/scripts/common/task/readiness.py +4 -4
  41. package/template/AGENTS.md +31 -12
  42. package/template/skills/batch-mode/SKILL.md +117 -0
  43. package/template/skills/before-dev/SKILL.md +8 -0
  44. package/template/skills/break-loop/SKILL.md +53 -0
  45. package/template/skills/check/SKILL.md +63 -0
  46. package/template/skills/doubt-review/SKILL.md +118 -0
  47. package/template/skills/tdd/SKILL.md +13 -0
  48. package/template/skills/writing-plans/SKILL.md +23 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
@@ -5,7 +5,7 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": ".cowork-flow/run python .codex/hooks/inject-workflow-state.py",
8
+ "command": ".codex/hooks/inject-workflow-state.py",
9
9
  "timeout": 5
10
10
  }
11
11
  ]
@@ -1 +1 @@
1
- 0.0.31
1
+ 0.0.33
@@ -988,6 +988,10 @@ def cmd_start(args: argparse.Namespace) -> int:
988
988
  )
989
989
  return 1
990
990
 
991
+ if getattr(args, "auto", False):
992
+ from common.task.batch_mode import run_batch_entry
993
+ return run_batch_entry(repo_root, full_path, args)
994
+
991
995
  state_blockers = transition_blockers(_load_task_status(full_path), "in_progress")
992
996
  if state_blockers:
993
997
  _print_transition_blockers(state_blockers)
@@ -1351,6 +1355,17 @@ def _print_blockers(blockers: list[str]) -> None:
1351
1355
  print(f" - {blocker}")
1352
1356
 
1353
1357
 
1358
+ def _is_git_dirty(repo_root: Path) -> bool:
1359
+ """Check if the git working tree has uncommitted changes."""
1360
+ try:
1361
+ rc, _, _ = _run_git_command(
1362
+ ["status", "--porcelain"], cwd=repo_root
1363
+ )
1364
+ return rc != 0
1365
+ except (OSError, Exception):
1366
+ return False
1367
+
1368
+
1354
1369
  def _linked_active_changes_for_task(repo_root: Path, task_dir: Path) -> list[str]:
1355
1370
  from commands.change import linked_active_changes_for_task
1356
1371
 
@@ -1479,7 +1494,7 @@ def cmd_next(args: argparse.Namespace) -> int:
1479
1494
  return 0
1480
1495
 
1481
1496
  if status in DONE_STATUSES:
1482
- print("Next action: finalize, commit, archive, and record session")
1497
+ print("Next action: finalize, archive, commit, and record session")
1483
1498
  print("Command: git status --short")
1484
1499
  linked_changes = _linked_active_changes_for_task(repo_root, task_dir)
1485
1500
  print(f"Then: ./.cowork-flow/run task archive {Path(task_path).name}")
@@ -1546,6 +1561,14 @@ def cmd_archive(args: argparse.Namespace) -> int:
1546
1561
  if linked_changes and not _linked_changes_ready_for_archive(repo_root, linked_changes):
1547
1562
  return 1
1548
1563
 
1564
+ # R-WF-011: warn if uncommitted changes detected (archive should come before commit)
1565
+ if _is_git_dirty(repo_root):
1566
+ print(colored(
1567
+ "Warning: Uncommitted changes detected. "
1568
+ "Archive the task first, then commit the archived result.",
1569
+ Colors.YELLOW,
1570
+ ), file=sys.stderr)
1571
+
1549
1572
  # Archive
1550
1573
  result = archive_task_complete(task_dir, repo_root)
1551
1574
  if "archived_to" not in result:
@@ -1938,6 +1961,10 @@ def main() -> int:
1938
1961
  # start
1939
1962
  p_start = subparsers.add_parser("start", help="Set active session task")
1940
1963
  p_start.add_argument("dir", help="Task directory")
1964
+ p_start.add_argument("--auto", action="store_true",
1965
+ help="Enable batch mode (requires --approved)")
1966
+ p_start.add_argument("--approved", action="store_true",
1967
+ help="User has approved the plan")
1941
1968
 
1942
1969
  # current
1943
1970
  subparsers.add_parser("current", help="Show active session task")
@@ -25,6 +25,14 @@ def read_json_file(path: Path) -> dict | None:
25
25
  return None
26
26
 
27
27
 
28
+ def read_text_utf8(path: Path) -> str:
29
+ """Read a text file as UTF-8; return "" if missing or unreadable."""
30
+ try:
31
+ return path.read_text(encoding="utf-8")
32
+ except (OSError, UnicodeDecodeError):
33
+ return ""
34
+
35
+
28
36
  def write_json_file(path: Path, data: dict) -> bool:
29
37
  """Write dict to JSON file atomically (temp + os.replace)."""
30
38
  try:
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python3
2
+ """Anti-rationalization pattern detection for TDD exemptions."""
3
+
4
+ from __future__ import annotations
5
+
6
+ RATIONALIZATION_PATTERNS = (
7
+ "太简单",
8
+ "显而易",
9
+ "没必要",
10
+ "不需要测试",
11
+ "已经有其他测试",
12
+ "肉眼",
13
+ "self-evident",
14
+ "trivial",
15
+ "obvious",
16
+ "no need to test",
17
+ "covered elsewhere",
18
+ "already tested",
19
+ "just a",
20
+ "only a",
21
+ )
22
+
23
+
24
+ def validate_exemption_rationalization(reason: str) -> list[dict]:
25
+ """Detect anti-rationalization patterns in TDD exemption reasons.
26
+
27
+ Returns violation dicts with rule_id TDD-RATIONALIZE-001.
28
+ Severity is warn (does not block task start/review).
29
+ """
30
+ violations: list[dict] = []
31
+ if not reason:
32
+ return violations
33
+ lower = reason.lower()
34
+ for pattern in RATIONALIZATION_PATTERNS:
35
+ if pattern.lower() in lower:
36
+ violations.append({
37
+ "rule_id": "TDD-RATIONALIZE-001",
38
+ "type": "tdd_evidence",
39
+ "severity": "warn",
40
+ "passed": False,
41
+ "message": (
42
+ f"TDD exemption reason contains rationalization pattern: "
43
+ f"'{pattern}'. Replace with a concrete behavior test or "
44
+ f"specific verification command."
45
+ ),
46
+ "file": "<task>/tdd.jsonl",
47
+ "fix_hint": (
48
+ "Remove rationalization from exemption reason. "
49
+ "Either write a failing behavior test for this AC "
50
+ "or provide a verification command that exercises "
51
+ "the claimed unchanged behavior."
52
+ ),
53
+ })
54
+ break
55
+ return violations
@@ -58,6 +58,11 @@ class GateRunner:
58
58
  violations.extend(
59
59
  validate_coding_standards(self.repo_root, normalized_task_dir)
60
60
  )
61
+ from .validate_coding_standards import validate_complexity_signals
62
+
63
+ violations.extend(
64
+ validate_complexity_signals(self.repo_root, normalized_task_dir)
65
+ )
61
66
  return GateResult.from_violations(scope, violations, normalized_task_dir)
62
67
 
63
68
  def log(self, result: GateResult) -> None:
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+ """Spec markdown section validator for cowork-flow."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ from pathlib import Path
8
+
9
+ SECTION_CONFIG: dict[str, tuple[str, ...]] = {
10
+ "contract": ("Goal", "When to Use", "Rules", "Examples"),
11
+ "skill": ("Overview", "When to Use", "Steps", "Verification"),
12
+ "guide": ("Goal", "When to Use", "Process", "Anti-Rationalization"),
13
+ "default": ("Goal", "Rules"),
14
+ }
15
+
16
+ HEADER_RE = re.compile(r"^##\s+(.+)$", re.MULTILINE)
17
+
18
+
19
+ def validate_sections(file_path: Path | str, spec_type: str = "default") -> tuple[bool, list[str]]:
20
+ """Validate a spec markdown file has all required sections.
21
+
22
+ Returns (is_valid, error_messages).
23
+ """
24
+ path = Path(file_path)
25
+ errors: list[str] = []
26
+
27
+ if not path.is_file():
28
+ return False, [f"File not found: {path}"]
29
+
30
+ content = path.read_text(encoding="utf-8").strip()
31
+ if not content:
32
+ return False, ["File is empty"]
33
+
34
+ required = SECTION_CONFIG.get(spec_type)
35
+ if required is None:
36
+ return False, [f"Unknown spec type: {spec_type}"]
37
+
38
+ matches = list(HEADER_RE.finditer(content))
39
+ found: dict[str, str] = {}
40
+ for i, m in enumerate(matches):
41
+ name = m.group(1).strip()
42
+ start = m.end()
43
+ end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
44
+ body = content[start:end].strip()
45
+ found[name] = body
46
+
47
+ for req in required:
48
+ if req not in found:
49
+ errors.append(f"Missing: {req}")
50
+ elif not found[req]:
51
+ errors.append(f"Empty: {req}")
52
+
53
+ return (not errors, errors)
@@ -161,11 +161,7 @@ def _task_requires_tdd(prd_text: str) -> bool:
161
161
  return any(marker.lower() in lower for marker in TDD_REQUIRED_MARKERS)
162
162
 
163
163
 
164
- def _read_text(path: Path) -> str:
165
- try:
166
- return path.read_text(encoding="utf-8")
167
- except (OSError, UnicodeDecodeError):
168
- return ""
164
+ from common.core.files import read_text_utf8 as _read_text
169
165
 
170
166
 
171
167
  def _read_jsonl(path: Path) -> tuple[list[dict], list[dict]]:
@@ -343,6 +339,9 @@ def _validate_exemption(
343
339
  )
344
340
  )
345
341
 
342
+ from .anti_rationalization import validate_exemption_rationalization
343
+ violations.extend(validate_exemption_rationalization(str(entry.get("reason", ""))))
344
+
346
345
  return violations
347
346
 
348
347
 
@@ -143,11 +143,7 @@ def _read_tdd_entries(path: Path) -> list[dict]:
143
143
  return entries
144
144
 
145
145
 
146
- def _read_text(path: Path) -> str:
147
- try:
148
- return path.read_text(encoding="utf-8")
149
- except (OSError, UnicodeDecodeError):
150
- return ""
146
+ from common.core.files import read_text_utf8 as _read_text
151
147
 
152
148
 
153
149
  def _classify_test_content(content: str, test_name: str) -> str: