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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,7 +35,7 @@ the task.
35
35
 
36
36
  Load context before checking:
37
37
  1. Read the task directory from the bound runtime context.
38
- 2. Read `<task>/prd.md`.
38
+ 2. Read `<task>/decision-anchor.md`.
39
39
  3. Read the plan file linked from this task (check `<task>/task.json` `relatedFiles` for a plan path, or search `.cowork-flow/plans/` for a plan file referencing this task directory). Use the plan's Verify commands and Expected results as the checklist for each step.
40
40
  4. Read `<task>/check.jsonl`.
41
41
  5. Read `git diff`.
@@ -35,8 +35,8 @@ the task.
35
35
 
36
36
  Load context before editing:
37
37
  1. Read the task directory from the bound runtime context.
38
- 2. Read `<task>/prd.md`.
39
- 3. Read `<task>/info.md` if present.
38
+ 2. Read `<task>/decision-anchor.md`.
39
+ 3. Read .
40
40
  4. Read `<task>/implement.jsonl`.
41
41
  5. Read each JSONL `file` entry.
42
42
  6. Read the plan file linked from this task (check `<task>/task.json` `relatedFiles` for a plan path, or search `.cowork-flow/plans/` for a plan file referencing this task directory). Follow the plan steps in order — each step has Files, Action, Verify, and Expected fields.
@@ -1 +1 @@
1
- 0.0.32
1
+ 0.0.34
@@ -485,9 +485,13 @@ def _task_start_blockers(task_dir: Path) -> list[str]:
485
485
  if not task_json.is_file():
486
486
  blockers.append("task.json is missing")
487
487
 
488
- prd_file = task_dir / "prd.md"
489
- if not _read_text_if_present(prd_file):
490
- blockers.append("prd.md is missing or empty")
488
+ # 只检查存在性 — R-WF-008 负责章节内容完整性
489
+ if _read_text_if_present(task_dir / "prd.md"):
490
+ # 旧格式:提示需要迁移(cmd_start 会自动迁移)
491
+ if not _read_text_if_present(task_dir / "decision-anchor.md"):
492
+ blockers.append("prd.md found; task start will auto-migrate to decision-anchor.md")
493
+ elif not _read_text_if_present(task_dir / "decision-anchor.md"):
494
+ blockers.append("decision-anchor.md is missing or empty")
491
495
 
492
496
  for jsonl_name in CONTEXT_JSONL_FILES:
493
497
  jsonl_file = task_dir / jsonl_name
@@ -661,10 +665,29 @@ def cmd_create(args: argparse.Namespace) -> int:
661
665
 
662
666
  print(colored(f"Linked as child of: {parent_dir.name}", Colors.GREEN), file=sys.stderr)
663
667
 
668
+ # --from-plan: 自动生成 decision-anchor.md 骨架
669
+ if getattr(args, "from_plan", None):
670
+ plan_path = Path(args.from_plan)
671
+ if plan_path.is_file():
672
+ plan_text = plan_path.read_text(encoding="utf-8")
673
+ goal = next(
674
+ (line.split(":", 1)[1].strip() for line in plan_text.splitlines()
675
+ if line.startswith("**Goal:**") or line.startswith("**目标:**")),
676
+ None,
677
+ )
678
+ if goal:
679
+ anchor_file = task_dir / "decision-anchor.md"
680
+ if not anchor_file.exists():
681
+ anchor_file.write_text(
682
+ f"## 目标\n\n{goal}\n\n## 验收标准\n- [ ] \n",
683
+ encoding="utf-8",
684
+ )
685
+ print(f" {colored('[OK]', Colors.GREEN)} Generated decision-anchor.md from plan")
686
+
664
687
  print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
665
688
  print("", file=sys.stderr)
666
689
  print(colored("Next steps:", Colors.BLUE), file=sys.stderr)
667
- print(" 1. Create prd.md with requirements", file=sys.stderr)
690
+ print(" 1. Create decision-anchor.md with requirements", file=sys.stderr)
668
691
  print(" 2. Run: ./.cowork-flow/run task init-context <dir> <dev_type>", file=sys.stderr)
669
692
  print(" 3. Run: ./.cowork-flow/run task start <dir>", file=sys.stderr)
670
693
  print("", file=sys.stderr)
