diff --git a/src/api/routes.ts b/src/api/routes.ts index 97017a71bb..dadbdf53a7 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -85,6 +85,7 @@ import { } from "../github/backfill"; import { contributorRepoStatsFromGittensor, fetchGittensorContributorSnapshot } from "../gittensor/api"; import { fetchPublicContributorProfile } from "../github/public"; +import { GITTENSORY_MENTION_COMMAND_CATALOG } from "../github/commands"; import { handleGitHubWebhook } from "../github/webhook"; import { handleMcpRequest } from "../mcp/server"; import { buildOpenApiSpec } from "../openapi/spec"; @@ -1691,6 +1692,14 @@ const APP_COMMANDS = [ description: "Preview the public-safe summary that may be posted to a PR thread.", endpoint: "/v1/app/commands/preview", }, + ...GITTENSORY_MENTION_COMMAND_CATALOG.filter((command) => !["help", "preflight", "blockers", "packet"].includes(command.id)).map((command) => ({ + id: command.id, + command: `@gittensory ${command.id}`, + audience: "public-safe", + boundary: "public", + description: command.description, + endpoint: "GitHub issue comment", + })), ] as const; function authRedirectWithError(env: Env, reason: string): string { diff --git a/src/github/commands.ts b/src/github/commands.ts index de9428ff6c..ca7da7c3af 100644 --- a/src/github/commands.ts +++ b/src/github/commands.ts @@ -4,23 +4,48 @@ import type { GittensorContributorSnapshot, OfficialGittensorMinerDetection } fr import type { AgentActionRecord } from "../types"; import type { GitHubIssuePayload, PullRequestRecord, RepositoryRecord } from "../types"; -export type GittensoryMentionCommandName = "help" | "preflight" | "blockers" | "duplicate-check" | "miner-context" | "next-action"; +export const GITTENSORY_MENTION_COMMAND_CATALOG = [ + { id: "help", title: "Gittensory command help", description: "Show public-safe @gittensory command help." }, + { id: "preflight", title: "Gittensory preflight", description: "Summarize public PR hygiene and validation readiness." }, + { id: "blockers", title: "Gittensory readiness blockers", description: "Explain public-safe readiness blockers." }, + { id: "duplicate-check", title: "Gittensory duplicate & WIP check", description: "Summarize duplicate and in-progress overlap caution." }, + { id: "miner-context", title: "Gittensory miner context", description: "Confirm public Gittensor miner context when available." }, + { id: "next-action", title: "Gittensory next step", description: "Suggest the next public-safe action." }, + { id: "reviewability", title: "Gittensory PR readiness", description: "Summarize maintainer-friendly PR readiness without private review internals." }, + { id: "repo-fit", title: "Gittensory repository fit", description: "Summarize public-safe repository fit signals." }, + { id: "packet", title: "Gittensory public packet", description: "Prepare public-safe PR packet guidance." }, +] as const; + +export type GittensoryMentionCommandName = (typeof GITTENSORY_MENTION_COMMAND_CATALOG)[number]["id"]; export type GittensoryMentionCommand = { name: GittensoryMentionCommandName; raw: string; }; -const COMMANDS = new Set(["help", "preflight", "blockers", "duplicate-check", "miner-context", "next-action"]); +const COMMANDS = new Set(GITTENSORY_MENTION_COMMAND_CATALOG.map((command) => command.id)); const MAINTAINER_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]); -const COMMAND_TITLES: Record = { - help: "Gittensory command help", - preflight: "Gittensory preflight", - blockers: "Gittensory readiness blockers", - "duplicate-check": "Gittensory duplicate & WIP check", - "miner-context": "Gittensory miner context", - "next-action": "Gittensory next step", +const COMMAND_TITLES = Object.fromEntries(GITTENSORY_MENTION_COMMAND_CATALOG.map((command) => [command.id, command.title])) as Record; + +const REFRESH_SECTION_TITLES: Record, string> = { + preflight: "Preflight snapshot refresh", + blockers: "Blocker snapshot refresh", + "duplicate-check": "Duplicate-check snapshot refresh", + "next-action": "Next-action snapshot refresh", + reviewability: "PR readiness snapshot refresh", + "repo-fit": "Repository fit snapshot refresh", + packet: "Public packet snapshot refresh", +}; + +const EMPTY_SECTION_TITLES: Record, string> = { + preflight: "Preflight summary", + blockers: "Readiness blockers", + "duplicate-check": "Duplicate & WIP caution", + "next-action": "Recommended next step", + reviewability: "PR readiness", + "repo-fit": "Repository fit", + packet: "Public packet", }; export function parseGittensoryMentionCommand(body: string | null | undefined): GittensoryMentionCommand | null { @@ -98,6 +123,12 @@ function commandSections( return duplicateCheckSections(bundle); case "next-action": return nextActionSections(bundle); + case "reviewability": + return reviewabilitySections(bundle); + case "repo-fit": + return repoFitSections(bundle); + case "packet": + return packetSections(bundle); } } @@ -111,6 +142,9 @@ function helpSections(): string[] { "- `@gittensory duplicate-check` summarizes duplicate/WIP caution.", "- `@gittensory miner-context` confirms public Gittensor miner context.", "- `@gittensory next-action` gives a public-safe next step.", + "- `@gittensory reviewability` summarizes PR readiness without private review internals.", + "- `@gittensory repo-fit` summarizes repository fit from cached public-safe signals.", + "- `@gittensory packet` prepares public-safe PR packet guidance.", ]; } @@ -215,28 +249,67 @@ function nextActionSections(bundle: AgentRunBundle | null | undefined): string[] ]; } +function reviewabilitySections(bundle: AgentRunBundle | null | undefined): string[] { + if (bundle?.run.status === "needs_snapshot_refresh") { + return refreshSections("reviewability"); + } + const actions = pickActions(bundle, (action) => + action.actionType === "preflight_branch" || action.actionType === "prepare_pr_packet" || /preflight|packet|validation|maintainer/i.test(action.publicSafeSummary), + ); + if (actions.length === 0) { + return emptySections("reviewability"); + } + return [ + "**PR readiness**", + "", + ...actions.slice(0, 3).flatMap((action) => formatActionBullets(action, { includeBlockers: true, includeRerun: true })), + ]; +} + +function repoFitSections(bundle: AgentRunBundle | null | undefined): string[] { + if (bundle?.run.status === "needs_snapshot_refresh") { + return refreshSections("repo-fit"); + } + const actions = pickActions(bundle, (action) => action.actionType === "explain_repo_fit" || action.actionType === "choose_next_work" || /repo fit|repository fit|lane fit/i.test(action.publicSafeSummary)); + if (actions.length === 0) { + return emptySections("repo-fit"); + } + const lines = ["**Repository fit**", ""]; + for (const action of actions.slice(0, 4)) { + if (action.targetRepoFullName) lines.push(`- Target: \`${sanitizePublicComment(action.targetRepoFullName)}\``); + lines.push(`- ${publicBlockerDetail(action.publicSafeSummary)}`); + if (action.rerunWhen) lines.push(`- Rerun when: ${publicBlockerDetail(action.rerunWhen)}`); + } + return dedupeBulletLines(lines); +} + +function packetSections(bundle: AgentRunBundle | null | undefined): string[] { + if (bundle?.run.status === "needs_snapshot_refresh") { + return refreshSections("packet"); + } + const actions = pickActions(bundle, (action) => action.actionType === "prepare_pr_packet" || action.safetyClass === "public_safe" || /packet|public-safe PR/i.test(action.publicSafeSummary)); + if (actions.length === 0) { + return emptySections("packet"); + } + return [ + "**Public packet**", + "", + ...actions.slice(0, 3).flatMap((action) => formatActionBullets(action, { includeBlockers: true, includeRerun: true })), + "", + "- Use this as public PR-thread guidance only; keep private scorer context in MCP or the control panel.", + ]; +} + function refreshSections(command: Exclude): string[] { - const labels: Record = { - preflight: "Preflight snapshot refresh", - blockers: "Blocker snapshot refresh", - "duplicate-check": "Duplicate-check snapshot refresh", - "next-action": "Next-action snapshot refresh", - }; return [ - `**${labels[command]}**`, + `**${REFRESH_SECTION_TITLES[command]}**`, "", "- Gittensory is refreshing the contributor decision snapshot. Try the command again shortly.", ]; } function emptySections(command: Exclude): string[] { - const labels: Record = { - preflight: "Preflight summary", - blockers: "Readiness blockers", - "duplicate-check": "Duplicate & WIP caution", - "next-action": "Recommended next step", - }; - return [`**${labels[command]}**`, "", "- No public-safe context is available from the current cached snapshot."]; + return [`**${EMPTY_SECTION_TITLES[command]}**`, "", "- No public-safe context is available from the current cached snapshot."]; } function pickActions( @@ -308,8 +381,16 @@ function dedupeBulletLines(lines: string[]): string[] { export function sanitizePublicComment(value: string): string { const sanitized = value .replace(/\b(raw trust score|trust score|wallet|hotkey|coldkey|seed phrase|mnemonic)\b/gi, "private context") - .replace(/\b(estimated score|score estimate|reward estimate|payout|farming|reviewability)\b/gi, "private context") + .replace(/\b(public score estimate|estimated score|score estimate|reward estimates?|payout|farming|scoreability|score preview)\b/gi, "private context") + .replace(/\b(private reviewability|reviewability internals?)\b/gi, "private context") .replace(/\b(private ranking|private rankings)\b/gi, "private context") .replace(/\blikely_duplicate\b/gi, "possible overlap with existing work"); - return sanitized.replace(/private context(?:,\s*private context)+/gi, "private context"); + return sanitizeReviewabilityTerm(sanitized).replace(/private context(?:,\s*private context)+/gi, "private context"); +} + +function sanitizeReviewabilityTerm(value: string): string { + return value.replace(/\breviewability\b/gi, (match, offset, fullText: string) => { + const prefix = fullText.slice(Math.max(0, offset - "@gittensory ".length), offset).toLowerCase(); + return prefix.endsWith("@gittensory ") ? match : "private context"; + }); } diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 4c162fda0d..691fb79296 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -52,6 +52,7 @@ import { createOrUpdateCheckRun, getInstallationId } from "../github/app"; import { createOrUpdateAgentCommandComment, createOrUpdatePrIntelligenceComment } from "../github/comments"; import { buildPublicAgentCommandComment, + type GittensoryMentionCommandName, isAuthorizedCommandActor, parseGittensoryMentionCommand, } from "../github/commands"; @@ -61,7 +62,7 @@ import { refreshRegistry } from "../registry/sync"; import { buildIssueAdvisory, buildPullRequestAdvisory } from "../rules/advisory"; import { getOrCreateScoringModelSnapshot, refreshScoringModelSnapshot } from "../scoring/model"; import { buildAndPersistContributorDecisionPack, loadDecisionPackSharedInputs } from "../services/decision-pack"; -import { executeAgentRun, explainBlockersWithAgent, planNextWork } from "../services/agent-orchestrator"; +import { executeAgentRun, explainBlockersWithAgent, planNextWork, preflightBranchWithAgent, preparePrPacketWithAgent } from "../services/agent-orchestrator"; import { loadIssueQualityReportMap } from "../services/issue-quality"; import { buildUpstreamRulesetSnapshot, @@ -95,8 +96,10 @@ import { detectGittensorContributor, } from "../signals/engine"; import { decidePublicSurface } from "../signals/settings-preview"; +import type { LocalBranchAnalysisInput } from "../signals/local-branch"; import type { ContributorEvidenceRecord, GitHubWebhookPayload, JobMessage, JsonValue } from "../types"; -import { errorMessage } from "../utils/json"; +import { sha256Hex } from "../utils/crypto"; +import { errorMessage, nowIso } from "../utils/json"; const OFFICIAL_MINER_DETECTION_TTL_MS = 5 * 60 * 1000; const OFFICIAL_MINER_DETECTION_UNAVAILABLE_TTL_MS = 60 * 1000; @@ -693,6 +696,7 @@ async function maybeProcessGittensoryMentionCommand(env: Env, deliveryId: string const issue = payload.issue; const installationId = getInstallationId(payload); const commenter = payload.comment?.user?.login; + const targetKey = repoFullName && issue ? `${repoFullName}#${issue.number}` : repoFullName; if (!repoFullName || !issue || !installationId || !commenter) { await recordAuditEvent(env, { eventType: "github_app.agent_command_skipped", @@ -702,6 +706,15 @@ async function maybeProcessGittensoryMentionCommand(env: Env, deliveryId: string detail: "missing_repo_issue_installation_or_actor", metadata: { deliveryId, command: command.name }, }); + await recordAgentCommandUsage(env, { + repoFullName, + targetKey, + actor: commenter, + command: command.name, + actorKind: "none", + outcome: "skipped", + detail: "missing_repo_issue_installation_or_actor", + }); return true; } if (payload.comment?.user?.type === "Bot" || /\[bot\]$/i.test(commenter)) { @@ -713,6 +726,7 @@ async function maybeProcessGittensoryMentionCommand(env: Env, deliveryId: string detail: "bot_author", metadata: { deliveryId, command: command.name }, }); + await recordAgentCommandUsage(env, { repoFullName, targetKey, actor: commenter, command: command.name, actorKind: "none", outcome: "skipped", detail: "bot_author" }); return true; } if (!issue.pull_request) { @@ -724,6 +738,7 @@ async function maybeProcessGittensoryMentionCommand(env: Env, deliveryId: string detail: "not_a_pull_request_thread", metadata: { deliveryId, command: command.name }, }); + await recordAgentCommandUsage(env, { repoFullName, targetKey, actor: commenter, command: command.name, actorKind: "none", outcome: "skipped", detail: "not_a_pull_request_thread" }); return true; } @@ -747,21 +762,25 @@ async function maybeProcessGittensoryMentionCommand(env: Env, deliveryId: string detail: authorization.reason, metadata: { deliveryId, command: command.name }, }); + await recordAgentCommandUsage(env, { + repoFullName, + targetKey, + actor: commenter, + command: command.name, + actorKind: authorization.actorKind, + outcome: authorization.reason === "miner_detection_unavailable" ? "error" : "skipped", + detail: authorization.reason, + }); return true; } const login = pullRequestAuthor ?? commenter; - const bundle = - command.name === "help" || command.name === "miner-context" - ? null - : command.name === "blockers" - ? await explainBlockersWithAgent(env, { login, repoFullName, surface: "github_comment" }) - : await planNextWork(env, { - login, - repoFullName, - surface: "github_comment", - objective: `Respond to @gittensory ${command.name} for ${repoFullName}#${issue.number}.`, - }); + const bundle = await buildMentionCommandBundle(env, command.name, { + login, + repoFullName, + issue, + pullRequest: cachedPullRequest, + }); const body = buildPublicAgentCommandComment({ command, repo, @@ -779,9 +798,95 @@ async function maybeProcessGittensoryMentionCommand(env: Env, deliveryId: string outcome: "completed", metadata: { deliveryId, command: command.name, actorKind: authorization.actorKind, runId: bundle?.run.id ?? null }, }); + await recordAgentCommandUsage(env, { + repoFullName, + targetKey, + actor: commenter, + command: command.name, + actorKind: authorization.actorKind, + outcome: "replied", + detail: bundle?.run.status ?? "no_run", + runId: bundle?.run.id ?? null, + }); return true; } +async function buildMentionCommandBundle( + env: Env, + commandName: GittensoryMentionCommandName, + context: { + login: string; + repoFullName: string; + issue: NonNullable; + pullRequest: Awaited>; + }, +) { + if (commandName === "help" || commandName === "miner-context") return null; + if (commandName === "blockers") return explainBlockersWithAgent(env, { login: context.login, repoFullName: context.repoFullName, surface: "github_comment" }); + if (commandName === "preflight" || commandName === "reviewability") return preflightBranchWithAgent(env, buildMentionBranchInput(context), "github_comment"); + if (commandName === "packet") return preparePrPacketWithAgent(env, buildMentionBranchInput(context), "github_comment"); + return planNextWork(env, { + login: context.login, + repoFullName: context.repoFullName, + surface: "github_comment", + objective: `Respond to @gittensory ${commandName} for ${context.repoFullName}#${context.issue.number}.`, + }); +} + +function buildMentionBranchInput(context: { + login: string; + repoFullName: string; + issue: NonNullable; + pullRequest: Awaited>; +}): LocalBranchAnalysisInput { + return { + login: context.login, + repoFullName: context.repoFullName, + branchName: `github-pr-${context.issue.number}`, + headRef: context.pullRequest?.headRef ?? undefined, + headSha: context.pullRequest?.headSha ?? undefined, + title: context.pullRequest?.title ?? context.issue.title, + body: context.pullRequest?.body ?? undefined, + labels: context.pullRequest?.labels ?? [], + linkedIssues: context.pullRequest?.linkedIssues ?? [], + }; +} + +async function recordAgentCommandUsage( + env: Env, + args: { + repoFullName?: string | null | undefined; + targetKey?: string | null | undefined; + actor?: string | null | undefined; + command: string; + actorKind: "maintainer" | "author" | "none"; + outcome: "replied" | "skipped" | "error"; + detail?: string | null | undefined; + runId?: string | null | undefined; + }, +): Promise { + try { + const actorHash = args.actor ? await sha256Hex(`github:${args.actor.toLowerCase()}`) : null; + await persistSignalSnapshot(env, { + id: crypto.randomUUID(), + signalType: "github-agent-command-usage", + targetKey: args.targetKey ?? args.repoFullName ?? "unknown", + repoFullName: args.repoFullName ?? null, + payload: { + command: args.command, + actorKind: args.actorKind, + actorHash, + outcome: args.outcome, + detail: args.detail ?? null, + runId: args.runId ?? null, + }, + generatedAt: nowIso(), + }); + } catch (error) { + console.warn("Failed to record GitHub agent command usage", { command: args.command, outcome: args.outcome, error: errorMessage(error) }); + } +} + async function auditPrVisibilitySkip( env: Env, repoFullName: string, diff --git a/test/unit/github-commands.test.ts b/test/unit/github-commands.test.ts index 1806edd5f0..4440387694 100644 --- a/test/unit/github-commands.test.ts +++ b/test/unit/github-commands.test.ts @@ -12,6 +12,9 @@ describe("GitHub mention commands", () => { expect(parseGittensoryMentionCommand("@gittensory")?.name).toBe("help"); expect(parseGittensoryMentionCommand("@gittensory preflight")?.name).toBe("preflight"); expect(parseGittensoryMentionCommand("please @gittensory duplicate-check now")?.name).toBe("duplicate-check"); + expect(parseGittensoryMentionCommand("@gittensory reviewability")?.name).toBe("reviewability"); + expect(parseGittensoryMentionCommand("@gittensory repo-fit")?.name).toBe("repo-fit"); + expect(parseGittensoryMentionCommand("@gittensory packet")?.name).toBe("packet"); expect(parseGittensoryMentionCommand("@gittensory unknown")?.name).toBe("help"); expect(parseGittensoryMentionCommand("gittensory preflight")).toBeNull(); }); @@ -103,6 +106,8 @@ describe("GitHub mention commands", () => { expect(sanitizePublicComment("wallet hotkey payout reviewability private ranking")).not.toMatch( /wallet|hotkey|payout|reviewability|private ranking/i, ); + expect(sanitizePublicComment("public score estimate and scoreability should stay private")).not.toMatch(/public score estimate|scoreability/i); + expect(sanitizePublicComment("Command: @gittensory reviewability")).toContain("@gittensory reviewability"); expect(sanitizePublicComment("private ranking, wallet, payout")).toBe("private context"); }); @@ -478,6 +483,229 @@ describe("GitHub mention commands", () => { }); expect(duplicateFallbackPick).toContain("No duplicate signal in this fallback action."); }); + + it("renders v2 reviewability, repo-fit, and packet sections without private internals", () => { + const reviewability = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory reviewability")!, + repo: { fullName: "owner/repo" } as any, + issue: { number: 31, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + bundle: preflightBundle(), + }); + expect(reviewability).toContain("### Gittensory PR readiness"); + expect(reviewability).toContain("Command: `@gittensory reviewability`"); + expect(reviewability).toContain("**PR readiness**"); + expect(reviewability).toContain("Run local branch preflight first."); + expect(reviewability).not.toMatch(/private reviewability|reviewability internals|scoreability|public score estimate|wallet|hotkey|payout|farming/i); + + const repoFit = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory repo-fit")!, + repo: { fullName: "owner/repo" } as any, + issue: { number: 32, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + bundle: repoFitBundle(), + }); + expect(repoFit).toContain("### Gittensory repository fit"); + expect(repoFit).toContain("**Repository fit**"); + expect(repoFit).toContain("Target: `owner/repo`"); + expect(repoFit).toContain("Use local branch preflight before posting."); + expect(repoFit).not.toMatch(/private reviewability|scoreability|public score estimate|wallet|hotkey|payout|farming/i); + + const packet = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory packet")!, + repo: { fullName: "owner/repo" } as any, + issue: { number: 33, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + bundle: packetBundle(), + }); + expect(packet).toContain("### Gittensory public packet"); + expect(packet).toContain("**Public packet**"); + expect(packet).toContain("public-safe PR packet prepared from metadata only."); + expect(packet).toContain("Use this as public PR-thread guidance only"); + expect(packet).not.toMatch(/private reviewability|scoreability|public score estimate|wallet|hotkey|payout|farming/i); + }); + + it("covers v2 refresh, empty, rerun, and duplicate-line fallbacks", () => { + const preflightRefresh = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory preflight")!, + repo: null, + issue: { number: 40, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: refreshBundle(), + }); + expect(preflightRefresh).toContain("**Preflight snapshot refresh**"); + + for (const [commandText, title, fallback] of [ + ["@gittensory blockers", "Readiness blockers", "No public readiness blockers are visible"], + ["@gittensory duplicate-check", "Duplicate & WIP caution", "No duplicate or work-in-progress collision signal is visible"], + ] as const) { + const body = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand(commandText)!, + repo: null, + issue: { number: 40, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: emptyBundle(), + }); + expect(body).toContain(`**${title}**`); + expect(body).toContain(fallback); + } + + for (const [commandText, title] of [ + ["@gittensory reviewability", "PR readiness snapshot refresh"], + ["@gittensory repo-fit", "Repository fit snapshot refresh"], + ["@gittensory packet", "Public packet snapshot refresh"], + ] as const) { + const body = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand(commandText)!, + repo: null, + issue: { number: 41, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: refreshBundle(), + }); + expect(body).toContain(`**${title}**`); + } + + for (const [commandText, title] of [ + ["@gittensory reviewability", "PR readiness"], + ["@gittensory repo-fit", "Repository fit"], + ["@gittensory packet", "Public packet"], + ] as const) { + const body = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand(commandText)!, + repo: null, + issue: { number: 42, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "author", + bundle: emptyBundle(), + }); + expect(body).toContain(`**${title}**`); + expect(body).toContain("No public-safe context is available"); + } + + const repoFitWithRerun = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory repo-fit")!, + repo: null, + issue: { number: 43, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + bundle: { + run: completedRun("run-repo-fit-rerun"), + actions: [ + { + id: "repo-fit-rerun", + runId: "run-repo-fit-rerun", + actionType: "choose_next_work" as const, + status: "recommended" as const, + recommendation: "Choose next work", + why: [], + blockedBy: [], + publicSafeSummary: "Repository fit is acceptable after public checks.", + rerunWhen: "After queue changes.", + approvalRequired: true, + safetyClass: "private" as const, + payload: {}, + }, + ], + contextSnapshots: [], + summary: "repo fit", + }, + }); + expect(repoFitWithRerun).toContain("Rerun when: After queue changes."); + expect(repoFitWithRerun).not.toContain("Target:"); + + const repoFitFromSummary = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory repo-fit")!, + repo: null, + issue: { number: 43, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + bundle: { + run: completedRun("run-repo-fit-summary"), + actions: [ + { + id: "repo-fit-summary", + runId: "run-repo-fit-summary", + actionType: "monitor_existing_pr" as const, + status: "recommended" as const, + recommendation: "Explain repository fit", + why: [], + blockedBy: [], + publicSafeSummary: "Repository fit looks clean from cached public evidence.", + approvalRequired: true, + safetyClass: "private" as const, + payload: {}, + }, + ], + contextSnapshots: [], + summary: "repo fit", + }, + }); + expect(repoFitFromSummary).toContain("Repository fit looks clean"); + + const packetFromSafetyClass = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory packet")!, + repo: null, + issue: { number: 43, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + bundle: { + run: completedRun("run-packet-safety-class"), + actions: [ + { + id: "packet-safety-class", + runId: "run-packet-safety-class", + actionType: "monitor_existing_pr" as const, + status: "recommended" as const, + recommendation: "Use packet", + why: [], + blockedBy: [], + publicSafeSummary: "Post the public-safe PR packet after validation.", + approvalRequired: false, + safetyClass: "public_safe" as const, + payload: {}, + }, + ], + contextSnapshots: [], + summary: "packet", + }, + }); + expect(packetFromSafetyClass).toContain("Post the public-safe PR packet"); + + const duplicateBlockers = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory blockers")!, + repo: null, + issue: { number: 44, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + bundle: { + run: completedRun("run-duplicate-blockers"), + actions: [ + { + id: "duplicate-blockers", + runId: "run-duplicate-blockers", + actionType: "explain_score_blockers" as const, + status: "blocked" as const, + recommendation: "Resolve blockers", + why: [], + blockedBy: ["open_pr_pressure", "open_pr_pressure"], + publicSafeSummary: "Resolve queue pressure before opening more work.", + approvalRequired: true, + safetyClass: "private" as const, + payload: {}, + }, + ], + contextSnapshots: [], + summary: "blockers", + }, + }); + expect(duplicateBlockers.match(/Open pull request queue pressure/g)).toHaveLength(1); + }); }); function completedRun(id: string) { @@ -591,6 +819,100 @@ function duplicateBundle() { }; } +function preflightBundle() { + return { + run: completedRun("run-preflight-v2"), + actions: [ + { + id: "preflight", + runId: "run-preflight-v2", + actionType: "preflight_branch" as const, + status: "ready" as const, + recommendation: "Preflight passed", + why: [], + blockedBy: [], + publicSafeSummary: "Run local branch preflight first.", + rerunWhen: "After CI completes.", + approvalRequired: true, + safetyClass: "private" as const, + payload: {}, + }, + ], + contextSnapshots: [], + summary: "preflight", + }; +} + +function repoFitBundle() { + return { + run: completedRun("run-repo-fit-v2"), + actions: [ + { + id: "repo-fit", + runId: "run-repo-fit-v2", + actionType: "explain_repo_fit" as const, + targetRepoFullName: "owner/repo", + status: "recommended" as const, + recommendation: "Use repo fit context", + why: [], + blockedBy: [], + publicSafeSummary: "Use local branch preflight before posting.", + approvalRequired: true, + safetyClass: "private" as const, + payload: {}, + }, + ], + contextSnapshots: [], + summary: "repo fit", + }; +} + +function packetBundle() { + return { + run: completedRun("run-packet-v2"), + actions: [ + { + id: "packet", + runId: "run-packet-v2", + actionType: "prepare_pr_packet" as const, + status: "ready" as const, + recommendation: "Prepare packet", + why: [], + blockedBy: [], + publicSafeSummary: "owner/repo: public-safe PR packet prepared from metadata only.", + rerunWhen: "After validation changes.", + approvalRequired: false, + safetyClass: "public_safe" as const, + payload: {}, + }, + ], + contextSnapshots: [], + summary: "packet", + }; +} + +function refreshBundle() { + return { + run: { + ...completedRun("run-refresh-v2"), + status: "needs_snapshot_refresh" as const, + dataQualityStatus: "unknown" as const, + }, + actions: [], + contextSnapshots: [], + summary: "refresh", + }; +} + +function emptyBundle() { + return { + run: completedRun("run-empty-v2"), + actions: [], + contextSnapshots: [], + summary: "empty", + }; +} + function minerSnapshot() { return { source: "gittensor_api" as const, diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 9d111a0549..fc4f96576d 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -1282,17 +1282,19 @@ describe("queue processors", () => { if (url === "https://api.gittensor.io/miners/123") return Response.json({ repositories: [] }); if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 3, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); if (url.includes("/access_tokens")) { calls.token += 1; return Response.json({ token: "installation-token" }); } - if (url.includes("/issues/77/comments") && method === "GET") return Response.json([]); - if (url.includes("/issues/77/comments") && method === "POST") { + if (url.includes("/issues/") && url.includes("/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/") && url.includes("/comments") && method === "POST") { calls.commentsCreated += 1; const body = JSON.parse(String(init?.body ?? "{}")) as { body?: string }; expect(body.body).toContain(""); expect(body.body).toContain("@gittensory"); - expect(body.body).not.toMatch(/wallet|hotkey|estimated score|reward estimate|payout|farming|raw trust score/i); + expect(body.body).not.toMatch(/wallet|hotkey|estimated score|reward estimate|payout|farming|raw trust score|private reviewability|reviewability internals|scoreability|public score estimate/i); return Response.json({ id: 1001 }, { status: 201 }); } return new Response("not found", { status: 404 }); @@ -1366,8 +1368,78 @@ describe("queue processors", () => { }, }, }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-reviewability", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 5, + body: "@gittensory reviewability", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-repo-fit", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 6, + body: "@gittensory repo-fit", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-packet", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 77, title: "Miner command context", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 7, + body: "@gittensory packet", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); + await processJob(env, { + type: "github-webhook", + deliveryId: "agent-command-packet-no-cache", + eventName: "issue_comment", + payload: { + action: "created", + installation: { id: 123, account: { login: "JSONbored", id: 1, type: "User" } }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + issue: { number: 78, title: "Uncached PR command", state: "open", pull_request: {}, user: { login: "oktofeesh1" }, author_association: "NONE" }, + comment: { + id: 8, + body: "@gittensory packet", + user: { login: "maintainer", type: "User" }, + author_association: "OWNER", + }, + }, + }); - expect(calls).toEqual({ commentsCreated: 4, token: 4, minerList: 1 }); + expect(calls.commentsCreated).toBe(8); + expect(calls.token).toBe(8); + expect(calls.minerList).toBeGreaterThanOrEqual(1); const audit = await env.DB.prepare("select event_type, detail from audit_events where target_key = ? order by created_at") .bind("JSONbored/gittensory#77") .all<{ event_type: string; detail: string | null }>(); @@ -1378,6 +1450,20 @@ describe("queue processors", () => { expect.objectContaining({ event_type: "github_app.miner_detection_cache_hit", detail: "confirmed" }), ]), ); + const usage = await env.DB.prepare("select payload_json from signal_snapshots where signal_type = ? and target_key = ? order by generated_at") + .bind("github-agent-command-usage", "JSONbored/gittensory#77") + .all<{ payload_json: string }>(); + const usagePayloads = usage.results.map((entry) => JSON.parse(entry.payload_json) as { command: string; outcome: string; actorKind: string; actorHash?: string }); + expect(usagePayloads).toEqual( + expect.arrayContaining([ + expect.objectContaining({ command: "reviewability", outcome: "replied", actorKind: "maintainer" }), + expect.objectContaining({ command: "repo-fit", outcome: "replied", actorKind: "maintainer" }), + expect.objectContaining({ command: "packet", outcome: "replied", actorKind: "maintainer" }), + ]), + ); + expect(usagePayloads.every((payload) => typeof payload.actorHash === "string" && /^[a-f0-9]{64}$/.test(payload.actorHash))).toBe(true); + expect(JSON.stringify(usagePayloads)).not.toContain('"actor":'); + expect(JSON.stringify(usagePayloads)).not.toMatch(/wallet|hotkey|raw trust score|payout|reward estimate|farming|private reviewability|public score estimate|@gittensory|oktofeesh1/i); }); it("skips unauthorized, bot, and non-PR @gittensory mention commands without public output", async () => {