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.
- package/README.md +65 -20
- package/package.json +1 -1
- package/src/cli.js +109 -2
- package/src/commands/init.js +61 -4
- package/src/commands/sync.js +10 -1
- package/src/lib/copy-template.js +38 -7
- package/src/lib/platforms.js +73 -0
- package/template/.agent/skills/brainstorming/SKILL.md +13 -9
- package/template/.agent/skills/finish-work/SKILL.md +3 -3
- package/template/.agent/skills/meta/SKILL.md +1 -1
- package/template/.agent/skills/start/SKILL.md +35 -16
- package/template/.agent/skills/writing-plans/SKILL.md +1 -1
- package/template/.claude/agents/cowork-check.md +41 -0
- package/template/.claude/agents/cowork-implement.md +40 -0
- package/template/.claude/agents/cowork-research.md +40 -0
- package/template/.claude/commands/cowork-check.md +27 -0
- package/template/.claude/commands/cowork-implement.md +27 -0
- package/template/.claude/commands/cowork-research.md +27 -0
- package/template/.claude/hooks/inject-workflow-state.py +298 -0
- package/template/.claude/settings.json +24 -0
- package/template/.claude/skills/before-dev/SKILL.md +31 -0
- package/template/.claude/skills/brainstorming/SKILL.md +32 -0
- package/template/.claude/skills/break-loop/SKILL.md +26 -0
- package/template/.claude/skills/check/SKILL.md +29 -0
- package/template/.claude/skills/continue/SKILL.md +29 -0
- package/template/.claude/skills/finish-work/SKILL.md +32 -0
- package/template/.claude/skills/meta/SKILL.md +30 -0
- package/template/.claude/skills/python-design/SKILL.md +27 -0
- package/template/.claude/skills/start/SKILL.md +78 -0
- package/template/.claude/skills/update-spec/SKILL.md +39 -0
- package/template/.claude/skills/writing-plans/SKILL.md +70 -0
- package/template/.codex/agents/cowork-check.toml +17 -22
- package/template/.codex/agents/cowork-implement.toml +18 -23
- package/template/.codex/agents/cowork-research.toml +14 -19
- package/template/.codex/agents/default.toml +27 -0
- package/template/.codex/agents/explorer.toml +28 -0
- package/template/.codex/agents/worker.toml +29 -0
- package/template/.codex/hooks/inject-workflow-state.py +161 -108
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/adapters/claude-code/adapter.yaml +35 -0
- package/template/.cowork-flow/adapters/codex/adapter.yaml +35 -0
- package/template/.cowork-flow/adapters/opencode/adapter.yaml +33 -0
- package/template/.cowork-flow/config.yaml +4 -6
- package/template/.cowork-flow/scripts/add_session.py +20 -7
- package/template/.cowork-flow/scripts/common/active_task.py +212 -1
- package/template/.cowork-flow/scripts/common/config.py +0 -17
- package/template/.cowork-flow/scripts/common/entry_classifier.py +122 -0
- package/template/.cowork-flow/scripts/common/execution_context.py +10 -17
- package/template/.cowork-flow/scripts/common/readiness.py +249 -0
- package/template/.cowork-flow/scripts/doctor.py +418 -40
- package/template/.cowork-flow/scripts/project_context.py +230 -0
- package/template/.cowork-flow/scripts/run.py +3 -0
- package/template/.cowork-flow/scripts/subagent.py +152 -238
- package/template/.cowork-flow/scripts/task.py +330 -17
- package/template/.cowork-flow/spec/adapter.schema.json +132 -0
- package/template/.cowork-flow/spec/capabilities.md +34 -0
- package/template/.cowork-flow/spec/entry-contract.md +45 -0
- package/template/.cowork-flow/spec/registry.json +54 -0
- package/template/.cowork-flow/spec/subagent-dispatch.md +77 -0
- package/template/.cowork-flow/spec/workflow-state-templates.md +66 -0
- package/template/.cowork-flow/workflow.md +158 -203
- package/template/.opencode/agents/cowork-check.md +46 -0
- package/template/.opencode/agents/cowork-implement.md +45 -0
- package/template/.opencode/agents/cowork-research.md +46 -0
- package/template/.opencode/commands/cowork-check.md +26 -0
- package/template/.opencode/commands/cowork-implement.md +26 -0
- package/template/.opencode/commands/cowork-research.md +26 -0
- package/template/.opencode/plugins/cowork-flow.js +317 -0
- package/template/CLAUDE.md +5 -0
- 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
|