feat(progress): merge-wait heartbeat with backoff and cold-start status - #12
Conversation
Greptile SummaryThis PR adds liveness feedback to
Confidence Score: 5/5Safe to merge — the change is purely additive (optional callback, optional stream field, new config key with default) and touches no mutation paths. All interface changes are backward-compatible: No files require special attention. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant S as Stack.landOne
participant P as Progress (stderr)
participant GH as CodeHost.wait (GitHub/GitLab)
participant API as gh / glab CLI
S->>P: heartbeat("reading open changes…")
S->>API: landTarget (fetch + refs + pulls)
S->>P: heartbeat("inspecting N branches…")
S->>P: "wait("waiting for #N to merge") [stdout]"
S->>GH: wait(pr, onPoll)
loop Poll until merged
GH->>API: pr view --json state,mergedAt
API-->>GH: "{state, mergedAt}"
alt mergedAt set
GH-->>S: return (done)
else "state != OPEN"
GH-->>S: fail (closed without merging)
else still OPEN
GH->>GH: sleep(interval)
GH->>GH: "elapsed += interval"
GH->>S: onPoll(elapsed)
S->>P: "heartbeat("waiting for #N to merge (Xs)") [stderr]"
GH->>GH: "interval = min(interval×2, maxInterval)"
end
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant S as Stack.landOne
participant P as Progress (stderr)
participant GH as CodeHost.wait (GitHub/GitLab)
participant API as gh / glab CLI
S->>P: heartbeat("reading open changes…")
S->>API: landTarget (fetch + refs + pulls)
S->>P: heartbeat("inspecting N branches…")
S->>P: "wait("waiting for #N to merge") [stdout]"
S->>GH: wait(pr, onPoll)
loop Poll until merged
GH->>API: pr view --json state,mergedAt
API-->>GH: "{state, mergedAt}"
alt mergedAt set
GH-->>S: return (done)
else "state != OPEN"
GH-->>S: fail (closed without merging)
else still OPEN
GH->>GH: sleep(interval)
GH->>GH: "elapsed += interval"
GH->>S: onPoll(elapsed)
S->>P: "heartbeat("waiting for #N to merge (Xs)") [stderr]"
GH->>GH: "interval = min(interval×2, maxInterval)"
end
end
Reviews (2): Last reviewed commit: "feat(progress): cold-start read-phase st..." | Re-trigger Greptile |
merge --auto could go silent for minutes while polling gh/glab for a merge. Back the poll interval off geometrically (5s floor, doubling to a 30s cap) and emit an elapsed-time heartbeat on every poll so the CLI stays visibly alive. CodeHost.wait now takes an optional onPoll callback instead of taking a Progress dependency directly, keeping the GitHub/GitLab adapters decoupled from progress rendering; Stack.ts (which already owns step/wait progress emission) wires the callback to the existing Wait progress event, tagged to render on stderr so stdout output is unchanged.
sync and merge could go silent for minutes on cold-start PR-list and
per-branch reads with no indication anything was happening. Emit a
couple of coarse, stderr-only status lines ("reading open changes…",
"inspecting N branches…") before those reads in Stack.ts's sync/land
paths. Stdout output, exit codes, and mutation behavior are unchanged;
a bare `stack sync` stays non-mutating.
Adds a changeset for the wait-heartbeat + cold-start progress work.
828ccf5 to
22fa16c
Compare
Problem
Field report (
merge-auto-cascade-papercuts-2026-07-10#6):stack merge --autowas silent for ~4 minutes during cold-start reads and silent whilewaiting for #N to merge(flat 5s poll, one static line). Liveness had to be checked out-of-band withps/gh.Change
CodeHost.waittakes an optionalonPoll(elapsedMillis)callback; GitHub and GitLab poll loops back off 5s → 10s → 20s → 30s (cap via newcodeHostWaitMaxIntervalMillis, floor unchanged).Stack.landOneemits… waiting for #N to merge (45s)per poll.reading open changes…,inspecting N branches…) before the expensive read phases in sync/land.ProgressEventgained an optionalstreamfield; new lines route to stderr viaProgress.live. Default-stream events render exactly as before, preserving the repo's outcome-oriented stdout rule.Seam rationale: the callback keeps GitHub/GitLab adapters decoupled from progress/UI concerns (Stack.ts already owns all progress emission); Memory's narrower
waitsignature remains structurally compatible.Notes for review
Validation
bun run typecheck/bun run lint/bun run format:checkclean; 141 tests pass (138 on base; +3: GitHub/GitLab backoff+heartbeat via TestClock, land wait-heartbeat elapsed assertions).