cowork-flow 1.9.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/docs/architecture.md +1 -34
  5. package/docs/direction.md +4 -3
  6. package/docs/hosts.md +14 -10
  7. package/docs/index.md +1 -1
  8. package/docs/release.md +1 -1
  9. package/package.json +6 -6
  10. package/presets/claude-code/.claude-plugin/plugin.json +1 -1
  11. package/presets/codex/.codex-plugin/plugin.json +1 -1
  12. package/presets/dsh/plugins/workflow-state.js +72 -24
  13. package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
  14. package/presets/kimi-code/hooks/cowork-flow-inject.mjs +28 -2
  15. package/presets/opencode/cowork-flow/plugin-core.js +70 -88
  16. package/presets/opencode/plugins/cowork-flow.js +7 -9
  17. package/presets/qoder/.qoder-plugin/plugin.json +1 -1
  18. package/presets/qoder/hooks/hooks.json +1 -1
  19. package/presets/qoder/hooks/inject-context.mjs +28 -2
  20. package/presets/zcode/.zcode-plugin/plugin.json +1 -1
  21. package/presets/zcode/hooks/inject-context.mjs +28 -3
  22. package/src/commands/host.js +23 -2
  23. package/src/commands/install-dsh-hook.js +1 -6
  24. package/src/commands/install-dsh-preset.js +1 -6
  25. package/src/commands/install-zcode-plugin.js +2 -3
  26. package/src/commands/registry.js +4 -2
  27. package/src/lib/asset-plan.js +1 -1
  28. package/src/lib/dsh-home.js +6 -0
  29. package/src/lib/plan-applier.js +1 -6
  30. package/template/.claude/agents/cowork-check.md +1 -1
  31. package/template/.claude/agents/cowork-implement.md +1 -1
  32. package/template/.claude/agents/cowork-research.md +1 -1
  33. package/template/.codex/agents/cowork-check.toml +1 -1
  34. package/template/.codex/agents/cowork-implement.toml +1 -1
  35. package/template/.codex/agents/cowork-research.toml +1 -1
  36. package/template/.codex/agents/default.toml +1 -1
  37. package/template/.codex/agents/explorer.toml +1 -1
  38. package/template/.codex/agents/worker.toml +1 -1
  39. package/template/.cowork-flow/.version +1 -1
  40. package/template/.cowork-flow/adapters/claude-code/adapter.yaml +1 -1
  41. package/template/.cowork-flow/adapters/codex/adapter.yaml +1 -1
  42. package/template/.cowork-flow/adapters/dsh/adapter.yaml +1 -1
  43. package/template/.cowork-flow/adapters/opencode/adapter.yaml +1 -1
  44. package/template/.cowork-flow/adapters/qoder/adapter.yaml +1 -1
  45. package/template/.cowork-flow/run +2 -2
  46. package/template/.cowork-flow/run.cmd +3 -3
  47. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +0 -1
  48. package/template/.cowork-flow/scripts/adapters/cli/task_parser.py +0 -7
  49. package/template/.cowork-flow/scripts/adapters/host/claude_code_policy.py +6 -4
  50. package/template/.cowork-flow/scripts/adapters/host/codex_policy.py +8 -6
  51. package/template/.cowork-flow/scripts/adapters/host/edit_scope.py +113 -0
  52. package/template/.cowork-flow/scripts/adapters/host/opencode_policy.py +12 -0
  53. package/template/.cowork-flow/scripts/adapters/host/qoder_policy.py +7 -5
  54. package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +0 -91
  55. package/template/.cowork-flow/scripts/adapters/host/zcode_policy.py +15 -77
  56. package/template/.cowork-flow/scripts/run.py +0 -2
  57. package/template/.cowork-flow/scripts/services/context_paths.py +2 -2
  58. package/template/.cowork-flow/scripts/services/fact_view.py +49 -21
  59. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +1 -216
  60. package/template/.cowork-flow/scripts/services/lifecycle_policy.py +0 -5
  61. package/template/.cowork-flow/scripts/services/task_gate_profiles.py +1 -1
  62. package/template/.cowork-flow/scripts/services/task_lifecycle.py +14 -31
  63. package/template/.cowork-flow/spec/backend/error-handling.md +0 -14
  64. package/template/.cowork-flow/spec/contracts/context-injection.md +53 -26
  65. package/template/.cowork-flow/spec/contracts/decision-anchor.md +3 -3
  66. package/template/.cowork-flow/spec/contracts/error-codes.md +1 -3
  67. package/template/.cowork-flow/spec/contracts/evidence-completion.md +6 -8
  68. package/template/.cowork-flow/spec/contracts/index.md +0 -1
  69. package/template/.cowork-flow/spec/contracts/plan-binding.md +3 -2
  70. package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +6 -21
  71. package/template/.cowork-flow/spec/contracts/task-adopt.md +1 -1
  72. package/template/.cowork-flow/spec/runtime/contract-registry.json +0 -13
  73. package/template/.cowork-flow/spec/runtime/host-assets.json +10 -7
  74. package/template/.cowork-flow/spec/runtime/scope-rules.json +2 -2
  75. package/template/.kimi-code/agents/cowork-check.md +1 -1
  76. package/template/.kimi-code/agents/cowork-implement.md +1 -1
  77. package/template/.kimi-code/agents/cowork-research.md +1 -1
  78. package/template/.opencode/agents/cowork-check.md +1 -1
  79. package/template/.opencode/agents/cowork-implement.md +1 -1
  80. package/template/.opencode/agents/cowork-research.md +1 -1
  81. package/template/.opencode/cowork-flow/plugin-core.js +70 -88
  82. package/template/.opencode/plugins/cowork-flow.js +7 -9
  83. package/template/AGENTS.md +0 -1
  84. package/template/skills/agent-dispatch/SKILL.md +0 -15
  85. package/template/skills/runtime-health/scripts/doctor.py +83 -58
  86. package/template/skills/spec-sync/SKILL.md +0 -1
  87. package/template/skills/task-planning/SKILL.md +1 -1
  88. package/template/skills/task-review/SKILL.md +3 -3
  89. package/template/.cowork-flow/scripts/adapters/cli/spec_check.py +0 -178
  90. package/template/.cowork-flow/scripts/services/spec_check.py +0 -573
  91. package/template/.cowork-flow/spec/contracts/spec-checks.md +0 -281
