Files
orca/config/scripts/pr-code-change-scope.mjs
T
Jinwoo Hong ac8ea9f958 fix(codex): write Orca's hook into ~/.codex only when something changed (#25743)
* fix(codex): write Orca's hook in ~/.codex only when something changed

One reconcile replaces the per-launch writer and its background approval
session. It runs at app start after PATH hydration, when the setting turns on,
once per native pane spawn, and (with a bounded 3 s wait) on Codex launches and
resumes. Orca's entry lives alone in a matcherless group, appended last unless
already in place; other copies are removed and the shifted user approvals move
verbatim under both key spellings. The approval, with Codex's own hash, goes in
first. The routing gate closes only for a hooks.json with a bad shape.

* fix(codex): report hook status for the home the next pane uses

Status reads ~/.codex (both key spellings) when launches use it, or the CLI
has no selection, and the selected managed home otherwise. It explains: update
Codex, Codex not found, not asked yet, approved by Orca but not yet confirmed
by Codex, a hooks.json shape that moves panes to Orca's own home, and inline
config.toml approvals Orca cannot add to.

* test(codex): pin the ~/.codex approval against a real Codex, and run it on those files

The contract now also writes Orca's entry into a throwaway ~/.codex and checks
that Codex lists it trusted and enabled, turns it back on over a /hooks
switch-off, lists it for review after a user inserts a hook ahead until the next
check, keeps an inline config.toml loadable, and shows no review in a real TUI
start. The real-binary CI job now runs when the ~/.codex writer changes.

* test(ci): keep the scope test under its line cap; assert the ~/.codex paths beside the contract job

* test(codex): type the hoisted test holders instead of asserting

* test(codex): cover the matcher rule, an older build's event, and the spawn trigger

Adds tests that failed against mutants which survived the first pass: an
entry alone in a matcher group, a current copy's approval in an event left to
an older build, one run per spawn, a spawn riding a running reconcile, and the
native-only spawn trigger. The opt-out's Codex-hash cleanup now runs once,
after the sweep, instead of twice.

* docs(codex): name the ~/.codex reconcile in the legacy sweep's lane comment

* fix(codex): approve ~/.codex with Orca's own hash while Codex's answer is pending

The reconcile waits at most 0.5 s for Codex's hash. If the lookup is still
running, an entry already in place keeps its approval and nothing is written;
a missing entry or approval gets Orca's computed hash, approval first, inside a
launch's 3 s wait, as main's did. When the lookup lands, the reconcile runs
again and Codex's hash replaces it.

* refactor(codex): one start for the hook lookup and the ~/.codex reconcile

startCodexHooks replaces the two start functions and takes the PATH wait that
the reconcile request's after option carried. A spawn reconciles unless one is
running, without a scheduled flag, launches call one reconcileCodexHooksForLaunch
on the shared withTimeout, and the reconcile alone reads the hooks setting. The
startup test now runs the ready phase instead of matching its source text.

* refactor(codex): plan ~/.codex with the shared Orca-hook pruner and approval reader

