From 3649b59f0f580c8872a5348ceb0f253abf1326d3 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:38:13 -0700 Subject: [PATCH] feat(review): enrich issue planning with gittensor type-label taxonomy when enrolled When a repo has opted into the gittensor plugin (the fleet-wide LOOPOVER_EXPERIMENTAL_GITTENSOR flag AND the repo's own .loopover.yml experimental.gittensor: true, gittensor-wire.ts's existing shouldEnableGittensorForRepo gate), loopover_plan_repo_issues now merges the repo's configured bug/feature/priority type-label taxonomy into the labels it already prefers reusing. Label names only, never any scoring weight. Zero footprint when the plugin is off (the fleet-wide default): the global flag check short-circuits before loading a manifest or resolved settings at all, matching gittensor-wire.ts's own contract. Uses resolveRepositorySettings (DB overlaid with .loopover.yml), not the raw DB-only getRepositorySettings, since typeLabels/ typeLabelsEnabled are config-as-code only (#6443) -- no DB column backs them anymore. Closes #7428 --- src/services/issue-plan-draft.ts | 36 +++++++++++++- test/unit/issue-plan-draft.test.ts | 78 ++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/services/issue-plan-draft.ts b/src/services/issue-plan-draft.ts index 7c727df1c..3be09af2c 100644 --- a/src/services/issue-plan-draft.ts +++ b/src/services/issue-plan-draft.ts @@ -34,8 +34,12 @@ import { recordAuditEvent, sumAiEstimatedNeuronsSince, } from "../db/repositories"; +import { isGittensorPluginEnabled, shouldEnableGittensorForRepo } from "../review/gittensor-wire"; import { isGlobalAgentPause } from "../settings/agent-execution"; +import { DEFAULT_TYPE_LABELS } from "../settings/pr-type-label"; +import { resolveRepositorySettings } from "../settings/repository-settings"; import { isFocusManifestPublicSafe } from "../signals/focus-manifest"; +import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; import { normalizeIssueTitleKey } from "./contributor-issue-draft"; import type { IssueRecord } from "../types"; import { sha256Hex } from "../utils/crypto"; @@ -224,6 +228,32 @@ function buildIssuePlanUserPrompt(goal: string, existingLabelNames: string[]): s return `Planning goal:\n${goal}\n\nRepository's existing labels (prefer reusing these):\n${labelsLine}`; } +/** + * Gittensor-enrollment-aware enrichment (#7428): when this repo has opted into the gittensor plugin + * (LOOPOVER_EXPERIMENTAL_GITTENSOR AND the repo's own `.loopover.yml experimental.gittensor: true` -- + * shouldEnableGittensorForRepo, gittensor-wire.ts), this repo's configured bug/feature/priority type-label + * taxonomy is merged into the "existing labels" the prompt already prefers reusing (buildIssuePlanUserPrompt) -- + * purely additive, no new prompt section, so a plan generated for a gittensor repo just sees a few more labels + * to choose from. Label NAMES only, never any scoring weight/multiplier -- those never leave the private + * scoring pipeline. + * + * ZERO FOOTPRINT when the plugin is off (the fleet-wide default, matching gittensor-wire.ts's own contract): the + * global flag check short-circuits before this ever loads a manifest or repository settings, so a plain + * self-host instance with no gittensor affiliation makes no additional reads for this at all. + * + * Uses resolveRepositorySettings (DB overlaid with `.loopover.yml`), NOT the raw DB-only getRepositorySettings: + * typeLabels/typeLabelsEnabled are config-as-code only now (no DB column backs them, #6443) -- reading the raw + * DB row would silently ignore a repo's actual configured taxonomy and always see the built-in default. + */ +async function resolveGittensorLabelEnrichment(env: Env, repoFullName: string): Promise { + if (!isGittensorPluginEnabled(env)) return []; + const manifest = await loadRepoFocusManifest(env, repoFullName); + if (!shouldEnableGittensorForRepo(env, manifest.experimental.gittensor)) return []; + const settings = await resolveRepositorySettings(env, repoFullName); + if (settings.typeLabelsEnabled !== true) return []; + return Object.values(settings.typeLabels ?? DEFAULT_TYPE_LABELS); +} + function normalizeIssuePlanCandidate(raw: RawIssuePlanCandidate): { title: string; body: string; labels: string[] } | null { const title = typeof raw.title === "string" ? raw.title.trim().slice(0, MAX_TITLE_CHARS) : ""; const body = typeof raw.body === "string" ? raw.body.trim().slice(0, MAX_BODY_CHARS) : ""; @@ -362,15 +392,17 @@ export async function generateIssuePlanDrafts(env: Env, repoFullName: string, go const trimmedGoal = goal.trim().slice(0, MAX_GOAL_CHARS); if (!trimmedGoal) return empty("no_output"); - const [repo, openIssues, declinedIssues, labels] = await Promise.all([ + const [repo, openIssues, declinedIssues, labels, gittensorTypeLabels] = await Promise.all([ getRepository(env, repoFullName), listOpenIssues(env, repoFullName), listClosedContributorDraftIssues(env, repoFullName, `