Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d2c4cd6
fix(triage): assign one accountable maintainer when deferring a PR
yiliang114 Aug 24, 2026
0a133a0
feat(triage): assign one area owner when a PR opens
yiliang114 Aug 24, 2026
f943d27
ci: update workflow size baseline
yiliang114 Aug 24, 2026
946b78c
feat(triage): route PRs to the module owner, with the area pool as fa…
yiliang114 Aug 24, 2026
de35631
test(triage): pin the assign-pr-owner bootstrap guard shape and ordering
yiliang114 Aug 24, 2026
560ffa4
test(triage): pin the assign-pr-owner sparse-checkout membership
yiliang114 Aug 24, 2026
8a29341
fix(triage): reject never-matching paths entries in the owner map
yiliang114 Aug 24, 2026
b12091c
test(triage): pin the re-throw of non-permission assign failures
yiliang114 Aug 24, 2026
0690b7b
fix(triage): reject empty paths lists in the owner map
yiliang114 Aug 24, 2026
45e6ca2
test(triage): pin that a label-only area never path-matches
yiliang114 Aug 24, 2026
8f8cf29
test(triage): pin the assign-pr-owner PR_NUMBER and DRY_RUN wiring
yiliang114 Aug 24, 2026
27c65c4
fix(triage): resolve the defer fallback to the newest human reviewer
yiliang114 Aug 24, 2026
06aa457
docs(triage): make the defer resolver env chain and jq filter robust
yiliang114 Aug 25, 2026
9fcb3af
test(triage): pin the privileged trigger and job-level env absence
yiliang114 Aug 25, 2026
14b7ba8
Merge remote-tracking branch 'origin/main' into resolve-9813
yiliang114 Aug 25, 2026
430b4b2
fix(triage): re-check PR coverage immediately before assigning
yiliang114 Aug 25, 2026
3a880a0
docs(triage): reuse the exported open-issue counter in the defer reso…
yiliang114 Aug 25, 2026
81f236d
test(triage): pin the sparse checkout of the statically imported sibling
yiliang114 Aug 25, 2026
cc4841a
test(triage): pin the denied and failing push-access paths
yiliang114 Aug 25, 2026
93ada94
fix(triage): decode changed filenames as base64 before area routing
yiliang114 Aug 25, 2026
e69e250
fix(triage): skip PRs whose author account was deleted
yiliang114 Aug 25, 2026
354b06e
test(triage): route a probe file through every mapped area prefix
yiliang114 Aug 25, 2026
7509821
fix(triage): guard the defer heredoc against a deleted PR author
yiliang114 Aug 25, 2026
1d6e950
Merge origin/main into feat/auto-request-pr-reviewer
yiliang114 Aug 26, 2026
a44cc5a
fix(triage): keep PR owner routing current
yiliang114 Aug 26, 2026
cea16f0
Merge remote-tracking branch 'origin/main' into resolve-9813
yiliang114 Aug 28, 2026
a78d5d5
fix(triage): keep the defer resolver heredoc backtick-free
yiliang114 Aug 28, 2026
fb02777
fix(triage): treat the agent-assignee GraphQL refusal as a graceful skip
yiliang114 Aug 28, 2026
b0dd32e
fix(triage): tolerate a transient issue-list failure in the load fan-out
yiliang114 Aug 28, 2026
5f04fd2
Merge branch 'main' into feat/auto-request-pr-reviewer
yiliang114 Aug 28, 2026
69736f8
fix(triage): don't count dismissed reviews as owner coverage
yiliang114 Aug 28, 2026
9816422
Merge branch 'main' into feat/auto-request-pr-reviewer
yiliang114 Aug 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion .github/issue-owners.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$comment": "Label-driven issue assignment map; areas are keyed on the existing issue label taxonomy. Assignment is a pure function of an issue's labels — no model output is involved. Owners need push access but do NOT need a CODEOWNERS entry; every candidate is re-checked against the collaborator API at write time, so adding a login here cannot grant access to someone who lacks it. Areas match in file order, first match wins. See docs/design/2026-08-07-issue-auto-assignment.md.",
"$comment": "Label-driven issue assignment map; areas are keyed on the existing issue label taxonomy. Assignment is a pure function of an issue's labels — no model output is involved. Owners need push access but do NOT need a CODEOWNERS entry; every candidate is re-checked against the collaborator API at write time, so adding a login here cannot grant access to someone who lacks it. Areas match in file order, first match wins. An area's optional paths list routes PR assignment (assign-pr-owner.mjs) by changed-file prefix, longest prefix wins; the core area's packages/core/ entry is the fallback for paths no module claims. Issue assignment ignores paths; module areas sit after core so label-based issue matching still resolves core first. See docs/design/2026-08-07-issue-auto-assignment.md.",
"requireLabels": ["need-discussion"],
"skipLabels": [
"welcome-pr",
Expand All @@ -13,6 +13,7 @@
{
"name": "core",
"labels": ["category/core", "scope/core"],
"paths": ["packages/core/"],
"owners": [
"wenshao",
"yiliang114",
Expand All @@ -30,6 +31,59 @@
"zjunothing",
"ZijianZhang989"
]
},
{
"name": "core-skills",
"labels": ["scope/core"],
"paths": ["packages/core/src/skills/"],
"owners": ["wenshao"]
},
{
"name": "core-memory",
"labels": ["scope/core"],
"paths": ["packages/core/src/memory/"],
"owners": ["wenshao"]
},
{
"name": "core-goals",
"labels": ["scope/core"],
"paths": ["packages/core/src/goals/"],
"owners": ["qqqys"]
},
{
"name": "core-telemetry",
"labels": ["scope/core"],
"paths": ["packages/core/src/telemetry/"],
"owners": ["zjunothing"]
},
{
"name": "core-extension",
"labels": ["scope/core"],
"paths": ["packages/core/src/extension/"],
"owners": ["callmeYe"]
},
{
"name": "core-agents",
"labels": ["scope/core"],
"paths": ["packages/core/src/agents/"],
"owners": ["qqqys"]
},
{
"name": "core-config",
"labels": ["scope/core"],
"paths": ["packages/core/src/config/"],
"owners": ["qqqys"]
},
{
"name": "core-runtime",
"labels": ["scope/core"],
"paths": [
"packages/core/src/core/",
"packages/core/src/services/",
"packages/core/src/tools/",
"packages/core/src/utils/"
],
"owners": ["yiliang114"]
}
]
}
37 changes: 36 additions & 1 deletion .github/scripts/assign-issue-owner.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ function isStringArray(value) {
return Array.isArray(value) && value.every((v) => typeof v === 'string');
}

