cowork-flow 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/docs/direction.md +179 -0
  3. package/docs/index.md +1 -0
  4. package/package.json +1 -1
  5. package/presets/claude-code/.claude-plugin/plugin.json +1 -1
  6. package/presets/codex/.codex-plugin/plugin.json +1 -1
  7. package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
  8. package/presets/opencode/cowork-flow/plugin-core.js +51 -3
  9. package/presets/qoder/.qoder-plugin/plugin.json +1 -1
  10. package/presets/zcode/.zcode-plugin/plugin.json +1 -1
  11. package/presets/zcode/hooks/hooks.json +3 -3
  12. package/presets/zcode/hooks/{inject-context.js → inject-context.mjs} +11 -6
  13. package/template/.cowork-flow/.version +1 -1
  14. package/template/.cowork-flow/scripts/adapters/cli/spec_check.py +10 -1
  15. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +3 -0
  16. package/template/.cowork-flow/scripts/adapters/cli/task_parser.py +1 -0
  17. package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +21 -0
  18. package/template/.cowork-flow/scripts/adapters/host/zcode_policy.py +1 -1
  19. package/template/.cowork-flow/scripts/services/ac_evidence.py +107 -0
  20. package/template/.cowork-flow/scripts/services/fact_view.py +16 -2
  21. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +84 -0
  22. package/template/.cowork-flow/scripts/services/lifecycle_policy.py +5 -0
  23. package/template/.cowork-flow/scripts/services/spec_check.py +11 -1
  24. package/template/.cowork-flow/scripts/services/task_lifecycle.py +12 -0
  25. package/template/.cowork-flow/spec/contracts/context-injection.md +6 -4
  26. package/template/.cowork-flow/spec/contracts/decision-anchor.md +1 -0
  27. package/template/.cowork-flow/spec/contracts/evidence-completion.md +48 -0
  28. package/template/.cowork-flow/spec/contracts/index.md +1 -0
  29. package/template/.cowork-flow/spec/contracts/spec-checks.md +49 -82
  30. package/template/.cowork-flow/spec/runtime/contract-registry.json +13 -0
  31. package/template/.opencode/cowork-flow/plugin-core.js +51 -3
  32. package/template/skills/spec-sync/SKILL.md +1 -0
  33. package/template/skills/task-review/SKILL.md +3 -2
  34. package/template/skills/test-first/SKILL.md +4 -0
@@ -23,7 +23,7 @@ VERIFY_COMMAND_MAX = 5
23
23
  SCOPE_BOUNDARY_TRUNCATE = 160
24
24
  ANCHOR_SECTIONS = ("目标", "验收标准", "被拒方案", "验证命令", "范围边界")
25
25
  ACCEPTANCE_RE = re.compile(
26
- r"^\s*-\s*\[[ xX]?\]\s*(AC-[A-Za-z0-9-]+)\s*[::]\s*(.+?)\s*$"
26
+ r"^\s*-\s*(?:\[([ xX])\]?\s*)?(AC-[A-Za-z0-9-]+)\s*[::]\s*(.+?)\s*$"
27
27
  )
28
28
  REJECTED_RE = re.compile(r"^\s*-\s*\*\*(.+?)\*\*")
29
29
  _HEADING_RE = re.compile(r"^##\s*(.+?)\s*$")
@@ -53,7 +53,11 @@ def parse_decision_anchor(text: str) -> dict[str, Any]:
53
53
  match = ACCEPTANCE_RE.match(raw)
54
54
  if match:
55
55
  acceptance.append(
56
- {"id": match.group(1), "text": match.group(2)}
56
+ {
57
+ "id": match.group(2),
58
+ "text": match.group(3),
59
+ "checked": match.group(1) in ("x", "X"),
60
+ }
57
61
  )
58
62
  elif section == "被拒方案":
59
63
  match = REJECTED_RE.match(raw)
@@ -488,6 +492,15 @@ def build_fact_view(repo_root: Path, task_dir: Path) -> dict[str, Any]:
488
492
  else:
489
493
  anchor = {"exists": False}
490
494
 
495
+ evidence_coverage: dict[str, Any] | None = None
496
+ if anchor.get("acceptanceCriteria"):
497
+ try:
498
+ from services.ac_evidence import coverage_summary
499
+
500
+ evidence_coverage = coverage_summary(task_dir)
501
+ except Exception:
502
+ evidence_coverage = None
503
+
491
504
  meta = task.get("meta") if isinstance(task, dict) else None
492
505
  plan_file = meta.get("planFile") if isinstance(meta, dict) else None
493
506
  plan: dict[str, Any] = {"bound": bool(plan_file)}
@@ -501,6 +514,7 @@ def build_fact_view(repo_root: Path, task_dir: Path) -> dict[str, Any]:
501
514
  "taskPath": rel_task,
502
515
  "task": task,
503
516
  "decisionAnchor": anchor,
517
+ "evidenceCoverage": evidence_coverage,
504
518
  "plan": plan,
505
519
  "whitelist": file_scope_whitelist(repo_root, task_dir),
506
520
  "sessions": _bound_sessions(repo_root, rel_task),
@@ -45,6 +45,7 @@ class LifecycleCheckResult:
45
45
  blockers: tuple[str, ...] = ()
