Skip to content

Commit a761a4b

Browse files
authored
feat: validate agent issue generator and canonicalize labels (#37)
- .github/labels.yml: add type:agent-task, role:*, status:open-claim, good-first-agent-task as canonical labels (single source of truth) - validate-repo.mjs: validateAgentIssueGenerator() asserts every task produces a well-formed title, >=4 labels all defined in labels.yml, and a body containing the done condition + problem link - generate-agent-issues.mjs: export builders; guard CLI for safe import Closes the board/labels drift gap noted in the prior PR.
1 parent 3482061 commit a761a4b

3 files changed

Lines changed: 94 additions & 24 deletions

File tree

‎.github/labels.yml‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,30 @@
2222
- name: "type:safety"
2323
color: "B60205"
2424
description: "Safety flag or high-risk misuse concern."
25+
- name: "type:agent-task"
26+
color: "1D76DB"
27+
description: "Scoped task published as an issue for an AI agent to pick up."
28+
- name: "good-first-agent-task"
29+
color: "7057FF"
30+
description: "Low-risk entry point for a new AI agent contributor."
31+
- name: "role:literature-scout"
32+
color: "5319E7"
33+
description: "Source inventory and evidence classification."
34+
- name: "role:data-cleaner"
35+
color: "5319E7"
36+
description: "Dataset provenance and cleaning."
37+
- name: "role:implementation-planner"
38+
color: "5319E7"
39+
description: "Task decomposition and back-test specs."
40+
- name: "role:red-team-reviewer"
41+
color: "B60205"
42+
description: "Failure mode and misuse analysis."
43+
- name: "role:field-reality-reviewer"
44+
color: "0052CC"
45+
description: "Operational relevance check."
46+
- name: "status:open-claim"
47+
color: "C5DEF5"
48+
description: "Unclaimed agent task, available to pick up."
2549
- name: "status:needs-triage"
2650
color: "C5DEF5"
2751
description: "Needs maintainer triage before work starts."

‎scripts/generate-agent-issues.mjs‎

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { dirname, join } from "node:path";
1919
const __dirname = dirname(fileURLToPath(import.meta.url));
2020
const ROOT = join(__dirname, "..");
2121

22-
function loadTasks() {
22+
export function loadTasks() {
2323
const raw = readFileSync(join(ROOT, "tasks-available.json"), "utf8");
2424
return JSON.parse(raw).tasks;
2525
}
@@ -34,11 +34,11 @@ const ROLE_GUIDE = {
3434

3535
const REPO = "https://github.com/Open-Problem-Lab/open-problem-lab/blob/main";
3636

37-
function issueTitle(task) {
37+
export function issueTitle(task) {
3838
return `[AGENT-TASK] ${task.pack_title}: ${task.title}`;
3939
}
4040

41-
function issueLabels(task) {
41+
export function issueLabels(task) {
4242
const labels = [
4343
"type:agent-task",
4444
"status:open-claim",
@@ -49,7 +49,7 @@ function issueLabels(task) {
4949
return labels;
5050
}
5151

52-
function issueBody(task) {
52+
export function issueBody(task) {
5353
const roleGuide = ROLE_GUIDE[task.owner_role] || "AGENTS.md";
5454
const domains = task.domain.join(", ");
5555
const regions = (task.region || []).join(", ") || "global";
@@ -95,7 +95,7 @@ This is one scoped step inside a neglected global problem. The repo's scarce res
9595
🤖 Generated from \`tasks-available.json\` by \`scripts/generate-agent-issues.mjs\`. Source of truth is the pack's \`tasks.json\`.`;
9696
}
9797

98-
function diversePicks(tasks, n) {
98+
export function diversePicks(tasks, n) {
9999
const perDomain = Math.max(1, Math.ceil(n / 11));
100100
const cap = {};
101101
const picks = [];
@@ -115,25 +115,30 @@ function diversePicks(tasks, n) {
115115
return picks;
116116
}
117117

118-
const args = process.argv.slice(2);
119-
const tasks = loadTasks();
120-
121-
if (args[0] === "--list") {
122-
for (const t of tasks) console.log(`${t.pack_id}\t${t.task_id}`);
123-
} else if (args[0] === "--body") {
124-
const t = tasks.find((x) => x.pack_id === args[1]);
125-
if (!t) {
126-
console.error(`No task for pack_id ${args[1]}`);
118+
// Only run the CLI when executed directly, not when imported (e.g. by the validator).
119+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
120+
const args = process.argv.slice(2);
121+
const tasks = loadTasks();
122+
123+
if (args[0] === "--list") {
124+
for (const t of tasks) console.log(`${t.pack_id}\t${t.task_id}`);
125+
} else if (args[0] === "--body") {
126+
const t = tasks.find((x) => x.pack_id === args[1]);
127+
if (!t) {
128+
console.error(`No task for pack_id ${args[1]}`);
129+
process.exit(1);
130+
}
131+
console.log(`TITLE\t${issueTitle(t)}`);
132+
console.log(`LABELS\t${issueLabels(t).join(",")}`);
133+
console.log("BODY");
134+
console.log(issueBody(t));
135+
} else if (args[0] === "--plan") {
136+
const n = Number(args[1]) || 24;
137+
for (const t of diversePicks(tasks, n)) console.log(t.pack_id);
138+
} else {
139+
console.error(
140+
"Usage: generate-agent-issues.mjs --list | --body <pack_id> | --plan [n]"
141+
);
127142
process.exit(1);
128143
}
129-
console.log(`TITLE\t${issueTitle(t)}`);
130-
console.log(`LABELS\t${issueLabels(t).join(",")}`);
131-
console.log("BODY");
132-
console.log(issueBody(t));
133-
} else if (args[0] === "--plan") {
134-
const n = Number(args[1]) || 24;
135-
for (const t of diversePicks(tasks, n)) console.log(t.pack_id);
136-
} else {
137-
console.error("Usage: generate-agent-issues.mjs --list | --body <pack_id> | --plan [n]");
138-
process.exit(1);
139144
}

‎scripts/validate-repo.mjs‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ const STATIC_LABELS = [
3131
"type:playbook",
3232
"type:review",
3333
"type:safety",
34+
"type:agent-task",
35+
"good-first-agent-task",
36+
"role:literature-scout",
37+
"role:data-cleaner",
38+
"role:implementation-planner",
39+
"role:red-team-reviewer",
40+
"role:field-reality-reviewer",
41+
"status:open-claim",
3442
"status:needs-triage",
3543
"status:scoped",
3644
"status:agent-working",
@@ -195,6 +203,38 @@ const validateLabels = async () => {
195203
}
196204
};
197205

206+
const validateAgentIssueGenerator = async () => {
207+
const { loadTasks, issueTitle, issueLabels, issueBody } =
208+
await import("./generate-agent-issues.mjs");
209+
const labelsPath = path.join(root, ".github", "labels.yml");
210+
const labelsContent = await fs.readFile(labelsPath, "utf8");
211+
const knownLabels = new Set(
212+
[...labelsContent.matchAll(/^\s*-\s+name:\s*"?([^"\n]+)"?\s*$/gm)].map((m) => m[1])
213+
);
214+
const tasks = loadTasks();
215+
assert(tasks.length > 0, "Agent issue generator found no tasks in tasks-available.json.");
216+
for (const task of tasks) {
217+
const title = issueTitle(task);
218+
assert(
219+
title.startsWith("[AGENT-TASK] ") && title.length > 14,
220+
`Agent issue title is empty or malformed for ${task.pack_id}.`
221+
);
222+
const labels = issueLabels(task);
223+
assert(labels.length >= 4, `Agent issue for ${task.pack_id} has too few labels.`);
224+
for (const label of labels) {
225+
assert(
226+
knownLabels.has(label),
227+
`Agent issue for ${task.pack_id} uses label "${label}" not defined in .github/labels.yml.`
228+
);
229+
}
230+
const body = issueBody(task);
231+
assert(
232+
body.includes(task.done_condition) && body.includes(task.problem_file),
233+
`Agent issue body for ${task.pack_id} is missing its done condition or problem link.`
234+
);
235+
}
236+
};
237+
198238
const validateMarkdown = async () => {
199239
const markdownFiles = await walkFiles(root, (file) => file.endsWith(".md"));
200240
assert(markdownFiles.length > 0, "Markdown documentation is required.");
@@ -285,6 +325,7 @@ const main = async () => {
285325
await validateExamples(schemas);
286326
await validateIssueTemplates();
287327
await validateLabels();
328+
await validateAgentIssueGenerator();
288329
await validateMarkdown();
289330
await validateWikiFreshness();
290331
await validateTaskIndexFreshness();

0 commit comments

Comments
 (0)