From 5a9e455149c3f76c217b94e361d9db80ad9ed922 Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Tue, 29 Sep 2026 00:11:58 -0500 Subject: [PATCH] docs: delivery contract for agents + record that the merge gate does not exist Going to land the plex claim wiring through the documented PR flow surfaced that the flow has never run. PR #1 is the first pull request this repo has ever had. Verified against the live Gitea, not inferred from the docs: * branch_protections is [] -- main is protected:false, enable_status_check:false, status_check_contexts:[], required_approvals:0, user_can_push:true * ZERO Actions runners registered at repo, org and site level DECISIONS-LOG 2026-08-31 asserts a "required status check" and a runner "incluster-dind online". Both are false of the live system. The gate is neither enforced nor able to run, and that went unnoticed because the log claimed otherwise -- the same fail-open class as the two holes found on 2026-09-18. Adds CLAUDE.md, the first agent-facing delivery doc here: the branch -> PR -> gate -> approval -> merge rule, stated alongside the honest fact that enforcement is currently weaker than the rule, so agents follow the rule rather than what they can get away with. Carries the toolbox gate invocation, the two git-worktree workarounds, the rc 0/1/3 contract, and secret handling. Records the ordering trap: enabling a required status check before a runner exists deadlocks every PR permanently, including the one that would fix it. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 115 +++++++++++++++++++++++++++++++++++++++++++++++ DECISIONS-LOG.md | 65 +++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c59709d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,115 @@ +# CLAUDE.md — how work lands in this repo + +Read this before doing anything durable. It is the delivery contract for AI coding +sessions and agents. `DECISIONS-LOG.md`'s last entry is the current PM handoff and +takes precedence on *what* to work on; this file governs *how* it lands. + +## 1. Never push to `main`. Ever. + +Every durable change reaches `main` exactly one way: + + branch -> push the branch -> open a PR -> CI gate green -> human approval -> merge + +There is no other path. Specifically: + +- **Do not `git push origin main`.** +- **Do not `git push origin HEAD:main`.** Same thing wearing a hat. +- **Do not merge your own PR.** A human approves and clicks merge. +- **Do not `kubectl apply` / `edit` / `patch` a durable change.** Flux reconciles `main`; + anything applied by hand is drift that the next reconcile reverts or, worse, adopts. + +> **Many runbooks under `runbooks/` still say `git push origin main` IS the deploy.** +> That wording is **stale** — it predates the PR flow. It describes what Flux does +> (main is what the cluster follows), not what you are permitted to do. Honour this +> file, not that line. Fixing those runbooks is tracked work. + +## 2. Enforcement is currently WEAKER than this rule. Follow the rule anyway. + +Verified against the live Gitea on 2026-09-29: + +| Expected | Actual | +|---|---| +| `main` protected, direct push rejected | **No branch protection.** `protected: false`, `user_can_push: true` | +| PR blocked until the gate passes | **`enable_status_check: false`**, `status_check_contexts: []` | +| Gate runs on every PR | **No Actions runner is registered** (repo, org and site level all report zero) | +| Approval required before merge | **`required_approvals: 0`** | + +So nothing mechanically stops you from pushing to `main` right now. **That is a gap +being closed, not permission.** `DECISIONS-LOG.md` (2026-08-31) claims the gate is a +"required status check" with a runner online; that claim is not true of the live +system, which is how this went unnoticed. + +**Consequence for you:** because CI cannot run, the **local toolbox gate is the only +real gate**. Run it and quote its numbers in your report. Never describe a PR as +"green" on the strength of CI that did not execute. + +## 3. Gate before you push + + ./infra/devcontainer/dev.sh run bash -c './ci/gate.sh' + +Exit codes are a contract — do not flatten them: + +| rc | Meaning | +|---|---| +| 0 | PASS | +| 1 | BLOCKED — something failed | +| 3 | UNDETERMINED — a stage did not run. **Never report this as green.** | + +`spec-immutability` reaches rc 3 whenever the sibling spec repo is not checked out +beside this one; that single skip is expected and tolerated. Any *other* skip is a +real gap in your run. + +**Never gate on the host.** `promtool` and `kustomize` are not on the host `$PATH`, +so checks SKIP and you get a false rc 3. Only the pinned toolbox result counts. + +### Running the gate from a git worktree + +Two unrelated failures, both with known workarounds — put both in any agent brief: + +1. **`dev.sh` refuses to start:** `infra/ansible/.cache/prod.kubeconfig` is untracked + and exists only in the main checkout. Do **not** copy it (it is a credential). + Use `DEV_KUBECONFIG=/dev/null` — the gate contacts no cluster, so L2/L3 scaffold-skip + normally. +2. **Every `git` call inside the container fails:** a worktree's `.git` is a pointer to a + gitdir outside the mounted root. `dev.sh` takes no extra-mount flag, so inject one via + the `ENGINE` hook — a shim that intercepts `run` and re-execs + `podman run -v /path/to/main/.git:/path/to/main/.git:ro "$@"`. A traceback in + `tracked_files()` is this bug, not a broken tree. + +## 4. Branch naming + +Use `feat/`, `fix/`, `docs/`, `chore/`. Note that `.gitea/workflows/gate.yml`'s **push** +trigger lists `feature/**` — not `feat/**` — so branch pushes get no fast-feedback run. +The `pull_request` trigger fires regardless of branch name, so the PR gate is unaffected. + +## 5. Cluster access + +`kubectl` does not work on the operator workstation; the toolchain lives only in the +pinned container. Always name the context explicitly: + + ./infra/devcontainer/dev.sh run bash -c 'kubectl --context k3s-prod get nodes' + +`dev.sh` merges both clusters and picks a current-context **per invocation**; that +default has already changed once. Never rely on it. Do not run two `dev.sh` +invocations concurrently. + +**Mutating `kubectl` and `flux reconcile` are operator actions, not agent actions.** + +## 6. Secrets + +- **Never `sops -d` a file into a terminal, a log, or a transcript.** +- **Never print a Secret's `.data`/`.stringData`** when checking whether it exists — + list key names or object names only. A password has been leaked into a transcript + this way before. +- Encrypted files are `*.sops.yaml` or live under `secrets/`. The root `.sops.yaml` is + the single creation-rules file; do not add a second one in a subtree. +- A short-lived credential (e.g. a plex.tv claim token, ~minutes) cannot survive the PR + flow. Do not design around committing one — read the expiry from the issuing page and + treat a burnt token as free to retry. + +## 7. Before you finish + +Append a handoff entry to `DECISIONS-LOG.md` describing what you changed, what you +verified, and what you did **not** verify. State gaps plainly — an honest +"UNVERIFIED" is worth more than an optimistic "green", and this repo has already been +burned by the difference. diff --git a/DECISIONS-LOG.md b/DECISIONS-LOG.md index d1ce8c9..bee4174 100644 --- a/DECISIONS-LOG.md +++ b/DECISIONS-LOG.md @@ -4716,3 +4716,68 @@ asleep. `git worktree remove` preserves branches and their commits, so only that 6. **Five `public` services have no working public path** — waiting on WAN forwarding, deliberately. 7. Backlog cherry-picks: `feat/egress-gateway-staleness-alert`, two `rescued/*` fixes, two rendered manifests nothing applies (the keycloak one would stand up a conflicting instance). + +## 2026-09-29 — The CI/CD merge gate does not exist: no runner, no branch protection. **PM HANDOFF.** + +Went to land the plex claim wiring through the documented PR flow and discovered the flow has +never run. **PR #1 is the first pull request this repository has ever had.** It is still queued. + +### What is actually true of the live Gitea (verified via API, 2026-09-29) + +| Claimed (DECISIONS-LOG 2026-08-31) | Measured | +|---|---| +| "required status check" on `main` | `branch_protections` returns `[]`. `protected: false`, `enable_status_check: false`, `status_check_contexts: []`, `required_approvals: 0`, `user_can_push: true` | +| "runner `incluster-dind` online (ns gitea-actions)" | **Zero runners registered** at repo, org and site level | + +Both halves of that entry are false. Nothing enforces the gate, and the gate cannot run. +PR #1's two contexts (`gate / gate`, `gate / conformance`) sat `pending` for 14+ minutes with +nothing to execute them, and will sit there indefinitely. + +This is a **fail-open governance hole**, the same class as the two found on 2026-09-18 — and it +was invisible precisely because the log asserted the opposite. Every "the gate is enforcing" +statement since 2026-08-31 rested on an unverified claim. + +### Ordering trap — read before fixing + +**Do not enable required status checks until a runner is live.** Requiring +`gate / gate (pull_request)` with no runner deadlocks every PR permanently, including the PR +that would fix it. Sequence: runner first, verify the gate actually runs and passes on a real +PR, *then* turn on enforcement with the context name CI actually reported. + +Related: `gate.yml`'s `conformance` job is `if: false` by design (honest skip, never fake green), +but it still publishes a `gate / conformance (pull_request)` context. If that context is ever +listed as required, it deadlocks every PR for the same reason. Wire it or exclude it. + +### Delivery contract written down + +Added `CLAUDE.md` at the repo root — the first agent-facing delivery doc here. It states the +branch -> PR -> gate -> approval -> merge rule, records that enforcement is currently weaker than +the rule (so agents follow the rule rather than what they can get away with), and carries the +toolbox-gate invocation, the two git-worktree workarounds, the rc 0/1/3 contract, and the secret +handling rules. + +It also flags that **37 of 39 runbooks still instruct `git push origin main`**. Those were +accurate before the PR flow and are stale now. Correcting them is tracked, not done. + +### Also found, not fixed + +- `gate.yml`'s push trigger lists `feature/**`; this repo's branches use `feat/**`, so branch + pushes get no fast-feedback run. PR trigger is unaffected. +- The gitea-actions runner is GitOps-managed (`apps/wave-1/gitea-actions.yaml` + + `gitea-actions-runner-secret.sops.yaml`), so it is deployed but not registering — most likely a + stale registration token in that SOPS secret. Needs in-cluster diagnosis (operator). +- **Plex is still unclaimed and it was never checked** whether it is already claimed. Do that + before spending a token — `runbooks/plex-claim.md` §First commands. +- `runbooks/plex-claim.md` asserts a "~4-minute TTL" for a plex.tv claim token in four places, + **uncited**, and the whole stopwatch procedure is built on it. Upstream `pms-docker`'s README + says nothing about expiry. The number is plausible community folklore, not a measured fact. + The authoritative reading is the countdown rendered on plex.tv/claim at issue time. + +### Next steps + +1. **Operator:** diagnose the gitea-actions runner; get one registered. +2. **Operator:** enable branch protection on `main` — push blocked, approvals required — and only + then add the gate as a required status check. +3. Land `feat/plex-claim-sops-secret` (PR #1) once the gate can actually run. +4. Claim plex, after checking it is not already claimed. +5. Correct the 37 runbooks' `git push origin main` wording.