Files
cozystack/AGENTS.md
T
Aleksei Sviridkin 575b41e1cd test(hack): let each bats file declare the EXIT traps it still holds
The ban on EXIT-trap cleanup in hack/*.bats was enforced by two lists
inside hack/cozyreport.bats: one naming the files known to be clean, one
freezing an exact trap count for each file that was not. Both had to be
edited from whatever change moved them, and that is where the guard kept
failing. A change adding a trap to its own file had to edit a string in
a suite it otherwise never touches, so two changes sharing no line still
invalidated each other: each stayed green against its own base, git
merged both cleanly, and the guard went red only once the second landed.
A file arriving with traps hit the same wall from the other side -- the
inventory did notice it, since the string it compared was built by
scanning the directory, but absorbing it meant an edit in a file nothing
in the author's diff pointed at.

Replace both lists with a declaration each file makes about itself: one
"# EXIT-TRAP DEBT: N" comment, exact rather than a ceiling. A file
carrying none must install no EXIT trap, which covers the converted
files, the files that never had one, and every file added later.
Growing or shedding a trap now fails in the file the change already
edits, so two changes that disagree about a count collide textually
instead of silently.

The declaration is read from the leading comment block, not from
anywhere in the file. A .bats file is shell that writes shell, so the
same line turns up inside a heredoc, a fixture writer or an
expected-output string, where it is data belonging to one test. Reading
it there as a statement about the whole file would let an unrelated
fixture excuse a real trap, and would do it silently, since nothing in
that test's own diff looks like a declaration. A comment block is the
region with no interior: stopping instead at the first @test would still
read a line out of a helper's heredoc.

Not every counted handler is debt. A trap inside an explicit subshell
does not replace the bats binary's own, so a test failing inside
`( ... )` still prints its `not ok`. hack/e2e-test-openapi.bats kills a
backgrounded kubectl proxy that way, and moving the kill to the end of
the body would leak a process holding a fixed port. Its declaration
records that rather than scheduling a conversion, and because the
ratchet is exact, removing the trap fails too -- the count protects the
construct. What the count cannot do is tell the two apart: substituting
a test-level trap for the subshell one keeps the total at 1 and stays
green, which the header states rather than leaves to be found.

Counting bounds the keyword and the signal the same way, at any
character that cannot be part of an identifier, and matches the signal
in either case. Whitespace on the right missed `trap ... EXIT; cd
"$tmp"`; whitespace on the left missed `tmp=$(mktemp -d);trap ... EXIT`
and `(trap ... EXIT; true)`; upper case missed `trap ... exit`, which
bash and dash both install. All are real handlers that scored zero, and
the left boundary matters most, since the inventory being replaced had
none and caught the semicolon form. A bare word boundary is not enough
either way: `bootstrap ` ends in `trap `, and it must keep scoring
nothing. A quoted signal counts for the same reason as the rest.

Two handlers sharing one line are reported rather than counted, since
the count is a count of lines and the second would otherwise arrive
without moving the total; splitting the line properly needs a shell
parser, a semicolon inside a handler's own action not being a
separator, and guessing wrong undercounts -- the one direction a ratchet
cannot afford.

The scan recurses, so hack/e2e-apps/*.bats is covered rather than
sitting one directory below the guard that claims the tree. It reads
.bats and nothing else, so a handler arriving through a sourced .sh
stays outside it -- hack/e2e-chainsaw/_lib/run-kubernetes.sh installs
two, each benign for its own reason rather than by design: one sits in
a function declared with `(` and so runs in a subshell, the other in a
brace function no @test calls. That boundary is stated in the header
rather than papered over.

The guard moves to hack/bats-no-exit-trap.bats: its subject is every
unit suite under hack/, not the report collector it grew up in. Its
fixture helpers assemble the trap keyword and the signal from separate
arguments, because the guard scans its own source and a fixture written
as a literal would be counted as a real trap in it.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-08-07 23:53:40 +03:00

8.6 KiB

AI Agents Overview

This file provides structured guidance for AI coding assistants and agents working with the Cozystack project.

Activation

CRITICAL: When the user asks you to do something that matches the scope of a documented process, you MUST read the corresponding documentation file and follow the instructions exactly as written.

  • Commits, PRs, git operations (e.g., "create a commit", "make a PR", "fix review comments", "rebase", "cherry-pick")

    • Read: contributing.md
    • Action: Read the entire file and follow ALL instructions step-by-step
  • Changelog generation (e.g., "generate changelog", "create changelog", "prepare changelog for version X")

    • Read: changelog.md
    • Action: Read the entire file and follow ALL steps in the checklist. Do NOT skip any mandatory steps
  • Release creation (e.g., "create release", "prepare release", "tag release", "make a release")

    • Read: releasing.md
    • Action: Read the file and follow the referenced release process in docs/release.md
  • Image references, tags and digests (e.g., "why is this image tagged rc", "add an image to a package", "change how an image is stamped", "promotion/nightly missed an image", "where do image refs live")

    • Read: image-refs.md
    • Action: Read the entire file. Image refs live in three storage shapes and carry three classes of tag; a tool that knows only one shape skips images silently rather than failing. hack/lib/image-refs.sh is the enumeration shared by the promote, retag and mirror tooling — extend it there rather than teaching an individual script a new path. Note hack/overlay-main-images.sh is a fourth consumer that still walks the tree itself and does not source the library
  • Project structure, conventions, code layout (e.g., "where should I put X", "what's the convention for Y", "how is the project organized")

    • Read: overview.md
    • Action: Read relevant sections to understand project structure and conventions
  • E2E tests and E2E CI (e.g., "write/fix an e2e test", "stabilize a flaky test", "add a bats test", "change the e2e workflow", "why did e2e fail")

    • Read: e2e-testing.md
    • Action: Read the entire file and follow the conventions exactly — no new retries on deterministic steps, event-driven backstops before every kubectl wait, no test-level EXIT/RETURN traps (a self-contained one inside a Chainsaw script step or an explicit subshell is fine — see the doc), fail-fast on HelmRelease readiness
  • General questions about contributing

    • Read: contributing.md
    • Action: Read the file to understand git workflow, commit format, PR process
  • Issue and PR labeling, triage (e.g., "label this issue", "what label should I use", "triage this", "categorize")

    • Read: .github/labels.yml
    • Action: Use labels defined there. Conventions follow the Kubernetes scheme — kind/* (type), area/* (subsystem), priority/* (urgency), triage/* (review state), lifecycle/* (auto-close), do-not-merge/* (PR blockers), security/* (severity)
    • For area/*: accuracy outweighs reuse. If no existing area/* truly fits the change, propose a new one via PR (extend .github/labels.yml and the scope mapping in .github/workflows/pr-labeler.yaml) — do not shoehorn the change into a wrong area. area/uncategorized is the auto-labeler fallback; treat it as a signal to pick a fit, create a new area, or correct the PR title
    • PR titles: a Conventional Commits header (type(scope): description, types from contributing.md) auto-applies kind/* and area/* via .github/workflows/pr-labeler.yaml. Append ! (or add a BREAKING CHANGE: footer) to apply kind/breaking-change

Important rules:

  • ✅ ONLY read the file if the task matches the documented process scope - do not read files for tasks that don't match their purpose
  • ✅ ALWAYS read the file FIRST before starting the task (when applicable)
  • ✅ Follow instructions EXACTLY as written in the documentation
  • ✅ Do NOT skip mandatory steps (especially in changelog.md)
  • ✅ Do NOT assume you know the process - always check the documentation when the task matches
  • ❌ Do NOT read files for tasks that are outside their documented scope
  • 📖 Note: overview.md can be useful as a reference to understand project structure and conventions, even when not explicitly required by the task

Project Overview

Cozystack is a Kubernetes-based platform for building cloud infrastructure with managed services (databases, VMs, K8s clusters), multi-tenancy, and GitOps delivery.

Quick Reference

Code Structure

  • packages/core/ - Core platform charts (installer, platform)
  • packages/system/ - System components (CSI, CNI, operators)
  • packages/apps/ - User-facing applications in catalog
  • packages/extra/ - Tenant-specific modules
  • cmd/, internal/, pkg/ - Go code
  • api/ - Kubernetes CRDs

Conventions

  • Helm Charts: Umbrella pattern, vendored upstream charts in charts/
  • Go Code: Controller-runtime patterns, kubebuilder style
  • Git Commits: Conventional Commits (type(scope): description) with --signoff
  • Prose: One continuous line per paragraph — see Prose Formatting

What NOT to Do

  • ❌ Edit /vendor/, zz_generated.*.go, upstream charts directly
  • ❌ Modify go.mod/go.sum manually (use go get)
  • ❌ Force push to main/master
  • ❌ Commit built artifacts from _out
  • ❌ Hardwrap a prose paragraph in markdown, a PR body, or an issue comment

Prose Formatting

Markdown files and everything an agent publishes to GitHub — PR bodies, issue bodies, review comments, release notes — use one continuous line per paragraph. Renderers collapse a soft line break into a space, so a paragraph hardwrapped at ~80 columns renders exactly like the same paragraph on one line, while breaking narrow viewports, mangling nested list and table rendering, and turning a one-word edit into a diff that reflows the entire block. The renderer decides where the line ends; the author does not.

Line breaks stay wherever they carry meaning: code fences, one item per list line, tables, headings, blockquotes, front matter, and README badge stacks. The rule governs prose paragraphs and nothing else. Commit messages are the deliberate exception — git log and the tooling around it still expect a body wrapped at ~72 columns.

This is the rule LLM agents break most often, because an ~80-column wrap is correct in the places they spend most of their time (Go comments, commit bodies) and the reflex leaks from there into documentation and GitHub. Treat it as a mechanical check at the moment of writing, not as a style preference to weigh: before saving a markdown file or passing --body to gh, look at each paragraph and confirm it is a single line.

The rule applies to prose this repository owns, and to new or changed paragraphs. Much of the existing tree predates it and is wrapped throughout; that is not a defect to go and fix. Vendored upstream chart docs (packages/*/*/charts/**) must stay verbatim — make update regenerates them — and published changelogs are historical records. Neither is in scope.

.claude/hooks/md-no-hardwrap.py enforces the rule for agents that support PreToolUse hooks (registered for Claude Code in .claude/settings.json). It refuses a Write/Edit/MultiEdit that would add a hardwrapped paragraph, list item or blockquote to a markdown file, and a gh command that would publish one to GitHub. A break counts as added only when the edit wrote both lines it joins, so fixing a typo inside a paragraph that was already wrapped goes through, while rewriting the same file as fresh hardwrapped prose does not. Anything it cannot read with certainty it allows — a guard that blocks legitimate work gets switched off, and then it guards nothing — which includes a body the shell assembles at runtime, and a --body-file written by the very command being checked, since the file does not exist yet when the hook runs. hack/md-no-hardwrap.bats covers it under make unit-tests and requires python3. Agents on other harnesses are held to the same rule without the hook.

.claude/settings.json is tracked, so keep personal Claude Code overrides in .claude/settings.local.json, which is not. A personal .claude/settings.json predating this will be overwritten on pull without warning, because git had been ignoring it.