Drive cloud agents via GitHub PR threads (@claude comments)#15
Merged
Conversation
The remote-control channel into a live cloud session (claude --remote) when there's no web UI: a comment tagging @claude on the task's PR thread wakes the worker and feeds it an instruction. gh.ts wraps `gh pr comment` with pure, testable arg/tag builders (commentArgs, claudeMessage) and an askClaude() that prefixes the @claude tag for callers. PM-Phase: 265
When github-watch sees a task PR go CONFLICTING with main, post one @claude rebase comment via gh so the live cloud session resolves it — no operator in the loop. Idempotency is the crux: rebaseDecision() drives one ask per conflict episode (CONFLICTING asks once, MERGEABLE clears so a later conflict re-asks, UNKNOWN/null is transient), an in-memory set dedupes across ticks, and `initial` events are skipped so a restart never re-spams pre-boot conflicts. Also reuses the shared gh() from gh.ts instead of a local copy. PM-Phase: 266
Replace the in-memory rebase-ask Set with a persisted pull_requests table, so "we already asked @claude to rebase" survives a restart. This closes a real re-spam window: after a restart the in-memory set was empty, so any sig change while a PR stayed CONFLICTING would ask again. The table mixes two kinds of data with deliberately different lifetimes, and the write paths are split to keep them safe together: - observed GitHub state (state/checks/mergeable/…) is a cache, refetched every tick; upsertPrState overwrites these columns freely. - rebaseAskedAt is a ledger of a side-effect we performed; only the ask logic writes it, and the poll upsert never touches it (covered by a pr-store test). taskId is a plain integer, not a FK: a branch like pm/task-166 resolves to 166 whether or not that task row exists locally, and persistence must not throw on the mismatch. GET /cloud-prs now serves from the table, so the Active panel shows last-known PR badges immediately on boot, before the first poll lands. rebaseDecision stays pure and unchanged; the watcher's event/boot semantics are untouched.
checks, mergeable, and state were bare `string`/inline unions. Give them named union types in shared/types.ts (CheckRollupState, MergeableState, PrState), mirroring GitHub's GraphQL enums, and thread them through CloudPr, the pull_requests row (text + $type, like the existing status enums), and the UI badge map (CHECK_BADGE is now an exhaustive Record<CheckRollupState, …>). One closed vocabulary instead of `string`, so a typo or a stale value is a compile error. No migration: $type is a type-only annotation — the columns stay text, which also keeps persistence robust if GitHub ever adds an enum value (stored as-is rather than rejected by a DB constraint).
The durable ledger plus a blanket `initial` skip could leave a stale "asked" flag: if a conflict resolved while the supervisor was down, the boot catch-up saw the PR as MERGEABLE but returned before clearing — so the next genuine conflict was suppressed as "already asked". The in-memory Set never hit this (a restart cleared it); persistence introduced it. Fold `initial` into a pure rebaseAction(mergeable, alreadyAsked, initial): MERGEABLE always clears (even on catch-up, reconciling the ledger), while only the *ask* is suppressed on initial. Replaces rebaseDecision; adds tests for the catch-up cases including the stale-flag regression. Also correct an over-claiming listPrs comment (the query has no ORDER BY).
zmaril
force-pushed
the
claude/cloud-agent-github-control-nh4if3
branch
from
June 28, 2026 15:46
5b0476f to
903b708
Compare
zmaril
marked this pull request as ready for review
June 28, 2026 15:49
This was referenced Jun 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Gives the supervisor a remote-control channel into a live cloud session (
claude --remote) when there's no web UI — by commenting@claudeon the task's PR thread — and uses it to auto-request a rebase when a task PR drifts into conflict withmain.Phases
PM-Phase: 265 — gh helper to comment on a task PR, tagging @claude
src/server/gh.ts: the write side of the@claudechannel. A comment tagging@claudeon a task's PR thread wakes the live cloud worker and feeds it an instruction.gh()(theghCLI spawn wrapper, moved here fromgithub-watch.tsso both sides share it),commentOnPr(), andaskClaude()which leads the comment with the@claudetag.commentArgs()(argv shape, optional--repo) andclaudeMessage()(the@claudeprefix) — so the behaviour is covered without spawning.PM-Phase: 266 — auto-request a rebase on a CONFLICTING PR, idempotently
github-watch.tsnow subscribes topr.opened/pr.updated: when GitHub reports a task PR asCONFLICTING, it posts one@clauderebase comment viagh.rebaseDecision()(pure) drives one ask per conflict episode:CONFLICTING+ not yet asked → ask; already asked → skip (no re-spam)MERGEABLE→ clear, so a later, distinct conflict re-asksUNKNOWN/null→ transient, leave memory untouchedinitial(startup catch-up) events are skipped so a restart never re-spams PRs that were already conflicting before boot; merged/closed PRs are forgotten.Testing
tests/gh.test.ts—commentArgsargv (with/without--repo),claudeMessagetag + trim.tests/github-watch.test.ts—rebaseDecisionacross CONFLICTING/MERGEABLE/UNKNOWN, asked/not-asked.bun run test(full suite) green;biome check srcclean.Server-side only (no UI changes), so no browser render needed.
🤖 Generated with Claude Code
Generated by Claude Code