# Adopting ANTfrastructure in a New Project

This repo is not only a set of Dockerfiles. It ships the build/run/automation
tooling that consuming projects import instead of copying: Windows container
builds (Stevedore), Linux container builds (Rancher Desktop / CI), the
planner–executor agentic loop, reusable PowerShell modules and bash libraries,
and CI composite actions.

This page is the checklist for wiring a **new** project to all of it.
BeschleunigerBallett is the reference consumer — when a detail here
is ambiguous, read how that repo does it.

## 0. Add the submodule

```bash
git submodule add https://github.com/Kataglyphis/ANTfrastructure.git third_party/ANTfrastructure
git submodule update --init --recursive
```

Everything below assumes that path. Consumers pin a commit like any other
submodule; bump the pin and the consuming change in the same commit, and push
ANTfrastructure `develop` **before** the consumer, because CI resolves composite
actions at `@develop` (§ 6 says why not `@main`).

## Submodule maintenance

### Bumping the pin

The family moves gitlinks with Renovate run as a local CLI
([`dependency-updates.md`](dependency-updates.md)): `renovate-local.sh`, which a
consumer calls through the wrapper template in `shared/linux/templates/`, reports
what is behind, and `--apply` moves the gitlink. By hand, name the path and leave
`--recursive` out:

```bash
git submodule update --remote --merge third_party/ANTfrastructure
```

A bare `git submodule update --remote` is forbidden in this family (`AGENTS.md`
§ Dependency Updates), and `--remote` follows the `branch =` that `.gitmodules`
declares for the submodule. Commit the resulting pointer change together with the
consuming change, and push ANTfrastructure `develop` **first** — CI resolves
composite actions at `@develop`.

### Putting every checkout on its branch

`git submodule update` leaves each submodule on a detached HEAD at its recorded
commit. To work on the latest tip of every repo instead, run this from the
superproject:

```bash
bash third_party/ANTfrastructure/linux/scripts/git-sync-branches.sh --dry-run   # the plan
bash third_party/ANTfrastructure/linux/scripts/git-sync-branches.sh             # do it
```

It checks out the superproject's default branch, then walks every submodule,
parents before children, and checks out the `branch =` that `.gitmodules`
declares for it, or the remote's default branch when none is declared. It only
fast-forwards: a submodule with uncommitted changes is skipped (`SKIP`), and a
local branch that has diverged from its remote fails the run (`FAIL`) while the
rest still syncs.

- **Every submodule moves, third-party ones included** (owner decision
  2026-10-07). To hold a third-party library at a release, declare that release
  branch as its `branch =`. `--owned-only` leaves every submodule hosted under
  another owner than the superproject's (`github.com/nlohmann` vs
  `github.com/Kataglyphis`) at its recorded commit (`PIN`).
- **A submodule inside a third-party submodule cannot be recorded** by any repo
  you own, so after a run it stays listed as moved until that upstream bumps it.
- **It moves working trees, never gitlinks.** It ends by listing every submodule
  that is no longer at its recorded commit. The next `git submodule update`, or a
  git GUI that runs one, puts them back. To keep them, commit the gitlinks in each
  parent, innermost repo first, through the pin bump above.
- A submodule that a newer parent commit **adds** is not initialised; run
  `git submodule update --init <path>` for it.

### Resolving a submodule conflict on merge

Merges that touch the pin from both sides leave `third_party/` paths unmerged,
and `git checkout --theirs` alone does not resolve them: the conflict is over
*which commit the superproject records*, not over file contents.

The situation this arises in: two branches whose histories diverged far enough
that you want one side wholesale. Set the merge up so it stages nothing
automatically, then resolve:

```bash
git fetch origin
git checkout main
git reset --hard origin/main

# take develop's content, but stop before committing
git merge --allow-unrelated-histories --no-commit -X theirs develop
```

```bash
# what is actually unmerged
git diff --name-only --diff-filter=U

# take the incoming side's commit for every conflicted submodule
for p in $(git diff --name-only --diff-filter=U | grep '^third_party/' || true); do
  echo "Resolving submodule conflict: $p"
  git submodule update --init --recursive "$p" || true
  git checkout --theirs -- "$p"
  git add "$p"
done

# then the ordinary file conflicts
git checkout --theirs -- .
git add -A
git commit

# and materialize the commits that were just recorded
git submodule update --init --recursive
```

Run `git submodule update --init --recursive` **after** committing as well as
before. Staging the pointer does not check the submodule out at that commit, so
skipping it leaves a working tree that does not match what you just recorded.

> `-X theirs` on the merge itself resolves file contents but still leaves
> submodule pointers conflicted. The loop above is the part that is easy to
> forget, and the resulting "resolved" merge silently pins the wrong commit.

### Recovering a broken submodule checkout

When a clone left submodules half-initialised — wrong commit, empty directory,
or a `.git` file pointing nowhere — reset them rather than deleting the tree:

```bash
git submodule deinit -f --all
git submodule update --init --recursive
```

`deinit -f` discards local submodule working trees, so commit or stash anything
inside them first.

### Long paths (Windows consumers)

This repo's nesting plus a deep build directory exceeds `MAX_PATH` quickly.
Beyond the host-level `LongPathsEnabled` setting in
[`windows-host-setup.md`](windows-host-setup.md), git needs telling separately:

```bash
git config --global core.longpaths true
```

