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/codex.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""The Codex harness: argv, result reading, and transcript rendering."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
from collections.abc import Iterator, Sequence
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import tomlkit
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from workgraph.harness import (
|
|
14
|
+
AgentInvocation,
|
|
15
|
+
NodeFailure,
|
|
16
|
+
iter_jsonl_events,
|
|
17
|
+
read_last_value,
|
|
18
|
+
split_lines,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# USD per million tokens: uncached input, cached input, cache write, output.
|
|
22
|
+
# A Pro model offers no cached input discount, so its cached rate is its input rate.
|
|
23
|
+
# Source: https://developers.openai.com/api/docs/pricing, read 2026-09-05.
|
|
24
|
+
MODEL_RATES: dict[str, tuple[float, float, float, float]] = {
|
|
25
|
+
"gpt-6-astra": (10.00, 1.00, 12.50, 50.00),
|
|
26
|
+
"gpt-5.6-sol": (4.00, 0.40, 5.00, 20.00),
|
|
27
|
+
"gpt-5.6": (4.00, 0.40, 5.00, 20.00), # the documented alias of gpt-5.6-sol
|
|
28
|
+
"gpt-daybreak-blue-latest": (4.00, 0.40, 5.00, 20.00), # the Daybreak alias of gpt-5.6-sol
|
|
29
|
+
"gpt-5.6-cyber": (12.50, 1.25, 15.625, 75.00),
|
|
30
|
+
"gpt-daybreak-red-latest": (12.50, 1.25, 15.625, 75.00), # the Daybreak alias of gpt-5.6-cyber
|
|
31
|
+
"gpt-5.6-terra": (2.00, 0.20, 2.50, 12.00),
|
|
32
|
+
"gpt-5.6-luna": (0.20, 0.02, 0.25, 1.20),
|
|
33
|
+
"gpt-5.5": (5.00, 0.50, 0.0, 30.00),
|
|
34
|
+
"gpt-5.5-pro": (30.00, 30.00, 0.0, 180.00),
|
|
35
|
+
"gpt-5.4": (2.50, 0.25, 0.0, 15.00),
|
|
36
|
+
"gpt-5.4-mini": (0.75, 0.075, 0.0, 4.50),
|
|
37
|
+
"gpt-5.4-nano": (0.20, 0.02, 0.0, 1.25),
|
|
38
|
+
"gpt-5.4-pro": (30.00, 30.00, 0.0, 180.00),
|
|
39
|
+
"gpt-5.3-codex": (1.75, 0.175, 0.0, 14.00),
|
|
40
|
+
"gpt-5.2": (1.75, 0.175, 0.0, 14.00),
|
|
41
|
+
"gpt-5.1": (1.25, 0.125, 0.0, 10.00),
|
|
42
|
+
"gpt-5": (1.25, 0.125, 0.0, 10.00),
|
|
43
|
+
"gpt-5-mini": (0.25, 0.025, 0.0, 2.00),
|
|
44
|
+
"gpt-5-nano": (0.05, 0.005, 0.0, 0.40),
|
|
45
|
+
"gpt-5-pro": (15.00, 15.00, 0.0, 120.00),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build_strict_schema(outcome_schema: dict[str, Any]) -> dict[str, Any]:
|
|
50
|
+
"""Adapt the outcome schema to OpenAI strict mode, which Codex structured output requires.
|
|
51
|
+
|
|
52
|
+
Strict mode forbids extra properties and requires every property, so an absent handoff
|
|
53
|
+
comes back as null.
|
|
54
|
+
"""
|
|
55
|
+
properties = dict(outcome_schema["properties"])
|
|
56
|
+
properties["handoff"] = {**properties["handoff"], "type": ["string", "null"]}
|
|
57
|
+
return {
|
|
58
|
+
**outcome_schema,
|
|
59
|
+
"properties": properties,
|
|
60
|
+
"required": list(properties),
|
|
61
|
+
"additionalProperties": False,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _format_toml_string(value: str) -> str:
|
|
66
|
+
"""Format a string as the TOML basic string a `-c` override takes.
|
|
67
|
+
|
|
68
|
+
A JSON string with raw non-ASCII characters is a TOML basic string, and ensure_ascii=False
|
|
69
|
+
avoids the surrogate-pair escapes TOML rejects.
|
|
70
|
+
"""
|
|
71
|
+
return json.dumps(value, ensure_ascii=False)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@contextmanager
|
|
75
|
+
def build_argv(invocation: AgentInvocation) -> Iterator[list[str]]:
|
|
76
|
+
"""Yield the argv that runs the agent through the Codex CLI.
|
|
77
|
+
|
|
78
|
+
The outcome schema lives in a temporary file for the duration of the with block.
|
|
79
|
+
"""
|
|
80
|
+
session = invocation.session
|
|
81
|
+
with tempfile.NamedTemporaryFile("w", suffix=".json") as schema_file:
|
|
82
|
+
schema_file.write(json.dumps(_build_strict_schema(invocation.outcome_schema)))
|
|
83
|
+
schema_file.flush()
|
|
84
|
+
# A resumed session already holds the developer instructions, and `codex exec resume`
|
|
85
|
+
# has no --sandbox flag, so the sandbox goes as a configuration override.
|
|
86
|
+
session_dependent_argv = (
|
|
87
|
+
[
|
|
88
|
+
"--sandbox",
|
|
89
|
+
invocation.sandbox,
|
|
90
|
+
"-c",
|
|
91
|
+
f"developer_instructions={_format_toml_string(invocation.agent_definition['prompt'])}",
|
|
92
|
+
]
|
|
93
|
+
if session is None
|
|
94
|
+
else ["-c", f"sandbox_mode={_format_toml_string(invocation.sandbox)}"]
|
|
95
|
+
)
|
|
96
|
+
argv = [
|
|
97
|
+
"codex",
|
|
98
|
+
"exec",
|
|
99
|
+
*([] if session is None else ["resume", session]),
|
|
100
|
+
"--json",
|
|
101
|
+
# The target directory of a run is any directory; codex exec refuses a non-git one.
|
|
102
|
+
"--skip-git-repo-check",
|
|
103
|
+
"--model",
|
|
104
|
+
invocation.model,
|
|
105
|
+
"-c",
|
|
106
|
+
f"model_reasoning_effort={_format_toml_string(invocation.effort)}",
|
|
107
|
+
*session_dependent_argv,
|
|
108
|
+
"--output-schema",
|
|
109
|
+
schema_file.name,
|
|
110
|
+
]
|
|
111
|
+
if invocation.web_search is not None:
|
|
112
|
+
web_search = tomlkit.inline_table()
|
|
113
|
+
web_search["value"] = invocation.web_search
|
|
114
|
+
argv += ["-c", f"tools.web_search={tomlkit.item(web_search['value']).as_string()}"]
|
|
115
|
+
# The prompt is any text; `--` keeps one starting with a hyphen out of the options.
|
|
116
|
+
yield [*argv, "--", invocation.prompt]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _read_auth_mode() -> str:
|
|
120
|
+
"""Return the Codex login mode; without a readable auth file, Codex runs on an API key."""
|
|
121
|
+
auth_file = Path.home() / ".codex" / "auth.json"
|
|
122
|
+
try:
|
|
123
|
+
return str(json.loads(auth_file.read_text()).get("auth_mode", "apikey"))
|
|
124
|
+
except (OSError, ValueError, AttributeError):
|
|
125
|
+
return "apikey"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _estimate_cost_usd(model: str, usage: Any) -> float:
|
|
129
|
+
"""Estimate the USD cost of the usage at list API prices for the model.
|
|
130
|
+
|
|
131
|
+
Cost is secondary to the run: an unknown model or unexpected usage yields 0, never a failure.
|
|
132
|
+
"""
|
|
133
|
+
if model not in MODEL_RATES or not isinstance(usage, dict):
|
|
134
|
+
return 0.0
|
|
135
|
+
input_rate, cached_rate, cache_write_rate, output_rate = MODEL_RATES[model]
|
|
136
|
+
if _read_auth_mode() == "chatgpt":
|
|
137
|
+
# A ChatGPT login pays nothing for cache writes.
|
|
138
|
+
cache_write_rate = 0.0
|
|
139
|
+
try:
|
|
140
|
+
input_tokens = int(usage["input_tokens"])
|
|
141
|
+
cached_tokens = int(usage.get("cached_input_tokens", 0))
|
|
142
|
+
cache_write_tokens = int(usage.get("cache_write_input_tokens", 0))
|
|
143
|
+
output_tokens = int(usage["output_tokens"])
|
|
144
|
+
except (KeyError, TypeError, ValueError):
|
|
145
|
+
return 0.0
|
|
146
|
+
# Reasoning tokens are a breakdown of the output tokens, not an addition.
|
|
147
|
+
uncached_tokens = input_tokens - cached_tokens - cache_write_tokens
|
|
148
|
+
if min(uncached_tokens, cached_tokens, cache_write_tokens, output_tokens) < 0:
|
|
149
|
+
return 0.0
|
|
150
|
+
return (
|
|
151
|
+
uncached_tokens * input_rate
|
|
152
|
+
+ cached_tokens * cached_rate
|
|
153
|
+
+ cache_write_tokens * cache_write_rate
|
|
154
|
+
+ output_tokens * output_rate
|
|
155
|
+
) / 1_000_000
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def read_result(invocation: AgentInvocation, stdout_lines: Sequence[str]) -> tuple[Any, float]:
|
|
159
|
+
"""Return the structured output of the last agent message and the estimated cost."""
|
|
160
|
+
agent_node_name = invocation.agent_node_name
|
|
161
|
+
last_agent_message = None
|
|
162
|
+
usage = None
|
|
163
|
+
for event in iter_jsonl_events(stdout_lines):
|
|
164
|
+
event_type = event.get("type")
|
|
165
|
+
if event_type in ("turn.failed", "error"):
|
|
166
|
+
# A turn.failed carries its message under `error`; an error event carries it directly.
|
|
167
|
+
error = event.get("error")
|
|
168
|
+
error_message = event.get("message") or (
|
|
169
|
+
error.get("message") if isinstance(error, dict) else error
|
|
170
|
+
)
|
|
171
|
+
raise NodeFailure(f"node '{agent_node_name}': agent reported an error: {error_message}")
|
|
172
|
+
item = event.get("item")
|
|
173
|
+
if (
|
|
174
|
+
event_type == "item.completed"
|
|
175
|
+
and isinstance(item, dict)
|
|
176
|
+
and item.get("type") == "agent_message"
|
|
177
|
+
and isinstance(item.get("text"), str)
|
|
178
|
+
):
|
|
179
|
+
last_agent_message = item["text"]
|
|
180
|
+
if event_type == "turn.completed":
|
|
181
|
+
usage = event.get("usage")
|
|
182
|
+
if last_agent_message is None:
|
|
183
|
+
raise NodeFailure(f"node '{agent_node_name}': agent output holds no agent message")
|
|
184
|
+
try:
|
|
185
|
+
structured_output = json.loads(last_agent_message)
|
|
186
|
+
except json.JSONDecodeError:
|
|
187
|
+
structured_output = None
|
|
188
|
+
return structured_output, _estimate_cost_usd(invocation.model, usage)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def read_session(stdout_lines: Sequence[str]) -> str | None:
|
|
192
|
+
"""Return the session the last thread.started event names by its Codex thread id."""
|
|
193
|
+
thread_events = (
|
|
194
|
+
event for event in iter_jsonl_events(stdout_lines) if event.get("type") == "thread.started"
|
|
195
|
+
)
|
|
196
|
+
return read_last_value(thread_events, "thread_id")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _read_narration(agent_message_text: str) -> str | None:
|
|
200
|
+
"""Return the handoff of a schema-shaped agent message, else the text itself.
|
|
201
|
+
|
|
202
|
+
Under --output-schema Codex shapes every agent message like the outcome, so the narration
|
|
203
|
+
of a message sits in its handoff.
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
handoff: str | None = json.loads(agent_message_text)["handoff"]
|
|
207
|
+
except (json.JSONDecodeError, TypeError, KeyError):
|
|
208
|
+
return agent_message_text
|
|
209
|
+
return handoff
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def render_transcript(stdout_lines: Sequence[str]) -> list[Text]:
|
|
213
|
+
"""Render the narration of the agent messages, command executions, and file changes.
|
|
214
|
+
|
|
215
|
+
Reasoning and every other item are dropped.
|
|
216
|
+
"""
|
|
217
|
+
completed_items = [
|
|
218
|
+
event["item"]
|
|
219
|
+
for event in iter_jsonl_events(stdout_lines)
|
|
220
|
+
if event.get("type") == "item.completed" and isinstance(event.get("item"), dict)
|
|
221
|
+
]
|
|
222
|
+
transcript_rows: list[Text] = []
|
|
223
|
+
for item in completed_items:
|
|
224
|
+
match item.get("type"):
|
|
225
|
+
case "agent_message":
|
|
226
|
+
transcript_rows += split_lines(_read_narration(item["text"]))
|
|
227
|
+
case "command_execution":
|
|
228
|
+
transcript_rows.append(Text(f"▸ command_execution: {item['command']}", "bold"))
|
|
229
|
+
case "file_change":
|
|
230
|
+
paths = ", ".join(change["path"] for change in item["changes"])
|
|
231
|
+
transcript_rows.append(Text(f"▸ file_change: {paths}", "bold"))
|
|
232
|
+
return transcript_rows
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wg_code-review
|
|
3
|
+
description: Reviews the branch's diff against the merge-base for standards and spec.
|
|
4
|
+
tools: Bash, Read, Glob, Grep, Skill, Task
|
|
5
|
+
---
|
|
6
|
+
Invoke the `/mattpocock-skills:code-review` skill over the branch's diff
|
|
7
|
+
against the merge-base with the default branch. The skill runs the Standards
|
|
8
|
+
and Spec axes in parallel sub-agents.
|
|
9
|
+
|
|
10
|
+
Your job is this review only. The workflow's other nodes implement and open
|
|
11
|
+
the PR: do not change code and do not open a PR.
|
|
12
|
+
|
|
13
|
+
Report `pass` when both axes are clean. Otherwise report `fail` and list
|
|
14
|
+
every finding in the handoff.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wg_design
|
|
3
|
+
description: Makes sure the change the run input names has an agent brief.
|
|
4
|
+
tools: Bash, Read, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
The run input names a GitHub issue in this repository, or a task with no
|
|
7
|
+
issue. Read the issue with `gh issue view <number> --comments`. Take the
|
|
8
|
+
owner and the repo from `git remote -v`. Where `gh` is unavailable, read
|
|
9
|
+
`https://api.github.com/repos/<owner>/<repo>/issues/<number>` and its
|
|
10
|
+
`/comments` with `curl`, and post or edit through the same API with `curl`
|
|
11
|
+
and the `GITHUB_TOKEN` environment variable. Do not modify the repository.
|
|
12
|
+
|
|
13
|
+
An agent brief is the issue body or an issue comment whose text contains the
|
|
14
|
+
heading `## Agent Brief`. Its link is the issue URL
|
|
15
|
+
(`https://github.com/<owner>/<repo>/issues/<number>`) when the brief is in the
|
|
16
|
+
body, and the comment's `html_url`
|
|
17
|
+
(`https://github.com/<owner>/<repo>/issues/<number>#issuecomment-<id>`) when
|
|
18
|
+
the brief is in a comment. `gh api repos/<owner>/<repo>/issues/<number>/comments`
|
|
19
|
+
gives the comment ids, bodies, and `html_url` values. When several comments
|
|
20
|
+
hold a brief, the last one is the brief.
|
|
21
|
+
|
|
22
|
+
## The four cases
|
|
23
|
+
|
|
24
|
+
1. No handoff and the issue carries a brief: do no other work. Report `done`
|
|
25
|
+
with the brief's link on one line as the handoff.
|
|
26
|
+
2. No handoff and the issue carries no brief: read the code the issue
|
|
27
|
+
touches, write the brief, and post it with
|
|
28
|
+
`gh issue comment <number> --body-file <file>` from a temporary file
|
|
29
|
+
outside the repository. Read the new comment's link with
|
|
30
|
+
`gh api repos/<owner>/<repo>/issues/<number>/comments --jq '.[-1].html_url'`
|
|
31
|
+
and report `done` with that link as the handoff.
|
|
32
|
+
3. A handoff with `received` and `feedback`: `received` holds the brief's
|
|
33
|
+
link. `feedback` holds the feedback on the rejected brief. Rewrite the
|
|
34
|
+
brief to address every point of the feedback, keeping the first line and
|
|
35
|
+
the seven sections. Edit it in place with
|
|
36
|
+
`gh api --method PATCH repos/<owner>/<repo>/issues/comments/<id> -F body=@<file>`
|
|
37
|
+
for a comment and `gh issue edit <number> --body-file <file>` for the body.
|
|
38
|
+
Report `done` with the same link as the handoff.
|
|
39
|
+
4. The run input names no issue: write the brief from the run input and the
|
|
40
|
+
code, and report `done` with the whole brief as the handoff. A later
|
|
41
|
+
handoff then carries the brief itself under `received`; revise it to
|
|
42
|
+
address every point of `feedback` and report the revised brief inline
|
|
43
|
+
again.
|
|
44
|
+
|
|
45
|
+
## Brief shape
|
|
46
|
+
|
|
47
|
+
Write the brief under the AGENTS.md writing rules. Its first line is
|
|
48
|
+
`> *This was generated by AI as the design node.*`. The heading
|
|
49
|
+
`## Agent Brief` follows, then seven sections:
|
|
50
|
+
|
|
51
|
+
- **Category**: the kind of change.
|
|
52
|
+
- **Summary**: one sentence.
|
|
53
|
+
- **Current behavior**: what the code does today.
|
|
54
|
+
- **Desired behavior**: what it does after the change.
|
|
55
|
+
- **Key interfaces**: the names and types the change touches.
|
|
56
|
+
- **Acceptance criteria**: a checklist of independently verifiable items.
|
|
57
|
+
- **Out of scope**: what the change leaves alone.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wg_implement
|
|
3
|
+
description: Implements a GitHub issue in the current repository.
|
|
4
|
+
tools: Bash, Read, Edit, Write, Glob, Grep, Skill
|
|
5
|
+
---
|
|
6
|
+
The run input names a GitHub issue in this repository, or a task with no
|
|
7
|
+
issue. Read the issue with `gh issue view <number> --comments`. Take the
|
|
8
|
+
owner and the repo from `git remote -v`. Where `gh` is unavailable, read
|
|
9
|
+
`https://api.github.com/repos/<owner>/<repo>/issues/<number>` and its
|
|
10
|
+
`/comments` with `curl`.
|
|
11
|
+
|
|
12
|
+
## The plan
|
|
13
|
+
|
|
14
|
+
The plan comes from the first of these that holds:
|
|
15
|
+
|
|
16
|
+
1. The handoff, when it is one line holding an issue comment link
|
|
17
|
+
(`https://github.com/<owner>/<repo>/issues/<number>#issuecomment-<id>`).
|
|
18
|
+
Read the plan with
|
|
19
|
+
`gh api repos/<owner>/<repo>/issues/comments/<id> --jq .body`.
|
|
20
|
+
2. Else the issue's last comment whose text contains the heading
|
|
21
|
+
`## Implementation Plan`, from
|
|
22
|
+
`gh api repos/<owner>/<repo>/issues/<number>/comments`.
|
|
23
|
+
3. Else the plan in the prompt, for a run whose input names no issue.
|
|
24
|
+
|
|
25
|
+
Follow the plan task by task, one commit per task, with the task's commit
|
|
26
|
+
summary. On re-entry from `test` or `review-loop`, the handoff is not the
|
|
27
|
+
plan, so locate the plan on the issue and still address every finding the
|
|
28
|
+
handoff carries.
|
|
29
|
+
|
|
30
|
+
Implement what the issue asks:
|
|
31
|
+
|
|
32
|
+
- Follow the repository standards in AGENTS.md.
|
|
33
|
+
- Use the `/mattpocock-skills:tdd` skill where possible.
|
|
34
|
+
- Run typechecking and single test files regularly, and the full test suite
|
|
35
|
+
once at the end.
|
|
36
|
+
- Commit the work to the current branch.
|
|
37
|
+
|
|
38
|
+
Report `done` when the implementation and its tests are complete and
|
|
39
|
+
committed.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wg_overengineering-review
|
|
3
|
+
description: Reviews the branch's diff against the merge-base for over-engineering.
|
|
4
|
+
tools: Bash, Read, Glob, Grep, Skill
|
|
5
|
+
---
|
|
6
|
+
Invoke the `/ponytail:ponytail-review` skill over the branch's diff against
|
|
7
|
+
the merge-base with the default branch.
|
|
8
|
+
|
|
9
|
+
Your job is this review only. The workflow's other nodes implement and open
|
|
10
|
+
the PR: do not change code and do not open a PR.
|
|
11
|
+
|
|
12
|
+
Report `pass` when there are no findings. Otherwise report `fail` and list
|
|
13
|
+
the findings in the handoff.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wg_plan
|
|
3
|
+
description: Makes sure the change the run input names has an implementation plan.
|
|
4
|
+
tools: Bash, Read, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
The run input names a GitHub issue in this repository, or a task with no
|
|
7
|
+
issue. Read the issue with `gh issue view <number> --comments`. Take the
|
|
8
|
+
owner and the repo from `git remote -v`. Where `gh` is unavailable, read
|
|
9
|
+
`https://api.github.com/repos/<owner>/<repo>/issues/<number>` and its
|
|
10
|
+
`/comments` with `curl`, and post or edit through the same API with `curl`
|
|
11
|
+
and the `GITHUB_TOKEN` environment variable. Do not modify the repository.
|
|
12
|
+
|
|
13
|
+
The handoff on entry from `approve-design` carries the link of the issue's
|
|
14
|
+
agent brief, or the brief itself when the run input names no issue. The brief
|
|
15
|
+
is the body or the comment at the link, under the heading `## Agent Brief`.
|
|
16
|
+
The plan follows the brief: its tasks implement the brief's desired behavior
|
|
17
|
+
and stay inside its scope.
|
|
18
|
+
|
|
19
|
+
An implementation plan is an issue comment whose text contains the heading
|
|
20
|
+
`## Implementation Plan`. Its link is the comment's `html_url`
|
|
21
|
+
(`https://github.com/<owner>/<repo>/issues/<number>#issuecomment-<id>`).
|
|
22
|
+
`gh api repos/<owner>/<repo>/issues/<number>/comments` gives the comment ids,
|
|
23
|
+
bodies, and `html_url` values. When several comments hold a plan, the last one
|
|
24
|
+
is the plan.
|
|
25
|
+
|
|
26
|
+
## The four cases
|
|
27
|
+
|
|
28
|
+
1. The handoff holds the brief's link and the issue carries a plan: do no
|
|
29
|
+
other work. Report `done` with the plan's link on one line as the handoff.
|
|
30
|
+
2. The handoff holds the brief's link and the issue carries no plan: read the
|
|
31
|
+
brief, read the code the issue touches, write the plan, and post it with
|
|
32
|
+
`gh issue comment <number> --body-file <file>` from a temporary file
|
|
33
|
+
outside the repository. Read the new comment's link with
|
|
34
|
+
`gh api repos/<owner>/<repo>/issues/<number>/comments --jq '.[-1].html_url'`
|
|
35
|
+
and report `done` with that link on one line as the handoff.
|
|
36
|
+
3. A handoff with `received` and `feedback`: `received` holds the plan's link.
|
|
37
|
+
`feedback` holds the feedback on the rejected plan. Rewrite the plan to
|
|
38
|
+
address every point of the feedback, keeping the first line and the
|
|
39
|
+
heading. Edit it in place with
|
|
40
|
+
`gh api --method PATCH repos/<owner>/<repo>/issues/comments/<id> -F body=@<file>`.
|
|
41
|
+
Report `done` with the same link as the handoff.
|
|
42
|
+
4. The run input names no issue: write the plan from the brief and the code,
|
|
43
|
+
and report `done` with the whole plan as the handoff. A later handoff then
|
|
44
|
+
carries the plan itself under `received`; revise it to address every point
|
|
45
|
+
of `feedback` and report the revised plan inline again.
|
|
46
|
+
|
|
47
|
+
## Plan shape
|
|
48
|
+
|
|
49
|
+
Write the plan under the AGENTS.md writing rules. Use file paths, never
|
|
50
|
+
line numbers. Do not use code blocks.
|
|
51
|
+
|
|
52
|
+
The plan's first line is `> *This was generated by AI as the plan node.*`.
|
|
53
|
+
The heading `## Implementation Plan` follows, then:
|
|
54
|
+
|
|
55
|
+
1. Header:
|
|
56
|
+
- **Goal**: one sentence.
|
|
57
|
+
- **Approach**: two or three sentences.
|
|
58
|
+
- **Issue**: `#<number>`.
|
|
59
|
+
2. **Files**: every file created or modified, one line each on its
|
|
60
|
+
responsibility.
|
|
61
|
+
3. **Tasks**, in order. A task is the smallest unit with its own test cycle
|
|
62
|
+
that a reviewer could reject on its own. Setup, configuration, and
|
|
63
|
+
documentation belong to the task that needs them. Each task lists:
|
|
64
|
+
- the files it touches;
|
|
65
|
+
- the interfaces it consumes from earlier tasks and produces for later
|
|
66
|
+
ones, with exact names and types;
|
|
67
|
+
- its tests, described by behavior;
|
|
68
|
+
- its commit summary.
|
|
69
|
+
|
|
70
|
+
Do not write placeholders: no "TBD", no "add error handling", no "similar
|
|
71
|
+
to task N", no reference to a name that no task defines.
|
|
72
|
+
|
|
73
|
+
## Self-check
|
|
74
|
+
|
|
75
|
+
Before reporting, check the plan against the brief:
|
|
76
|
+
|
|
77
|
+
- every acceptance criterion of the brief maps to a task;
|
|
78
|
+
- no placeholder remains;
|
|
79
|
+
- a name used in several tasks is the same in all of them.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wg_pr
|
|
3
|
+
description: Squashes the branch to one commit and creates or updates the pull request.
|
|
4
|
+
tools: Bash, Read, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
The run input names a GitHub issue in this repository.
|
|
7
|
+
|
|
8
|
+
1. Run `git fetch origin main`, then squash the branch to exactly one
|
|
9
|
+
commit on top of `origin/main` with a rebase or `git commit --amend`,
|
|
10
|
+
then `git push --force-with-lease`. Never use the local `main`: it is
|
|
11
|
+
stale. The branch carries only this run's commits; never rewrite
|
|
12
|
+
history containing someone else's commits.
|
|
13
|
+
2. Rewrite the commit message to fit the rules below, amend, and push
|
|
14
|
+
again. Do not just validate — fix any violation yourself.
|
|
15
|
+
3. Create the pull request for the branch with `gh pr create`, or update
|
|
16
|
+
the existing one with `gh pr edit`. Never merge it.
|
|
17
|
+
4. When the prompt carries a handoff from `summary`, add a `Not addressed`
|
|
18
|
+
section to the PR body that lists every finding in it.
|
|
19
|
+
|
|
20
|
+
Rules for the commit message and the PR text:
|
|
21
|
+
|
|
22
|
+
- Commit message: one summary line, then at most one paragraph of 3–5
|
|
23
|
+
sentences, then a final line `Closes #<number>` that names the issue
|
|
24
|
+
from the run input.
|
|
25
|
+
- PR title: the commit summary line, verbatim.
|
|
26
|
+
- PR description: the first line is `Closes #<number>`, naming the issue
|
|
27
|
+
from the run input. The rest is more detailed than the commit message,
|
|
28
|
+
at most 3 sections plus the `Not addressed` section, formatted with
|
|
29
|
+
markdown; itemized lists replace long paragraphs and sentences.
|
|
30
|
+
- Everywhere: simple sentences, active voice, and one term per concept —
|
|
31
|
+
reuse the terms the repository already uses instead of varying them.
|
|
32
|
+
|
|
33
|
+
Report `done` when the PR exists and matches the branch.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wg_summarize-review
|
|
3
|
+
description: Summarizes the findings of the last review as not addressed.
|
|
4
|
+
---
|
|
5
|
+
The prompt carries a handoff from `review` with the findings of the last
|
|
6
|
+
review. Those findings, and only those, are not addressed. Summarize them:
|
|
7
|
+
one bullet per finding, naming its location and what it asks for. When the
|
|
8
|
+
prompt carries no handoff from `review`, report `done` with no handoff.
|
|
9
|
+
|
|
10
|
+
Report `done` with the summary as the handoff.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
start = "design"
|
|
2
|
+
|
|
3
|
+
[defaults]
|
|
4
|
+
harness = "claude"
|
|
5
|
+
model = "claude-fable-5-1"
|
|
6
|
+
effort = "high"
|
|
7
|
+
|
|
8
|
+
[nodes.design]
|
|
9
|
+
agent = "wg_design"
|
|
10
|
+
effort = "xhigh"
|
|
11
|
+
outcomes = ["done"]
|
|
12
|
+
|
|
13
|
+
[nodes.design.transitions]
|
|
14
|
+
done = "approve-design"
|
|
15
|
+
|
|
16
|
+
[nodes.approve-design]
|
|
17
|
+
gate = "Plan from this design?"
|
|
18
|
+
|
|
19
|
+
[nodes.approve-design.transitions]
|
|
20
|
+
accept = "plan"
|
|
21
|
+
reject = "design"
|
|
22
|
+
|
|
23
|
+
[nodes.plan]
|
|
24
|
+
agent = "wg_plan"
|
|
25
|
+
effort = "xhigh"
|
|
26
|
+
outcomes = ["done"]
|
|
27
|
+
|
|
28
|
+
[nodes.plan.transitions]
|
|
29
|
+
done = "approve-plan"
|
|
30
|
+
|
|
31
|
+
[nodes.approve-plan]
|
|
32
|
+
gate = "Implement this plan?"
|
|
33
|
+
|
|
34
|
+
[nodes.approve-plan.transitions]
|
|
35
|
+
accept = "implement"
|
|
36
|
+
reject = "plan"
|
|
37
|
+
|
|
38
|
+
[nodes.implement]
|
|
39
|
+
agent = "wg_implement"
|
|
40
|
+
model = "claude-opus-5"
|
|
41
|
+
outcomes = ["done"]
|
|
42
|
+
|
|
43
|
+
[nodes.implement.transitions]
|
|
44
|
+
done = "test"
|
|
45
|
+
|
|
46
|
+
[nodes.test]
|
|
47
|
+
command = "sh -c 'uv run ruff check && uv run ruff format --check && uv run mypy && uv run pytest'"
|
|
48
|
+
|
|
49
|
+
[nodes.test.limits]
|
|
50
|
+
visits = 5
|
|
51
|
+
reset = "pass"
|
|
52
|
+
|
|
53
|
+
[nodes.test.transitions]
|
|
54
|
+
pass = "review"
|
|
55
|
+
fail = "implement"
|
|
56
|
+
|
|
57
|
+
[nodes.review]
|
|
58
|
+
map = ["code-review", "overengineering-review"]
|
|
59
|
+
resolve = "all"
|
|
60
|
+
|
|
61
|
+
[nodes.review.transitions]
|
|
62
|
+
pass = "pr"
|
|
63
|
+
fail = "review-loop"
|
|
64
|
+
|
|
65
|
+
# Holds the review limit so the LIMIT diversion carries the review findings.
|
|
66
|
+
[nodes.review-loop]
|
|
67
|
+
command = "true"
|
|
68
|
+
|
|
69
|
+
[nodes.review-loop.limits]
|
|
70
|
+
visits = 2
|
|
71
|
+
|
|
72
|
+
[nodes.review-loop.transitions]
|
|
73
|
+
pass = "implement"
|
|
74
|
+
fail = "implement"
|
|
75
|
+
LIMIT = "summary"
|
|
76
|
+
|
|
77
|
+
[nodes.code-review]
|
|
78
|
+
agent = "wg_code-review"
|
|
79
|
+
outcomes = ["pass", "fail"]
|
|
80
|
+
|
|
81
|
+
[nodes.overengineering-review]
|
|
82
|
+
agent = "wg_overengineering-review"
|
|
83
|
+
outcomes = ["pass", "fail"]
|
|
84
|
+
|
|
85
|
+
[nodes.summary]
|
|
86
|
+
agent = "wg_summarize-review"
|
|
87
|
+
model = "sonnet"
|
|
88
|
+
effort = "medium"
|
|
89
|
+
outcomes = ["done"]
|
|
90
|
+
|
|
91
|
+
[nodes.summary.transitions]
|
|
92
|
+
done = "pr"
|
|
93
|
+
|
|
94
|
+
[nodes.pr]
|
|
95
|
+
agent = "wg_pr"
|
|
96
|
+
model = "haiku"
|
|
97
|
+
effort = "medium"
|
|
98
|
+
outcomes = ["done"]
|
|
99
|
+
|
|
100
|
+
[nodes.pr.transitions]
|
|
101
|
+
done = "END"
|