#!/usr/bin/env node import { execFileSync } from "node:child_process"; const REPO = "openclaw/openclaw"; const APPLY = process.argv.includes("--apply"); const COLORS = { saturatedRed: "B60205", saturatedBugRed: "D73A4A", saturatedOrangeRed: "D93F0B", saturatedAmber: "FBCA04", softerAmber: "F9D65C", paleYellow: "F7E7A1", saturatedGreen: "0E8A16", mutedGreen: "8C959F", paleGreen: "D6E3DA", proofGreen: "2DA44E", mutedProofGreen: "1A7F37", overrideGreen: "2DA44E", saturatedBlue: "0F2CCE", paleBlue: "0A3069", channelBlue: "0969DA", dedupeBlue: "57606A", triageBlue: "0969DA", saturatedPurple: "7057FF", mutedPurple: "57606A", taxonomyGray: "6E7781", taxonomySteel: "57606A", appPurple: "6E7781", neutralGray: "E5E7EB", duplicateGray: "D1D5DB", darkGray: "8C8C8C", mutedRose: "8C959F", mutedRed: "E99695", black: "000000", white: "FFFFFF", clawsweeperOrange: "F97316", helpGreen: "008672", maintainerYellow: "FFD700", }; const EXACT_COLORS = new Map( Object.entries({ P0: COLORS.saturatedRed, P1: COLORS.saturatedOrangeRed, P2: COLORS.saturatedAmber, P3: COLORS.mutedGreen, "rating: 🦀 challenger crab": "1F883D", "rating: 🦞 diamond lobster": "0969DA", "rating: 🐚 platinum hermit": "0F766E", "rating: 🦐 gold shrimp": "B7791F", "rating: 🦪 silver shellfish": "7A828E", "rating: 🧂 unranked krab": "8C2F39", "rating: 🌊 off-meta tidepool": "6E7781", "impact:data-loss": COLORS.saturatedRed, "impact:security": COLORS.saturatedRed, "impact:crash-loop": COLORS.saturatedOrangeRed, "impact:message-loss": COLORS.saturatedOrangeRed, "impact:auth-provider": COLORS.softerAmber, "impact:session-state": COLORS.softerAmber, bug: COLORS.saturatedBugRed, "bug:crash": COLORS.saturatedRed, "bug:behavior": COLORS.saturatedBugRed, regression: COLORS.saturatedOrangeRed, security: COLORS.saturatedRed, "beta-blocker": COLORS.saturatedOrangeRed, "proof: supplied": COLORS.proofGreen, "proof: sufficient": COLORS.mutedProofGreen, "proof: override": COLORS.overrideGreen, "triage:blocked": COLORS.saturatedAmber, "triage:bug": COLORS.saturatedBugRed, "triage:done": COLORS.mutedGreen, "triage:needs-review": COLORS.paleBlue, "triage:started": COLORS.mutedPurple, agents: COLORS.taxonomySteel, docs: COLORS.paleBlue, cli: COLORS.paleBlue, commands: COLORS.paleBlue, scripts: COLORS.mutedPurple, gateway: COLORS.mutedPurple, codex: COLORS.taxonomySteel, docker: COLORS.paleGreen, tui: COLORS.paleGreen, "extensions: NEW": COLORS.channelBlue, clawsweeper: COLORS.clawsweeperOrange, "clawsweeper:human-review": COLORS.saturatedRed, "clawsweeper:needs-security-review": COLORS.saturatedRed, "clawsweeper:needs-live-repro": COLORS.saturatedAmber, "clawsweeper:needs-maintainer-review": COLORS.saturatedAmber, "clawsweeper:needs-product-decision": COLORS.saturatedAmber, "clawsweeper:automerge": COLORS.saturatedGreen, "clawsweeper:queueable-fix": COLORS.saturatedGreen, "clawsweeper:fix-shape-clear": COLORS.mutedProofGreen, "clawsweeper:autofix": COLORS.paleBlue, "clawsweeper:commit-finding": COLORS.paleBlue, "clawsweeper:autogenerated": COLORS.mutedPurple, "clawsweeper:linked-pr-open": COLORS.mutedPurple, "clawsweeper:merge-ready": COLORS.mutedPurple, "clawsweeper:current-main-repro": COLORS.paleBlue, "clawsweeper:source-repro": COLORS.paleBlue, "clawsweeper:not-repro-on-main": COLORS.proofGreen, "clawsweeper:no-new-fix-pr": COLORS.neutralGray, "clawsweeper:needs-info": COLORS.appPurple, "close:spam": COLORS.black, "close:duplicate": COLORS.mutedRed, "close:already-fixed": COLORS.paleBlue, "close:cannot-repro": COLORS.paleYellow, "close:invalid": COLORS.neutralGray, "close:not-planned": COLORS.darkGray, "close:superseded": COLORS.mutedPurple, duplicate: COLORS.duplicateGray, invalid: COLORS.paleYellow, wontfix: COLORS.white, "r: spam": COLORS.saturatedRed, "r: moltbook": COLORS.saturatedRed, "r: too-many-prs": COLORS.saturatedOrangeRed, "r: no-ci-pr": COLORS.saturatedOrangeRed, "r: bluebubbles": COLORS.saturatedOrangeRed, "r: testflight": COLORS.saturatedOrangeRed, "r: false-positive": COLORS.saturatedOrangeRed, "r: skill": COLORS.mutedPurple, "r: third-party-extension": COLORS.mutedPurple, "r: support": COLORS.mutedGreen, "r: too-many-prs-override": COLORS.overrideGreen, maintainer: COLORS.maintainerYellow, "trusted-contributor": COLORS.neutralGray, "help wanted": COLORS.helpGreen, "good first issue": COLORS.saturatedPurple, "In Progress": COLORS.saturatedBlue, "no-stale": COLORS.mutedGreen, stale: COLORS.paleYellow, "trigger-response": COLORS.saturatedAmber, "bad-barnacle": COLORS.mutedRed, clownfish: COLORS.neutralGray, "mantis: telegram-visible-proof": COLORS.mutedPurple, "dedupe:parent": COLORS.dedupeBlue, "dedupe:child": COLORS.paleBlue, dependencies: COLORS.neutralGray, "dependencies-changed": COLORS.paleYellow, github_actions: COLORS.neutralGray, go: COLORS.neutralGray, java: COLORS.neutralGray, swift_package_manager: COLORS.neutralGray, "cannot-reproduce": COLORS.dedupeBlue, question: COLORS.appPurple, enhancement: COLORS.channelBlue, "pi-issue": COLORS.saturatedOrangeRed, aardvark: COLORS.neutralGray, "cohort-4": COLORS.mutedPurple, dirty: COLORS.saturatedRed, }), ); const FAMILY_RULES = [ { family: "channel", match: (name) => name.startsWith("channel: "), color: COLORS.channelBlue, reason: "routine channel taxonomy stays visually quiet", }, { family: "app", match: (name) => name.startsWith("app: "), color: COLORS.appPurple, reason: "app platform taxonomy stays muted", }, { family: "extension", match: (name) => name.startsWith("extensions: "), color: COLORS.taxonomyGray, reason: "plugin implementation taxonomy should not compete with priority", }, { family: "plugin", match: (name) => name.startsWith("plugin: "), color: COLORS.taxonomyGray, reason: "plugin taxonomy stays neutral unless it becomes an action gate", }, { family: "size", match: (name) => name.startsWith("size: "), color: COLORS.mutedRose, reason: "size text carries the value, so one muted family color is enough", }, { family: "triage-candidate", match: (name) => name.startsWith("triage:"), color: COLORS.triageBlue, reason: "routine triage candidates stay softer than blockers and priorities", }, ]; function wantedPolicy(name) { if (EXACT_COLORS.has(name)) { return { color: EXACT_COLORS.get(name), family: exactFamily(name), source: "exact" }; } const rule = FAMILY_RULES.find((candidate) => candidate.match(name)); if (rule) { return { color: rule.color, family: rule.family, source: "family" }; } return null; } function exactFamily(name) { if (/^P[0-3]$/.test(name)) { return "priority"; } if (name.startsWith("rating:")) { return "rating"; } if (name.startsWith("impact:")) { return "impact"; } if (name.startsWith("clawsweeper")) { return "clawsweeper"; } if (name.startsWith("close:") || name.startsWith("r:")) { return "resolution"; } if (name.startsWith("proof:")) { return "proof"; } if (name.startsWith("triage:")) { return "triage-status"; } if (name.startsWith("bug") || name === "security" || name === "regression") { return "risk"; } return "specific"; } function ghJson(args) { return JSON.parse(execFileSync("gh", args, { encoding: "utf8" })); } function gh(args) { execFileSync("gh", args, { encoding: "utf8", stdio: APPLY ? "inherit" : "pipe" }); } function fetchLabels() { const labels = []; let cursor = null; for (;;) { const query = `query($cursor:String) { repository(owner:"openclaw", name:"openclaw") { labels(first:100, after:$cursor, orderBy:{field:NAME,direction:ASC}) { nodes { name color description } pageInfo { hasNextPage endCursor } } } }`; const response = ghJson([ "api", "graphql", "-f", `query=${query}`, "-f", `cursor=${cursor ?? ""}`, ]); const page = response.data.repository.labels; labels.push(...page.nodes); if (!page.pageInfo.hasNextPage) { return labels; } cursor = page.pageInfo.endCursor; } } const changes = fetchLabels() .map((label) => ({ name: label.name, current: label.color.toUpperCase(), policy: wantedPolicy(label.name), })) .filter((label) => label.policy && label.current !== label.policy.color) .toSorted((a, b) => a.name.localeCompare(b.name)); if (changes.length === 0) { console.log("No label color changes needed."); process.exit(0); } const familyCounts = new Map(); for (const change of changes) { familyCounts.set(change.policy.family, (familyCounts.get(change.policy.family) ?? 0) + 1); console.log( `${APPLY ? "update" : "would update"} [${change.policy.family}] ${change.name}: ${change.current} -> ${change.policy.color}`, ); if (APPLY) { gh(["label", "edit", change.name, "--repo", REPO, "--color", change.policy.color]); } } console.log("Family summary:"); for (const [family, count] of [...familyCounts.entries()].toSorted(([a], [b]) => a.localeCompare(b), )) { console.log(`- ${family}: ${count}`); } console.log(`${APPLY ? "Updated" : "Would update"} ${changes.length} labels.`);