Tracker: GitHub support — forge flavor on credentials + GitHub REST client #1

Closed
opened 2026-07-05 22:39:07 +02:00 by dominik.polakovics · 2 comments

Context

The MVP (see docs/agent-brief.md, decisions D10/D16) ships the Tracker interface 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 Tracker interface for GitHub's REST API:

  • Repo detection: github.com remotes map to this tracker when the repo's tracker binding is forge (config override for GitHub Enterprise hosts later, out of scope here).
  • Auth: uses a stored forge token credential (GitHub PAT) from the credential vault.
  • Ready queue: list open issues labeled ready-for-agent (label names per repo triage-label mapping).
  • Issue read: title/body/labels/comments for labctl issue view.
  • Comments: create issue comment for labctl issue comment.
  • PR list: by head branch (the AFK done-signal query) — mind GitHub's head filter requiring owner:branch.
  • PR create: title/body/base/head for labctl pr create, body carries Closes #N.
  • Rate-limit handling: respect X-RateLimit-*/Retry-After, surface a clear error state to the scheduler instead of counting as run failures.

Acceptance

  • A GitHub-hosted repo added via the UI with a PAT credential supports the full AFK lifecycle (claim → run → PR → reap) and manual instances with labctl working end to end.
  • Unit tests against a fake GitHub server covering the queue, PR-by-head, create paths, and rate-limit backoff.
  • No changes required in 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)

  1. Tracker-only, full current interface. One new implementation internal/tracker/github behind the pinned tracker.Tracker seam — including the ADR-0014 surface: CreateIssue, AddIssueLabels/RemoveIssueLabels, Labels, EnsureLabel, CloseIssue, with strict ErrUnknownLabel semantics. 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).
  2. Hand-rolled twin package. Structural twin of internal/tracker/forgejo: one do() transport (per-request timeout, auth header, bounded error snippet, token never in errors/URL), statusError unwrapping to tracker.ErrNotFound on 404. No SDK dependency. GitHub deltas: Authorization: Bearer, X-GitHub-Api-Version: 2022-11-28, merged derived from merged_at != null, and /issues folds PRs into the list — filter client-side on the pull_request field.
  3. Forge flavor lives in the credential payload. ForgeTokenPayload gains forge: "forgejo"|"github". An absent field decodes as forgejo — correct by construction for every existing credential; covered by a decode test. Payloads stay write-only (design §12); no schema migration, no new column.
  4. Host field = API origin, verbatim. github flavor: UI prefills api.github.com; the registry builds BaseURL = "https://" + value. GHE operators enter their real API root (ghe.example.com/api/v3, or api.ghe.example.com under 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.
  5. Credential flavor is the routing authority. Registry.forgeTracker routes on the decrypted payload's flavor, not repos.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 at TrackerFor: detected kind != none and != flavor is a loud config error in the ErrForgeCredentialKind style. The reposvc explicit-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.
  6. Rate limits: typed error, no retry. 403/429 with x-ratelimit-remaining: 0 or retry-after becomes tracker.ErrRateLimited (sentinel; message carries the reset time), unwrapped like ErrNotFound. 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).
  7. Pagination. Terminate on absent Link: rel="next" (no empty-page probe), per_page=100, maxPages-style loud-truncation guard as in the forgejo client. ReadyIssues keeps the client-side label recheck for parity.

Deliverables beyond code

  • ADR-0015 ("forge flavor on the credential; GitHub tracker client") committed in the PR, written from the decisions above.
  • CONTEXT.md: update the Tracker binding entry (drop "GitHub is fast-follow") and the forge token vocabulary (a credential carries flavor + API origin).
  • PAT scope guidance in the credential form help text and the ops doc: fine-grained PAT needs Issues RW, Pull requests RW, Metadata R; classic PAT needs repo.
  • UI: flavor selector on the forge_token credential form (web/src/components/PayloadFields.tsx), host prefilled per flavor, overridable.
  • Update the "fast-follow #1" breadcrumb comments in tracker.go/registry.go/detect.go as they are touched.

Acceptance (updated)

  • Hermetic, three layers mirroring Forgejo: client wire tests against an httptest fake 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.
  • Manual gate — left open deliberately: one real GitHub repo added via the UI with a PAT, driven through the full AFK lifecycle (claim -> run -> PR -> reap). GHE ships with unit coverage only (no instance to test against).
  • Existing Forgejo credentials and repos keep working unchanged.

