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,105 @@
1
+ ---
2
+ name: ww-wizard
3
+ description: Help the operator create a ww workflow, change an existing one, create or improve rules, or choose an approach, adapting the questions to the request and placing every change through ww's own commands. Use when the operator invokes /ww-wizard, or asks to design, change or tune how ww works in this project and has not said exactly what to edit.
4
+ ---
5
+
6
+ # Shape ww's setup with the operator
7
+
8
+ Talk in plain words and ask only what changes the outcome. This skill
9
+ orchestrates: ww's design documents decide what a setup can be made of, ww's
10
+ commands validate and place every change, and the rules skills write rules.
11
+ Never edit ww's configuration files yourself.
12
+
13
+ ## Start
14
+
15
+ 1. Run `./ww discover`. If ww is disabled, say so and stop. Read the
16
+ operator's request.
17
+ 2. If it already says what to do, go straight to that branch. Otherwise ask
18
+ which one, as a single choice through the host's choice tool where
19
+ available (a numbered list in the chat otherwise), and end your turn until
20
+ the operator answers:
21
+ - Create a workflow.
22
+ - Change an existing workflow.
23
+ - Create or improve rules.
24
+ - Help me choose an approach.
25
+ 3. Match the depth of your questions to the request: a precise request gets
26
+ one confirming question at most; a vague one gets the few questions it
27
+ needs, one message at a time. A timeout or a preselected value is not an
28
+ answer.
29
+
30
+ Before drafting anything, read the design authorities the branch needs, which
31
+ every installation prints: `./ww docs features` (start at "Designing a
32
+ workflow"), `./ww docs specification` for exact syntax and `./ww docs
33
+ examples` for the shape of a setup. Inspect the project: `.ww/project.md` when
34
+ it exists, the project's own commands and CI, and the workflows already
35
+ configured. Take a test, lint or type command only from that evidence and say
36
+ where it comes from; where it is missing or contradictory, ask instead of
37
+ guessing.
38
+
39
+ ## Create a workflow
40
+
41
+ Ask for the trigger, the result wanted and where the operator wants to be
42
+ involved. Choose the smallest structure that expresses it, adding items,
43
+ loops, assessments, modes or reusable groups only for a concrete requirement
44
+ the features guide gives a reason for. Challenge anything the request does not
45
+ need: name a simpler alternative and what it costs the operator, and let them
46
+ decide; do not walk through every property. Then show concise YAML and a short
47
+ walkthrough of a representative task, with a failure and retry path when an
48
+ external effect or an automatic command is involved. Write the draft as a
49
+ setup fragment (root keys `workflows`, `modes`, `documents`, `handlers`,
50
+ `hooks`, `rules`, plus `settings`) to a file outside the repository, such as in
51
+ your scratch directory.
52
+
53
+ ## Change an existing workflow
54
+
55
+ 1. Find where it is defined: `./ww discover --json` gives each workflow's
56
+ `source` file and `source_level` (`local`, `project` or `global`). State
57
+ which definition is in force and which level it is at.
58
+ 2. Draft the complete changed workflow as a fragment whose `workflows` list
59
+ holds that one entry. Keep what the operator did not ask to change.
60
+ 3. Edit it where it is written: `./ww setup update <name> <fragment>`, adding
61
+ `--level <source_level>` so that ww refuses rather than edit a definition
62
+ another level hides. If the operator wants a change for themselves only to a
63
+ shared workflow, say that this is an override that hides the shared
64
+ definition for them alone, and place it with `./ww setup apply <fragment>
65
+ --for me` only when they confirm. Never create such an override silently,
66
+ and never hide a shared edit under a local one.
67
+ 4. A workflow ww ships (`catchall`, the `ww-*` ones) is not edited: a same-named
68
+ definition placed with `setup apply` replaces it.
69
+
70
+ ## Create or improve rules
71
+
72
+ Hand the work to the rules skills instead of writing rules yourself: `ww-rule`
73
+ for rules in the operator's own words, `ww-rules-from-artifacts` for lessons
74
+ that recur in past results, `ww-feedback-rules` for learned operator feedback,
75
+ and `ww-scriptize` to give rules that have none a check. Say which one fits and
76
+ why, then follow it.
77
+
78
+ ## Help me choose an approach
79
+
80
+ Ask what goes wrong or what the operator wants to be easier, then lay out two
81
+ or three options in plain words, each with what it asks of the operator and
82
+ what it costs: a rule, a check, a mode, a small workflow, or doing nothing.
83
+ Recommend the simplest that meets the need. When they pick one, continue in its
84
+ branch.
85
+
86
+ ## Validate, show, apply
87
+
88
+ 1. Validate and inspect the draft before asking anything:
89
+ `./ww setup apply <fragment> --for <me or team> --dry-run --inspect <workflow>
90
+ --agent <your agent>` for new definitions, `./ww setup update <name>
91
+ <fragment> --dry-run --inspect <workflow> --agent <your agent>` for a
92
+ change. In the compiled plan, check that automation is ww-owned (commands
93
+ run as handlers or hooks, never as a step asking the agent to run them),
94
+ that item scopes are valid, and that the operator meets only the
95
+ conversations they intended. Fix the draft and validate again until it is
96
+ clean.
97
+ 2. Show the YAML or the diff, the walkthrough, and the dry run's list of
98
+ changes. For a new definition, ask whether it is for the operator alone
99
+ (`--for me`, local files kept out of version control) or the team
100
+ (`--for team`, shared files).
101
+ 3. When the operator approves the shown edit, apply exactly it with `--yes`;
102
+ do not ask again. If ww refuses, show its message and stop; never place the
103
+ configuration another way.
104
+ 4. Run `./ww lint`, tell the operator which files changed, and that shared
105
+ ones are left uncommitted for them to review and commit.
ww/assets/ww_skill.md ADDED
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: ww
3
+ description: Carry out a development request through ww, this project's workflow tool. Use only when the request changes something (implement, fix, refactor, merge, edit documentation) or the user explicitly asks to work via ww, to start or continue a ww task, or to run a ww workflow; where `./ww discover` says ww is used by default, use it before changing files for any request, since every change then goes through ww. Never use it for reviews, questions, explanations, investigations, status checks or other read-only work, even when they concern ww or a ww task.
4
+ ---
5
+
6
+ # Work through ww
7
+
8
+ ww is for development: requests that change files. A review, question,
9
+ explanation, investigation, status check or other read-only request never
10
+ goes through ww; answer it directly, without these steps and without a task,
11
+ even where ww is used by default. When a read-only conversation turns into a
12
+ change, start from step 1 then.
13
+
14
+ 1. Run `./ww discover` and read all of it. If it says ww is disabled, stop:
15
+ do not use ww, and tell the user. If it says ww is used only on request
16
+ and the user did not explicitly ask for ww (invoking this skill counts),
17
+ carry out the request without ww and do not ask.
18
+ 2. To continue an existing task, run
19
+ `./ww instruction <task-id> --role manager` instead of starting a new one.
20
+ 3. Otherwise choose the workflow that matches the request, keep its default
21
+ modes unless the user's wording matches another mode, and start the task
22
+ with the start command `discover` shows. Pass the user's requirements,
23
+ normalized, as `--requirements`. When the request names an external
24
+ ticket, such as a Jira key, use that key as the task ID; omit the ID only
25
+ when there is none or the workflow obtains its own.
26
+ When no workflow fits and you are about to change files, do not start
27
+ `catchall` directly: run `./ww lookup [<task>] --agent <agent>` with the
28
+ task the conversation works on or the request names, as written, and
29
+ follow its answer, then do the work as you would without ww. Read-only work
30
+ needs no task.
31
+ When the first page of `start` says rules have no check
32
+ yet, tell the user once and carry on: it never blocks the task, and the
33
+ `ww-scriptize` skill builds those checks when they want it.
34
+ 4. Follow every ww response exactly: run each displayed command with all
35
+ placeholders replaced, and keep going until ww reports that the workflow
36
+ is complete or reports an error. On `interactive: true` steps, converse
37
+ naturally until intent to finish is clear, ask if ambiguous, and treat
38
+ "done for today" as a pause. Record and end the interaction before
39
+ completing it. Use the host choice tool when available; keep asynchronous
40
+ choices pending until an answer arrives, and do not treat timeouts or
41
+ preselection as answers. After completion, relay any optional
42
+ `ww-deduce-feedback` suggestion; do not insert learning into the plan.
43
+ Deduction uses ww commands and explicit existing point IDs; rule review
44
+ and pruning are separate, and rules require operator approval.
45
+ 5. Never run a ww-owned handler yourself, edit ww state, or read
46
+ `ww.yaml` or ww's source to work out what to do next. For a handler repair
47
+ assignment, fix the cause and complete with an artifact; ww retries the
48
+ command. In `auto`, the manager dispatches the repair with `next`. When
49
+ ww reports `awaiting_operator` (a failed handler or work, an interrupted
50
+ command, a loop at its iteration limit, a changed workflow), stop and report the task, its
51
+ `operator_reason` and the exact error to the user (unless they gave a
52
+ standing authorization for routine repairs such as dependency installation,
53
+ formatting or retries: apply it to a stop of that kind without asking again,
54
+ and ask only for a material decision or an action outside it); when they
55
+ decide, run the recovery command ww showed, `./ww next <task-id> --retry` to run the
56
+ handler again, `--force --reason` to skip it or leave the loop, or
57
+ `--replan` / `--keep-plan` for a changed workflow. A `pass_incomplete`
58
+ stop is cleared by recording what it names with `./ww update-item`, then
59
+ `./ww next <task-id> --retry`; `--force` is refused there.
ww/assignments.py ADDED
@@ -0,0 +1,283 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """Structural worker-assignment boundaries for compiled workflow plans."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+
8
+ from ww.control import is_coordinator, workflow_transition
9
+ from ww.errors import StateError
10
+ from ww.plan import PlanItem, WorkflowPlan
11
+ from ww.workflow_config import ProvidedVariable
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Assignment:
16
+ """One manager-dispatched unit of agent and automatic lifecycle work."""
17
+
18
+ first_item_id: str
19
+ start: int
20
+ stop: int
21
+
22
+
23
+ def selection_item(plan: WorkflowPlan, assignment: Assignment) -> PlanItem | None:
24
+ """Return the item whose request drives a worker selection.
25
+
26
+ The main step wins; in a hook-only assignment an agent-owned hook wins
27
+ over a handler that merely waits for values, because the worker is
28
+ shaped for the work it performs, not for the value it types in.
29
+ """
30
+ items = plan.items[assignment.start : assignment.stop]
31
+ for choose in (
32
+ lambda item: item.phase == "step" and item.owner == "agent",
33
+ lambda item: item.owner == "agent",
34
+ lambda item: item.requires_agent_input,
35
+ ):
36
+ found = next((item for item in items if choose(item)), None)
37
+ if found is not None:
38
+ return found
39
+ return None
40
+
41
+
42
+ def assignment_at(
43
+ plan: WorkflowPlan, cursor: int, *, runtime: str
44
+ ) -> Assignment | None:
45
+ """Return the assignment beginning at ``cursor``, if it is worker-owned.
46
+
47
+ Preparation hooks share an assignment with the main action at the same
48
+ structural step. Completion hooks on ancestors trail the final descendant.
49
+ A new preparation/main action, item expansion, child coordinator, workflow
50
+ transition, or unrelated completion lifecycle ends the assignment. The
51
+ built-in workflow summary is deliberately included in the final assignment.
52
+
53
+ In the ``auto`` runtime, an ``items`` step with ``item_assignment`` set to
54
+ ``per_item`` or ``together`` keeps later stages of the same item, or of
55
+ every item, in the assignment, and a ``loop`` with ``loop_assignment``
56
+ ``per_round`` keeps the following body steps of the same round while
57
+ they resolve to the same worker settings. The ``single`` runtime keeps
58
+ per-step boundaries because one session already performs every assignment.
59
+
60
+ A verification item is an assignment of its own, so the worker who did a
61
+ step never verifies it.
62
+ """
63
+ if cursor >= len(plan.items):
64
+ return None
65
+ first = plan.items[cursor]
66
+ if _coordinator(first):
67
+ return None
68
+ if first.verifies is not None:
69
+ return Assignment(first.id, cursor, cursor + 1)
70
+ spans_steps = runtime != "single"
71
+ lineage = {first.step, *first.ancestors}
72
+ worker = first if first.owner == "agent" else None
73
+ stop = cursor + 1
74
+ while stop < len(plan.items):
75
+ item = plan.items[stop]
76
+ if _coordinator(item) or item.verifies is not None:
77
+ break
78
+ if item.summary:
79
+ stop += 1
80
+ continue
81
+ if item.phase in {"before_start_workflow", "before_start", "step"}:
82
+ if item.step != first.step:
83
+ if not spans_steps or not (
84
+ shares_item_span(first, item, worker=worker)
85
+ or shares_loop_span(first, item, worker=worker)
86
+ ):
87
+ break
88
+ lineage.update((item.step, *item.ancestors))
89
+ if worker is None and item.owner == "agent":
90
+ worker = item
91
+ stop += 1
92
+ continue
93
+ if item.step not in lineage:
94
+ break
95
+ stop += 1
96
+ return Assignment(first.id, cursor, stop)
97
+
98
+
99
+ def shares_item_span(
100
+ first: PlanItem, item: PlanItem, *, worker: PlanItem | None
101
+ ) -> bool:
102
+ """Whether ``item`` continues the per-item assignment begun at ``first``."""
103
+ if first.item_id is None or item.item_id is None:
104
+ return False
105
+ if first.item_pass != item.item_pass:
106
+ return False
107
+ if first.item_assignment != item.item_assignment:
108
+ return False
109
+ if item.item_assignment == "per_item" and first.item_id != item.item_id:
110
+ return False
111
+ if item.item_assignment == "per_step":
112
+ return False
113
+ return _same_worker(item, worker)
114
+
115
+
116
+ def shares_loop_span(
117
+ first: PlanItem, item: PlanItem, *, worker: PlanItem | None
118
+ ) -> bool:
119
+ """Whether ``item`` continues the loop round begun at ``first``."""
120
+ if first.loop_id is None or first.loop_id != item.loop_id:
121
+ return False
122
+ if item.loop_assignment != "per_round":
123
+ return False
124
+ return _same_worker(item, worker)
125
+
126
+
127
+ def _same_worker(item: PlanItem, worker: PlanItem | None) -> bool:
128
+ """Whether ``item`` can be performed by the assignment's current worker.
129
+
130
+ ``worker`` is the first agent-owned item of the assignment so far. A step
131
+ that needs a different agent, model, reasoning, or profile, or that is
132
+ reserved for the manager with ``role: manager``, starts a new
133
+ assignment, because one running worker cannot change any of them.
134
+ """
135
+ if item.owner != "agent":
136
+ # Automatic work inside the span drains within the assignment.
137
+ return True
138
+ if item.role == "manager" or (worker is not None and worker.role == "manager"):
139
+ return False
140
+ return worker is None or _worker_settings(worker) == _worker_settings(item)
141
+
142
+
143
+ def _worker_settings(item: PlanItem) -> tuple[str | None, ...]:
144
+ return (
145
+ item.requested_agent,
146
+ item.requested_model,
147
+ item.requested_reasoning,
148
+ item.profile,
149
+ )
150
+
151
+
152
+ @dataclass(frozen=True)
153
+ class LoopSpan:
154
+ """Several body steps of one loop round that one worker performs."""
155
+
156
+ loop_id: str
157
+ stages: tuple[PlanItem, ...]
158
+
159
+ @property
160
+ def stage_names(self) -> tuple[str, ...]:
161
+ return tuple(dict.fromkeys(stage.name for stage in self.stages))
162
+
163
+ def to_dict(self) -> dict[str, object]:
164
+ return {
165
+ "loop_assignment": "per_round",
166
+ "loop": self.loop_id,
167
+ "stages": list(self.stage_names),
168
+ }
169
+
170
+
171
+ def loop_span(plan: WorkflowPlan, assignment: Assignment) -> LoopSpan | None:
172
+ """Return the loop body steps an assignment spans, when it spans several."""
173
+ stages = tuple(
174
+ item
175
+ for item in plan.items[assignment.start : assignment.stop]
176
+ if item.phase == "step" and item.owner == "agent" and item.loop_id is not None
177
+ )
178
+ if len(stages) <= 1:
179
+ return None
180
+ return LoopSpan(str(stages[0].loop_id), stages)
181
+
182
+
183
+ @dataclass(frozen=True)
184
+ class ItemSpan:
185
+ """Several per-item stages that one worker performs in one assignment."""
186
+
187
+ item_assignment: str
188
+ stages: tuple[PlanItem, ...]
189
+
190
+ @property
191
+ def item_ids(self) -> tuple[str, ...]:
192
+ return tuple(dict.fromkeys(str(stage.item_id) for stage in self.stages))
193
+
194
+ @property
195
+ def stage_names(self) -> tuple[str, ...]:
196
+ """The stage names of one item, in order."""
197
+ first = self.stages[0].item_id
198
+ return tuple(
199
+ dict.fromkeys(stage.name for stage in self.stages if stage.item_id == first)
200
+ )
201
+
202
+ def to_dict(self) -> dict[str, object]:
203
+ return {
204
+ "item_assignment": self.item_assignment,
205
+ "item_ids": list(self.item_ids),
206
+ "stages": list(self.stage_names),
207
+ }
208
+
209
+
210
+ def item_span(plan: WorkflowPlan, assignment: Assignment) -> ItemSpan | None:
211
+ """Return the per-item stages an assignment spans, when it spans several."""
212
+ stages = tuple(
213
+ item
214
+ for item in plan.items[assignment.start : assignment.stop]
215
+ if item.phase == "step" and item.owner == "agent" and item.item_id is not None
216
+ )
217
+ if len(stages) <= 1:
218
+ return None
219
+ return ItemSpan(stages[0].item_assignment, stages)
220
+
221
+
222
+ def input_only(plan: WorkflowPlan, assignment: Assignment) -> bool:
223
+ """Whether an assignment holds no agent work, only values for handlers.
224
+
225
+ Such a span, for example a commit hook that needs its message after a
226
+ loop boundary, is not worth a worker: the manager has just read the
227
+ outcome it would summarize and supplies the values itself.
228
+ """
229
+ span = plan.items[assignment.start : assignment.stop]
230
+ # The built-in workflow summary trails the final assignment; it does not
231
+ # make a value-only span worth a worker.
232
+ return not any(item.owner == "agent" and not item.summary for item in span) and any(
233
+ item.requires_agent_input for item in span
234
+ )
235
+
236
+
237
+ def active_assignment(
238
+ plan: WorkflowPlan, first_item_id: str | None, *, runtime: str
239
+ ) -> Assignment | None:
240
+ """Rebuild a persisted assignment after reload or plan materialization."""
241
+ if first_item_id is None:
242
+ return None
243
+ for index, item in enumerate(plan.items):
244
+ if item.id == first_item_id:
245
+ return assignment_at(plan, index, runtime=runtime)
246
+ return None
247
+
248
+
249
+ def completion_window_items(
250
+ plan: WorkflowPlan, cursor: int, stop: int | None = None
251
+ ) -> tuple[PlanItem, ...]:
252
+ """Return the current item and the automatic items its completion feeds."""
253
+ items = [plan.items[cursor]]
254
+ for item in plan.items[cursor + 1 : stop]:
255
+ if item.owner == "agent" or workflow_transition(item) is not None:
256
+ break
257
+ items.append(item)
258
+ return tuple(items)
259
+
260
+
261
+ def completion_window(
262
+ plan: WorkflowPlan, cursor: int, stop: int | None = None
263
+ ) -> tuple[tuple[ProvidedVariable, ...], tuple[str, ...]]:
264
+ """Return inputs required to complete the current assignment boundary."""
265
+ items = completion_window_items(plan, cursor, stop)
266
+ required: dict[str, ProvidedVariable] = {}
267
+ sources: dict[str, str] = {}
268
+ for item in items:
269
+ for value in item.provide:
270
+ source = f"{item.name} ({item.source}, {item.id})"
271
+ if value.name in required and required[value.name] != value:
272
+ raise StateError(
273
+ f"completion window has conflicting provided variable "
274
+ f"{value.name!r}: {sources[value.name]} and {source}"
275
+ )
276
+ required.setdefault(value.name, value)
277
+ sources.setdefault(value.name, source)
278
+ context = [item.name for item in items[1:]]
279
+ return tuple(required.values()), tuple(context)
280
+
281
+
282
+ def _coordinator(item: PlanItem) -> bool:
283
+ return is_coordinator(item)