Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/github/commands.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
}
Expand Down
35 changes: 35 additions & 0 deletions src/github/footer.ts
Original file line number Diff line number Diff line change
@@ -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");
}
3 changes: 2 additions & 1 deletion src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");
}

Expand Down
31 changes: 28 additions & 3 deletions src/signals/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -3599,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
Expand Down Expand Up @@ -3650,9 +3656,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 [
"<!-- gittensory-pr-panel:v1 -->",
"",
Expand Down Expand Up @@ -3712,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 [
"<!-- gittensory-pr-panel:v1 -->",
"",
...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;
Expand Down
29 changes: 29 additions & 0 deletions test/unit/footer.test.ts
Original file line number Diff line number Diff line change
@@ -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());
}
});
});
11 changes: 10 additions & 1 deletion test/unit/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
41 changes: 37 additions & 4 deletions test/unit/signals-coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,13 +724,14 @@ 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,
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(
Expand Down Expand Up @@ -945,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,
Expand All @@ -958,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("<!-- gittensory-pr-panel:v1 -->");
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" });
Expand Down Expand Up @@ -1336,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,
Expand Down
2 changes: 1 addition & 1 deletion test/unit/signals-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, [], []),
Expand Down