cowork-flow 1.9.2 → 2.0.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 (61) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/docs/architecture.md +1 -34
  3. package/docs/direction.md +1 -1
  4. package/docs/hosts.md +13 -9
  5. package/package.json +1 -1
  6. package/presets/claude-code/.claude-plugin/plugin.json +1 -1
  7. package/presets/codex/.codex-plugin/plugin.json +1 -1
  8. package/presets/dsh/plugins/workflow-state.js +31 -16
  9. package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
  10. package/presets/opencode/cowork-flow/plugin-core.js +42 -87
  11. package/presets/opencode/plugins/cowork-flow.js +7 -9
  12. package/presets/qoder/.qoder-plugin/plugin.json +1 -1
  13. package/presets/qoder/hooks/hooks.json +1 -1
  14. package/presets/zcode/.zcode-plugin/plugin.json +1 -1
  15. package/src/commands/host.js +23 -2
  16. package/src/commands/registry.js +4 -2
  17. package/template/.cowork-flow/.version +1 -1
  18. package/template/.cowork-flow/adapters/claude-code/adapter.yaml +1 -1
  19. package/template/.cowork-flow/adapters/codex/adapter.yaml +1 -1
  20. package/template/.cowork-flow/adapters/dsh/adapter.yaml +1 -1
  21. package/template/.cowork-flow/adapters/opencode/adapter.yaml +1 -1
  22. package/template/.cowork-flow/adapters/qoder/adapter.yaml +1 -1
  23. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +0 -1
  24. package/template/.cowork-flow/scripts/adapters/cli/task_parser.py +0 -7
  25. package/template/.cowork-flow/scripts/adapters/host/claude_code_policy.py +6 -4
  26. package/template/.cowork-flow/scripts/adapters/host/codex_policy.py +8 -6
  27. package/template/.cowork-flow/scripts/adapters/host/edit_scope.py +113 -0
  28. package/template/.cowork-flow/scripts/adapters/host/opencode_policy.py +12 -0
  29. package/template/.cowork-flow/scripts/adapters/host/qoder_policy.py +7 -5
  30. package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +0 -91
  31. package/template/.cowork-flow/scripts/adapters/host/zcode_policy.py +15 -77
  32. package/template/.cowork-flow/scripts/run.py +0 -2
  33. package/template/.cowork-flow/scripts/services/context_paths.py +2 -2
  34. package/template/.cowork-flow/scripts/services/fact_view.py +49 -21
  35. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +1 -216
  36. package/template/.cowork-flow/scripts/services/lifecycle_policy.py +0 -5
  37. package/template/.cowork-flow/scripts/services/task_gate_profiles.py +1 -1
  38. package/template/.cowork-flow/scripts/services/task_lifecycle.py +14 -31
  39. package/template/.cowork-flow/spec/backend/error-handling.md +0 -14
  40. package/template/.cowork-flow/spec/contracts/context-injection.md +49 -22
  41. package/template/.cowork-flow/spec/contracts/decision-anchor.md +3 -3
  42. package/template/.cowork-flow/spec/contracts/error-codes.md +1 -3
  43. package/template/.cowork-flow/spec/contracts/evidence-completion.md +6 -8
  44. package/template/.cowork-flow/spec/contracts/index.md +0 -1
  45. package/template/.cowork-flow/spec/contracts/plan-binding.md +3 -2
  46. package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +6 -21
  47. package/template/.cowork-flow/spec/contracts/task-adopt.md +1 -1
  48. package/template/.cowork-flow/spec/runtime/contract-registry.json +0 -13
  49. package/template/.cowork-flow/spec/runtime/host-assets.json +10 -7
  50. package/template/.cowork-flow/spec/runtime/scope-rules.json +2 -2
  51. package/template/.opencode/cowork-flow/plugin-core.js +42 -87
  52. package/template/.opencode/plugins/cowork-flow.js +7 -9
  53. package/template/AGENTS.md +0 -1
  54. package/template/skills/agent-dispatch/SKILL.md +0 -15
  55. package/template/skills/runtime-health/scripts/doctor.py +0 -49
  56. package/template/skills/spec-sync/SKILL.md +0 -1
  57. package/template/skills/task-planning/SKILL.md +1 -1
  58. package/template/skills/task-review/SKILL.md +3 -3
  59. package/template/.cowork-flow/scripts/adapters/cli/spec_check.py +0 -178
  60. package/template/.cowork-flow/scripts/services/spec_check.py +0 -573
  61. package/template/.cowork-flow/spec/contracts/spec-checks.md +0 -281
@@ -46,13 +46,8 @@ PROTECTED_WORKFLOW_PATTERNS = (
46
46
  )
47
47
 
48
48
  # A recorded exemption names what it waived: the flag that allowed it, the
49
- # actor who applied it, and the concrete items. The item list is capped so a
50
- # pathological repo cannot turn one completion into pages of output, and a
51
- # single command is clipped so one long declaration cannot do the same.
52
- SPEC_EXEMPTION_FLAG = "--allow-unchecked"
49
+ # actor who applied it, and the concrete items.
53
50
  EVIDENCE_EXEMPTION_FLAG = "--allow-missing-evidence"