@@ -672,52 +672,6 @@ def check_runtime(repo_root: Path) -> list[str]:
672
672
  return errors
673
673
 
674
674
 
675
- def check_spec_checks(repo_root: Path) -> list[dict[str, str]]:
676
- """Spec-check declaration health: parse errors and command entry
677
- existence. Execution belongs to `run spec-check`; doctor only proves
678
- the declarations can run, before a completion gate finds out they
679
- cannot."""
680
- issues: list[dict[str, str]] = []
681
- import shutil
682
-
683
- try:
684
- from services.spec_check import collect_spec_declarations
685
- except Exception as error:
686
- return [
687
- {
688
- "kind": "spec-checks",
689
- "spec": "",
690
- "message": f"spec-check service unavailable: {error}",
691
- }
692
- ]
693
- for spec_decls in collect_spec_declarations(repo_root):
694
- for message in spec_decls.errors:
695
- issues.append(
696
- {"kind": "spec-checks", "spec": spec_decls.spec, "message": message}
697
- )
698
- for decl in spec_decls.decls:
699
- entry = (decl.cmd_win if sys.platform == "win32" else decl.cmd) or decl.cmd
700
- first_token = entry.split()[0] if entry.split() else ""
701
- if not first_token:
702
- issues.append(
703
- {
704
- "kind": "spec-checks",
705
- "spec": spec_decls.spec,
706
- "message": f"empty command declaration: {decl.cmd!r}",
707
- }
708
- )
709
- continue
710
- if shutil.which(first_token) is None:
711
- issues.append(
712
- {
713
- "kind": "spec-checks",
714
- "spec": spec_decls.spec,
715
- "message": f"command entry not found: {first_token}",
716
- }
717
- )
718
- return issues
719
-
720
-
721
675
  def _global_mcp_registered() -> bool:
722
676
  """Tolerant probe of user-level host configs for a cowork-flow MCP
723
677
  entry. Read-only; missing files simply mean "not registered"."""
