Browse Source

ci: remove automatic review requests

Turtle 1 week ago
parent
commit
d6ed693f23

+ 0 - 6
.agents/notes/implemented/process/2026-09-08-comment-only-review-routing.i18n.yaml

@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# side as of the last confirmed-consistent state. Both languages carry equal authority;
-# after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-08-comment-only-review-routing.md
-2026-09-08-comment-only-review-routing.md: 050905285b2291b34da9873d19c2f122c088a9e5
-2026-09-08-comment-only-review-routing.zh.md: b98f5d70b4d5c0fd27df1c393238b0802e420e09

+ 0 - 41
.agents/notes/implemented/process/2026-09-08-comment-only-review-routing.md

@@ -1,41 +0,0 @@
-# Agent Note: Exclude documentation and comment-only changes from review routing
-
-Status: implemented
-
-English | [中文](2026-09-08-comment-only-review-routing.zh.md)
-
-## Problem
-
-Directory ownership alone treats documentation and comment edits like executable changes. These edits do not require the automatic code-owner request that protects behavior changes.
-
-GitHub may omit or truncate a file patch. A scanner that assumes every patch is complete can miss executable changes that occur outside the supplied hunks.
-
-## Decision
-
-Review routing classifies every old and new path in this order: test, documentation, comment-only, then reviewable code. Test classification wins when a test path also has a documentation extension. Every filename ending in `.md` or `.yaml`, matched without case sensitivity, is documentation. A `.yml` file is not documentation under this rule.
-
-Comment-only classification applies only to files with `status: modified` and a declared source-comment syntax. The scanner reconstructs the before and after text for each patch hunk, removes comments outside quoted strings, removes empty lines left by comments, and requires the remaining text to be identical.
-
-The scanner counts added and deleted patch lines and compares them with GitHub's file record before accepting a comment-only result. A missing patch, a count mismatch, a rename, an unsupported extension, or a comment form that remains visible to the lexer keeps the file reviewable. This fail-safe result can request an unnecessary review but cannot suppress a known code change.
-
-The supported lexical rules cover C-style line and block comments, hash comments, SQL comments, CSS block comments, and HTML comments for an explicit extension set in the scanner. Comment directives such as JSDoc tags, lint controls, compiler controls, and coverage controls are comments for routing purposes.
-
-## Verification
-
-[Scanner tests](../../../../.github/review-ownership/request-review.test.mjs) cover documentation extensions, supported comment forms, quoted comment markers, executable token changes, incomplete patches, renames, unsupported extensions, exclusion precedence, and the no-request result when every file is excluded.
-
-## Alternatives considered
-
-**Keep every non-test file reviewable.** This requests code owners for documentation and comment maintenance even though the routing policy is intended to identify executable changes.
-
-**Infer arbitrary semantic equivalence.** Proving behavior equivalence across the repository's languages requires language toolchains and still cannot assign one stable meaning to generated files, configuration, or build directives. The scanner performs only lexical comment removal.
-
-**Trust every patch returned by GitHub.** GitHub can omit or truncate patches. Matching the patch's added and deleted line counts to the file record prevents a partial patch from producing a comment-only verdict.
-
-**Fetch and parse every complete file revision.** Per-file content requests multiply API traffic for large pull requests and still require the same language-specific parsing. The changed-file response already carries enough evidence for complete ordinary patches.
-
-## Consequences
-
-Documentation and proven comment-only changes request nobody. The workflow logs them separately from tests so maintainers can audit why owner matching ignored a file.
-
-Unsupported or incomplete inputs remain reviewable. Comment directives do not request owners even when another tool interprets them, because this policy classifies their lexical form rather than downstream tool behavior.

+ 0 - 41
.agents/notes/implemented/process/2026-09-08-comment-only-review-routing.zh.md

@@ -1,41 +0,0 @@
-# Agent Note: 从评审路由中排除文档和纯注释变更
-
-Status: implemented
-
-[English](2026-09-08-comment-only-review-routing.md) | 中文
-
-## 问题
-
-只按目录分配 owner 会把文档和注释编辑视为可执行变更。这些编辑不需要用于保护行为变更的自动代码 owner 请求。
-
-GitHub 可能省略或截断文件 patch。如果扫描器假定每个 patch 都完整,就可能漏掉位于已提供 hunk 之外的可执行变更。
-
-## 决策
-
-评审路由按测试、文档、纯注释、可评审代码的顺序对每个新旧路径分类。当测试路径同时具有文档扩展名时,测试分类优先。所有以 `.md` 或 `.yaml` 结尾的文件均视为文档,扩展名匹配不区分大小写;此规则不把 `.yml` 文件视为文档。
-
-纯注释分类只适用于 `status: modified` 且已声明源码注释语法的文件。扫描器重建每个 patch hunk 的变更前后文本,移除引号字符串外的注释和注释留下的空行,并要求其余文本完全相同。
-
-扫描器会统计 patch 的新增行和删除行,并在接受纯注释结果前与 GitHub 文件记录比较。缺失 patch、计数不符、重命名、不受支持的扩展名,或词法分析器仍能看到的注释形式都会使文件保持可评审状态。该保守结果可能产生不必要的评审请求,但不会隐藏已知代码变更。
-
-受支持的词法规则按扫描器中显式的扩展名集合覆盖 C 风格行注释和块注释、井号注释、SQL 注释、CSS 块注释及 HTML 注释。JSDoc 标签、lint 控制、编译器控制和覆盖率控制等注释指令在评审路由中仍属于注释。
-
-## 验证
-
-[扫描器测试](../../../../.github/review-ownership/request-review.test.mjs)覆盖文档扩展名、受支持的注释形式、引号内的注释标记、可执行 token 变更、不完整 patch、重命名、不受支持的扩展名、排除优先级,以及所有文件均被排除时不发出请求的结果。
-
-## 考虑过的替代方案
-
-**让每个非测试文件都保持可评审。** 这会为文档和注释维护请求代码 owner,但该路由策略的目标是识别可执行变更。
-
-**推断任意语义等价。** 证明仓库中多种语言的行为等价需要各语言工具链,而且仍然无法为生成文件、配置或构建指令提供一种稳定含义。扫描器只执行词法注释移除。
-
-**信任 GitHub 返回的每个 patch。** GitHub 可能省略或截断 patch。将 patch 的新增和删除行数与文件记录匹配,可以防止不完整 patch 产生纯注释结论。
-
-**获取并解析每个文件的完整修订版本。** 对于大型 PR,逐文件内容请求会增加多倍 API 流量,而且仍需相同的语言专用解析。普通完整 patch 所需的证据已包含在变更文件响应中。
-
-## 后果
-
-文档和确认的纯注释变更不会请求任何人。Workflow 会将它们与测试分开记录,以便维护者检查 owner 匹配忽略文件的原因。
-
-不受支持或不完整的输入仍需评审。即使其他工具会解释注释指令,这些指令也不会请求 owner,因为该策略按词法形式分类,而不是按下游工具行为分类。

+ 0 - 55
.agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.md

@@ -1,55 +0,0 @@
-# Agent Note: Route reviews from trusted changed-file policy
-
-Status: implemented
-
-## Problem
-
-GitHub's native CODEOWNERS behavior requests reviewers whenever a matching path changes. It cannot apply this repository's distinction between reviewable implementation or documentation files and test-only evidence. A native CODEOWNERS file also makes GitHub, rather than an inspected repository program, responsible for the request decision.
-
-Review routing needs an observable changed-file input, explicit owner rules, complete test exclusions, and a write-capable workflow that remains safe for pull requests from forks.
-
-## Decision
-
-The repository keeps a CODEOWNERS-compatible map at [`.github/review-ownership/CODEOWNERS`](../../../../.github/review-ownership/CODEOWNERS), outside GitHub's native CODEOWNERS locations. The map accepts only explicit absolute directory patterns with one or two individual GitHub users. It rejects wildcards, hidden-directory patterns, teams, more than two owners, duplicate patterns, and duplicate owners. Later matching patterns replace earlier matches.
-
-The policy test counts non-test tracked lines in directories that match an ownership rule. It rejects a map in which `@turtle1999` owns more than one third of that eligible owned codebase.
-
-The [`request-review` workflow](../../../../.github/workflows/request-review.yml) runs on `pull_request_target` events for opened, synchronized, reopened, ready-for-review, and converted-to-draft pull requests. Its write-capable job checks out the default branch and executes only the default branch's scanner and ownership map. It does not check out pull-request code or read repository secrets.
-
-The scanner fetches every changed-file record before deciding. It fails if the pull request reports more than GitHub's 3,000-file API limit or if pagination returns an incomplete list. It normalizes repository paths, evaluates old and new paths of a rename independently, and escapes filenames before logging them.
-
-The scanner excludes test-only paths before owner matching. Excluded paths comprise directories named `test`, `tests`, `__tests__`, `__snapshots__`, `benches`, or `stress-tests`; the top-level `benchmarks` and `snapshots` trees; `packages/test-support`; `scripts/fixtures` and `scripts/snapshots`; filenames ending in `.bench.<ext>`, `.corpus.<ext>`, `.e2e.<ext>`, `.perf.<ext>`, `.snapshot.<ext>`, `.spec.<ext>`, `.stress.<ext>`, or `.test.<ext>`; and Python `test_*.py`, `*_test.py`, or `*_tests.py` files. Test infrastructure such as `vitest*.config.ts` and gate implementations remains reviewable because it changes how repository evidence is produced. The [comment-only routing decision](2026-09-08-comment-only-review-routing.md) owns the additional documentation and comment exclusions.
-
-The workflow prints the changed code paths, each exclusion class, per-file owner matches and changed LOC, aggregate owner relevance, approved owners omitted from new requests, current individual requests, the available counted slot after planned cancellations, and final reviewer actions before any review-request mutation. For a non-draft pull request, it fetches the complete chronological review list and reduces each owner's undismissed `APPROVED` and `CHANGES_REQUESTED` reviews to the latest decisive state; `COMMENTED` and `PENDING` reviews leave that state unchanged. It removes the pull-request author, owners with an active approval, and users who remain requested from the matched individual owners. An active approval remains sufficient after later synchronize events, while a later changes-requested review makes the owner eligible again. The review-list operation fails before mutation at 3,000 entries or on an invalid record.
-
-The workflow keeps at most one current individual review request other than `@turtle1999`. An existing request for `@turtle1999` does not consume that slot, but each workflow run adds at most one reviewer. An existing non-turtle request leaves no slot, so the workflow does not add anyone, including `@turtle1999`. Existing individual requests consume the slot even when they do not match the ownership map. An owner's relevance is the sum of GitHub-reported additions and deletions for each reviewable changed-file record whose current or previous path matches that owner. Each record contributes once per owner, including when both paths of a rename match the same owner. Higher changed LOC selects candidates first when the available slot cannot cover the remaining owners; login order resolves equal scores.
-
-When current review requests exist, the workflow reads the complete review-request timeline before mutation. A current reviewer is workflow-authored only when its latest matching `review_requested` event identifies `github-actions[bot]` as `review_requester`; a request without an attributable event is preserved. A non-draft run cancels workflow-authored reviewers that no longer match the current candidates and excess workflow-authored non-turtle reviewers above the counted limit; current relevance order selects which matching workflow reviewer remains. Planned cancellations release capacity before the workflow selects a new reviewer. A draft run cancels every current workflow-authored request. Requests made by people remain unchanged. An attributable event with invalid provenance and timelines above 3,000 events fail before mutation.
-
-## Verification
-
-[Scanner tests](../../../../.github/review-ownership/request-review.test.mjs) cover admitted ownership syntax, rejected syntax, each exclusion class, production-name negative controls, renames, last-match behavior, unmatched files, changed-LOC aggregation and ranking, complete pagination, file and review limits, approval-state reduction, approved-owner suppression and next-owner selection, log-before-mutation ordering, author and existing-reviewer filtering, non-draft reconciliation, draft cancellation provenance, and API failures. [Workflow tests](../../../../scripts/ci-workflow.spec.ts) pin the event set, least permissions, trusted default-branch checkout, absence of pull-request-head references and secrets, and executed command. The gate graph includes both suites in static CI and `check-all`.
-
-## Alternatives considered
-
-**Use native CODEOWNERS.** Native routing cannot ignore test-only changes and offers no repository-owned decision log before requesting reviewers.
-
-**Run under `pull_request` and check out the pull-request head.** A fork workflow does not receive a write-capable token, while granting a write token to code from an untrusted head is unsafe.
-
-**Execute the pull request's scanner or owner map under `pull_request_target`.** This lets an untrusted pull request choose its own write-capable behavior or owners.
-
-**Select capped candidates by login order.** Login order is stable but ignores how much reviewable code changed under each owner's directories. Changed LOC makes the limited requests follow the pull request's strongest ownership relevance while retaining login order for ties.
-
-**Cancel every reviewer that no longer matches.** A person may request a reviewer for reasons outside the ownership map. Only requests attributed to the workflow identity are safe for automated reconciliation.
-
-**Treat an empty current request as an owner who still needs review.** GitHub removes the pending request when the reviewer submits a review. Requesting an owner with an active approval again adds no ownership coverage and creates repeated notifications after later synchronize events.
-
-**Infer arbitrary semantic source changes from patches or language parsers.** GitHub can omit or truncate patches, and the repository spans many languages. The scanner does not try to prove that two programs behave identically. The later [comment-only routing decision](2026-09-08-comment-only-review-routing.md) adds a narrow lexical comparison only when changed-line counts prove that GitHub supplied the complete patch.
-
-## Consequences
-
-Reviewer mutations are reproducible from a trusted policy, the file classifications printed in the workflow log, and review-request provenance in the pull-request timeline. Excluded changes do not request owners, rule and changed-file updates remove obsolete workflow-authored requests on the next run, and draft pull requests do not retain workflow-authored requests. Ownership changes become effective only after merge, so the pull request that changes policy cannot apply its untrusted policy to itself.
-
-The workflow requests at most one reviewer per run, does not repeat a request while that owner has an active approval, keeps no more than one current individual reviewer other than `@turtle1999`, and prefers owners whose matched reviewable files carry more changed LOC. An existing `@turtle1999` request leaves the counted slot available; an existing non-turtle request prevents every additional request. Shared ownership gives each owner the same file-level relevance without counting one renamed file twice for the same owner. GitHub-generated review-request events may not start other workflows that depend on recursively triggered events from `GITHUB_TOKEN`; those workflows must not rely on this request as their only trigger.
-
-Any change that does not match an explicit exclusion remains eligible under an owned directory. Unmatched paths are logged and request nobody. Pull requests above the file, review, or timeline API limit fail without applying a partial reviewer mutation.

