Tracker: GitHub support — forge flavor on credentials + GitHub REST client #1
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
The MVP (see
docs/agent-brief.md, decisions D10/D16) ships theTrackerinterface with two implementations: Forgejo REST and the built-in tracker. GitHub was explicitly deferred to first fast-follow. The interface,labctl, scoped run tokens, and the AFK engine are all tracker-agnostic by design, so this issue is one new implementation, no refactor.Scope
Implement the
Trackerinterface for GitHub's REST API:github.comremotes map to this tracker when the repo's tracker binding isforge(config override for GitHub Enterprise hosts later, out of scope here).ready-for-agent(label names per repo triage-label mapping).labctl issue view.labctl issue comment.headfilter requiringowner:branch.labctl pr create, body carriesCloses #N.X-RateLimit-*/Retry-After, surface a clear error state to the scheduler instead of counting as run failures.Acceptance
labctlworking end to end.labctl, seed prompts, or the AFK engine.Expanded scope (2026-07-06 design interview)
Scope grew from "GitHub client only" to mechanism + client, one PR. The sections above stay as filed; where they conflict with the decisions below, the decisions win — notably: GHE is now IN scope via the credential mechanism (superseding "config override … out of scope here"), and the Tracker interface has grown the ADR-0014 triage surface since this issue was filed.
Decisions (pinned; write these up as ADR-0015 in the PR)
internal/tracker/githubbehind the pinnedtracker.Trackerseam — including the ADR-0014 surface:CreateIssue,AddIssueLabels/RemoveIssueLabels,Labels,EnsureLabel,CloseIssue, with strictErrUnknownLabelsemantics. GitHub auto-creates unknown labels on some write paths, so the client must pre-resolve names against the repo's labels to keep typos loud. No Tracker interface changes. No webhooks — lab stays polling. Git push auth untouched (separate git credential, already forge-agnostic).internal/tracker/forgejo: onedo()transport (per-request timeout, auth header, bounded error snippet, token never in errors/URL),statusErrorunwrapping totracker.ErrNotFoundon 404. No SDK dependency. GitHub deltas:Authorization: Bearer,X-GitHub-Api-Version: 2022-11-28, merged derived frommerged_at != null, and/issuesfolds PRs into the list — filter client-side on thepull_requestfield.ForgeTokenPayloadgainsforge: "forgejo"|"github". An absent field decodes asforgejo— correct by construction for every existing credential; covered by a decode test. Payloads stay write-only (design §12); no schema migration, no new column.api.github.com; the registry buildsBaseURL = "https://" + value. GHE operators enter their real API root (ghe.example.com/api/v3, orapi.ghe.example.comunder subdomain isolation) — no derivation heuristics, because GHE's URL layouts make any heuristic silently wrong. forgejo flavor unchanged: bare host, registry appends/api/v1, existing credentials untouched. Host shape is validated at create time (400), flavor-aware: a path is allowed for the github flavor only; https-only, no userinfo/query/CRLF for both.Registry.forgeTrackerroutes on the decrypted payload's flavor, notrepos.forge_kind. Detection demotes to (a) the auto-binding hint, unchanged — GHE/other-instance remotes auto-resolve to builtin, the operator picks forge explicitly; and (b) a mismatch tripwire atTrackerFor: detected kind != none and != flavor is a loud config error in theErrForgeCredentialKindstyle. Thereposvcexplicit-forge gate relaxes: a forge credential alone suffices (drop the detected-forge-host requirement). Side effect, intended: arbitrary Forgejo instances (codeberg, a second private instance) unlock too. Known blind spot, accepted: a github.com remote with a github-flavored credential pointing at a GHE host passes both checks and surfaces as a loud 404 on first use.x-ratelimit-remaining: 0orretry-afterbecomestracker.ErrRateLimited(sentinel; message carries the reset time), unwrapped likeErrNotFound. Scheduler/reaper already log-and-skip per tick, so behavior self-heals after reset. No in-client sleeps, no proactive backoff. Budget math: ~480 req/h per active GitHub repo at 30s ticks vs 5000/h per user. Fast-follows if it ever pinches: proactive backoff, ETag conditional requests (304s do not count against the budget).Link: rel="next"(no empty-page probe),per_page=100,maxPages-style loud-truncation guard as in the forgejo client.ReadyIssueskeeps the client-side label recheck for parity.Deliverables beyond code
CONTEXT.md: update the Tracker binding entry (drop "GitHub is fast-follow") and the forge token vocabulary (a credential carries flavor + API origin).repo.web/src/components/PayloadFields.tsx), host prefilled per flavor, overridable.tracker.go/registry.go/detect.goas they are touched.Acceptance (updated)
httptestfake GitHub (auth header, ready-queue params, PR-folding filter, merged_at state derivation, Link pagination, label strictness, rate-limit ->ErrRateLimited, 404 ->ErrNotFound); registry tests (flavor routing, absent-flavor default, mismatch tripwire, GHE base-URL passthrough, relaxed reposvc gate); full-chain composition smoke (forgelive-style). Nothing in CI touches the network.Sequencing
Starts after
feat/agent-triage-surfacelands (it pins the interface surface this client compiles against). Branchfeat/github-trackeroffmain; this one PR closes this issue.Tracker: GitHub REST implementation behind the Tracker interfaceto Tracker: GitHub support — forge flavor on credentials + GitHub REST clientAgent Brief
Category: enhancement
Summary: Implement the GitHub tracker client and the forge-flavor-on-credential mechanism, per the pinned decision record above (the full PRD narrative is a comment on this issue).
Current behavior:
The Tracker seam has two implementations: the Forgejo REST client and the built-in store-backed tracker. The tracker registry rejects a forge-bound repo whose detected forge kind is
githubas unsupported, and the repo service refuses an explicit forge binding unless the remote host was detected as a known forge. The forge token payload carries only host + token; the registry always builds a Forgejo-style API base URL from it. GitHub and GHE repos can therefore only use the built-in tracker, and non-pinned Forgejo instances cannot forge-bind at all.Desired behavior:
A repo hosted on GitHub (github.com or GHE) or on any Forgejo instance, added with a forge tracker binding and a matching forge token, supports the full AFK lifecycle and the complete labctl surface through the Tracker seam. The forge token's payload names its flavor; the decrypted flavor — not host detection — decides which REST client the registry builds. Existing Forgejo credentials keep working unchanged (absent flavor decodes as forgejo).
Key interfaces:
tracker.Tracker— pinned; the new GitHub client implements ALL of it, including the ADR-0014 triage surface (CreateIssue,AddIssueLabels/RemoveIssueLabels,Labels,EnsureLabel,CloseIssue) with strictErrUnknownLabelsemantics (pre-resolve names; GitHub auto-creates labels on some write paths — a typo must stay a loud error). No interface changes.vault.ForgeTokenPayload— gainsforge: "forgejo"|"github"; absent decodes asforgejo. Flavor-aware host validation at create/rotate (github flavor may carry a path — the API origin verbatim; https-only, no userinfo/query/control chars for both). Payloads stay write-only.tracker.Registry— routes on the decrypted payload's flavor; needs a GitHub factory config carrying a verbatim base URL (github.com: built from the prefilled API host; GHE: whatever origin the operator stored). Detected forge kind demotes to a mismatch tripwire: detected ≠ none and ≠ flavor is a loud config error in the style of the existing credential-kind errors.tracker.ErrRateLimited— 403/429 with exhausted quota or retry-after becomes this typed error naming the reset time; no in-client retries or sleeps.ErrNotFound, Link-header pagination with truncation guard,Authorization: Bearer, pinnedX-GitHub-Api-Version, merged state frommerged_at, client-side PR filtering on the issues list).Acceptance criteria:
Out of scope:
Sequencing: branch
feat/github-trackeroff main afterfeat/agent-triage-surfacelands (it pins the interface surface). One PR; it closes this issue.PRD (narrative form), synthesized from the 2026-07-06 design interview whose pinned decisions live in this issue's body above. Where this PRD and that decision record disagree, the decision record wins.
Problem Statement
lab can only drive a repo's real issue tracker when the repo lives on the one pinned Forgejo instance. A GitHub-hosted repo added today auto-binds to the built-in tracker — the maintainer's issues, triage labels, and pull requests on GitHub are invisible to lab, so the AFK lifecycle and the triage/to-issues/to-prd skill workflow run against a parallel shadow tracker instead of where collaborators actually file and read work. GitHub Enterprise repos and Forgejo instances other than the pinned one (codeberg.org, a second private instance) are locked out of forge binding entirely.
Solution
Teach the forge token credential which forge dialect it speaks. A forge token gains a forge flavor (
forgejoorgithub); the operator stores a GitHub PAT with flavorgithuband the API origin as host, binds the repo's tracker toforge, and everything downstream — ready queue, labctl, the ADR-0014 triage surface, PR done-signal, reaper — works unchanged through the Tracker seam via a new GitHub REST client. GitHub Enterprise works by entering the instance's real API root as the host. As an intended side effect, arbitrary Forgejo instances unlock too, because the credential — not host detection — becomes the routing authority.User Stories
ready-for-agentissues are claimed the same way on every binding.Implementation Decisions
All pinned in this issue's decision record above (2026-07-06 design interview); write them up as ADR-0015 in the PR.
Authorization: Bearer, the pinnedX-GitHub-Api-Versionheader, merged state derived frommerged_at, and client-side filtering of pull requests out of the issues list (GitHub folds PRs into it).forgejo|github); an absent field decodes asforgejo, which is correct by construction for every existing credential. Payloads stay write-only; no schema migration, no new column.Testing Decisions
A good test exercises external behavior at a seam — wire shapes, sentinel errors, routing outcomes — never implementation details. Nothing in CI touches the network. Three hermetic layers, mirroring the Forgejo binding's existing coverage (which is the prior art throughout):
merged_at, Link-header pagination termination and truncation guard, strict unknown-label behavior, rate-limit responses mapping to the typed sentinel, 404 mapping to not-found.Manual gate, left open deliberately: one real GitHub repo added via the UI with a PAT and driven through the full AFK lifecycle (claim → run → PR → reap). GHE ships with unit coverage only — there is no instance to test against.
Out of Scope
Further Notes