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.
- package/CHANGELOG.md +23 -1
- package/docs/architecture.md +1 -34
- package/docs/direction.md +1 -1
- package/docs/hosts.md +13 -9
- 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/dsh/plugins/workflow-state.js +31 -16
- package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
- package/presets/opencode/cowork-flow/plugin-core.js +42 -87
- package/presets/opencode/plugins/cowork-flow.js +7 -9
- package/presets/qoder/.qoder-plugin/plugin.json +1 -1
- package/presets/qoder/hooks/hooks.json +1 -1
- package/presets/zcode/.zcode-plugin/plugin.json +1 -1
- package/src/commands/host.js +23 -2
- package/src/commands/registry.js +4 -2
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/adapters/claude-code/adapter.yaml +1 -1
- package/template/.cowork-flow/adapters/codex/adapter.yaml +1 -1
- package/template/.cowork-flow/adapters/dsh/adapter.yaml +1 -1
- package/template/.cowork-flow/adapters/opencode/adapter.yaml +1 -1
- package/template/.cowork-flow/adapters/qoder/adapter.yaml +1 -1
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/task_parser.py +0 -7
- package/template/.cowork-flow/scripts/adapters/host/claude_code_policy.py +6 -4
- package/template/.cowork-flow/scripts/adapters/host/codex_policy.py +8 -6
- package/template/.cowork-flow/scripts/adapters/host/edit_scope.py +113 -0
- package/template/.cowork-flow/scripts/adapters/host/opencode_policy.py +12 -0
- package/template/.cowork-flow/scripts/adapters/host/qoder_policy.py +7 -5
- package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +0 -91
- package/template/.cowork-flow/scripts/adapters/host/zcode_policy.py +15 -77
- package/template/.cowork-flow/scripts/run.py +0 -2
- package/template/.cowork-flow/scripts/services/context_paths.py +2 -2
- package/template/.cowork-flow/scripts/services/fact_view.py +49 -21
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +1 -216
- package/template/.cowork-flow/scripts/services/lifecycle_policy.py +0 -5
- package/template/.cowork-flow/scripts/services/task_gate_profiles.py +1 -1
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +14 -31
- package/template/.cowork-flow/spec/backend/error-handling.md +0 -14
- package/template/.cowork-flow/spec/contracts/context-injection.md +49 -22
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +3 -3
- package/template/.cowork-flow/spec/contracts/error-codes.md +1 -3
- package/template/.cowork-flow/spec/contracts/evidence-completion.md +6 -8
- package/template/.cowork-flow/spec/contracts/index.md +0 -1
- package/template/.cowork-flow/spec/contracts/plan-binding.md +3 -2
- package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +6 -21
- package/template/.cowork-flow/spec/contracts/task-adopt.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +0 -13
- package/template/.cowork-flow/spec/runtime/host-assets.json +10 -7
- package/template/.cowork-flow/spec/runtime/scope-rules.json +2 -2
- package/template/.opencode/cowork-flow/plugin-core.js +42 -87
- package/template/.opencode/plugins/cowork-flow.js +7 -9
- package/template/AGENTS.md +0 -1
- package/template/skills/agent-dispatch/SKILL.md +0 -15
- package/template/skills/runtime-health/scripts/doctor.py +0 -49
- package/template/skills/spec-sync/SKILL.md +0 -1
- package/template/skills/task-planning/SKILL.md +1 -1
- package/template/skills/task-review/SKILL.md +3 -3
- package/template/.cowork-flow/scripts/adapters/cli/spec_check.py +0 -178
- package/template/.cowork-flow/scripts/services/spec_check.py +0 -573
- 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.
|
|
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
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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)
|
|
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 (
|
|
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
|
-
(
|
|
274
|
-
hard fence; the label says so. Delegated
|
|
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
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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)
|
|
300
|
-
the
|
|
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
|
|
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
|
|
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
|
|
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`)
|
|
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 —
|
|
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
|
|
55
|
-
|
|
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.
|
|
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
|
|
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
|
|
18
|
-
|
|
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
|
|