+ 0 - 59
.github/review-ownership/CODEOWNERS

@@ -1,59 +0,0 @@
-# Custom static-scanner input. Its nested path keeps GitHub from loading it as
-# the repository's native CODEOWNERS file.
-/apps/cli/ @turtle1999
-/apps/web/ @imccyu
-/docs/ @turtle1999
-/native/ @mektpoy
-/patches/ @mektpoy
-/python/ @LegGasai
-/vendor/ @turtle1999
-/website/ @LegGasai
-/packages/acp/ @mektpoy
-/packages/api/ @imccyu
-/packages/attachment/ @CreatixChu
-/packages/boot/ @turtle1999
-/packages/bundle/ @turtle1999
-/packages/client/ @imccyu
-/packages/code-runtime/ @Chinesezjc
-/packages/compaction/ @imccyu
-/packages/context/ @turtle1999
-/packages/core/ @turtle1999 @mektpoy
-/packages/credentials/ @mektpoy
-/packages/e2b/ @mektpoy
-/packages/experimental/ @mektpoy
-/packages/extensions/ @mektpoy
-/packages/feedback/ @mektpoy
-/packages/fs/ @mektpoy
-/packages/goal/ @mektpoy
-/packages/guard/ @turtle1999
-/packages/hooks/ @mektpoy
-/packages/host/ @turtle1999
-/packages/identity/ @imccyu
-/packages/interaction/ @imccyu
-/packages/jobs/ @imccyu
-/packages/llm/ @LegGasai
-/packages/lsp/ @mektpoy
-/packages/mcp/ @mektpoy
-/packages/plan/ @mektpoy
-/packages/preset/ @LegGasai @turtle1999
-/packages/runtime-diagnostics/ @mektpoy
-/packages/sandbox/ @mektpoy
-/packages/schedule/ @imccyu
-/packages/sdk/ @mektpoy
-/packages/session/ @turtle1999 @mektpoy
-/packages/session-query/ @mektpoy
-/packages/settings/ @mektpoy
-/packages/shell/ @mektpoy
-/packages/skill/ @mektpoy
-/packages/spill/ @mektpoy
-/packages/storage/ @imccyu
-/packages/subagent/ @Dudu-0223
-/packages/subprocess/ @mektpoy
-/packages/terminal/ @imccyu
-/packages/todo/ @mektpoy
-/packages/typert/ @imccyu
-/packages/util/ @mektpoy
-/packages/web/ @imccyu
-/packages/webhook/ @mektpoy
-/packages/workflow/ @mektpoy
-/packages/workspace/ @imccyu

+ 6 - 36
.github/review-ownership/README.md

@@ -1,34 +1,16 @@
-# Automated pull-request reviews
+# Pull-request approval policy
 
 ## Summary
 
-The [`request-review` workflow](../workflows/request-review.yml) requests owners for reviewable code. The [`weighted-approval` workflow](../workflows/weighted-approval.yml) publishes an approval score for branch rules. Both write-capable workflows execute policy from the trusted default branch.
+The [`weighted-approval` workflow](../workflows/weighted-approval.yml) publishes an approval score for branch rules. Reviewer selection and review requests remain manual.
 
 ## Table of Contents
 
