feat(tracker): GitHub issue intake (backend + dashboard)#2325
Open
anirudh5harma wants to merge 10 commits into
Open
feat(tracker): GitHub issue intake (backend + dashboard)#2325anirudh5harma wants to merge 10 commits into
anirudh5harma wants to merge 10 commits into
Conversation
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.
09dd175 to
ed49b24
Compare
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.
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
TrackerIntakeConfigonProjectConfig(enabled,provider— fixed togithubfor now,repo,labels,assignee), withValidate()rejecting broad/unscoped intake and non-GitHub providers.trackerintake.Observer— a poll loop dispatching through aTrackerResolverinterface (SingleTrackerResolvertoday). Adding Linear/Jira later is additive: a new adapter implementingports.Trackerplus a resolver-map entry, no changes to poll/eligibility/dedupe/backoff.Listnow caches{ETag, mapped issues}per request path and sendsIf-None-Matchon the next identical query. On304 Not Modifiedit returns the cached issues without re-decoding — and a304does not consume the primary rate limit — so steady-state polling is nearly free. The HTTP round-trip was extracted soGet/Preflightkeep identical behavior; the cache is mutex-guarded and bounded by the intake-enabled repo/filter set (no eviction needed).AO_GITHUB_TOKEN/gh auth tokencredential 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.project set-config --tracker-intake --tracker-repo --tracker-label --tracker-assignee.TrackerIntakeConfig.Frontend
IntakeFieldscomponent consumed by both the project-settings form and the create-project sheet, so the two surfaces can't drift. Acompactvariant drives the create sheet's leaner presentation.--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.github:<native>issue id when a session came from intake.Design notes
github; the dashboard doesn't render a picker while there's nothing to pick between.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 onmain, unrelated to this change and already tracked byfix/migrations-0020-collision.)npm run apinpm run frontend:typechecknpx 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).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 thetrackerIntakepayload intoPOST /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 returning304(served from cache). Rollback trigger: repeated spawn for the same issue, or intake running without an explicit label/assignee rule; immediate mitigation is unsettingtrackerIntake.enabledon the affected project.