cowork-flow 2.4.1 → 2.4.2
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/CHANGELOG.md +12 -1
- package/package.json +1 -1
- package/presets/claude-code/.claude-plugin/plugin.json +1 -1
- package/presets/codex/.codex-plugin/plugin.json +1 -1
- package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
- package/presets/qoder/.qoder-plugin/plugin.json +1 -1
- package/presets/zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/adapters/cli/plan.py +218 -0
- package/template/.cowork-flow/scripts/run.py +3 -1
- package/template/.cowork-flow/scripts/services/plan_readiness.py +583 -0
- package/template/.cowork-flow/scripts/services/plan_scaffold.py +133 -0
- package/template/.cowork-flow/scripts/services/plan_schema.py +40 -0
- package/template/.cowork-flow/scripts/services/task_context.py +12 -0
- package/template/.cowork-flow/scripts/services/task_creation.py +23 -24
- package/template/.cowork-flow/spec/contracts/plan-binding.md +62 -4
- package/template/.cowork-flow/spec/runtime/contract-registry.json +2 -1
- package/template/skills/task-planning/SKILL.md +24 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Template-driven plan skeleton generation."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from infra.paths import DIR_WORKFLOW
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PLAN_TEMPLATE_PATH = f"{DIR_WORKFLOW}/templates/plan-template.md"
|
|
14
|
+
PLAN_DIRECTORY = f"{DIR_WORKFLOW}/plans"
|
|
15
|
+
SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
16
|
+
H1_RE = re.compile(r"^#(?!#)\s+(.+?)\s*$")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PlanScaffoldError(RuntimeError):
|
|
20
|
+
"""Raised when a plan skeleton cannot be generated."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, code: str, path: Path, detail: str) -> None:
|
|
23
|
+
self.code = code
|
|
24
|
+
self.path = path
|
|
25
|
+
self.detail = detail
|
|
26
|
+
super().__init__(f"{code}: {detail}: {path}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PlanScaffoldService:
|
|
30
|
+
"""Create a plan file from the installed, localized plan template."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, repo_root: Path) -> None:
|
|
33
|
+
self.repo_root = Path(repo_root)
|
|
34
|
+
|
|
35
|
+
def create(
|
|
36
|
+
self,
|
|
37
|
+
title: str,
|
|
38
|
+
slug: str,
|
|
39
|
+
*,
|
|
40
|
+
force: bool = False,
|
|
41
|
+
date_prefix: str | None = None,
|
|
42
|
+
) -> Path:
|
|
43
|
+
normalized_title = title.strip()
|
|
44
|
+
normalized_slug = slug.strip()
|
|
45
|
+
if not normalized_title:
|
|
46
|
+
raise PlanScaffoldError(
|
|
47
|
+
"PLAN-NEW-TITLE-001",
|
|
48
|
+
self.repo_root,
|
|
49
|
+
"plan title is required",
|
|
50
|
+
)
|
|
51
|
+
if not SLUG_RE.match(normalized_slug):
|
|
52
|
+
raise PlanScaffoldError(
|
|
53
|
+
"PLAN-NEW-SLUG-001",
|
|
54
|
+
self.repo_root,
|
|
55
|
+
"plan slug must contain lowercase ASCII letters, digits, and hyphens",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
template = self.repo_root / PLAN_TEMPLATE_PATH
|
|
59
|
+
if not template.is_file():
|
|
60
|
+
raise PlanScaffoldError(
|
|
61
|
+
"PLAN-NEW-TEMPLATE-001",
|
|
62
|
+
template,
|
|
63
|
+
"installed plan template is missing; run `cwf project sync .`",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
plans_dir = self.repo_root / PLAN_DIRECTORY
|
|
67
|
+
day = date_prefix or datetime.now().strftime("%Y-%m-%d")
|
|
68
|
+
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", day):
|
|
69
|
+
raise PlanScaffoldError(
|
|
70
|
+
"PLAN-NEW-DATE-001",
|
|
71
|
+
plans_dir,
|
|
72
|
+
"plan date must use YYYY-MM-DD",
|
|
73
|
+
)
|
|
74
|
+
plan_path = plans_dir / f"{day}-{normalized_slug}.md"
|
|
75
|
+
if plan_path.exists() and not force:
|
|
76
|
+
raise PlanScaffoldError(
|
|
77
|
+
"PLAN-NEW-EXISTS-001",
|
|
78
|
+
plan_path,
|
|
79
|
+
"plan already exists; pass --force to overwrite",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
text = template.read_text(encoding="utf-8")
|
|
84
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
85
|
+
raise PlanScaffoldError(
|
|
86
|
+
"PLAN-NEW-TEMPLATE-002",
|
|
87
|
+
template,
|
|
88
|
+
"installed plan template cannot be read as UTF-8",
|
|
89
|
+
) from error
|
|
90
|
+
|
|
91
|
+
rendered = self._render_title(text, normalized_title, template)
|
|
92
|
+
try:
|
|
93
|
+
plans_dir.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
with plan_path.open("w", encoding="utf-8", newline="\n") as handle:
|
|
95
|
+
handle.write(rendered)
|
|
96
|
+
except OSError as error:
|
|
97
|
+
raise PlanScaffoldError(
|
|
98
|
+
"PLAN-NEW-WRITE-001",
|
|
99
|
+
plan_path,
|
|
100
|
+
"plan file could not be written",
|
|
101
|
+
) from error
|
|
102
|
+
return plan_path
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _render_title(text: str, title: str, template: Path) -> str:
|
|
106
|
+
lines = text.splitlines()
|
|
107
|
+
for index, line in enumerate(lines):
|
|
108
|
+
match = H1_RE.match(line.strip())
|
|
109
|
+
if match is None:
|
|
110
|
+
continue
|
|
111
|
+
heading = match.group(1)
|
|
112
|
+
if "<功能>" in heading:
|
|
113
|
+
if title.endswith("实施计划"):
|
|
114
|
+
lines[index] = f"# {title}"
|
|
115
|
+
else:
|
|
116
|
+
lines[index] = f"# {heading.replace('<功能>', title)}"
|
|
117
|
+
return "\n".join(lines) + "\n"
|
|
118
|
+
if "<Feature>" in heading:
|
|
119
|
+
if title.endswith("Implementation Plan"):
|
|
120
|
+
lines[index] = f"# {title}"
|
|
121
|
+
else:
|
|
122
|
+
lines[index] = f"# {heading.replace('<Feature>', title)}"
|
|
123
|
+
return "\n".join(lines) + "\n"
|
|
124
|
+
raise PlanScaffoldError(
|
|
125
|
+
"PLAN-NEW-TITLE-002",
|
|
126
|
+
template,
|
|
127
|
+
"plan template title is missing the <功能>/<Feature> placeholder",
|
|
128
|
+
)
|
|
129
|
+
raise PlanScaffoldError(
|
|
130
|
+
"PLAN-NEW-TITLE-003",
|
|
131
|
+
template,
|
|
132
|
+
"plan template has no top-level title",
|
|
133
|
+
)
|
|
@@ -52,6 +52,15 @@ H1_RE = re.compile(r"^#(?!#)\s+(.+?)\s*$")
|
|
|
52
52
|
H2_RE = re.compile(r"^##(?!#)\s+(.+?)\s*$")
|
|
53
53
|
H3_RE = re.compile(r"^###\s+(.+?)\s*$")
|
|
54
54
|
|
|
55
|
+
GOAL_LINE_RE = re.compile(r"^\*\*(?:Goal|目标):\*\*\s*(.+?)\s*$")
|
|
56
|
+
GOAL_BOLD_COLON_RE = re.compile(r"^\*\*(?:Goal|目标)\*\*:\s*(.+?)\s*$")
|
|
57
|
+
GOAL_TABLE_RE = re.compile(
|
|
58
|
+
r"^\|\s*\*\*(?:Goal|目标)\*\*\s*\|\s*(.+?)\s*\|\s*$"
|
|
59
|
+
)
|
|
60
|
+
PLAN_AC_RE = re.compile(
|
|
61
|
+
r"^\s*-\s*(?:\[[ xX]\]\s*)?(AC-\d{3})\s*[::]\s*(.+?)\s*$"
|
|
62
|
+
)
|
|
63
|
+
|
|
55
64
|
|
|
56
65
|
def _language(language: str) -> str:
|
|
57
66
|
return language if language in PLAN_SECTION_HEADINGS else DEFAULT_LANGUAGE
|
|
@@ -106,6 +115,37 @@ def _task_prefix_blocker(heading: str, language: str, prefix: str) -> str | None
|
|
|
106
115
|
)
|
|
107
116
|
|
|
108
117
|
|
|
118
|
+
def plan_goal(text: str) -> str | None:
|
|
119
|
+
"""First machine-readable plan goal line, then the legacy table row."""
|
|
120
|
+
for raw in structural_lines(text):
|
|
121
|
+
line = raw.strip()
|
|
122
|
+
for pattern in (GOAL_LINE_RE, GOAL_BOLD_COLON_RE, GOAL_TABLE_RE):
|
|
123
|
+
match = pattern.match(line)
|
|
124
|
+
if match:
|
|
125
|
+
return match.group(1).strip()
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def plan_acceptance_criteria(
|
|
130
|
+
text: str,
|
|
131
|
+
language: str,
|
|
132
|
+
) -> list[dict[str, str]]:
|
|
133
|
+
"""Acceptance rows declared by the plan, in document order."""
|
|
134
|
+
resolved = _language(language)
|
|
135
|
+
heading = PLAN_SECTION_HEADINGS[resolved][1]
|
|
136
|
+
rows: list[dict[str, str]] = []
|
|
137
|
+
for raw in _section_lines(structural_lines(text), heading):
|
|
138
|
+
match = PLAN_AC_RE.match(raw)
|
|
139
|
+
if match:
|
|
140
|
+
rows.append({"id": match.group(1), "text": match.group(2).strip()})
|
|
141
|
+
return rows
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def section_body(text: str, heading: str) -> list[str]:
|
|
145
|
+
"""Raw lines under one `##` heading, excluding fenced code."""
|
|
146
|
+
return _section_lines(structural_lines(text), heading)
|
|
147
|
+
|
|
148
|
+
|
|
109
149
|
def plan_template_blockers(
|
|
110
150
|
text: str,
|
|
111
151
|
language: str,
|
|
@@ -49,6 +49,7 @@ from services.plan_binding import (
|
|
|
49
49
|
plan_file_blockers,
|
|
50
50
|
plan_template_blockers,
|
|
51
51
|
)
|
|
52
|
+
from services.plan_readiness import plan_ready_snapshot_blockers
|
|
52
53
|
from services.task_gate_profiles import (
|
|
53
54
|
GATE_PROFILE_STRICT,
|
|
54
55
|
acceptance_row_blockers,
|
|
@@ -376,6 +377,17 @@ class TaskContextService:
|
|
|
376
377
|
blockers.extend(
|
|
377
378
|
plan_template_blockers(self.repo_root, task_data, language)
|
|
378
379
|
)
|
|
380
|
+
if strict and not light:
|
|
381
|
+
meta = task_data.get("meta")
|
|
382
|
+
meta = meta if isinstance(meta, dict) else {}
|
|
383
|
+
blockers.extend(
|
|
384
|
+
plan_ready_snapshot_blockers(
|
|
385
|
+
self.repo_root,
|
|
386
|
+
task_dir,
|
|
387
|
+
task_data,
|
|
388
|
+
required=meta.get("planReadyRequired") is True,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
379
391
|
if strict:
|
|
380
392
|
blockers.extend(dev_type_blocker(task_data))
|
|
381
393
|
return tuple(blockers)
|
|
@@ -6,7 +6,6 @@ from __future__ import annotations
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
import re
|
|
10
9
|
|
|
11
10
|
from infra.config import get_language
|
|
12
11
|
from services.decision_anchor_schema import (
|
|
@@ -16,6 +15,7 @@ from services.decision_anchor_schema import (
|
|
|
16
15
|
heading_for,
|
|
17
16
|
spec_check_placeholder,
|
|
18
17
|
)
|
|
18
|
+
from services.plan_schema import plan_acceptance_criteria, plan_goal
|
|
19
19
|
from services.task_tree import TaskTreeError, TaskTreeService
|
|
20
20
|
from services import task_graph
|
|
21
21
|
from infra.paths import (
|
|
@@ -24,16 +24,10 @@ from infra.paths import (
|
|
|
24
24
|
get_tasks_dir,
|
|
25
25
|
)
|
|
26
26
|
from services.plan_binding import PlanBindingError, bind_plan_file
|
|
27
|
-
from services.task_gate_profiles import GATE_PROFILE_STRICT
|
|
27
|
+
from services.task_gate_profiles import GATE_PROFILE_STRICT, LIGHT_TASK_TYPE
|
|
28
28
|
from services.task_repository import TaskRepository, TaskRepositoryError
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
_GOAL_LINE_RE = re.compile(r"^\*\*(?:Goal|目标):\*\*\s*(.+?)\s*$")
|
|
32
|
-
_GOAL_BOLD_COLON_RE = re.compile(r"^\*\*(?:Goal|目标)\*\*:\s*(.+?)\s*$")
|
|
33
|
-
_GOAL_TABLE_RE = re.compile(
|
|
34
|
-
r"^\|\s*\*\*(?:Goal|目标)\*\*\s*\|\s*(.+?)\s*\|\s*$"
|
|
35
|
-
)
|
|
36
|
-
|
|
37
31
|
_ANCHOR_PLACEHOLDERS = {
|
|
38
32
|
("zh-CN", GOAL): "<!-- 用一句话描述可观察的目标。 -->",
|
|
39
33
|
("zh-CN", ACCEPTANCE): "<!-- 至少一条,例如:- [ ] AC-001: ... -->",
|
|
@@ -92,17 +86,6 @@ class TaskCreationResult:
|
|
|
92
86
|
generated_anchor_from_plan: bool
|
|
93
87
|
|
|
94
88
|
|
|
95
|
-
def _extract_plan_goal(plan_text: str) -> str | None:
|
|
96
|
-
"""First machine-readable goal line, then the legacy table row."""
|
|
97
|
-
for raw in plan_text.splitlines():
|
|
98
|
-
line = raw.strip()
|
|
99
|
-
for pattern in (_GOAL_LINE_RE, _GOAL_BOLD_COLON_RE, _GOAL_TABLE_RE):
|
|
100
|
-
match = pattern.match(line)
|
|
101
|
-
if match:
|
|
102
|
-
return match.group(1).strip()
|
|
103
|
-
return None
|
|
104
|
-
|
|
105
|
-
|
|
106
89
|
def _anchor_placeholder(section: str, language: str) -> str:
|
|
107
90
|
return _ANCHOR_PLACEHOLDERS.get(
|
|
108
91
|
(language, section),
|
|
@@ -110,6 +93,12 @@ def _anchor_placeholder(section: str, language: str) -> str:
|
|
|
110
93
|
)
|
|
111
94
|
|
|
112
95
|
|
|
96
|
+
def _render_acceptance(rows: list[dict[str, str]]) -> str:
|
|
97
|
+
return "\n".join(
|
|
98
|
+
f"- [ ] {row['id']}: {row['text']}" for row in rows
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
113
102
|
def ensure_tasks_dir(repo_root: Path) -> Path:
|
|
114
103
|
"""Ensure active and archived task directories exist."""
|
|
115
104
|
tasks_dir = get_tasks_dir(repo_root)
|
|
@@ -192,6 +181,10 @@ class TaskCreationService:
|
|
|
192
181
|
created_at = request.created_at or datetime.now().strftime("%Y-%m-%d")
|
|
193
182
|
meta = dict(plan_metadata) if isinstance(plan_metadata, dict) else {}
|
|
194
183
|
meta.setdefault("gateProfile", GATE_PROFILE_STRICT)
|
|
184
|
+
if meta.get("gateProfile") == GATE_PROFILE_STRICT and (
|
|
185
|
+
request.task_type != LIGHT_TASK_TYPE
|
|
186
|
+
):
|
|
187
|
+
meta["planReadyRequired"] = True
|
|
195
188
|
# The light profile is declarable at creation only: this is the sole
|
|
196
189
|
# write of the field, and no lifecycle command changes it later.
|
|
197
190
|
if request.task_type:
|
|
@@ -368,15 +361,17 @@ class TaskCreationService:
|
|
|
368
361
|
) -> tuple[bool, bool]:
|
|
369
362
|
"""Create a language-specific anchor scaffold.
|
|
370
363
|
|
|
371
|
-
A bound plan's goal
|
|
372
|
-
otherwise
|
|
364
|
+
A bound plan's goal and AC rows fill the corresponding sections;
|
|
365
|
+
otherwise each section carries a localized placeholder. An anchor that
|
|
373
366
|
already exists is never overwritten.
|
|
374
367
|
"""
|
|
375
368
|
anchor_file = task_dir / "decision-anchor.md"
|
|
376
369
|
if anchor_file.exists():
|
|
377
370
|
return False, False
|
|
378
371
|
|
|
372
|
+
language = get_language(self.repo_root)
|
|
379
373
|
goal: str | None = None
|
|
374
|
+
acceptance_rows: list[dict[str, str]] = []
|
|
380
375
|
if plan_path is not None and plan_path.is_file():
|
|
381
376
|
try:
|
|
382
377
|
plan_text = plan_path.read_text(encoding="utf-8")
|
|
@@ -386,14 +381,18 @@ class TaskCreationService:
|
|
|
386
381
|
plan_path,
|
|
387
382
|
"plan cannot be read as UTF-8",
|
|
388
383
|
) from error
|
|
389
|
-
goal =
|
|
384
|
+
goal = plan_goal(plan_text)
|
|
385
|
+
acceptance_rows = plan_acceptance_criteria(plan_text, language)
|
|
390
386
|
|
|
391
|
-
language = get_language(self.repo_root)
|
|
392
387
|
goal_heading = heading_for(GOAL, language)
|
|
393
388
|
acceptance_heading = heading_for(ACCEPTANCE, language)
|
|
394
389
|
verification_heading = heading_for(VERIFICATION, language)
|
|
395
390
|
goal_body = goal or _anchor_placeholder(GOAL, language)
|
|
396
|
-
acceptance_body =
|
|
391
|
+
acceptance_body = (
|
|
392
|
+
_render_acceptance(acceptance_rows)
|
|
393
|
+
if acceptance_rows
|
|
394
|
+
else _anchor_placeholder(ACCEPTANCE, language)
|
|
395
|
+
)
|
|
397
396
|
verification_comment = (
|
|
398
397
|
"<!-- strict 任务启动前:把 <dev_type> 和 "
|
|
399
398
|
"<pass / not_applicable + 理由> 替换为实际核对结果;"
|
|
@@ -28,6 +28,60 @@ the plan into a second workflow kernel.
|
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
- A newly created strict non-Tiny task also records
|
|
32
|
+
`meta.planReadyRequired: true`. Existing tasks without that marker are not
|
|
33
|
+
retroactively required to have a snapshot; if they are finalized, the
|
|
34
|
+
snapshot is recorded and validated, but a missing snapshot is not a new
|
|
35
|
+
blocker for them.
|
|
36
|
+
|
|
37
|
+
## Skeleton generation
|
|
38
|
+
|
|
39
|
+
- `./.cowork-flow/run plan new --title <title> --slug <slug>` reads the
|
|
40
|
+
installed localized `.cowork-flow/templates/plan-template.md` and writes a
|
|
41
|
+
plan skeleton to `.cowork-flow/plans/YYYY-MM-DD-<slug>.md`.
|
|
42
|
+
- The title placeholder (`<功能>` / `<Feature>`) is replaced; the rest of the
|
|
43
|
+
template shape is preserved. The file is written as UTF-8 with LF endings.
|
|
44
|
+
- An existing plan is never overwritten unless `--force` is explicit.
|
|
45
|
+
- Generation is optional; a planning agent may still fill the installed
|
|
46
|
+
template directly. `plan finalize` remains the completeness gate.
|
|
47
|
+
|
|
48
|
+
## Plan finalize
|
|
49
|
+
|
|
50
|
+
- `./.cowork-flow/run plan finalize [task-dir]` is allowed only on a planning
|
|
51
|
+
task. It does not edit the plan or anchor.
|
|
52
|
+
- It requires the bound plan, the configured-language template shape, a
|
|
53
|
+
machine-readable concrete goal line, at least one concrete `AC-NNN` row,
|
|
54
|
+
content in `## 目标` / `## 全局约束` / `## 文件边界` beyond the installed
|
|
55
|
+
template body, complete task briefs (full fields for Normal/High-risk; file
|
|
56
|
+
boundaries, verification command, and completion conditions for Tiny), a
|
|
57
|
+
concrete command inside the task's fenced verification block, a non-empty
|
|
58
|
+
`implement.jsonl`, anchor acceptance rows matching the plan IDs/text, an
|
|
59
|
+
anchor goal matching the plan goal, and the filled anchor spec-check result.
|
|
60
|
+
Template bodies are read from the installed localized template at finalize
|
|
61
|
+
time, so placeholder matching follows template changes instead of a copied
|
|
62
|
+
sentinel list.
|
|
63
|
+
- On success it writes `task.json.meta.planReady`:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"meta": {
|
|
68
|
+
"planReady": {
|
|
69
|
+
"planDigest": "<sha256>",
|
|
70
|
+
"anchorDigest": "<sha256>",
|
|
71
|
+
"readyAt": "2026-10-23T00:00:00Z",
|
|
72
|
+
"devType": "backend"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- A failed finalize writes nothing. A successful finalize is the handoff point
|
|
79
|
+
for human review: the reviewer sees the complete plan and anchor.
|
|
80
|
+
- A new strict non-Tiny task requires the snapshot at start. Start rejects a
|
|
81
|
+
missing snapshot and rejects any snapshot whose digests no longer match the
|
|
82
|
+
current plan or anchor; editing either file after review requires another
|
|
83
|
+
finalize and another review. Unmarked old tasks remain exempt.
|
|
84
|
+
|
|
31
85
|
## Binding during planning
|
|
32
86
|
|
|
33
87
|
- A planning task not bound at creation time can bind a plan later with
|
|
@@ -66,10 +120,13 @@ into `in_progress`:
|
|
|
66
120
|
- The strict non-Tiny anchor carries the configured-language `## 验证命令` /
|
|
67
121
|
`## Verification Commands` section with the same filled spec-check result;
|
|
68
122
|
see `decision-anchor.md`.
|
|
123
|
+
- The plan passes `plan finalize`; the anchor acceptance IDs/text match the
|
|
124
|
+
plan, and the stored plan-ready digests still match both files.
|
|
69
125
|
|
|
70
126
|
Creation and `--from-plan` binding stay permissive: a planning task may create
|
|
71
|
-
and bind a plan before its structure is final.
|
|
72
|
-
|
|
127
|
+
and bind a plan before its structure is final. The completeness checks run at
|
|
128
|
+
`plan finalize`; the start gate repeats the structural backstop and verifies the
|
|
129
|
+
reviewed snapshot.
|
|
73
130
|
|
|
74
131
|
A Tiny task may have no planFile and is exempt from the plan-shape and anchor
|
|
75
132
|
spec-check gates. A legacy task (no `meta.gateProfile: "strict"`) keeps the
|
|
@@ -97,11 +154,12 @@ The recovery output lists paths only and never expands the plan prose.
|
|
|
97
154
|
|
|
98
155
|
- No parsing of plan step status.
|
|
99
156
|
- No Markdown AST or DSL.
|
|
100
|
-
- No
|
|
157
|
+
- No semantic validation of plan prose beyond the completeness checks above.
|
|
101
158
|
- No matching verification commands against evidence records.
|
|
102
159
|
- No semantic validation of the spec-check result or reason; the structural
|
|
103
160
|
check only requires an actual `pass` / `not_applicable` token.
|
|
104
|
-
- No plan-approval
|
|
161
|
+
- No approval record beyond the plan-ready digest snapshot; human approval is
|
|
162
|
+
still a process step, not a separate workflow state.
|
|
105
163
|
|
|
106
164
|
These capabilities are considered only after warning noise and real payoff have been
|
|
107
165
|
validated.
|
|
@@ -109,7 +109,8 @@
|
|
|
109
109
|
"Plan binding records repo-relative task.json.meta.planFile.",
|
|
110
110
|
"Task start blocks clear missing plan facts for non-Tiny tasks.",
|
|
111
111
|
"Strict non-Tiny start requires the configured-language plan template shape and a filled spec-check line.",
|
|
112
|
-
"
|
|
112
|
+
"New strict non-Tiny tasks require a plan-ready snapshot from `plan finalize`; edits after finalize invalidate it.",
|
|
113
|
+
"V1 does not parse plan step status or verification evidence; AC text is only compared between plan and anchor."
|
|
113
114
|
],
|
|
114
115
|
"readWhen": [
|
|
115
116
|
"when changing task creation plan binding",
|
|
@@ -30,6 +30,28 @@ If the installed template is missing, stop and report it (run `cwf project sync
|
|
|
30
30
|
|
|
31
31
|
The template is distributed at install time for the project language configured in `.cowork-flow/config.yaml` (`zh-CN` by default, or `en-US`). Write the plan in that language. Keep the template's machine-readable goal line and `AC-NNN` identifiers unchanged so task creation and workflow gates can parse them. Do not translate paths, commands, code identifiers, or spec file names.
|
|
32
32
|
|
|
33
|
+
### Skeleton Generation
|
|
34
|
+
|
|
35
|
+
Generate the first version from the installed template instead of retyping its structure:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
./.cowork-flow/run plan new --title "<plan title>" --slug <ascii-slug>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`plan new` replaces the title placeholder, writes `.cowork-flow/plans/YYYY-MM-DD-<slug>.md`, and refuses to overwrite an existing plan unless `--force` is explicit. Fill the generated skeleton; do not invent a second plan shape. If the installed template is missing, the command stops with a `cwf project sync .` hint.
|
|
42
|
+
|
|
43
|
+
### Plan Readiness Gate
|
|
44
|
+
|
|
45
|
+
After filling the plan and anchor, finalize them before human review:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
./.cowork-flow/run plan finalize <task-dir>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`plan finalize` checks the plan and anchor for template shape, concrete section content, AC rows, complete task briefs, verification commands, matching goal/AC text, the spec-check result, and a non-empty `implement.jsonl`. On success it writes `task.json.meta.planReady` with plan/anchor digests.
|
|
52
|
+
|
|
53
|
+
New strict non-Tiny tasks require this snapshot at start. Present the finalized plan and anchor for human review; start only verifies that the reviewed files have not changed. If either file changes after finalize, run `plan finalize` again and re-review before starting. Tiny and legacy tasks keep their existing exemptions.
|
|
54
|
+
|
|
33
55
|
### Plan Style: Task Briefs, Not Design Docs
|
|
34
56
|
|
|
35
57
|
Plans exist to prevent implementation drift. They must guide implementation by pinning task order, file boundaries, key symbols, the core patch shape, test proof, completion conditions, and prohibited drift. They are not architecture essays and must not explain every possible option.
|
|
@@ -82,6 +104,7 @@ A plan can be marked approved only when all conditions hold:
|
|
|
82
104
|
- Every task has implementation steps and verification commands.
|
|
83
105
|
- Task dependencies are explicit, including whether work is serial or parallelizable.
|
|
84
106
|
- The plan uses the installed `.cowork-flow/templates/plan-template.md` shape and the configured output language. For strict non-Tiny tasks, the runtime blocks start when the plan or anchor lacks the configured-language verification section or a filled spec-check result.
|
|
107
|
+
- For new strict non-Tiny tasks, the plan and anchor have passed `plan finalize` and the human has reviewed that snapshot. Any later edit requires another finalize and review.
|
|
85
108
|
|
|
86
109
|
`implement.jsonl` is the current task's context index — the files in scope and
|
|
87
110
|
the spec pointers it needs — not the task list. The plan owns the task list.
|
|
@@ -120,3 +143,4 @@ Before handoff:
|
|
|
120
143
|
5. Confirm the plan says to stop and report back when code facts conflict with the plan, a plan-external file becomes necessary, or a test fails for an unplanned reason.
|
|
121
144
|
6. Confirm Final Verification carries the applicable `.cowork-flow/spec/<dev_type>/` clause check with an actual `pass` / `not_applicable` result, or states why no spec file applies. For strict non-Tiny tasks the runtime blocks start when this line is missing or still the template placeholder.
|
|
122
145
|
7. Confirm the plan is written in the configured language and keeps the machine-readable goal line and `AC-NNN` identifiers unchanged.
|
|
146
|
+
8. Run `plan finalize <task-dir>` and fix every reported blocker before handing the plan to the user for review.
|