Without it, clone or checkout fails with `Filename too long` on files that are
perfectly legal for the filesystem.

### Resetting to a clean tree

```bash
git clean -fdx
git reset --hard
```

`-x` also removes ignored files — build outputs, `.venv`, cached toolchains.
That is usually the point, but it means a rebuild from cold.

### `git pull` hanging in PowerShell

An SSH remote needs the agent running as a Windows service; without it the
client waits on a passphrase prompt nothing is displaying:

```powershell
Start-Service ssh-agent
ssh-add "$env:USERPROFILE\.ssh\id_ed25519"
```

`Set-Service -Name ssh-agent -StartupType Automatic` makes it stick.

### Shallow fetches

Full history of every submodule is rarely needed on a build agent:

```bash
git fetch --depth=1 origin <branch>
git reset --hard FETCH_HEAD
```

## 1. The one file that cannot live here

Each consumer needs a tiny bootstrap that *finds* this submodule, since it runs
before anything upstream is importable. Copy
[`shared/windows/templates/Resolve-BuildModule.ps1`](../shared/windows/templates/README.md)
to `scripts/windows/Resolve-BuildModule.ps1` and adjust
`$script:RepoRootRelativeToHere` if the script does not sit exactly two
directories below the repo root. It resolves a module
name to
`third_party/ANTfrastructure/windows/scripts/modules/<Name>.psm1`
first, then a local `modules/` fallback beside itself, and throws with both
paths if neither exists.

Copy the template, do not re-author it. All four consumers had written this
file independently and they had drifted — different search orders, different
error text, one missing `-Global` on the import.

That preference order is the whole contract: **put reusable modules upstream
and they win automatically**; keep only genuinely project-specific modules in
the local fallback directory. If a second consumer needs it, it belongs here
instead — that test is what moved `WindowsTesting.Common` and
`WindowsClang.Common` upstream on 2026-08-11.

Bash consumers have the same problem and the same answer: copy
[`shared/linux/templates/antfrastructure.sh`](../shared/linux/templates/README.md)
to `scripts/linux/lib/antfrastructure.sh` (or `scripts/lib/` in a flat Flutter
repo), adjust `KATAGLYPHIS_REPO_ROOT_RELATIVE`, and declare it as
`antfrastructure-sh` in `.antfrastructure-shared.manifest` so the drift gate
watches it. It resolves paths from `${BASH_SOURCE[0]}`, never from the caller's
working directory, and its failure message names the fix.

**A consumer with no submodule is supported.** The bootstrap takes
`$ANTFRASTRUCTURE_DIR` first (a container mounts the workspace somewhere else
than the host, so an explicit answer always wins), then
`third_party/ANTfrastructure`, then a plain sibling clone at
`<repo>/antfrastructure-tools`. Its error text follows the same fact: it offers
`git submodule update` only when `.gitmodules` actually names that path, and
otherwise tells you to clone or to export `ANTFRASTRUCTURE_DIR`.

## 2. Windows container builds (Stevedore)

Image: `ghcr.io/kataglyphis/kataglyphis_beschleuniger:winamd64` (clang-cl,
CMake, Ninja, Vulkan SDK, Rust, sccache preinstalled).

Import `WindowsContainerBuild.Reuse` through the resolver and build on its
functions rather than re-implementing the pattern:

| Function | Purpose |
|---|---|
| `Resolve-DockerExe` | Find Stevedore's `docker.exe` (nerdctl is not viable on Windows) |
| `Get-ContainerIsolationArgs` | Process vs Hyper-V isolation, CPU/memory args |
| `Get-ReusableBuildContainer` | Reuse/start/recreate one long-lived build container; recreates on image change |
| `Copy-IntoBuildContainer` / `Copy-FromBuildContainer` | tar-pipe transfers with mandatory exclusion support |
| `Initialize-ContainerPwsh` | Ensure PowerShell 7 exists inside the container |
| `Remove-StaleContainerSources` | Prune deleted sources on reuse (tar never deletes) |
| `Test-BuildArtifactsDelivered` | Fail when a "green" build produced or delivered nothing |
| `Test-ContainerBindMount` / `Remove-BuildContainerSafe` | Bind-mount probe; wcifs-tolerant removal |
| `Wait-ContainerExit` | Trust the container's state, not the docker client's exit code, when the CLI drops its pipe mid-run (needs a named run, never `--rm`) |

Read [`windows-container-build-performance.md`](windows-container-build-performance.md)
before designing your flow — it documents both transports and their setup, why
the build tree must not live on a named volume, the Windows path limit that
silently truncates tar transfers, and the container-reuse measurements.

Three rules that cost real debugging time to learn:

- **Mount/stream to the same in-container path under every transport.** CMake
  bakes absolute paths into `CMakeCache.txt` and rejects a cache generated
  elsewhere.
- **A green build is not proof of delivery.** Always end with
  `Test-BuildArtifactsDelivered`; both "built nothing" and "delivered nothing"
  have happened silently.
- **A red docker client is not proof of failure.** The CLI drops its pipe
  mid-run on this host while the container keeps building. Name the run, leave
  `--rm` off, and take the verdict from `Wait-ContainerExit`.

## 3. Linux container builds (Rancher Desktop / CI)

Image: `ghcr.io/kataglyphis/kataglyphis_beschleuniger:latest`.

