workgraph 0.3.3__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.
- workgraph/__init__.py +1 -0
- workgraph/claude.py +106 -0
- workgraph/cli.py +379 -0
- workgraph/codex.py +232 -0
- workgraph/definitions/agents/wg_code-review.md +14 -0
- workgraph/definitions/agents/wg_design.md +57 -0
- workgraph/definitions/agents/wg_implement.md +39 -0
- workgraph/definitions/agents/wg_overengineering-review.md +13 -0
- workgraph/definitions/agents/wg_plan.md +79 -0
- workgraph/definitions/agents/wg_pr.md +33 -0
- workgraph/definitions/agents/wg_summarize-review.md +10 -0
- workgraph/definitions/workflows/wg.toml +101 -0
- workgraph/definitions/workflows/wg_codex.toml +106 -0
- workgraph/graph.py +360 -0
- workgraph/harness.py +112 -0
- workgraph/run.py +942 -0
- workgraph/show.py +676 -0
- workgraph/workflow.py +263 -0
- workgraph-0.3.3.dist-info/METADATA +139 -0
- workgraph-0.3.3.dist-info/RECORD +23 -0
- workgraph-0.3.3.dist-info/WHEEL +4 -0
- workgraph-0.3.3.dist-info/entry_points.txt +3 -0
- workgraph-0.3.3.dist-info/licenses/LICENSE +21 -0
workgraph/workflow.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Workflow discovery, loading, validation, and mermaid rendering."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import re
|
|
5
|
+
import tomllib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, NoReturn
|
|
8
|
+
|
|
9
|
+
from workgraph.harness import HARNESS_NAMES
|
|
10
|
+
|
|
11
|
+
END = "END"
|
|
12
|
+
LIMIT = "LIMIT"
|
|
13
|
+
RESERVED_NAMES = frozenset({END, LIMIT})
|
|
14
|
+
AGENT_SETTINGS = ("harness", "model", "effort")
|
|
15
|
+
HARNESS_SETTINGS = {"allowed_tools": "claude", "sandbox": "codex", "web_search": "codex"}
|
|
16
|
+
NODE_KINDS = ("agent", "command", "map", "gate")
|
|
17
|
+
TIME_KEYS = ("time_soft", "time_hard")
|
|
18
|
+
BUDGET_KEYS = (*TIME_KEYS, "cost")
|
|
19
|
+
DURATION_PATTERN = re.compile(r"(\d+(?:\.\d+)?)([smh]?)")
|
|
20
|
+
DURATION_UNITS = {"": 1, "s": 1, "m": 60, "h": 3600}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WorkflowError(Exception):
|
|
24
|
+
"""A workflow failed to load or validate."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_workflow(workflow_name: str) -> dict[str, Any]:
|
|
28
|
+
"""Find the workflow by name, parse it, and validate every load-time rule."""
|
|
29
|
+
path = _find_workflow_file(workflow_name)
|
|
30
|
+
with path.open("rb") as file:
|
|
31
|
+
try:
|
|
32
|
+
workflow = tomllib.load(file)
|
|
33
|
+
except tomllib.TOMLDecodeError as error:
|
|
34
|
+
raise WorkflowError(f"{workflow_name}: invalid TOML: {error}") from error
|
|
35
|
+
_validate_workflow(workflow_name, workflow)
|
|
36
|
+
if "budget" in workflow:
|
|
37
|
+
workflow["budget"] = _validate_budget(workflow_name, workflow["budget"])
|
|
38
|
+
return workflow
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_duration(duration: object) -> float:
|
|
42
|
+
"""Return the seconds a duration denotes: a positive number, or a string with unit s, m, or h.
|
|
43
|
+
|
|
44
|
+
parse_duration raises ValueError for any other input.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(duration, int | float) and not isinstance(duration, bool):
|
|
47
|
+
seconds = float(duration)
|
|
48
|
+
elif isinstance(duration, str) and (match := DURATION_PATTERN.fullmatch(duration)):
|
|
49
|
+
seconds = float(match[1]) * DURATION_UNITS[match[2]]
|
|
50
|
+
else:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"invalid duration {duration!r}: expected seconds or a number with unit s, m, or h"
|
|
53
|
+
)
|
|
54
|
+
if seconds <= 0:
|
|
55
|
+
raise ValueError(f"invalid duration {duration!r}: must be positive")
|
|
56
|
+
return seconds
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_cost(cost: object) -> float:
|
|
60
|
+
"""Return the USD a cost denotes: a positive number, or its string form.
|
|
61
|
+
|
|
62
|
+
parse_cost raises ValueError for any other input.
|
|
63
|
+
"""
|
|
64
|
+
message = f"invalid cost {cost!r}: expected a positive USD number"
|
|
65
|
+
if isinstance(cost, bool) or not isinstance(cost, int | float | str):
|
|
66
|
+
raise ValueError(message)
|
|
67
|
+
try:
|
|
68
|
+
usd = float(cost)
|
|
69
|
+
except ValueError:
|
|
70
|
+
raise ValueError(message) from None
|
|
71
|
+
if not usd > 0:
|
|
72
|
+
raise ValueError(message)
|
|
73
|
+
return usd
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_agent_settings(
|
|
77
|
+
node_definition: dict[str, Any], defaults: dict[str, Any]
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
"""Return the agent settings of a node: its own value, else the default."""
|
|
80
|
+
settings = {key: node_definition.get(key, defaults.get(key)) for key in AGENT_SETTINGS}
|
|
81
|
+
settings.update(
|
|
82
|
+
(key, node_definition.get(key, defaults.get(key)))
|
|
83
|
+
for key, owner in HARNESS_SETTINGS.items()
|
|
84
|
+
if owner == settings["harness"] and (key in node_definition or key in defaults)
|
|
85
|
+
)
|
|
86
|
+
return settings
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def render_mermaid(workflow: dict[str, Any]) -> str:
|
|
90
|
+
"""Render a validated workflow as a bare mermaid flowchart.
|
|
91
|
+
|
|
92
|
+
The start node is drawn as a stadium and a gate node as a hexagon so
|
|
93
|
+
every viz style (unicode, ascii, mermaid) marks them.
|
|
94
|
+
"""
|
|
95
|
+
start_node = workflow["start"]
|
|
96
|
+
lines = ["flowchart TD", f" {start_node}([{start_node}])"]
|
|
97
|
+
for node_name, node_definition in workflow["nodes"].items():
|
|
98
|
+
if "gate" in node_definition:
|
|
99
|
+
lines.append(f" {node_name}{{{{{node_name}}}}}")
|
|
100
|
+
for fanned_out_node in node_definition.get("map", []):
|
|
101
|
+
lines.append(f" {node_name} --> {fanned_out_node}")
|
|
102
|
+
for outcome, target in node_definition.get("transitions", {}).items():
|
|
103
|
+
lines.append(f" {node_name} -->|{outcome}| {target}")
|
|
104
|
+
return "\n".join(lines)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def list_definition_directories() -> tuple[Path, Path, Path]:
|
|
108
|
+
"""Return where definitions resolve from, in order: invocation directory, home, package."""
|
|
109
|
+
return (
|
|
110
|
+
Path.cwd() / ".workgraph",
|
|
111
|
+
Path.home() / ".workgraph",
|
|
112
|
+
Path(__file__).parent / "definitions",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _find_workflow_file(workflow_name: str) -> Path:
|
|
117
|
+
for definition_directory in list_definition_directories():
|
|
118
|
+
path = definition_directory / "workflows" / f"{workflow_name}.toml"
|
|
119
|
+
if path.is_file():
|
|
120
|
+
return path
|
|
121
|
+
raise WorkflowError(
|
|
122
|
+
f"workflow '{workflow_name}' not found in a .workgraph/workflows directory of the"
|
|
123
|
+
" invocation directory or the home directory, nor among the bundled workflows"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _validate_workflow(workflow_name: str, workflow: dict[str, Any]) -> None:
|
|
128
|
+
nodes = workflow.get("nodes", {})
|
|
129
|
+
start_node = workflow.get("start")
|
|
130
|
+
if start_node is None:
|
|
131
|
+
raise WorkflowError(f"{workflow_name}: missing top-level 'start'")
|
|
132
|
+
if start_node not in nodes:
|
|
133
|
+
raise WorkflowError(f"{workflow_name}: start node '{start_node}' does not exist")
|
|
134
|
+
fanned_out_by = _collect_fanned_out(workflow_name, nodes)
|
|
135
|
+
if start_node in fanned_out_by:
|
|
136
|
+
raise WorkflowError(
|
|
137
|
+
f"{workflow_name}: start node '{start_node}' is fanned out by map node '{fanned_out_by[start_node]}'"
|
|
138
|
+
)
|
|
139
|
+
defaults = workflow.get("defaults", {})
|
|
140
|
+
for node_name, node_definition in nodes.items():
|
|
141
|
+
_validate_node(workflow_name, nodes, defaults, fanned_out_by, node_name, node_definition)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _validate_budget(workflow_name: str, budget: dict[str, Any]) -> dict[str, float]:
|
|
145
|
+
"""Check the budget keys and return the time limits in seconds and the cost limit in USD."""
|
|
146
|
+
if not isinstance(budget, dict):
|
|
147
|
+
raise WorkflowError(f"{workflow_name}: [budget] must be a table")
|
|
148
|
+
limits: dict[str, float] = {}
|
|
149
|
+
for key, value in budget.items():
|
|
150
|
+
if key not in BUDGET_KEYS:
|
|
151
|
+
raise WorkflowError(f"{workflow_name}: [budget]: unknown key '{key}'")
|
|
152
|
+
try:
|
|
153
|
+
limits[key] = parse_cost(value) if key == "cost" else parse_duration(value)
|
|
154
|
+
except ValueError as error:
|
|
155
|
+
raise WorkflowError(f"{workflow_name}: [budget]: {key}: {error}") from error
|
|
156
|
+
if limits.get("time_hard", math.inf) < limits.get("time_soft", 0):
|
|
157
|
+
raise WorkflowError(f"{workflow_name}: [budget]: time_hard is below time_soft")
|
|
158
|
+
return limits
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _collect_fanned_out(workflow_name: str, nodes: dict[str, Any]) -> dict[str, str]:
|
|
162
|
+
"""Map each fanned-out node to its map node, checking the fan-out lists."""
|
|
163
|
+
fanned_out_by: dict[str, str] = {}
|
|
164
|
+
for node_name, node_definition in nodes.items():
|
|
165
|
+
for fanned_out_node in node_definition.get("map", []):
|
|
166
|
+
if fanned_out_node not in nodes:
|
|
167
|
+
raise WorkflowError(
|
|
168
|
+
f"{workflow_name}: node '{node_name}': fanned-out node '{fanned_out_node}' does not exist"
|
|
169
|
+
)
|
|
170
|
+
for kind in ("map", "gate"):
|
|
171
|
+
if kind in nodes[fanned_out_node]:
|
|
172
|
+
raise WorkflowError(
|
|
173
|
+
f"{workflow_name}: node '{node_name}': fanned-out node '{fanned_out_node}'"
|
|
174
|
+
f" is a {kind} node"
|
|
175
|
+
)
|
|
176
|
+
if fanned_out_node in fanned_out_by:
|
|
177
|
+
raise WorkflowError(
|
|
178
|
+
f"{workflow_name}: node '{node_name}': fanned-out node '{fanned_out_node}'"
|
|
179
|
+
f" is already fanned out by map node '{fanned_out_by[fanned_out_node]}'"
|
|
180
|
+
)
|
|
181
|
+
fanned_out_by[fanned_out_node] = node_name
|
|
182
|
+
return fanned_out_by
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _validate_node(
|
|
186
|
+
workflow_name: str,
|
|
187
|
+
nodes: dict[str, Any],
|
|
188
|
+
defaults: dict[str, Any],
|
|
189
|
+
fanned_out_by: dict[str, str],
|
|
190
|
+
node_name: str,
|
|
191
|
+
node_definition: dict[str, Any],
|
|
192
|
+
) -> None:
|
|
193
|
+
def fail(rule: str) -> NoReturn:
|
|
194
|
+
raise WorkflowError(f"{workflow_name}: node '{node_name}': {rule}")
|
|
195
|
+
|
|
196
|
+
if node_name in RESERVED_NAMES:
|
|
197
|
+
fail(f"'{node_name}' is reserved and cannot name a node")
|
|
198
|
+
if node_name.endswith("#"):
|
|
199
|
+
fail("a node name cannot end with '#'")
|
|
200
|
+
if sum(kind in node_definition for kind in NODE_KINDS) != 1:
|
|
201
|
+
fail("declare exactly one of 'agent', 'command', 'map', or 'gate'")
|
|
202
|
+
if "agent" not in node_definition:
|
|
203
|
+
kind = next(kind for kind in NODE_KINDS if kind in node_definition)
|
|
204
|
+
if "outcomes" in node_definition:
|
|
205
|
+
fail(f"a {kind} node cannot declare 'outcomes'")
|
|
206
|
+
for setting in (*AGENT_SETTINGS, *HARNESS_SETTINGS):
|
|
207
|
+
if setting in node_definition:
|
|
208
|
+
fail(f"a {kind} node cannot declare '{setting}'")
|
|
209
|
+
if kind == "map":
|
|
210
|
+
if not node_definition["map"]:
|
|
211
|
+
fail("'map' must list at least one node")
|
|
212
|
+
if node_definition.get("resolve") not in ("any", "all"):
|
|
213
|
+
fail("'resolve' must be 'any' or 'all'")
|
|
214
|
+
if kind == "gate":
|
|
215
|
+
if not isinstance(node_definition["gate"], str) or not node_definition["gate"]:
|
|
216
|
+
fail("'gate' must be a non-empty question")
|
|
217
|
+
if "limits" in node_definition:
|
|
218
|
+
fail("a gate node cannot declare 'limits'")
|
|
219
|
+
outcomes = ["accept", "reject"] if kind == "gate" else ["pass", "fail"]
|
|
220
|
+
else:
|
|
221
|
+
outcomes = node_definition.get("outcomes", [])
|
|
222
|
+
if not outcomes:
|
|
223
|
+
fail("an agent node must declare a non-empty 'outcomes' list")
|
|
224
|
+
for outcome in outcomes:
|
|
225
|
+
if outcome in RESERVED_NAMES:
|
|
226
|
+
fail(f"'{outcome}' is reserved and cannot name an outcome")
|
|
227
|
+
settings = resolve_agent_settings(node_definition, defaults)
|
|
228
|
+
for setting in AGENT_SETTINGS:
|
|
229
|
+
if settings[setting] is None:
|
|
230
|
+
fail(f"'{setting}' is set neither on the node nor in [defaults]")
|
|
231
|
+
harness = settings["harness"]
|
|
232
|
+
if harness not in HARNESS_NAMES:
|
|
233
|
+
accepted = ", ".join(repr(name) for name in HARNESS_NAMES)
|
|
234
|
+
fail(f"harness '{harness}' is not supported; accepted: {accepted}")
|
|
235
|
+
for setting, owner in HARNESS_SETTINGS.items():
|
|
236
|
+
if setting in node_definition and harness != owner:
|
|
237
|
+
fail(f"'{setting}' belongs to harness '{owner}', but the node uses '{harness}'")
|
|
238
|
+
if node_name in fanned_out_by:
|
|
239
|
+
for field in ("transitions", "limits"):
|
|
240
|
+
if field in node_definition:
|
|
241
|
+
fail(f"a fanned-out node cannot declare '{field}'")
|
|
242
|
+
if "pass" not in outcomes:
|
|
243
|
+
fail("a fanned-out node must have 'pass' in its outcomes")
|
|
244
|
+
return
|
|
245
|
+
limits = node_definition.get("limits", {})
|
|
246
|
+
if "reset" in limits:
|
|
247
|
+
if "visits" not in limits:
|
|
248
|
+
fail("'reset' requires 'visits' in the same limits table")
|
|
249
|
+
if limits["reset"] not in outcomes:
|
|
250
|
+
fail(f"reset outcome '{limits['reset']}' is not an outcome of the node")
|
|
251
|
+
transitions = node_definition.get("transitions", {})
|
|
252
|
+
for outcome in outcomes:
|
|
253
|
+
if outcome not in transitions:
|
|
254
|
+
fail(f"missing a transition for outcome '{outcome}'")
|
|
255
|
+
for transition_key, target in transitions.items():
|
|
256
|
+
if transition_key != LIMIT and transition_key not in outcomes:
|
|
257
|
+
fail(f"transition key '{transition_key}' is not an outcome of the node")
|
|
258
|
+
if target != END and target not in nodes:
|
|
259
|
+
fail(f"transition target '{target}' does not exist")
|
|
260
|
+
if target in fanned_out_by:
|
|
261
|
+
fail(
|
|
262
|
+
f"transition target '{target}' is fanned out by map node '{fanned_out_by[target]}'"
|
|
263
|
+
)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: workgraph
|
|
3
|
+
Version: 0.3.3
|
|
4
|
+
Summary: Graph workflow orchestrator.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: termaid[rich]>=0.8.0
|
|
8
|
+
Requires-Dist: tomlkit>=0.15.1
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Project-URL: Changelog, https://github.com/sylmarien/workgraph/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Repository, https://github.com/sylmarien/workgraph
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# workgraph
|
|
15
|
+
|
|
16
|
+
workgraph orchestrates development workflows declared as graphs. Nodes run
|
|
17
|
+
agents or commands; a node's outcome selects the transition to follow. The
|
|
18
|
+
developer writes the workflow once and starts a run from a harness session
|
|
19
|
+
with `/workgraph`. The run prints one progress line per node. One command
|
|
20
|
+
resumes a stopped run.
|
|
21
|
+
|
|
22
|
+
The CLI bundles workflow and agent definitions; [Workflow
|
|
23
|
+
files](docs/workflow-files.md) lists them. This repository runs the bundled
|
|
24
|
+
`wg` workflow on itself:
|
|
25
|
+
|
|
26
|
+
```mermaid
|
|
27
|
+
flowchart TD
|
|
28
|
+
design([design])
|
|
29
|
+
design -->|done| approve-design
|
|
30
|
+
approve-design -->|accept| plan
|
|
31
|
+
approve-design -->|reject| design
|
|
32
|
+
plan -->|done| approve-plan
|
|
33
|
+
approve-plan -->|accept| implement
|
|
34
|
+
approve-plan -->|reject| plan
|
|
35
|
+
implement -->|done| test
|
|
36
|
+
test -->|pass| review
|
|
37
|
+
test -->|fail| implement
|
|
38
|
+
review --> code-review
|
|
39
|
+
review --> overengineering-review
|
|
40
|
+
review -->|pass| pr
|
|
41
|
+
review -->|fail| review-loop
|
|
42
|
+
review-loop -->|pass| implement
|
|
43
|
+
review-loop -->|fail| implement
|
|
44
|
+
review-loop -->|LIMIT| summary
|
|
45
|
+
summary -->|done| pr
|
|
46
|
+
pr -->|done| END
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
As a Claude Code plugin:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
/plugin marketplace add sylmarien/workgraph
|
|
55
|
+
/plugin install workgraph@workgraph
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Installing the plugin adds the `/workgraph` skill. The plugin's `install`
|
|
59
|
+
skill installs the CLI with `uv`, and its `update` skill upgrades it. Both
|
|
60
|
+
check that `uv` is on `PATH` and install nothing else.
|
|
61
|
+
|
|
62
|
+
The CLI bundles workflow and agent definitions. A user's own definitions
|
|
63
|
+
shadow them; see [Workflow files](docs/workflow-files.md) for the bundled
|
|
64
|
+
definitions and the resolution order.
|
|
65
|
+
|
|
66
|
+
As a Codex plugin:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
codex plugin marketplace add sylmarien/workgraph
|
|
70
|
+
codex plugin add workgraph@workgraph
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Start a new Codex session and invoke `$workgraph <workflow> "#<issue>"`.
|
|
74
|
+
The plugin shares the install, update, and run skills with the Claude Code
|
|
75
|
+
plugin. The bundled workflows run nodes on the Claude harness, so they
|
|
76
|
+
require `claude` and `uv` on `PATH` in Codex too.
|
|
77
|
+
|
|
78
|
+
Codex reads the repository's existing marketplace at
|
|
79
|
+
`.claude-plugin/marketplace.json` and its manifest at
|
|
80
|
+
`.codex-plugin/plugin.json`. Both plugins ship in the same Git release.
|
|
81
|
+
The patch, minor, and major release workflows update both manifests and
|
|
82
|
+
the CLI to the same version. See the
|
|
83
|
+
[Codex packaging documentation](https://developers.openai.com/plugins/build/plugins).
|
|
84
|
+
|
|
85
|
+
Without the plugin:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
uv tool install workgraph
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`pip install workgraph` installs the same package from PyPI.
|
|
92
|
+
`uv tool upgrade workgraph` upgrades it. Requires Python 3.12+. An agent
|
|
93
|
+
node additionally requires the CLI of its harness on `PATH`: `claude` for
|
|
94
|
+
`harness = "claude"`, `codex` for `harness = "codex"`.
|
|
95
|
+
|
|
96
|
+
## Example
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
workgraph run wg "#12"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
design: done
|
|
104
|
+
approve-design: parked
|
|
105
|
+
parked at approve-design: Plan from this design? · spent 1m20s · $0.15
|
|
106
|
+
Review material from design:
|
|
107
|
+
https://github.com/sylmarien/workgraph/issues/12#issuecomment-5550441682
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
workgraph resume --decision accept
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
approve-design: accept
|
|
116
|
+
plan: done
|
|
117
|
+
approve-plan: parked
|
|
118
|
+
parked at approve-plan: Implement this plan? · spent 4m05s · $0.42
|
|
119
|
+
Review material from plan:
|
|
120
|
+
<the plan>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`workgraph resume --decision accept` delivers the decision and resumes the
|
|
124
|
+
run.
|
|
125
|
+
|
|
126
|
+
## Reference
|
|
127
|
+
|
|
128
|
+
- [Commands](docs/commands.md)
|
|
129
|
+
- [Workflow files](docs/workflow-files.md)
|
|
130
|
+
- [Agent definitions](docs/agent-definitions.md)
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
uv sync
|
|
136
|
+
uv run ruff check && uv run ruff format --check && uv run mypy && uv run pytest
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The dogfood workflow runs the same gate: `workgraph run wg "#<issue>"`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
workgraph/__init__.py,sha256=-98aA3TsgzNO0jLI6qXojt-dAKhg0jJJxHAgMgoaqCo,35
|
|
2
|
+
workgraph/claude.py,sha256=I6Beld-t8AfNl1K1QY9WA9RHzlM5Gi4uEWb3lkBoenY,3841
|
|
3
|
+
workgraph/cli.py,sha256=bRXDLmZMelm-RCb-XUuzMMmIharVyOFTabBv9j68PYo,12872
|
|
4
|
+
workgraph/codex.py,sha256=6AG2rQtY31oH4iivbxTnrRwA4882wYEPQozs9wazgbI,9589
|
|
5
|
+
workgraph/definitions/agents/wg_code-review.md,sha256=q5mX_DIkOr_KfmnJH6BXYAm-1ME76RAHCSRUiabMJus,576
|
|
6
|
+
workgraph/definitions/agents/wg_design.md,sha256=mbNULWzJFtkhJ1JLCMui0YePyFsBFnygpdUYOnbd0cU,2960
|
|
7
|
+
workgraph/definitions/agents/wg_implement.md,sha256=ZhhKkmrta9hweXRp1BadAlrx4nk9QG6fHvbvmAediH0,1576
|
|
8
|
+
workgraph/definitions/agents/wg_overengineering-review.md,sha256=tBhBTygGQvHW5eA592GGmp586WAyHFsjZ1dcNU6RzVc,508
|
|
9
|
+
workgraph/definitions/agents/wg_plan.md,sha256=GZq-2isG-en2fZ1eKTVDVE5zJiOIBYQvs3NT3XmqBWo,3798
|
|
10
|
+
workgraph/definitions/agents/wg_pr.md,sha256=oNGkLbdTehraFUEV_htbKdpCJA2q7GUMuUAll6OB7M4,1664
|
|
11
|
+
workgraph/definitions/agents/wg_summarize-review.md,sha256=17Qe-3fBEyjTavaCtGDNACrYxQn3Cushu2_2dPDeFRg,451
|
|
12
|
+
workgraph/definitions/workflows/wg.toml,sha256=kzSY_6qoetRHxpV1NwkE1U6k-HoDycVJlxoSBz3XL48,1751
|
|
13
|
+
workgraph/definitions/workflows/wg_codex.toml,sha256=O3UVY0_G_UNz5IpFZu_dOhqDRIcrgoJHapUzMSLwGgc,1873
|
|
14
|
+
workgraph/graph.py,sha256=sm6oZAHhNyCQ77Xb3FnjIaUEYpH16mReTjqHnNK1Ya0,14332
|
|
15
|
+
workgraph/harness.py,sha256=xa2_lYXWkWpzcTwrJdBzpRBmzFUtIDY09pVhnIu9JQQ,3993
|
|
16
|
+
workgraph/run.py,sha256=tD_8RKbKYmkl0vxaX69rN9D8ZU4Z9b286F5du5G3zkk,36316
|
|
17
|
+
workgraph/show.py,sha256=PJP7CnjUjSSFySztZQej7XMGOtlo6fGlXXkIsZpYC7w,29516
|
|
18
|
+
workgraph/workflow.py,sha256=1kvBqkjwN8_O-byaf68IQ35gRYHTVVUxs-uI_AJXz6A,11575
|
|
19
|
+
workgraph-0.3.3.dist-info/licenses/LICENSE,sha256=Giuyc4C51f_S6HrS6dHoqObZR76kzgYkqYbmNe7_wGg,1071
|
|
20
|
+
workgraph-0.3.3.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
21
|
+
workgraph-0.3.3.dist-info/entry_points.txt,sha256=Tjt1r5Q4yUb4AkbmXj8A-wNQAT6WcxtcXEiFu9lofc4,50
|
|
22
|
+
workgraph-0.3.3.dist-info/METADATA,sha256=rmzHmAzAfG50TXjG1i0EQ_850qp-NDIVZwhtV4W5uqI,4030
|
|
23
|
+
workgraph-0.3.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maxime Schmitt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|