Create a standalone Go program that runs on an external server, watches for GitHub issues labeled good-for-ai, spawns headless Claude Code to implement fixes, creates PRs, responds to review comments, and cleans up when PRs are merged/closed.
Single long-running Go binary with a sequential polling loop. No webhooks (avoids inbound connectivity requirements). No goroutine-per-issue (keeps it simple and debuggable). All GitHub interaction via github.com/google/go-github/v84 behind a GitHubClient interface. All Claude interaction via claude -p CLI (headless mode) using Google Vertex AI as the backend.
main.go -- entry point, config, main loop
github.go -- GitHubClient interface + go-github implementation
github_test.go -- tests for GitHub client (httptest mock server)
claude.go -- CommandRunner interface + Claude Code CLI invocation
claude_test.go -- tests for Claude invocation (mock CommandRunner)
worktree.go -- git worktree management
worktree_test.go -- tests for worktree logic
state.go -- JSON file state persistence
state_test.go -- tests for state load/save
prompt.go -- prompt templates
prompt_test.go -- tests for prompt generation
loop.go -- main loop logic (processNewIssues, processReviewComments, cleanupDone)
loop_test.go -- integration tests with all interfaces mocked
go.mod -- standalone module
Standalone go.mod with one external dependency: github.com/google/go-github/v84 for type-safe GitHub API access. All GitHub interaction goes through a GitHubClient interface to enable unit testing with mocks.
STARTUP: parse config → load state from JSON → signal handler
LOOP (every poll-interval):
1. New Issues: list issues with label via go-github
→ skip if already in state
→ git fetch + git worktree add -b ai/issue-N
→ claude -p "implement fix..."
→ extract PR number from branch via go-github
→ save state
2. Review Comments: for each active PR in state
→ list PR comments via go-github (filter ID > last seen)
→ skip bot comments
→ claude -p "address review comments..."
→ update lastCommentID
3. Cleanup: for each active PR
→ get PR state via go-github
→ if MERGED/CLOSED: remove worktree, remove from state
Configstruct with fields:Owner,Repo,Label,CloneDir,StatePath,PollInterval,VertexRegion,VertexProject,LogLevel,DryRun- Config from env vars (
OOMPA_*) with flag overrides, read inmain() slogfor structured loggingsignal.NotifyContextfor graceful shutdown- Constructs concrete implementations of
GitHubClient,CommandRunner,WorktreeManager - Passes them to the loop functions (dependency injection — enables testing)
Agentstruct holds all dependencies:type Agent struct { gh GitHubClient runner CommandRunner worktrees WorktreeManager state *State cfg Config logger *slog.Logger }
(a *Agent) ProcessNewIssues(ctx)→(a *Agent) ProcessReviewComments(ctx)→(a *Agent) CleanupDone(ctx)- Main loop lives in
main.go, calls these methods sequentially
- JSON file at configurable path (default
~/.oompa-state.json) - Types:
type State struct { ActiveIssues map[int]*IssueWork `json:"activeIssues"` } type IssueWork struct { IssueNumber int `json:"issueNumber"` IssueTitle string `json:"issueTitle"` WorktreePath string `json:"worktreePath"` BranchName string `json:"branchName"` PRNumber int `json:"prNumber"` LastCommentID int64 `json:"lastCommentID"` Status string `json:"status"` // implementing, pr-open, failed, done CreatedAt time.Time `json:"createdAt"` }
- Load at startup, save after every mutation
- Defines a
GitHubClientinterface for all GitHub operations (enables mocking in tests) - Concrete implementation uses
github.com/google/go-github/v84with token auth:github.NewClient(nil).WithAuthToken(token) - Interface methods:
type GitHubClient interface { ListLabeledIssues(ctx context.Context, owner, repo, label string) ([]Issue, error) GetPRReviewComments(ctx context.Context, owner, repo string, prNumber int, sinceID int64) ([]ReviewComment, error) GetIssueComments(ctx context.Context, owner, repo string, issueNumber int, sinceID int64) ([]ReviewComment, error) GetPRState(ctx context.Context, owner, repo string, prNumber int) (string, error) AddIssueComment(ctx context.Context, owner, repo string, issueNumber int, body string) error AddLabel(ctx context.Context, owner, repo string, issueNumber int, label string) error RemoveLabel(ctx context.Context, owner, repo string, issueNumber int, label string) error ListPRsByHead(ctx context.Context, owner, repo, branch string) ([]PR, error) }
- go-github mapping:
ListLabeledIssues→client.Issues.ListByRepo()withLabels: []string{label}, State: "open"GetPRReviewComments→client.PullRequests.ListComments(), filter bycomment.ID > sinceIDGetIssueComments→client.Issues.ListComments(), filter bycomment.ID > sinceIDGetPRState→client.PullRequests.Get(), returnpr.GetState()+ checkpr.GetMerged()AddIssueComment→client.Issues.CreateComment()AddLabel→client.Issues.AddLabelsToIssue()RemoveLabel→client.Issues.RemoveLabelForIssue()ListPRsByHead→client.PullRequests.List()withHead: branch
- Defines a
CommandRunnerinterface for executing external commands (enables mocking in tests):type CommandRunner interface { Run(ctx context.Context, workDir string, name string, args ...string) (stdout []byte, stderr []byte, err error) }
ExecRunneris the concrete implementation usingos/execrunClaude(ctx, runner CommandRunner, workDir, prompt string, cfg Config) (ClaudeResult, error):- Invokes:
claude -p --output-format json --dangerously-skip-permissions "prompt" - Sets Vertex env vars on the command:
CLAUDE_CODE_USE_VERTEX=1,CLOUD_ML_REGION,ANTHROPIC_VERTEX_PROJECT_ID - Parses JSON stdout into
ClaudeResult
- Invokes:
- Defines a
WorktreeManagerinterface:type WorktreeManager interface { EnsureRepoCloned(ctx context.Context) error CreateWorktree(ctx context.Context, branchName string) (worktreePath string, err error) RemoveWorktree(ctx context.Context, worktreePath string) error }
GitWorktreeManageris the concrete implementation usingCommandRunner:EnsureRepoCloned—git cloneorgit fetch originCreateWorktree—git worktree add -b ai/issue-N ... origin/mainRemoveWorktree—git worktree remove --force
buildImplementationPrompt(issue Issue) string— tells Claude to:- Read Claude.md for conventions
- Implement the fix, run
make lintandmake test - Commit (no trailing period, 72 char body)
- Create PR via
gh pr createwith/kind,Fixes #N, release-note block
buildReviewResponsePrompt(work IssueWork, comments []ReviewComment) string— tells Claude to:- Address each review comment
- Run lint/test, commit, push
- No force-push
| Env Var | Flag | Default | Description |
|---|---|---|---|
OOMPA_OWNER |
--owner |
openperouter |
GitHub repo owner |
OOMPA_REPO |
--repo |
openperouter |
GitHub repo name |
OOMPA_LABEL |
--label |
good-for-ai |
Issue label to watch |
OOMPA_CLONE_DIR |
--clone-dir |
~/oompa-work |
Clone/worktree directory |
OOMPA_STATE_PATH |
--state-path |
~/.oompa-state.json |
State file |
OOMPA_POLL_INTERVAL |
--poll-interval |
2m |
Poll frequency |
GITHUB_TOKEN |
— | (required) | GitHub PAT for go-github client |
CLOUD_ML_REGION |
--vertex-region |
(required) | GCP Vertex AI region (e.g. us-east5) |
ANTHROPIC_VERTEX_PROJECT_ID |
--vertex-project |
(required) | GCP project ID for Vertex |
The server must have Google Cloud ADC configured (gcloud auth application-default login or a service account key). CLAUDE_CODE_USE_VERTEX=1 is set automatically by the agent when invoking Claude. GITHUB_TOKEN is used by both the go-github client and passed to Claude for gh pr create.
- Claude failure: Add
ai-failedlabel to issue, comment with error, skip. Human removes label + re-addsgood-for-aito retry. - GitHub API failure: Log and skip, retry on next poll cycle.
- Process restart: State file ensures pickup where left off. Worktrees persist on disk.
- Infinite loop prevention: Bot's own comments filtered out. No CI-failure auto-retry.
- Claude never merges — only creates PRs
- No force-push in prompts
--dangerously-skip-permissionsis acceptable since this runs unattended on a trusted server- Uses Vertex AI — billing goes through your GCP project, controlled by GCP IAM and quotas
- Sequential processing avoids race conditions
All external dependencies are behind interfaces (GitHubClient, CommandRunner, WorktreeManager), enabling full unit testing without real GitHub, Claude, or git.
state_test.go — State persistence
TestLoadState_Empty— returns empty state when file doesn't existTestLoadState_Valid— round-trip load/save with active issuesTestLoadState_Corrupt— returns empty state on corrupt JSONTestSaveState_CreatesFile— creates file and parent dirs
prompt_test.go — Prompt generation
TestBuildImplementationPrompt— verifies issue number, title, body are interpolated;/kindandrelease-noteinstructions presentTestBuildReviewResponsePrompt— verifies each comment's file/line/body is included
github_test.go — GitHub client (using net/http/httptest)
- Spin up an
httptest.Serverthat returns canned JSON responses - Point go-github client at the test server:
client, _ := github.NewClient(nil).WithAuthToken("test")+ override BaseURL TestListLabeledIssues— returns issues matching labelTestGetPRReviewComments_FiltersBySinceID— only returns comments with ID > sinceIDTestGetPRState_Merged/_Closed/_OpenTestAddIssueComment— verifies request bodyTestAddLabel/TestRemoveLabelTestListPRsByHead— filters by branch
claude_test.go — Claude invocation (mock CommandRunner)
TestRunClaude_Success— mock runner returns valid JSON, verify parsed resultTestRunClaude_Failure— mock runner returns error, verify error wrappingTestRunClaude_VertexEnvVars— verify the correct env vars are passed to the commandTestRunClaude_InvalidJSON— mock returns non-JSON stdout, verify error
worktree_test.go — Worktree management (mock CommandRunner)
TestCreateWorktree— verifies correct git args and returns expected pathTestRemoveWorktree— verifiesgit worktree remove --forceis calledTestEnsureRepoCloned_AlreadyCloned— callsgit fetchnotgit cloneTestEnsureRepoCloned_Fresh— callsgit clone
loop_test.go — Main loop integration (all interfaces mocked)
TestProcessNewIssues_SkipsAlreadyTracked— issue in state is not re-processedTestProcessNewIssues_HappyPath— creates worktree, runs claude, extracts PR, updates stateTestProcessNewIssues_ClaudeFailure— addsai-failedlabel, comments on issueTestProcessReviewComments_NoNewComments— no action takenTestProcessReviewComments_AddressesHumanComments— runs claude, updates lastCommentIDTestProcessReviewComments_SkipsBotComments— filters out bot's own commentsTestCleanupDone_MergedPR— removes worktree, deletes from stateTestCleanupDone_ClosedPR— removes worktree, deletes from stateTestCleanupDone_OpenPR— no action
type mockGitHubClient struct {
issues []Issue
prComments []ReviewComment
issueComments []ReviewComment
prState string
prs []PR
addedComments []string
addedLabels []string
removedLabels []string
// errors to inject
listIssuesErr error
// ...
}
type mockCommandRunner struct {
calls []commandCall // records all calls for assertions
stdout []byte
stderr []byte
err error
}
type mockWorktreeManager struct {
createdBranches []string
removedPaths []string
cloneCalled bool
createErr error
// ...
}go test ./...- Build:
go build -o oompa . - Dry run:
./oompa --dry-run --poll-interval 10s— logs what it would do without executing - Test with a trivial issue (e.g., "Fix typo in comment") labeled
good-for-ai - Verify PR is created with correct format
- Post a review comment, verify Claude responds and pushes
- Merge the PR, verify worktree cleanup