54
- SPEC_EXEMPTION_ITEM_LIMIT = 10
55
- SPEC_EXEMPTION_COMMAND_LIMIT = 80
56
51
 
57
52
 
58
53
  @dataclass(frozen=True)
@@ -71,9 +66,7 @@ class LifecycleCheckResult:
71
66
  stage: str
72
67
  blockers: tuple[str, ...] = ()
73
68
  issues: tuple[LifecycleCheckIssue, ...] = ()
74
- spec_check: dict | None = None
75
69
  evidence_exemption: dict | None = None
76
- spec_exemption: dict | None = None
77
70
 
78
71
  def __post_init__(self) -> None:
79
72
  if self.issues and not self.blockers:
@@ -124,20 +117,9 @@ class LifecycleCheckRunner:
124
117
  task_dir: Path,
125
118
  *,
126
119
  allow_spec_file_modifications: bool | None = None,
127
- allow_unchecked_specs: bool | None = None,
128
120
  allow_missing_evidence: bool | None = None,
129
121
  execution_policy: LifecycleExecutionPolicy | None = None,
130
122
  ) -> LifecycleCheckResult:
131
- spec_report = _spec_check_report(self.repo_root)
132
- allow_unchecked = (
133
- bool(allow_unchecked_specs)
134
- if allow_unchecked_specs is not None
135
- else _policy_allows_unchecked(execution_policy)
136
- )
137
- spec_issues = _spec_check_completion_issues(
138
- spec_report,
139
- allow_unchecked_specs=allow_unchecked,
140
- )
141
123
  ac_issues, evidence_exemption = _ac_evidence_completion_facts(
142
124
  task_dir,
143
125
  allow_missing_evidence=(
@@ -161,15 +143,9 @@ class LifecycleCheckRunner:
161
143
  self.repo_root,
162
144
  task_dir,
163
145
  ))
164
- + list(spec_issues)
165
146
  + list(ac_issues)
166
147
  ),
167
- spec_check=spec_report,
168
148
  evidence_exemption=evidence_exemption,
169
- spec_exemption=_spec_check_exemption(
170
- spec_report,
171
- allow_unchecked_specs=allow_unchecked,
172
- ),
173
149
  )
174
150
 
175
151
 
@@ -182,12 +158,6 @@ def _policy_allows_spec_changes(
182
158
  return bool(allow_spec_file_modifications)
183
159
 
184
160
 
185
- def _policy_allows_unchecked(
186
- execution_policy: LifecycleExecutionPolicy | None,
187
- ) -> bool:
188
- return bool(execution_policy is not None and execution_policy.allow_unchecked_specs)
189
-
190
-
191
161
  def _policy_allows_missing_evidence(
192
162
  execution_policy: LifecycleExecutionPolicy | None,
193
163
  ) -> bool:
@@ -299,83 +269,6 @@ def _ac_evidence_completion_facts(
299
269
  return issues, exemption
300
270
 
301
271
 
302
- def _spec_check_report(repo_root: Path) -> dict | None:
303
- """Run the lifecycle-phase spec checks. A structural failure degrades to
304
- an unchecked report — never to a silently passing gate."""
305
- try:
306
- from services.spec_check import run_checks
307
- except Exception: # pragma: no cover - import surface must stay lazy
308
- return {"schemaVersion": 1, "phase": "lifecycle", "results": [],
309
- "parseErrors": [], "summary": {"pass": 0, "violation": 0,
310
- "unchecked": 1, "executorError": 1,
311
- "classification": "executor-failure"}}
312
- try:
313
- return run_checks(repo_root, phase="lifecycle")
314
- except Exception as error: # fail closed: unchecked, not pass
315
- return {"schemaVersion": 1, "phase": "lifecycle", "results": [],
316
- "parseErrors": [], "summary": {"pass": 0, "violation": 0,
317
- "unchecked": 1, "executorError": 1, "error": str(error),
318
- "classification": "executor-failure"}}
319
-
320
-
321
- def _exemption_command(cmd: object) -> str:
322
- """One declared command, clipped so a long line cannot swamp the output.
323
-
324
- The clip never splits the first token: that token names the program, which
325
- is the part that says what could not be verified.
326
- """
327
- text = " ".join(str(cmd or "").split())
328
- if len(text) <= SPEC_EXEMPTION_COMMAND_LIMIT:
329
- return text
330
- head = text.split(" ", 1)[0]
331
- return text[: max(SPEC_EXEMPTION_COMMAND_LIMIT, len(head))] + "…"
332
-
333
-
334
- def _unchecked_exemption_items(spec_report: dict) -> tuple[list[dict], bool]:
335
- """The unchecked declarations, capped, plus whether anything was dropped."""
336
- items = [
337
- {
338
- "spec": item.get("spec") or "?",
339
- "cmd": _exemption_command(item.get("cmd")),
340
- }
341
- for item in spec_report.get("results") or []
342
- if isinstance(item, dict) and item.get("status") == "unchecked"
343
- ]
344
- return items[:SPEC_EXEMPTION_ITEM_LIMIT], len(items) > SPEC_EXEMPTION_ITEM_LIMIT
345
-
346
-
347
- def _spec_check_exemption(
348
- spec_report: dict | None,
349
- *,
350
- allow_unchecked_specs: bool,
351
- ) -> dict | None:
352
- """The exemption this completion applied, or None.
353
-
354
- Single source for both the persisted telemetry and the delivery output:
355
- a waived gate that is recorded but never shown reads as a clean pass, and
356
- one that is shown as a bare count reads as a clean pass to whoever has to
357
- check it afterwards. An executor failure is never exemptible, so it never
358
- yields one.
359
- """
360
- if not spec_report or not allow_unchecked_specs:
361
- return None
362
- summary = spec_report.get("summary") or {}
363
- if int(summary.get("executorError") or 0):
364
- return None
365
- unchecked = int(summary.get("unchecked") or 0)
366
- if not unchecked:
367
- return None
368
- items, truncated = _unchecked_exemption_items(spec_report)
369
- exemption = {
370
- "unchecked": unchecked,
371
- "uncheckedItems": items,
372
- "flag": SPEC_EXEMPTION_FLAG,
373
- }
374
- if truncated:
375
- exemption["truncated"] = True
376
- return exemption
377
-
378
-
379
272
  def _actor_text(actor: object) -> str:
380
273
  """The recorded actor, or `null` — never a stand-in identity."""
381
274
  return "null" if actor is None else str(actor)
@@ -390,23 +283,6 @@ def exemption_report_lines(meta: dict | None) -> list[str]:
390
283
  """
391
284
  meta = meta if isinstance(meta, dict) else {}
392
285
  lines: list[str] = []
393
- spec_exempt = meta.get("specCheckExempt")
394
- if isinstance(spec_exempt, dict):
395
- lines.append(
396
- f"Exempted: {spec_exempt.get('unchecked', 0)} unchecked "
397
- f"spec-check command(s) via {spec_exempt.get('flag')} "
398
- "(recorded in task.json meta.specCheckExempt, "
399
- f"by {_actor_text(spec_exempt.get('by'))})"
400
- )
401
- items = spec_exempt.get("uncheckedItems") or []
402
- for item in items:
403
- if isinstance(item, dict):
404
- lines.append(f" - {item.get('spec')}: {item.get('cmd')}")
405
- if spec_exempt.get("truncated"):
406
- lines.append(
407
- f" - (more than {len(items)} unchecked commands; the list is "
408
- "truncated — see meta.specCheckExempt.uncheckedItems)"
409
- )
410
286
  evidence_exempt = meta.get("evidenceExempt")
411
287
  if isinstance(evidence_exempt, dict):
412
288
  missing = evidence_exempt.get("missing") or []
@@ -436,103 +312,12 @@ def exemption_meta_payloads(
436
312
  such as the creator or assignee.
437
313
  """
438
314
  payloads: dict[str, dict] = {}
439
- spec_exemption = getattr(check_result, "spec_exemption", None)
440
- if spec_exemption:
441
- payloads["specCheckExempt"] = {"by": executor, **spec_exemption}
442
315
  evidence_exemption = getattr(check_result, "evidence_exemption", None)
443
316
  if evidence_exemption:
444
317
  payloads["evidenceExempt"] = {"by": executor, **evidence_exemption}
445
318
  return payloads
446
319
 
447
320
 
448
- def _spec_check_parse_error_issue(parse_errors: list) -> LifecycleCheckIssue | None:
449
- """The blocking fact for declarations the parser could not read."""
450
- if not parse_errors:
451
- return None
452
- details = []
453
- for item in parse_errors:
454
- if isinstance(item, dict):
455
- spec = item.get("spec") or "?"
456
- error = item.get("error") or "unknown parse error"
457
- details.append(f"{spec}: {error}")
458
- suffix = "; ".join(details[:3])
459
- return LifecycleCheckIssue(
460
- code="SPEC-CHECK-PARSE-ERROR",
461
- message=(
462
- f"spec checks report {len(parse_errors)} parse error(s)"
463
- + (f": {suffix}" if suffix else "")
464
- ),
465
- )
466
-
467
-
468
- def _spec_check_executor_issue(summary: dict) -> LifecycleCheckIssue | None:
469
- """The blocking fact for a spec-check executor that never ran.
470
-
471
- Not waivable: --allow-unchecked exempts a command that could not run, not
472
- an executor that is broken. The `unchecked: 1` in a degraded report is a
473
- placeholder for "no verdict", so it never becomes a finding of its own.
474
- """
475
- if not int(summary.get("executorError") or 0):
476
- return None
477
- return LifecycleCheckIssue(
478
- code="SPEC-CHECK-EXECUTOR-ERROR",
479
- message=(
480
- "spec-check executor failed; the declared checks did not run, so "
481
- "this is not an exemption-eligible unchecked result — fix the "
482
- "spec-check executor and rerun"
483
- ),
484
- )
485
-
486
-
487
- def _spec_check_completion_issues(
488
- spec_report: dict | None,
489
- *,
490
- allow_unchecked_specs: bool,
491
- ) -> list[LifecycleCheckIssue]:
492
- if not spec_report:
493
- return []
494
- summary = spec_report.get("summary") or {}
495
- issues: list[LifecycleCheckIssue] = []
496
- parse_error_issue = _spec_check_parse_error_issue(
497
- spec_report.get("parseErrors") or []
498
- )
499
- if parse_error_issue is not None:
500
- issues.append(parse_error_issue)
501
- executor_issue = _spec_check_executor_issue(summary)
502
- if executor_issue is not None:
503
- issues.append(executor_issue)
504
- violation = int(summary.get("violation") or 0)
505
- unchecked = int(summary.get("unchecked") or 0)
506
- if violation:
507
- first_lines = []
508
- for item in spec_report.get("results") or []:
509
- if item.get("status") == "violation":
510
- detail = item.get("firstViolationLine") or item.get("reason") or ""
511
- first_lines.append(f"{item.get('spec')}: {detail}")
512
- issues.append(
513
- LifecycleCheckIssue(
514
- code="SPEC-CHECK-VIOLATION",
515
- message=(
516
- "spec checks report "
517
- f"{violation} violation(s): {'; '.join(first_lines[:3])}"
518
- ),
519
- )
520
- )
521
- if unchecked and executor_issue is None and not allow_unchecked_specs:
522
- issues.append(
523
- LifecycleCheckIssue(
524
- code="SPEC-CHECK-UNCHECKED",
525
- message=(
526
- f"spec checks report {unchecked} unchecked command(s) "
527
- "(missing/timeout); rerun spec-check, or pass "
528
- "--allow-unchecked to complete with the exemption recorded"
529
- ),
530
- )
531
- )
532
- return issues
533
-
534
-
535
-
536
321
  def task_changed_paths(repo_root: Path, task_dir: Path) -> tuple[list[str], bool]:
537
322
  """Review change set for one task: baseline..HEAD merged with the
538
323
  working tree, plus whether the snapshot degraded to status-only."""
@@ -35,7 +35,6 @@ class LifecycleExecutionPolicy:
35
35
 
36
36
  execution_scope: str
37
37
  allow_spec_file_modifications: bool
38
- allow_unchecked_specs: bool = False
39
38
  allow_missing_evidence: bool = False
40
39
 
41
40
 
@@ -106,20 +105,17 @@ def resolve_execution_policy(
106
105
  execution_context: ExecutionContext | None = None,
107
106
  *,
108
107
  allow_spec_file_modifications: bool | None = None,
109
- allow_unchecked_specs: bool | None = None,
110
108
  allow_missing_evidence: bool | None = None,
111
109
  ) -> LifecycleExecutionPolicy:
112
110
  """Resolve lifecycle scope capability from execution facts."""
113
111
  if execution_context is None:
114
112
  if (
115
113
  allow_spec_file_modifications is not None
116
- or allow_unchecked_specs is not None
117
114
  or allow_missing_evidence is not None
118
115
  ):
119
116
  return LifecycleExecutionPolicy(
120
117
  execution_scope="explicit",
121
118
  allow_spec_file_modifications=bool(allow_spec_file_modifications),
122
- allow_unchecked_specs=bool(allow_unchecked_specs),
123
119
  allow_missing_evidence=bool(allow_missing_evidence),
124
120
  )
125
121
  execution_context = ExecutionContext()
@@ -133,7 +129,6 @@ def resolve_execution_policy(
133
129
  return LifecycleExecutionPolicy(
134
130
  execution_scope=execution_context.mode,
135
131
  allow_spec_file_modifications=allowed,
136
- allow_unchecked_specs=bool(allow_unchecked_specs),
137
132
  allow_missing_evidence=bool(allow_missing_evidence),
138
133
  )
139
134
 
@@ -64,7 +64,7 @@ def is_light_task(task_data: dict) -> bool:
64
64
 
65
65
  A light task still runs under the strict profile: it skips the review
66
66
  receipt, the acceptance-evidence and the dev_type gates, and nothing
67
- else. The spec-check, file-scope and acceptance-row gates stay in force,
67
+ else. The file-scope and acceptance-row gates stay in force,
68
68
  so this is a narrower reading list rather than an exemption channel.
69
69
  """
70
70
  if resolve_gate_profile(task_data) != GATE_PROFILE_STRICT:
@@ -234,7 +234,6 @@ class TaskLifecycleService:
234
234
  *,
235
235
  completed_at: str | None = None,
236
236
  allow_spec_file_modifications: bool | None = None,
237
- allow_unchecked_specs: bool = False,
238
237
  allow_missing_evidence: bool = False,
239
238
  execution_context: object | None = None,
240
239
  ) -> LifecycleResult:
@@ -243,7 +242,6 @@ class TaskLifecycleService:
243
242
  task,
244
243
  completed_at=completed_at,
245
244
  allow_spec_file_modifications=allow_spec_file_modifications,
246
- allow_unchecked_specs=allow_unchecked_specs,
247
245
  allow_missing_evidence=allow_missing_evidence,
248
246
  execution_context=execution_context,
249
247
  )
@@ -255,7 +253,6 @@ class TaskLifecycleService:
255
253
  *,
256
254
  preflight: Preflight | None = None,
257
255
  allow_spec_file_modifications: bool | None = None,
258
- allow_unchecked_specs: bool = False,
259
256
  allow_missing_evidence: bool = False,
260
257
  completed_at: str | None = None,
261
258
  execution_context: object | None = None,
@@ -268,7 +265,6 @@ class TaskLifecycleService:
268
265
  self.repo_root,
269
266
  execution_context,
270
267
  allow_spec_file_modifications=allow_spec_file_modifications,
271
- allow_unchecked_specs=allow_unchecked_specs,
272
268
  allow_missing_evidence=allow_missing_evidence,
273
269
  )
274
270
  prepared = self._prepare_transition(
@@ -667,7 +663,6 @@ class TaskLifecycleService:
667
663
  allow_spec_file_modifications=(
668
664
  execution_policy.allow_spec_file_modifications
669
665
  ),
670
- allow_unchecked_specs=execution_policy.allow_unchecked_specs,
671
666
  allow_missing_evidence=execution_policy.allow_missing_evidence,
672
667
  execution_policy=execution_policy,
673
668
  )
@@ -749,32 +744,20 @@ class TaskLifecycleService:
749
744
  persisted["meta"] = meta
750
745
  if stage.name == COMPLETE_STAGE.name:
751
746
  meta = dict(persisted.get("meta") or {})
752
- spec_report = getattr(check_result, "spec_check", None)
753
- if isinstance(spec_report, dict):
754
- # Overwrite-on-each-completion telemetry: retry loops must
755
- # not accumulate, the latest gate result wins.
756
- meta["specCheckSummary"] = dict(
757
- spec_report.get("summary") or {}
758
- )
759
- # Resolved here rather than reusing resolved_executor: the
760
- # complete stage does not activate a session, so that one is
761
- # always None and the actor would never be recorded.
762
- payloads = exemption_meta_payloads(
763
- check_result,
764
- executor=self._resolve_executor(executor),
765
- )
766
- if "evidenceExempt" in payloads:
767
- meta["evidenceExempt"] = {
768
- "at": datetime.now(timezone.utc).strftime(
769
- "%Y-%m-%dT%H:%M:%SZ"
770
- ),
771
- **payloads["evidenceExempt"],
772
- }
773
- if "specCheckExempt" in payloads:
774
- meta["specCheckExempt"] = {
775
- "at": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
776
- **payloads["specCheckExempt"],
777
- }
747
+ # Resolved here rather than reusing resolved_executor: the
748
+ # complete stage does not activate a session, so that one is
749
+ # always None and the actor would never be recorded.
750
+ payloads = exemption_meta_payloads(
751
+ check_result,
752
+ executor=self._resolve_executor(executor),
753
+ )
754
+ if "evidenceExempt" in payloads:
755
+ meta["evidenceExempt"] = {
756
+ "at": datetime.now(timezone.utc).strftime(
757
+ "%Y-%m-%dT%H:%M:%SZ"
758
+ ),
759
+ **payloads["evidenceExempt"],
760
+ }
778
761
  persisted["meta"] = meta
779
762
  return persisted
780
763
 
@@ -1,19 +1,5 @@
1
1
  # 后端异常处理规范
2
2
 
3
- <!--
4
- 本 spec 支持 frontmatter `checks:` 声明检查命令(规范挂命令契约见
5
- spec/contracts/spec-checks.md)。模板自带的声明由上游维护、sync 会覆盖
6
- 本地修改;请把实际声明写进自建 spec 文件。示例:
7
-
8
- ---
9
- checks:
10
- - cmd: python scripts/check_error_codes.py
11
- files: "src/"
12
- when: lifecycle
13
- ---
14
-
15
- -->
16
-
17
3
  ## 目标
18
4
 
19
5
  - 调用方能稳定感知错误
@@ -23,7 +23,7 @@ would make the digest self-referential).
23
23
  | opencode | plugin `experimental.chat.system.transform` → shim (`cowork-flow/plugin-core.js`) → `inject.py --host opencode`, bare-text stdout | system-prompt section push; `shell.env` exports the context key to shell commands |
24
24
  | dsh | preset plugin system-prompt section | named section, replace semantics |
25
25
  | kimi-code | user-level `config.toml` (`$KIMI_CODE_HOME`, default `~/.kimi-code/`) `[[hooks]]` row on `UserPromptSubmit` → shim (`cowork-flow-inject.mjs`) → `inject.py --host kimi-code` | bare-text stdout appended to the prompt context, no `hookSpecificOutput` envelope |
26
- | qoder | machine-level plugin `hooks/hooks.json` (node, `${QODER_PLUGIN_ROOT}/hooks/inject-context.mjs`) → project `.cowork-flow/scripts/.../inject.py --host qoder`, stdout JSON | `hookSpecificOutput.{hookEventName, additionalContext}` plus a top-level `systemMessage` (a CLI-visible trace; the model context is unchanged); `PostToolUse` is not blockable there, so the edit advisory rides `additionalContext` on exit 0 |
26
+ | qoder | machine-level plugin `hooks/hooks.json` (node, `${QODER_PLUGIN_ROOT}/hooks/inject-context.mjs`) → project `.cowork-flow/scripts/.../inject.py --host qoder`, stdout JSON | `hookSpecificOutput.{hookEventName, additionalContext}` plus a top-level `systemMessage` (a CLI-visible trace; the model context is unchanged) |
27
27
 
28
28
  The qoder row carries one delta beyond the shared envelope: `systemMessage`,
29
29
  emitted only on the two injecting events (`SessionStart`, `UserPromptSubmit`)
@@ -260,7 +260,7 @@ review time:
260
260
  <stage-contract task=".cowork-flow/tasks/08-30-demo">
261
261
  Scope: src/demo.py; src/next.py (+1 more in implement.jsonl) [agent-mutable]
262
262
  Specs: .cowork-flow/spec/backend/index.md (+1 more)
263
- Gates: edits outside Scope are review blockers; CLAUDE.md and workflow files are protected; spec/ edits may be allowed by review policy; scope is agent-mutable (self-declared via task context add)
263
+ Gates: edits outside Scope are review blockers; CLAUDE.md and workflow files are protected; spec/ edits may be allowed by review policy; scope is agent-mutable (declare more in implement.jsonl)
264
264
  Verify: npm run test:fast; python3 -m pytest tests/ -q
265
265
  </stage-contract>
266
266
  ```
@@ -270,38 +270,65 @@ Verify: npm run test:fast; python3 -m pytest tests/ -q
270
270
  absolute paths, drive letters, wildcards, unsupported types — authorize
271
271
  nothing and are dropped, matching `context_paths.normalize_context_file_scope_entry`),
272
272
  first 8 + `(+N more)`. The scope is **agent-mutable** on main
273
- (`task context add` can extend it) — it is a self-declared boundary, not a
274
- hard fence; the label says so. Delegated sessions inherit the parent
273
+ (appending a scope entry to `implement.jsonl` extends it) — it is a
274
+ self-declared boundary, not a hard fence; the label says so. Delegated
275
+ sessions inherit the parent
275
276
  task's scope and see it as `[read-only]` — the child must not believe it
276
277
  can self-declare scope on the parent's behalf.
277
278
  - **Specs**: `.cowork-flow/spec/` pointer entries from implement.jsonl
278
279
  (first 4, same canonicality filter as Scope). Read them before coding; the
279
- full files are not injected.
280
+ full files are not injected. Once the task is `in_progress`, the line
281
+ upgrades to `Specs: <spec>(<h2 heading tree>); …`: at most 6 h2 entries,
282
+ each truncated to 24 characters with `();` characters stripped; a missing
283
+ file adds no annotation. The digest is an entry-name index, not the prose,
284
+ and is hard-capped by `scope-rules.json` `stageContract.budget`; over
285
+ budget the whole line yields. The python/zcode output comes from a single
286
+ Python entrypoint, locked by the `test/stage-contract.test.js` matrix;
287
+ opencode transports that same entrypoint's output, so byte-exact equality
288
+ holds by construction.
280
289
  - **Gates**: static preview text, identical on all hosts; the delegated
281
290
  variant replaces the self-declaration sentence with "scope is inherited
282
291
  from the parent task (read-only reference)".
283
- - **Verify**: `## 验证命令` lines from decision-anchor (first 3, 120 chars
292
+ - **Verify**: `## 验证命令` lines from decision-anchor (first 10, 120 chars
284
293
  each) — the agent's own declared self-checks; omitted when none.
285
- - **Per-edit warning (zcode only)**: PostToolUse on Edit/Write/MultiEdit
286
- injects at most one line when the edited file is outside the scope
287
- (`editScopeWarning: plugin`; every other host declares `unsupported` —
288
- their fallback is this static stage-contract preview). Bash-initiated
289
- writes (redirection, tee) are a declared residual gap: the warning covers
290
- Edit/Write/MultiEdit tool invocations only. The PostToolUse matcher text
291
- (`Bash|Edit|Write|MultiEdit`) is asserted by template tests but depends on
292
- the ZCode runtime's tool names — there is no external runtime contract to
293
- verify them against, so a runtime rename would silently disable the
294
- warning; the matcher is the single switch. This bullet covers the scope
295
- warning only; the spec-check violation line is a separate channel that
296
- also reaches codex and opencode sessions (see spec-checks.md).
297
- - Budget: the whole block stays ≤ 1200 characters. Over-budget inputs
294
+ - **Per-edit warning**: PostToolUse on an edit tool injects at most one
295
+ line when the edited file is outside the task's declared scope —
296
+ a frozen single line shared by every host, differing only in transport:
297
+ zcode / claude-code / codex / qoder return it as
298
+ `hookSpecificOutput.additionalContext`, opencode (bare-text `emit_text`)
299
+ appends it to the finished tool output, and dsh attaches it to the tool
300
+ result as a plugin notice. `editScopeWarning: plugin` is declared for
301
+ those six hosts; kimi-code declares `unsupported` because the host drops
302
+ PostToolUse stdout. Silence rules: no active in_progress/review task, a
303
+ subagent session, an in-scope path, or no extractable path; every failure
304
+ stays silent and every host exits 0 — the edit has already happened. No
305
+ host refreshes the workflow-state digest from this hook: the advisory is
306
+ the whole payload (state refreshes on the next injecting event, or on
307
+ dsh's `tools/result`).
308
+ Bash-initiated writes (redirection, tee) are a declared residual gap: the
309
+ warning covers edit tool invocations only. Each host's matcher text
310
+ (`Edit|Write|MultiEdit`, codex's `apply_patch|Write|Edit`, qoder's
311
+ `Edit|Write`) is asserted by template tests but depends on that host's
312
+ runtime's tool names — there is no external runtime contract to verify
313
+ them against, so a runtime rename would silently disable the warning; the
314
+ matcher is the single switch.
315
+ - Budget: the whole block stays ≤ 3400 characters. Over-budget inputs
298
316
  degrade by dropping Verify, then Specs, then shrinking Scope entries
299
- (min 1) — the closing tag and the Gates row always survive; tests assert
300
- the block stays well-formed. The scope-filter rules and the stage-contract
317
+ (min 1), then truncating the Scope row's own text. The opening tag, the
318
+ closing tag and the Gates row always survive intact — a body-wide cut
319
+ would either truncate the tag every consumer matches on (making the block
320
+ invisible to them) or silently drop the edit-policy row, so neither is
321
+ allowed. Below the size those fixed rows plus a minimal `Scope: X` row
322
+ need, or when the cut would shave a `Scope: ` label off any row, the block
323
+ is returned whole and may exceed the budget: guard rows and parseability
324
+ outrank the budget.
325
+ The scope-filter rules and the stage-contract
301
326
  budget/limits come from `.cowork-flow/spec/runtime/scope-rules.json`, the
302
327
  single source consumed at runtime; a missing or malformed file degrades to
303
328
  embedded defaults that are byte-identical with the shipped file (locked by
304
- `tests/test_scope_rules.py`).
329
+ `tests/test_scope_rules.py`). The file is template-managed
330
+ (`syncPolicy.safeFiles`): `cwf project sync` refreshes it, so local edits to
331
+ it do not survive a sync.
305
332
  - Absent for `no_task` / `planning` / `completed` states. Block content is
306
333
  byte-identical across host lines for the whole fixture matrix
307
334
  (`test/fixtures/stage-contract-matrix.json` drives the equality test:
@@ -52,7 +52,7 @@ translated heading would simply stop being read.
52
52
  fact view (`services/fact_view.parse_decision_anchor`). Line-format conventions:
53
53
 
54
54
  - `## 验证命令`: one command per line (an optional `- ` prefix), truncated to 120
55
- characters, at most 5 lines; the implementing agent runs them itself
55
+ characters, at most 10 lines; the implementing agent runs them itself
56
56
  (self-declared, self-checked) and the reviewer verifies the execution evidence.
57
57
  - `## 范围边界`: only the first non-empty line is taken, truncated to 160
58
58
  characters.
@@ -110,14 +110,14 @@ After merging (5 files): `decision-anchor.md` + `implement.jsonl` + `check.jsonl
110
110
  component.
111
111
  A **consequence** (expected cost, not a defect): **any byte change in
112
112
  `implement.jsonl`** (appending or removing a scope entry, rewording a `reason`,
113
- a domain-guide entry auto-appended by `task context add`) **or a rewrite of a
113
+ a domain-guide entry) **or a rewrite of a
114
114
  referenced spec's prose invalidates an existing receipt**, requiring one more
115
115
  review run against the final state. Appending evidence, ticking ACs, and runtime
116
116
  metadata are not in that set.
117
117
  - **Light profile `meta.taskType: "tiny"`**: applies to `strict` tasks only
118
118
  (`legacy` + `tiny` stays a compatibility path). A light task **is still strict**
119
119
  — what it waives are three things: the review receipt, AC evidence, and
120
- `dev_type`. It does **not** waive spec-check, the file-scope gate, or "AC rows
120
+ `dev_type`. It does **not** waive the file-scope gate or "AC rows
121
121
  are non-empty" (`LIFECYCLE-AC-003`/`004` still apply). Plan-file binding is not
122
122
  among the three, but the pre-existing field-level exemption for tiny applies, so
123
123
  a light task does not require `planFile` either. `tiny` **can only be declared at
@@ -44,6 +44,4 @@ to them:
44
44
  ## Exemptions
45
45
 
46
46
  `LIFECYCLE-AC-002` is the only code in this set a flag can waive
47
- (`--allow-missing-evidence`); spec-check's `--allow-unchecked` waives `unchecked`
48
- declarations, not a `LIFECYCLE-*` code. Details are in `evidence-completion.md` and
49
- `spec-checks.md`.
47
+ (`--allow-missing-evidence`). Details are in `evidence-completion.md`.
@@ -40,7 +40,7 @@ direct lifecycle checks:
40
40
  | `LIFECYCLE-AC-001` | an unchecked AC row exists | none — do the work, or tick the completed rows |
41
41
  | `LIFECYCLE-AC-002` | a declared AC has no valid evidence record | `--allow-missing-evidence` (recorded in task.json `meta.evidenceExempt`, with `at`, `flag`, `by`, and the `missing` detail list) |
42
42
  | `LIFECYCLE-AC-003` | `gateProfile=strict` and decision-anchor yields no AC row | none — a strict task was already required to declare ACs at start, so reading none at completion has only three explanations: the anchor was rewritten, the task was created wrongly, or the anchor is undecodable (corrupted encoding) |
43
- | `LIFECYCLE-AC-004` | `gateProfile=strict` and the ac_evidence module cannot be imported | none — same family as `SPEC-CHECK-EXECUTOR-ERROR`: a broken module is not "nothing to check"; the message carries the original exception, and you rerun once the runtime install is fixed |
43
+ | `LIFECYCLE-AC-004` | `gateProfile=strict` and the ac_evidence module cannot be imported | none — a broken module is not "nothing to check"; the message carries the original exception, and you rerun once the runtime install is fixed |
44
44
 
45
45
  - The first two codes can appear together; `--allow-missing-evidence` waives
46
46
  `LIFECYCLE-AC-002` only, never `003`/`004`.
@@ -51,8 +51,8 @@ direct lifecycle checks:
51
51
  - **Strict path**: a `gateProfile=strict` task re-verifies AC existence and
52
52
  evidence-module availability at completion — the start-time AC declaration
53
53
  requirement does not lapse because completion cannot read it.
54
- - The `meta.evidenceExempt` trace has the same shape as spec-check's
55
- `specCheckExempt`: retries do not accumulate, the latest completion result wins.
54
+ - The `meta.evidenceExempt` trace does not accumulate: retries leave only the
55
+ latest completion result.
56
56
  - **An exemption record must name which items were waived and who approved it**:
57
57
  `meta.evidenceExempt` = `{"at", "flag": "--allow-missing-evidence",
58
58
  "by": <actor or null>, "missing": [<AC id>…]}`. `by` is the resolved session
@@ -60,9 +60,7 @@ direct lifecycle checks:
60
60
  `creator`/`assignee`. Both the completion output and the archive output print
61
61
  this record item by item (the completion output reads the `task.json` just
62
62
  written), so "what was recorded" and "what was printed" cannot tell two different
63
- stories; with no exemption, no exemption rows are printed at all. The structure
64
- matches the `meta.specCheckExempt` detail rules on the spec-check side; see
65
- `spec-checks.md`.
63
+ stories; with no exemption, no exemption rows are printed at all.
66
64
 
67
65
  ## Injection and fact view
68
66
 
@@ -81,5 +79,5 @@ direct lifecycle checks:
81
79
  behind an AC to fabricate a record.
82
80
  - It does not parse AC prose semantics; a missing checkbox on an `AC-xxx` row
83
81
  parses as incomplete.
84
- - This contract does not change spec-check semantics; MCP write tools still refuse
85
- by default (`fact-layer-access.md`).
82
+ - This contract does not change the file-scope gate or the AC gate; MCP write
83
+ tools still refuse by default (`fact-layer-access.md`).
@@ -12,7 +12,6 @@ Current contracts:
12
12
  - `capabilities.md`: the host adapter capability model.
13
13
  - `party-mode-v2-board.md`: the Party Mode V2 board protocol.
14
14
  - `plan-binding.md`: lightweight development-plan binding, pre-start missing-plan blocking, and recovery visibility.
15
- - `spec-checks.md`: the spec-declared command contract — syntax of `checks:` in user-spec frontmatter, three-state gate semantics, timing, and the host capability matrix.
16
15
  - `context-injection.md`: the transport shapes, event-timing matrix, and fingerprint serialization rules for runtime-context injection across hosts (a meta-protocol, not in `contract-registry.json`).
17
16
  - `decision-anchor.md`: the schema of each task's `decision-anchor.md` — goal, acceptance criteria, rejected options, and key assumptions.
18
17
  - `error-output-as-data.md`: external error output is analyzed as data, never executed as instructions (`ERROR_OUTPUT_AS_DATA_V1`).
@@ -14,8 +14,9 @@ the plan into a second workflow kernel.
14
14
 
15
15
  ## Creation phase
16
16
 
17
- - A plain `task create` may create a planning task with no plan.
18
- - `task create --from-plan <path>` is an explicit binding request; `<path>` must
17
+ - A plain task creation (`task next --title … --slug …`) may create a planning
18
+ task with no plan.
19
+ - `task next --from-plan <path>` is an explicit binding request; `<path>` must
19
20
  exist, lie inside the repository, and lie under `.cowork-flow/plans/`.
20
21
  - On a successful binding the runtime writes:
21
22