Sequencing

Starts after feat/agent-triage-surface lands (it pins the interface surface this client compiles against). Branch feat/github-tracker off main; this one PR closes this issue.

## Context The MVP (see `docs/agent-brief.md`, decisions D10/D16) ships the `Tracker` interface 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 `Tracker` interface for GitHub's REST API: - Repo detection: `github.com` remotes map to this tracker when the repo's tracker binding is `forge` (config override for GitHub Enterprise hosts later, out of scope here). - Auth: uses a stored **forge token** credential (GitHub PAT) from the credential vault. - Ready queue: list open issues labeled `ready-for-agent` (label names per repo triage-label mapping). - Issue read: title/body/labels/comments for `labctl issue view`. - Comments: create issue comment for `labctl issue comment`. - PR list: by head branch (the AFK done-signal query) — mind GitHub's `head` filter requiring `owner:branch`. - PR create: title/body/base/head for `labctl pr create`, body carries `Closes #N`. - Rate-limit handling: respect `X-RateLimit-*`/`Retry-After`, surface a clear error state to the scheduler instead of counting as run failures. ## Acceptance - A GitHub-hosted repo added via the UI with a PAT credential supports the full AFK lifecycle (claim → run → PR → reap) and manual instances with `labctl` working end to end. - Unit tests against a fake GitHub server covering the queue, PR-by-head, create paths, and rate-limit backoff. - No changes required in `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) 1. **Tracker-only, full current interface.** One new implementation `internal/tracker/github` behind the pinned `tracker.Tracker` seam — including the ADR-0014 surface: `CreateIssue`, `AddIssueLabels`/`RemoveIssueLabels`, `Labels`, `EnsureLabel`, `CloseIssue`, with strict `ErrUnknownLabel` semantics. 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). 2. **Hand-rolled twin package.** Structural twin of `internal/tracker/forgejo`: one `do()` transport (per-request timeout, auth header, bounded error snippet, token never in errors/URL), `statusError` unwrapping to `tracker.ErrNotFound` on 404. No SDK dependency. GitHub deltas: `Authorization: Bearer`, `X-GitHub-Api-Version: 2022-11-28`, merged derived from `merged_at != null`, and `/issues` folds PRs into the list — filter client-side on the `pull_request` field. 3. **Forge flavor lives in the credential payload.** `ForgeTokenPayload` gains `forge: "forgejo"|"github"`. An absent field decodes as `forgejo` — correct by construction for every existing credential; covered by a decode test. Payloads stay write-only (design §12); no schema migration, no new column. 4. **Host field = API origin, verbatim.** github flavor: UI prefills `api.github.com`; the registry builds `BaseURL = "https://" + value`. GHE operators enter their real API root (`ghe.example.com/api/v3`, or `api.ghe.example.com` under 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. 5. **Credential flavor is the routing authority.** `Registry.forgeTracker` routes on the decrypted payload's flavor, not `repos.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 at `TrackerFor`: detected kind != none and != flavor is a loud config error in the `ErrForgeCredentialKind` style. The `reposvc` explicit-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. 6. **Rate limits: typed error, no retry.** 403/429 with `x-ratelimit-remaining: 0` or `retry-after` becomes `tracker.ErrRateLimited` (sentinel; message carries the reset time), unwrapped like `ErrNotFound`. 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). 7. **Pagination.** Terminate on absent `Link: rel="next"` (no empty-page probe), `per_page=100`, `maxPages`-style loud-truncation guard as in the forgejo client. `ReadyIssues` keeps the client-side label recheck for parity. ### Deliverables beyond code - **ADR-0015** ("forge flavor on the credential; GitHub tracker client") committed in the PR, written from the decisions above. - `CONTEXT.md`: update the *Tracker binding* entry (drop "GitHub is fast-follow") and the *forge token* vocabulary (a credential carries flavor + API origin). - PAT scope guidance in the credential form help text and the ops doc: fine-grained PAT needs Issues RW, Pull requests RW, Metadata R; classic PAT needs `repo`. - UI: flavor selector on the forge_token credential form (`web/src/components/PayloadFields.tsx`), host prefilled per flavor, overridable. - Update the "fast-follow #1" breadcrumb comments in `tracker.go`/`registry.go`/`detect.go` as they are touched. ### Acceptance (updated) - Hermetic, three layers mirroring Forgejo: client wire tests against an `httptest` fake 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. - **Manual gate — left open deliberately**: one real GitHub repo added via the UI with a PAT, driven through the full AFK lifecycle (claim -> run -> PR -> reap). GHE ships with unit coverage only (no instance to test against). - Existing Forgejo credentials and repos keep working unchanged. ### Sequencing Starts after `feat/agent-triage-surface` lands (it pins the interface surface this client compiles against). Branch `feat/github-tracker` off `main`; this one PR closes this issue.
dominik.polakovics changed title from Tracker: GitHub REST implementation behind the Tracker interface to Tracker: GitHub support — forge flavor on credentials + GitHub REST client 2026-07-06 22:01:58 +02:00
Author
Owner

This was generated by AI during triage.

Agent 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 github as 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 strict ErrUnknownLabel semantics (pre-resolve names; GitHub auto-creates labels on some write paths — a typo must stay a loud error). No interface changes.
  • vault.ForgeTokenPayload — gains forge: "forgejo"|"github"; absent decodes as forgejo. 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.
  • Repo service explicit-forge gate — relaxes to "a forge_token credential suffices" (drop the detected-forge-host requirement); auto-binding behavior unchanged.
  • New sentinel tracker.ErrRateLimited — 403/429 with exhausted quota or retry-after becomes this typed error naming the reset time; no in-client retries or sleeps.
  • New package: hand-rolled GitHub client as a structural twin of the Forgejo client (single transport, per-request timeout, bounded error snippets, token never in errors/URLs, 404 → ErrNotFound, Link-header pagination with truncation guard, Authorization: Bearer, pinned X-GitHub-Api-Version, merged state from merged_at, client-side PR filtering on the issues list).
  • Credential form payload fields (web UI) — flavor selector; host prefilled per flavor, overridable; PAT scope guidance in help text (fine-grained: Issues RW, Pull requests RW, Metadata R; classic: repo).

Acceptance criteria:

  • Client wire tests against an in-process fake GitHub: auth + API-version headers, ready-queue params, PR-folding filter, merged_at state derivation, Link pagination + truncation guard, strict unknown-label behavior, rate-limit → ErrRateLimited, 404 → ErrNotFound.
  • Registry/payload tests: flavor routing, absent-flavor default (compatibility guarantee), mismatch tripwire, GHE base-URL passthrough verbatim, flavor-aware host validation, relaxed repo-service gate.
  • Full-chain composition smoke (existing forge-composition style) resolving a github-flavored repo end to end.
  • Existing Forgejo credentials and repos work unchanged; nothing in CI touches the network.
  • ADR-0015 committed, written from the pinned decisions above; domain glossary updated (tracker binding drops "GitHub is fast-follow"; forge token gains flavor + API origin); fast-follow breadcrumb comments updated as touched.
  • UI flavor selector with per-flavor host prefill; PAT scope help text; ops doc updated.

Out of scope:

  • Webhooks; proactive backoff / ETag conditional requests; Tracker interface changes; git push credential handling; GitHub App/OAuth auth; GraphQL; GHE URL heuristics; schema migration.
  • The manual gate (one real GitHub repo through claim → run → PR → reap) stays open for the maintainer — GHE ships with unit coverage only.

Sequencing: branch feat/github-tracker off main after feat/agent-triage-surface lands (it pins the interface surface). One PR; it closes this issue.