@@ -961,11 +984,29 @@ def cmd_start(args: argparse.Namespace) -> int:
961
984
  for blocker in blockers:
962
985
  print(f" - {blocker}", file=sys.stderr)
963
986
  print(
964
- "Hint: write prd.md, run ./.cowork-flow/run task init-context <dir> <dev_type>, then retry",
987
+ "Hint: write decision-anchor.md, run ./.cowork-flow/run task init-context <dir> <dev_type>, then retry",
965
988
  file=sys.stderr,
966
989
  )
967
990
  return 1
968
991
 
992
+ # 自动迁移旧 prd.md 到 decision-anchor.md
993
+ prd_file = full_path / "prd.md"
994
+ anchor_file = full_path / "decision-anchor.md"
995
+ if prd_file.exists() and not anchor_file.exists():
996
+ content = prd_file.read_text(encoding="utf-8").strip()
997
+ if not content:
998
+ content = "(empty legacy prd.md)"
999
+ if "## 目标" not in content and "## Goal" not in content:
1000
+ content = "## 目标\n\n" + content
1001
+ if "## 验收标准" not in content and "## Acceptance" not in content:
1002
+ content += "\n\n## 验收标准\n- [ ] \n"
1003
+ anchor_file.write_text(content, encoding="utf-8")
1004
+ prd_file.unlink()
1005
+ print(
1006
+ colored(" [迁移] prd.md → decision-anchor.md", Colors.YELLOW),
1007
+ file=sys.stderr,
1008
+ )
1009
+
969
1010
  validation_issues = _task_context_validation_issues(full_path, repo_root)
970
1011
  if validation_issues:
971
1012
  print(colored("Error: Task context validation failed", Colors.RED), file=sys.stderr)
@@ -988,6 +1029,10 @@ def cmd_start(args: argparse.Namespace) -> int:
988
1029
  )
989
1030
  return 1
990
1031
 
1032
+ if getattr(args, "auto", False):
1033
+ from common.task.batch_mode import run_batch_entry
1034
+ return run_batch_entry(repo_root, full_path, args)
1035
+
991
1036
  state_blockers = transition_blockers(_load_task_status(full_path), "in_progress")
992
1037
  if state_blockers:
993
1038
  _print_transition_blockers(state_blockers)
@@ -1351,6 +1396,17 @@ def _print_blockers(blockers: list[str]) -> None:
1351
1396
  print(f" - {blocker}")
1352
1397
 
1353
1398
 
1399
+ def _is_git_dirty(repo_root: Path) -> bool:
1400
+ """Check if the git working tree has uncommitted changes."""
1401
+ try:
1402
+ rc, _, _ = _run_git_command(
1403
+ ["status", "--porcelain"], cwd=repo_root
1404
+ )
1405
+ return rc != 0
1406
+ except (OSError, Exception):
1407
+ return False
1408
+
1409
+
1354
1410
  def _linked_active_changes_for_task(repo_root: Path, task_dir: Path) -> list[str]:
1355
1411
  from commands.change import linked_active_changes_for_task
1356
1412
 
@@ -1479,7 +1535,7 @@ def cmd_next(args: argparse.Namespace) -> int:
1479
1535
  return 0
1480
1536
 
1481
1537
  if status in DONE_STATUSES:
1482
- print("Next action: finalize, commit, archive, and record session")
1538
+ print("Next action: finalize, archive, commit, and record session")
1483
1539
  print("Command: git status --short")
1484
1540
  linked_changes = _linked_active_changes_for_task(repo_root, task_dir)
1485
1541
  print(f"Then: ./.cowork-flow/run task archive {Path(task_path).name}")
@@ -1546,6 +1602,14 @@ def cmd_archive(args: argparse.Namespace) -> int:
1546
1602
  if linked_changes and not _linked_changes_ready_for_archive(repo_root, linked_changes):
1547
1603
  return 1
1548
1604
 
1605
+ # R-WF-011: warn if uncommitted changes detected (archive should come before commit)
1606
+ if _is_git_dirty(repo_root):
1607
+ print(colored(
1608
+ "Warning: Uncommitted changes detected. "
1609
+ "Archive the task first, then commit the archived result.",
1610
+ Colors.YELLOW,
1611
+ ), file=sys.stderr)
1612
+
1549
1613
  # Archive
