Skip to content

feat(tracker): GitHub issue intake (backend + dashboard)#2325

Open
anirudh5harma wants to merge 10 commits into
AgentWrapper:mainfrom
anirudh5harma:feat/github-issue-intake
Open

feat(tracker): GitHub issue intake (backend + dashboard)#2325
anirudh5harma wants to merge 10 commits into
AgentWrapper:mainfrom
anirudh5harma:feat/github-issue-intake

Conversation

@anirudh5harma

@anirudh5harma anirudh5harma commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Registered projects can opt into issue-driven session intake against GitHub. The daemon polls each opted-in project's configured repo for eligible open issues and starts one worker session per issue, preserving the canonical github:<owner>/<repo>#<n> identifier so restarts cannot create duplicates. Existing projects are unaffected until intake is enabled with a label or assignee rule; tracker or spawn failures back off per-project without blocking the daemon or other projects.

Closes #2324. Scoped down from the multi-provider design in #2288/#2289 (now parked as draft) to ship GitHub first; Linear and Jira follow as additive slices behind the same seam.

What this adds

Backend

  • TrackerIntakeConfig on ProjectConfig (enabled, provider — fixed to github for now, repo, labels, assignee), with Validate() rejecting broad/unscoped intake and non-GitHub providers.
  • trackerintake.Observer — a poll loop dispatching through a TrackerResolver interface (SingleTrackerResolver today). Adding Linear/Jira later is additive: a new adapter implementing ports.Tracker plus a resolver-map entry, no changes to poll/eligibility/dedupe/backoff.
  • Rate-limit-aware GitHub tracker adapter. Any poll failure — including a GitHub rate limit — puts that project into a 5-minute backoff instead of retrying in a tight loop, and never blocks other projects' polling.
  • ETag conditional caching in the tracker adapter. Because intake re-polls the same repo+filter every tick, List now caches {ETag, mapped issues} per request path and sends If-None-Match on the next identical query. On 304 Not Modified it returns the cached issues without re-decoding — and a 304 does not consume the primary rate limit — so steady-state polling is nearly free. The HTTP round-trip was extracted so Get/Preflight keep identical behavior; the cache is mutex-guarded and bounded by the intake-enabled repo/filter set (no eviction needed).
  • Daemon wiring constructs the GitHub adapter lazily (only if some project has intake enabled), reusing the existing AO_GITHUB_TOKEN / gh auth token credential precedence. The observer re-reads each project's config every tick, so a project that enables intake — at creation or later — is picked up on the next poll without a daemon restart.
  • CLI: project set-config --tracker-intake --tracker-repo --tracker-label --tracker-assignee.
  • OpenAPI + generated TS schema regenerated for TrackerIntakeConfig.

