From 45fc9c6c8fdb3624c302ab0e790a90adb1763952 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 12 Jun 2026 06:24:42 -0700 Subject: [PATCH 1/2] feat(comments): always-on Gittensor earn footer (viral loop) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a shared `gittensoryFooter()` (src/github/footer.ts) used by the PR panel, command responses, and the closed-PR notice. It ALWAYS renders an 'earn for your contributions → register to start earning' CTA — a permanent, free marketing surface on every reviewed PR (the link persists forever): non-registered authors see the invite, and viewers of a registered contributor's PR see it too. The CTA points at the repo's public Gittensor miner page (social proof for that repo). Uses only 'earn' wording — never reward/payout/score (those throw in sanitizePublicComment); a unit test guards the invariant. The registered/non- registered distinction belongs in the review body (full panel vs. minimal), not here. --- src/github/commands.ts | 3 ++- src/github/footer.ts | 35 ++++++++++++++++++++++++++++++ src/queue/processors.ts | 3 ++- src/signals/engine.ts | 7 +++--- test/unit/footer.test.ts | 29 +++++++++++++++++++++++++ test/unit/signals-coverage.test.ts | 3 ++- 6 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 src/github/footer.ts create mode 100644 test/unit/footer.test.ts diff --git a/src/github/commands.ts b/src/github/commands.ts index 10ab8622e5..ddba12e762 100644 --- a/src/github/commands.ts +++ b/src/github/commands.ts @@ -1,4 +1,5 @@ import { AGENT_COMMAND_COMMENT_MARKER } from "./comments"; +import { gittensoryFooter } from "./footer"; import type { AgentRunBundle } from "../services/agent-orchestrator"; import type { GittensorContributorSnapshot, OfficialGittensorMinerDetection } from "../gittensor/api"; import type { AgentActionRecord, RepositoryCommandAuthorizationPolicy } from "../types"; @@ -230,7 +231,7 @@ export function buildPublicAgentCommandComment(args: { ...feedbackPromptSections(args.answerId), "", "---", - "Checked by [Gittensory](https://github.com/JSONbored/gittensory), a quiet PR intelligence layer for OSS maintainers.", + gittensoryFooter(), ].join("\n"); return sanitizePublicComment(body); } diff --git a/src/github/footer.ts b/src/github/footer.ts new file mode 100644 index 0000000000..60c2d50686 --- /dev/null +++ b/src/github/footer.ts @@ -0,0 +1,35 @@ +// Shared footer for public Gittensory PR comments — and the viral-growth loop that drives outside +// contributors to register on Gittensor. The audience-aware variant leads with an "earn" CTA for +// contributors who are NOT yet registered (the conversion target); confirmed contributors get a +// lighter line. The link persists on the PR forever, so every reviewed PR keeps inviting. +// +// PRIVACY: public comments must never use reward/payout/score/ranking wording (those throw in +// `sanitizePublicComment` via FORBIDDEN_PUBLIC_COMMENT_WORDS, and the product keeps private +// scoreability out of public output). This footer uses ONLY "earn" — a factual, public invitation, +// not a payout guarantee or a private-score disclosure. + +/** The Gittensory product site (marketing on-ramp / attribution target). */ +export const GITTENSORY_SITE_URL = "https://gittensory.aethereal.dev"; +/** The Gittensor network — where GitHub contributors register to earn for their contributions. */ +export const GITTENSOR_HOME_URL = "https://gittensor.io"; + +/** Public "who's earning on this repo" page — social proof + a registration path, scoped to one + * repo. Used for repos already registered on Gittensor (a contributor who just opened a PR here + * sees that contributions to THIS repo earn, then a path to join). */ +export function gittensorRepoEarnUrl(repoFullName: string): string { + return `${GITTENSOR_HOME_URL}/miners/repository?name=${encodeURIComponent(repoFullName)}&tab=miners`; +} + +/** Always-on public-comment footer + earn CTA. This is a permanent, free marketing surface: it + * appears on EVERY reviewed PR (the link persists forever), so non-registered authors see the + * invite and anyone viewing a registered contributor's PR sees it too. The registered/non-registered + * distinction lives in the review BODY (full panel vs. minimal), not here. + * Uses only "earn" wording — never reward/payout/score (forbidden in public comments). */ +export function gittensoryFooter(opts: { earnUrl?: string | undefined } = {}): string { + const earnUrl = opts.earnUrl ?? GITTENSOR_HOME_URL; + return [ + `💰 **Earn for open-source contributions like this.** [Gittensor](${GITTENSOR_HOME_URL}) lets GitHub contributors earn for the work they already do — [register to start earning →](${earnUrl}).`, + "", + `Checked by [Gittensory](${GITTENSORY_SITE_URL}), a quiet PR intelligence layer for OSS maintainers.`, + ].join("\n"); +} diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 4d606b5e2f..0c6423ae0c 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -62,6 +62,7 @@ import { import { contributorRepoStatsFromGittensor, fetchGittensorContributorSnapshot, fetchOfficialGittensorMiner, type GittensorContributorSnapshot, type OfficialGittensorMinerDetection } from "../gittensor/api"; import { createOrUpdateCheckRun, createOrUpdateGateCheckRun, createOrUpdatePendingGateCheckRun, createOrUpdateSkippedGateCheckRun, getInstallationId, getRepositoryCollaboratorPermission } from "../github/app"; import { createOrUpdateAgentCommandComment, createOrUpdatePrIntelligenceComment, PR_PANEL_COMMENT_MARKER } from "../github/comments"; +import { gittensoryFooter, gittensorRepoEarnUrl } from "../github/footer"; import { buildMaintainerQueueDigest, buildPublicAgentCommandComment, @@ -832,7 +833,7 @@ function buildClosedPrPanelUpdate(repoFullName: string, pullNumber: number): str `> | Gate result | ⚠️ Skipped | ${repoFullName}#${pullNumber} is no longer open. | No action. |`, "", "---", - "Checked by [Gittensory](https://github.com/JSONbored/gittensory), a quiet PR intelligence layer for OSS maintainers.", + gittensoryFooter({ earnUrl: gittensorRepoEarnUrl(repoFullName) }), ].join("\n"); } diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 0632156529..b4c18efe4b 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -20,6 +20,7 @@ import type { ScoringModelSnapshotRecord, } from "../types"; import type { PublicContributorProfile } from "../github/public"; +import { gittensoryFooter, gittensorRepoEarnUrl } from "../github/footer"; import type { GittensorContributorSnapshot } from "../gittensor/api"; import { nowIso } from "../utils/json"; import { hasLocalTestEvidence } from "./test-evidence"; @@ -3650,9 +3651,9 @@ export function buildPublicPrIntelligenceComment(args: { publicFindings.length > 0 ? publicFindings.map((finding) => `- ${sanitizePanelText(finding.title)}: ${sanitizePanelText(finding.publicText ?? finding.detail)}`) : ["- No public-safe advisory findings were generated from cached metadata."]; - const footer = confirmedMiner || args.settings.publicAudienceMode === "gittensor_only" - ? "Checked by [Gittensory](https://github.com/JSONbored/gittensory), a quiet PR intelligence layer for OSS maintainers. Learn more about [Gittensor](https://gittensor.io) contribution workflows." - : "Checked by [Gittensory](https://github.com/JSONbored/gittensory), a quiet PR intelligence layer for OSS maintainers."; + // Always-on earn CTA — a permanent, free marketing surface on every reviewed PR. The CTA points at + // this repo's public Gittensor miner page (social proof for THIS repo + a path to register). + const footer = gittensoryFooter({ earnUrl: gittensorRepoEarnUrl(args.pr.repoFullName) }); return [ "", "", diff --git a/test/unit/footer.test.ts b/test/unit/footer.test.ts new file mode 100644 index 0000000000..ab33142c98 --- /dev/null +++ b/test/unit/footer.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { gittensoryFooter, gittensorRepoEarnUrl, GITTENSOR_HOME_URL, GITTENSORY_SITE_URL } from "../../src/github/footer"; +import { FORBIDDEN_PUBLIC_COMMENT_WORDS } from "../../src/queue-intelligence"; + +describe("gittensory public-comment footer", () => { + it("always shows the earn CTA + attribution (permanent marketing surface on every PR)", () => { + const footer = gittensoryFooter(); + expect(footer).toMatch(/earn/i); + expect(footer).toContain("register to start earning"); + expect(footer).toContain(GITTENSOR_HOME_URL); + expect(footer).toContain(GITTENSORY_SITE_URL); + }); + + it("points the CTA at a specific repo's public miner page when given an earnUrl", () => { + const footer = gittensoryFooter({ earnUrl: gittensorRepoEarnUrl("JSONbored/gittensory") }); + expect(footer).toContain("https://gittensor.io/miners/repository?name=JSONbored%2Fgittensory&tab=miners"); + }); + + it("falls back to the Gittensor home URL when no earnUrl is given", () => { + expect(gittensoryFooter()).toContain(`(${GITTENSOR_HOME_URL})`); + }); + + it("never uses reward/payout/score wording (would throw in sanitizePublicComment)", () => { + const footer = gittensoryFooter({ earnUrl: gittensorRepoEarnUrl("o/r") }).toLowerCase(); + for (const word of FORBIDDEN_PUBLIC_COMMENT_WORDS) { + expect(footer).not.toContain(word.toLowerCase()); + } + }); +}); diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 0c180beed8..9782a1a150 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -724,7 +724,8 @@ describe("signal coverage edge cases", () => { expect(collisionComment).not.toContain("possible overlaps"); expect(collisionComment).not.toContain("Cached OSS contributor activity"); expect(collisionComment).not.toContain("Cached prior PRs/issues"); - expect(collisionComment).not.toContain("gittensor.io"); + // The always-on earn CTA footer is a permanent marketing surface on every PR. + expect(collisionComment).toContain("register to start earning"); const repoBlockedComment = buildPublicPrIntelligenceComment({ repo: null, From cdd081d723e0dc79eef7ea26da5f585005701778 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 12 Jun 2026 07:09:12 -0700 Subject: [PATCH 2/2] feat(comments): minimal earn-invite for non-registered contributors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a PR author has no Gittensor footprint (not detected via official API or cache), gittensory's readiness/contribution analysis doesn't apply — so post a brief welcome + earn invite instead of the full readiness panel. A known contributor (official or cached) still gets the full review. The always-on earn footer appears either way. Keyed on detection.detected so cached contributors keep the full panel. Adds a minimal-invite unit test. --- src/signals/engine.ts | 24 +++++++++++++++++++ test/unit/queue.test.ts | 11 ++++++++- test/unit/signals-coverage.test.ts | 38 +++++++++++++++++++++++++++--- test/unit/signals-v2.test.ts | 2 +- 4 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/signals/engine.ts b/src/signals/engine.ts index b4c18efe4b..cd2b193588 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -3600,6 +3600,11 @@ export function buildPublicPrIntelligenceComment(args: { const gateBlocking = gateEnabled && (gateConclusion === "failure" || gateConclusion === "action_required"); const missingLinkedIssue = args.pr.linkedIssues.length === 0 && !hasClearNoIssueRationale(args.pr); const confirmedMiner = isOfficialContributorDetection(args.detection); + // Author with no Gittensor footprint at all (not detected via official API or cache): gittensory's + // contribution analysis is for Gittensor contributors, so fire MINIMALLY — a brief welcome + the + // earn invite — instead of the full readiness panel. A KNOWN contributor (official or cached) still + // gets the full review. The always-on footer CTA appears either way, so every PR keeps marketing. + if (!args.detection.detected) return buildMinimalInviteComment(args); const genericOssMode = args.settings.publicAudienceMode === "oss_maintainer"; const hasPublicWarnings = publicFindings.some((finding) => finding.severity === "warning"); const alert = gateBlocking @@ -3713,6 +3718,25 @@ export function buildPublicPrIntelligenceComment(args: { ].join("\n"); } +/** Minimal public comment for a non-registered contributor. gittensory's readiness/contribution + * analysis is for registered Gittensor contributors, so we skip the panel and post a brief welcome + * + earn invite; the always-on footer CTA does the conversion. Carries the same panel marker so it + * updates in place if the author later registers (the full panel then replaces it). */ +function buildMinimalInviteComment(args: { pr: PullRequestRecord }): string { + return [ + "", + "", + ...formatAlertBlock([ + "[!NOTE]", + "## 👋 Thanks for the contribution", + "The maintainer will review your PR. Open-source work like this can earn on Gittensor — register your GitHub account and contributions like this become eligible to earn.", + ]), + "", + "---", + gittensoryFooter({ earnUrl: gittensorRepoEarnUrl(args.pr.repoFullName) }), + ].join("\n"); +} + type PublicPrPanelGateEvaluation = { conclusion: "success" | "failure" | "action_required" | "neutral" | "skipped"; summary: string; diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 00b6e43046..92dde1c237 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -1092,8 +1092,17 @@ describe("queue processors", () => { const method = init?.method ?? "GET"; if (url === "https://api.gittensor.io/miners") { calls.minerList += 1; - return Response.json([]); + // A confirmed official Gittensor contributor → the rerun renders the FULL readiness panel + // (which carries the rerun task); a non-registered author would get the minimal invite. + return Response.json([ + { uid: 7, githubUsername: "contributor", githubId: "123", totalPrs: 4, totalMergedPrs: 3, totalOpenPrs: 1, totalClosedPrs: 0, totalOpenIssues: 0, totalClosedIssues: 0, totalSolvedIssues: 0, totalValidSolvedIssues: 0, isEligible: true, credibility: 1, eligibleRepoCount: 1 }, + ]); } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ repositories: [{ repositoryFullName: "JSONbored/gittensory", totalPrs: "4", totalMergedPrs: "3", totalOpenPrs: "1", totalClosedPrs: "0", totalOpenIssues: "0", totalClosedIssues: "0", isEligible: true, credibility: "1.000000" }] }); + } + 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/contributor")) return Response.json({ login: "contributor", public_repos: 2, followers: 1 }); if (url.includes("/users/contributor/repos")) return Response.json([]); if (url.includes("/access_tokens")) { diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 9782a1a150..854bedbd86 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -731,7 +731,7 @@ describe("signal coverage edge cases", () => { repo: null, pr: { ...currentPr, linkedIssues: [99], body: "Fixes #99" }, profile, - detection: { detected: false, reason: "no cache", priorPullRequests: 0, priorMergedPullRequests: 0, priorIssues: 0 }, + detection: { detected: true, source: "github_cache" as const, reason: "cached", priorPullRequests: 0, priorMergedPullRequests: 0, priorIssues: 0 }, queueHealth: buildQueueHealth(null, [], [], buildCollisionReport(directRepo.fullName, [], [])), collisions: buildCollisionReport(directRepo.fullName, [], []), preflight: buildPreflightResult( @@ -946,7 +946,7 @@ describe("signal coverage edge cases", () => { repo: directRepo, pr: currentPr, profile: buildContributorProfile("dev", { login: "dev", topLanguages: ["Markdown"], source: "github" }, [], []), - detection: { detected: false, reason: "no official context", priorPullRequests: 0, priorMergedPullRequests: 0, priorIssues: 0 }, + detection: { detected: true, source: "github_cache" as const, reason: "cached", priorPullRequests: 0, priorMergedPullRequests: 0, priorIssues: 0 }, queueHealth, collisions, preflight, @@ -959,6 +959,38 @@ describe("signal coverage edge cases", () => { expect(comment).not.toContain("12"); }); + it("posts a minimal earn-invite (no readiness panel) for a non-registered contributor", () => { + const directRepo = repo("owner/invite"); + const currentPr = pr(directRepo.fullName, 42, "Add docs", { authorLogin: "newcomer", linkedIssues: [], body: "" }); + const collisions = buildCollisionReport(directRepo.fullName, [], [currentPr]); + const queueHealth = buildQueueHealth(directRepo, [], [currentPr], collisions); + const preflight = buildPreflightResult( + { repoFullName: directRepo.fullName, title: currentPr.title, body: currentPr.body ?? undefined, linkedIssues: currentPr.linkedIssues }, + directRepo, + [], + [currentPr], + ); + + const comment = buildPublicPrIntelligenceComment({ + repo: directRepo, + pr: currentPr, + profile: buildContributorProfile("newcomer", { login: "newcomer", topLanguages: [], source: "github" }, [], []), + detection: { detected: false, reason: "no gittensor footprint", priorPullRequests: 0, priorMergedPullRequests: 0, priorIssues: 0 }, + queueHealth, + collisions, + preflight, + settings: repoSettings(directRepo.fullName), + }); + + // Minimal: brief welcome + earn invite + the always-on footer CTA; NO readiness table. + expect(comment).toContain(""); + expect(comment).toContain("Thanks for the contribution"); + expect(comment).toMatch(/earn/i); + expect(comment).toContain("register to start earning"); + expect(comment).not.toContain("Readiness score"); + expect(comment).not.toContain("| Signal | Result | Evidence | Action |"); + }); + it("covers PR panel edge formatting without publishing unconfirmed cache counts", () => { const directRepo = repo("owner/edge"); const currentPr = pr(directRepo.fullName, 9, "Fix edge state", { authorLogin: "dev", linkedIssues: [1], body: "Fixes #1" }); @@ -1337,7 +1369,7 @@ describe("signal coverage edge cases", () => { repo: directRepo, pr: currentPr, profile, - detection: { detected: false, reason: "no official match", priorPullRequests: 0, priorMergedPullRequests: 0, priorIssues: 0 }, + detection: { detected: true, source: "github_cache" as const, reason: "cached", priorPullRequests: 0, priorMergedPullRequests: 0, priorIssues: 0 }, queueHealth: queueHealthFixture(directRepo.fullName, "low"), collisions: buildCollisionReport(directRepo.fullName, [], [currentPr]), preflight, diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index 7cff271c1f..16ec9a2437 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -1601,7 +1601,7 @@ describe("v2 signal builders", () => { repo, pr: { ...pullRequests[0]!, authorLogin: undefined, linkedIssues: [] }, profile: noLanguageProfile, - detection: { detected: false, reason: "none", priorPullRequests: 0, priorMergedPullRequests: 0, priorIssues: 0 }, + detection: { detected: true, source: "github_cache" as const, reason: "cached", priorPullRequests: 0, priorMergedPullRequests: 0, priorIssues: 0 }, queueHealth: buildQueueHealth(repo, [], [], buildCollisionReport(repo.fullName, [], [])), collisions: buildCollisionReport(repo.fullName, [], []), preflight: buildPreflightResult({ repoFullName: repo.fullName, title: "Docs", body: "No linked issue", changedFiles: ["README.md"], tests: ["manual"] }, repo, [], []),