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
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Persisted, plan-driven workflow execution models.
|
|
3
|
+
|
|
4
|
+
These models deliberately contain no YAML or handler-resolution concepts. A
|
|
5
|
+
task starts with a compiled :class:`WorkflowPlan` snapshot and progresses only
|
|
6
|
+
through the normalized plan items in that snapshot.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
|
|
13
|
+
from ww.actions import Commands, actions
|
|
14
|
+
from ww.contracts import (
|
|
15
|
+
CheckSource,
|
|
16
|
+
ChildOperation,
|
|
17
|
+
ExecutionKind,
|
|
18
|
+
ItemAssignment,
|
|
19
|
+
ItemOperation,
|
|
20
|
+
LoopAssignment,
|
|
21
|
+
PlanItemOwner,
|
|
22
|
+
PlanItemPhase,
|
|
23
|
+
StepRole,
|
|
24
|
+
)
|
|
25
|
+
from ww.operations import decode_operation
|
|
26
|
+
from ww.plan import (
|
|
27
|
+
PlanItem,
|
|
28
|
+
PlannedCheck,
|
|
29
|
+
PlannedMode,
|
|
30
|
+
PlannedRule,
|
|
31
|
+
VerificationTarget,
|
|
32
|
+
WorkflowPlan,
|
|
33
|
+
)
|
|
34
|
+
from ww.validation import (
|
|
35
|
+
expect_bool,
|
|
36
|
+
expect_literal,
|
|
37
|
+
expect_optional_string,
|
|
38
|
+
expect_positive_int,
|
|
39
|
+
expect_string,
|
|
40
|
+
is_positive_int,
|
|
41
|
+
require_keys,
|
|
42
|
+
)
|
|
43
|
+
from ww.workflow_config import (
|
|
44
|
+
ChoiceDefinition,
|
|
45
|
+
DocumentDefinition,
|
|
46
|
+
DocumentScope,
|
|
47
|
+
DocumentUpdate,
|
|
48
|
+
ItemFieldUpdate,
|
|
49
|
+
MetadataScope,
|
|
50
|
+
ProvidedVariable,
|
|
51
|
+
RuleHints,
|
|
52
|
+
SavedMetadata,
|
|
53
|
+
)
|
|
54
|
+
from ww.workspace import Workdir
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _plan_from_dict(data: Any) -> WorkflowPlan:
|
|
58
|
+
"""Decode a persisted plan."""
|
|
59
|
+
if not isinstance(data, dict):
|
|
60
|
+
raise ValueError("plan must be a mapping")
|
|
61
|
+
required = {
|
|
62
|
+
"workflow",
|
|
63
|
+
"workflow_description",
|
|
64
|
+
"agent",
|
|
65
|
+
"task_id",
|
|
66
|
+
"modes",
|
|
67
|
+
"handoff",
|
|
68
|
+
"items",
|
|
69
|
+
}
|
|
70
|
+
require_keys(data, required, "plan")
|
|
71
|
+
if not isinstance(data["items"], list) or not isinstance(data["modes"], list):
|
|
72
|
+
raise ValueError("plan items and modes must be lists")
|
|
73
|
+
items = tuple(
|
|
74
|
+
_plan_item_from_dict(raw, item_index, data["agent"])
|
|
75
|
+
for item_index, raw in enumerate(data["items"])
|
|
76
|
+
)
|
|
77
|
+
if not all(isinstance(item, str) for item in data["modes"]):
|
|
78
|
+
raise ValueError("plan modes must be strings")
|
|
79
|
+
if not isinstance(data["handoff"], bool):
|
|
80
|
+
raise ValueError("plan handoff must be a boolean")
|
|
81
|
+
items = _checked_item_passes(items)
|
|
82
|
+
return WorkflowPlan(
|
|
83
|
+
workflow=expect_string(data["workflow"], "workflow"),
|
|
84
|
+
workflow_description=expect_string(
|
|
85
|
+
data["workflow_description"], "workflow description"
|
|
86
|
+
),
|
|
87
|
+
agent=expect_string(data["agent"], "agent"),
|
|
88
|
+
task_id=expect_optional_string(data["task_id"], "task ID"),
|
|
89
|
+
modes=tuple(data["modes"]),
|
|
90
|
+
handoff=data["handoff"],
|
|
91
|
+
items=items,
|
|
92
|
+
documents=_documents_from_list(data.get("documents", [])),
|
|
93
|
+
recommended_next_workflow=expect_optional_string(
|
|
94
|
+
data.get("recommended_next_workflow"), "recommended next workflow"
|
|
95
|
+
),
|
|
96
|
+
hooks_from=expect_optional_string(data.get("hooks_from"), "hooks_from"),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _is_item_flow_member(item: PlanItem) -> bool:
|
|
101
|
+
"""Whether a per-item stage or hook of an ``items`` step, never a child's."""
|
|
102
|
+
return item.child_stage is None and (item.item_template or item.item_id is not None)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _checked_item_passes(items: tuple[PlanItem, ...]) -> tuple[PlanItem, ...]:
|
|
106
|
+
"""Refuse a plan whose ``items`` declarations lack their pass identity.
|
|
107
|
+
|
|
108
|
+
The collection item and every per-item template must already carry
|
|
109
|
+
``item_pass``; a plan missing it was not written by a compatible ww and is
|
|
110
|
+
refused rather than guessed at.
|
|
111
|
+
"""
|
|
112
|
+
collectors = [
|
|
113
|
+
item
|
|
114
|
+
for item in items
|
|
115
|
+
if item.item_operation == "collect" and item.child_operation is None
|
|
116
|
+
]
|
|
117
|
+
members = [item for item in items if _is_item_flow_member(item)]
|
|
118
|
+
missing = [
|
|
119
|
+
item
|
|
120
|
+
for item in (*collectors, *(m for m in members if m.item_template))
|
|
121
|
+
if item.item_pass is None
|
|
122
|
+
]
|
|
123
|
+
if missing:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"plan item {missing[0].id!r} belongs to an items step but has "
|
|
126
|
+
"no item_pass; the snapshot was not written by a compatible ww, "
|
|
127
|
+
"so it cannot be resumed safely. Finish the run with the ww "
|
|
128
|
+
"that started it, or reset the task and start it again"
|
|
129
|
+
)
|
|
130
|
+
return items
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _plan_item_from_dict(raw: Any, item_index: int, default_agent: Any) -> PlanItem:
|
|
134
|
+
if not isinstance(raw, dict):
|
|
135
|
+
raise ValueError(f"plan.items[{item_index}] must be a mapping")
|
|
136
|
+
item_path = f"plan.items[{item_index}]"
|
|
137
|
+
provide = _provided_variables_from_list(raw.get("provide", []), item_path)
|
|
138
|
+
save_metadata = _saved_metadata_from_list(raw.get("save_metadata", []), item_path)
|
|
139
|
+
update_document = _document_updates_from_list(
|
|
140
|
+
raw.get("update_document", []), item_path
|
|
141
|
+
)
|
|
142
|
+
outputs = _string_list(raw.get("outputs", []), "plan item outputs")
|
|
143
|
+
ancestors = _string_list(raw.get("ancestors", []), "plan item ancestors")
|
|
144
|
+
step_ordinals = _positive_int_list(
|
|
145
|
+
raw.get("step_ordinals", []), "plan item step_ordinals"
|
|
146
|
+
)
|
|
147
|
+
dependencies = _string_list(raw.get("dependencies", []), "plan item dependencies")
|
|
148
|
+
return PlanItem(
|
|
149
|
+
id=expect_string(raw.get("id"), "plan item ID"),
|
|
150
|
+
position=expect_positive_int(raw.get("position"), "plan position"),
|
|
151
|
+
name=expect_string(raw.get("name"), "plan item name"),
|
|
152
|
+
description=expect_string(raw.get("description", ""), "description"),
|
|
153
|
+
on_failure=expect_string(raw.get("on_failure", "operator"), "on_failure"),
|
|
154
|
+
on_failure_instruction=expect_optional_string(
|
|
155
|
+
raw.get("on_failure_instruction"), "on_failure_instruction"
|
|
156
|
+
),
|
|
157
|
+
max_handler_fixes=expect_positive_int(
|
|
158
|
+
raw.get("max_handler_fixes", 3), "max_handler_fixes"
|
|
159
|
+
),
|
|
160
|
+
operation=decode_operation(raw.get("operation")),
|
|
161
|
+
owner=_plan_item_owner(raw.get("owner")),
|
|
162
|
+
execution=_execution_kind(raw.get("execution")),
|
|
163
|
+
requires_agent_input=expect_bool(
|
|
164
|
+
raw.get("requires_agent_input"), f"{item_path}.requires_agent_input"
|
|
165
|
+
),
|
|
166
|
+
workflow=expect_string(raw.get("workflow"), "workflow"),
|
|
167
|
+
step=expect_string(raw.get("step"), "step"),
|
|
168
|
+
parent=expect_optional_string(raw.get("parent"), "parent"),
|
|
169
|
+
phase=_plan_item_phase(raw.get("phase")),
|
|
170
|
+
source=expect_string(raw.get("source"), "source"),
|
|
171
|
+
registered_handler=expect_optional_string(
|
|
172
|
+
raw.get("registered_handler"), "registered handler"
|
|
173
|
+
),
|
|
174
|
+
provide=provide,
|
|
175
|
+
save_metadata=save_metadata,
|
|
176
|
+
update_document=update_document,
|
|
177
|
+
update_item=_item_field_updates_from_list(
|
|
178
|
+
raw.get("update_item", []), item_path
|
|
179
|
+
),
|
|
180
|
+
item_identity=expect_optional_string(
|
|
181
|
+
raw.get("item_identity"), f"{item_path}.item_identity"
|
|
182
|
+
),
|
|
183
|
+
item_unique=_string_list(
|
|
184
|
+
raw.get("item_unique", []), f"{item_path}.item_unique"
|
|
185
|
+
),
|
|
186
|
+
outputs=outputs,
|
|
187
|
+
dependencies=dependencies,
|
|
188
|
+
requested_agent=expect_optional_string(
|
|
189
|
+
raw.get("requested_agent", default_agent), "requested agent"
|
|
190
|
+
),
|
|
191
|
+
requested_model=expect_optional_string(
|
|
192
|
+
raw.get("requested_model", raw.get("model", "auto")), "requested model"
|
|
193
|
+
),
|
|
194
|
+
requested_reasoning=expect_optional_string(
|
|
195
|
+
raw.get("requested_reasoning", raw.get("reasoning", "auto")),
|
|
196
|
+
"requested reasoning",
|
|
197
|
+
),
|
|
198
|
+
role=cast(
|
|
199
|
+
StepRole,
|
|
200
|
+
expect_literal(raw.get("role", "worker"), StepRole, f"{item_path}.role"),
|
|
201
|
+
),
|
|
202
|
+
subagents=expect_bool(raw.get("subagents", True), f"{item_path}.subagents"),
|
|
203
|
+
interactive=expect_bool(
|
|
204
|
+
raw.get("interactive", False), f"{item_path}.interactive"
|
|
205
|
+
),
|
|
206
|
+
explicit=expect_bool(raw.get("explicit", False), f"{item_path}.explicit"),
|
|
207
|
+
learnable=expect_bool(raw.get("learnable", False), f"{item_path}.learnable"),
|
|
208
|
+
choices=_choices_from_list(raw.get("choices", []), item_path),
|
|
209
|
+
ui=expect_bool(raw.get("ui", False), f"{item_path}.ui"),
|
|
210
|
+
model=expect_optional_string(raw.get("model"), "model"),
|
|
211
|
+
reasoning=expect_optional_string(raw.get("reasoning"), "reasoning"),
|
|
212
|
+
profile=expect_optional_string(raw.get("profile"), "profile"),
|
|
213
|
+
profile_instruction=expect_optional_string(
|
|
214
|
+
raw.get("profile_instruction"), "profile instruction"
|
|
215
|
+
),
|
|
216
|
+
profile_path=expect_optional_string(raw.get("profile_path"), "profile path"),
|
|
217
|
+
workdir=_workdir(raw.get("workdir", "task")),
|
|
218
|
+
summary=expect_bool(raw.get("summary", False), f"{item_path}.summary"),
|
|
219
|
+
item_operation=_item_operation(raw.get("item_operation")),
|
|
220
|
+
item_template=expect_bool(
|
|
221
|
+
raw.get("item_template", False), f"{item_path}.item_template"
|
|
222
|
+
),
|
|
223
|
+
item_id=expect_optional_string(raw.get("item_id"), "item ID"),
|
|
224
|
+
item_pass=expect_optional_string(
|
|
225
|
+
raw.get("item_pass"), f"{item_path}.item_pass"
|
|
226
|
+
),
|
|
227
|
+
item_collect_only=expect_bool(
|
|
228
|
+
raw.get("item_collect_only", False), f"{item_path}.item_collect_only"
|
|
229
|
+
),
|
|
230
|
+
item_assignment=_item_assignment(raw.get("item_assignment", "per_step")),
|
|
231
|
+
loop_id=expect_optional_string(raw.get("loop_id"), "loop ID"),
|
|
232
|
+
loop_assignment=_loop_assignment(raw.get("loop_assignment")),
|
|
233
|
+
shared_items=expect_bool(
|
|
234
|
+
raw.get("shared_items", False), f"{item_path}.shared_items"
|
|
235
|
+
),
|
|
236
|
+
split_instruction=expect_optional_string(
|
|
237
|
+
raw.get("split_instruction"), "split instruction"
|
|
238
|
+
),
|
|
239
|
+
artifact=expect_bool(raw.get("artifact", True), f"{item_path}.artifact"),
|
|
240
|
+
child_operation=_child_operation(raw.get("child_operation")),
|
|
241
|
+
child_identity=expect_bool(
|
|
242
|
+
raw.get("child_identity", False), f"{item_path}.child_identity"
|
|
243
|
+
),
|
|
244
|
+
child_stage=expect_optional_string(
|
|
245
|
+
raw.get("child_stage"), f"{item_path}.child_stage"
|
|
246
|
+
),
|
|
247
|
+
child_number=_optional_positive_int(
|
|
248
|
+
raw.get("child_number"), f"{item_path}.child_number"
|
|
249
|
+
),
|
|
250
|
+
ancestors=ancestors,
|
|
251
|
+
step_ordinals=step_ordinals,
|
|
252
|
+
artifact_dependency=expect_optional_string(
|
|
253
|
+
raw.get("artifact_dependency"), "artifact dependency"
|
|
254
|
+
),
|
|
255
|
+
loop_break=expect_optional_string(raw.get("loop_break"), "loop break"),
|
|
256
|
+
loop_continue=expect_optional_string(raw.get("loop_continue"), "loop continue"),
|
|
257
|
+
assessment_question=expect_optional_string(
|
|
258
|
+
raw.get("assessment_question"), "assessment question"
|
|
259
|
+
),
|
|
260
|
+
assessment_outcomes=tuple(
|
|
261
|
+
_string_list(raw.get("assessment_outcomes", []), "assessment outcomes")
|
|
262
|
+
),
|
|
263
|
+
assessment_stops=tuple(
|
|
264
|
+
_string_list(raw.get("assessment_stops", []), "assessment stops")
|
|
265
|
+
),
|
|
266
|
+
assessment_parent=expect_optional_string(
|
|
267
|
+
raw.get("assessment_parent"), "assessment parent"
|
|
268
|
+
),
|
|
269
|
+
assessment_outcome=expect_optional_string(
|
|
270
|
+
raw.get("assessment_outcome"), "assessment outcome"
|
|
271
|
+
),
|
|
272
|
+
rules=_planned_rules_from_list(raw.get("rules", []), item_path),
|
|
273
|
+
checks=_planned_checks_from_list(raw.get("checks", []), item_path),
|
|
274
|
+
modes=_planned_modes_from_list(raw.get("modes", []), item_path),
|
|
275
|
+
verifies=_verification_target(raw.get("verifies"), item_path),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _verification_target(value: Any, item_path: str) -> VerificationTarget | None:
|
|
280
|
+
if value is None:
|
|
281
|
+
return None
|
|
282
|
+
path = f"{item_path}.verifies"
|
|
283
|
+
if not isinstance(value, dict) or not set(value) <= {
|
|
284
|
+
"item_id",
|
|
285
|
+
"ordinal",
|
|
286
|
+
"hints",
|
|
287
|
+
}:
|
|
288
|
+
raise ValueError(f"{path} must be an object of item_id, ordinal, hints")
|
|
289
|
+
require_keys(value, {"item_id", "ordinal"}, path)
|
|
290
|
+
return VerificationTarget(
|
|
291
|
+
item_id=expect_string(value["item_id"], f"{path}.item_id"),
|
|
292
|
+
ordinal=expect_positive_int(value["ordinal"], f"{path}.ordinal"),
|
|
293
|
+
hints=_rule_hints(value.get("hints", {}), f"{path}.hints"),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _planned_modes_from_list(value: Any, item_path: str) -> tuple[PlannedMode, ...]:
|
|
298
|
+
if not isinstance(value, list):
|
|
299
|
+
raise ValueError(f"{item_path}.modes must be a list")
|
|
300
|
+
return tuple(
|
|
301
|
+
PlannedMode.from_dict(raw, f"{item_path}.modes[{index}]")
|
|
302
|
+
for index, raw in enumerate(value)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _planned_rules_from_list(value: Any, item_path: str) -> tuple[PlannedRule, ...]:
|
|
307
|
+
if not isinstance(value, list):
|
|
308
|
+
raise ValueError(f"{item_path}.rules must be a list")
|
|
309
|
+
result = []
|
|
310
|
+
for index, raw in enumerate(value):
|
|
311
|
+
path = f"{item_path}.rules[{index}]"
|
|
312
|
+
if not isinstance(raw, dict):
|
|
313
|
+
raise ValueError(f"{path} must be an object")
|
|
314
|
+
require_keys(
|
|
315
|
+
raw,
|
|
316
|
+
{"id", "summary", "text", "text_hash", "paths", "has_command", "max_fixes"},
|
|
317
|
+
path,
|
|
318
|
+
)
|
|
319
|
+
result.append(
|
|
320
|
+
PlannedRule(
|
|
321
|
+
id=expect_string(raw["id"], f"{path}.id"),
|
|
322
|
+
summary=expect_string(raw["summary"], f"{path}.summary"),
|
|
323
|
+
text=expect_string(raw["text"], f"{path}.text"),
|
|
324
|
+
text_hash=expect_string(raw["text_hash"], f"{path}.text_hash"),
|
|
325
|
+
paths=_string_list(raw["paths"], f"{path}.paths"),
|
|
326
|
+
has_command=expect_bool(raw["has_command"], f"{path}.has_command"),
|
|
327
|
+
max_fixes=expect_positive_int(raw["max_fixes"], f"{path}.max_fixes"),
|
|
328
|
+
hints=_rule_hints(raw.get("hints", {}), f"{path}.hints"),
|
|
329
|
+
source=expect_optional_string(raw.get("source"), f"{path}.source"),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
return tuple(result)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _rule_hints(value: Any, path: str) -> RuleHints:
|
|
336
|
+
if not isinstance(value, dict) or not set(value) <= {
|
|
337
|
+
"agent",
|
|
338
|
+
"model",
|
|
339
|
+
"reasoning",
|
|
340
|
+
}:
|
|
341
|
+
raise ValueError(f"{path} must be an object of agent, model, reasoning")
|
|
342
|
+
return RuleHints(
|
|
343
|
+
expect_optional_string(value.get("agent"), f"{path}.agent"),
|
|
344
|
+
expect_optional_string(value.get("model"), f"{path}.model"),
|
|
345
|
+
expect_optional_string(value.get("reasoning"), f"{path}.reasoning"),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _planned_checks_from_list(value: Any, item_path: str) -> tuple[PlannedCheck, ...]:
|
|
350
|
+
"""Decode planned checks; resolved derived checks on records use it too."""
|
|
351
|
+
if not isinstance(value, list):
|
|
352
|
+
raise ValueError(f"{item_path}.checks must be a list")
|
|
353
|
+
result = []
|
|
354
|
+
for index, raw in enumerate(value):
|
|
355
|
+
path = f"{item_path}.checks[{index}]"
|
|
356
|
+
if not isinstance(raw, dict):
|
|
357
|
+
raise ValueError(f"{path} must be an object")
|
|
358
|
+
require_keys(
|
|
359
|
+
raw, {"id", "source", "summary", "command", "paths", "max_fixes"}, path
|
|
360
|
+
)
|
|
361
|
+
command = raw["command"]
|
|
362
|
+
if not isinstance(command, dict):
|
|
363
|
+
raise ValueError(f"{path}.command must be an object")
|
|
364
|
+
decoded = actions.get("cli").decode(command)
|
|
365
|
+
if not isinstance(decoded, Commands): # pragma: no cover - cli contract
|
|
366
|
+
raise ValueError(f"{path}.command is not a command")
|
|
367
|
+
result.append(
|
|
368
|
+
PlannedCheck(
|
|
369
|
+
id=expect_string(raw["id"], f"{path}.id"),
|
|
370
|
+
source=cast(
|
|
371
|
+
CheckSource,
|
|
372
|
+
expect_literal(raw["source"], CheckSource, f"{path}.source"),
|
|
373
|
+
),
|
|
374
|
+
summary=expect_string(raw["summary"], f"{path}.summary"),
|
|
375
|
+
command=decoded,
|
|
376
|
+
paths=_string_list(raw["paths"], f"{path}.paths"),
|
|
377
|
+
on_failure_instruction=expect_optional_string(
|
|
378
|
+
raw.get("on_failure_instruction"), "check failure instruction"
|
|
379
|
+
),
|
|
380
|
+
max_fixes=expect_positive_int(raw["max_fixes"], f"{path}.max_fixes"),
|
|
381
|
+
covers=_string_list(raw.get("covers", []), f"{path}.covers"),
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
return tuple(result)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _provided_variables_from_list(
|
|
388
|
+
value: Any, item_path: str
|
|
389
|
+
) -> tuple[ProvidedVariable, ...]:
|
|
390
|
+
if not isinstance(value, list):
|
|
391
|
+
raise ValueError(f"{item_path}.provide must be a list")
|
|
392
|
+
if not all(isinstance(item, dict) for item in value):
|
|
393
|
+
raise ValueError(f"{item_path}.provide entries must be objects")
|
|
394
|
+
return tuple(
|
|
395
|
+
ProvidedVariable(
|
|
396
|
+
expect_string(item.get("name"), "provided name"),
|
|
397
|
+
expect_optional_string(item.get("description", ""), "provided description")
|
|
398
|
+
or "",
|
|
399
|
+
)
|
|
400
|
+
for item in value
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _choices_from_list(value: Any, item_path: str) -> tuple[ChoiceDefinition, ...]:
|
|
405
|
+
if not isinstance(value, list):
|
|
406
|
+
raise ValueError(f"{item_path}.choices must be a list")
|
|
407
|
+
if not all(isinstance(item, dict) for item in value):
|
|
408
|
+
raise ValueError(f"{item_path}.choices entries must be objects")
|
|
409
|
+
return tuple(
|
|
410
|
+
ChoiceDefinition(
|
|
411
|
+
expect_string(item.get("label"), "choice label"),
|
|
412
|
+
expect_optional_string(item.get("description", ""), "choice description")
|
|
413
|
+
or "",
|
|
414
|
+
)
|
|
415
|
+
for item in value
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _document_updates_from_list(
|
|
420
|
+
value: Any, item_path: str
|
|
421
|
+
) -> tuple[DocumentUpdate, ...]:
|
|
422
|
+
if not isinstance(value, list):
|
|
423
|
+
raise ValueError(f"{item_path}.update_document must be a list")
|
|
424
|
+
if not all(isinstance(item, dict) for item in value):
|
|
425
|
+
raise ValueError(f"{item_path}.update_document entries must be objects")
|
|
426
|
+
return tuple(
|
|
427
|
+
DocumentUpdate(
|
|
428
|
+
expect_string(item.get("name"), "document update name"),
|
|
429
|
+
expect_optional_string(item.get("instruction", ""), "document instruction")
|
|
430
|
+
or "",
|
|
431
|
+
)
|
|
432
|
+
for item in value
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _documents_from_list(value: Any) -> tuple[DocumentDefinition, ...]:
|
|
437
|
+
if not isinstance(value, list):
|
|
438
|
+
raise ValueError("plan.documents must be a list")
|
|
439
|
+
if not all(isinstance(item, dict) for item in value):
|
|
440
|
+
raise ValueError("plan.documents entries must be objects")
|
|
441
|
+
return tuple(
|
|
442
|
+
DocumentDefinition(
|
|
443
|
+
expect_string(item.get("name"), "document name"),
|
|
444
|
+
expect_optional_string(item.get("description", ""), "document description")
|
|
445
|
+
or "",
|
|
446
|
+
cast(
|
|
447
|
+
DocumentScope,
|
|
448
|
+
expect_literal(
|
|
449
|
+
item.get("scope", "task"), DocumentScope, "document scope"
|
|
450
|
+
),
|
|
451
|
+
),
|
|
452
|
+
path=expect_optional_string(item.get("path"), "document path"),
|
|
453
|
+
)
|
|
454
|
+
for item in value
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _saved_metadata_from_list(value: Any, item_path: str) -> tuple[SavedMetadata, ...]:
|
|
459
|
+
if not isinstance(value, list):
|
|
460
|
+
raise ValueError(f"{item_path}.save_metadata must be a list")
|
|
461
|
+
if not all(isinstance(item, dict) for item in value):
|
|
462
|
+
raise ValueError(f"{item_path}.save_metadata entries must be objects")
|
|
463
|
+
return tuple(
|
|
464
|
+
SavedMetadata(
|
|
465
|
+
expect_string(item.get("name"), "saved metadata name"),
|
|
466
|
+
expect_string(item.get("key"), "saved metadata key"),
|
|
467
|
+
expect_optional_string(
|
|
468
|
+
item.get("description", ""), "saved metadata description"
|
|
469
|
+
)
|
|
470
|
+
or "",
|
|
471
|
+
_metadata_scope(item.get("scope", "task")),
|
|
472
|
+
append=expect_bool(item.get("append", False), "saved metadata append"),
|
|
473
|
+
source=expect_optional_string(item.get("source"), "saved metadata source"),
|
|
474
|
+
)
|
|
475
|
+
for item in value
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _item_field_updates_from_list(
|
|
480
|
+
value: Any, item_path: str
|
|
481
|
+
) -> tuple[ItemFieldUpdate, ...]:
|
|
482
|
+
if not isinstance(value, list):
|
|
483
|
+
raise ValueError(f"{item_path}.update_item must be a list")
|
|
484
|
+
if not all(isinstance(item, dict) for item in value):
|
|
485
|
+
raise ValueError(f"{item_path}.update_item entries must be objects")
|
|
486
|
+
return tuple(
|
|
487
|
+
ItemFieldUpdate(
|
|
488
|
+
expect_string(item.get("name"), "item field name"),
|
|
489
|
+
expect_optional_string(item.get("description", ""), "item field text")
|
|
490
|
+
or "",
|
|
491
|
+
)
|
|
492
|
+
for item in value
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _optional_positive_int(value: Any, context: str) -> int | None:
|
|
497
|
+
if value is None:
|
|
498
|
+
return None
|
|
499
|
+
if not is_positive_int(value):
|
|
500
|
+
raise ValueError(f"{context} must be a positive integer or null")
|
|
501
|
+
return int(value)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _string_list(value: Any, context: str) -> tuple[str, ...]:
|
|
505
|
+
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
506
|
+
raise ValueError(f"{context} must be a list of strings")
|
|
507
|
+
return tuple(value)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _positive_int_list(value: Any, context: str) -> tuple[int, ...]:
|
|
511
|
+
if not isinstance(value, list) or not all(is_positive_int(item) for item in value):
|
|
512
|
+
raise ValueError(f"{context} must be positive integers")
|
|
513
|
+
return tuple(value)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _plan_item_owner(value: Any) -> PlanItemOwner:
|
|
517
|
+
return cast(PlanItemOwner, expect_literal(value, PlanItemOwner, "plan item owner"))
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _execution_kind(value: Any) -> ExecutionKind:
|
|
521
|
+
return cast(
|
|
522
|
+
ExecutionKind, expect_literal(value, ExecutionKind, "plan item execution")
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _metadata_scope(value: Any) -> MetadataScope:
|
|
527
|
+
return cast(
|
|
528
|
+
MetadataScope, expect_literal(value, MetadataScope, "saved metadata scope")
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _plan_item_phase(value: Any) -> PlanItemPhase:
|
|
533
|
+
return cast(PlanItemPhase, expect_literal(value, PlanItemPhase, "plan item phase"))
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _loop_assignment(value: Any) -> LoopAssignment | None:
|
|
537
|
+
if value is None:
|
|
538
|
+
return None
|
|
539
|
+
return cast(
|
|
540
|
+
LoopAssignment, expect_literal(value, LoopAssignment, "loop assignment")
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _item_assignment(value: Any) -> ItemAssignment:
|
|
545
|
+
return cast(
|
|
546
|
+
ItemAssignment, expect_literal(value, ItemAssignment, "item assignment")
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _workdir(value: Any) -> Workdir:
|
|
551
|
+
return cast(Workdir, expect_literal(value, Workdir, "workdir"))
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _item_operation(value: Any) -> ItemOperation | None:
|
|
555
|
+
if value is None:
|
|
556
|
+
return None
|
|
557
|
+
return cast(ItemOperation, expect_literal(value, ItemOperation, "item operation"))
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _child_operation(value: Any) -> ChildOperation | None:
|
|
561
|
+
if value is None:
|
|
562
|
+
return None
|
|
563
|
+
return cast(
|
|
564
|
+
ChildOperation, expect_literal(value, ChildOperation, "child operation")
|
|
565
|
+
)
|