Skip to content

Drive cloud agents via GitHub PR threads (@claude comments)#15

Merged
zmaril merged 5 commits into
mainfrom
claude/cloud-agent-github-control-nh4if3
Jun 28, 2026
Merged

Drive cloud agents via GitHub PR threads (@claude comments)#15
zmaril merged 5 commits into
mainfrom
claude/cloud-agent-github-control-nh4if3

Conversation

@zmaril

@zmaril zmaril commented Jun 28, 2026

Copy link
Copy Markdown
Owner

What

Gives the supervisor a remote-control channel into a live cloud session (claude --remote) when there's no web UI — by commenting @claude on the task's PR thread — and uses it to auto-request a rebase when a task PR drifts into conflict with main.

Phases

PM-Phase: 265 — gh helper to comment on a task PR, tagging @claude

  • New src/server/gh.ts: the write side of the @claude channel. A comment tagging @claude on a task's PR thread wakes the live cloud worker and feeds it an instruction.
  • gh() (the gh CLI spawn wrapper, moved here from github-watch.ts so both sides share it), commentOnPr(), and askClaude() which leads the comment with the @claude tag.
  • Pure, unit-testable pieces — commentArgs() (argv shape, optional --repo) and claudeMessage() (the @claude prefix) — so the behaviour is covered without spawning.

PM-Phase: 266 — auto-request a rebase on a CONFLICTING PR, idempotently

  • github-watch.ts now subscribes to pr.opened / pr.updated: when GitHub reports a task PR as CONFLICTING, it posts one @claude rebase comment via gh.
  • Idempotency is the crux — 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-asks
    • UNKNOWN/null → transient, leave memory untouched
  • An in-memory set dedupes across ticks; initial (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.tscommentArgs argv (with/without --repo), claudeMessage tag + trim.
  • tests/github-watch.test.tsrebaseDecision across CONFLICTING/MERGEABLE/UNKNOWN, asked/not-asked.
  • bun run test (full suite) green; biome check src clean.

Server-side only (no UI changes), so no browser render needed.

🤖 Generated with Claude Code


Generated by Claude Code

claude added 5 commits June 28, 2026 15:38
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
zmaril force-pushed the claude/cloud-agent-github-control-nh4if3 branch from 5b0476f to 903b708 Compare June 28, 2026 15:46
@zmaril
zmaril marked this pull request as ready for review June 28, 2026 15:49
@zmaril
zmaril merged commit b8fd4f3 into main Jun 28, 2026
2 checks passed
@zmaril
zmaril deleted the claude/cloud-agent-github-control-nh4if3 branch July 3, 2026 23:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants