PRD: GitHub tracker — forge flavor on credentials + GitHub REST client #5

Closed
opened 2026-07-06 22:11:01 +02:00 by dominik.polakovics · 1 comment

This was generated by AI during triage.

PRD for issue #1, synthesized from the 2026-07-06 design interview whose pinned decisions live on that issue. Where this PRD and the issue #1 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 the issue #1 decision record (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 and this PRD.
  • Issue #1 carries the authoritative pinned decision record; this PRD is the narrative form. The implementing agent should read both.
> *This was generated by AI during triage.* PRD for issue #1, synthesized from the 2026-07-06 design interview whose pinned decisions live on that issue. Where this PRD and the issue #1 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 the issue #1 decision record (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 and this PRD. - Issue #1 carries the authoritative pinned decision record; this PRD is the narrative form. The implementing agent should read both.
Author
Owner

This was generated by AI during triage.

Folded into #1 — closing as duplicate.

This PRD should have been appended to the source issue, not spun off as a separate issue. The full PRD narrative now lives as a comment on #1, alongside the authoritative pinned decision record in that issue's body and the Agent Brief. Nothing is lost.

The GitHub-tracker work is tracked entirely on #1; the implementing PR closes #1 only.

> *This was generated by AI during triage.* **Folded into #1 — closing as duplicate.** This PRD should have been *appended to the source issue*, not spun off as a separate issue. The full PRD narrative now lives as a comment on #1, alongside the authoritative pinned decision record in that issue's body and the Agent Brief. Nothing is lost. The GitHub-tracker work is tracked entirely on **#1**; the implementing PR closes #1 only.
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#5
No description provided.