Development guide¶
How to work on the code: where things are, the rules every change follows, and step-by-step recipes for the most common changes. Read architecture.md first for the big picture, and code-walkthrough.md for a guided tour of how the code works; AGENTS.md holds the same conventions in short form.
Setup¶
python3.14 -m venv .venv && .venv/bin/pip install -e '.[dev,azure]'
.venv/bin/pytest # the whole suite, ~10 s, no cluster and no model needed
set -a; source .env; set +a # for manual runs against a real cluster and model
If .venv/bin/... fails with "bad interpreter" after a Homebrew update, recreate the
venv (see troubleshooting.md).
Code map¶
| If you want to change ... | Look at |
|---|---|
| A setting | config.py (and the table in docs/configuration.md, see below) |
| How the model is called (endpoint, auth, reasoning, token log) | llm/ |
| The system prompt or the tool-calling loop | agent.py |
| Which tools a session gets, and in which order | runtime.py |
| What counts as "unhealthy" in the sweep | tools/cluster_tools.py |
| What kubectl may do | tools/kubectl_tool.py (allow-list) |
| Prometheus / Alertmanager access | tools/prometheus_tool.py |
| The health check, fingerprints, skipping unchanged checks | healthcheck.py, findings.py |
| What is remembered | memory.py |
| The morning report | report.py |
| Teams cards | teams.py |
| The scheduler | scheduler.py |
| A CLI command | cli.py |
| What the agent knows about the platform | examples/knowledge-values.yaml (example), charts/k8s-servant/knowledge/skills/ (built-in skills) |
Rules for every change¶
- Read-only, always. No code path may change cluster objects or read Secrets. A
new tool reads; if it runs kubectl, it uses fixed arguments or goes through
check_kubectl_args(). - The kubectl guard is an allow-list. Add a flag only if it cannot change the identity or target server, read or write local files, or block. Never switch to a deny-list (security.md).
- Model text never becomes a command-line flag. Validate every argument the model
passes (see
_check_namespace()incluster_tools.py). - No shell.
asyncio.create_subprocess_execwith an argument list. - stdout carries only the answer; logs go to stderr.
- Settings only from environment variables, through
config.configure_app(), each with a comment that says why it exists. - A failed or unparseable health check never changes the findings in memory.
- The Teams webhook URL is a credential. Never log it, not even inside an error.
- Times are UTC, except the morning report's schedule and title.
- Built-in skills stay generic: no site names, no write commands
(
tests/test_builtin_skills.py). Site facts go into the environment description. - Test first. Write the failing test, watch it fail for the right reason, then the code. One test file per module.
- Document as you go. Every module starts with a docstring that explains what and
why; every public function and every function longer than 15 lines has a Google-style
docstring (
tests/test_docs.pyenforces this). Comments explain intent, not syntax. - Conventional commit messages and PR titles (
feat:,fix:,docs:,refactor:,test:); release-please writesCHANGELOG.mdfrom them, so never edit it by hand.
Tests¶
tests/fakes.pyholds the shared test doubles:ScriptedChat(*replies): a fake model that returns the replies in order (an exception instance is raised instead) and records every request in.calls.answer(text),tool_call(name, args): model replies.findings_answer(summary, findings): a health-check answer with the JSON block.- MCP is tested with an in-process
mcp.server.MCPServer; HTTP withhttpx2.MockTransport; kubectl-based tools with a fakefetchfunction or, in CLI tests, a fakekubectlscript (CLI tests must never read a real cluster). cli.main()reconfigures logging to stderr, so CLI tests read stderr withcapsys, notcaplog.tests/test_docs.pychecks the documentation: every setting in thedocs/configuration.mdtables, links between docs, docstrings.- There is no automated test of answer quality yet. After changing the prompt, a
skill, the model or the summary tools, run
askandcheckagainst a real cluster and compare with what you find by hand (kubectl).
Recipes¶
Add a setting¶
- Add it to
configure_app()inconfig.pywith a comment, using the helpers (_int,_bool,_choice,_name_list, ...) so a bad value names the variable. - Test it in
tests/test_config.py(default, a valid value, an invalid value). - Add a row to the right table in
docs/configuration.md.tests/test_docs.pyfails until you do. - Add a commented line to
.env.example. - If it has a sensible per-cluster value, add it to
appConfigincharts/k8s-servant/values.yaml(with a comment) or to the environment files.
Add a check to cluster_problems¶
- Write a pure function
xyz_problems(items: list[dict]) -> list[str]intools/cluster_tools.pythat returns Markdown lines- Kind namespace/name: what is wrong. Report only real problems; say in the docstring what is deliberately not reported. - Test it in
tests/test_cluster_tools.pywith hand-written minimal JSON, including a healthy object that must not be reported. - Add the resource to
_RESOURCESand a section incluster_problems(). - Make sure the chart's RBAC can list the resource type (
charts/k8s-servant/templates/clusterrole.yaml;viewcovers most namespaced types). - Mention it in the
cluster-health-checkskill and in tools.md. - Try it on a real cluster: run the tool directly and compare with
kubectl.
Note: the problem lines feed the "anything changed?" comparison of scheduled checks. A line that contains a value changing every hour (a counter, a timestamp) makes every check run in full. Keep lines stable.
Add a tool¶
- Build a
Tool(name, description, parameters, run, surface)(tools/registry.py).run(arguments: dict) -> strmust never raise for expected failures: returnERROR: ...text the model can react to. - Write the description for the model: what it returns, when to use it, limits.
- Register it in
runtime.open_runtime(); registration order decides precedence on name clashes (MCP first). - A new surface needs a name in
SURFACE_NAMES(agent.py). - Tests: arguments validated, output format, failure as
ERROR:text, registration. - Document it in tools.md; review security.md if it reads something new.
Add or change a skill¶
- A built-in skill:
charts/k8s-servant/knowledge/skills/<name>/SKILL.mdwithnameanddescriptionfrontmatter. It must stay generic. - A site-specific skill:
knowledge.extraSkillsin the environment values. - The description decides whether the model ever loads it: start with "Use when ...".
- Check:
k8s-servant --values ... toolslists it;aska question that should trigger it and look forload_skillin the log. See knowledge-and-skills.md.
Change the system prompt¶
BASE_PROMPT in agent.py. Every rule there costs tokens on every call and can change
behaviour in unexpected places, so add a rule only for a failure you have seen, write
it as an instruction with the reason, and add a test that the rule is present (see
tests/test_agent.py). Then run ask and check against a real cluster.
Add a CLI command¶
cmd_<name>(args, config, knowledge, memory) -> intincli.py; register the sub-parser inbuild_parser().- Exit codes: 0 ok, 1 runtime failure, 2 configuration error.
- Test it through
cli.main([...])intests/test_cli.py. - Document it in cli.md.
Before you commit¶
Then a pull request with a Conventional Commit title that says why: release-please builds the CHANGELOG from those titles. See CONTRIBUTING.md.