Local runs go through one driver, so the image tag, the mount layout, the git
safe.directory and the Git Bash path-mangling escape are answered in one place
rather than re-typed per repo:

```bash
bash third_party/ANTfrastructure/linux/scripts/run-in-ci-image.sh . -- \
  bash scripts/linux/cmake-configure-build.sh --preset <preset> \
       --build-dir /tmp/build
```

The image, the mount layout, safe.directory, the engine and the Git Bash escape
are the driver's, and
[`shared-script-libraries.md` § `run-in-ci-image.sh`](shared-script-libraries.md#run-in-ci-imagesh--run-a-command-in-the-ci-image)
owns the explanation. It mounts the checkout and nothing else, so a cargo cache
that outlives the run (below) needs a plain `nerdctl run`/`docker run` with the
image from `ci-image-ref.sh` and a named volume: the driver has no option for a
second mount (2026-09-25).

Three constraints worth internalising:

- **The CMake build directory must be container-native** (`/tmp/...`), not on
  the bind-mounted host tree: FetchContent's rename and cargo's temp-file
  cleanup both fail on that filesystem.
- **Persist cargo via a named volume** (`--cargo-cache-dir`, which
  `lib/cmake-build.sh` turns into `CARGO_HOME` and `CARGO_TARGET_DIR`). The
  image's own `CARGO_HOME` is writable
  ([`consumer-image-contract.md`](consumer-image-contract.md), promise 2), but it
  lives in the container and dies with it.
- **Never assume a tool is present because it is on your dev box.** `jq`, for
  instance, is *not* in the Linux image; `python3` is. A hard dependency on the
  former silently broke shader precompilation and left CI with no artifacts.

Prefer `linux/scripts/01-core/` helpers (logging, retry, downloads with SHA
verification, uv/python env, parallelism) over new implementations.

## 4. The agentic loop

Reusable core, already written:

- PowerShell: `windows/scripts/modules/WindowsAgenticLoop.Common.psm1`
- Bash: `linux/scripts/lib/agentic-loop.sh`
- Default task prompts: `shared/agentic-loop/prompts/{planner,refactor-planner,executor}.md`
  — the single source both platforms read. Never hard-code prompt text in a
  consumer wrapper; that is precisely how the two platforms drifted apart.

Start from the copy-and-edit templates in
[`shared/agentic-loop/templates/`](../shared/agentic-loop/templates/README.md)
(config with every project-specific field marked `TODO`, plus both runner
wrappers). A consumer supplies four things:

1. **`BACKLOG.md`** with the checkbox protocol: `- [ ]` actionable, `- [b]`
   blocked (skipped, and excluded from the pending count so a blocked-only
   backlog lets the planner run again), `- [x]` completed (pruned; history
   lives in git).
2. **A config JSON.** Shape (see the reference consumer's
   `scripts/agentic-loop/AgenticLoop.config.json`): `engine`, per-engine model
   and prompt settings under `engines.*`, cadences and timeouts under
   `intervals.*`, per-platform `buildMatrix.{windows,linux}` entries
   (`name`/`sanitizer`/`buildDir`/`buildType`/`testCommand`), `build.*`
   commands, `git.*` auto-commit settings, `backlog.*` policy, `logging.logDir`.
3. **Thin runner wrappers** — `Invoke-AgenticLoop.ps1` / `.sh`. These load the
   config, resolve the module/library, and call `Invoke-AgenticLoop` /
   `run_agentic_loop`. Build configs and prompts both default from the config
   and the shared prompt files, so the wrappers stay tiny.
4. **Project role-prompt overlays** — `scripts/agentic-loop/prompts/planner-overlay.md`
   and `executor-overlay.md`, wired via the config's top-level `promptOverlays`
   block. Write only your project's delta. The loop composes
   `shared/agentic-loop/system-prompts/<role>.md` + your overlay once and
   delivers that one text to both engines (the opencode engine needs opencode
   **v2**, [install](windows-agentic-loop.md#opencode-v2)): to `claude` via
   `--append-system-prompt-file`, and to `opencode` by GENERATING
   `.opencode/agents/<role>.md`, which is its only role-prompt channel. Add
   `.opencode/agents/` to `.gitignore` — the loop warns if you have not. A
   tracked, hand-edited copy of that file is how one consumer ended up with two
   full copies of the role prompt that had drifted 271 lines apart, the stale
   one having lost the executor's incident narrative, its `timeout: 600000`
   guidance and the `- [b]` commit step.

API reference: [`windows-agentic-loop.md`](windows-agentic-loop.md).
Build-matrix semantics and sanitizer env handling:
[`agentic-loop-build-matrix.md`](agentic-loop-build-matrix.md).

**Operational lesson worth inheriting:** an autonomous loop does not watch CI,
and it auto-commits with `git add -A`. Expect it to keep committing over a red
pipeline, and do not run interactive work in the same tree without checking
whether the loop is live.

## 5. Application launchers

`linux/scripts/lib/app-runner.sh` provides argument parsing
(`--exe-name/--build-dir/--build-type`), executable discovery with a bounded
fallback search, `LD_LIBRARY_PATH` export, and hooks
(`app_runner_post_vulkan_hook`, `app_runner_env_hook`,
`APP_RUNNER_ENABLE_SHADER_CLEAN`). Consumers keep only per-profile wrappers
holding defaults and hooks.

## 6. CI

Composite actions live in [`.github/actions/`](../.github/actions/README.md)
and are referenced from a consumer workflow as
`Kataglyphis/ANTfrastructure/.github/actions/<name>@develop`.

**`@develop`, not `@main`** (owner directive 2026-09-25). Work lands on
`develop`; `main` is a release branch that lags it, and its `versions.env` still
names the retired `:latest-cross`, whose tags were deleted on 2026-09-29, so an
action at `@main` fails at `docker pull`. The same ref applies to the reusable workflows below.

All fifteen, with every input and output, are listed once in
[`.github/actions/README.md`](../.github/actions/README.md) — that page is the
list, and a table here would be a second copy of it to keep in sync. They
replace the hand-rolled `docker run` blocks that otherwise accumulate (in the
reference consumer, twenty-plus copies across two workflows), and they include
`deploy-over-ftp`, the one FTP publish policy for the family
([`ftp-deploys.md`](ftp-deploys.md)).

**One ordering rule does not live in that page, because it is about your
workflow rather than about an action:** on a Windows runner, `set-docker-data-root`
runs **FIRST** (`prepare-windows-container-host` runs it before its own pull). It
moves docker's data root to the big `D:` drive — the ~54 GB
image does not fit on a stock `windows-2025` runner's `C:`, and a pull without
the move dies late with `hcsshim::ImportLayer 0x70` (measured; see
[windows-build-resources.md](windows-build-resources.md)).

Four lanes are reusable workflows rather than actions, each called with
`uses: Kataglyphis/ANTfrastructure/.github/workflows/<name>.yml@develop`:
`lint-gates.yml` (the consumer lint gates, § 9), `submodule-pins.yml` (the pin
suite, § 9), the Python pair `python-ci-linux.yml` / `python-ci-windows.yml`
(§ 9), and `container-ci-windows.yml`, the Windows container lane. Its caller
names a build script, the script's arguments and the directory the product lands
in (`dist/windows-x64` or `dist/windows-arm64` in every caller, with the DLL
closure the product needs); `target-arch: arm64` adds the arch gate and a run on
`windows-11-arm`
([`windows-cross-builds.md` § Consumer cross lanes](windows-cross-builds.md#consumer-cross-lanes-container-ci-windowsyml)).
Since 2026-09-26 the `windows-x64.yml` and `windows-arm64-cross.yml` of OxidANT,
AccelerANTgine and BeschleunigerBallett call it. A caller hands its own secrets
to the container through the `CONTAINER_SECRET_ENV` secret, one `KEY=value` per
line, which reaches docker as an `--env-file`.

Because actions resolve at `@develop`, a consumer workflow change that depends on
an action change requires the ANTfrastructure push to land first.

### Workflow file names and display names

Owner decision 2026-09-24, for every repository in the family, this one
included.

- **Files are kebab-case, and a build gets one file per platform and arch:**
  `linux-x64.yml`, `linux-arm64.yml`, `windows-x64.yml`,
  `windows-arm64-cross.yml`, `android.yml`, `web.yml`. A lane every repo has
  carries the same file name everywhere: `lint-gates.yml`, `submodule-pins.yml`,
  `codeql.yml`, `docs.yml`. A reusable workflow that lives in a consumer is
  `reusable-<platform>.yml`.
- **Display names read `<Platform> <Arch> · <what>`** for a build (`Linux x64 ·
  preflight + mutation gate`) and `<Area> · <what>` for anything else (`Docs ·
  stale check`, `GHCR · cleanup`). The separator is U+00B7, the middle dot. The
  shared lanes are `Lint gates`, `Submodule pins`, `CodeQL` and `Docs · …`; a
  consumer's reusable workflow is `<Platform> · reusable build`.
- **This hub's reusable workflows keep their file names.** Every consumer calls
  them as `Kataglyphis/ANTfrastructure/.github/workflows/<file>@develop`, so a
  rename breaks the fleet at once. Only their display names changed, and they
  end in `(reusable)`: `Docs · build (reusable)`, `Lint gates (reusable)`,
  `Python CI · Linux (reusable)`, `Python CI · Windows (reusable)`,
  `Submodule pins (reusable)`, and `Container CI · Windows (reusable)`, new on
  2026-09-25.
- **Splitting a Python lane per arch** needs no copied job:
  `python-ci-linux.yml`'s `arches` input picks the rows
  ([`python-ci.md`](python-ci.md#one-arch-per-caller-the-arches-input)).

The rename across the fleet, old name to new:

| Repository | Before | After |
|---|---|---|
| ANTfrastructure | `ubuntu26.04.yml` | `linux-x64.yml` |
| ANTfrastructure | `windows-scripts.yml` | `windows-x64.yml` |
| OxidANT | `rust_ubuntu26_04.yml` | `linux-x64.yml` + `linux-arm64.yml`, sharing `reusable-linux.yml` |
| OxidANT | `rust_windows2025.yml` | `windows-x64.yml` |
| AccelerANTgine | `linux_run.yml` | `reusable-linux.yml` |
| AccelerANTgine | `linux_run_x86.yml` | `linux-x64.yml` |
| AccelerANTgine | `linux_run_arm.yml` | `linux-arm64.yml` |
| AccelerANTgine | `windows_run.yml` | `windows-x64.yml` |
| OmniAccelerANT | `dart_on_native_linux.yml` | `linux-x64.yml` + `linux-arm64.yml`, sharing `reusable-linux.yml` |
| OmniAccelerANT | `dart_on_native_windows.yml` | `windows-x64.yml` |
| OmniAccelerANT | `dart_build_android_app.yml` | `android.yml` |
| OmniAccelerANT | `dart_on_web_linux.yml` | `web.yml` |
| BeschleunigerBallett | `Linux.yml` | `reusable-linux.yml` |
| BeschleunigerBallett | `Linux_x86.yml` | `linux-x64.yml` |
| BeschleunigerBallett | `Linux_arm.yml` | `linux-arm64.yml` |
| BeschleunigerBallett | `Windows.yml` | `windows-x64.yml` |
| OrchestrANT, WebDavClient | `windows-2025.yml` | `windows-x64.yml` |
| WebDavClient | `ubuntu-26.04-amd64-arm64.yml` | `linux-x64.yml` + `linux-arm64.yml` (2026-09-25, through `arches`) |
| OrchestrANT | `ubuntu-26.04-amd64-arm64.yml` | kept for now; the same split is open to it (`arches`) |
| jotrockenmitlocken | `dart.yml` | `web.yml` |
| ANThology | `dart.yml` | `docs.yml` |
| DocumANTation | `docs-pages.yml` | `docs.yml` |

A hub page that cites a consumer workflow with a LINE number from before the
rename (the table in [`ftp-deploys.md`](ftp-deploys.md)) is a dated record and
keeps the old name; this table translates it.

## 7. Certificates / packaging (Windows)

`windows/scripts/certificates/` holds MSIX certificate generation and import
(`README.md` there). The `WindowsMsix.Common`, `WindowsMsix.Signing` and
`WindowsWebDav.Common` modules drive it. To sign a package, call
`Invoke-MsixPackage -Sign -SigningRoot <repo root>`: it signs with the `*.pfx`
it finds in that directory (not recursively; the family keeps it there,
gitignored) and `MSIX_PFX_PASSWORD`. Since 2026-09-25 `-Sign` without
`-SigningRoot` throws before anything is staged.

An MSI built from the project's own `.wxs` goes through `Invoke-MsiPackage`
(`WindowsMsix.Common`, since 2026-10-05). It runs `wix build` with `Version`, `ExeSource`,
`LicenseRtf`, `ProductName` and `Manufacturer` as preprocessor variables. With
`-PayloadFiles` it also writes a fragment for `<ComponentGroupRef Id='PayloadFiles'/>`,
one component per file. OxidANT is the first caller; a CMake repo builds its MSI
through CPack's WiX generator instead.

What a package carries is the consumer's to stage, with two hub helpers. The
DLL closure of a product comes from `Copy-PeImportClosure`
(`WindowsCrossBundle.Common`, [`windows-cross-builds.md`](windows-cross-builds.md)).
ONNX Runtime comes from `WindowsOrtPayload.Common`: stage the chain copy with
`Copy-ChainOrtBeside`, then prove the payload with `New-OrtProvenPayload` or
`Assert-ChainOrtTree`
([`onnxruntime-single-source.md` § The shared Windows glue](onnxruntime-single-source.md#the-shared-windows-glue)).

The WebDAV downloader that fetches those signing certificates in CI (rather than
committing them) is **not** Windows-specific and no longer lives there: it is
`linux/scripts/01-core/download-webdav-files.py`, with a shim at the old
`windows/scripts/certificates/download_webdav_files.py` path because the
PowerShell module resolves it relative to itself. A Linux lane calls it through
`webdav_download_tree`
([`shared-script-libraries.md`](shared-script-libraries.md#01-corewebdav-downloadsh)).
Both halves install the client at `WEBDAVCLIENT_REF` from `versions.env`.

## 8. Calling conventions (what every consumer looks like)

Eight repos consume this one (`.github/consumers.json`). The shapes below are
what they converged on; a new consumer that follows them is immediately legible
to anyone who has read another. Recorded 2026-08-11 after measuring the seven
consumers of that day (WebDavClient joined the list later), because until then
the convention was folklore and had drifted.

**Windows entry point** — `<scripts>/windows/Build-Windows.ps1`, PascalCase
`Verb-Noun` like every other PowerShell file. It must:

```powershell
#requires -Version 7.0          # every module here declares it; pwsh, never powershell
. (Join-Path $PSScriptRoot 'Resolve-BuildModule.ps1')
Import-BuildModule @('WindowsScripts.Shared', 'WindowsBuild.Common', ...)
```

Run the app with a sibling `Start-Windows.ps1`. Project-specific modules go in
`<scripts>/windows/modules/`, which the resolver checks after this repo.

**The verb carries the meaning**, and the family uses three of PowerShell's
approved ones with narrower senses than the approved-verb list gives them:
`Start-` launches the built application, `Invoke-` runs a build step or a tool,
and `Build-` produces artifacts. `Build-Windows.ps1` builds, `Start-Windows.ps1`
runs, `Invoke-Lint.ps1` lints. **PowerShell lives under `scripts/windows/`**
regardless of which lane it drives — a `.ps1` that starts a *Linux* container is
still PowerShell and still belongs there, because the question a reader asks is
"which shell do I need", not "which OS does it target".

**Bash entry points** — `set -euo pipefail`, resolve the script's own directory,
then source a per-repo bridge that pulls in the bootstrap of § 1, and reach a hub
file through it rather than through a `../../third_party/...` literal:

```bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/common.sh"          # sources lib/antfrastructure.sh
antfrastructure_source linux/scripts/lib/<lib>.sh
```

Long flags are `--kebab-case value`. A wrapper around one of the `lib/*.sh`
drivers should be ~30 lines: source the library, set the project's defaults,
call its `*_main`. `BeschleunigerBallett/scripts/linux/run-ctest.sh`
is the canonical example.

**Shell safety** — the five bug classes in this repo's `AGENTS.md`
(§ *Shell safety conventions*) apply to consumer scripts too. Every one of them
falsified or killed a real build here; they are not style preferences.

**Directory layout is uniform across the six consumers that have a Windows
lane** (BeschleunigerBallett, OmniAccelerANT, OrchestrANT, AccelerANTgine,
OxidANT, WebDavClient; normalised 2026-08-11, WebDavClient checked 2026-09-25):
lowercase `scripts/`, with `scripts/windows/`, `scripts/linux/` and — where the
agentic loop is wired up — `scripts/agentic-loop/`, plus `scripts/windows/modules/`
for a repo with project-specific modules and `scripts/windows/container/` for
scripts that run inside the Windows image rather than on the host (OxidANT's
`Invoke-StevedoreBuild` callees). jotrockenmitlocken, a Linux-only
Flutter repo, keeps a flat `scripts/`, with the bootstrap at `scripts/lib/` and
that path declared in `.antfrastructure-shared.manifest`. ANThology has no hub
submodule and no bootstrap: its `scripts/lib/find-hub.sh` looks for a hub
checkout instead. Two repos used `Scripts/` + `Scripts/Windows/` until that
sweep. Use lowercase in a new consumer; there is no per-repo casing rule to look
up any more.

**Bash filenames still differ**, but not the way this note used to claim.
Measured 2026-09-14: kebab-case everywhere except OrchestrANT, where the four
`ci_*.sh` wrappers mirror the hub's own snake_case drivers under
`linux/scripts/02-toolchain/python/` by name and `benchmarks/run_benchmarks.sh`
kept its name from the hub's llm-stack; its other scripts are kebab. The old
wording said "snake_case in the rest" and was wrong about OxidANT and
AccelerANTgine — both are kebab. That one is
left alone deliberately: unlike a directory rename it buys no structural
consistency, and renaming every script would churn history for a purely lexical
preference. Match the repo you are in.

`set -euo pipefail` applies to **entry points**, not to sourced libraries — a
file-scope `set -e` in a library leaks into whoever sources it. This repo's own
libraries follow the same rule (`AGENTS.md` § Development Rules).

### Reach for these before writing a helper

Every row was first written locally in a consumer and later found here, usually
in a better form and twice with a bug the local copy did not have. The list
moved here from OxidANT's `AGENTS.md` on 2026-10-05 (CON55), so each consumer
links one list instead of keeping its own.

| Need | Use | Defined in | Not |
| --- | --- | --- | --- |
| `docker.exe` discovery (Stevedore) | `Resolve-DockerExe` | [`WindowsContainerBuild.Reuse.psm1`](../windows/scripts/modules/WindowsContainerBuild.Reuse.psm1) | a hand-rolled candidate list |
| `--isolation process` and friends | `Get-ContainerIsolationArgs` | same file | inline flags |
| Container teardown | `Remove-BuildContainerSafe` | same file | `docker rm -f`, which misses the wcifs teardown lock |
| Bind-mount probe, artifact delivery | `Test-ContainerBindMount`, `Test-BuildArtifactsDelivered` | same file | assuming a green build delivered something |
| Stage, manifest, pack and sign one MSIX | `Invoke-MsixPackage` | [`WindowsMsix.Common.psm1`](../windows/scripts/modules/WindowsMsix.Common.psm1) | a local makeappx/assets/tokens/pack sequence |
| The version to stamp a package with | `Get-PackageVersion` | same file | reading `VERSION.txt` inline, with a different fallback per step |
| SDK tools (makeappx, signtool) | `Resolve-WindowsSdkToolPath` | same file | `Get-ChildItem -Recurse` over the Kits tree |
| Manifest tokens, XML escaping, placeholder PNGs | `Expand-XmlTemplateTokens`, `ConvertTo-XmlSafeText`, `New-TransparentPng` | same file | `-replace`, or a local redefinition |
| Config access | `Get-OrDefault`, `Get-ConfigValue` | [`WindowsConfig.Common.psm1`](../windows/scripts/modules/WindowsConfig.Common.psm1) | copies |
| Build logging and steps | `New-BuildContext`, `Invoke-BuildStep`, `Invoke-BuildExternal`, `Write-BuildLog*` | [`WindowsBuild.Common.psm1`](../windows/scripts/modules/WindowsBuild.Common.psm1) | ad-hoc `Write-Host` wrappers |
| Tool guards, workspace paths, a hub pin from `versions.env` | `Assert-Command`, `Resolve-WorkspacePath`, `Get-ANTfrastructurePin` | [`WindowsScripts.Shared.psm1`](../windows/scripts/modules/WindowsScripts.Shared.psm1) | a second implementation, or a `versions.env` regex |
| Logging inside a container | `Start-ContainerLog`, `Write-ContainerLog`, `Invoke-ContainerLoggedCommand` | [`WindowsContainerLog.Common.psm1`](../windows/scripts/modules/WindowsContainerLog.Common.psm1) | a `Say`/`Run-Logged` pair per script |
| CI version stamping (bash) | `version_util.sh --github-env`, `--resolve-ci`, `--normalize` | [`version_util.sh`](../linux/scripts/02-toolchain/rust/version_util.sh) | re-reading `VERSION.txt` or `Cargo.toml` yourself |
| In-container cargo steps | `cargo_debug.sh`, `cargo_release.sh`, `cargo_test.sh`, `cargo_coverage.sh`, `cargo_fmt_clippy.sh` (`CARGO_CLIPPY_ARGS`), … | [`02-toolchain/rust/`](../linux/scripts/02-toolchain/rust/) | inline cargo invocations |
| Publishing a GitHub release: `v<version>` from `pubspec.yaml`, `Cargo.toml` or `VERSION.txt`, the CHANGELOG section as notes, a `SHA256SUMS` | `publish-github-release.sh --check` / `--assets` | [`publish-github-release.sh`](../linux/scripts/06-packaging/publish-github-release.sh) | a per-repo release script |
| Linux packaging (tar, deb, AppImage, Flatpak) | `package_archive.sh` | [`package_archive.sh`](../linux/scripts/06-packaging/package_archive.sh) | bespoke packaging |
| CI job plumbing | `prepare-linux-ci-host`, `run-in-linux-container`, `run-in-windows-container`, `clone-into-short-path`, `cleanup-disk-space`, `assert-docker-disk-space` | [`.github/actions/`](../.github/actions/README.md) | hand-written `docker run` blocks |
| Linting workflows locally | `lint-workflows.sh <root>` (pinned, SHA-verified actionlint) | [`lint-workflows.sh`](../linux/scripts/lint-workflows.sh) | bootstrapping your own |
| The agentic loop | config and runner templates | [`shared/agentic-loop/templates/`](../shared/agentic-loop/templates/README.md) | writing one from scratch |
| Bash helpers: logging, retry, SHA-checked downloads, parallelism | `logging.sh`, `downloads.sh`, `parallelism.sh`, … | [`linux/scripts/01-core/`](../linux/scripts/01-core/) | new implementations |

Two caveats:

- **A module's own imports are private to it.** `WindowsBuild.Common` importing
  `WindowsScripts.Shared` does not re-export it to you. Import each module you
  call into directly, or the first run of that code path stops with "command not
  found".
- **Change the hub, not a local copy.** The same owner may edit this repo, but
  every consumer reads it: change it here, push, then move the consumer's pin.

### The rest of the naming rules, in one place

Small decisions that were folklore until 2026-09-15. Each is one line because
each is genuinely one line; the point is that the answer exists and is findable.

- **Version file.** Where a repo has one, it is `VERSION.txt` at the repo root —
  the default every entry point of
  [`02-toolchain/rust/version_util.sh`](../linux/scripts/02-toolchain/rust/version_util.sh)
  falls back to. A repo whose ecosystem already carries the version (a
  `pyproject.toml`, a `pubspec.yaml`, a workspace `Cargo.toml`) does not add a
  second one.
- **CHANGELOG.** A repo that publishes a package (PyPI, pub.dev, crates.io)
  keeps `CHANGELOG.md` **at the package root**, because that is what the registry
  renders. Applications rely on git history; do not add an unmaintained file to
  look tidy.
- **Instruction file.** `AGENTS.md` is the **only** instruction file. Anything an
  agent-specific tool wants at its own path (`.github/copilot-instructions.md`,
  `CLAUDE.md`, a `.cursorrules`) is a two-line pointer to `AGENTS.md`, never a
  second copy — the copies drift, and the one nobody re-reads wins.
- **Sphinx layout.** Consumers keep their Sphinx sources under `docs/source/`,
  which is `docs-build.sh`'s default. This hub and DocumANTation are the
  exception: they keep `docs/conf.py` beside the pages.
- **Package names.** A package name is unique within its registry. Reusing one
  *across* registries is allowed only when the two artifacts are the same
  capability in two languages, and the reuse is recorded here. There is exactly
  one: `kataglyphis_inference` is OxidANT's `crates/inference` (an ONNX Runtime
  Rust crate) and AccelerANTgine's staged Python binding around the same C++
  inference path. Neither is published, the registries are disjoint, and they
  are two faces of one capability — so both keep the name. A third use, or a
  publish, reopens this.
- **Environment-variable prefixes.** `KATAGLYPHIS_*` for build/tree knobs shared
  by every family repo, `ANTFRASTRUCTURE_*` for knobs that configure this hub's
  own machinery (the pin suite, the shared-config sync), `AGENTIC_*` for the
  agentic loop, and the repo's own name for consumer-private knobs
  (`ORCHESTRANT_*`). The hub's registry gate
  ([`code-quality-tooling.md` § `env-knobs`](code-quality-tooling.md#env-knobs--a-stale-allow-row-always-fails))
  grades the hub's own knobs against that vocabulary.
- **Workflow files.** Kebab-case, one per platform and arch, display names
  `<Platform> <Arch> · <what>`: § 6,
  [Workflow file names and display names](#workflow-file-names-and-display-names).

### The pre-commit hook, by reference

A consumer gets the whole lint aggregator on every commit with one config line
and no copied file:

```bash
git config core.hooksPath third_party/ANTfrastructure/shared/linux/templates/git-hooks
```

That directory holds a single `pre-commit` that `cd`s to the toplevel and runs
`run-lint-gates.sh` over it. It is deliberately **not** a
`shared-assets.manifest` row: a copied hook is a fork nobody re-syncs, and a
stale hook is invisible because it keeps passing. The hub's own hooks under
`linux/host-config/git-hooks/` are a different pair — they gate this
repository's internals and are not for consumers.

## 9. Quality gates

Every gate the hub runs over itself has a consumer-facing half; a consumer wires
four things and gets all of it:

1. **The shared-asset manifest.** `.antfrastructure-shared.manifest` at the
   consumer root declares which hub-owned files the repo holds a copy of
   (configs, the two bootstrap templates); `sync-shared-config.sh --repo-root .
   --check` is the drift gate, `--write` refreshes. The registry of asset ids and
   the rules are in [`../shared/config/README.md`](../shared/config/README.md).
2. **The lint aggregator.** `bash third_party/ANTfrastructure/linux/scripts/run-lint-gates.sh .`
   runs shellcheck, actionlint + CI image refs, gitleaks (with a self-test),
   ruff, the manifest drift check and the consumer pin-forwarding check, all
   from pinned and SHA-verified binaries — see
   [`shared-script-libraries.md`](shared-script-libraries.md) § run-lint-gates.sh.
   Keep a ~5-line `scripts/linux/run-lint-gates.sh` wrapper so the dev-box
   command and the CI step are the same string, and call the reusable lane from
   CI: `jobs: lint: uses: Kataglyphis/ANTfrastructure/.github/workflows/lint-gates.yml@develop`
   (inputs: `exclude`, `submodules`, `hub-checkout` for a consumer without a
   submodule). Add `--ratchets` (the lane's `ratchets: true`) once the eight
   `<gate>.allow` freeze files are seeded and committed; that switches on the
   measurement gates (code size,
   complexity, dead functions, comment size, stdout returns, masked
   declarations, trailing conditionals, shellcheck warnings) over the consumer's
   own shell.
3. **The pin suite.** `Submodule.Pins.Tests.ps1` asserts every submodule sits at
   its recorded, remotely reachable commit; consumers call
   `uses: Kataglyphis/ANTfrastructure/.github/workflows/submodule-pins.yml@develop`
   (the AGENTS.md template's § 3 names the suite) instead of copying the job.
4. **Python repos** get the whole CI surface from the reusable
   `python-ci-linux.yml` / `python-ci-windows.yml` workflows —
   [`python-ci.md`](python-ci.md).

5. **The docs gates take `--root` too** (2026-09-15). `doc-links` runs in the
   `--ratchets` step — it has no freeze file, so it is safe the first time —
   and grades every tracked Markdown page, which is what finally puts a
   consumer's own `README.md` into the cross-reference graph. `doc-dupes` and
   `code-dupes` take `--root` as well, with budgets at `<root>/doc-dupes.allow`
   and `<root>/code-dupes.allow`, and stay off until a consumer seeds one: a
   duplication ratchet with no budget is red on its first run by construction.

Still hub-only: the versioned git hooks under `linux/host-config/git-hooks/`
`cd` into the hub layout and gate this repository's internals. Consumers get the
hook of § 8 instead, which runs the aggregator and nothing hub-specific.

## Checklist

- [ ] Submodule added; `Resolve-BuildModule.ps1` copied
- [ ] Entry points named and shaped as in § 8
- [ ] Windows build script built on `WindowsContainerBuild.Reuse`, ending in a delivery check (where the consumer has a Windows lane)
- [ ] Linux build uses a container-native build dir and a cargo cache volume
- [ ] No consumer copy of anything that exists upstream (check before writing) — the manifest drift gate and the lint aggregator of § 9 are the mechanism
- [ ] `.antfrastructure-shared.manifest` declared; `sync-shared-config.sh --check` green
- [ ] `run-lint-gates.sh` wired as a wrapper and as the reusable `lint-gates.yml` lane; ratchets on with freeze files committed
- [ ] `submodule-pins.yml` lane called (any repo with a submodule)
- [ ] `core.hooksPath` pointed at `third_party/ANTfrastructure/shared/linux/templates/git-hooks` (§ 8)
- [ ] `BACKLOG.md` + loop config + thin runners in place, prompts left upstream
- [ ] Role prompts are overlays only; `.opencode/agents/` gitignored, never hand-edited
- [ ] Workflows call the composite actions, FTP publishes through `deploy-over-ftp`
- [ ] A Windows lane whose logic lives in one build script is a thin caller of `container-ci-windows.yml` (§ 6)
- [ ] Workflow files and display names follow § 6 (`linux-x64.yml`, `Linux x64 · …`)
- [ ] Consumer AGENTS.md links to these docs instead of restating them

### When the push is refused

`main` is usually protected, so the merge above cannot be pushed directly. Do
not force it — put the result on a branch and open a PR:

```bash
git push origin main || {
  git branch overwrite-main-from-develop
  git push origin overwrite-main-from-develop
}
```

### Repointing a remote

After a rename, a transfer, or moving from HTTPS to SSH:

```bash
git remote set-url origin git@github.com:<org>/<repo>.git
git remote -v
```
