cowork-flow 2.4.0 → 2.4.1

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 CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  ### Added
6
6
 
7
+ ### Changed
8
+
9
+ ## [2.4.1] - 2026-10-23
10
+
11
+ ### Added
12
+
13
+ - **新任务 anchor 预置规范核对脚手架**:`task create` 生成的 `decision-anchor.md` 在目标/验收标准后追加配置语言的 `## 验证命令` / `## Verification Commands`,并写入带 `<dev_type>` 与 `<pass / not_applicable + 理由>` 的 `规范核对:` / `Spec check:` 占位行和启动前替换说明。创建与 `--from-plan` 绑定仍保持宽松,占位行在 start 时才被门禁拒绝。
14
+
15
+ ### Changed
16
+
17
+ - **strict 非 Tiny 任务的计划模板与规范核对硬门禁**:start 前校验绑定计划使用配置语言的标题标记、六个模板章节顺序、`### 任务` / `### Task` 前缀,以及 `## 最终验证` / `## Final Verification` 中已填写的 `规范核对:` / `Spec check:` 结果;同时要求 anchor 具有配置语言的验证节和已填写结果。模板占位符不算通过;事实视图不再把规范核对行当作 shell 命令。Tiny 与 legacy 任务保持原有行为,创建/绑定阶段不新增拒绝路径。
18
+
7
19
  ## [2.4.0] - 2026-10-23
8
20
 
9
21
  ### Added
@@ -423,7 +435,8 @@
423
435
 
