// Package github provides GitHub integration for Kandev, including PR monitoring, // review queue management, and CI/check status tracking. package github import ( "encoding/json" "time" ) type AppRegistrationOwnerType string const ( AppRegistrationOwnerUser AppRegistrationOwnerType = "User" AppRegistrationOwnerOrganization AppRegistrationOwnerType = "Organization" ) type DeploymentAppOwnerType = AppRegistrationOwnerType const ( DeploymentAppOwnerUser = AppRegistrationOwnerUser DeploymentAppOwnerOrganization = AppRegistrationOwnerOrganization ) type AppRegistrationSource string const ( AppRegistrationSourceManaged AppRegistrationSource = "managed" AppRegistrationSourceImported AppRegistrationSource = "imported" ) type AppRegistrationVisibility string const ( AppRegistrationVisibilityPrivate AppRegistrationVisibility = "private" AppRegistrationVisibilityPublic AppRegistrationVisibility = "public" ) type AppRegistrationStatus string const ( AppRegistrationStatusActive AppRegistrationStatus = "active" AppRegistrationStatusInvalid AppRegistrationStatus = "invalid" ) type DeploymentAppWebhookStatus string const ( DeploymentAppWebhookUnverified DeploymentAppWebhookStatus = "unverified" DeploymentAppWebhookVerified DeploymentAppWebhookStatus = "verified" DeploymentAppWebhookFailing DeploymentAppWebhookStatus = "failing" ) // AppRegistration is non-secret metadata for one GitHub App in the deployment catalog. type AppRegistration struct { ID string `json:"id" db:"id"` Source AppRegistrationSource `json:"source" db:"source"` DisplayName string `json:"display_name" db:"display_name"` GitHubHost string `json:"github_host" db:"github_host"` AppID int64 `json:"app_id" db:"app_id"` ClientID string `json:"client_id" db:"client_id"` Slug string `json:"slug" db:"slug"` OwnerLogin string `json:"owner_login" db:"owner_login"` OwnerType AppRegistrationOwnerType `json:"owner_type" db:"owner_type"` Visibility AppRegistrationVisibility `json:"visibility" db:"visibility"` PublicBaseURL string `json:"public_base_url" db:"public_base_url"` CreatedForWorkspaceID string `json:"created_for_workspace_id,omitempty" db:"created_for_workspace_id"` CredentialGeneration int64 `json:"credential_generation" db:"credential_generation"` CredentialSecretID string `json:"-" db:"credential_secret_id"` Status AppRegistrationStatus `json:"status" db:"status"` WebhookStatus DeploymentAppWebhookStatus `json:"webhook_status" db:"webhook_status"` LastWebhookAt *time.Time `json:"last_webhook_at,omitempty" db:"last_webhook_at"` LastError string `json:"last_error,omitempty" db:"last_error"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } type DeploymentAppRegistration = AppRegistration // DeploymentAppRegistrationFlow binds one manifest callback to its initiating operator. type DeploymentAppRegistrationFlow struct { StateHash string `json:"-" db:"state_hash"` RegistrationID string `json:"registration_id" db:"registration_id"` WorkspaceID string `json:"workspace_id" db:"workspace_id"` UserID string `json:"-" db:"user_id"` OperatorUserID string `json:"-" db:"operator_user_id"` OwnerType DeploymentAppOwnerType `json:"owner_type" db:"owner_type"` OwnerLogin string `json:"owner_login" db:"owner_login"` DisplayName string `json:"display_name" db:"display_name"` Visibility AppRegistrationVisibility `json:"visibility" db:"visibility"` PublicBaseURL string `json:"public_base_url" db:"public_base_url"` ManifestRevision int `json:"manifest_revision" db:"manifest_revision"` ExpiresAt time.Time `json:"expires_at" db:"expires_at"` ConsumedAt *time.Time `json:"-" db:"consumed_at"` CreatedAt time.Time `json:"created_at" db:"created_at"` } // AppRegistrationImportPreparation reserves one server-generated registration // identity for a short-lived, single-use existing-App import. type AppRegistrationImportPreparation struct { RegistrationID string `json:"registration_id" db:"registration_id"` WorkspaceID string `json:"workspace_id" db:"workspace_id"` UserID string `json:"-" db:"user_id"` PublicBaseURL string `json:"public_base_url" db:"public_base_url"` ExpiresAt time.Time `json:"expires_at" db:"expires_at"` ConsumedAt *time.Time `json:"-" db:"consumed_at"` CreatedAt time.Time `json:"created_at" db:"created_at"` } // ConnectionSource identifies the credential source selected for workspace // automation. Legacy shared auth is migration-only and cannot be selected for // a new workspace. type ConnectionSource string const ( ConnectionSourceLegacyShared ConnectionSource = "legacy_shared" ConnectionSourcePAT ConnectionSource = "pat" ConnectionSourceGHCLI ConnectionSource = "gh_cli" ConnectionSourceGitHubAppInstallation ConnectionSource = "github_app_installation" ConnectionSourceGitHubAppUser ConnectionSource = "github_app_user" ) // ConnectionStatus is the persisted health/lifecycle state for a GitHub // automation or personal connection. type ConnectionStatus string const ( ConnectionStatusActive ConnectionStatus = "active" ConnectionStatusInvalid ConnectionStatus = "invalid" ConnectionStatusSuspended ConnectionStatus = "suspended" ConnectionStatusRevoked ConnectionStatus = "revoked" ) // WorkspaceConnection is the single automation identity selected by a // workspace. Long-lived secret material is stored separately under a // deterministic workspace-scoped secret key. type WorkspaceConnection struct { WorkspaceID string `json:"workspace_id" db:"workspace_id"` Source ConnectionSource `json:"source" db:"source"` GitHubHost string `json:"github_host" db:"github_host"` Login string `json:"login,omitempty" db:"login"` InstallationID *int64 `json:"installation_id,omitempty" db:"installation_id"` InstallationAccountLogin string `json:"installation_account_login,omitempty" db:"installation_account_login"` InstallationAccountType string `json:"installation_account_type,omitempty" db:"installation_account_type"` AppRegistrationID string `json:"app_registration_id,omitempty" db:"app_registration_id"` Status ConnectionStatus `json:"status" db:"status"` CredentialGeneration int64 `json:"credential_generation" db:"credential_generation"` LastError string `json:"last_error,omitempty" db:"last_error"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // UserConnection is an optional personal GitHub identity for one Kandev user // in one workspace. Its OAuth access and refresh tokens live in the encrypted // secret store and are never placed in this row. type UserConnection struct { WorkspaceID string `json:"workspace_id" db:"workspace_id"` UserID string `json:"user_id" db:"user_id"` AppRegistrationID string `json:"app_registration_id" db:"app_registration_id"` GitHubUserID int64 `json:"github_user_id" db:"github_user_id"` Login string `json:"login" db:"login"` Status ConnectionStatus `json:"status" db:"status"` AccessExpiresAt time.Time `json:"access_expires_at" db:"access_expires_at"` RefreshExpiresAt *time.Time `json:"refresh_expires_at,omitempty" db:"refresh_expires_at"` CredentialGeneration int64 `json:"credential_generation" db:"credential_generation"` LastError string `json:"last_error,omitempty" db:"last_error"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } type AuthFlowKind string const ( AuthFlowKindAppInstallation AuthFlowKind = "app_installation" AuthFlowKindPersonal AuthFlowKind = "personal" ) // AuthFlow stores one short-lived, single-use GitHub authorization attempt. // StateHash stores a digest rather than the bearer state returned to a browser. type AuthFlow struct { StateHash string `json:"-" db:"state_hash"` WorkspaceID string `json:"workspace_id" db:"workspace_id"` UserID string `json:"user_id" db:"user_id"` AppRegistrationID string `json:"app_registration_id" db:"app_registration_id"` Kind AuthFlowKind `json:"kind" db:"kind"` PKCEVerifier string `json:"-" db:"pkce_verifier"` ExpectedWorkspaceSource ConnectionSource `json:"-" db:"expected_workspace_source"` ExpectedWorkspaceGeneration int64 `json:"-" db:"expected_workspace_generation"` ExpectedInstallationID *int64 `json:"-" db:"expected_installation_id"` ExpectedWorkspaceAppRegistrationID string `json:"-" db:"expected_workspace_app_registration_id"` ExpectedPersonalGeneration int64 `json:"-" db:"expected_personal_generation"` ExpiresAt time.Time `json:"expires_at" db:"expires_at"` ConsumedAt *time.Time `json:"consumed_at,omitempty" db:"consumed_at"` CreatedAt time.Time `json:"created_at" db:"created_at"` } type WorkspaceConnectionExpectation struct { Source ConnectionSource CredentialGeneration int64 InstallationID *int64 AppRegistrationID string } type WebhookDeliveryStatus string const ( WebhookDeliveryStatusReceived WebhookDeliveryStatus = "received" WebhookDeliveryStatusProcessed WebhookDeliveryStatus = "processed" WebhookDeliveryStatusIgnored WebhookDeliveryStatus = "ignored" WebhookDeliveryStatusFailed WebhookDeliveryStatus = "failed" ) // WebhookDelivery records GitHub delivery IDs so webhook state transitions are // idempotent without persisting payloads or authentication material. type WebhookDelivery struct { AppRegistrationID string `json:"app_registration_id" db:"app_registration_id"` DeliveryID string `json:"delivery_id" db:"delivery_id"` Event string `json:"event" db:"event"` Status WebhookDeliveryStatus `json:"status" db:"status"` Result string `json:"result,omitempty" db:"result"` ReceivedAt time.Time `json:"received_at" db:"received_at"` ProcessedAt *time.Time `json:"processed_at,omitempty" db:"processed_at"` } type WebhookDeliveryClaim struct { Acquired bool Status WebhookDeliveryStatus } // WorkspacePATSecretKey returns the encrypted-secret ID for workspace PAT auth. func WorkspacePATSecretKey(workspaceID string) string { return "github:workspace:" + workspaceID + ":pat" } // UserAccessTokenSecretKey returns the encrypted-secret ID for personal access. func UserAccessTokenSecretKey(workspaceID, userID string) string { return "github:user:" + workspaceID + ":" + userID + ":access" } // UserRefreshTokenSecretKey returns the encrypted-secret ID for personal refresh. func UserRefreshTokenSecretKey(workspaceID, userID string) string { return "github:user:" + workspaceID + ":" + userID + ":refresh" } // TaskCIAutoFixMaxRounds is the server-enforced CI auto-fix loop guard. const TaskCIAutoFixMaxRounds = 10 // PR represents a GitHub Pull Request. type PR struct { ID int64 `json:"id"` NodeID string `json:"node_id"` Number int `json:"number"` Title string `json:"title"` URL string `json:"url"` HTMLURL string `json:"html_url"` State string `json:"state"` // open, closed, merged HeadBranch string `json:"head_branch"` HeadSHA string `json:"head_sha"` BaseBranch string `json:"base_branch"` AuthorLogin string `json:"author_login"` RepoOwner string `json:"repo_owner"` RepoName string `json:"repo_name"` HeadRepoID int64 `json:"head_repo_id,omitempty"` HeadRepoNodeID string `json:"head_repo_node_id,omitempty"` HeadRepoOwner string `json:"head_repo_owner,omitempty"` HeadRepoName string `json:"head_repo_name,omitempty"` HeadRepoCloneURL string `json:"head_repo_clone_url,omitempty"` BaseRepoID int64 `json:"base_repo_id,omitempty"` BaseRepoOwner string `json:"base_repo_owner,omitempty"` BaseRepoName string `json:"base_repo_name,omitempty"` BaseDefaultBranch string `json:"base_default_branch,omitempty"` MaintainerCanModify bool `json:"maintainer_can_modify"` Body string `json:"body"` Draft bool `json:"draft"` Mergeable bool `json:"mergeable"` MergeableState string `json:"mergeable_state"` // clean, blocked, behind, dirty, has_hooks, unstable, draft, unknown, "" Additions int `json:"additions"` Deletions int `json:"deletions"` RequestedReviewers []RequestedReviewer `json:"requested_reviewers"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` MergedAt *time.Time `json:"merged_at,omitempty"` ClosedAt *time.Time `json:"closed_at,omitempty"` } // RequestedReviewer represents a pending reviewer request on a PR. type RequestedReviewer struct { Login string `json:"login"` Type string `json:"type"` // user, team } // PRReview represents a review on a PR. type PRReview struct { ID int64 `json:"id"` Author string `json:"author"` AuthorAvatar string `json:"author_avatar"` State string `json:"state"` // APPROVED, CHANGES_REQUESTED, COMMENTED, PENDING, DISMISSED Body string `json:"body"` CreatedAt time.Time `json:"created_at"` } // PRComment represents a review comment on specific code. type PRComment struct { ID int64 `json:"id"` Author string `json:"author"` AuthorAvatar string `json:"author_avatar"` AuthorIsBot bool `json:"author_is_bot"` Body string `json:"body"` Path string `json:"path"` Line int `json:"line"` Side string `json:"side"` // LEFT, RIGHT CommentType string `json:"comment_type"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` InReplyTo *int64 `json:"in_reply_to,omitempty"` } // CheckRun represents a CI check result. type CheckRun struct { Name string `json:"name"` Source string `json:"source"` // check_run, status_context Status string `json:"status"` // queued, in_progress, completed Conclusion string `json:"conclusion"` // success, failure, neutral, cancelled, timed_out, action_required, skipped HTMLURL string `json:"html_url"` Output string `json:"output"` StartedAt *time.Time `json:"started_at,omitempty"` CompletedAt *time.Time `json:"completed_at,omitempty"` } // PRFeedback aggregates all feedback for a PR (fetched live from GitHub). type PRFeedback struct { PR *PR `json:"pr"` Reviews []PRReview `json:"reviews"` Comments []PRComment `json:"comments"` Checks []CheckRun `json:"checks"` HasIssues bool `json:"has_issues"` } // PRStatus contains lightweight PR state used by the background poller. // Unlike PRFeedback, it skips comments to reduce API calls. type PRStatus struct { PR *PR `json:"pr"` ReviewState string `json:"review_state"` // "approved", "changes_requested", "pending", "" ChecksState string `json:"checks_state"` // "success", "failure", "pending", "" MergeableState string `json:"mergeable_state"` // "clean", "blocked", "behind", "dirty", "has_hooks", "unstable", "draft", "unknown", "" ReviewCount int `json:"review_count"` PendingReviewCount int `json:"pending_review_count"` RequiredReviews *int `json:"required_reviews,omitempty"` // nil when no branch protection rule found ChecksTotal int `json:"checks_total"` ChecksPassing int `json:"checks_passing"` // ChecksPopulated reports whether the sync path actually computed // ChecksTotal / ChecksPassing. The batched GraphQL poller doesn't (it // only carries the rollup state), so SyncTaskPR uses this flag to // decide whether to overwrite the persisted counts. A value of true // with both counts at 0 is a real "no checks" answer; a value of false // means "I didn't look, keep what's there." ChecksPopulated bool `json:"checks_populated,omitempty"` UnresolvedReviewThreads int `json:"unresolved_review_threads"` // UnresolvedReviewThreadsPopulated mirrors ChecksPopulated for the // review-threads field. The REST path (getPRStatus) doesn't fetch // review threads, so it leaves the field at zero; SyncTaskPR uses // this flag to avoid clobbering a non-zero value set by the GraphQL // path during a subsequent REST sync. UnresolvedReviewThreadsPopulated bool `json:"unresolved_review_threads_populated,omitempty"` // ReviewCountsPopulated covers ReviewCount + PendingReviewCount. Both // REST and GraphQL paths compute these now, but historically only the // REST path did, so without the guard a GraphQL poll would clobber // the REST value back to 0 (the popover's "Approved (1)" turning // into "Approved (0)" until a new REST call landed). ReviewCountsPopulated bool `json:"review_counts_populated,omitempty"` } // PRSearchPage is a paginated slice of PR search results, with the total // count reported by the GitHub Search API (capped at 1000). type PRSearchPage struct { PRs []*PR `json:"prs"` TotalCount int `json:"total_count"` Page int `json:"page"` PerPage int `json:"per_page"` } // IssueSearchPage is a paginated slice of Issue search results. type IssueSearchPage struct { Issues []*Issue `json:"issues"` TotalCount int `json:"total_count"` Page int `json:"page"` PerPage int `json:"per_page"` } // PRWatch tracks active PR monitoring (session → PR). RepositoryID identifies // which task repository the watched PR belongs to (multi-repo support; empty // for legacy rows). type PRWatch struct { ID string `json:"id" db:"id"` WorkspaceID string `json:"workspace_id" db:"workspace_id"` SessionID string `json:"session_id" db:"session_id"` TaskID string `json:"task_id" db:"task_id"` RepositoryID string `json:"repository_id,omitempty" db:"repository_id"` Owner string `json:"owner" db:"owner"` Repo string `json:"repo" db:"repo"` PRNumber int `json:"pr_number" db:"pr_number"` Branch string `json:"branch" db:"branch"` LastCheckedAt *time.Time `json:"last_checked_at,omitempty" db:"last_checked_at"` LastCommentAt *time.Time `json:"last_comment_at,omitempty" db:"last_comment_at"` LastCheckStatus string `json:"last_check_status" db:"last_check_status"` LastReviewState string `json:"last_review_state" db:"last_review_state"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // TaskPR associates a PR with a task. RepositoryID identifies which task // repository this PR belongs to (multi-repo tasks can have one PR per repo). // Empty for legacy rows persisted before multi-repo support. type TaskPR struct { ID string `json:"id" db:"id"` WorkspaceID string `json:"workspace_id" db:"workspace_id"` TaskID string `json:"task_id" db:"task_id"` RepositoryID string `json:"repository_id,omitempty" db:"repository_id"` Owner string `json:"owner" db:"owner"` Repo string `json:"repo" db:"repo"` PRNumber int `json:"pr_number" db:"pr_number"` PRURL string `json:"pr_url" db:"pr_url"` PRTitle string `json:"pr_title" db:"pr_title"` HeadBranch string `json:"head_branch" db:"head_branch"` BaseBranch string `json:"base_branch" db:"base_branch"` AuthorLogin string `json:"author_login" db:"author_login"` State string `json:"state" db:"state"` // open, closed, merged ReviewState string `json:"review_state" db:"review_state"` // approved, changes_requested, pending, "" ChecksState string `json:"checks_state" db:"checks_state"` // success, failure, pending, "" MergeableState string `json:"mergeable_state" db:"mergeable_state"` // clean, blocked, behind, dirty, has_hooks, unstable, draft, unknown, "" ReviewCount int `json:"review_count" db:"review_count"` PendingReviewCount int `json:"pending_review_count" db:"pending_review_count"` // RequiredReviews is the branch protection's required_approving_review_count. // Nil when no protection rule exists or the token lacks scope to read it. RequiredReviews *int `json:"required_reviews,omitempty" db:"required_reviews"` CommentCount int `json:"comment_count" db:"comment_count"` UnresolvedReviewThreads int `json:"unresolved_review_threads" db:"unresolved_review_threads"` ChecksTotal int `json:"checks_total" db:"checks_total"` ChecksPassing int `json:"checks_passing" db:"checks_passing"` Additions int `json:"additions" db:"additions"` Deletions int `json:"deletions" db:"deletions"` CreatedAt time.Time `json:"created_at" db:"created_at"` MergedAt *time.Time `json:"merged_at,omitempty" db:"merged_at"` ClosedAt *time.Time `json:"closed_at,omitempty" db:"closed_at"` LastSyncedAt *time.Time `json:"last_synced_at,omitempty" db:"last_synced_at"` DetachedAt *time.Time `json:"-" db:"detached_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // TaskCIOptions stores task-level PR automation preferences. type TaskCIOptions struct { TaskID string `json:"task_id" db:"task_id"` AutoFixEnabled bool `json:"auto_fix_enabled" db:"auto_fix_enabled"` AutoMergeEnabled bool `json:"auto_merge_enabled" db:"auto_merge_enabled"` AutoFixPromptOverride *string `json:"auto_fix_prompt_override,omitempty" db:"auto_fix_prompt_override"` PromptOnReviewRequested bool `json:"prompt_on_review_requested" db:"prompt_on_review_requested"` PromptOnMerged bool `json:"prompt_on_merged" db:"prompt_on_merged"` PromptOnClosed bool `json:"prompt_on_closed" db:"prompt_on_closed"` ReviewReviewerLogin string `json:"review_reviewer_login" db:"review_reviewer_login"` // Lifecycle override columns remain only to read and clear legacy rows during // the additive schema migration. They are not part of the update or API model. ReviewPromptOverride *string `json:"-" db:"review_prompt_override"` MergedPromptOverride *string `json:"-" db:"merged_prompt_override"` ClosedPromptOverride *string `json:"-" db:"closed_prompt_override"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // TaskCIOptionsPatch is a partial update for task CI automation options. type TaskCIOptionsPatch struct { AutoFixEnabled *bool AutoMergeEnabled *bool AutoFixPromptOverride *string PromptOnReviewRequested *bool PromptOnMerged *bool PromptOnClosed *bool ReviewReviewerLogin *string } // HasAny reports whether the patch contains at least one requested field change. func (p TaskCIOptionsPatch) HasAny() bool { return p.AutoFixEnabled != nil || p.AutoMergeEnabled != nil || p.AutoFixPromptOverride != nil || p.PromptOnReviewRequested != nil || p.PromptOnMerged != nil || p.PromptOnClosed != nil || p.ReviewReviewerLogin != nil } // TaskCIOptionsResponse is the HTTP shape for task CI automation options. type TaskCIOptionsResponse struct { TaskID string `json:"task_id"` AutoFixEnabled bool `json:"auto_fix_enabled"` AutoMergeEnabled bool `json:"auto_merge_enabled"` AutoFixPromptOverride *string `json:"auto_fix_prompt_override"` AutoFixMaxRounds int `json:"auto_fix_max_rounds"` EffectiveAutoFixPrompt string `json:"effective_auto_fix_prompt"` UsingDefaultPrompt bool `json:"using_default_prompt"` PromptOnReviewRequested bool `json:"prompt_on_review_requested"` PromptOnMerged bool `json:"prompt_on_merged"` PromptOnClosed bool `json:"prompt_on_closed"` ReviewReviewerLogin string `json:"review_reviewer_login"` EffectiveReviewPrompt string `json:"-"` EffectiveMergedPrompt string `json:"-"` EffectiveClosedPrompt string `json:"-"` UpdatedAt time.Time `json:"updated_at"` PRStates []*TaskCIPRAutomationState `json:"pr_states"` // WorkspaceID is routing metadata for workspace-scoped WebSocket delivery. // It stays JSON-visible because NATS-backed event buses round-trip payloads. WorkspaceID string `json:"workspace_id,omitempty"` } // GetWorkspaceID lets the WebSocket broadcaster route CI-option updates to // the task's owning workspace when the in-memory event retains its typed shape. func (r *TaskCIOptionsResponse) GetWorkspaceID() string { if r == nil { return "" } return r.WorkspaceID } // TaskCIPRAutomationState stores per-PR dedupe and error state for CI automation. type TaskCIPRAutomationState struct { TaskID string `json:"task_id" db:"task_id"` RepositoryID string `json:"repository_id" db:"repository_id"` PRNumber int `json:"pr_number" db:"pr_number"` LastFixSignature string `json:"last_fix_signature" db:"last_fix_signature"` LastFixCheckpointJSON string `json:"last_fix_checkpoint_json" db:"last_fix_checkpoint_json"` LastFixEnqueuedAt *time.Time `json:"last_fix_enqueued_at,omitempty" db:"last_fix_enqueued_at"` LastFixSessionID *string `json:"last_fix_session_id,omitempty" db:"last_fix_session_id"` AutoFixRoundCount int `json:"auto_fix_round_count" db:"auto_fix_round_count"` AutoFixExhaustedAt *time.Time `json:"auto_fix_exhausted_at" db:"auto_fix_exhausted_at"` LastMergeSignature string `json:"last_merge_signature" db:"last_merge_signature"` LastMergeAttemptAt *time.Time `json:"last_merge_attempt_at,omitempty" db:"last_merge_attempt_at"` ReviewRequestInitialized bool `json:"review_request_initialized" db:"review_request_initialized"` LastReviewRequested bool `json:"last_review_requested" db:"last_review_requested"` LastObservedPRState string `json:"last_observed_pr_state" db:"last_observed_pr_state"` LastLifecycleEvent string `json:"last_lifecycle_event" db:"last_lifecycle_event"` LastLifecyclePromptAt *time.Time `json:"last_lifecycle_prompt_at,omitempty" db:"last_lifecycle_prompt_at"` LastLifecycleSessionID *string `json:"last_lifecycle_session_id,omitempty" db:"last_lifecycle_session_id"` LastError *string `json:"last_error,omitempty" db:"last_error"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // TaskCIFixAttempt records an auto-fix prompt attempt for a task PR. type TaskCIFixAttempt struct { TaskID string RepositoryID string PRNumber int Signature string CheckpointJSON string SessionID string EnqueuedAt time.Time IncrementRound bool } // TaskCIMergeAttempt records an auto-merge attempt for a task PR. type TaskCIMergeAttempt struct { TaskID string RepositoryID string PRNumber int Signature string AttemptedAt time.Time } // TaskPRLifecyclePrompt records an accepted lifecycle prompt checkpoint. type TaskPRLifecyclePrompt struct { TaskID string RepositoryID string PRNumber int Event string SessionID string PromptedAt time.Time ReviewRequested bool ObservedState string } // RepoFilter identifies a GitHub repository for review watch filtering. type RepoFilter struct { Owner string `json:"owner"` Name string `json:"name"` } // ReviewScope controls which GitHub search qualifier is used for review-requested PRs. const ( // ReviewScopeUser matches only PRs where the user is explicitly requested // (user-review-requested:@me). ReviewScopeUser = "user" // ReviewScopeUserAndTeams matches PRs requested from the user or any of their teams // (review-requested:@me). This is the default for backwards compatibility. ReviewScopeUserAndTeams = "user_and_teams" ) // CleanupPolicy controls how a review or issue watch handles its auto-created // tasks once the underlying PR / issue reaches a terminal state. const ( // CleanupPolicyAuto deletes the task once the PR/issue is merged or closed // UNLESS the user authored at least one message in the task (the agent's // auto-start prompt does not count). CleanupPolicyAuto = "auto" // CleanupPolicyAlways deletes the task on terminal state regardless of // user interaction. Use when the watch is purely informational and the // user never expects a banner / history for merged PRs. CleanupPolicyAlways = "always" // CleanupPolicyNever disables automatic cleanup. Tasks pile up until the // user invokes manual cleanup or deletes them by hand. CleanupPolicyNever = "never" ) // IsValidCleanupPolicy reports whether s is one of the recognized policies. // Empty string is treated as valid so legacy rows (pre-migration) and zero // values default to "auto" downstream. func IsValidCleanupPolicy(s string) bool { switch s { case "", CleanupPolicyAuto, CleanupPolicyAlways, CleanupPolicyNever: return true } return false } // NormalizeCleanupPolicy maps the empty string to CleanupPolicyAuto. Unknown // values are returned unchanged so the caller can surface a validation error. func NormalizeCleanupPolicy(s string) string { if s == "" { return CleanupPolicyAuto } return s } // ReviewWatch configures periodic polling for PRs needing the user's review. // Repos holds the list of repositories to monitor. An empty list means all repos. type ReviewWatch struct { ID string `json:"id" db:"id"` WorkspaceID string `json:"workspace_id" db:"workspace_id"` WorkflowID string `json:"workflow_id" db:"workflow_id"` WorkflowStepID string `json:"workflow_step_id" db:"workflow_step_id"` Repos []RepoFilter `json:"repos" db:"-"` ReposJSON string `json:"-" db:"repos"` AgentProfileID string `json:"agent_profile_id" db:"agent_profile_id"` ExecutorProfileID string `json:"executor_profile_id" db:"executor_profile_id"` Prompt string `json:"prompt" db:"prompt"` ReviewScope string `json:"review_scope" db:"review_scope"` CustomQuery string `json:"custom_query" db:"custom_query"` TargetLogin string `json:"target_login" db:"target_login"` Enabled bool `json:"enabled" db:"enabled"` PollIntervalSeconds int `json:"poll_interval_seconds" db:"poll_interval_seconds"` CleanupPolicy string `json:"cleanup_policy" db:"cleanup_policy"` LastPolledAt *time.Time `json:"last_polled_at,omitempty" db:"last_polled_at"` // LastError / LastErrorAt are stamped when the dispatch pipeline self- // heals the watcher (e.g. the bound agent profile was soft-deleted). LastError string `json:"last_error,omitempty" db:"last_error"` LastErrorAt *time.Time `json:"last_error_at,omitempty" db:"last_error_at"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // ReviewPRTask records which PRs have already had tasks created (deduplication). type ReviewPRTask struct { ID string `json:"id" db:"id"` ReviewWatchID string `json:"review_watch_id" db:"review_watch_id"` RepoOwner string `json:"repo_owner" db:"repo_owner"` RepoName string `json:"repo_name" db:"repo_name"` PRNumber int `json:"pr_number" db:"pr_number"` PRURL string `json:"pr_url" db:"pr_url"` TaskID string `json:"task_id" db:"task_id"` CreatedAt time.Time `json:"created_at" db:"created_at"` } // GitHubOrg represents a GitHub organization the authenticated user belongs to. type GitHubOrg struct { Login string `json:"login"` AvatarURL string `json:"avatar_url"` } // GitHubRepo represents a GitHub repository (lightweight, for autocomplete). // PushedAt is the timestamp of the latest push, used to sort the // list-accessible-repos result (most-recently-active first). It is a pointer // so `omitempty` actually drops it from JSON when unset — a zero `time.Time` // struct would otherwise serialize as `"0001-01-01T00:00:00Z"`. // // DefaultBranch lets the Remote picker pre-fill the row's branch immediately // on selection (skips the branch-list round-trip for the common case). The // GitHub API returns it on every repo, so it is required. Description is // omitempty because GitHub may return null/empty. type GitHubRepo struct { FullName string `json:"full_name"` Owner string `json:"owner"` Name string `json:"name"` Private bool `json:"private"` DefaultBranch string `json:"default_branch"` Description string `json:"description,omitempty"` PushedAt *time.Time `json:"pushed_at,omitempty"` } // RepoBranch represents a branch in a GitHub repository. type RepoBranch struct { Name string `json:"name"` } // Repo content entry types, as reported by the GitHub contents API's "type" field. const ( RepoContentTypeFile = "file" RepoContentTypeDir = "dir" ) // RepoContentEntry describes one entry returned when listing a repository directory. type RepoContentEntry struct { Name string `json:"name"` Path string `json:"path"` Type string `json:"type"` // "file" or "dir" SHA string `json:"sha"` Size int `json:"size"` } // RepoMergeMethods reports which merge methods a repository allows. Mirrors // the allow_*_merge booleans from GET /repos/{owner}/{repo}. type RepoMergeMethods struct { Merge bool `json:"merge"` Squash bool `json:"squash"` Rebase bool `json:"rebase"` } // GitHubStatus represents GitHub connection status. type GitHubStatus struct { Authenticated bool `json:"authenticated"` Username string `json:"username"` AuthMethod string `json:"auth_method"` // "gh_cli", "pat", "none" TokenConfigured bool `json:"token_configured"` TokenSecretID string `json:"token_secret_id,omitempty"` RequiredScopes []string `json:"required_scopes"` Diagnostics *AuthDiagnostics `json:"diagnostics,omitempty"` RateLimit *GitHubRateLimitInfo `json:"rate_limit,omitempty"` } // GitHubRateLimitInfo bundles known rate-limit snapshots per resource bucket // for surfacing in the UI. type GitHubRateLimitInfo struct { Core *RateSnapshot `json:"core,omitempty"` GraphQL *RateSnapshot `json:"graphql,omitempty"` Search *RateSnapshot `json:"search,omitempty"` } // ConfigureTokenRequest is the request body for configuring a GitHub token. type ConfigureTokenRequest struct { Token string `json:"token" binding:"required"` } // AuthDiagnostics captures the output of gh auth status for troubleshooting. type AuthDiagnostics struct { Command string `json:"command"` Output string `json:"output"` ExitCode int `json:"exit_code"` } // CreateReviewWatchRequest is the request body for creating a review watch. type CreateReviewWatchRequest struct { WorkspaceID string `json:"workspace_id"` WorkflowID string `json:"workflow_id"` WorkflowStepID string `json:"workflow_step_id"` Repos []RepoFilter `json:"repos"` AgentProfileID string `json:"agent_profile_id"` ExecutorProfileID string `json:"executor_profile_id"` Prompt string `json:"prompt"` ReviewScope string `json:"review_scope"` CustomQuery string `json:"custom_query"` PollIntervalSeconds int `json:"poll_interval_seconds"` CleanupPolicy string `json:"cleanup_policy"` } // UpdateReviewWatchRequest is the request body for updating a review watch. type UpdateReviewWatchRequest struct { WorkflowID *string `json:"workflow_id,omitempty"` WorkflowStepID *string `json:"workflow_step_id,omitempty"` Repos *[]RepoFilter `json:"repos,omitempty"` AgentProfileID *string `json:"agent_profile_id,omitempty"` ExecutorProfileID *string `json:"executor_profile_id,omitempty"` Prompt *string `json:"prompt,omitempty"` ReviewScope *string `json:"review_scope,omitempty"` CustomQuery *string `json:"custom_query,omitempty"` Enabled *bool `json:"enabled,omitempty"` PollIntervalSeconds *int `json:"poll_interval_seconds,omitempty"` CleanupPolicy *string `json:"cleanup_policy,omitempty"` } // PRFeedbackEvent is published to the event bus when a PR has new feedback. type PRFeedbackEvent struct { SessionID string `json:"session_id"` TaskID string `json:"task_id"` PRNumber int `json:"pr_number"` Owner string `json:"owner"` Repo string `json:"repo"` NewCheckStatus string `json:"new_check_status"` NewReviewState string `json:"new_review_state"` } // NewReviewPREvent is published when a new PR needing review is found. type NewReviewPREvent struct { ReviewWatchID string `json:"review_watch_id"` WorkspaceID string `json:"workspace_id"` WorkflowID string `json:"workflow_id"` WorkflowStepID string `json:"workflow_step_id"` AgentProfileID string `json:"agent_profile_id"` ExecutorProfileID string `json:"executor_profile_id"` Prompt string `json:"prompt"` PR *PR `json:"pr"` } // PRStatsRequest defines filters for PR stats queries. type PRStatsRequest struct { WorkspaceID string `json:"workspace_id"` StartDate *time.Time `json:"start_date,omitempty"` EndDate *time.Time `json:"end_date,omitempty"` } // PRStats holds aggregated PR analytics. type PRStats struct { TotalPRsCreated int `json:"total_prs_created"` TotalPRsReviewed int `json:"total_prs_reviewed"` TotalComments int `json:"total_comments"` CIPassRate float64 `json:"ci_pass_rate"` ApprovalRate float64 `json:"approval_rate"` AvgTimeToMergeHours float64 `json:"avg_time_to_merge_hours"` PRsByDay []DailyCount `json:"prs_by_day"` } // PRFile represents a file changed in a pull request. type PRFile struct { Filename string `json:"filename"` Status string `json:"status"` // added, removed, modified, renamed, copied, changed, unchanged Additions int `json:"additions"` Deletions int `json:"deletions"` Patch string `json:"patch,omitempty"` OldPath string `json:"old_path,omitempty"` } // PRCommitInfo represents a commit in a pull request. type PRCommitInfo struct { SHA string `json:"sha"` Message string `json:"message"` AuthorLogin string `json:"author_login"` AuthorDate string `json:"author_date"` Additions int `json:"additions"` Deletions int `json:"deletions"` FilesChanged int `json:"files_changed"` StatsAvailable bool `json:"stats_available"` } // PRCommitsResult contains the current provider head and the ancestry data // used to classify a contribution checkout. Complete is true only when the // provider client finished loading the full PR commit list. type PRCommitsResult struct { Commits []PRCommitInfo `json:"commits"` HeadSHA string `json:"head_sha"` Complete bool `json:"complete"` } // PRCommitDetail represents the metadata and changed files for one GitHub // commit. Unlike PRCommitInfo, its statistics come from GitHub's individual // commit endpoint and are always measured. type PRCommitDetail struct { SHA string `json:"sha"` Message string `json:"message"` AuthorLogin string `json:"author_login"` AuthorName string `json:"author_name"` AuthorDate string `json:"author_date"` Additions int `json:"additions"` Deletions int `json:"deletions"` FilesChanged int `json:"files_changed"` Files []PRFile `json:"files"` } // DailyCount holds a date and count for chart data. type DailyCount struct { Date string `json:"date"` Count int `json:"count"` } // --- Issue Watch models --- // Issue represents a GitHub Issue (not a PR). type Issue struct { ID int64 `json:"id"` NodeID string `json:"node_id"` Number int `json:"number"` Title string `json:"title"` Body string `json:"body"` URL string `json:"url"` HTMLURL string `json:"html_url"` State string `json:"state"` // open, closed AuthorLogin string `json:"author_login"` RepoOwner string `json:"repo_owner"` RepoName string `json:"repo_name"` Labels []string `json:"labels"` Assignees []string `json:"assignees"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` ClosedAt *time.Time `json:"closed_at,omitempty"` } // IssueWatch configures periodic polling for GitHub issues matching a query. // Repos holds the list of repositories to monitor. An empty list means all repos. type IssueWatch struct { ID string `json:"id" db:"id"` WorkspaceID string `json:"workspace_id" db:"workspace_id"` WorkflowID string `json:"workflow_id" db:"workflow_id"` WorkflowStepID string `json:"workflow_step_id" db:"workflow_step_id"` Repos []RepoFilter `json:"repos" db:"-"` ReposJSON string `json:"-" db:"repos"` AgentProfileID string `json:"agent_profile_id" db:"agent_profile_id"` ExecutorProfileID string `json:"executor_profile_id" db:"executor_profile_id"` Prompt string `json:"prompt" db:"prompt"` Labels []string `json:"labels" db:"-"` LabelsJSON string `json:"-" db:"labels"` CustomQuery string `json:"custom_query" db:"custom_query"` Enabled bool `json:"enabled" db:"enabled"` PollIntervalSeconds int `json:"poll_interval_seconds" db:"poll_interval_seconds"` CleanupPolicy string `json:"cleanup_policy" db:"cleanup_policy"` LastPolledAt *time.Time `json:"last_polled_at,omitempty" db:"last_polled_at"` // LastError / LastErrorAt are stamped when the dispatch pipeline self- // heals the watcher (e.g. the bound agent profile was soft-deleted). LastError string `json:"last_error,omitempty" db:"last_error"` LastErrorAt *time.Time `json:"last_error_at,omitempty" db:"last_error_at"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // IssueWatchTask records which issues have already had tasks created (deduplication). type IssueWatchTask struct { ID string `json:"id" db:"id"` IssueWatchID string `json:"issue_watch_id" db:"issue_watch_id"` RepoOwner string `json:"repo_owner" db:"repo_owner"` RepoName string `json:"repo_name" db:"repo_name"` IssueNumber int `json:"issue_number" db:"issue_number"` IssueURL string `json:"issue_url" db:"issue_url"` TaskID string `json:"task_id" db:"task_id"` CreatedAt time.Time `json:"created_at" db:"created_at"` } // NewIssueEvent is published when a new issue matching a watch is found. type NewIssueEvent struct { IssueWatchID string `json:"issue_watch_id"` WorkspaceID string `json:"workspace_id"` WorkflowID string `json:"workflow_id"` WorkflowStepID string `json:"workflow_step_id"` AgentProfileID string `json:"agent_profile_id"` ExecutorProfileID string `json:"executor_profile_id"` Prompt string `json:"prompt"` Issue *Issue `json:"issue"` } // CreateIssueWatchRequest is the request body for creating an issue watch. type CreateIssueWatchRequest struct { WorkspaceID string `json:"workspace_id"` WorkflowID string `json:"workflow_id"` WorkflowStepID string `json:"workflow_step_id"` Repos []RepoFilter `json:"repos"` AgentProfileID string `json:"agent_profile_id"` ExecutorProfileID string `json:"executor_profile_id"` Prompt string `json:"prompt"` Labels []string `json:"labels"` CustomQuery string `json:"custom_query"` PollIntervalSeconds int `json:"poll_interval_seconds"` CleanupPolicy string `json:"cleanup_policy"` } // UpdateIssueWatchRequest is the request body for updating an issue watch. type UpdateIssueWatchRequest struct { WorkflowID *string `json:"workflow_id,omitempty"` WorkflowStepID *string `json:"workflow_step_id,omitempty"` Repos *[]RepoFilter `json:"repos,omitempty"` AgentProfileID *string `json:"agent_profile_id,omitempty"` ExecutorProfileID *string `json:"executor_profile_id,omitempty"` Prompt *string `json:"prompt,omitempty"` Labels *[]string `json:"labels,omitempty"` CustomQuery *string `json:"custom_query,omitempty"` Enabled *bool `json:"enabled,omitempty"` PollIntervalSeconds *int `json:"poll_interval_seconds,omitempty"` CleanupPolicy *string `json:"cleanup_policy,omitempty"` } // --- Workspace GitHub scope/settings --- const ( RepoScopeModeAll = "all" RepoScopeModeOrgs = "orgs" RepoScopeModeRepos = "repos" TaskGitCredentialsModeManaged = "managed" TaskGitCredentialsModeExecutor = "executor" ) // WorkspaceSettings stores per-workspace GitHub operational settings. // Authentication is stored separately and is intentionally not copied with // these operational settings. type WorkspaceSettings struct { WorkspaceID string `json:"workspace_id" db:"workspace_id"` TaskGitCredentialsMode string `json:"task_git_credentials_mode" db:"task_git_credentials_mode"` RepoScopeMode string `json:"repo_scope_mode" db:"repo_scope_mode"` RepoScopeOrgs []string `json:"repo_scope_orgs" db:"-"` RepoScopeRepos []RepoFilter `json:"repo_scope_repos" db:"-"` RepoScopeOrgsJSON string `json:"-" db:"repo_scope_orgs"` RepoScopeReposJSON string `json:"-" db:"repo_scope_repos"` SavedPresets json.RawMessage `json:"saved_presets,omitempty" db:"saved_presets"` DefaultQueryPresets json.RawMessage `json:"default_query_presets,omitempty" db:"default_query_presets"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // UpdateWorkspaceSettingsRequest replaces the workspace GitHub scope and/or // preference blobs. Nil blobs leave values unchanged; explicit JSON null clears // default query presets back to built-ins. type UpdateWorkspaceSettingsRequest struct { WorkspaceID string `json:"workspace_id"` TaskGitCredentialsMode *string `json:"task_git_credentials_mode,omitempty"` RepoScopeMode *string `json:"repo_scope_mode,omitempty"` RepoScopeOrgs *[]string `json:"repo_scope_orgs,omitempty"` RepoScopeRepos *[]RepoFilter `json:"repo_scope_repos,omitempty"` SavedPresets *json.RawMessage `json:"saved_presets,omitempty"` DefaultQueryPresets *json.RawMessage `json:"default_query_presets,omitempty"` SavedPresetsSet bool `json:"-"` DefaultQueriesSet bool `json:"-"` } func (r *UpdateWorkspaceSettingsRequest) UnmarshalJSON(data []byte) error { type alias UpdateWorkspaceSettingsRequest var raw map[string]json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { return err } var decoded alias if err := json.Unmarshal(data, &decoded); err != nil { return err } *r = UpdateWorkspaceSettingsRequest(decoded) if value, ok := raw["saved_presets"]; ok { r.SavedPresetsSet = true if string(value) == jsonNullLiteral { r.SavedPresets = nil } else { next := cloneRawMessage(value) r.SavedPresets = &next } } if value, ok := raw["default_query_presets"]; ok { r.DefaultQueriesSet = true if string(value) == jsonNullLiteral { r.DefaultQueryPresets = nil } else { next := cloneRawMessage(value) r.DefaultQueryPresets = &next } } return nil } // --- Action presets (quick-launch prompts on the /github page) --- // ActionPresetKind enumerates the two lists of quick-launch presets. const ( ActionPresetKindPR = "pr" ActionPresetKindIssue = "issue" ) // ActionPreset is a single configurable quick-task launcher entry. // PromptTemplate supports `{{url}}` and `{{title}}` placeholders which are // substituted client-side when the dialog is opened. type ActionPreset struct { ID string `json:"id"` Label string `json:"label"` Hint string `json:"hint"` Icon string `json:"icon"` PromptTemplate string `json:"prompt_template"` } // ActionPresets groups the PR and Issue preset lists for a workspace. type ActionPresets struct { WorkspaceID string `json:"workspace_id"` PR []ActionPreset `json:"pr"` Issue []ActionPreset `json:"issue"` } // UpdateActionPresetsRequest replaces one or both preset lists for a workspace. // Nil fields are left unchanged. type UpdateActionPresetsRequest struct { WorkspaceID string `json:"workspace_id"` PR *[]ActionPreset `json:"pr,omitempty"` Issue *[]ActionPreset `json:"issue,omitempty"` } // DefaultPRActionPresets returns the built-in PR presets used when a workspace // has no stored overrides. func DefaultPRActionPresets() []ActionPreset { return []ActionPreset{ { ID: "review", Label: "Review", Hint: "Read the diff, flag issues", Icon: "eye", PromptTemplate: "Review the pull request at {{url}}. Provide feedback on code quality, correctness, and suggest improvements.", }, { ID: "address_feedback", Label: "Address feedback", Hint: "Apply review comments", Icon: "message", PromptTemplate: "Review the feedback on the pull request at {{url}}. Evaluate each comment critically — apply changes that improve the code, push back on suggestions that are unnecessary or harmful, and explain your reasoning. Push the changes when done.", }, { ID: "fix_ci", Label: "Fix CI", Hint: "Diagnose failing checks", Icon: "tool", PromptTemplate: "Investigate and fix the CI failures and merge conflicts on the pull request at {{url}}. Run the failing checks locally, resolve any conflicts, diagnose issues, and push fixes.", }, } } // DefaultIssueActionPresets returns the built-in Issue presets used when a // workspace has no stored overrides. func DefaultIssueActionPresets() []ActionPreset { return []ActionPreset{ { ID: "implement", Label: "Implement", Hint: "Build and open a PR", Icon: "code", PromptTemplate: `Implement the changes described in the GitHub issue at {{url}} (title: "{{title}}"). Open a pull request when complete.`, }, { ID: "investigate", Label: "Investigate", Hint: "Find the root cause", Icon: "search", PromptTemplate: `Investigate the GitHub issue at {{url}} (title: "{{title}}"). Identify root cause and summarize findings.`, }, { ID: "reproduce", Label: "Reproduce", Hint: "Document repro steps", Icon: "bug", PromptTemplate: `Reproduce the bug described in the GitHub issue at {{url}} (title: "{{title}}"). Document the reproduction steps.`, }, } }