cowork-flow 0.0.37 → 0.0.39

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 (111) hide show
  1. package/README.md +27 -14
  2. package/package.json +1 -1
  3. package/src/commands/init.js +39 -28
  4. package/src/commands/sync.js +2 -2
  5. package/src/lib/asset-plan.js +127 -0
  6. package/src/lib/copy-template.js +101 -141
  7. package/src/lib/host-assets.js +231 -0
  8. package/src/lib/plan-applier.js +329 -0
  9. package/src/lib/platforms.js +15 -76
  10. package/src/lib/skill-registry.js +503 -0
  11. package/template/.claude/agents/cowork-check.md +10 -1
  12. package/template/.claude/agents/cowork-implement.md +11 -2
  13. package/template/.claude/hooks/inject-workflow-state.py +51 -254
  14. package/template/.codex/agents/cowork-check.toml +12 -3
  15. package/template/.codex/agents/cowork-implement.toml +11 -2
  16. package/template/.codex/agents/default.toml +3 -2
  17. package/template/.codex/agents/explorer.toml +3 -2
  18. package/template/.codex/agents/worker.toml +2 -1
  19. package/template/.codex/hooks/inject-workflow-state.py +46 -265
  20. package/template/.cowork-flow/.version +1 -1
  21. package/template/.cowork-flow/scripts/application/__init__.py +1 -0
  22. package/template/.cowork-flow/scripts/application/batch_execution.py +743 -0
  23. package/template/.cowork-flow/scripts/application/runtime_context_service.py +365 -0
  24. package/template/.cowork-flow/scripts/application/task_archive.py +220 -0
  25. package/template/.cowork-flow/scripts/application/task_context.py +765 -0
  26. package/template/.cowork-flow/scripts/application/task_creation.py +201 -0
  27. package/template/.cowork-flow/scripts/application/task_lifecycle.py +532 -0
  28. package/template/.cowork-flow/scripts/application/task_tree.py +179 -0
  29. package/template/.cowork-flow/scripts/commands/doctor.py +78 -57
  30. package/template/.cowork-flow/scripts/commands/party_mode_v2.py +671 -425
  31. package/template/.cowork-flow/scripts/commands/subagent.py +38 -26
  32. package/template/.cowork-flow/scripts/commands/task.py +535 -1953
  33. package/template/.cowork-flow/scripts/commands/task_archive_commands.py +195 -0
  34. package/template/.cowork-flow/scripts/commands/task_context_commands.py +240 -0
  35. package/template/.cowork-flow/scripts/commands/task_create_command.py +164 -0
  36. package/template/.cowork-flow/scripts/commands/task_navigation.py +407 -0
  37. package/template/.cowork-flow/scripts/commands/task_parser.py +364 -0
  38. package/template/.cowork-flow/scripts/commands/task_support.py +76 -0
  39. package/template/.cowork-flow/scripts/commands/task_tree_commands.py +160 -0
  40. package/template/.cowork-flow/scripts/common/core/execution_context.py +161 -85
  41. package/template/.cowork-flow/scripts/common/core/host_manifest.py +393 -0
  42. package/template/.cowork-flow/scripts/common/core/skill_registry.py +568 -0
  43. package/template/.cowork-flow/scripts/common/gates/coding_standards.py +58 -7
  44. package/template/.cowork-flow/scripts/common/gates/gates.py +323 -46
  45. package/template/.cowork-flow/scripts/common/gates/models.py +196 -0
  46. package/template/.cowork-flow/scripts/common/gates/registry.py +79 -0
  47. package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +13 -13
  48. package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +104 -62
  49. package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +39 -22
  50. package/template/.cowork-flow/scripts/common/gates/validate_rules.py +223 -40
  51. package/template/.cowork-flow/scripts/common/git/git_context.py +216 -182
  52. package/template/.cowork-flow/scripts/common/host/__init__.py +1 -0
  53. package/template/.cowork-flow/scripts/common/host/workflow_state_hook.py +303 -0
  54. package/template/.cowork-flow/scripts/common/storage/__init__.py +1 -0
  55. package/template/.cowork-flow/scripts/common/storage/operation_log.py +72 -0
  56. package/template/.cowork-flow/scripts/common/storage/state_store.py +279 -0
  57. package/template/.cowork-flow/scripts/common/storage/unit_of_work.py +214 -0
  58. package/template/.cowork-flow/scripts/common/task/active_task.py +93 -135
  59. package/template/.cowork-flow/scripts/common/task/batch_mode.py +33 -120
  60. package/template/.cowork-flow/scripts/common/task/decision_review.py +85 -0
  61. package/template/.cowork-flow/scripts/common/task/readiness.py +9 -15
  62. package/template/.cowork-flow/scripts/common/task/task_repository.py +156 -0
  63. package/template/.cowork-flow/scripts/run.py +24 -0
  64. package/template/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
  65. package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
  66. package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
  67. package/template/.cowork-flow/spec/protocols/decision-review.md +28 -0
  68. package/template/.cowork-flow/spec/protocols/review.md +34 -0
  69. package/template/.cowork-flow/spec/protocols/spec-maintenance.md +16 -0
  70. package/template/.cowork-flow/spec/protocols/tdd.md +30 -0
  71. package/template/.cowork-flow/spec/references/definition-of-done.md +4 -4
  72. package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
  73. package/template/.cowork-flow/spec/runtime/contract-registry.json +25 -0
  74. package/template/.cowork-flow/spec/runtime/host-assets.json +214 -0
  75. package/template/.cowork-flow/spec/runtime/index.md +8 -0
  76. package/template/.cowork-flow/spec/runtime/rules.json +61 -10
  77. package/template/.cowork-flow/spec/runtime/skill-registry.json +397 -0
  78. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +173 -0
  79. package/template/.cowork-flow/spec/schemas/rules.schema.json +10 -1
  80. package/template/.cowork-flow/spec/schemas/skill-registry.schema.json +202 -0
  81. package/template/.cowork-flow/workflow.md +18 -13
  82. package/template/.opencode/agents/cowork-check.md +11 -0
  83. package/template/.opencode/agents/cowork-implement.md +16 -1
  84. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  85. package/template/.zcode/hooks/inject-context.js +3 -85
  86. package/template/.zcode/hooks/inject-context.selfcheck.mjs +6 -6
  87. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
  88. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
  89. package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +61 -10
  90. package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +10 -1
  91. package/template/.zcode/scaffold/.cowork-flow/workflow.md +18 -13
  92. package/template/.zcode/scaffold/AGENTS.md +18 -8
  93. package/template/AGENTS.md +1 -1
  94. package/template/skills/batch-mode/SKILL.md +140 -76
  95. package/template/skills/brainstorming/SKILL.md +2 -2
  96. package/template/skills/cowork-flow/SKILL.md +42 -0
  97. package/template/skills/party-mode/SHARED-BOUNDARIES.md +1 -1
  98. package/template/skills/party-mode/SKILL.md +48 -119
  99. package/template/.cowork-flow/scripts/common/gates/spec_validator.py +0 -53
  100. package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +0 -32
  101. package/template/skills/before-dev/SKILL.md +0 -41
  102. package/template/skills/check/SKILL.md +0 -53
  103. package/template/skills/check/simplification-guide.md +0 -47
  104. package/template/skills/continue/SKILL.md +0 -29
  105. package/template/skills/doubt-review/SKILL.md +0 -118
  106. package/template/skills/finish-work/SKILL.md +0 -32
  107. package/template/skills/party-mode-v2/SKILL.md +0 -75
  108. package/template/skills/start/SKILL.md +0 -78
  109. package/template/skills/tdd/SKILL.md +0 -74
  110. package/template/skills/update-spec/SKILL.md +0 -39
  111. package/template/skills/using-cowork-flow/SKILL.md +0 -80
