Files
Teknium a5bd246865 Old pre-decomposition import paths are gone: plugin compat layer removed on schedule (#126164)
* refactor(plugins): remove the Sep 2026 decomposition compat layer on schedule

The PLUGIN-COMPAT layer (2776813df3 + d63e380324 + 0a5164cebe) kept pre-#102117 import paths
alive for external plugins until 2026-09-14. That window closed two weeks ago; since then the loader
has already been skipping plugins that use the old paths. This removes the layer itself:

- 328 appended `PLUGIN-COMPAT` blocks (lazy `__getattr__` pointer tables, re-exported third-party
  names, restored dead definitions) and the three re-export stub modules
  (gateway/startup_watchdog, hermes_cli/observability/relay_runtime, tools/environments/modal_utils)
- COMPAT_MANIFEST.md, compat_manifest.json, scripts/check_compat_pointers.py and its lint step
- the reporting surfaces: CLI banner notice, `hermes plugins compat`, the `hermes doctor` section,
  the post-update notice, the Desktop one-time dialog, the loader's pre-import skip and the
  `plugins.allow_deprecated_imports` escape hatch

An external plugin that still imports an old path now fails to load with its ImportError as the
reason in `hermes plugins list`, the same path as any broken plugin.

hermes_cli/plugin_compat.py stays as three inert stubs (compat_report, removal_in_effect,
summary_lines): an already-running pre-removal `hermes update` lazy-imports them after the checkout
swap (tests/compat/old_updater_surface.json).

In-tree fallout, both already dead: hermes_cli/setup.py::_check_espeak_ng (no callers; its
`shutil` came from a compat block) and gateway/config.py::SessionResetPolicy ("retained solely for
the scheduled plugin-compat window"). Two test_run_agent patches targeted the removed
`run_agent.handle_function_call` pointer; they now patch `model_tools.handle_function_call`, the
seam production reads, like every sibling test in that file.

* chore: retrigger CI (zero-job startup_failure phantom)

* test: drop resolution allowlist rows for the two deleted which() sites

hermes_cli/setup.py::_check_espeak_ng (dead) and tools/skillevaluator_scan.py::scanner_available
(a restored definition inside a PLUGIN-COMPAT block) no longer exist; the stale-row gate requires
their allowlist entries go with them.
2026-09-28 10:21:41 -07:00

235 lines
11 KiB
YAML

name: Lint (ruff + ty)
# Two things here:
# 1. Advisory diff — ruff + ty diagnostics as a diff vs the target branch.
# Writes a Markdown summary to the run page. Exit zero always.
# 2. Blocking ``ruff check .`` — enforces the explicit rules in
# ``[tool.ruff.lint.select]`` (currently PLW1514). Failure blocks merge.
# Separate job so the advisory diff still runs even when enforcement
# fails.
#
# CI-sensitive file review was previously here as a ``ci-review`` job but
# has moved to ``review-labels.yml`` so it can be rerun independently.
on:
workflow_call:
inputs:
event_name:
description: The event name from the calling orchestrator (pull_request or push).
type: string
required: true
permissions:
contents: read
concurrency:
group: lint-${{ github.ref_type == 'tag' && github.run_id || github.ref }}
cancel-in-progress: ${{ github.ref_type != 'tag' }}
jobs:
lint-diff:
name: ruff + ty diff
if: inputs.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0 # need full history for merge-base + worktree
- uses: ./.github/actions/setup-pm
with:
cache-python: true
prune-python-cache: true
- name: Install ruff + ty
uses: ./.github/actions/retry
with:
command: python -m scripts.ci.python_packages ruff==0.15.10 ty==0.0.82
- name: Determine base ref
id: base
run: |
# For PRs, diff against the merge base with the target branch.
# For pushes to main, diff against the previous commit on main.
if [ "${{ inputs.event_name }}" = "pull_request" ]; then
BASE_SHA=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
BASE_REF="origin/${{ github.base_ref }}"
else
BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD)
BASE_REF="HEAD~1"
fi
echo "sha=${BASE_SHA}" >> "$GITHUB_OUTPUT"
echo "ref=${BASE_REF}" >> "$GITHUB_OUTPUT"
echo "Base SHA: ${BASE_SHA}"
echo "Base ref: ${BASE_REF}"
- name: Run ruff + ty on HEAD
run: |
mkdir -p .lint-reports/head
ruff check --output-format json --exit-zero \
> .lint-reports/head/ruff.json || true
ty check --output-format gitlab --exit-zero \
> .lint-reports/head/ty.json || true
echo "HEAD ruff: $(wc -c < .lint-reports/head/ruff.json) bytes"
echo "HEAD ty: $(wc -c < .lint-reports/head/ty.json) bytes"
- name: Run ruff + ty on base (via git worktree)
run: |
mkdir -p .lint-reports/base
# Use a worktree so we don't clobber the main checkout. If the basex
# SHA is identical to HEAD (e.g. first commit), skip and leave the
# base reports empty — the diff script handles missing files.
HEAD_SHA=$(git rev-parse HEAD)
BASE_SHA="${{ steps.base.outputs.sha }}"
if [ "$BASE_SHA" = "$HEAD_SHA" ]; then
echo "Base SHA == HEAD SHA, skipping base scan."
echo '[]' > .lint-reports/base/ruff.json
echo '[]' > .lint-reports/base/ty.json
else
git worktree add --detach /tmp/lint-base "$BASE_SHA"
(
cd /tmp/lint-base
ruff check --output-format json --exit-zero \
> "$GITHUB_WORKSPACE/.lint-reports/base/ruff.json" || true
ty check --output-format gitlab --exit-zero \
> "$GITHUB_WORKSPACE/.lint-reports/base/ty.json" || true
)
git worktree remove --force /tmp/lint-base
fi
echo "base ruff: $(wc -c < .lint-reports/base/ruff.json) bytes"
echo "base ty: $(wc -c < .lint-reports/base/ty.json) bytes"
- name: Generate diff summary
env:
HEAD_REF: ${{ inputs.event_name == 'pull_request' && github.head_ref || github.ref_name }}
run: |
python scripts/lint_diff.py \
--base-ruff .lint-reports/base/ruff.json \
--head-ruff .lint-reports/head/ruff.json \
--base-ty .lint-reports/base/ty.json \
--head-ty .lint-reports/head/ty.json \
--base-ref "${{ steps.base.outputs.ref }}" \
--head-ref "$HEAD_REF" \
--output .lint-reports/summary.md
cat .lint-reports/summary.md >> "$GITHUB_STEP_SUMMARY"
ruff-blocking:
# Enforce the rules in pyproject.toml [tool.ruff.lint.select]. Currently
# PLW1514 (unspecified-encoding) — catches bare ``open()`` /
# ``read_text()`` / ``write_text()`` calls that default to locale
# encoding on Windows. Failure here blocks merge; the advisory
# ``lint-diff`` job above runs independently so reviewers still get
# the diff comment even when enforcement fails.
name: ruff enforcement (blocking)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-pm
with:
cache-python: true
prune-python-cache: true
- name: Install ruff
uses: ./.github/actions/retry
with:
command: python -m scripts.ci.python_packages ruff==0.15.10
- name: ruff check .
# No --exit-zero, no || true. Exit code propagates to the job,
# which propagates to the required-check gate.
run: |
ruff check .
windows-footguns:
# Static guardrails on Windows-unsafe Python primitives — os.kill(pid, 0),
# os.killpg, os.setsid, signal.SIGKILL without getattr fallback,
# shebang scripts via subprocess, bare open() without encoding=, etc.
# See scripts/check-windows-footguns.py for the full rule list.
name: Windows footguns (blocking)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-pm
with:
cache-python: true
prune-python-cache: true
- name: Run footgun checker
run: python scripts/check-windows-footguns.py --all
- name: Require portable Bash shebangs
run: python scripts/check_bash_shebangs.py
# /tmp is not portable (Termux has none, native Windows has none, macOS aliases it to
# /private/tmp, Linux mounts it as a small tmpfs). Production code resolves scratch space
# through the scratch-dir helper; skills, docs and prompts must not teach the model a
# literal /tmp either. `no-tmp: ok — <why>` marks a deliberate line; _BASELINE in the
# script is the burn-down list of pre-existing hits.
- name: Forbid literal /tmp paths outside the baseline
run: python scripts/check_no_tmp_literals.py
# config.yaml is hand-edited and commented; a PyYAML dump of it destroys every comment
# (#92554, regressed repeatedly). All writers go through hermes_cli.config.atomic_config_write.
- name: Forbid config.yaml writers that bypass the comment-preserving writer
run: python scripts/check_config_yaml_writers.py
# The OS lanes import only files carrying the matching marker, so a test that fakes
# macOS (is_macos -> True, sys.platform -> "darwin") without `platforms("macos")` is green on
# Linux over a faked branch and never runs on macOS (#111866, AGENTS.md § Don't fake the host OS).
- name: Forbid unmarked macOS fakes in tests
run: python scripts/ci/check_os_marker_fakes.py
# Advisory: profile-scope hazard shapes on the lines this PR adds (child env from os.environ,
# raw os.getenv of a platform credential, HOME-only RPC binding, bare-PID liveness). One
# process serves many profiles; every pattern leaked the launch profile at least once. Printed
# into the log for the reviewer; never fails the job (root AGENTS.md § Code Shape Rules).
- name: Profile-scope patterns on added lines (advisory)
if: github.event_name == 'pull_request'
continue-on-error: true
timeout-minutes: 3
env:
PR_HEAD: "+refs/pull/${{ github.event.pull_request.number }}/head:refs/remotes/origin/pr-head"
run: |
git fetch --no-tags --deepen=200 origin "${{ github.base_ref }}" "$PR_HEAD"
for i in 1 2 3; do
git merge-base "origin/${{ github.base_ref }}" origin/pr-head >/dev/null 2>&1 && break
git fetch --no-tags --deepen=1000 origin "${{ github.base_ref }}" "$PR_HEAD"
done
python scripts/check_profile_scope_patterns.py --base "origin/${{ github.base_ref }}" --head origin/pr-head
# Advisory: dropped public names / methods / test defs vs the PR base, printed into the log.
# A refactor that silently removes a public symbol breaks plugins that import it; the Sep 2026
# decomposition opened with 1,703 such drops that reviewers had to find by hand.
# Advisory: it never fails the job. The checkout above is depth-1, so deepen both sides until
# a merge-base exists (the script refuses to report a clean diff without one, by design).
# Diff the PR head ref, not the checked-out refs/pull/N/merge commit: that synthetic commit
# is recomputed server-side whenever main moves, and once it is unreachable no amount of
# deepening ever reaches its parents (run 34285107265: 4 fetches, 4m15s, "no merge-base").
- name: Public-surface diff vs base (advisory)
if: github.event_name == 'pull_request'
continue-on-error: true
# Bound the advisory step so a long deepen/fetch (e.g. a distant or missing
# merge-base) can't consume the blocking job's 5-minute budget and cancel it.
# continue-on-error already keeps a step *failure* off the job; a step-level
# timeout keeps a step *overrun* off the job-level timeout the same way.
# Sizing: a --deepen=200 fetch took ~56s and a --deepen=1000 ~67s on the runner, and
# main gains ~170 commits/day, so a day-old branch legitimately needs both (~2 min).
timeout-minutes: 3
env:
PR_HEAD: "+refs/pull/${{ github.event.pull_request.number }}/head:refs/remotes/origin/pr-head"
run: |
git fetch --no-tags --deepen=200 origin "${{ github.base_ref }}" "$PR_HEAD"
for i in 1 2 3; do
git merge-base "origin/${{ github.base_ref }}" origin/pr-head >/dev/null 2>&1 && break
git fetch --no-tags --deepen=1000 origin "${{ github.base_ref }}" "$PR_HEAD"
done
python scripts/ci/check_public_surface.py --base "origin/${{ github.base_ref }}" --head origin/pr-head