mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-13 15:44:56 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
129
script/triage-unassigned.ts
Normal 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()
|
||||
Reference in New Issue
Block a user