46
46
  issues: tuple[LifecycleCheckIssue, ...] = ()
47
47
  spec_check: dict | None = None
48
+ evidence_exemption: dict | None = None
48
49
 
49
50
  def __post_init__(self) -> None:
50
51
  if self.issues and not self.blockers:
@@ -96,6 +97,7 @@ class LifecycleCheckRunner:
96
97
  *,
97
98
  allow_spec_file_modifications: bool | None = None,
98
99
  allow_unchecked_specs: bool | None = None,
100
+ allow_missing_evidence: bool | None = None,
99
101
  execution_policy: LifecycleExecutionPolicy | None = None,
100
102
  ) -> LifecycleCheckResult:
101
103
  spec_report = _spec_check_report(self.repo_root)
@@ -107,6 +109,14 @@ class LifecycleCheckRunner:
107
109
  else _policy_allows_unchecked(execution_policy)
108
110
  ),
109
111
  )
112
+ ac_issues, evidence_exemption = _ac_evidence_completion_facts(
113
+ task_dir,
114
+ allow_missing_evidence=(
115
+ bool(allow_missing_evidence)
116
+ if allow_missing_evidence is not None
117
+ else _policy_allows_missing_evidence(execution_policy)
118
+ ),
119
+ )
110
120
  return LifecycleCheckResult(
111
121
  stage="complete",
112
122
  issues=tuple(
@@ -119,8 +129,10 @@ class LifecycleCheckRunner:
119
129
  ),
120
130
  ))
121
131
  + list(spec_issues)
132
+ + list(ac_issues)
122
133
  ),
123
134
  spec_check=spec_report,
135
+ evidence_exemption=evidence_exemption,
124
136
  )
125
137
 
126
138
 
