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,183 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """Recover a conversation with the operator from an agent's own transcript.
3
+
4
+ When a session ends while an interactive step is still talking, the
5
+ ``interrupt`` hook reads the session's transcript file and keeps only what
6
+ the two sides said to each other: the operator's typed messages and the
7
+ agent's text replies. Tool calls and their results, reasoning, injected
8
+ context and subagent lines are left out.
9
+
10
+ The transcript formats are the agents' internal ones, not a published
11
+ contract, so every reader is best effort: a line it does not understand is
12
+ skipped, and an unreadable file recovers nothing. Only the end of the file
13
+ is read, since the conversation of the current step is at its end.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from collections.abc import Callable, Iterator
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ # The conversation of the step in progress is recent; reading only the end
25
+ # of a long session's file keeps the hook within its few seconds.
26
+ READ_LIMIT = 2 * 1024 * 1024
27
+ # Text that starts like this was put into the conversation by the agent's
28
+ # tooling, not typed by the operator: slash-command echoes, injected
29
+ # context, and the agent's own note that a turn was interrupted.
30
+ _INJECTED = (
31
+ "<command-",
32
+ "<local-command-",
33
+ "<system-reminder>",
34
+ "<environment_context>",
35
+ "<user_instructions>",
36
+ "[Request interrupted",
37
+ )
38
+
39
+ Entry = tuple[str, str]
40
+
41
+
42
+ def recover_conversation(agent: str, path: Path, since: datetime) -> tuple[Entry, ...]:
43
+ """The ``(speaker, text)`` entries newer than ``since``, in order.
44
+
45
+ Speakers are ``operator`` and ``agent``; consecutive messages of one
46
+ side form one entry. An agent whose format ww does not read, or a file
47
+ it cannot read, recovers nothing.
48
+ """
49
+ reader = _READERS.get(agent)
50
+ if reader is None:
51
+ return ()
52
+ try:
53
+ records = list(_records(path))
54
+ except OSError:
55
+ return ()
56
+ return _merged(reader(records, since))
57
+
58
+
59
+ def _records(path: Path) -> Iterator[dict[str, Any]]:
60
+ """The JSON objects of the file's last ``READ_LIMIT`` bytes, one per line."""
61
+ with path.open("rb") as handle:
62
+ size = handle.seek(0, 2)
63
+ handle.seek(max(0, size - READ_LIMIT))
64
+ if size > READ_LIMIT:
65
+ handle.readline() # the first line read is cut short
66
+ for raw in handle:
67
+ try:
68
+ value = json.loads(raw.decode("utf-8", errors="replace"))
69
+ except json.JSONDecodeError:
70
+ continue
71
+ if isinstance(value, dict):
72
+ yield value
73
+
74
+
75
+ def _claude_code(records: list[dict[str, Any]], since: datetime) -> Iterator[Entry]:
76
+ """Claude Code's session JSONL.
77
+
78
+ A kept line looks like ``{"type": "user", "timestamp":
79
+ "2026-10-01T09:00:00.000Z", "message": {"role": "user", "content":
80
+ "Use a queue."}}``; an assistant line holds a list of blocks, of which
81
+ only ``{"type": "text", "text": ...}`` is kept, never ``tool_use`` or
82
+ ``thinking``. Subagent (``isSidechain``) and injected (``isMeta``)
83
+ lines are skipped, and so is a user line carrying a ``tool_result``.
84
+ """
85
+ for record in records:
86
+ kind = record.get("type")
87
+ if kind not in ("user", "assistant"):
88
+ continue
89
+ if record.get("isSidechain") is True or record.get("isMeta") is True:
90
+ continue
91
+ if not _newer(record.get("timestamp"), since):
92
+ continue
93
+ message = record.get("message")
94
+ if not isinstance(message, dict):
95
+ continue
96
+ text = _text_of(message.get("content"), ("text",))
97
+ if text:
98
+ yield ("operator" if kind == "user" else "agent", text)
99
+
100
+
101
+ def _codex(records: list[dict[str, Any]], since: datetime) -> Iterator[Entry]:
102
+ """A Codex rollout JSONL.
103
+
104
+ The conversation is read from event lines such as ``{"timestamp":
105
+ "2026-10-01T09:00:00.000Z", "type": "event_msg", "payload": {"type":
106
+ "user_message", "message": "Use a queue."}}`` and ``agent_message``.
107
+ A rollout without them falls back to ``response_item`` lines of payload
108
+ type ``message``, whose ``role`` is ``user`` or ``assistant`` and whose
109
+ content blocks are ``input_text`` or ``output_text``.
110
+ """
111
+ recent = [record for record in records if _newer(record.get("timestamp"), since)]
112
+ payloads: list[tuple[object, dict[str, Any]]] = [
113
+ (record.get("type"), payload)
114
+ for record in recent
115
+ if isinstance(payload := record.get("payload"), dict)
116
+ ]
117
+ speakers = {"user_message": "operator", "agent_message": "agent"}
118
+ if any(kind == "event_msg" for kind, _ in payloads):
119
+ for kind, payload in payloads:
120
+ speaker = speakers.get(str(payload.get("type")), "")
121
+ message = payload.get("message")
122
+ if kind == "event_msg" and speaker and isinstance(message, str):
123
+ text = _kept(message)
124
+ if text:
125
+ yield (speaker, text)
126
+ return
127
+ roles = {"user": "operator", "assistant": "agent"}
128
+ for kind, payload in payloads:
129
+ speaker = roles.get(str(payload.get("role")), "")
130
+ if kind != "response_item" or payload.get("type") != "message" or not speaker:
131
+ continue
132
+ text = _text_of(payload.get("content"), ("input_text", "output_text", "text"))
133
+ if text:
134
+ yield (speaker, text)
135
+
136
+
137
+ def _text_of(content: object, kinds: tuple[str, ...]) -> str:
138
+ """The kept text of a message: a plain string, or its text blocks joined."""
139
+ if isinstance(content, str):
140
+ return _kept(content)
141
+ if not isinstance(content, list):
142
+ return ""
143
+ parts = (
144
+ _kept(block["text"])
145
+ for block in content
146
+ if isinstance(block, dict)
147
+ and block.get("type") in kinds
148
+ and isinstance(block.get("text"), str)
149
+ )
150
+ return "\n\n".join(part for part in parts if part)
151
+
152
+
153
+ def _kept(text: str) -> str:
154
+ """``text`` stripped, or empty when the agent's tooling put it there."""
155
+ text = text.strip()
156
+ return "" if text.startswith(_INJECTED) else text
157
+
158
+
159
+ def _newer(value: object, since: datetime) -> bool:
160
+ if not isinstance(value, str):
161
+ return False
162
+ try:
163
+ moment = datetime.fromisoformat(value.replace("Z", "+00:00"))
164
+ except ValueError:
165
+ return False
166
+ return moment.tzinfo is not None and moment > since
167
+
168
+
169
+ def _merged(entries: Iterator[Entry]) -> tuple[Entry, ...]:
170
+ """Consecutive messages of one side as one entry, as a reader sees a turn."""
171
+ result: list[Entry] = []
172
+ for speaker, text in entries:
173
+ if result and result[-1][0] == speaker:
174
+ result[-1] = (speaker, result[-1][1] + "\n\n" + text)
175
+ else:
176
+ result.append((speaker, text))
177
+ return tuple(result)
178
+
179
+
180
+ _READERS: dict[str, Callable[[list[dict[str, Any]], datetime], Iterator[Entry]]] = {
181
+ "claudecode": _claude_code,
182
+ "codex": _codex,
183
+ }