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/cli/lookup.py ADDED
@@ -0,0 +1,287 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """``lookup``: which task a catch-all change belongs to, and what to do next.
3
+
4
+ An agent about to change files outside any workflow runs ``lookup`` with what
5
+ the operator called the task, or with nothing when they named none. ww maps
6
+ the reference onto the project's task format and existing tasks and answers
7
+ with one next step: continue an unfinished run, start ``catchall`` on the task
8
+ found, or ask the operator. A task ww has never seen is only created after the
9
+ operator confirms it, through the agent's own choice menu.
10
+
11
+ ``lookup`` is read-only; the command it prints does the work.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from dataclasses import dataclass
18
+
19
+ from ww.agents import choice_mechanism
20
+ from ww.builtin_workflows import CATCHALL
21
+ from ww.config import load_configuration
22
+ from ww.discovery import normalize_agent
23
+ from ww.errors import ConfigurationError, StateError
24
+ from ww.extensions import ExtensionRegistry
25
+ from ww.hooks.notices import (
26
+ recent_interruptions_pointer,
27
+ )
28
+ from ww.hooks.records import HookRecords
29
+ from ww.instructions.commands import (
30
+ TASK_PLACEHOLDER,
31
+ instruction_command,
32
+ start_command,
33
+ )
34
+ from ww.project_config import FILE_NAME
35
+ from ww.storage import Storage
36
+ from ww.storage_adapters import TaskStorageAdapter
37
+ from ww.task_ids import EXPLICIT_TASK_FORMAT
38
+ from ww.task_references import resolve_task_reference
39
+
40
+ WITHOUT_WW = "Work without ww"
41
+ # Under ``"enabled": "on_request"`` lookup runs only because the user asked
42
+ # for ww; an unasked change is made without it, and without asking.
43
+ ON_REQUEST_NOTE = (
44
+ "ww is used here only on request. Go on only if the user explicitly asked "
45
+ "for ww; otherwise make the change without ww and do not ask."
46
+ )
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class _Choice:
51
+ label: str
52
+ description: str
53
+ # ``None`` for working without ww: no command follows that choice.
54
+ command: str | None
55
+
56
+ def to_dict(self) -> dict[str, object]:
57
+ return {
58
+ "label": self.label,
59
+ "description": self.description,
60
+ "command": self.command,
61
+ }
62
+
63
+
64
+ def lookup(
65
+ storage: Storage,
66
+ extensions: ExtensionRegistry,
67
+ tasks: TaskStorageAdapter,
68
+ reference: str | None,
69
+ agent: str,
70
+ ) -> dict[str, object]:
71
+ """Resolve ``reference`` for a catch-all change and choose the next step."""
72
+ normalize_agent(agent)
73
+ configuration = load_configuration(storage.config_path, extensions)
74
+ if CATCHALL not in configuration.workflows_by_name:
75
+ raise ConfigurationError(
76
+ f"the {CATCHALL} workflow is switched off in {FILE_NAME}"
77
+ )
78
+ task_format = extensions.task_format()
79
+ explicit = task_format == EXPLICIT_TASK_FORMAT
80
+ known = tasks.task_ids()
81
+ if reference is not None and "/" in reference and tasks.task_exists(reference):
82
+ # A child task is named by its full ID; top-level listing leaves it out.
83
+ known = (*known, reference)
84
+ resolution = (
85
+ resolve_task_reference(reference, task_format, known)
86
+ if reference is not None
87
+ else None
88
+ )
89
+ report: dict[str, object] = {
90
+ "on_request": extensions.config.on_request,
91
+ "reference": reference,
92
+ "matches": [
93
+ _task(tasks, task_id)
94
+ for task_id in (resolution.matches if resolution else ())
95
+ ],
96
+ }
97
+ if resolution is not None and resolution.resolved is not None:
98
+ task_id = resolution.resolved
99
+ open_run = _open_run(tasks, task_id)
100
+ if open_run is not None and open_run["workflow"] != CATCHALL:
101
+ return {
102
+ **report,
103
+ "outcome": "continue",
104
+ "task_id": task_id,
105
+ "message": (
106
+ f"`{reference}` is task `{task_id}`, which has an unfinished "
107
+ f"`{open_run['workflow']}` run. The change belongs to that "
108
+ "run: continue it instead of starting another workflow."
109
+ ),
110
+ "command": instruction_command(task_id, role="manager"),
111
+ }
112
+ return {
113
+ **report,
114
+ "outcome": "start",
115
+ "task_id": task_id,
116
+ "message": (
117
+ f"`{reference}` is task `{task_id}`. Record the change there: "
118
+ f"start `{CATCHALL}` on it."
119
+ ),
120
+ "command": start_command(task_id, CATCHALL, agent),
121
+ }
122
+ if resolution is not None and resolution.matches:
123
+ choices = [
124
+ _Choice(
125
+ f"Use {task_id}",
126
+ _describe(tasks, task_id),
127
+ _continue_or_start(tasks, task_id, agent),
128
+ )
129
+ for task_id in resolution.matches
130
+ ]
131
+ message = (
132
+ f"`{reference}` matches several existing tasks. Ask the operator "
133
+ "which one this change belongs to."
134
+ )
135
+ outcome = "choose"
136
+ elif resolution is not None and resolution.proposed is not None:
137
+ proposed = resolution.proposed
138
+ choices = [
139
+ _Choice(
140
+ f"Create {proposed}",
141
+ f"Start a new ww task `{proposed}` and record this change in it.",
142
+ start_command(proposed, CATCHALL, agent),
143
+ )
144
+ ]
145
+ message = (
146
+ f"No task matches `{reference}`; ww has never seen `{proposed}`. "
147
+ "Do not create it on your own: ask the operator to confirm."
148
+ )
149
+ outcome = "confirm"
150
+ else:
151
+ choices = [_new_task_choice(explicit, agent)]
152
+ message = (
153
+ "The request names no task. Do not create one on your own: ask the "
154
+ "operator whether to record this change in a new task."
155
+ if resolution is None
156
+ else f"`{reference}` is not a valid task ID. Ask the operator "
157
+ "whether to record this change in a new task."
158
+ )
159
+ outcome = "confirm"
160
+ choices.append(
161
+ _Choice(WITHOUT_WW, "Make the change without recording it in ww.", None)
162
+ )
163
+ mechanism = choice_mechanism(agent)
164
+ return {
165
+ **report,
166
+ "outcome": outcome,
167
+ "message": message,
168
+ "choice_mechanism": mechanism.name,
169
+ "choice_instruction": mechanism.instruction,
170
+ "choices": [choice.to_dict() for choice in choices],
171
+ }
172
+
173
+
174
+ def _new_task_choice(explicit: bool, agent: str) -> _Choice:
175
+ if explicit:
176
+ # The project never generates IDs; the operator types the key through
177
+ # the menu's free-form answer, and the agent fills it in.
178
+ return _Choice(
179
+ "Create a task",
180
+ "Start a new ww task under the ID the operator gives, such as a "
181
+ "tracker key; ask for it if they did not type one.",
182
+ start_command(TASK_PLACEHOLDER, CATCHALL, agent),
183
+ )
184
+ return _Choice(
185
+ "Create a new task",
186
+ "Start a new ww task, with an ID ww assigns, and record this change in it.",
187
+ start_command(None, CATCHALL, agent),
188
+ )
189
+
190
+
191
+ def _open_run(tasks: TaskStorageAdapter, task_id: str) -> dict[str, str] | None:
192
+ run_id = tasks.active_execution_run(task_id)
193
+ if run_id is None:
194
+ return None
195
+ run = next(run for run in tasks.execution_runs(task_id) if run.run_id == run_id)
196
+ return {"run_id": run.run_id, "workflow": run.workflow, "status": run.status}
197
+
198
+
199
+ def _task(tasks: TaskStorageAdapter, task_id: str) -> dict[str, object]:
200
+ runs = tasks.execution_runs(task_id)
201
+ return {
202
+ "task_id": task_id,
203
+ "runs": len(runs),
204
+ "last_workflow": runs[-1].workflow if runs else None,
205
+ "open_run": _open_run(tasks, task_id),
206
+ }
207
+
208
+
209
+ def _describe(tasks: TaskStorageAdapter, task_id: str) -> str:
210
+ open_run = _open_run(tasks, task_id)
211
+ if open_run is not None:
212
+ return f"Existing task with an unfinished `{open_run['workflow']}` run."
213
+ runs = tasks.execution_runs(task_id)
214
+ if runs:
215
+ return f"Existing task; its last run was `{runs[-1].workflow}`."
216
+ return "Existing task with no runs yet."
217
+
218
+
219
+ def _continue_or_start(tasks: TaskStorageAdapter, task_id: str, agent: str) -> str:
220
+ open_run = _open_run(tasks, task_id)
221
+ if open_run is not None and open_run["workflow"] != CATCHALL:
222
+ return instruction_command(task_id, role="manager")
223
+ return start_command(task_id, CATCHALL, agent)
224
+
225
+
226
+ def render_lookup(
227
+ storage: Storage,
228
+ extensions: ExtensionRegistry,
229
+ tasks: TaskStorageAdapter,
230
+ reference: str | None,
231
+ agent: str,
232
+ json_output: bool,
233
+ ) -> str:
234
+ report = lookup(storage, extensions, tasks, reference, agent)
235
+ days = extensions.config.agent_hooks.recent_days
236
+ report["interrupted_recently"] = len(HookRecords(storage, tasks).recent(days))
237
+ if json_output:
238
+ return json.dumps(report, indent=2)
239
+ return "\n".join(_markdown(report, days))
240
+
241
+
242
+ def _markdown(report: dict[str, object], days: int) -> list[str]:
243
+ lines = ["# ww lookup", ""]
244
+ if report.get("on_request"):
245
+ lines.extend([f"**{ON_REQUEST_NOTE}**", ""])
246
+ lines.append(str(report["message"]))
247
+ count = report.get("interrupted_recently")
248
+ pointer = recent_interruptions_pointer(count if isinstance(count, int) else 0, days)
249
+ if pointer:
250
+ lines.extend(["", pointer])
251
+ command = report.get("command")
252
+ if isinstance(command, str):
253
+ return [*lines, "", "```console", command, "```"]
254
+ choices = report["choices"]
255
+ if not isinstance(choices, list): # pragma: no cover - built above
256
+ raise StateError("lookup choices are missing")
257
+ lines.extend(["", "### Ask the operator", "", str(report["choice_instruction"])])
258
+ lines.append("")
259
+ lines.extend(
260
+ f"{index}. **{choice['label']}** — {choice['description']}"
261
+ for index, choice in enumerate(choices, start=1)
262
+ )
263
+ lines.extend(["", "### After the answer", ""])
264
+ for choice in choices:
265
+ if choice["command"] is None:
266
+ lines.append(
267
+ f"- **{choice['label']}**: make the change directly, without "
268
+ "any ww command."
269
+ )
270
+ else:
271
+ lines.extend(
272
+ [
273
+ f"- **{choice['label']}**:",
274
+ "",
275
+ " ```console",
276
+ f" {choice['command']}",
277
+ " ```",
278
+ ]
279
+ )
280
+ lines.extend(
281
+ [
282
+ "",
283
+ "Run a command only after the operator picked its choice, and follow "
284
+ "each ww response from there.",
285
+ ]
286
+ )
287
+ return lines