Skip to content

Commit fe91dbe

Browse files
stack72claude
andauthored
feat: add post-PR lifecycle phases and ship method (#1153)
## Summary - Adds two new phases (`pr_failed`, `releasing`) between `pr_open` and `done` for visibility into CI failures and release builds - Adds three new methods: `pr_merged` (pr_open → releasing), `pr_failed` (pr_open → pr_failed), `ship` (releasing → done) - Adds `pr-cooldown` check enforcing a 3-minute wait after `link_pr` before checking PR status, giving CI time to run - Enables recovery from `pr_failed` via `link_pr` (re-link) or `implement` (major rework) - Adds skill guidance requiring human confirmation before opening PRs ## Test Plan - [x] `deno check` — type checking passes - [x] `deno lint` — no lint errors - [x] `deno fmt` — all files formatted - [x] `deno run test` — all 4260 tests pass (26 in lifecycle + schema tests) - [x] `deno run compile` — binary compiles successfully - [ ] Manual: create an issue-lifecycle instance, walk through `link_pr → pr_merged → ship` flow - [ ] Manual: verify `pr_failed → link_pr` recovery loop works 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4f579e9 commit fe91dbe

6 files changed

Lines changed: 738 additions & 34 deletions

File tree

.claude/skills/issue-lifecycle/SKILL.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,10 @@ Use this table to determine what to do next:
125125
| `classified` | Read [references/planning.md](references/planning.md) |
126126
| `plan_generated` | Read [references/adversarial-review.md](references/adversarial-review.md) |
127127
| `approved` | Read [references/implementation.md](references/implementation.md) |
128-
| `implementing` | Check PR status or call `complete` |
129-
| `pr_open` | Check PR status or call `complete` |
128+
| `implementing` | Link a PR with `link_pr` or call `complete` |
129+
| `pr_open` | Wait 3 min, then check PR: `pr_merged` if merged, `pr_failed` if failed |
130+
| `pr_failed` | Fix the issue, then `link_pr` (new PR) or `implement` (major rework) |
131+
| `releasing` | Check release build: `ship` when done, or `complete` as fallback |
130132
| `done` | Nothing to do — lifecycle is complete |
131133

132134
The canonical phase list lives in the `TRANSITIONS` constant in
@@ -140,11 +142,20 @@ When a PR has already merged and the lifecycle just needs to be marked done:
140142
```
141143
swamp data get issue-<N> state-main --json
142144
```
143-
2. If the phase is `implementing` and you have a PR URL, link it first:
145+
2. If the phase is `implementing`, link the PR first:
144146
```
145147
swamp model method run issue-<N> link_pr --input url=<PR URL>
146148
```
147-
3. Call `complete` (accepts both `implementing` and `pr_open` as source phases):
149+
3. If the phase is `pr_open`, record the merge:
150+
```
151+
swamp model method run issue-<N> pr_merged
152+
```
153+
4. If the phase is `releasing`, ship it:
154+
```
155+
swamp model method run issue-<N> ship
156+
```
157+
5. For quick close-out, `complete` still works from `implementing`, `pr_open`,
158+
or `releasing`:
148159
```
149160
swamp model method run issue-<N> complete
150161
```
@@ -162,7 +173,9 @@ When a PR has already merged and the lifecycle just needs to be marked done:
162173
reference specific files, functions, and test paths.
163174
6. **Follow the planning conventions for this repository.** Read
164175
`agent-constraints/planning-conventions.md` if it exists.
165-
7. **File unrelated issues immediately.** If you discover a bug, code smell, or
176+
7. **Never open a PR without asking first.** Present the changes summary and
177+
wait for the human to confirm before creating the pull request.
178+
8. **File unrelated issues immediately.** If you discover a bug, code smell, or
166179
problem during investigation that is NOT related to the current issue, file
167180
it as a new swamp-club issue. Do not try to fix it in the current work span —
168181
keep the scope focused.

.claude/skills/issue-lifecycle/references/implementation.md

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Report verification results to the human before creating the PR:
3737

3838
## 4. Create a PR
3939

40+
**Always ask the human before opening a PR.** Do not create the PR automatically
41+
— present a summary of the changes and ask if they are ready to open it. Only
42+
proceed after explicit confirmation.
43+
4044
Use the repository's normal PR tooling. Read
4145
`agent-constraints/implementation-conventions.md` for repo-specific PR
4246
conventions.
@@ -69,13 +73,56 @@ Calling `link_pr` is **encouraged but not enforced** — `complete` still accept
6973
records. Prefer the `implementing → link_pr → complete` flow for all new work so
7074
the lifecycle record carries the PR link.
7175

72-
## 5. Complete the Issue
76+
## 4b. Wait for CI and Check PR Status
77+
78+
After linking the PR, wait at least 3 minutes for CI to run. The model enforces
79+
a `pr-cooldown` check — calling `pr_merged` or `pr_failed` within 3 minutes of
80+
`link_pr` will be rejected.
81+
82+
Check the PR status externally (e.g.,
83+
`gh pr view <url> --json state,statusCheckRollup`):
84+
85+
- **PR merged**: call `pr_merged` to transition to `releasing`
86+
```
87+
swamp model method run issue-<N> pr_merged
88+
```
89+
- **PR failed** (CI red, changes requested): call `pr_failed` with the reason
90+
```
91+
swamp model method run issue-<N> pr_failed --input reason="CI failed: type check errors"
92+
```
93+
- **PR still open and passing**: wait and check again later.
94+
95+
## 4c. Handle PR Failure
96+
97+
When in `pr_failed`, diagnose and fix the issue. Then either:
98+
99+
- Push fixes and call `link_pr` again (same or new PR URL) to return to
100+
`pr_open`:
101+
```
102+
swamp model method run issue-<N> link_pr --input url=<PR URL>
103+
```
104+
- Call `implement` to go back to the implementing phase for major rework:
105+
```
106+
swamp model method run issue-<N> implement
107+
```
73108

74-
Once the PR has merged and the work is done, call `complete`:
109+
## 5. Ship the Release
110+
111+
After `pr_merged` transitions to `releasing`, wait for the release build to
112+
complete. Once the release is out, call `ship`:
113+
114+
```
115+
swamp model method run issue-<N> ship
116+
```
117+
118+
Optionally pass release metadata:
75119

76120
```
77-
swamp model method run issue-<N> complete
121+
swamp model method run issue-<N> ship --input releaseUrl=<URL> --input releaseNotes="Bug fix release"
78122
```
79123

80124
This transitions the phase to `done`, transitions the swamp-club status to
81-
`shipped`, and posts a `complete` lifecycle entry.
125+
`shipped`, and posts a `shipped` lifecycle entry.
126+
127+
For quick close-out (e.g., the PR merged and you just want to wrap up),
128+
`complete` still works from `implementing`, `pr_open`, or `releasing`.

extensions/models/_lib/schemas.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,16 @@ export const Phase = z.enum([
4444
"approved",
4545
"implementing",
4646
"pr_open",
47+
"pr_failed",
48+
"releasing",
4749
"done",
4850
]);
4951

5052
export type Phase = z.infer<typeof Phase>;
5153

54+
/** Minimum time (ms) between link_pr and pr_merged/pr_failed to allow CI to run. */
55+
export const PR_COOLDOWN_MS = 3 * 60 * 1000;
56+
5257
/** Valid transitions: method name → allowed source phases */
5358
export const TRANSITIONS: Record<string, Phase[]> = {
5459
start: [
@@ -59,16 +64,21 @@ export const TRANSITIONS: Record<string, Phase[]> = {
5964
"approved",
6065
"implementing",
6166
"pr_open",
67+
"pr_failed",
68+
"releasing",
6269
],
6370
triage: ["triaging"],
6471
plan: ["classified"],
6572
iterate: ["plan_generated"],
6673
approve: ["plan_generated"],
67-
implement: ["approved"],
74+
implement: ["approved", "pr_failed"],
6875
adversarial_review: ["plan_generated"],
6976
resolve_findings: ["plan_generated"],
70-
link_pr: ["implementing", "pr_open"],
71-
complete: ["implementing", "pr_open"],
77+
link_pr: ["implementing", "pr_open", "pr_failed"],
78+
pr_merged: ["pr_open"],
79+
pr_failed: ["pr_open"],
80+
ship: ["releasing"],
81+
complete: ["implementing", "pr_open", "releasing"],
7282
};
7383

7484
// ---------------------------------------------------------------------------
@@ -170,10 +180,23 @@ export const PullRequestSchema = z.object({
170180
"Canonical URL of the pull request. Opaque to the model — the agent " +
171181
"supplies whatever URL their git host produced.",
172182
),
183+
attempt: z.number().describe(
184+
"Sequential attempt number. Starts at 1 on the first link_pr call, " +
185+
"incremented on each subsequent link_pr call after a pr_failed cycle.",
186+
),
173187
linkedAt: z.string().describe(
174188
"ISO-8601 timestamp of when link_pr was called. Updated on every " +
175189
"subsequent link_pr call so the record reflects the latest link.",
176190
),
191+
mergedAt: z.string().optional().describe(
192+
"ISO-8601 timestamp of when pr_merged was called. Set once.",
193+
),
194+
failedAt: z.string().optional().describe(
195+
"ISO-8601 timestamp of when pr_failed was called. Cleared on next link_pr.",
196+
),
197+
failureReason: z.string().optional().describe(
198+
"Why the PR failed (CI failure, review rejection, etc.). Cleared on next link_pr.",
199+
),
177200
});
178201

179202
export type PullRequestData = z.infer<typeof PullRequestSchema>;

extensions/models/_lib/schemas_test.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,35 @@
1717
import { assertEquals } from "@std/assert";
1818
import { Phase, PullRequestSchema, TRANSITIONS } from "./schemas.ts";
1919

20-
Deno.test("Phase: includes pr_open between implementing and done", () => {
20+
Deno.test("Phase: includes pr_open, pr_failed, releasing between implementing and done", () => {
2121
const phases = Phase.options;
2222
const implementingIdx = phases.indexOf("implementing");
2323
const prOpenIdx = phases.indexOf("pr_open");
24+
const prFailedIdx = phases.indexOf("pr_failed");
25+
const releasingIdx = phases.indexOf("releasing");
2426
const doneIdx = phases.indexOf("done");
2527

2628
assertEquals(prOpenIdx, implementingIdx + 1);
27-
assertEquals(doneIdx, prOpenIdx + 1);
29+
assertEquals(prFailedIdx, prOpenIdx + 1);
30+
assertEquals(releasingIdx, prFailedIdx + 1);
31+
assertEquals(doneIdx, releasingIdx + 1);
2832
});
2933

30-
Deno.test("TRANSITIONS: link_pr is idempotent from implementing and pr_open", () => {
31-
assertEquals(TRANSITIONS.link_pr, ["implementing", "pr_open"]);
34+
Deno.test("TRANSITIONS: link_pr accepts implementing, pr_open, and pr_failed", () => {
35+
assertEquals(TRANSITIONS.link_pr, ["implementing", "pr_open", "pr_failed"]);
3236
});
3337

34-
Deno.test("TRANSITIONS: complete accepts both implementing (legacy) and pr_open (new)", () => {
35-
// Accepting both keeps existing records (created before this phase was
36-
// introduced) able to finish without being forced through link_pr.
37-
assertEquals(TRANSITIONS.complete, ["implementing", "pr_open"]);
38+
Deno.test("TRANSITIONS: complete accepts implementing, pr_open, and releasing", () => {
39+
// Accepting all three keeps backwards compatibility while allowing
40+
// the new releasing phase to be closed out directly.
41+
assertEquals(TRANSITIONS.complete, ["implementing", "pr_open", "releasing"]);
3842
});
3943

40-
Deno.test("TRANSITIONS: start (resume) includes pr_open so in-flight issues can be picked up", () => {
44+
Deno.test("TRANSITIONS: start (resume) includes pr_open, pr_failed, and releasing", () => {
4145
const startPhases = TRANSITIONS.start;
4246
assertEquals(startPhases.includes("pr_open"), true);
47+
assertEquals(startPhases.includes("pr_failed"), true);
48+
assertEquals(startPhases.includes("releasing"), true);
4349
});
4450

4551
Deno.test("TRANSITIONS: link_pr is rejected from earlier lifecycle phases", () => {
@@ -73,6 +79,7 @@ Deno.test("PullRequestSchema: accepts any non-empty URL string", () => {
7379
for (const url of samples) {
7480
const parsed = PullRequestSchema.parse({
7581
url,
82+
attempt: 1,
7683
linkedAt: "2026-04-08T15:00:00.000Z",
7784
});
7885
assertEquals(parsed.url, url);
@@ -82,6 +89,7 @@ Deno.test("PullRequestSchema: accepts any non-empty URL string", () => {
8289
Deno.test("PullRequestSchema: rejects empty url string", () => {
8390
const result = PullRequestSchema.safeParse({
8491
url: "",
92+
attempt: 1,
8593
linkedAt: "2026-04-08T15:00:00.000Z",
8694
});
8795
assertEquals(result.success, false);

0 commit comments

Comments
 (0)