> *This was generated by AI during triage.* ## Agent 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 `github` as 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 strict `ErrUnknownLabel` semantics (pre-resolve names; GitHub auto-creates labels on some write paths — a typo must stay a loud error). No interface changes. - `vault.ForgeTokenPayload` — gains `forge: "forgejo"|"github"`; absent decodes as `forgejo`. 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. - Repo service explicit-forge gate — relaxes to "a forge_token credential suffices" (drop the detected-forge-host requirement); auto-binding behavior unchanged. - New sentinel `tracker.ErrRateLimited` — 403/429 with exhausted quota or retry-after becomes this typed error naming the reset time; no in-client retries or sleeps. - New package: hand-rolled GitHub client as a structural twin of the Forgejo client (single transport, per-request timeout, bounded error snippets, token never in errors/URLs, 404 → `ErrNotFound`, Link-header pagination with truncation guard, `Authorization: Bearer`, pinned `X-GitHub-Api-Version`, merged state from `merged_at`, client-side PR filtering on the issues list). - Credential form payload fields (web UI) — flavor selector; host prefilled per flavor, overridable; PAT scope guidance in help text (fine-grained: Issues RW, Pull requests RW, Metadata R; classic: repo). **Acceptance criteria:** - [ ] Client wire tests against an in-process fake GitHub: auth + API-version headers, ready-queue params, PR-folding filter, merged_at state derivation, Link pagination + truncation guard, strict unknown-label behavior, rate-limit → ErrRateLimited, 404 → ErrNotFound. - [ ] Registry/payload tests: flavor routing, absent-flavor default (compatibility guarantee), mismatch tripwire, GHE base-URL passthrough verbatim, flavor-aware host validation, relaxed repo-service gate. - [ ] Full-chain composition smoke (existing forge-composition style) resolving a github-flavored repo end to end. - [ ] Existing Forgejo credentials and repos work unchanged; nothing in CI touches the network. - [ ] ADR-0015 committed, written from the pinned decisions above; domain glossary updated (tracker binding drops "GitHub is fast-follow"; forge token gains flavor + API origin); fast-follow breadcrumb comments updated as touched. - [ ] UI flavor selector with per-flavor host prefill; PAT scope help text; ops doc updated. **Out of scope:** - Webhooks; proactive backoff / ETag conditional requests; Tracker interface changes; git push credential handling; GitHub App/OAuth auth; GraphQL; GHE URL heuristics; schema migration. - The manual gate (one real GitHub repo through claim → run → PR → reap) stays open for the maintainer — GHE ships with unit coverage only. **Sequencing:** branch `feat/github-tracker` off main after `feat/agent-triage-surface` lands (it pins the interface surface). One PR; it closes this issue.
Author
Owner

