ww-agentic-workflows 1.0.0.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ww/__init__.py +18 -0
- ww/_bundled_extensions/ww/git/extension.py +1728 -0
- ww/action_execution.py +887 -0
- ww/actions/__init__.py +94 -0
- ww/actions/command.py +444 -0
- ww/actions/contracts.py +699 -0
- ww/actions/extension.py +197 -0
- ww/actions/mcp.py +84 -0
- ww/actions/prompt.py +74 -0
- ww/actions/skill.py +62 -0
- ww/actions/slash_command.py +63 -0
- ww/agents.py +151 -0
- ww/amendments.py +54 -0
- ww/artifacts.py +93 -0
- ww/assessments.py +181 -0
- ww/assets/__init__.py +2 -0
- ww/assets/agent_instructions.md +49 -0
- ww/assets/docs/examples.md +879 -0
- ww/assets/docs/features.md +4639 -0
- ww/assets/docs/specification.md +1876 -0
- ww/assets/noww_skill.md +11 -0
- ww/assets/workflows/catchall.yaml +26 -0
- ww/assets/workflows/onboarding.yaml +586 -0
- ww/assets/workflows/scriptize.yaml +130 -0
- ww/assets/ww-automate_skill.md +23 -0
- ww/assets/ww-deduce-feedback_skill.md +38 -0
- ww/assets/ww-feedback-rules_skill.md +48 -0
- ww/assets/ww-learn-project_skill.md +22 -0
- ww/assets/ww-refresh_skill.md +26 -0
- ww/assets/ww-rule_skill.md +83 -0
- ww/assets/ww-rules-from-artifacts_skill.md +22 -0
- ww/assets/ww-scriptize_skill.md +33 -0
- ww/assets/ww-setup_skill.md +94 -0
- ww/assets/ww-solve_skill.md +23 -0
- ww/assets/ww-suggest_skill.md +32 -0
- ww/assets/ww-wizard_skill.md +105 -0
- ww/assets/ww_skill.md +59 -0
- ww/assignments.py +283 -0
- ww/bootstrap.py +405 -0
- ww/builtin_workflows.py +215 -0
- ww/changes.py +225 -0
- ww/child_coordination.py +482 -0
- ww/children.py +106 -0
- ww/claude_permissions.py +115 -0
- ww/cli/__init__.py +7 -0
- ww/cli/__main__.py +6 -0
- ww/cli/audit.py +129 -0
- ww/cli/catalogs.py +131 -0
- ww/cli/discover.py +607 -0
- ww/cli/initialization.py +898 -0
- ww/cli/lookup.py +287 -0
- ww/cli/main.py +1768 -0
- ww/cli/parser.py +1200 -0
- ww/cli/prompts.py +217 -0
- ww/cli/updates.py +117 -0
- ww/completion_artifacts.py +156 -0
- ww/completion_inputs.py +39 -0
- ww/config/__init__.py +582 -0
- ww/config/actions.py +591 -0
- ww/config/composition.py +571 -0
- ww/config/rules.py +511 -0
- ww/config/steps.py +1220 -0
- ww/config/values.py +223 -0
- ww/config_files.py +191 -0
- ww/config_writes.py +264 -0
- ww/contracts.py +155 -0
- ww/control.py +41 -0
- ww/defaults.py +130 -0
- ww/design_docs.py +32 -0
- ww/discovery.py +104 -0
- ww/documents.py +217 -0
- ww/errors.py +18 -0
- ww/executable.py +43 -0
- ww/execution_models/__init__.py +64 -0
- ww/execution_models/construction.py +148 -0
- ww/execution_models/decoding.py +38 -0
- ww/execution_models/plan_codec.py +565 -0
- ww/execution_models/records.py +1206 -0
- ww/execution_models/runs.py +266 -0
- ww/extensions/__init__.py +40 -0
- ww/extensions/api.py +559 -0
- ww/extensions/registry.py +864 -0
- ww/extensions/store.py +78 -0
- ww/feedback.py +342 -0
- ww/handler_repairs.py +57 -0
- ww/hooks/__init__.py +40 -0
- ww/hooks/agents.py +380 -0
- ww/hooks/install.py +168 -0
- ww/hooks/notices.py +206 -0
- ww/hooks/records.py +209 -0
- ww/hooks/runtime.py +266 -0
- ww/hooks/transcripts.py +183 -0
- ww/inspect.py +896 -0
- ww/instructions/__init__.py +17 -0
- ww/instructions/builder.py +1682 -0
- ww/instructions/commands.py +335 -0
- ww/instructions/handoff.py +149 -0
- ww/instructions/models.py +686 -0
- ww/instructions/policy.py +219 -0
- ww/instructions/text.py +168 -0
- ww/interactions.py +187 -0
- ww/interpolation.py +37 -0
- ww/item_passes.py +167 -0
- ww/items.py +99 -0
- ww/locking.py +207 -0
- ww/metadata_publication.py +230 -0
- ww/onboarding.py +229 -0
- ww/open_work.py +236 -0
- ww/operations.py +193 -0
- ww/operator_ui/__init__.py +16 -0
- ww/operator_ui/page.html +351 -0
- ww/operator_ui/server.py +215 -0
- ww/operator_ui/session.py +389 -0
- ww/operator_ui/sheet.py +104 -0
- ww/operator_ui/view.py +109 -0
- ww/output.py +339 -0
- ww/output_adapters/__init__.py +12 -0
- ww/output_adapters/base.py +25 -0
- ww/output_adapters/json_adapter.py +37 -0
- ww/output_adapters/markdown.py +2293 -0
- ww/output_adapters/rule_pages.py +337 -0
- ww/output_adapters/terminal.py +21 -0
- ww/package_updates.py +167 -0
- ww/plan/__init__.py +38 -0
- ww/plan/actions.py +207 -0
- ww/plan/compiler.py +1492 -0
- ww/plan/constructs.py +456 -0
- ww/plan/models.py +665 -0
- ww/project_config.py +752 -0
- ww/recovery.py +401 -0
- ww/replanning.py +367 -0
- ww/results.py +77 -0
- ww/rule_checks.py +230 -0
- ww/rule_conversion.py +331 -0
- ww/rule_disputes.py +148 -0
- ww/rule_store.py +456 -0
- ww/rule_verification.py +714 -0
- ww/rule_views.py +447 -0
- ww/rule_writes.py +920 -0
- ww/run_coordination.py +158 -0
- ww/runtimes.py +105 -0
- ww/service.py +4405 -0
- ww/setup_apply.py +428 -0
- ww/step_values.py +20 -0
- ww/storage.py +447 -0
- ww/storage_adapters/__init__.py +36 -0
- ww/storage_adapters/base.py +540 -0
- ww/storage_adapters/filesystem.py +370 -0
- ww/storage_adapters/memory.py +195 -0
- ww/storage_adapters/project_metadata.py +69 -0
- ww/storage_adapters/task_document.py +484 -0
- ww/task_ids.py +114 -0
- ww/task_references.py +124 -0
- ww/transitions.py +1619 -0
- ww/updates.py +399 -0
- ww/upgrade.py +95 -0
- ww/validation.py +168 -0
- ww/variables.py +275 -0
- ww/workflow_config.py +854 -0
- ww/workflow_update.py +239 -0
- ww/workflow_validation.py +1260 -0
- ww/workspace.py +50 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/METADATA +690 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/RECORD +167 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/WHEEL +4 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/entry_points.txt +2 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/licenses/LICENSE +674 -0
ww/artifacts.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Rendering for durable agent-produced workflow step artifacts."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
RuleStatus = Literal[
|
|
10
|
+
"passed", "failed", "not applicable", "self-declared", "verified pass"
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RuleOutcome:
|
|
16
|
+
"""One rule or check of a completed step, as its artifact reports it.
|
|
17
|
+
|
|
18
|
+
``detail`` names what decided a rule without a command of its own: the
|
|
19
|
+
derived check that ran, or the verification item that gave the verdict.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
id: str
|
|
23
|
+
status: RuleStatus
|
|
24
|
+
hook: bool = False
|
|
25
|
+
detail: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class RulesSummary:
|
|
30
|
+
"""The ``## Rules`` section of a step artifact.
|
|
31
|
+
|
|
32
|
+
``waived`` names the checks the step completed without, each with the
|
|
33
|
+
operator's reason; ``fix_attempts`` counts the completions ww rejected
|
|
34
|
+
before.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
outcomes: tuple[RuleOutcome, ...]
|
|
38
|
+
fix_attempts: int = 0
|
|
39
|
+
waived: tuple[tuple[str, str], ...] = ()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def render_step_artifact(
|
|
43
|
+
*,
|
|
44
|
+
task_id: str,
|
|
45
|
+
workflow: str,
|
|
46
|
+
step: str,
|
|
47
|
+
step_number: int,
|
|
48
|
+
step_total: int,
|
|
49
|
+
skill: str,
|
|
50
|
+
result: str,
|
|
51
|
+
rules: RulesSummary | None = None,
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Wrap an agent result in the built-in, stable Markdown artifact format."""
|
|
54
|
+
body = _normalize_result(result).rstrip()
|
|
55
|
+
return (
|
|
56
|
+
f"# {task_id} — {step}\n\n"
|
|
57
|
+
"## Workflow context\n\n"
|
|
58
|
+
f"- Workflow: {workflow}\n"
|
|
59
|
+
f"- Step: {step_number} of {step_total}\n"
|
|
60
|
+
f"- Skill: {skill}\n\n"
|
|
61
|
+
"## Result\n\n"
|
|
62
|
+
f"{body}\n" + (_rules_section(rules) if rules is not None else "")
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _rules_section(rules: RulesSummary) -> str:
|
|
67
|
+
lines = ["", "## Rules", ""]
|
|
68
|
+
lines.extend(
|
|
69
|
+
f"- `{outcome.id}`{' (hook)' if outcome.hook else ''}: {outcome.status}"
|
|
70
|
+
+ (f" ({outcome.detail})" if outcome.detail else "")
|
|
71
|
+
for outcome in rules.outcomes
|
|
72
|
+
)
|
|
73
|
+
lines.append("")
|
|
74
|
+
reasons: dict[str, list[str]] = {}
|
|
75
|
+
for check_id, reason in rules.waived:
|
|
76
|
+
reasons.setdefault(reason, []).append(check_id)
|
|
77
|
+
for reason, waived in reasons.items():
|
|
78
|
+
names = ", ".join(f"`{check_id}`" for check_id in waived)
|
|
79
|
+
lines.append(f"Checks waived by the operator ({names}): {reason}")
|
|
80
|
+
attempts = rules.fix_attempts
|
|
81
|
+
lines.append(
|
|
82
|
+
f"Completions rejected before this one: {attempts}."
|
|
83
|
+
if attempts
|
|
84
|
+
else "No completion was rejected."
|
|
85
|
+
)
|
|
86
|
+
return "\n".join(lines) + "\n"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _normalize_result(result: str) -> str:
|
|
90
|
+
"""Restore line endings when Markdown arrived as one escaped CLI argument."""
|
|
91
|
+
if "\n" in result or "\r" in result:
|
|
92
|
+
return result
|
|
93
|
+
return result.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\\r", "\n")
|
ww/assessments.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""The outcomes of an ``assess`` step, and when one is waiting to be chosen.
|
|
3
|
+
|
|
4
|
+
An assessment is completed like any agent step; its answer is then given to
|
|
5
|
+
``next --outcome``. The transition that applies the answer and the pages that
|
|
6
|
+
ask for it both read the choice from here, so they cannot disagree about which
|
|
7
|
+
answers exist or what each one does.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from ww.errors import StateError
|
|
15
|
+
from ww.execution_models import ExecutionState
|
|
16
|
+
from ww.plan import PlanItem, WorkflowPlan
|
|
17
|
+
|
|
18
|
+
# The answers every assessment with declared outcomes accepts. One it does not
|
|
19
|
+
# declare runs nothing and continues after the assessment, so a gate declares
|
|
20
|
+
# only the outcome that has work. The compact form keeps its own rule:
|
|
21
|
+
# positive continues, negative ends the workflow.
|
|
22
|
+
STANDARD_OUTCOMES = ("positive", "negative", "mixed")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class AssessmentOutcome:
|
|
27
|
+
"""One answer: the step it runs first, or that it ends the workflow."""
|
|
28
|
+
|
|
29
|
+
label: str
|
|
30
|
+
stops: bool = False
|
|
31
|
+
first_step: str | None = None
|
|
32
|
+
# A standard answer the assessment does not declare: it skips every
|
|
33
|
+
# outcome's work and continues after the assessment.
|
|
34
|
+
declared: bool = True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class PendingAssessment:
|
|
39
|
+
"""A completed assessment whose outcome ``next`` must be given."""
|
|
40
|
+
|
|
41
|
+
index: int
|
|
42
|
+
question: str
|
|
43
|
+
outcomes: tuple[AssessmentOutcome, ...]
|
|
44
|
+
# Declared outcomes, as opposed to the compact form.
|
|
45
|
+
declared: bool
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def labels(self) -> tuple[str, ...]:
|
|
49
|
+
return tuple(outcome.label for outcome in self.outcomes)
|
|
50
|
+
|
|
51
|
+
def outcome(self, label: str) -> AssessmentOutcome | None:
|
|
52
|
+
return next((item for item in self.outcomes if item.label == label), None)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def assessment_outcomes(
|
|
56
|
+
plan: WorkflowPlan, index: int
|
|
57
|
+
) -> tuple[AssessmentOutcome, ...]:
|
|
58
|
+
"""What each answer to the assessment at ``index`` does."""
|
|
59
|
+
item = plan.items[index]
|
|
60
|
+
if not item.assessment_outcomes:
|
|
61
|
+
following = plan.items[index + 1].name if index + 1 < len(plan.items) else None
|
|
62
|
+
return (
|
|
63
|
+
AssessmentOutcome("positive", first_step=following),
|
|
64
|
+
AssessmentOutcome("negative", stops=True),
|
|
65
|
+
)
|
|
66
|
+
region = outcome_region(plan, index)
|
|
67
|
+
after = max(region, default=index) + 1
|
|
68
|
+
declared = tuple(
|
|
69
|
+
AssessmentOutcome(
|
|
70
|
+
label,
|
|
71
|
+
stops=label in item.assessment_stops,
|
|
72
|
+
first_step=next(
|
|
73
|
+
(
|
|
74
|
+
plan.items[position].name
|
|
75
|
+
for position in region
|
|
76
|
+
if plan.items[position].assessment_outcome == label
|
|
77
|
+
),
|
|
78
|
+
None,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
for label in item.assessment_outcomes
|
|
82
|
+
)
|
|
83
|
+
implied = tuple(
|
|
84
|
+
AssessmentOutcome(
|
|
85
|
+
label,
|
|
86
|
+
first_step=plan.items[after].name if after < len(plan.items) else None,
|
|
87
|
+
declared=False,
|
|
88
|
+
)
|
|
89
|
+
for label in STANDARD_OUTCOMES
|
|
90
|
+
if label not in item.assessment_outcomes
|
|
91
|
+
)
|
|
92
|
+
return (*declared, *implied)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def outcome_region(plan: WorkflowPlan, index: int) -> tuple[int, ...]:
|
|
96
|
+
"""The plan positions of every declared outcome's work, in order."""
|
|
97
|
+
step = plan.items[index].step
|
|
98
|
+
return tuple(
|
|
99
|
+
position
|
|
100
|
+
for position, candidate in enumerate(plan.items)
|
|
101
|
+
if _assessment_parent(candidate) == step
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def pending_assessment(
|
|
106
|
+
state: ExecutionState, plan: WorkflowPlan
|
|
107
|
+
) -> PendingAssessment | None:
|
|
108
|
+
"""The assessment just completed whose outcome is still to be chosen.
|
|
109
|
+
|
|
110
|
+
Its outcome items, or for the compact form the step after it, are still
|
|
111
|
+
pending at the cursor until an outcome is chosen; the choice is recorded
|
|
112
|
+
on the assessment's record and the cursor moves into its work.
|
|
113
|
+
"""
|
|
114
|
+
if not 0 < state.cursor < len(plan.items):
|
|
115
|
+
return None
|
|
116
|
+
if state.item_executions[state.cursor].status != "pending":
|
|
117
|
+
return None
|
|
118
|
+
item = plan.items[state.cursor]
|
|
119
|
+
parent = _assessment_parent(item)
|
|
120
|
+
if parent is not None:
|
|
121
|
+
group = [
|
|
122
|
+
index
|
|
123
|
+
for index, candidate in enumerate(plan.items)
|
|
124
|
+
if _assessment_parent(candidate) == parent
|
|
125
|
+
]
|
|
126
|
+
if state.cursor != min(group):
|
|
127
|
+
return None
|
|
128
|
+
index = next(
|
|
129
|
+
(
|
|
130
|
+
index
|
|
131
|
+
for index, candidate in enumerate(plan.items)
|
|
132
|
+
if candidate.step == parent
|
|
133
|
+
and candidate.assessment_question is not None
|
|
134
|
+
),
|
|
135
|
+
None,
|
|
136
|
+
)
|
|
137
|
+
if index is None:
|
|
138
|
+
raise StateError(f"assessment outcome has no parent assessment: {parent}")
|
|
139
|
+
return _unanswered(state, plan, index, declared=True)
|
|
140
|
+
previous = plan.items[state.cursor - 1]
|
|
141
|
+
if previous.assessment_question is not None and not previous.assessment_outcomes:
|
|
142
|
+
return _unanswered(state, plan, state.cursor - 1, declared=False)
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _unanswered(
|
|
147
|
+
state: ExecutionState, plan: WorkflowPlan, index: int, *, declared: bool
|
|
148
|
+
) -> PendingAssessment | None:
|
|
149
|
+
"""The assessment at ``index``, unless its outcome is already chosen.
|
|
150
|
+
|
|
151
|
+
A chosen outcome is kept on the assessment's record, so a stop between
|
|
152
|
+
the answer and the next step, such as a pass gate, never asks again.
|
|
153
|
+
"""
|
|
154
|
+
if state.item_executions[index].assessment_outcome is not None:
|
|
155
|
+
return None
|
|
156
|
+
return _pending(plan, index, declared=declared)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _pending(plan: WorkflowPlan, index: int, *, declared: bool) -> PendingAssessment:
|
|
160
|
+
question = plan.items[index].assessment_question
|
|
161
|
+
assert question is not None # pragma: no cover - callers check it
|
|
162
|
+
return PendingAssessment(
|
|
163
|
+
index, question, assessment_outcomes(plan, index), declared
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _assessment_parent(item: PlanItem) -> str | None:
|
|
168
|
+
"""Resolve template paths retained by previously materialized snapshots.
|
|
169
|
+
|
|
170
|
+
Keep the persisted plan unchanged so its digest and completed work remain
|
|
171
|
+
valid. Concrete paths bind each outcome to its own item or child.
|
|
172
|
+
"""
|
|
173
|
+
parent = item.assessment_parent
|
|
174
|
+
if parent is None:
|
|
175
|
+
return None
|
|
176
|
+
parts = parent.split("/")
|
|
177
|
+
step_parts = item.step.split("/")
|
|
178
|
+
for index, part in enumerate(parts):
|
|
179
|
+
if part in {"{item}", "{child}"} and index < len(step_parts):
|
|
180
|
+
parts[index] = step_parts[index]
|
|
181
|
+
return "/".join(parts)
|
ww/assets/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Working with ww
|
|
2
|
+
|
|
3
|
+
ww saves progress, runs handlers, and names the next role; it is only for
|
|
4
|
+
development. Where ww is used by default, every file change goes through ww
|
|
5
|
+
unless the user asks you not to. Reviews, questions, investigations and other
|
|
6
|
+
read-only work never use ww, its skill or a task. When no workflow fits a change, run
|
|
7
|
+
`./ww lookup [<task>] --agent <agent>` and follow it; it never creates a task without operator confirmation.
|
|
8
|
+
|
|
9
|
+
## Start here
|
|
10
|
+
|
|
11
|
+
Before starting new work, run:
|
|
12
|
+
|
|
13
|
+
```console
|
|
14
|
+
./ww discover
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
It lists the project's workflows, modes, the start command, resume commands, and
|
|
18
|
+
whether to use ww unasked. If disabled, stop; if used only on request, use it only when
|
|
19
|
+
the user asks for ww. Choose the matching workflow and keep its default modes
|
|
20
|
+
unless the request matches another mode; ask only when the choice would
|
|
21
|
+
materially change the work.
|
|
22
|
+
|
|
23
|
+
When a request names an external ticket, use that as the task ID. Omit the ID
|
|
24
|
+
only when none is named or the workflow obtains it in its first step.
|
|
25
|
+
|
|
26
|
+
To continue an existing task, run `./ww instruction <task-id> --role manager`;
|
|
27
|
+
a worker resuming its assignment uses `--role worker`. Check status with
|
|
28
|
+
`./ww status <task-id>`.
|
|
29
|
+
|
|
30
|
+
## Rules
|
|
31
|
+
- Follow each authoritative ww page: replace placeholders and supply requested
|
|
32
|
+
values until completion, error, or assignment end. One completion rarely ends a task.
|
|
33
|
+
- Perform only agent-owned work. Never run or bypass a ww handler, edit ww state, or read `ww.yaml` or ww source to infer the next step.
|
|
34
|
+
- For handler repair, fix the cause and complete with an artifact; ww retries it. In `auto`, the manager dispatches repairs with `next`.
|
|
35
|
+
- On an `interactive: true` step, converse in the operator session and finish
|
|
36
|
+
on clear contextual intent; ask if it is ambiguous. "Done for today" means
|
|
37
|
+
pause. Record both sides and end the interaction before completing it.
|
|
38
|
+
- Use a host choice tool when available, following its contract; otherwise use
|
|
39
|
+
a numbered chat list. Keep async choices pending until an answer arrives;
|
|
40
|
+
timeout, dismissal, and preselection are not answers.
|
|
41
|
+
- On nonzero exit, read the full response. For "Fix required", repair and complete again
|
|
42
|
+
(`./ww check <task-id>` previews checks) or `./ww dispute`; record impossible work
|
|
43
|
+
with `./ww fail` and the page's role/assignment. On `awaiting_operator`, stop and
|
|
44
|
+
report the reason and exact error; run only the operator's chosen recovery option;
|
|
45
|
+
never reset unasked. A standing operator authorization for routine repairs (dependency
|
|
46
|
+
installation, formatting, retries) covers such stops: apply it, asking only for a
|
|
47
|
+
material decision or an action outside it.
|
|
48
|
+
- Relay feedback-deduction suggestions after completion; use its skill and ww
|
|
49
|
+
commands. Rule approval and pruning are separate follow-ups.
|