Frontend

  • Shared IntakeFields component consumed by both the project-settings form and the create-project sheet, so the two surfaces can't drift. A compact variant drives the create sheet's leaner presentation.
  • Project settings Tracker Intake card (verbose): enable toggle; repository auto-detected from the project's git origin and shown as a link (no manual override input for now); assignee rule with inline validation (a label or assignee is required before saving enabled intake); a credential hint. Labels stay wired through the payload/CLI (--tracker-label) so a CLI-set value survives a UI save, but the labels input is hidden for now — assignee is the only eligibility rule editable in the UI.
  • Project creation the new-project sheet exposes the same intake in a compact form: the enable toggle plus an info (ⓘ) icon whose hover tooltip explains what enabling does, then the assignee input. It intentionally drops the descriptive prose, the inline validation text, the credential hint, and the repository row (the git origin isn't known there; the daemon derives it server-side). Submit gating is unchanged — "Create and start" stays disabled until a label/assignee rule is set.
  • Session cards and the inspector surface the canonical github:<native> issue id when a session came from intake.

Design notes

  • Intake is read-only toward GitHub — no comments, no state transitions, no webhook receiver.
  • Credentials stay env-var only, daemon-side; the UI never collects or displays tokens.
  • The provider field exists end-to-end today but only accepts github; the dashboard doesn't render a picker while there's nothing to pick between.
  • Intake defaults to off in the create sheet to keep onboarding lean — it's an explicit opt-in.
  • List pagination is intentionally single-page (per_page capped at 100); this bounds how much a single poll can spawn and is a deliberate v1 limit rather than an oversight.

Validation

  • cd backend && go build ./... && go vet ./...
  • go test -race ./internal/adapters/tracker/github/... ./internal/observe/trackerintake/... ./internal/domain/... ./internal/daemon/... ./internal/cli/... — new tests pass, including the ETag revalidation / 304-cache-hit / per-filter-key / no-ETag-not-cached cases. (go test ./... still hits the pre-existing duplicate-migration-version-20 panic on main, unrelated to this change and already tracked by fix/migrations-0020-collision.)
  • npm run api
  • npm run frontend:typecheck
  • npx vitest run — 41 files, 383 tests, all green (includes coverage for the intake save round-trip, the settings label/assignee guard, and the create-sheet intake gating + compact presentation).
  • Create-sheet flow driven in the web preview (dev:web): the info-icon tooltip surfaces the one-line explanation, enabling reveals the assignee field, submit stays gated until a rule is set, and a valid submit builds the trackerIntake payload into POST /api/v1/projects.

Post-deploy monitoring

For the first project with intake enabled, watch the tracker-intake log lines and confirm one eligible issue creates one worker session carrying the canonical github:<native> id. Healthy: no repeated spawn after a second poll or daemon restart, no polling for projects with intake disabled, and steady-state polls returning 304 (served from cache). Rollback trigger: repeated spawn for the same issue, or intake running without an explicit label/assignee rule; immediate mitigation is unsetting trackerIntake.enabled on the affected project.

Adds an opt-in daemon poll loop that spawns one worker session per
eligible open GitHub issue, keyed by the canonical "github:<native>"
id so restarts cannot double-spawn. Eligibility requires an explicit
label or assignee rule to avoid draining an entire backlog.

Provider dispatch goes through a TrackerResolver interface
(SingleTrackerResolver for now) so Linear/Jira can be added later as
new adapters plus a resolver-map entry, without touching the poll,
eligibility, or backoff logic. Reuses the existing rate-limit-aware
GitHub tracker adapter and backs off per-project on any poll failure
(including rate limits) instead of retrying in a tight loop.

Closes AgentWrapper#2324.
Adds a Tracker Intake card to project settings (enable, repository
override, labels, assignee) with inline validation requiring at
least one label or assignee before intake can be saved. Session
cards and the inspector show the canonical "github:<native>" issue
id when a session was spawned by intake.

Regenerated frontend/src/api/schema.ts against the backend's
TrackerIntakeConfig. The provider is currently fixed to "github";
adding a picker for future providers is additive once the backend
enum grows.

Closes AgentWrapper#2324.
@anirudh5harma anirudh5harma force-pushed the feat/github-issue-intake branch from 09dd175 to ed49b24 Compare July 1, 2026 10:36
anirudh5harma and others added 3 commits July 1, 2026 16:19
startTrackerIntake scanned projects once at daemon startup and skipped
starting the observer loop entirely when none had intake enabled yet.
Poll() already re-reads every project's config on each tick and skips
disabled projects there, so the boot-time gate only broke the common
case: enabling intake on a project after the daemon is already running
silently never got picked up until the next restart, with no error or
log line to explain why.

Always start the loop; the adapter (and its token resolution) stays
lazy regardless, so there's no added cost when intake is unused.

Found while manually verifying issue intake end-to-end against a live
dev daemon: enabled intake via the settings UI, saved successfully,
but no session ever spawned for a matching labeled issue.
The Electron app only registers git projects today, so the daemon
always has a usable git origin to derive owner/repo from when
trackerIntake.repo is unset (trackerRepo() in observer.go, already
covered by every Poll-level test in observer_test.go). Replace the
manual Repository input with a read-only link derived client-side
from the project's own git origin, purely for display — the daemon's
own derivation at poll time is unaffected either way.

Comment out the Labels input; Assignee is the only intake eligibility
rule editable from this form for now. form.intakeLabels/intakeRepo
stay wired into buildIntake so a value set via the CLI round-trips
on a UI save instead of being silently cleared.
anirudh5harma and others added 5 commits July 2, 2026 15:04
Add the GitHub tracker intake controls to the create-project sheet so a
project can opt into issue-driven worker spawning at creation time, not
only later via Settings.

- Extract the shared IntakeFields component (enable toggle, assignee
  input, validation, credential hint) plus buildIntake/intakeNeedsRule/
  deriveGitHubRepo helpers into IntakeFields.tsx, and consume it from
  ProjectSettingsForm so the two surfaces can't drift.
- CreateProjectAgentSheet renders IntakeFields (no repo-preview row,
  since the git origin isn't known there; the daemon derives the repo).
  The selection now carries an optional trackerIntake payload, gated by
  the same "requires a label or assignee" rule the backend enforces.
- Thread trackerIntake through Sidebar's onCreateProject into the
  POST /api/v1/projects config. No backend change: the endpoint already
  accepts a full ProjectConfig and the intake observer picks up newly
  enabled projects on its next tick.

Verified in the web preview: enabling reveals the assignee field and
gates submit until a rule is set; submit builds the intake payload.
Reduce the create sheet's tracker-intake block to the essentials: the
enable toggle plus an info icon whose hover tooltip explains what
enabling does, then the assignee input. Drop the descriptive intro
paragraph, the inline "requires a label or assignee" guard text, and
the credential hint — the sheet stays minimal and submit gating already
communicates the missing rule via the disabled button.

- IntakeFields gains a `compact` prop: hides the prose, folds the
  explanation into an Info tooltip, and drops the trailing help text.
  The tooltip is wrapped in its own TooltipProvider so the component is
  self-contained regardless of ancestor. The verbose settings card is
  unchanged (renders without `compact`).
- Assignee placeholder reworded to "type username or * for any".

Verified in the web preview: the info tooltip surfaces the one-line
description on hover, the assignee placeholder is updated, and no other
copy remains in the create sheet.
The intake observer polls Tracker.List for the same repo+filter every
tick, and each poll did an uncached GET + full JSON decode even when
nothing changed. Add HTTP conditional requests so unchanged polls are
cheap and don't consume the primary rate limit.

- Tracker holds a per-request-path cache of {etag, mapped issues},
  guarded by a mutex. The key space is bounded by intake-enabled
  repo/filter pairs, so no eviction is needed.
- List sends If-None-Match with the cached ETag (verbatim, preserving
  weak validators). On 304 it returns the cached issues without
  re-decoding (GitHub 304s don't count against the primary rate limit);
  a rotated ETag on the 304 is recorded. On 200 it stores the new
  {etag, issues}, or drops a stale entry if the response omits an ETag.
  A 304 without a prior validator falls back to an unconditional refetch.
- Extract the HTTP round-trip into roundTrip(); do() delegates to it so
  Get and Preflight keep byte-for-byte identical behavior. roundTrip
  owns If-None-Match, ETag capture, and 304 handling before
  classifyError.

Tests cover revalidation returning cached issues on 304, ETag rotation
on change, separate cache keys per filter, and no caching when the
response carries no ETag.
Reduce the settings Tracker Intake card to a one-line description
("Auto-spawn worker sessions from matching tracker issues.") and drop
the trailing credential/daemon-restart hint. The compact create sheet is
unaffected (it already renders neither).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracker intake: GitHub-only issue-driven session spawning

1 participant