Work on OpenVINO GitHub Actions CI
Guides changes to the regular GitHub Actions CI in this repository: validation and reusable
workflows, custom actions, CI scripts, Dockerfiles, and the Smart CI configuration that drives them.
Read the CI developer docs under docs/dev/ci/github_actions
first — they are the authoritative, repo-specific reference and this skill is a checklist on top of
them. Keep them in sync when behavior changes (see Skill self-improvement). The key pages:
For framework syntax, use the official GitHub Actions documentation. If user requires an out-of-scope feature, consult the official documentation.
Out of scope: gh-aw agentic workflows (.github/workflows/*.md + *.lock.yml, e.g. ci-doctor)
— use the ov-agentic-workflows skill instead.
Repository layout
- Workflows —
.github/workflows/
- Validation workflows: named after the OS/config, e.g.
ubuntu_22.yml,
windows_vs2022_release.yml, mac_arm64.yml, linux_arm64.yml, android.yml. Entry points with
on: triggers; they wire together Build + test jobs.
- Reusable workflows:
job_*.yml (e.g. job_python_unit_tests.yml, job_cxx_unit_tests.yml).
Called via uses: ./.github/workflows/job_*.yml with on: workflow_call: inputs. Not triggered
directly.
- Custom actions —
.github/actions/ (composite action.yml): setup_python, system_info,
smart-ci, handle_docker, openvino_provider, store_artifacts/restore_artifacts, cache, etc.
- CI scripts —
.github/scripts/ (Python helpers: workflow_rerun/, external_pr_labeller.py,
check_copyright.py, ...).
- Dockerfiles —
.github/dockerfiles/ov_build/** and ov_test/**, plus the docker_tag file.
- Smart CI config —
.github/labeler.yml (path globs → component/label) and
.github/components.yml (component dependency graph).
- Scans/meta —
workflows_scans.yml (CodeQL actions + semgrep on workflow changes),
dependency_review.yml.
Golden rules
- Pin third-party actions to a full commit SHA, with the version in a trailing comment. First-party
./.github/actions/* are referenced by path, not pinned.uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- Least-privilege
permissions:. Start from permissions: read-all (workflow level) and grant the
minimum extra scope at the job level only where needed. Never widen without a reason.
- Follow security.md — it is the source of truth.
Key rules: never use
pull_request_target, never hard-code secrets, and treat all github.* /
github.event.* user-controlled values as untrusted (route them through env: or action inputs, never
interpolate directly into run: shell). Ping the CI task force for anything involving secrets or
elevated triggers.
- Put shared job logic in a reusable
job_*.yml, not copy-paste. If the same job appears in more
than one validation workflow, it should be a workflow_call reusable workflow parameterized by
runner, image/container, and affected-components.
- Respect Smart CI. Test jobs/steps that validate a specific component must gate on
fromJSON(needs.smart_ci.outputs.affected_components).<COMPONENT>.{build,test} and list Smart_CI in
needs. Do not make an expensive job run unconditionally.
- Custom Docker images come from
handle_docker, not hard-coded tags. Reference build/test images
as ${{ fromJSON(needs.docker.outputs.images).ov_build.<name> }} and add Docker to needs. Plain
passthrough images must use the ACR mirror openvinogithubactions.azurecr.io/..., never docker.io.
- Keep changes scoped and validate before finishing (see Validation).
.github/** is owned by
@openvinotoolkit/openvino-ci-maintainers and changes there are label category: CI
(workflows also get github_actions).
Key conventions
- Runner selection (
runs-on) — self-hosted Azure pools aks-{os}-{cores}-cores-{ram}gb[-arm] for
heavy build/test; GitHub-hosted (ubuntu-22.04, ...) for light jobs (labelers, style). GPU jobs use
[ self-hosted, gpu|igpu|dgpu ] and must run in Docker. Azure aks-* runners are required to
pull from the ACR / use custom images. Match cores to parallelism (see runners.md).
- Containers — most self-hosted jobs run in a
container:. Mount the shared drive with
volumes: [ /mount:/mount ] and add ${{ github.workspace }}:${{ github.workspace }} where the
workspace must be identical inside/outside the container.
- Caches — GHA cache (
actions/cache, ≤10 GB) for small deps; shared drive (/mount/..., e.g.
PIP_CACHE_PATH: /mount/caches/pip/linux) for large assets on Linux self-hosted; sccache → Azure
Blob for C/C++ build cache (needs SCCACHE_AZURE_* env + CMAKE_*_COMPILER_LAUNCHER: sccache +
SCCACHE_AZURE_KEY_PREFIX).
- Artifacts —
Build job packs and uploads; test jobs needs: Build and download. Follow the
existing store_artifacts/restore_artifacts actions and artifact-name conventions in the workflow.
timeout-minutes — always set a sensible per-job timeout.
Common tasks
Add a test (choose the smallest unit)
Follow adding_tests.md:
- New tests inside an existing suite → no workflow change needed.
- New step in an existing job → add a
name + run step, gate with
if: fromJSON(inputs.affected-components).<COMPONENT>.test when component-specific.
- New job → add to the right validation workflow (or a
job_*.yml); set needs: [Build, Smart_CI],
runs-on, container, timeout-minutes, and a Smart CI if:.
Add / edit a reusable workflow (job_*.yml)
- Give it
on: workflow_call: with typed inputs (runner, image, affected-components,
python-version, ...) and permissions: read-all.
- Reference it from validation workflows via
uses: ./.github/workflows/job_<name>.yml with with: and
needs: [ Build, Smart_CI ].
- Keep the input contract minimal and documented via
description:.
Wire up Smart CI for a component
- Map source paths → label in
.github/labeler.yml ('category: X': [globs]).
- Declare dependents in
.github/components.yml under revalidate: (build+test) / build: (build
only); use [] for none, or 'all' to force full validation. Dependencies are not transitive.
- Add
Smart_CI to the validating job's needs and gate with
if: fromJSON(needs.smart_ci.outputs.affected_components).<COMPONENT>.{build,test}. Keep the same
condition on every step/job in the dependency chain — a skipped step feeding an ungated dependent
leaves it running against missing outputs.
- Aggregate results into the
Overall_Status job (it needs: the real jobs and reports one required
check). A workflow that must be required cannot use a paths: filter — a filtered-out run reports
no status and blocks the merge queue; rely on Smart CI + Overall_Status instead.
Add / change a custom action
- Composite
action.yml under .github/actions/<name>/. Declare inputs (with description,
required, default) and runs: using: composite. Every run step needs an explicit shell:.
- Reference untrusted input via
env: inside the step, not inline interpolation.
- Update custom_actions.md if the action is
user-facing.
Choosing the implementation environment
- Python is the default and preferred implementation language for a custom action's logic. Use it for
any action that does not need extensive access to the GitHub (Actions) API — file/artifact
handling, environment setup, packaging, running tools, parsing, etc.
- Use JavaScript/TypeScript only when the action uses the GitHub (Actions) API extensively — the
Octokit/
@actions/* toolkit gives first-class typed access to it. The bundled
.github/actions/cache action is the reference example of a
JS-based action.
- When in doubt, prefer Python and keep API interaction minimal.
Add / use a custom Docker image
Follow docker_images.md: add a Dockerfile under
.github/dockerfiles/{ov_build,ov_test}/<platform>/, ensure a Docker job runs handle_docker with the
image path in images:, add Docker to consumers' needs, and set
image: ${{ fromJSON(needs.docker.outputs.images).<group>.<name> }}. When adding a new env-setup script,
add it under category: docker_env in labeler.yml, exclude it from .dockerignore, and bump
.github/dockerfiles/docker_tag (handle_docker prompts you).
Change a runner or container
Pick the pool from runners.md; keep container
volumes/options (shared drive, sccache) consistent with sibling jobs. GPU → Docker + self-hosted label.
Validation before finishing
- YAML/lint: run
actionlint if available (actionlint .github/workflows/<file>.yml); otherwise
sanity-check YAML parses. Mirror what workflows_scans.yml (CodeQL actions + semgrep) and
dependency_review.yml enforce — those run on any .github/workflows/** change.
- Pinning: every third-party
uses: is a full SHA + version comment.
- Permissions: workflow defaults to least privilege; extra scopes are job-scoped and justified.
- Smart CI: new component-specific jobs/steps are gated and
Smart_CI is in needs.
- Scope: only intended files changed; no stray
docker_tag/labeler/components edits unless required.
- Do not hand-edit any
*.lock.yml — those belong to agentic workflows.
Pitfalls to check
- Unpinned or tag-pinned third-party action — supply-chain risk; CodeQL/semgrep will flag it.
- Untrusted input in
run: — ${{ github.event.* }} interpolated into shell is an injection vector;
route through env:.
- Over-broad
permissions: — especially write scopes at workflow level.
- Job that ignores Smart CI — burns limited self-hosted/GPU capacity on unaffected PRs.
- Smart CI condition mismatch across a dependency chain — a step skipped by a Smart CI
if: whose
dependent step/job lacks the same condition runs against missing outputs/artifacts. Gate the whole
chain consistently.
paths: filter on a required workflow — filtered-out runs report no status and hang the merge
queue; use Smart CI + Overall_Status instead of paths:.
- Hard-coded
docker.io / non-ACR image on aks-* — pulls fail or hit rate limits; use the ACR
mirror or handle_docker output.
- Missing
Docker/Smart_CI/Build in needs — races or missing artifacts/inputs.
- Editing a
job_*.yml input contract without updating every caller's with: block.
- Docker env change without
docker_tag bump — the image won't rebuild; handle_docker fails the check.
- Missing
timeout-minutes — a hung job can occupy a runner indefinitely.
- GPU job without Docker — required on persistent GPU runners.
Skill self-improvement
Keep this skill and the CI docs in sync with reality. When a change reveals a new rule, pattern, or
footgun:
- New reusable workflow / custom action / Dockerfile convention — note it under Common tasks and,
if user-facing, in the matching page under
docs/dev/ci/github_actions/.
- New footgun (a pinning/permission/Smart CI/Docker/cache mistake that bit you) — add it to
Pitfalls.
- A rule becomes obsolete (a workflow removed, a runner pool renamed, a path moved) — update or
remove the stale entry instead of leaving it.
- Keep it concise — prefer linking to the (updated) doc over duplicating detail; this file is a short
actionable checklist, not a second copy of the documentation.
- This directory is reachable via both
.agents/skills/ and .claude/skills/ (the former is a symlink to
the latter), so a single edit updates both — do not create a duplicate copy.
1---2name: ov-github-actions-ci3description: Author, edit, debug, and review the OpenVINO GitHub Actions CI infrastructure — regular (non-agentic) workflows under .github/workflows, reusable job_*.yml workflows, custom composite actions under .github/actions, CI helper scripts under .github/scripts, Dockerfiles under .github/dockerfiles, and the Smart CI / labeler / components configuration. Use when a user wants to add or change a build/test job or step, create or modify a reusable workflow, write or fix a custom action, adjust runners/containers/caches, wire up Smart CI for a component, pin action versions, fix workflow permissions/security, or debug a failing CI workflow's YAML. Do NOT use for gh-aw agentic workflows (*.md with gh-aw frontmatter — use ov-agentic-workflows), for diagnosing a specific product/test failure's root cause in C++/Python code, or for non-CI GitHub configuration.4---56# Work on OpenVINO GitHub Actions CI78Guides changes to the **regular** GitHub Actions CI in this repository: validation and reusable9workflows, custom actions, CI scripts, Dockerfiles, and the Smart CI configuration that drives them.1011**Read the CI developer docs under [docs/dev/ci/github_actions](../../../docs/dev/ci/github_actions)12first** — they are the authoritative, repo-specific reference and this skill is a checklist on top of13them. Keep them in sync when behavior changes (see **Skill self-improvement**). The key pages:1415| Topic | Doc |16|-------|-----|17| Big picture, workflow structure, triggers, results/artifacts/logs | [overview.md](../../../docs/dev/ci/github_actions/overview.md) |18| Reusable `job_*.yml` workflows | [reusable_workflows.md](../../../docs/dev/ci/github_actions/reusable_workflows.md) |19| Custom composite actions | [custom_actions.md](../../../docs/dev/ci/github_actions/custom_actions.md) |20| Smart CI (skip unaffected jobs) | [smart_ci.md](../../../docs/dev/ci/github_actions/smart_ci.md) |21| Runners (`runs-on`) | [runners.md](../../../docs/dev/ci/github_actions/runners.md) |22| Docker images / `handle_docker` | [docker_images.md](../../../docs/dev/ci/github_actions/docker_images.md) |23| Caches (GHA / shared drive / sccache) | [caches.md](../../../docs/dev/ci/github_actions/caches.md) |24| Adding tests (step / job / workflow) | [adding_tests.md](../../../docs/dev/ci/github_actions/adding_tests.md) |25| OpenVINO Provider (prebuilt artifacts) | [openvino_provider.md](../../../docs/dev/ci/github_actions/openvino_provider.md) |26| Workflow security | [security.md](../../../docs/dev/ci/github_actions/security.md) |2728For framework syntax, use the official [GitHub Actions documentation](https://docs.github.com/en/actions). If user requires an out-of-scope feature, consult the official documentation.2930**Out of scope:** `gh-aw` agentic workflows (`.github/workflows/*.md` + `*.lock.yml`, e.g. `ci-doctor`)31— use the **ov-agentic-workflows** skill instead.3233## Repository layout3435* **Workflows** — `.github/workflows/`36 * **Validation workflows**: named after the OS/config, e.g. `ubuntu_22.yml`,37 `windows_vs2022_release.yml`, `mac_arm64.yml`, `linux_arm64.yml`, `android.yml`. Entry points with38 `on:` triggers; they wire together `Build` + test jobs.39 * **Reusable workflows**: `job_*.yml` (e.g. `job_python_unit_tests.yml`, `job_cxx_unit_tests.yml`).40 Called via `uses: ./.github/workflows/job_*.yml` with `on: workflow_call:` inputs. **Not** triggered41 directly.42* **Custom actions** — `.github/actions/` (composite `action.yml`): `setup_python`, `system_info`,43 `smart-ci`, `handle_docker`, `openvino_provider`, `store_artifacts`/`restore_artifacts`, `cache`, etc.44* **CI scripts** — `.github/scripts/` (Python helpers: `workflow_rerun/`, `external_pr_labeller.py`,45 `check_copyright.py`, ...).46* **Dockerfiles** — `.github/dockerfiles/ov_build/**` and `ov_test/**`, plus the `docker_tag` file.47* **Smart CI config** — `.github/labeler.yml` (path globs → component/label) and48 `.github/components.yml` (component dependency graph).49* **Scans/meta** — `workflows_scans.yml` (CodeQL `actions` + semgrep on workflow changes),50 `dependency_review.yml`.5152## Golden rules53541. **Pin third-party actions to a full commit SHA**, with the version in a trailing comment. First-party55 `./.github/actions/*` are referenced by path, not pinned.56 ```yaml57 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.258 ```592. **Least-privilege `permissions:`.** Start from `permissions: read-all` (workflow level) and grant the60 minimum extra scope at the **job** level only where needed. Never widen without a reason.613. **Follow [security.md](../../../docs/dev/ci/github_actions/security.md)** — it is the source of truth.62 Key rules: never use `pull_request_target`, never hard-code secrets, and treat all `github.*` /63 `github.event.*` user-controlled values as untrusted (route them through `env:` or action inputs, never64 interpolate directly into `run:` shell). Ping the CI task force for anything involving secrets or65 elevated triggers.664. **Put shared job logic in a reusable `job_*.yml`, not copy-paste.** If the same job appears in more67 than one validation workflow, it should be a `workflow_call` reusable workflow parameterized by68 `runner`, `image`/`container`, and `affected-components`.695. **Respect Smart CI.** Test jobs/steps that validate a specific component must gate on70 `fromJSON(needs.smart_ci.outputs.affected_components).<COMPONENT>.{build,test}` and list `Smart_CI` in71 `needs`. Do not make an expensive job run unconditionally.726. **Custom Docker images come from `handle_docker`, not hard-coded tags.** Reference build/test images73 as `${{ fromJSON(needs.docker.outputs.images).ov_build.<name> }}` and add `Docker` to `needs`. Plain74 passthrough images must use the ACR mirror `openvinogithubactions.azurecr.io/...`, never `docker.io`.757. **Keep changes scoped and validate before finishing** (see **Validation**). `.github/**` is owned by76 `@openvinotoolkit/openvino-ci-maintainers` and changes there are label `category: CI`77 (workflows also get `github_actions`).7879## Key conventions8081* **Runner selection** (`runs-on`) — self-hosted Azure pools `aks-{os}-{cores}-cores-{ram}gb[-arm]` for82 heavy build/test; GitHub-hosted (`ubuntu-22.04`, ...) for light jobs (labelers, style). GPU jobs use83 `[ self-hosted, gpu|igpu|dgpu ]` and **must** run in Docker. Azure `aks-*` runners are required to84 pull from the ACR / use custom images. Match cores to parallelism (see runners.md).85* **Containers** — most self-hosted jobs run in a `container:`. Mount the shared drive with86 `volumes: [ /mount:/mount ]` and add `${{ github.workspace }}:${{ github.workspace }}` where the87 workspace must be identical inside/outside the container.88* **Caches** — GHA cache (`actions/cache`, ≤10 GB) for small deps; shared drive (`/mount/...`, e.g.89 `PIP_CACHE_PATH: /mount/caches/pip/linux`) for large assets on Linux self-hosted; `sccache` → Azure90 Blob for C/C++ build cache (needs `SCCACHE_AZURE_*` env + `CMAKE_*_COMPILER_LAUNCHER: sccache` +91 `SCCACHE_AZURE_KEY_PREFIX`).92* **Artifacts** — `Build` job packs and uploads; test jobs `needs: Build` and download. Follow the93 existing `store_artifacts`/`restore_artifacts` actions and artifact-name conventions in the workflow.94* **`timeout-minutes`** — always set a sensible per-job timeout.9596## Common tasks9798### Add a test (choose the smallest unit)99Follow [adding_tests.md](../../../docs/dev/ci/github_actions/adding_tests.md):100* **New tests inside an existing suite** → no workflow change needed.101* **New step in an existing job** → add a `name` + `run` step, gate with102 `if: fromJSON(inputs.affected-components).<COMPONENT>.test` when component-specific.103* **New job** → add to the right validation workflow (or a `job_*.yml`); set `needs: [Build, Smart_CI]`,104 `runs-on`, `container`, `timeout-minutes`, and a Smart CI `if:`.105106### Add / edit a reusable workflow (`job_*.yml`)1071. Give it `on: workflow_call:` with typed `inputs` (`runner`, `image`, `affected-components`,108 `python-version`, ...) and `permissions: read-all`.1092. Reference it from validation workflows via `uses: ./.github/workflows/job_<name>.yml` with `with:` and110 `needs: [ Build, Smart_CI ]`.1113. Keep the input contract minimal and documented via `description:`.112113### Wire up Smart CI for a component1141. Map source paths → label in `.github/labeler.yml` (`'category: X': [globs]`).1152. Declare dependents in `.github/components.yml` under `revalidate:` (build+test) / `build:` (build116 only); use `[]` for none, or `'all'` to force full validation. Dependencies are **not** transitive.1173. Add `Smart_CI` to the validating job's `needs` and gate with118 `if: fromJSON(needs.smart_ci.outputs.affected_components).<COMPONENT>.{build,test}`. Keep the same119 condition on **every** step/job in the dependency chain — a skipped step feeding an ungated dependent120 leaves it running against missing outputs.1214. Aggregate results into the `Overall_Status` job (it `needs:` the real jobs and reports one required122 check). A workflow that must be **required** cannot use a `paths:` filter — a filtered-out run reports123 no status and blocks the merge queue; rely on Smart CI + `Overall_Status` instead.124125### Add / change a custom action126* Composite `action.yml` under `.github/actions/<name>/`. Declare `inputs` (with `description`,127 `required`, `default`) and `runs: using: composite`. Every `run` step needs an explicit `shell:`.128* Reference untrusted input via `env:` inside the step, not inline interpolation.129* Update [custom_actions.md](../../../docs/dev/ci/github_actions/custom_actions.md) if the action is130 user-facing.131132#### Choosing the implementation environment133* **Python is the default and preferred** implementation language for a custom action's logic. Use it for134 any action that does **not** need extensive access to the GitHub (Actions) API — file/artifact135 handling, environment setup, packaging, running tools, parsing, etc.136 * If the action needs a `requirements.txt`, it must pin the **full dependency tree**, not just137 top-level packages. Generate it from a clean environment with `pip freeze`:138 ```bash139 python3 -m venv /tmp/act-env && . /tmp/act-env/bin/activate140 pip install <top-level-deps> # only the packages you directly import141 pip freeze > .github/actions/<name>/requirements.txt142 ```143 This makes installs reproducible and pinned. Regenerate the same way whenever deps change.144* **Use JavaScript/TypeScript only when the action uses the GitHub (Actions) API extensively** — the145 Octokit/`@actions/*` toolkit gives first-class typed access to it. The bundled146 [`.github/actions/cache`](../../../.github/actions/cache) action is the reference example of a147 JS-based action.148* When in doubt, prefer Python and keep API interaction minimal.149150### Add / use a custom Docker image151Follow [docker_images.md](../../../docs/dev/ci/github_actions/docker_images.md): add a Dockerfile under152`.github/dockerfiles/{ov_build,ov_test}/<platform>/`, ensure a `Docker` job runs `handle_docker` with the153image path in `images:`, add `Docker` to consumers' `needs`, and set154`image: ${{ fromJSON(needs.docker.outputs.images).<group>.<name> }}`. When adding a new env-setup script,155add it under `category: docker_env` in `labeler.yml`, exclude it from `.dockerignore`, and bump156`.github/dockerfiles/docker_tag` (handle_docker prompts you).157158### Change a runner or container159Pick the pool from [runners.md](../../../docs/dev/ci/github_actions/runners.md); keep `container`160volumes/options (shared drive, sccache) consistent with sibling jobs. GPU → Docker + `self-hosted` label.161162## Validation before finishing163164* **YAML/lint**: run `actionlint` if available (`actionlint .github/workflows/<file>.yml`); otherwise165 sanity-check YAML parses. Mirror what `workflows_scans.yml` (CodeQL `actions` + semgrep) and166 `dependency_review.yml` enforce — those run on any `.github/workflows/**` change.167* **Pinning**: every third-party `uses:` is a full SHA + version comment.168* **Permissions**: workflow defaults to least privilege; extra scopes are job-scoped and justified.169* **Smart CI**: new component-specific jobs/steps are gated and `Smart_CI` is in `needs`.170* **Scope**: only intended files changed; no stray `docker_tag`/labeler/components edits unless required.171* Do **not** hand-edit any `*.lock.yml` — those belong to agentic workflows.172173## Pitfalls to check174175* **Unpinned or tag-pinned third-party action** — supply-chain risk; CodeQL/semgrep will flag it.176* **Untrusted input in `run:`** — `${{ github.event.* }}` interpolated into shell is an injection vector;177 route through `env:`.178* **Over-broad `permissions:`** — especially `write` scopes at workflow level.179* **Job that ignores Smart CI** — burns limited self-hosted/GPU capacity on unaffected PRs.180* **Smart CI condition mismatch across a dependency chain** — a step skipped by a Smart CI `if:` whose181 dependent step/job lacks the same condition runs against missing outputs/artifacts. Gate the whole182 chain consistently.183* **`paths:` filter on a required workflow** — filtered-out runs report no status and hang the merge184 queue; use Smart CI + `Overall_Status` instead of `paths:`.185* **Hard-coded `docker.io` / non-ACR image on `aks-*`** — pulls fail or hit rate limits; use the ACR186 mirror or `handle_docker` output.187* **Missing `Docker`/`Smart_CI`/`Build` in `needs`** — races or missing artifacts/inputs.188* **Editing a `job_*.yml` input contract** without updating every caller's `with:` block.189* **Docker env change without `docker_tag` bump** — the image won't rebuild; handle_docker fails the check.190* **Missing `timeout-minutes`** — a hung job can occupy a runner indefinitely.191* **GPU job without Docker** — required on persistent GPU runners.192193## Skill self-improvement194195Keep this skill and the CI docs in sync with reality. When a change reveals a new rule, pattern, or196footgun:197198* **New reusable workflow / custom action / Dockerfile convention** — note it under **Common tasks** and,199 if user-facing, in the matching page under `docs/dev/ci/github_actions/`.200* **New footgun** (a pinning/permission/Smart CI/Docker/cache mistake that bit you) — add it to201 **Pitfalls**.202* **A rule becomes obsolete** (a workflow removed, a runner pool renamed, a path moved) — update or203 remove the stale entry instead of leaving it.204* **Keep it concise** — prefer linking to the (updated) doc over duplicating detail; this file is a short205 actionable checklist, not a second copy of the documentation.206* This directory is reachable via both `.agents/skills/` and `.claude/skills/` (the former is a symlink to207 the latter), so a single edit updates both — do not create a duplicate copy.