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/bootstrap.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Bootstrap external task IDs before a workflow run exists."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Protocol, cast
|
|
8
|
+
|
|
9
|
+
from ww.artifacts import render_step_artifact
|
|
10
|
+
from ww.builtin_workflows import require_lane
|
|
11
|
+
from ww.completion_inputs import validate_requested_values, validate_values
|
|
12
|
+
from ww.errors import ConfigurationError, StateError
|
|
13
|
+
from ww.extensions import ExtensionRegistry
|
|
14
|
+
from ww.instructions import Instruction, action_text, build_bootstrap_instruction
|
|
15
|
+
from ww.plan import PlanCompilationOptions, PlanItem, compile_workflow_plan
|
|
16
|
+
from ww.storage import Storage
|
|
17
|
+
from ww.storage_adapters import ArtifactAddress, TaskStorageAdapter
|
|
18
|
+
from ww.task_ids import validate_child_id
|
|
19
|
+
from ww.workflow_config import ProvidedVariable, WorkflowConfiguration
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StartBootstrapRun(Protocol):
|
|
23
|
+
"""Start the normal workflow run created by a resolved bootstrap request."""
|
|
24
|
+
|
|
25
|
+
def __call__(
|
|
26
|
+
self,
|
|
27
|
+
task_id: str,
|
|
28
|
+
workflow_name: str,
|
|
29
|
+
mode_names: tuple[str, ...],
|
|
30
|
+
agent: str,
|
|
31
|
+
*,
|
|
32
|
+
bootstrap_step: str,
|
|
33
|
+
bootstrap_values: tuple[tuple[str, str], ...],
|
|
34
|
+
bootstrap_request_id: str,
|
|
35
|
+
model: str,
|
|
36
|
+
reasoning: str,
|
|
37
|
+
workflow_runtime: str,
|
|
38
|
+
branch_naming_strategy: str | None,
|
|
39
|
+
init_artifact: str,
|
|
40
|
+
project: str | None,
|
|
41
|
+
parent_task_id: str | None = None,
|
|
42
|
+
start_operation_id: str | None = None,
|
|
43
|
+
) -> Instruction: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BindChild(Protocol):
|
|
47
|
+
"""Rename a parent's child record once the child's external ID is known."""
|
|
48
|
+
|
|
49
|
+
def __call__(
|
|
50
|
+
self, parent_task_id: str, temporary_id: str, child_task_id: str
|
|
51
|
+
) -> None: ...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ReadWorkflowStatus(Protocol):
|
|
55
|
+
"""Read the instruction for an existing workflow run."""
|
|
56
|
+
|
|
57
|
+
def __call__(self, task_id: str, run_id: str | None) -> Instruction: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BootstrapCoordinator:
|
|
61
|
+
"""Own bootstrap-request persistence and external-ID binding.
|
|
62
|
+
|
|
63
|
+
The service supplies the two workflow lifecycle operations at the binding
|
|
64
|
+
boundary. This keeps task locks and the transition into a normal workflow
|
|
65
|
+
run explicit without coupling this coordinator to ``WorkflowService``.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
storage: Storage,
|
|
71
|
+
tasks: TaskStorageAdapter,
|
|
72
|
+
extensions: ExtensionRegistry,
|
|
73
|
+
*,
|
|
74
|
+
now: Callable[[], str],
|
|
75
|
+
generated_request_id: Callable[[], str],
|
|
76
|
+
validate_task_id: Callable[[str], None],
|
|
77
|
+
task_exists: Callable[[str], bool],
|
|
78
|
+
) -> None:
|
|
79
|
+
self.storage = storage
|
|
80
|
+
self.tasks = tasks
|
|
81
|
+
self.extensions = extensions
|
|
82
|
+
self.now = now
|
|
83
|
+
self.generated_request_id = generated_request_id
|
|
84
|
+
self.validate_task_id = validate_task_id
|
|
85
|
+
self.task_exists = task_exists
|
|
86
|
+
|
|
87
|
+
def step(
|
|
88
|
+
self,
|
|
89
|
+
configuration: WorkflowConfiguration,
|
|
90
|
+
workflow_name: str,
|
|
91
|
+
mode_names: tuple[str, ...],
|
|
92
|
+
agent: str,
|
|
93
|
+
unknown_modes_for: Callable[[tuple[str, ...], WorkflowConfiguration], set[str]],
|
|
94
|
+
project: str | None = None,
|
|
95
|
+
) -> PlanItem | None:
|
|
96
|
+
"""Return the first agent step that establishes an external task ID."""
|
|
97
|
+
if workflow_name not in configuration.workflows_by_name:
|
|
98
|
+
raise ConfigurationError(f"workflow not found: {workflow_name}")
|
|
99
|
+
workflow = configuration.workflows_by_name[workflow_name]
|
|
100
|
+
require_lane(workflow)
|
|
101
|
+
unknown_modes = unknown_modes_for(mode_names, configuration)
|
|
102
|
+
if unknown_modes:
|
|
103
|
+
raise StateError("unknown mode(s): " + ", ".join(sorted(unknown_modes)))
|
|
104
|
+
providers = [
|
|
105
|
+
(index, step)
|
|
106
|
+
for index, step in enumerate(workflow.steps)
|
|
107
|
+
if any(value.name == "task_id" for value in step.provide)
|
|
108
|
+
]
|
|
109
|
+
if not providers:
|
|
110
|
+
return None
|
|
111
|
+
if len(providers) != 1 or providers[0][0] != 0:
|
|
112
|
+
raise ConfigurationError(
|
|
113
|
+
"variables: task_id is only supported on the first workflow step "
|
|
114
|
+
"when start has no task ID"
|
|
115
|
+
)
|
|
116
|
+
step = providers[0][1]
|
|
117
|
+
if (
|
|
118
|
+
step.hooks
|
|
119
|
+
or step.child_steps
|
|
120
|
+
or step.loop_steps
|
|
121
|
+
or step.items is not None
|
|
122
|
+
or step.children is not None
|
|
123
|
+
):
|
|
124
|
+
raise ConfigurationError(
|
|
125
|
+
"the bootstrap task_id step cannot have hooks, nested steps, "
|
|
126
|
+
"or children"
|
|
127
|
+
)
|
|
128
|
+
plan = compile_workflow_plan(
|
|
129
|
+
configuration,
|
|
130
|
+
self.storage.root,
|
|
131
|
+
workflow_name,
|
|
132
|
+
agent,
|
|
133
|
+
None,
|
|
134
|
+
self.extensions,
|
|
135
|
+
PlanCompilationOptions(project=project, modes=mode_names or None),
|
|
136
|
+
self.extensions.config,
|
|
137
|
+
)
|
|
138
|
+
item = next(
|
|
139
|
+
(
|
|
140
|
+
entry
|
|
141
|
+
for entry in plan.items
|
|
142
|
+
if entry.phase == "step" and entry.step == step.name
|
|
143
|
+
),
|
|
144
|
+
None,
|
|
145
|
+
)
|
|
146
|
+
if item is None or item.owner != "agent":
|
|
147
|
+
raise ConfigurationError("the bootstrap task_id step must be agent-owned")
|
|
148
|
+
if tuple(value.name for value in item.provide) != ("task_id",):
|
|
149
|
+
raise ConfigurationError(
|
|
150
|
+
"the bootstrap task_id step must hand back exactly the variable task_id"
|
|
151
|
+
)
|
|
152
|
+
return item
|
|
153
|
+
|
|
154
|
+
def start(
|
|
155
|
+
self,
|
|
156
|
+
workflow_name: str,
|
|
157
|
+
mode_names: tuple[str, ...],
|
|
158
|
+
agent: str,
|
|
159
|
+
item: PlanItem,
|
|
160
|
+
model: str,
|
|
161
|
+
reasoning: str,
|
|
162
|
+
workflow_runtime: str,
|
|
163
|
+
branch_naming_strategy: str | None,
|
|
164
|
+
init_artifact: str,
|
|
165
|
+
project: str | None = None,
|
|
166
|
+
*,
|
|
167
|
+
request_id: str | None = None,
|
|
168
|
+
parent_task_id: str | None = None,
|
|
169
|
+
start_operation_id: str | None = None,
|
|
170
|
+
) -> Instruction:
|
|
171
|
+
"""Record an identity request; a child's request reuses its temporary ID."""
|
|
172
|
+
explicit = request_id is not None
|
|
173
|
+
request_id = request_id or self.generated_request_id()
|
|
174
|
+
request: dict[str, object] = {
|
|
175
|
+
"request_id": request_id,
|
|
176
|
+
"workflow": workflow_name,
|
|
177
|
+
"modes": list(mode_names),
|
|
178
|
+
"agent": agent,
|
|
179
|
+
"model": model,
|
|
180
|
+
"reasoning": reasoning,
|
|
181
|
+
"requested_agent": item.requested_agent,
|
|
182
|
+
"requested_model": item.requested_model,
|
|
183
|
+
"requested_reasoning": item.requested_reasoning,
|
|
184
|
+
"requested_profile": item.profile,
|
|
185
|
+
"workflow_runtime": workflow_runtime,
|
|
186
|
+
"branch_naming_strategy": branch_naming_strategy,
|
|
187
|
+
"init_artifact": init_artifact,
|
|
188
|
+
"project": project,
|
|
189
|
+
"parent_task_id": parent_task_id,
|
|
190
|
+
"start_operation_id": start_operation_id,
|
|
191
|
+
"step": item.step,
|
|
192
|
+
"item_id": item.id,
|
|
193
|
+
"item_name": item.name,
|
|
194
|
+
"action_kind": item.kind,
|
|
195
|
+
"action_text": action_text(item),
|
|
196
|
+
"profile_instruction": item.profile_instruction,
|
|
197
|
+
"profile_path": item.profile_path,
|
|
198
|
+
"step_modes": [mode.to_dict() for mode in item.modes],
|
|
199
|
+
"status": "pending",
|
|
200
|
+
"created_at": self.now(),
|
|
201
|
+
}
|
|
202
|
+
with self.storage.lock_project():
|
|
203
|
+
while self.storage.read_bootstrap(request_id) is not None:
|
|
204
|
+
if explicit:
|
|
205
|
+
raise StateError(f"identity request {request_id!r} already exists")
|
|
206
|
+
request_id = self.generated_request_id()
|
|
207
|
+
request["request_id"] = request_id
|
|
208
|
+
self.storage.write_bootstrap(request_id, request)
|
|
209
|
+
return self.instruction(request)
|
|
210
|
+
|
|
211
|
+
def next(
|
|
212
|
+
self,
|
|
213
|
+
request: dict[str, object],
|
|
214
|
+
force: bool,
|
|
215
|
+
*,
|
|
216
|
+
selected_agent: str | None,
|
|
217
|
+
selected_model: str | None,
|
|
218
|
+
selected_reasoning: str | None,
|
|
219
|
+
start_task: StartBootstrapRun,
|
|
220
|
+
status_task: ReadWorkflowStatus,
|
|
221
|
+
bind_child: BindChild | None = None,
|
|
222
|
+
) -> Instruction:
|
|
223
|
+
status = request.get("status")
|
|
224
|
+
if status == "completed":
|
|
225
|
+
raise StateError("bootstrap request is already completed")
|
|
226
|
+
if status == "binding":
|
|
227
|
+
resolved = request.get("resolved_task_id")
|
|
228
|
+
if isinstance(resolved, str) and resolved:
|
|
229
|
+
return self.complete(
|
|
230
|
+
str(request["request_id"]),
|
|
231
|
+
request,
|
|
232
|
+
(),
|
|
233
|
+
None,
|
|
234
|
+
start_task=start_task,
|
|
235
|
+
status_task=status_task,
|
|
236
|
+
bind_child=bind_child,
|
|
237
|
+
)
|
|
238
|
+
raise StateError(
|
|
239
|
+
"binding bootstrap request is missing its resolved task ID"
|
|
240
|
+
)
|
|
241
|
+
if status == "failed":
|
|
242
|
+
if not force:
|
|
243
|
+
return self.instruction(request)
|
|
244
|
+
request["status"] = "pending"
|
|
245
|
+
request.pop("error", None)
|
|
246
|
+
if request.get("status") == "in_progress":
|
|
247
|
+
raise StateError("a bootstrap step is already in progress; use complete")
|
|
248
|
+
request["status"] = "in_progress"
|
|
249
|
+
request["selected_agent"] = selected_agent
|
|
250
|
+
request["selected_model"] = selected_model
|
|
251
|
+
request["selected_reasoning"] = selected_reasoning
|
|
252
|
+
self.storage.write_bootstrap(str(request["request_id"]), request)
|
|
253
|
+
return self.instruction(request)
|
|
254
|
+
|
|
255
|
+
def complete(
|
|
256
|
+
self,
|
|
257
|
+
request_id: str,
|
|
258
|
+
request: dict[str, object],
|
|
259
|
+
variables: tuple[tuple[str, str], ...],
|
|
260
|
+
artifact: str | None,
|
|
261
|
+
*,
|
|
262
|
+
start_task: StartBootstrapRun,
|
|
263
|
+
status_task: ReadWorkflowStatus,
|
|
264
|
+
bind_child: BindChild | None = None,
|
|
265
|
+
) -> Instruction:
|
|
266
|
+
"""Serialize a bootstrap binding and refresh stale request state."""
|
|
267
|
+
request_path = self.storage.runtime_path / "bootstrap" / f"{request_id}.json"
|
|
268
|
+
with self.storage.locks.lock(
|
|
269
|
+
request_path, purpose=f"bootstrap request {request_id!r}"
|
|
270
|
+
):
|
|
271
|
+
current = self.storage.read_bootstrap(request_id)
|
|
272
|
+
if current is not None:
|
|
273
|
+
request = current
|
|
274
|
+
return self._complete_locked(
|
|
275
|
+
request_id,
|
|
276
|
+
request,
|
|
277
|
+
variables,
|
|
278
|
+
artifact,
|
|
279
|
+
start_task=start_task,
|
|
280
|
+
status_task=status_task,
|
|
281
|
+
bind_child=bind_child,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _complete_locked(
|
|
285
|
+
self,
|
|
286
|
+
request_id: str,
|
|
287
|
+
request: dict[str, object],
|
|
288
|
+
variables: tuple[tuple[str, str], ...],
|
|
289
|
+
artifact: str | None,
|
|
290
|
+
*,
|
|
291
|
+
start_task: StartBootstrapRun,
|
|
292
|
+
status_task: ReadWorkflowStatus,
|
|
293
|
+
bind_child: BindChild | None = None,
|
|
294
|
+
) -> Instruction:
|
|
295
|
+
if request.get("status") not in {"in_progress", "binding"}:
|
|
296
|
+
raise StateError("no bootstrap step is in progress; use next")
|
|
297
|
+
supplied = validate_values(variables)
|
|
298
|
+
parent_task_id = request.get("parent_task_id")
|
|
299
|
+
parent = str(parent_task_id) if isinstance(parent_task_id, str) else None
|
|
300
|
+
if request.get("status") == "binding":
|
|
301
|
+
resolved_id = str(request.get("resolved_task_id", ""))
|
|
302
|
+
if supplied:
|
|
303
|
+
validate_requested_values(supplied, (ProvidedVariable("task_id"),))
|
|
304
|
+
else:
|
|
305
|
+
validate_requested_values(supplied, (ProvidedVariable("task_id"),))
|
|
306
|
+
resolved_id = supplied["task_id"]
|
|
307
|
+
if parent is not None:
|
|
308
|
+
# A child's external ID is one segment beneath its parent.
|
|
309
|
+
validate_child_id(resolved_id)
|
|
310
|
+
resolved_id = f"{parent}/{resolved_id}"
|
|
311
|
+
self.validate_task_id(resolved_id)
|
|
312
|
+
if request.get("status") != "binding" and self.task_exists(resolved_id):
|
|
313
|
+
raise StateError(
|
|
314
|
+
f"task {resolved_id!r} already exists and cannot be rebound"
|
|
315
|
+
)
|
|
316
|
+
workflow = str(request["workflow"])
|
|
317
|
+
modes = tuple(str(value) for value in cast(list[object], request["modes"]))
|
|
318
|
+
agent = str(request["agent"])
|
|
319
|
+
step = str(request["step"])
|
|
320
|
+
request["status"] = "binding"
|
|
321
|
+
request["resolved_task_id"] = resolved_id
|
|
322
|
+
request["binding_started_at"] = self.now()
|
|
323
|
+
if artifact is not None:
|
|
324
|
+
request["binding_artifact"] = artifact
|
|
325
|
+
self.storage.write_bootstrap(request_id, request)
|
|
326
|
+
with self.tasks.lock_task(resolved_id):
|
|
327
|
+
if self.task_exists(resolved_id):
|
|
328
|
+
runs, _ = self.tasks.read_task_aggregate(resolved_id)
|
|
329
|
+
if not any(run.bootstrap_request_id == request_id for run in runs):
|
|
330
|
+
raise StateError(
|
|
331
|
+
f"task {resolved_id!r} exists but is not bound to "
|
|
332
|
+
"bootstrap request "
|
|
333
|
+
f"{request_id!r}"
|
|
334
|
+
)
|
|
335
|
+
instruction = status_task(resolved_id, None)
|
|
336
|
+
else:
|
|
337
|
+
instruction = start_task(
|
|
338
|
+
resolved_id,
|
|
339
|
+
workflow,
|
|
340
|
+
modes,
|
|
341
|
+
agent,
|
|
342
|
+
bootstrap_step=step,
|
|
343
|
+
bootstrap_values=(("task_id", resolved_id),),
|
|
344
|
+
bootstrap_request_id=request_id,
|
|
345
|
+
model=str(request.get("model", "auto")),
|
|
346
|
+
reasoning=str(request.get("reasoning", "auto")),
|
|
347
|
+
workflow_runtime=str(request.get("workflow_runtime", "single")),
|
|
348
|
+
branch_naming_strategy=(
|
|
349
|
+
str(request["branch_naming_strategy"])
|
|
350
|
+
if request.get("branch_naming_strategy") is not None
|
|
351
|
+
else None
|
|
352
|
+
),
|
|
353
|
+
init_artifact=str(request["init_artifact"]),
|
|
354
|
+
project=(
|
|
355
|
+
str(request["project"])
|
|
356
|
+
if request.get("project") is not None
|
|
357
|
+
else None
|
|
358
|
+
),
|
|
359
|
+
parent_task_id=parent,
|
|
360
|
+
start_operation_id=(
|
|
361
|
+
str(request["start_operation_id"])
|
|
362
|
+
if request.get("start_operation_id") is not None
|
|
363
|
+
else None
|
|
364
|
+
),
|
|
365
|
+
)
|
|
366
|
+
artifact = artifact or (
|
|
367
|
+
str(request["binding_artifact"])
|
|
368
|
+
if request.get("binding_artifact") is not None
|
|
369
|
+
else None
|
|
370
|
+
)
|
|
371
|
+
if artifact is not None:
|
|
372
|
+
run_id = self.tasks.active_execution_run(resolved_id)
|
|
373
|
+
if run_id is not None:
|
|
374
|
+
self.tasks.write_execution_artifact(
|
|
375
|
+
ArtifactAddress(
|
|
376
|
+
resolved_id,
|
|
377
|
+
workflow,
|
|
378
|
+
step,
|
|
379
|
+
2,
|
|
380
|
+
str(request["item_name"]),
|
|
381
|
+
"step",
|
|
382
|
+
run_id=run_id,
|
|
383
|
+
step_ordinals=(2,),
|
|
384
|
+
),
|
|
385
|
+
render_step_artifact(
|
|
386
|
+
task_id=resolved_id,
|
|
387
|
+
workflow=workflow,
|
|
388
|
+
step=step,
|
|
389
|
+
step_number=1,
|
|
390
|
+
step_total=1,
|
|
391
|
+
skill="auto",
|
|
392
|
+
result=artifact,
|
|
393
|
+
),
|
|
394
|
+
)
|
|
395
|
+
if parent is not None:
|
|
396
|
+
if bind_child is None: # pragma: no cover - service always binds
|
|
397
|
+
raise StateError("child identity requests require a parent binding")
|
|
398
|
+
bind_child(parent, request_id, resolved_id)
|
|
399
|
+
request["status"] = "completed"
|
|
400
|
+
request["completed_at"] = self.now()
|
|
401
|
+
self.storage.write_bootstrap(request_id, request)
|
|
402
|
+
return instruction
|
|
403
|
+
|
|
404
|
+
def instruction(self, request: dict[str, object]) -> Instruction:
|
|
405
|
+
return build_bootstrap_instruction(request, self.storage.root)
|
ww/builtin_workflows.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""The workflows ww ships with, composed below every configuration level.
|
|
3
|
+
|
|
4
|
+
Each file in ``ww/assets/workflows/`` is ordinary ``ww.yaml``
|
|
5
|
+
notation holding one or more workflows, and optionally the root ``documents``
|
|
6
|
+
and ``modes`` that belong to them. Together they form a built-in level beneath
|
|
7
|
+
the user, repo and local levels:
|
|
8
|
+
|
|
9
|
+
- a workflow, document or mode that any configuration level defines under the
|
|
10
|
+
same name replaces the built-in one;
|
|
11
|
+
- ``ww.json`` switches a built-in workflow off with
|
|
12
|
+
``"workflows": {"<name>": {"enabled": false}}``; a file whose workflows are
|
|
13
|
+
all switched off contributes nothing, its documents and modes included.
|
|
14
|
+
|
|
15
|
+
The built-in level is added after composition and parsing, when the
|
|
16
|
+
configuration is validated, so a level's ``extends: false`` never removes it
|
|
17
|
+
and the project's own files are parsed exactly as they are written.
|
|
18
|
+
|
|
19
|
+
``catchall`` records a change no configured workflow covers. It exists so that
|
|
20
|
+
every change goes through ww, including the small ones an agent would
|
|
21
|
+
otherwise just make, without adding any process to them: the agent works
|
|
22
|
+
exactly as it would on a plain prompt and ww keeps the record.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass, replace
|
|
28
|
+
from importlib.resources import files
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import TYPE_CHECKING
|
|
31
|
+
|
|
32
|
+
import yaml
|
|
33
|
+
|
|
34
|
+
from ww.errors import ConfigurationError
|
|
35
|
+
from ww.workflow_config import (
|
|
36
|
+
DocumentDefinition,
|
|
37
|
+
ModeDefinition,
|
|
38
|
+
WorkflowConfiguration,
|
|
39
|
+
WorkflowDefinition,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from importlib.abc import Traversable
|
|
44
|
+
|
|
45
|
+
from ww.project_config import ProjectConfig
|
|
46
|
+
|
|
47
|
+
CATCHALL = "catchall"
|
|
48
|
+
# The root keys a built-in file may declare.
|
|
49
|
+
BUILTIN_KEYS = frozenset({"workflows", "documents", "modes"})
|
|
50
|
+
# Where the built-in files live; tests point it at a directory of their own.
|
|
51
|
+
BUILTIN_DIRECTORY: Traversable | Path = files("ww.assets").joinpath("workflows")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class BuiltinFile:
|
|
56
|
+
"""One built-in YAML file: its workflows and what belongs to them."""
|
|
57
|
+
|
|
58
|
+
name: str
|
|
59
|
+
workflows: tuple[WorkflowDefinition, ...]
|
|
60
|
+
documents: tuple[DocumentDefinition, ...] = ()
|
|
61
|
+
modes: tuple[ModeDefinition, ...] = ()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_parsed: dict[str, tuple[BuiltinFile, ...]] = {}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def builtin_files() -> tuple[BuiltinFile, ...]:
|
|
68
|
+
"""Every built-in file, parsed once per directory, in file-name order."""
|
|
69
|
+
directory = BUILTIN_DIRECTORY
|
|
70
|
+
key = str(directory)
|
|
71
|
+
if key not in _parsed:
|
|
72
|
+
_parsed[key] = tuple(
|
|
73
|
+
_parse_file(entry)
|
|
74
|
+
for entry in sorted(directory.iterdir(), key=lambda item: item.name)
|
|
75
|
+
if entry.name.endswith(".yaml") and entry.is_file()
|
|
76
|
+
)
|
|
77
|
+
return _parsed[key]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def builtin_workflows() -> tuple[WorkflowDefinition, ...]:
|
|
81
|
+
return tuple(
|
|
82
|
+
workflow for builtin in builtin_files() for workflow in builtin.workflows
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def builtin_workflow_names() -> frozenset[str]:
|
|
87
|
+
return frozenset(workflow.name for workflow in builtin_workflows())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def builtin_workflow(name: str) -> WorkflowDefinition:
|
|
91
|
+
"""The built-in workflow ``name``, as ww ships it."""
|
|
92
|
+
for workflow in builtin_workflows():
|
|
93
|
+
if workflow.name == name:
|
|
94
|
+
return workflow
|
|
95
|
+
raise KeyError(name)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def is_builtin(workflow: WorkflowDefinition) -> bool:
|
|
99
|
+
"""Whether ``workflow`` is a built-in one, not a configured replacement.
|
|
100
|
+
|
|
101
|
+
A built-in whose recommended workflow is switched off loses the
|
|
102
|
+
recommendation and is still the built-in.
|
|
103
|
+
"""
|
|
104
|
+
return any(
|
|
105
|
+
replace(
|
|
106
|
+
workflow,
|
|
107
|
+
recommended_next_workflow=builtin.recommended_next_workflow,
|
|
108
|
+
hooks_from=builtin.hooks_from,
|
|
109
|
+
)
|
|
110
|
+
== builtin
|
|
111
|
+
for builtin in builtin_workflows()
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def missing_lane(workflow: WorkflowDefinition) -> str | None:
|
|
116
|
+
"""Why ``workflow`` cannot run: it needs ``hooks_from`` and has none.
|
|
117
|
+
|
|
118
|
+
``None`` when it can. The lane is set in the workflow's ``ww.yaml`` definition.
|
|
119
|
+
"""
|
|
120
|
+
if not workflow.needs_hooks_from or workflow.hooks_from is not None:
|
|
121
|
+
return None
|
|
122
|
+
name = workflow.name
|
|
123
|
+
where = (
|
|
124
|
+
"name that lane with hooks_from in its definition in ww.yaml "
|
|
125
|
+
'first, for example "hooks_from: task"'
|
|
126
|
+
)
|
|
127
|
+
return (
|
|
128
|
+
f"workflow {name!r} runs with a project lane's branch, worktree and "
|
|
129
|
+
f"commit handling; {where}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def require_lane(workflow: WorkflowDefinition) -> None:
|
|
134
|
+
"""Refuse to run ``workflow`` while it needs ``hooks_from`` and has none."""
|
|
135
|
+
reason = missing_lane(workflow)
|
|
136
|
+
if reason is not None:
|
|
137
|
+
raise ConfigurationError(reason)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def with_builtin_workflows(
|
|
141
|
+
configuration: WorkflowConfiguration, project_config: ProjectConfig
|
|
142
|
+
) -> WorkflowConfiguration:
|
|
143
|
+
"""Add each enabled built-in the configuration does not define itself.
|
|
144
|
+
|
|
145
|
+
The built-in workflows follow the configured ones. A built-in file's
|
|
146
|
+
documents and modes come along while any of its workflows is enabled,
|
|
147
|
+
unless the configuration declares one of the same name. A built-in's
|
|
148
|
+
``recommended_next_workflow`` is dropped while that workflow is off.
|
|
149
|
+
"""
|
|
150
|
+
workflows = {workflow.name for workflow in configuration.workflows}
|
|
151
|
+
documents = {document.name for document in configuration.documents}
|
|
152
|
+
modes = {mode.name for mode in configuration.modes}
|
|
153
|
+
added_workflows: list[WorkflowDefinition] = []
|
|
154
|
+
added_documents: list[DocumentDefinition] = []
|
|
155
|
+
added_modes: list[ModeDefinition] = []
|
|
156
|
+
for builtin in builtin_files():
|
|
157
|
+
enabled = [
|
|
158
|
+
workflow
|
|
159
|
+
for workflow in builtin.workflows
|
|
160
|
+
if project_config.workflow_enabled(workflow.name)
|
|
161
|
+
]
|
|
162
|
+
if not enabled:
|
|
163
|
+
continue
|
|
164
|
+
added_workflows.extend(
|
|
165
|
+
workflow for workflow in enabled if workflow.name not in workflows
|
|
166
|
+
)
|
|
167
|
+
for document in builtin.documents:
|
|
168
|
+
if document.name not in documents:
|
|
169
|
+
documents.add(document.name)
|
|
170
|
+
added_documents.append(document)
|
|
171
|
+
for mode in builtin.modes:
|
|
172
|
+
if mode.name not in modes:
|
|
173
|
+
modes.add(mode.name)
|
|
174
|
+
added_modes.append(mode)
|
|
175
|
+
if not (added_workflows or added_documents or added_modes):
|
|
176
|
+
return configuration
|
|
177
|
+
# A built-in recommending one that is switched off recommends nothing.
|
|
178
|
+
present = workflows | {workflow.name for workflow in added_workflows}
|
|
179
|
+
added_workflows = [
|
|
180
|
+
workflow
|
|
181
|
+
if workflow.recommended_next_workflow in (None, *present)
|
|
182
|
+
else replace(workflow, recommended_next_workflow=None)
|
|
183
|
+
for workflow in added_workflows
|
|
184
|
+
]
|
|
185
|
+
return replace(
|
|
186
|
+
configuration,
|
|
187
|
+
workflows=(*configuration.workflows, *added_workflows),
|
|
188
|
+
documents=(*configuration.documents, *added_documents),
|
|
189
|
+
modes=(*configuration.modes, *added_modes),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _parse_file(entry: Traversable | Path) -> BuiltinFile:
|
|
194
|
+
# The notation frontend validates through this module, so it is imported
|
|
195
|
+
# only once a built-in file is first read.
|
|
196
|
+
from ww.config import parse_yaml_text
|
|
197
|
+
|
|
198
|
+
label = f"built-in {entry.name}"
|
|
199
|
+
text = entry.read_text(encoding="utf-8")
|
|
200
|
+
raw = yaml.safe_load(text)
|
|
201
|
+
if not isinstance(raw, dict):
|
|
202
|
+
raise ConfigurationError(f"{label} must contain a mapping")
|
|
203
|
+
unknown = set(raw) - BUILTIN_KEYS
|
|
204
|
+
if unknown:
|
|
205
|
+
raise ConfigurationError(
|
|
206
|
+
f"{label} declares {', '.join(sorted(unknown))}; a built-in file "
|
|
207
|
+
"holds only " + ", ".join(sorted(BUILTIN_KEYS))
|
|
208
|
+
)
|
|
209
|
+
parsed = parse_yaml_text(text, label)
|
|
210
|
+
return BuiltinFile(
|
|
211
|
+
entry.name.removesuffix(".yaml"),
|
|
212
|
+
parsed.workflows,
|
|
213
|
+
parsed.documents,
|
|
214
|
+
parsed.modes,
|
|
215
|
+
)
|