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.
- package/README.md +27 -14
- package/package.json +1 -1
- package/src/commands/init.js +39 -28
- package/src/commands/sync.js +2 -2
- package/src/lib/asset-plan.js +127 -0
- package/src/lib/copy-template.js +101 -141
- package/src/lib/host-assets.js +231 -0
- package/src/lib/plan-applier.js +329 -0
- package/src/lib/platforms.js +15 -76
- package/src/lib/skill-registry.js +503 -0
- package/template/.claude/agents/cowork-check.md +10 -1
- package/template/.claude/agents/cowork-implement.md +11 -2
- package/template/.claude/hooks/inject-workflow-state.py +51 -254
- package/template/.codex/agents/cowork-check.toml +12 -3
- package/template/.codex/agents/cowork-implement.toml +11 -2
- package/template/.codex/agents/default.toml +3 -2
- package/template/.codex/agents/explorer.toml +3 -2
- package/template/.codex/agents/worker.toml +2 -1
- package/template/.codex/hooks/inject-workflow-state.py +46 -265
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/application/__init__.py +1 -0
- package/template/.cowork-flow/scripts/application/batch_execution.py +743 -0
- package/template/.cowork-flow/scripts/application/runtime_context_service.py +365 -0
- package/template/.cowork-flow/scripts/application/task_archive.py +220 -0
- package/template/.cowork-flow/scripts/application/task_context.py +765 -0
- package/template/.cowork-flow/scripts/application/task_creation.py +201 -0
- package/template/.cowork-flow/scripts/application/task_lifecycle.py +532 -0
- package/template/.cowork-flow/scripts/application/task_tree.py +179 -0
- package/template/.cowork-flow/scripts/commands/doctor.py +78 -57
- package/template/.cowork-flow/scripts/commands/party_mode_v2.py +671 -425
- package/template/.cowork-flow/scripts/commands/subagent.py +38 -26
- package/template/.cowork-flow/scripts/commands/task.py +535 -1953
- package/template/.cowork-flow/scripts/commands/task_archive_commands.py +195 -0
- package/template/.cowork-flow/scripts/commands/task_context_commands.py +240 -0
- package/template/.cowork-flow/scripts/commands/task_create_command.py +164 -0
- package/template/.cowork-flow/scripts/commands/task_navigation.py +407 -0
- package/template/.cowork-flow/scripts/commands/task_parser.py +364 -0
- package/template/.cowork-flow/scripts/commands/task_support.py +76 -0
- package/template/.cowork-flow/scripts/commands/task_tree_commands.py +160 -0
- package/template/.cowork-flow/scripts/common/core/execution_context.py +161 -85
- package/template/.cowork-flow/scripts/common/core/host_manifest.py +393 -0
- package/template/.cowork-flow/scripts/common/core/skill_registry.py +568 -0
- package/template/.cowork-flow/scripts/common/gates/coding_standards.py +58 -7
- package/template/.cowork-flow/scripts/common/gates/gates.py +323 -46
- package/template/.cowork-flow/scripts/common/gates/models.py +196 -0
- package/template/.cowork-flow/scripts/common/gates/registry.py +79 -0
- package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +13 -13
- package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +104 -62
- package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +39 -22
- package/template/.cowork-flow/scripts/common/gates/validate_rules.py +223 -40
- package/template/.cowork-flow/scripts/common/git/git_context.py +216 -182
- package/template/.cowork-flow/scripts/common/host/__init__.py +1 -0
- package/template/.cowork-flow/scripts/common/host/workflow_state_hook.py +303 -0
- package/template/.cowork-flow/scripts/common/storage/__init__.py +1 -0
- package/template/.cowork-flow/scripts/common/storage/operation_log.py +72 -0
- package/template/.cowork-flow/scripts/common/storage/state_store.py +279 -0
- package/template/.cowork-flow/scripts/common/storage/unit_of_work.py +214 -0
- package/template/.cowork-flow/scripts/common/task/active_task.py +93 -135
- package/template/.cowork-flow/scripts/common/task/batch_mode.py +33 -120
- package/template/.cowork-flow/scripts/common/task/decision_review.py +85 -0
- package/template/.cowork-flow/scripts/common/task/readiness.py +9 -15
- package/template/.cowork-flow/scripts/common/task/task_repository.py +156 -0
- package/template/.cowork-flow/scripts/run.py +24 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
- package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
- package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
- package/template/.cowork-flow/spec/protocols/decision-review.md +28 -0
- package/template/.cowork-flow/spec/protocols/review.md +34 -0
- package/template/.cowork-flow/spec/protocols/spec-maintenance.md +16 -0
- package/template/.cowork-flow/spec/protocols/tdd.md +30 -0
- package/template/.cowork-flow/spec/references/definition-of-done.md +4 -4
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +25 -0
- package/template/.cowork-flow/spec/runtime/host-assets.json +214 -0
- package/template/.cowork-flow/spec/runtime/index.md +8 -0
- package/template/.cowork-flow/spec/runtime/rules.json +61 -10
- package/template/.cowork-flow/spec/runtime/skill-registry.json +397 -0
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +173 -0
- package/template/.cowork-flow/spec/schemas/rules.schema.json +10 -1
- package/template/.cowork-flow/spec/schemas/skill-registry.schema.json +202 -0
- package/template/.cowork-flow/workflow.md +18 -13
- package/template/.opencode/agents/cowork-check.md +11 -0
- package/template/.opencode/agents/cowork-implement.md +16 -1
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/inject-context.js +3 -85
- package/template/.zcode/hooks/inject-context.selfcheck.mjs +6 -6
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
- package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +61 -10
- package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +10 -1
- package/template/.zcode/scaffold/.cowork-flow/workflow.md +18 -13
- package/template/.zcode/scaffold/AGENTS.md +18 -8
- package/template/AGENTS.md +1 -1
- package/template/skills/batch-mode/SKILL.md +140 -76
- package/template/skills/brainstorming/SKILL.md +2 -2
- package/template/skills/cowork-flow/SKILL.md +42 -0
- package/template/skills/party-mode/SHARED-BOUNDARIES.md +1 -1
- package/template/skills/party-mode/SKILL.md +48 -119
- package/template/.cowork-flow/scripts/common/gates/spec_validator.py +0 -53
- package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +0 -32
- package/template/skills/before-dev/SKILL.md +0 -41
- package/template/skills/check/SKILL.md +0 -53
- package/template/skills/check/simplification-guide.md +0 -47
- package/template/skills/continue/SKILL.md +0 -29
- package/template/skills/doubt-review/SKILL.md +0 -118
- package/template/skills/finish-work/SKILL.md +0 -32
- package/template/skills/party-mode-v2/SKILL.md +0 -75
- package/template/skills/start/SKILL.md +0 -78
- package/template/skills/tdd/SKILL.md +0 -74
- package/template/skills/update-spec/SKILL.md +0 -39
- package/template/skills/using-cowork-flow/SKILL.md +0 -80
|
@@ -1,71 +1,348 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Typed, fail-closed gate pipeline for lifecycle commands."""
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from collections.abc import Mapping
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
+
from .models import (
|
|
10
|
+
GateContext,
|
|
11
|
+
GateDefinition,
|
|
12
|
+
GateExecution,
|
|
13
|
+
GateResult,
|
|
14
|
+
ValidatorBinding,
|
|
15
|
+
Violation,
|
|
16
|
+
)
|
|
17
|
+
from .registry import GateLoadError, GateRegistry
|
|
9
18
|
|
|
10
|
-
@dataclass
|
|
11
|
-
class GateResult:
|
|
12
|
-
"""Normalized result returned by workflow gates."""
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
STAGE_GATES = {
|
|
21
|
+
"task_start": ("runtime_rules",),
|
|
22
|
+
"task_review": (
|
|
23
|
+
"implementation",
|
|
24
|
+
"tdd_evidence",
|
|
25
|
+
"test_intent",
|
|
26
|
+
"runtime_rules",
|
|
27
|
+
"coding_standards",
|
|
28
|
+
"complexity",
|
|
29
|
+
),
|
|
30
|
+
"task_complete": (
|
|
31
|
+
"tdd_evidence",
|
|
32
|
+
"test_intent",
|
|
33
|
+
"runtime_rules",
|
|
34
|
+
"coding_standards",
|
|
35
|
+
"complexity",
|
|
36
|
+
),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_default_registry() -> GateRegistry:
|
|
41
|
+
registry = GateRegistry()
|
|
42
|
+
for binding in (
|
|
43
|
+
ValidatorBinding(
|
|
44
|
+
key="runtime_rules",
|
|
45
|
+
module="validate_rules",
|
|
46
|
+
function="validate_rules",
|
|
47
|
+
positional=("repo_root", "scope", "task_dir"),
|
|
48
|
+
),
|
|
49
|
+
ValidatorBinding(
|
|
50
|
+
key="implementation",
|
|
51
|
+
module="validate_implementation",
|
|
52
|
+
function="validate_implementation",
|
|
53
|
+
positional=("repo_root", "task_dir"),
|
|
54
|
+
keyword={
|
|
55
|
+
"allow_spec_file_modifications": "allow_spec_file_modifications",
|
|
56
|
+
},
|
|
57
|
+
),
|
|
58
|
+
ValidatorBinding(
|
|
59
|
+
key="tdd_evidence",
|
|
60
|
+
module="tdd_evidence",
|
|
61
|
+
function="validate_tdd_evidence",
|
|
62
|
+
positional=("task_dir",),
|
|
63
|
+
),
|
|
64
|
+
ValidatorBinding(
|
|
65
|
+
key="test_intent",
|
|
66
|
+
module="test_intent",
|
|
67
|
+
function="validate_test_intent",
|
|
68
|
+
positional=("repo_root", "task_dir"),
|
|
69
|
+
),
|
|
70
|
+
ValidatorBinding(
|
|
71
|
+
key="coding_standards",
|
|
72
|
+
module="validate_coding_standards",
|
|
73
|
+
function="validate_coding_standards",
|
|
74
|
+
positional=("repo_root", "task_dir"),
|
|
75
|
+
),
|
|
76
|
+
ValidatorBinding(
|
|
77
|
+
key="complexity",
|
|
78
|
+
module="validate_coding_standards",
|
|
79
|
+
function="validate_complexity_signals",
|
|
80
|
+
positional=("repo_root", "task_dir"),
|
|
81
|
+
),
|
|
82
|
+
):
|
|
83
|
+
registry.register_validator(binding)
|
|
84
|
+
|
|
85
|
+
definitions = (
|
|
86
|
+
GateDefinition(
|
|
87
|
+
id="runtime_rules",
|
|
88
|
+
validator_key="runtime_rules",
|
|
89
|
+
required=True,
|
|
90
|
+
block_message="Spec enforcement blocked lifecycle transition",
|
|
91
|
+
log_violations=True,
|
|
92
|
+
),
|
|
93
|
+
GateDefinition(
|
|
94
|
+
id="implementation",
|
|
95
|
+
validator_key="implementation",
|
|
96
|
+
required=True,
|
|
97
|
+
block_message="Implementation gate blocked task review",
|
|
98
|
+
warning_message="Implementation violations detected",
|
|
99
|
+
),
|
|
100
|
+
GateDefinition(
|
|
101
|
+
id="tdd_evidence",
|
|
102
|
+
validator_key="tdd_evidence",
|
|
103
|
+
required=True,
|
|
104
|
+
block_message="TDD evidence gate blocked lifecycle transition",
|
|
105
|
+
),
|
|
106
|
+
GateDefinition(
|
|
107
|
+
id="test_intent",
|
|
108
|
+
validator_key="test_intent",
|
|
109
|
+
required=True,
|
|
110
|
+
block_message="Test intent gate blocked lifecycle transition",
|
|
111
|
+
warning_message="Test intent review warnings",
|
|
112
|
+
),
|
|
113
|
+
GateDefinition(
|
|
114
|
+
id="coding_standards",
|
|
115
|
+
validator_key="coding_standards",
|
|
116
|
+
required=True,
|
|
117
|
+
block_message="Coding standards gate blocked lifecycle transition",
|
|
118
|
+
),
|
|
119
|
+
GateDefinition(
|
|
120
|
+
id="complexity",
|
|
121
|
+
validator_key="complexity",
|
|
122
|
+
required=False,
|
|
123
|
+
block_message="",
|
|
124
|
+
warning_message="Complexity review warnings",
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
for definition in definitions:
|
|
128
|
+
registry.register_gate(definition)
|
|
129
|
+
return registry
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _pipeline_violation(
|
|
133
|
+
*,
|
|
134
|
+
rule_id: str,
|
|
135
|
+
context: GateContext,
|
|
136
|
+
definition: GateDefinition,
|
|
137
|
+
message: str,
|
|
138
|
+
fix_hint: str,
|
|
139
|
+
) -> dict:
|
|
140
|
+
severity = "block" if definition.required else "warn"
|
|
141
|
+
return Violation(
|
|
142
|
+
rule_id=rule_id,
|
|
143
|
+
type="gate_pipeline",
|
|
144
|
+
severity=severity,
|
|
145
|
+
message=message,
|
|
146
|
+
file=definition.validator_key,
|
|
147
|
+
fix_hint=fix_hint,
|
|
148
|
+
scope=context.scope,
|
|
149
|
+
gate_id=definition.id,
|
|
150
|
+
error_code=rule_id,
|
|
151
|
+
).to_dict()
|
|
17
152
|
|
|
18
|
-
@classmethod
|
|
19
|
-
def from_violations(
|
|
20
|
-
cls,
|
|
21
|
-
scope: str,
|
|
22
|
-
violations: list[dict],
|
|
23
|
-
task_dir: Path | None = None,
|
|
24
|
-
) -> "GateResult":
|
|
25
|
-
return cls(scope=scope, task_dir=task_dir, violations=list(violations))
|
|
26
153
|
|
|
27
|
-
|
|
28
|
-
|
|
154
|
+
def _normalize_violations(
|
|
155
|
+
raw_result: object,
|
|
156
|
+
context: GateContext,
|
|
157
|
+
definition: GateDefinition,
|
|
158
|
+
) -> list[dict]:
|
|
159
|
+
if not isinstance(raw_result, list):
|
|
29
160
|
return [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
161
|
+
_pipeline_violation(
|
|
162
|
+
rule_id="GATE-PROTOCOL-001",
|
|
163
|
+
context=context,
|
|
164
|
+
definition=definition,
|
|
165
|
+
message=(
|
|
166
|
+
f"Gate {definition.id} returned "
|
|
167
|
+
f"{type(raw_result).__name__}; expected a list of violations"
|
|
168
|
+
),
|
|
169
|
+
fix_hint="Return list[dict] from the registered gate validator.",
|
|
170
|
+
)
|
|
33
171
|
]
|
|
34
172
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
173
|
+
normalized: list[dict] = []
|
|
174
|
+
for raw_violation in raw_result:
|
|
175
|
+
if not isinstance(raw_violation, Mapping):
|
|
176
|
+
normalized.append(
|
|
177
|
+
_pipeline_violation(
|
|
178
|
+
rule_id="GATE-PROTOCOL-001",
|
|
179
|
+
context=context,
|
|
180
|
+
definition=definition,
|
|
181
|
+
message=(
|
|
182
|
+
f"Gate {definition.id} returned a non-object violation"
|
|
183
|
+
),
|
|
184
|
+
fix_hint="Return violation dictionaries with stable metadata.",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
continue
|
|
188
|
+
try:
|
|
189
|
+
normalized.append(
|
|
190
|
+
Violation.from_mapping(
|
|
191
|
+
raw_violation,
|
|
192
|
+
scope=context.scope,
|
|
193
|
+
gate_id=definition.id,
|
|
194
|
+
).to_dict()
|
|
195
|
+
)
|
|
196
|
+
except ValueError as error:
|
|
197
|
+
normalized.append(
|
|
198
|
+
_pipeline_violation(
|
|
199
|
+
rule_id="GATE-PROTOCOL-001",
|
|
200
|
+
context=context,
|
|
201
|
+
definition=definition,
|
|
202
|
+
message=f"Gate {definition.id} returned invalid metadata: {error}",
|
|
203
|
+
fix_hint=(
|
|
204
|
+
"Return violations with rule_id, severity, and message."
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
return normalized
|
|
42
209
|
|
|
43
210
|
|
|
44
|
-
class
|
|
45
|
-
"""
|
|
211
|
+
class GatePipeline:
|
|
212
|
+
"""Execute the configured stage gates with fail-closed semantics."""
|
|
46
213
|
|
|
47
|
-
def __init__(self,
|
|
48
|
-
self.
|
|
214
|
+
def __init__(self, registry: GateRegistry | None = None) -> None:
|
|
215
|
+
self.registry = registry or build_default_registry()
|
|
49
216
|
|
|
50
|
-
def run(self,
|
|
51
|
-
|
|
217
|
+
def run(self, context: GateContext) -> GateResult:
|
|
218
|
+
executions: list[GateExecution] = []
|
|
219
|
+
priority_violations: list[dict] = []
|
|
220
|
+
ordinary_violations: list[dict] = []
|
|
52
221
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
222
|
+
for gate_id in STAGE_GATES[context.scope]:
|
|
223
|
+
definition = self.registry.gate(gate_id)
|
|
224
|
+
try:
|
|
225
|
+
raw_result = self.registry.invoke(definition, context)
|
|
226
|
+
except GateLoadError as error:
|
|
227
|
+
violations = [
|
|
228
|
+
_pipeline_violation(
|
|
229
|
+
rule_id="GATE-LOAD-001",
|
|
230
|
+
context=context,
|
|
231
|
+
definition=definition,
|
|
232
|
+
message=f"Gate {definition.id} could not load: {error}",
|
|
233
|
+
fix_hint=(
|
|
234
|
+
"Restore the required validator module and function."
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
]
|
|
238
|
+
except Exception as error:
|
|
239
|
+
violations = [
|
|
240
|
+
_pipeline_violation(
|
|
241
|
+
rule_id="GATE-EXEC-001",
|
|
242
|
+
context=context,
|
|
243
|
+
definition=definition,
|
|
244
|
+
message=f"Gate {definition.id} failed during execution: {error}",
|
|
245
|
+
fix_hint=(
|
|
246
|
+
"Fix the validator exception; lifecycle gates fail closed."
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
]
|
|
250
|
+
else:
|
|
251
|
+
violations = _normalize_violations(
|
|
252
|
+
raw_result,
|
|
253
|
+
context,
|
|
254
|
+
definition,
|
|
255
|
+
)
|
|
57
256
|
|
|
58
|
-
|
|
59
|
-
|
|
257
|
+
gate_result = GateResult.from_violations(
|
|
258
|
+
context.scope,
|
|
259
|
+
violations,
|
|
260
|
+
context.task_dir,
|
|
261
|
+
)
|
|
262
|
+
executions.append(
|
|
263
|
+
GateExecution(
|
|
264
|
+
definition=definition,
|
|
265
|
+
result=gate_result,
|
|
266
|
+
)
|
|
60
267
|
)
|
|
61
|
-
|
|
268
|
+
for violation in gate_result.violations:
|
|
269
|
+
if str(violation.get("rule_id", "")).startswith("GATE-"):
|
|
270
|
+
priority_violations.append(violation)
|
|
271
|
+
else:
|
|
272
|
+
ordinary_violations.append(violation)
|
|
273
|
+
|
|
274
|
+
return GateResult(
|
|
275
|
+
scope=context.scope,
|
|
276
|
+
task_dir=context.task_dir,
|
|
277
|
+
violations=[*priority_violations, *ordinary_violations],
|
|
278
|
+
executions=executions,
|
|
279
|
+
)
|
|
280
|
+
|
|
62
281
|
|
|
63
|
-
|
|
64
|
-
|
|
282
|
+
class GateRunner:
|
|
283
|
+
"""Compatibility facade used by lifecycle commands and tests."""
|
|
284
|
+
|
|
285
|
+
def __init__(
|
|
286
|
+
self,
|
|
287
|
+
repo_root: Path,
|
|
288
|
+
registry: GateRegistry | None = None,
|
|
289
|
+
) -> None:
|
|
290
|
+
self.repo_root = Path(repo_root)
|
|
291
|
+
self.pipeline = GatePipeline(registry)
|
|
292
|
+
|
|
293
|
+
def run(
|
|
294
|
+
self,
|
|
295
|
+
scope: str,
|
|
296
|
+
task_dir: Path | None = None,
|
|
297
|
+
*,
|
|
298
|
+
allow_spec_file_modifications: bool = False,
|
|
299
|
+
) -> GateResult:
|
|
300
|
+
return self.pipeline.run(
|
|
301
|
+
GateContext(
|
|
302
|
+
repo_root=self.repo_root,
|
|
303
|
+
scope=scope,
|
|
304
|
+
task_dir=task_dir,
|
|
305
|
+
allow_spec_file_modifications=allow_spec_file_modifications,
|
|
65
306
|
)
|
|
66
|
-
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def coding_standards_summary(self, task_dir: Path | None = None) -> str:
|
|
310
|
+
module = __import__(
|
|
311
|
+
"validate_coding_standards",
|
|
312
|
+
globals(),
|
|
313
|
+
locals(),
|
|
314
|
+
["get_coding_standards_summary"],
|
|
315
|
+
1,
|
|
316
|
+
)
|
|
317
|
+
return module.get_coding_standards_summary(self.repo_root, task_dir)
|
|
67
318
|
|
|
68
319
|
def log(self, result: GateResult) -> None:
|
|
69
|
-
|
|
320
|
+
try:
|
|
321
|
+
module = __import__(
|
|
322
|
+
"validate_rules",
|
|
323
|
+
globals(),
|
|
324
|
+
locals(),
|
|
325
|
+
["log_violations"],
|
|
326
|
+
1,
|
|
327
|
+
)
|
|
328
|
+
except ImportError:
|
|
329
|
+
return
|
|
330
|
+
module.log_violations(
|
|
331
|
+
result.violations,
|
|
332
|
+
result.scope,
|
|
333
|
+
result.task_dir,
|
|
334
|
+
self.repo_root,
|
|
335
|
+
)
|
|
336
|
+
|
|
70
337
|
|
|
71
|
-
|
|
338
|
+
__all__ = [
|
|
339
|
+
"GateContext",
|
|
340
|
+
"GateDefinition",
|
|
341
|
+
"GateExecution",
|
|
342
|
+
"GatePipeline",
|
|
343
|
+
"GateRegistry",
|
|
344
|
+
"GateResult",
|
|
345
|
+
"GateRunner",
|
|
346
|
+
"Violation",
|
|
347
|
+
"build_default_registry",
|
|
348
|
+
]
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Typed models shared by the cowork-flow gate pipeline."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Mapping
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
GATE_SCOPES = frozenset({"task_start", "task_review", "task_complete"})
|
|
12
|
+
GATE_SEVERITIES = frozenset({"block", "warn", "advisory"})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class GateContext:
|
|
17
|
+
"""Runtime inputs available to every gate validator."""
|
|
18
|
+
|
|
19
|
+
repo_root: Path
|
|
20
|
+
scope: str
|
|
21
|
+
task_dir: Path | None = None
|
|
22
|
+
allow_spec_file_modifications: bool = False
|
|
23
|
+
|
|
24
|
+
def __post_init__(self) -> None:
|
|
25
|
+
if self.scope not in GATE_SCOPES:
|
|
26
|
+
raise ValueError(f"unsupported gate scope: {self.scope}")
|
|
27
|
+
object.__setattr__(self, "repo_root", Path(self.repo_root))
|
|
28
|
+
if self.task_dir is not None:
|
|
29
|
+
object.__setattr__(self, "task_dir", Path(self.task_dir))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ValidatorBinding:
|
|
34
|
+
"""Lazy module/function binding plus its GateContext argument mapping."""
|
|
35
|
+
|
|
36
|
+
key: str
|
|
37
|
+
module: str
|
|
38
|
+
function: str
|
|
39
|
+
positional: tuple[str, ...] = ()
|
|
40
|
+
keyword: Mapping[str, str] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class GateDefinition:
|
|
45
|
+
"""Declarative gate definition registered once and reused by stages."""
|
|
46
|
+
|
|
47
|
+
id: str
|
|
48
|
+
validator_key: str
|
|
49
|
+
required: bool
|
|
50
|
+
block_message: str
|
|
51
|
+
warning_message: str | None = None
|
|
52
|
+
log_violations: bool = False
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
if not self.id.strip():
|
|
56
|
+
raise ValueError("gate id must be non-empty")
|
|
57
|
+
if not self.validator_key.strip():
|
|
58
|
+
raise ValueError(f"gate {self.id} validator_key must be non-empty")
|
|
59
|
+
if self.required and not self.block_message.strip():
|
|
60
|
+
raise ValueError(f"required gate {self.id} needs a block message")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class Violation:
|
|
65
|
+
"""Stable typed representation of a validator failure."""
|
|
66
|
+
|
|
67
|
+
rule_id: str
|
|
68
|
+
type: str
|
|
69
|
+
severity: str
|
|
70
|
+
message: str
|
|
71
|
+
file: str
|
|
72
|
+
fix_hint: str
|
|
73
|
+
scope: str
|
|
74
|
+
gate_id: str
|
|
75
|
+
passed: bool = False
|
|
76
|
+
error_code: str | None = None
|
|
77
|
+
extra: Mapping[str, object] = field(default_factory=dict)
|
|
78
|
+
|
|
79
|
+
def __post_init__(self) -> None:
|
|
80
|
+
if not self.rule_id.strip():
|
|
81
|
+
raise ValueError("violation rule_id must be non-empty")
|
|
82
|
+
if self.severity not in GATE_SEVERITIES:
|
|
83
|
+
raise ValueError(f"unsupported violation severity: {self.severity}")
|
|
84
|
+
if self.scope not in GATE_SCOPES:
|
|
85
|
+
raise ValueError(f"unsupported violation scope: {self.scope}")
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_mapping(
|
|
89
|
+
cls,
|
|
90
|
+
raw: Mapping[str, object],
|
|
91
|
+
*,
|
|
92
|
+
scope: str,
|
|
93
|
+
gate_id: str,
|
|
94
|
+
) -> "Violation":
|
|
95
|
+
rule_id = str(raw.get("rule_id") or raw.get("id") or "").strip()
|
|
96
|
+
severity = str(raw.get("severity") or "").strip()
|
|
97
|
+
message = str(raw.get("message") or "").strip()
|
|
98
|
+
if not rule_id or not severity or not message:
|
|
99
|
+
raise ValueError("validator violation must define rule_id, severity, and message")
|
|
100
|
+
|
|
101
|
+
known_fields = {
|
|
102
|
+
"rule_id",
|
|
103
|
+
"id",
|
|
104
|
+
"type",
|
|
105
|
+
"severity",
|
|
106
|
+
"message",
|
|
107
|
+
"file",
|
|
108
|
+
"fix_hint",
|
|
109
|
+
"scope",
|
|
110
|
+
"gate_id",
|
|
111
|
+
"passed",
|
|
112
|
+
"error_code",
|
|
113
|
+
}
|
|
114
|
+
extra = {key: value for key, value in raw.items() if key not in known_fields}
|
|
115
|
+
return cls(
|
|
116
|
+
rule_id=rule_id,
|
|
117
|
+
type=str(raw.get("type") or "gate"),
|
|
118
|
+
severity=severity,
|
|
119
|
+
message=message,
|
|
120
|
+
file=str(raw.get("file") or ""),
|
|
121
|
+
fix_hint=str(raw.get("fix_hint") or ""),
|
|
122
|
+
scope=scope,
|
|
123
|
+
gate_id=gate_id,
|
|
124
|
+
passed=bool(raw.get("passed", False)),
|
|
125
|
+
error_code=str(raw.get("error_code") or rule_id),
|
|
126
|
+
extra=extra,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def to_dict(self) -> dict:
|
|
130
|
+
result = {
|
|
131
|
+
"rule_id": self.rule_id,
|
|
132
|
+
"type": self.type,
|
|
133
|
+
"severity": self.severity,
|
|
134
|
+
"passed": self.passed,
|
|
135
|
+
"message": self.message,
|
|
136
|
+
"file": self.file,
|
|
137
|
+
"fix_hint": self.fix_hint,
|
|
138
|
+
"scope": self.scope,
|
|
139
|
+
"gate_id": self.gate_id,
|
|
140
|
+
"error_code": self.error_code or self.rule_id,
|
|
141
|
+
}
|
|
142
|
+
result.update(self.extra)
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class GateResult:
|
|
148
|
+
"""Normalized aggregate or per-gate result."""
|
|
149
|
+
|
|
150
|
+
scope: str
|
|
151
|
+
task_dir: Path | None = None
|
|
152
|
+
violations: list[dict] = field(default_factory=list)
|
|
153
|
+
executions: list["GateExecution"] = field(default_factory=list)
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def from_violations(
|
|
157
|
+
cls,
|
|
158
|
+
scope: str,
|
|
159
|
+
violations: list[dict],
|
|
160
|
+
task_dir: Path | None = None,
|
|
161
|
+
) -> "GateResult":
|
|
162
|
+
normalized = []
|
|
163
|
+
for violation in violations:
|
|
164
|
+
item = dict(violation)
|
|
165
|
+
item.setdefault("scope", scope)
|
|
166
|
+
item.setdefault("error_code", item.get("rule_id") or item.get("id"))
|
|
167
|
+
normalized.append(item)
|
|
168
|
+
return cls(scope=scope, task_dir=task_dir, violations=normalized)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def blockers(self) -> list[dict]:
|
|
172
|
+
return [
|
|
173
|
+
violation
|
|
174
|
+
for violation in self.violations
|
|
175
|
+
if violation.get("severity") == "block"
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def blocked(self) -> bool:
|
|
180
|
+
return bool(self.blockers)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def exit_code(self) -> int:
|
|
184
|
+
return 1 if self.blocked else 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass(frozen=True)
|
|
188
|
+
class GateExecution:
|
|
189
|
+
"""One gate definition evaluated within a pipeline stage."""
|
|
190
|
+
|
|
191
|
+
definition: GateDefinition
|
|
192
|
+
result: GateResult
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def blocked(self) -> bool:
|
|
196
|
+
return self.result.blocked
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Gate definition and validator registry."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
from .models import GateContext, GateDefinition, ValidatorBinding
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GateRegistryError(ValueError):
|
|
13
|
+
"""Raised when registry declarations are internally inconsistent."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GateLoadError(RuntimeError):
|
|
17
|
+
"""Raised when a registered validator cannot be loaded."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GateRegistry:
|
|
21
|
+
"""Register gate definitions and lazily resolve validator functions."""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._bindings: OrderedDict[str, ValidatorBinding] = OrderedDict()
|
|
25
|
+
self._definitions: OrderedDict[str, GateDefinition] = OrderedDict()
|
|
26
|
+
|
|
27
|
+
def register_validator(self, binding: ValidatorBinding) -> None:
|
|
28
|
+
if not binding.key.strip():
|
|
29
|
+
raise GateRegistryError("validator key must be non-empty")
|
|
30
|
+
if binding.key in self._bindings:
|
|
31
|
+
raise GateRegistryError(f"duplicate validator key: {binding.key}")
|
|
32
|
+
self._bindings[binding.key] = binding
|
|
33
|
+
|
|
34
|
+
def register_gate(self, definition: GateDefinition) -> None:
|
|
35
|
+
if definition.id in self._definitions:
|
|
36
|
+
raise GateRegistryError(f"duplicate gate id: {definition.id}")
|
|
37
|
+
if definition.validator_key not in self._bindings:
|
|
38
|
+
raise GateRegistryError(
|
|
39
|
+
f"gate {definition.id} references unknown validator: "
|
|
40
|
+
f"{definition.validator_key}"
|
|
41
|
+
)
|
|
42
|
+
self._definitions[definition.id] = definition
|
|
43
|
+
|
|
44
|
+
def gate(self, gate_id: str) -> GateDefinition:
|
|
45
|
+
try:
|
|
46
|
+
return self._definitions[gate_id]
|
|
47
|
+
except KeyError as error:
|
|
48
|
+
raise GateRegistryError(f"unknown gate id: {gate_id}") from error
|
|
49
|
+
|
|
50
|
+
def invoke(self, definition: GateDefinition, context: GateContext) -> object:
|
|
51
|
+
binding = self._bindings[definition.validator_key]
|
|
52
|
+
validator = self._load(binding)
|
|
53
|
+
args = [getattr(context, name) for name in binding.positional]
|
|
54
|
+
kwargs = {
|
|
55
|
+
argument: getattr(context, context_field)
|
|
56
|
+
for argument, context_field in binding.keyword.items()
|
|
57
|
+
}
|
|
58
|
+
return validator(*args, **kwargs)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _load(binding: ValidatorBinding) -> Callable:
|
|
62
|
+
try:
|
|
63
|
+
module = __import__(
|
|
64
|
+
binding.module,
|
|
65
|
+
globals(),
|
|
66
|
+
locals(),
|
|
67
|
+
[binding.function],
|
|
68
|
+
1,
|
|
69
|
+
)
|
|
70
|
+
validator = getattr(module, binding.function)
|
|
71
|
+
except (ImportError, AttributeError) as error:
|
|
72
|
+
raise GateLoadError(
|
|
73
|
+
f"{binding.module}.{binding.function}: {error}"
|
|
74
|
+
) from error
|
|
75
|
+
if not callable(validator):
|
|
76
|
+
raise GateLoadError(
|
|
77
|
+
f"{binding.module}.{binding.function} is not callable"
|
|
78
|
+
)
|
|
79
|
+
return validator
|