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.
- ww/__init__.py +18 -0
- ww/_bundled_extensions/ww/git/extension.py +1728 -0
- ww/action_execution.py +887 -0
- ww/actions/__init__.py +94 -0
- ww/actions/command.py +444 -0
- ww/actions/contracts.py +699 -0
- ww/actions/extension.py +197 -0
- ww/actions/mcp.py +84 -0
- ww/actions/prompt.py +74 -0
- ww/actions/skill.py +62 -0
- ww/actions/slash_command.py +63 -0
- ww/agents.py +151 -0
- ww/amendments.py +54 -0
- ww/artifacts.py +93 -0
- ww/assessments.py +181 -0
- ww/assets/__init__.py +2 -0
- ww/assets/agent_instructions.md +49 -0
- ww/assets/docs/examples.md +879 -0
- ww/assets/docs/features.md +4639 -0
- ww/assets/docs/specification.md +1876 -0
- ww/assets/noww_skill.md +11 -0
- ww/assets/workflows/catchall.yaml +26 -0
- ww/assets/workflows/onboarding.yaml +586 -0
- ww/assets/workflows/scriptize.yaml +130 -0
- ww/assets/ww-automate_skill.md +23 -0
- ww/assets/ww-deduce-feedback_skill.md +38 -0
- ww/assets/ww-feedback-rules_skill.md +48 -0
- ww/assets/ww-learn-project_skill.md +22 -0
- ww/assets/ww-refresh_skill.md +26 -0
- ww/assets/ww-rule_skill.md +83 -0
- ww/assets/ww-rules-from-artifacts_skill.md +22 -0
- ww/assets/ww-scriptize_skill.md +33 -0
- ww/assets/ww-setup_skill.md +94 -0
- ww/assets/ww-solve_skill.md +23 -0
- ww/assets/ww-suggest_skill.md +32 -0
- ww/assets/ww-wizard_skill.md +105 -0
- ww/assets/ww_skill.md +59 -0
- ww/assignments.py +283 -0
- ww/bootstrap.py +405 -0
- ww/builtin_workflows.py +215 -0
- ww/changes.py +225 -0
- ww/child_coordination.py +482 -0
- ww/children.py +106 -0
- ww/claude_permissions.py +115 -0
- ww/cli/__init__.py +7 -0
- ww/cli/__main__.py +6 -0
- ww/cli/audit.py +129 -0
- ww/cli/catalogs.py +131 -0
- ww/cli/discover.py +607 -0
- ww/cli/initialization.py +898 -0
- ww/cli/lookup.py +287 -0
- ww/cli/main.py +1768 -0
- ww/cli/parser.py +1200 -0
- ww/cli/prompts.py +217 -0
- ww/cli/updates.py +117 -0
- ww/completion_artifacts.py +156 -0
- ww/completion_inputs.py +39 -0
- ww/config/__init__.py +582 -0
- ww/config/actions.py +591 -0
- ww/config/composition.py +571 -0
- ww/config/rules.py +511 -0
- ww/config/steps.py +1220 -0
- ww/config/values.py +223 -0
- ww/config_files.py +191 -0
- ww/config_writes.py +264 -0
- ww/contracts.py +155 -0
- ww/control.py +41 -0
- ww/defaults.py +130 -0
- ww/design_docs.py +32 -0
- ww/discovery.py +104 -0
- ww/documents.py +217 -0
- ww/errors.py +18 -0
- ww/executable.py +43 -0
- ww/execution_models/__init__.py +64 -0
- ww/execution_models/construction.py +148 -0
- ww/execution_models/decoding.py +38 -0
- ww/execution_models/plan_codec.py +565 -0
- ww/execution_models/records.py +1206 -0
- ww/execution_models/runs.py +266 -0
- ww/extensions/__init__.py +40 -0
- ww/extensions/api.py +559 -0
- ww/extensions/registry.py +864 -0
- ww/extensions/store.py +78 -0
- ww/feedback.py +342 -0
- ww/handler_repairs.py +57 -0
- ww/hooks/__init__.py +40 -0
- ww/hooks/agents.py +380 -0
- ww/hooks/install.py +168 -0
- ww/hooks/notices.py +206 -0
- ww/hooks/records.py +209 -0
- ww/hooks/runtime.py +266 -0
- ww/hooks/transcripts.py +183 -0
- ww/inspect.py +896 -0
- ww/instructions/__init__.py +17 -0
- ww/instructions/builder.py +1682 -0
- ww/instructions/commands.py +335 -0
- ww/instructions/handoff.py +149 -0
- ww/instructions/models.py +686 -0
- ww/instructions/policy.py +219 -0
- ww/instructions/text.py +168 -0
- ww/interactions.py +187 -0
- ww/interpolation.py +37 -0
- ww/item_passes.py +167 -0
- ww/items.py +99 -0
- ww/locking.py +207 -0
- ww/metadata_publication.py +230 -0
- ww/onboarding.py +229 -0
- ww/open_work.py +236 -0
- ww/operations.py +193 -0
- ww/operator_ui/__init__.py +16 -0
- ww/operator_ui/page.html +351 -0
- ww/operator_ui/server.py +215 -0
- ww/operator_ui/session.py +389 -0
- ww/operator_ui/sheet.py +104 -0
- ww/operator_ui/view.py +109 -0
- ww/output.py +339 -0
- ww/output_adapters/__init__.py +12 -0
- ww/output_adapters/base.py +25 -0
- ww/output_adapters/json_adapter.py +37 -0
- ww/output_adapters/markdown.py +2293 -0
- ww/output_adapters/rule_pages.py +337 -0
- ww/output_adapters/terminal.py +21 -0
- ww/package_updates.py +167 -0
- ww/plan/__init__.py +38 -0
- ww/plan/actions.py +207 -0
- ww/plan/compiler.py +1492 -0
- ww/plan/constructs.py +456 -0
- ww/plan/models.py +665 -0
- ww/project_config.py +752 -0
- ww/recovery.py +401 -0
- ww/replanning.py +367 -0
- ww/results.py +77 -0
- ww/rule_checks.py +230 -0
- ww/rule_conversion.py +331 -0
- ww/rule_disputes.py +148 -0
- ww/rule_store.py +456 -0
- ww/rule_verification.py +714 -0
- ww/rule_views.py +447 -0
- ww/rule_writes.py +920 -0
- ww/run_coordination.py +158 -0
- ww/runtimes.py +105 -0
- ww/service.py +4405 -0
- ww/setup_apply.py +428 -0
- ww/step_values.py +20 -0
- ww/storage.py +447 -0
- ww/storage_adapters/__init__.py +36 -0
- ww/storage_adapters/base.py +540 -0
- ww/storage_adapters/filesystem.py +370 -0
- ww/storage_adapters/memory.py +195 -0
- ww/storage_adapters/project_metadata.py +69 -0
- ww/storage_adapters/task_document.py +484 -0
- ww/task_ids.py +114 -0
- ww/task_references.py +124 -0
- ww/transitions.py +1619 -0
- ww/updates.py +399 -0
- ww/upgrade.py +95 -0
- ww/validation.py +168 -0
- ww/variables.py +275 -0
- ww/workflow_config.py +854 -0
- ww/workflow_update.py +239 -0
- ww/workflow_validation.py +1260 -0
- ww/workspace.py +50 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/METADATA +690 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/RECORD +167 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/WHEEL +4 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/entry_points.txt +2 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/licenses/LICENSE +674 -0
ww/config/__init__.py
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Strict loading for the normalized ``ww.yaml`` schema."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from ww.config_files import configuration_file_exists
|
|
13
|
+
from ww.errors import ConfigurationError
|
|
14
|
+
from ww.extensions import ExtensionRegistry
|
|
15
|
+
from ww.runtimes import RUNTIME_INSTRUCTIONS
|
|
16
|
+
from ww.workflow_config import (
|
|
17
|
+
ALL,
|
|
18
|
+
DOCUMENT_SCOPES,
|
|
19
|
+
DocumentDefinition,
|
|
20
|
+
DocumentScope,
|
|
21
|
+
HandlerDefinition,
|
|
22
|
+
HookDefinition,
|
|
23
|
+
ModeDefinition,
|
|
24
|
+
NameFilter,
|
|
25
|
+
ProfileDefinition,
|
|
26
|
+
RuleGroup,
|
|
27
|
+
StepDefinition,
|
|
28
|
+
WorkflowConfiguration,
|
|
29
|
+
WorkflowDefinition,
|
|
30
|
+
)
|
|
31
|
+
from ww.workflow_validation import validate_configuration
|
|
32
|
+
|
|
33
|
+
from .actions import _parse_hooks
|
|
34
|
+
from .composition import compose_configuration
|
|
35
|
+
from .rules import RuleGroupContribution, parse_rules_root, resolve_step_rules
|
|
36
|
+
from .steps import _parse_handlers, _parse_step
|
|
37
|
+
from .values import (
|
|
38
|
+
_NAME,
|
|
39
|
+
_description,
|
|
40
|
+
_description_items,
|
|
41
|
+
_mapping,
|
|
42
|
+
_name,
|
|
43
|
+
_name_filter,
|
|
44
|
+
_named_entry,
|
|
45
|
+
_only,
|
|
46
|
+
_optional_agent,
|
|
47
|
+
_optional_string,
|
|
48
|
+
_profile,
|
|
49
|
+
_required_list,
|
|
50
|
+
_role,
|
|
51
|
+
_string_list,
|
|
52
|
+
_subagents,
|
|
53
|
+
_unique,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class YamlConfigurationLoader:
|
|
59
|
+
"""The built-in ``ww.yaml`` notation frontend.
|
|
60
|
+
|
|
61
|
+
``extensions`` supply the rule groups the project's configured extensions
|
|
62
|
+
ship, merged under the root ``rules`` before any step names them.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
path: Path
|
|
66
|
+
extensions: ExtensionRegistry | None = None
|
|
67
|
+
|
|
68
|
+
def __call__(self) -> WorkflowConfiguration:
|
|
69
|
+
return parse_yaml_configuration(self.path, self.extensions)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def parse_yaml_configuration(
|
|
73
|
+
path: Path, extensions: ExtensionRegistry | None = None
|
|
74
|
+
) -> WorkflowConfiguration:
|
|
75
|
+
"""Load a normalized configuration without deriving execution behavior.
|
|
76
|
+
|
|
77
|
+
Parsing checks the YAML notation's shape. Cross-definition semantics are
|
|
78
|
+
deliberately checked by :func:`validate_configuration` after any frontend
|
|
79
|
+
has produced this same normalized model. The files ``path`` imports are
|
|
80
|
+
composed into it first, so the parser reads one document; rule paths
|
|
81
|
+
resolve against ``path``'s directory, where composition rebases them.
|
|
82
|
+
"""
|
|
83
|
+
if not configuration_file_exists(path):
|
|
84
|
+
raise ConfigurationError(f"workflow configuration not found: {path}")
|
|
85
|
+
composed = compose_configuration(path)
|
|
86
|
+
configuration = parse_yaml_text(
|
|
87
|
+
composed.text,
|
|
88
|
+
str(path),
|
|
89
|
+
base=path.parent,
|
|
90
|
+
extension_rule_groups=(
|
|
91
|
+
extensions.rule_groups() if extensions is not None else ()
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
return replace(
|
|
95
|
+
configuration,
|
|
96
|
+
workflow_provenance=composed.workflow_provenance,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_yaml_text(
|
|
101
|
+
text: str,
|
|
102
|
+
source: str = "<string>",
|
|
103
|
+
*,
|
|
104
|
+
base: Path | None = None,
|
|
105
|
+
extension_rule_groups: tuple[tuple[str, RuleGroupContribution], ...] = (),
|
|
106
|
+
) -> WorkflowConfiguration:
|
|
107
|
+
"""Parse ``ww.yaml`` notation held in memory, named ``source``.
|
|
108
|
+
|
|
109
|
+
``base`` is the directory rule paths resolve against; it defaults to the
|
|
110
|
+
current directory.
|
|
111
|
+
"""
|
|
112
|
+
raw = _raw_from_text(text, source)
|
|
113
|
+
base = base if base is not None else Path.cwd()
|
|
114
|
+
modes = _parse_modes(raw.get("modes", []))
|
|
115
|
+
profiles = _parse_profiles(raw.get("profiles", {}))
|
|
116
|
+
rule_groups = parse_rules_root(raw.get("rules"), base, extension_rule_groups)
|
|
117
|
+
handlers = _parse_handlers(raw.get("handlers", []))
|
|
118
|
+
workflows_raw = _required_list(raw, "workflows", "configuration")
|
|
119
|
+
global_hooks = _parse_hooks(raw.get("hooks", {}), "global", "hooks")
|
|
120
|
+
handlers_by_name = {handler.name: handler for handler in handlers}
|
|
121
|
+
parsed = tuple(
|
|
122
|
+
_parse_workflow(item, f"workflows[{index}]", handlers_by_name)
|
|
123
|
+
for index, item in enumerate(workflows_raw)
|
|
124
|
+
)
|
|
125
|
+
workflows = _resolve_explicit(_resolve_inheritance(parsed))
|
|
126
|
+
handlers = _resolve_explicit_handlers(handlers)
|
|
127
|
+
workflows, handlers = resolve_step_rules(workflows, handlers, rule_groups, base)
|
|
128
|
+
return WorkflowConfiguration(
|
|
129
|
+
_extend_modes_to_heirs(modes, workflows),
|
|
130
|
+
profiles,
|
|
131
|
+
handlers,
|
|
132
|
+
_extend_to_heirs(global_hooks, workflows),
|
|
133
|
+
workflows,
|
|
134
|
+
documents=_parse_documents(raw.get("documents", [])),
|
|
135
|
+
rule_groups=_extend_groups_to_heirs(rule_groups, workflows),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def load_configuration(
|
|
140
|
+
path: Path, extensions: ExtensionRegistry | None = None
|
|
141
|
+
) -> WorkflowConfiguration:
|
|
142
|
+
"""Load and semantically validate the built-in YAML notation."""
|
|
143
|
+
return validate_configuration(
|
|
144
|
+
YamlConfigurationLoader(path, extensions)(), extensions
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def load_modes(
|
|
149
|
+
path: Path, extensions: ExtensionRegistry | None = None
|
|
150
|
+
) -> dict[str, ModeDefinition]:
|
|
151
|
+
return {mode.name: mode for mode in load_configuration(path, extensions).modes}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _raw_from_text(text: str, source: str) -> dict[str, Any]:
|
|
155
|
+
try:
|
|
156
|
+
raw = yaml.safe_load(text)
|
|
157
|
+
except yaml.YAMLError as error:
|
|
158
|
+
raise ConfigurationError(f"invalid YAML in {source}: {error}") from error
|
|
159
|
+
if not isinstance(raw, dict):
|
|
160
|
+
raise ConfigurationError("workflow configuration must be a mapping")
|
|
161
|
+
allowed = {
|
|
162
|
+
"modes",
|
|
163
|
+
"profiles",
|
|
164
|
+
"documents",
|
|
165
|
+
"handlers",
|
|
166
|
+
"hooks",
|
|
167
|
+
"workflows",
|
|
168
|
+
"rules",
|
|
169
|
+
}
|
|
170
|
+
unknown = set(raw) - allowed
|
|
171
|
+
if unknown:
|
|
172
|
+
raise ConfigurationError(
|
|
173
|
+
f"configuration has unknown key(s): {', '.join(sorted(unknown))}"
|
|
174
|
+
)
|
|
175
|
+
return raw
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _parse_modes(data: Any) -> tuple[ModeDefinition, ...]:
|
|
179
|
+
if not isinstance(data, list):
|
|
180
|
+
raise ConfigurationError("modes must be a list")
|
|
181
|
+
result = []
|
|
182
|
+
for index, item in enumerate(data):
|
|
183
|
+
path = f"modes[{index}]"
|
|
184
|
+
allowed = {"name", "description", "workflows", "steps"}
|
|
185
|
+
mapping = _named_entry(
|
|
186
|
+
_mapping(item, path),
|
|
187
|
+
path,
|
|
188
|
+
allowed=allowed,
|
|
189
|
+
ignored={"workflows", "steps"},
|
|
190
|
+
)
|
|
191
|
+
_only(mapping, allowed, path)
|
|
192
|
+
name = _name(mapping, path)
|
|
193
|
+
# A mode's ``[]`` admits every name, as a hook's does.
|
|
194
|
+
filters = {
|
|
195
|
+
key: _name_filter(mapping[key], f"mode {name!r} {key}", empty=ALL)
|
|
196
|
+
for key in ("workflows", "steps")
|
|
197
|
+
if key in mapping
|
|
198
|
+
}
|
|
199
|
+
result.append(
|
|
200
|
+
ModeDefinition(
|
|
201
|
+
name,
|
|
202
|
+
_description_items(mapping.get("description"), f"mode {name!r}"),
|
|
203
|
+
workflows=filters.get("workflows"),
|
|
204
|
+
steps=filters.get("steps"),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
return tuple(result)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _parse_documents(data: Any) -> tuple[DocumentDefinition, ...]:
|
|
211
|
+
"""Parse root ``documents``: named entries with a description and scope."""
|
|
212
|
+
if data is None:
|
|
213
|
+
return ()
|
|
214
|
+
if not isinstance(data, list):
|
|
215
|
+
raise ConfigurationError("documents must be a list")
|
|
216
|
+
result = []
|
|
217
|
+
for index, item in enumerate(data):
|
|
218
|
+
path = f"documents[{index}]"
|
|
219
|
+
mapping = _named_entry(_mapping(item, path), path)
|
|
220
|
+
_only(mapping, {"name", "description", "scope", "path"}, path)
|
|
221
|
+
scope = mapping.get("scope", "task")
|
|
222
|
+
if scope not in DOCUMENT_SCOPES:
|
|
223
|
+
raise ConfigurationError(
|
|
224
|
+
f"{path}.scope must be 'task', 'project', or 'user'"
|
|
225
|
+
)
|
|
226
|
+
try:
|
|
227
|
+
result.append(
|
|
228
|
+
DocumentDefinition(
|
|
229
|
+
_name(mapping, path),
|
|
230
|
+
_description(mapping.get("description"), path),
|
|
231
|
+
cast(DocumentScope, scope),
|
|
232
|
+
path=_optional_string(mapping, "path", path),
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
except ValueError as error:
|
|
236
|
+
raise ConfigurationError(f"{path}: {error}") from error
|
|
237
|
+
_unique((item.name for item in result), "document")
|
|
238
|
+
return tuple(result)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _parse_profiles(data: Any) -> tuple[ProfileDefinition, ...]:
|
|
242
|
+
if data is None:
|
|
243
|
+
return ()
|
|
244
|
+
if not isinstance(data, dict):
|
|
245
|
+
raise ConfigurationError("profiles must be a mapping")
|
|
246
|
+
result = []
|
|
247
|
+
for name, description in data.items():
|
|
248
|
+
if not isinstance(name, str) or not _NAME.fullmatch(name):
|
|
249
|
+
raise ConfigurationError("profiles keys must be normalized names")
|
|
250
|
+
if description is not None and (
|
|
251
|
+
not isinstance(description, str) or not description.strip()
|
|
252
|
+
):
|
|
253
|
+
raise ConfigurationError(f"profiles.{name} must be a string or null")
|
|
254
|
+
result.append(ProfileDefinition(name, description))
|
|
255
|
+
return tuple(result)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# The workflow keys an inheriting workflow may set, and the definition fields
|
|
259
|
+
# each one overrides; everything else comes from the workflow it inherits.
|
|
260
|
+
_OVERRIDES = {
|
|
261
|
+
"description": ("description",),
|
|
262
|
+
"modes": ("modes",),
|
|
263
|
+
"agent": ("agent",),
|
|
264
|
+
"model": ("model",),
|
|
265
|
+
"reasoning": ("reasoning",),
|
|
266
|
+
"profile": ("profile", "profile_description"),
|
|
267
|
+
"role": ("role",),
|
|
268
|
+
"subagents": ("subagents",),
|
|
269
|
+
"explicit": ("explicit",),
|
|
270
|
+
"runtime": ("runtime",),
|
|
271
|
+
"restartable": ("restartable",),
|
|
272
|
+
"recommended_next_workflow": ("recommended_next_workflow",),
|
|
273
|
+
"hooks_from": ("hooks_from",),
|
|
274
|
+
"needs_hooks_from": ("needs_hooks_from",),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@dataclass(frozen=True)
|
|
279
|
+
class _ParsedWorkflow:
|
|
280
|
+
"""One workflow entry, and which of its fields the entry set itself."""
|
|
281
|
+
|
|
282
|
+
definition: WorkflowDefinition
|
|
283
|
+
overrides: frozenset[str]
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _parse_workflow(
|
|
287
|
+
data: Any, path: str, handlers_by_name: dict[str, HandlerDefinition]
|
|
288
|
+
) -> _ParsedWorkflow:
|
|
289
|
+
workflow_keys = {
|
|
290
|
+
"name",
|
|
291
|
+
"description",
|
|
292
|
+
"steps",
|
|
293
|
+
"hooks",
|
|
294
|
+
"modes",
|
|
295
|
+
"agent",
|
|
296
|
+
"model",
|
|
297
|
+
"reasoning",
|
|
298
|
+
"profile",
|
|
299
|
+
"role",
|
|
300
|
+
"subagents",
|
|
301
|
+
"explicit",
|
|
302
|
+
"runtime",
|
|
303
|
+
"restartable",
|
|
304
|
+
"inherit",
|
|
305
|
+
"recommended_next_workflow",
|
|
306
|
+
"hooks_from",
|
|
307
|
+
"needs_hooks_from",
|
|
308
|
+
}
|
|
309
|
+
mapping = _named_entry(
|
|
310
|
+
_mapping(data, path),
|
|
311
|
+
path,
|
|
312
|
+
allowed=workflow_keys,
|
|
313
|
+
ignored={"steps"},
|
|
314
|
+
)
|
|
315
|
+
if "workflows" in mapping:
|
|
316
|
+
raise ConfigurationError(
|
|
317
|
+
f"{path}.workflows defines nested workflows, which are not supported"
|
|
318
|
+
)
|
|
319
|
+
_only(mapping, workflow_keys, path)
|
|
320
|
+
name = _name(mapping, path)
|
|
321
|
+
runtime = mapping.get("runtime")
|
|
322
|
+
if runtime is not None and runtime not in RUNTIME_INSTRUCTIONS:
|
|
323
|
+
raise ConfigurationError(
|
|
324
|
+
f"{path}.runtime must be one of: " + ", ".join(RUNTIME_INSTRUCTIONS)
|
|
325
|
+
)
|
|
326
|
+
restartable = mapping.get("restartable", False)
|
|
327
|
+
if not isinstance(restartable, bool):
|
|
328
|
+
raise ConfigurationError(f"{path}.restartable must be true or false")
|
|
329
|
+
inherits = mapping.get("inherit")
|
|
330
|
+
if inherits is not None and (not isinstance(inherits, str) or not inherits):
|
|
331
|
+
raise ConfigurationError(f"{path}.inherit must name a workflow")
|
|
332
|
+
if inherits is not None:
|
|
333
|
+
# A workflow that needs other steps or hooks is a workflow of its
|
|
334
|
+
# own, not a copy of another one.
|
|
335
|
+
declared = sorted({"steps", "hooks"}.intersection(mapping))
|
|
336
|
+
if declared:
|
|
337
|
+
raise ConfigurationError(
|
|
338
|
+
f"workflow {name!r} inherits {inherits!r} and cannot declare "
|
|
339
|
+
+ " or ".join(declared)
|
|
340
|
+
)
|
|
341
|
+
steps: tuple[StepDefinition, ...] = ()
|
|
342
|
+
else:
|
|
343
|
+
steps_data = _required_list(mapping, "steps", f"workflow {name!r}")
|
|
344
|
+
steps = tuple(
|
|
345
|
+
_parse_step(item, f"{path}.steps[{index}]", handlers_by_name)
|
|
346
|
+
for index, item in enumerate(steps_data)
|
|
347
|
+
)
|
|
348
|
+
recommended = mapping.get("recommended_next_workflow")
|
|
349
|
+
if recommended is not None and (
|
|
350
|
+
not isinstance(recommended, str) or not recommended.strip()
|
|
351
|
+
):
|
|
352
|
+
raise ConfigurationError(
|
|
353
|
+
f"{path}.recommended_next_workflow must name a workflow"
|
|
354
|
+
)
|
|
355
|
+
hooks_from = mapping.get("hooks_from")
|
|
356
|
+
if hooks_from is not None and (
|
|
357
|
+
not isinstance(hooks_from, str) or not hooks_from.strip()
|
|
358
|
+
):
|
|
359
|
+
raise ConfigurationError(f"{path}.hooks_from must name a workflow")
|
|
360
|
+
needs_hooks_from = mapping.get("needs_hooks_from", False)
|
|
361
|
+
if not isinstance(needs_hooks_from, bool):
|
|
362
|
+
raise ConfigurationError(f"{path}.needs_hooks_from must be true or false")
|
|
363
|
+
definition = WorkflowDefinition(
|
|
364
|
+
name=name,
|
|
365
|
+
description=_description(mapping.get("description"), f"workflow {name!r}"),
|
|
366
|
+
steps=steps,
|
|
367
|
+
hooks=_parse_hooks(
|
|
368
|
+
mapping.get("hooks", {}), "workflow", f"workflow {name!r} hooks"
|
|
369
|
+
),
|
|
370
|
+
modes=_string_list(mapping.get("modes", []), f"workflow {name!r} modes"),
|
|
371
|
+
agent=_optional_agent(mapping, "agent", f"workflow {name!r}"),
|
|
372
|
+
model=_optional_string(mapping, "model", f"workflow {name!r}"),
|
|
373
|
+
reasoning=_optional_string(mapping, "reasoning", f"workflow {name!r}"),
|
|
374
|
+
**_profile(mapping, f"workflow {name!r}"),
|
|
375
|
+
role=_role(mapping, f"workflow {name!r}"),
|
|
376
|
+
subagents=_subagents(mapping, f"workflow {name!r}"),
|
|
377
|
+
explicit=_optional_bool(mapping, "explicit", f"workflow {name!r}"),
|
|
378
|
+
runtime=runtime,
|
|
379
|
+
restartable=restartable,
|
|
380
|
+
inherits=inherits,
|
|
381
|
+
recommended_next_workflow=recommended,
|
|
382
|
+
hooks_from=hooks_from,
|
|
383
|
+
needs_hooks_from=needs_hooks_from,
|
|
384
|
+
)
|
|
385
|
+
return _ParsedWorkflow(
|
|
386
|
+
definition,
|
|
387
|
+
frozenset(
|
|
388
|
+
field
|
|
389
|
+
for key, fields in _OVERRIDES.items()
|
|
390
|
+
if key in mapping
|
|
391
|
+
for field in fields
|
|
392
|
+
),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _resolve_inheritance(
|
|
397
|
+
parsed: tuple[_ParsedWorkflow, ...],
|
|
398
|
+
) -> tuple[WorkflowDefinition, ...]:
|
|
399
|
+
"""Complete every inheriting workflow from the one it names, in order.
|
|
400
|
+
|
|
401
|
+
The copy takes everything, steps and workflow hooks included, and the
|
|
402
|
+
entry's own settings replace the copied ones. Chains resolve from the
|
|
403
|
+
root; a cycle or an unknown name is an error.
|
|
404
|
+
"""
|
|
405
|
+
entries = {entry.definition.name: entry for entry in parsed}
|
|
406
|
+
if len(entries) != len(parsed):
|
|
407
|
+
# Duplicate names are reported by validation, with its usual message.
|
|
408
|
+
return tuple(entry.definition for entry in parsed)
|
|
409
|
+
resolved: dict[str, WorkflowDefinition] = {}
|
|
410
|
+
|
|
411
|
+
def resolve(name: str, chain: tuple[str, ...]) -> WorkflowDefinition:
|
|
412
|
+
if name in resolved:
|
|
413
|
+
return resolved[name]
|
|
414
|
+
entry = entries[name]
|
|
415
|
+
parent = entry.definition.inherits
|
|
416
|
+
if parent is None:
|
|
417
|
+
resolved[name] = entry.definition
|
|
418
|
+
return entry.definition
|
|
419
|
+
if parent not in entries:
|
|
420
|
+
raise ConfigurationError(
|
|
421
|
+
f"workflow {name!r} inherits unknown workflow {parent!r}"
|
|
422
|
+
)
|
|
423
|
+
if parent in chain:
|
|
424
|
+
cycle = " -> ".join((*chain, name, parent))
|
|
425
|
+
raise ConfigurationError(f"workflow inheritance cycle: {cycle}")
|
|
426
|
+
base = resolve(parent, (*chain, name))
|
|
427
|
+
resolved[name] = replace(
|
|
428
|
+
base,
|
|
429
|
+
name=name,
|
|
430
|
+
inherits=parent,
|
|
431
|
+
**{
|
|
432
|
+
field: getattr(entry.definition, field)
|
|
433
|
+
for field in sorted(entry.overrides)
|
|
434
|
+
},
|
|
435
|
+
)
|
|
436
|
+
return resolved[name]
|
|
437
|
+
|
|
438
|
+
return tuple(resolve(entry.definition.name, ()) for entry in parsed)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _optional_bool(mapping: dict[str, Any], key: str, path: str) -> bool | None:
|
|
442
|
+
if key not in mapping:
|
|
443
|
+
return None
|
|
444
|
+
value = mapping[key]
|
|
445
|
+
if not isinstance(value, bool):
|
|
446
|
+
raise ConfigurationError(f"{path}.{key} must be true or false")
|
|
447
|
+
return value
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _resolve_explicit(
|
|
451
|
+
workflows: tuple[WorkflowDefinition, ...],
|
|
452
|
+
) -> tuple[WorkflowDefinition, ...]:
|
|
453
|
+
"""Freeze inherited visibility guidance after workflow inheritance resolves."""
|
|
454
|
+
resolved = []
|
|
455
|
+
for workflow in workflows:
|
|
456
|
+
explicit = workflow.explicit if workflow.explicit is not None else False
|
|
457
|
+
resolved.append(
|
|
458
|
+
replace(
|
|
459
|
+
workflow,
|
|
460
|
+
explicit=explicit,
|
|
461
|
+
steps=_resolve_explicit_steps(workflow.steps, explicit),
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
return tuple(resolved)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _resolve_explicit_handlers(
|
|
468
|
+
handlers: tuple[HandlerDefinition, ...],
|
|
469
|
+
) -> tuple[HandlerDefinition, ...]:
|
|
470
|
+
"""Give reusable step handlers a default after workflow references resolve."""
|
|
471
|
+
return tuple(
|
|
472
|
+
_resolve_explicit_steps((handler,), False)[0]
|
|
473
|
+
if isinstance(handler, StepDefinition)
|
|
474
|
+
else handler
|
|
475
|
+
for handler in handlers
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _resolve_explicit_steps(
|
|
480
|
+
values: tuple[StepDefinition, ...], parent: bool
|
|
481
|
+
) -> tuple[StepDefinition, ...]:
|
|
482
|
+
result = []
|
|
483
|
+
for step in values:
|
|
484
|
+
effective = step.explicit if step.explicit is not None else parent
|
|
485
|
+
items = (
|
|
486
|
+
replace(
|
|
487
|
+
step.items,
|
|
488
|
+
steps=_resolve_explicit_steps(step.items.steps, effective),
|
|
489
|
+
)
|
|
490
|
+
if step.items is not None
|
|
491
|
+
else None
|
|
492
|
+
)
|
|
493
|
+
children = (
|
|
494
|
+
replace(
|
|
495
|
+
step.children,
|
|
496
|
+
steps=_resolve_explicit_steps(step.children.steps, effective),
|
|
497
|
+
)
|
|
498
|
+
if step.children is not None
|
|
499
|
+
else None
|
|
500
|
+
)
|
|
501
|
+
result.append(
|
|
502
|
+
replace(
|
|
503
|
+
step,
|
|
504
|
+
explicit=effective,
|
|
505
|
+
child_steps=_resolve_explicit_steps(step.child_steps, effective),
|
|
506
|
+
loop_steps=_resolve_explicit_steps(step.loop_steps, effective),
|
|
507
|
+
assessment_outcomes=_resolve_explicit_steps(
|
|
508
|
+
step.assessment_outcomes, effective
|
|
509
|
+
),
|
|
510
|
+
items=items,
|
|
511
|
+
children=children,
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
return tuple(result)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _extend_to_heirs(
|
|
518
|
+
hooks: tuple[HookDefinition, ...], workflows: tuple[WorkflowDefinition, ...]
|
|
519
|
+
) -> tuple[HookDefinition, ...]:
|
|
520
|
+
"""Let a global hook filtered to a workflow also run for its heirs.
|
|
521
|
+
|
|
522
|
+
Inheriting a workflow means behaving like it, so a hook written for
|
|
523
|
+
``hotfix`` also runs for a ``bugfix`` that inherits it.
|
|
524
|
+
"""
|
|
525
|
+
return tuple(
|
|
526
|
+
replace(hook, workflows=_with_heirs(hook.workflows, workflows))
|
|
527
|
+
if hook.workflows.listed
|
|
528
|
+
else hook
|
|
529
|
+
for hook in hooks
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _extend_groups_to_heirs(
|
|
534
|
+
groups: tuple[RuleGroup, ...], workflows: tuple[WorkflowDefinition, ...]
|
|
535
|
+
) -> tuple[RuleGroup, ...]:
|
|
536
|
+
"""Let a rule group filtered to a workflow also apply to its heirs."""
|
|
537
|
+
return tuple(
|
|
538
|
+
replace(group, workflows=_with_heirs(group.workflows, workflows))
|
|
539
|
+
if group.workflows.listed
|
|
540
|
+
else group
|
|
541
|
+
for group in groups
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _extend_modes_to_heirs(
|
|
546
|
+
modes: tuple[ModeDefinition, ...], workflows: tuple[WorkflowDefinition, ...]
|
|
547
|
+
) -> tuple[ModeDefinition, ...]:
|
|
548
|
+
"""Let a mode filtered to a workflow also apply automatically to its heirs."""
|
|
549
|
+
return tuple(
|
|
550
|
+
replace(mode, workflows=_with_heirs(mode.workflows, workflows))
|
|
551
|
+
if mode.workflows is not None and mode.workflows.listed
|
|
552
|
+
else mode
|
|
553
|
+
for mode in modes
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _with_heirs(
|
|
558
|
+
selected: NameFilter, workflows: tuple[WorkflowDefinition, ...]
|
|
559
|
+
) -> NameFilter:
|
|
560
|
+
"""``selected`` followed by every workflow that inherits one of them."""
|
|
561
|
+
names = selected.listed
|
|
562
|
+
parents = {workflow.name: workflow.inherits for workflow in workflows}
|
|
563
|
+
|
|
564
|
+
def lineage(name: str) -> set[str]:
|
|
565
|
+
found: set[str] = set()
|
|
566
|
+
current: str | None = name
|
|
567
|
+
while current is not None and current not in found:
|
|
568
|
+
found.add(current)
|
|
569
|
+
current = parents.get(current)
|
|
570
|
+
return found
|
|
571
|
+
|
|
572
|
+
return NameFilter.of(
|
|
573
|
+
(
|
|
574
|
+
*names,
|
|
575
|
+
*(
|
|
576
|
+
workflow.name
|
|
577
|
+
for workflow in workflows
|
|
578
|
+
if workflow.name not in names
|
|
579
|
+
and lineage(workflow.name).intersection(names)
|
|
580
|
+
),
|
|
581
|
+
)
|
|
582
|
+
)
|