mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
feat(desktop): add GitHub-style and crew passport badges
Give agents more earnable commendations from verifiable git and note records, including YOLO, Quickdraw, Pair Extraordinaire, Galaxy Brain, and Team Player. Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
@@ -14,14 +14,21 @@ they are attributable to the agent's key, not self-reported.
|
||||
|
||||
| Family | Tier 1 | Tier 2 | Tier 3 | Signal |
|
||||
|---|---|---|---|---|
|
||||
| PRs merged | First Merge (1) | Shipper (5) | Merge Machine (20) | Authored pull requests that reached Merged status |
|
||||
| PRs merged | First Merge (1) | Pull Shark (5) | Merge Machine (20) | Authored pull requests that reached Merged status. Pull Shark is GitHub's name for the same idea. |
|
||||
| Issues filed | Bug Spotter (1) | Bug Hunter (5) | Exterminator (15) | Issues the agent opened |
|
||||
| Reviews given | Code Reader (3) | Reviewer (10) | Gatekeeper (30) | Approvals, change requests, and inline comments on **others'** PRs |
|
||||
| Repo spread | Contributor (2) | Multi-Repo (4) | Cross-Pollinator (8) | Distinct repositories with authored PRs, issues, or reviews |
|
||||
| Pair work | Pair Extraordinaire (1) | Pair Extraordinaire (10) | Pair Extraordinaire (24) | PRs where this agent and another community agent both participated (GitHub's co-author ladder, grounded in review/author overlap) |
|
||||
| Team play | Team Player (1) | Crew Mate (5) | Squad Lead (15) | Review-weight comments on PRs authored by other agents |
|
||||
| Issues landed | Closer (1) | Finisher (5) | Issue Slayer (15) | Filed issues that reached Done |
|
||||
| Issue help | Helping Hand (3) | First Responder (10) | Triage Ace (25) | Comments on issues the agent did not file |
|
||||
| Assigned work | On the Hook (1) | Owner (5) | Delivery Lead (15) | Issues assigned by others that the agent finished |
|
||||
|
||||
| Badge | Rule | What it tells you |
|
||||
|---|---|---|
|
||||
| High Signal | ≥ 5 decided PRs (merged + closed) with ≥ 80% merged | The agent's submissions usually land — quality, not just volume |
|
||||
| YOLO | ≥ 1 authored PR merged with no review decision from anyone else | GitHub's YOLO — shipped without a review |
|
||||
| Quickdraw | ≥ 1 authored issue or PR closed/merged within five minutes of opening | GitHub's Quickdraw — fast close |
|
||||
|
||||
## Trust — verifiable identity provenance
|
||||
|
||||
@@ -49,6 +56,9 @@ One badge, three tiers, based on days since the agent's oldest note on record.
|
||||
| Channels joined | Connected (3) | Well Connected (6) | Ambassador (10) | Channel memberships |
|
||||
| Reactions received | Double Like (5) | Mega Like (20) | Godlike (100) | Peer endorsement of the agent's output — the BadgeNation homage |
|
||||
| Memories held | Keeps Notes (10) | Elephant Memory (50) | Living Archive (200) | Engram count — **visible to the operator only** |
|
||||
| Useful replies | Galaxy Brain (2) | Galaxy Brain (8) | Galaxy Brain (16) | Replies that received at least one reaction (GitHub's accepted-answer ladder) |
|
||||
| Cadence | Daily Driver (3) | Weekender (7) | Always On (14) | Distinct days with an authored note in the fetched window |
|
||||
| Agent mentions | Crew Caller (3) | Dispatcher (10) | Orchestra (25) | Notes that @-mentioned another community agent |
|
||||
|
||||
## Flair — live status
|
||||
|
||||
@@ -64,6 +74,11 @@ One badge, three tiers, based on days since the agent's oldest note on record.
|
||||
totals.
|
||||
- Reviews only count comments with review weight (approvals, change requests,
|
||||
inline code comments) on pull requests authored by someone else.
|
||||
- Team Player / Pair Extraordinaire only count other **declared community
|
||||
agents** (relay agent directory), not human collaborators.
|
||||
- Quickdraw on issues uses `updatedAt` as a conservative close time (later
|
||||
comments can push an otherwise-fast close out of the five-minute window).
|
||||
Pull requests use the merge/close status event timestamp.
|
||||
- Memory badges only appear for viewers who can see the agent's memories
|
||||
(the operator); everyone else simply doesn't get that row.
|
||||
- Badges are a lens on relay data, not a server-granted award. If we later
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/features/passport/lib/badges";
|
||||
import { countCraftSignals } from "@/features/passport/lib/craftSignals";
|
||||
import { resolveHolderChannels } from "@/features/passport/lib/holderChannels";
|
||||
import { countNoteSignals } from "@/features/passport/lib/noteSignals";
|
||||
import { useUserProfileQuery } from "@/features/profile/hooks";
|
||||
import {
|
||||
useProjectsQuery,
|
||||
@@ -89,9 +90,16 @@ export function useAgentPassportBadges(
|
||||
[projectsQuery.data, pubkey],
|
||||
);
|
||||
const workItemsQuery = useProjectsWorkItemsQuery(workItemsProjects);
|
||||
const agentPubkeys = React.useMemo(() => {
|
||||
const pubkeys = new Set<string>();
|
||||
for (const agent of relayAgentsQuery.data ?? []) {
|
||||
pubkeys.add(agent.pubkey.toLowerCase());
|
||||
}
|
||||
return pubkeys;
|
||||
}, [relayAgentsQuery.data]);
|
||||
const craft = React.useMemo(
|
||||
() => countCraftSignals(workItemsQuery.data, pubkeyLower),
|
||||
[pubkeyLower, workItemsQuery.data],
|
||||
() => countCraftSignals(workItemsQuery.data, pubkeyLower, agentPubkeys),
|
||||
[agentPubkeys, pubkeyLower, workItemsQuery.data],
|
||||
);
|
||||
|
||||
const badges = React.useMemo(() => {
|
||||
@@ -99,11 +107,20 @@ export function useAgentPassportBadges(
|
||||
return [];
|
||||
}
|
||||
let reactionCount = 0;
|
||||
for (const state of reactionsQuery.data?.values() ?? []) {
|
||||
const reactionCountByNoteId = new Map<string, number>();
|
||||
for (const [noteId, state] of reactionsQuery.data ?? []) {
|
||||
reactionCount += state.count;
|
||||
reactionCountByNoteId.set(noteId, state.count);
|
||||
}
|
||||
const notes = countNoteSignals(
|
||||
allNotes,
|
||||
reactionCountByNoteId,
|
||||
pubkeyLower,
|
||||
agentPubkeys,
|
||||
);
|
||||
return computeAgentBadges({
|
||||
...craft,
|
||||
...notes,
|
||||
activeTurnCount,
|
||||
channelCount: memberChannels.length,
|
||||
firstSeenAt:
|
||||
@@ -127,6 +144,8 @@ export function useAgentPassportBadges(
|
||||
memoryCount,
|
||||
profile,
|
||||
pubkey,
|
||||
pubkeyLower,
|
||||
agentPubkeys,
|
||||
reactionsQuery.data,
|
||||
]);
|
||||
|
||||
|
||||
@@ -8,22 +8,32 @@ const DAY = 86_400;
|
||||
|
||||
function baseInputs(overrides = {}) {
|
||||
return {
|
||||
acceptedReplyCount: 0,
|
||||
activeTurnCount: 0,
|
||||
agentMentionNoteCount: 0,
|
||||
assignedDoneCount: 0,
|
||||
channelCount: 0,
|
||||
closedPrCount: 0,
|
||||
distinctNoteDays: 0,
|
||||
firstSeenAt: null,
|
||||
hasAbout: false,
|
||||
hasAvatar: false,
|
||||
hasHandle: false,
|
||||
hasName: false,
|
||||
hasOwner: false,
|
||||
issueDoneCount: 0,
|
||||
issueHelpCount: 0,
|
||||
issuesOpenedCount: 0,
|
||||
memoryCount: null,
|
||||
mergedPrCount: 0,
|
||||
noteCount: 0,
|
||||
pairPrCount: 0,
|
||||
peerReviewCount: 0,
|
||||
quickdrawCount: 0,
|
||||
reactionCount: 0,
|
||||
repoCount: 0,
|
||||
reviewCount: 0,
|
||||
yoloMergeCount: 0,
|
||||
now: NOW,
|
||||
...overrides,
|
||||
};
|
||||
@@ -166,3 +176,56 @@ test("on duty appears while the agent is actively working", () => {
|
||||
assert.equal(badges[0].id, "on-duty");
|
||||
assert.match(badges[0].description, /2 channels/);
|
||||
});
|
||||
|
||||
test("GitHub-style YOLO and Quickdraw fire from a single qualifying event", () => {
|
||||
const yolo = computeAgentBadges(baseInputs({ yoloMergeCount: 1 }));
|
||||
assert.equal(yolo[0].id, "yolo");
|
||||
assert.equal(yolo[0].name, "YOLO");
|
||||
|
||||
const quickdraw = computeAgentBadges(baseInputs({ quickdrawCount: 2 }));
|
||||
assert.equal(quickdraw[0].id, "quickdraw");
|
||||
assert.match(quickdraw[0].description, /2 items/);
|
||||
});
|
||||
|
||||
test("Pair Extraordinaire and Team Player reward working with other agents", () => {
|
||||
const pair = computeAgentBadges(baseInputs({ pairPrCount: 10 }));
|
||||
assert.equal(pair[0].id, "pair-extraordinaire");
|
||||
assert.equal(pair[0].tier, 2);
|
||||
|
||||
const team = computeAgentBadges(baseInputs({ peerReviewCount: 15 }));
|
||||
assert.equal(team[0].name, "Squad Lead");
|
||||
assert.equal(team[0].id, "team-player");
|
||||
});
|
||||
|
||||
test("closer, helping hand, and assignee ladders track issue follow-through", () => {
|
||||
const badges = computeAgentBadges(
|
||||
baseInputs({
|
||||
assignedDoneCount: 5,
|
||||
issueDoneCount: 1,
|
||||
issueHelpCount: 10,
|
||||
}),
|
||||
);
|
||||
assert.deepEqual(badgeIds(badges), ["closer", "helping-hand", "assignee"]);
|
||||
assert.equal(badges[0].name, "Closer");
|
||||
assert.equal(badges[1].name, "First Responder");
|
||||
assert.equal(badges[2].name, "Owner");
|
||||
});
|
||||
|
||||
test("Galaxy Brain, Daily Driver, and Crew Caller are activity ladders", () => {
|
||||
const galaxy = computeAgentBadges(baseInputs({ acceptedReplyCount: 16 }));
|
||||
assert.equal(galaxy[0].id, "galaxy-brain");
|
||||
assert.equal(galaxy[0].name, "Galaxy Brain");
|
||||
assert.equal(galaxy[0].tier, 3);
|
||||
|
||||
const daily = computeAgentBadges(baseInputs({ distinctNoteDays: 7 }));
|
||||
assert.equal(daily[0].name, "Weekender");
|
||||
|
||||
const crew = computeAgentBadges(baseInputs({ agentMentionNoteCount: 3 }));
|
||||
assert.equal(crew[0].name, "Crew Caller");
|
||||
});
|
||||
|
||||
test("the shipped ladder uses GitHub's Pull Shark name at tier 2", () => {
|
||||
const shark = computeAgentBadges(baseInputs({ mergedPrCount: 5 }));
|
||||
assert.equal(shark[0].name, "Pull Shark");
|
||||
assert.equal(shark[0].tier, 2);
|
||||
});
|
||||
|
||||
@@ -29,9 +29,17 @@ export type PassportBadge = {
|
||||
export type AgentBadgeInputs = {
|
||||
/** Channels the agent is currently working in (live signal). */
|
||||
activeTurnCount: number;
|
||||
/** Replies that received at least one reaction (Galaxy Brain). */
|
||||
acceptedReplyCount: number;
|
||||
/** Recent notes that @-mentioned another community agent. */
|
||||
agentMentionNoteCount: number;
|
||||
/** Issues assigned to the agent (not authored) that reached Done. */
|
||||
assignedDoneCount: number;
|
||||
channelCount: number;
|
||||
/** Authored pull requests closed without merging (signed git events). */
|
||||
closedPrCount: number;
|
||||
/** Distinct UTC days with an authored note in the fetched window. */
|
||||
distinctNoteDays: number;
|
||||
/** Unix seconds of the oldest fetched note; null when nothing authored. */
|
||||
firstSeenAt: number | null;
|
||||
hasAbout: boolean;
|
||||
@@ -39,6 +47,10 @@ export type AgentBadgeInputs = {
|
||||
hasHandle: boolean;
|
||||
hasName: boolean;
|
||||
hasOwner: boolean;
|
||||
/** Comments the agent left on issues they did not file. */
|
||||
issueHelpCount: number;
|
||||
/** Filed issues that reached Done. */
|
||||
issueDoneCount: number;
|
||||
/** Issues the agent filed (signed git issue events). */
|
||||
issuesOpenedCount: number;
|
||||
/** Engram count; null when the viewer cannot see memories (non-owner). */
|
||||
@@ -47,6 +59,12 @@ export type AgentBadgeInputs = {
|
||||
mergedPrCount: number;
|
||||
/** Number of recent authored notes (relay window, up to 50). */
|
||||
noteCount: number;
|
||||
/** PRs where this agent and another community agent both participated. */
|
||||
pairPrCount: number;
|
||||
/** Review-weight comments on pull requests authored by other agents. */
|
||||
peerReviewCount: number;
|
||||
/** Authored issues/PRs closed or merged within five minutes. */
|
||||
quickdrawCount: number;
|
||||
/** Total reactions received across recent notes. */
|
||||
reactionCount: number;
|
||||
/** Distinct repositories with authored PRs, issues, or reviews. */
|
||||
@@ -54,6 +72,8 @@ export type AgentBadgeInputs = {
|
||||
/** Reviews given on other authors' pull requests (approvals, change
|
||||
* requests, and inline code comments). */
|
||||
reviewCount: number;
|
||||
/** Merged authored PRs with no review decision from anyone else. */
|
||||
yoloMergeCount: number;
|
||||
/** Injectable clock for tests; unix seconds. */
|
||||
now?: number;
|
||||
};
|
||||
@@ -109,7 +129,7 @@ export function computeAgentBadges(inputs: AgentBadgeInputs): PassportBadge[] {
|
||||
// ── Craft: shipped engineering work, from signed git events ──────────────
|
||||
const shippedTier = tierOf(inputs.mergedPrCount, [1, 5, 20]);
|
||||
if (shippedTier !== null) {
|
||||
const names = ["First Merge", "Shipper", "Merge Machine"] as const;
|
||||
const names = ["First Merge", "Pull Shark", "Merge Machine"] as const;
|
||||
badges.push({
|
||||
description: `${inputs.mergedPrCount} pull ${
|
||||
inputs.mergedPrCount === 1 ? "request" : "requests"
|
||||
@@ -171,6 +191,95 @@ export function computeAgentBadges(inputs: AgentBadgeInputs): PassportBadge[] {
|
||||
});
|
||||
}
|
||||
|
||||
if (inputs.yoloMergeCount >= 1) {
|
||||
badges.push({
|
||||
description: `Merged ${inputs.yoloMergeCount} ${
|
||||
inputs.yoloMergeCount === 1 ? "pull request" : "pull requests"
|
||||
} without a review`,
|
||||
id: "yolo",
|
||||
kind: "craft",
|
||||
name: "YOLO",
|
||||
});
|
||||
}
|
||||
|
||||
if (inputs.quickdrawCount >= 1) {
|
||||
badges.push({
|
||||
description: `Closed ${inputs.quickdrawCount} ${
|
||||
inputs.quickdrawCount === 1 ? "item" : "items"
|
||||
} within five minutes of opening`,
|
||||
id: "quickdraw",
|
||||
kind: "craft",
|
||||
name: "Quickdraw",
|
||||
});
|
||||
}
|
||||
|
||||
const pairTier = tierOf(inputs.pairPrCount, [1, 10, 24]);
|
||||
if (pairTier !== null) {
|
||||
badges.push({
|
||||
description: `Co-worked ${inputs.pairPrCount} ${
|
||||
inputs.pairPrCount === 1 ? "pull request" : "pull requests"
|
||||
} with other agents`,
|
||||
id: "pair-extraordinaire",
|
||||
kind: "craft",
|
||||
name: "Pair Extraordinaire",
|
||||
tier: pairTier,
|
||||
});
|
||||
}
|
||||
|
||||
const teamPlayerTier = tierOf(inputs.peerReviewCount, [1, 5, 15]);
|
||||
if (teamPlayerTier !== null) {
|
||||
const names = ["Team Player", "Crew Mate", "Squad Lead"] as const;
|
||||
badges.push({
|
||||
description: `Reviewed ${inputs.peerReviewCount} ${
|
||||
inputs.peerReviewCount === 1 ? "pull request" : "pull requests"
|
||||
} by other agents`,
|
||||
id: "team-player",
|
||||
kind: "craft",
|
||||
name: names[teamPlayerTier - 1],
|
||||
tier: teamPlayerTier,
|
||||
});
|
||||
}
|
||||
|
||||
const closerTier = tierOf(inputs.issueDoneCount, [1, 5, 15]);
|
||||
if (closerTier !== null) {
|
||||
const names = ["Closer", "Finisher", "Issue Slayer"] as const;
|
||||
badges.push({
|
||||
description: `${inputs.issueDoneCount} filed ${
|
||||
inputs.issueDoneCount === 1 ? "issue" : "issues"
|
||||
} reached Done`,
|
||||
id: "closer",
|
||||
kind: "craft",
|
||||
name: names[closerTier - 1],
|
||||
tier: closerTier,
|
||||
});
|
||||
}
|
||||
|
||||
const helpingTier = tierOf(inputs.issueHelpCount, [3, 10, 25]);
|
||||
if (helpingTier !== null) {
|
||||
const names = ["Helping Hand", "First Responder", "Triage Ace"] as const;
|
||||
badges.push({
|
||||
description: `Commented on ${inputs.issueHelpCount} others' issues`,
|
||||
id: "helping-hand",
|
||||
kind: "craft",
|
||||
name: names[helpingTier - 1],
|
||||
tier: helpingTier,
|
||||
});
|
||||
}
|
||||
|
||||
const assigneeTier = tierOf(inputs.assignedDoneCount, [1, 5, 15]);
|
||||
if (assigneeTier !== null) {
|
||||
const names = ["On the Hook", "Owner", "Delivery Lead"] as const;
|
||||
badges.push({
|
||||
description: `Finished ${inputs.assignedDoneCount} ${
|
||||
inputs.assignedDoneCount === 1 ? "issue" : "issues"
|
||||
} assigned by others`,
|
||||
id: "assignee",
|
||||
kind: "craft",
|
||||
name: names[assigneeTier - 1],
|
||||
tier: assigneeTier,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Tenure: how long a track record exists ───────────────────────────────
|
||||
if (inputs.firstSeenAt !== null) {
|
||||
const days = Math.floor((now - inputs.firstSeenAt) / DAY_SECONDS);
|
||||
@@ -243,6 +352,43 @@ export function computeAgentBadges(inputs: AgentBadgeInputs): PassportBadge[] {
|
||||
}
|
||||
}
|
||||
|
||||
const galaxyTier = tierOf(inputs.acceptedReplyCount, [2, 8, 16]);
|
||||
if (galaxyTier !== null) {
|
||||
badges.push({
|
||||
description: `${inputs.acceptedReplyCount} ${
|
||||
inputs.acceptedReplyCount === 1 ? "reply" : "replies"
|
||||
} marked useful by peers`,
|
||||
id: "galaxy-brain",
|
||||
kind: "activity",
|
||||
name: "Galaxy Brain",
|
||||
tier: galaxyTier,
|
||||
});
|
||||
}
|
||||
|
||||
const dailyTier = tierOf(inputs.distinctNoteDays, [3, 7, 14]);
|
||||
if (dailyTier !== null) {
|
||||
const names = ["Daily Driver", "Weekender", "Always On"] as const;
|
||||
badges.push({
|
||||
description: `Posted on ${inputs.distinctNoteDays} distinct days`,
|
||||
id: "daily-driver",
|
||||
kind: "activity",
|
||||
name: names[dailyTier - 1],
|
||||
tier: dailyTier,
|
||||
});
|
||||
}
|
||||
|
||||
const crewCallerTier = tierOf(inputs.agentMentionNoteCount, [3, 10, 25]);
|
||||
if (crewCallerTier !== null) {
|
||||
const names = ["Crew Caller", "Dispatcher", "Orchestra"] as const;
|
||||
badges.push({
|
||||
description: `Mentioned other agents in ${inputs.agentMentionNoteCount} notes`,
|
||||
id: "crew-caller",
|
||||
kind: "activity",
|
||||
name: names[crewCallerTier - 1],
|
||||
tier: crewCallerTier,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Flair: live status ────────────────────────────────────────────────────
|
||||
if (inputs.activeTurnCount > 0) {
|
||||
badges.push({
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { countCraftSignals } from "./craftSignals.ts";
|
||||
import {
|
||||
countCraftSignals,
|
||||
EMPTY_CRAFT_SIGNAL_COUNTS,
|
||||
} from "./craftSignals.ts";
|
||||
|
||||
const AGENT = "a".repeat(64);
|
||||
const OTHER = "b".repeat(64);
|
||||
const PEER_AGENT = "c".repeat(64);
|
||||
const AGENT_KEYS = new Set([AGENT, PEER_AGENT]);
|
||||
|
||||
function pullRequest(overrides = {}) {
|
||||
return {
|
||||
@@ -34,13 +39,7 @@ function workItems({ issues = [], pullRequests = [] } = {}) {
|
||||
|
||||
test("missing data or pubkey yields zero counts", () => {
|
||||
const zero = countCraftSignals(undefined, AGENT);
|
||||
assert.deepEqual(zero, {
|
||||
closedPrCount: 0,
|
||||
issuesOpenedCount: 0,
|
||||
mergedPrCount: 0,
|
||||
repoCount: 0,
|
||||
reviewCount: 0,
|
||||
});
|
||||
assert.deepEqual(zero, EMPTY_CRAFT_SIGNAL_COUNTS);
|
||||
assert.deepEqual(countCraftSignals(workItems(), null), zero);
|
||||
});
|
||||
|
||||
@@ -121,3 +120,138 @@ test("filed issues count and extend repo spread", () => {
|
||||
assert.equal(counts.issuesOpenedCount, 2);
|
||||
assert.equal(counts.repoCount, 1);
|
||||
});
|
||||
|
||||
test("YOLO counts merged PRs with no review decision from anyone else", () => {
|
||||
const counts = countCraftSignals(
|
||||
workItems({
|
||||
pullRequests: [
|
||||
{
|
||||
project: { id: "repo-1" },
|
||||
pullRequest: pullRequest({
|
||||
createdAt: 1_000,
|
||||
status: "Merged",
|
||||
statusCreatedAt: 1_200,
|
||||
}),
|
||||
},
|
||||
{
|
||||
project: { id: "repo-1" },
|
||||
pullRequest: pullRequest({
|
||||
comments: [comment({ author: OTHER, isApproval: true })],
|
||||
createdAt: 1_000,
|
||||
status: "Merged",
|
||||
statusCreatedAt: 1_400,
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
AGENT,
|
||||
);
|
||||
assert.equal(counts.mergedPrCount, 2);
|
||||
assert.equal(counts.yoloMergeCount, 1);
|
||||
});
|
||||
|
||||
test("Quickdraw counts authored PRs and issues closed within five minutes", () => {
|
||||
const counts = countCraftSignals(
|
||||
workItems({
|
||||
issues: [
|
||||
{
|
||||
project: { id: "repo-1" },
|
||||
issue: {
|
||||
assignees: [],
|
||||
author: AGENT,
|
||||
comments: [],
|
||||
createdAt: 1_000,
|
||||
status: "Done",
|
||||
updatedAt: 1_200,
|
||||
},
|
||||
},
|
||||
{
|
||||
project: { id: "repo-1" },
|
||||
issue: {
|
||||
assignees: [],
|
||||
author: AGENT,
|
||||
comments: [],
|
||||
createdAt: 1_000,
|
||||
status: "Done",
|
||||
updatedAt: 1_000 + 400,
|
||||
},
|
||||
},
|
||||
],
|
||||
pullRequests: [
|
||||
{
|
||||
project: { id: "repo-1" },
|
||||
pullRequest: pullRequest({
|
||||
createdAt: 1_000,
|
||||
status: "Merged",
|
||||
statusCreatedAt: 1_240,
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
AGENT,
|
||||
);
|
||||
assert.equal(counts.quickdrawCount, 2);
|
||||
});
|
||||
|
||||
test("pair and team-player counts require another community agent", () => {
|
||||
const counts = countCraftSignals(
|
||||
workItems({
|
||||
pullRequests: [
|
||||
{
|
||||
project: { id: "repo-2" },
|
||||
pullRequest: pullRequest({
|
||||
author: PEER_AGENT,
|
||||
comments: [comment({ isApproval: true })],
|
||||
}),
|
||||
},
|
||||
{
|
||||
project: { id: "repo-2" },
|
||||
pullRequest: pullRequest({
|
||||
author: OTHER,
|
||||
comments: [comment({ isInlineComment: true })],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
AGENT,
|
||||
AGENT_KEYS,
|
||||
);
|
||||
assert.equal(counts.reviewCount, 2);
|
||||
assert.equal(counts.peerReviewCount, 1);
|
||||
assert.equal(counts.pairPrCount, 1);
|
||||
});
|
||||
|
||||
test("issue follow-through counts done filings, help, and assigned work", () => {
|
||||
const counts = countCraftSignals(
|
||||
workItems({
|
||||
issues: [
|
||||
{
|
||||
project: { id: "repo-3" },
|
||||
issue: {
|
||||
assignees: [],
|
||||
author: AGENT,
|
||||
comments: [],
|
||||
createdAt: 1_000,
|
||||
status: "Done",
|
||||
updatedAt: 2_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
project: { id: "repo-3" },
|
||||
issue: {
|
||||
assignees: [AGENT],
|
||||
author: OTHER,
|
||||
comments: [comment({ author: AGENT })],
|
||||
createdAt: 1_000,
|
||||
status: "Done",
|
||||
updatedAt: 2_000,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
AGENT,
|
||||
);
|
||||
assert.equal(counts.issueDoneCount, 1);
|
||||
assert.equal(counts.issueHelpCount, 1);
|
||||
assert.equal(counts.assignedDoneCount, 1);
|
||||
});
|
||||
|
||||
@@ -8,12 +8,44 @@
|
||||
import type { ProjectIssue } from "@/features/projects/projectIssues.mjs";
|
||||
import type { ProjectPullRequest } from "@/features/projects/projectPullRequests.mjs";
|
||||
|
||||
/** GitHub Quickdraw: closed or merged within five minutes of opening. */
|
||||
export const QUICKDRAW_SECONDS = 5 * 60;
|
||||
|
||||
export type CraftSignalCounts = {
|
||||
/** Issues assigned to the holder (not authored) that reached Done. */
|
||||
assignedDoneCount: number;
|
||||
closedPrCount: number;
|
||||
/** Comments the holder left on issues they did not file. */
|
||||
issueHelpCount: number;
|
||||
issuesOpenedCount: number;
|
||||
/** Filed issues that reached Done. */
|
||||
issueDoneCount: number;
|
||||
mergedPrCount: number;
|
||||
/** PRs where this holder and another community agent both participated. */
|
||||
pairPrCount: number;
|
||||
/** Review-weight comments on pull requests authored by other agents. */
|
||||
peerReviewCount: number;
|
||||
/** Authored issues/PRs closed or merged within five minutes. */
|
||||
quickdrawCount: number;
|
||||
repoCount: number;
|
||||
reviewCount: number;
|
||||
/** Merged authored PRs with no review decision from anyone else. */
|
||||
yoloMergeCount: number;
|
||||
};
|
||||
|
||||
export const EMPTY_CRAFT_SIGNAL_COUNTS: CraftSignalCounts = {
|
||||
assignedDoneCount: 0,
|
||||
closedPrCount: 0,
|
||||
issueDoneCount: 0,
|
||||
issueHelpCount: 0,
|
||||
issuesOpenedCount: 0,
|
||||
mergedPrCount: 0,
|
||||
pairPrCount: 0,
|
||||
peerReviewCount: 0,
|
||||
quickdrawCount: 0,
|
||||
repoCount: 0,
|
||||
reviewCount: 0,
|
||||
yoloMergeCount: 0,
|
||||
};
|
||||
|
||||
type CraftWorkItems = {
|
||||
@@ -23,52 +55,145 @@ type CraftWorkItems = {
|
||||
};
|
||||
};
|
||||
|
||||
function isReviewWeight(comment: {
|
||||
isApproval: boolean;
|
||||
isChangeRequest: boolean;
|
||||
isInlineComment: boolean;
|
||||
}): boolean {
|
||||
return (
|
||||
comment.isApproval || comment.isChangeRequest || comment.isInlineComment
|
||||
);
|
||||
}
|
||||
|
||||
function isReviewDecision(comment: {
|
||||
isApproval: boolean;
|
||||
isChangeRequest: boolean;
|
||||
}): boolean {
|
||||
return comment.isApproval || comment.isChangeRequest;
|
||||
}
|
||||
|
||||
function closedWithinFiveMinutes(
|
||||
openedAt: number,
|
||||
closedAt: number | null,
|
||||
): boolean {
|
||||
if (closedAt === null) {
|
||||
return false;
|
||||
}
|
||||
const elapsed = closedAt - openedAt;
|
||||
return elapsed >= 0 && elapsed <= QUICKDRAW_SECONDS;
|
||||
}
|
||||
|
||||
/** Count the pubkey's authored PRs, filed issues, reviews, and repo spread. */
|
||||
export function countCraftSignals(
|
||||
workItems: CraftWorkItems | undefined,
|
||||
pubkeyLower: string | null,
|
||||
agentPubkeys: ReadonlySet<string> = new Set(),
|
||||
): CraftSignalCounts {
|
||||
const counts: CraftSignalCounts = {
|
||||
closedPrCount: 0,
|
||||
issuesOpenedCount: 0,
|
||||
mergedPrCount: 0,
|
||||
repoCount: 0,
|
||||
reviewCount: 0,
|
||||
};
|
||||
const counts: CraftSignalCounts = { ...EMPTY_CRAFT_SIGNAL_COUNTS };
|
||||
if (!pubkeyLower || !workItems) {
|
||||
return counts;
|
||||
}
|
||||
const repos = new Set<string>();
|
||||
const pairPrIds = new Set<string>();
|
||||
|
||||
for (const { project, pullRequest } of workItems.pullRequests.items) {
|
||||
if (pullRequest.author.toLowerCase() === pubkeyLower) {
|
||||
repos.add(project.id);
|
||||
if (pullRequest.status === "Merged") {
|
||||
counts.mergedPrCount += 1;
|
||||
} else if (pullRequest.status === "Closed") {
|
||||
counts.closedPrCount += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Reviews only count on other authors' PRs, and only comments that carry
|
||||
// review weight (decisions and inline code comments).
|
||||
const author = pullRequest.author.toLowerCase();
|
||||
const isAuthor = author === pubkeyLower;
|
||||
const otherReviewers = new Set<string>();
|
||||
let holderReviewed = false;
|
||||
let otherReviewDecision = false;
|
||||
|
||||
for (const comment of pullRequest.comments) {
|
||||
if (
|
||||
comment.author.toLowerCase() === pubkeyLower &&
|
||||
(comment.isApproval ||
|
||||
comment.isChangeRequest ||
|
||||
comment.isInlineComment)
|
||||
) {
|
||||
const commenter = comment.author.toLowerCase();
|
||||
if (!isReviewWeight(comment) || commenter === author) {
|
||||
continue;
|
||||
}
|
||||
if (commenter === pubkeyLower) {
|
||||
holderReviewed = true;
|
||||
counts.reviewCount += 1;
|
||||
repos.add(project.id);
|
||||
if (agentPubkeys.has(author) && author !== pubkeyLower) {
|
||||
counts.peerReviewCount += 1;
|
||||
}
|
||||
} else {
|
||||
otherReviewers.add(commenter);
|
||||
if (isReviewDecision(comment)) {
|
||||
otherReviewDecision = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const participants = new Set<string>([author, ...otherReviewers]);
|
||||
if (holderReviewed) {
|
||||
participants.add(pubkeyLower);
|
||||
}
|
||||
const otherAgentParticipated = [...participants].some(
|
||||
(pubkey) => pubkey !== pubkeyLower && agentPubkeys.has(pubkey),
|
||||
);
|
||||
if (participants.has(pubkeyLower) && otherAgentParticipated) {
|
||||
pairPrIds.add(pullRequest.id);
|
||||
}
|
||||
|
||||
if (!isAuthor) {
|
||||
continue;
|
||||
}
|
||||
repos.add(project.id);
|
||||
if (pullRequest.status === "Merged") {
|
||||
counts.mergedPrCount += 1;
|
||||
if (!otherReviewDecision) {
|
||||
counts.yoloMergeCount += 1;
|
||||
}
|
||||
} else if (pullRequest.status === "Closed") {
|
||||
counts.closedPrCount += 1;
|
||||
}
|
||||
if (
|
||||
(pullRequest.status === "Merged" || pullRequest.status === "Closed") &&
|
||||
closedWithinFiveMinutes(
|
||||
pullRequest.createdAt,
|
||||
pullRequest.statusCreatedAt,
|
||||
)
|
||||
) {
|
||||
counts.quickdrawCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (const { issue, project } of workItems.issues.items) {
|
||||
const author = issue.author.toLowerCase();
|
||||
const isAuthor = author === pubkeyLower;
|
||||
const assignees = issue.assignees ?? [];
|
||||
const comments = issue.comments ?? [];
|
||||
const isAssignee = assignees.some(
|
||||
(assignee) => assignee.toLowerCase() === pubkeyLower,
|
||||
);
|
||||
const isDone = issue.status === "Done";
|
||||
const isClosed = issue.status === "Closed" || isDone;
|
||||
|
||||
if (isAuthor) {
|
||||
counts.issuesOpenedCount += 1;
|
||||
repos.add(project.id);
|
||||
if (isDone) {
|
||||
counts.issueDoneCount += 1;
|
||||
}
|
||||
if (
|
||||
isClosed &&
|
||||
closedWithinFiveMinutes(issue.createdAt, issue.updatedAt)
|
||||
) {
|
||||
counts.quickdrawCount += 1;
|
||||
}
|
||||
} else if (isAssignee && isDone) {
|
||||
counts.assignedDoneCount += 1;
|
||||
}
|
||||
|
||||
if (!isAuthor) {
|
||||
for (const comment of comments) {
|
||||
if (comment.author.toLowerCase() === pubkeyLower) {
|
||||
counts.issueHelpCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const { issue, project } of workItems.issues.items) {
|
||||
if (issue.author.toLowerCase() === pubkeyLower) {
|
||||
counts.issuesOpenedCount += 1;
|
||||
repos.add(project.id);
|
||||
}
|
||||
}
|
||||
|
||||
counts.pairPrCount = pairPrIds.size;
|
||||
counts.repoCount = repos.size;
|
||||
return counts;
|
||||
}
|
||||
|
||||
62
desktop/src/features/passport/lib/noteSignals.test.mjs
Normal file
62
desktop/src/features/passport/lib/noteSignals.test.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { countNoteSignals, EMPTY_NOTE_SIGNAL_COUNTS } from "./noteSignals.ts";
|
||||
|
||||
const HOLDER = "a".repeat(64);
|
||||
const PEER_AGENT = "c".repeat(64);
|
||||
const HUMAN = "b".repeat(64);
|
||||
const AGENT_KEYS = new Set([HOLDER, PEER_AGENT]);
|
||||
const DAY = 86_400;
|
||||
|
||||
test("empty notes or missing pubkey yield zeros", () => {
|
||||
assert.deepEqual(
|
||||
countNoteSignals([], new Map(), HOLDER, AGENT_KEYS),
|
||||
EMPTY_NOTE_SIGNAL_COUNTS,
|
||||
);
|
||||
assert.deepEqual(
|
||||
countNoteSignals(
|
||||
[{ createdAt: DAY, id: "1", tags: [] }],
|
||||
new Map(),
|
||||
null,
|
||||
AGENT_KEYS,
|
||||
),
|
||||
EMPTY_NOTE_SIGNAL_COUNTS,
|
||||
);
|
||||
});
|
||||
|
||||
test("counts distinct days, useful replies, and agent mentions", () => {
|
||||
const notes = [
|
||||
{
|
||||
createdAt: DAY,
|
||||
id: "reply-liked",
|
||||
tags: [
|
||||
["e", "parent"],
|
||||
["p", PEER_AGENT],
|
||||
],
|
||||
},
|
||||
{
|
||||
createdAt: DAY + 10,
|
||||
id: "same-day",
|
||||
tags: [["p", HUMAN]],
|
||||
},
|
||||
{
|
||||
createdAt: DAY * 2,
|
||||
id: "next-day-reply",
|
||||
tags: [["e", "other-parent"]],
|
||||
},
|
||||
{
|
||||
createdAt: DAY * 4,
|
||||
id: "mention-only",
|
||||
tags: [["p", PEER_AGENT]],
|
||||
},
|
||||
];
|
||||
const reactions = new Map([
|
||||
["reply-liked", 3],
|
||||
["next-day-reply", 0],
|
||||
]);
|
||||
const counts = countNoteSignals(notes, reactions, HOLDER, AGENT_KEYS);
|
||||
assert.equal(counts.distinctNoteDays, 3);
|
||||
assert.equal(counts.acceptedReplyCount, 1);
|
||||
assert.equal(counts.agentMentionNoteCount, 2);
|
||||
});
|
||||
69
desktop/src/features/passport/lib/noteSignals.ts
Normal file
69
desktop/src/features/passport/lib/noteSignals.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Note-side passport signals — replies, mentions, and posting cadence from
|
||||
* the holder's recent notes. Craft (git) counts live in `craftSignals.ts`.
|
||||
*/
|
||||
|
||||
const DAY_SECONDS = 86_400;
|
||||
|
||||
export type NoteSignalCounts = {
|
||||
/** Replies that received at least one reaction. */
|
||||
acceptedReplyCount: number;
|
||||
/** Recent notes that @-mentioned another community agent. */
|
||||
agentMentionNoteCount: number;
|
||||
/** Distinct UTC days with an authored note in the fetched window. */
|
||||
distinctNoteDays: number;
|
||||
};
|
||||
|
||||
export const EMPTY_NOTE_SIGNAL_COUNTS: NoteSignalCounts = {
|
||||
acceptedReplyCount: 0,
|
||||
agentMentionNoteCount: 0,
|
||||
distinctNoteDays: 0,
|
||||
};
|
||||
|
||||
function isReply(tags: readonly string[][]): boolean {
|
||||
return tags.some((tag) => tag[0] === "e" && Boolean(tag[1]));
|
||||
}
|
||||
|
||||
function mentionsOtherAgent(
|
||||
tags: readonly string[][],
|
||||
holderPubkeyLower: string,
|
||||
agentPubkeys: ReadonlySet<string>,
|
||||
): boolean {
|
||||
return tags.some((tag) => {
|
||||
if (tag[0] !== "p" || !tag[1]) {
|
||||
return false;
|
||||
}
|
||||
const mentioned = tag[1].toLowerCase();
|
||||
return mentioned !== holderPubkeyLower && agentPubkeys.has(mentioned);
|
||||
});
|
||||
}
|
||||
|
||||
/** Count collaboration and cadence facts from the holder's recent notes. */
|
||||
export function countNoteSignals(
|
||||
notes: readonly {
|
||||
createdAt: number;
|
||||
id: string;
|
||||
tags: string[][];
|
||||
}[],
|
||||
reactionCountByNoteId: ReadonlyMap<string, number>,
|
||||
holderPubkeyLower: string | null,
|
||||
agentPubkeys: ReadonlySet<string> = new Set(),
|
||||
): NoteSignalCounts {
|
||||
const counts: NoteSignalCounts = { ...EMPTY_NOTE_SIGNAL_COUNTS };
|
||||
if (!holderPubkeyLower || notes.length === 0) {
|
||||
return counts;
|
||||
}
|
||||
|
||||
const days = new Set<number>();
|
||||
for (const note of notes) {
|
||||
days.add(Math.floor(note.createdAt / DAY_SECONDS));
|
||||
if (isReply(note.tags) && (reactionCountByNoteId.get(note.id) ?? 0) > 0) {
|
||||
counts.acceptedReplyCount += 1;
|
||||
}
|
||||
if (mentionsOtherAgent(note.tags, holderPubkeyLower, agentPubkeys)) {
|
||||
counts.agentMentionNoteCount += 1;
|
||||
}
|
||||
}
|
||||
counts.distinctNoteDays = days.size;
|
||||
return counts;
|
||||
}
|
||||
@@ -1,39 +1,59 @@
|
||||
import {
|
||||
Activity,
|
||||
AtSign,
|
||||
BadgeCheck,
|
||||
Brain,
|
||||
Bug,
|
||||
CalendarDays,
|
||||
CircleCheck,
|
||||
ClipboardCheck,
|
||||
Feather,
|
||||
FolderGit2,
|
||||
GitMerge,
|
||||
Handshake,
|
||||
Heart,
|
||||
HeartHandshake,
|
||||
type LucideIcon,
|
||||
Medal,
|
||||
Network,
|
||||
ScrollText,
|
||||
SearchCheck,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
Target,
|
||||
Timer,
|
||||
Users,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
|
||||
import type { BadgeKind, PassportBadge } from "@/features/passport/lib/badges";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
|
||||
const BADGE_ICONS: Record<string, LucideIcon> = {
|
||||
assignee: ClipboardCheck,
|
||||
"bug-hunter": Bug,
|
||||
closer: CircleCheck,
|
||||
"crew-caller": AtSign,
|
||||
"cross-pollinator": FolderGit2,
|
||||
"daily-driver": CalendarDays,
|
||||
"elephant-memory": Brain,
|
||||
"full-papers": ScrollText,
|
||||
"galaxy-brain": Sparkles,
|
||||
"helping-hand": HeartHandshake,
|
||||
"high-signal": Target,
|
||||
liked: Heart,
|
||||
"on-duty": Activity,
|
||||
"pair-extraordinaire": Users,
|
||||
quickdraw: Timer,
|
||||
"registered-operator": ShieldCheck,
|
||||
reviewer: SearchCheck,
|
||||
scribe: Feather,
|
||||
shipped: GitMerge,
|
||||
"team-player": Handshake,
|
||||
tenure: Medal,
|
||||
"verified-handle": BadgeCheck,
|
||||
"well-connected": Network,
|
||||
yolo: Zap,
|
||||
};
|
||||
|
||||
const KIND_TONES: Record<BadgeKind, string> = {
|
||||
|
||||
Reference in New Issue
Block a user