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.
- package/CHANGELOG.md +17 -1
- package/docs/direction.md +179 -0
- package/docs/index.md +1 -0
- 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/opencode/cowork-flow/plugin-core.js +51 -3
- package/presets/qoder/.qoder-plugin/plugin.json +1 -1
- package/presets/zcode/.zcode-plugin/plugin.json +1 -1
- package/presets/zcode/hooks/hooks.json +3 -3
- package/presets/zcode/hooks/{inject-context.js → inject-context.mjs} +11 -6
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/adapters/cli/spec_check.py +10 -1
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +3 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_parser.py +1 -0
- package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +21 -0
- package/template/.cowork-flow/scripts/adapters/host/zcode_policy.py +1 -1
- package/template/.cowork-flow/scripts/services/ac_evidence.py +107 -0
- package/template/.cowork-flow/scripts/services/fact_view.py +16 -2
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +84 -0
- package/template/.cowork-flow/scripts/services/lifecycle_policy.py +5 -0
- package/template/.cowork-flow/scripts/services/spec_check.py +11 -1
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +12 -0
- package/template/.cowork-flow/spec/contracts/context-injection.md +6 -4
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +1 -0
- package/template/.cowork-flow/spec/contracts/evidence-completion.md +48 -0
- package/template/.cowork-flow/spec/contracts/index.md +1 -0
- package/template/.cowork-flow/spec/contracts/spec-checks.md +49 -82
- package/template/.cowork-flow/spec/runtime/contract-registry.json +13 -0
- package/template/.opencode/cowork-flow/plugin-core.js +51 -3
- package/template/skills/spec-sync/SKILL.md +1 -0
- package/template/skills/task-review/SKILL.md +3 -2
- 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
|
|
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
|
-
{
|
|
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
|
|
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.
|
|
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
|
-
-
|
|
244
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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("没跑成"不是"通过"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
93
|
+
## 附录 D 开放决策
|
|
121
94
|
|
|
122
|
-
|
|
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": [
|