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
|
@@ -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["
|
|
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
|
|
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
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
368
|
+
def _validate_l2_plan_link(
|
|
275
369
|
rule: dict,
|
|
276
370
|
repo_root: Path,
|
|
277
371
|
task_dir: Path | None,
|
|
278
|
-
|
|
372
|
+
parameters: Mapping[str, object],
|
|
279
373
|
) -> dict | None:
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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,
|