@@ -1098,6 +1052,82 @@ def check_qoder_plugin(repo_root: Path) -> list[dict[str, str]]:
1098
1052
  return []
1099
1053
 
1100
1054
 
1055
+ _ZCODE_PLUGIN_CONTRACT = "runtime-health:zcode-plugin"
1056
+
1057
+
1058
+ def _zcode_warning(code: str, path: Path, message: str, hint: str) -> list[dict[str, str]]:
1059
+ return [
1060
+ _issue(
1061
+ code=code,
1062
+ severity="warning",
1063
+ path=str(path),
1064
+ message=message,
1065
+ command_hint=hint,
1066
+ contract=_ZCODE_PLUGIN_CONTRACT,
1067
+ )
1068
+ ]
1069
+
1070
+
1071
+ def _zcode_marketplace_path() -> Path:
1072
+ home = Path(os.environ.get("ZCODE_HOME") or (Path.home() / ".zcode"))
1073
+ return (
1074
+ home
1075
+ / "cli"
1076
+ / "plugins"
1077
+ / "marketplaces"
1078
+ / "cowork-flow-local"
1079
+ / "marketplace.json"
1080
+ )
1081
+
1082
+
1083
+ def check_zcode_plugin(repo_root: Path) -> list[dict[str, str]]:
1084
+ """ZCode plugin health. Advisory, and silent while the project never selected
1085
+ the ZCode host: the plugin is a machine-level asset installed once into the
1086
+ ZCode config directory, so it never updates through sync or npm.
1087
+
1088
+ Staleness is judged on the payload's stamped manifest version rather than
1089
+ per file: the payload also carries a materialized copy of the project
1090
+ runtime, and that copy may legitimately differ from this project."""
1091
+ adapter = repo_root / DIR_WORKFLOW / "adapters" / "zcode" / "adapter.yaml"
1092
+ if not adapter.is_file():
1093
+ return []
1094
+
1095
+ marketplace = _zcode_marketplace_path()
1096
+ payload = _machine_plugin_payload("zcode")
1097
+ if payload is None:
1098
+ return _zcode_warning(
1099
+ "PLUGIN-NOT-INSTALLED",
1100
+ marketplace,
1101
+ "this project declares the ZCode host, but no cowork-flow plugin is "
1102
+ "installed in the ZCode config directory, so ZCode sessions inject no "
1103
+ "workflow context",
1104
+ "cwf host add zcode",
1105
+ )
1106
+
1107
+ manifest_relative = _payload_manifest("zcode", repo_root) or ".zcode-plugin/plugin.json"
1108
+ manifest = payload / manifest_relative
1109
+ recorded = _marker_version(manifest)
1110
+ if recorded is None:
1111
+ return _zcode_warning(
1112
+ "PLUGIN-UNKNOWN-VERSION",
1113
+ manifest,
1114
+ "the installed ZCode plugin carries no readable manifest version, so "
1115
+ "its injection logic may predate the current release",
1116
+ "cwf host add zcode --force",
1117
+ )
1118
+ project_version = _project_version(repo_root)
1119
+ if project_version and recorded != project_version:
1120
+ return _zcode_warning(
1121
+ "PLUGIN-STALE",
1122
+ manifest,
1123
+ f"the ZCode plugin was installed from {recorded} but this project "
1124
+ f"runs {project_version}; the plugin does not update with sync or npm, "
1125
+ "so injection may lag the project runtime",
1126
+ "cwf host add zcode --force",
1127
+ )
1128
+ return []
1129
+
1130
+
1101
1131
  _CLAUDE_CODE_PLUGIN_CONTRACT = "runtime-health:claude-code-plugin"
1102
1132
  _CLAUDE_CODE_PLUGIN_NAME = "cowork-flow"
1103
1133
  _CLAUDE_CODE_SKILL = "skills/cowork-flow-bootstrap/SKILL.md"
@@ -1524,15 +1554,7 @@ def _machine_plugin_payload(platform_id: str) -> Path | None:
1524
1554
  payload = Path(str(entry.get("installPath") or "")) if entry else None
1525
1555
  return payload if payload is not None and payload.is_dir() else None
1526
1556
  if platform_id == "zcode":
