The runner came up on 2026-09-29 and the first CI jobs this repo has ever run went green, which falsified a claim merged in #2. MEASURED: gate / gate (pull_request) success 1m58s (PR 1), 1m34s (PR 2) gate / gate (push) success 1m32s (PR 2 only) gate / conformance (pull_request) success "Has been skipped" #2 said listing the conformance context as a required check would deadlock every PR, because the job is `if: false` and never runs. That is wrong. Gitea reports a skipped job as SUCCESS. Requiring it would not deadlock anything -- it would pass VACUOUSLY, forever, while nothing ran. The conclusion is unchanged (never require it) but the reason inverts, and the corrected reason matters more: under HARD RULE 4 a permanently-green check that verifies nothing is worse than a deadlock. A deadlock is loud. This is the exact fail-open shape the 2026-09-18 audit and this one both keep finding. Also records that the (push) contexts only appear for branches matching gate.yml's push trigger -- feature/**, fix/**, docs/**, chore/** -- and NOT feat/**, which is why PR 1's feat/ branch produced only pull_request contexts. Require the pull_request context alone, or PRs from non-matching prefixes never satisfy the rule. CLAUDE.md's enforcement table is updated: the runner half is genuinely fixed and observed green, the branch-protection half is not. Adds the warning that a green tick is not by itself evidence that anything ran. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
5.6 KiB
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/patcha durable change. Flux reconcilesmain; anything applied by hand is drift that the next reconcile reverts or, worse, adopts.
Many runbooks under
runbooks/still saygit push origin mainIS 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 | Fixed 2026-09-29. Runner incluster-dind is online; gate / gate (pull_request) runs and passes in ~1m30s–2m |
| 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) claimed the gate was a
"required status check" with a runner online; neither was true, which is how this went
unnoticed for a month. The runner half is now genuinely fixed and observed green. The
protection half is not — see infra/gitea/.
Consequence for you: CI now runs, but it does not yet block anything. Always run
the toolbox gate yourself and quote its numbers. Never describe a PR as "green" on the
strength of a check you have not seen report — and note that a skipped Gitea job
reports as success, so a green tick is not by itself evidence that anything ran.
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:
dev.shrefuses to start:infra/ansible/.cache/prod.kubeconfigis untracked and exists only in the main checkout. Do not copy it (it is a credential). UseDEV_KUBECONFIG=/dev/null— the gate contacts no cluster, so L2/L3 scaffold-skip normally.- Every
gitcall inside the container fails: a worktree's.gitis a pointer to a gitdir outside the mounted root.dev.shtakes no extra-mount flag, so inject one via theENGINEhook — a shim that interceptsrunand re-execspodman run -v /path/to/main/.git:/path/to/main/.git:ro "$@". A traceback intracked_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 -da file into a terminal, a log, or a transcript. - Never print a Secret's
.data/.stringDatawhen 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.yamlor live undersecrets/. The root.sops.yamlis 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.