The planner prunes with removeManagedCommands, as the opt-out does (so a hook
that runs Orca's script through its args goes too), and returns a prune or
settle union. While Codex has not answered, the kept approval comes from the
shared per-event reader. The reconcile result is its outcome alone, a
concurrent save spends a pass of the one bound, the opt-out removes Orca's
approvals once, the approval-first writer takes the hooks path, and
getRealHomeConfigTomlPath gives way to getSystemCodexConfigTomlPath.

* fix(codex): ~/.codex keeps only an approval holding a hash Orca's entry may carry

* refactor(codex): name the ~/.codex hooks-file check for what it reports

* test(codex): the ~/.codex entry tests know Codex's earlier hashes, as the app's lookup does

* refactor(codex): the ~/.codex reconcile uses the lookup's answer names

* test(codex): ~/.codex status tests keep a Codex on PATH unless one is missing

* refactor(codex): one stopgap for both homes; the ~/.codex pass finds its own home, hash and user data

Also passes every approval to the approval-first writer, which already skips the ones in place.

* refactor(codex): the reconcile start owns the warm-up; no rerun-on-answer flag

The lookup start only records the hydrated PATH; one catch, one request type, and the app config is kept as given.

* refactor(codex): the ~/.codex pass returns nothing; tests read the files

Also makes the ~/.codex opt-out take Codex's hashes, as every production caller passes them.

* refactor(codex): the ~/.codex approval cleanup takes Codex's hashes

* test(startup): check the Codex hook start waits for PATH by behavior, not identity

* fix(codex): a status-hook problem never moves the system default off ~/.codex

* chore(ci): run the real-Codex contract when the ~/.codex reconcile changes

* docs(codex): the ~/.codex reconcile's refused branch covers every definitive refusal

* test(codex): start the failure-memo lookup with the reconcile's PATH-only start

* fix(codex): ~/.codex gets nothing while Codex is missing, a pending answer uses the saved one, a conversion waits for a run that writes, and status and the reconcile read one home and one Orca-hash rule

* test(codex): ~/.codex and managed-home status tests name their home; ~/.codex rewrites the backslash key Codex on Windows reads

* test(codex): the Windows upgrade test reads status for the managed home it installs

* test(codex): the real-TUI contract trusts its workdir by its real path, and its userData exists

* fix(codex): keep Orca's approval at every slot its entry holds in ~/.codex
2026-10-06 16:12:23 -04:00

606 lines
23 KiB
JavaScript

import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import process from 'node:process'
import { pathToFileURL } from 'node:url'
const DOCS_ONLY_FILES = new Set([
'README.md',
'LICENSE',
'AGENTS.md',
'CLAUDE.md',
'Agents.md',
'Claude.md',
'.github/CONTRIBUTING.md',
'.github/pull_request_template.md',
'.github/CODEOWNERS'
])
const DOCS_ONLY_PREFIXES = ['docs/', '.github/ISSUE_TEMPLATE/']
export const PR_CHECK_JOBS = [
'static_analysis',
'typecheck',
'git_compatibility',
'codex_index_heal_contract',
'xterm_patch_sync',
'shell_contracts',
'test',
'orcad_browser',
'mobile_web_app',
'cross-version-wire',
'managed_hook_node18',
'package',
'package_windows'
]
const ALWAYS_ON_CODE_JOBS = new Set(['static_analysis', 'typecheck', 'test'])
const GLOBAL_FORCE_PREFIXES = [
'.github/workflows/pr.yml',
'.github/actions/install-node-dependencies/',
'.github/actions/restore-pnpm-verification/',
'.github/actions/prepare-native-runtime/',
'config/scripts/pr-code-change-scope'
]
const GLOBAL_FORCE_FILES = new Set(['package.json', 'pnpm-lock.yaml'])
const GIT_COMPAT_PREFIXES = [
'.github/actions/prepare-git-compatibility/',
'src/shared/git-',
'src/shared/review-head-tracking-ref',
'src/shared/worktree/local-base-branch-fast-forward',
'src/main/git/',
'src/relay/git-',
'config/scripts/git-binary-compatibility'
]
// Why narrow: the contract pins Codex's read-repair, so it runs when the heal that
// depends on it, its app-server transport, or the contract itself changes. The same
// job pins --no-daemon for Orca's codex shell wrapper, the project-trust key, and the
// approval Orca writes for its hook entry in managed Codex homes and ~/.codex.
const CODEX_INDEX_HEAL_CONTRACT_PREFIXES = [
'src/main/codex/codex-hook-file-entry-binary-contract',
'src/main/codex/codex-hook-trust-',
'src/main/codex/codex-hook-approval-first-write',
'src/main/codex/codex-hook-hash-lookup',
'src/main/codex/codex-hook-orca-approvals',
'src/main/codex/codex-hook-reconcile',
'src/main/codex/codex-hook-local-install',
'src/main/codex/codex-hook-user-mirroring',
'src/main/codex/codex-real-home-hook-',
'src/main/codex/codex-real-home-hooks-json',
'src/main/codex/codex-user-hook-trust-moves',
'src/main/codex/codex-hook-definition',
'src/main/codex/codex-hook-command-form',
'src/main/codex/codex-hook-identity',
'src/main/codex/codex-app-server-client',
'src/main/codex/codex-trust-identity',
'src/main/codex/config-toml-hook-trust-',
'src/main/codex/config-toml-key-path',
'src/main/agent-hooks/posix-hook-command',
'src/main/agent-hooks/hook-post-command',
'src/main/codex-cli/codex-read-only-app-server-args',
'src/main/agent-trust-presets',
'src/main/codex/config-toml-trust',
'src/main/pty/codex-no-daemon-binary-contract',
'src/main/pty/codex-shell-launch-preflight',
'src/shared/codex-shell-function',
'src/main/codex/codex-index-heal-binary-contract',
'src/main/codex/codex-session-index-heal',
'src/main/codex/codex-app-server-session',
'src/main/codex/codex-state-db',
'src/main/sqlite/sync-database',
'src/main/codex/codex-app-server-capability-signal',
'src/main/provider-process/provider-process-exit-deadline',
'src/main/provider-process/provider-process-launch',
'src/main/provider-process/provider-record-reader',
'src/main/codex/codex-session-backfill',
'src/main/codex/codex-session-index-heal-state',
'src/main/codex-cli/command',
'src/main/win32-utils',
'src/shared/node-cli-command-resolution',
'src/shared/windows-batch-spawn'
]
const XTERM_PREFIXES = [
'config/patches/xterm-upstream.json',
'config/patches/@xterm',
'config/patches/xterm-src/',
'config/scripts/regenerate-xterm-patches'
]
const SHELL_PREFIXES = [
'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec',
'src/main/daemon/shell-ready',
'src/main/daemon/daemon-bash-shell-ready',
'src/main/daemon/daemon-shell-ready-wrapper',
'src/main/daemon/node-pty-fd-leak',
'src/main/providers/local-pty-shell-ready',
'src/main/providers/__tests__/shell-ready-framework-example',
'src/main/pty/',
'src/main/shell-templates',
'src/main/shell-startup-',
'src/main/shell-wrapper-',
'src/main/terminal-history-fish',
'src/main/zsh-',
'src/main/runtime/structured-session-cli-login-shell',
'src/main/runtime/structured-session-login-shell-test-harness',
'src/main/runtime/structured-session-child-identity-env',
'src/renderer/src/components/terminal-pane/fish-color-scheme',
'src/shared/fish-',
'src/shared/pty-reply-echo-shapes',
'src/shared/startup-shell-portability',
'src/shared/posix-command-path-lookup',
'config/patches/node-pty@',
'config/scripts/ensure-native-runtime',
'config/scripts/node-pty-job-ownership'
]
const ORCAD_BROWSER_PREFIXES = [
'src/main/orcad/external-chromium-',
'src/main/orcad/orcad-browser-provider',
'src/main/orcad/orcad-agent-browser-binary',
'src/main/orcad/electron-serve-browser-process'
]
// The page bundle the desktop packages: the builder and verifier, the manifest writer and the
// packaging guard they share, the entry, the route tree it mounts, the mobile source those routes
// import, and the shell policy the render check runs the page under.
const MOBILE_WEB_APP_PREFIXES = [
'config/scripts/build-mobile-web-app',
'config/scripts/run-mobile-web-app-checks',
'config/scripts/script-child-process.mjs',
'src/shared/child-process/',
'config/scripts/verify-mobile-web-app-bundle',
'config/scripts/mobile-web-app-',
'config/scripts/mobile-web-bundle-',
'config/scripts/verify-packaged-mobile-web-bundle',
'config/scripts/mobile-web-source-line-endings',
'config/scripts/script-entry-detection',
'mobile/web-entry/',
'mobile/app/',
'mobile/src/',
'mobile/packages/',
'mobile/package.json',
'mobile/pnpm-lock.yaml',
'mobile/modules/orca-mobile-web-shell/'
]
function changesMobileWebApp(changedFiles) {
return changedFiles.some((file) => matchesPrefix(file, MOBILE_WEB_APP_PREFIXES))
}
const CROSS_VERSION_WIRE_PREFIXES = [
'tests/e2e/cross-version-wire/',
'config/scripts/stable-release-tags',
// The R1 daemon protocol crossing gate runs in this job.
'config/scripts/daemon-protocol-facts',
'config/scripts/check-daemon-protocol-crossing',
// R3 runtime launcher protocol ratchet; a bump always routes here via the protocol file.
'config/scripts/check-runtime-launcher-protocol-ratchet',
'src/main/daemon/daemon-protocol-version.ts',
'src/shared/protocol-version',
'src/shared/terminal-stream-protocol',
'src/shared/browser-client-host-protocol',
'src/shared/browser-network-tunnel-protocol',
'src/shared/browser-client-host-placement',
'src/shared/agent-launch-intent',
'src/shared/rpc-contract/agent-launch-params',
'src/shared/agent-session-wire',
'src/shared/agent-session-mutation-envelope',
// The send a client builds (the agent-session suite sends it to the release host) and the
// fingerprint the host's ledger and journal re-derive.
'src/shared/structured-agent-session-mutation.ts',
'src/shared/structured-agent-session-send-mutation.ts',
'src/shared/structured-agent-session-outbox.ts',
'src/shared/agent-session-record',
'src/shared/agent-session-provider-handle',
'src/shared/agent-session-journal-',
'src/main/ai-vault/structured-session-ownership.ts',
'src/main/native-chat/agent-session-journal/',
'src/main/native-chat/agent-session-wire/',
'src/main/runtime/agent-session-record-store',
'src/main/runtime/agent-session-recovery-capsule',
'src/shared/agent-session-resume-marker',
'src/main/runtime/rpc/dispatcher',
// Run on every request the suites dispatch, whatever its method.
'src/main/runtime/rpc/core.ts',
'src/main/runtime/rpc/errors.ts',
'src/main/runtime/rpc/rpc-streaming-dispatcher.ts',
'src/main/runtime/rpc/orchestration-contract-fence.ts',
'src/main/runtime/rpc/orchestration-session-caller.ts',
'src/main/runtime/rpc/orchestration-legacy-compatibility.ts',
'src/main/runtime/rpc/orchestration-mutation-executor.ts',
'src/shared/orchestration-rpc-contract.ts',
'src/main/runtime/rpc/methods/agent-launch',
'src/main/runtime/rpc/methods/ai-vault.ts',
'src/main/runtime/rpc/methods/browser-tab-create-schema',
'src/main/runtime/rpc/methods/session-tabs.ts',
'src/main/runtime/rpc/methods/structured-agent-session',
'src/main/runtime/rpc/methods/terminal',
'src/main/runtime/runtime-worktree-agent-',
'src/main/runtime/runtime-worktree-pty-agent-sources',
'src/shared/runtime-worktree-contracts',
'src/renderer/src/runtime/remote-runtime-terminal-multiplexer',
// Turn-end status a newer host publishes and an older desktop reads (cross-version-host-observed-turn-end).
'src/shared/agent-turn-outcome',
'src/shared/agent-status-types',
'src/shared/agent-lead-status-fold',
'src/shared/agent-session-turn-record',
'src/shared/structured-agent-session-agent-status',
'src/shared/structured-agent-session-projection',
'src/shared/workspace-session-sleeping-agents',
// A current desktop's launch route against a released server's capabilities (cross-version-paired-structured-launch).
'src/shared/structured-native-chat-launch-route.ts',
'src/renderer/src/lib/agent-launch-routing.ts',
'src/renderer/src/runtime/paired-host-client-capabilities.ts',
'src/shared/electron-remote-runtime-client-capabilities.ts',
'src/shared/remote-runtime-client-capabilities.ts',
// An older app opening a newer orchestration database (orchestration-delivery-downgrade).
'src/main/runtime/orchestration/db.ts',
'src/main/runtime/orchestration/db/',
'src/main/runtime/orchestration/orchestration-schema-version-skew'
]
const MANAGED_HOOK_PREFIXES = [
'config/scripts/smoke-managed-hook-runtime-node18',
'config/scripts/build-relay',
'src/relay/',
'src/shared/agent-hook',
'src/main/agent-hooks/'
]
const NATIVE_RUNTIME_PREFIXES = [
'config/scripts/ensure-native-runtime',
'config/scripts/rebuild-native-deps',
'config/scripts/node-pty-job-ownership',
'config/scripts/windows-pe-machine',
'config/scripts/windows-pe-image-fixture',
'config/scripts/script-module-dependencies',
'config/scripts/windows-process-tree-creation-time',
'config/scripts/windows-process-tree-gyp-rebuild',
'config/scripts/electron-builder-native-rebuild',
'config/patches/node-pty@',
'config/patches/@vscode__windows-process-tree'
]
const NATIVE_CACHE_FILES = new Set([
'package.json',
'pnpm-lock.yaml',
'.github/actions/install-node-dependencies/action.yml',
'.github/actions/prepare-native-runtime/action.yml',
'pnpm-workspace.yaml',
'.npmrc',
'.pnpmfile.cjs',
'config/scripts/ensure-native-runtime.mjs',
'config/scripts/rebuild-native-deps.mjs',
'config/scripts/node-pty-job-ownership.cjs',
'config/scripts/windows-pe-machine.cjs',
'config/scripts/windows-process-tree-gyp-rebuild.mjs',
'config/scripts/windows-process-tree-creation-time.cjs',
'config/scripts/install-electron-package-binary.mjs',
'config/scripts/electron-platform-path.mjs',
'config/scripts/zip-extractor-command.mjs',
'src/shared/zip-extractor-command.ts',
'config/scripts/shared-electron-dist-cache.mjs',
'config/scripts/space-sharing-copy.mjs',
'native/windows-registry/src/addon.cc',
'native/windows-registry/binding.gyp',
'native/windows-registry/package.json',
'native/windows-registry/index.js'
])
const NATIVE_CACHE_PREFIXES = [
'config/patches/node-pty@',
'config/patches/@vscode__windows-process-tree'
]
const SHARED_PACKAGE_PREFIXES = [
'electron.vite.config.ts',
'config/electron-builder',
'config/packaged-runtime',
'config/build-plugins/',
'config/scripts/build-',
'config/scripts/smoke-packaged',
'config/scripts/install-electron-package-binary',
'config/scripts/verify-packaged',
'config/scripts/verify-skills-cli-runtime',
'config/scripts/verify-linux-glibc',
'config/scripts/run-electron-vite',
'skills/',
'skill-guides/',
'resources/build/',
'resources/onboarding/',
'resources/plugins/',
'resources/skills/',
...NATIVE_RUNTIME_PREFIXES
]
const LINUX_PACKAGE_PREFIXES = [
...SHARED_PACKAGE_PREFIXES,
'config/scripts/package-linux-formats',
'config/scripts/script-child-process.mjs',
'config/scripts/space-sharing-copy.mjs',
'.github/actions/prepare-linux-package-fixture/',
'config/docker/cli-launch-contract/',
'config/docker/headless-pairing/',
'config/docker/headless-serve-shutdown/',
'config/docker/daemon-shutdown-descendants/',
'config/scripts/run-linux-cli-launch-contract',
'config/scripts/run-headless-linux-pairing-docker',
'config/scripts/run-daemon-shutdown-descendants-docker',
'config/scripts/static-appimage-package-contract',
'native/computer-use-linux/',
'resources/linux/',
'config/scripts/run-headless-serve'
]
const WINDOWS_PACKAGE_PREFIXES = [
...SHARED_PACKAGE_PREFIXES,
'native/windows-cli-launcher/',
'native/computer-use-windows/',
'resources/win32/',
'config/scripts/build-windows-cli-launcher',
'config/scripts/windows-pty-native-capability',
'tests/tools/windows-pty-native-capability-smoke/'
]
const LINUX_PACKAGE_TESTS = [
'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts',
'src/main/browser/browser-route-tcp-egress.electron.test.ts',
'src/main/browser/browser-route-webrtc-egress.electron.test.ts',
'src/main/browser/browser-route-h3-egress.electron.test.ts',
'src/main/browser/browser-route-dns-prefetch.electron.test.ts'
]
const WINDOWS_PACKAGE_TESTS = [
...LINUX_PACKAGE_TESTS,
'config/scripts/rebuild-native-deps.test.mjs',
'config/scripts/rebuild-native-deps-windows-process-tree.test.mjs',
'config/scripts/rebuild-native-deps-node-pty.test.mjs',
'config/scripts/nsis-process-check.test.mjs',
'config/scripts/ensure-native-runtime-job-ownership.test.mjs',
'config/scripts/verify-packaged-node-pty-job-ownership.test.mjs',
'config/scripts/windows-pe-machine.test.mjs',
'config/scripts/script-module-dependencies.test.mjs',
'src/main/windows-registry-addon.test.ts',
'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts',
'src/shared/child-process/windows-command-line.win32.test.ts',
'src/shared/child-process/windows-cmd-shim-resolution.test.ts',
'src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts',
'src/main/agent-hooks/windows-hook-payload-delivery.test.ts',
'src/main/jcode/hook-gate-script.test.ts',
'src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts',
'src/main/codex/windows-hook-command.test.ts',
'src/main/codex/windows-hook-upgrade.test.ts',
'src/main/codex/hook-service-managed-install.test.ts',
'src/main/windows/windows-pty-job.win32.test.ts',
'src/main/windows/windows-msys-job.win32.test.ts',
'src/main/providers/agent-foreground-process-git-bash.win32.test.ts',
'src/main/windows/windows-host-job.win32.test.ts',
'src/main/windows/windows-process-tree-command-line-patch.test.ts',
'src/main/windows/windows-process-table-native-addon.win32.test.ts',
'src/main/persistence/profile-state/profile-state-access-windows-native.win32.test.ts',
'src/main/windows-live-tree-kill.win32.test.ts',
'src/main/wsl/wsl-runner.test.ts',
'src/main/wsl/wsl-guest-environment.test.ts',
'src/main/wsl/wsl-executable-path.win32.test.ts',
'src/main/wsl/wsl-w1-w3-contract.test.ts',
'src/shared/source-scan/source-tree-scan.test.ts',
'src/main/cli/wsl-cli-powershell-boundary.test.ts',
'src/main/computer/desktop-script-runtime-host.win32.test.ts',
'src/main/cursor/hook-service.test.ts',
'src/main/orca-profiles/profile-index-store.test.ts',
'src/main/startup/windows-install-dir-acl-repair.win32.test.ts',
'src/main/runtime/repo-worktree-admin-fingerprint.test.ts',
'src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts',
'src/shared/secure-file-fsync-flags.test.ts',
'src/shared/secure-path-windows-acl.win32.test.ts',
'src/main/runtime/unreadable-secret-store-preservation.win32.test.ts',
'src/main/ipc/pty-codex-account-attribution.test.ts',
'src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts',
'src/main/ipc/preflight-provider-command-selection.test.ts',
'src/main/ipc/preflight-runnable-local-cli.test.ts',
'src/relay/windows-port-scan.win32.test.ts',
'src/main/ssh/ssh-relay-upload-stage-windows-identity.test.ts',
'src/main/ssh/remote-node-runtime-store-windows.test.ts'
]
const DESKTOP_IRRELEVANT_PREFIXES = [
'mobile/',
'cloud/',
'.github/workflows/cloud-',
'.github/workflows/mobile.yml',
'.github/workflows/mobile-ios-release.yml',
'.github/workflows/mobile-android-release.yml'
]
const STATIC_ANALYSIS_AUDIT_SCRIPTS = [
'audit:code-quality:native',
'audit:code-quality:type-aware',
'audit:anti-slop'
]
// Positional arguments of an oxlint invocation are the trees it lints. `--config` consumes the
// next token; every other flag here is valueless.
function oxlintScanRoots(command) {
const roots = []
for (const segment of command.split('&&')) {
const tokens = segment.trim().split(/\s+/).filter(Boolean)
if (tokens[0] !== 'oxlint') {
continue
}
for (let index = 1; index < tokens.length; index += 1) {
if (tokens[index] === '--config') {
index += 1
} else if (!tokens[index].startsWith('-')) {
roots.push(tokens[index])
}
}
}
return roots
}
// Why derived from the commands rather than listed here: `mobile/` is desktop-irrelevant for every
// other job, yet these audits lint it. A second, hand-maintained copy of "which trees the gate
// reads" is what let #20702 land violations no PR check ran, so read it off the argv instead.
function readStaticAnalysisScanRoots() {
const manifest = join(import.meta.dirname, '../../package.json')
const { scripts = {} } = JSON.parse(readFileSync(manifest, 'utf8'))
return [
...new Set(
STATIC_ANALYSIS_AUDIT_SCRIPTS.flatMap((name) => oxlintScanRoots(scripts[name] ?? ''))
)
]
}
export const STATIC_ANALYSIS_SCAN_ROOTS = readStaticAnalysisScanRoots()
const STATIC_ANALYSIS_SCAN_PREFIXES = STATIC_ANALYSIS_SCAN_ROOTS.map((root) => `${root}/`)
export function isDocsOnlyPath(file) {
if (DOCS_ONLY_FILES.has(file)) {
return true
}
if (DOCS_ONLY_PREFIXES.some((prefix) => file.startsWith(prefix))) {
return true
}
return /^README\.[^/]+\.md$/.test(file)
}
export function shouldRunPrChecks(changedFiles) {
// Why empty-run: a silent empty diff is more likely a detector bug than a
// genuine no-op PR, so fail closed and keep the expensive jobs.
if (changedFiles.length === 0) {
return true
}
return changedFiles.some((file) => !isDocsOnlyPath(file) && !isDesktopIrrelevantPath(file))
}
export function needsMobileDependencies(changedFiles) {
// Why: static analysis lints CHANGED files, mobile ones included, and its
// type-aware pass resolves types from mobile/node_modules. Mobile is a
// separate pnpm project, so without this the root-only install leaves every
// mobile type an `error` type and the gate reports phantom findings.
return changedFiles.length === 0 || changedFiles.some((file) => file.startsWith('mobile/'))
}
export function classifyPrJobs(changedFiles) {
const emptyDiff = changedFiles.length === 0
const shouldRun = shouldRunPrChecks(changedFiles)
const forceAll = emptyDiff || changedFiles.some(isGlobalForcePath)
const jobs = Object.fromEntries(
PR_CHECK_JOBS.map((job) => [
job,
shouldRun && (forceAll || ALWAYS_ON_CODE_JOBS.has(job) || jobDetector(job)(changedFiles))
])
)
// Why outside should_run: a mobile-only diff is desktop-irrelevant and skips every job above,
// but the repo-wide audits lint mobile/, and skipping them lands the violation on main, where
// it then fails this same gate on every later PR's merge ref.
jobs.static_analysis = jobs.static_analysis || changedFiles.some(isStaticAnalysisScannedPath)
// Why outside should_run, for the same reason: a mobile-only diff is desktop-irrelevant, and
// that is exactly the diff that changes the page this job builds. Gated on should_run it would
// skip on every PR that can break it and run on none.
jobs.mobile_web_app = jobs.mobile_web_app || changesMobileWebApp(changedFiles)
return {
should_run: shouldRun,
native_cache_changed: shouldRun && (emptyDiff || changedFiles.some(isNativeCacheInputPath)),
mobile_dependencies:
(shouldRun || jobs.static_analysis) && needsMobileDependencies(changedFiles),
...jobs
}
}
function jobDetector(job) {
switch (job) {
case 'git_compatibility':
return (files) => files.some((file) => matchesPrefix(file, GIT_COMPAT_PREFIXES))
case 'codex_index_heal_contract':
return (files) =>
files.some((file) => matchesPrefix(file, CODEX_INDEX_HEAL_CONTRACT_PREFIXES))
case 'xterm_patch_sync':
return (files) => files.some((file) => matchesPrefix(file, XTERM_PREFIXES))
case 'shell_contracts':
return (files) => files.some((file) => matchesPrefix(file, SHELL_PREFIXES))
case 'orcad_browser':
return (files) => files.some((file) => matchesPrefix(file, ORCAD_BROWSER_PREFIXES))
// Not redundant with the lift below the jobs map: without a case here the default detector
// returns true, which would run this job on every desktop-relevant PR.
case 'mobile_web_app':
return changesMobileWebApp
case 'cross-version-wire':
return (files) => files.some((file) => matchesPrefix(file, CROSS_VERSION_WIRE_PREFIXES))
case 'managed_hook_node18':
return (files) => files.some((file) => matchesPrefix(file, MANAGED_HOOK_PREFIXES))
case 'package':
return (files) => files.some(isLinuxPackagePath)
case 'package_windows':
return (files) => files.some(isWindowsPackagePath)
default:
return () => true
}
}
function isLinuxPackagePath(file) {
return LINUX_PACKAGE_TESTS.includes(file) || isProductBundlePath(file, LINUX_PACKAGE_PREFIXES)
}
function isWindowsPackagePath(file) {
return WINDOWS_PACKAGE_TESTS.includes(file) || isProductBundlePath(file, WINDOWS_PACKAGE_PREFIXES)
}
function isProductBundlePath(file, extraPrefixes) {
if (isTestFile(file)) {
return false
}
if (file.startsWith('src/')) {
return true
}
return matchesPrefix(file, extraPrefixes)
}
function isTestFile(file) {
return /\.(?:test|spec)\.(?:js|cjs|mjs|ts|tsx)$/.test(file) || file.includes('/__tests__/')
}
function isDesktopIrrelevantPath(file) {
return matchesPrefix(file, DESKTOP_IRRELEVANT_PREFIXES)
}
function isStaticAnalysisScannedPath(file) {
// Fail closed: roots we failed to parse must keep the gate, not silently drop it.
return (
STATIC_ANALYSIS_SCAN_PREFIXES.length === 0 || matchesPrefix(file, STATIC_ANALYSIS_SCAN_PREFIXES)
)
}
function isNativeCacheInputPath(file) {
return NATIVE_CACHE_FILES.has(file) || matchesPrefix(file, NATIVE_CACHE_PREFIXES)
}
function isGlobalForcePath(file) {
return GLOBAL_FORCE_FILES.has(file) || matchesPrefix(file, GLOBAL_FORCE_PREFIXES)
}
function matchesPrefix(file, prefixes) {
return prefixes.some((prefix) => file === prefix || file.startsWith(prefix))
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
// Why streamed, not readFileSync(0): a single read of fd 0 throws EAGAIN once the writer
// outgrows the 64 KB pipe buffer, which a stale PR base.sha reaches easily.
let input = ''
process.stdin.setEncoding('utf8')
for await (const chunk of process.stdin) {
input += chunk
}
const files = input.split(/\r?\n/).filter(Boolean)
const classification = classifyPrJobs(files)
for (const [name, value] of Object.entries(classification)) {
process.stdout.write(`${name}=${value ? 'true' : 'false'}\n`)
}
}