424
436
  此版本未保留本地条目正文,详情见 [GitHub Release](https://github.com/fishlikewater/cowork-flow/releases/tag/v0.0.47)。
425
437
 
426
- [Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v2.4.0...HEAD
438
+ [Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v2.4.1...HEAD
439
+ [2.4.1]: https://github.com/fishlikewater/cowork-flow/compare/v2.4.0...v2.4.1
427
440
  [2.4.0]: https://github.com/fishlikewater/cowork-flow/compare/v2.3.0...v2.4.0
428
441
  [2.3.0]: https://github.com/fishlikewater/cowork-flow/compare/v2.2.0...v2.3.0
429
442
  [2.2.0]: https://github.com/fishlikewater/cowork-flow/compare/v2.1.0...v2.2.0
@@ -73,7 +73,7 @@ Installers and the synchronizer do not read and write incrementally; they execut
73
73
 
74
74
  `config.yaml`, `.developer`, specs, tasks, and plans are protected by the sync policy. `--force` widens the overwrite range, but `.cowork-flow/config.yaml` is in `forceProtectedFiles`: once created, neither `init --force` nor `sync --force` overwrites it. For an existing project, run with `--dry-run` first to see the changes.
75
75
 
76
- The installation language is written to `language` in `config.yaml` (`zh-CN` by default, or `en-US`). The `localization` field of the Host Asset Manifest declares which assets have language variants; their source files live under `template/locales/<language>/<canonical-path>`. `init`, `sync`, and `dev refresh` choose the source file through `localizedSource()`, while the target path is always the canonical path, and `locales/` is excluded by `excludedPrefixes` so it never enters the project. Currently only `spec/contracts/decision-anchor.md` and `.cowork-flow/templates/plan-template.md` are localized: the former sets the heading language of a new anchor, the latter sets the language of plan artifacts, and parsers always accept both Chinese and English headings.
76
+ The installation language is written to `language` in `config.yaml` (`zh-CN` by default, or `en-US`). The `localization` field of the Host Asset Manifest declares which assets have language variants; their source files live under `template/locales/<language>/<canonical-path>`. `init`, `sync`, and `dev refresh` choose the source file through `localizedSource()`, while the target path is always the canonical path, and `locales/` is excluded by `excludedPrefixes` so it never enters the project. Currently only `spec/contracts/decision-anchor.md` and `.cowork-flow/templates/plan-template.md` are localized: the former sets the heading language of a new anchor, the latter sets the language of plan artifacts, and parsers always accept both Chinese and English headings. The start gate for strict non-Tiny tasks additionally requires the plan and anchor to use the configured-language template shape and a filled `Spec check:` / `规范核对:` result; the template placeholder blocks start, while Tiny and legacy tasks are unaffected.
77
77
 
78
78
  `.cowork-flow/spec/` splits its sync policy by ownership. Framework-maintained `spec/contracts/`, `spec/runtime/`, `spec/schemas/`, and `spec/index.md` follow `sync`: contract errata, `contract-registry.json`, `host-assets.json`, `scope-rules.json`, and schemas therefore reach installed projects in the same generation as the scripts, and obsolete framework spec files registered in `obsoleteFiles` are deleted as well. Project-maintained `spec/backend/`, `spec/frontend/`, `spec/guides/`, and `spec/references/` stay protected, and a normal `sync` never overwrites or deletes files there. The cost is that a project which has locally modified a framework contract or `scope-rules.json` loses those changes on the next `sync`; customizations worth keeping should go into the four project directories, `AGENTS.md`, `config.yaml`, or a project-owned Skill.
79
79
 
@@ -73,7 +73,7 @@
73
73
 
74
74
  `config.yaml`、`.developer`、规范、任务和计划按同步策略保护。`--force` 会扩大覆盖范围,但 `.cowork-flow/config.yaml` 是 `forceProtectedFiles`:首次创建后 `init --force` 和 `sync --force` 都不再覆盖它。已有项目应先用 `--dry-run` 查看变更。
75
75
 
76
- 安装语言写在 `config.yaml` 的 `language`(`zh-CN` 默认,或 `en-US`)。Host Asset Manifest 的 `localization` 声明哪些资产存在语言变体,源文件位于 `template/locales/<language>/<canonical-path>`;`init`、`sync` 与 `dev refresh` 通过 `localizedSource()` 选择源文件,目标路径始终是 canonical path,`locales/` 被 `excludedPrefixes` 排除,不会进入项目。当前只有 `spec/contracts/decision-anchor.md` 与 `.cowork-flow/templates/plan-template.md` 本地化;前者决定新 anchor 的标题语言,后者决定计划产物语言,解析器始终同时接受中英两套标题。
76
+ 安装语言写在 `config.yaml` 的 `language`(`zh-CN` 默认,或 `en-US`)。Host Asset Manifest 的 `localization` 声明哪些资产存在语言变体,源文件位于 `template/locales/<language>/<canonical-path>`;`init`、`sync` 与 `dev refresh` 通过 `localizedSource()` 选择源文件,目标路径始终是 canonical path,`locales/` 被 `excludedPrefixes` 排除,不会进入项目。当前只有 `spec/contracts/decision-anchor.md` 与 `.cowork-flow/templates/plan-template.md` 本地化;前者决定新 anchor 的标题语言,后者决定计划产物语言,解析器始终同时接受中英两套标题。strict 非 Tiny 任务的 start 门禁另外要求计划与 anchor 使用配置语言的模板形状,并要求 `规范核对:` 行填写实际 `pass` / `not_applicable` 结果;模板占位符会被阻断,Tiny 与 legacy 任务不受影响。
77
77
 
78
78
  `.cowork-flow/spec/` 按归属拆分同步策略。框架维护的 `spec/contracts/`、`spec/runtime/`、`spec/schemas/` 与 `spec/index.md` 随 `sync` 更新:契约勘误、`contract-registry.json`、`host-assets.json`、`scope-rules.json` 和 schema 因此能与脚本同代际到达已装项目,`obsoleteFiles` 中登记的过期框架 spec 文件也会被删除。项目维护的 `spec/backend/`、`spec/frontend/`、`spec/guides/`、`spec/references/` 保持保护,普通 `sync` 不会覆盖或删除其中的文件。代价是本地修改过框架契约或 `scope-rules.json` 的项目会在下一次 `sync` 丢掉这些改动;需要保留的定制应写入四个项目目录、`AGENTS.md`、`config.yaml` 或项目自有 Skill。
79
79
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 Claude Code。",
5
5
  "displayName": "Cowork Flow",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 Codex。",
5
5
  "author": {
6
6
  "name": "fishlikewater",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 Kimi Code。",
5
5
  "author": {
6
6
  "name": "fishlikewater",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 Qoder。",
5
5
  "displayName": "Cowork Flow",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "AI 协作工作流引擎:任务生命周期、子代理调度、门禁与状态注入。将 cowork-flow 的完整工作流带入 ZCode。",
5
5
  "author": {
6
6
  "name": "fishlikewater",
@@ -1 +1 @@
1
- 2.4.0
1
+ 2.4.1
@@ -23,6 +23,11 @@ REQUIRED_SECTIONS = (GOAL, ACCEPTANCE)
23
23
 
24
24
  DEFAULT_LANGUAGE = "zh-CN"
25
25
 
26
+ SPEC_CHECK_LABELS: dict[str, str] = {
27
+ "zh-CN": "规范核对:",
28
+ "en-US": "Spec check:",
29
+ }
30
+
26
31
  LANGUAGE_HEADINGS: dict[str, dict[str, str]] = {
27
32
  "zh-CN": {
28
33
  GOAL: "目标",
@@ -53,6 +58,10 @@ ACCEPTANCE_RE = re.compile(
53
58
  r"^\s*-\s*(?:\[([ xX])\]?\s*)?(AC-[A-Za-z0-9-]+)\s*[::]\s*(.+?)\s*$"
54
59
  )
55
60
  LIST_ITEM_RE = re.compile(r"^\s*[-*]\s")
61
+ LIST_PREFIX_RE = re.compile(r"^\s*[-*+]\s*")
62
+ FENCE_MARKER_RE = re.compile(r"^(```|~~~)")
63
+ SPEC_CHECK_RESULT_RE = re.compile(r"(?i)\b(?:pass|not_applicable)\b")
64
+ ANGLE_SPAN_RE = re.compile(r"[<<][^>>]*[>>]")
56
65
 
57
66
 
58
67
  def canonical_section(heading: str) -> str | None:
@@ -77,6 +86,130 @@ def section_headings(section: str) -> tuple[str, ...]:
77
86
  )
78
87
 
79
88
 
89
+ def spec_check_label(language: str) -> str:
90
+ """The spec-check label for a language, falling back to the default."""
91
+ return SPEC_CHECK_LABELS.get(language, SPEC_CHECK_LABELS[DEFAULT_LANGUAGE])
92
+
93
+
94
+ def _spec_check_remainder(line: str, language: str | None) -> str | None:
95
+ text = LIST_PREFIX_RE.sub("", line.strip(), count=1)
96
+ labels = (
97
+ (spec_check_label(language),)
98
+ if language is not None
99
+ else tuple(SPEC_CHECK_LABELS.values())
100
+ )
101
+ for label in labels:
102
+ if text.startswith(label):
103
+ return text[len(label):]
104
+ return None
105
+
106
+
107
+ def is_spec_check_line(line: str, language: str | None = None) -> bool:
108
+ """Whether a line starts with the configured or any spec-check label."""
109
+ return _spec_check_remainder(line, language) is not None
110
+
111
+
112
+ def is_spec_check_result_line(line: str, language: str | None = None) -> bool:
113
+ """Whether a spec-check line carries an actual result.
114
+
115
+ Angle-bracket spans are the template's placeholder syntax, so a result
116
+ mentioned only inside `<...>`/`<...>` does not count.
117
+ """
118
+ remainder = _spec_check_remainder(line, language)
119
+ if remainder is None:
120
+ return False
121
+ return SPEC_CHECK_RESULT_RE.search(ANGLE_SPAN_RE.sub("", remainder)) is not None
122
+
123
+
124
+ def _spec_scope(dev_type: str | None) -> str:
125
+ """The spec source a task's dev_type dispatches to.
126
+
127
+ `spec` is the whole-spec dev type and its reading list starts at the spec
128
+ index, not at a `.cowork-flow/spec/spec/` directory.
129
+ """
130
+ if dev_type == "spec":
131
+ return ".cowork-flow/spec/"
132
+ return f".cowork-flow/spec/{dev_type or '<dev_type>'}/"
133
+
134
+
135
+ def spec_check_placeholder(language: str, dev_type: str | None = None) -> str:
136
+ """The localized scaffold line that the start gate treats as unfilled."""
137
+ spec_dir = _spec_scope(dev_type)
138
+ if language == "en-US":
139
+ return (
140
+ f"- Spec check: clause-by-clause check against applicable "
141
+ f"`{spec_dir}` rules -> <pass / not_applicable + reason>."
142
+ )
143
+ return (
144
+ f"- 规范核对:逐条核对 `{spec_dir}` 适用规范 "
145
+ "→ <pass / not_applicable + 理由>。"
146
+ )
147
+
148
+
149
+ def spec_check_example(language: str, dev_type: str | None = None) -> str:
150
+ """A filled spec-check example for start-gate error messages."""
151
+ spec_dir = _spec_scope(dev_type)
152
+ if language == "en-US":
153
+ return (
154
+ f"- Spec check: clause-by-clause check against applicable "
155
+ f"`{spec_dir}` rules -> pass (reason: ...)"
156
+ )
157
+ return (
158
+ f"- 规范核对:逐条核对 `{spec_dir}` 适用规范 → pass(理由:...)"
159
+ )
160
+
161
+
162
+ def structural_lines(text: str) -> list[str]:
163
+ """Lines outside fenced code blocks, for structural Markdown checks."""
164
+ lines: list[str] = []
165
+ fence: str | None = None
166
+ for raw in text.splitlines():
167
+ marker = FENCE_MARKER_RE.match(raw.strip())
168
+ if marker is not None:
169
+ if fence is None:
170
+ fence = marker.group(1)
171
+ elif marker.group(1) == fence:
172
+ fence = None
173
+ continue
174
+ if fence is None:
175
+ lines.append(raw)
176
+ return lines
177
+
178
+
179
+ def section_lines(
180
+ text: str,
181
+ section: str,
182
+ *,
183
+ skip_fenced: bool = False,
184
+ ) -> list[str]:
185
+ """Raw lines under a recognized heading, up to the next `##` heading."""
186
+ lines: list[str] = []
187
+ inside = False
188
+ source = structural_lines(text) if skip_fenced else text.splitlines()
189
+ for raw in source:
190
+ heading = HEADING_RE.match(raw.strip())
191
+ if heading:
192
+ inside = canonical_section(heading.group(1)) == section
193
+ continue
194
+ if inside:
195
+ lines.append(raw)
196
+ return lines
197
+
198
+
199
+ def verification_section_lines(text: str) -> list[str]:
200
+ """Raw verification lines, ignoring fenced command examples."""
201
+ return section_lines(text, VERIFICATION, skip_fenced=True)
202
+
203
+
204
+ def headings(text: str) -> tuple[str, ...]:
205
+ """Every `##` heading text in the document order."""
206
+ return tuple(
207
+ heading.group(1)
208
+ for raw in text.splitlines()
209
+ if (heading := HEADING_RE.match(raw.strip()))
210
+ )
211
+
212
+
80
213
  def present_sections(text: str) -> set[str]:
81
214
  """Canonical keys of the recognized `##` sections present in the text."""
82
215
  present: set[str] = set()
@@ -92,13 +225,4 @@ def present_sections(text: str) -> set[str]:
92
225
 
93
226
  def acceptance_section_lines(text: str) -> list[str]:
94
227
  """Raw lines under the acceptance heading, up to the next heading."""
95
- lines: list[str] = []
96
- inside = False
97
- for raw in text.splitlines():
98
- heading = HEADING_RE.match(raw.strip())
99
- if heading:
100
- inside = canonical_section(heading.group(1)) == ACCEPTANCE
101
- continue
102
- if inside:
103
- lines.append(raw)
104
- return lines
228
+ return section_lines(text, ACCEPTANCE)
@@ -26,6 +26,7 @@ from services.decision_anchor_schema import (
26
26
  SCOPE,
27
27
  VERIFICATION,
28
28
  canonical_section,
29
+ is_spec_check_line,
29
30
  )
30
31
 
31
32
  GOAL_TRUNCATE = 300
@@ -73,6 +74,11 @@ def parse_decision_anchor(text: str) -> dict[str, Any]:
73
74
  command = raw.strip()
74
75
  if command.startswith("- "):
75
76
  command = command[2:].strip()
77
+ if command.startswith("<!--"):
78
+ # Scaffold instructions and author notes are not commands.
79
+ continue
80
+ if is_spec_check_line(command):
81
+ continue
76
82
  if command and len(verify_commands) < VERIFY_COMMAND_MAX:
77
83
  verify_commands.append(command[:VERIFY_COMMAND_TRUNCATE])
78
84
  elif section == SCOPE:
@@ -9,6 +9,7 @@ from pathlib import Path
9
9
 
10
10
  from infra.files import read_text_utf8
11
11
  from infra.paths import DIR_WORKFLOW
12
+ from services.plan_schema import plan_template_blockers as plan_text_blockers
12
13
  from services.task_repository import TaskRepository
13
14
 
14
15
 
@@ -165,3 +166,36 @@ def plan_file_blockers(repo_root: Path, task_data: dict) -> list[str]:
165
166
  return [f"planFile is empty: {error.normalized}"]
166
167
  return [error.detail]
167
168
  return []
169
+
170
+
171
+ def plan_template_blockers(
172
+ repo_root: Path,
173
+ task_data: dict,
174
+ language: str,
175
+ ) -> list[str]:
176
+ """Structure blockers for an already-bound, non-empty plan."""
177
+ meta = task_data.get("meta")
178
+ plan_file = meta.get("planFile") if isinstance(meta, dict) else None
179
+ if not isinstance(plan_file, str) or not plan_file.strip():
180
+ return []
181
+ try:
182
+ bound = bind_plan_file(
183
+ repo_root,
184
+ plan_file,
185
+ allow_absolute=False,
186
+ require_exists=True,
187
+ require_non_empty=True,
188
+ )
189
+ except PlanBindingError:
190
+ # plan_file_blockers already reported the binding fact.
191
+ return []
192
+ text = read_text_utf8(bound.path)
193
+ if not text.strip():
194
+ return []
195
+ dev_type = task_data.get("dev_type")
196
+ if not isinstance(dev_type, str) or not dev_type.strip():
197
+ dev_type = None
198
+ return [
199
+ f"plan {bound.normalized}: {message}"
200
+ for message in plan_text_blockers(text, language, dev_type=dev_type)
201
+ ]
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ """Plan template language and structure checks for strict task start.
3
+
4
+ The checks are intentionally structural: a plan must use the configured
5
+ language's title marker, the six template sections, task-heading prefixes and a
6
+ filled spec-check line. Prose, step status, acceptance text and evidence
7
+ matching stay out of scope.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+
14
+ from services.decision_anchor_schema import (
15
+ DEFAULT_LANGUAGE,
16
+ is_spec_check_result_line,
17
+ spec_check_example,
18
+ spec_check_label,
19
+ structural_lines,
20
+ )
21
+
22
+ PLAN_TITLE_MARKERS: dict[str, str] = {
23
+ "zh-CN": "实施计划",
24
+ "en-US": "Implementation Plan",
25
+ }
26
+
27
+ PLAN_SECTION_HEADINGS: dict[str, tuple[str, ...]] = {
28
+ "zh-CN": (
29
+ "目标",
30
+ "验收标准",
31
+ "全局约束",
32
+ "文件边界",
33
+ "任务",
34
+ "最终验证",
35
+ ),
36
+ "en-US": (
37
+ "Goal",
38
+ "Acceptance Criteria",
39
+ "Global Constraints",
40
+ "File Boundaries",
41
+ "Tasks",
42
+ "Final Verification",
43
+ ),
44
+ }
45
+
46
+ PLAN_TASK_PREFIXES: dict[str, str] = {
47
+ "zh-CN": "任务",
48
+ "en-US": "Task",
49
+ }
50
+
51
+ H1_RE = re.compile(r"^#(?!#)\s+(.+?)\s*$")
52
+ H2_RE = re.compile(r"^##(?!#)\s+(.+?)\s*$")
53
+ H3_RE = re.compile(r"^###\s+(.+?)\s*$")
54
+
55
+
56
+ def _language(language: str) -> str:
57
+ return language if language in PLAN_SECTION_HEADINGS else DEFAULT_LANGUAGE
58
+
59
+
60
+ def _headings(lines: list[str]) -> tuple[list[str], list[str], list[str]]:
61
+ h1: list[str] = []
62
+ h2: list[str] = []
63
+ h3: list[str] = []
64
+ for raw in lines:
65
+ line = raw.strip()
66
+ match = H3_RE.match(line)
67
+ if match:
68
+ h3.append(match.group(1))
69
+ continue
70
+ match = H2_RE.match(line)
71
+ if match:
72
+ h2.append(match.group(1))
73
+ continue
74
+ match = H1_RE.match(line)
75
+ if match:
76
+ h1.append(match.group(1))
77
+ return h1, h2, h3
78
+
79
+
80
+ def _section_lines(lines: list[str], heading: str) -> list[str]:
81
+ body: list[str] = []
82
+ inside = False
83
+ for raw in lines:
84
+ line = raw.strip()
85
+ match = H2_RE.match(line)
86
+ if match:
87
+ inside = match.group(1) == heading
88
+ continue
89
+ if inside:
90
+ body.append(raw)
91
+ return body
92
+
93
+
94
+ def _task_prefix_blocker(heading: str, language: str, prefix: str) -> str | None:
95
+ lowered = heading.lower()
96
+ if not (heading.startswith("任务") or lowered.startswith("task")):
97
+ return None
98
+ if heading.startswith(prefix):
99
+ return None
100
+ return (
101
+ f"plan task heading '### {heading}' must use the {language} prefix "
102
+ f"'### {prefix} N:'"
103
+ if language == "zh-CN"
104
+ else f"plan task heading '### {heading}' must use the {language} prefix "
105
+ f"'### {prefix} N:'"
106
+ )
107
+
108
+
109
+ def plan_template_blockers(
110
+ text: str,
111
+ language: str,
112
+ *,
113
+ dev_type: str | None = None,
114
+ ) -> list[str]:
115
+ """Why a plan does not match the configured-language template shape."""
116
+ if not text.strip():
117
+ return ["plan is empty"]
118
+ resolved = _language(language)
119
+ expected = PLAN_SECTION_HEADINGS[resolved]
120
+ other = "en-US" if resolved == "zh-CN" else "zh-CN"
121
+ other_headings = set(PLAN_SECTION_HEADINGS[other])
122
+ lines = structural_lines(text)
123
+ h1, h2, h3 = _headings(lines)
124
+ blockers: list[str] = []
125
+
126
+ marker = PLAN_TITLE_MARKERS[resolved]
127
+ if not h1:
128
+ blockers.append(
129
+ f"plan is missing a top-level title; expected: # <功能> {marker}"
130
+ )
131
+ elif marker not in h1[0]:
132
+ blockers.append(
133
+ f"plan title must contain '{marker}' for {resolved}; "
134
+ f"expected: # <功能> {marker}"
135
+ )
136
+
137
+ wrong_language = [heading for heading in h2 if heading in other_headings]
138
+ if wrong_language:
139
+ blockers.append(
140
+ f"plan uses {other} plan headings in a {resolved} project: "
141
+ + ", ".join(f"## {heading}" for heading in wrong_language)
142
+ + "; expected: "
143
+ + ", ".join(f"## {heading}" for heading in expected)
144
+ )
145
+ else:
146
+ missing = [heading for heading in expected if heading not in h2]
147
+ if missing:
148
+ blockers.append(
149
+ f"plan is missing required {resolved} template heading(s): "
150
+ + ", ".join(f"## {heading}" for heading in missing)
151
+ )
152
+ else:
153
+ positions = [h2.index(heading) for heading in expected]
154
+ if positions != sorted(positions):
155
+ blockers.append(
156
+ f"plan template headings are out of order; expected: "
157
+ + " -> ".join(f"## {heading}" for heading in expected)
158
+ )
159
+
160
+ prefix = PLAN_TASK_PREFIXES[resolved]
161
+ for heading in h3:
162
+ blocker = _task_prefix_blocker(heading, resolved, prefix)
163
+ if blocker is not None:
164
+ blockers.append(blocker)
165
+ break
166
+
167
+ final_heading = expected[-1]
168
+ if final_heading in h2:
169
+ body = _section_lines(lines, final_heading)
170
+ if not any(
171
+ spec_check_label(resolved) in line
172
+ and is_spec_check_result_line(line, resolved)
173
+ for line in body
174
+ ):
175
+ blockers.append(
176
+ f"plan `## {final_heading}` is missing a filled spec-check "
177
+ f"result; expected a line starting with '{spec_check_label(resolved)}' "
178
+ f"and containing 'pass' or 'not_applicable', e.g. "
179
+ f"{spec_check_example(resolved, dev_type)}"
180
+ )
181
+
182
+ return blockers
@@ -7,6 +7,7 @@ import json
7
7
  from dataclasses import dataclass
8
8
  from pathlib import Path
9
9
 
10
+ from infra.config import get_language
10
11
  from infra.files import read_text_utf8
11
12
  from infra.paths import FILE_TASK_JSON
12
13
  from services.context_discovery import (
@@ -34,10 +35,20 @@ from services.context_paths import (
34
35
  )
35
36
  from services.decision_anchor_schema import (
36
37
  REQUIRED_SECTIONS,
38
+ VERIFICATION,
39
+ heading_for,
40
+ headings,
41
+ is_spec_check_result_line,
37
42
  present_sections,
38
43
  section_headings,
44
+ spec_check_example,
45
+ verification_section_lines,
46
+ )
47
+ from services.plan_binding import (
48
+ normalize_plan_file,
49
+ plan_file_blockers,
50
+ plan_template_blockers,
39
51
  )
40
- from services.plan_binding import normalize_plan_file, plan_file_blockers
41
52
  from services.task_gate_profiles import (
42
53
  GATE_PROFILE_STRICT,
43
54
  acceptance_row_blockers,
@@ -335,6 +346,9 @@ class TaskContextService:
335
346
  task_dir = Path(task_dir)
336
347
  blockers: list[str] = []
337
348
  task_data = self._task_data(task_dir)
349
+ strict = resolve_gate_profile(task_data) == GATE_PROFILE_STRICT
350
+ light = self._is_tiny_task(task_data)
351
+ language = get_language(self.repo_root)
338
352
  if not (task_dir / FILE_TASK_JSON).is_file():
339
353
  blockers.append("task.json is missing")
340
354
  anchor_text = read_text_utf8(task_dir / "decision-anchor.md")
@@ -342,14 +356,27 @@ class TaskContextService:
342
356
  blockers.append("decision-anchor.md is missing or empty")
343
357
  else:
344
358
  blockers.extend(self._decision_anchor_section_blockers(anchor_text))
345
- if resolve_gate_profile(task_data) == GATE_PROFILE_STRICT:
359
+ if strict:
346
360
  blockers.extend(acceptance_row_blockers(anchor_text))
361
+ if not light:
362
+ blockers.extend(
363
+ self._anchor_spec_check_blockers(
364
+ anchor_text,
365
+ language,
366
+ task_data.get("dev_type"),
367
+ )
368
+ )
347
369
  for context_file in ("implement.jsonl",):
348
370
  if not read_text_utf8(task_dir / context_file):
349
371
  blockers.append(f"{context_file} is missing or empty")
350
- if (task_dir / FILE_TASK_JSON).is_file() and not self._is_tiny_task(task_data):
351
- blockers.extend(self._plan_file_blockers(task_data))
352
- if resolve_gate_profile(task_data) == GATE_PROFILE_STRICT:
372
+ if (task_dir / FILE_TASK_JSON).is_file() and not light:
373
+ plan_blockers = self._plan_file_blockers(task_data)
374
+ blockers.extend(plan_blockers)
375
+ if strict and not plan_blockers:
376
+ blockers.extend(
377
+ plan_template_blockers(self.repo_root, task_data, language)
378
+ )
379
+ if strict:
353
380
  blockers.extend(dev_type_blocker(task_data))
354
381
  return tuple(blockers)
355
382
 
@@ -374,6 +401,45 @@ class TaskContextService:
374
401
  if section not in present
375
402
  ]
376
403
 
404
+ @staticmethod
405
+ def _anchor_spec_check_blockers(
406
+ anchor_text: str,
407
+ language: str,
408
+ dev_type: str | None,
409
+ ) -> list[str]:
410
+ expected = heading_for(VERIFICATION, language)
411
+ actual = headings(anchor_text)
412
+ normalized_dev_type = (
413
+ dev_type.strip()
414
+ if isinstance(dev_type, str) and dev_type.strip()
415
+ else None
416
+ )
417
+ example = spec_check_example(language, normalized_dev_type)
418
+ if expected not in actual:
419
+ other = heading_for(
420
+ VERIFICATION,
421
+ "en-US" if language == "zh-CN" else "zh-CN",
422
+ )
423
+ if other in actual:
424
+ return [
425
+ f"decision-anchor.md uses `## {other}`; a strict {language} "
426
+ f"task requires `## {expected}` with a filled spec-check "
427
+ f"result, e.g. {example}"
428
+ ]
429
+ return [
430
+ f"decision-anchor.md is missing `## {expected}` with a filled "
431
+ f"spec-check result, e.g. {example}"
432
+ ]
433
+ if not any(
434
+ is_spec_check_result_line(line, language)
435
+ for line in verification_section_lines(anchor_text)
436
+ ):
437
+ return [
438
+ f"decision-anchor.md `## {expected}` is missing a filled "
439
+ f"spec-check result, e.g. {example}"
440
+ ]
441
+ return []
442
+
377
443
  @staticmethod
378
444
  def _is_tiny_task(task_data: dict) -> bool:
379
445
  """Field-level reading only: the plan-file waiver also covers
@@ -12,7 +12,9 @@ from infra.config import get_language
12
12
  from services.decision_anchor_schema import (
13
13
  ACCEPTANCE,
14
14
  GOAL,
15
+ VERIFICATION,
15
16
  heading_for,
17
+ spec_check_placeholder,
16
18
  )
17
19
  from services.task_tree import TaskTreeError, TaskTreeService
18
20
  from services import task_graph
@@ -165,6 +167,7 @@ class TaskCreationService:
165
167
  generated_anchor, generated_anchor_from_plan = self._generate_anchor(
166
168
  task_dir,
167
169
  bound_plan_path,
170
+ request.dev_type,
168
171
  )
169
172
  if generated_anchor:
170
173
  created_files.append(task_dir / "decision-anchor.md")
@@ -361,6 +364,7 @@ class TaskCreationService:
361
364
  self,
362
365
  task_dir: Path,
363
366
  plan_path: Path | None,
367
+ dev_type: str | None = None,
364
368
  ) -> tuple[bool, bool]:
365
369
  """Create a language-specific anchor scaffold.
366
370
 
@@ -387,12 +391,25 @@ class TaskCreationService:
387
391
  language = get_language(self.repo_root)
388
392
  goal_heading = heading_for(GOAL, language)
389
393
  acceptance_heading = heading_for(ACCEPTANCE, language)
394
+ verification_heading = heading_for(VERIFICATION, language)
390
395
  goal_body = goal or _anchor_placeholder(GOAL, language)
391
396
  acceptance_body = _anchor_placeholder(ACCEPTANCE, language)
397
+ verification_comment = (
398
+ "<!-- strict 任务启动前:把 <dev_type> 和 "
399
+ "<pass / not_applicable + 理由> 替换为实际核对结果;"
400
+ "保留模板占位符会被启动门禁阻断。 -->"
401
+ if language == "zh-CN"
402
+ else "<!-- Before a strict task starts: replace <dev_type> and "
403
+ "<pass / not_applicable + reason> with the actual result; "
404
+ "the start gate rejects the template placeholder. -->"
405
+ )
406
+ verification_body = spec_check_placeholder(language, dev_type)
392
407
  try:
393
408
  anchor_file.write_text(
394
409
  f"## {goal_heading}\n\n{goal_body}\n\n"
395
- f"## {acceptance_heading}\n\n{acceptance_body}\n",
410
+ f"## {acceptance_heading}\n\n{acceptance_body}\n\n"
411
+ f"## {verification_heading}\n\n{verification_comment}\n"
412
+ f"{verification_body}\n",
396
413
  encoding="utf-8",
397
414
  )
398
415
  except OSError as error:
@@ -49,7 +49,7 @@ and the right column the `en-US` output.
49
49
  | `## 被拒方案` / `## Rejected Options` | optional | required for brainstorming tasks, so a rejected option is not reconsidered during execution |
50
50
  | `## 关键假设` / `## Key Assumptions` | optional | filled in for complex tasks to surface implicit constraints; not read by the runtime parser |
51
51
  | `## 范围边界` / `## Scope Boundary` | optional | prevents scope creep |
52
- | `## 验证命令` / `## Verification Commands` | optional | filled in for complex tasks, as the basis for automated verification; when the changed files are covered by `.cowork-flow/spec/backend/` or `frontend/` rules, it must also carry the spec/gate check for the task's `dev_type` (deciding command, or clause-by-clause `pass` / `not_applicable` with reason) |
52
+ | `## 验证命令` / `## Verification Commands` | required for strict non-Tiny; optional for legacy/Tiny | filled in for complex tasks, as the basis for automated verification; a strict non-Tiny task also needs the configured-language section and a filled spec/gate check for the task's `dev_type` (deciding command, or clause-by-clause `pass` / `not_applicable` with reason) |
53
53
 
54
54
  ### Machine-readable line formats (for injection)
55
55
 
@@ -59,11 +59,13 @@ fact view (`services/fact_view.parse_decision_anchor`). Line-format conventions:
59
59
  - `## 验证命令`: one command per line (an optional `- ` prefix), truncated to 120
60
60
  characters, at most 10 lines; the implementing agent runs them itself
61
61
  (self-declared, self-checked) and the reviewer verifies the execution evidence.
62
- When the changed files are covered by `.cowork-flow/spec/<dev_type>/` rules, the
63
- section must also carry the spec/gate check for the task's `dev_type`: a deciding
64
- command, or a `规范核对:` line reviewed clause by clause (`pass` /
65
- `not_applicable` + reason). This is a strong review reminder, not a `spec-check`
66
- hard gate.
62
+ A strict non-Tiny task must also carry a `规范核对:` line in the configured
63
+ language's section, with the actual `pass` / `not_applicable` result and a
64
+ reason. The template placeholder (`<pass / not_applicable + 理由>`) does not
65
+ satisfy the start gate. This line is a hard start gate for strict non-Tiny
66
+ tasks, while its content remains self-declared and is reviewed by the check
67
+ agent. The fact view recognizes the line and does not return it as a shell
68
+ command.
67
69
  - `## 范围边界`: only the first non-empty line is taken, truncated to 160
68
70
  characters.
69
71
 
@@ -98,6 +100,13 @@ After merging (5 files): `decision-anchor.md` + `implement.jsonl` + `check.jsonl
98
100
  `## Acceptance Criteria`, no duplicate AC ids, no non-AC list rows in that
99
101
  section, and a declared `dev_type` (otherwise `task specs` dev-type dispatch
100
102
  silently comes back shorter).
103
+ - A strict non-Tiny task also requires the configured-language
104
+ `## 验证命令` / `## Verification Commands` section with a filled
105
+ `规范核对:` / `Spec check:` line. A missing section, the other-language
106
+ heading, or the template placeholder blocks start. Tiny tasks and legacy tasks
107
+ are exempt. The bound plan's `## 最终验证` / `## Final Verification` section
108
+ carries the same filled check; plan structure and language are covered by
109
+ `plan-binding.md`.
101
110
  - An acceptance row's checkbox state (`- [ ]` / `- [x]`) and the evidence records
102
111
  (the task directory's `evidence.jsonl`) are the fact source of the completion
103
112
  gate: completion is blocked when an unchecked AC exists (`LIFECYCLE-AC-001`), a
@@ -51,9 +51,29 @@ into `in_progress`:
51
51
  - `decision-anchor.md` contains `## Goal`/`## 目标` and
52
52
  `## Acceptance Criteria`/`## 验收标准`.
53
53
  - `implement.jsonl` passes the existing context validation.
54
-
55
- A Tiny task may have no planFile, but must still satisfy the existing anchor and
56
- implement-scope requirements.
54
+ - For a strict non-Tiny task, the plan title contains the configured-language
55
+ marker (`实施计划` / `Implementation Plan`).
56
+ - The plan contains the six configured-language `##` template headings in
57
+ template order: `目标` / `验收标准` / `全局约束` / `文件边界` / `任务` /
58
+ `最终验证`, or their `en-US` equivalents. Extra custom sections are allowed;
59
+ fenced code is ignored; a known template heading from the other language is
60
+ rejected.
61
+ - Any `### 任务` / `### Task` task heading uses the configured-language prefix.
62
+ - `## 最终验证` / `## Final Verification` contains a line starting with
63
+ `规范核对:` / `Spec check:` and carrying an actual `pass` or
64
+ `not_applicable` result. The template placeholder (`<pass / not_applicable +
65
+ 理由>` / `<pass / not_applicable + reason>`) does not satisfy the gate.
66
+ - The strict non-Tiny anchor carries the configured-language `## 验证命令` /
67
+ `## Verification Commands` section with the same filled spec-check result;
68
+ see `decision-anchor.md`.
69
+
70
+ Creation and `--from-plan` binding stay permissive: a planning task may create
71
+ and bind a plan before its structure is final. These checks run only at the
72
+ planning → `in_progress` start gate.
73
+
74
+ A Tiny task may have no planFile and is exempt from the plan-shape and anchor
75
+ spec-check gates. A legacy task (no `meta.gateProfile: "strict"`) keeps the
76
+ historical checks; this contract does not retro-fit either profile.
57
77
 
58
78
  ## Implementation recovery
59
79
 
@@ -79,6 +99,8 @@ The recovery output lists paths only and never expands the plan prose.
79
99
  - No Markdown AST or DSL.
80
100
  - No comparing AC text against plan task prose.
81
101
  - No matching verification commands against evidence records.
102
+ - No semantic validation of the spec-check result or reason; the structural
103
+ check only requires an actual `pass` / `not_applicable` token.
82
104
  - No plan-approval data model.
83
105
 
84
106
  These capabilities are considered only after warning noise and real payoff have been
@@ -108,6 +108,7 @@
108
108
  "digest": [
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
+ "Strict non-Tiny start requires the configured-language plan template shape and a filled spec-check line.",
111
112
  "V1 does not parse plan steps, AC text, or verification evidence."
112
113
  ],
113
114
  "readWhen": [
@@ -71,7 +71,7 @@ Tiny 任务可保留简化 brief,但仍须写明精确文件边界、完成条
71
71
  <command>
72
72
  ```
73
73
 
74
- 当改动文件受 `.cowork-flow/spec/<dev_type>/` 规则覆盖时,在这里加入决定命令或规范门禁核对行(`pass` / `not_applicable` 并附理由);这是强复核提醒,不是硬门禁。
74
+ 当改动文件受 `.cowork-flow/spec/<dev_type>/` 规则覆盖时,在这里加入决定命令或规范门禁核对行(`pass` / `not_applicable` 并附理由)。strict 非 Tiny 任务启动前必须把 `<pass / not_applicable + 理由>` 替换为实际核对结果;保留模板占位符会被 start 门禁阻断。
75
75
 
76
76
  **完成条件**
77
77
  - <2-4 条可检查条件>
@@ -49,7 +49,7 @@ and the left column the `zh-CN` output.
49
49
  | `## Rejected Options` / `## 被拒方案` | optional | required for brainstorming tasks, so a rejected option is not reconsidered during execution |
50
50
  | `## Key Assumptions` / `## 关键假设` | optional | filled in for complex tasks to surface implicit constraints; not read by the runtime parser |
51
51
  | `## Scope Boundary` / `## 范围边界` | optional | prevents scope creep |
52
- | `## Verification Commands` / `## 验证命令` | optional | filled in for complex tasks, as the basis for automated verification; when the changed files are covered by `.cowork-flow/spec/backend/` or `frontend/` rules, it must also carry the spec/gate check for the task's `dev_type` (deciding command, or clause-by-clause `pass` / `not_applicable` with reason) |
52
+ | `## Verification Commands` / `## 验证命令` | required for strict non-Tiny; optional for legacy/Tiny | filled in for complex tasks, as the basis for automated verification; a strict non-Tiny task also needs the configured-language section and a filled spec/gate check for the task's `dev_type` (deciding command, or clause-by-clause `pass` / `not_applicable` with reason) |
53
53
 
54
54
  ### Machine-readable line formats (for injection)
55
55
 
@@ -60,11 +60,13 @@ Line-format conventions:
60
60
  - `## Verification Commands`: one command per line (an optional `- ` prefix),
61
61
  truncated to 120 characters, at most 10 lines; the implementing agent runs them
62
62
  itself (self-declared, self-checked) and the reviewer verifies the execution
63
- evidence. When the changed files are covered by `.cowork-flow/spec/<dev_type>/`
64
- rules, the section must also carry the spec/gate check for the task's
65
- `dev_type`: a deciding command, or a `Spec check:` line reviewed clause by
66
- clause (`pass` / `not_applicable` + reason). This is a strong review reminder,
67
- not a `spec-check` hard gate.
63
+ evidence. A strict non-Tiny task must also carry a `Spec check:` line in the
64
+ configured language's section, with the actual `pass` / `not_applicable` result
65
+ and a reason. The template placeholder (`<pass / not_applicable + reason>`) does
66
+ not satisfy the start gate. This line is a hard start gate for strict non-Tiny
67
+ tasks, while its content remains self-declared and is reviewed by the check
68
+ agent. The fact view recognizes the line and does not return it as a shell
69
+ command.
68
70
  - `## Scope Boundary`: only the first non-empty line is taken, truncated to 160
69
71
  characters.
70
72
 
@@ -99,6 +101,13 @@ After merging (5 files): `decision-anchor.md` + `implement.jsonl` + `check.jsonl
99
101
  `## 验收标准`, no duplicate AC ids, no non-AC list rows in that section, and a
100
102
  declared `dev_type` (otherwise `task specs` dev-type dispatch silently comes
101
103
  back shorter).
104
+ - A strict non-Tiny task also requires the configured-language
105
+ `## Verification Commands` / `## 验证命令` section with a filled
106
+ `Spec check:` / `规范核对:` line. A missing section, the other-language
107
+ heading, or the template placeholder blocks start. Tiny tasks and legacy tasks
108
+ are exempt. The bound plan's `## Final Verification` / `## 最终验证` section
109
+ carries the same filled check; plan structure and language are covered by
110
+ `plan-binding.md`.
102
111
  - An acceptance row's checkbox state (`- [ ]` / `- [x]`) and the evidence records
103
112
  (the task directory's `evidence.jsonl`) are the fact source of the completion
104
113
  gate: completion is blocked when an unchecked AC exists (`LIFECYCLE-AC-001`), a
@@ -71,7 +71,7 @@ Tiny plans may keep a shorter brief, but must still name exact file boundaries,
71
71
  <command>
72
72
  ```
73
73
 
74
- When changed files are covered by `.cowork-flow/spec/<dev_type>/` rules, add the deciding command or a spec/gate check line (`pass` / `not_applicable` with reason) here; it is a strong review reminder, not a hard gate.
74
+ When changed files are covered by `.cowork-flow/spec/<dev_type>/` rules, add the deciding command or a spec/gate check line (`pass` / `not_applicable` with reason) here. Before a strict non-Tiny task starts, replace `<pass / not_applicable + reason>` with the actual result; the start gate rejects the template placeholder.
75
75
 
76
76
  **Completion Conditions**
77
77
  - <2-4 checkable conditions>
@@ -49,7 +49,7 @@ Fill the template in order: header table and goal first, then acceptance criteri
49
49
  - Every Normal or High-risk task fills every required field in the template's task brief: Purpose, Code Fact Sources, File Boundaries, Key Symbols, Implementation Notes, Test Proof, Verification Command, Completion Conditions, Prohibited Drift, and Deviation Conditions.
50
50
  - For Tiny tasks, keep the brief short but still name exact file boundaries, completion conditions, and verification commands.
51
51
  - For behavior changes, include a failing test before implementation when behavior can be tested.
52
- - For implementation tasks whose changed files are covered by `.cowork-flow/spec/` requirements, name the covered spec files in the plan and add the clause check to the task's `Verification Command` and the plan's `Final Verification`: run the deciding command when one exists, otherwise check clause by clause and record `pass` / `not_applicable` with reason. This is a strong review reminder, not a separate hard gate.
52
+ - For implementation tasks whose changed files are covered by `.cowork-flow/spec/` requirements, name the covered spec files in the plan and add the clause check to the task's `Verification Command` and the plan's `Final Verification`: run the deciding command when one exists, otherwise check clause by clause and record `pass` / `not_applicable` with reason. For strict non-Tiny tasks this is a hard start gate: the bound plan and the task anchor must use the configured-language verification heading and carry a filled spec-check result. The template placeholder `<pass / not_applicable + reason>` blocks start; replace it before starting. The content remains self-declared and is reviewed by the check agent.
53
53
  - Do not add shallow tests just to satisfy process. Avoid tests that only assert existence, mirror implementation details, count mock calls without behavior, or snapshot empty structure.
54
54
  - For complex problems, test depth first: business invariants, cross-layer contracts, state transitions, error boundaries, and real regression paths before narrow unit cases.
55
55
  - Map behavior-changing tests to stable `decision-anchor.md` acceptance IDs when useful; do not create `tdd.jsonl` or write TDD evidence records into `check.jsonl`.
@@ -81,7 +81,7 @@ A plan can be marked approved only when all conditions hold:
81
81
  - Every task in the plan has clear acceptance criteria.
82
82
  - Every task has implementation steps and verification commands.
83
83
  - Task dependencies are explicit, including whether work is serial or parallelizable.
84
- - The plan uses the installed `.cowork-flow/templates/plan-template.md` shape and the configured output language.
84
+ - 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.
85
85
 
86
86
  `implement.jsonl` is the current task's context index — the files in scope and
87
87
  the spec pointers it needs — not the task list. The plan owns the task list.
@@ -118,5 +118,5 @@ Before handoff:
118
118
  3. Confirm every Normal or High-risk task fills every required brief field in the template.
119
119
  4. Check names, paths, command syntax, and expected outputs.
120
120
  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
- 6. Confirm Final Verification carries the applicable `.cowork-flow/spec/<dev_type>/` clause check, or states why no spec file applies.
121
+ 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
122
  7. Confirm the plan is written in the configured language and keeps the machine-readable goal line and `AC-NNN` identifiers unchanged.