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
|
@@ -0,0 +1,2293 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Human-first rendering for normalized execution instructions."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shlex
|
|
9
|
+
import shutil
|
|
10
|
+
import textwrap
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ww.agents import WAIT_VARIABLE, choice_mechanism, wait_mechanism
|
|
14
|
+
from ww.assessments import AssessmentOutcome
|
|
15
|
+
from ww.children import ChildTask
|
|
16
|
+
from ww.config_files import SETTINGS_FILE
|
|
17
|
+
from ww.contracts import OperatorReason
|
|
18
|
+
from ww.executable import DEFAULT_EXECUTABLE, ww_command
|
|
19
|
+
from ww.instructions import Instruction, InteractCommands
|
|
20
|
+
from ww.instructions.commands import (
|
|
21
|
+
add_item_command,
|
|
22
|
+
artifacts_command,
|
|
23
|
+
check_command,
|
|
24
|
+
dispute_command,
|
|
25
|
+
instruction_command,
|
|
26
|
+
next_command,
|
|
27
|
+
remove_item_command,
|
|
28
|
+
replan_command,
|
|
29
|
+
reword_item_command,
|
|
30
|
+
rule_command,
|
|
31
|
+
set_item_fields_command,
|
|
32
|
+
start_command,
|
|
33
|
+
)
|
|
34
|
+
from ww.instructions.handoff import HANDOFF_TITLE, handoff_markdown
|
|
35
|
+
from ww.instructions.models import (
|
|
36
|
+
FixFailure,
|
|
37
|
+
HandoffBlock,
|
|
38
|
+
VerificationPage,
|
|
39
|
+
)
|
|
40
|
+
from ww.instructions.policy import Audience, audience
|
|
41
|
+
from ww.instructions.text import NO_SUBAGENTS
|
|
42
|
+
from ww.interactions import RECOVERED
|
|
43
|
+
from ww.output_adapters.base import OutputAdapter
|
|
44
|
+
from ww.output_adapters.terminal import initialization_progress, terminal_accent
|
|
45
|
+
from ww.results import (
|
|
46
|
+
INITIALIZATION_NEXT_STEP,
|
|
47
|
+
NO_WORKFLOWS_ACTION,
|
|
48
|
+
InitializationResult,
|
|
49
|
+
ResetResult,
|
|
50
|
+
)
|
|
51
|
+
from ww.runtimes import requested_setting
|
|
52
|
+
|
|
53
|
+
Lines = list[str]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MarkdownOutputAdapter(OutputAdapter):
|
|
57
|
+
def render_instruction(self, instruction: Instruction) -> str:
|
|
58
|
+
page = self._page(instruction)
|
|
59
|
+
if instruction.handoff_block is None:
|
|
60
|
+
return page
|
|
61
|
+
return _document([page.rstrip("\n"), *_handoff(instruction.handoff_block)])
|
|
62
|
+
|
|
63
|
+
def _page(self, instruction: Instruction) -> str:
|
|
64
|
+
"""The instruction page itself, before any handoff block."""
|
|
65
|
+
if instruction.status == "task_summary":
|
|
66
|
+
return _document(_task_summary(instruction))
|
|
67
|
+
lines = _header(instruction)
|
|
68
|
+
if instruction.plan_change is not None:
|
|
69
|
+
_plan_changed(lines, instruction)
|
|
70
|
+
return _document(lines)
|
|
71
|
+
if instruction.status == "completed":
|
|
72
|
+
_completed(lines, instruction)
|
|
73
|
+
return _document(lines)
|
|
74
|
+
if instruction.status == "abandoned":
|
|
75
|
+
lines.extend(
|
|
76
|
+
[
|
|
77
|
+
"## Run abandoned",
|
|
78
|
+
"",
|
|
79
|
+
f"Run `{instruction.run_id}` was abandoned by a later start of "
|
|
80
|
+
"the workflow and is kept as history. Nothing is to be done "
|
|
81
|
+
"in it; work on the task's current run.",
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
return _document(lines)
|
|
85
|
+
if instruction.manager_only:
|
|
86
|
+
_manager_only(lines, instruction)
|
|
87
|
+
return _document(lines)
|
|
88
|
+
if instruction.fix_required is not None and instruction.status != "failed":
|
|
89
|
+
_fix_required(lines, instruction)
|
|
90
|
+
_continuation(lines, instruction)
|
|
91
|
+
return _document(lines)
|
|
92
|
+
if _continues_assignment(instruction):
|
|
93
|
+
return _document(_next_stage(lines, instruction))
|
|
94
|
+
_heading(lines, instruction)
|
|
95
|
+
_worker_selection(lines, instruction)
|
|
96
|
+
_assignment_preview(lines, instruction)
|
|
97
|
+
delegating = audience(instruction) is Audience.MANAGER_DELEGATING
|
|
98
|
+
if delegating and instruction.role == "worker":
|
|
99
|
+
_assignment_explicit_guidance(lines, instruction)
|
|
100
|
+
_worker_bootstrap(lines, instruction)
|
|
101
|
+
return _document(lines)
|
|
102
|
+
_assignment_scope(lines, instruction)
|
|
103
|
+
_working_directory(lines, instruction)
|
|
104
|
+
_profile(lines, instruction)
|
|
105
|
+
_task_requirements(lines, instruction)
|
|
106
|
+
_previous_step_result(lines, instruction)
|
|
107
|
+
_work(lines, instruction)
|
|
108
|
+
_explicit_guidance(lines, instruction)
|
|
109
|
+
_modes(lines, instruction)
|
|
110
|
+
_rules(lines, instruction)
|
|
111
|
+
_verification(lines, instruction)
|
|
112
|
+
_item_fields(lines, instruction)
|
|
113
|
+
_stored_items(lines, instruction)
|
|
114
|
+
_interaction(lines, instruction)
|
|
115
|
+
_documents(lines, instruction)
|
|
116
|
+
_run_handovers(lines, instruction)
|
|
117
|
+
_input_context(lines, instruction)
|
|
118
|
+
_loop_limit_recovery(lines, instruction)
|
|
119
|
+
_next_steps(lines, instruction)
|
|
120
|
+
_loop_outcome(lines, instruction)
|
|
121
|
+
_previous_artifacts(lines, instruction)
|
|
122
|
+
_error(lines, instruction)
|
|
123
|
+
_failure(lines, instruction)
|
|
124
|
+
_interrupted(lines, instruction)
|
|
125
|
+
if instruction.artifact:
|
|
126
|
+
lines.extend(["", f"Artifact: `{instruction.artifact}`"])
|
|
127
|
+
_continuation(lines, instruction)
|
|
128
|
+
return _document(lines)
|
|
129
|
+
|
|
130
|
+
def render_reset(self, result: ResetResult) -> str:
|
|
131
|
+
if result.removed:
|
|
132
|
+
return (
|
|
133
|
+
f"Task {result.task_id} was reset. Its state, plan, artifacts, "
|
|
134
|
+
"and handoff marker were removed.\n"
|
|
135
|
+
)
|
|
136
|
+
return f"Task {result.task_id} was not found; nothing was removed.\n"
|
|
137
|
+
|
|
138
|
+
def render_initialization(self, result: InitializationResult) -> str:
|
|
139
|
+
lines = [
|
|
140
|
+
f"{initialization_progress(100)} ww setup completed successfully in: "
|
|
141
|
+
f"{result.root}"
|
|
142
|
+
]
|
|
143
|
+
for title, entries in (
|
|
144
|
+
("Created or restored:", result.created),
|
|
145
|
+
("Already present and preserved:", result.preserved),
|
|
146
|
+
(
|
|
147
|
+
"Setup notes:",
|
|
148
|
+
tuple(
|
|
149
|
+
action for action in result.actions if action != NO_WORKFLOWS_ACTION
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
):
|
|
153
|
+
if entries:
|
|
154
|
+
lines.extend(["", title, *(f"- {item}" for item in entries)])
|
|
155
|
+
if _git_extension_active(result.root):
|
|
156
|
+
lines.extend(
|
|
157
|
+
[
|
|
158
|
+
"",
|
|
159
|
+
terminal_accent("Git support is enabled"),
|
|
160
|
+
" The ww/git extension is configured in ww.json",
|
|
161
|
+
" with its default settings. See:",
|
|
162
|
+
" https://github.com/from-developers-for-developers/agentic-workflows/blob/main/documentation/features.md#configuring-one",
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
lines.append("")
|
|
166
|
+
if result.permission_notice:
|
|
167
|
+
lines.extend(
|
|
168
|
+
_permission_notice(
|
|
169
|
+
result.executable,
|
|
170
|
+
result.commands,
|
|
171
|
+
result.permissions,
|
|
172
|
+
result.other_agents,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
# Getting started matters only until the first workflow exists.
|
|
176
|
+
if NO_WORKFLOWS_ACTION in result.actions:
|
|
177
|
+
lines.extend(
|
|
178
|
+
[
|
|
179
|
+
terminal_accent("Getting started"),
|
|
180
|
+
"",
|
|
181
|
+
" " + terminal_accent("1. Create your first workflow"),
|
|
182
|
+
" Define the steps in ww.yaml.",
|
|
183
|
+
"",
|
|
184
|
+
" " + terminal_accent("2. Start developing with your agent"),
|
|
185
|
+
" For example, type:",
|
|
186
|
+
"",
|
|
187
|
+
" /ww implement a user sign-in page",
|
|
188
|
+
"",
|
|
189
|
+
" " + terminal_accent("Run commands manually"),
|
|
190
|
+
" Use the project launcher for any ww command:",
|
|
191
|
+
"",
|
|
192
|
+
f" {shlex.quote(result.executable)} workflows",
|
|
193
|
+
"",
|
|
194
|
+
*_initialization_shortcut(result.executable),
|
|
195
|
+
]
|
|
196
|
+
)
|
|
197
|
+
lines.extend(
|
|
198
|
+
[
|
|
199
|
+
terminal_accent("Documentation"),
|
|
200
|
+
"",
|
|
201
|
+
" " + terminal_accent("README"),
|
|
202
|
+
" https://github.com/from-developers-for-developers/agentic-workflows",
|
|
203
|
+
"",
|
|
204
|
+
" " + terminal_accent("Workflow specification"),
|
|
205
|
+
" https://github.com/from-developers-for-developers/agentic-workflows/blob/main/documentation/specification.md",
|
|
206
|
+
"",
|
|
207
|
+
" " + terminal_accent("Examples"),
|
|
208
|
+
" https://github.com/from-developers-for-developers/agentic-workflows/blob/main/documentation/examples.md",
|
|
209
|
+
"",
|
|
210
|
+
" " + terminal_accent("All features"),
|
|
211
|
+
" https://github.com/from-developers-for-developers/agentic-workflows/blob/main/documentation/features.md",
|
|
212
|
+
"",
|
|
213
|
+
terminal_accent("Next steps"),
|
|
214
|
+
"",
|
|
215
|
+
f" {INITIALIZATION_NEXT_STEP}",
|
|
216
|
+
" In Claude Code, for example, type /ww-setup.",
|
|
217
|
+
]
|
|
218
|
+
)
|
|
219
|
+
return _document(lines)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _git_extension_active(root: str) -> bool:
|
|
223
|
+
try:
|
|
224
|
+
config = json.loads((Path(root) / SETTINGS_FILE).read_text())
|
|
225
|
+
extensions = config.get("extensions", {})
|
|
226
|
+
return isinstance(extensions, dict) and isinstance(
|
|
227
|
+
extensions.get("ww/git"), dict
|
|
228
|
+
)
|
|
229
|
+
except (OSError, json.JSONDecodeError, AttributeError):
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _permission_notice(
|
|
234
|
+
executable: str = DEFAULT_EXECUTABLE,
|
|
235
|
+
commands: tuple[str, ...] = (),
|
|
236
|
+
permissions: tuple[tuple[str, str, str], ...] = (),
|
|
237
|
+
other_agents: tuple[str, ...] = (),
|
|
238
|
+
) -> Lines:
|
|
239
|
+
"""The one setup step that fails loudly later if it is skipped.
|
|
240
|
+
|
|
241
|
+
Left as a trailing "tip" it was routinely missed, and the symptoms arrive
|
|
242
|
+
much later looking unrelated: a confirmation prompt on every ww command,
|
|
243
|
+
and an interactive step whose operator page cannot open a local port at
|
|
244
|
+
all. It gets a boxed heading of its own, above the next steps, for that
|
|
245
|
+
reason, and wraps to the terminal so the box never breaks.
|
|
246
|
+
|
|
247
|
+
For each agent whose permission format ww knows it names the file and the
|
|
248
|
+
exact entries; every other agent gets the command prefixes to allow.
|
|
249
|
+
"""
|
|
250
|
+
width = max(44, min(72, shutil.get_terminal_size((80, 24))[0]))
|
|
251
|
+
commands = commands or tuple(dict.fromkeys((executable, "./ww", "ww")))
|
|
252
|
+
heading = "Allow ww to run without confirmation"
|
|
253
|
+
rule = "─" * (width - 2)
|
|
254
|
+
|
|
255
|
+
def wrapped(paragraph: str) -> Lines:
|
|
256
|
+
return textwrap.wrap(
|
|
257
|
+
paragraph,
|
|
258
|
+
width - 2,
|
|
259
|
+
initial_indent=" ",
|
|
260
|
+
subsequent_indent=" ",
|
|
261
|
+
break_on_hyphens=False,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
lines: Lines = [
|
|
265
|
+
terminal_accent(f"┌{rule}┐"),
|
|
266
|
+
*(
|
|
267
|
+
# Padded before it is coloured, so the escape codes never count
|
|
268
|
+
# toward the width and the right edge always lines up.
|
|
269
|
+
terminal_accent(f"│ {row.ljust(width - 4)} │")
|
|
270
|
+
for row in textwrap.wrap(heading, width - 4)
|
|
271
|
+
),
|
|
272
|
+
terminal_accent(f"└{rule}┘"),
|
|
273
|
+
"",
|
|
274
|
+
*wrapped(
|
|
275
|
+
"ww runs the commands your ww.yaml configures — "
|
|
276
|
+
"your tests, linters, and commits — so an agent treats it as a "
|
|
277
|
+
"command needing confirmation and asks every single time. Allow "
|
|
278
|
+
"it once:"
|
|
279
|
+
),
|
|
280
|
+
"",
|
|
281
|
+
]
|
|
282
|
+
for agent, file, entries in permissions:
|
|
283
|
+
lines.extend(wrapped(f"{agent}: merge these entries into {file}"))
|
|
284
|
+
lines.extend(["", *(f" {row}" for row in entries.splitlines()), ""])
|
|
285
|
+
if other_agents or not permissions:
|
|
286
|
+
who = (
|
|
287
|
+
f"{', '.join(other_agents)}: allow"
|
|
288
|
+
if other_agents
|
|
289
|
+
else "In your agent, allow"
|
|
290
|
+
)
|
|
291
|
+
lines.extend(wrapped(f"{who} every command starting with:"))
|
|
292
|
+
lines.append("")
|
|
293
|
+
shortcut = " (when the shortcut exists)"
|
|
294
|
+
lines.extend(
|
|
295
|
+
f" {command}{shortcut if command == 'ww' else ''}"
|
|
296
|
+
for command in commands
|
|
297
|
+
)
|
|
298
|
+
lines.append("")
|
|
299
|
+
for paragraph in (
|
|
300
|
+
"Without this you get a prompt per step, and an interactive step's "
|
|
301
|
+
"operator page cannot open its local port from inside an agent "
|
|
302
|
+
"sandbox — it fails with a permission error rather than a busy port.",
|
|
303
|
+
"What you are trusting is your own ww.yaml, with its local and user "
|
|
304
|
+
"levels (ww.local.yaml here, ww.yaml in your user configuration "
|
|
305
|
+
"directory): review changes to them like a CI config, since whoever "
|
|
306
|
+
"edits them can run commands here.",
|
|
307
|
+
):
|
|
308
|
+
lines.extend(wrapped(paragraph))
|
|
309
|
+
lines.append("")
|
|
310
|
+
return lines
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _initialization_shortcut(name: str) -> Lines:
|
|
314
|
+
lines = [" " + terminal_accent("Optional global shortcut")]
|
|
315
|
+
executable = shutil.which(name)
|
|
316
|
+
if executable:
|
|
317
|
+
source = Path(executable).absolute()
|
|
318
|
+
target = source.with_name("ww")
|
|
319
|
+
if os.path.lexists(target):
|
|
320
|
+
lines.extend(
|
|
321
|
+
[f" A ww shortcut or executable already exists: {target}", ""]
|
|
322
|
+
)
|
|
323
|
+
return lines
|
|
324
|
+
lines.extend(
|
|
325
|
+
[
|
|
326
|
+
" Link the installed executable as ww in the same PATH directory:",
|
|
327
|
+
"",
|
|
328
|
+
f" ln -s {shlex.quote(str(source))} {shlex.quote(str(target))}",
|
|
329
|
+
]
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
lines.extend(
|
|
333
|
+
[
|
|
334
|
+
f" After installing {name}, create a ww shortcut:",
|
|
335
|
+
"",
|
|
336
|
+
f" ww_bin=$(command -v {shlex.quote(name)})",
|
|
337
|
+
' ln -s "$ww_bin" "$(dirname "$ww_bin")/ww"',
|
|
338
|
+
]
|
|
339
|
+
)
|
|
340
|
+
lines.extend(["", " Then run: ww workflows", ""])
|
|
341
|
+
return lines
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _handoff(block: HandoffBlock) -> Lines:
|
|
345
|
+
"""ww's report of the ended assignment, which the worker returns verbatim."""
|
|
346
|
+
lines: Lines = []
|
|
347
|
+
_append_section(lines, HANDOFF_TITLE)
|
|
348
|
+
lines.extend(
|
|
349
|
+
[
|
|
350
|
+
"Return the block below verbatim as your final message, nothing "
|
|
351
|
+
"else, and run no further `ww` command.",
|
|
352
|
+
"",
|
|
353
|
+
"```text",
|
|
354
|
+
handoff_markdown(block),
|
|
355
|
+
"```",
|
|
356
|
+
]
|
|
357
|
+
)
|
|
358
|
+
return lines
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _document(lines: Lines) -> str:
|
|
362
|
+
return "\n".join(lines) + "\n"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _task_summary(instruction: Instruction) -> Lines:
|
|
366
|
+
lines = [
|
|
367
|
+
f"# {instruction.task_id}",
|
|
368
|
+
"",
|
|
369
|
+
"> Generated by `ww`; authoritative for this task.",
|
|
370
|
+
"",
|
|
371
|
+
]
|
|
372
|
+
if instruction.manager_intro:
|
|
373
|
+
lines.extend(_manager_intro())
|
|
374
|
+
lines.extend(["## Manager: choose a workflow run", ""])
|
|
375
|
+
for run in instruction.task_runs:
|
|
376
|
+
summary = f" — {run.summary}" if run.summary else ""
|
|
377
|
+
lines.append(f"- `{run.run_id}` · `{run.workflow}` · {run.status}{summary}")
|
|
378
|
+
lines.extend(
|
|
379
|
+
[
|
|
380
|
+
"",
|
|
381
|
+
"Use `"
|
|
382
|
+
+ instruction_command(instruction.task_id, "<run-id>", role="manager")
|
|
383
|
+
+ "` for one run's detailed status.",
|
|
384
|
+
]
|
|
385
|
+
)
|
|
386
|
+
return lines
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _header(instruction: Instruction) -> Lines:
|
|
390
|
+
lines = [
|
|
391
|
+
f"# {instruction.task_id} · {instruction.workflow}",
|
|
392
|
+
"",
|
|
393
|
+
"> Generated by `ww`; authoritative for the current task state.",
|
|
394
|
+
]
|
|
395
|
+
if instruction.completion_held and instruction.fix_required is None:
|
|
396
|
+
lines.extend(
|
|
397
|
+
[
|
|
398
|
+
"> Completion accepted by `ww` and held: it is recorded once "
|
|
399
|
+
"its rules are verified.",
|
|
400
|
+
"",
|
|
401
|
+
]
|
|
402
|
+
)
|
|
403
|
+
elif (
|
|
404
|
+
instruction.completion_registered
|
|
405
|
+
and instruction.error is None
|
|
406
|
+
and instruction.fix_required is None
|
|
407
|
+
and instruction.status not in {"failed", "interrupted"}
|
|
408
|
+
):
|
|
409
|
+
lines.extend(["> Completion recorded successfully by `ww`.", ""])
|
|
410
|
+
else:
|
|
411
|
+
lines.append("")
|
|
412
|
+
if instruction.manager_intro:
|
|
413
|
+
lines.extend(_manager_intro())
|
|
414
|
+
if instruction.rules_notice:
|
|
415
|
+
lines.extend([instruction.rules_notice, ""])
|
|
416
|
+
for notice in instruction.notices:
|
|
417
|
+
lines.extend([f"> {notice}", ""])
|
|
418
|
+
return lines
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _completed(lines: Lines, instruction: Instruction) -> None:
|
|
422
|
+
lines.extend(
|
|
423
|
+
[
|
|
424
|
+
"## Manager: workflow complete",
|
|
425
|
+
"",
|
|
426
|
+
"All agent work and automated handlers completed successfully.",
|
|
427
|
+
]
|
|
428
|
+
)
|
|
429
|
+
if instruction.feedback_deduction_command:
|
|
430
|
+
lines.extend(
|
|
431
|
+
[
|
|
432
|
+
"",
|
|
433
|
+
"Eligible `learnable: true` artifacts are available. Suggest that the "
|
|
434
|
+
"agent run the `ww-deduce-feedback` skill to deduce negative feedback "
|
|
435
|
+
"points after this workflow. This is optional follow-up work; the "
|
|
436
|
+
"workflow is already complete. "
|
|
437
|
+
"It does not install rules or prune points.",
|
|
438
|
+
"",
|
|
439
|
+
"```console",
|
|
440
|
+
instruction.feedback_deduction_command,
|
|
441
|
+
"```",
|
|
442
|
+
]
|
|
443
|
+
)
|
|
444
|
+
if instruction.handoff:
|
|
445
|
+
lines.extend(["", f"Handoff: `{instruction.handoff}`"])
|
|
446
|
+
if instruction.parent_task_id is not None:
|
|
447
|
+
lines.extend(
|
|
448
|
+
[
|
|
449
|
+
"",
|
|
450
|
+
f"This is a child task; its parent `{instruction.parent_task_id}` "
|
|
451
|
+
"continues with:",
|
|
452
|
+
"",
|
|
453
|
+
"```console",
|
|
454
|
+
next_command(instruction.parent_task_id),
|
|
455
|
+
"```",
|
|
456
|
+
]
|
|
457
|
+
)
|
|
458
|
+
elif instruction.control == "handoff_manager":
|
|
459
|
+
lines.extend(["", "Control is with the manager for final reporting."])
|
|
460
|
+
_recommendation(lines, instruction)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _recommendation(lines: Lines, instruction: Instruction) -> None:
|
|
464
|
+
"""Offer the recommended next workflow; only the operator may accept it."""
|
|
465
|
+
workflow = instruction.recommended_workflow
|
|
466
|
+
if workflow is None:
|
|
467
|
+
return
|
|
468
|
+
start = f"Start {workflow}"
|
|
469
|
+
lines.extend(
|
|
470
|
+
[
|
|
471
|
+
"",
|
|
472
|
+
"### Recommended next workflow",
|
|
473
|
+
"",
|
|
474
|
+
f"This workflow recommends `{workflow}` next, on the same task. Do "
|
|
475
|
+
"not start it on your own: after your final report, ask the operator.",
|
|
476
|
+
"",
|
|
477
|
+
choice_mechanism(instruction.agent).instruction,
|
|
478
|
+
"",
|
|
479
|
+
f"1. **{start}** — Start the `{workflow}` workflow on "
|
|
480
|
+
f"`{instruction.task_id}`.",
|
|
481
|
+
"2. **Stop here** — End with this workflow and start nothing else.",
|
|
482
|
+
"",
|
|
483
|
+
f"If the operator picks **{start}**, run the command below and follow "
|
|
484
|
+
"each ww response from there; otherwise stop.",
|
|
485
|
+
"",
|
|
486
|
+
"```console",
|
|
487
|
+
start_command(instruction.task_id, workflow, instruction.agent),
|
|
488
|
+
"```",
|
|
489
|
+
]
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _heading(lines: Lines, instruction: Instruction) -> None:
|
|
494
|
+
heading = f"{_role(instruction)}: {_action_heading(instruction)}"
|
|
495
|
+
if instruction.item_status == "awaiting_input" and (
|
|
496
|
+
audience(instruction) is not Audience.MANAGER_DELEGATING
|
|
497
|
+
):
|
|
498
|
+
# A delegating manager keeps the "delegate the assignment" heading:
|
|
499
|
+
# the worker it selects is the one who provides the input.
|
|
500
|
+
heading = f"{_role(instruction)}: provide required input"
|
|
501
|
+
if instruction.operator_reason is not None:
|
|
502
|
+
reason = _OPERATOR_REASONS[instruction.operator_reason]
|
|
503
|
+
if instruction.operator_reason == "fix_limit" and instruction.handler_repair:
|
|
504
|
+
reason = "the handler repair reached its fix limit"
|
|
505
|
+
heading = f"Operator decision: {reason}"
|
|
506
|
+
if instruction.operation_id and instruction.item_status == "in_progress":
|
|
507
|
+
heading = "Manager: resolve the active automatic handler"
|
|
508
|
+
lines.extend([f"## {heading}", ""])
|
|
509
|
+
lines.extend(_awaiting_operator(instruction))
|
|
510
|
+
lines.extend(_role_instruction(instruction))
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
_OPERATOR_REASONS: dict[OperatorReason, str] = {
|
|
514
|
+
"handler_failed": "the automatic handler failed",
|
|
515
|
+
"work_failed": "the step's work failed",
|
|
516
|
+
"child_failed": "a child task failed",
|
|
517
|
+
"handler_interrupted": "an automatic handler was interrupted",
|
|
518
|
+
"loop_limit": "the loop reached its iteration limit",
|
|
519
|
+
"fix_limit": "the step's checks reached their fix limit",
|
|
520
|
+
"check_disputed": "the step's worker disputed a check",
|
|
521
|
+
"value_unavailable": "a value the step reads is not available yet",
|
|
522
|
+
"pass_incomplete": "an items pass is missing item records",
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _awaiting_operator(instruction: Instruction) -> Lines:
|
|
527
|
+
"""Say that ww now waits for the operator, to whoever may reach them."""
|
|
528
|
+
if instruction.operator_reason is None or (
|
|
529
|
+
instruction.workflow_runtime != "single" and instruction.caller_role == "worker"
|
|
530
|
+
):
|
|
531
|
+
return []
|
|
532
|
+
return [
|
|
533
|
+
"`ww` is waiting for the operator, the user "
|
|
534
|
+
f"(`operator_reason: {instruction.operator_reason}`). Stop and ask "
|
|
535
|
+
"them: the recovery commands below are theirs to choose, and nothing "
|
|
536
|
+
"runs until they do. If they already gave a standing authorization "
|
|
537
|
+
"for routine repairs of this kind (dependency installation, "
|
|
538
|
+
"formatting, retries), apply it without asking again; ask only for a "
|
|
539
|
+
"material decision or an action it does not cover.",
|
|
540
|
+
"",
|
|
541
|
+
]
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _worker_selection(lines: Lines, instruction: Instruction) -> None:
|
|
545
|
+
"""The worker the step requests, for the manager who selects it."""
|
|
546
|
+
if audience(instruction) is Audience.WORKER or not (
|
|
547
|
+
instruction.workflow_runtime == "auto"
|
|
548
|
+
and instruction.next_role == "worker"
|
|
549
|
+
and (
|
|
550
|
+
instruction.requested_agent
|
|
551
|
+
or requested_setting(instruction.requested_model)
|
|
552
|
+
or requested_setting(instruction.requested_reasoning)
|
|
553
|
+
or instruction.requested_profile
|
|
554
|
+
)
|
|
555
|
+
):
|
|
556
|
+
return
|
|
557
|
+
lines.extend(
|
|
558
|
+
[
|
|
559
|
+
"Requested worker:",
|
|
560
|
+
"",
|
|
561
|
+
*(
|
|
562
|
+
[f"- Agent: `{instruction.requested_agent}` (advisory)"]
|
|
563
|
+
if instruction.requested_agent
|
|
564
|
+
else []
|
|
565
|
+
),
|
|
566
|
+
*requested_setting_lines(
|
|
567
|
+
instruction.requested_model, instruction.requested_reasoning
|
|
568
|
+
),
|
|
569
|
+
f"- Profile: `{instruction.requested_profile or 'none'}`",
|
|
570
|
+
"",
|
|
571
|
+
]
|
|
572
|
+
)
|
|
573
|
+
if (
|
|
574
|
+
instruction.selected_agent
|
|
575
|
+
or instruction.selected_model
|
|
576
|
+
or instruction.selected_reasoning
|
|
577
|
+
):
|
|
578
|
+
lines.extend(
|
|
579
|
+
[
|
|
580
|
+
"Selected worker:",
|
|
581
|
+
"",
|
|
582
|
+
*(
|
|
583
|
+
f"- {label}: `{value}`"
|
|
584
|
+
for label, value in (
|
|
585
|
+
("Agent", instruction.selected_agent),
|
|
586
|
+
("Model", instruction.selected_model),
|
|
587
|
+
("Reasoning", instruction.selected_reasoning),
|
|
588
|
+
)
|
|
589
|
+
if value
|
|
590
|
+
),
|
|
591
|
+
"",
|
|
592
|
+
]
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _assignment_preview(lines: Lines, instruction: Instruction) -> None:
|
|
597
|
+
preview = instruction.assignment_preview
|
|
598
|
+
if not preview or audience(instruction) is Audience.WORKER_RETURNING:
|
|
599
|
+
return
|
|
600
|
+
if "selection_item_name" in preview:
|
|
601
|
+
_append_section(lines, "Upcoming assignment")
|
|
602
|
+
lines.extend(
|
|
603
|
+
[
|
|
604
|
+
f"Selection item: `{preview['selection_item_name']}` ",
|
|
605
|
+
f"Requested agent: `{preview['requested_agent']}` ",
|
|
606
|
+
*(
|
|
607
|
+
f"Requested {label}: `{value}` "
|
|
608
|
+
for label, value in (
|
|
609
|
+
("model", requested_setting(preview["requested_model"])),
|
|
610
|
+
(
|
|
611
|
+
"reasoning",
|
|
612
|
+
requested_setting(preview["requested_reasoning"]),
|
|
613
|
+
),
|
|
614
|
+
)
|
|
615
|
+
if value is not None
|
|
616
|
+
),
|
|
617
|
+
f"Requested profile: `{preview['requested_profile'] or 'none'}`",
|
|
618
|
+
*(
|
|
619
|
+
[
|
|
620
|
+
"Explicit work guidance applies to: "
|
|
621
|
+
+ ", ".join(
|
|
622
|
+
f"`{step}`" for step in _strings(preview["explicit_steps"])
|
|
623
|
+
)
|
|
624
|
+
+ "."
|
|
625
|
+
]
|
|
626
|
+
if preview.get("explicit_steps")
|
|
627
|
+
else []
|
|
628
|
+
),
|
|
629
|
+
]
|
|
630
|
+
)
|
|
631
|
+
scope = preview.get("item_scope") or preview.get("loop_scope")
|
|
632
|
+
if isinstance(scope, dict):
|
|
633
|
+
lines.extend(["", f"Scope: {_scope_summary(scope)}"])
|
|
634
|
+
else:
|
|
635
|
+
coordinator = "coordinator_item_id" in preview
|
|
636
|
+
title = "Coordinator work" if coordinator else "Upcoming assignment"
|
|
637
|
+
_append_section(lines, title)
|
|
638
|
+
lines.append(str(preview["message"]))
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _scope_summary(scope: dict[str, object]) -> str:
|
|
642
|
+
stages = ", ".join(f"`{name}`" for name in _strings(scope["stages"]))
|
|
643
|
+
if "loop_assignment" in scope:
|
|
644
|
+
return (
|
|
645
|
+
f"one worker performs these steps ({stages}) of one round of the "
|
|
646
|
+
f"`{scope['loop']}` loop."
|
|
647
|
+
)
|
|
648
|
+
item_ids = _strings(scope["item_ids"])
|
|
649
|
+
if scope["item_assignment"] == "together":
|
|
650
|
+
return (
|
|
651
|
+
f"one worker performs every stage ({stages}) of all {len(item_ids)} items."
|
|
652
|
+
)
|
|
653
|
+
return f"one worker performs every stage ({stages}) of item `{item_ids[0]}`."
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _strings(value: object) -> list[str]:
|
|
657
|
+
return [str(entry) for entry in value] if isinstance(value, list) else []
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _assignment_scope(lines: Lines, instruction: Instruction) -> None:
|
|
661
|
+
scope = instruction.assignment_scope
|
|
662
|
+
if scope is None or audience(instruction) is not Audience.WORKER:
|
|
663
|
+
return
|
|
664
|
+
_append_section(lines, "Assignment scope")
|
|
665
|
+
unit = "step" if "loop_assignment" in scope else "stage"
|
|
666
|
+
lines.extend(
|
|
667
|
+
[
|
|
668
|
+
f"In this assignment, {_scope_summary(scope)}",
|
|
669
|
+
"",
|
|
670
|
+
f"Do one {unit} at a time and complete each with its own worker "
|
|
671
|
+
f"completion command; `ww` replies with the next {unit} straight "
|
|
672
|
+
f"away. Do not start a later {unit} early. Keep going until `ww` "
|
|
673
|
+
"says control returns to the manager.",
|
|
674
|
+
]
|
|
675
|
+
)
|
|
676
|
+
_assignment_explicit_guidance(lines, instruction)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _assignment_explicit_guidance(lines: Lines, instruction: Instruction) -> None:
|
|
680
|
+
if not instruction.assignment_explicit_steps:
|
|
681
|
+
return
|
|
682
|
+
lines.extend(
|
|
683
|
+
[
|
|
684
|
+
"",
|
|
685
|
+
"Explicit work guidance applies to: "
|
|
686
|
+
+ ", ".join(f"`{step}`" for step in instruction.assignment_explicit_steps)
|
|
687
|
+
+ ". Their own pages carry the operation and per-file diff details.",
|
|
688
|
+
]
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _continues_assignment(instruction: Instruction) -> bool:
|
|
693
|
+
"""A worker moving to the next stage of the item assignment it already holds."""
|
|
694
|
+
return (
|
|
695
|
+
instruction.continues_assignment
|
|
696
|
+
and instruction.completion_registered
|
|
697
|
+
and instruction.item_status == "in_progress"
|
|
698
|
+
and audience(instruction) is Audience.WORKER
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _next_stage(lines: Lines, instruction: Instruction) -> Lines:
|
|
703
|
+
"""The compact instruction for a later stage of the same assignment.
|
|
704
|
+
|
|
705
|
+
The worker already holds the role, workspace, profile, and item or loop
|
|
706
|
+
context, so only the new stage's work and its completion command are
|
|
707
|
+
repeated.
|
|
708
|
+
"""
|
|
709
|
+
lines.extend(
|
|
710
|
+
[
|
|
711
|
+
f"## Worker: next stage, `{instruction.item_name}`",
|
|
712
|
+
"",
|
|
713
|
+
"Continue in the same assignment.",
|
|
714
|
+
]
|
|
715
|
+
)
|
|
716
|
+
_task_requirements(lines, instruction)
|
|
717
|
+
_work(lines, instruction)
|
|
718
|
+
_explicit_guidance(lines, instruction)
|
|
719
|
+
_modes(lines, instruction)
|
|
720
|
+
_rules(lines, instruction)
|
|
721
|
+
_documents(lines, instruction)
|
|
722
|
+
_loop_outcome(lines, instruction)
|
|
723
|
+
_continuation(lines, instruction)
|
|
724
|
+
return lines
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _worker_bootstrap(lines: Lines, instruction: Instruction) -> None:
|
|
728
|
+
_append_section(lines, "Worker bootstrap")
|
|
729
|
+
lines.extend(
|
|
730
|
+
[
|
|
731
|
+
"Pass only this command to the selected worker, with no task "
|
|
732
|
+
"details or commentary; it gives the worker its whole assignment:",
|
|
733
|
+
"",
|
|
734
|
+
"```console",
|
|
735
|
+
instruction_command(
|
|
736
|
+
instruction.task_id,
|
|
737
|
+
instruction.run_id,
|
|
738
|
+
role="worker",
|
|
739
|
+
assignment=instruction.assignment_token,
|
|
740
|
+
),
|
|
741
|
+
"```",
|
|
742
|
+
"",
|
|
743
|
+
f"The worker's final message is `ww`'s \"{HANDOFF_TITLE}\" block "
|
|
744
|
+
f"for assignment `{instruction.assignment_token or '<token>'}`: read "
|
|
745
|
+
"the outcome there; nothing else the worker says is a `ww` result.",
|
|
746
|
+
]
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _working_directory(lines: Lines, instruction: Instruction) -> None:
|
|
751
|
+
if instruction.working_directory:
|
|
752
|
+
_append_section(lines, "Working directory")
|
|
753
|
+
lines.extend(
|
|
754
|
+
[
|
|
755
|
+
"Before doing any task work, switch to this task workspace "
|
|
756
|
+
"and keep it as your current directory:",
|
|
757
|
+
"",
|
|
758
|
+
"```console",
|
|
759
|
+
f"cd {shlex.quote(instruction.working_directory)}",
|
|
760
|
+
"```",
|
|
761
|
+
]
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _profile(lines: Lines, instruction: Instruction) -> None:
|
|
766
|
+
if instruction.item_status == "in_progress" and instruction.profile_instruction:
|
|
767
|
+
lines.extend(["", "### Profile", "", instruction.profile_instruction])
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
# A paragraph shorter than this is not worth replacing by a pointer.
|
|
771
|
+
_DUPLICATE_PARAGRAPH = 80
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _task_requirements(lines: Lines, instruction: Instruction) -> None:
|
|
775
|
+
"""Show the requirements once per session, then point; amendments always show.
|
|
776
|
+
|
|
777
|
+
The first work page of a session carries the user's wording in full; its
|
|
778
|
+
later pages name the command that prints it again. What the work
|
|
779
|
+
instruction already quotes verbatim is not printed twice.
|
|
780
|
+
"""
|
|
781
|
+
if instruction.item_status != "in_progress":
|
|
782
|
+
return
|
|
783
|
+
text = instruction.task_requirements
|
|
784
|
+
amendments = instruction.task_amendments
|
|
785
|
+
if not text and not amendments:
|
|
786
|
+
return
|
|
787
|
+
_append_section(lines, "Task requirements")
|
|
788
|
+
if text and instruction.requirements_in_full:
|
|
789
|
+
lines.append(_without_duplicates(text, instruction.action_text))
|
|
790
|
+
elif text and instruction.requirements_command:
|
|
791
|
+
lines.append(
|
|
792
|
+
"The full task requirements are not repeated on this page; "
|
|
793
|
+
f"print them with `{instruction.requirements_command}`."
|
|
794
|
+
)
|
|
795
|
+
if amendments:
|
|
796
|
+
lines.extend(["", "Amendments to the requirements, oldest first:", ""])
|
|
797
|
+
lines.extend(
|
|
798
|
+
f"- {entry.at} · {entry.role}: {entry.text}" for entry in amendments
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def _without_duplicates(requirements: str, work: str | None) -> str:
|
|
803
|
+
"""The requirements with every paragraph the work text already quotes cut.
|
|
804
|
+
|
|
805
|
+
Only whole paragraphs found verbatim in the work instruction are replaced,
|
|
806
|
+
so nothing the work instruction does not repeat is ever dropped.
|
|
807
|
+
"""
|
|
808
|
+
if not work:
|
|
809
|
+
return requirements
|
|
810
|
+
paragraphs = requirements.split("\n\n")
|
|
811
|
+
marker = "(quoted in the work instruction below)"
|
|
812
|
+
kept: list[str] = []
|
|
813
|
+
for paragraph in paragraphs:
|
|
814
|
+
quoted = len(paragraph.strip()) >= _DUPLICATE_PARAGRAPH and (
|
|
815
|
+
paragraph.strip() in work
|
|
816
|
+
)
|
|
817
|
+
if quoted and kept[-1:] == [marker]:
|
|
818
|
+
continue
|
|
819
|
+
kept.append(marker if quoted else paragraph)
|
|
820
|
+
return "\n\n".join(kept)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _previous_step_result(lines: Lines, instruction: Instruction) -> None:
|
|
824
|
+
"""Hand the previous step's summary and artifact reference to this step."""
|
|
825
|
+
if instruction.item_status != "in_progress" or not instruction.previous_step:
|
|
826
|
+
return
|
|
827
|
+
_append_section(lines, "Previous step result")
|
|
828
|
+
if instruction.previous_step_summary:
|
|
829
|
+
lines.extend(
|
|
830
|
+
[
|
|
831
|
+
f"The `{instruction.previous_step}` step left this summary for you:",
|
|
832
|
+
"",
|
|
833
|
+
*_blockquote(instruction.previous_step_summary),
|
|
834
|
+
"",
|
|
835
|
+
]
|
|
836
|
+
)
|
|
837
|
+
lines.append(
|
|
838
|
+
f"Its full result, when you need more: `{instruction.previous_step_artifact}`"
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _work(lines: Lines, instruction: Instruction) -> None:
|
|
843
|
+
if not instruction.action_text:
|
|
844
|
+
return
|
|
845
|
+
_append_section(
|
|
846
|
+
lines,
|
|
847
|
+
"Loop limit reached" if instruction.loop_limit_reached else "Work instruction",
|
|
848
|
+
)
|
|
849
|
+
if instruction.ui and instruction.item_status == "in_progress":
|
|
850
|
+
lines.extend(
|
|
851
|
+
[
|
|
852
|
+
"> **Operator page.** This stage is answered by the operator on "
|
|
853
|
+
"the operator page and completed by ww from the answer. Do not "
|
|
854
|
+
"present the item or ask the operator yourself; read the "
|
|
855
|
+
"Operator page section below and run its command.",
|
|
856
|
+
"",
|
|
857
|
+
]
|
|
858
|
+
)
|
|
859
|
+
lines.append(instruction.action_text)
|
|
860
|
+
if not instruction.subagents and instruction.item_status == "in_progress":
|
|
861
|
+
lines.extend(["", f"> **No subagents.** {NO_SUBAGENTS}"])
|
|
862
|
+
_loop_round(lines, instruction)
|
|
863
|
+
_assessment_answers(lines, instruction)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _explicit_guidance(lines: Lines, instruction: Instruction) -> None:
|
|
867
|
+
if not instruction.explicit or instruction.item_status != "in_progress":
|
|
868
|
+
return
|
|
869
|
+
_append_section(lines, "Visible work")
|
|
870
|
+
lines.extend(
|
|
871
|
+
[
|
|
872
|
+
"Before each meaningful operation, describe what you are about to do. "
|
|
873
|
+
"After changing each file, show its concrete edits or a focused diff. "
|
|
874
|
+
"For large changes, provide a concrete diff artifact. Name every "
|
|
875
|
+
"changed file, and redact secrets. You may group related files into "
|
|
876
|
+
"batches, but do not hide edits behind a vague summary.",
|
|
877
|
+
]
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _modes(lines: Lines, instruction: Instruction) -> None:
|
|
882
|
+
"""The modes the step works in, each with its guidance."""
|
|
883
|
+
if not instruction.modes or instruction.item_status != "in_progress":
|
|
884
|
+
return
|
|
885
|
+
_append_section(lines, "Modes")
|
|
886
|
+
lines.append("Work in these modes throughout this step:")
|
|
887
|
+
lines.append("")
|
|
888
|
+
for mode in instruction.modes:
|
|
889
|
+
guidance = " ".join(mode.description)
|
|
890
|
+
lines.append(f"- `{mode.name}`" + (f" — {guidance}" if guidance else ""))
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def _rules(lines: Lines, instruction: Instruction) -> None:
|
|
894
|
+
"""The rules the step's worker follows, and those ww checks at completion."""
|
|
895
|
+
if not instruction.rules or instruction.item_status != "in_progress":
|
|
896
|
+
return
|
|
897
|
+
_append_section(lines, "Rules")
|
|
898
|
+
lines.extend(
|
|
899
|
+
[
|
|
900
|
+
"Follow these while working; ww checks them when you complete. "
|
|
901
|
+
f"Preview with `{check_command(instruction.task_id)}`; read a full "
|
|
902
|
+
f"rule with `{rule_command(instruction.task_id)}`.",
|
|
903
|
+
]
|
|
904
|
+
)
|
|
905
|
+
judged = [rule for rule in instruction.rules if not rule.has_command]
|
|
906
|
+
checked = [rule for rule in instruction.rules if rule.has_command]
|
|
907
|
+
if judged:
|
|
908
|
+
lines.append("")
|
|
909
|
+
for rule in judged:
|
|
910
|
+
scope = f" — {', '.join(rule.paths)}" if rule.paths else ""
|
|
911
|
+
lines.append(f"- `{rule.id}`{scope} — {rule.summary}")
|
|
912
|
+
if rule.interpretation:
|
|
913
|
+
lines.append(f" {rule.interpretation}")
|
|
914
|
+
if rule.missing is not None:
|
|
915
|
+
lines.append(
|
|
916
|
+
f" Its check `{rule.check}` does not run here: "
|
|
917
|
+
f"`{rule.missing}` is missing in this step's directory."
|
|
918
|
+
)
|
|
919
|
+
lines.extend(
|
|
920
|
+
[
|
|
921
|
+
"",
|
|
922
|
+
"A verifier, not you, judges these rules against the files you "
|
|
923
|
+
"changed after you complete.",
|
|
924
|
+
]
|
|
925
|
+
)
|
|
926
|
+
if checked:
|
|
927
|
+
lines.extend(
|
|
928
|
+
[
|
|
929
|
+
"",
|
|
930
|
+
"Checked automatically when you complete: "
|
|
931
|
+
+ ", ".join(f"`{rule.id}`" for rule in checked)
|
|
932
|
+
+ ".",
|
|
933
|
+
]
|
|
934
|
+
)
|
|
935
|
+
for reason, waived in _waivers(instruction.checks_waived):
|
|
936
|
+
names = ", ".join(f"`{check_id}`" for check_id in waived)
|
|
937
|
+
lines.extend(
|
|
938
|
+
[
|
|
939
|
+
"",
|
|
940
|
+
f"The operator waived these checks for this step ({names}): "
|
|
941
|
+
f"{reason}. ww does not run or verify them when you complete, "
|
|
942
|
+
"and the artifact records the waiver.",
|
|
943
|
+
]
|
|
944
|
+
)
|
|
945
|
+
lines.extend(
|
|
946
|
+
[
|
|
947
|
+
"",
|
|
948
|
+
"State in your artifact, under a **Rules** heading, which rules you "
|
|
949
|
+
"applied and any deviation with its reason.",
|
|
950
|
+
]
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _fix_required(lines: Lines, instruction: Instruction) -> None:
|
|
955
|
+
"""The page after ww rejected a completion: what failed, and how to go on."""
|
|
956
|
+
fix = instruction.fix_required
|
|
957
|
+
assert fix is not None
|
|
958
|
+
failed = len(fix.failures)
|
|
959
|
+
lines.extend(
|
|
960
|
+
[
|
|
961
|
+
f"## Fix required: {failed} of {fix.checks} checks failed "
|
|
962
|
+
f"(attempt {fix.attempt} of {fix.max_fixes})",
|
|
963
|
+
"",
|
|
964
|
+
"ww did not record your completion: the checks below failed on the "
|
|
965
|
+
"files this step changed. The step is still yours.",
|
|
966
|
+
]
|
|
967
|
+
)
|
|
968
|
+
_fix_failures(lines, fix.failures)
|
|
969
|
+
lines.extend(
|
|
970
|
+
[
|
|
971
|
+
"",
|
|
972
|
+
"Fix the causes, then complete again with a revised artifact (your "
|
|
973
|
+
"previous artifact is kept as a draft); "
|
|
974
|
+
f"`{check_command(instruction.task_id)}` previews the checks.",
|
|
975
|
+
"",
|
|
976
|
+
"If a check is wrong for this change, do not work around it: "
|
|
977
|
+
"dispute it with the evidence, and the operator decides:",
|
|
978
|
+
"",
|
|
979
|
+
"```console",
|
|
980
|
+
dispute_command(
|
|
981
|
+
instruction.task_id, assignment=instruction.assignment_token
|
|
982
|
+
),
|
|
983
|
+
"```",
|
|
984
|
+
]
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def _waivers(
|
|
989
|
+
waived: tuple[tuple[str, str], ...],
|
|
990
|
+
) -> list[tuple[str, list[str]]]:
|
|
991
|
+
"""Waived check IDs grouped by the operator's reason, in waiver order."""
|
|
992
|
+
reasons: dict[str, list[str]] = {}
|
|
993
|
+
for check_id, reason in waived:
|
|
994
|
+
reasons.setdefault(reason, []).append(check_id)
|
|
995
|
+
return list(reasons.items())
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _manager_only(lines: Lines, instruction: Instruction) -> None:
|
|
999
|
+
"""A worker asked for an item the manager performs: no command for it."""
|
|
1000
|
+
why = (
|
|
1001
|
+
"is interactive: only the manager's session can talk to the operator"
|
|
1002
|
+
if instruction.interactive
|
|
1003
|
+
else "is the manager's (`role: manager`)"
|
|
1004
|
+
)
|
|
1005
|
+
lines.extend(
|
|
1006
|
+
[
|
|
1007
|
+
f"## `{instruction.item_name}` is the manager's",
|
|
1008
|
+
"",
|
|
1009
|
+
f"`{instruction.item_name}` {why}, so the manager performs it in "
|
|
1010
|
+
"its own session; a worker never does. Your assignment has ended: "
|
|
1011
|
+
"do not perform this item and run no further `ww` command. Return "
|
|
1012
|
+
"to the manager with your last `ww` response.",
|
|
1013
|
+
]
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def _fix_failures(lines: Lines, failures: tuple[FixFailure, ...]) -> None:
|
|
1018
|
+
for failure in failures:
|
|
1019
|
+
if failure.hook:
|
|
1020
|
+
suffix = " (hook)"
|
|
1021
|
+
elif failure.judged:
|
|
1022
|
+
suffix = " (verifier's verdict)"
|
|
1023
|
+
elif failure.covers:
|
|
1024
|
+
suffix = (
|
|
1025
|
+
" (check covering "
|
|
1026
|
+
+ ", ".join(f"`{rule_id}`" for rule_id in failure.covers)
|
|
1027
|
+
+ ")"
|
|
1028
|
+
)
|
|
1029
|
+
else:
|
|
1030
|
+
suffix = ""
|
|
1031
|
+
_append_section(lines, f"`{failure.id}`{suffix}")
|
|
1032
|
+
if failure.text:
|
|
1033
|
+
lines.extend([failure.text, ""])
|
|
1034
|
+
if failure.command:
|
|
1035
|
+
lines.append(f"Command: {failure.command}")
|
|
1036
|
+
output = failure.output.strip()
|
|
1037
|
+
if failure.judged:
|
|
1038
|
+
lines.append("The verifier found:")
|
|
1039
|
+
else:
|
|
1040
|
+
lines.append("Output:" if output else "Output: nothing")
|
|
1041
|
+
lines.extend(f" {line}" for line in output.splitlines())
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def _verification(lines: Lines, instruction: Instruction) -> None:
|
|
1045
|
+
"""A verification item's rules, evidence, instructions, and result shape."""
|
|
1046
|
+
page = instruction.verification
|
|
1047
|
+
if page is None or instruction.item_status != "in_progress":
|
|
1048
|
+
return
|
|
1049
|
+
_append_section(lines, "Verification")
|
|
1050
|
+
if instruction.workflow_runtime == "single":
|
|
1051
|
+
lines.extend(
|
|
1052
|
+
[
|
|
1053
|
+
"This session also did that step's work: read the change as a "
|
|
1054
|
+
"reviewer would, not from memory of writing it.",
|
|
1055
|
+
"",
|
|
1056
|
+
]
|
|
1057
|
+
)
|
|
1058
|
+
lines.extend(["#### Rules", ""])
|
|
1059
|
+
for rule in page.rules:
|
|
1060
|
+
lines.append(f"- `{rule.id}`")
|
|
1061
|
+
lines.extend(f" {line}" for line in rule.text.splitlines())
|
|
1062
|
+
if rule.interpretation:
|
|
1063
|
+
lines.append(f" Interpretation: {rule.interpretation}")
|
|
1064
|
+
if rule.missing is not None:
|
|
1065
|
+
lines.append(
|
|
1066
|
+
f" Its check `{rule.check}` does not run here: `{rule.missing}` "
|
|
1067
|
+
"is missing in this step's directory, so judge it instead."
|
|
1068
|
+
)
|
|
1069
|
+
_verification_evidence(lines, page)
|
|
1070
|
+
_verification_results(lines)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _verification_evidence(lines: Lines, page: VerificationPage) -> None:
|
|
1074
|
+
lines.extend(["", "#### Change set", ""])
|
|
1075
|
+
if page.all_files:
|
|
1076
|
+
lines.append(
|
|
1077
|
+
"There is no git change set here: every file in the directory is in scope."
|
|
1078
|
+
)
|
|
1079
|
+
elif page.files:
|
|
1080
|
+
lines.append(f"Files the step changed ({len(page.files)}):")
|
|
1081
|
+
lines.append("")
|
|
1082
|
+
lines.extend(f"- `{path}`" for path in page.files)
|
|
1083
|
+
else:
|
|
1084
|
+
lines.append("The step changed no files.")
|
|
1085
|
+
if page.diff_command:
|
|
1086
|
+
lines.extend(
|
|
1087
|
+
["", "Show the change itself:", "", "```console", page.diff_command, "```"]
|
|
1088
|
+
)
|
|
1089
|
+
if page.draft_artifact:
|
|
1090
|
+
lines.extend(
|
|
1091
|
+
[
|
|
1092
|
+
"",
|
|
1093
|
+
"The step worker's artifact, held until you finish: "
|
|
1094
|
+
f"`{page.draft_artifact}`",
|
|
1095
|
+
]
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def _verification_results(lines: Lines) -> None:
|
|
1100
|
+
lines.extend(
|
|
1101
|
+
[
|
|
1102
|
+
"",
|
|
1103
|
+
"#### What to report",
|
|
1104
|
+
"",
|
|
1105
|
+
"Judge each rule against the change set: a verdict `pass` or "
|
|
1106
|
+
"`fail`, with evidence `file:line — what` for each failure. "
|
|
1107
|
+
"Complete with your findings as the artifact and one "
|
|
1108
|
+
"`--rule-result` JSON object per rule:",
|
|
1109
|
+
"",
|
|
1110
|
+
'`{"id": "<rule>", "status": "judged", "verdict": "fail", '
|
|
1111
|
+
'"failures": [{"file": "<path>", "line": 12, "what": "<what>"}]}`, '
|
|
1112
|
+
'or `"verdict": "pass"` without failures.',
|
|
1113
|
+
]
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def _outcome_effect(outcome: AssessmentOutcome) -> str:
|
|
1118
|
+
if outcome.stops:
|
|
1119
|
+
return "ends the workflow here, skipping everything after it"
|
|
1120
|
+
if not outcome.declared:
|
|
1121
|
+
return (
|
|
1122
|
+
f"runs nothing extra and continues with `{outcome.first_step}`"
|
|
1123
|
+
if outcome.first_step is not None
|
|
1124
|
+
else "runs nothing extra and continues the workflow"
|
|
1125
|
+
)
|
|
1126
|
+
if outcome.first_step is not None:
|
|
1127
|
+
return f"continues with `{outcome.first_step}`"
|
|
1128
|
+
return "continues the workflow"
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def _assessment_answers(lines: Lines, instruction: Instruction) -> None:
|
|
1132
|
+
if not instruction.assessment_outcomes or instruction.choosing_outcome_of:
|
|
1133
|
+
return
|
|
1134
|
+
_append_section(lines, "Possible outcomes")
|
|
1135
|
+
lines.append(
|
|
1136
|
+
"Your answer decides what runs next. Say plainly in the artifact which "
|
|
1137
|
+
"of these outcomes your assessment supports:"
|
|
1138
|
+
)
|
|
1139
|
+
lines.append("")
|
|
1140
|
+
lines.extend(
|
|
1141
|
+
f"- `{outcome.label}` — {_outcome_effect(outcome)}."
|
|
1142
|
+
for outcome in instruction.assessment_outcomes
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def _loop_round(lines: Lines, instruction: Instruction) -> None:
|
|
1147
|
+
"""Tell a loop body step which round it is in and what that round covers."""
|
|
1148
|
+
if (
|
|
1149
|
+
instruction.loop_name is None
|
|
1150
|
+
or instruction.loop_iteration is None
|
|
1151
|
+
or instruction.is_loop_control
|
|
1152
|
+
):
|
|
1153
|
+
return
|
|
1154
|
+
name = instruction.loop_name
|
|
1155
|
+
if instruction.loop_iteration <= 1:
|
|
1156
|
+
lines.extend(
|
|
1157
|
+
[
|
|
1158
|
+
"",
|
|
1159
|
+
f"This is the first round of the `{name}` loop. It builds on the "
|
|
1160
|
+
"work of the steps before the loop.",
|
|
1161
|
+
]
|
|
1162
|
+
)
|
|
1163
|
+
return
|
|
1164
|
+
lines.extend(
|
|
1165
|
+
[
|
|
1166
|
+
"",
|
|
1167
|
+
f"This is round {instruction.loop_iteration} of the `{name}` loop, "
|
|
1168
|
+
f"limit {instruction.max_rounds}. Concentrate on the work done "
|
|
1169
|
+
"in the previous rounds of this loop, not on the whole task. Their "
|
|
1170
|
+
"results are the artifacts in the loop's earlier iteration "
|
|
1171
|
+
"directories; list them with:",
|
|
1172
|
+
"",
|
|
1173
|
+
"```console",
|
|
1174
|
+
artifacts_command(instruction.task_id, instruction.run_id),
|
|
1175
|
+
"```",
|
|
1176
|
+
]
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def _item_fields(lines: Lines, instruction: Instruction) -> None:
|
|
1181
|
+
"""Custom item fields: what this step must set, and the flow's rules."""
|
|
1182
|
+
if instruction.item_status != "in_progress":
|
|
1183
|
+
return
|
|
1184
|
+
rules = instruction.item_identity or instruction.item_unique
|
|
1185
|
+
if not instruction.required_item_fields and not rules:
|
|
1186
|
+
return
|
|
1187
|
+
_append_section(lines, "Item fields")
|
|
1188
|
+
task_id = instruction.task_id
|
|
1189
|
+
if instruction.required_item_fields:
|
|
1190
|
+
target = (
|
|
1191
|
+
"every collected item" if instruction.collects_items else "this step's item"
|
|
1192
|
+
)
|
|
1193
|
+
lines.extend(
|
|
1194
|
+
[
|
|
1195
|
+
f"Set these custom fields on {target} before completing; "
|
|
1196
|
+
"completion is refused while any is empty. Several go in one "
|
|
1197
|
+
"command:",
|
|
1198
|
+
"",
|
|
1199
|
+
]
|
|
1200
|
+
)
|
|
1201
|
+
lines.extend(
|
|
1202
|
+
f"- `{field.name}`"
|
|
1203
|
+
+ (f" — {field.description}" if field.description else "")
|
|
1204
|
+
for field in instruction.required_item_fields
|
|
1205
|
+
)
|
|
1206
|
+
lines.extend(["", "```console", set_item_fields_command(task_id), "```"])
|
|
1207
|
+
if rules:
|
|
1208
|
+
lines.append("")
|
|
1209
|
+
if instruction.item_identity:
|
|
1210
|
+
lines.append(
|
|
1211
|
+
f"A new item must carry `{instruction.item_identity}`; `add-item` "
|
|
1212
|
+
"refuses one without it:"
|
|
1213
|
+
)
|
|
1214
|
+
command = add_item_command(task_id, instruction.item_identity)
|
|
1215
|
+
lines.extend(["", "```console", command, "```"])
|
|
1216
|
+
if instruction.item_unique:
|
|
1217
|
+
lines.extend(
|
|
1218
|
+
[
|
|
1219
|
+
"",
|
|
1220
|
+
"Across "
|
|
1221
|
+
+ ", ".join(f"`{n}`" for n in instruction.item_unique)
|
|
1222
|
+
+ " a value may appear once over all items, in this run and in "
|
|
1223
|
+
"the task's stored items; ww refuses a duplicate and names the "
|
|
1224
|
+
"item that holds it. Look an item up by a field with "
|
|
1225
|
+
f"`{_command_by(task_id)}`.",
|
|
1226
|
+
]
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def _command_by(task_id: str) -> str:
|
|
1231
|
+
return f"{ww_command()} item {task_id} --by <name>=<value>"
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def _stored_items(lines: Lines, instruction: Instruction) -> None:
|
|
1235
|
+
"""A shared item flow's collection: reconcile the stored items."""
|
|
1236
|
+
if instruction.item_status != "in_progress" or not instruction.shared_items:
|
|
1237
|
+
return
|
|
1238
|
+
_append_section(lines, "Stored items")
|
|
1239
|
+
task_id = instruction.task_id
|
|
1240
|
+
if not instruction.stored_items:
|
|
1241
|
+
lines.extend(
|
|
1242
|
+
[
|
|
1243
|
+
"This task shares its items across runs, and none are stored yet: "
|
|
1244
|
+
"split as instructed above. Every later run starts from the items "
|
|
1245
|
+
"you record now.",
|
|
1246
|
+
]
|
|
1247
|
+
)
|
|
1248
|
+
return
|
|
1249
|
+
lines.extend(
|
|
1250
|
+
[
|
|
1251
|
+
"This task shares its items across runs. The items below were "
|
|
1252
|
+
"collected in an earlier run and this run starts from them, with "
|
|
1253
|
+
"their outcomes cleared. Do not split again: compare the source "
|
|
1254
|
+
"with this list and make the list match it, adding what is new, "
|
|
1255
|
+
"removing what is gone, and rewording what changed. Keep IDs "
|
|
1256
|
+
"stable, so an item that changed is reworded, not replaced. Remove "
|
|
1257
|
+
"an item only when it is gone from the source, never because it "
|
|
1258
|
+
"was done: outcomes are per run, and every run keeps its own copy. "
|
|
1259
|
+
"If nothing changed, complete the step as it is.",
|
|
1260
|
+
"",
|
|
1261
|
+
]
|
|
1262
|
+
)
|
|
1263
|
+
lines.extend(
|
|
1264
|
+
f"- `{item.id}`: {item.item}"
|
|
1265
|
+
+ (f" (refers to `{item.reference_to_id}`)" if item.reference_to_id else "")
|
|
1266
|
+
+ (
|
|
1267
|
+
" [" + ", ".join(f"{k}={v}" for k, v in item.fields) + "]"
|
|
1268
|
+
if item.fields
|
|
1269
|
+
else ""
|
|
1270
|
+
)
|
|
1271
|
+
for item in instruction.stored_items
|
|
1272
|
+
)
|
|
1273
|
+
lines.extend(
|
|
1274
|
+
[
|
|
1275
|
+
"",
|
|
1276
|
+
"```console",
|
|
1277
|
+
add_item_command(task_id),
|
|
1278
|
+
remove_item_command(task_id),
|
|
1279
|
+
reword_item_command(task_id),
|
|
1280
|
+
"```",
|
|
1281
|
+
]
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
def _interaction(lines: Lines, instruction: Instruction) -> None:
|
|
1286
|
+
"""The contract of an interactive step: talk first, then record it once."""
|
|
1287
|
+
if instruction.item_status != "in_progress" or not instruction.interactive:
|
|
1288
|
+
return
|
|
1289
|
+
commands = instruction.interact_commands
|
|
1290
|
+
if commands is None: # pragma: no cover - the builder sets them together
|
|
1291
|
+
raise ValueError("an interactive step needs its interact commands")
|
|
1292
|
+
if instruction.ui:
|
|
1293
|
+
_operator_page(lines, instruction, commands)
|
|
1294
|
+
return
|
|
1295
|
+
_append_section(lines, "Interaction with the operator")
|
|
1296
|
+
recovered = sum(
|
|
1297
|
+
entry.speaker.endswith(RECOVERED) for entry in instruction.conversation
|
|
1298
|
+
)
|
|
1299
|
+
if instruction.interaction_ended:
|
|
1300
|
+
state = "The operator has ended this interaction; complete the step now."
|
|
1301
|
+
elif instruction.operator_paused:
|
|
1302
|
+
state = (
|
|
1303
|
+
"The operator is done for now. Stop here: do not complete the step, "
|
|
1304
|
+
"do not wait for them again, and do not delegate. The task keeps "
|
|
1305
|
+
"this state; when the operator returns, show this page with "
|
|
1306
|
+
f"`{commands.resume}` and go on."
|
|
1307
|
+
)
|
|
1308
|
+
elif recovered:
|
|
1309
|
+
state = (
|
|
1310
|
+
f"{recovered} entries recovered from the previous session's "
|
|
1311
|
+
"transcript; read them and continue from the last unanswered point."
|
|
1312
|
+
)
|
|
1313
|
+
elif instruction.interaction_entries:
|
|
1314
|
+
count = instruction.interaction_entries
|
|
1315
|
+
state = f"{count} entries recorded so far; the interaction is still open."
|
|
1316
|
+
else:
|
|
1317
|
+
state = "Nothing is recorded yet."
|
|
1318
|
+
lines.extend(
|
|
1319
|
+
[
|
|
1320
|
+
"Hold this conversation with the operator in this session, "
|
|
1321
|
+
"because a delegated worker cannot talk to them: present the matter, "
|
|
1322
|
+
"ask, listen, and clarify. Record nothing while you talk. "
|
|
1323
|
+
"Respond to the operator's questions and corrections until the "
|
|
1324
|
+
"conversation covers what this step needs. Treat clear contextual "
|
|
1325
|
+
"completion, such as `done`, `I'm done`, `looks good, continue`, "
|
|
1326
|
+
"or an appropriate final choice, as permission to end; if it is "
|
|
1327
|
+
"ambiguous, ask naturally whether they want to continue or finish. "
|
|
1328
|
+
"`Done for today` means pause: record the conversation and use the "
|
|
1329
|
+
"pause command, leaving the interaction open to resume later.",
|
|
1330
|
+
"",
|
|
1331
|
+
"When it ends, record both sides verbatim and end the interaction "
|
|
1332
|
+
"in one command, then complete the step; completion is refused "
|
|
1333
|
+
"while it is open.",
|
|
1334
|
+
"",
|
|
1335
|
+
"```console",
|
|
1336
|
+
commands.transcript,
|
|
1337
|
+
"```",
|
|
1338
|
+
"",
|
|
1339
|
+
state,
|
|
1340
|
+
]
|
|
1341
|
+
)
|
|
1342
|
+
_choices(lines, instruction)
|
|
1343
|
+
_conversation(lines, instruction)
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def _choices(lines: Lines, instruction: Instruction) -> None:
|
|
1347
|
+
if not instruction.choices:
|
|
1348
|
+
return
|
|
1349
|
+
lines.extend(["", "#### Choices", ""])
|
|
1350
|
+
lines.extend(
|
|
1351
|
+
f"{number}. `{option.label}`"
|
|
1352
|
+
+ (f" — {option.description}" if option.description else "")
|
|
1353
|
+
for number, option in enumerate(instruction.choices, 1)
|
|
1354
|
+
)
|
|
1355
|
+
lines.extend(
|
|
1356
|
+
[
|
|
1357
|
+
"",
|
|
1358
|
+
str(instruction.choice_mechanism),
|
|
1359
|
+
"",
|
|
1360
|
+
"The pick is required; it goes in `--choice` above.",
|
|
1361
|
+
"",
|
|
1362
|
+
(
|
|
1363
|
+
f"Chosen so far: `{instruction.chosen}`."
|
|
1364
|
+
if instruction.chosen
|
|
1365
|
+
else "Nothing chosen yet."
|
|
1366
|
+
),
|
|
1367
|
+
]
|
|
1368
|
+
)
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
def _operator_page(
|
|
1372
|
+
lines: Lines, instruction: Instruction, commands: InteractCommands
|
|
1373
|
+
) -> None:
|
|
1374
|
+
"""A stage answered on the operator page: the agent waits, ww applies."""
|
|
1375
|
+
_append_section(lines, "Operator page")
|
|
1376
|
+
if instruction.operator_paused:
|
|
1377
|
+
situation = (
|
|
1378
|
+
"The operator is done for now. Stop here: do not wait again and do "
|
|
1379
|
+
f"not delegate. When they return, show this page with "
|
|
1380
|
+
f"`{commands.resume}` and wait again."
|
|
1381
|
+
)
|
|
1382
|
+
else:
|
|
1383
|
+
situation = "Wait for the operator's answers:"
|
|
1384
|
+
mechanism = wait_mechanism(instruction.agent)
|
|
1385
|
+
command = commands.wait
|
|
1386
|
+
if mechanism.wait_seconds is not None:
|
|
1387
|
+
command = f"{WAIT_VARIABLE}={mechanism.wait_seconds} {command}"
|
|
1388
|
+
lines.extend(
|
|
1389
|
+
[
|
|
1390
|
+
"This stage is answered on the operator page, a local page that "
|
|
1391
|
+
"lists every item of the run. The operator answers each item there, "
|
|
1392
|
+
"in any order, with a pick and a comment. You do not talk to the "
|
|
1393
|
+
"operator for this stage and you do not complete it yourself: when "
|
|
1394
|
+
"the wait ends, ww applies the answers, completing this stage and "
|
|
1395
|
+
"every following item stage whose item was answered, in order, and "
|
|
1396
|
+
"the command prints what it applied and what remains, after the "
|
|
1397
|
+
"next page.",
|
|
1398
|
+
"",
|
|
1399
|
+
situation,
|
|
1400
|
+
"",
|
|
1401
|
+
"```console",
|
|
1402
|
+
command,
|
|
1403
|
+
"```",
|
|
1404
|
+
"",
|
|
1405
|
+
"The command returns when every item is answered, when the operator "
|
|
1406
|
+
"says they are done for now or closes the page, or after a while "
|
|
1407
|
+
"with nothing new. " + mechanism.instruction,
|
|
1408
|
+
]
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def _conversation(lines: Lines, instruction: Instruction) -> None:
|
|
1413
|
+
if not instruction.conversation:
|
|
1414
|
+
return
|
|
1415
|
+
lines.extend(["", "#### Conversation so far", ""])
|
|
1416
|
+
for entry in instruction.conversation:
|
|
1417
|
+
lines.append(f"**{entry.speaker}** · {entry.at}")
|
|
1418
|
+
lines.extend(_blockquote(entry.text))
|
|
1419
|
+
lines.append("")
|
|
1420
|
+
while lines and lines[-1] == "":
|
|
1421
|
+
lines.pop()
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def _documents(lines: Lines, instruction: Instruction) -> None:
|
|
1425
|
+
"""Name the documents this step edits in place, the one exception under .ww."""
|
|
1426
|
+
if instruction.item_status != "in_progress" or not instruction.documents:
|
|
1427
|
+
return
|
|
1428
|
+
_append_section(lines, "Documents to update")
|
|
1429
|
+
lines.extend(
|
|
1430
|
+
[
|
|
1431
|
+
"Create or edit these files in place; they are the only files under "
|
|
1432
|
+
"`.ww` you may write. Keep whatever format the document already has. "
|
|
1433
|
+
"Each must exist when you complete, and `ww` then records that this "
|
|
1434
|
+
"step updated it.",
|
|
1435
|
+
"",
|
|
1436
|
+
]
|
|
1437
|
+
)
|
|
1438
|
+
for document in instruction.documents:
|
|
1439
|
+
state = "exists" if document.exists else "does not exist yet; create it"
|
|
1440
|
+
lines.append(f"- `{document.name}` at `{document.path}` ({state})")
|
|
1441
|
+
if document.instruction:
|
|
1442
|
+
lines.append(f" {document.instruction}")
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
def _run_handovers(lines: Lines, instruction: Instruction) -> None:
|
|
1446
|
+
"""Give the workflow summary its inputs: each step's own handover."""
|
|
1447
|
+
if instruction.item_status != "in_progress" or not instruction.run_handovers:
|
|
1448
|
+
return
|
|
1449
|
+
_append_section(lines, "Step handovers of this run")
|
|
1450
|
+
lines.extend(
|
|
1451
|
+
[
|
|
1452
|
+
"Build the summary from these handovers, in order; each names its "
|
|
1453
|
+
"artifact for detail. State only what they, or the artifacts you "
|
|
1454
|
+
"actually read, contain: no counts, test results, or statuses from "
|
|
1455
|
+
"anywhere else, and nothing from other runs of this task.",
|
|
1456
|
+
"",
|
|
1457
|
+
]
|
|
1458
|
+
)
|
|
1459
|
+
for handover in instruction.run_handovers:
|
|
1460
|
+
text = handover.summary or "(no handover recorded)"
|
|
1461
|
+
lines.append(f"- `{handover.step}`: {text} (artifact: `{handover.artifact}`)")
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def _input_context(lines: Lines, instruction: Instruction) -> None:
|
|
1465
|
+
"""Show a value-waiting handler the work its values are about."""
|
|
1466
|
+
if instruction.item_status != "awaiting_input" or not instruction.input_context:
|
|
1467
|
+
return
|
|
1468
|
+
handler = instruction.item_name or "this handler"
|
|
1469
|
+
_append_section(lines, "Work these values describe")
|
|
1470
|
+
lines.extend(
|
|
1471
|
+
[
|
|
1472
|
+
f"Steps completed since `{handler}` last ran in this run, in order. "
|
|
1473
|
+
"Base the values on this work only, not on earlier rounds or runs:",
|
|
1474
|
+
"",
|
|
1475
|
+
]
|
|
1476
|
+
)
|
|
1477
|
+
for handover in instruction.input_context:
|
|
1478
|
+
text = handover.summary or "(no handover recorded)"
|
|
1479
|
+
lines.append(f"- `{handover.step}`: {text} (artifact: `{handover.artifact}`)")
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def _loop_limit_recovery(lines: Lines, instruction: Instruction) -> None:
|
|
1483
|
+
"""The operator's only way past a loop that reached its iteration limit."""
|
|
1484
|
+
if not instruction.loop_limit_reached or not instruction.recovery_commands:
|
|
1485
|
+
return
|
|
1486
|
+
if instruction.workflow_runtime != "single" and instruction.caller_role == "worker":
|
|
1487
|
+
return
|
|
1488
|
+
_append_section(lines, "Operator recovery")
|
|
1489
|
+
lines.append(
|
|
1490
|
+
"Wait for the decision of the user, who is the `ww` operator. If they "
|
|
1491
|
+
"resolve the remaining findings themselves, or accept them, run exactly "
|
|
1492
|
+
"this to leave the loop and continue with the steps after it:"
|
|
1493
|
+
)
|
|
1494
|
+
for command in instruction.recovery_commands:
|
|
1495
|
+
lines.extend(["", "```console", command.command, "```"])
|
|
1496
|
+
lines.extend(
|
|
1497
|
+
[
|
|
1498
|
+
"",
|
|
1499
|
+
"Do not run it without the operator's explicit approval. A further "
|
|
1500
|
+
"iteration needs a higher `max_rounds` in the configuration.",
|
|
1501
|
+
]
|
|
1502
|
+
)
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
def _next_steps(lines: Lines, instruction: Instruction) -> None:
|
|
1506
|
+
if instruction.next_steps:
|
|
1507
|
+
_append_section(lines, "Next steps")
|
|
1508
|
+
lines.extend(["Leave to them the work they cover:", ""])
|
|
1509
|
+
lines.extend(f"- {step}" for step in instruction.next_steps)
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def _loop_outcome(lines: Lines, instruction: Instruction) -> None:
|
|
1513
|
+
if instruction.loop_break_prompt and instruction.loop_break_command:
|
|
1514
|
+
_append_section(
|
|
1515
|
+
lines, "Children outcome" if instruction.breaks_children else "Loop outcome"
|
|
1516
|
+
)
|
|
1517
|
+
lines.extend(
|
|
1518
|
+
[
|
|
1519
|
+
f"Break condition: {instruction.loop_break_prompt}",
|
|
1520
|
+
"",
|
|
1521
|
+
(
|
|
1522
|
+
"Condition met — stop running children; the ones not started "
|
|
1523
|
+
"yet are skipped:"
|
|
1524
|
+
if instruction.breaks_children
|
|
1525
|
+
else "Condition met — break the loop:"
|
|
1526
|
+
),
|
|
1527
|
+
"",
|
|
1528
|
+
"```console",
|
|
1529
|
+
instruction.loop_break_command,
|
|
1530
|
+
"```",
|
|
1531
|
+
"",
|
|
1532
|
+
"Condition not met — use the worker completion command below.",
|
|
1533
|
+
]
|
|
1534
|
+
)
|
|
1535
|
+
if instruction.loop_continue_prompt and instruction.loop_continue_command:
|
|
1536
|
+
_append_section(lines, "Loop control")
|
|
1537
|
+
lines.extend(
|
|
1538
|
+
[
|
|
1539
|
+
f"Continue condition: {instruction.loop_continue_prompt}",
|
|
1540
|
+
"",
|
|
1541
|
+
"Condition met — continue from the beginning of the loop:",
|
|
1542
|
+
"",
|
|
1543
|
+
"```console",
|
|
1544
|
+
instruction.loop_continue_command,
|
|
1545
|
+
"```",
|
|
1546
|
+
"",
|
|
1547
|
+
"Condition not met — use the worker completion command below.",
|
|
1548
|
+
]
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
def _previous_artifacts(lines: Lines, instruction: Instruction) -> None:
|
|
1553
|
+
if instruction.item_status == "in_progress" and instruction.has_previous_artifacts:
|
|
1554
|
+
_append_section(lines, "Previous artifacts")
|
|
1555
|
+
lines.append(
|
|
1556
|
+
"Earlier steps' artifacts, listed as JSON with absolute paths: "
|
|
1557
|
+
f"`{artifacts_command(instruction.task_id, instruction.run_id)}`"
|
|
1558
|
+
)
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def _error(lines: Lines, instruction: Instruction) -> None:
|
|
1562
|
+
if instruction.error:
|
|
1563
|
+
_append_section(lines, "Error")
|
|
1564
|
+
lines.append(instruction.error)
|
|
1565
|
+
if instruction.result_saved is not None:
|
|
1566
|
+
saved = "was" if instruction.result_saved else "was not"
|
|
1567
|
+
lines.extend(["", f"The worker result {saved} saved before this stop."])
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
def _failure(lines: Lines, instruction: Instruction) -> None:
|
|
1571
|
+
if instruction.status != "failed":
|
|
1572
|
+
return
|
|
1573
|
+
if instruction.fix_required is not None:
|
|
1574
|
+
_fix_limit(lines, instruction)
|
|
1575
|
+
return
|
|
1576
|
+
if instruction.dispute is not None:
|
|
1577
|
+
_check_disputed(lines, instruction)
|
|
1578
|
+
return
|
|
1579
|
+
if instruction.operator_reason == "value_unavailable":
|
|
1580
|
+
_value_unavailable(lines, instruction)
|
|
1581
|
+
return
|
|
1582
|
+
if instruction.operator_reason == "pass_incomplete":
|
|
1583
|
+
_pass_incomplete(lines, instruction)
|
|
1584
|
+
return
|
|
1585
|
+
child = _failed_child(instruction)
|
|
1586
|
+
if child is None:
|
|
1587
|
+
lines.extend(["", *_failed_handler_guidance(instruction)])
|
|
1588
|
+
if instruction.recovery_commands and not (
|
|
1589
|
+
instruction.workflow_runtime != "single"
|
|
1590
|
+
and instruction.caller_role == "worker"
|
|
1591
|
+
):
|
|
1592
|
+
_append_section(lines, "Operator recovery")
|
|
1593
|
+
lines.append(
|
|
1594
|
+
"Tell the user, who is the `ww` operator, what failed, quoting "
|
|
1595
|
+
"its output, and that the work so far is saved and nothing after "
|
|
1596
|
+
"this step has run. Then wait for their choice; do not pick for "
|
|
1597
|
+
"them or move on to another task. Run exactly the option they "
|
|
1598
|
+
"choose:"
|
|
1599
|
+
)
|
|
1600
|
+
for command in instruction.recovery_commands:
|
|
1601
|
+
purpose = (
|
|
1602
|
+
"to run the failed handler again, once they fixed the cause"
|
|
1603
|
+
if command.action == "retry"
|
|
1604
|
+
else "to skip it, only with their explicit approval and a reason"
|
|
1605
|
+
)
|
|
1606
|
+
lines.extend(
|
|
1607
|
+
[
|
|
1608
|
+
"",
|
|
1609
|
+
f"{purpose.capitalize()}:",
|
|
1610
|
+
"",
|
|
1611
|
+
"```console",
|
|
1612
|
+
command.command,
|
|
1613
|
+
"```",
|
|
1614
|
+
]
|
|
1615
|
+
)
|
|
1616
|
+
return
|
|
1617
|
+
_append_section(lines, "Child task recovery")
|
|
1618
|
+
lines.extend(
|
|
1619
|
+
[
|
|
1620
|
+
f"Child `{child.id}` failed. This requires an operator "
|
|
1621
|
+
"decision. After addressing the child failure, resume its "
|
|
1622
|
+
"workflow with:",
|
|
1623
|
+
"",
|
|
1624
|
+
"```console",
|
|
1625
|
+
next_command(child.task_id),
|
|
1626
|
+
"```",
|
|
1627
|
+
"",
|
|
1628
|
+
"When the child completes, `ww` resumes the parent workflow automatically.",
|
|
1629
|
+
]
|
|
1630
|
+
)
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
def _plan_changed(lines: Lines, instruction: Instruction) -> None:
|
|
1634
|
+
"""The operator's stop when the workflow's definition changed mid-run."""
|
|
1635
|
+
change = instruction.plan_change
|
|
1636
|
+
assert change is not None
|
|
1637
|
+
lines.extend(
|
|
1638
|
+
[
|
|
1639
|
+
"## Operator: the workflow changed",
|
|
1640
|
+
"",
|
|
1641
|
+
f"The definition of workflow `{instruction.workflow}` changed since "
|
|
1642
|
+
"this run's plan was saved, and ww stopped before going on. Tell "
|
|
1643
|
+
"the user, who is the `ww` operator, what changed, as listed "
|
|
1644
|
+
"below, and wait for their choice; do not pick for them. Run "
|
|
1645
|
+
"exactly the option they choose.",
|
|
1646
|
+
]
|
|
1647
|
+
)
|
|
1648
|
+
_append_section(lines, "What changed")
|
|
1649
|
+
for item in change.changes:
|
|
1650
|
+
lines.append(f"- {item.kind.capitalize()}: {item.label}")
|
|
1651
|
+
for field in item.fields:
|
|
1652
|
+
lines.append(f" - `{field.name}`: `{field.before}` → `{field.after}`")
|
|
1653
|
+
if change.refusal is not None:
|
|
1654
|
+
_append_section(lines, "Options")
|
|
1655
|
+
lines.extend(
|
|
1656
|
+
[
|
|
1657
|
+
f"This change cannot be applied to this run: {change.refusal}.",
|
|
1658
|
+
"",
|
|
1659
|
+
"To carry on with the saved plan; ww does not ask again for "
|
|
1660
|
+
"this configuration:",
|
|
1661
|
+
"",
|
|
1662
|
+
"```console",
|
|
1663
|
+
replan_command(instruction.task_id, keep=True),
|
|
1664
|
+
"```",
|
|
1665
|
+
]
|
|
1666
|
+
)
|
|
1667
|
+
return
|
|
1668
|
+
_append_section(lines, "Options")
|
|
1669
|
+
take = (
|
|
1670
|
+
"To take the new definition from the first change on. This reruns "
|
|
1671
|
+
"finished steps, which needs the operator's explicit agreement: "
|
|
1672
|
+
+ ", ".join(change.reruns)
|
|
1673
|
+
+ "."
|
|
1674
|
+
if change.reruns
|
|
1675
|
+
else "To take the new definition from the first change on; nothing "
|
|
1676
|
+
"that already finished runs again."
|
|
1677
|
+
)
|
|
1678
|
+
lines.extend(
|
|
1679
|
+
[
|
|
1680
|
+
take,
|
|
1681
|
+
"",
|
|
1682
|
+
"```console",
|
|
1683
|
+
replan_command(instruction.task_id, rerun=bool(change.reruns)),
|
|
1684
|
+
"```",
|
|
1685
|
+
"",
|
|
1686
|
+
"To carry on with the saved plan; ww does not ask again for this "
|
|
1687
|
+
"configuration:",
|
|
1688
|
+
"",
|
|
1689
|
+
"```console",
|
|
1690
|
+
replan_command(instruction.task_id, keep=True),
|
|
1691
|
+
"```",
|
|
1692
|
+
]
|
|
1693
|
+
)
|
|
1694
|
+
|
|
1695
|
+
|
|
1696
|
+
def _fix_limit(lines: Lines, instruction: Instruction) -> None:
|
|
1697
|
+
"""A step whose checks failed as often as they allow: the operator decides."""
|
|
1698
|
+
fix = instruction.fix_required
|
|
1699
|
+
assert fix is not None
|
|
1700
|
+
worker = (
|
|
1701
|
+
instruction.workflow_runtime != "single" and instruction.caller_role == "worker"
|
|
1702
|
+
)
|
|
1703
|
+
lines.extend(
|
|
1704
|
+
[
|
|
1705
|
+
"",
|
|
1706
|
+
f"ww rejected this step's completion {fix.attempt} times; the last "
|
|
1707
|
+
"attempt failed these checks:",
|
|
1708
|
+
]
|
|
1709
|
+
)
|
|
1710
|
+
_fix_failures(lines, fix.failures)
|
|
1711
|
+
if worker:
|
|
1712
|
+
lines.extend(
|
|
1713
|
+
[
|
|
1714
|
+
"",
|
|
1715
|
+
f"Stop here. {_return_phrase(instruction)}: the operator "
|
|
1716
|
+
"decides how the step continues.",
|
|
1717
|
+
]
|
|
1718
|
+
)
|
|
1719
|
+
return
|
|
1720
|
+
_append_section(lines, "Operator recovery")
|
|
1721
|
+
lines.extend(
|
|
1722
|
+
[
|
|
1723
|
+
"The step is paused and nothing else runs until the user, who is "
|
|
1724
|
+
"the `ww` operator, decides. Show them the failures above, quoted, "
|
|
1725
|
+
"and ask for one of these choices; do not pick for them.",
|
|
1726
|
+
]
|
|
1727
|
+
)
|
|
1728
|
+
for command in instruction.recovery_commands:
|
|
1729
|
+
purpose = (
|
|
1730
|
+
"to give the worker another round of fixes, after the cause is understood"
|
|
1731
|
+
if command.action == "retry"
|
|
1732
|
+
else "to complete the step without these checks, only with the "
|
|
1733
|
+
"operator's explicit approval; the artifact records the reason"
|
|
1734
|
+
)
|
|
1735
|
+
lines.extend(
|
|
1736
|
+
["", f"{purpose.capitalize()}:", "", "```console", command.command, "```"]
|
|
1737
|
+
)
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
def _check_disputed(lines: Lines, instruction: Instruction) -> None:
|
|
1741
|
+
"""The worker disputes a check: its argument, the check, the choices."""
|
|
1742
|
+
dispute = instruction.dispute
|
|
1743
|
+
assert dispute is not None
|
|
1744
|
+
worker = (
|
|
1745
|
+
instruction.workflow_runtime != "single" and instruction.caller_role == "worker"
|
|
1746
|
+
)
|
|
1747
|
+
lines.extend(
|
|
1748
|
+
[
|
|
1749
|
+
"",
|
|
1750
|
+
f"The step's worker disputes `{dispute.failure.id}`, which rejected "
|
|
1751
|
+
f"completion attempt {dispute.attempt}. Its argument:",
|
|
1752
|
+
"",
|
|
1753
|
+
*(f"> {line}" for line in dispute.reason.splitlines()),
|
|
1754
|
+
]
|
|
1755
|
+
)
|
|
1756
|
+
_fix_failures(lines, (dispute.failure,))
|
|
1757
|
+
if worker:
|
|
1758
|
+
lines.extend(
|
|
1759
|
+
[
|
|
1760
|
+
"",
|
|
1761
|
+
f"Stop here. {_return_phrase(instruction)}: the operator "
|
|
1762
|
+
"decides whether the check stands.",
|
|
1763
|
+
]
|
|
1764
|
+
)
|
|
1765
|
+
return
|
|
1766
|
+
_append_section(lines, "Operator recovery")
|
|
1767
|
+
lines.extend(
|
|
1768
|
+
[
|
|
1769
|
+
"The step is paused and nothing else runs until the user, who is "
|
|
1770
|
+
"the `ww` operator, decides. Show them the check, its output and "
|
|
1771
|
+
"the worker's argument above, quoted, and ask for one of these "
|
|
1772
|
+
"choices; do not pick for them.",
|
|
1773
|
+
]
|
|
1774
|
+
)
|
|
1775
|
+
for command in instruction.recovery_commands:
|
|
1776
|
+
purpose = (
|
|
1777
|
+
"to let the check stand: the worker fixes its work, and the "
|
|
1778
|
+
"rejection still counts toward the fix limit"
|
|
1779
|
+
if command.action == "retry"
|
|
1780
|
+
else f"to waive `{dispute.failure.id}` for this step, only with the "
|
|
1781
|
+
"operator's explicit approval; the artifact records the reason"
|
|
1782
|
+
)
|
|
1783
|
+
lines.extend(
|
|
1784
|
+
["", f"{purpose.capitalize()}:", "", "```console", command.command, "```"]
|
|
1785
|
+
)
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
def _value_unavailable(lines: Lines, instruction: Instruction) -> None:
|
|
1789
|
+
"""A ``ww.`` value the step reads is missing: the step has not started."""
|
|
1790
|
+
worker = (
|
|
1791
|
+
instruction.workflow_runtime != "single" and instruction.caller_role == "worker"
|
|
1792
|
+
)
|
|
1793
|
+
lines.extend(
|
|
1794
|
+
[
|
|
1795
|
+
"",
|
|
1796
|
+
"The step has not started: its text reads the value(s) named in the "
|
|
1797
|
+
"error above, and what provides them has none for this task yet.",
|
|
1798
|
+
]
|
|
1799
|
+
)
|
|
1800
|
+
if worker:
|
|
1801
|
+
lines.extend(
|
|
1802
|
+
[
|
|
1803
|
+
"",
|
|
1804
|
+
f"Stop here. {_return_phrase(instruction)}: the operator decides "
|
|
1805
|
+
"how to go on.",
|
|
1806
|
+
]
|
|
1807
|
+
)
|
|
1808
|
+
return
|
|
1809
|
+
_append_section(lines, "Operator recovery")
|
|
1810
|
+
lines.extend(
|
|
1811
|
+
[
|
|
1812
|
+
"Nothing else runs until the user, who is the `ww` operator, "
|
|
1813
|
+
"decides. Show them the error above and ask for one of these "
|
|
1814
|
+
"choices; do not pick for them, and do not create what is missing "
|
|
1815
|
+
"yourself unless they ask you to.",
|
|
1816
|
+
]
|
|
1817
|
+
)
|
|
1818
|
+
for command in instruction.recovery_commands:
|
|
1819
|
+
purpose = (
|
|
1820
|
+
"to check the values again, once the operator made them available "
|
|
1821
|
+
"(for example created the task's branch)"
|
|
1822
|
+
if command.action == "retry"
|
|
1823
|
+
else "to skip the step, only with the operator's explicit approval"
|
|
1824
|
+
)
|
|
1825
|
+
lines.extend(
|
|
1826
|
+
["", f"{purpose.capitalize()}:", "", "```console", command.command, "```"]
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
def _pass_incomplete(lines: Lines, instruction: Instruction) -> None:
|
|
1831
|
+
"""A pass gate: the pass's items lack records; nothing failed or ran."""
|
|
1832
|
+
lines.extend(
|
|
1833
|
+
[
|
|
1834
|
+
"",
|
|
1835
|
+
"The items pass has finished its stages, but the items named in "
|
|
1836
|
+
"the error above lack what those stages declare. No handler "
|
|
1837
|
+
"failed, and the next step has not started.",
|
|
1838
|
+
]
|
|
1839
|
+
)
|
|
1840
|
+
if instruction.workflow_runtime != "single" and instruction.caller_role == "worker":
|
|
1841
|
+
lines.extend(
|
|
1842
|
+
[
|
|
1843
|
+
"",
|
|
1844
|
+
f"Stop here. {_return_phrase(instruction)}: the operator decides "
|
|
1845
|
+
"how to go on.",
|
|
1846
|
+
]
|
|
1847
|
+
)
|
|
1848
|
+
return
|
|
1849
|
+
_append_section(lines, "Operator recovery")
|
|
1850
|
+
lines.append(
|
|
1851
|
+
"Nothing else runs until the user, who is the `ww` operator, decides. "
|
|
1852
|
+
"Show them what each item lacks. Once the missing values are recorded "
|
|
1853
|
+
"with `update-item`, by them or by you at their request, check the "
|
|
1854
|
+
"items again:"
|
|
1855
|
+
)
|
|
1856
|
+
for command in instruction.recovery_commands:
|
|
1857
|
+
lines.extend(["", "```console", command.command, "```"])
|
|
1858
|
+
|
|
1859
|
+
|
|
1860
|
+
def _interrupted(lines: Lines, instruction: Instruction) -> None:
|
|
1861
|
+
locked_next = f"```console\n{next_command(instruction.task_id)}\n```"
|
|
1862
|
+
if instruction.item_status == "interrupted":
|
|
1863
|
+
lines.extend(
|
|
1864
|
+
[
|
|
1865
|
+
"",
|
|
1866
|
+
"The external outcome is unknown. Inspect it before choosing "
|
|
1867
|
+
"whether to retry or attest success:",
|
|
1868
|
+
"",
|
|
1869
|
+
locked_next,
|
|
1870
|
+
"",
|
|
1871
|
+
"Use `next --retry` to replay it, or `next --force "
|
|
1872
|
+
"--reason` to skip it after operator confirmation.",
|
|
1873
|
+
]
|
|
1874
|
+
)
|
|
1875
|
+
if instruction.recovery_commands:
|
|
1876
|
+
lines.extend(
|
|
1877
|
+
[
|
|
1878
|
+
"",
|
|
1879
|
+
"Recovery actions:",
|
|
1880
|
+
"",
|
|
1881
|
+
*(
|
|
1882
|
+
f"- `{recovery.action}`: `{recovery.command}`"
|
|
1883
|
+
for recovery in instruction.recovery_commands
|
|
1884
|
+
),
|
|
1885
|
+
]
|
|
1886
|
+
)
|
|
1887
|
+
elif instruction.operation_id and instruction.item_status == "in_progress":
|
|
1888
|
+
lines.extend(
|
|
1889
|
+
[
|
|
1890
|
+
"",
|
|
1891
|
+
"This automatic operation is still marked in progress. "
|
|
1892
|
+
"Use the locked next command if the process that owns "
|
|
1893
|
+
"it is no longer running:",
|
|
1894
|
+
"",
|
|
1895
|
+
locked_next,
|
|
1896
|
+
]
|
|
1897
|
+
)
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
def _continuation(lines: Lines, instruction: Instruction) -> None:
|
|
1901
|
+
if not instruction.continuation_command or instruction.status == "failed":
|
|
1902
|
+
return
|
|
1903
|
+
if instruction.handoff_block is not None:
|
|
1904
|
+
# The worker's turn ended: its block names the manager's command.
|
|
1905
|
+
return
|
|
1906
|
+
if instruction.choosing_outcome_of is not None:
|
|
1907
|
+
_outcome_commands(lines, instruction)
|
|
1908
|
+
return
|
|
1909
|
+
command = instruction.continuation_command
|
|
1910
|
+
if instruction.status == "interrupted":
|
|
1911
|
+
title = "Recover"
|
|
1912
|
+
elif instruction.item_status == "pending" or instruction.manager_input:
|
|
1913
|
+
title = "Manager command"
|
|
1914
|
+
elif instruction.workflow_runtime == "auto" and instruction.role == "manager":
|
|
1915
|
+
title = "Manager completion command"
|
|
1916
|
+
else:
|
|
1917
|
+
title = "Worker completion command"
|
|
1918
|
+
_append_section(lines, title)
|
|
1919
|
+
if instruction.item_status == "pending":
|
|
1920
|
+
worker_caller = (
|
|
1921
|
+
instruction.workflow_runtime == "auto"
|
|
1922
|
+
and instruction.caller_role == "worker"
|
|
1923
|
+
)
|
|
1924
|
+
if worker_caller:
|
|
1925
|
+
lines.extend(["Return this response to the manager. The manager runs:", ""])
|
|
1926
|
+
else:
|
|
1927
|
+
preview = instruction.assignment_preview
|
|
1928
|
+
if (
|
|
1929
|
+
instruction.workflow_runtime == "auto"
|
|
1930
|
+
and preview
|
|
1931
|
+
and "selection_item_name" in preview
|
|
1932
|
+
):
|
|
1933
|
+
command = next_command(
|
|
1934
|
+
instruction.task_id,
|
|
1935
|
+
selected_agent=str(preview["requested_agent"]),
|
|
1936
|
+
model=requested_setting(preview["requested_model"]),
|
|
1937
|
+
reasoning=requested_setting(preview["requested_reasoning"]),
|
|
1938
|
+
)
|
|
1939
|
+
lines.extend(
|
|
1940
|
+
[
|
|
1941
|
+
"To continue the workflow, run this; its worker values "
|
|
1942
|
+
"follow the request, so replace them if you select a "
|
|
1943
|
+
"different available worker:",
|
|
1944
|
+
"",
|
|
1945
|
+
]
|
|
1946
|
+
)
|
|
1947
|
+
else:
|
|
1948
|
+
lines.extend(["To continue the workflow, run:", ""])
|
|
1949
|
+
elif instruction.manager_input:
|
|
1950
|
+
lines.extend(["Provide the values and run:", ""])
|
|
1951
|
+
lead: list[str] = []
|
|
1952
|
+
if (
|
|
1953
|
+
instruction.item_status != "pending"
|
|
1954
|
+
and not instruction.manager_input
|
|
1955
|
+
and (instruction.next_role == "worker")
|
|
1956
|
+
):
|
|
1957
|
+
lead.append("When the work is finished, run this with every `<...>` replaced.")
|
|
1958
|
+
if instruction.summary_required and instruction.item_status == "in_progress":
|
|
1959
|
+
lead.append(_SUMMARY_GUIDANCE)
|
|
1960
|
+
if lead:
|
|
1961
|
+
lines.extend([" ".join(lead), ""])
|
|
1962
|
+
_required_values(lines, instruction)
|
|
1963
|
+
_required_metadata(lines, instruction)
|
|
1964
|
+
lines.extend(["```console", command, "```"])
|
|
1965
|
+
|
|
1966
|
+
|
|
1967
|
+
_SUMMARY_GUIDANCE = (
|
|
1968
|
+
"`--summary` is the next step's handover in a sentence or two (what you "
|
|
1969
|
+
"did, what it must know); the detail belongs in the artifact."
|
|
1970
|
+
)
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
def _required_values(lines: Lines, instruction: Instruction) -> None:
|
|
1974
|
+
if not instruction.required_values:
|
|
1975
|
+
return
|
|
1976
|
+
lines.extend(
|
|
1977
|
+
[
|
|
1978
|
+
"Values to supply in place of their `<...>` placeholders:",
|
|
1979
|
+
"",
|
|
1980
|
+
*(
|
|
1981
|
+
f"- `{value.name}` — {value.description}"
|
|
1982
|
+
for value in instruction.required_values
|
|
1983
|
+
),
|
|
1984
|
+
]
|
|
1985
|
+
)
|
|
1986
|
+
if any(value.name == "task_id" for value in instruction.required_values):
|
|
1987
|
+
lines.extend(
|
|
1988
|
+
[
|
|
1989
|
+
"",
|
|
1990
|
+
"Use the external ID returned by the tracker, such as "
|
|
1991
|
+
"`PROJ-482`; do not submit the literal `<task_id>` "
|
|
1992
|
+
"placeholder.",
|
|
1993
|
+
]
|
|
1994
|
+
)
|
|
1995
|
+
if instruction.previous_values:
|
|
1996
|
+
lines.extend(
|
|
1997
|
+
[
|
|
1998
|
+
"",
|
|
1999
|
+
"The handler failed with the values it was given last time. "
|
|
2000
|
+
"Supply corrected ones, or the same again when the cause lay "
|
|
2001
|
+
"elsewhere:",
|
|
2002
|
+
"",
|
|
2003
|
+
*(
|
|
2004
|
+
f"- `{name}`: {value.replace(chr(10), chr(92) + 'n')}"
|
|
2005
|
+
for name, value in instruction.previous_values
|
|
2006
|
+
),
|
|
2007
|
+
]
|
|
2008
|
+
)
|
|
2009
|
+
if instruction.automatic_context:
|
|
2010
|
+
names = ", ".join(f"`{name}`" for name in instruction.automatic_context)
|
|
2011
|
+
lines.extend(
|
|
2012
|
+
["", f"ww will run {names} automatically. Do not run it yourself."]
|
|
2013
|
+
)
|
|
2014
|
+
lines.append("")
|
|
2015
|
+
|
|
2016
|
+
|
|
2017
|
+
def _required_metadata(lines: Lines, instruction: Instruction) -> None:
|
|
2018
|
+
if not instruction.required_metadata:
|
|
2019
|
+
return
|
|
2020
|
+
scopes = {value.scope for value in instruction.required_metadata}
|
|
2021
|
+
scope_label = (
|
|
2022
|
+
f"{next(iter(scopes))} metadata"
|
|
2023
|
+
if len(scopes) == 1
|
|
2024
|
+
else "task and project metadata"
|
|
2025
|
+
)
|
|
2026
|
+
lines.extend([f"Detect and preserve these {scope_label} values:", ""])
|
|
2027
|
+
lines.extend(
|
|
2028
|
+
f"- `{'project_metadata' if value.scope == 'project' else 'metadata'}"
|
|
2029
|
+
f".{value.key}`, passed as `--metadata {value.name}=<value>`"
|
|
2030
|
+
+ (f" — {value.description}" if value.description else "")
|
|
2031
|
+
+ (
|
|
2032
|
+
f" (a list: repeat `--metadata {value.name}=<value>` once per value, "
|
|
2033
|
+
"or omit it when there is none; earlier values are kept)"
|
|
2034
|
+
if value.append
|
|
2035
|
+
else ""
|
|
2036
|
+
)
|
|
2037
|
+
for value in instruction.required_metadata
|
|
2038
|
+
)
|
|
2039
|
+
lines.append("")
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
def _manager_intro() -> Lines:
|
|
2043
|
+
return [
|
|
2044
|
+
"`ww` keeps this task's plan and progress and names each role's next "
|
|
2045
|
+
"command: follow its pages, without reading `ww.yaml` "
|
|
2046
|
+
"or the `ww` source to work out what comes next.",
|
|
2047
|
+
"",
|
|
2048
|
+
]
|
|
2049
|
+
|
|
2050
|
+
|
|
2051
|
+
def _failed_handler_guidance(instruction: Instruction) -> Lines:
|
|
2052
|
+
if instruction.workflow_runtime != "single" and instruction.caller_role == "worker":
|
|
2053
|
+
return [
|
|
2054
|
+
f"Stop here. {_return_phrase(instruction)} for resolution. Do not "
|
|
2055
|
+
"fix, rerun, or work around the failed automatic handler.",
|
|
2056
|
+
]
|
|
2057
|
+
return [
|
|
2058
|
+
"Do not retry on your own, and do not work around the failed automatic "
|
|
2059
|
+
"handler.",
|
|
2060
|
+
]
|
|
2061
|
+
|
|
2062
|
+
|
|
2063
|
+
def _failed_child(instruction: Instruction) -> ChildTask | None:
|
|
2064
|
+
if instruction.workflow_runtime != "single" and instruction.caller_role == "worker":
|
|
2065
|
+
return None
|
|
2066
|
+
if not instruction.is_child_workflow_control:
|
|
2067
|
+
return None
|
|
2068
|
+
return next(
|
|
2069
|
+
(child for child in instruction.child_tasks if child.status == "failed"), None
|
|
2070
|
+
)
|
|
2071
|
+
|
|
2072
|
+
|
|
2073
|
+
def _append_section(lines: Lines, title: str) -> None:
|
|
2074
|
+
while lines and lines[-1] == "":
|
|
2075
|
+
lines.pop()
|
|
2076
|
+
lines.extend(["", f"### {title}", ""])
|
|
2077
|
+
|
|
2078
|
+
|
|
2079
|
+
def _blockquote(value: str) -> Lines:
|
|
2080
|
+
return [f"> {line}" if line else ">" for line in value.splitlines()]
|
|
2081
|
+
|
|
2082
|
+
|
|
2083
|
+
def _role(instruction: Instruction) -> str:
|
|
2084
|
+
if instruction.loop_limit_reached:
|
|
2085
|
+
return "Manager"
|
|
2086
|
+
match audience(instruction):
|
|
2087
|
+
case Audience.SINGLE_SESSION:
|
|
2088
|
+
return "Manager and worker"
|
|
2089
|
+
case Audience.MANAGER_DELEGATING | Audience.MANAGER:
|
|
2090
|
+
return "Manager"
|
|
2091
|
+
case Audience.WORKER_RETURNING | Audience.WORKER:
|
|
2092
|
+
return "Worker"
|
|
2093
|
+
|
|
2094
|
+
|
|
2095
|
+
def _outcome_commands(lines: Lines, instruction: Instruction) -> None:
|
|
2096
|
+
_append_section(lines, "Choose the outcome")
|
|
2097
|
+
lines.extend(
|
|
2098
|
+
[
|
|
2099
|
+
f"`{instruction.choosing_outcome_of}` is complete. Read its result, "
|
|
2100
|
+
"choose the outcome it supports, and run that outcome's command:",
|
|
2101
|
+
"",
|
|
2102
|
+
]
|
|
2103
|
+
)
|
|
2104
|
+
for outcome in instruction.assessment_outcomes:
|
|
2105
|
+
lines.extend(
|
|
2106
|
+
[
|
|
2107
|
+
f"- `{outcome.label}` — {_outcome_effect(outcome)}:",
|
|
2108
|
+
"",
|
|
2109
|
+
" ```console",
|
|
2110
|
+
" " + next_command(instruction.task_id, outcome=outcome.label),
|
|
2111
|
+
" ```",
|
|
2112
|
+
]
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
def _action_heading(instruction: Instruction) -> str:
|
|
2117
|
+
name = instruction.item_name or "workflow"
|
|
2118
|
+
reader = audience(instruction)
|
|
2119
|
+
if instruction.handler_repair is not None:
|
|
2120
|
+
return (
|
|
2121
|
+
f"repair `{name}`"
|
|
2122
|
+
if instruction.item_status != "pending"
|
|
2123
|
+
else f"dispatch repair of `{name}`"
|
|
2124
|
+
)
|
|
2125
|
+
if instruction.choosing_outcome_of is not None:
|
|
2126
|
+
# Choosing is the manager's; the worker that assessed hands back.
|
|
2127
|
+
if reader is Audience.WORKER_RETURNING:
|
|
2128
|
+
return "return control to the manager"
|
|
2129
|
+
return f"choose the outcome of `{instruction.choosing_outcome_of}`"
|
|
2130
|
+
if instruction.loop_limit_reached:
|
|
2131
|
+
return f"escalate the `{name}` loop limit"
|
|
2132
|
+
if instruction.is_loop_control:
|
|
2133
|
+
return f"advance the `{name}` loop"
|
|
2134
|
+
if reader is Audience.MANAGER_DELEGATING:
|
|
2135
|
+
verb = "delegate" if instruction.role == "worker" else "perform"
|
|
2136
|
+
return f"{verb} the `{instruction.assignment_step or name}` assignment"
|
|
2137
|
+
if instruction.item_status == "pending":
|
|
2138
|
+
if reader is Audience.WORKER_RETURNING:
|
|
2139
|
+
return "return control to the manager"
|
|
2140
|
+
return f"dispatch the `{name}` assignment"
|
|
2141
|
+
if name == "init":
|
|
2142
|
+
return "record the task requirements"
|
|
2143
|
+
return f"perform `{name}`"
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
_ASSIGNMENT_COMPLETE = (
|
|
2147
|
+
"This assignment is complete. Stop here: do not run a manager command "
|
|
2148
|
+
"or any further `ww` command."
|
|
2149
|
+
)
|
|
2150
|
+
|
|
2151
|
+
|
|
2152
|
+
def _return_phrase(instruction: Instruction) -> str:
|
|
2153
|
+
"""What a worker whose turn ended hands back: ww's block when it has one."""
|
|
2154
|
+
if instruction.handoff_block is not None:
|
|
2155
|
+
return f'Return the "{HANDOFF_TITLE}" block below to the manager'
|
|
2156
|
+
return "Return this `ww` response to the manager"
|
|
2157
|
+
|
|
2158
|
+
|
|
2159
|
+
def _assignment_complete(instruction: Instruction) -> Lines:
|
|
2160
|
+
if instruction.handoff_block is not None:
|
|
2161
|
+
# The block's own section says what to return, and to stop.
|
|
2162
|
+
return ["This assignment is complete.", ""]
|
|
2163
|
+
return [f"{_ASSIGNMENT_COMPLETE} {_return_phrase(instruction)}.", ""]
|
|
2164
|
+
|
|
2165
|
+
|
|
2166
|
+
_RUN_MANAGER_COMMAND = (
|
|
2167
|
+
"You are the manager. Run the displayed manager command yourself."
|
|
2168
|
+
)
|
|
2169
|
+
|
|
2170
|
+
|
|
2171
|
+
def _assignment_coverage(instruction: Instruction) -> Lines:
|
|
2172
|
+
"""Name every item one worker performs in this assignment, when several."""
|
|
2173
|
+
first, *rest = instruction.assignment_items or ("",)
|
|
2174
|
+
if not rest:
|
|
2175
|
+
return []
|
|
2176
|
+
names = ", ".join(f"`{name}`" for name in (first, *rest))
|
|
2177
|
+
performer = "One worker performs" if instruction.role == "worker" else "You perform"
|
|
2178
|
+
return [
|
|
2179
|
+
f"This assignment covers, in order: {names}. {performer} them "
|
|
2180
|
+
"all; `ww` hands each one over after the previous completion.",
|
|
2181
|
+
"",
|
|
2182
|
+
]
|
|
2183
|
+
|
|
2184
|
+
|
|
2185
|
+
def _role_instruction(instruction: Instruction) -> Lines:
|
|
2186
|
+
reader = audience(instruction)
|
|
2187
|
+
if instruction.loop_limit_reached:
|
|
2188
|
+
worker_caller = (
|
|
2189
|
+
instruction.workflow_runtime == "auto"
|
|
2190
|
+
and instruction.caller_role == "worker"
|
|
2191
|
+
)
|
|
2192
|
+
if worker_caller:
|
|
2193
|
+
returned = (
|
|
2194
|
+
_return_phrase(instruction)
|
|
2195
|
+
if instruction.handoff_block is not None
|
|
2196
|
+
else "Return this response and the saved iteration results to "
|
|
2197
|
+
"the manager"
|
|
2198
|
+
)
|
|
2199
|
+
return [
|
|
2200
|
+
"This loop has reached its configured limit. Do not start another "
|
|
2201
|
+
f"iteration. {returned} for user escalation.",
|
|
2202
|
+
"",
|
|
2203
|
+
]
|
|
2204
|
+
return [
|
|
2205
|
+
"Do not start another iteration. Report the saved loop results and "
|
|
2206
|
+
"this warning to the user for manual resolution.",
|
|
2207
|
+
"",
|
|
2208
|
+
]
|
|
2209
|
+
if instruction.is_loop_control:
|
|
2210
|
+
if reader is Audience.WORKER_RETURNING:
|
|
2211
|
+
return _assignment_complete(instruction)
|
|
2212
|
+
return [_RUN_MANAGER_COMMAND, ""]
|
|
2213
|
+
if instruction.manager_input:
|
|
2214
|
+
return [
|
|
2215
|
+
"This assignment only supplies values to an automatic handler. You "
|
|
2216
|
+
"are the manager: provide them yourself with the command below. Do "
|
|
2217
|
+
"not delegate this to a worker.",
|
|
2218
|
+
"",
|
|
2219
|
+
]
|
|
2220
|
+
match reader:
|
|
2221
|
+
case Audience.SINGLE_SESSION:
|
|
2222
|
+
active_role = "worker" if instruction.next_role == "worker" else "manager"
|
|
2223
|
+
if instruction.operator_reason is not None:
|
|
2224
|
+
# The operator decides; there is no work to do in either role.
|
|
2225
|
+
return []
|
|
2226
|
+
return [
|
|
2227
|
+
f"You play both roles in this session: act as the {active_role} "
|
|
2228
|
+
"and do the work yourself, not through a subagent.",
|
|
2229
|
+
"",
|
|
2230
|
+
]
|
|
2231
|
+
case Audience.WORKER_RETURNING:
|
|
2232
|
+
return _assignment_complete(instruction)
|
|
2233
|
+
case Audience.MANAGER_DELEGATING if instruction.role == "manager":
|
|
2234
|
+
return [
|
|
2235
|
+
"You are the manager. This step is yours (`role: manager`): perform "
|
|
2236
|
+
"it yourself in this session, not through a worker, and run the "
|
|
2237
|
+
"displayed manager completion command; a worker's completion of "
|
|
2238
|
+
"it is refused.",
|
|
2239
|
+
"",
|
|
2240
|
+
*_assignment_coverage(instruction),
|
|
2241
|
+
]
|
|
2242
|
+
case Audience.MANAGER_DELEGATING:
|
|
2243
|
+
return [
|
|
2244
|
+
"You are the manager. Select the worker and give it the bootstrap "
|
|
2245
|
+
"command below:",
|
|
2246
|
+
"",
|
|
2247
|
+
*_assignment_coverage(instruction),
|
|
2248
|
+
]
|
|
2249
|
+
case Audience.WORKER:
|
|
2250
|
+
if instruction.assignment_continues and instruction.completion_registered:
|
|
2251
|
+
item = instruction.item_name
|
|
2252
|
+
return [
|
|
2253
|
+
f"Same assignment continues: next item `{item}`. "
|
|
2254
|
+
"Do not return to the manager yet. Perform it and run the "
|
|
2255
|
+
"displayed worker completion command.",
|
|
2256
|
+
"",
|
|
2257
|
+
]
|
|
2258
|
+
return [
|
|
2259
|
+
"You are the worker for this assignment. Do the work, run the "
|
|
2260
|
+
"worker completion command below, and keep following `ww` until "
|
|
2261
|
+
"it returns control to the manager. `ww` saves your result from "
|
|
2262
|
+
"`--artifact`; write nothing under `.ww` except a document this "
|
|
2263
|
+
"page names.",
|
|
2264
|
+
"",
|
|
2265
|
+
"This assignment is addressed to you, the worker. Running the "
|
|
2266
|
+
"commands this page displays is expected, even where they name "
|
|
2267
|
+
"the parent task or another task ID than the one you were "
|
|
2268
|
+
"given. Change only your own branch and worktree; leave every "
|
|
2269
|
+
"other branch and worktree as it is.",
|
|
2270
|
+
"",
|
|
2271
|
+
*_assignment_coverage(instruction),
|
|
2272
|
+
]
|
|
2273
|
+
case Audience.MANAGER if instruction.choosing_outcome_of is not None:
|
|
2274
|
+
return [_RUN_MANAGER_COMMAND, ""]
|
|
2275
|
+
case Audience.MANAGER:
|
|
2276
|
+
preview = instruction.assignment_preview
|
|
2277
|
+
if preview and "selection_item_name" not in preview:
|
|
2278
|
+
# No worker is selected for what comes next; the preview says
|
|
2279
|
+
# who performs it.
|
|
2280
|
+
return [_RUN_MANAGER_COMMAND, ""]
|
|
2281
|
+
return [_RUN_MANAGER_COMMAND, ""]
|
|
2282
|
+
|
|
2283
|
+
|
|
2284
|
+
def requested_setting_lines(model: str | None, reasoning: str | None) -> list[str]:
|
|
2285
|
+
"""Bullet lines for the model and reasoning a step requests, if any."""
|
|
2286
|
+
return [
|
|
2287
|
+
f"- {label}: `{value}`"
|
|
2288
|
+
for label, value in (
|
|
2289
|
+
("Model", requested_setting(model)),
|
|
2290
|
+
("Reasoning", requested_setting(reasoning)),
|
|
2291
|
+
)
|
|
2292
|
+
if value is not None
|
|
2293
|
+
]
|