This was generated by AI during triage.

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 (forgejo or github); the operator stores a GitHub PAT with flavor github and the API origin as host, binds the repo's tracker to forge, 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

  1. As a maintainer with a GitHub-hosted repo, I want to add it with a forge tracker binding and a GitHub PAT credential, so that lab drives the repo's real issues instead of a parallel built-in tracker.
  2. As a maintainer, I want the full AFK lifecycle (claim → run → PR → reap) to work on a GitHub-bound repo, so that AFK agents deliver PRs where my collaborators review them.
  3. As a maintainer running the triage skill against a GitHub-bound repo, I want the complete ADR-0014 triage surface (issue create with labels, label add/remove, label list, idempotent label ensure, issue close), so that the triage state machine runs on GitHub issues exactly as it does on Forgejo.
  4. As an operator creating a forge token, I want to pick the forge flavor on the credential form, so that the credential itself names which REST dialect the token speaks.
  5. As an operator with existing Forgejo forge tokens, I want them to keep working with no edits or migration, so that the upgrade is invisible to running deployments.
  6. As a GitHub Enterprise operator, I want to enter my instance's real API root as the credential host, so that GHE works regardless of whether my instance uses path-style or subdomain-isolated API URLs.
  7. As an operator creating a github-flavored credential, I want the host prefilled with the public GitHub API origin but overridable, so that the common case is zero-thought and GHE stays possible.
  8. As an operator, I want the host validated flavor-aware at credential-create time, so that a malformed host is a clear 400 at create instead of an opaque dial error on the first tracker call.
  9. As an operator of a repo on codeberg.org or a second private Forgejo instance, I want a forge credential alone to suffice for an explicit forge binding, so that non-pinned Forgejo hosts unlock without host-detection changes.
  10. As an operator who attaches a credential whose flavor contradicts the repo's detected forge, I want a loud configuration error at tracker resolution, so that the mismatch is diagnosable instead of surfacing as confusing API failures.
  11. As a maintainer, I want a typo'd label name to fail loudly on GitHub too, so that GitHub's auto-create-label behavior on write paths cannot silently mint garbage labels.
  12. As the AFK scheduler or reaper, I want a rate-limited GitHub call surfaced as a typed rate-limit error naming the reset time, so that the tick is skipped and the system self-heals after reset instead of counting run failures.
  13. As an operator, I want the PAT to never appear in logs, error strings, or URLs, so that the credential-hygiene guarantees of the Forgejo client hold identically for GitHub.
  14. As the reaper, I want pull requests matched by head branch across all states on GitHub, so that the merged-PR done-signal and the closed-unmerged failure signal both work.
  15. As a maintainer, I want the ready queue to honor the repo's triage-label mapping on GitHub, so that ready-for-agent issues are claimed the same way on every binding.
  16. As an AFK run on a GitHub-bound repo, I want to file follow-up issues and comments mid-run through labctl, so that findings outside my claimed issue survive the reap.
  17. As an operator, I want PAT scope guidance in the credential form help text and the ops doc, so that I can mint a least-privilege token without reading GitHub docs archaeology.
  18. As a future contributor, I want ADR-0015 to record why flavor lives on the credential and why the host is a verbatim API origin, so that the routing authority and the no-URL-heuristics rule aren't silently re-litigated.
  19. As a future contributor, I want the domain glossary's tracker-binding and forge-token vocabulary updated, so that the docs stop calling GitHub a fast-follow once it ships.

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.

  • Tracker-only, full current interface. One new GitHub implementation behind the pinned Tracker seam, covering the whole interface including the ADR-0014 triage surface with strict unknown-label semantics. Because GitHub auto-creates unknown labels on some write paths, the client pre-resolves label names against the repo's labels so a typo stays a loud error. No Tracker interface changes. No webhooks — lab stays polling. Git push auth untouched.
  • Hand-rolled twin. Structural twin of the Forgejo client: one transport with per-request timeout, auth header, bounded error snippets, token never in errors or URLs, and 404 unwrapping to the tracker's not-found sentinel. No SDK dependency. GitHub dialect deltas: Authorization: Bearer, the pinned X-GitHub-Api-Version header, merged state derived from merged_at, and client-side filtering of pull requests out of the issues list (GitHub folds PRs into it).
  • Forge flavor lives in the credential payload. The forge token payload gains a flavor field (forgejo | github); an absent field decodes as forgejo, which is correct by construction for every existing credential. Payloads stay write-only; no schema migration, no new column.
  • Host = API origin, verbatim. For the github flavor the UI prefills the public GitHub API host and the registry builds the base URL directly from the stored value; GHE operators enter their real API root. No URL-derivation heuristics — GHE's URL layouts make any heuristic silently wrong. The forgejo flavor is unchanged (bare host; registry appends the API path). Create-time validation is flavor-aware: a path is allowed for github only; https-only, no userinfo, query, or control characters for both.
  • Credential flavor is the routing authority. The registry routes on the decrypted payload's flavor, not the repo's detected forge kind. Detection demotes to (a) the auto-binding hint, unchanged, and (b) a mismatch tripwire at tracker resolution — a detected kind that is neither none nor equal to the flavor is a loud config error. The repo service's explicit-forge gate relaxes: a forge credential alone suffices. Accepted blind spot: 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.
  • Rate limits: typed error, no retry. A 403/429 with exhausted-quota or retry-after markers becomes a typed rate-limited sentinel in the tracker vocabulary, message carrying the reset time. No in-client sleeps, no proactive backoff — scheduler and reaper already log-and-skip per tick, so behavior self-heals after reset. Budget headroom: roughly 480 requests/hour per active repo at 30-second ticks against a 5000/hour PAT limit.
  • Pagination. Terminate on the absence of a next-page Link relation, page size 100, loud truncation guard as in the Forgejo client. The ready queue keeps its client-side label recheck for parity.
  • UI. Flavor selector on the forge token credential form; host prefilled per flavor, overridable.
  • Docs. ADR-0015; domain glossary updates (tracker binding entry drops "GitHub is fast-follow", forge token entry gains flavor + API origin); PAT scope guidance (fine-grained: Issues RW, Pull requests RW, Metadata R; classic: repo) in form help text and the ops doc; fast-follow breadcrumb comments updated as touched.

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):

  • Client wire tests against an in-process fake GitHub server: auth and API-version headers, ready-queue query parameters, PR-folding filter on the issues list, merged-state derivation from 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.
  • Registry and payload tests: flavor routing to the right client, absent-flavor decoding as forgejo (the compatibility guarantee), the detected-kind/flavor mismatch tripwire, GHE base-URL passthrough verbatim, flavor-aware host validation, and the relaxed repo-service forge gate.
  • Composition smoke in the style of the existing full-chain forge-composition test, proving the wired system resolves a github-flavored repo end to end.

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

  • Webhooks — lab stays polling.
  • Proactive rate-limit backoff and ETag conditional requests (fast-follows if the budget ever pinches).
  • Tracker interface changes of any kind.
  • Git push credential handling (already forge-agnostic).
  • GitHub App or OAuth auth flows — PAT only.
  • GitHub's GraphQL API.
  • URL-derivation heuristics for GHE hosts.
  • Credential schema migration or new columns.
  • Real-GHE integration testing.

