Problem
The orchestrator currently only supports a file-based Local Board tracker (internal/tracker/local.go). This is excellent for integration tests and demos, but it is a toy for real-world usage. To make Contrabass-PI a daily-driver tool, it must be able to poll, claim, and update issues from GitHub Issues.
The IssueTracker interface in internal/types/types.go is already abstracted and clean. Adding a GitHub adapter is a natural plug-in that requires zero architectural changes to the orchestrator, pipeline, or TUI.
Goals
- Implement
IssueTracker for GitHub Issues REST API v3
- Support authentication via
GITHUB_TOKEN (with $ENV_VAR interpolation in config)
- Map GitHub issue states ↔ Contrabass issue states
- Reuse the existing
IssueTracker interface exactly — no interface changes
- Add config validation and YAML parsing for the new tracker type
Non-Goals
- GraphQL API (REST v3 is sufficient and simpler)
- Webhooks / push-based updates (keep polling model)
- Pull Request support (skip PRs when fetching issues)
- GitHub Projects / custom fields
Interface Contract
// Existing interface in internal/types/types.go
type IssueTracker interface {
FetchIssues() ([]Issue, error)
ClaimIssue(id string) (Issue, error)
ReleaseIssue(id string) (Issue, error)
GetIssue(id string) (Issue, error)
UpdateIssueState(id string, state IssueState) (Issue, error)
SetRetryQueue(id string, retryAt time.Time) (Issue, error)
}
State Mapping
| Contrabass State |
GitHub State |
Action |
todo |
open + no assignee |
Poll as candidate |
in_progress |
open + assigned to bot |
Claim via assign |
retry_queued |
open + assigned + label contrabass:retry |
Same as in_progress |
in_review |
open + label contrabass:review |
Post comment with handoff |
done |
closed |
Close issue on approval |
Configuration
Add to WORKFLOW.md:
---
tracker:
type: github
owner: Fatih0234
repo: contrabass-minimal
token: $GITHUB_TOKEN
label_prefix: contrabass
assignee_bot: contrabass-bot # optional: bot username for claiming
---
Add to internal/config/config.go:
type TrackerConfig struct {
Type string `yaml:"type"` // internal | github | linear
BoardDir string `yaml:"board_dir"` // for internal
IssuePrefix string `yaml:"issue_prefix"` // for internal
Owner string `yaml:"owner"` // for github
Repo string `yaml:"repo"` // for github
Token string `yaml:"token"` // for github
LabelPrefix string `yaml:"label_prefix"` // for github
AssigneeBot string `yaml:"assignee_bot"` // for github
}
API Surface
Implement a GitHubTracker in a new file internal/tracker/github.go.
FetchIssues()
GET /repos/{owner}/{repo}/issues?state=open&assignee=none&per_page=100
- Filter out Pull Requests (
pull_request key present)
- Convert each to
types.Issue with State = StateUnclaimed
- Use
issue.Number as the display identifier, "GH-" + number as ID
- Store the GitHub
number in a private field for API calls
ClaimIssue(id)
PATCH /repos/{owner}/{repo}/issues/{number}
Body: {"assignees": ["bot-username"]}
- Returns the updated issue with
State = StateClaimed
- If already assigned, return error (another instance claimed it)
ReleaseIssue(id)
PATCH /repos/{owner}/{repo}/issues/{number}
Body: {"assignees": [], "state": "open"}
- Unassigns the bot and ensures the issue is open
- Returns updated issue with
State = StateUnclaimed
UpdateIssueState(id, state)
- Maps the Contrabass state to GitHub actions:
StateInReview → add label contrabass:review, post a comment with handoff link
StateReleased → PATCH to close the issue
StateRetryQueued → add label contrabass:retry
- Returns updated
types.Issue
SetRetryQueue(id, retryAt)
- Add label
contrabass:retry
- Post a comment:
Retry scheduled at {retryAt}
- Returns updated issue
GetIssue(id)
GET /repos/{owner}/{repo}/issues/{number}
- Returns a single issue by its numeric ID
Edge Cases & Error Handling
| Scenario |
Behavior |
Rate limited (403 + X-RateLimit-Remaining: 0) |
Return wrapped error with retry-after header |
| Issue not found (404) |
Return clear error, do not panic |
| Network timeout |
Retry once with 1s backoff, then surface error |
| Token missing / invalid |
Fail fast at config validation time |
| PR returned in issues list |
Silently skip it |
| Assignee conflict (race) |
Treat as "already claimed", return error so orchestrator skips |
Existing Reference
See docs/references/contrabass/internal/tracker/github.go for the full Contrabass reference implementation. It handles pagination, credential validation, and close-on-release behavior. Use it as a guide, but keep the minimal version simpler.
Files to Create / Modify
- Create:
internal/tracker/github.go
- Create:
internal/tracker/github_test.go (use httptest to mock GitHub API)
- Modify:
internal/config/config.go — add GitHub fields to TrackerConfig
- Modify:
internal/config/config_test.go — add validation tests
- Modify:
cmd/contrabass/main.go — wire tracker.NewGitHub(...) when tracker.type == "github"
Acceptance Criteria
Problem
The orchestrator currently only supports a file-based Local Board tracker (
internal/tracker/local.go). This is excellent for integration tests and demos, but it is a toy for real-world usage. To make Contrabass-PI a daily-driver tool, it must be able to poll, claim, and update issues from GitHub Issues.The
IssueTrackerinterface ininternal/types/types.gois already abstracted and clean. Adding a GitHub adapter is a natural plug-in that requires zero architectural changes to the orchestrator, pipeline, or TUI.Goals
IssueTrackerfor GitHub Issues REST API v3GITHUB_TOKEN(with$ENV_VARinterpolation in config)IssueTrackerinterface exactly — no interface changesNon-Goals
Interface Contract
State Mapping
todoopen+ no assigneein_progressopen+ assigned to botretry_queuedopen+ assigned + labelcontrabass:retryin_reviewopen+ labelcontrabass:reviewdoneclosedConfiguration
Add to
WORKFLOW.md:Add to
internal/config/config.go:API Surface
Implement a
GitHubTrackerin a new fileinternal/tracker/github.go.FetchIssues()pull_requestkey present)types.IssuewithState = StateUnclaimedissue.Numberas the display identifier,"GH-" + numberasIDnumberin a private field for API callsClaimIssue(id)State = StateClaimedReleaseIssue(id)State = StateUnclaimedUpdateIssueState(id, state)StateInReview→ add labelcontrabass:review, post a comment with handoff linkStateReleased→PATCHto close the issueStateRetryQueued→ add labelcontrabass:retrytypes.IssueSetRetryQueue(id, retryAt)contrabass:retryRetry scheduled at {retryAt}GetIssue(id)Edge Cases & Error Handling
X-RateLimit-Remaining: 0)Existing Reference
See
docs/references/contrabass/internal/tracker/github.gofor the full Contrabass reference implementation. It handles pagination, credential validation, and close-on-release behavior. Use it as a guide, but keep the minimal version simpler.Files to Create / Modify
internal/tracker/github.gointernal/tracker/github_test.go(usehttptestto mock GitHub API)internal/config/config.go— add GitHub fields toTrackerConfiginternal/config/config_test.go— add validation testscmd/contrabass/main.go— wiretracker.NewGitHub(...)whentracker.type == "github"Acceptance Criteria
gh issue listshows open, unassigned issues are picked up byFetchIssues()in_reviewadds the review label and posts a commentdonecloses the issuetracker.type == "github"butowner,repo, ortokenare missing--no-tuimode against a real repo with a test issue