0052 — PR lifecycle: the phase chip tells the truth #132

Merged
chad merged 10 commits from feat/0052-pr-lifecycle into main 2026-10-19 10:09:00 -04:00
Collaborator

What

The header phase chip (and the PR transcript card) said "PR open" forever after a create_pr call returned a URL — merges/closes happen on the host and nothing ever asked again. 0052 makes them ask: while the derived phase is an open PR, the client polls GET /api/x/prs (visible-tab, 5 s, stops once settled) and the chip relabels to PR merged / PR closed per the host's live answer. derivePhase stays pure (0041 holds) — the override is render-time (applyPRState); the transcript snapshot keeps recording what was true when the call ran.

Pieces

  • GitHost.getPRState (github + forgejo) — GET pulls/{n}, merged/closed/open mapping, errors → null, never throws
  • GET /api/x/prs?url=… — batched, cap 10, credential-resolved by host scope, unresolvable → null, never 500s
  • usePRStates — the useChildStatus polling rule; settled PRs are never refetched
  • Phase chip + PullRequestToolCard pill follow the live state; create_pr result now records state: "open"
  • Design doc docs/0052-pr-lifecycle.md (approved in chat); AGENTS.md phase-chip bullet updated

Evidence

  • npm run typecheck clean, npm run lint 0 errors, npm run test 838 passed / 16 skipped (pre-existing spike skips) — run in this checkout, twice (before and after review fixes)
  • Fresh-eyes review child: no blockers, no majors, verdict mergeable; its three cheap findings fixed in 8a8985b (listener always removed on stop, empty-url early return, owner/repo path-encoded). Follow-up noted, not blocking: the hook's fetch loop has no direct test coverage — the repo has no DOM test env and adding one is a new dependency (needs your sign-off if wanted).
## What The header phase chip (and the PR transcript card) said "PR open" forever after a `create_pr` call returned a URL — merges/closes happen on the host and nothing ever asked again. 0052 makes them ask: while the derived phase is an open PR, the client polls `GET /api/x/prs` (visible-tab, 5 s, stops once settled) and the chip relabels to **PR merged / PR closed** per the host's live answer. `derivePhase` stays pure (0041 holds) — the override is render-time (`applyPRState`); the transcript snapshot keeps recording what was true when the call ran. ## Pieces - `GitHost.getPRState` (github + forgejo) — GET `pulls/{n}`, merged/closed/open mapping, errors → null, never throws - `GET /api/x/prs?url=…` — batched, cap 10, credential-resolved by host scope, unresolvable → null, never 500s - `usePRStates` — the `useChildStatus` polling rule; settled PRs are never refetched - Phase chip + `PullRequestToolCard` pill follow the live state; `create_pr` result now records `state: "open"` - Design doc `docs/0052-pr-lifecycle.md` (approved in chat); AGENTS.md phase-chip bullet updated ## Evidence - `npm run typecheck` clean, `npm run lint` 0 errors, `npm run test` **838 passed / 16 skipped** (pre-existing spike skips) — run in this checkout, twice (before and after review fixes) - Fresh-eyes review child: **no blockers, no majors, verdict mergeable**; its three cheap findings fixed in `8a8985b` (listener always removed on stop, empty-url early return, owner/repo path-encoded). Follow-up noted, not blocking: the hook's fetch loop has no direct test coverage — the repo has no DOM test env and adding one is a new dependency (needs your sign-off if wanted).
glen added 10 commits 2026-10-19 10:04:29 -04:00
0052: the seam can now ask a host what a pull request's lifecycle state
is. GitHub maps merged:true (+merged_at), Forgejo maps its merged
timestamp; anything not 2xx or shape-off is null — never thrown. A
getJSON sibling joins postJSON for the read side.
0052: one route answers every visible PR at once. Each ?url= resolves
host + credential by the 0004 host-scope rule (github.com, the forgejo
instance), asks the seam, and returns { [url]: state | null } keyed by
the exact input URL. Bad URLs, missing credentials, host errors all map
to null — the route never 500s. Capped at 10 URLs.
0052: mirrors useChildStatus — fetch on change, poll every 5 s while
visible, refetch on visibilitychange back. The stop rule is pure and
exported: once every tracked PR's known state is settled (merged/
closed) polling stops for good — a settled PR is never refetched;
unknown (null) keeps polling. URLs are keyed by sorted content so array
identity churn never retriggers.
0052: derivePhase stays pure and untouched, but its PR-open phase now
carries kind: "pr-open" and the session asks the host while it shows:
merged relabels the chip 'PR merged' (ok), closed 'PR closed' (warn),
unknown/open keeps the derived label. The mapping is the pure
applyPRState in lib/plan.ts; the session wires it through usePRStates
with the phase's single PR URL.
0052: PullRequestToolCard watches its PR URL through usePRStates — the
pill reads 'opened' while the host says open (or before a first
answer), 'merged' (success tone) or 'closed' (warning tone) once
settled, and the GitPullRequest icon follows the pill. Everything else
on the card is unchanged; the tool result's own text stays as-written.
0052: the success payload gains state: "open" — what the host said at
creation. No behavior change to the tool; the model's transcript copy
gets one more true field.
getPRState per host against a stubbed fetch (URL/auth, merged/closed/
open mapping, 404 and garbage shapes → null); /api/x/prs against a
temp db + stubbed global fetch (batch keyed by exact URL, per-host API
shapes, host errors and unknown hosts → null, the 10-URL cap);
applyPRState's relabeling; and the pure poll-stop rule (the hook itself
is browser-bound — fetch + visibility — so its logic lives in
pollSettled, tested here).
fix(prs): review fixes — listener always removed, empty-url early return, owner/repo path-encoded
All checks were successful
deploy / checks (pull_request) Successful in 43s
deploy / deploy (pull_request) Has been skipped
8a8985bbfc
chad approved these changes 2026-10-19 10:08:56 -04:00
chad merged commit 57bd8ac26e into main 2026-10-19 10:09:00 -04:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chad/sweatshop-legacy!132
No description provided.