# 11. CI stays in the repository but dormant; the pre-push gate is primary

**Status:** Accepted · 2026-08-31 · amended 2026-09-13 · amended 2026-09-17 · superseded 2026-09-21 (GitHub-hosted CI reawakened)

> **Amendment, 2026-09-21 (supersedes the conclusion below).** `test.yml` and
> `ci-lite.yml` now run on every push and pull request to `main`
> (`workflow_dispatch` also triggers them by hand); their `on:` blocks are the
> source of truth for what runs. The dormancy decision recorded in this ADR no
> longer holds for the GitHub-hosted workflows. The pre-push gate stays the
> primary check and CI is the backstop, so the reasoning below still explains
> why local verification leads; only "no workflow runs" has changed. The
> self-hosted-runner discussion in the 2026-09-17 amendment is unaffected.

> **Amendment, 2026-09-13 (ADR-0012, macOS only).** Linux and Windows support
> was removed. `package.json` now declares `os: ["darwin"]`, which npm enforces
> on the root package, so no non-macOS runner can `npm ci` this tree at all.
> Every workflow moved to a macOS runner, `test.yml`'s matrix lost its Ubuntu
> leg and its `windows-test` job, and `npm run gate:linux` and
> `pipeline/scripts/gate-linux.sh` are gone. Where the body below says a leg is
> reachable through `act` on Ubuntu, read it as history: the gap it describes no
> longer exists because the platform no longer does.

## Context

The repository carries four workflows. `test.yml` runs the full chain across a
macOS/Ubuntu matrix plus a separate `windows-test` job, `ci-lite.yml` runs a
single cheap Ubuntu job, `release.yml`
publishes on a version tag and `live-eval.yml` is a manual, budgeted evaluation.

None of the first three has executed since 2026-07-02. That day's last run took
318 seconds and passed; every attempt after it - five, the last on 2026-07-25 -
finished in about four seconds with zero steps recorded, which is the shape of a
quota block rather than a test failure. All three were then disabled from the
Actions UI. That stopped the noise but moved the fact out of the repository:
the files still declared `on: push` and `on: pull_request`, so anyone reading
them, forking them, or re-enabling the workflow inherited a promise the project
does not keep.

Meanwhile the gate did not disappear. `pipeline/scripts/pre-push-check.sh` runs
the canonical `npm test` chain, stamps the exact tree it verified, and refuses
any push whose tree carries no stamp. It is installed as `.git/hooks/pre-push`.
So the question was never "is this code verified" but "verified where, and what
does that leave uncovered".

There is no plan to run this on a server in the near term. There is a possible
later direction (an issue-triggered run that starts development automatically),
which argues for keeping the workflow files rather than deleting them.

## Decision

Keep every workflow file. Comment out the `push` and `pull_request` triggers in
`test.yml` and `ci-lite.yml`, leaving `workflow_dispatch` live, and state the
dormancy in the file header next to the triggers.

Treat `npm run gate` as the primary gate, not as a fallback, and close the parts
of the ci-lite job that had no local equivalent: `format:check` joins the `npm
test` chain, the install dry-run plus personal-data audit moves onto the
existing `smoke-install-leak-gate.sh` (which was already doing the same install
into a temp HOME, against a private copy of the pattern list that had drifted to
less than half of it), the scorecard gains the two dependency checks that lived
only in ci-lite - moderate advisories and `npm audit signatures` - and the Linux
leg is reachable as `npm run gate:linux` through `act`.

Waking CI is a two-line edit: uncomment the trigger blocks and re-enable the
workflow in the Actions UI. Nothing else in the files changes.

## Consequences

A reader of the repository now sees what actually runs. A fork does not
inherit surprise runs that fail for a reason belonging to this account.

The local gate runs the canonical chain rather than a hand-picked subset: the
hook calls `npm test`, so a step added to that script is a step the hook runs.
`ci-lite.yml` still enumerates its steps one by one and can therefore drift
behind the script - which is a further reason the local gate, not the workflow,
is the one being trusted.

What stays unverified while the workflows sleep, stated rather than implied:

- Windows. `lib/credential-store.sh` has a Windows branch exercised only by
  `test.yml`'s separate `windows-test` job (the matrix itself is Ubuntu and
  macOS).
- A clean `npm ci` from the lockfile into an empty tree. The local gate runs
  against an installed `node_modules` that has accumulated across months.
- Linux, unless someone actually runs `npm run gate:linux`. It is deliberately
  not a required step: `act` is not installed everywhere, and a required gate
  that cannot run is the failure this ADR exists to avoid.

`release.yml` keeps its tag trigger and stays disabled in the UI. Publishing is
done locally from a clean tag clone, so the tag flow is what a future wake-up
would want unchanged.

## Alternatives

**Pay for Actions minutes.** Buys the matrix and the clean room. Rejected for
now: there is no server-side plan to justify a recurring cost, and the local
gate already covers everything except the three items listed above.

**Make this machine a self-hosted runner.** Free and real. Rejected: it covers
only macOS, disappears when the laptop is closed, and a self-hosted runner on a
public repository executes code from any fork's pull request.

> **Amendment, 2026-09-17.** Two of those three reasons have moved.
>
> "Covers only macOS" stopped being a cost when ADR-0012 removed the other
> platforms: macOS is now the only target, so a macOS-only runner covers all of
> it.
>
> The fork argument is weaker than it was but is NOT gone, and the difference is
> worth stating precisely because the plan that revisited this got it wrong.
> The repository is private, which is the important half. It is not
> fork-free: measured on 2026-09-17 it has **one fork**, owned by another
> account. GitHub requires approval before a fork's pull request runs a
> workflow, so the exposure is gated - but the gate is a setting and an
> approval given by reflex puts that code on the runner, with the runner's
> keychain.
>
> "Disappears when the laptop is closed" is the reason this is still not the
> default. It becomes a real option only on a machine that stays awake, which
> is what `docs/server-readiness.md` describes and why that page is a
> prerequisite rather than a companion.
>
> The decision is unchanged: CI stays dormant and the pre-push gate stays
> primary. What changed is that a self-hosted runner is now a supported option
> for someone who has read that page, rather than a rejected one.

**Delete the workflows.** Honest but lossy. The files encode which steps matter
and in what order, and the possible issue-triggered direction would have to
rebuild them from nothing.
