core: simplify triage workflow to focus on issue ownership

Switch triage agent to gpt-5.4-nano for faster issue assignment. Remove label
management from the triage tool so it only assigns owners based on team
ownership rules. This reduces noise in the issue tracker and ensures issues
get to the right team member immediately without unnecessary labels.

Update team structures to reflect current ownership and add script for
processing unassigned issues.
This commit is contained in:
Dax Raad
2026-05-03 01:21:17 -04:00
parent 7ccab8d272
commit a08e4c9651
3 changed files with 146 additions and 87 deletions

View File

@@ -1,7 +1,7 @@
---
mode: primary
hidden: true
model: opencode/qwen3.6-plus
model: opencode/gpt-5.4-nano
color: "#44BA81"
tools:
"*": false
@@ -14,7 +14,11 @@ Use your github-triage tool to triage issues.
This file is the source of truth for ownership/routing rules.
Assign issues by choosing the team with the strongest overlap, then assign a member from that team at random.
Assign issues by choosing the team with the strongest overlap. The github-triage tool will assign a random member from that team.
Do not add labels to issues. Only assign an owner.
When calling github-triage, pass one of these team values: tui, desktop_web, core, inference, windows.
## Teams
@@ -22,34 +26,18 @@ Assign issues by choosing the team with the strongest overlap, then assign a mem
Terminal UI issues, including rendering, keybindings, scrolling, terminal compatibility, SSH behavior, crashes in the TUI, and low-level TUI performance.
- kommander
- simonklee
### Desktop / Web
Desktop application and browser-based app issues, including `opencode web`, desktop-specific UI behavior, packaging, and web view problems.
- Hona
- Brendonovich
### Core
Core opencode server and harness issues, including sqlite, snapshots, memory, API behavior, agent context construction, tool execution, provider integrations, model behavior, and larger architectural features.
- jlongster
- rekram1-node
- nexxeln
- kitlangton
Core opencode server and harness issues, including sqlite, snapshots, memory, API behavior, agent context construction, tool execution, provider integrations, model behavior, documentation, and larger architectural features.
### Inference
OpenCode Zen, OpenCode Go, and billing issues.
- fwang
- MrMushrooooom
### Windows
Windows-specific issues, including native Windows behavior, WSL interactions, path handling, shell compatibility, and installation or runtime problems that only happen on Windows.
- Hona

View File

@@ -1,16 +1,14 @@
/// <reference path="../env.d.ts" />
import { tool } from "@opencode-ai/plugin"
const TEAM = {
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
zen: ["fwang", "MrMushrooooom"],
tui: ["kommander", "rekram1-node", "simonklee"],
core: ["kitlangton", "rekram1-node", "jlongster"],
docs: ["R44VC0RP"],
tui: ["kommander", "simonklee"],
desktop_web: ["Hona", "Brendonovich"],
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
inference: ["fwang", "MrMushrooooom"],
windows: ["Hona"],
} as const
const ASSIGNEES = [...new Set(Object.values(TEAM).flat())]
function pick<T>(items: readonly T[]) {
return items[Math.floor(Math.random() * items.length)]!
}
@@ -38,79 +36,23 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
}
export default tool({
description: `Use this tool to assign and/or label a GitHub issue.
description: `Use this tool to assign a GitHub issue.
Choose labels and assignee using the current triage policy and ownership rules.
Pick the most fitting labels for the issue and assign one owner.
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.`,
Provide the team that should own the issue. This tool picks a random assignee from that team and does not apply labels.`,
args: {
assignee: tool.schema
.enum(ASSIGNEES as [string, ...string[]])
.describe("The username of the assignee")
.default("rekram1-node"),
labels: tool.schema
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
.describe("The labels(s) to add to the issue")
.default([]),
team: tool.schema.enum(Object.keys(TEAM) as [keyof typeof TEAM, ...(keyof typeof TEAM)[]]).describe("The owning team"),
},
async execute(args) {
const issue = getIssueNumber()
const owner = "anomalyco"
const repo = "opencode"
const results: string[] = []
let labels = [...new Set(args.labels.map((x) => (x === "desktop" ? "web" : x)))]
const web = labels.includes("web")
const text = `${process.env.ISSUE_TITLE ?? ""}\n${process.env.ISSUE_BODY ?? ""}`.toLowerCase()
const zen = /\bzen\b/.test(text) || text.includes("opencode black")
const nix = /\bnix(os)?\b/.test(text)
if (labels.includes("nix") && !nix) {
labels = labels.filter((x) => x !== "nix")
results.push("Dropped label: nix (issue does not mention nix)")
}
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
if (labels.includes("zen") && !zen) {
throw new Error("Only add the zen label when issue title/body contains 'zen'")
}
if (web && !nix && !(TEAM.desktop as readonly string[]).includes(assignee)) {
throw new Error("Web issues must be assigned to adamdotdevin, iamdavidhill, Brendonovich, or nexxeln")
}
if ((TEAM.zen as readonly string[]).includes(assignee) && !labels.includes("zen")) {
throw new Error("Only zen issues should be assigned to fwang or MrMushrooooom")
}
if (assignee === "Hona" && !labels.includes("windows")) {
throw new Error("Only windows issues should be assigned to Hona")
}
if (assignee === "R44VC0RP" && !labels.includes("docs")) {
throw new Error("Only docs issues should be assigned to R44VC0RP")
}
if (assignee === "kommander" && !labels.includes("opentui")) {
throw new Error("Only opentui issues should be assigned to kommander")
}
const assignee = pick(TEAM[args.team])
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
method: "POST",
body: JSON.stringify({ assignees: [assignee] }),
})
results.push(`Assigned @${assignee} to issue #${issue}`)
if (labels.length > 0) {
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, {
method: "POST",
body: JSON.stringify({ labels }),
})
results.push(`Added labels: ${labels.join(", ")}`)
}
return results.join("\n")
return `Assigned @${assignee} from ${args.team} to issue #${issue}`
},
})