1527
- home = Path(os.environ.get("ZCODE_HOME") or (Path.home() / ".zcode"))
1528
- marketplace = (
1529
- home
1530
- / "cli"
1531
- / "plugins"
1532
- / "marketplaces"
1533
- / "cowork-flow-local"
1534
- / "marketplace.json"
1535
- )
1557
+ marketplace = _zcode_marketplace_path()
1536
1558
  try:
1537
1559
  data = json.loads(marketplace.read_text(encoding="utf-8"))
1538
1560
  except (OSError, json.JSONDecodeError, ValueError):
@@ -1654,7 +1676,6 @@ def _all_check_result(repo_root: Path) -> dict[str, object]:
1654
1676
  task_hygiene_issues = check_task_hygiene(repo_root)
1655
1677
  state_recovery_issues = check_state_recovery(repo_root)
1656
1678
  session_hygiene_issues = check_session_hygiene(repo_root)
1657
- spec_check_issues = check_spec_checks(repo_root)
1658
1679
  mcp_issues = check_mcp_registration(repo_root)
1659
1680
  dsh_preset_issues = check_dsh_preset(repo_root)
1660
1681
  kimi_hook_issues = check_kimi_hook(repo_root)
@@ -1662,6 +1683,7 @@ def _all_check_result(repo_root: Path) -> dict[str, object]:
1662
1683
  codex_plugin_issues = check_codex_plugin(repo_root)
1663
1684
  claude_code_plugin_issues = check_claude_code_plugin(repo_root)
1664
1685
  opencode_plugin_issues = check_opencode_plugin(repo_root)
1686
+ zcode_plugin_issues = check_zcode_plugin(repo_root)
1665
1687
  kimi_code_plugin_issues = check_kimi_code_plugin(repo_root)
1666
1688
  skill_delivery_issues = check_skill_delivery(repo_root)
1667
1689
  errors: list[dict[str, object]] = []
@@ -1675,7 +1697,6 @@ def _all_check_result(repo_root: Path) -> dict[str, object]:
1675
1697
  {"kind": "distribution", "message": error}
1676
1698
  for error in distribution_errors
1677
1699
  )
1678
- errors.extend({"kind": "spec-checks", **issue} for issue in spec_check_issues)
1679
1700
  return {
1680
1701
  "ok": not errors,
1681
1702
  "errors": errors,
@@ -1684,7 +1705,6 @@ def _all_check_result(repo_root: Path) -> dict[str, object]:
1684
1705
  "taskHygiene": task_hygiene_issues,
1685
1706
  "stateRecovery": state_recovery_issues,
1686
1707
  "sessionHygiene": session_hygiene_issues,
1687
- "specChecks": spec_check_issues,
1688
1708
  "mcpRegistration": mcp_issues,
1689
1709
  "dshPreset": dsh_preset_issues,
1690
1710
  "kimiHook": kimi_hook_issues,
@@ -1692,6 +1712,7 @@ def _all_check_result(repo_root: Path) -> dict[str, object]:
1692
1712
  "codexPlugin": codex_plugin_issues,
1693
1713
  "claudeCodePlugin": claude_code_plugin_issues,
1694
1714
  "opencodePlugin": opencode_plugin_issues,
1715
+ "zcodePlugin": zcode_plugin_issues,
1695
1716
  "kimiCodePlugin": kimi_code_plugin_issues,
1696
1717
  "skillDelivery": skill_delivery_issues,
1697
1718
  },
@@ -1733,6 +1754,10 @@ def _run_checks(repo_root: Path, *, structured: bool = False) -> int:
1733
1754
  print(f"OpenCode plugin ({issue['code']}): {issue['message']}")
1734
1755
  if issue.get("commandHint"):
1735
1756
  print(f" fix: {issue['commandHint']}")
1757
+ for issue in result["issues"]["zcodePlugin"]:
1758
+ print(f"ZCode plugin ({issue['code']}): {issue['message']}")
1759
+ if issue.get("commandHint"):
1760
+ print(f" fix: {issue['commandHint']}")
1736
1761
  for issue in result["issues"]["kimiCodePlugin"]:
1737
1762
  print(f"Kimi Code plugin ({issue['code']}): {issue['message']}")