-- [Routing](#routing)
 - [Approval scoring](#approval-scoring)
-- [Review exclusions](#review-exclusions)
 - [Security](#security)
 - [Verification](#verification)
 - [Dev Note](#dev-note)
 
-<a id="routing"></a>
-
-## Routing
-
-Pull requests run the workflow when opened, synchronized, reopened, marked ready for review, or converted to a draft. The scanner fetches the complete pull-request file list, evaluates both paths of a rename, and fails instead of routing from a partial list. GitHub exposes at most 3,000 files for this API.
-
-For a non-draft pull request, the workflow keeps at most one current individual review request other than `@turtle1999`; an existing request for `@turtle1999` does not consume that slot. Each run adds at most one reviewer. An existing non-turtle request leaves no slot, so the workflow does not add anyone, including `@turtle1999`. Existing individual requests consume the slot even when made by people outside the ownership map. When more candidates remain than the available counted slot can cover, the workflow ranks them by the total GitHub-reported additions plus deletions in reviewable changed-file records that match each owner. A rename contributes its changed LOC once to an owner even when both paths match that owner. Higher changed LOC ranks first, and login order resolves ties.
-
-Before selecting a new reviewer, a non-draft run fetches the pull request's complete chronological review list. An owner's latest undismissed decisive review is `APPROVED` or `CHANGES_REQUESTED`; comments and pending reviews do not replace that decision. An approved owner remains omitted after later synchronize events, while a later changes-requested review makes the owner eligible again. The workflow fails before mutation when the list reaches the supported 3,000-review limit or contains an invalid record.
-
-On every run with current review requests, the workflow reads the pull-request timeline. A current reviewer is workflow-authored only when the latest matching `review_requested` event names `github-actions[bot]` as `review_requester`; a request without an attributable event is preserved. On a non-draft pull request, the workflow cancels workflow-authored reviewers that no longer match the current candidates and excess workflow-authored non-turtle reviewers above the counted limit. Current relevance order decides which matching workflow reviewer remains when the limit shrinks. It then fills any slot left by the planned cancellations. On a draft, it cancels every current workflow-authored request. Requests made by people remain unchanged in both states. An attributable event with invalid provenance fails before mutation, and the workflow also fails without cancellation when the timeline exceeds 3,000 events.
-
-The ownership map accepts explicit absolute directory patterns and one or two individual GitHub users per pattern. It rejects wildcards, hidden-directory patterns, teams, more than two owners, and duplicate patterns or owners. Matching follows CODEOWNERS last-match semantics. The scanner prints the changed code, excluded test, documentation, and comment-only files; per-file owner matches and LOC; the aggregate owner relevance ranking; approved owners omitted from new requests; current individual requests and the available counted slot after planned cancellations; and the reviewers it will request or cancel before it mutates review requests. Unmatched files remain visible in the log. The pull-request author, approved owners, and users who remain requested are omitted from new requests.
-
-The policy test measures non-test tracked lines under matched directories and requires `@turtle1999` to own no more than one third of that eligible owned codebase.
-
 <a id="approval-scoring"></a>
 
 ## Approval scoring
@@ -41,34 +23,22 @@ Each reviewer contributes only the current `APPROVED` or `CHANGES_REQUESTED` dec
 
 The publisher runs when a pull request opens, synchronizes, reopens, becomes ready, or becomes a draft. Review submissions, edits, and dismissals run the no-permission [`weighted-approval-review-event` workflow](../workflows/weighted-approval-review-event.yml); its validated run title supplies the pull-request number to the default-branch publisher. The publisher validates the current head, fetches every review, and resolves current repository permission before publishing the status. Permission changes take effect on the next subscribed pull-request or review event.
 
-<a id="review-exclusions"></a>
-
-## Review exclusions
-
-Review routing excludes the repository's unit, end-to-end, expected-output, snapshot, benchmark, performance, stress, corpus, native, and Python test conventions. This includes `test`, `tests`, `__tests__`, `__snapshots__`, `benches`, and `stress-tests` directories; the top-level `benchmarks` and `snapshots` trees; `packages/test-support`; `scripts/fixtures` and `scripts/snapshots`; recognized test filename suffixes; and Python `test_*.py` or `*_test.py` files.
-
-Test infrastructure that can alter how evidence is produced remains reviewable, including `vitest*.config.ts` and gate implementations under `scripts`. A production file named `test.ts`, `spec.ts`, or `snapshot.ts` is not excluded solely by that name.
-
-Files ending in `.md` or `.yaml`, with case-insensitive extension matching, are documentation and never contribute owners. A `.yml` file remains reviewable unless another exclusion applies.
-
-For a modified file with a supported source extension, the scanner compares the pre-change and post-change text after removing parsed comments. It excludes the file only when GitHub supplies a patch whose counted additions and deletions prove that the patch is complete and the remaining code is identical. The parser recognizes C-style line and block comments, hash comments, SQL comments, CSS block comments, and HTML comments for their declared extensions. Renames, unsupported languages, missing or partial patches, and uncertain comment forms remain reviewable.
-
 <a id="security"></a>
 
 ## Security
 
-The write-capable jobs check out only the repository default branch. They do not check out or execute pull-request code and do not use repository secrets. The review-event workflow has no `GITHUB_TOKEN` permissions and passes only a decimal pull-request number in its run title. The publisher rejects an invalid run title and a number that does not resolve to the workflow run's current pull-request head. Pull-request filenames and reviews are treated as API data and escaped in logs.
+The status-writing job checks out only the repository default branch. It does not check out or execute pull-request code and does not use repository secrets. The review-event workflow has no `GITHUB_TOKEN` permissions and passes only a decimal pull-request number in its run title. The publisher rejects an invalid run title and a number that does not resolve to the workflow run's current pull-request head. Pull-request reviews are treated as API data and escaped in logs.
 
-Ownership and approval policy changes take effect only after they merge into the default branch. This prevents an untrusted pull request from changing either program or policy for its own run.
+Approval policy changes take effect only after they merge into the default branch. This prevents an untrusted pull request from changing the program or policy for its own run.
 
 <a id="verification"></a>
 
 ## Verification
 
-Run `pnpm run test:request-review` for ownership parsing, file classification, complete-patch checks, comment parsing, changed-LOC ranking, pagination, approval-state reduction, logging order, non-draft reconciliation, draft cancellation, reviewer provenance, reviewer filtering, and API behavior. Run `pnpm run test:approval-policy` for policy parsing, effective review decisions, review-event validation, pagination, permission filtering, weighted scoring, blockers, drafts, status publication, and API failures. [Workflow tests](../../scripts/ci-workflow.spec.ts) pin the trusted checkout, no-permission review handoff, permissions, events, and commands. The repository gate graph runs both policy checks and the workflow tests in CI.
+Run `pnpm run test:approval-policy` for policy parsing, effective review decisions, review-event validation, pagination, permission filtering, weighted scoring, blockers, drafts, status publication, and API failures. [Workflow tests](../../scripts/ci-workflow.spec.ts) pin the trusted checkout, no-permission review handoff, permissions, events, and commands. The repository gate graph runs the approval policy and workflow tests in CI.
 
 <a id="dev-note"></a>
 
 ## Dev Note
 
-The [review-routing decision](../../.agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.md) records the security model, test exclusions, and alternatives.
+None.

+ 0 - 660
.github/review-ownership/request-review.mjs

@@ -1,660 +0,0 @@
-#!/usr/bin/env node
-
-import { readFileSync } from 'node:fs'
-import process from 'node:process'
-import { pathToFileURL } from 'node:url'
-
-const API_VERSION = '2026-03-10'
-const MAX_OWNERS_PER_RULE = 2
-const MAX_PULL_REQUEST_FILES = 3_000
-const MAX_PULL_REQUEST_REVIEWS = 3_000
-const MAX_COUNTED_REQUESTED_REVIEWERS = 1
-const MAX_TIMELINE_EVENTS = 3_000
-const PAGE_SIZE = 100
-const PULL_REQUEST_REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'COMMENTED', 'DISMISSED', 'PENDING'])
-const UNCOUNTED_REVIEWER = 'turtle1999'
-const WORKFLOW_REVIEW_REQUESTER = 'github-actions[bot]'
-const TEST_DIRECTORY_NAMES = new Set(['__snapshots__', '__tests__', 'benches', 'stress-tests', 'test', 'tests'])
-const TEST_FILE_MARKER = /\.(?:bench|corpus|e2e|perf|snapshot|spec|stress|test)\.[^./]+$/u
-const PYTHON_TEST_FILE = /^(?:test_.+|.+_tests?)\.py$/u
-const DOCUMENTATION_FILE = /\.(?:md|yaml)$/iu
-const C_STYLE_EXTENSIONS = new Set([
-  'c', 'cc', 'cjs', 'cpp', 'cts', 'cxx', 'go', 'h', 'hpp', 'java', 'js', 'jsx',
-  'kt', 'kts', 'less', 'mjs', 'mts', 'rs', 'scss', 'swift', 'ts', 'tsx',
-])
-const BLOCK_COMMENT_EXTENSIONS = new Set(['css'])
-const HASH_COMMENT_EXTENSIONS = new Set(['bash', 'ps1', 'py', 'pyi', 'r', 'rb', 'sh', 'toml', 'yml', 'zsh'])
-const HTML_COMMENT_EXTENSIONS = new Set(['htm', 'html'])
-
-/**
- * Parse the explicit directory subset accepted from the review ownership file.
- * @param {string} source CODEOWNERS-compatible source text.
- * @returns {Array<{pattern: string, prefix: string, owners: string[]}>} Ordered ownership rules.
- */
-export function parseOwnership(source) {
-  const rules = []
-  const patterns = new Set()
-  for (const [index, rawLine] of source.split('\n').entries()) {
-    const line = rawLine.trim()
-    if (!line || line.startsWith('#')) continue
-    const [pattern, ...owners] = line.split(/\s+/u)
-    const location = `ownership line ${index + 1}`
-    if (!/^\/[^*?[\]#!\\]+\/$/u.test(pattern)) {
-      throw new Error(`${location}: expected one explicit absolute directory pattern`)
-    }
-    if (pattern.startsWith('/.')) throw new Error(`${location}: hidden-directory patterns are not allowed`)
-    if (patterns.has(pattern)) throw new Error(`${location}: duplicate pattern ${JSON.stringify(pattern)}`)
-    if (owners.length === 0) throw new Error(`${location}: expected at least one owner`)
-    if (owners.length > MAX_OWNERS_PER_RULE) {
-      throw new Error(`${location}: expected at most ${MAX_OWNERS_PER_RULE} owners`)
-    }
-    const normalizedOwners = []
-    const seenOwners = new Set()
-    for (const owner of owners) {
-      if (!/^@[A-Za-z0-9-]+$/u.test(owner)) {
-        throw new Error(`${location}: only individual GitHub users are supported`)
-      }
-      const key = owner.toLowerCase()
-      if (seenOwners.has(key)) throw new Error(`${location}: duplicate owner ${owner}`)
-      seenOwners.add(key)
-      normalizedOwners.push(owner)
-    }
-    patterns.add(pattern)
-    rules.push({ pattern, prefix: pattern.slice(1), owners: normalizedOwners })
-  }
-  if (rules.length === 0) throw new Error('ownership file contains no rules')
-  return rules
-}
-
-/**
- * Normalize a repository-relative path received from GitHub.
- * @param {unknown} value GitHub file path.
- * @returns {string} Slash-normalized repository path.
- */
-export function normalizeRepositoryPath(value) {
-  if (typeof value !== 'string' || value.length === 0) throw new Error('changed file has no path')
-  const normalized = value.replaceAll('\\', '/').replace(/^\.\/+/, '')
-  if (
-    normalized.startsWith('/')
-    || normalized.includes('\0')
-    || normalized.split('/').some(segment => !segment || segment === '.' || segment === '..')
-  ) {
-    throw new Error(`invalid repository path ${JSON.stringify(value)}`)
-  }
-  return normalized
-}
-
-/**
- * Decide whether a repository path belongs only to test evidence or test support.
- * @param {string} value Repository-relative path.
- * @returns {boolean} Whether reviewer routing must ignore the path.
- */
-export function isTestPath(value) {
-  const file = normalizeRepositoryPath(value)
-  const segments = file.split('/')
-  if (segments[0] === 'benchmarks' || segments[0] === 'snapshots') return true
-  if (segments[0] === 'packages' && segments[1] === 'test-support') return true
-  if (segments[0] === 'scripts' && (segments[1] === 'fixtures' || segments[1] === 'snapshots')) return true
-  if (segments.some(segment => TEST_DIRECTORY_NAMES.has(segment))) return true
-  const basename = segments.at(-1) ?? ''
-  return TEST_FILE_MARKER.test(basename) || PYTHON_TEST_FILE.test(basename)
-}
-
-/**
- * Decide whether a repository path is documentation excluded from review routing.
- * @param {string} value Repository-relative path.
- * @returns {boolean} Whether the path has an excluded documentation extension.
- */
-export function isDocumentationPath(value) {
-  return DOCUMENTATION_FILE.test(normalizeRepositoryPath(value))
-}
-
-/**
- * Decide whether a complete modified-file patch changes comments only.
- * @param {unknown} value GitHub changed-file record.
- * @returns {boolean} Whether supported comment parsing removes every changed token.
- */
-export function isCommentOnlyChange(value) {
-  if (!isRecord(value) || value.status !== 'modified' || typeof value.filename !== 'string'
-    || typeof value.patch !== 'string' || !Number.isSafeInteger(value.additions)
-    || value.additions < 0 || !Number.isSafeInteger(value.deletions) || value.deletions < 0) return false
-  const syntax = commentSyntax(value.filename)
-  if (syntax === undefined) return false
-  if (value.filename.toLowerCase().endsWith('.rs') && /\b(?:br|r)#{0,255}"/u.test(value.patch)) return false
-  const hunks = parsePatchHunks(value.patch)
-  if (hunks === undefined || hunks.additions !== value.additions || hunks.deletions !== value.deletions) {
-    return false
-  }
-  return hunks.values.every(({ before, after }) =>
-    normalizedCode(before, syntax) === normalizedCode(after, syntax))
-}
-
-function commentSyntax(filename) {
-  const normalized = normalizeRepositoryPath(filename)
-  const basename = normalized.slice(normalized.lastIndexOf('/') + 1).toLowerCase()
-  const extension = basename.includes('.') ? basename.slice(basename.lastIndexOf('.') + 1) : ''
-  const line = []
-  const block = []
-  if (C_STYLE_EXTENSIONS.has(extension)) {
-    line.push('//')
-    block.push(['/*', '*/'])
-  }
-  if (BLOCK_COMMENT_EXTENSIONS.has(extension)) block.push(['/*', '*/'])
-  if (HASH_COMMENT_EXTENSIONS.has(extension) || basename === 'dockerfile' || basename.startsWith('dockerfile.')
-    || basename === 'makefile' || basename.startsWith('makefile.')) line.push('#')
-  if (extension === 'sql') {
-    line.push('--')
-    block.push(['/*', '*/'])
-  }
-  if (HTML_COMMENT_EXTENSIONS.has(extension)) block.push(['<!--', '-->'])
-  return line.length === 0 && block.length === 0 ? undefined : { line, block }
-}
-
-function parsePatchHunks(patch) {
-  const values = []
-  let current
-  let additions = 0
-  let deletions = 0
-  for (const line of patch.split('\n')) {
-    if (line.startsWith('@@')) {
-      current = { before: [], after: [] }
-      values.push(current)
-      continue
-    }
-    if (current === undefined || line.startsWith('\\ No newline at end of file')) continue
-    const prefix = line[0]
-    const content = line.slice(1)
-    if (prefix === ' ') {
-      current.before.push(content)
-      current.after.push(content)
-    } else if (prefix === '-') {
-      current.before.push(content)
-      deletions++
-    } else if (prefix === '+') {
-      current.after.push(content)
-      additions++
-    }
-  }
-  return values.length === 0 ? undefined : { values, additions, deletions }
-}
-
-function normalizedCode(lines, syntax) {
-  return stripComments(lines.join('\n'), syntax)
-    .split('\n')
-    .map(line => line.trimEnd())
-    .filter(line => line.trim().length > 0)
-    .join('\n')
-}
-
-function stripComments(source, syntax) {
-  let result = ''
-  let quote
-  let blockEnd
-  for (let index = 0; index < source.length;) {
-    if (blockEnd !== undefined) {
-      if (source.startsWith(blockEnd, index)) {
-        index += blockEnd.length
-        blockEnd = undefined
-      } else {
-        index++
-      }
-      continue
-    }
-    const character = source[index]
-    if (quote !== undefined) {
-      result += character
-      index++
-      if (character === '\\' && index < source.length) {
-        result += source[index]
-        index++
-      } else if (character === quote) {
-        quote = undefined
-      }
-      continue
-    }
-    if (character === '\'' || character === '"' || character === '`') {
-      quote = character
-      result += character
-      index++
-      continue
-    }
-    const block = syntax.block.find(([start]) => source.startsWith(start, index))
-    if (block !== undefined) {
-      index += block[0].length
-      blockEnd = block[1]
-      continue
-    }
-    const line = syntax.line.find(marker => source.startsWith(marker, index))
-    const lineStart = index === 0 || source[index - 1] === '\n'
-    const hashStartsComment = line !== '#' || lineStart || /\s/u.test(source[index - 1] ?? '')
-    if (line !== undefined && hashStartsComment && !(line === '#' && lineStart && source[index + 1] === '!')) {
-      const newline = source.indexOf('\n', index + line.length)
-      if (newline === -1) break
-      result += '\n'
-      index = newline + 1
-      continue
-    }
-    result += character
-    index++
-  }
-  return result
-}
-
-/**
- * Expand changed-file records into reviewable, test, documentation, and comment-only paths.
- * @param {unknown[]} files Pull-request file records from GitHub.
- * @returns {{changedCodeFiles: string[], reviewableChanges: Array<{paths: string[], changedLines: number}>, excludedTestFiles: string[], excludedDocumentationFiles: string[], excludedCommentOnlyFiles: string[]}} Classified paths and their GitHub-reported changed-line counts.
- */
-export function classifyChangedFiles(files) {
-  const changedCodeFiles = new Set()
-  const reviewableChanges = []
-  const excludedTestFiles = new Set()
-  const excludedDocumentationFiles = new Set()
-  const excludedCommentOnlyFiles = new Set()
-  for (const entry of files) {
-    if (!isRecord(entry)) throw new Error('changed-file response contains a non-object entry')
-    const changedLines = changedLineCount(entry)
-    const paths = [normalizeRepositoryPath(entry.filename)]
-    const commentOnly = isCommentOnlyChange(entry)
-    if (entry.previous_filename !== undefined) {
-      paths.unshift(normalizeRepositoryPath(entry.previous_filename))
-    }
-    const reviewablePaths = []
-    for (const file of new Set(paths)) {
-      if (isTestPath(file)) excludedTestFiles.add(file)
-      else if (isDocumentationPath(file)) excludedDocumentationFiles.add(file)
-      else if (commentOnly) excludedCommentOnlyFiles.add(file)
-      else {
-        changedCodeFiles.add(file)
-        reviewablePaths.push(file)
-      }
-    }
-    if (reviewablePaths.length > 0) {
-      reviewableChanges.push({ paths: reviewablePaths.sort(), changedLines })
-    }
-  }
-  return {
-    changedCodeFiles: [...changedCodeFiles].sort(),
-    reviewableChanges,
-    excludedTestFiles: [...excludedTestFiles].sort(),
-    excludedDocumentationFiles: [...excludedDocumentationFiles].sort(),
-    excludedCommentOnlyFiles: [...excludedCommentOnlyFiles].sort(),
-  }
-}
-
-function changedLineCount(entry) {
-  for (const field of ['additions', 'deletions']) {
-    if (!Number.isSafeInteger(entry[field]) || entry[field] < 0) {
-      throw new Error(`changed-file ${field} must be a non-negative integer`)
-    }
-  }
-  const changedLines = entry.additions + entry.deletions
-  if (!Number.isSafeInteger(changedLines)) throw new Error('changed-file LOC exceeds the safe integer range')
-  return changedLines
-}
-
-/**
- * Match changed paths and rank owners by their reviewable changed LOC.
- * @param {Array<{prefix: string, owners: string[]}>} rules Ordered ownership rules.
- * @param {Array<{paths: string[], changedLines: number}>} reviewableChanges Reviewable GitHub file records.
- * @returns {{matches: Array<{file: string, changedLines: number, owners: string[]}>, reviewers: Array<{login: string, changedLines: number}>}} Routing plan.
- */
-export function planReviewers(rules, reviewableChanges) {
-  const matches = []
-  const reviewers = new Map()
-  for (const change of reviewableChanges) {
-    const changeOwners = new Map()
-    for (const file of change.paths) {
-      let owners = []
-      for (const rule of rules) {
-        if (file.startsWith(rule.prefix)) owners = rule.owners
-      }
-      matches.push({ file, changedLines: change.changedLines, owners })
-      for (const owner of owners) changeOwners.set(owner.toLowerCase(), owner.slice(1))
-    }
-    for (const [key, login] of changeOwners) {
-      const changedLines = (reviewers.get(key)?.changedLines ?? 0) + change.changedLines
-      if (!Number.isSafeInteger(changedLines)) throw new Error(`changed LOC for @${login} exceeds the safe integer range`)
-      reviewers.set(key, { login, changedLines })
-    }
-  }
-  return {
-    matches: matches.sort((left, right) => left.file.localeCompare(right.file, 'en')),
-    reviewers: [...reviewers.values()].sort((left, right) => {
-      if (left.changedLines !== right.changedLines) return left.changedLines < right.changedLines ? 1 : -1
-      return left.login.localeCompare(right.login, 'en')
-    }),
-  }
-}
-
-/**
- * Create a repository-scoped GitHub JSON API caller.
- * @param {{token: string, apiUrl?: string, fetchImpl?: typeof fetch}} options API dependencies.
- * @returns {(path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>} API caller.
- */
-export function createGitHubApi({ token, apiUrl = 'https://api.github.com', fetchImpl = globalThis.fetch }) {
-  if (!token) throw new Error('GITHUB_TOKEN is not set')
-  if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable')
-  const root = apiUrl.replace(/\/+$/u, '')
-  return async (path, { method = 'GET', body } = {}) => {
-    const response = await fetchImpl(`${root}${path}`, {
-      method,
-      headers: {
-        Accept: 'application/vnd.github+json',
-        Authorization: `Bearer ${token}`,
-        'Content-Type': 'application/json',
-        'User-Agent': 'deepseek-harness-request-review',
-        'X-GitHub-Api-Version': API_VERSION,
-      },
-      ...(body === undefined ? {} : { body: JSON.stringify(body) }),
-    })
-    if (!response.ok) {
-      const responseBody = await response.text()
-      throw new Error(`GitHub API ${method} ${path} returned ${response.status}: ${JSON.stringify(responseBody)}`)
-    }
-    if (response.status === 204) return undefined
-    return response.json()
-  }
-}
-
-/**
- * Fetch the complete pull-request file list or fail before routing a partial list.
- * @param {(path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>} api GitHub API caller.
- * @param {string} repository Owner/name repository identifier.
- * @param {number} pullNumber Pull-request number.
- * @param {number} expectedCount Pull-request changed-file count.
- * @returns {Promise<unknown[]>} Complete changed-file records.
- */
-export async function listPullRequestFiles(api, repository, pullNumber, expectedCount) {
-  if (!Number.isSafeInteger(expectedCount) || expectedCount < 0) {
-    throw new Error('pull request changed_files must be a non-negative integer')
-  }
-  if (expectedCount > MAX_PULL_REQUEST_FILES) {
-    throw new Error(`pull request has ${expectedCount} files; GitHub exposes at most ${MAX_PULL_REQUEST_FILES}`)
-  }
-  const files = []
-  for (let page = 1; files.length < expectedCount; page++) {
-    const response = await api(`/repos/${repository}/pulls/${pullNumber}/files?per_page=${PAGE_SIZE}&page=${page}`)
-    if (!Array.isArray(response) || response.length === 0) {
-      throw new Error(`GitHub returned ${files.length} of ${expectedCount} changed files`)
-    }
-    files.push(...response)
-    if (files.length > expectedCount) {
-      throw new Error(`GitHub returned ${files.length} files but the pull request reports ${expectedCount}`)
-    }
-  }
-  return files
-}
-
-/**
- * Fetch the complete chronological pull-request review list.
- * @param {(path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>} api GitHub API caller.
- * @param {string} repository Owner/name repository identifier.
- * @param {number} pullNumber Pull-request number.
- * @returns {Promise<unknown[]>} Complete review list within the supported limit.
- */
-export async function listPullRequestReviews(api, repository, pullNumber) {
-  const reviews = []
-  for (let page = 1; ; page++) {
-    const response = await api(`/repos/${repository}/pulls/${pullNumber}/reviews?per_page=${PAGE_SIZE}&page=${page}`)
-    if (!Array.isArray(response)) throw new Error('pull-request reviews response is not an array')
-    reviews.push(...response)
-    if (response.length < PAGE_SIZE) return reviews
-    if (reviews.length >= MAX_PULL_REQUEST_REVIEWS) {
-      throw new Error(`pull-request reviews exceed ${MAX_PULL_REQUEST_REVIEWS} entries`)
-    }
-  }
-}
-
-/**
- * Return users whose latest undismissed decisive review approves the pull request.
- * @param {unknown[]} reviews Chronological GitHub pull-request review records.
- * @returns {string[]} Approved reviewer logins in stable order.
- */
-export function approvedReviewerLogins(reviews) {
-  const approved = new Map()
-  for (const review of reviews) {
-    if (!isRecord(review) || !isRecord(review.user) || typeof review.user.login !== 'string') {
-      throw new Error('pull-request reviews response contains an invalid reviewer')
-    }
-    if (typeof review.state !== 'string' || !PULL_REQUEST_REVIEW_STATES.has(review.state)) {
-      throw new Error('pull-request reviews response contains an invalid state')
-    }
-    const key = review.user.login.toLowerCase()
-    if (review.state === 'APPROVED') approved.set(key, review.user.login)
-    else if (review.state === 'CHANGES_REQUESTED') approved.delete(key)
-  }
-  return [...approved.values()].sort((left, right) => left.localeCompare(right, 'en'))
-}
-
-/**
- * Fetch the pull request timeline used to identify workflow-authored review requests.
- * @param {(path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>} api GitHub API caller.
- * @param {string} repository Owner/name repository identifier.
- * @param {number} pullNumber Pull-request number.
- * @returns {Promise<unknown[]>} Complete timeline event list within the supported limit.
- */
-export async function listPullRequestTimeline(api, repository, pullNumber) {
-  const events = []
-  for (let page = 1; ; page++) {
-    const response = await api(`/repos/${repository}/issues/${pullNumber}/timeline?per_page=${PAGE_SIZE}&page=${page}`)
-    if (!Array.isArray(response)) throw new Error('pull-request timeline response is not an array')
-    events.push(...response)
-    if (response.length < PAGE_SIZE) return events
-    if (events.length >= MAX_TIMELINE_EVENTS) {
-      throw new Error(`pull-request timeline exceeds ${MAX_TIMELINE_EVENTS} events`)
-    }
-  }
-}
-
-/** Return current requested reviewers whose latest request came from this workflow identity. */
-function workflowRequestedReviewers(events, requestedReviewers) {
-  const requested = new Map(requestedReviewers.map(login => [login.toLowerCase(), login]))
-  const latestRequester = new Map()
-  for (const event of events) {
-    if (!isRecord(event) || event.event !== 'review_requested') continue
-    if (!isRecord(event.requested_reviewer) || typeof event.requested_reviewer.login !== 'string') continue
-    const key = event.requested_reviewer.login.toLowerCase()
-    if (!requested.has(key)) continue
-    if (!isRecord(event.review_requester) || typeof event.review_requester.login !== 'string') {
-      throw new Error('review-request timeline event has no requester login')
-    }
-    latestRequester.set(key, event.review_requester.login.toLowerCase())
-  }
-  return [...requested]
-    .filter(([key]) => latestRequester.get(key) === WORKFLOW_REVIEW_REQUESTER)
-    .map(([, login]) => login)
-}
-
-/** Extract and validate individual logins from GitHub's requested-reviewer response. */
-function requestedReviewerLogins(response) {
-  if (!isRecord(response) || !Array.isArray(response.users)) {
-    throw new Error('requested-reviewers response has no users array')
-  }
-  return response.users.map((user) => {
-    if (!isRecord(user) || typeof user.login !== 'string') {
-      throw new Error('requested-reviewers response contains an invalid user')
-    }
-    return user.login
-  })
-}
-
-/**
- * Print changed paths, reconcile workflow-authored requests with current
- * ownership, and cancel workflow-authored requests on drafts.
- * @param {{event: unknown, ownershipSource: string, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>, write?: (line: string) => void}} options Runtime inputs.
- * @returns {Promise<{changedCodeFiles: string[], excludedTestFiles: string[], excludedDocumentationFiles: string[], excludedCommentOnlyFiles: string[], requestedReviewers: string[], cancelledReviewers: string[]}>} Applied routing result.
- */
-export async function requestReviews({ event, ownershipSource, api, write = line => process.stdout.write(`${line}\n`) }) {
-  const pull = pullRequestFromEvent(event)
-  write('This is by automated Angry Turtle Cyborg, not a human')
-  const files = await listPullRequestFiles(api, pull.repository, pull.number, pull.changedFileCount)
-  const { reviewableChanges, ...classified } = classifyChangedFiles(files)
-  const plan = planReviewers(parseOwnership(ownershipSource), reviewableChanges)
-  writeList(write, 'Changed code files', classified.changedCodeFiles.map(file => JSON.stringify(file)))
-  writeList(write, 'Excluded test files', classified.excludedTestFiles.map(file => JSON.stringify(file)))
-  writeList(
-    write,
-    'Excluded documentation files',
-    classified.excludedDocumentationFiles.map(file => JSON.stringify(file)),
-  )
-  writeList(
-    write,
-    'Excluded comment-only files',
-    classified.excludedCommentOnlyFiles.map(file => JSON.stringify(file)),
-  )
-  writeList(
-    write,
-    'Owners by changed file',
-    plan.matches.map(({ file, changedLines, owners }) =>
-      `${JSON.stringify(file)} (${changedLines} LOC): ${owners.length ? owners.join(' ') : '(none)'}`),
-  )
-  writeList(
-    write,
-    'Owner relevance by changed LOC',
-    plan.reviewers.map(({ login, changedLines }) => `@${login}: ${changedLines}`),
-  )
-
-  const ownerCandidates = plan.reviewers.filter(({ login }) => login.toLowerCase() !== pull.author.toLowerCase())
-  if (pull.draft) {
-    const existing = await api(`/repos/${pull.repository}/pulls/${pull.number}/requested_reviewers`)
-    const requestedReviewers = requestedReviewerLogins(existing)
-    const reviewers = requestedReviewers.length === 0
-      ? []
-      : workflowRequestedReviewers(
-          await listPullRequestTimeline(api, pull.repository, pull.number),
-          requestedReviewers,
-        )
-    writeList(write, 'Review requests to cancel', reviewers.map(login => `@${login}`))
-    if (reviewers.length === 0) return { ...classified, requestedReviewers: [], cancelledReviewers: [] }
-
-    await api(`/repos/${pull.repository}/pulls/${pull.number}/requested_reviewers`, {
-      method: 'DELETE',
-      body: { reviewers },
-    })
-    const requestLabel = reviewers.length === 1 ? 'request' : 'requests'
-    write(`Cancelled review ${requestLabel} for ${reviewers.map(login => `@${login}`).join(' ')}.`)
-    return { ...classified, requestedReviewers: [], cancelledReviewers: reviewers }
-  }
-
-  const approvedReviewerKeys = new Set(
-    (ownerCandidates.length === 0
-      ? []
-      : approvedReviewerLogins(await listPullRequestReviews(api, pull.repository, pull.number)))
-      .map(login => login.toLowerCase()),
-  )
-  const approvedOwners = ownerCandidates.filter(({ login }) => approvedReviewerKeys.has(login.toLowerCase()))
-  const candidates = ownerCandidates.filter(({ login }) => !approvedReviewerKeys.has(login.toLowerCase()))
-  writeList(write, 'Approved owners omitted from review requests', approvedOwners.map(({ login }) => `@${login}`))
-
-  const existing = await api(`/repos/${pull.repository}/pulls/${pull.number}/requested_reviewers`)
-  const currentReviewers = requestedReviewerLogins(existing).sort((left, right) => left.localeCompare(right, 'en'))
-  const workflowReviewers = currentReviewers.length === 0
-    ? []
-    : workflowRequestedReviewers(
-        await listPullRequestTimeline(api, pull.repository, pull.number),
-        currentReviewers,
-      )
-  const workflowReviewerKeys = new Set(workflowReviewers.map(login => login.toLowerCase()))
-  const manualReviewers = currentReviewers.filter(login => !workflowReviewerKeys.has(login.toLowerCase()))
-  let retainedCountedSlots = Math.max(
-    0,
-    MAX_COUNTED_REQUESTED_REVIEWERS
-      - manualReviewers.filter(login => login.toLowerCase() !== UNCOUNTED_REVIEWER).length,
-  )
-  const retainedWorkflowReviewerKeys = new Set()
-  for (const { login } of candidates) {
-    const key = login.toLowerCase()
-    if (!workflowReviewerKeys.has(key)) continue
-    if (key === UNCOUNTED_REVIEWER) retainedWorkflowReviewerKeys.add(key)
-    else if (retainedCountedSlots > 0) {
-      retainedWorkflowReviewerKeys.add(key)
-      retainedCountedSlots--
-    }
-  }
-  const reviewersToCancel = workflowReviewers.filter(
-    login => !retainedWorkflowReviewerKeys.has(login.toLowerCase()),
-  )
-  const cancelledReviewerKeys = new Set(reviewersToCancel.map(login => login.toLowerCase()))
-  const remainingReviewers = currentReviewers.filter(login => !cancelledReviewerKeys.has(login.toLowerCase()))
-  const alreadyRequested = new Set(remainingReviewers.map(login => login.toLowerCase()))
-  const availableSlots = Math.max(
-    0,
-    MAX_COUNTED_REQUESTED_REVIEWERS
-      - remainingReviewers.filter(login => login.toLowerCase() !== UNCOUNTED_REVIEWER).length,
-  )
-  writeList(write, 'Current individual review requests', currentReviewers.map(login => `@${login}`))
-  write(`Available counted review request slots: ${availableSlots}.`)
-  const reviewers = candidates
-    .filter(({ login }) => !alreadyRequested.has(login.toLowerCase()))
-    .slice(0, availableSlots)
-    .map(({ login }) => login)
-  writeList(write, 'Review requests to cancel', reviewersToCancel.map(login => `@${login}`))
-  writeList(write, 'Reviewers to request', reviewers.map(login => `@${login}`))
-  if (reviewersToCancel.length > 0) {
-    await api(`/repos/${pull.repository}/pulls/${pull.number}/requested_reviewers`, {
-      method: 'DELETE',
-      body: { reviewers: reviewersToCancel },
-    })
-    const requestLabel = reviewersToCancel.length === 1 ? 'request' : 'requests'
-    write(`Cancelled review ${requestLabel} for ${reviewersToCancel.map(login => `@${login}`).join(' ')}.`)
-  }
-
-  if (reviewers.length > 0) {
-    await api(`/repos/${pull.repository}/pulls/${pull.number}/requested_reviewers`, {
-      method: 'POST',
-      body: { reviewers },
-    })
-    write(`Requested ${reviewers.map(login => `@${login}`).join(' ')}.`)
-  }
-  return { ...classified, requestedReviewers: reviewers, cancelledReviewers: reviewersToCancel }
-}
-
-function pullRequestFromEvent(event) {
-  if (!isRecord(event) || !isRecord(event.repository) || typeof event.repository.full_name !== 'string') {
-    throw new Error('event has no repository.full_name')
-  }
-  if (!isRecord(event.pull_request) || !isRecord(event.pull_request.user)) {
-    throw new Error('event has no pull_request')
-  }
-  const { pull_request: pull } = event
-  if (!Number.isSafeInteger(pull.number) || pull.number <= 0) throw new Error('pull request has no valid number')
-  if (typeof pull.draft !== 'boolean') throw new Error('pull request has no draft flag')
-  if (typeof pull.user.login !== 'string' || !pull.user.login) throw new Error('pull request has no author login')
-  return {
-    repository: event.repository.full_name,
-    number: pull.number,
-    draft: pull.draft,
-    author: pull.user.login,
-    changedFileCount: pull.changed_files,
-  }
-}
-
-function writeList(write, title, entries) {
-  write(`${title}:`)
-  if (entries.length === 0) write('- (none)')
-  else for (const entry of entries) write(`- ${entry}`)
-}
-
-function isRecord(value) {
-  return typeof value === 'object' && value !== null && !Array.isArray(value)
-}
-
-async function main() {
-  const eventPath = process.env.GITHUB_EVENT_PATH
-  if (!eventPath) throw new Error('GITHUB_EVENT_PATH is not set')
-  const event = JSON.parse(readFileSync(eventPath, 'utf8'))
-  const ownershipSource = readFileSync(new URL('CODEOWNERS', import.meta.url), 'utf8')
-  const api = createGitHubApi({
-    token: process.env.GITHUB_TOKEN ?? '',
-    apiUrl: process.env.GITHUB_API_URL,
-  })
-  await requestReviews({ event, ownershipSource, api })
-}
-
-if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
-  main().catch((error) => {
-    process.stderr.write(`request-review failed: ${error instanceof Error ? error.message : String(error)}\n`)
-    process.exitCode = 1
-  })
-}

+ 0 - 869
.github/review-ownership/request-review.test.mjs

@@ -1,869 +0,0 @@
-import assert from 'node:assert/strict'
-import { execFileSync } from 'node:child_process'
-import { existsSync, readFileSync } from 'node:fs'
-import test from 'node:test'
-
-import {
-  approvedReviewerLogins,
-  classifyChangedFiles,
-  createGitHubApi,
-  isCommentOnlyChange,
-  isDocumentationPath,
-  isTestPath,
-  listPullRequestFiles,
-  listPullRequestReviews,
-  listPullRequestTimeline,
-  normalizeRepositoryPath,
-  parseOwnership,
-  planReviewers,
-  requestReviews,
-} from './request-review.mjs'
-
-const ownershipSource = readFileSync(new URL('CODEOWNERS', import.meta.url), 'utf8')
-
-const pullRequestEvent = ({ author = 'author', changedFiles = 1, draft = false } = {}) => ({
-  repository: { full_name: 'deepseek-harness/deepseek-harness' },
-  pull_request: {
-    number: 42,
-    draft,
-    changed_files: changedFiles,
-    user: { login: author },
-  },
-})
-
-test('loads the repository ownership policy without test-only directory rules', () => {
-  const rules = parseOwnership(ownershipSource)
-  const ownersByPattern = new Map(rules.map(rule => [rule.pattern, rule.owners]))
-  assert.equal(rules.length, 57)
-  assert.equal(rules.some(rule => rule.pattern === '/benchmarks/'), false)
-  assert.equal(rules.some(rule => rule.pattern === '/scripts/'), false)
-  assert.equal(rules.some(rule => rule.pattern === '/snapshots/'), false)
-  assert.equal(rules.some(rule => rule.pattern === '/packages/test-support/'), false)
-  assert.deepEqual(ownersByPattern.get('/apps/cli/'), ['@turtle1999'])
-  assert.deepEqual(ownersByPattern.get('/docs/'), ['@turtle1999'])
-  assert.deepEqual(ownersByPattern.get('/packages/core/'), ['@turtle1999', '@mektpoy'])
-  assert.deepEqual(ownersByPattern.get('/packages/llm/'), ['@LegGasai'])
-  assert.deepEqual(ownersByPattern.get('/packages/preset/'), ['@LegGasai', '@turtle1999'])
-  assert.deepEqual(ownersByPattern.get('/packages/session/'), ['@turtle1999', '@mektpoy'])
-  assert.deepEqual(ownersByPattern.get('/packages/subagent/'), ['@Dudu-0223'])
-  assert.deepEqual(ownersByPattern.get('/packages/web/'), ['@imccyu'])
-  assert.deepEqual(ownersByPattern.get('/python/'), ['@LegGasai'])
-  assert.deepEqual(ownersByPattern.get('/website/'), ['@LegGasai'])
-  assert.equal(rules.every(rule => rule.owners.length <= 2), true)
-  for (const excludedOwner of ['@tianyicui', '@kermeanx', '@pkh-xht']) {
-    assert.equal(rules.some(rule => rule.owners.some(owner => owner.toLowerCase() === excludedOwner)), false)
-  }
-})
-
-test('keeps turtle below one third of the eligible owned codebase', () => {
-  const rules = parseOwnership(ownershipSource)
-  const trackedFiles = execFileSync('git', ['ls-files', '-z'], { encoding: 'utf8' })
-    .split('\0')
-    .filter(file => file && existsSync(file))
-  let ownedLines = 0
-  let turtleLines = 0
-  for (const file of trackedFiles) {
-    if (isTestPath(file) || isDocumentationPath(file)) continue
-    const owners = planReviewers(rules, [{ paths: [file], changedLines: 0 }]).matches[0]?.owners ?? []
-    if (owners.length === 0) continue
-    const content = readFileSync(file)
-    const lines = content.length === 0
-      ? 0
-      : content.reduce((count, byte) => count + (byte === 10 ? 1 : 0), 0) + (content.at(-1) === 10 ? 0 : 1)
-    ownedLines += lines
-    if (owners.includes('@turtle1999')) turtleLines += lines
-  }
-  assert.ok(
-    turtleLines * 3 <= ownedLines,
-    `@turtle1999 owns ${turtleLines} of ${ownedLines} eligible owned lines`,
-  )
-})
-
-test('rejects ownership forms the requester cannot apply safely', () => {
-  for (const [source, message] of [
-    ['', /contains no rules/u],
-    ['* @owner\n', /explicit absolute directory/u],
-    ['/.github/ @owner\n', /hidden-directory/u],
-    ['/packages/*/ @owner\n', /explicit absolute directory/u],
-    ['/packages/core/\n', /at least one owner/u],
-    ['/packages/core/ @org/team\n', /individual GitHub users/u],
-    ['/packages/core/ @one @two @three\n', /at most 2 owners/u],
-    ['/packages/core/ @owner @OWNER\n', /duplicate owner/u],
-    ['/packages/core/ @owner\n/packages/core/ @other\n', /duplicate pattern/u],
-  ]) {
-    assert.throws(() => parseOwnership(source), message)
-  }
-})
-
-test('recognizes every repository test location and filename convention', () => {
-  for (const file of [
-    'apps/cli/tests/args.spec.ts',
-    'apps/cli/tests/harness.ts',
-    'apps/web/stress-tests/reasoning-chunks.stress.ts',
-    'benchmarks/session-open/workload.ts',
-    'native/landlock-run/test/entry.test.js',
-    'packages/core/agent/__tests__/agent.ts',
-    'packages/core/agent/benches/agent.rs',
-    'packages/core/agent/src/agent.compat.spec.ts',
-    'packages/core/agent/src/__snapshots__/agent.ts.snap',
-    'packages/session-query/session-query/tests/test-service.ts',
-    'packages/test-support/session-snapshot/src/index.ts',
-    'python/sdk/src/test_client.py',
-    'python/sdk/src/client_test.py',
-    'scripts/fixtures/translation-prompt/response.txt',
-    'scripts/session-snapshot-corpus.corpus.ts',
-    'scripts/snapshots/translation-prompt-v4/request-response.expected.json',
-    'snapshots/session/headless.snapshot.ts',
-  ]) {
-    assert.equal(isTestPath(file), true, file)
-  }
-})
-
-test('does not confuse production names with tests', () => {
-  for (const file of [
-    'apps/cli/src/testing.ts',
-    'packages/core/agent/src/contest.ts',
-    'packages/session/session-format/src/snapshot.ts',
-    'packages/session/session-format/src/spec.ts',
-    'packages/session/session-format/src/test.ts',
-    'scripts/run-gates.ts',
-    'vitest.config.ts',
-    'vitest.bench.config.ts',
-    'vitest.e2e.config.ts',
-    'vitest.snapshot.config.ts',
-    'vitest.web.perf.config.ts',
-    'website/docs.ts',
-  ]) {
-    assert.equal(isTestPath(file), false, file)
-  }
-})
-
-test('excludes Markdown and YAML documentation extensions', () => {
-  for (const file of [
-    'README.md',
-    'docs/architecture.MD',
-    'packages/subagent/subagent/guide.yaml',
-    'profiles/example.YAML',
-  ]) {
-    assert.equal(isDocumentationPath(file), true, file)
-  }
-  for (const file of [
-    '.github/workflows/request-review.yml',
-    'packages/subagent/subagent/src/index.ts',
-    'website/docs.ts',
-  ]) {
-    assert.equal(isDocumentationPath(file), false, file)
-  }
-})
-
-test('detects comment-only changes only from complete supported patches', () => {
-  for (const file of [
-    {
-      filename: 'packages/core/agent/src/index.ts',
-      status: 'modified', additions: 1, deletions: 1,
-      patch: '@@ -1,2 +1,2 @@\n-// old note\n+// new note\n const value = "https://example.com"',
-    },
-    {
-      filename: 'python/sdk/src/client.py',
-      status: 'modified', additions: 1, deletions: 1,
-      patch: '@@ -1 +1 @@\n-value = 1  # old note\n+value = 1  # new note',
-    },
-    {
-      filename: 'native/landlock-run/src/main.rs',
-      status: 'modified', additions: 1, deletions: 1,
-      patch: '@@ -1 +1 @@\n-let value = 1; /* old note */\n+let value = 1; /* new note */',
-    },
-  ]) {
-    assert.equal(isCommentOnlyChange(file), true, file.filename)
-  }
-
-  for (const file of [
-    {
-      filename: 'packages/core/agent/src/index.ts',
-      status: 'modified', additions: 1, deletions: 1,
-      patch: '@@ -1 +1 @@\n-const value = 1 // note\n+const value = 2 // note',
-    },
-    {
-      filename: 'packages/core/agent/src/index.ts',
-      status: 'modified', additions: 2, deletions: 1,
-      patch: '@@ -1 +1 @@\n-// old note\n+// new note',
-    },
-    {
-      filename: 'packages/core/agent/src/data.json',
-      status: 'modified', additions: 1, deletions: 1,
-      patch: '@@ -1 +1 @@\n-{"value":1}\n+{"value":2}',
-    },
-    {
-      filename: 'native/landlock-run/src/main.rs',
-      status: 'modified', additions: 1, deletions: 1,
-      patch: '@@ -1 +1 @@\n-let value = r#"https://old.example"#;\n+let value = r#"https://new.example"#;',
-    },
-    {
-      filename: 'packages/core/agent/src/index.ts',
-      status: 'renamed', additions: 1, deletions: 1,
-      patch: '@@ -1 +1 @@\n-// old note\n+// new note',
-    },
-  ]) {
-    assert.equal(isCommentOnlyChange(file), false, file.filename)
-  }
-})
-
-test('normalizes separators and rejects paths that are not repository-relative', () => {
-  assert.equal(normalizeRepositoryPath('./packages\\core\\agent\\src\\index.ts'), 'packages/core/agent/src/index.ts')
-  for (const file of ['', '/absolute.ts', '../escape.ts', 'packages//empty.ts', 'packages/./same.ts']) {
-    assert.throws(() => normalizeRepositoryPath(file), /path/u, file)
-  }
-})
-
-test('classifies both sides of a rename independently', () => {
-  assert.deepEqual(
-    classifyChangedFiles([
-      {
-        filename: 'packages/core/agent/tests/moved.spec.ts',
-        previous_filename: 'packages/core/agent/src/moved.ts',
-        additions: 3,
-        deletions: 2,
-      },
-      {
-        filename: 'packages/client/store/src/restored.ts',
-        previous_filename: 'packages/client/store/tests/restored.spec.ts',
-        additions: 2,
-        deletions: 1,
-      },
-      { filename: 'packages/core/agent/README.md', additions: 1, deletions: 0 },
-      {
-        filename: 'packages/core/agent/src/commented.ts',
-        status: 'modified', additions: 1, deletions: 1,
-        patch: '@@ -1 +1 @@\n-// old note\n+// new note',
-      },
-    ]),
-    {
-      changedCodeFiles: [
-        'packages/client/store/src/restored.ts',
-        'packages/core/agent/src/moved.ts',
-      ],
-      reviewableChanges: [
-        { paths: ['packages/core/agent/src/moved.ts'], changedLines: 5 },
-        { paths: ['packages/client/store/src/restored.ts'], changedLines: 3 },
-      ],
-      excludedTestFiles: [
-        'packages/client/store/tests/restored.spec.ts',
-        'packages/core/agent/tests/moved.spec.ts',
-      ],
-      excludedDocumentationFiles: ['packages/core/agent/README.md'],
-      excludedCommentOnlyFiles: ['packages/core/agent/src/commented.ts'],
-    },
-  )
-})
-
-test('uses the last matching ownership rule and ranks owners by changed LOC', () => {
-  const rules = parseOwnership('/packages/ @broad\n/packages/core/ @core @second\n')
-  assert.deepEqual(
-    planReviewers(rules, [
-      { paths: ['AGENTS.md'], changedLines: 1 },
-      { paths: ['packages/core/agent/src/index.ts'], changedLines: 8 },
-      { paths: ['packages/fs/fs/src/index.ts'], changedLines: 3 },
-    ]),
-    {
-      matches: [
-        { file: 'AGENTS.md', changedLines: 1, owners: [] },
-        { file: 'packages/core/agent/src/index.ts', changedLines: 8, owners: ['@core', '@second'] },
-        { file: 'packages/fs/fs/src/index.ts', changedLines: 3, owners: ['@broad'] },
-      ],
-      reviewers: [
-        { login: 'core', changedLines: 8 },
-        { login: 'second', changedLines: 8 },
-        { login: 'broad', changedLines: 3 },
-      ],
-    },
-  )
-})
-
-test('counts each changed-file record once per owner across rename paths', () => {
-  const rules = parseOwnership('/packages/a/ @same @a\n/packages/b/ @same @b\n/packages/c/ @c\n')
-  const plan = planReviewers(rules, [
-    { paths: ['packages/a/old.ts', 'packages/b/new.ts'], changedLines: 10 },
-    { paths: ['packages/a/other.ts'], changedLines: 5 },
-    { paths: ['packages/c/tiny.ts'], changedLines: 1 },
-  ])
-  assert.deepEqual(plan.reviewers, [
-    { login: 'a', changedLines: 15 },
-    { login: 'same', changedLines: 15 },
-    { login: 'b', changedLines: 10 },
-    { login: 'c', changedLines: 1 },
-  ])
-})
-
-test('rejects invalid changed-file LOC', () => {
-  for (const file of [
-    { filename: 'packages/core/index.ts', deletions: 0 },
-    { filename: 'packages/core/index.ts', additions: -1, deletions: 0 },
-    { filename: 'packages/core/index.ts', additions: Number.MAX_SAFE_INTEGER, deletions: 1 },
-  ]) {
-    assert.throws(() => classifyChangedFiles([file]), /changed-file|LOC/u)
-  }
-})
-
-test('fetches every declared changed file across pages', async () => {
-  const calls = []
-  const pageOne = Array.from({ length: 100 }, (_, index) => ({ filename: `packages/core/file-${index}.ts` }))
-  const pageTwo = [{ filename: 'packages/core/file-100.ts' }]
-  const api = async (path) => {
-    calls.push(path)
-    return calls.length === 1 ? pageOne : pageTwo
-  }
-  const files = await listPullRequestFiles(api, 'owner/repo', 42, 101)
-  assert.equal(files.length, 101)
-  assert.deepEqual(calls, [
-    '/repos/owner/repo/pulls/42/files?per_page=100&page=1',
-    '/repos/owner/repo/pulls/42/files?per_page=100&page=2',
-  ])
-})
-
-test('fails closed when GitHub cannot provide the complete file list', async () => {
-  let calls = 0
-  await assert.rejects(
-    listPullRequestFiles(async () => {
-      calls++
-      return calls === 1 ? [{ filename: 'one.ts' }] : []
-    }, 'owner/repo', 42, 2),
-    /returned 1 of 2/u,
-  )
-  await assert.rejects(
-    listPullRequestFiles(async () => [], 'owner/repo', 42, 3_001),
-    /at most 3000/u,
-  )
-})
-
-test('fetches pull-request reviews across pages', async () => {
-  const calls = []
-  const pageOne = Array.from({ length: 100 }, (_, index) => ({
-    user: { login: `reviewer-${index}` },
-    state: 'COMMENTED',
-  }))
-  const pageTwo = [{ user: { login: 'approver' }, state: 'APPROVED' }]
-  const reviews = await listPullRequestReviews(async (path) => {
-    calls.push(path)
-    return calls.length === 1 ? pageOne : pageTwo
-  }, 'owner/repo', 42)
-
-  assert.equal(reviews.length, 101)
-  assert.deepEqual(calls, [
-    '/repos/owner/repo/pulls/42/reviews?per_page=100&page=1',
-    '/repos/owner/repo/pulls/42/reviews?per_page=100&page=2',
-  ])
-})
-
-test('tracks each reviewer\'s latest undismissed approval decision', () => {
-  assert.deepEqual(approvedReviewerLogins([
-    { user: { login: 'commented-after' }, state: 'APPROVED' },
-    { user: { login: 'commented-after' }, state: 'COMMENTED' },
-    { user: { login: 'changes-after' }, state: 'APPROVED' },
-    { user: { login: 'changes-after' }, state: 'CHANGES_REQUESTED' },
-    { user: { login: 'dismissed' }, state: 'DISMISSED' },
-    { user: { login: 'approved-after' }, state: 'CHANGES_REQUESTED' },
-    { user: { login: 'approved-after' }, state: 'APPROVED' },
-    { user: { login: 'pending-after' }, state: 'APPROVED' },
-    { user: { login: 'pending-after' }, state: 'PENDING' },
-  ]), ['approved-after', 'commented-after', 'pending-after'])
-
-  assert.throws(
-    () => approvedReviewerLogins([{ user: { login: 'reviewer' }, state: 'UNKNOWN' }]),
-    /invalid state/u,
-  )
-  assert.throws(() => approvedReviewerLogins([{ state: 'APPROVED' }]), /invalid reviewer/u)
-})
-
-test('fails closed when the pull-request review list exceeds its limit', async () => {
-  let calls = 0
-  await assert.rejects(
-    listPullRequestReviews(async () => {
-      calls++
-      return Array.from({ length: 100 }, () => ({ user: { login: 'reviewer' }, state: 'COMMENTED' }))
-    }, 'owner/repo', 42),
-    /exceed 3000 entries/u,
-  )
-  assert.equal(calls, 30)
-})
-
-test('fails closed when the review-request timeline exceeds its limit', async () => {
-  let calls = 0
-  await assert.rejects(
-    listPullRequestTimeline(async () => {
-      calls++
-      return Array.from({ length: 100 }, () => ({ event: 'commented' }))
-    }, 'owner/repo', 42),
-    /exceeds 3000 events/u,
-  )
-  assert.equal(calls, 30)
-})
-
-test('prints changed code files and requests the highest-ranked counted owner', async () => {
-  const trace = []
-  const files = [
-    { filename: 'packages/core/agent/src/index.ts', additions: 70, deletions: 10 },
-    { filename: 'packages/preset/agent-presets/src/index.ts', additions: 5, deletions: 5 },
-    { filename: 'packages/client/store/src/index.ts', additions: 2, deletions: 0 },
-    { filename: 'packages/subagent/subagent/src/index.ts', additions: 40, deletions: 0 },
-    { filename: 'packages/core/agent/tests/index.spec.ts', additions: 100, deletions: 0 },
-    { filename: 'AGENTS.md', additions: 200, deletions: 0 },
-  ]
-  const api = async (path, options = {}) => {
-    trace.push({ type: 'api', path, options })
-    if (path.endsWith('/files?per_page=100&page=1')) return files
-    if (path.endsWith('/reviews?per_page=100&page=1')) return []
-    if (path.endsWith('/requested_reviewers') && options.method !== 'POST') {
-      return { users: [], teams: [] }
-    }
-    if (path.endsWith('/requested_reviewers') && options.method === 'POST') return {}
-    throw new Error(`unexpected API path ${path}`)
-  }
-
-  const result = await requestReviews({
-    event: pullRequestEvent({ author: 'turtle1999', changedFiles: files.length }),
-    ownershipSource,
-    api,
-    write: line => trace.push({ type: 'log', line }),
-  })
-
-  assert.deepEqual(result, {
-    changedCodeFiles: [
-      'packages/client/store/src/index.ts',
-      'packages/core/agent/src/index.ts',
-      'packages/preset/agent-presets/src/index.ts',
-      'packages/subagent/subagent/src/index.ts',
-    ],
-    excludedTestFiles: ['packages/core/agent/tests/index.spec.ts'],
-    excludedDocumentationFiles: ['AGENTS.md'],
-    excludedCommentOnlyFiles: [],
-    requestedReviewers: ['mektpoy'],
-    cancelledReviewers: [],
-  })
-  assert.equal(trace[0].type, 'log')
-  assert.equal(trace[0].line, 'This is by automated Angry Turtle Cyborg, not a human')
-  const changedHeading = trace.findIndex(item => item.type === 'log' && item.line === 'Changed code files:')
-  const relevanceHeading = trace.findIndex(item => item.type === 'log' && item.line === 'Owner relevance by changed LOC:')
-  const post = trace.findIndex(item => item.type === 'api' && item.options.method === 'POST')
-  assert.ok(changedHeading >= 0 && changedHeading < relevanceHeading && relevanceHeading < post)
-  assert.deepEqual(trace.slice(relevanceHeading, relevanceHeading + 6).map(item => item.line), [
-    'Owner relevance by changed LOC:',
-    '- @turtle1999: 90',
-    '- @mektpoy: 80',
-    '- @Dudu-0223: 40',
-    '- @LegGasai: 10',
-    '- @imccyu: 2',
-  ])
-  assert.deepEqual(trace[post], {
-    type: 'api',
-    path: '/repos/deepseek-harness/deepseek-harness/pulls/42/requested_reviewers',
-    options: {
-      method: 'POST',
-      body: { reviewers: ['mektpoy'] },
-    },
-  })
-})
-
-test('does not request an owner again after that owner approves', async () => {
-  const calls = []
-  const output = []
-  const result = await requestReviews({
-    event: pullRequestEvent(),
-    ownershipSource: '/packages/typert/ @imccyu\n',
-    api: async (path, options = {}) => {
-      calls.push({ path, options })
-      if (path.endsWith('/files?per_page=100&page=1')) {
-        return [{ filename: 'packages/typert/generator/src/analyzer.ts', additions: 150, deletions: 47 }]
-      }
-      if (path.endsWith('/reviews?per_page=100&page=1')) {
-        return [
-          { user: { login: 'imccyu' }, state: 'APPROVED' },
-          { user: { login: 'imccyu' }, state: 'COMMENTED' },
-        ]
-      }
-      if (path.endsWith('/requested_reviewers') && options.method === undefined) {
-        return { users: [], teams: [] }
-      }
-      throw new Error(`unexpected API path ${path}`)
-    },
-    write: line => output.push(line),
-  })
-
-  assert.deepEqual(result.requestedReviewers, [])
-  assert.equal(calls.some(call => call.options.method === 'POST'), false)
-  const approvedHeading = output.indexOf('Approved owners omitted from review requests:')
-  assert.ok(approvedHeading >= 0)
-  assert.equal(output[approvedHeading + 1], '- @imccyu')
-})
-
-test('fills the counted slot with the next owner after omitting an approved owner', async () => {
-  const calls = []
-  const result = await requestReviews({
-    event: pullRequestEvent(),
-    ownershipSource: '/packages/core/ @imccyu @mektpoy\n',
-    api: async (path, options = {}) => {
-      calls.push({ path, options })
-      if (path.endsWith('/files?per_page=100&page=1')) {
-        return [{ filename: 'packages/core/agent/src/index.ts', additions: 20, deletions: 10 }]
-      }
-      if (path.endsWith('/reviews?per_page=100&page=1')) {
-        return [{ user: { login: 'imccyu' }, state: 'APPROVED' }]
-      }
-      if (path.endsWith('/requested_reviewers') && options.method === undefined) {
-        return { users: [], teams: [] }
-      }
-      if (path.endsWith('/requested_reviewers') && options.method === 'POST') return {}
-      throw new Error(`unexpected API path ${path}`)
-    },
-    write: () => {},
-  })
-
-  assert.deepEqual(result.requestedReviewers, ['mektpoy'])
-  assert.deepEqual(calls.find(call => call.options.method === 'POST'), {
-    path: '/repos/deepseek-harness/deepseek-harness/pulls/42/requested_reviewers',
-    options: { method: 'POST', body: { reviewers: ['mektpoy'] } },
-  })
-})
-
-test('does not add another counted owner when one is already requested', async () => {
-  const calls = []
-  const output = []
-  const result = await requestReviews({
-    event: pullRequestEvent(),
-    ownershipSource: '/packages/core/ @mektpoy\n',
-    api: async (path, options = {}) => {
-      calls.push({ path, options })
-      if (path.endsWith('/files?per_page=100&page=1')) {
-        return [{ filename: 'packages/core/agent/src/index.ts', additions: 20, deletions: 10 }]
-      }
-      if (path.endsWith('/reviews?per_page=100&page=1')) return []
-      if (path.endsWith('/requested_reviewers') && options.method === undefined) {
-        return { users: [{ login: 'first' }], teams: [] }
-      }
-      if (path.endsWith('/timeline?per_page=100&page=1')) return []
-      throw new Error(`unexpected API path ${path}`)
-    },
-    write: line => output.push(line),
-  })
-
-  assert.deepEqual(result.requestedReviewers, [])
-  assert.equal(calls.some(call => call.options.method === 'POST'), false)
-  assert.deepEqual(output.slice(-7), [
-    'Current individual review requests:',
-    '- @first',
-    'Available counted review request slots: 0.',
-    'Review requests to cancel:',
-    '- (none)',
-    'Reviewers to request:',
-    '- (none)',
-  ])
-})
-
-test('requests at most one owner per run when turtle ranks first', async () => {
-  const calls = []
-  const result = await requestReviews({
-    event: pullRequestEvent({ author: 'contributor', changedFiles: 2 }),
-    ownershipSource: '/packages/core/ @turtle1999\n/packages/client/ @mektpoy\n',
-    api: async (path, options = {}) => {
-      calls.push({ path, options })
-      if (path.endsWith('/files?per_page=100&page=1')) {
-        return [
-          { filename: 'packages/core/agent/src/index.ts', additions: 25, deletions: 5 },
-          { filename: 'packages/client/store/src/index.ts', additions: 8, deletions: 2 },
-        ]
-      }
-      if (path.endsWith('/reviews?per_page=100&page=1')) return []
-      if (path.endsWith('/requested_reviewers') && options.method === undefined) {
-        return { users: [], teams: [] }
-      }
-      if (path.endsWith('/requested_reviewers') && options.method === 'POST') return {}
-      throw new Error(`unexpected API path ${path}`)
-    },
-    write: () => {},
-  })
-
-  assert.deepEqual(result.requestedReviewers, ['turtle1999'])
-  assert.deepEqual(calls.find(call => call.options.method === 'POST'), {
-    path: '/repos/deepseek-harness/deepseek-harness/pulls/42/requested_reviewers',
-    options: { method: 'POST', body: { reviewers: ['turtle1999'] } },
-  })
-})
-
-test('does not add turtle when one counted reviewer is already requested', async () => {
-  const calls = []
-  const result = await requestReviews({
-    event: pullRequestEvent({ author: 'contributor' }),
-    ownershipSource: '/packages/core/ @turtle1999 @mektpoy\n',
-    api: async (path, options = {}) => {
-      calls.push({ path, options })
-      if (path.endsWith('/files?per_page=100&page=1')) {
-        return [{ filename: 'packages/core/agent/src/index.ts', additions: 20, deletions: 10 }]
-      }
-      if (path.endsWith('/reviews?per_page=100&page=1')) return []
-      if (path.endsWith('/requested_reviewers') && options.method === undefined) {
-        return { users: [{ login: 'first' }], teams: [] }
-      }
-      if (path.endsWith('/timeline?per_page=100&page=1')) return []
-      throw new Error(`unexpected API path ${path}`)
-    },
-    write: () => {},
-  })
-
-  assert.deepEqual(result.requestedReviewers, [])
-  assert.equal(calls.some(call => call.options.method === 'POST'), false)
-})
-
-test('keeps the counted slot available when turtle is already requested', async () => {
-  const calls = []
-  const result = await requestReviews({
-    event: pullRequestEvent({ author: 'contributor' }),
-    ownershipSource: '/packages/core/ @turtle1999 @mektpoy\n',
-    api: async (path, options = {}) => {
-      calls.push({ path, options })
-      if (path.endsWith('/files?per_page=100&page=1')) {
-        return [{ filename: 'packages/core/agent/src/index.ts', additions: 20, deletions: 10 }]
-      }
-      if (path.endsWith('/reviews?per_page=100&page=1')) return []
-      if (path.endsWith('/requested_reviewers') && options.method === undefined) {
-        return { users: [{ login: 'turtle1999' }], teams: [] }
-      }
-      if (path.endsWith('/timeline?per_page=100&page=1')) return []
-      if (path.endsWith('/requested_reviewers') && options.method === 'POST') return {}
-      throw new Error(`unexpected API path ${path}`)
-    },
-    write: () => {},
-  })
-
-  assert.deepEqual(result.requestedReviewers, ['mektpoy'])
-  assert.deepEqual(calls.find(call => call.options.method === 'POST'), {
-    path: '/repos/deepseek-harness/deepseek-harness/pulls/42/requested_reviewers',
-    options: { method: 'POST', body: { reviewers: ['mektpoy'] } },
-  })
-})
-
-test('replaces a workflow reviewer that no longer matches current ownership', async () => {
-  const trace = []
-  const result = await requestReviews({
-    event: pullRequestEvent({ author: 'contributor' }),
-    ownershipSource: '/packages/core/ @mektpoy\n',
-    api: async (path, options = {}) => {
-      trace.push({ type: 'api', path, options })
-      if (path.endsWith('/files?per_page=100&page=1')) {
-        return [{ filename: 'packages/core/agent/src/index.ts', additions: 20, deletions: 10 }]
-      }
-      if (path.endsWith('/reviews?per_page=100&page=1')) return []
-      if (path.endsWith('/requested_reviewers') && options.method === undefined) {
-        return { users: [{ login: 'Dudu-0223' }], teams: [] }
-      }
-      if (path.endsWith('/timeline?per_page=100&page=1')) {
-        return [{
-          event: 'review_requested',
-          requested_reviewer: { login: 'Dudu-0223' },
-          review_requester: { login: 'github-actions[bot]' },
-        }]
-      }
-      if (path.endsWith('/requested_reviewers') && options.method === 'DELETE') return {}
-      if (path.endsWith('/requested_reviewers') && options.method === 'POST') return {}
-      throw new Error(`unexpected API path ${path}`)
-    },
-    write: line => trace.push({ type: 'log', line }),
-  })
-
-  assert.deepEqual(result.requestedReviewers, ['mektpoy'])
-  assert.deepEqual(result.cancelledReviewers, ['Dudu-0223'])
-  const cancelLog = trace.findIndex(item => item.type === 'log' && item.line === 'Review requests to cancel:')
-  const requestLog = trace.findIndex(item => item.type === 'log' && item.line === 'Reviewers to request:')
-  const firstMutation = trace.findIndex(item => item.type === 'api' && item.options.method !== undefined)
-  assert.ok(cancelLog >= 0 && requestLog >= 0 && cancelLog < firstMutation && requestLog < firstMutation)
-  assert.equal(trace[cancelLog + 1].line, '- @Dudu-0223')
-  assert.equal(trace[requestLog + 1].line, '- @mektpoy')
-  assert.deepEqual(trace.filter(item => item.type === 'api' && item.options.method !== undefined), [
-    {
-      type: 'api',
-      path: '/repos/deepseek-harness/deepseek-harness/pulls/42/requested_reviewers',
-      options: { method: 'DELETE', body: { reviewers: ['Dudu-0223'] } },
-    },
-    {
-      type: 'api',
-      path: '/repos/deepseek-harness/deepseek-harness/pulls/42/requested_reviewers',
-      options: { method: 'POST', body: { reviewers: ['mektpoy'] } },
-    },
-  ])
-})
-
-test('removes excess workflow reviewers using current relevance order', async () => {
-  const calls = []
-  const result = await requestReviews({
-    event: pullRequestEvent({ author: 'contributor', changedFiles: 2 }),
-    ownershipSource: '/packages/core/ @mektpoy\n/packages/subagent/ @Dudu-0223\n',
-    api: async (path, options = {}) => {
-      calls.push({ path, options })
-      if (path.endsWith('/files?per_page=100&page=1')) {
-        return [
-          { filename: 'packages/core/agent/src/index.ts', additions: 25, deletions: 5 },
-          { filename: 'packages/subagent/subagent/src/index.ts', additions: 8, deletions: 2 },
-        ]
-      }
-      if (path.endsWith('/reviews?per_page=100&page=1')) return []
-      if (path.endsWith('/requested_reviewers') && options.method === undefined) {
-        return { users: [{ login: 'Dudu-0223' }, { login: 'mektpoy' }], teams: [] }
-      }
-      if (path.endsWith('/timeline?per_page=100&page=1')) {
-        return ['Dudu-0223', 'mektpoy'].map(login => ({
-          event: 'review_requested',
-          requested_reviewer: { login },
-          review_requester: { login: 'github-actions[bot]' },
-        }))
-      }
-      if (path.endsWith('/requested_reviewers') && options.method === 'DELETE') return {}
-      throw new Error(`unexpected API path ${path}`)
-    },
-    write: () => {},
-  })
-
-  assert.deepEqual(result, {
-    changedCodeFiles: [
-      'packages/core/agent/src/index.ts',
-      'packages/subagent/subagent/src/index.ts',
-    ],
-    excludedTestFiles: [],
-    excludedDocumentationFiles: [],
-    excludedCommentOnlyFiles: [],
-    requestedReviewers: [],
-    cancelledReviewers: ['Dudu-0223'],
-  })
-  assert.deepEqual(calls.find(call => call.options.method === 'DELETE'), {
-    path: '/repos/deepseek-harness/deepseek-harness/pulls/42/requested_reviewers',
-    options: { method: 'DELETE', body: { reviewers: ['Dudu-0223'] } },
-  })
-})
-
-test('does not request reviewers for test, documentation, or comment-only changes', async () => {
-  const calls = []
-  const output = []
-  const files = [
-    { filename: 'apps/web/tests/chat.e2e.ts', additions: 10, deletions: 0 },
-    { filename: 'packages/core/agent/tests/agent.spec.ts', additions: 10, deletions: 0 },
-    { filename: 'packages/core/agent/README.md', additions: 10, deletions: 0 },
-    { filename: 'packages/core/agent/examples.yaml', additions: 10, deletions: 0 },
-    {
-      filename: 'packages/core/agent/src/index.ts',
-      status: 'modified', additions: 1, deletions: 1,
-      patch: '@@ -1 +1 @@\n-// old note\n+// new note',
-    },
-  ]
-  const result = await requestReviews({
-    event: pullRequestEvent({ changedFiles: files.length }),
-    ownershipSource,
-    api: async (path) => {
-      calls.push(path)
-      if (path.endsWith('/files?per_page=100&page=1')) return files
-      if (path.endsWith('/requested_reviewers')) return { users: [], teams: [] }
-      throw new Error(`unexpected API path ${path}`)
-    },
-    write: line => output.push(line),
-  })
-  assert.deepEqual(result, {
-    changedCodeFiles: [],
-    excludedTestFiles: files.slice(0, 2).map(file => file.filename),
-    excludedDocumentationFiles: files.slice(2, 4).map(file => file.filename),
-    excludedCommentOnlyFiles: ['packages/core/agent/src/index.ts'],
-    requestedReviewers: [],
-    cancelledReviewers: [],
-  })
-  assert.equal(calls.length, 2)
-  assert.deepEqual(output.slice(0, 4), [
-    'This is by automated Angry Turtle Cyborg, not a human',
-    'Changed code files:',
-    '- (none)',
-    'Excluded test files:',
-  ])
-})
-
-test('cancels workflow-authored review requests on draft pull requests', async () => {
-  const trace = []
-  const files = [
-    { filename: 'packages/subagent/subagent/src/index.ts', additions: 10, deletions: 2 },
-    { filename: 'packages/subagent/subagent/tests/index.spec.ts', additions: 10, deletions: 0 },
-    { filename: 'packages/subagent/subagent/README.md', additions: 10, deletions: 0 },
-  ]
-  const result = await requestReviews({
-    event: pullRequestEvent({ draft: true, changedFiles: files.length }),
-    ownershipSource,
-    api: async (path, options = {}) => {
-      trace.push({ type: 'api', path, options })
-      if (path.endsWith('/files?per_page=100&page=1')) return files
-      if (path.endsWith('/requested_reviewers') && options.method === undefined) {
-        return { users: [{ login: 'Dudu-0223' }, { login: 'manual-reviewer' }], teams: [] }
-      }
-      if (path.endsWith('/timeline?per_page=100&page=1')) {
-        return [
-          {
-            event: 'review_requested',
-            requested_reviewer: { login: 'Dudu-0223' },
-            review_requester: { login: 'maintainer' },
-          },
-          {
-            event: 'review_requested',
-            requested_reviewer: { login: 'Dudu-0223' },
-            review_requester: { login: 'github-actions[bot]' },
-          },
-          {
-            event: 'review_requested',
-            requested_reviewer: { login: 'manual-reviewer' },
-            review_requester: { login: 'github-actions[bot]' },
-          },
-          {
-            event: 'review_requested',
-            requested_reviewer: { login: 'manual-reviewer' },
-            review_requester: { login: 'maintainer' },
-          },
-        ]
-      }
-      if (path.endsWith('/requested_reviewers') && options.method === 'DELETE') return {}
-      throw new Error(`unexpected API path ${path}`)
-    },
-    write: line => trace.push({ type: 'log', line }),
-  })
-  assert.deepEqual(result, {
-    changedCodeFiles: ['packages/subagent/subagent/src/index.ts'],
-    excludedTestFiles: ['packages/subagent/subagent/tests/index.spec.ts'],
-    excludedDocumentationFiles: ['packages/subagent/subagent/README.md'],
-    excludedCommentOnlyFiles: [],
-    requestedReviewers: [],
-    cancelledReviewers: ['Dudu-0223'],
-  })
-  const remove = trace.find(item => item.type === 'api' && item.options.method === 'DELETE')
-  assert.deepEqual(remove, {
-    type: 'api',
-    path: '/repos/deepseek-harness/deepseek-harness/pulls/42/requested_reviewers',
-    options: { method: 'DELETE', body: { reviewers: ['Dudu-0223'] } },
-  })
-  assert.equal(trace.some(item => item.type === 'log' && item.line === '- @manual-reviewer'), false)
-  assert.equal(trace.at(-1).line, 'Cancelled review request for @Dudu-0223.')
-})
-
-test('sends authenticated JSON and escapes an API error body', async () => {
-  const requests = []
-  const api = createGitHubApi({
-    token: 'secret',
-    apiUrl: 'https://github.example/api/v3/',
-    fetchImpl: async (url, options) => {
-      requests.push({ url, options })
-      return new Response(JSON.stringify({ ok: true }), {
-        status: 200,
-        headers: { 'Content-Type': 'application/json' },
-      })
-    },
-  })
-  assert.deepEqual(await api('/repos/owner/repo', { method: 'POST', body: { value: 1 } }), { ok: true })
-  assert.equal(requests[0].url, 'https://github.example/api/v3/repos/owner/repo')
-  assert.equal(requests[0].options.headers.Authorization, 'Bearer secret')
-  assert.equal(requests[0].options.headers['X-GitHub-Api-Version'], '2026-03-10')
-  assert.equal(requests[0].options.body, '{"value":1}')
-
-  const failing = createGitHubApi({
-    token: 'secret',
-    fetchImpl: async () => new Response('::error::untrusted\nbody', { status: 422 }),
-  })
-  await assert.rejects(failing('/failure'), /"::error::untrusted\\nbody"/u)
-})

+ 0 - 31
.github/workflows/request-review.yml

@@ -1,31 +0,0 @@
-name: request-review
-
-on:
-  pull_request_target:
-    types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
-
-permissions:
-  contents: read
-  pull-requests: write
-
-concurrency:
-  group: request-review-${{ github.event.pull_request.number }}
-  cancel-in-progress: true
-
-jobs:
-  request-review:
-    name: request-review
-    runs-on: ubuntu-latest
-    timeout-minutes: 5
-    steps:
-      # SECURITY: the write-capable job executes policy from the trusted default
-      # branch and reads pull-request filenames only as API data.
-      - name: Check out trusted review policy
-        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
-        with:
-          ref: ${{ github.event.repository.default_branch }}
-          persist-credentials: false
-      - name: Request reviewers
-        env:
-          GITHUB_TOKEN: ${{ github.token }}
-        run: node .github/review-ownership/request-review.mjs

+ 2 - 2
docs/i18n/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/i18n/README.md
-README.md: 55ae07c18e09fde141ecf5344f715dfa25658325
-README.zh.md: 674edeb9da4bf0083c607216a3c992a98f61897a
+README.md: 2ff8fc62f21d58a4d31b8aadd80c7a0c14556e6d
+README.zh.md: 73da4445bc35e779a990d4cd4aef605cc9078a06

+ 1 - 1
docs/i18n/README.md

@@ -51,7 +51,7 @@ Generated English references and graphs participate in pairing when a reviewed C
 - `docs/AGENTS.md`, `.agents/notes/**/AGENTS.md`, and their `CLAUDE.md` instruction symlinks — agent instructions, maintained in English only like the root `AGENTS.md`.
 - `docs/i18n/terminology.md` and [style-samples.md](style-samples.md) — both are bilingual by construction.
 - [translation-prompt.md](translation-prompt.md) — the automated pipeline's prompt template; its body is machine-consumed verbatim, so a paired translation would change pipeline behavior.
-- [review-ownership/README.md](../../.github/review-ownership/README.md) and its [Agent Note](../../.agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.md) — repository-internal automation policy maintained in English only.
+- [review-ownership/README.md](../../.github/review-ownership/README.md) — repository-internal approval policy maintained in English only.
 - `.agents/notes/archived/` — frozen historical triplets. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) validates their completeness and content seals; translation maintenance must never rewrite them.
 
 **Universal requirement**: every current or future document in scope must merge as a complete bilingual pair. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) contains only explicit exclusions; there is no per-file rollout list, date cutoff, or README-specific policy class.

+ 1 - 1
docs/i18n/README.zh.md

@@ -53,7 +53,7 @@
 - `docs/AGENTS.md`、`.agents/notes/**/AGENTS.md` 以及指向它们的 `CLAUDE.md` 指令符号链接:agent 指令,与根 `AGENTS.md` 一样只以英文维护。
 - `docs/i18n/terminology.md` 与 [style-samples.md](style-samples.md):二者本身即为中英对照文档。
 - [translation-prompt.md](translation-prompt.md):自动翻译流水线的提示词模板;正文逐字进入模型请求,配对翻译会改变流水线行为。
-- [review-ownership/README.md](../../.github/review-ownership/README.md) 及其 [Agent Note](../../.agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.md):仓库内部自动化政策,只以英文维护。
+- [review-ownership/README.md](../../.github/review-ownership/README.md):仓库内部审批策略,只以英文维护。
 - `.agents/notes/archived/`:冻结的历史三文件配对。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 校验其完整性和内容封存记录;翻译维护绝不能重写这些文件。
 
 **统一要求**:当前及今后纳入范围的每篇文档,合并时都必须构成完整的双语配对。[scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 只包含显式排除项;不存在逐文件推进清单、日期分界或 README 专用政策类别。

+ 0 - 1
package.json

@@ -59,7 +59,6 @@
     "test:expected:refresh": "DSH_SNAPSHOT=refresh vitest run --config vitest.expected.config.ts",
     "test:approval-policy": "node --test .github/review-ownership/check-approval.test.mjs",
     "test:issue-management": "node .github/issue-management/policy.test.mjs",
-    "test:request-review": "node --test .github/review-ownership/request-review.test.mjs",
     "test:snapshot": "vitest run --config vitest.snapshot.config.ts",
     "test:snapshot:record": "DSH_SNAPSHOT=record vitest run --config vitest.snapshot.config.ts --update",
     "test:snapshot:refresh": "DSH_SNAPSHOT=refresh vitest run --config vitest.snapshot.config.ts",

+ 0 - 41
scripts/ci-workflow.spec.ts

@@ -774,47 +774,6 @@ describe('Python release workflows', () => {
   })
 })
 
-describe('Request review workflow', () => {
-  it('runs trusted routing on pull request review-state updates', () => {
-    const workflow = loadWorkflow('.github/workflows/request-review.yml')
-    const event = workflowEvent(workflow, 'pull_request_target')
-    const job = workflowJob(workflow, 'request-review')
-    if (!isRecord(workflow.on)) throw new TypeError('request-review workflow must define events')
-    if (!Array.isArray(job.steps)) throw new TypeError('request-review job must define steps')
-    const steps = job.steps.filter(isRecord)
-    const checkout = steps.find(step => step.name === 'Check out trusted review policy')
-    const request = steps.find(step => step.name === 'Request reviewers')
-
-    expect(workflow.name).toBe('request-review')
-    expect(Object.keys(workflow.on)).toEqual(['pull_request_target'])
-    expect(event.types).toEqual(['opened', 'synchronize', 'reopened', 'ready_for_review', 'converted_to_draft'])
-    expect(workflow.permissions).toEqual({ contents: 'read', 'pull-requests': 'write' })
-    expect(workflow.concurrency).toEqual({
-      group: 'request-review-${{ github.event.pull_request.number }}',
-      'cancel-in-progress': true,
-    })
-    expect(job).toMatchObject({
-      name: 'request-review',
-      'runs-on': 'ubuntu-latest',
-      'timeout-minutes': 5,
-    })
-    expect(job).not.toHaveProperty('if')
-    expect(checkout).toMatchObject({
-      uses: 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1',
-      with: {
-        ref: '${{ github.event.repository.default_branch }}',
-        'persist-credentials': false,
-      },
-    })
-    expect(request).toMatchObject({
-      env: { GITHUB_TOKEN: '${{ github.token }}' },
-      run: 'node .github/review-ownership/request-review.mjs',
-    })
-    expect(JSON.stringify(workflow)).not.toContain('github.event.pull_request.head')
-    expect(JSON.stringify(workflow)).not.toContain('secrets.')
-  })
-})
-
 describe('Weighted approval workflow', () => {
   it('publishes from the trusted default branch after pull request and review updates', () => {
     const publisher = loadWorkflow('.github/workflows/weighted-approval.yml')

+ 0 - 9
scripts/run-gates.spec.ts

@@ -276,15 +276,6 @@ describe('gate graph validation', () => {
     },
   )
 
-  it.each(['ci-primary', 'ci-static', 'check-all'] as const)(
-    'keeps review request policy tests in %s',
-    (mode) => {
-      const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
-
-      expect(ids).toContain('request-review')
-    },
-  )
-
   it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
     'keeps hard-coded Client UI copy enforcement in %s',
     (mode) => {

+ 0 - 2
scripts/run-gates.ts

@@ -268,7 +268,6 @@ export function gatesForMode(selected: Mode): Gate[] {
         pnpmScript('test', 'test'),
         pnpmScript('approval-policy', 'test:approval-policy', { label: 'Weighted approval policy' }),
         pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
-        pnpmScript('request-review', 'test:request-review', { label: 'Review request policy' }),
         pnpmScript('duplication', 'duplication'),
         snapshotGate(),
         expectedOutputGate(),
@@ -313,7 +312,6 @@ function ciSharedStaticGates(): Gate[] {
     pnpmScript('no-bare-dispatcher', 'verify-no-bare-dispatcher', { label: 'proxy-aware dispatchers' }),
     pnpmScript('approval-policy', 'test:approval-policy', { label: 'Weighted approval policy' }),
     pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
-    pnpmScript('request-review', 'test:request-review', { label: 'Review request policy' }),
   ]
 }
 

+ 0 - 1
scripts/translation-pairing.manifest.json

@@ -3,7 +3,6 @@
     ".agents/notes/AGENTS.md",
     ".agents/notes/implemented/AGENTS.md",
     ".agents/notes/implemented/CLAUDE.md",
-    ".agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.md",
     ".github/review-ownership/README.md",
     "docs/AGENTS.md",
     "docs/cordis-api/inherited.md",