Further Notes

  • Sequencing: starts after the agent-triage-surface change lands — it pins the interface surface this client compiles against. One branch, one PR; the PR closes issue #1.
  • This issue's body carries the authoritative pinned decision record; this comment is its narrative form. The implementing agent should read both.
> *This was generated by AI during triage.* 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** (`forgejo` or `github`); the operator stores a GitHub PAT with flavor `github` and the API origin as host, binds the repo's tracker to `forge`, 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 1. As a maintainer with a GitHub-hosted repo, I want to add it with a forge tracker binding and a GitHub PAT credential, so that lab drives the repo's real issues instead of a parallel built-in tracker. 2. As a maintainer, I want the full AFK lifecycle (claim → run → PR → reap) to work on a GitHub-bound repo, so that AFK agents deliver PRs where my collaborators review them. 3. As a maintainer running the triage skill against a GitHub-bound repo, I want the complete ADR-0014 triage surface (issue create with labels, label add/remove, label list, idempotent label ensure, issue close), so that the triage state machine runs on GitHub issues exactly as it does on Forgejo. 4. As an operator creating a forge token, I want to pick the forge flavor on the credential form, so that the credential itself names which REST dialect the token speaks. 5. As an operator with existing Forgejo forge tokens, I want them to keep working with no edits or migration, so that the upgrade is invisible to running deployments. 6. As a GitHub Enterprise operator, I want to enter my instance's real API root as the credential host, so that GHE works regardless of whether my instance uses path-style or subdomain-isolated API URLs. 7. As an operator creating a github-flavored credential, I want the host prefilled with the public GitHub API origin but overridable, so that the common case is zero-thought and GHE stays possible. 8. As an operator, I want the host validated flavor-aware at credential-create time, so that a malformed host is a clear 400 at create instead of an opaque dial error on the first tracker call. 9. As an operator of a repo on codeberg.org or a second private Forgejo instance, I want a forge credential alone to suffice for an explicit forge binding, so that non-pinned Forgejo hosts unlock without host-detection changes. 10. As an operator who attaches a credential whose flavor contradicts the repo's detected forge, I want a loud configuration error at tracker resolution, so that the mismatch is diagnosable instead of surfacing as confusing API failures. 11. As a maintainer, I want a typo'd label name to fail loudly on GitHub too, so that GitHub's auto-create-label behavior on write paths cannot silently mint garbage labels. 12. As the AFK scheduler or reaper, I want a rate-limited GitHub call surfaced as a typed rate-limit error naming the reset time, so that the tick is skipped and the system self-heals after reset instead of counting run failures. 13. As an operator, I want the PAT to never appear in logs, error strings, or URLs, so that the credential-hygiene guarantees of the Forgejo client hold identically for GitHub. 14. As the reaper, I want pull requests matched by head branch across all states on GitHub, so that the merged-PR done-signal and the closed-unmerged failure signal both work. 15. As a maintainer, I want the ready queue to honor the repo's triage-label mapping on GitHub, so that `ready-for-agent` issues are claimed the same way on every binding. 16. As an AFK run on a GitHub-bound repo, I want to file follow-up issues and comments mid-run through labctl, so that findings outside my claimed issue survive the reap. 17. As an operator, I want PAT scope guidance in the credential form help text and the ops doc, so that I can mint a least-privilege token without reading GitHub docs archaeology. 18. As a future contributor, I want ADR-0015 to record why flavor lives on the credential and why the host is a verbatim API origin, so that the routing authority and the no-URL-heuristics rule aren't silently re-litigated. 19. As a future contributor, I want the domain glossary's tracker-binding and forge-token vocabulary updated, so that the docs stop calling GitHub a fast-follow once it ships. ## 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. - **Tracker-only, full current interface.** One new GitHub implementation behind the pinned Tracker seam, covering the whole interface including the ADR-0014 triage surface with strict unknown-label semantics. Because GitHub auto-creates unknown labels on some write paths, the client pre-resolves label names against the repo's labels so a typo stays a loud error. No Tracker interface changes. No webhooks — lab stays polling. Git push auth untouched. - **Hand-rolled twin.** Structural twin of the Forgejo client: one transport with per-request timeout, auth header, bounded error snippets, token never in errors or URLs, and 404 unwrapping to the tracker's not-found sentinel. No SDK dependency. GitHub dialect deltas: `Authorization: Bearer`, the pinned `X-GitHub-Api-Version` header, merged state derived from `merged_at`, and client-side filtering of pull requests out of the issues list (GitHub folds PRs into it). - **Forge flavor lives in the credential payload.** The forge token payload gains a flavor field (`forgejo` | `github`); an absent field decodes as `forgejo`, which is correct by construction for every existing credential. Payloads stay write-only; no schema migration, no new column. - **Host = API origin, verbatim.** For the github flavor the UI prefills the public GitHub API host and the registry builds the base URL directly from the stored value; GHE operators enter their real API root. No URL-derivation heuristics — GHE's URL layouts make any heuristic silently wrong. The forgejo flavor is unchanged (bare host; registry appends the API path). Create-time validation is flavor-aware: a path is allowed for github only; https-only, no userinfo, query, or control characters for both. - **Credential flavor is the routing authority.** The registry routes on the decrypted payload's flavor, not the repo's detected forge kind. Detection demotes to (a) the auto-binding hint, unchanged, and (b) a mismatch tripwire at tracker resolution — a detected kind that is neither none nor equal to the flavor is a loud config error. The repo service's explicit-forge gate relaxes: a forge credential alone suffices. Accepted blind spot: 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. - **Rate limits: typed error, no retry.** A 403/429 with exhausted-quota or retry-after markers becomes a typed rate-limited sentinel in the tracker vocabulary, message carrying the reset time. No in-client sleeps, no proactive backoff — scheduler and reaper already log-and-skip per tick, so behavior self-heals after reset. Budget headroom: roughly 480 requests/hour per active repo at 30-second ticks against a 5000/hour PAT limit. - **Pagination.** Terminate on the absence of a next-page Link relation, page size 100, loud truncation guard as in the Forgejo client. The ready queue keeps its client-side label recheck for parity. - **UI.** Flavor selector on the forge token credential form; host prefilled per flavor, overridable. - **Docs.** ADR-0015; domain glossary updates (tracker binding entry drops "GitHub is fast-follow", forge token entry gains flavor + API origin); PAT scope guidance (fine-grained: Issues RW, Pull requests RW, Metadata R; classic: repo) in form help text and the ops doc; fast-follow breadcrumb comments updated as touched. ## 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): - **Client wire tests** against an in-process fake GitHub server: auth and API-version headers, ready-queue query parameters, PR-folding filter on the issues list, merged-state derivation from `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. - **Registry and payload tests**: flavor routing to the right client, absent-flavor decoding as forgejo (the compatibility guarantee), the detected-kind/flavor mismatch tripwire, GHE base-URL passthrough verbatim, flavor-aware host validation, and the relaxed repo-service forge gate. - **Composition smoke** in the style of the existing full-chain forge-composition test, proving the wired system resolves a github-flavored repo end to end. 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 - Webhooks — lab stays polling. - Proactive rate-limit backoff and ETag conditional requests (fast-follows if the budget ever pinches). - Tracker interface changes of any kind. - Git push credential handling (already forge-agnostic). - GitHub App or OAuth auth flows — PAT only. - GitHub's GraphQL API. - URL-derivation heuristics for GHE hosts. - Credential schema migration or new columns. - Real-GHE integration testing. ## Further Notes - Sequencing: starts after the agent-triage-surface change lands — it pins the interface surface this client compiles against. One branch, one PR; the PR closes issue #1. - This issue's body carries the authoritative pinned decision record; this comment is its narrative form. The implementing agent should read both.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#1
No description provided.