129
script/triage-unassigned.ts Normal file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env bun
import { parseArgs } from "util"
async function run(command: string, args: string[], options: Bun.SpawnOptions.OptionsObject = {}) {
const process = Bun.spawn([command, ...args], options)
const status = await process.exited
if (status !== 0) throw new Error(`${command} ${args.join(" ")} exited with ${status}`)
return process
}
async function text(command: string, args: string[]) {
const process = await run(command, args, { stdout: "pipe", stderr: "inherit" })
return new Response(process.stdout).text()
}
async function main() {
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
days: { type: "string", short: "d", default: "30" },
limit: { type: "string", short: "l", default: "200" },
"dry-run": { type: "boolean", default: false },
help: { type: "boolean", short: "h", default: false },
},
})
if (values.help) {
console.log(`
Usage: bun script/triage-unassigned.ts [options]
Triage open GitHub issues created in the last 30 days with no assignee.
Options:
-d, --days <days> Look back this many days (default: 30)
-l, --limit <count> Maximum issues to process (default: 200)
--dry-run Print matching issues without running triage
-h, --help Show this help message
Examples:
bun script/triage-unassigned.ts
bun script/triage-unassigned.ts --limit 3
bun script/triage-unassigned.ts --dry-run
`)
process.exit(0)
}
const days = Number(values.days)
const limit = Number(values.limit)
if (!Number.isInteger(days) || days < 1) throw new Error("--days must be a positive integer")
if (!Number.isInteger(limit) || limit < 1) throw new Error("--limit must be a positive integer")
const created = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().slice(0, 10)
const query = `no:assignee created:>=${created}`
const issues = JSON.parse(
await text("gh", [
"issue",
"list",
"--state",
"open",
"--search",
query,
"--limit",
String(limit),
"--json",
"number,title,body",
]),
) as Array<{ number: number; title: string; body?: string | null }>
console.log(`Found ${issues.length} open unassigned issues created since ${created}`)
if (issues.length === 0) return
if (values["dry-run"]) {
for (const issue of issues) console.log(`#${issue.number} ${issue.title}`)
return
}
const githubToken = process.env.GITHUB_TOKEN || (await text("gh", ["auth", "token"])).trim()
const failures: Array<{ issue: number; error: string }> = []
for (const [index, issue] of issues.entries()) {
console.log(`\n[${index + 1}/${issues.length}] Triaging #${issue.number} ${issue.title}`)
const result = Bun.spawn(
[
"opencode",
"run",
"--agent",
"triage",
`The following issue was just opened, triage it:
Issue: #${issue.number}
Title: ${issue.title}
Body:
${issue.body ?? ""}`,
],
{
env: {
...process.env,
GITHUB_TOKEN: githubToken,
ISSUE_NUMBER: String(issue.number),
ISSUE_TITLE: issue.title,
ISSUE_BODY: issue.body ?? "",
},
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
},
)
const status = await result.exited
if (status === 0) {
console.log(`[${index + 1}/${issues.length}] Done #${issue.number}`)
continue
}
failures.push({ issue: issue.number, error: `opencode exited with ${status}` })
console.error(`[${index + 1}/${issues.length}] Failed #${issue.number}: opencode exited with ${status}`)
}
console.log(`\nFinished triaging ${issues.length - failures.length}/${issues.length} issues`)
if (failures.length === 0) return
console.error("Failures:")
for (const failure of failures) console.error(`#${failure.issue}: ${failure.error}`)
process.exit(1)
}
void main()