@@ -139,6 +151,78 @@ def _policy_allows_unchecked(
139
151
  return bool(execution_policy is not None and execution_policy.allow_unchecked_specs)
140
152
 
141
153
 
154
+ def _policy_allows_missing_evidence(
155
+ execution_policy: LifecycleExecutionPolicy | None,
156
+ ) -> bool:
157
+ return bool(
158
+ execution_policy is not None
159
+ and execution_policy.allow_missing_evidence
160
+ )
161
+
162
+
163
+ def _ac_evidence_completion_facts(
164
+ task_dir: Path,
165
+ *,
166
+ allow_missing_evidence: bool,
167
+ ) -> tuple[list[LifecycleCheckIssue], dict | None]:
168
+ """Completion gate over acceptance-criteria evidence.
169
+
170
+ The gate only decides existence facts: which AC ids the decision anchor
171
+ declares, which are checked, and which carry at least one evidence
172
+ record. Tasks without AC rows are exempt (compatibility path); an
173
+ unreadable evidence module degrades the same way instead of blocking
174
+ every legacy task. Returns the issues plus the recorded exemption
175
+ (missing ids) when --allow-missing-evidence absorbed the gap."""
176
+ try:
177
+ from services.ac_evidence import (
178
+ evidence_coverage,
179
+ load_acceptance_criteria,
180
+ read_evidence,
181
+ )
182
+ except Exception: # pragma: no cover - broken runtime copy degrades
183
+ return [], None
184
+ acceptance = load_acceptance_criteria(task_dir)
185
+ if not acceptance:
186
+ return [], None
187
+ coverage = evidence_coverage(acceptance, read_evidence(task_dir))
188
+ unchecked = [item["id"] for item in acceptance if not item["checked"]]
189
+ unproven = [
190
+ ac_id for ac_id in coverage["missing"] if ac_id not in unchecked
191
+ ]
192
+ issues: list[LifecycleCheckIssue] = []
193
+ if unchecked:
194
+ issues.append(
195
+ LifecycleCheckIssue(
196
+ code="LIFECYCLE-AC-001",
197
+ message=(
198
+ f"decision-anchor has {len(unchecked)} unchecked "
199
+ "acceptance criterion(ies): "
200
+ f"{', '.join(unchecked[:5])}; finish them and tick the "
201
+ "checkbox, or record evidence in evidence.jsonl"
202
+ ),
203
+ )
204
+ )
205
+ exemption: dict | None = None
206
+ if unproven:
207
+ if allow_missing_evidence:
208
+ exemption = {"missing": unproven}
209
+ else:
210
+ issues.append(
211
+ LifecycleCheckIssue(
212
+ code="LIFECYCLE-AC-002",
213
+ message=(
214
+ f"{len(unproven)} acceptance criterion(ies) lack "
215
+ "evidence records: "
216
+ f"{', '.join(unproven[:5])}; append "
217
+ "{ac, kind: test|command|manual, ref, note?, "
218
+ "recordedAt, by} records to evidence.jsonl, or pass "
219
+ "--allow-missing-evidence to record the exemption"
220
+ ),
221
+ )
222
+ )
223
+ return issues, exemption
224
+
225
+
142
226
  def _spec_check_report(repo_root: Path) -> dict | None:
143
227
  """Run the lifecycle-phase spec checks. A structural failure degrades to
144
228
  an unchecked report — never to a silently passing gate."""
@@ -35,6 +35,7 @@ class LifecycleExecutionPolicy:
35
35
  execution_scope: str
36
36
  allow_spec_file_modifications: bool
37
37
  allow_unchecked_specs: bool = False
38
+ allow_missing_evidence: bool = False
38
39
 
39
40
 
40
41
  def start_readiness_failure(
@@ -93,17 +94,20 @@ def resolve_execution_policy(
93
94
  *,
94
95
  allow_spec_file_modifications: bool | None = None,
95
96
  allow_unchecked_specs: bool | None = None,
97
+ allow_missing_evidence: bool | None = None,
96
98
  ) -> LifecycleExecutionPolicy:
97
99
  """Resolve lifecycle scope capability from execution facts."""
98
100
  if execution_context is None:
99
101
  if (
100
102
  allow_spec_file_modifications is not None
101
103
  or allow_unchecked_specs is not None
104
+ or allow_missing_evidence is not None
102
105
  ):
103
106
  return LifecycleExecutionPolicy(
104
107
  execution_scope="explicit",
105
108
  allow_spec_file_modifications=bool(allow_spec_file_modifications),
106
109
  allow_unchecked_specs=bool(allow_unchecked_specs),
110
+ allow_missing_evidence=bool(allow_missing_evidence),
107
111
  )
108
112
  execution_context = ExecutionContext()
109
113
 
@@ -117,6 +121,7 @@ def resolve_execution_policy(
117
121
  execution_scope=execution_context.mode,
118
122
  allow_spec_file_modifications=allowed,
119
123
  allow_unchecked_specs=bool(allow_unchecked_specs),
124
+ allow_missing_evidence=bool(allow_missing_evidence),
120
125
  )
121
126
 
122
127
 
@@ -357,9 +357,16 @@ def run_checks(
357
357
  if file_filter:
358
358
  changed = [file_filter.replace("\\", "/")]
359
359
 
360
+ collected = collect_spec_declarations(repo_root)
361
+ # Repository-wide declaration count, phase-independent: "no checks
362
+ # declared" is a property of the repo, not of this phase's result list
363
+ # (a lifecycle run skips every edit-only declaration and would otherwise
364
+ # read as "nothing declared").
365
+ declarations_total = sum(len(spec_decls.decls) for spec_decls in collected)
366
+
360
367
  results: list[CheckResult] = []
361
368
  parse_errors: list[dict[str, str]] = []
362
- for spec_decls in collect_spec_declarations(repo_root):
369
+ for spec_decls in collected:
363
370
  for message in spec_decls.errors:
364
371
  parse_errors.append({"spec": spec_decls.spec, "error": message})
365
372
  for decl in spec_decls.decls:
@@ -400,11 +407,14 @@ def run_checks(
400
407
  "pass": sum(1 for r in results if r.status == "pass"),
401
408
  "violation": sum(1 for r in results if r.status == "violation"),
402
409
  "unchecked": sum(1 for r in results if r.status == "unchecked"),
410
+ "declarations": declarations_total,
403
411
  }
404
412
  return {
405
413
  "schemaVersion": SCHEMA_VERSION,
406
414
  "phase": phase,
407
415
  "changedFiles": changed,
416
+ "declarations": declarations_total,
417
+ "specFiles": len(collected),
408
418
  "results": [r.to_json() for r in results],
409
419
  "parseErrors": parse_errors,
410
420
  "summary": summary,
@@ -160,6 +160,7 @@ class TaskLifecycleService:
160
160
  completed_at: str | None = None,
161
161
  allow_spec_file_modifications: bool | None = None,
162
162
  allow_unchecked_specs: bool = False,
163
+ allow_missing_evidence: bool = False,
163
164
  execution_context: object | None = None,
164
165
  ) -> LifecycleResult:
165
166
  return self.execute(
@@ -168,6 +169,7 @@ class TaskLifecycleService:
168
169
  completed_at=completed_at,
169
170
  allow_spec_file_modifications=allow_spec_file_modifications,
170
171
  allow_unchecked_specs=allow_unchecked_specs,
172
+ allow_missing_evidence=allow_missing_evidence,
171
173
  execution_context=execution_context,
172
174
  )
173
175
 
@@ -179,6 +181,7 @@ class TaskLifecycleService:
179
181
  preflight: Preflight | None = None,
180
182
  allow_spec_file_modifications: bool | None = None,
181
183
  allow_unchecked_specs: bool = False,
184
+ allow_missing_evidence: bool = False,
182
185
  completed_at: str | None = None,
183
186
  execution_context: object | None = None,
184
187
  executor: str | None = None,
@@ -191,6 +194,7 @@ class TaskLifecycleService:
191
194
  execution_context,
192
195
  allow_spec_file_modifications=allow_spec_file_modifications,
193
196
  allow_unchecked_specs=allow_unchecked_specs,
197
+ allow_missing_evidence=allow_missing_evidence,
194
198
  )
195
199
  prepared = self._prepare_transition(
196
200
  stage, task_dir, preflight, executor=executor, takeover=takeover
@@ -590,6 +594,7 @@ class TaskLifecycleService:
590
594
  execution_policy.allow_spec_file_modifications
591
595
  ),
592
596
  allow_unchecked_specs=execution_policy.allow_unchecked_specs,
597
+ allow_missing_evidence=execution_policy.allow_missing_evidence,
593
598
  execution_policy=execution_policy,
594
599
  )
595
600
  else:
@@ -678,6 +683,13 @@ class TaskLifecycleService:
678
683
  meta["specCheckSummary"] = dict(
679
684
  spec_report.get("summary") or {}
680
685
  )
686
+ if getattr(check_result, "evidence_exemption", None):
687
+ meta["evidenceExempt"] = {
688
+ "at": datetime.now(timezone.utc).strftime(
689
+ "%Y-%m-%dT%H:%M:%SZ"
690
+ ),
691
+ **check_result.evidence_exemption,
692
+ }
681
693
  if (
682
694
  execution_policy is not None
683
695
  and execution_policy.allow_unchecked_specs
@@ -17,7 +17,7 @@ would make the digest self-referential).
17
17
 
18
18
  | Host | Transport | Shape |
19
19
  |---|---|---|
20
- | zcode | process hook shim (`inject-context.js`) → `inject.py --host zcode`, stdout JSON | `hookSpecificOutput.{hookEventName, additionalContext}` |
20
+ | zcode | process hook shim (`inject-context.mjs`) → `inject.py --host zcode`, stdout JSON | `hookSpecificOutput.{hookEventName, additionalContext}` |
21
21
  | codex | thin wrapper (`inject-workflow-state.py`) → `inject.py --host codex`, stdout JSON | same shape |
22
22
  | claude-code | thin wrapper (`inject-workflow-state.py`) → `inject.py --host claude-code`, stdout JSON | same shape |
23
23
  | opencode | plugin `experimental.chat.system.transform` + `shell.env` | system-prompt section push / env object |
@@ -233,6 +233,7 @@ means without re-reading the file:
233
233
  <decision-anchor task=".cowork-flow/tasks/08-28-demo">
234
234
  Goal: <first goal line, max 160 chars>
235
235
  Acceptance: AC-001 <text, max 80>; AC-002 <...> (first 8 items)
236
+ AC evidence: <with evidence>/<total>[ missing=<id,...>, max 5]
236
237
  Rejected: <rejected option name>; ... (first 6 names)
237
238
  </decision-anchor>
238
239
  ```
@@ -240,9 +241,10 @@ Rejected: <rejected option name>; ... (first 6 names)
240
241
  - Extracted from `decision-anchor.md` with the line-level parser frozen in
241
242
  `services/fact_view.py` (`parse_decision_anchor`); the zcode and opencode JS
242
243
  lines carry an identical parser shape.
243
- - Missing anchor file, no extractable essentials, or terminal statuses
244
- (`completed`) inject nothing.
245
-
244
+ - The `AC evidence:` row appears only for tasks that declare AC rows, right
245
+ after the `Acceptance:` row, and renders byte-identically across the
246
+ python / zcode-shim / opencode lines (matrix-locked). Schema and gate
247
+ semantics live in `evidence-completion.md`.
246
248
  - Missing anchor file, no extractable essentials, or terminal statuses
247
249
  (`completed`) inject nothing.
248
250
 
@@ -66,6 +66,7 @@
66
66
  ## Lifecycle Check 联动
67
67
 
68
68
  - `task next <dir> --run` 启动任务时的 preflight 检查 `decision-anchor.md` 非空,并要求 `## 目标` 和 `## 验收标准` 章节存在。
69
+ - `## 验收标准` 行的勾选状态(`- [ ]` / `- [x]`)与证据记录(任务目录 `evidence.jsonl`)是完成门禁的事实源:存在未勾选 AC(`LIFECYCLE-AC-001`)或已声明 AC 缺证据(`LIFECYCLE-AC-002`)时完成被阻断。schema 与豁免见 `evidence-completion.md`。
69
70
  - `cowork-implement` 读取:见 .cowork-flow/spec/contracts/subagent-dispatch.md
70
71
  - `cowork-check` 读取:同上
71
72
  - `debug.jsonl` 引用:可选,偏差诊断时写入 anchor 锚点差异
@@ -0,0 +1,48 @@
1
+ # Evidence Completion Contract
2
+
3
+ **任务完成从"流程走完"升级为"证据齐全":decision-anchor 的验收标准(AC)是机器可查询的事实,完成门禁校验每个 AC 的证据记录存在性。**
4
+
5
+ 机器只裁决存在性事实(AC 声明、勾选状态、证据条目有无);一条证据是否真正证明对应 AC,由 review 判断。
6
+
7
+ ## 证据记录:`evidence.jsonl`
8
+
9
+ 任务目录下的追加式 JSONL 文件,由实现方(主会话或子代理)在实现过程中写入,runtime 只读:
10
+
11
+ ```json
12
+ {"ac": "AC-001", "kind": "test", "ref": "tests/test_ac_evidence.py", "note": "覆盖计算三态断言", "recordedAt": "2026-10-06", "by": "cwf-cli-1004"}
13
+ ```
14
+
15
+ | 字段 | 必填 | 含义 |
16
+ |---|---|---|
17
+ | `ac` | 是 | 对应的 AC id(`AC-xxx`,与 decision-anchor 一致) |
18
+ | `kind` | 是 | `test`(自动化测试)、`command`(命令及退出码)、`manual`(手工验证记录) |
19
+ | `ref` | 是 | 证据指向:测试文件/用例、命令、手工记录位置;非空字符串 |
20
+ | `note` | 否 | 核心断言一句话 |
21
+ | `recordedAt` | 否 | ISO 日期 |
22
+ | `by` | 否 | 写入者会话/执行者标识 |
23
+
24
+ 坏行、缺字段、未知 `kind` 的记录被读取端跳过(与 context JSONL 容错一致),门禁只看有效记录。
25
+
26
+ ## 完成门禁
27
+
28
+ `task next <dir> --run` 的完成动作在直接生命周期检查中追加两条稳定 code:
29
+
30
+ | code | 触发事实 | 豁免 |
31
+ |---|---|---|
32
+ | `LIFECYCLE-AC-001` | 存在未勾选的 AC 行 | 无——完成工作本身,或勾选已完成的行 |
33
+ | `LIFECYCLE-AC-002` | 存在已声明但无任何有效证据记录的 AC | `--allow-missing-evidence`(记入 task.json `meta.evidenceExempt`,含 `at` 与 `missing`) |
34
+
35
+ - 两个 code 可同时出现;`--allow-missing-evidence` 只豁免 `LIFECYCLE-AC-002`。
36
+ - **兼容路径**:decision-anchor 缺失或未声明任何 AC 行的任务(全部存量任务)不触发本门禁;ac_evidence 模块不可读时同样降级为不阻断。
37
+ - 豁免留痕 `meta.evidenceExempt` 与 spec-check 的 `specCheckExempt` 同型:重试不累积,最新一次完成结果为准。
38
+
39
+ ## 注入与事实视图
40
+
41
+ - decision-anchor 注入块:声明了 AC 的任务在 `Acceptance:` 行后追加一行 `AC evidence: <有证据数>/<总数>`,有缺失时附 ` missing=<id 逗号列表,最多 5 个>`;三线(Python / zcode shim / opencode 插件)逐字节同格式,由矩阵测试锁定。
42
+ - `run state <task> --json` / MCP 事实视图:`evidenceCoverage: {total, withEvidence, missing[]}`;未声明 AC 时为 `null`。
43
+
44
+ ## 边界
45
+
46
+ - runtime 不代写证据,不执行 AC 对应命令"代打"记录。
47
+ - 不解析 AC 正文语义;`AC-xxx` 行的勾选框缺失时按未完成解析。
48
+ - 本契约不改变 spec-check 语义;MCP 写工具仍默认拒绝(`fact-layer-access.md`)。
@@ -16,3 +16,4 @@
16
16
  - `decision-anchor.md`: 每个 task 的 `decision-anchor.md` schema——目标、验收标准、被拒方案与关键假设。
17
17
  - `error-output-as-data.md`: 外部错误输出按数据分析而非指令执行(`ERROR_OUTPUT_AS_DATA_V1`)。
18
18
  - `fact-layer-access.md`: 无注入 hook 的宿主与外部工具读取任务事实的通道(`FACT_LAYER_ACCESS_V1`)。
19
+ - `evidence-completion.md`: 证据化完成——`evidence.jsonl` schema、`LIFECYCLE-AC-001/002` 完成门禁、`--allow-missing-evidence` 豁免留痕与注入覆盖行。
@@ -1,8 +1,16 @@
1
1
  # Spec Check 契约(规范挂命令)
2
2
 
3
- 用户规范(`.cowork-flow/spec/` 下 `backend/`、`frontend/`、自建目录)可通过
4
- frontmatter 的 `checks:` 声明检查命令。机制只执行声明、不解析规范正文——
5
- 规则变化 = 用户改同一个文件里的命令,天然同步。
3
+ ## 它做什么
4
+
5
+ 用户规范(`.cowork-flow/spec/` 下 `backend/`、`frontend/`、自建目录)可通过 frontmatter 的
6
+ `checks:` 声明检查命令。机制只执行声明、不解析规范正文——规则变化 = 用户改同一个文件里的命令,天然同步。
7
+
8
+ ## 什么时候挂
9
+
10
+ 条款能用命令退出码判定时(lint / 测试 / 类型检查 / schema 校验)就值得挂 `checks:`。两条硬约束:
11
+
12
+ - **声明的命令不存在 = `unchecked` = 阻断 `complete`**:挂之前先确认命令在当前仓库能跑通。
13
+ - **edit-only 不是门禁**:`when: edit` 只是编辑期 best-effort 提示——Bash 直写绕过 PostToolUse、无编辑期快跑能力的宿主(见附录 A)都收不到这层反馈,收口期也不补跑;需要硬门禁的检查必须声明 `when: lifecycle` 或默认 `both`。
6
14
 
7
15
  ## 语法
8
16
 
@@ -19,17 +27,12 @@ checks:
19
27
  # 规范正文(机制不读)
20
28
  ```
21
29
 
22
- - `files` 只支持两种形式:目录前缀(`src/`,以 `/` 结尾或自动补齐)与
23
- 扩展名(`*.ts`)。完整 glob 形态(`**`、`[]`)解析为错误,声明不执行,
24
- doctor 报告——绝不回退为"匹配一切"。多值形态(如 `files: "src/", "lib/"`)
25
- 每个 token 按同样的引号规则去引号后生效。
26
- - `when: edit` 的声明在编辑期(PostToolUse)对命中 `files` 的改动文件
27
- 就地执行,超时被钳制到 2.5 秒(为解释器启动与 cmd 包装留 spawn 预算);
28
- 慢命令请声明 `when: lifecycle`。
29
- - 引号规则:仅值整体为单对引号时去引号;`"python" -c "..."`
30
- 这类含多对引号的命令值原样保留,由执行器按 shell 规则拆分。
30
+ - `files` 只支持两种形式:目录前缀(`src/`,以 `/` 结尾或自动补齐)与扩展名(`*.ts`)。完整 glob 形态(`**`、`[]`)解析为错误、声明不执行、doctor 报告——绝不回退为"匹配一切";多值(`files: "src/", "lib/"`)每个 token 按同一引号规则去引号后生效。
31
+ - 引号规则:仅值整体为单对引号时去引号;`"python" -c "..."` 这类含多对引号的命令值原样保留,由执行器按 shell 规则拆分。
32
+ - `when: edit` 在编辑期(PostToolUse)对命中 `files` 的改动文件就地执行,超时被钳制到 2.5 秒(为解释器启动与 cmd 包装留 spawn 预算);慢命令请声明 `when: lifecycle`。
33
+ - **文件必须 UTF-8 无 BOM**:BOM 开头的 frontmatter 会被解析器静默忽略(声明数 0、无解析错误)——声明无声失效。挂完声明请跑一次 `spec-check` 确认被解析。
31
34
 
32
- ## 三态结果(完成门禁语义)
35
+ ## 三态与门禁
33
36
 
34
37
  | 状态 | 含义 | complete 门禁 |
35
38
  | --- | --- | --- |
@@ -37,58 +40,26 @@ checks:
37
40
  | `violation` | 退出码非 0 | **阻断**(状态不推进) |
38
41
  | `unchecked` | 命令不存在/解释器缺失/超时 | **阻断**;显式 `--allow-unchecked` 可放行,豁免留痕进 task.json `meta.specCheckExempt` |
39
42
 
40
- unchecked 永不冒充 pass("没跑成"不是"通过")。frontmatter 畸形视该
41
- spec 为无检查并进 doctor 报告,不炸流程。
42
-
43
- 解析失败是独立于三态之外的信号:`files` 形态非法等声明解析错误归入
44
- `parseErrors`,不折进 `pass`/`unchecked`,退出码与 `unchecked` 同为 2。
45
- 文本模式与 `--json` 必须使用同一退出码,且文本模式必须打印解析错误行
46
- (`spec-check: N parse error(s): <spec>: <error>`)——只打印
47
- `0 passed, 0 violations, 0 unchecked` 会让畸形声明看起来像干净通过。
48
-
49
- 退出码契约:`0` 全部 pass;`1` 存在 violation;`2` 存在 unchecked 或
50
- `parseErrors`。`--allow-unchecked` 只豁免 unchecked 的阻断,不豁免
51
- 解析错误。
52
-
53
- ## 时机与单行输出
54
-
55
- - **编辑期**(zcode/claude):违规输出单行
56
- `spec-check[<spec>] violation: <首个违规行>`;同一文件 10 秒内只报一次
57
- (节流状态在 `.cowork-flow/.runtime/spec-edit-throttle.json`,检查完整
58
- 跑过才写节流——执行器崩溃不消费时间窗,重试仍会执行);执行器
59
- 缺失或超时静默,绝不阻断编辑流。delegated 子代理会话同样收到 spec
60
- 违规反馈(子代理是主力写码者);scope 警告保持 main-only。zcode 主
61
- 会话的 hook 身份(对话自身 session id)不会被绑定——激活发生在 Bash
62
- 侧显式身份下——显示与编辑期警告按最新主会话兜底;claude/codex 保持
63
- 严格会话身份(其 Bash env 携带同一 session id)。zcode 经
64
- shim 转发到单源入口(`inject.py`),scope 警告与 spec 警告合并进同一个
65
- additionalContext 载荷;claude 走 exit 2 stderr 反馈。
66
- - **收口期**(task complete):全量执行,结果进 `meta.specCheckSummary`
67
- 遥测;阻断消息只列未过项摘要。
68
- - 手动查询:`./.cowork-flow/run spec-check [--file <path>] [--json]
69
- [--verbose]`(退出码见上)。
70
- - **edit-only 是 best-effort 提示,不是门禁**:Bash 直写绕过 PostToolUse
71
- 不会触发编辑期反馈;无编辑期快跑能力的宿主(见能力矩阵)同样没有
72
- 这层提示,且收口期不会对 `when: edit` 声明补跑。需要硬门禁的检查
73
- 必须声明 `when: lifecycle` 或默认 `both`。
74
- - **开放决策**:收口期全量执行没有聚合预算——声明多且慢的仓库 complete
75
- 会线性变慢;是否给收口加并行/预算上限留给后续产品决策,当前保持
76
- 简单串行。
77
-
78
- ## 归属与 sync 策略
79
-
80
- - **`spec/` 整体是 sync 保护前缀**:已存在的 spec 文件不会被 `sync`
81
- 覆盖——模板里 spec 的更新只对 `init` 新装生效,已安装项目的本地
82
- 内容不会被同步任务改写。
83
- - **具名例外**:`spec/contracts/` 下列入 `spec/runtime/host-assets.json`
84
- 的 `syncPolicy.safeFiles` 条目(本文件与 `workflow-state-templates.md`)
85
- 随 sync 更新,使契约修正能到达已安装项目;其余 spec 文件保持保护。
86
- - **声明由使用方维护**:模板自带的 spec 文件不含生效的 `checks:` 声明
87
- ——模板无法预知项目命令,而命令缺失会归 `unchecked` 并阻断
88
- `complete`。请把声明写进自建 spec 文件(如 `spec/team-xxx.md` 或
89
- 自建子目录),sync 不触碰,声明随规范正文一起由使用方维护。
90
-
91
- ## 宿主能力矩阵
43
+ unchecked 永不冒充 pass("没跑成"不是"通过");frontmatter 畸形视该 spec 为无检查并进 doctor 报告,不炸流程。解析失败是独立于三态之外的信号:`files` 形态非法等声明解析错误归入 `parseErrors`,不折进 `pass`/`unchecked`,退出码与 `unchecked` 同为 2。文本模式与 `--json` 必须使用同一退出码,且文本模式必须打印解析错误行(`spec-check: N parse error(s): <spec>: <error>`)——只打印 `0 passed, 0 violations, 0 unchecked` 会让畸形声明看起来像干净通过。
44
+
45
+ 退出码契约:`0` 全部 pass;`1` 存在 violation;`2` 存在 unchecked 或 `parseErrors`。`--allow-unchecked` 只豁免 unchecked 的阻断,不豁免解析错误。
46
+
47
+ **零声明是合法状态,但必须显式可见**:仓库无任何生效声明时退出码为 `0`,文本模式输出独立事实行 `spec-check: no checks declared (<N> spec files scanned)`,与"有声明且全过"的 `spec-check: <n> passed, 0 violations, 0 unchecked` 可区分;`--json` 顶层含 `specFiles`、summary 含 `declarations`。零声明 ≠ 干净通过——它意味着规范链上当前没有可机检约束,是需要被看见的事实,而不是可冒充通过的绿色。
48
+
49
+ ## 时机与输出
50
+
51
+ - **编辑期**(PostToolUse):仅 `when: edit`/`both` 且改动命中 `files` 时执行,违规输出单行 `spec-check[<spec>] violation: <首个违规行>`;执行器缺失或超时静默,绝不阻断编辑流。宿主差异与节流细节见附录 A。
52
+ - **收口期**(task complete):全量执行,结果进 `meta.specCheckSummary` 遥测;阻断消息只列未过项摘要。
53
+ - **手动查询**:`./.cowork-flow/run spec-check [--file <path>] [--json] [--verbose]`(退出码见上)。
54
+
55
+ ## 声明归使用方维护
56
+
57
+ - **模板不预置声明**:模板自带的 spec 文件不含生效的 `checks:` 声明——模板无法预知项目命令,而命令缺失会归 `unchecked` 并阻断 `complete`。请把声明写进自建 spec 文件(如 `spec/team-xxx.md` 或自建子目录),sync 不触碰,声明随规范正文一起由使用方维护。
58
+ - **sync 保护前缀与具名例外**:`spec/` 整体是 sync 保护前缀,已存在的 spec 文件不会被 `sync` 覆盖(模板里 spec 的更新只对 `init` 新装生效);唯一具名例外是 `spec/contracts/` 下列入 `spec/runtime/host-assets.json` `syncPolicy.safeFiles` 的条目(本文件与 `workflow-state-templates.md`),它们随 sync 更新,使契约修正能到达已安装项目。
59
+ - **识别并挂声明是主会话 AI 的职责**:声明归项目所有。review / 规范维护时,主会话 AI 负责识别可机检条款并补 `checks:` 声明(条款正文不擅自改——发现条款与实现冲突时按 review 流程提出,由用户决策);仅主会话写入 spec,delegated 子代理只上报候选、不越 scope。完整职责见 `task-review` 与 `spec-sync` Skill。
60
+ - **质量纪律四条**:① 在干净树上跑出 `pass`;② 用一个反例证明该检查确实能报错(不是恒过的装饰);③ 用 `files` 限定适用范围;④ 挂后跑一次 `spec-check` 确认声明被解析(防 BOM / `files` 形态错误静默失效);并在 review 报告列出「条款 → 命令」清单。
61
+
62
+ ## 附录 A 宿主能力矩阵
92
63
 
93
64
  | 宿主 | digest 注入 | 编辑期快跑 | 收口强制 |
94
65
  | --- | --- | --- | --- |
@@ -102,27 +73,23 @@ spec 为无检查并进 doctor 报告,不炸流程。
102
73
 
103
74
  zcode / codex / claude-code 经 Python 单源共享 `run_edit_checks`;opencode 在插件内以 CLI 拉取同一执行器(`run spec-check --phase edit --throttled`),dsh 经预设插件的 `tools/post-execute` 调同一 Python 协议,qoder 经插件 hook 的 `PostToolUse` 调同一执行器(该事件在 Qoder 不可阻断,告警以 `additionalContext` 随 exit 0 返回,而非 claude-code/codex 的 stderr + exit 2)——具备编辑期快跑的六家共享同一节流状态与三态语义,不各自实现检查逻辑;kimi-code 只走收口强制与子代理自查。
104
75
 
105
- **delegated 子代理编辑期缺口(zcode 实测,2026-09-09 两轮)**:ZCode 的插件
106
- hook 只在主会话工具流上派发(事件模型七事件均挂主会话;子代理是独立内部
107
- 会话流 `sess_subagent_agent_*`),Agent 子代理的 Edit/Write 收不到
108
- `spec-check[...] violation` 行——runtime 绑定与检查逻辑均正常,缺的是宿主
109
- 派发。补偿机制(subagent-dispatch.md 契约固化):
76
+ 编辑期节流:同一文件 10 秒内只报一次;节流状态在 `.cowork-flow/.runtime/spec-edit-throttle.json`,检查完整跑过才写节流——执行器崩溃不消费时间窗,重试仍会执行。delegated 子代理会话同样收到 spec 违规反馈(子代理是主力写码者);scope 警告保持 main-only。zcode 主会话的 hook 身份(对话自身 session id)不会被绑定——激活发生在 Bash 侧显式身份下——显示与编辑期警告按最新主会话兜底;claude/codex 保持严格会话身份(其 Bash env 携带同一 session id)。zcode 经 shim 转发到单源入口(`inject.py`),scope 警告与 spec 警告合并进同一个 additionalContext 载荷;claude 走 exit 2 stderr 反馈。
77
+
78
+ ## 附录 B delegated 编辑期缺口与补偿
110
79
 
111
- 1. 子代理完成前自查:`./.cowork-flow/run spec-check` 全量并修复(拉取路径,
112
- 实测有效);
80
+ **delegated 子代理编辑期缺口(zcode 实测,2026-09-09 两轮)**:ZCode 的插件 hook 只在主会话工具流上派发(事件模型七事件均挂主会话;子代理是独立内部会话流 `sess_subagent_agent_*`),Agent 子代理的 Edit/Write 收不到 `spec-check[...] violation` 行——runtime 绑定与检查逻辑均正常,缺的是宿主派发。补偿机制(subagent-dispatch.md 契约固化):
81
+
82
+ 1. 子代理完成前自查:`./.cowork-flow/run spec-check` 全量并修复(拉取路径,实测有效);
113
83
  2. 父会话验收前再查:violation 视为验收阻塞(反馈时点从收口提前到返回);
114
84
  3. 实现类任务 Verify 命令默认含 spec-check 自查;
115
85
  4. 收口硬门禁兜底不变(violation 阻断 complete)。
116
86
 
117
- 治理方向:向宿主提需求扩展子代理工具流的 hook 派发;落地后第 1 条自动
118
- 退化为冗余保险。
87
+ 治理方向:向宿主提需求扩展子代理工具流的 hook 派发;落地后第 1 条自动退化为冗余保险。
88
+
89
+ ## 附录 C digest 注入
90
+
91
+ 进入 in_progress 后,stage-contract 的 Specs 行升级为 `Specs: <spec>(<h2 标题树>); …`:最多 6 个 h2 条目、每条截断 24 字符、剔除 `();` 字符;文件缺失不加注解。digest 是条目名索引不是正文,受 scope-rules.json `stageContract.budget` 硬顶,超预算整行让位(Scope/Gates 行永不丢)。python/zcode 输出同源于单一 Python 入口,opencode 镜像与它逐字相等由 matrix 用例锁定(`test/stage-contract.test.js` matrix)。
119
92
 
120
- ## digest 注入
93
+ ## 附录 D 开放决策
121
94
 
122
- 进入 in_progress 后,stage-contract 的 Specs 行升级为
123
- `Specs: <spec>(<h2 标题树>); …`:最多 6 个 h2 条目、每条截断 24 字符、剔除
124
- `();` 字符;文件缺失不加注解。digest 是条目名索引不是正文,受
125
- scope-rules.json `stageContract.budget` 硬顶,超预算整行让位
126
- (Scope/Gates 行永不丢)。python/zcode 输出同源于单一 Python 入口,
127
- opencode 镜像与它逐字相等由 matrix 用例锁定
128
- (`test/stage-contract.test.js` matrix)。
95
+ **收口期聚合预算**:收口期全量执行没有聚合预算——声明多且慢的仓库 complete 会线性变慢;是否给收口加并行/预算上限留给后续产品决策,当前保持简单串行。
@@ -115,6 +115,19 @@
115
115
  "when changing task start readiness",
116
116
  "when changing resume context for active tasks"
117
117
  ]
118
+ },
119
+ {
120
+ "id": "SPEC_CHECKS_V1",
121
+ "path": ".cowork-flow/spec/contracts/spec-checks.md",
122
+ "digest": [
123
+ "Specs may declare `checks:` commands in frontmatter; the mechanism runs declarations, never prose.",
124
+ "A declared command that cannot be found counts as unchecked and blocks completion."
125
+ ],
126
+ "readWhen": [
127
+ "before adding or editing spec files",
128
+ "when a spec clause can be judged by a command exit code",
129
+ "when spec-check reports violations or unchecked"
130
+ ]
118
131
  }
119
132
  ],
120
133
  "references": [