ci: harden deterministic PR quality gates

Route required checks by changed surface, add offline provider and chat contracts, and keep fork PRs independent of live credentials. Layer agent guidance by subtree and enforce a compact instruction budget.

Tested: bun run check:policy
Confidence: high
Scope-risk: broad
This commit is contained in:
程序员阿江(Relakkes)
2026-07-10 20:29:01 +08:00
parent a6964f1932
commit 435e4ccc9f
33 changed files with 1228 additions and 675 deletions
+12
View File
@@ -0,0 +1,12 @@
# CI and Repository Policy
These rules apply to `.github/` changes in addition to the root instructions.
- Treat workflow changes as product changes. Run `bun run check:policy`; run `actionlint` when available.
- `scripts/pr/change-policy.ts` is the source of truth for path-to-check routing. Do not duplicate the routing graph in advisory automation.
- Keep `pr-quality-gate` as the stable required status. Selected jobs must succeed and unselected jobs must be explicitly skipped; never convert failures into success.
- Required PR jobs must remain offline and must not receive provider credentials or depend on paid/live services.
- A `pull_request_target` workflow may inspect PR metadata using trusted base code, but must never execute the PR head, install PR-controlled dependencies, or expose secrets to contributor code.
- Keep Bun aligned with `package.json#packageManager` and use frozen lockfile installs in required jobs.
- Do not weaken coverage, test routing, CODEOWNERS, or branch protections to make another change pass. Maintainer override labels require an explicit, documented decision.
- Repository settings, rulesets, secrets, and required-check changes require explicit user authorization; editing workflow files alone does not grant it.
+40
View File
@@ -0,0 +1,40 @@
# Quality policy and required-check topology are maintainer-owned.
/.github/workflows/ @NanmiCoder
/.github/CODEOWNERS @NanmiCoder
/.github/copilot-instructions.md @NanmiCoder
/.github/pull_request_template.md @NanmiCoder
/AGENTS.md @NanmiCoder
**/AGENTS.md @NanmiCoder
/CONTRIBUTING.md @NanmiCoder
/docs/en/guide/contributing.md @NanmiCoder
/docs/guide/contributing.md @NanmiCoder
/scripts/pr/ @NanmiCoder
/scripts/quality-gate/ @NanmiCoder
# Cross-process and provider boundaries carry a wider blast radius.
/desktop/electron/ @NanmiCoder
/desktop/scripts/ @NanmiCoder
/desktop/src/api/providers* @NanmiCoder
/desktop/src/api/websocket* @NanmiCoder
/desktop/src/lib/providerSettingsJson* @NanmiCoder
/desktop/src/pages/ActiveSession* @NanmiCoder
/desktop/src/pages/EmptySession* @NanmiCoder
/desktop/src/stores/chatStore* @NanmiCoder
/desktop/src/stores/providerStore* @NanmiCoder
/desktop/src/stores/sessionRuntimeStore* @NanmiCoder
/desktop/src/lib/persistenceMigrations* @NanmiCoder
/src/server/api/providers* @NanmiCoder
/src/server/config/provider* @NanmiCoder
/src/server/proxy/ @NanmiCoder
/src/server/services/conversationService* @NanmiCoder
/src/server/services/persistentStorageMigrations* @NanmiCoder
/src/server/services/openaiOfficialProvider* @NanmiCoder
/src/server/services/provider* @NanmiCoder
/src/server/types/provider* @NanmiCoder
/src/server/ws/ @NanmiCoder
/src/services/api/client* @NanmiCoder
/src/services/compact/autoCompact* @NanmiCoder
/src/services/openaiAuth/ @NanmiCoder
/src/utils/managedEnv* @NanmiCoder
/src/utils/model/ @NanmiCoder
/src/utils/providerManagedEnvCompat* @NanmiCoder
+11 -6
View File
@@ -1,13 +1,18 @@
# AI Coding Instructions
Follow the repository contract in `AGENTS.md` before editing code.
Follow the root `AGENTS.md` and the nearest nested `AGENTS.md` for the files you edit.
For every feature or bugfix:
For every feature, bugfix, refactor, or workflow change:
- Identify the changed surface before coding: `desktop`, `server`, `adapter`, `native`, `docs`, `provider/runtime`, `agent-loop`, or `release`.
- Treat tool access as capability, not authorization. Do not commit, push, open/merge a PR, release, run live providers, or change repository settings unless explicitly requested.
- Inspect `git status --short`, identify the changed surface, define the intended behavior and failure signal, and inspect the nearest implementation and tests before editing.
- Identify the changed surface before coding: `desktop`, `server/runtime`, `adapter`, `native`, `docs`, `provider/runtime`, `agent-loop`, `persistence`, `policy/ci`, or `release`.
- Add same-area tests with the production change. Do not leave production behavior untested unless the PR explicitly carries the maintainer override `allow-missing-tests`.
- Preserve or improve the coverage ratchet. New or changed executable production lines must pass the changed-line coverage threshold in `scripts/quality-gate/coverage-thresholds.json`; do not edit coverage baselines or thresholds without maintainer approval via `allow-coverage-baseline-change`.
- Use unit tests for pure logic, API/request-shape tests for server/provider/runtime behavior, Testing Library/Vitest for desktop UI and stores, and E2E or agent-browser smoke for user-visible cross-boundary flows.
- For agent loop, tool execution, provider routing, model selection, file editing, permissions, session resume, and desktop chat changes, include mock/fixture tests and provide live smoke or baseline evidence when provider access is available.
- Before marking work complete, run the narrow relevant check and then `bun run verify`; for high-risk Coding Agent paths, also run `bun run quality:providers` plus the appropriate `quality:smoke`, `quality:baseline`, or `quality:release` command.
- In the final handoff or PR description, include changed files, tests added, coverage report path, E2E/live report path or blocker, and known residual risk.
- Provider/auth/runtime-env/model-window/proxy changes require offline `bun run check:provider-contract`; desktop chat/WebSocket/session-runtime changes require `bun run check:chat-contract`.
- Required PR evidence must be deterministic: use fake credentials, temporary config/home paths, mocked or loopback transports, explicit cleanup, and restored environment state. Never call a real provider or use saved machine credentials in required tests.
- For agent loop, tool execution, provider routing, model selection, file editing, permissions, session resume, and desktop chat changes, include mock/fixture/contract tests. Live smoke is trusted-maintainer evidence only and requires explicit authorization; finding local credentials is not authorization.
- Run the focused regression first, then `bun run check:impact` and every selected surface/contract check. Run `bun run verify` only before claiming PR-ready/push-ready or when full validation was requested.
- Do not present skipped, blocked, not-run, mock, build-only, or stale evidence as passed live/runtime verification.
- In the final handoff or PR description, include changed files, tests added, commands actually run with pass/fail counts, checks not run, coverage report path when generated, deterministic E2E evidence, live report path or explicit maintainer-only deferral, and known residual risk.
+4 -6
View File
@@ -17,17 +17,15 @@
- [ ] I ran the relevant local checks, or explained why they do not apply.
- [ ] I added or updated same-area tests for every production behavior change.
- [ ] I ran `bun run verify` for code changes, including the coverage gate.
- [ ] I ran the checks selected by `bun run check:impact`; if I claim PR-ready/full validation, I also ran `bun run verify`.
- [ ] New or changed executable production lines meet the changed-line coverage threshold, or the blocker/maintainer override is documented.
- [ ] I attached or summarized the quality report path, JUnit/log artifact path, and pass/fail/skip counts.
- [ ] I ran E2E/live smoke for cross-boundary, provider/runtime, desktop chat, agent-loop, native, or release changes, or documented the blocker.
- [ ] I ran deterministic E2E/contract checks for cross-boundary changes. Live-model evidence is attached when a trusted maintainer ran it, otherwise it is explicitly marked not run.
## Risk
- [ ] This PR does not touch CLI core paths, or it has maintainer approval for `allow-cli-core-change`.
- [ ] Production code changes include matching tests, or have maintainer approval for `allow-missing-tests`.
- [ ] Coverage baseline/threshold changes have maintainer approval for `allow-coverage-baseline-change`.
- [ ] Quarantined tests still have owners, exit criteria, and unexpired review windows.
- [ ] Provider/runtime changes were covered by mock contract tests, and live smoke was run or explicitly deferred.
@dosubot review this PR for changed-area risk, missing tests, docs impact, desktop startup risk, and CLI core impact.
- [ ] Any quarantine-policy change has maintainer approval; deterministic provider/chat contract tests were not quarantined.
- [ ] Provider/runtime changes are covered by offline mock/fixture contract tests; live smoke was run by a trusted maintainer or explicitly deferred.
+164 -106
View File
@@ -13,17 +13,22 @@ concurrency:
cancel-in-progress: true
jobs:
change-policy:
name: change-policy
scope-plan:
name: scope-plan
runs-on: ubuntu-latest
outputs:
areas: ${{ steps.policy.outputs.areas }}
area_labels: ${{ steps.policy.outputs.area_labels }}
blocked: ${{ steps.policy.outputs.blocked }}
blocking_reasons: ${{ steps.policy.outputs.blocking_reasons }}
desktop_checks: ${{ steps.policy.outputs.desktop_checks }}
server_checks: ${{ steps.policy.outputs.server_checks }}
adapter_checks: ${{ steps.policy.outputs.adapter_checks }}
desktop_native_checks: ${{ steps.policy.outputs.desktop_native_checks }}
provider_contract_checks: ${{ steps.policy.outputs.provider_contract_checks }}
chat_contract_checks: ${{ steps.policy.outputs.chat_contract_checks }}
persistence_checks: ${{ steps.policy.outputs.persistence_checks }}
policy_checks: ${{ steps.policy.outputs.policy_checks }}
docs_checks: ${{ steps.policy.outputs.docs_checks }}
coverage_checks: ${{ steps.policy.outputs.coverage_checks }}
steps:
@@ -35,10 +40,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Run policy tests
run: bun run check:policy
bun-version: 1.3.12
- name: Collect changed files
env:
@@ -49,86 +51,134 @@ jobs:
gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename' > changed-files.txt
jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH" > labels.txt
- name: Evaluate change policy
- name: Build deterministic scope plan
id: policy
run: bun run scripts/pr/change-policy.ts --files changed-files.txt --labels-file labels.txt
run: bun run scripts/pr/change-policy.ts --files changed-files.txt --labels-file labels.txt --plan-only
policy-enforcement:
name: policy-enforcement
needs: scope-plan
if: ${{ always() && needs.scope-plan.result == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout policy sources
if: needs.scope-plan.outputs.policy_checks == 'true'
uses: actions/checkout@v4
- name: Setup Bun
if: needs.scope-plan.outputs.policy_checks == 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
if: needs.scope-plan.outputs.policy_checks == 'true'
run: bun install --frozen-lockfile
- name: Run policy regression tests
if: needs.scope-plan.outputs.policy_checks == 'true'
run: bun run check:policy
- name: Enforce change policy
env:
BLOCKED: ${{ needs.scope-plan.outputs.blocked }}
BLOCKING_REASONS: ${{ needs.scope-plan.outputs.blocking_reasons }}
run: |
if [ "$BLOCKED" = "true" ]; then
echo "::error::${BLOCKING_REASONS:-Change policy blocked this pull request}"
exit 1
fi
echo "Change policy passed"
desktop-checks:
name: desktop-checks
needs: change-policy
if: needs.change-policy.outputs.desktop_checks == 'true'
needs: scope-plan
if: needs.scope-plan.outputs.desktop_checks == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: 1.3.12
- name: Install root dependencies
run: bun install
run: bun install --frozen-lockfile
- name: Install desktop dependencies
working-directory: desktop
run: bun install
run: bun install --frozen-lockfile
- name: Run desktop checks
run: bun run check:desktop
server-checks:
name: server-checks
needs: change-policy
if: needs.change-policy.outputs.server_checks == 'true'
needs: scope-plan
if: needs.scope-plan.outputs.server_checks == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: 1.3.12
- name: Install root dependencies
run: bun install
- name: Run server checks
run: bun install --frozen-lockfile
- name: Run root runtime checks
run: bun run check:server
provider-contract-checks:
name: provider-contract-checks
needs: scope-plan
if: needs.scope-plan.outputs.provider_contract_checks == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Run offline provider contracts
run: bun run check:provider-contract
chat-contract-checks:
name: chat-contract-checks
needs: scope-plan
if: needs.scope-plan.outputs.chat_contract_checks == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Install desktop dependencies
working-directory: desktop
run: bun install --frozen-lockfile
- name: Run desktop-server chat contracts
run: bun run check:chat-contract
adapter-checks:
name: adapter-checks
needs: change-policy
if: needs.change-policy.outputs.adapter_checks == 'true'
needs: scope-plan
if: needs.scope-plan.outputs.adapter_checks == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: 1.3.12
- name: Install adapter dependencies
working-directory: adapters
run: bun install
run: bun install --frozen-lockfile
- name: Run adapter checks
run: bun run check:adapters
desktop-native-checks:
name: desktop-native-checks
needs: change-policy
if: needs.change-policy.outputs.desktop_native_checks == 'true'
needs: scope-plan
if: needs.scope-plan.outputs.desktop_native_checks == 'true'
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
- name: Install Linux dependencies
run: |
sudo apt-get update
@@ -136,72 +186,73 @@ jobs:
build-essential curl wget file \
libxdo-dev libssl-dev patchelf \
libfuse2
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: 1.3.12
- name: Install root dependencies
run: bun install
run: bun install --frozen-lockfile
- name: Install desktop dependencies
working-directory: desktop
run: bun install
run: bun install --frozen-lockfile
- name: Run desktop native checks
run: bun run check:native
docs-checks:
name: docs-checks
needs: change-policy
if: needs.change-policy.outputs.docs_checks == 'true'
persistence-checks:
name: persistence-checks
needs: scope-plan
if: needs.scope-plan.outputs.persistence_checks == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Install desktop dependencies
working-directory: desktop
run: bun install --frozen-lockfile
- name: Run persistence upgrade contracts
run: bun run check:persistence-upgrade
- name: Setup Node
uses: actions/setup-node@v4
docs-checks:
name: docs-checks
needs: scope-plan
if: needs.scope-plan.outputs.docs_checks == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Run docs checks
run: npm ci && npm run docs:build
coverage-checks:
name: coverage-checks
needs: change-policy
if: needs.change-policy.outputs.coverage_checks == 'true'
needs: scope-plan
if: needs.scope-plan.outputs.coverage_checks == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: 1.3.12
- name: Install root dependencies
run: bun install
run: bun install --frozen-lockfile
- name: Install desktop dependencies
working-directory: desktop
run: bun install
run: bun install --frozen-lockfile
- name: Install adapter dependencies
working-directory: adapters
run: bun install
run: bun install --frozen-lockfile
- name: Run coverage checks
env:
COVERAGE_BASE_REF: origin/${{ github.base_ref }}
run: bun run check:coverage
- name: Summarize coverage report
if: always()
run: |
@@ -209,7 +260,6 @@ jobs:
if [ -n "$latest_report" ]; then
cat "$latest_report" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
@@ -221,17 +271,21 @@ jobs:
pr-quality-gate:
name: pr-quality-gate
needs:
- change-policy
- scope-plan
- policy-enforcement
- desktop-checks
- server-checks
- provider-contract-checks
- chat-contract-checks
- adapter-checks
- desktop-native-checks
- persistence-checks
- docs-checks
- coverage-checks
if: always()
runs-on: ubuntu-latest
steps:
- name: Require all selected quality jobs to pass
- name: Require every selected quality job to pass
run: |
failures=0
@@ -246,27 +300,31 @@ jobs:
fi
}
allow_skip_or_success() {
require_selected() {
local name="$1"
local result="$2"
case "$result" in
success|skipped)
echo "$name: $result"
;;
*)
echo "::error::$name ended with result: $result"
failures=$((failures + 1))
;;
esac
local expected="$2"
local result="$3"
if [ "$expected" = "true" ]; then
require_success "$name" "$result"
elif [ "$result" != "skipped" ]; then
echo "::error::$name was not selected but ended with result: $result"
failures=$((failures + 1))
else
echo "$name: skipped by scope plan"
fi
}
require_success "change-policy" "${{ needs.change-policy.result }}"
allow_skip_or_success "desktop-checks" "${{ needs.desktop-checks.result }}"
allow_skip_or_success "server-checks" "${{ needs.server-checks.result }}"
allow_skip_or_success "adapter-checks" "${{ needs.adapter-checks.result }}"
allow_skip_or_success "desktop-native-checks" "${{ needs.desktop-native-checks.result }}"
allow_skip_or_success "docs-checks" "${{ needs.docs-checks.result }}"
allow_skip_or_success "coverage-checks" "${{ needs.coverage-checks.result }}"
require_success "scope-plan" "${{ needs.scope-plan.result }}"
require_success "policy-enforcement" "${{ needs.policy-enforcement.result }}"
require_selected "desktop-checks" "${{ needs.scope-plan.outputs.desktop_checks }}" "${{ needs.desktop-checks.result }}"
require_selected "server-checks" "${{ needs.scope-plan.outputs.server_checks }}" "${{ needs.server-checks.result }}"
require_selected "provider-contract-checks" "${{ needs.scope-plan.outputs.provider_contract_checks }}" "${{ needs.provider-contract-checks.result }}"
require_selected "chat-contract-checks" "${{ needs.scope-plan.outputs.chat_contract_checks }}" "${{ needs.chat-contract-checks.result }}"
require_selected "adapter-checks" "${{ needs.scope-plan.outputs.adapter_checks }}" "${{ needs.adapter-checks.result }}"
require_selected "desktop-native-checks" "${{ needs.scope-plan.outputs.desktop_native_checks }}" "${{ needs.desktop-native-checks.result }}"
require_selected "persistence-checks" "${{ needs.scope-plan.outputs.persistence_checks }}" "${{ needs.persistence-checks.result }}"
require_selected "docs-checks" "${{ needs.scope-plan.outputs.docs_checks }}" "${{ needs.docs-checks.result }}"
require_selected "coverage-checks" "${{ needs.scope-plan.outputs.coverage_checks }}" "${{ needs.coverage-checks.result }}"
if [ "$failures" -gt 0 ]; then
exit 1
+19 -34
View File
@@ -97,6 +97,7 @@ jobs:
)
const changedProduct = (predicate) => filenames.filter((file) =>
predicate(file) &&
/\.[cm]?[jt]sx?$/.test(file) &&
!/\.test\.[cm]?[jt]sx?$/.test(file) &&
!file.includes('/__tests__/') &&
!file.includes('/fixtures/')
@@ -105,11 +106,15 @@ jobs:
const serverProduct = changedProduct((file) => file.startsWith('src/server/'))
const adapterProduct = changedProduct((file) => file.startsWith('adapters/'))
const agentRuntimeProduct = changedProduct((file) =>
file.startsWith('src/server/ws/') ||
file.startsWith('src/server/services/conversation') ||
file.startsWith('src/tools/') ||
file.startsWith('src/utils/')
)
const rootRuntimeProduct = changedProduct((file) =>
file.startsWith('src/') &&
!file.startsWith('src/server/') &&
!file.startsWith('src/tools/') &&
!file.startsWith('src/utils/')
)
const missingTestSignals = []
if (desktopProduct.length > 0 && !hasTest('desktop/src/')) {
missingTestSignals.push('Desktop product files changed without a desktop test file in the PR.')
@@ -126,6 +131,15 @@ jobs:
)) {
missingTestSignals.push('Agent/runtime product files changed without a tools/utils test file in the PR.')
}
if (rootRuntimeProduct.length > 0 && !filenames.some((file) =>
file.startsWith('src/') &&
!file.startsWith('src/server/') &&
!file.startsWith('src/tools/') &&
!file.startsWith('src/utils/') &&
(/\.test\.[cm]?[jt]sx?$/.test(file) || file.includes('/__tests__/'))
)) {
missingTestSignals.push('Root runtime product files changed without a matching root runtime test file in the PR.')
}
if (missingTestSignals.length > 0 && !currentLabels.has('allow-missing-tests')) {
areas.add('needs-maintainer-approval')
}
@@ -137,31 +151,6 @@ jobs:
areas.add('needs-maintainer-approval')
}
const requiredChecks = ['change-policy']
if (areas.has('area:desktop') || areas.has('area:server')) requiredChecks.push('desktop-checks')
if (areas.has('area:server') || filenames.some((file) => file.startsWith('src/tools/') || file.startsWith('src/utils/'))) requiredChecks.push('server-checks')
if (areas.has('area:adapters')) requiredChecks.push('adapter-checks')
if (
filenames.some((file) =>
file.startsWith('desktop/') ||
file.startsWith('adapters/') ||
file.startsWith('src/server/') ||
['bun.lock', 'package.json', 'desktop/bun.lock', 'desktop/package.json', 'desktop/package-lock.json', 'desktop/electron/tsconfig.json', 'desktop/scripts/build-linux.sh', 'desktop/scripts/build-macos-arm64.sh', 'desktop/scripts/build-windows-x64.ps1'].includes(file)
)
) requiredChecks.push('desktop-native-checks')
if (areas.has('area:docs')) requiredChecks.push('docs-checks')
if (
filenames.some((file) =>
file.startsWith('desktop/src/') ||
file.startsWith('src/server/') ||
file.startsWith('src/tools/') ||
file.startsWith('src/utils/') ||
file.startsWith('adapters/') ||
file.startsWith('scripts/quality-gate/') ||
['package.json', 'desktop/package.json', 'desktop/bun.lock'].includes(file)
)
) requiredChecks.push('coverage-checks')
const testSignals = []
testSignals.push(...missingTestSignals.map((signal) => `BLOCKING unless \`allow-missing-tests\` is applied: ${signal}`))
if (agentRuntimeProduct.length > 0) {
@@ -266,8 +255,8 @@ jobs:
'**Coverage policy files:**',
coveragePolicyFilesText,
'',
'**Expected checks:**',
requiredChecks.map((check) => `- \`${check}\``).join('\n'),
'**Required check plan:**',
'- See `PR Quality / scope-plan`; it is the single source of truth for selected jobs.',
'',
'**Test coverage signals:**',
testSignals.map((signal) => `- ${signal}`).join('\n'),
@@ -275,11 +264,7 @@ jobs:
'**Risk notes:**',
riskNotes.map((note) => `- ${note}`).join('\n'),
'',
'Hard merge gates still come from GitHub Actions, not AI review.',
'',
'**Dosu handoff:** Dosu can be used as the AI reviewer for risk explanation, missing-test prompts, and maintainer Q&A. If it does not comment automatically from the PR template, ask:',
'',
'@dosubot review this PR for changed-area risk, missing tests, docs impact, desktop startup risk, and CLI core impact.',
'Hard merge gates come from the deterministic GitHub Actions contract lanes above.',
].join('\n')
const comments = await github.paginate(github.rest.issues.listComments, {