@@ -9,8 +9,12 @@ from __future__ import annotations
9
9
 
10
10
  import json
11
11
  import sys
12
+ from collections.abc import Callable, Mapping
12
13
  from datetime import datetime, timezone
13
14
  from pathlib import Path
15
+ from typing import Optional
16
+
17
+ from common.task.decision_review import validate_decision_review_file
14
18
 
15
19
 
16
20
  RULES_RELATIVE_PATH = ".cowork-flow/spec/runtime/rules.json"
@@ -24,6 +28,7 @@ REQUIRED_RULE_FIELDS = (
24
28
  "fix_hint",
25
29
  "source_file",
26
30
  "enforcement",
31
+ "validator",
27
32
  )
28
33
  ENUM_RULE_FIELDS = {
29
34
  "type": {"phase_gate", "forbidden_action"},
@@ -39,6 +44,16 @@ ENUM_RULE_FIELDS = {
39
44
  }
40
45
 
41
46
 
47
+ class RuleParameterError(ValueError):
48
+ """Raised when runtime rule validator parameters are invalid."""
49
+
50
+
51
+ RuntimeRuleValidator = Callable[
52
+ [dict, Path, Optional[Path], Mapping[str, object]],
53
+ Optional[dict],
54
+ ]
55
+
56
+
42
57
  def runtime_rules_path(repo_root: Path) -> Path:
43
58
  """Return the runtime rules file path for a repository."""
44
59
  return repo_root / ".cowork-flow" / "spec" / "runtime" / "rules.json"
@@ -169,6 +184,14 @@ def _validate_runtime_rule_metadata(rules: list[object], rules_path: Path) -> li
169
184
  )
