ww-agentic-workflows 1.0.0.dev3__py3-none-any.whl

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 (167) hide show
  1. ww/__init__.py +18 -0
  2. ww/_bundled_extensions/ww/git/extension.py +1728 -0
  3. ww/action_execution.py +887 -0
  4. ww/actions/__init__.py +94 -0
  5. ww/actions/command.py +444 -0
  6. ww/actions/contracts.py +699 -0
  7. ww/actions/extension.py +197 -0
  8. ww/actions/mcp.py +84 -0
  9. ww/actions/prompt.py +74 -0
  10. ww/actions/skill.py +62 -0
  11. ww/actions/slash_command.py +63 -0
  12. ww/agents.py +151 -0
  13. ww/amendments.py +54 -0
  14. ww/artifacts.py +93 -0
  15. ww/assessments.py +181 -0
  16. ww/assets/__init__.py +2 -0
  17. ww/assets/agent_instructions.md +49 -0
  18. ww/assets/docs/examples.md +879 -0
  19. ww/assets/docs/features.md +4639 -0
  20. ww/assets/docs/specification.md +1876 -0
  21. ww/assets/noww_skill.md +11 -0
  22. ww/assets/workflows/catchall.yaml +26 -0
  23. ww/assets/workflows/onboarding.yaml +586 -0
  24. ww/assets/workflows/scriptize.yaml +130 -0
  25. ww/assets/ww-automate_skill.md +23 -0
  26. ww/assets/ww-deduce-feedback_skill.md +38 -0
  27. ww/assets/ww-feedback-rules_skill.md +48 -0
  28. ww/assets/ww-learn-project_skill.md +22 -0
  29. ww/assets/ww-refresh_skill.md +26 -0
  30. ww/assets/ww-rule_skill.md +83 -0
  31. ww/assets/ww-rules-from-artifacts_skill.md +22 -0
  32. ww/assets/ww-scriptize_skill.md +33 -0
  33. ww/assets/ww-setup_skill.md +94 -0
  34. ww/assets/ww-solve_skill.md +23 -0
  35. ww/assets/ww-suggest_skill.md +32 -0
  36. ww/assets/ww-wizard_skill.md +105 -0
  37. ww/assets/ww_skill.md +59 -0
  38. ww/assignments.py +283 -0
  39. ww/bootstrap.py +405 -0
  40. ww/builtin_workflows.py +215 -0
  41. ww/changes.py +225 -0
  42. ww/child_coordination.py +482 -0
  43. ww/children.py +106 -0
  44. ww/claude_permissions.py +115 -0
  45. ww/cli/__init__.py +7 -0
  46. ww/cli/__main__.py +6 -0
  47. ww/cli/audit.py +129 -0
  48. ww/cli/catalogs.py +131 -0
  49. ww/cli/discover.py +607 -0
  50. ww/cli/initialization.py +898 -0
  51. ww/cli/lookup.py +287 -0
  52. ww/cli/main.py +1768 -0
  53. ww/cli/parser.py +1200 -0
  54. ww/cli/prompts.py +217 -0
  55. ww/cli/updates.py +117 -0
  56. ww/completion_artifacts.py +156 -0
  57. ww/completion_inputs.py +39 -0
  58. ww/config/__init__.py +582 -0
  59. ww/config/actions.py +591 -0
  60. ww/config/composition.py +571 -0
  61. ww/config/rules.py +511 -0
  62. ww/config/steps.py +1220 -0
  63. ww/config/values.py +223 -0
  64. ww/config_files.py +191 -0
  65. ww/config_writes.py +264 -0
  66. ww/contracts.py +155 -0
  67. ww/control.py +41 -0
  68. ww/defaults.py +130 -0
  69. ww/design_docs.py +32 -0
  70. ww/discovery.py +104 -0
  71. ww/documents.py +217 -0
  72. ww/errors.py +18 -0
  73. ww/executable.py +43 -0
  74. ww/execution_models/__init__.py +64 -0
  75. ww/execution_models/construction.py +148 -0
  76. ww/execution_models/decoding.py +38 -0
  77. ww/execution_models/plan_codec.py +565 -0
  78. ww/execution_models/records.py +1206 -0
  79. ww/execution_models/runs.py +266 -0
  80. ww/extensions/__init__.py +40 -0
  81. ww/extensions/api.py +559 -0
  82. ww/extensions/registry.py +864 -0
  83. ww/extensions/store.py +78 -0
  84. ww/feedback.py +342 -0
  85. ww/handler_repairs.py +57 -0
  86. ww/hooks/__init__.py +40 -0
  87. ww/hooks/agents.py +380 -0
  88. ww/hooks/install.py +168 -0
  89. ww/hooks/notices.py +206 -0
  90. ww/hooks/records.py +209 -0
  91. ww/hooks/runtime.py +266 -0
  92. ww/hooks/transcripts.py +183 -0
  93. ww/inspect.py +896 -0
  94. ww/instructions/__init__.py +17 -0
  95. ww/instructions/builder.py +1682 -0
  96. ww/instructions/commands.py +335 -0
  97. ww/instructions/handoff.py +149 -0
  98. ww/instructions/models.py +686 -0
  99. ww/instructions/policy.py +219 -0
  100. ww/instructions/text.py +168 -0
  101. ww/interactions.py +187 -0
  102. ww/interpolation.py +37 -0
  103. ww/item_passes.py +167 -0
  104. ww/items.py +99 -0
  105. ww/locking.py +207 -0
  106. ww/metadata_publication.py +230 -0
  107. ww/onboarding.py +229 -0
  108. ww/open_work.py +236 -0
  109. ww/operations.py +193 -0
  110. ww/operator_ui/__init__.py +16 -0
  111. ww/operator_ui/page.html +351 -0
  112. ww/operator_ui/server.py +215 -0
  113. ww/operator_ui/session.py +389 -0
  114. ww/operator_ui/sheet.py +104 -0
  115. ww/operator_ui/view.py +109 -0
  116. ww/output.py +339 -0
  117. ww/output_adapters/__init__.py +12 -0
  118. ww/output_adapters/base.py +25 -0
  119. ww/output_adapters/json_adapter.py +37 -0
  120. ww/output_adapters/markdown.py +2293 -0
  121. ww/output_adapters/rule_pages.py +337 -0
  122. ww/output_adapters/terminal.py +21 -0
  123. ww/package_updates.py +167 -0
  124. ww/plan/__init__.py +38 -0
  125. ww/plan/actions.py +207 -0
  126. ww/plan/compiler.py +1492 -0
  127. ww/plan/constructs.py +456 -0
  128. ww/plan/models.py +665 -0
  129. ww/project_config.py +752 -0
  130. ww/recovery.py +401 -0
  131. ww/replanning.py +367 -0
  132. ww/results.py +77 -0
  133. ww/rule_checks.py +230 -0
  134. ww/rule_conversion.py +331 -0
  135. ww/rule_disputes.py +148 -0
  136. ww/rule_store.py +456 -0
  137. ww/rule_verification.py +714 -0
  138. ww/rule_views.py +447 -0
  139. ww/rule_writes.py +920 -0
  140. ww/run_coordination.py +158 -0
  141. ww/runtimes.py +105 -0
  142. ww/service.py +4405 -0
  143. ww/setup_apply.py +428 -0
  144. ww/step_values.py +20 -0
  145. ww/storage.py +447 -0
  146. ww/storage_adapters/__init__.py +36 -0
  147. ww/storage_adapters/base.py +540 -0
  148. ww/storage_adapters/filesystem.py +370 -0
  149. ww/storage_adapters/memory.py +195 -0
  150. ww/storage_adapters/project_metadata.py +69 -0
  151. ww/storage_adapters/task_document.py +484 -0
  152. ww/task_ids.py +114 -0
  153. ww/task_references.py +124 -0
  154. ww/transitions.py +1619 -0
  155. ww/updates.py +399 -0
  156. ww/upgrade.py +95 -0
  157. ww/validation.py +168 -0
  158. ww/variables.py +275 -0
  159. ww/workflow_config.py +854 -0
  160. ww/workflow_update.py +239 -0
  161. ww/workflow_validation.py +1260 -0
  162. ww/workspace.py +50 -0
  163. ww_agentic_workflows-1.0.0.dev3.dist-info/METADATA +690 -0
  164. ww_agentic_workflows-1.0.0.dev3.dist-info/RECORD +167 -0
  165. ww_agentic_workflows-1.0.0.dev3.dist-info/WHEEL +4 -0
  166. ww_agentic_workflows-1.0.0.dev3.dist-info/entry_points.txt +2 -0
  167. ww_agentic_workflows-1.0.0.dev3.dist-info/licenses/LICENSE +674 -0