1738
1763
  if issue.get("commandHint"):
@@ -33,7 +33,6 @@ Keep each update concrete:
33
33
  - Show the contract or command shape.
34
34
  - Include good/bad cases when useful.
35
35
  - State tests or checks that protect the behavior.
36
- - When a clause is machine-checkable (可机检), add or refresh its `checks:` declaration in the same pass (main session only — never rewrite the clause text): the command must pass on a clean tree, a negative case must prove it can fail, `files` must narrow the scope, and a `spec-check` run after writing must confirm the declaration parses.
37
36
  - Update the matching `index.md` when adding a new topic.
38
37
  - Check for duplicate guidance and remove stale wording before finishing.
39
38
 
@@ -128,7 +128,7 @@ Then list task briefs, followed by integrated verification and completion checks
128
128
 
129
129
  - For Tiny tasks, keep the brief short but still name exact file boundaries, completion conditions, and verification commands.
130
130
  - For behavior changes, include a failing test before implementation when behavior can be tested.
131
- - For implementation tasks whose changed files are covered by spec `checks:` declarations, include the spec self-check in the task's Verification Command (for example `./.cowork-flow/run spec-check`): delegated subagents receive no edit-phase hook feedback on some hosts (zcode), so the explicit command is the reliable pull path for both self-check and acceptance.
131
+ - For implementation tasks whose changed files are covered by `.cowork-flow/spec/` requirements, name the covered spec files in the plan so review can check them clause by clause.
132
132
  - 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.
133
133
  - For complex problems, test depth first: business invariants, cross-layer contracts, state transitions, error boundaries, and real regression paths before narrow unit cases.
