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/workflow_update.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""``ww setup update``: replace one workflow definition where it is written.
|
|
3
|
+
|
|
4
|
+
``setup apply`` adds definitions to ww's own setup files. Changing a workflow
|
|
5
|
+
the project already defines, in its root YAML or in any imported file, needs
|
|
6
|
+
more care: the edit must land in the definition that is in force, and nothing
|
|
7
|
+
else in the file may move. The update path is deliberately small:
|
|
8
|
+
|
|
9
|
+
- the fragment holds ``workflows`` with exactly one entry, named like the
|
|
10
|
+
workflow being updated, and nothing else;
|
|
11
|
+
- the target is the definition that wins in the composed configuration; with
|
|
12
|
+
``--level``, the one at that level, refused when a higher-precedence
|
|
13
|
+
definition hides it, since the edit would report success and change nothing;
|
|
14
|
+
- only that list item's lines are replaced, in ww's YAML style; every other
|
|
15
|
+
line of the file, comments and other definitions included, stays byte for
|
|
16
|
+
byte. Comments inside the replaced entry are the unavoidable loss, and the
|
|
17
|
+
preview says so;
|
|
18
|
+
- the result is checked to load back as the old data with exactly that entry
|
|
19
|
+
swapped, then validated in memory as ww would load it, and the workflow's
|
|
20
|
+
provenance must point at the file written;
|
|
21
|
+
- a workflow that only ww ships, or one that is not defined anywhere, is
|
|
22
|
+
refused with the command to use instead.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import difflib
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
import yaml
|
|
33
|
+
|
|
34
|
+
from ww.config import load_configuration
|
|
35
|
+
from ww.config.composition import WorkflowSite, entry_name, workflow_sites
|
|
36
|
+
from ww.config_files import read_configuration_file, staged_files
|
|
37
|
+
from ww.config_writes import FileWrite, Transaction, dump_yaml
|
|
38
|
+
from ww.errors import StateError, WwError
|
|
39
|
+
from ww.extensions import ExtensionRegistry
|
|
40
|
+
from ww.workflow_config import WorkflowConfigLevel
|
|
41
|
+
|
|
42
|
+
LEVELS = ("local", "project", "global")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class UpdatePlan:
|
|
47
|
+
"""The one file an update writes, and what the operator is shown first."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
level: WorkflowConfigLevel
|
|
51
|
+
label: str
|
|
52
|
+
write: FileWrite
|
|
53
|
+
diff: str
|
|
54
|
+
notes: tuple[str, ...]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def plan_update(
|
|
58
|
+
root: Path, config_path: Path, name: str, fragment: Path, level: str | None
|
|
59
|
+
) -> UpdatePlan:
|
|
60
|
+
"""Plan replacing workflow ``name``; nothing is written."""
|
|
61
|
+
if level is not None and level not in LEVELS:
|
|
62
|
+
raise StateError(f"--level takes {', '.join(LEVELS)}")
|
|
63
|
+
entry = _read_fragment(fragment, name)
|
|
64
|
+
site = _target(config_path, name, level)
|
|
65
|
+
text = read_configuration_file(site.file)
|
|
66
|
+
new_text, comments = _replace_entry(text, name, entry, site.label)
|
|
67
|
+
diff = "".join(
|
|
68
|
+
difflib.unified_diff(
|
|
69
|
+
text.splitlines(keepends=True),
|
|
70
|
+
new_text.splitlines(keepends=True),
|
|
71
|
+
f"a/{site.label}",
|
|
72
|
+
f"b/{site.label}",
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
notes = ["the entry is rewritten in ww's YAML style; the rest of the file is kept"]
|
|
76
|
+
if comments:
|
|
77
|
+
notes.append("comments inside the replaced entry are not kept")
|
|
78
|
+
return UpdatePlan(
|
|
79
|
+
name, site.level, site.label, FileWrite(site.file, new_text), diff, tuple(notes)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def validate_update(root: Path, config_path: Path, plan: UpdatePlan) -> None:
|
|
84
|
+
"""Load the configuration as the plan leaves it; nothing is written."""
|
|
85
|
+
with staged_files({plan.write.path: plan.write.content}):
|
|
86
|
+
try:
|
|
87
|
+
configuration = load_configuration(
|
|
88
|
+
config_path, ExtensionRegistry.discover(root)
|
|
89
|
+
)
|
|
90
|
+
except WwError as error:
|
|
91
|
+
raise StateError(
|
|
92
|
+
f"refused: the configuration would not be valid ({error}); "
|
|
93
|
+
"nothing was written"
|
|
94
|
+
) from error
|
|
95
|
+
provenance = configuration.workflow_provenance.get(plan.name)
|
|
96
|
+
if provenance is None or provenance.source != plan.label:
|
|
97
|
+
winner = provenance.source if provenance else "another definition"
|
|
98
|
+
raise StateError(
|
|
99
|
+
f"refused: after this edit workflow `{plan.name}` would still come "
|
|
100
|
+
f"from {winner}, not {plan.label}; nothing was written"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def apply_update(plan: UpdatePlan) -> None:
|
|
105
|
+
with Transaction() as transaction:
|
|
106
|
+
transaction.apply([plan.write])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def render_plan(plan: UpdatePlan) -> str:
|
|
110
|
+
lines = [
|
|
111
|
+
f"`ww setup update` replaces workflow `{plan.name}` in {plan.label} "
|
|
112
|
+
f"({plan.level} level):",
|
|
113
|
+
*(f"Note: {note}" for note in plan.notes),
|
|
114
|
+
plan.diff.rstrip("\n"),
|
|
115
|
+
]
|
|
116
|
+
return "\n".join(lines) + "\n"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def plan_to_dict(plan: UpdatePlan, *, applied: bool) -> dict[str, object]:
|
|
120
|
+
return {
|
|
121
|
+
"workflow": plan.name,
|
|
122
|
+
"file": plan.label,
|
|
123
|
+
"level": plan.level,
|
|
124
|
+
"notes": list(plan.notes),
|
|
125
|
+
"diff": plan.diff,
|
|
126
|
+
"applied": applied,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _read_fragment(path: Path, name: str) -> Any:
|
|
131
|
+
try:
|
|
132
|
+
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
133
|
+
except OSError as error:
|
|
134
|
+
raise StateError(f"cannot read {path}: {error}") from error
|
|
135
|
+
except yaml.YAMLError as error:
|
|
136
|
+
raise StateError(f"invalid YAML in {path}: {error}") from error
|
|
137
|
+
entries = raw.get("workflows") if isinstance(raw, dict) else None
|
|
138
|
+
if not isinstance(raw, dict) or set(raw) != {"workflows"}:
|
|
139
|
+
raise StateError(
|
|
140
|
+
f"{path} must hold only `workflows` with the one workflow to "
|
|
141
|
+
"update; other changes go through `ww setup apply`"
|
|
142
|
+
)
|
|
143
|
+
if not isinstance(entries, list) or len(entries) != 1:
|
|
144
|
+
raise StateError(f"{path}: workflows must list exactly one workflow")
|
|
145
|
+
if entry_name(entries[0]) != name:
|
|
146
|
+
raise StateError(
|
|
147
|
+
f"{path} defines `{entry_name(entries[0])}`, not `{name}`; an update "
|
|
148
|
+
"does not rename"
|
|
149
|
+
)
|
|
150
|
+
return entries[0]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _target(config_path: Path, name: str, level: str | None) -> WorkflowSite:
|
|
154
|
+
sites = workflow_sites(config_path, name)
|
|
155
|
+
if not sites:
|
|
156
|
+
raise StateError(
|
|
157
|
+
f"no configuration file defines workflow `{name}`; add or override "
|
|
158
|
+
"one with `ww setup apply`"
|
|
159
|
+
)
|
|
160
|
+
winner = sites[-1]
|
|
161
|
+
if level is None:
|
|
162
|
+
return winner
|
|
163
|
+
chosen = [site for site in sites if site.level == level]
|
|
164
|
+
if not chosen:
|
|
165
|
+
where = ", ".join(f"{site.label} ({site.level})" for site in sites)
|
|
166
|
+
raise StateError(
|
|
167
|
+
f"workflow `{name}` has no definition at the {level} level; it is "
|
|
168
|
+
f"defined in {where}"
|
|
169
|
+
)
|
|
170
|
+
if chosen[-1] is not winner:
|
|
171
|
+
raise StateError(
|
|
172
|
+
f"refused: workflow `{name}` at the {level} level ({chosen[-1].label}) "
|
|
173
|
+
f"is hidden by {winner.label} ({winner.level} level), which takes "
|
|
174
|
+
"precedence, so editing it would change nothing; update that "
|
|
175
|
+
f"definition (--level {winner.level}) or remove it first"
|
|
176
|
+
)
|
|
177
|
+
return chosen[-1]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _replace_entry(text: str, name: str, entry: Any, label: str) -> tuple[str, bool]:
|
|
181
|
+
"""``text`` with workflow ``name``'s list item replaced, and nothing else."""
|
|
182
|
+
try:
|
|
183
|
+
raw = yaml.safe_load(text)
|
|
184
|
+
document = yaml.compose(text)
|
|
185
|
+
except yaml.YAMLError as error:
|
|
186
|
+
raise StateError(f"invalid YAML in {label}: {error}") from error
|
|
187
|
+
workflows = raw["workflows"]
|
|
188
|
+
index = next(i for i, item in enumerate(workflows) if entry_name(item) == name)
|
|
189
|
+
if workflows[index] == entry:
|
|
190
|
+
raise StateError(f"workflow `{name}` in {label} already is that")
|
|
191
|
+
node = next(
|
|
192
|
+
value for key, value in document.value if key.value == "workflows"
|
|
193
|
+
).value[index]
|
|
194
|
+
lines = text.splitlines(keepends=True)
|
|
195
|
+
first = node.start_mark.line
|
|
196
|
+
prefix = lines[first][: node.start_mark.column].rstrip()
|
|
197
|
+
if not prefix.endswith("-"):
|
|
198
|
+
raise _cannot(label)
|
|
199
|
+
dash = len(prefix) - 1
|
|
200
|
+
end = node.end_mark
|
|
201
|
+
if end.line >= len(lines):
|
|
202
|
+
after = len(lines)
|
|
203
|
+
elif lines[end.line][: end.column].strip() == "":
|
|
204
|
+
after = end.line
|
|
205
|
+
else:
|
|
206
|
+
after = end.line + 1
|
|
207
|
+
while after - 1 > first and (
|
|
208
|
+
not lines[after - 1].strip() or lines[after - 1].lstrip().startswith("#")
|
|
209
|
+
):
|
|
210
|
+
after -= 1
|
|
211
|
+
old = lines[first:after]
|
|
212
|
+
comments = any(line.lstrip().startswith("#") or " #" in line for line in old)
|
|
213
|
+
shift = dash - 2
|
|
214
|
+
block = dump_yaml({"workflows": [entry]}).splitlines(keepends=True)[1:]
|
|
215
|
+
if shift > 0:
|
|
216
|
+
block = [" " * shift + line if line.strip() else line for line in block]
|
|
217
|
+
elif shift < 0:
|
|
218
|
+
block = [line[-shift:] if line.strip() else line for line in block]
|
|
219
|
+
if old and not old[-1].endswith("\n"):
|
|
220
|
+
block[-1] = block[-1].rstrip("\n")
|
|
221
|
+
updated = "".join([*lines[:first], *block, *lines[after:]])
|
|
222
|
+
expected = {
|
|
223
|
+
**raw,
|
|
224
|
+
"workflows": [*workflows[:index], entry, *workflows[index + 1 :]],
|
|
225
|
+
}
|
|
226
|
+
try:
|
|
227
|
+
reloaded = yaml.safe_load(updated)
|
|
228
|
+
except yaml.YAMLError:
|
|
229
|
+
raise _cannot(label) from None
|
|
230
|
+
if reloaded != expected:
|
|
231
|
+
raise _cannot(label)
|
|
232
|
+
return updated, comments
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _cannot(label: str) -> StateError:
|
|
236
|
+
return StateError(
|
|
237
|
+
f"ww cannot replace that workflow in {label} without changing anything "
|
|
238
|
+
"else; edit it by hand"
|
|
239
|
+
)
|