1550
1614
  result = archive_task_complete(task_dir, repo_root)
1551
1615
  if "archived_to" not in result:
@@ -1914,6 +1978,7 @@ def main() -> int:
1914
1978
  p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)")
1915
1979
  p_create.add_argument("--description", "-d", help="Task description")
1916
1980
  p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)")
1981
+ p_create.add_argument("--from-plan", "-f", help="Path to plan file (auto-generate decision-anchor skeleton)")
1917
1982
 
1918
1983
  # init-context
1919
1984
  p_init = subparsers.add_parser("init-context", help="Initialize context files")
@@ -1938,6 +2003,10 @@ def main() -> int:
1938
2003
  # start
1939
2004
  p_start = subparsers.add_parser("start", help="Set active session task")
1940
2005
  p_start.add_argument("dir", help="Task directory")
2006
+ p_start.add_argument("--auto", action="store_true",
2007
+ help="Enable batch mode (requires --approved)")
2008
+ p_start.add_argument("--approved", action="store_true",
2009
+ help="User has approved the plan")
1941
2010
 
1942
2011
  # current
1943
2012
  subparsers.add_parser("current", help="Show active session task")
@@ -268,8 +268,8 @@ def build_worker_resume_text(
268
268
  if isinstance(file_value, str) and file_value.strip():
269
269
  suffix = f" - {reason}" if isinstance(reason, str) and reason.strip() else ""
270
270
  lines.append(f"- {file_value}{suffix}")
271
- if context.task_dir and (repo_root / context.task_dir / "prd.md").is_file():
272
- lines.append(f"- Read task PRD: {context.task_dir}/prd.md")
271
+ if context.task_dir and (repo_root / context.task_dir / "decision-anchor.md").is_file():
272
+ lines.append(f"- Read task PRD: {context.task_dir}/decision-anchor.md")
273
273
  lines.append("- Follow only the files, steps, and commands named in the worker brief.")
274
274
 
275
275
  forbidden_actions = context_data.get("forbiddenActions")
@@ -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)
@@ -82,7 +82,7 @@ SETUP_FAILURE_MARKERS = (
82
82
  def validate_tdd_evidence(task_dir: Path) -> list[dict]:
83
83
  """Return TDD evidence violations for a task directory."""
84
84
  task_dir = Path(task_dir)
85
- prd_text = _read_text(task_dir / "prd.md")
85
+ prd_text = _read_text(task_dir / "decision-anchor.md")
86
86
  evidence_path = task_dir / EVIDENCE_FILE
87
87
 
88
88
  if not evidence_path.is_file():
@@ -127,7 +127,7 @@ def validate_tdd_evidence(task_dir: Path) -> list[dict]:
127
127
  def validate_tdd_red_evidence(task_dir: Path) -> list[dict]:
128
128
  """Return violations when implementation would start without red evidence."""
129
129
  task_dir = Path(task_dir)
130
- prd_text = _read_text(task_dir / "prd.md")
130
+ prd_text = _read_text(task_dir / "decision-anchor.md")
131
131
  if not _task_requires_tdd(prd_text):
132
132
  return []
133
133
 
@@ -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]]:
@@ -214,7 +210,10 @@ def _read_jsonl(path: Path) -> tuple[list[dict], list[dict]]:
214
210
 
215
211
 
216
212
  def _acceptance_ids(prd_text: str) -> set[str]:
217
- return set(re.findall(r"\bAC-\d{3}\b", prd_text))
213
+ """提取验收标准 ID,支持 AC-001、AC-1、验收标准:1 等格式。"""
214
+ ids = set(re.findall(r"\bAC-(\d{1,4})\b", prd_text, re.IGNORECASE))
215
+ ids |= set(re.findall(r"验收标准[::]\s*(\d+)", prd_text))
216
+ return {f"AC-{n}" for n in ids}
218
217
 
219
218
 
220
219
  def _validate_evidence(
@@ -343,6 +342,9 @@ def _validate_exemption(
343
342
  )
344
343
  )
345
344
 
345
+ from .anti_rationalization import validate_exemption_rationalization
346
+ violations.extend(validate_exemption_rationalization(str(entry.get("reason", ""))))
347
+
346
348
  return violations
347
349
 
348
350
 
@@ -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: