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 +14 -1
- package/docs/architecture.en.md +1 -1
- package/docs/architecture.md +1 -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/services/decision_anchor_schema.py +134 -10
- package/template/.cowork-flow/scripts/services/fact_view.py +6 -0
- package/template/.cowork-flow/scripts/services/plan_binding.py +34 -0
- package/template/.cowork-flow/scripts/services/plan_schema.py +182 -0
- package/template/.cowork-flow/scripts/services/task_context.py +71 -5
- package/template/.cowork-flow/scripts/services/task_creation.py +18 -1
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +15 -6
- package/template/.cowork-flow/spec/contracts/plan-binding.md +25 -3
- package/template/.cowork-flow/spec/runtime/contract-registry.json +1 -0
- package/template/.cowork-flow/templates/plan-template.md +1 -1
- package/template/locales/en-US/.cowork-flow/spec/contracts/decision-anchor.md +15 -6
- package/template/locales/en-US/.cowork-flow/templates/plan-template.md +1 -1
- package/template/skills/task-planning/SKILL.md +3 -3
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.
|
|
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
|
package/docs/architecture.en.md
CHANGED
|
@@ -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
|
|
package/docs/architecture.md
CHANGED
|
@@ -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 +1 @@
|
|
|
1
|
-
2.4.
|
|
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
|
-
|
|
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
|
|
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
|
|
351
|
-
|
|
352
|
-
|
|
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;
|
|
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
|
-
|
|
63
|
-
section
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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;
|
|
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.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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.
|
|
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.
|