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/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
|