ww/rule_views.py ADDED
@@ -0,0 +1,447 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """Read-only views of rules for the ``check``, ``rule`` and ``rules`` commands.
3
+
4
+ A task's rules are read from its frozen plan, never from the configuration as
5
+ it reads now, so ``ww rule`` shows the wording the task was given. The
6
+ project's rules as declared, for ``ww rules``, come from the composed
7
+ configuration; the rule-automation store adds what ww learned about them.
8
+ Nothing here changes a task, the store, or a file; ``prune`` returns the
9
+ store with its orphans removed for the caller to save.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, replace
15
+ from pathlib import Path
16
+
17
+ from ww.config.rules import rule_source
18
+ from ww.errors import StateError
19
+ from ww.execution_models import CheckReport, ExecutionState, PlanItemExecution
20
+ from ww.instructions.builder import fix_failures
21
+ from ww.instructions.models import CheckPreview
22
+ from ww.plan import PlanItem, PlannedCheck, PlannedRule, WorkflowPlan
23
+ from ww.rule_conversion import ScriptizeState, scriptize_state
24
+ from ww.rule_disputes import DisputeEntry
25
+ from ww.rule_store import RuleAutomation, describe_command
26
+ from ww.rule_verification import to_verify
27
+ from ww.workflow_config import (
28
+ NameFilter,
29
+ RuleDefinition,
30
+ RuleGroupRef,
31
+ WorkflowConfiguration,
32
+ every_step,
33
+ )
34
+
35
+
36
+ def check_preview(
37
+ state: ExecutionState, item: PlanItem, report: CheckReport
38
+ ) -> CheckPreview:
39
+ """What ``ww check`` shows for a report it ran and did not record."""
40
+ record = state.item_executions[state.cursor]
41
+ return CheckPreview(
42
+ task_id=state.task_id,
43
+ step=item.name,
44
+ checks=len(report.results),
45
+ failures=fix_failures(item, record, report.failed),
46
+ passed=tuple(
47
+ result.id for result in report.results if result.status == "passed"
48
+ ),
49
+ not_applicable=tuple(
50
+ result.id for result in report.results if result.status == "not_applicable"
51
+ ),
52
+ judged=tuple(
53
+ rule.id for rule in to_verify(item, record) if not _covered(rule, record)
54
+ ),
55
+ waived=record.checks_waived,
56
+ all_files=report.all_files,
57
+ )
58
+
59
+
60
+ def _covered(rule: PlannedRule, record: PlanItemExecution) -> bool:
61
+ return any(rule.id in check.covers for check in record.resolved_checks)
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class RuleView:
66
+ """One rule or check of a task, in full, for ``ww rule``.
67
+
68
+ ``kind`` is ``rule`` (a rule with or without a command of its own),
69
+ ``hook`` (a ``before_complete`` hook with ``on_failure: fix``) or
70
+ ``derived`` (an approved store check the step resolved). ``steps`` are
71
+ the task's steps that carry it. For a rule without a command,
72
+ ``store_status``, ``store_check`` and ``interpretation`` are what the
73
+ rule-automation store knows about its wording, and ``resolution`` how
74
+ the current step enforces it.
75
+ """
76
+
77
+ id: str
78
+ kind: str
79
+ summary: str
80
+ text: str
81
+ steps: tuple[str, ...]
82
+ paths: tuple[str, ...] = ()
83
+ source: str | None = None
84
+ command: str | None = None
85
+ assertion: str | None = None
86
+ max_fixes: int | None = None
87
+ text_hash: str | None = None
88
+ store_status: str | None = None
89
+ store_check: str | None = None
90
+ interpretation: str | None = None
91
+ resolution: str | None = None
92
+ covers: tuple[str, ...] = ()
93
+
94
+ def to_dict(self) -> dict[str, object]:
95
+ return {
96
+ "id": self.id,
97
+ "kind": self.kind,
98
+ "summary": self.summary,
99
+ "text": self.text,
100
+ "steps": list(self.steps),
101
+ "paths": list(self.paths),
102
+ "source": self.source,
103
+ "command": self.command,
104
+ "assert": self.assertion,
105
+ "max_fixes": self.max_fixes,
106
+ "text_hash": self.text_hash,
107
+ "store_status": self.store_status,
108
+ "store_check": self.store_check,
109
+ "interpretation": self.interpretation,
110
+ "resolution": self.resolution,
111
+ "covers": list(self.covers),
112
+ }
113
+
114
+
115
+ def rule_view(
116
+ state: ExecutionState,
117
+ plan: WorkflowPlan,
118
+ automation: RuleAutomation,
119
+ rule_id: str,
120
+ ) -> RuleView:
121
+ """Find a rule or check of the task by ID, the current step first."""
122
+ order = sorted(range(len(plan.items)), key=lambda index: index != state.cursor)
123
+ steps = tuple(
124
+ dict.fromkeys(
125
+ plan.items[index].name
126
+ for index in order
127
+ if any(rule.id == rule_id for rule in plan.items[index].rules)
128
+ or any(check.id == rule_id for check in plan.items[index].checks)
129
+ )
130
+ )
131
+ for index in order:
132
+ item = plan.items[index]
133
+ record = state.item_executions[index]
134
+ rule = next((rule for rule in item.rules if rule.id == rule_id), None)
135
+ if rule is not None:
136
+ return _rule(
137
+ item, record, automation, rule, steps, current=index == state.cursor
138
+ )
139
+ check = next(
140
+ (
141
+ check
142
+ for check in (*item.checks, *record.resolved_checks)
143
+ if check.id == rule_id
144
+ ),
145
+ None,
146
+ )
147
+ if check is not None:
148
+ return _check(check, steps or (item.name,))
149
+ raise StateError(
150
+ f"task {state.task_id!r} has no rule or check {rule_id!r}; the step "
151
+ "page and the fix page name them"
152
+ )
153
+
154
+
155
+ def _rule(
156
+ item: PlanItem,
157
+ record: PlanItemExecution,
158
+ automation: RuleAutomation,
159
+ rule: PlannedRule,
160
+ steps: tuple[str, ...],
161
+ *,
162
+ current: bool,
163
+ ) -> RuleView:
164
+ own = next((check for check in item.checks if check.id == rule.id), None)
165
+ entry = None if rule.has_command else automation.rules.get(rule.text_hash)
166
+ resolution = next(
167
+ (entry for entry in record.rule_resolutions if entry.id == rule.id), None
168
+ )
169
+ derived = next(
170
+ (check for check in record.resolved_checks if rule.id in check.covers), None
171
+ )
172
+ command = own or derived
173
+ return RuleView(
174
+ id=rule.id,
175
+ kind="rule",
176
+ summary=rule.summary,
177
+ text=rule.text,
178
+ steps=steps,
179
+ paths=rule.paths,
180
+ source=rule.source,
181
+ command=describe_command(command.command) if command else None,
182
+ assertion=(
183
+ command.command.assertion.describe()
184
+ if command and command.command.assertion
185
+ else None
186
+ ),
187
+ max_fixes=rule.max_fixes,
188
+ text_hash=rule.text_hash,
189
+ store_status=entry.status if entry else None,
190
+ store_check=entry.check if entry else None,
191
+ interpretation=entry.interpretation if entry else None,
192
+ resolution=resolution.status if resolution and current else None,
193
+ )
194
+
195
+
196
+ def _check(check: PlannedCheck, steps: tuple[str, ...]) -> RuleView:
197
+ return RuleView(
198
+ id=check.id,
199
+ kind=check.source,
200
+ summary=check.summary,
201
+ text=check.summary,
202
+ steps=steps,
203
+ paths=check.paths,
204
+ command=describe_command(check.command),
205
+ assertion=(
206
+ check.command.assertion.describe() if check.command.assertion else None
207
+ ),
208
+ max_fixes=check.max_fixes,
209
+ covers=check.covers,
210
+ )
211
+
212
+
213
+ @dataclass(frozen=True)
214
+ class ListedRule:
215
+ """One declared rule: its ID, first sentence, globs, and file.
216
+
217
+ ``store_check`` names the approved rule-automation check that runs for a
218
+ rule without a command of its own, the one ``rules promote`` copies into
219
+ its file. ``scriptize`` says where the rule stands: ``command``,
220
+ ``converted``, ``not_convertible``, ``rejected`` or ``unscriptized``.
221
+ """
222
+
223
+ id: str
224
+ summary: str
225
+ paths: tuple[str, ...] = ()
226
+ has_check: bool = False
227
+ source: str | None = None
228
+ disputes: int = 0
229
+ store_check: str | None = None
230
+ scriptize: ScriptizeState = "unscriptized"
231
+
232
+ def to_dict(self) -> dict[str, object]:
233
+ return {
234
+ "id": self.id,
235
+ "summary": self.summary,
236
+ "paths": list(self.paths),
237
+ "has_check": self.has_check,
238
+ "source": self.source,
239
+ "disputes": self.disputes,
240
+ "store_check": self.store_check,
241
+ "scriptize": self.scriptize,
242
+ }
243
+
244
+
245
+ @dataclass(frozen=True)
246
+ class ListedGroup:
247
+ """One root rule group: where it applies on its own, and its rules.
248
+
249
+ ``workflows`` and ``steps`` admit every workflow or step, render as
250
+ ``"*"``, or list names; an empty one means the group applies only where a
251
+ step names it.
252
+ """
253
+
254
+ name: str
255
+ origin: str
256
+ workflows: NameFilter
257
+ steps: NameFilter
258
+ hints: dict[str, str]
259
+ rules: tuple[ListedRule, ...]
260
+
261
+ def to_dict(self) -> dict[str, object]:
262
+ return {
263
+ "name": self.name,
264
+ "origin": self.origin,
265
+ "workflows": self.workflows.to_data(),
266
+ "steps": self.steps.to_data(),
267
+ "hints": self.hints,
268
+ "rules": [rule.to_dict() for rule in self.rules],
269
+ }
270
+
271
+
272
+ @dataclass(frozen=True)
273
+ class ListedStep:
274
+ """One step's own ``rules`` list: its rules, and the groups it names."""
275
+
276
+ step: str
277
+ rules: tuple[ListedRule, ...]
278
+ groups: tuple[str, ...]
279
+
280
+ def to_dict(self) -> dict[str, object]:
281
+ return {
282
+ "step": self.step,
283
+ "rules": [rule.to_dict() for rule in self.rules],
284
+ "groups": list(self.groups),
285
+ }
286
+
287
+
288
+ @dataclass(frozen=True)
289
+ class RulesListing:
290
+ """The project's declared rules, for ``ww rules``.
291
+
292
+ ``check_guidance`` is the project's ``rules.check_guidance``, so an agent
293
+ writing a check reads it without opening ww's configuration files.
294
+ """
295
+
296
+ groups: tuple[ListedGroup, ...]
297
+ steps: tuple[ListedStep, ...]
298
+ check_guidance: str | None = None
299
+
300
+ def to_dict(self) -> dict[str, object]:
301
+ return {
302
+ "groups": [group.to_dict() for group in self.groups],
303
+ "steps": [step.to_dict() for step in self.steps],
304
+ "check_guidance": self.check_guidance,
305
+ }
306
+
307
+
308
+ def rules_listing(
309
+ configuration: WorkflowConfiguration,
310
+ root: Path,
311
+ disputes: tuple[DisputeEntry, ...] = (),
312
+ automation: RuleAutomation | None = None,
313
+ ) -> RulesListing:
314
+ """Every root group with its filters and rules, then every step's own list.
315
+
316
+ A step name repeated across workflows is listed once; ``disputes`` count
317
+ how often each rule was disputed; ``automation`` names the approved
318
+ store check of each rule without a command.
319
+ """
320
+ automation = automation if automation is not None else RuleAutomation()
321
+ counts: dict[str, int] = {}
322
+ for dispute in disputes:
323
+ counts[dispute.check] = counts.get(dispute.check, 0) + 1
324
+
325
+ def listed(rule: RuleDefinition) -> ListedRule:
326
+ return ListedRule(
327
+ id=rule.id,
328
+ summary=rule.summary,
329
+ paths=rule.paths,
330
+ has_check=rule.check is not None,
331
+ source=rule_source(rule.source, root),
332
+ disputes=counts.get(rule.id, 0),
333
+ store_check=_store_check(automation, rule),
334
+ scriptize=scriptize_state(automation, rule),
335
+ )
336
+
337
+ groups = tuple(
338
+ ListedGroup(
339
+ name=group.name,
340
+ origin=group.origin,
341
+ workflows=group.workflows,
342
+ steps=group.steps,
343
+ hints=group.hints.to_dict(),
344
+ rules=tuple(listed(rule) for rule in group.rules),
345
+ )
346
+ for group in configuration.rule_groups
347
+ )
348
+ steps: dict[str, ListedStep] = {}
349
+ for step in every_step(configuration):
350
+ if not step.rules or step.name in steps:
351
+ continue
352
+ steps[step.name] = ListedStep(
353
+ step=step.name,
354
+ rules=tuple(
355
+ listed(entry)
356
+ for entry in step.rules
357
+ if isinstance(entry, RuleDefinition)
358
+ ),
359
+ groups=tuple(
360
+ entry.name for entry in step.rules if isinstance(entry, RuleGroupRef)
361
+ ),
362
+ )
363
+ return RulesListing(groups, tuple(steps.values()))
364
+
365
+
366
+ def _store_check(automation: RuleAutomation, rule: RuleDefinition) -> str | None:
367
+ """The approved store check that runs for a rule without its own command."""
368
+ if rule.check is not None:
369
+ return None
370
+ converted = automation.converted_check(rule.text_hash)
371
+ return converted[0] if converted is not None else None
372
+
373
+
374
+ def declared_hashes(configuration: WorkflowConfiguration) -> frozenset[str]:
375
+ """The wording hash of every rule the composed configuration declares."""
376
+ return frozenset(
377
+ {rule.text_hash for group in configuration.rule_groups for rule in group.rules}
378
+ | {
379
+ entry.text_hash
380
+ for step in every_step(configuration)
381
+ for entry in step.rules
382
+ if isinstance(entry, RuleDefinition)
383
+ }
384
+ )
385
+
386
+
387
+ @dataclass(frozen=True)
388
+ class Orphans:
389
+ """Store entries no declared rule needs any more.
390
+
391
+ ``rules`` are rule hashes no rule has the wording of; ``checks`` are
392
+ checks that cover only such rules and that no remaining rule names.
393
+ """
394
+
395
+ rules: tuple[str, ...] = ()
396
+ checks: tuple[str, ...] = ()
397
+
398
+ def __bool__(self) -> bool:
399
+ return bool(self.rules or self.checks)
400
+
401
+
402
+ def orphans(automation: RuleAutomation, declared: frozenset[str]) -> Orphans:
403
+ rules = tuple(key for key in automation.rules if key not in declared)
404
+ gone = set(rules)
405
+ named = {
406
+ entry.check
407
+ for key, entry in automation.rules.items()
408
+ if key not in gone and entry.check is not None
409
+ }
410
+ checks = tuple(
411
+ name
412
+ for name, check in automation.checks.items()
413
+ if name not in named
414
+ and all(
415
+ key in gone
416
+ for key in (
417
+ *check.spec.covers,
418
+ *(check.pending.covers if check.pending is not None else ()),
419
+ )
420
+ )
421
+ )
422
+ return Orphans(rules, checks)
423
+
424
+
425
+ def prune(
426
+ automation: RuleAutomation, listed: Orphans, declared: frozenset[str]
427
+ ) -> RuleAutomation:
428
+ """The store without the listed entries that are still orphans.
429
+
430
+ An entry that stopped being an orphan since it was listed, because its
431
+ wording was declared again meanwhile, is kept; one that became an orphan
432
+ since is not removed unasked.
433
+ """
434
+ current = orphans(automation, declared)
435
+ rules = set(listed.rules) & set(current.rules)
436
+ checks = set(listed.checks) & set(current.checks)
437
+ return replace(
438
+ automation,
439
+ rules={
440
+ key: entry for key, entry in automation.rules.items() if key not in rules
441
+ },
442
+ checks={
443
+ name: check
444
+ for name, check in automation.checks.items()
445
+ if name not in checks
446
+ },
447
+ )