170
185
  )
171
186
 
187
+ if not isinstance(raw_rule.get("parameters"), dict):
188
+ violations.append(
189
+ _rule_schema_violation(
190
+ rules_path,
191
+ f"{label} must define parameters as an object",
192
+ )
193
+ )
194
+
172
195
  if not (
173
196
  _is_nonempty_string(raw_rule.get("source_anchor"))
174
197
  or _is_nonempty_string(raw_rule.get("source_excerpt"))
@@ -246,13 +269,14 @@ def validate_rules(
246
269
  # Filter rules by scope
247
270
  applicable_rules = [
248
271
  r for r in rules
249
- if r["scope"] == scope or r["scope"] == "all"
272
+ if r["enforcement"] == "validate_rules"
273
+ and (r["scope"] == scope or r["scope"] == "all")
250
274
  ]
251
275
 
252
276
  # Validate each rule
253
277
  violations = []
254
278
  for rule in applicable_rules:
255
- violation = _validate_rule(rule, repo_root, task_dir, scope)
279
+ violation = _validate_rule(rule, repo_root, task_dir)
256
280
  if violation:
257
281
  violations.append(violation)
258
282
 
@@ -263,55 +287,192 @@ def _validate_rule(
263
287
  rule: dict,
264
288
  repo_root: Path,
265
289
  task_dir: Path | None,
266
- scope: str,
267
290
  ) -> dict | None:
268
- """Validate a single rule. Returns violation dict if rule fails."""
269
- if rule["type"] == "phase_gate":
270
- return _validate_phase_gate(rule, repo_root, task_dir, scope)
271
- return None
291
+ """Validate one rule through its registered validator capability."""
292
+ validator_key = rule["validator"]
293
+ validator = RUNTIME_RULE_VALIDATORS.get(validator_key)
294
+ if validator is None:
295
+ return config_violation(
296
+ "RULES-CONFIG-005",
297
+ (
298
+ f"Runtime workflow rule {rule['id']} references unknown "
299
+ f"validator: {validator_key}"
300
+ ),
301
+ runtime_rules_path(repo_root),
302
+ "Register the validator capability or fix the rule validator key.",
303
+ )
304
+
305
+ try:
306
+ return validator(rule, repo_root, task_dir, rule["parameters"])
307
+ except RuleParameterError as error:
308
+ return config_violation(
309
+ "RULES-CONFIG-006",
310
+ (
311
+ f"Runtime workflow rule {rule['id']} has invalid parameters "
312
+ f"for {validator_key}: {error}"
313
+ ),
314
+ runtime_rules_path(repo_root),
315
+ "Fix the validator parameters in the runtime rules file.",
316
+ )
317
+
318
+
319
+ def _reject_unknown_parameters(
320
+ parameters: Mapping[str, object],
321
+ allowed: frozenset[str],
322
+ ) -> None:
323
+ unknown = sorted(set(parameters) - allowed)
324
+ if unknown:
325
+ raise RuleParameterError(
326
+ f"unexpected parameters: {', '.join(unknown)}"
327
+ )
328
+
329
+
330
+ def _string_parameter(
331
+ parameters: Mapping[str, object],
332
+ name: str,
333
+ ) -> str:
334
+ value = parameters.get(name)
335
+ if not _is_nonempty_string(value):
336
+ raise RuleParameterError(f"{name} must be a non-empty string")
337
+ return str(value)
338
+
339
+
340
+ def _string_list_parameter(
341
+ parameters: Mapping[str, object],
342
+ name: str,
343
+ ) -> list[str]:
344
+ value = parameters.get(name)
345
+ if not isinstance(value, list) or not value:
346
+ raise RuleParameterError(f"{name} must be a non-empty string array")
347
+ if not all(_is_nonempty_string(item) for item in value):
348
+ raise RuleParameterError(f"{name} must contain only non-empty strings")
349
+ return [str(item) for item in value]
350
+
351
+
352
+ def _validate_l2_required_file(
353
+ rule: dict,
354
+ repo_root: Path,
355
+ task_dir: Path | None,
356
+ parameters: Mapping[str, object],
357
+ ) -> dict | None:
358
+ _reject_unknown_parameters(parameters, frozenset({"filename"}))
359
+ filename = _string_parameter(parameters, "filename")
360
+ if task_dir is None:
361
+ return None
362
+ change_dir = _find_linked_change(repo_root, task_dir)
363
+ if change_dir is None:
364
+ return None
365
+ return _check_file_exists(change_dir / filename, rule, filename)
272
366
 
273
367
 
274
- def _validate_phase_gate(
368
+ def _validate_l2_plan_link(
275
369
  rule: dict,
276
370
  repo_root: Path,
277
371
  task_dir: Path | None,
278
- scope: str,
372
+ parameters: Mapping[str, object],
279
373
  ) -> dict | None:
280
- """Validate a phase_gate rule."""
281
- rule_id = rule["id"]
374
+ _reject_unknown_parameters(parameters, frozenset())
375
+ if task_dir is None:
376
+ return None
377
+ change_dir = _find_linked_change(repo_root, task_dir)
378
+ if change_dir is None:
379
+ return None
380
+ return _check_plan_link(repo_root, change_dir, rule)
282
381
 
283
- # R-WF-001 to R-WF-005: L2 readiness checks
284
- if rule_id in ["R-WF-001", "R-WF-002", "R-WF-003", "R-WF-004", "R-WF-005"]:
285
- if task_dir is None:
286
- return None
287
382
 
288
- # Check if task has L2 change
289
- change_dir = _find_linked_change(repo_root, task_dir)
290
- if change_dir is None:
291
- return None
383
+ def _validate_l2_task_link(
384
+ rule: dict,
385
+ repo_root: Path,
386
+ task_dir: Path | None,
387
+ parameters: Mapping[str, object],
388
+ ) -> dict | None:
389
+ _reject_unknown_parameters(parameters, frozenset())
390
+ if task_dir is None:
391
+ return None
392
+ change_dir = _find_linked_change(repo_root, task_dir)
393
+ if change_dir is None:
394
+ return None
395
+ return _check_task_link(change_dir, task_dir, rule)
292
396
 
293
- # Validate specific file requirements
294
- if rule_id == "R-WF-001":
295
- return _check_file_exists(change_dir / "proposal.md", rule, "proposal.md")
296
- elif rule_id == "R-WF-002":
297
- return _check_file_exists(change_dir / "spec.md", rule, "spec.md")
298
- elif rule_id == "R-WF-003":
299
- return _check_file_exists(change_dir / "design.md", rule, "design.md")
300
- elif rule_id == "R-WF-004":
301
- return _check_plan_link(repo_root, change_dir, rule)
302
- elif rule_id == "R-WF-005":
303
- return _check_task_link(change_dir, task_dir, rule)
304
-
305
- # R-WF-007: Task completion without check
306
- if rule_id == "R-WF-007":
307
- if scope == "task_complete" and task_dir:
308
- task_json = task_dir / "task.json"
309
- if task_json.exists():
310
- with open(task_json, encoding="utf-8") as f:
311
- task_data = json.load(f)
312
- if task_data.get("status") not in ("review",):
313
- return rule_violation(rule, task_json)
314
397
 
398
+ def _validate_l2_decision_review(
399
+ rule: dict,
400
+ repo_root: Path,
401
+ task_dir: Path | None,
402
+ parameters: Mapping[str, object],
403
+ ) -> dict | None:
404
+ _reject_unknown_parameters(parameters, frozenset({"filename"}))
405
+ filename = _string_parameter(parameters, "filename")
406
+ if task_dir is None:
407
+ return None
408
+ change_dir = _find_linked_change(repo_root, task_dir)
409
+ if change_dir is None or _read_change_level(change_dir) != "L2":
410
+ return None
411
+ evidence_path = task_dir / filename
412
+ issues = validate_decision_review_file(evidence_path)
413
+ if issues:
414
+ return rule_violation(rule, evidence_path, detail=issues[0])
415
+ return None
416
+
417
+
418
+ def _validate_task_status(
419
+ rule: dict,
420
+ repo_root: Path,
421
+ task_dir: Path | None,
422
+ parameters: Mapping[str, object],
423
+ ) -> dict | None:
424
+ del repo_root
425
+ _reject_unknown_parameters(parameters, frozenset({"allowed_statuses"}))
426
+ allowed_statuses = _string_list_parameter(parameters, "allowed_statuses")
427
+ if task_dir is None:
428
+ return None
429
+ task_json = task_dir / "task.json"
430
+ if not task_json.exists():
431
+ return None
432
+ with open(task_json, encoding="utf-8") as f:
433
+ task_data = json.load(f)
434
+ if task_data.get("status") not in allowed_statuses:
435
+ return rule_violation(rule, task_json)
436
+ return None
437
+
438
+
439
+ def _validate_decision_anchor(
440
+ rule: dict,
441
+ repo_root: Path,
442
+ task_dir: Path | None,
443
+ parameters: Mapping[str, object],
444
+ ) -> dict | None:
445
+ del repo_root
446
+ _reject_unknown_parameters(parameters, frozenset({"required_sections"}))
447
+ required_sections = _string_list_parameter(
448
+ parameters,
449
+ "required_sections",
450
+ )
451
+ if task_dir is None:
452
+ return None
453
+ anchor_path = task_dir / "decision-anchor.md"
454
+ if not anchor_path.exists():
455
+ return rule_violation(
456
+ rule,
457
+ anchor_path,
458
+ detail="decision-anchor.md is missing",
459
+ )
460
+ with open(anchor_path, encoding="utf-8") as f:
461
+ content = f.read()
462
+ missing_sections = [
463
+ section
464
+ for section in required_sections
465
+ if f"## {section}" not in content
466
+ ]
467
+ if missing_sections:
468
+ return rule_violation(
469
+ rule,
470
+ anchor_path,
471
+ detail=(
472
+ "decision-anchor.md is missing sections: "
473
+ + ", ".join(missing_sections)
474
+ ),
475
+ )
315
476
  return None
316
477
 
317
478
 
@@ -334,6 +495,18 @@ def _find_linked_change(repo_root: Path, task_dir: Path) -> Path | None:
334
495
  return None
335
496
 
336
497
 
498
+ def _read_change_level(change_dir: Path) -> str | None:
499
+ change_yaml = change_dir / "change.yaml"
500
+ if not change_yaml.is_file():
501
+ return None
502
+ with change_yaml.open("r", encoding="utf-8") as stream:
503
+ for raw_line in stream:
504
+ line = raw_line.strip()
505
+ if line.startswith("level:"):
506
+ return line.split(":", 1)[1].strip()
507
+ return None
508
+
509
+
337
510
  def _metadata_has_link(content: str, keys: tuple[str, ...]) -> bool:
338
511
  for raw_line in content.splitlines():
339
512
  line = raw_line.strip()
@@ -408,6 +581,16 @@ def _check_task_link(
408
581
  return rule_violation(rule, change_yaml, detail="task link is missing")
409
582
 
410
583
 
584
+ RUNTIME_RULE_VALIDATORS: dict[str, RuntimeRuleValidator] = {
585
+ "runtime.l2_required_file": _validate_l2_required_file,
586
+ "runtime.l2_plan_link": _validate_l2_plan_link,
587
+ "runtime.l2_task_link": _validate_l2_task_link,
588
+ "runtime.l2_decision_review": _validate_l2_decision_review,
589
+ "runtime.task_status": _validate_task_status,
590
+ "runtime.decision_anchor": _validate_decision_anchor,
591
+ }
592
+
593
+
411
594
  def log_violations(
412
595
  violations: list[dict],
413
596
  scope: str,