diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 30748cad6d..59444ad94f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,83 +2,84 @@ # .github/GOVERNANCE.md ("Maintainer And Review Roster", "Review Service Levels"). # # GitHub applies the LAST matching pattern, so this file runs from broad to -# narrow: fallback, module map, subsystem routes, then lead-owned governance -# paths, which must win. When a line lists several owners, approval from any -# one of them satisfies code-owner review. +# narrow: fallback, module map, subsystem routes, then governance paths. +# @loopx-agent shares every route; narrower rules must retain that owner. +# When a line lists several owners, approval from any one of them satisfies +# code-owner review. # # Only accounts with repository write access and an accepted, publicly recorded # scope are listed. First-review contacts, pending invitations and read-only # contributors are routed through GOVERNANCE.md, not through this file. # Fallback for paths no module line claims, such as a new top-level directory. -* @huangruiteng +* @huangruiteng @loopx-agent # --- Module map -------------------------------------------------------------- # Every top-level module has an explicit line so ownership is visible per # module. A subsystem appointment narrows a path in the next section. # Control-plane kernel: authority, state, quota, todos, leases, TS transactions. -/loopx/control_plane/ @huangruiteng -/loopx/semantics/ @huangruiteng -/loopx/*.py @huangruiteng +/loopx/control_plane/ @huangruiteng @loopx-agent +/loopx/semantics/ @huangruiteng @loopx-agent +/loopx/*.py @huangruiteng @loopx-agent # CLI surface. -/loopx/cli_commands/ @huangruiteng +/loopx/cli_commands/ @huangruiteng @loopx-agent # Capabilities, extensions, domain packs and experiments. -/loopx/capabilities/ @huangruiteng -/loopx/extensions/ @huangruiteng -/loopx/domain_packs/ @huangruiteng -/loopx/experiments/ @huangruiteng +/loopx/capabilities/ @huangruiteng @loopx-agent +/loopx/extensions/ @huangruiteng @loopx-agent +/loopx/domain_packs/ @huangruiteng @loopx-agent +/loopx/experiments/ @huangruiteng @loopx-agent # Host goal-mode adapters (Claude, DSH, OpenCode, Pi, ...). -/loopx/*_goal_mode/ @huangruiteng +/loopx/*_goal_mode/ @huangruiteng @loopx-agent # Presentation: Python projections, dashboard, site and desktop shell. -/loopx/presentation/ @huangruiteng -/apps/presentation/ @huangruiteng -/apps/desktop/ @huangruiteng +/loopx/presentation/ @huangruiteng @loopx-agent +/apps/presentation/ @huangruiteng @loopx-agent +/apps/desktop/ @huangruiteng @loopx-agent # Canary metrics, benchmarks and regression corpora. -/loopx/canary/ @huangruiteng -/benchmark/ @huangruiteng -/regression/ @huangruiteng +/loopx/canary/ @huangruiteng @loopx-agent +/benchmark/ @huangruiteng @loopx-agent +/regression/ @huangruiteng @loopx-agent # Optional provider packages. -/packages/ @huangruiteng +/packages/ @huangruiteng @loopx-agent # Agent skills, man pages, docs, examples, tests, scripts and demos. -/skills/ @huangruiteng -/man/ @huangruiteng -/docs/ @huangruiteng -/examples/ @huangruiteng -/tests/ @huangruiteng -/scripts/ @huangruiteng -/demo/ @huangruiteng -/deprecate/ @huangruiteng +/skills/ @huangruiteng @loopx-agent +/man/ @huangruiteng @loopx-agent +/docs/ @huangruiteng @loopx-agent +/examples/ @huangruiteng @loopx-agent +/tests/ @huangruiteng @loopx-agent +/scripts/ @huangruiteng @loopx-agent +/demo/ @huangruiteng @loopx-agent +/deprecate/ @huangruiteng @loopx-agent # --- Subsystem routes -------------------------------------------------------- # Frontend source and chat bundle delivery (built assets are not tracked). -/apps/presentation/dashboard/ @huangruiteng @maxliux5 -/loopx/presentation/chat_bundle.py @huangruiteng @maxliux5 -/scripts/chat_bundle* @huangruiteng @maxliux5 +/apps/presentation/dashboard/ @huangruiteng @maxliux5 @loopx-agent +/loopx/presentation/chat_bundle.py @huangruiteng @maxliux5 @loopx-agent +/scripts/chat_bundle* @huangruiteng @maxliux5 @loopx-agent # Lark integration (subsystem maintainer: @steven-kid). -/loopx/extensions/lark/ @huangruiteng @steven-kid -/loopx/cli_commands/lark_*.py @huangruiteng @steven-kid -/docs/integrations/lark-*.md @huangruiteng @steven-kid -/docs/reference/protocols/lark-*.md @huangruiteng @steven-kid -/examples/lark-* @huangruiteng @steven-kid -/examples/lark_extension_test_support.py @huangruiteng @steven-kid -/examples/control_plane/lark-* @huangruiteng @steven-kid -/tests/extensions/test_lark_*.py @huangruiteng @steven-kid +/loopx/extensions/lark/ @huangruiteng @steven-kid @loopx-agent +/loopx/cli_commands/lark_*.py @huangruiteng @steven-kid @loopx-agent +/docs/integrations/lark-*.md @huangruiteng @steven-kid @loopx-agent +/docs/reference/protocols/lark-*.md @huangruiteng @steven-kid @loopx-agent +/examples/lark-* @huangruiteng @steven-kid @loopx-agent +/examples/lark_extension_test_support.py @huangruiteng @steven-kid @loopx-agent +/examples/control_plane/lark-* @huangruiteng @steven-kid @loopx-agent +/tests/extensions/test_lark_*.py @huangruiteng @steven-kid @loopx-agent -# --- Lead-owned governance, CI and release policy (must stay last) ----------- -/.github/ @huangruiteng -/scripts/ci/ @huangruiteng -/AGENTS.md @huangruiteng -/CONTRIBUTING.md @huangruiteng -/docs/product/release-*.md @huangruiteng -/docs/development/contributor-tasks.md @huangruiteng -/docs/project/technical-directions*.md @huangruiteng +# --- Governance, CI and release policy (must stay last) --------------------- +/.github/ @huangruiteng @loopx-agent +/scripts/ci/ @huangruiteng @loopx-agent +/AGENTS.md @huangruiteng @loopx-agent +/CONTRIBUTING.md @huangruiteng @loopx-agent +/docs/product/release-*.md @huangruiteng @loopx-agent +/docs/development/contributor-tasks.md @huangruiteng @loopx-agent +/docs/project/technical-directions*.md @huangruiteng @loopx-agent diff --git a/.github/GOVERNANCE.md b/.github/GOVERNANCE.md index 53f48528d0..fe9d9c21b0 100644 --- a/.github/GOVERNANCE.md +++ b/.github/GOVERNANCE.md @@ -23,26 +23,50 @@ repository-wide maintainer authority. This is the public list of everyone who can satisfy a review requirement on `main`, and the paths they answer for. It is a snapshot audited on -**2026-09-26**; the matching routes live in [`CODEOWNERS`](CODEOWNERS) and a +**2026-10-04**; the matching routes live in [`CODEOWNERS`](CODEOWNERS) and a change to either file changes both in one pull request. | Account | Role | Scope | `CODEOWNERS` route | Since | | --- | --- | --- | --- | --- | -| [`@huangruiteng`](https://github.com/huangruiteng) | Lead maintainer | Whole repository; governance, CI, releases, security handling, cross-subsystem decisions | Every path, and sole owner of `.github/`, `scripts/ci/`, release policy and technical directions | 2026-05-31 | +| [`@huangruiteng`](https://github.com/huangruiteng) | Lead maintainer | Whole repository; governance, CI, releases, security handling, cross-subsystem decisions | Every path, including `.github/`, `scripts/ci/`, release policy and technical directions | 2026-05-31 | | [`@steven-kid`](https://github.com/steven-kid) | Subsystem maintainer | [Lark integration](#lark-integration) | Lark extension, its CLI delegates, Lark docs and focused tests | 2026-08-16 ([#3236](https://github.com/loopx-project/loopx/pull/3236)) | | [`@maxliux5`](https://github.com/maxliux5) | Code owner | Frontend source under `apps/presentation/dashboard/` and chat bundle delivery | Dashboard source, `loopx/presentation/chat_bundle.py`, `scripts/chat_bundle*` | 2026-09-08 ([#4071](https://github.com/loopx-project/loopx/pull/4071)) | - -Every other module is owned by the lead maintainer: `CODEOWNERS` gives each -top-level module an explicit line so that gap stays visible rather than hidden -behind the `*` fallback. First-review contacts in the -[table below](#first-review-responsibilities) route the first technical -response but do not satisfy code-owner review. +| [`@loopx-agent`](https://github.com/loopx-agent) | Automated technical code owner | Whole repository; evidence-backed technical review, including governance, CI and release changes | Every route, including the `*` fallback and later subsystem overrides | 2026-10-04 | + +`@loopx-agent` shares code ownership on every path with the existing owners. +This includes paths introduced later through the `*` fallback; every narrower +route retains the account so GitHub's last-match rule cannot erase it. +Subsystem maintainers and the lead maintainer retain their responsibilities. +Code ownership qualifies a technical approval for the protected-branch review +requirement; it does not grant release, appointment, security-handling or +admin-bypass merge authority. First-review contacts in the +[table below](#first-review-responsibilities) still do not satisfy code-owner +review. + +The automated account accepts this repository-wide technical review scope +through the pull request adding this appointment. Its recent cross-author, +exact-head reviews include [#5533](https://github.com/loopx-project/loopx/pull/5533#pullrequestreview-5406199196), +[#5538](https://github.com/loopx-project/loopx/pull/5538#pullrequestreview-5405727853), +[#5540](https://github.com/loopx-project/loopx/pull/5540#pullrequestreview-5405732798) +and [#5541](https://github.com/loopx-project/loopx/pull/5541#pullrequestreview-5406093154). +These records name the reviewed head, contract, validation and remaining gaps. +They count as one account's reviews; multiple agent sessions do not create +independent GitHub reviewers. Author-owned pull requests still need another +eligible reviewer because GitHub does not accept self-approval. + +An `APPROVED` review and GitHub's aggregate review decision are separate facts. +Required code-owner approval uses `CODEOWNERS` on the pull request's base +branch. An account's repository admin permission alone does not satisfy that +requirement when the account is absent from the matching route. After an +appointment reaches `main`, read back the target pull request's review decision; +do not report it as approved solely from the submitted review. See +[GitHub's code-owner rules](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners). ### Code Owner Eligibility GitHub only honours code owners with repository write access, and a code-owner -approval can merge a change on its paths. A route is therefore added only when -all of the following hold, and the pull request adding it links the evidence: +approval satisfies the code-owner review requirement on its paths. A route is +therefore added only when all of the following hold, and the pull request adding it links the evidence: 1. the account has repository write access; 2. the account has publicly accepted a cohesive path scope, in @@ -76,8 +100,11 @@ left draft. A "response" means a review, comment or merge by an account on the [roster](#maintainer-and-review-roster) above — the accounts that can satisfy a review requirement. Comments from anyone else are real conversation, but they -do not satisfy this target, and neither do bot accounts. Both facts come from -GitHub's typed actor data rather than from login names. +do not satisfy this target, and neither do GitHub `Bot` actors. `@loopx-agent` +is an automated account represented by GitHub as a `User`; its qualifying +responses count in the current roster, without treating its sessions as separate +reviewers. These facts come from GitHub's typed actor data rather than from +login names. | Event | Target | Observed 2026-06-28 → 2026-09-26 | | --- | --- | --- | diff --git a/scripts/review_sla_report.py b/scripts/review_sla_report.py index 4dc7c5a36f..e89430291f 100644 --- a/scripts/review_sla_report.py +++ b/scripts/review_sla_report.py @@ -42,7 +42,7 @@ # .github/GOVERNANCE.md. Only these accounts can satisfy the published # first-response target; tests/test_review_sla_report.py checks this list # against that table. -DEFAULT_RESPONDERS = ("huangruiteng", "steven-kid", "maxliux5") +DEFAULT_RESPONDERS = ("huangruiteng", "steven-kid", "maxliux5", "loopx-agent") # Fallback only for inputs that carry no actor type (an old --input file). # GitHub's typed actors are authoritative: a Bot is never a responder even # when its login looks human, and a human whose login starts with "copilot" diff --git a/tests/test_codeowners.py b/tests/test_codeowners.py new file mode 100644 index 0000000000..67d1fca1e1 --- /dev/null +++ b/tests/test_codeowners.py @@ -0,0 +1,37 @@ +"""Required technical reviews keep the automated owner on every path.""" + +from pathlib import Path + +from loopx.capabilities.issue_fix.reviewer_recommendation import ( + _load_codeowners, + _owners_for_path, +) + + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def test_every_codeowner_rule_retains_the_repository_wide_reviewer(): + source, rules = _load_codeowners(REPO_ROOT) + assert source == ".github/CODEOWNERS" + assert rules[0][0] == "*" + for pattern, owners in rules: + assert "@loopx-agent" in owners, pattern + assert "@huangruiteng" in owners, pattern + + +def test_narrower_rules_and_new_paths_keep_existing_owners(): + _, rules = _load_codeowners(REPO_ROOT) + for path, retained_owner in ( + ("future-module/new.ts", "@huangruiteng"), + ("loopx/control_plane/new.ts", "@huangruiteng"), + ("apps/presentation/dashboard/new.tsx", "@maxliux5"), + ("loopx/extensions/lark/new.py", "@steven-kid"), + (".github/CODEOWNERS", "@huangruiteng"), + (".github/GOVERNANCE.md", "@huangruiteng"), + ("scripts/ci/new.py", "@huangruiteng"), + ("docs/product/release-readiness.md", "@huangruiteng"), + ): + _, owners = _owners_for_path(rules, path) + assert retained_owner in owners, path + assert "@loopx-agent" in owners, path diff --git a/tests/test_review_sla_report.py b/tests/test_review_sla_report.py index 5bd8e6ee7b..9c02974e17 100644 --- a/tests/test_review_sla_report.py +++ b/tests/test_review_sla_report.py @@ -78,15 +78,16 @@ def test_business_hours_skip_the_utc8_weekend(): def test_default_responders_match_the_published_roster(): """The metric counts appointed reviewers, so the list has to be the roster.""" + roster_section = GOVERNANCE.read_text(encoding="utf-8").split( + "## Maintainer And Review Roster\n", 1 + )[1].split("## Review Service Levels", 1)[0] roster = { match.group(1).casefold() for match in re.finditer( - r"^\| \[`@([^`]+)`\]\([^)]*\) \| ", GOVERNANCE.read_text(encoding="utf-8"), re.M + r"^\| \[`@([^`]+)`\]\([^)]*\) \| ", roster_section, re.M ) } - assert set(report.DEFAULT_RESPONDERS) <= roster, sorted(roster) - # The roster table lists exactly the accounts the report treats as responders. - assert set(report.DEFAULT_RESPONDERS) == {"huangruiteng", "steven-kid", "maxliux5"} + assert set(report.DEFAULT_RESPONDERS) == roster, sorted(roster) def test_actor_identity_comes_from_the_typed_actor_not_the_login():