134
134
  - 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`.
@@ -39,11 +39,11 @@ Read only what is needed for the current task:
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
40
  - **Evidence coverage**: when the decision anchor declares acceptance criteria (`AC-xxx`), verify each one has an evidence record in `<task>/evidence.jsonl` — or a recorded `meta.evidenceExempt` exemption from an explicit `--allow-missing-evidence` completion. Schema and gate codes (`LIFECYCLE-AC-001..004`) are defined in `.cowork-flow/spec/contracts/evidence-completion.md`. A record must point at real proof (test, command output, or manual verification note); a record that restates the AC without evidence is a finding.
41
41
  - **Receipt freshness**: a strict task's completion gate requires the latest receipt to match the current change fingerprint, which covers the changed paths' content, the anchor's goal and AC text, the bound plan, `dev_type`, the referenced spec bodies, and `implement.jsonl`. Appending a scope entry to `implement.jsonl` — or editing a referenced spec body — therefore invalidates the receipt, so the review is re-run on the final state. That is the intended cost, not a defect. Evidence appends, AC ticks, and `.cowork-flow/.runtime/**` stay fingerprint-neutral, so closeout never invalidates what it just checked.
42
- - **User specs**: every applicable backend/frontend requirement is verified against the diff and marked `pass`, `finding`, or `not_applicable` with reason. Every `finding` is fixed during review; an unfixed spec violation blocks completion and is never accepted as-is or deferred to the user. For each requirement, also ask whether it is machine-checkable (可机检) and already declared with `checks:`; identifying checkable clauses and adding or refreshing declarations is a main-session duty (never rewrite the clause text — raise a conflict through the review instead), and a declared check must earn its place: the command passes on a clean tree, a negative case proves it can fail, `files` narrows the scope, and a `spec-check` run after declaring confirms the declaration parses. Delegated subagents do not edit specs; they report candidates up.
42
+ - **User specs**: every applicable backend/frontend requirement is verified against the diff and marked `pass`, `finding`, or `not_applicable` with reason. Every `finding` is fixed during review; an unfixed spec violation blocks completion and is never accepted as-is or deferred to the user. For each requirement, also ask whether it is machine-checkable (可机检) and name the command that decides it together with what that command reports when the clause is broken: a machine-checkable clause left with no deciding command is itself a finding. Review never rewrites the clause text — a conflict between a clause and the implementation is raised through the review for the user to decide. Delegated subagents do not edit specs; they report candidates up.
43
43
  - **Specs**: project specs are updated when behavior/contracts changed, or the review states why no spec update is needed.
44
44
  - **Code quality**: naming, layering, error handling, state boundaries, security-sensitive paths, and complexity are reviewed against applicable user specs.
45
45
  - **Lifecycle blockers**: state/scope blockers are fixed before acceptance; review does not invent hard blockers for natural-language specs.
46
- - **Delegated acceptance**: when the implementation came from a delegated subagent, run `./.cowork-flow/run spec-check` before accepting its report — delegated sessions receive no edit-phase hook feedback on some hosts (zcode), so the report can be silently non-compliant; every violation is fixed or blocks acceptance.
46
+ - **Delegated acceptance**: when the implementation came from a delegated subagent, verify the changed paths against the task scope and the applicable `spec/` requirements before accepting its report — delegated sessions receive no edit-phase hook feedback on some hosts, so the report can be silently out of scope or out of spec; every violation is fixed or blocks acceptance.
47
47
  - **Advisory facts**: helper output is used to focus review, not to declare pass/fail or block completion.
48
48
  - **Anti-self-proof**: every claimed pass maps to source, diff, test output, or lifecycle output.
49
49
  - **Anti-rationalization**: every accepted gap has an explicit user decision or narrow technical reason.
@@ -85,7 +85,7 @@ Return a concise review result with:
85
85
  - `status`: `pass`, `needs_fix`, or `blocked`.
86
86
  - `findings`: severity, file, line/scope, impact, fix, plus the `clause` / `command` / `negativeProof` triple defined in Review Report Format.
87
87
  - `test_intent_review`: why tests prove the intended behavior or what is missing.
88
- - `user_spec_review`: applicable spec files, per-requirement result, findings, and the "clause → command → negative proof" list for every `checks:` declaration added or refreshed in this review (`none` when nothing became machine-checkable).
88
+ - `user_spec_review`: applicable spec files, per-requirement result, findings, and the "clause → command → negative proof" list for every machine-checkable requirement the review judged (`none` when nothing was machine-checkable).
89
89
  - `lifecycle_check_review`: lifecycle commands run, blocker status, and resolution.
90
90
  - `verification`: exact commands run from this checkout.
91
91
  - `specUpdates`: files updated or reason no update was needed.
@@ -1,178 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """`run spec-check [--phase edit|lifecycle] [--file <path>...] [--json]`.
4
-
5
- Executes user-declared spec checks (frontmatter `checks:`) and prints a
6
- normalized one-line summary unless `--json`/`--verbose` asks for more. See
7
- services/spec_check.py for the three-state contract (pass/violation/unchecked).
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import argparse
13
- import json
14
- import sys
15
-
16
- if __package__:
17
- from . import _bootstrap as _bootstrap # noqa: F401
18
- else:
19
- import _bootstrap # noqa: F401
20
-
21
- from infra.paths import get_repo_root
22
- from services.spec_check import SpecCheckError, run_checks, summary_has
23
-
24
-
25
- def _report_exit_code(report: dict) -> int:
26
- """Map a report to the public spec-check exit contract."""
27
- if summary_has(report, "violation"):
28
- return 1
29
- # An executor failure means the checks never produced a verdict; it must
30
- # never exit 0 just because the synthetic report carries no counter.
31
- if (
32
- summary_has(report, "unchecked")
33
- or summary_has(report, "executorError")
34
- or report.get("parseErrors")
35
- ):
36
- return 2
37
- return 0
38
-
39
-
40
- def _parse_error_line(parse_errors: list) -> str:
41
- """Render parse failures so text mode cannot look like a clean run."""
42
- details = []
43
- for item in parse_errors[:3]:
44
- if isinstance(item, dict):
45
- spec = item.get("spec") or "?"
46
- error = item.get("error") or "unknown parse error"
47
- details.append(f"{spec}: {error}")
48
- suffix = f": {'; '.join(details)}" if details else ""
49
- return f"spec-check: {len(parse_errors)} parse error(s){suffix}"
50
-
51
-
52
- def _build_parser() -> argparse.ArgumentParser:
53
- parser = argparse.ArgumentParser(
54
- prog="spec-check",
55
- description=(
56
- "Run user-declared spec checks (frontmatter checks:) for one phase."
57
- ),
58
- )
59
- parser.add_argument(
60
- "--phase",
61
- choices=("edit", "lifecycle"),
62
- default="lifecycle",
63
- help="edit: only declarations matching the given files (2.5s clamp); "
64
- "lifecycle: every declaration honoring its files scope.",
65
- )
66
- parser.add_argument(
67
- "--file",
68
- action="append",
69
- default=None,
70
- help="Changed file path to match against declaration files filters; "
71
- "repeatable. Drives the edit-phase filter and single-file queries.",
72
- )
73
- parser.add_argument(
74
- "--verbose",
75
- action="store_true",
76
- help="Print full command output per result (query mode only).",
77
- )
78
- parser.add_argument(
79
- "--throttled",
80
- action="store_true",
81
- help="Editor-phase mode: single-line report for one file, throttled "
82
- "per file (empty output when nothing to report).",
83
- )
84
- parser.add_argument(
85
- "--json",
86
- action="store_true",
87
- dest="as_json",
88
- help="Print the machine-readable report.",
89
- )
90
- return parser
91
-
92
-
93
- def _run_throttled(repo_root, changed_files: list[str]) -> int:
94
- from services.spec_check import run_edit_checks
95
-
96
- if len(changed_files) != 1:
97
- print("Error: --throttled requires exactly one --file", file=sys.stderr)
98
- return 2
99
- print(run_edit_checks(repo_root, changed_files[0]))
100
- return 0
101
-
102
-
103
- def _print_text_report(report: dict, *, verbose: bool) -> None:
104
- if verbose:
105
- for result in report.get("results", []):
106
- print(f"--- {result.get('spec')} :: {result.get('cmd')}")
107
- for line in result.get("output", []):
108
- print(line)
109
- if result.get("reason"):
110
- print(f"reason: {result['reason']}")
111
-
112
- parse_errors = report.get("parseErrors") or []
113
- if parse_errors:
114
- print(_parse_error_line(parse_errors))
115
- return
116
-
117
- summary = report.get("summary", {})
118
- if not parse_errors and summary.get("declarations") == 0:
119
- # Zero declarations is a fact, not a pass: name it so a missing or
120
- # silently ignored declaration block cannot look like a clean run.
121
- print(
122
- "spec-check: no checks declared "
123
- f"({report.get('specFiles', 0)} spec files scanned)"
124
- )
125
- # The reader of this branch is the session that has just been told
126
- # nothing is enforced, not a human skimming a terminal: without the
127
- # write path and the syntax reference in the same breath, the fact it
128
- # is looking at has no obvious next action attached to it.
129
- print(
130
- "next: write .cowork-flow/spec/<name>.md with frontmatter "
131
- "checks: (any .md under spec/ outside contracts/, runtime/, "
132
- "schemas/); syntax and example: "
133
- ".cowork-flow/spec/contracts/spec-checks.md"
134
- )
135
- return
136
-
137
- from services.spec_check import normalized_one_line
138
-
139
- line = normalized_one_line(report)
140
- if line:
141
- print(line)
142
- return
143
- print(
144
- "spec-check: {pass} passed, {violation} violations, "
145
- "{unchecked} unchecked".format(
146
- **{**{"pass": 0, "violation": 0, "unchecked": 0}, **summary}
147
- )
148
- )
149
-
150
-
151
- def main() -> int:
152
- args = _build_parser().parse_args()
153
- repo_root = get_repo_root()
154
-
155
- if args.throttled:
156
- return _run_throttled(repo_root, args.file or [])
157
-
158
- try:
159
- report = run_checks(
160
- repo_root,
161
- phase=args.phase,
162
- changed_files=args.file or [],
163
- )
164
- except SpecCheckError as error:
165
- print(f"Error: {error}", file=sys.stderr)
166
- return 2
167
-
168
- exit_code = _report_exit_code(report)
169
- if args.as_json:
170
- print(json.dumps(report, ensure_ascii=False, indent=2))
171
- return exit_code
172
-
173
- _print_text_report(report, verbose=args.verbose)
174
- return exit_code
175
-
176
-
177
- if __name__ == "__main__":
178
- sys.exit(main())