// A changed-file prefix for assign-pr-owner.mjs: relative, no `//`, no
// backslash, no `.`/`..` segments, and ending in `/` so startsWith cannot
// leak into a sibling directory (packages/core matching packages/coredump/).
function isPathPrefix(prefix) {
if (typeof prefix !== 'string' || prefix.length === 0) return false;
if (prefix.startsWith('/') || prefix.startsWith('./')) return false;
if (!prefix.endsWith('/')) return false;
if (prefix.includes('\\') || prefix.includes('//')) return false;
return !prefix
.split('/')
.some((segment) => segment === '.' || segment === '..');
}

export function loadPolicy(raw) {
const policy = JSON.parse(raw);
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
Expand Down Expand Up @@ -74,6 +87,28 @@ export function loadPolicy(raw) {
}
seen.add(normalizedOwner);
}
// A never-matching paths entry silently unroutes the area from PR
// assignment, so reject it here like other malformed config.
if (area.paths !== undefined && !Array.isArray(area.paths)) {
throw new Error(
`${OWNERS_FILE}: area ${area.name} paths must be an array`,
);
}
// An explicitly empty list can never route the area either, yet the
// entry loop below cannot catch it — reject it like the sibling
// labels/owners checks do.
if (Array.isArray(area.paths) && area.paths.length === 0) {
throw new Error(
`${OWNERS_FILE}: area ${area.name} paths must not be empty; omit paths for a label-only area`,
);
}
for (const prefix of area.paths ?? []) {
if (!isPathPrefix(prefix)) {
Comment thread
yiliang114 marked this conversation as resolved.
throw new Error(
`${OWNERS_FILE}: invalid paths entry in ${area.name}: ${JSON.stringify(prefix)}`,
);
}
}
}
return policy;
}
Expand Down Expand Up @@ -151,7 +186,7 @@ function canWrite(repository, login) {
}
}

function openIssueCount(repository, login) {
export function openIssueCount(repository, login) {
return Number(
gh([
'issue',
Expand Down
37 changes: 37 additions & 0 deletions .github/scripts/assign-issue-owner.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,43 @@ describe('assign-issue-owner: owner map', () => {
broken.areas[0].owners = [42];
assert.throws(() => loadPolicy(JSON.stringify(broken)), /invalid login/);
});

it('rejects paths entries that could never route the area', () => {
// startsWith matching can never honour these spellings; accepting them
// would silently unroute the area from PR assignment forever.
for (const paths of [
['./packages/core/'],
['packages//core/'],
['.github/../packages/core/'],
['/packages/core/'],
['packages/core'],
['packages\\core/'],
[''],
[42],
]) {
const broken = JSON.parse(ownersRaw);
broken.areas[0].paths = paths;
assert.throws(
() => loadPolicy(JSON.stringify(broken)),
/invalid paths entry/,
);
}

const notArray = JSON.parse(ownersRaw);
notArray.areas[0].paths = 'packages/core/';
assert.throws(
() => loadPolicy(JSON.stringify(notArray)),
/paths must be an array/,
);

// An explicitly empty list can never route the area either.
const emptyPaths = JSON.parse(ownersRaw);
emptyPaths.areas[0].paths = [];
assert.throws(
() => loadPolicy(JSON.stringify(emptyPaths)),
/paths must not be empty/,
);
});
});

describe('assign-issue-owner: skip policy', () => {
Expand Down
Loading
Loading