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.
- package/CHANGELOG.md +45 -1
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/architecture.md +1 -34
- package/docs/direction.md +4 -3
- package/docs/hosts.md +14 -10
- package/docs/index.md +1 -1
- package/docs/release.md +1 -1
- package/package.json +6 -6
- 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 +72 -24
- package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
- package/presets/kimi-code/hooks/cowork-flow-inject.mjs +28 -2
- package/presets/opencode/cowork-flow/plugin-core.js +70 -88
- 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/qoder/hooks/inject-context.mjs +28 -2
- package/presets/zcode/.zcode-plugin/plugin.json +1 -1
- package/presets/zcode/hooks/inject-context.mjs +28 -3
- package/src/commands/host.js +23 -2
- package/src/commands/install-dsh-hook.js +1 -6
- package/src/commands/install-dsh-preset.js +1 -6
- package/src/commands/install-zcode-plugin.js +2 -3
- package/src/commands/registry.js +4 -2
- package/src/lib/asset-plan.js +1 -1
- package/src/lib/dsh-home.js +6 -0
- package/src/lib/plan-applier.js +1 -6
- package/template/.claude/agents/cowork-check.md +1 -1
- package/template/.claude/agents/cowork-implement.md +1 -1
- package/template/.claude/agents/cowork-research.md +1 -1
- package/template/.codex/agents/cowork-check.toml +1 -1
- package/template/.codex/agents/cowork-implement.toml +1 -1
- package/template/.codex/agents/cowork-research.toml +1 -1
- package/template/.codex/agents/default.toml +1 -1
- package/template/.codex/agents/explorer.toml +1 -1
- package/template/.codex/agents/worker.toml +1 -1
- 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/run +2 -2
- package/template/.cowork-flow/run.cmd +3 -3
- 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 +53 -26
- 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/.kimi-code/agents/cowork-check.md +1 -1
- package/template/.kimi-code/agents/cowork-implement.md +1 -1
- package/template/.kimi-code/agents/cowork-research.md +1 -1
- package/template/.opencode/agents/cowork-check.md +1 -1
- package/template/.opencode/agents/cowork-implement.md +1 -1
- package/template/.opencode/agents/cowork-research.md +1 -1
- package/template/.opencode/cowork-flow/plugin-core.js +70 -88
- 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 +83 -58
- 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
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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())
|