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/discover.py
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""``discover``: everything an agent needs to choose and start a ww task.
|
|
3
|
+
|
|
4
|
+
The embeddable agent instructions stay short and send agents here, so the
|
|
5
|
+
choices shown always come from the project's current configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ww.builtin_workflows import CATCHALL, is_builtin
|
|
14
|
+
from ww.config import load_configuration, load_modes
|
|
15
|
+
from ww.contracts import CALLER_ROLES
|
|
16
|
+
from ww.discovery import AGENT_DIRECTORIES, CUSTOM_AGENT_PREFIX
|
|
17
|
+
from ww.errors import StateError
|
|
18
|
+
from ww.executable import ww_command
|
|
19
|
+
from ww.extensions import ExtensionRegistry
|
|
20
|
+
from ww.hooks.notices import (
|
|
21
|
+
display_workspace,
|
|
22
|
+
interruption_notice,
|
|
23
|
+
recent_interruptions_pointer,
|
|
24
|
+
task_line,
|
|
25
|
+
)
|
|
26
|
+
from ww.hooks.records import HookRecords
|
|
27
|
+
from ww.instructions.commands import TASK_PLACEHOLDER, instruction_command
|
|
28
|
+
from ww.onboarding import Onboarding, OnboardingState
|
|
29
|
+
from ww.open_work import OpenTask, open_work
|
|
30
|
+
from ww.project_config import FILE_NAME, ON_REQUEST
|
|
31
|
+
from ww.rule_conversion import scriptize_notice
|
|
32
|
+
from ww.rule_store import RuleStore
|
|
33
|
+
from ww.runtimes import RUNTIME_DESCRIPTIONS
|
|
34
|
+
from ww.storage import Storage
|
|
35
|
+
from ww.task_ids import EXPLICIT_TASK_FORMAT
|
|
36
|
+
from ww.workflow_config import (
|
|
37
|
+
ALL_NAMES,
|
|
38
|
+
WorkflowConfiguration,
|
|
39
|
+
delegation_requests,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
START_ARGUMENTS = (
|
|
43
|
+
"start <TASK-ID> --workflow <workflow> --agent <agent> "
|
|
44
|
+
'--requirements "<the user\'s requirements, normalized>" --role manager'
|
|
45
|
+
)
|
|
46
|
+
DISABLED_MESSAGE = (
|
|
47
|
+
"Do not use ww for this work: do not start, continue, or complete ww "
|
|
48
|
+
"tasks. Carry out the request without ww, and tell the user that ww is "
|
|
49
|
+
f'disabled in {FILE_NAME} ("enabled": false).'
|
|
50
|
+
)
|
|
51
|
+
ON_REQUEST_MESSAGE = (
|
|
52
|
+
"ww is used here only on request: use it only when the user explicitly "
|
|
53
|
+
"asks for ww, such as by saying to use ww, naming a ww task, or invoking "
|
|
54
|
+
"the `ww` skill; otherwise carry out the request without ww and do not ask."
|
|
55
|
+
)
|
|
56
|
+
ROLE_DESCRIPTIONS = {
|
|
57
|
+
"manager": "Runs start and next, dispatches assignments, and handles recovery.",
|
|
58
|
+
"worker": (
|
|
59
|
+
"Performs one assignment and runs only the --role worker commands ww shows it."
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
# Under ``"on_request"`` an unasked change never reaches the catch-all.
|
|
63
|
+
ON_REQUEST_CATCHALL_PREFIX = (
|
|
64
|
+
"Only when the user has asked for ww; otherwise make the change without ww. "
|
|
65
|
+
)
|
|
66
|
+
SETUP_GUIDANCE = (
|
|
67
|
+
"ww has not been set up in this project yet. Setup is optional and never "
|
|
68
|
+
"blocks ordinary work: carry on with the request, and mention the "
|
|
69
|
+
"`ww-setup` skill only if the operator asks to set ww up or asks what ww "
|
|
70
|
+
"can do here."
|
|
71
|
+
)
|
|
72
|
+
ON_REQUEST_SETUP_NOTE = (
|
|
73
|
+
"For information: ww has not been set up in this project yet; when the "
|
|
74
|
+
"operator asks for ww, the `ww-setup` skill can set it up."
|
|
75
|
+
)
|
|
76
|
+
UNREADABLE_GUIDANCE = (
|
|
77
|
+
"Other tasks and new work are unaffected. Commands addressing these tasks "
|
|
78
|
+
"fail with the error shown; ask the operator, whose choice it is to repair, "
|
|
79
|
+
"reset, or delete each task directory."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
ENABLED_SHORT = "ww is enabled for this project."
|
|
83
|
+
ON_REQUEST_START = (
|
|
84
|
+
"When the user has asked for ww, choose a workflow and start a task as below."
|
|
85
|
+
)
|
|
86
|
+
SELECTION_GUIDANCE = (
|
|
87
|
+
"Choose a workflow matching the request. When multiple workflows fit, prefer "
|
|
88
|
+
"local over project over global. Honor an explicitly requested workflow. If "
|
|
89
|
+
"the choice remains unclear, ask the operator."
|
|
90
|
+
)
|
|
91
|
+
BUILTIN_POINTER = (
|
|
92
|
+
"ww's own workflows (setup, learning, rule automation) are not listed here: "
|
|
93
|
+
"`{command} workflows` lists every workflow. Start one when the operator asks "
|
|
94
|
+
"for what it does or a ww skill says to."
|
|
95
|
+
)
|
|
96
|
+
CATCHALL_GUIDANCE = (
|
|
97
|
+
"Use it only when no workflow above fits and you are about to change files; "
|
|
98
|
+
"read-only work needs no task. Do not start it directly: run `lookup` with "
|
|
99
|
+
"the task this conversation works on, as the operator wrote it, such as "
|
|
100
|
+
"`12345`, or without one when there is none. It continues an unfinished run, "
|
|
101
|
+
"starts the catch-all on the task it found, or asks the operator before it "
|
|
102
|
+
"creates a task ww has never seen."
|
|
103
|
+
)
|
|
104
|
+
PROJECTS_GUIDANCE = (
|
|
105
|
+
"`--project <name>` works in that project's directory; without it the task "
|
|
106
|
+
"works in the root. A project's own `ww.json` may add `extensions` and a "
|
|
107
|
+
"`task_format` that apply there."
|
|
108
|
+
)
|
|
109
|
+
TASK_ID_GUIDANCE = (
|
|
110
|
+
"When the request names an external ticket, such as a Jira key, use it as "
|
|
111
|
+
"the task ID so the task matches the issue. Omit the task ID only when the "
|
|
112
|
+
"request names none, or when the workflow obtains its own in its first "
|
|
113
|
+
"step; ww then assigns one."
|
|
114
|
+
)
|
|
115
|
+
EXPLICIT_ID_GUIDANCE = (
|
|
116
|
+
"This project requires an explicit task ID: use the external ticket key "
|
|
117
|
+
"named in the request. Omit it only for a workflow that obtains its own ID "
|
|
118
|
+
"in its first step; ww never generates one here."
|
|
119
|
+
)
|
|
120
|
+
MODES_GUIDANCE = (
|
|
121
|
+
"Explicit `--mode` values replace the workflow's default modes, so repeat "
|
|
122
|
+
"any default you want to keep. Select a mode only when the request matches "
|
|
123
|
+
"its description; a mode marked always on applies by itself."
|
|
124
|
+
)
|
|
125
|
+
RUNTIME_GUIDANCE = (
|
|
126
|
+
"Use `auto` when the workflow requests specific workers (marked above); "
|
|
127
|
+
"`single` when nothing is requested, delegation is unavailable, or the user "
|
|
128
|
+
"asked you to do the work yourself. Omitted, the workflow's own runtime "
|
|
129
|
+
"applies, then the project default `{default}`."
|
|
130
|
+
)
|
|
131
|
+
MODEL_GUIDANCE = (
|
|
132
|
+
"`--model` and `--reasoning` describe your own session; omit them unless "
|
|
133
|
+
"you know them or the user asks."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def discover(storage: Storage, extensions: ExtensionRegistry) -> dict[str, object]:
|
|
138
|
+
"""Collect the choices and commands for starting a task in this project."""
|
|
139
|
+
config = extensions.config
|
|
140
|
+
if config.disabled:
|
|
141
|
+
return {"enabled": False, "message": DISABLED_MESSAGE}
|
|
142
|
+
configuration = load_configuration(storage.config_path, extensions)
|
|
143
|
+
explicit_ids = extensions.task_format() == EXPLICIT_TASK_FORMAT
|
|
144
|
+
default_runtime = extensions.config.runtime
|
|
145
|
+
modes = load_modes(storage.config_path, extensions)
|
|
146
|
+
modes.update({mode.name: mode for mode in extensions.qualified_modes()})
|
|
147
|
+
catchall = configuration.workflows_by_name.get(CATCHALL)
|
|
148
|
+
onboarding = Onboarding(storage.root, storage.project_metadata).read()
|
|
149
|
+
return {
|
|
150
|
+
# ``"on_request"`` still lists everything, so an explicit request can
|
|
151
|
+
# proceed; the header tells an agent not to use ww unasked.
|
|
152
|
+
"enabled": ON_REQUEST if config.on_request else True,
|
|
153
|
+
"onboarding": {
|
|
154
|
+
"setup_done": onboarding.setup_done,
|
|
155
|
+
"explain": onboarding.explain,
|
|
156
|
+
"guidance": _onboarding_guidance(onboarding, on_request=config.on_request),
|
|
157
|
+
},
|
|
158
|
+
# Declared rules no check covers yet; never a reason not to start.
|
|
159
|
+
"rules_notice": _rules_notice(configuration, storage.root),
|
|
160
|
+
"projects": [
|
|
161
|
+
{
|
|
162
|
+
**project.to_dict(),
|
|
163
|
+
"branch_strategies": list(extensions.branch_strategies(project.name)),
|
|
164
|
+
# The project's own task ID format; null when the root's applies.
|
|
165
|
+
"task_format": extensions.project_settings(project.name).task_format,
|
|
166
|
+
}
|
|
167
|
+
for project in extensions.config.projects
|
|
168
|
+
],
|
|
169
|
+
"workflows": [
|
|
170
|
+
{
|
|
171
|
+
"name": workflow.name,
|
|
172
|
+
"description": workflow.description,
|
|
173
|
+
**_workflow_source_fields(configuration, workflow.name),
|
|
174
|
+
"default_modes": list(workflow.modes),
|
|
175
|
+
"runtime": workflow.runtime,
|
|
176
|
+
"inherits": workflow.inherits,
|
|
177
|
+
"recommended_next_workflow": workflow.recommended_next_workflow,
|
|
178
|
+
"delegation_requests": list(delegation_requests(workflow)),
|
|
179
|
+
}
|
|
180
|
+
for workflow in configuration.workflows
|
|
181
|
+
if workflow is not catchall and not is_builtin(workflow)
|
|
182
|
+
],
|
|
183
|
+
# ww's own workflows, such as its learning ones; the catch-all is
|
|
184
|
+
# listed apart below.
|
|
185
|
+
"builtin_workflows": [
|
|
186
|
+
{
|
|
187
|
+
"name": workflow.name,
|
|
188
|
+
"description": workflow.description,
|
|
189
|
+
**_workflow_source_fields(configuration, workflow.name),
|
|
190
|
+
}
|
|
191
|
+
for workflow in configuration.workflows
|
|
192
|
+
if workflow is not catchall and is_builtin(workflow)
|
|
193
|
+
],
|
|
194
|
+
"catchall": (
|
|
195
|
+
{
|
|
196
|
+
"name": catchall.name,
|
|
197
|
+
"description": catchall.description,
|
|
198
|
+
**_workflow_source_fields(configuration, catchall.name),
|
|
199
|
+
"guidance": (
|
|
200
|
+
ON_REQUEST_CATCHALL_PREFIX + CATCHALL_GUIDANCE
|
|
201
|
+
if config.on_request
|
|
202
|
+
else CATCHALL_GUIDANCE
|
|
203
|
+
),
|
|
204
|
+
"start": f"{ww_command()} lookup [<task>] --agent <agent>",
|
|
205
|
+
}
|
|
206
|
+
if catchall is not None
|
|
207
|
+
else None
|
|
208
|
+
),
|
|
209
|
+
"modes": [
|
|
210
|
+
{
|
|
211
|
+
"name": mode.name,
|
|
212
|
+
"description": " ".join(mode.description),
|
|
213
|
+
"automatic": (
|
|
214
|
+
{
|
|
215
|
+
key: value.to_data()
|
|
216
|
+
for key, value in (
|
|
217
|
+
("workflows", mode.workflows),
|
|
218
|
+
("steps", mode.steps),
|
|
219
|
+
)
|
|
220
|
+
if value is not None
|
|
221
|
+
}
|
|
222
|
+
if mode.automatic
|
|
223
|
+
else None
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
for mode in modes.values()
|
|
227
|
+
],
|
|
228
|
+
"runtimes": [
|
|
229
|
+
{
|
|
230
|
+
"name": name,
|
|
231
|
+
"description": description,
|
|
232
|
+
"default": name == default_runtime,
|
|
233
|
+
}
|
|
234
|
+
for name, description in RUNTIME_DESCRIPTIONS.items()
|
|
235
|
+
],
|
|
236
|
+
"roles": [
|
|
237
|
+
{"name": role, "description": ROLE_DESCRIPTIONS[role]}
|
|
238
|
+
for role in CALLER_ROLES
|
|
239
|
+
],
|
|
240
|
+
"agents": [*AGENT_DIRECTORIES, f"{CUSTOM_AGENT_PREFIX}<name>"],
|
|
241
|
+
"branch_strategies": list(extensions.branch_strategies()),
|
|
242
|
+
"model_and_reasoning": MODEL_GUIDANCE,
|
|
243
|
+
"runtime_guidance": RUNTIME_GUIDANCE.format(default=default_runtime),
|
|
244
|
+
"task_id": (EXPLICIT_ID_GUIDANCE if explicit_ids else TASK_ID_GUIDANCE),
|
|
245
|
+
# Whether the project requires the task ID; the guidance above is prose.
|
|
246
|
+
"explicit_task_id": explicit_ids,
|
|
247
|
+
"modes_guidance": MODES_GUIDANCE,
|
|
248
|
+
"commands": {
|
|
249
|
+
"start": f"{ww_command()} {START_ARGUMENTS}",
|
|
250
|
+
"instruction": instruction_command(TASK_PLACEHOLDER, role="manager"),
|
|
251
|
+
"status": f"{ww_command()} status {TASK_PLACEHOLDER}",
|
|
252
|
+
"plan": f"{ww_command()} plan --workflow <workflow> --agent <agent>",
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _workflow_source_fields(
|
|
258
|
+
configuration: WorkflowConfiguration, name: str
|
|
259
|
+
) -> dict[str, str | None]:
|
|
260
|
+
"""Additive JSON provenance; null means a non-configured workflow source."""
|
|
261
|
+
provenance = configuration.workflow_provenance.get(name)
|
|
262
|
+
return {
|
|
263
|
+
"source": provenance.source if provenance is not None else None,
|
|
264
|
+
"source_level": provenance.level if provenance is not None else None,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def render_discover(
|
|
269
|
+
storage: Storage, extensions: ExtensionRegistry, json_output: bool
|
|
270
|
+
) -> str:
|
|
271
|
+
report = discover(storage, extensions)
|
|
272
|
+
days = extensions.config.agent_hooks.recent_days
|
|
273
|
+
unfinished: list[str] = []
|
|
274
|
+
if report["enabled"]:
|
|
275
|
+
records = HookRecords(storage, storage.task_persistence)
|
|
276
|
+
report["interrupted_recently"] = len(records.recent(days))
|
|
277
|
+
work = open_work(storage.task_persistence, storage.root)
|
|
278
|
+
report["unfinished_tasks"], unfinished = _unfinished(
|
|
279
|
+
work.tasks, records, storage.root
|
|
280
|
+
)
|
|
281
|
+
report["unreadable_tasks"] = [task.to_dict() for task in work.unreadable]
|
|
282
|
+
if json_output:
|
|
283
|
+
return json.dumps(report, indent=2)
|
|
284
|
+
if not report["enabled"]:
|
|
285
|
+
return "\n".join(
|
|
286
|
+
[
|
|
287
|
+
"# ww discover",
|
|
288
|
+
"",
|
|
289
|
+
"**ww is disabled for this project.**",
|
|
290
|
+
"",
|
|
291
|
+
DISABLED_MESSAGE,
|
|
292
|
+
]
|
|
293
|
+
)
|
|
294
|
+
return "\n".join(_markdown(report, days, unfinished))
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _unfinished(
|
|
298
|
+
tasks: tuple[OpenTask, ...], records: HookRecords, root: Path
|
|
299
|
+
) -> tuple[list[dict[str, object]], list[str]]:
|
|
300
|
+
"""Every unfinished task for the JSON, and its lines for the page."""
|
|
301
|
+
ww = ww_command()
|
|
302
|
+
entries: list[dict[str, object]] = []
|
|
303
|
+
lines: list[str] = []
|
|
304
|
+
for task in tasks:
|
|
305
|
+
interruption = records.interruption(task.task_id)
|
|
306
|
+
entries.append(
|
|
307
|
+
{
|
|
308
|
+
"task_id": task.task_id,
|
|
309
|
+
"workflow": task.workflow,
|
|
310
|
+
"agent": task.agent,
|
|
311
|
+
"step": task.label,
|
|
312
|
+
"item_status": task.item_status,
|
|
313
|
+
"workspace": display_workspace(task.workspace, root),
|
|
314
|
+
"updated_at": task.updated_at,
|
|
315
|
+
"resume": f"{ww} instruction {task.task_id} --role manager",
|
|
316
|
+
"interrupted": interruption is not None,
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
lines.append(task_line(task, root, ww))
|
|
320
|
+
if interruption is not None:
|
|
321
|
+
lines.append(" " + interruption_notice(interruption, task.task_id))
|
|
322
|
+
return entries, lines
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _mode_line(mode: dict[str, object]) -> str:
|
|
326
|
+
"""One catalog mode, with where it applies by itself when it is automatic."""
|
|
327
|
+
line = f"- `{mode['name']}` — {mode['description']}"
|
|
328
|
+
automatic = mode.get("automatic")
|
|
329
|
+
if isinstance(automatic, dict):
|
|
330
|
+
places = "; ".join(
|
|
331
|
+
f"{key} "
|
|
332
|
+
+ (
|
|
333
|
+
"all"
|
|
334
|
+
if value == ALL_NAMES
|
|
335
|
+
else ", ".join(f"`{name}`" for name in _strings(value))
|
|
336
|
+
)
|
|
337
|
+
for key, value in automatic.items()
|
|
338
|
+
)
|
|
339
|
+
line += f" Always on: {places}."
|
|
340
|
+
return line
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _markdown(report: dict[str, object], days: int, unfinished: list[str]) -> list[str]:
|
|
344
|
+
# Preference order is a display concern; the JSON keeps declaration order.
|
|
345
|
+
workflows = sorted(_entries(report["workflows"]), key=_level_rank)
|
|
346
|
+
modes = _entries(report["modes"])
|
|
347
|
+
runtimes = _entries(report["runtimes"])
|
|
348
|
+
projects = _entries(report["projects"])
|
|
349
|
+
strategies = _strings(report["branch_strategies"])
|
|
350
|
+
commands = report["commands"]
|
|
351
|
+
assert isinstance(commands, dict)
|
|
352
|
+
on_request = report["enabled"] == ON_REQUEST
|
|
353
|
+
lines = [
|
|
354
|
+
"# ww discover",
|
|
355
|
+
"",
|
|
356
|
+
*(
|
|
357
|
+
[f"**{ON_REQUEST_MESSAGE}**", "", ON_REQUEST_START]
|
|
358
|
+
if on_request
|
|
359
|
+
else [ENABLED_SHORT]
|
|
360
|
+
),
|
|
361
|
+
"",
|
|
362
|
+
*_pointer_lines(report, days),
|
|
363
|
+
*_unreadable_lines(report),
|
|
364
|
+
*(["## Unfinished tasks", "", *unfinished, ""] if unfinished else []),
|
|
365
|
+
*_onboarding_lines(report),
|
|
366
|
+
"## Workflows",
|
|
367
|
+
"",
|
|
368
|
+
]
|
|
369
|
+
if workflows:
|
|
370
|
+
lines.extend([SELECTION_GUIDANCE, ""])
|
|
371
|
+
lines.extend(_workflow_line(workflow) for workflow in workflows)
|
|
372
|
+
catchall = report["catchall"]
|
|
373
|
+
if not workflows and not catchall and not report["builtin_workflows"]:
|
|
374
|
+
lines.append("No workflows are configured; ww cannot start a task.")
|
|
375
|
+
if report["builtin_workflows"]:
|
|
376
|
+
lines.extend(["", BUILTIN_POINTER.format(command=ww_command())])
|
|
377
|
+
if isinstance(catchall, dict):
|
|
378
|
+
lines.extend(
|
|
379
|
+
[
|
|
380
|
+
"",
|
|
381
|
+
"## Changes no workflow covers",
|
|
382
|
+
"",
|
|
383
|
+
f"- `{catchall['name']}` — {catchall['description']}",
|
|
384
|
+
"",
|
|
385
|
+
str(catchall["guidance"]),
|
|
386
|
+
"",
|
|
387
|
+
"```console",
|
|
388
|
+
str(catchall["start"]),
|
|
389
|
+
"```",
|
|
390
|
+
]
|
|
391
|
+
)
|
|
392
|
+
if projects:
|
|
393
|
+
lines.extend(["", "## Projects", ""])
|
|
394
|
+
lines.extend(_project_line(project, strategies) for project in projects)
|
|
395
|
+
lines.extend(["", PROJECTS_GUIDANCE])
|
|
396
|
+
if modes:
|
|
397
|
+
lines.extend(["", "## Modes", ""])
|
|
398
|
+
lines.extend(_mode_line(mode) for mode in modes)
|
|
399
|
+
lines.extend(
|
|
400
|
+
[
|
|
401
|
+
"",
|
|
402
|
+
"## Start a task",
|
|
403
|
+
"",
|
|
404
|
+
"Square brackets denote optional arguments.",
|
|
405
|
+
"",
|
|
406
|
+
"```console",
|
|
407
|
+
*_start_synopsis(report, runtimes, projects, strategies),
|
|
408
|
+
"```",
|
|
409
|
+
"",
|
|
410
|
+
str(report["task_id"]),
|
|
411
|
+
"",
|
|
412
|
+
*_start_notes(report, runtimes),
|
|
413
|
+
"",
|
|
414
|
+
"To continue a task, or to check one, and optionally to preview a "
|
|
415
|
+
"workflow's plan:",
|
|
416
|
+
"",
|
|
417
|
+
"```console",
|
|
418
|
+
str(commands["instruction"]),
|
|
419
|
+
str(commands["status"]),
|
|
420
|
+
str(commands["plan"]),
|
|
421
|
+
"```",
|
|
422
|
+
"",
|
|
423
|
+
"After `start`, follow each ww response exactly until ww reports that "
|
|
424
|
+
"the workflow is complete.",
|
|
425
|
+
]
|
|
426
|
+
)
|
|
427
|
+
return lines
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
LEVEL_ORDER = {"local": 0, "project": 1, "global": 2}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _level_rank(workflow: dict[str, object]) -> int:
|
|
434
|
+
"""Where a workflow sorts: local, project, global, then any other source."""
|
|
435
|
+
return LEVEL_ORDER.get(str(workflow.get("source_level")), len(LEVEL_ORDER))
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _display_source(source: str) -> str:
|
|
439
|
+
"""A source path as an operator reads it: the home directory is ``~``."""
|
|
440
|
+
try:
|
|
441
|
+
return "~/" + Path(source).relative_to(Path.home()).as_posix()
|
|
442
|
+
except ValueError:
|
|
443
|
+
return source
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _workflow_label(workflow: dict[str, object]) -> str:
|
|
447
|
+
level, source = workflow.get("source_level"), workflow.get("source")
|
|
448
|
+
if isinstance(level, str) and isinstance(source, str):
|
|
449
|
+
return f"[{level}: {_display_source(source)}]"
|
|
450
|
+
return "[other: not from a configuration file]"
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _workflow_line(workflow: dict[str, object]) -> str:
|
|
454
|
+
text = (
|
|
455
|
+
f"- {workflow['name']} — {_workflow_label(workflow)} "
|
|
456
|
+
f"{workflow['description'] or 'No description.'}"
|
|
457
|
+
)
|
|
458
|
+
defaults = _strings(workflow["default_modes"])
|
|
459
|
+
if defaults:
|
|
460
|
+
text += " Default modes: " + ", ".join(f"`{m}`" for m in defaults) + "."
|
|
461
|
+
if workflow.get("runtime"):
|
|
462
|
+
text += f" Runtime: `{workflow['runtime']}`."
|
|
463
|
+
if workflow.get("inherits"):
|
|
464
|
+
text += f" Same steps as `{workflow['inherits']}`."
|
|
465
|
+
if workflow.get("recommended_next_workflow"):
|
|
466
|
+
text += (
|
|
467
|
+
f" Offers `{workflow['recommended_next_workflow']}` next, "
|
|
468
|
+
"on the operator's confirmation."
|
|
469
|
+
)
|
|
470
|
+
requests = _strings(workflow.get("delegation_requests", []))
|
|
471
|
+
if requests:
|
|
472
|
+
text += (
|
|
473
|
+
" Requests specific workers on "
|
|
474
|
+
+ ", ".join(f"`{name}`" for name in requests)
|
|
475
|
+
+ "; use `--runtime auto`."
|
|
476
|
+
)
|
|
477
|
+
return text
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _project_line(project: dict[str, object], strategies: list[str]) -> str:
|
|
481
|
+
line = f"- `{project['name']}` at `{project['path']}`"
|
|
482
|
+
if project["description"]:
|
|
483
|
+
line += f" — {project['description']}"
|
|
484
|
+
project_strategies = _strings(project.get("branch_strategies", []))
|
|
485
|
+
if project_strategies != strategies:
|
|
486
|
+
line += (
|
|
487
|
+
" Branch strategies there: "
|
|
488
|
+
+ (", ".join(f"`{name}`" for name in project_strategies) or "none")
|
|
489
|
+
+ "."
|
|
490
|
+
)
|
|
491
|
+
task_format = project.get("task_format")
|
|
492
|
+
if isinstance(task_format, str):
|
|
493
|
+
line += (
|
|
494
|
+
" Tasks there require an explicit ID."
|
|
495
|
+
if task_format == EXPLICIT_TASK_FORMAT
|
|
496
|
+
else f" Generated task IDs there follow `{task_format}`."
|
|
497
|
+
)
|
|
498
|
+
return line
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _start_synopsis(
|
|
502
|
+
report: dict[str, object],
|
|
503
|
+
runtimes: list[dict[str, object]],
|
|
504
|
+
projects: list[dict[str, object]],
|
|
505
|
+
strategies: list[str],
|
|
506
|
+
) -> list[str]:
|
|
507
|
+
task_id = "<task-id>" if report["explicit_task_id"] else "[<task-id>]"
|
|
508
|
+
runtime_names = "|".join(str(runtime["name"]) for runtime in runtimes)
|
|
509
|
+
optional = [
|
|
510
|
+
"[--mode <mode>]",
|
|
511
|
+
f"[--runtime <{runtime_names}>]",
|
|
512
|
+
"[--model <model>]",
|
|
513
|
+
"[--reasoning <level>]",
|
|
514
|
+
]
|
|
515
|
+
if projects:
|
|
516
|
+
names = "|".join(str(project["name"]) for project in projects)
|
|
517
|
+
optional.append(f"[--project <{names}>]")
|
|
518
|
+
if strategies:
|
|
519
|
+
optional.append(f"[--branch-strategy <{'|'.join(strategies)}>]")
|
|
520
|
+
indent = " "
|
|
521
|
+
return [
|
|
522
|
+
f"{ww_command()} start {task_id} --workflow <workflow> --agent <agent> \\",
|
|
523
|
+
f'{indent}--requirements "<the user\'s requirements, normalized>" \\',
|
|
524
|
+
*(f"{indent}{option} \\" for option in optional),
|
|
525
|
+
f"{indent}--role manager",
|
|
526
|
+
]
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _start_notes(
|
|
530
|
+
report: dict[str, object], runtimes: list[dict[str, object]]
|
|
531
|
+
) -> list[str]:
|
|
532
|
+
*named, custom = (f"`{agent}`" for agent in _strings(report["agents"]))
|
|
533
|
+
return [
|
|
534
|
+
f"- Agent: {', '.join(named)}, or {custom}.",
|
|
535
|
+
f"- {report['modes_guidance']}",
|
|
536
|
+
"- Runtimes: "
|
|
537
|
+
+ "; ".join(
|
|
538
|
+
f"`{runtime['name']}`"
|
|
539
|
+
+ (" (project default)" if runtime["default"] else "")
|
|
540
|
+
+ f" — {str(runtime['description']).rstrip('.')}"
|
|
541
|
+
for runtime in runtimes
|
|
542
|
+
)
|
|
543
|
+
+ f". {report['runtime_guidance']}",
|
|
544
|
+
f"- {report['model_and_reasoning']}",
|
|
545
|
+
]
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _entries(value: object) -> list[dict[str, object]]:
|
|
549
|
+
return (
|
|
550
|
+
[entry for entry in value if isinstance(entry, dict)]
|
|
551
|
+
if isinstance(value, list)
|
|
552
|
+
else []
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _strings(value: object) -> list[str]:
|
|
557
|
+
return [str(entry) for entry in value] if isinstance(value, list) else []
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _unreadable_lines(report: dict[str, object]) -> list[str]:
|
|
561
|
+
tasks = _entries(report.get("unreadable_tasks", []))
|
|
562
|
+
if not tasks:
|
|
563
|
+
return []
|
|
564
|
+
return [
|
|
565
|
+
"## Unreadable tasks",
|
|
566
|
+
"",
|
|
567
|
+
*(f"- `{task['task_id']}` — {task['reason']}" for task in tasks),
|
|
568
|
+
"",
|
|
569
|
+
UNREADABLE_GUIDANCE,
|
|
570
|
+
"",
|
|
571
|
+
]
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _pointer_lines(report: dict[str, object], days: int) -> list[str]:
|
|
575
|
+
count = report.get("interrupted_recently")
|
|
576
|
+
pointer = recent_interruptions_pointer(count if isinstance(count, int) else 0, days)
|
|
577
|
+
return [pointer, ""] if pointer else []
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _onboarding_guidance(state: OnboardingState, *, on_request: bool) -> list[str]:
|
|
581
|
+
"""What to offer on a first use; under ``"on_request"``, information only."""
|
|
582
|
+
if state.setup_done:
|
|
583
|
+
return []
|
|
584
|
+
return [ON_REQUEST_SETUP_NOTE if on_request else SETUP_GUIDANCE]
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _onboarding_lines(report: dict[str, object]) -> list[str]:
|
|
588
|
+
onboarding = report.get("onboarding")
|
|
589
|
+
guidance = (
|
|
590
|
+
_strings(onboarding.get("guidance")) if isinstance(onboarding, dict) else []
|
|
591
|
+
)
|
|
592
|
+
if not guidance:
|
|
593
|
+
return []
|
|
594
|
+
return ["## Onboarding", "", *(f"- {line}" for line in guidance), ""]
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _rules_notice(configuration: WorkflowConfiguration, root: Path) -> str | None:
|
|
598
|
+
"""The unscriptized-rules notice, or none when the store cannot be read.
|
|
599
|
+
|
|
600
|
+
``discover`` is the first command an agent runs; a malformed store is
|
|
601
|
+
reported by ``lint`` and by the commands that use it, never here.
|
|
602
|
+
"""
|
|
603
|
+
try:
|
|
604
|
+
automation = RuleStore(root).load()
|
|
605
|
+
except StateError:
|
|
606
|
+
return None
|
|
607
|
+
return scriptize_notice(configuration, automation)
|