cowork-flow 0.0.20 → 0.0.21

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 (70) hide show
  1. package/README.md +65 -20
  2. package/package.json +1 -1
  3. package/src/cli.js +109 -2
  4. package/src/commands/init.js +61 -4
  5. package/src/commands/sync.js +10 -1
  6. package/src/lib/copy-template.js +38 -7
  7. package/src/lib/platforms.js +73 -0
  8. package/template/.agent/skills/brainstorming/SKILL.md +13 -9
  9. package/template/.agent/skills/finish-work/SKILL.md +3 -3
  10. package/template/.agent/skills/meta/SKILL.md +1 -1
  11. package/template/.agent/skills/start/SKILL.md +35 -16
  12. package/template/.agent/skills/writing-plans/SKILL.md +1 -1
  13. package/template/.claude/agents/cowork-check.md +41 -0
  14. package/template/.claude/agents/cowork-implement.md +40 -0
  15. package/template/.claude/agents/cowork-research.md +40 -0
  16. package/template/.claude/commands/cowork-check.md +27 -0
  17. package/template/.claude/commands/cowork-implement.md +27 -0
  18. package/template/.claude/commands/cowork-research.md +27 -0
  19. package/template/.claude/hooks/inject-workflow-state.py +298 -0
  20. package/template/.claude/settings.json +24 -0
  21. package/template/.claude/skills/before-dev/SKILL.md +31 -0
  22. package/template/.claude/skills/brainstorming/SKILL.md +32 -0
  23. package/template/.claude/skills/break-loop/SKILL.md +26 -0
  24. package/template/.claude/skills/check/SKILL.md +29 -0
  25. package/template/.claude/skills/continue/SKILL.md +29 -0
  26. package/template/.claude/skills/finish-work/SKILL.md +32 -0
  27. package/template/.claude/skills/meta/SKILL.md +30 -0
  28. package/template/.claude/skills/python-design/SKILL.md +27 -0
  29. package/template/.claude/skills/start/SKILL.md +78 -0
  30. package/template/.claude/skills/update-spec/SKILL.md +39 -0
  31. package/template/.claude/skills/writing-plans/SKILL.md +70 -0
  32. package/template/.codex/agents/cowork-check.toml +17 -22
  33. package/template/.codex/agents/cowork-implement.toml +18 -23
  34. package/template/.codex/agents/cowork-research.toml +14 -19
  35. package/template/.codex/agents/default.toml +27 -0
  36. package/template/.codex/agents/explorer.toml +28 -0
  37. package/template/.codex/agents/worker.toml +29 -0
  38. package/template/.codex/hooks/inject-workflow-state.py +161 -108
  39. package/template/.cowork-flow/.version +1 -1
  40. package/template/.cowork-flow/adapters/claude-code/adapter.yaml +35 -0
  41. package/template/.cowork-flow/adapters/codex/adapter.yaml +35 -0
  42. package/template/.cowork-flow/adapters/opencode/adapter.yaml +33 -0
  43. package/template/.cowork-flow/config.yaml +4 -6
  44. package/template/.cowork-flow/scripts/add_session.py +20 -7
  45. package/template/.cowork-flow/scripts/common/active_task.py +212 -1
  46. package/template/.cowork-flow/scripts/common/config.py +0 -17
  47. package/template/.cowork-flow/scripts/common/entry_classifier.py +122 -0
  48. package/template/.cowork-flow/scripts/common/execution_context.py +10 -17
  49. package/template/.cowork-flow/scripts/common/readiness.py +249 -0
  50. package/template/.cowork-flow/scripts/doctor.py +418 -40
  51. package/template/.cowork-flow/scripts/project_context.py +230 -0
  52. package/template/.cowork-flow/scripts/run.py +3 -0
  53. package/template/.cowork-flow/scripts/subagent.py +152 -238
  54. package/template/.cowork-flow/scripts/task.py +330 -17
  55. package/template/.cowork-flow/spec/adapter.schema.json +132 -0
  56. package/template/.cowork-flow/spec/capabilities.md +34 -0
  57. package/template/.cowork-flow/spec/entry-contract.md +45 -0
  58. package/template/.cowork-flow/spec/registry.json +54 -0
  59. package/template/.cowork-flow/spec/subagent-dispatch.md +77 -0
  60. package/template/.cowork-flow/spec/workflow-state-templates.md +66 -0
  61. package/template/.cowork-flow/workflow.md +158 -203
  62. package/template/.opencode/agents/cowork-check.md +46 -0
  63. package/template/.opencode/agents/cowork-implement.md +45 -0
  64. package/template/.opencode/agents/cowork-research.md +46 -0
  65. package/template/.opencode/commands/cowork-check.md +26 -0
  66. package/template/.opencode/commands/cowork-implement.md +26 -0
  67. package/template/.opencode/commands/cowork-research.md +26 -0
  68. package/template/.opencode/plugins/cowork-flow.js +317 -0
  69. package/template/CLAUDE.md +5 -0
  70. package/template/.agent/skills/entry-boundary/SKILL.md +0 -81
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env python3
2
+ """Readiness checks shared by task lifecycle commands."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ from pathlib import Path
8
+
9
+ from .paths import DIR_ARCHIVE, DIR_CHANGES, DIR_TASKS, DIR_WORKFLOW, FILE_TASK_JSON, get_tasks_dir
10
+ from .task_utils import find_task_by_name
11
+
12
+ REQUIRED_L2_MARKERS = {
13
+ "goal and user value": (
14
+ ("## goal", "## problem", "目标"),
15
+ ("## benefits", "user value", "用户价值", "价值"),
16
+ ),
17
+ "non-goals": (("## non-goals", "non-goals", "非目标"),),
18
+ "key assumptions": (("assumption", "assumptions", "关键假设"),),
19
+ "scope boundary": (("## scope", "scope boundary", "范围边界", "scope:"),),
20
+ "acceptance criteria": (("acceptance criteria", "## acceptance", "验收标准", "验收"),),
21
+ }
22
+
23
+ VERIFICATION_COMMAND_MARKERS = (
24
+ "python -m unittest",
25
+ "npm test",
26
+ "npm run",
27
+ "git diff --check",
28
+ )
29
+
30
+
31
+ def _read_text(path: Path) -> str:
32
+ try:
33
+ return path.read_text(encoding="utf-8").strip()
34
+ except (OSError, UnicodeDecodeError):
35
+ return ""
36
+
37
+
38
+ def _read_json(path: Path) -> dict:
39
+ try:
40
+ data = json.loads(path.read_text(encoding="utf-8"))
41
+ except (OSError, json.JSONDecodeError):
42
+ return {}
43
+ return data if isinstance(data, dict) else {}
44
+
45
+
46
+ def _parse_scalar(value: str) -> object:
47
+ if value == "null":
48
+ return None
49
+ if value == "true":
50
+ return True
51
+ if value == "false":
52
+ return False
53
+ return value
54
+
55
+
56
+ def _read_metadata(path: Path) -> dict[str, object]:
57
+ data: dict[str, object] = {}
58
+ text = _read_text(path)
59
+ if not text:
60
+ return data
61
+
62
+ for raw_line in text.splitlines():
63
+ line = raw_line.strip()
64
+ if not line or line.startswith("#") or ":" not in line:
65
+ continue
66
+ key, value = line.split(":", 1)
67
+ data[key.strip()] = _parse_scalar(value.strip())
68
+ return data
69
+
70
+
71
+ def _display_path(repo_root: Path, path: Path) -> str:
72
+ try:
73
+ return path.resolve().relative_to(repo_root.resolve()).as_posix()
74
+ except ValueError:
75
+ return str(path)
76
+
77
+
78
+ def _resolve_link(repo_root: Path, base_dir: str, value: object) -> Path | None:
79
+ if not isinstance(value, str) or not value.strip():
80
+ return None
81
+
82
+ raw_path = Path(value)
83
+ if raw_path.is_absolute():
84
+ return raw_path
85
+
86
+ direct = repo_root / raw_path
87
+ workflow_base = Path(DIR_WORKFLOW) / base_dir
88
+ if direct.exists() or raw_path.parts[:2] == workflow_base.parts:
89
+ return direct
90
+ return repo_root / workflow_base / raw_path
91
+
92
+
93
+ def _same_path(left: Path, right: Path) -> bool:
94
+ try:
95
+ return left.resolve() == right.resolve()
96
+ except OSError:
97
+ return left == right
98
+
99
+
100
+ def _task_ancestry(repo_root: Path, task_dir: Path) -> list[Path]:
101
+ tasks_dir = get_tasks_dir(repo_root)
102
+ ancestry = [task_dir]
103
+ seen = {task_dir.name}
104
+ current = task_dir
105
+
106
+ while True:
107
+ data = _read_json(current / FILE_TASK_JSON)
108
+ parent = data.get("parent")
109
+ if not isinstance(parent, str) or not parent.strip() or parent in seen:
110
+ break
111
+ parent_dir = find_task_by_name(parent, tasks_dir)
112
+ if parent_dir is None:
113
+ break
114
+ ancestry.append(parent_dir)
115
+ seen.add(parent)
116
+ current = parent_dir
117
+
118
+ return ancestry
119
+
120
+
121
+ def _task_tokens(repo_root: Path, ancestry: list[Path]) -> set[str]:
122
+ tokens: set[str] = set()
123
+ for path in ancestry:
124
+ tokens.add(path.name)
125
+ tokens.add(_display_path(repo_root, path))
126
+ return {token for token in tokens if token}
127
+
128
+
129
+ def _plan_mentions_task(repo_root: Path, plan_path: Path | None, ancestry: list[Path]) -> bool:
130
+ if plan_path is None or not plan_path.is_file():
131
+ return False
132
+ text = _read_text(plan_path)
133
+ return any(token in text for token in _task_tokens(repo_root, ancestry))
134
+
135
+
136
+ def _change_applies_to_task(
137
+ repo_root: Path,
138
+ metadata: dict[str, object],
139
+ task_dir: Path,
140
+ ) -> tuple[bool, bool]:
141
+ ancestry = _task_ancestry(repo_root, task_dir)
142
+ linked_task = _resolve_link(repo_root, DIR_TASKS, metadata.get("task"))
143
+ if linked_task is not None and any(_same_path(linked_task, item) for item in ancestry):
144
+ return True, False
145
+
146
+ plan_path = _resolve_link(repo_root, "plans", metadata.get("plan"))
147
+ if _plan_mentions_task(repo_root, plan_path, ancestry):
148
+ return True, linked_task is None
149
+
150
+ return False, False
151
+
152
+
153
+ def _has_all_marker_groups(text: str, groups: tuple[tuple[str, ...], ...]) -> bool:
154
+ lower = text.lower()
155
+ return all(any(marker.lower() in lower for marker in group) for group in groups)
156
+
157
+
158
+ def _has_verification_command(plan_text: str) -> bool:
159
+ lower = plan_text.lower()
160
+ if "verification" not in lower and "验证" not in lower:
161
+ return False
162
+ return any(marker in lower for marker in VERIFICATION_COMMAND_MARKERS)
163
+
164
+
165
+ def _read_l2_context(repo_root: Path, change_dir: Path, plan_path: Path | None, task_dir: Path) -> str:
166
+ parts = [
167
+ _read_text(change_dir / "proposal.md"),
168
+ _read_text(change_dir / "spec.md"),
169
+ _read_text(change_dir / "design.md"),
170
+ _read_text(task_dir / "prd.md"),
171
+ ]
172
+ for ancestor in _task_ancestry(repo_root, task_dir)[1:]:
173
+ parts.append(_read_text(ancestor / "prd.md"))
174
+ if plan_path is not None:
175
+ parts.append(_read_text(plan_path))
176
+ return "\n\n".join(part for part in parts if part)
177
+
178
+
179
+ def _append_missing_file_blocker(
180
+ blockers: list[str],
181
+ slug: str,
182
+ path: Path,
183
+ label: str,
184
+ ) -> None:
185
+ if not _read_text(path):
186
+ blockers.append(f"L2 readiness ({slug}): {label} is missing or empty")
187
+
188
+
189
+ def _iter_change_dirs(repo_root: Path) -> list[Path]:
190
+ changes_dir = repo_root / DIR_WORKFLOW / DIR_CHANGES
191
+ if not changes_dir.is_dir():
192
+ return []
193
+ return [
194
+ path
195
+ for path in sorted(changes_dir.iterdir())
196
+ if path.is_dir() and path.name != DIR_ARCHIVE
197
+ ]
198
+
199
+
200
+ def task_readiness_blockers(repo_root: Path, task_dir: Path) -> list[str]:
201
+ """Return readiness blockers for work linked to L2 changes."""
202
+ blockers: list[str] = []
203
+
204
+ for change_dir in _iter_change_dirs(repo_root):
205
+ metadata = _read_metadata(change_dir / "change.yaml")
206
+ if metadata.get("level") != "L2" or metadata.get("status") == "archived":
207
+ continue
208
+
209
+ applies, missing_task_link = _change_applies_to_task(repo_root, metadata, task_dir)
210
+ if not applies:
211
+ continue
212
+
213
+ slug = change_dir.name
214
+ if missing_task_link:
215
+ blockers.append(f"L2 readiness ({slug}): change.yaml task link is missing")
216
+
217
+ _append_missing_file_blocker(blockers, slug, change_dir / "proposal.md", "proposal.md")
218
+ _append_missing_file_blocker(blockers, slug, change_dir / "spec.md", "spec.md")
219
+ _append_missing_file_blocker(blockers, slug, change_dir / "design.md", "design.md")
220
+
221
+ plan_path = _resolve_link(repo_root, "plans", metadata.get("plan"))
222
+ if plan_path is None:
223
+ blockers.append(f"L2 readiness ({slug}): change.yaml plan link is missing")
224
+ plan_text = ""
225
+ elif not plan_path.exists():
226
+ blockers.append(
227
+ f"L2 readiness ({slug}): plan points to missing path: {_display_path(repo_root, plan_path)}"
228
+ )
229
+ plan_text = ""
230
+ else:
231
+ plan_text = _read_text(plan_path)
232
+ if not plan_text:
233
+ blockers.append(f"L2 readiness ({slug}): linked plan is empty")
234
+
235
+ linked_task = _resolve_link(repo_root, DIR_TASKS, metadata.get("task"))
236
+ if linked_task is not None and not linked_task.exists():
237
+ blockers.append(
238
+ f"L2 readiness ({slug}): task points to missing path: {_display_path(repo_root, linked_task)}"
239
+ )
240
+
241
+ context = _read_l2_context(repo_root, change_dir, plan_path, task_dir)
242
+ for label, marker_groups in REQUIRED_L2_MARKERS.items():
243
+ if not _has_all_marker_groups(context, marker_groups):
244
+ blockers.append(f"L2 readiness ({slug}): missing {label}")
245
+
246
+ if not _has_verification_command(plan_text):
247
+ blockers.append(f"L2 readiness ({slug}): linked plan is missing verification commands")
248
+
249
+ return blockers