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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. ww/__init__.py +18 -0
  2. ww/_bundled_extensions/ww/git/extension.py +1728 -0
  3. ww/action_execution.py +887 -0
  4. ww/actions/__init__.py +94 -0
  5. ww/actions/command.py +444 -0
  6. ww/actions/contracts.py +699 -0
  7. ww/actions/extension.py +197 -0
  8. ww/actions/mcp.py +84 -0
  9. ww/actions/prompt.py +74 -0
  10. ww/actions/skill.py +62 -0
  11. ww/actions/slash_command.py +63 -0
  12. ww/agents.py +151 -0
  13. ww/amendments.py +54 -0
  14. ww/artifacts.py +93 -0
  15. ww/assessments.py +181 -0
  16. ww/assets/__init__.py +2 -0
  17. ww/assets/agent_instructions.md +49 -0
  18. ww/assets/docs/examples.md +879 -0
  19. ww/assets/docs/features.md +4639 -0
  20. ww/assets/docs/specification.md +1876 -0
  21. ww/assets/noww_skill.md +11 -0
  22. ww/assets/workflows/catchall.yaml +26 -0
  23. ww/assets/workflows/onboarding.yaml +586 -0
  24. ww/assets/workflows/scriptize.yaml +130 -0
  25. ww/assets/ww-automate_skill.md +23 -0
  26. ww/assets/ww-deduce-feedback_skill.md +38 -0
  27. ww/assets/ww-feedback-rules_skill.md +48 -0
  28. ww/assets/ww-learn-project_skill.md +22 -0
  29. ww/assets/ww-refresh_skill.md +26 -0
  30. ww/assets/ww-rule_skill.md +83 -0
  31. ww/assets/ww-rules-from-artifacts_skill.md +22 -0
  32. ww/assets/ww-scriptize_skill.md +33 -0
  33. ww/assets/ww-setup_skill.md +94 -0
  34. ww/assets/ww-solve_skill.md +23 -0
  35. ww/assets/ww-suggest_skill.md +32 -0
  36. ww/assets/ww-wizard_skill.md +105 -0
  37. ww/assets/ww_skill.md +59 -0
  38. ww/assignments.py +283 -0
  39. ww/bootstrap.py +405 -0
  40. ww/builtin_workflows.py +215 -0
  41. ww/changes.py +225 -0
  42. ww/child_coordination.py +482 -0
  43. ww/children.py +106 -0
  44. ww/claude_permissions.py +115 -0
  45. ww/cli/__init__.py +7 -0
  46. ww/cli/__main__.py +6 -0
  47. ww/cli/audit.py +129 -0
  48. ww/cli/catalogs.py +131 -0
  49. ww/cli/discover.py +607 -0
  50. ww/cli/initialization.py +898 -0
  51. ww/cli/lookup.py +287 -0
  52. ww/cli/main.py +1768 -0
  53. ww/cli/parser.py +1200 -0
  54. ww/cli/prompts.py +217 -0
  55. ww/cli/updates.py +117 -0
  56. ww/completion_artifacts.py +156 -0
  57. ww/completion_inputs.py +39 -0
  58. ww/config/__init__.py +582 -0
  59. ww/config/actions.py +591 -0
  60. ww/config/composition.py +571 -0
  61. ww/config/rules.py +511 -0
  62. ww/config/steps.py +1220 -0
  63. ww/config/values.py +223 -0
  64. ww/config_files.py +191 -0
  65. ww/config_writes.py +264 -0
  66. ww/contracts.py +155 -0
  67. ww/control.py +41 -0
  68. ww/defaults.py +130 -0
  69. ww/design_docs.py +32 -0
  70. ww/discovery.py +104 -0
  71. ww/documents.py +217 -0
  72. ww/errors.py +18 -0
  73. ww/executable.py +43 -0
  74. ww/execution_models/__init__.py +64 -0
  75. ww/execution_models/construction.py +148 -0
  76. ww/execution_models/decoding.py +38 -0
  77. ww/execution_models/plan_codec.py +565 -0
  78. ww/execution_models/records.py +1206 -0
  79. ww/execution_models/runs.py +266 -0
  80. ww/extensions/__init__.py +40 -0
  81. ww/extensions/api.py +559 -0
  82. ww/extensions/registry.py +864 -0
  83. ww/extensions/store.py +78 -0
  84. ww/feedback.py +342 -0
  85. ww/handler_repairs.py +57 -0
  86. ww/hooks/__init__.py +40 -0
  87. ww/hooks/agents.py +380 -0
  88. ww/hooks/install.py +168 -0
  89. ww/hooks/notices.py +206 -0
  90. ww/hooks/records.py +209 -0
  91. ww/hooks/runtime.py +266 -0
  92. ww/hooks/transcripts.py +183 -0
  93. ww/inspect.py +896 -0
  94. ww/instructions/__init__.py +17 -0
  95. ww/instructions/builder.py +1682 -0
  96. ww/instructions/commands.py +335 -0
  97. ww/instructions/handoff.py +149 -0
  98. ww/instructions/models.py +686 -0
  99. ww/instructions/policy.py +219 -0
  100. ww/instructions/text.py +168 -0
  101. ww/interactions.py +187 -0
  102. ww/interpolation.py +37 -0
  103. ww/item_passes.py +167 -0
  104. ww/items.py +99 -0
  105. ww/locking.py +207 -0
  106. ww/metadata_publication.py +230 -0
  107. ww/onboarding.py +229 -0
  108. ww/open_work.py +236 -0
  109. ww/operations.py +193 -0
  110. ww/operator_ui/__init__.py +16 -0
  111. ww/operator_ui/page.html +351 -0
  112. ww/operator_ui/server.py +215 -0
  113. ww/operator_ui/session.py +389 -0
  114. ww/operator_ui/sheet.py +104 -0
  115. ww/operator_ui/view.py +109 -0
  116. ww/output.py +339 -0
  117. ww/output_adapters/__init__.py +12 -0
  118. ww/output_adapters/base.py +25 -0
  119. ww/output_adapters/json_adapter.py +37 -0
  120. ww/output_adapters/markdown.py +2293 -0
  121. ww/output_adapters/rule_pages.py +337 -0
  122. ww/output_adapters/terminal.py +21 -0
  123. ww/package_updates.py +167 -0
  124. ww/plan/__init__.py +38 -0
  125. ww/plan/actions.py +207 -0
  126. ww/plan/compiler.py +1492 -0
  127. ww/plan/constructs.py +456 -0
  128. ww/plan/models.py +665 -0
  129. ww/project_config.py +752 -0
  130. ww/recovery.py +401 -0
  131. ww/replanning.py +367 -0
  132. ww/results.py +77 -0
  133. ww/rule_checks.py +230 -0
  134. ww/rule_conversion.py +331 -0
  135. ww/rule_disputes.py +148 -0
  136. ww/rule_store.py +456 -0
  137. ww/rule_verification.py +714 -0
  138. ww/rule_views.py +447 -0
  139. ww/rule_writes.py +920 -0
  140. ww/run_coordination.py +158 -0
  141. ww/runtimes.py +105 -0
  142. ww/service.py +4405 -0
  143. ww/setup_apply.py +428 -0
  144. ww/step_values.py +20 -0
  145. ww/storage.py +447 -0
  146. ww/storage_adapters/__init__.py +36 -0
  147. ww/storage_adapters/base.py +540 -0
  148. ww/storage_adapters/filesystem.py +370 -0
  149. ww/storage_adapters/memory.py +195 -0
  150. ww/storage_adapters/project_metadata.py +69 -0
  151. ww/storage_adapters/task_document.py +484 -0
  152. ww/task_ids.py +114 -0
  153. ww/task_references.py +124 -0
  154. ww/transitions.py +1619 -0
  155. ww/updates.py +399 -0
  156. ww/upgrade.py +95 -0
  157. ww/validation.py +168 -0
  158. ww/variables.py +275 -0
  159. ww/workflow_config.py +854 -0
  160. ww/workflow_update.py +239 -0
  161. ww/workflow_validation.py +1260 -0
  162. ww/workspace.py +50 -0
  163. ww_agentic_workflows-1.0.0.dev3.dist-info/METADATA +690 -0
  164. ww_agentic_workflows-1.0.0.dev3.dist-info/RECORD +167 -0
  165. ww_agentic_workflows-1.0.0.dev3.dist-info/WHEEL +4 -0
  166. ww_agentic_workflows-1.0.0.dev3.dist-info/entry_points.txt +2 -0
  167. ww_agentic_workflows-1.0.0.dev3.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,335 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """The ``./ww`` command lines that instructions ask agents to run.
3
+
4
+ Every rendered command is built here, so a renamed flag changes in one place.
5
+ Real values are shell-quoted; ``<placeholder>`` tokens that a person or agent
6
+ must replace are left as written.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ import shlex
13
+
14
+ from ww.contracts import CallerRole
15
+ from ww.executable import ww_command
16
+ from ww.workflow_config import ProvidedVariable, SavedMetadata
17
+
18
+ from .models import InteractCommands, RecoveryCommand
19
+
20
+ TASK_PLACEHOLDER = "<task-id>"
21
+ SUMMARY_FLAG = "--summary"
22
+
23
+
24
+ # A bare placeholder such as ``<run-id>``, or one shown already quoted such as
25
+ # ``"<reason>"`` so the agent keeps the quotes when it fills the value in;
26
+ # "<Run-ID>" does not match.
27
+ _PLACEHOLDER = re.compile(r'"?<[a-z][a-z0-9-]*>"?')
28
+
29
+
30
+ def _arg(value: str) -> str:
31
+ """Quote a real value; keep a human placeholder such as ``<run-id>`` as is."""
32
+ if _PLACEHOLDER.fullmatch(value):
33
+ return value
34
+ return shlex.quote(value)
35
+
36
+
37
+ def _command(*parts: str) -> str:
38
+ return " ".join((ww_command(), *parts))
39
+
40
+
41
+ def _worker(role: CallerRole, assignment: str | None) -> tuple[str, ...]:
42
+ """``--role`` and, for a worker of an open assignment, its token."""
43
+ if role == "worker" and assignment:
44
+ return ("--role", role, "--assignment", _arg(assignment))
45
+ return ("--role", role)
46
+
47
+
48
+ def replan_command(task_id: str, *, keep: bool = False, rerun: bool = False) -> str:
49
+ """The operator's choice at a ``plan_changed`` stop.
50
+
51
+ Rerunning finished items is confirmed like a retry: ``--yes`` records
52
+ that the operator agreed, since the agent's shell has no terminal.
53
+ """
54
+ if keep:
55
+ return _command("next", _arg(task_id), "--keep-plan", "--role", "manager")
56
+ flags = ("--replan", "--yes") if rerun else ("--replan",)
57
+ return _command("next", _arg(task_id), *flags, "--role", "manager")
58
+
59
+
60
+ def next_command(
61
+ task_id: str,
62
+ *,
63
+ retry: bool = False,
64
+ force_reason: str | None = None,
65
+ selected_agent: str | None = None,
66
+ model: str | None = None,
67
+ reasoning: str | None = None,
68
+ outcome: str | None = None,
69
+ ) -> str:
70
+ """The manager command that advances a task, with optional recovery flags.
71
+
72
+ A retry or force is the operator's choice, which the agent carries out
73
+ only once they made it; ``--yes`` records that, since the agent's shell
74
+ has no terminal to confirm at.
75
+ """
76
+ parts = ["next", _arg(task_id)]
77
+ if retry:
78
+ parts.append("--retry")
79
+ if force_reason is not None:
80
+ parts.extend(("--force", "--reason", _arg(force_reason)))
81
+ if retry or force_reason is not None:
82
+ parts.append("--yes")
83
+ parts.extend(("--role", "manager"))
84
+ for flag, value in (
85
+ ("--selected-agent", selected_agent),
86
+ ("--model", model),
87
+ ("--reasoning", reasoning),
88
+ ("--outcome", outcome),
89
+ ):
90
+ if value is not None:
91
+ parts.extend((flag, _arg(value)))
92
+ return _command(*parts)
93
+
94
+
95
+ def instruction_command(
96
+ task_id: str,
97
+ run_id: str | None = None,
98
+ *,
99
+ role: CallerRole,
100
+ assignment: str | None = None,
101
+ ) -> str:
102
+ parts = ["instruction", _arg(task_id)]
103
+ if run_id is not None:
104
+ parts.extend(("--run", _arg(run_id)))
105
+ parts.extend(_worker(role, assignment))
106
+ return _command(*parts)
107
+
108
+
109
+ def start_command(task_id: str | None, workflow: str, agent: str) -> str:
110
+ """Start ``workflow``; without ``task_id`` ww assigns one."""
111
+ return _command(
112
+ "start",
113
+ *((_arg(task_id),) if task_id is not None else ()),
114
+ "--workflow",
115
+ _arg(workflow),
116
+ "--agent",
117
+ _arg(agent),
118
+ "--requirements",
119
+ '"<the request, normalized>"',
120
+ "--role",
121
+ "manager",
122
+ )
123
+
124
+
125
+ def lookup_command(reference: str = "<task>", agent: str = "<agent>") -> str:
126
+ return _command("lookup", _arg(reference), "--agent", _arg(agent))
127
+
128
+
129
+ def artifacts_command(task_id: str, run_id: str | None = None) -> str:
130
+ parts = ["artifacts", _arg(task_id)]
131
+ if run_id is not None:
132
+ parts.extend(("--run", _arg(run_id)))
133
+ return _command(*parts)
134
+
135
+
136
+ def check_command(task_id: str) -> str:
137
+ """Run the active step's checks now, without completing it."""
138
+ return _command("check", _arg(task_id))
139
+
140
+
141
+ def rule_command(task_id: str, rule_id: str = "<id>") -> str:
142
+ """Show one rule of the task in full."""
143
+ return _command("rule", _arg(task_id), _arg(rule_id))
144
+
145
+
146
+ def dispute_command(
147
+ task_id: str, check_id: str = "<id>", *, assignment: str | None = None
148
+ ) -> str:
149
+ """Ask the operator to overrule a check that rejected the completion."""
150
+ return _command(
151
+ "dispute",
152
+ _arg(task_id),
153
+ *_worker("worker", assignment),
154
+ "--rule",
155
+ _arg(check_id),
156
+ "--reason",
157
+ '"<why the check is wrong here>"',
158
+ )
159
+
160
+
161
+ def requirements_command(task_id: str) -> str:
162
+ """Print the task's requirements and their amendments."""
163
+ return _command("requirements", _arg(task_id))
164
+
165
+
166
+ def start_child_command(task_id: str, child_id: str) -> str:
167
+ return _command("start-child", _arg(task_id), _arg(child_id))
168
+
169
+
170
+ def update_child_command(task_id: str, child_id: str) -> str:
171
+ return _command(
172
+ "update-child", _arg(task_id), _arg(child_id), '--text="<child task text>"'
173
+ )
174
+
175
+
176
+ def add_item_command(
177
+ task_id: str = TASK_PLACEHOLDER, identity: str | None = None
178
+ ) -> str:
179
+ parts = ["add-item", _arg(task_id), "--id", "<id>", "--text", "<text>"]
180
+ if identity:
181
+ parts.extend(("--field", f"{identity}=<value>"))
182
+ return _command(*parts)
183
+
184
+
185
+ def set_item_fields_command(task_id: str, item_id: str | None = None) -> str:
186
+ """Set custom fields on an item; several in one call."""
187
+ return _command(
188
+ "update-item",
189
+ _arg(task_id),
190
+ "--id",
191
+ _arg(item_id) if item_id else "<id>",
192
+ "--field",
193
+ "<name>=<value>",
194
+ "[--field <name>=<value> ...]",
195
+ )
196
+
197
+
198
+ def remove_item_command(task_id: str = TASK_PLACEHOLDER) -> str:
199
+ return _command("remove-item", _arg(task_id), "--id", "<id>")
200
+
201
+
202
+ def reword_item_command(task_id: str = TASK_PLACEHOLDER) -> str:
203
+ return _command("update-item", _arg(task_id), "--id", "<id>", "--text", "<text>")
204
+
205
+
206
+ def add_child_command(
207
+ task_id: str = TASK_PLACEHOLDER, *, project: bool = False, identity: bool = False
208
+ ) -> str:
209
+ """The command that records one child; ``identity`` children take no ID."""
210
+ return _command(
211
+ "add-child",
212
+ _arg(task_id),
213
+ *(() if identity else ("--id", "<child-id>")),
214
+ '--text="<child task text>"',
215
+ *(("--project", "<project>") if project else ()),
216
+ )
217
+
218
+
219
+ def items_command(task_id: str = TASK_PLACEHOLDER) -> str:
220
+ return _command("items", _arg(task_id))
221
+
222
+
223
+ def item_command(task_id: str, item_id: str) -> str:
224
+ return _command("item", _arg(task_id), "--id", _arg(item_id))
225
+
226
+
227
+ _ITEM_UPDATE_FLAGS = {
228
+ "process_item": ('--processed-item="<agent-friendly analysis>"',),
229
+ "resolve_item": ('--actual-solution="<actual solution>"', "--resolved=true"),
230
+ "report_item": ("--reported=true",),
231
+ "handle_item": (
232
+ '--processed-item="<agent-friendly analysis>"',
233
+ '--actual-solution="<actual solution>"',
234
+ "--resolved=true",
235
+ "--reported=true",
236
+ ),
237
+ }
238
+
239
+
240
+ def update_item_command(task_id: str, item_id: str, operation: str | None) -> str:
241
+ """The update that saves progress for one item operation."""
242
+ return _command(
243
+ "update-item",
244
+ _arg(task_id),
245
+ "--id",
246
+ _arg(item_id),
247
+ *_ITEM_UPDATE_FLAGS.get(operation or "", ()),
248
+ )
249
+
250
+
251
+ def complete_command(
252
+ task_id: str,
253
+ values: tuple[ProvidedVariable, ...],
254
+ artifact: bool = True,
255
+ metadata: tuple[SavedMetadata, ...] = (),
256
+ *,
257
+ selected_agent: str | None = None,
258
+ selected_model: str | None = None,
259
+ selected_reasoning: str | None = None,
260
+ loop_control: str | None = None,
261
+ role: CallerRole = "worker",
262
+ summary: bool = False,
263
+ rule_results: tuple[str, ...] = (),
264
+ assignment: str | None = None,
265
+ ) -> str:
266
+ """The command that submits a step or breaks/continues its loop.
267
+
268
+ ``role`` is the manager only when the pending input belongs to an
269
+ input-only assignment that the manager supplies itself. A verification
270
+ item reports one ``--rule-result`` per rule ID in ``rule_results``.
271
+ """
272
+ parts = ["loop" if loop_control else "complete", _arg(task_id)]
273
+ if loop_control:
274
+ parts.append(f"--{loop_control}")
275
+ parts.extend(_worker(role, assignment))
276
+ parts.extend(f'--variable {value.name}="<{value.name}>"' for value in values)
277
+ parts.extend(f'--metadata {value.name}="<{value.name}>"' for value in metadata)
278
+ for flag, value in (
279
+ ("--selected-agent", selected_agent),
280
+ ("--selected-model", selected_model),
281
+ ("--selected-reasoning", selected_reasoning),
282
+ ):
283
+ if value:
284
+ parts.extend((flag, shlex.quote(value)))
285
+ if artifact:
286
+ parts.append('--artifact="<whole result in Markdown>"')
287
+ if summary:
288
+ parts.append(f'{SUMMARY_FLAG}="<one or two sentences for the next step>"')
289
+ parts.extend(f"--rule-result='<JSON result for {rule}>'" for rule in rule_results)
290
+ return _command(*parts)
291
+
292
+
293
+ def interact_commands(
294
+ task_id: str,
295
+ role: CallerRole,
296
+ assignment: str | None = None,
297
+ *,
298
+ choices: bool = False,
299
+ ) -> InteractCommands:
300
+ """The commands of an interactive step, for the role that performs it."""
301
+
302
+ def interact(*parts: str) -> str:
303
+ return _command("interact", _arg(task_id), *_worker(role, assignment), *parts)
304
+
305
+ pick = ('--choice="<label or number>"',) if choices else ()
306
+ transcript = "\n".join(
307
+ [
308
+ interact("--transcript", "-", *pick, "--end") + " <<'EOF'",
309
+ "Agent: <what you said, verbatim>",
310
+ "Operator: <what the operator said, verbatim>",
311
+ "EOF",
312
+ ]
313
+ )
314
+ return InteractCommands(
315
+ transcript=transcript,
316
+ operator=interact('--operator-said="<what the operator said>"'),
317
+ agent=interact('--agent-said="<what the agent said>"'),
318
+ choice=interact('--choice="<label or number>"'),
319
+ end=interact("--end"),
320
+ wait=interact("--await"),
321
+ resume=instruction_command(task_id, role=role, assignment=assignment),
322
+ )
323
+
324
+
325
+ def force_command(task_id: str) -> RecoveryCommand:
326
+ """The operator's force: skip a failed item, or leave a loop at its limit."""
327
+ return RecoveryCommand("force", next_command(task_id, force_reason='"<reason>"'))
328
+
329
+
330
+ def recovery_commands(task_id: str) -> tuple[RecoveryCommand, ...]:
331
+ """The two operator choices for an interrupted automatic item."""
332
+ return (
333
+ RecoveryCommand("retry", next_command(task_id, retry=True)),
334
+ force_command(task_id),
335
+ )
@@ -0,0 +1,149 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """The "Handoff to manager" block, built by ww when an assignment ends.
3
+
4
+ The facts of a delegated worker's assignment are all in the saved state, so ww
5
+ reports them to the manager itself rather than leaving it to the worker's text:
6
+ the items performed and how each ended, the artifacts, the files changed,
7
+ the checks run and waived, and the fix rounds. The worker's judgment reaches
8
+ the manager only through its short ``--summary``, and the
9
+ worker returns the block verbatim.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ from ww.execution_models import PlanItemExecution
17
+ from ww.plan import PlanItem
18
+
19
+ from .commands import next_command
20
+ from .models import HandoffBlock, HandoffStep
21
+
22
+ HANDOFF_TITLE = "Handoff to manager"
23
+
24
+
25
+ def handoff_block(
26
+ task_id: str,
27
+ token: str,
28
+ performed: tuple[tuple[PlanItem, PlanItemExecution], ...],
29
+ *,
30
+ root: Path,
31
+ files: tuple[str, ...] | None,
32
+ error: str | None,
33
+ loop_outcome: tuple[str, str] | None = None,
34
+ ) -> HandoffBlock:
35
+ """Report the agent items of one ended assignment, in plan order.
36
+
37
+ ``performed`` pairs each agent item of the assignment with its latest
38
+ record; items the worker never reached are left out. ``error`` is the
39
+ run's failure, such as a handler that failed after the last completion.
40
+ ``loop_outcome`` names the item that broke or continued its loop and
41
+ which it did.
42
+ """
43
+ steps = tuple(
44
+ step
45
+ for item, record in performed
46
+ if (step := _step(item, record, root, loop_outcome)) is not None
47
+ )
48
+ summary = next(
49
+ (
50
+ record.summary_for_next
51
+ for _, record in reversed(performed)
52
+ if record.summary_for_next
53
+ ),
54
+ None,
55
+ )
56
+ if error is not None and any(step.error == error for step in steps):
57
+ # A failed step already carries it.
58
+ error = None
59
+ return HandoffBlock(
60
+ task_id=task_id,
61
+ token=token,
62
+ steps=steps,
63
+ files=files,
64
+ summary=summary,
65
+ error=error,
66
+ )
67
+
68
+
69
+ def _step(
70
+ item: PlanItem,
71
+ record: PlanItemExecution,
72
+ root: Path,
73
+ loop_outcome: tuple[str, str] | None,
74
+ ) -> HandoffStep | None:
75
+ if record.status == "completed" and not record.attempts:
76
+ # Completed without being performed: skipped by a loop break.
77
+ return None
78
+ if record.status == "completed":
79
+ outcome = "completed"
80
+ if loop_outcome is not None and loop_outcome[0] == item.id:
81
+ outcome = f"loop {loop_outcome[1]}"
82
+ elif record.status == "failed":
83
+ outcome = "failed"
84
+ elif record.held_completion is not None:
85
+ outcome = "held for verification"
86
+ elif record.attempts:
87
+ outcome = "not completed"
88
+ else:
89
+ return None
90
+ last = record.check_reports[-1] if record.check_reports else None
91
+ return HandoffStep(
92
+ name=item.name,
93
+ outcome=outcome,
94
+ artifact=str((root / record.repair_artifacts[-1]).resolve())
95
+ if record.repair_artifacts
96
+ else str((root / record.artifact).resolve())
97
+ if record.artifact
98
+ else None,
99
+ checks=(
100
+ tuple((result.id, result.status) for result in last.results)
101
+ if last is not None
102
+ else ()
103
+ ),
104
+ checks_waived=tuple(check_id for check_id, _ in record.checks_waived),
105
+ fix_rounds=record.repair_failures
106
+ or sum(1 for report in record.check_reports if report.failed),
107
+ error=record.error if record.status == "failed" else None,
108
+ )
109
+
110
+
111
+ def handoff_markdown(block: HandoffBlock) -> str:
112
+ """The block's text, as the worker returns it and the manager reads it."""
113
+ lines = [f"{HANDOFF_TITLE} · assignment {block.token}", ""]
114
+ lines.append("Steps:")
115
+ if not block.steps:
116
+ lines.append("- none completed")
117
+ for step in block.steps:
118
+ lines.append(f"- {step.name}: {step.outcome}")
119
+ if step.artifact:
120
+ lines.append(f" artifact: {step.artifact}")
121
+ if step.checks:
122
+ lines.append(
123
+ " checks: "
124
+ + ", ".join(f"{check} {status}" for check, status in step.checks)
125
+ )
126
+ if step.checks_waived:
127
+ lines.append(" waived: " + ", ".join(step.checks_waived))
128
+ if step.fix_rounds:
129
+ lines.append(f" fix rounds: {step.fix_rounds}")
130
+ if step.error:
131
+ lines.append(f" error: {_one_line(step.error)}")
132
+ lines.append("")
133
+ if block.files is None:
134
+ lines.append("Files changed: not tracked (no step here has rules or checks)")
135
+ elif block.files:
136
+ lines.extend(["Files changed:", *(f"- {path}" for path in block.files)])
137
+ else:
138
+ lines.append("Files changed: none")
139
+ if block.error:
140
+ lines.extend(["", f"Error: {_one_line(block.error)}"])
141
+ if block.summary:
142
+ lines.extend(["", f"Worker summary: {_one_line(block.summary)}"])
143
+ manager = next_command(block.task_id)
144
+ lines.extend(["", f"Manager: continue with `{manager}`"])
145
+ return "\n".join(lines)
146
+
147
+
148
+ def _one_line(text: str) -> str:
149
+ return " ".join(text.split())