cowork-flow 0.0.47 → 0.0.49
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 +41 -0
- package/README.md +102 -23
- package/package.json +8 -3
- package/src/cli.js +6 -0
- package/src/commands/init.js +0 -83
- package/src/commands/source-refresh.js +40 -0
- package/src/commands/sync.js +6 -0
- package/src/commands/update.js +59 -6
- package/src/lib/copy-template.js +200 -50
- package/src/lib/host-assets.js +367 -64
- package/src/lib/plan-applier.js +47 -0
- package/template/.claude/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/config.yaml +0 -11
- package/template/.cowork-flow/scripts/adapters/cli/init_developer.py +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/subagent.py +7 -4
- package/template/.cowork-flow/scripts/adapters/cli/task.py +8 -484
- package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +15 -134
- package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +7 -1
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +388 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +163 -2
- package/template/.cowork-flow/scripts/adapters/git/git_context.py +16 -193
- package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +170 -84
- package/template/.cowork-flow/scripts/infra/config.py +0 -17
- package/template/.cowork-flow/scripts/infra/developer_profile.py +0 -95
- package/template/.cowork-flow/scripts/infra/paths.py +0 -79
- package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
- package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
- package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
- package/template/.cowork-flow/scripts/kernel/workflow_route.py +15 -15
- package/template/.cowork-flow/scripts/run.py +0 -5
- package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
- package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
- package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +186 -69
- package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
- package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
- package/template/.cowork-flow/scripts/services/readiness.py +3 -170
- package/template/.cowork-flow/scripts/services/task_archive.py +115 -24
- package/template/.cowork-flow/scripts/services/task_context.py +103 -365
- package/template/.cowork-flow/scripts/services/task_creation.py +40 -5
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
- package/template/.cowork-flow/scripts/services/task_routing.py +80 -67
- package/template/.cowork-flow/scripts/services/task_utils.py +0 -146
- package/template/.cowork-flow/scripts/services/workflow_runtime.py +1 -1
- package/template/.cowork-flow/spec/contracts/capabilities.md +23 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +2 -2
- package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
- package/template/.cowork-flow/spec/contracts/index.md +1 -0
- package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +20 -0
- package/template/.cowork-flow/spec/contracts/plan-binding.md +50 -0
- package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +31 -7
- package/template/.cowork-flow/spec/guides/index.md +3 -1
- package/template/.cowork-flow/spec/index.md +5 -4
- package/template/.cowork-flow/spec/references/definition-of-done.md +2 -3
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +14 -0
- package/template/.cowork-flow/spec/runtime/host-assets.json +73 -3
- package/template/.cowork-flow/spec/runtime/index.md +1 -1
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +150 -8
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/skills/batch-execution/SKILL.md +37 -3
- package/template/skills/batch-execution/scripts/batch_action.py +4 -0
- package/template/skills/batch-execution/scripts/batch_execution.py +120 -499
- package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
- package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
- package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
- package/template/skills/brainstorming/SKILL.md +8 -7
- package/template/skills/cowork-flow/SKILL.md +3 -0
- package/template/skills/party-mode/scripts/party_action_contract.py +113 -0
- package/template/skills/party-mode/scripts/party_actions.py +95 -0
- package/template/skills/party-mode/scripts/party_board_store.py +148 -0
- package/template/skills/party-mode/scripts/party_mode_v2.py +345 -317
- package/template/skills/party-mode/scripts/party_reports.py +177 -0
- package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
- package/template/skills/runtime-health/SKILL.md +14 -2
- package/template/skills/runtime-health/scripts/doctor.py +278 -41
- package/template/skills/task-planning/SKILL.md +105 -61
- package/template/skills/task-review/SKILL.md +4 -4
- package/template/skills/task-review/scripts/review_check.py +277 -20
- package/template/skills/test-first/SKILL.md +18 -3
- package/template/skills/test-first/manifest.json +8 -4
- package/template/.cowork-flow/changes/archive/.gitkeep +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/add_session.py +0 -465
- package/template/.cowork-flow/scripts/adapters/cli/change.py +0 -412
- package/template/.cowork-flow/workspace/index.md +0 -102
- package/template/.zcode/scaffold/AGENTS.md +0 -145
- package/template/.zcode/scaffold/CLAUDE.md +0 -5
|
@@ -29,12 +29,30 @@ from adapters.host.host_manifest import (
|
|
|
29
29
|
)
|
|
30
30
|
from infra.paths import get_repo_root
|
|
31
31
|
from infra.skill_manifest import SkillManifestError, action_owners, load_skill_manifests
|
|
32
|
+
from infra.storage.operation_log import OperationLog
|
|
33
|
+
from infra.storage.state_store import DEFAULT_STALE_LOCK_SECONDS, StateStore
|
|
32
34
|
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
def _issue(
|
|
37
|
+
*,
|
|
38
|
+
code: str,
|
|
39
|
+
severity: str,
|
|
40
|
+
path: str,
|
|
41
|
+
message: str,
|
|
42
|
+
command_hint: str = "",
|
|
43
|
+
contract: str,
|
|
44
|
+
**extra: str,
|
|
45
|
+
) -> dict[str, str]:
|
|
46
|
+
issue = {
|
|
47
|
+
"code": code,
|
|
48
|
+
"severity": severity,
|
|
49
|
+
"path": path,
|
|
50
|
+
"message": message,
|
|
51
|
+
"commandHint": command_hint,
|
|
52
|
+
"contract": contract,
|
|
53
|
+
}
|
|
54
|
+
issue.update(extra)
|
|
55
|
+
return issue
|
|
38
56
|
|
|
39
57
|
|
|
40
58
|
def _compare_file(
|
|
@@ -84,6 +102,40 @@ def _host_errors(repo_root: Path) -> list[str]:
|
|
|
84
102
|
return validate_host_assets(distribution_root, platform_ids=platform_ids)
|
|
85
103
|
|
|
86
104
|
|
|
105
|
+
def _host_issue(error: str) -> dict[str, str]:
|
|
106
|
+
path = ""
|
|
107
|
+
if ":" in error:
|
|
108
|
+
path = error.rsplit(":", 1)[-1].strip()
|
|
109
|
+
lowered = error.lower()
|
|
110
|
+
if "missing command target" in lowered:
|
|
111
|
+
code = "HOST-ASSET-MISSING-COMMAND-TARGET"
|
|
112
|
+
elif "missing command config" in lowered:
|
|
113
|
+
code = "HOST-ASSET-MISSING-COMMAND-CONFIG"
|
|
114
|
+
elif "invalid command config" in lowered:
|
|
115
|
+
code = "HOST-ASSET-INVALID-COMMAND-CONFIG"
|
|
116
|
+
elif "illegal capability" in lowered:
|
|
117
|
+
code = "HOST-ASSET-ILLEGAL-CAPABILITY"
|
|
118
|
+
elif "capability mismatch" in lowered:
|
|
119
|
+
code = "HOST-ASSET-CAPABILITY-MISMATCH"
|
|
120
|
+
elif "adapter host mismatch" in lowered:
|
|
121
|
+
code = "HOST-ASSET-HOST-MISMATCH"
|
|
122
|
+
elif "invalid adapter yaml" in lowered:
|
|
123
|
+
code = "HOST-ASSET-INVALID-ADAPTER"
|
|
124
|
+
else:
|
|
125
|
+
code = "HOST-ASSET-VALIDATION-ERROR"
|
|
126
|
+
return _issue(
|
|
127
|
+
code=code,
|
|
128
|
+
severity="error",
|
|
129
|
+
path=path,
|
|
130
|
+
message=error,
|
|
131
|
+
contract="runtime-health:host-adapters",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _host_issues(repo_root: Path) -> list[dict[str, str]]:
|
|
136
|
+
return [_host_issue(error) for error in _host_errors(repo_root)]
|
|
137
|
+
|
|
138
|
+
|
|
87
139
|
def _distribution_files(root: Path) -> tuple[Path, ...]:
|
|
88
140
|
files = [
|
|
89
141
|
path
|
|
@@ -95,26 +147,31 @@ def _distribution_files(root: Path) -> tuple[Path, ...]:
|
|
|
95
147
|
return tuple(sorted(files))
|
|
96
148
|
|
|
97
149
|
|
|
150
|
+
def _source_checkout_protected_path(relative: Path) -> bool:
|
|
151
|
+
value = relative.as_posix()
|
|
152
|
+
if value in {".cowork-flow/config.yaml", ".cowork-flow/.developer"}:
|
|
153
|
+
return True
|
|
154
|
+
return value.startswith(".cowork-flow/tasks/") or value.startswith(
|
|
155
|
+
".cowork-flow/plans/"
|
|
156
|
+
) or value.startswith(".cowork-flow/.runtime/")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _source_checkout_live_files(template: Path) -> tuple[Path, ...]:
|
|
160
|
+
runtime_root = template / ".cowork-flow"
|
|
161
|
+
return tuple(
|
|
162
|
+
source
|
|
163
|
+
for source in _distribution_files(runtime_root)
|
|
164
|
+
if not _source_checkout_protected_path(source.relative_to(template))
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
98
168
|
def check_distribution(repo_root: Path) -> list[str]:
|
|
99
169
|
errors: list[str] = []
|
|
100
170
|
template = repo_root / "template"
|
|
101
171
|
if _distribution_root(repo_root) == repo_root:
|
|
102
172
|
return errors
|
|
103
|
-
|
|
104
|
-
bootstrap = set(SOURCE_CHECKOUT_BOOTSTRAP_FILES)
|
|
105
|
-
for relative in SOURCE_CHECKOUT_BOOTSTRAP_FILES:
|
|
106
|
-
_compare_file(
|
|
107
|
-
template / relative,
|
|
108
|
-
repo_root / relative,
|
|
109
|
-
errors,
|
|
110
|
-
missing_left="missing template bootstrap asset",
|
|
111
|
-
missing_right="missing source bootstrap asset",
|
|
112
|
-
drift="source bootstrap drift",
|
|
113
|
-
)
|
|
114
|
-
for source in _distribution_files(runtime_root):
|
|
173
|
+
for source in _source_checkout_live_files(template):
|
|
115
174
|
relative = source.relative_to(template)
|
|
116
|
-
if relative in bootstrap:
|
|
117
|
-
continue
|
|
118
175
|
target = repo_root / relative
|
|
119
176
|
if target.is_file():
|
|
120
177
|
_compare_file(
|
|
@@ -123,11 +180,6 @@ def check_distribution(repo_root: Path) -> list[str]:
|
|
|
123
180
|
errors,
|
|
124
181
|
drift="local live runtime drift",
|
|
125
182
|
)
|
|
126
|
-
for relative in (Path(".cowork-flow/run"), Path(".cowork-flow/run.cmd")):
|
|
127
|
-
source = template / relative
|
|
128
|
-
target = repo_root / relative
|
|
129
|
-
if source.is_file() and target.is_file():
|
|
130
|
-
_compare_file(source, target, errors, drift="local live runtime drift")
|
|
131
183
|
try:
|
|
132
184
|
host_manifest = load_host_manifest(template)
|
|
133
185
|
except HostManifestError as error:
|
|
@@ -186,13 +238,19 @@ def _task_hygiene_issue(
|
|
|
186
238
|
message: str,
|
|
187
239
|
hint: str,
|
|
188
240
|
) -> dict[str, str]:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
"
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
241
|
+
code = f"TASK-HYGIENE-{kind.replace('_', '-').upper()}"
|
|
242
|
+
return _issue(
|
|
243
|
+
code=code,
|
|
244
|
+
severity="warning",
|
|
245
|
+
path=task,
|
|
246
|
+
message=message,
|
|
247
|
+
command_hint=hint,
|
|
248
|
+
contract="runtime-health:task-hygiene",
|
|
249
|
+
kind=kind,
|
|
250
|
+
task=task,
|
|
251
|
+
status=status,
|
|
252
|
+
hint=hint,
|
|
253
|
+
)
|
|
196
254
|
|
|
197
255
|
|
|
198
256
|
def _missing_context_files(task_dir: Path) -> tuple[str, ...]:
|
|
@@ -257,6 +315,151 @@ def _print_task_hygiene_issues(issues: list[dict[str, str]]) -> None:
|
|
|
257
315
|
print(f"Hint: {issue['hint']}", file=sys.stderr)
|
|
258
316
|
|
|
259
317
|
|
|
318
|
+
def _diagnostic_path(repo_root: Path, path: Path) -> str:
|
|
319
|
+
try:
|
|
320
|
+
return path.resolve().relative_to(repo_root.resolve()).as_posix()
|
|
321
|
+
except ValueError:
|
|
322
|
+
return str(path)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _state_lock_code(status: str) -> str:
|
|
326
|
+
return f"STATE-RECOVERY-LOCK-{status.upper().replace('_', '-')}"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _state_lock_command_hint(info) -> str:
|
|
330
|
+
if info.status != "recoverable":
|
|
331
|
+
return "Inspect the lock owner facts; do not delete unless the owner PID is missing and the lock is older than the stale threshold."
|
|
332
|
+
return (
|
|
333
|
+
"Use an explicit recovery action only after verification, for example "
|
|
334
|
+
"StateStore().remove_stale_lock(Path(<target>), stale_after_seconds="
|
|
335
|
+
f"{DEFAULT_STALE_LOCK_SECONDS})"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _state_lock_issue(repo_root: Path, info) -> dict[str, object]:
|
|
340
|
+
fields = info.to_dict()
|
|
341
|
+
fields.update(
|
|
342
|
+
{
|
|
343
|
+
"kind": "state_lock",
|
|
344
|
+
"lockPath": _diagnostic_path(repo_root, info.lock_path),
|
|
345
|
+
"target": str(info.target),
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
if info.age_seconds is not None:
|
|
349
|
+
fields["ageSeconds"] = round(info.age_seconds, 3)
|
|
350
|
+
return _issue(
|
|
351
|
+
code=_state_lock_code(info.status),
|
|
352
|
+
severity="warning",
|
|
353
|
+
path=_diagnostic_path(repo_root, info.lock_path),
|
|
354
|
+
message=info.detail,
|
|
355
|
+
command_hint=_state_lock_command_hint(info),
|
|
356
|
+
contract="runtime-health:state-recovery",
|
|
357
|
+
**fields,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _operation_command_hint() -> str:
|
|
362
|
+
return (
|
|
363
|
+
"Inspect the pending operation record; a trusted recovery command may "
|
|
364
|
+
"call UnitOfWork.recover_all(repo_root) without interpreting error "
|
|
365
|
+
"output as instructions."
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _pending_operation_issue(repo_root: Path, fact: dict) -> dict[str, object]:
|
|
370
|
+
error = fact.get("error")
|
|
371
|
+
if fact.get("phase") == "unreadable" and isinstance(error, dict):
|
|
372
|
+
return _issue(
|
|
373
|
+
code="STATE-RECOVERY-OPERATION-UNREADABLE",
|
|
374
|
+
severity="warning",
|
|
375
|
+
path=_diagnostic_path(repo_root, Path(str(fact.get("path") or ""))),
|
|
376
|
+
message=str(error.get("detail") or "operation record is unreadable"),
|
|
377
|
+
command_hint="Inspect the operation record manually; Doctor does not mutate recovery state.",
|
|
378
|
+
contract="runtime-health:state-recovery",
|
|
379
|
+
kind="pending_operation",
|
|
380
|
+
operationId=str(fact.get("operation_id") or ""),
|
|
381
|
+
phase=str(fact.get("phase") or "unreadable"),
|
|
382
|
+
errorCode=str(error.get("code") or ""),
|
|
383
|
+
)
|
|
384
|
+
operation_id = str(fact.get("operation_id") or "")
|
|
385
|
+
phase = str(fact.get("phase") or "unknown")
|
|
386
|
+
record_error = fact.get("error")
|
|
387
|
+
if phase == "conflicted":
|
|
388
|
+
error_detail = ""
|
|
389
|
+
error_code = ""
|
|
390
|
+
if isinstance(record_error, dict):
|
|
391
|
+
error_code = str(record_error.get("code") or "")
|
|
392
|
+
error_detail = str(record_error.get("detail") or "")
|
|
393
|
+
suffix = ": ".join(value for value in (error_code, error_detail) if value)
|
|
394
|
+
message = (
|
|
395
|
+
f"conflicted UnitOfWork operation {operation_id}"
|
|
396
|
+
+ (f": {suffix}" if suffix else "")
|
|
397
|
+
)
|
|
398
|
+
return _issue(
|
|
399
|
+
code="STATE-RECOVERY-CONFLICTED-OPERATION",
|
|
400
|
+
severity="warning",
|
|
401
|
+
path=_diagnostic_path(repo_root, Path(str(fact.get("path") or ""))),
|
|
402
|
+
message=message,
|
|
403
|
+
command_hint=(
|
|
404
|
+
"Resolve the recorded conflict manually; Doctor does not retry "
|
|
405
|
+
"or mutate conflicted operation state."
|
|
406
|
+
),
|
|
407
|
+
contract="runtime-health:state-recovery",
|
|
408
|
+
kind="pending_operation",
|
|
409
|
+
operationId=operation_id,
|
|
410
|
+
operationKind=str(fact.get("kind") or "unknown"),
|
|
411
|
+
phase=phase,
|
|
412
|
+
participantCount=fact.get("participant_count", 0),
|
|
413
|
+
)
|
|
414
|
+
return _issue(
|
|
415
|
+
code="STATE-RECOVERY-PENDING-OPERATION",
|
|
416
|
+
severity="warning",
|
|
417
|
+
path=_diagnostic_path(repo_root, Path(str(fact.get("path") or ""))),
|
|
418
|
+
message=f"pending UnitOfWork operation {operation_id} is in phase {phase}",
|
|
419
|
+
command_hint=_operation_command_hint(),
|
|
420
|
+
contract="runtime-health:state-recovery",
|
|
421
|
+
kind="pending_operation",
|
|
422
|
+
operationId=operation_id,
|
|
423
|
+
operationKind=str(fact.get("kind") or "unknown"),
|
|
424
|
+
phase=phase,
|
|
425
|
+
participantCount=fact.get("participant_count", 0),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def check_state_recovery(repo_root: Path) -> list[dict[str, object]]:
|
|
430
|
+
workflow = repo_root / ".cowork-flow"
|
|
431
|
+
if not workflow.is_dir():
|
|
432
|
+
return []
|
|
433
|
+
store = StateStore()
|
|
434
|
+
issues: list[dict[str, object]] = []
|
|
435
|
+
for lock_path in sorted(workflow.rglob("*.lock")):
|
|
436
|
+
if not lock_path.is_file():
|
|
437
|
+
continue
|
|
438
|
+
info = store.inspect_lock_path(
|
|
439
|
+
lock_path,
|
|
440
|
+
stale_after_seconds=DEFAULT_STALE_LOCK_SECONDS,
|
|
441
|
+
)
|
|
442
|
+
if info.status != "absent":
|
|
443
|
+
issues.append(_state_lock_issue(repo_root, info))
|
|
444
|
+
operation_log = OperationLog(repo_root, state_store=store)
|
|
445
|
+
for fact in operation_log.pending_facts():
|
|
446
|
+
issues.append(_pending_operation_issue(repo_root, fact))
|
|
447
|
+
return issues
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _print_state_recovery_issues(issues: list[dict[str, object]]) -> None:
|
|
451
|
+
for issue in issues:
|
|
452
|
+
print(
|
|
453
|
+
"WARNING: "
|
|
454
|
+
f"{issue['kind']}: {issue['path']}: "
|
|
455
|
+
f"{issue['message']}",
|
|
456
|
+
file=sys.stderr,
|
|
457
|
+
)
|
|
458
|
+
hint = issue.get("commandHint")
|
|
459
|
+
if hint:
|
|
460
|
+
print(f"Hint: {hint}", file=sys.stderr)
|
|
461
|
+
|
|
462
|
+
|
|
260
463
|
def check_runtime(repo_root: Path) -> list[str]:
|
|
261
464
|
errors: list[str] = []
|
|
262
465
|
try:
|
|
@@ -286,22 +489,56 @@ def check_runtime(repo_root: Path) -> list[str]:
|
|
|
286
489
|
return errors
|
|
287
490
|
|
|
288
491
|
|
|
289
|
-
def
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
492
|
+
def _all_check_result(repo_root: Path) -> dict[str, object]:
|
|
493
|
+
host_issues = _host_issues(repo_root)
|
|
494
|
+
runtime_errors = check_runtime(repo_root)
|
|
495
|
+
distribution_errors = check_distribution(repo_root)
|
|
496
|
+
task_hygiene_issues = check_task_hygiene(repo_root)
|
|
497
|
+
state_recovery_issues = check_state_recovery(repo_root)
|
|
498
|
+
errors: list[dict[str, object]] = []
|
|
499
|
+
for issue in host_issues:
|
|
500
|
+
errors.append({"kind": "host_adapter", **issue})
|
|
501
|
+
errors.extend(
|
|
502
|
+
{"kind": "runtime", "message": error}
|
|
503
|
+
for error in runtime_errors
|
|
504
|
+
)
|
|
505
|
+
errors.extend(
|
|
506
|
+
{"kind": "distribution", "message": error}
|
|
507
|
+
for error in distribution_errors
|
|
508
|
+
)
|
|
509
|
+
return {
|
|
510
|
+
"ok": not errors,
|
|
511
|
+
"errors": errors,
|
|
512
|
+
"issues": {
|
|
513
|
+
"hostAdapters": host_issues,
|
|
514
|
+
"taskHygiene": task_hygiene_issues,
|
|
515
|
+
"stateRecovery": state_recovery_issues,
|
|
516
|
+
},
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _run_checks(repo_root: Path, *, structured: bool = False) -> int:
|
|
521
|
+
result = _all_check_result(repo_root)
|
|
522
|
+
errors = result["errors"]
|
|
523
|
+
if structured:
|
|
524
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
525
|
+
return 1 if errors else 0
|
|
526
|
+
_print_task_hygiene_issues(result["issues"]["taskHygiene"])
|
|
527
|
+
_print_state_recovery_issues(result["issues"]["stateRecovery"])
|
|
295
528
|
if errors:
|
|
296
529
|
for error in errors:
|
|
297
|
-
print(f"ERROR: {error}", file=sys.stderr)
|
|
530
|
+
print(f"ERROR: {error['message']}", file=sys.stderr)
|
|
298
531
|
return 1
|
|
299
532
|
print("runtime health checks passed")
|
|
300
533
|
return 0
|
|
301
534
|
|
|
302
535
|
|
|
303
|
-
def _run_host_checks(repo_root: Path) -> int:
|
|
304
|
-
|
|
536
|
+
def _run_host_checks(repo_root: Path, *, structured: bool = False) -> int:
|
|
537
|
+
issues = _host_issues(repo_root)
|
|
538
|
+
if structured:
|
|
539
|
+
print(json.dumps({"issues": issues}, ensure_ascii=False))
|
|
540
|
+
return 1 if issues else 0
|
|
541
|
+
errors = [issue["message"] for issue in issues]
|
|
305
542
|
if errors:
|
|
306
543
|
for error in errors:
|
|
307
544
|
print(f"ERROR: {error}", file=sys.stderr)
|
|
@@ -346,9 +583,9 @@ def main() -> int:
|
|
|
346
583
|
args = build_parser().parse_args()
|
|
347
584
|
repo_root = get_repo_root()
|
|
348
585
|
if args.all:
|
|
349
|
-
return _run_checks(repo_root)
|
|
586
|
+
return _run_checks(repo_root, structured=bool(args.json))
|
|
350
587
|
if args.host_adapters:
|
|
351
|
-
return _run_host_checks(repo_root)
|
|
588
|
+
return _run_host_checks(repo_root, structured=bool(args.json))
|
|
352
589
|
if args.subagent_safety:
|
|
353
590
|
return _run_runtime_checks(repo_root)
|
|
354
591
|
if args.task_hygiene:
|
|
@@ -13,108 +13,151 @@ Before writing a plan, confirm the request has an executable scope, acceptance c
|
|
|
13
13
|
|
|
14
14
|
Read:
|
|
15
15
|
|
|
16
|
-
- Active task decision-anchor.md
|
|
17
|
-
- Relevant
|
|
16
|
+
- Active task `decision-anchor.md`.
|
|
17
|
+
- Relevant decision-anchor, design, guide, or spec files.
|
|
18
18
|
- Relevant `.cowork-flow/spec/` indexes and target specs.
|
|
19
|
+
- `.cowork-flow/spec/guides/index.md` for non-trivial, ambiguous, cross-layer, or reusable-work planning.
|
|
19
20
|
- Files that define the contracts being changed.
|
|
20
21
|
|
|
21
22
|
## Plan Shape
|
|
22
23
|
|
|
23
24
|
Save plans to `.cowork-flow/plans/YYYY-MM-DD-<slug>.md` unless the user asks for another path.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
### Plan Style: Task Briefs, Not Design Docs
|
|
27
|
+
|
|
28
|
+
Plans exist to prevent implementation drift. They must guide implementation by pinning task order, file boundaries, key symbols, the core patch shape, test proof, completion conditions, and prohibited drift. They are not architecture essays and must not explain every possible option.
|
|
29
|
+
|
|
30
|
+
Do not write option-comparison tables unless the user explicitly asks. Do not copy complete implementations into the plan. The plan should expose the core patch shape: where to change, what behavior changes, what key branch or pseudocode should guide the edit, how tests prove it, and which boundaries must not be crossed.
|
|
31
|
+
|
|
32
|
+
Start with a concise brief:
|
|
26
33
|
|
|
27
34
|
```markdown
|
|
28
35
|
# <Feature> Implementation Plan
|
|
29
36
|
|
|
37
|
+
> Execution rule: this plan exists to prevent implementation drift. The implementer must work through tasks in order.
|
|
38
|
+
> If the plan conflicts with current code facts, a plan-external file becomes necessary, or a test fails for an unplanned reason, stop and report back instead of expanding scope.
|
|
39
|
+
>
|
|
30
40
|
> For formal fixed-agent work: create a runtime context with `.cowork-flow/run subagent init`, pass `cowork_runtime_context_id: <runtime_context_id>` through the active Host Adapter, then dispatch `cowork-implement` or `cowork-check`. Close the runtime context after verification.
|
|
31
41
|
|
|
32
|
-
|
|
|
42
|
+
| Field | Content |
|
|
33
43
|
|----|------|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
##
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
| **Goal** | <One sentence describing the observable behavior change> |
|
|
45
|
+
| **Task Type** | Tiny / Normal / High-risk: <reason> |
|
|
46
|
+
| **Strategy** | Serial / Parallel: <why> |
|
|
47
|
+
| **Success Criteria** | AC-001 / AC-002 / AC-003 |
|
|
48
|
+
| **Final Verification** | `<command>` -> exit 0; `<command>` -> exit 0 |
|
|
49
|
+
| **Primary Risk** | Low / Medium / High: <one sentence> |
|
|
50
|
+
|
|
51
|
+
## Goal
|
|
52
|
+
|
|
53
|
+
<1-3 sentences describing the expected observable behavior.>
|
|
54
|
+
|
|
55
|
+
## Acceptance Criteria
|
|
56
|
+
|
|
57
|
+
- AC-001: <observable behavior>
|
|
58
|
+
- AC-002: <observable behavior>
|
|
59
|
+
|
|
60
|
+
## Global Constraints
|
|
61
|
+
|
|
62
|
+
- <Architecture boundary, compatibility requirement, command, or output field that must be preserved>
|
|
63
|
+
- <Explicitly prohibited scope expansion>
|
|
64
|
+
|
|
65
|
+
## File Boundaries
|
|
66
|
+
|
|
67
|
+
- `path/a.py`: <responsibility; only change this function or region>
|
|
68
|
+
- `tests/test_a.py`: <responsibility; only add or modify these tests>
|
|
69
|
+
|
|
70
|
+
## Tasks
|
|
48
71
|
```
|
|
49
72
|
|
|
50
|
-
Then list
|
|
73
|
+
Then list task briefs, followed by integrated verification and completion checks.
|
|
51
74
|
|
|
52
75
|
## Task Rules
|
|
53
76
|
|
|
77
|
+
- Classify the plan before writing tasks:
|
|
78
|
+
- **Tiny**: pure docs, names, comments, or no-behavior cleanup. Use file boundaries, completion conditions, and one verification command.
|
|
79
|
+
- **Normal**: ordinary behavior, test, or workflow guidance changes. Use the full task brief below.
|
|
80
|
+
- **High-risk**: workflow, runtime, template, spec, hook, state-machine, migration, or security-sensitive changes. Use the full task brief plus code-fact sources and explicit drift boundaries.
|
|
54
81
|
- Each task names exact files to create, modify, or test.
|
|
55
|
-
- Each
|
|
56
|
-
-
|
|
82
|
+
- Each task is small enough to execute and verify independently.
|
|
83
|
+
- Every Normal or High-risk task must use this brief format:
|
|
57
84
|
|
|
58
85
|
```markdown
|
|
59
|
-
###
|
|
86
|
+
### Task N: <small goal>
|
|
60
87
|
|
|
61
|
-
|
|
62
|
-
-
|
|
63
|
-
- **验证**: `<命令>` → exit 0(实现前 exit 1 → 实现后 exit 0 | AC-XXX)
|
|
64
|
-
- **回滚**: <一句话或"仅增测试,无回滚成本">
|
|
65
|
-
```
|
|
88
|
+
**Purpose**
|
|
89
|
+
- <Observable behavior after this task is complete>
|
|
66
90
|
|
|
67
|
-
|
|
91
|
+
**Code Fact Sources**
|
|
92
|
+
- Read: `path/a.py::function_name`
|
|
93
|
+
- Read: `tests/test_a.py::test_existing_behavior`
|
|
68
94
|
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
95
|
+
**File Boundaries**
|
|
96
|
+
- Create / Modify / Test: `path/a.py` (only change <function/region>)
|
|
97
|
+
- Test: `tests/test_a.py` (only change <test/region>)
|
|
72
98
|
|
|
73
|
-
|
|
99
|
+
**Key Symbols**
|
|
100
|
+
- Change: `function_name()`, `ClassName.method()`, `<config-key>`
|
|
101
|
+
- Preserve: `existing_helper()`, output field `<field>`
|
|
74
102
|
|
|
75
|
-
|
|
76
|
-
|
|
103
|
+
**Implementation Notes**
|
|
104
|
+
```python
|
|
105
|
+
# Core control-flow sketch; adjust names to match existing code.
|
|
106
|
+
# Do not copy a complete implementation into the plan.
|
|
107
|
+
...
|
|
77
108
|
```
|
|
78
109
|
|
|
79
|
-
|
|
110
|
+
**Test Proof**
|
|
111
|
+
- `tests/test_a.py::test_x`: proves <specific behavior>
|
|
112
|
+
- Core assertion: <one sentence describing the assertion, not only the test name>
|
|
80
113
|
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
- Expected/断言 field is retired. Its responsibility moves into Verify parentheses — describe state changes that exit code alone cannot cover:
|
|
114
|
+
**Verification Command**
|
|
115
|
+
```bash
|
|
116
|
+
<command>
|
|
86
117
|
```
|
|
87
|
-
|
|
118
|
+
|
|
119
|
+
**Completion Conditions**
|
|
120
|
+
- <2-4 checkable conditions>
|
|
121
|
+
|
|
122
|
+
**Prohibited Drift**
|
|
123
|
+
- <2-5 explicit prohibited changes>
|
|
124
|
+
|
|
125
|
+
**Deviation Conditions**
|
|
126
|
+
- If <plan-external file/layer> becomes necessary, must stop and report back because the file boundary is wrong.
|
|
88
127
|
```
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
- Do not add shallow tests just to satisfy process. Avoid tests that only assert existence, mirror implementation details, count
|
|
93
|
-
- For complex problems, test depth first:
|
|
94
|
-
- Map
|
|
95
|
-
- Avoid placeholders such as TODO
|
|
128
|
+
|
|
129
|
+
- For Tiny tasks, keep the brief short but still name exact file boundaries, completion conditions, and verification commands.
|
|
130
|
+
- For behavior changes, include a failing test before implementation when behavior can be tested.
|
|
131
|
+
- Do not add shallow tests just to satisfy process. Avoid tests that only assert existence, mirror implementation details, count mock calls without behavior, or snapshot empty structure.
|
|
132
|
+
- For complex problems, test depth first: business invariants, cross-layer contracts, state transitions, error boundaries, and real regression paths before narrow unit cases.
|
|
133
|
+
- Map behavior-changing tests to stable `decision-anchor.md` acceptance IDs when useful; do not create `tdd.jsonl` or write TDD evidence records into `check.jsonl`.
|
|
134
|
+
- Avoid placeholders such as `TODO`, `TBD`, "handle edge cases", or "write tests".
|
|
96
135
|
- Keep root/template parity explicit when both copies exist.
|
|
136
|
+
- When guide material changes the intended approach, capture the selected conclusion in `decision-anchor.md`, the task brief, or explicit prohibited drift; do not leave it as unstated background knowledge.
|
|
137
|
+
- If a task needs more than about 15 lines of implementation pseudocode, split it or narrow the scope. Plans should guide the edit, not replace the implementation.
|
|
97
138
|
|
|
98
139
|
### Anti-Rationalization - Planning Phase
|
|
99
140
|
|
|
100
141
|
| Agent Rationalization | Rebuttal | Alternative |
|
|
101
142
|
|---|---|---|
|
|
102
|
-
| "This task is too small, let's merge it." | Task granularity is determined by acceptance criteria, not line count. Merge only when AC cannot be split
|
|
103
|
-
| "Write a rough plan first, refine during implementation." | Rough
|
|
104
|
-
| "Skip the plan
|
|
105
|
-
| "This task
|
|
143
|
+
| "This task is too small, let's merge it." | Task granularity is determined by acceptance criteria, not line count. Merge only when AC cannot be split. | Split until each step has a distinct AC; keep steps independent even when short. |
|
|
144
|
+
| "Write a rough plan first, refine during implementation." | Rough plans do not constrain implementation. Unconstrained implementation invites scope creep and rework. | Use `decision-anchor.md` to derive acceptance criteria, then write task briefs with file boundaries, key symbols, implementation notes, test proof, completion conditions, and prohibited drift. |
|
|
145
|
+
| "Skip the plan since we already have decision-anchor." | `decision-anchor.md` defines what to do; the plan defines how to do it and in what order. Without a plan, dependency mistakes surface mid-implementation and increase rework. | Derive ACs from `decision-anchor.md`, then map each AC to concrete files and verification commands. |
|
|
146
|
+
| "This task does not need a verification command." | No verification means no completion signal. Every task needs an executable verification command. | Add at least one automated check, such as typecheck, test, lint, or build. |
|
|
147
|
+
| "The plan should explore every alternative." | Heavy option analysis slows routine implementation and hides the path to execute. | State the chosen path and drift boundaries. Add option analysis only when the user asks or the decision is genuinely architectural. |
|
|
106
148
|
|
|
107
149
|
## Plan Approval Gate
|
|
108
150
|
|
|
109
|
-
A plan can be marked
|
|
110
|
-
|
|
111
|
-
- Every task
|
|
112
|
-
-
|
|
151
|
+
A plan can be marked approved only when all conditions hold:
|
|
152
|
+
|
|
153
|
+
- Every task in `implement.jsonl` has clear acceptance criteria.
|
|
154
|
+
- Every task has implementation steps and verification commands.
|
|
155
|
+
- Task dependencies are explicit, including whether work is serial or parallelizable.
|
|
113
156
|
|
|
114
157
|
## Post-Approval Options
|
|
115
158
|
|
|
116
|
-
- **Step-by-task progression** (default path): user runs `task next <task-dir> --run` for the current safe action, completes the task, then runs `task next <next-task-dir> --run` for the next task.
|
|
117
|
-
- **Batch mode**: triggered when the user explicitly says "auto" or "batch"
|
|
159
|
+
- **Step-by-task progression** (default path): the user runs `task next <task-dir> --run` for the current safe action, completes the task, then runs `task next <next-task-dir> --run` for the next task.
|
|
160
|
+
- **Batch mode**: triggered only when the user explicitly says "auto" or "batch"; see `skills/batch-execution/SKILL.md`. Each task still must be verified independently.
|
|
118
161
|
|
|
119
162
|
> **Default is step-by-task progression.** Do not enter batch mode unless the user explicitly says "auto".
|
|
120
163
|
|
|
@@ -137,7 +180,8 @@ Execution strategy guide:
|
|
|
137
180
|
|
|
138
181
|
Before handoff:
|
|
139
182
|
|
|
140
|
-
1. Confirm every decision-anchor.md acceptance criterion maps to a
|
|
183
|
+
1. Confirm every `decision-anchor.md` acceptance criterion maps to a task.
|
|
141
184
|
2. Search the plan for placeholders.
|
|
142
|
-
3.
|
|
143
|
-
4.
|
|
185
|
+
3. Confirm every Normal or High-risk task has file boundaries, key symbols, implementation notes, test proof, completion conditions, and prohibited drift.
|
|
186
|
+
4. Check names, paths, command syntax, and expected outputs.
|
|
187
|
+
5. Confirm the plan says to stop and report back when code facts conflict with the plan, a plan-external file becomes necessary, or a test fails for an unplanned reason.
|
|
@@ -12,7 +12,7 @@ Use this Skill during the check phase, before the `task next --run` completion a
|
|
|
12
12
|
- Review early enough that fixes are cheap; do not wait until archive/commit to discover blockers.
|
|
13
13
|
- Review the current diff and exact task context, not broad conversation memory.
|
|
14
14
|
- Separate machine-decidable lifecycle facts from human judgment: lifecycle blockers cover state/scope facts; review judgment explains spec, quality, and risk findings.
|
|
15
|
-
- Treat backend/frontend
|
|
15
|
+
- Treat backend/frontend natural-language markdown as user-defined review requirements, not dynamic hard validators.
|
|
16
16
|
- Prefer fresh verification from the current checkout over stale prior output.
|
|
17
17
|
- Do not create task-local review artifact files. The review result and command output are the evidence.
|
|
18
18
|
- Use advisory helper output only as review input; it must not become a second lifecycle gate.
|
|
@@ -28,16 +28,16 @@ Read only what is needed for the current task:
|
|
|
28
28
|
2. The linked implementation plan, if present.
|
|
29
29
|
3. The check context index (`<task>/check.jsonl`) and each referenced file, if present. Treat it as context only; do not write review conclusions or readiness evidence to it.
|
|
30
30
|
4. Current `git diff` / `git status --short`.
|
|
31
|
-
5. Relevant `.cowork-flow/spec/backend
|
|
31
|
+
5. Relevant `.cowork-flow/spec/backend/` and `.cowork-flow/spec/frontend/` files selected by changed paths and task scope.
|
|
32
32
|
6. Advisory facts from `review-check <task-dir> --json`, when useful.
|
|
33
|
-
7. Lifecycle blocker output, if any, from `task next`, `task
|
|
33
|
+
7. Lifecycle blocker output, if any, from `task next --json`, `task next <dir> --validate`, or `task next <dir> --run --intent review`.
|
|
34
34
|
|
|
35
35
|
## Task Review Checklist
|
|
36
36
|
|
|
37
37
|
- **Scope**: every changed file is planned or justified; no unrelated cleanup sneaks in.
|
|
38
38
|
- **Behavior**: acceptance criteria are satisfied through observable behavior, not implementation-shaped assertions.
|
|
39
39
|
- **Tests**: test intent is explicit; tests fail for meaningful regressions, reject shallow tests such as existence/mock/snapshot-only checks, and cover boundary/error paths when relevant.
|
|
40
|
-
- **User specs**: every applicable backend/frontend
|
|
40
|
+
- **User specs**: every applicable backend/frontend requirement is marked `pass`, `finding`, `not_applicable`, or `needs_user_judgment`.
|
|
41
41
|
- **Specs**: project specs are updated when behavior/contracts changed, or the review states why no spec update is needed.
|
|
42
42
|
- **Code quality**: naming, layering, error handling, state boundaries, security-sensitive paths, and complexity are reviewed against applicable user specs.
|
|
43
43
|
- **Lifecycle blockers**: state/scope blockers are fixed before acceptance; review does not invent hard blockers for natural-language specs.
|