Files
home-network-k3s-impl/CLAUDE.md
jakman85andClaude Opus 5 5a9e455149
gate / gate (push) Has been cancelled
gate / conformance (push) Has been cancelled
gate / gate (pull_request) Successful in 1m33s
gate / conformance (pull_request) Has been skipped
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) <[email protected]>
2026-09-29 00:11:58 -05:00

5.3 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 / 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.