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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. ww/__init__.py +18 -0
  2. ww/_bundled_extensions/ww/git/extension.py +1728 -0
  3. ww/action_execution.py +887 -0
  4. ww/actions/__init__.py +94 -0
  5. ww/actions/command.py +444 -0
  6. ww/actions/contracts.py +699 -0
  7. ww/actions/extension.py +197 -0
  8. ww/actions/mcp.py +84 -0
  9. ww/actions/prompt.py +74 -0
  10. ww/actions/skill.py +62 -0
  11. ww/actions/slash_command.py +63 -0
  12. ww/agents.py +151 -0
  13. ww/amendments.py +54 -0
  14. ww/artifacts.py +93 -0
  15. ww/assessments.py +181 -0
  16. ww/assets/__init__.py +2 -0
  17. ww/assets/agent_instructions.md +49 -0
  18. ww/assets/docs/examples.md +879 -0
  19. ww/assets/docs/features.md +4639 -0
  20. ww/assets/docs/specification.md +1876 -0
  21. ww/assets/noww_skill.md +11 -0
  22. ww/assets/workflows/catchall.yaml +26 -0
  23. ww/assets/workflows/onboarding.yaml +586 -0
  24. ww/assets/workflows/scriptize.yaml +130 -0
  25. ww/assets/ww-automate_skill.md +23 -0
  26. ww/assets/ww-deduce-feedback_skill.md +38 -0
  27. ww/assets/ww-feedback-rules_skill.md +48 -0
  28. ww/assets/ww-learn-project_skill.md +22 -0
  29. ww/assets/ww-refresh_skill.md +26 -0
  30. ww/assets/ww-rule_skill.md +83 -0
  31. ww/assets/ww-rules-from-artifacts_skill.md +22 -0
  32. ww/assets/ww-scriptize_skill.md +33 -0
  33. ww/assets/ww-setup_skill.md +94 -0
  34. ww/assets/ww-solve_skill.md +23 -0
  35. ww/assets/ww-suggest_skill.md +32 -0
  36. ww/assets/ww-wizard_skill.md +105 -0
  37. ww/assets/ww_skill.md +59 -0
  38. ww/assignments.py +283 -0
  39. ww/bootstrap.py +405 -0
  40. ww/builtin_workflows.py +215 -0
  41. ww/changes.py +225 -0
  42. ww/child_coordination.py +482 -0
  43. ww/children.py +106 -0
  44. ww/claude_permissions.py +115 -0
  45. ww/cli/__init__.py +7 -0
  46. ww/cli/__main__.py +6 -0
  47. ww/cli/audit.py +129 -0
  48. ww/cli/catalogs.py +131 -0
  49. ww/cli/discover.py +607 -0
  50. ww/cli/initialization.py +898 -0
  51. ww/cli/lookup.py +287 -0
  52. ww/cli/main.py +1768 -0
  53. ww/cli/parser.py +1200 -0
  54. ww/cli/prompts.py +217 -0
  55. ww/cli/updates.py +117 -0
  56. ww/completion_artifacts.py +156 -0
  57. ww/completion_inputs.py +39 -0
  58. ww/config/__init__.py +582 -0
  59. ww/config/actions.py +591 -0
  60. ww/config/composition.py +571 -0
  61. ww/config/rules.py +511 -0
  62. ww/config/steps.py +1220 -0
  63. ww/config/values.py +223 -0
  64. ww/config_files.py +191 -0
  65. ww/config_writes.py +264 -0
  66. ww/contracts.py +155 -0
  67. ww/control.py +41 -0
  68. ww/defaults.py +130 -0
  69. ww/design_docs.py +32 -0
  70. ww/discovery.py +104 -0
  71. ww/documents.py +217 -0
  72. ww/errors.py +18 -0
  73. ww/executable.py +43 -0
  74. ww/execution_models/__init__.py +64 -0
  75. ww/execution_models/construction.py +148 -0
  76. ww/execution_models/decoding.py +38 -0
  77. ww/execution_models/plan_codec.py +565 -0
  78. ww/execution_models/records.py +1206 -0
  79. ww/execution_models/runs.py +266 -0
  80. ww/extensions/__init__.py +40 -0
  81. ww/extensions/api.py +559 -0
  82. ww/extensions/registry.py +864 -0
  83. ww/extensions/store.py +78 -0
  84. ww/feedback.py +342 -0
  85. ww/handler_repairs.py +57 -0
  86. ww/hooks/__init__.py +40 -0
  87. ww/hooks/agents.py +380 -0
  88. ww/hooks/install.py +168 -0
  89. ww/hooks/notices.py +206 -0
  90. ww/hooks/records.py +209 -0
  91. ww/hooks/runtime.py +266 -0
  92. ww/hooks/transcripts.py +183 -0
  93. ww/inspect.py +896 -0
  94. ww/instructions/__init__.py +17 -0
  95. ww/instructions/builder.py +1682 -0
  96. ww/instructions/commands.py +335 -0
  97. ww/instructions/handoff.py +149 -0
  98. ww/instructions/models.py +686 -0
  99. ww/instructions/policy.py +219 -0
  100. ww/instructions/text.py +168 -0
  101. ww/interactions.py +187 -0
  102. ww/interpolation.py +37 -0
  103. ww/item_passes.py +167 -0
  104. ww/items.py +99 -0
  105. ww/locking.py +207 -0
  106. ww/metadata_publication.py +230 -0
  107. ww/onboarding.py +229 -0
  108. ww/open_work.py +236 -0
  109. ww/operations.py +193 -0
  110. ww/operator_ui/__init__.py +16 -0
  111. ww/operator_ui/page.html +351 -0
  112. ww/operator_ui/server.py +215 -0
  113. ww/operator_ui/session.py +389 -0
  114. ww/operator_ui/sheet.py +104 -0
  115. ww/operator_ui/view.py +109 -0
  116. ww/output.py +339 -0
  117. ww/output_adapters/__init__.py +12 -0
  118. ww/output_adapters/base.py +25 -0
  119. ww/output_adapters/json_adapter.py +37 -0
  120. ww/output_adapters/markdown.py +2293 -0
  121. ww/output_adapters/rule_pages.py +337 -0
  122. ww/output_adapters/terminal.py +21 -0
  123. ww/package_updates.py +167 -0
  124. ww/plan/__init__.py +38 -0
  125. ww/plan/actions.py +207 -0
  126. ww/plan/compiler.py +1492 -0
  127. ww/plan/constructs.py +456 -0
  128. ww/plan/models.py +665 -0
  129. ww/project_config.py +752 -0
  130. ww/recovery.py +401 -0
  131. ww/replanning.py +367 -0
  132. ww/results.py +77 -0
  133. ww/rule_checks.py +230 -0
  134. ww/rule_conversion.py +331 -0
  135. ww/rule_disputes.py +148 -0
  136. ww/rule_store.py +456 -0
  137. ww/rule_verification.py +714 -0
  138. ww/rule_views.py +447 -0
  139. ww/rule_writes.py +920 -0
  140. ww/run_coordination.py +158 -0
  141. ww/runtimes.py +105 -0
  142. ww/service.py +4405 -0
  143. ww/setup_apply.py +428 -0
  144. ww/step_values.py +20 -0
  145. ww/storage.py +447 -0
  146. ww/storage_adapters/__init__.py +36 -0
  147. ww/storage_adapters/base.py +540 -0
  148. ww/storage_adapters/filesystem.py +370 -0
  149. ww/storage_adapters/memory.py +195 -0
  150. ww/storage_adapters/project_metadata.py +69 -0
  151. ww/storage_adapters/task_document.py +484 -0
  152. ww/task_ids.py +114 -0
  153. ww/task_references.py +124 -0
  154. ww/transitions.py +1619 -0
  155. ww/updates.py +399 -0
  156. ww/upgrade.py +95 -0
  157. ww/validation.py +168 -0
  158. ww/variables.py +275 -0
  159. ww/workflow_config.py +854 -0
  160. ww/workflow_update.py +239 -0
  161. ww/workflow_validation.py +1260 -0
  162. ww/workspace.py +50 -0
  163. ww_agentic_workflows-1.0.0.dev3.dist-info/METADATA +690 -0
  164. ww_agentic_workflows-1.0.0.dev3.dist-info/RECORD +167 -0
  165. ww_agentic_workflows-1.0.0.dev3.dist-info/WHEEL +4 -0
  166. ww_agentic_workflows-1.0.0.dev3.dist-info/entry_points.txt +2 -0
  167. ww_agentic_workflows-1.0.0.dev3.dist-info/licenses/LICENSE +674 -0
ww/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
+ )