Skip to content

Commit 7026548

Browse files
docs: adopt the fleet agent-context standard (#42)
Add a root AGENTS.md stating what this repo owns and does not own, the build and test commands derived from .github/workflows/ci.yml, and the behavioural rules from obin-ai/handbook#68 anchored to code that already behaves that way. Add one skill, lifecycle-test-triage, for the procedure that is repeated and easy to get wrong: telling a real runner-lifecycle regression from a contended Docker daemon, a tool-missing skip, or the default go test timeout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 144810b commit 7026548

2 files changed

Lines changed: 239 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
name: lifecycle-test-triage
3+
description: Triage a red or hanging runner-lifecycle test in service-python-fastapi (TestCreateToRunDocker, TestPythonFastAPILifecycle_Matrix, the uvicorn and SBOM tests) to tell a genuine regression from Docker-daemon contention, a missing tool that turned the test into a silent skip, or the default go test timeout. Use before concluding that a lifecycle failure came from your diff, and before raising a timeout or relaxing a wait.
4+
---
5+
6+
# Triaging a lifecycle test
7+
8+
These tests drive a real uvicorn through `uv` and real containers through the
9+
Docker daemon. That makes them the ones worth trusting and the ones that fail
10+
for reasons outside the diff. Work down this list before changing test code.
11+
12+
## 1. Did it actually run?
13+
14+
A missing tool skips, it does not fail, so a green run can mean almost nothing
15+
was exercised:
16+
17+
```bash
18+
go test -v ./... 2>&1 | grep -E '^\s*--- SKIP'
19+
```
20+
21+
| Missing | Skips |
22+
| --- | --- |
23+
| `uv` | the real uvicorn lifecycle |
24+
| `docker` | destroy-under-docker |
25+
| `syft`, `docker`, `uv` | the image SBOM inventory |
26+
| `python3` | the active-environment test |
27+
28+
CI installs `uv` and pins `syft` v1.48.0. If a skip covers the surface you
29+
changed, install the tool and re-run — do not report the green.
30+
31+
## 2. `panic: test timed out after 10m0s`
32+
33+
That is `go test`'s default, not an assertion. Read the `running tests:` block
34+
the panic prints: when it names `TestPythonFastAPILifecycle_Matrix/docker` or
35+
another container case, the suite was still making progress. CI finishes the
36+
whole suite inside the default; a busy local daemon does not.
37+
38+
```bash
39+
go test -timeout 40m ./...
40+
```
41+
42+
If it passes with the longer budget, the timeout was the environment. Say so
43+
rather than raising the timeout in the repo.
44+
45+
## 3. `main_test.go: too many tries`
46+
47+
`TestCreateToRunDocker` polls `GET <address>/version` once a second and gives
48+
up after ten tries. That budget is fixed and small — about eleven seconds —
49+
while the surrounding work (image pull, container create, `uv sync`) is not
50+
bounded by it at all. So this message means *the service did not answer within
51+
eleven seconds of `Start` returning*, which a contended daemon produces on
52+
completely healthy code.
53+
54+
Check contention before suspecting the diff, then re-run the one test:
55+
56+
```bash
57+
docker ps --format '{{.Names}}\t{{.Status}}'
58+
go test -run TestCreateToRunDocker -count=1 -timeout 20m -v ./...
59+
```
60+
61+
Other lazybox sessions share this daemon. A pass in isolation is the verdict.
62+
63+
Do **not** "fix" it by widening the retry budget. The poll loop is asserting
64+
that a started service becomes reachable promptly; a longer wait hides a real
65+
startup regression the next time there is one.
66+
67+
## 4. When it is real
68+
69+
Prove it the way the rule requires — put the suspected cause back and confirm
70+
it breaks. Two failures are almost never environmental:
71+
72+
- **Anything about locking.** `runtime.go` serializes Init/Start/Stop/Destroy
73+
with a documented lock order (`initMu` → `runnerCreateMu` → `runnerMu`). A
74+
lost lock is a data race, not a failed assertion, so plain `go test` stays
75+
green. `go test -race ./...` is the verdict, and it is its own CI job.
76+
- **A status carried in the response, not the error.** `Init` reports failure
77+
through `InitStatus`, so a `require.NoError` alone passes over a failed Init
78+
and surfaces later as an unrelated empty-network-mappings error. Read the
79+
status message in the failure output before tracing the downstream symptom.

‎AGENTS.md‎

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Working in codefly-dev/service-python-fastapi
2+
3+
This repo is one codefly agent binary: the **FastAPI specialization** of the
4+
generic Python agent. It composes `github.com/codefly-dev/service-python`'s
5+
`pkg/*` types and adds what FastAPI needs — uvicorn supervision and hot reload,
6+
OpenAPI-derived REST endpoints, an opt-in `grpc.aio` listener, the Dockerfile
7+
and kustomize recipes, and the scaffold a new FastAPI service is created from.
8+
Dependency management is `uv`; poetry is not supported.
9+
10+
It does **not** own:
11+
12+
- **Generic Python behaviour** — `uv`, AST/ruff/pytest tooling, the Python
13+
`Code` and `Tooling` surfaces. Those come from `service-python` and are
14+
wired through unchanged in `main.go`. A defect there is fixed there.
15+
- **The resource, network and configuration model** — ports, endpoints,
16+
environment injection, readiness, the agent gRPC contracts. That is
17+
`codefly-dev/core`.
18+
- **The `codefly` CLI** (`codefly-dev/cli`, private — do not import it, do not
19+
make CI depend on it) and user workspaces.
20+
21+
When the change you need belongs to one of those, it gets made there.
22+
23+
## How to behave
24+
25+
Fleet standard — [handbook#68](https://github.com/obin-ai/handbook/issues/68).
26+
These bite here because this agent sits between core's resolution machinery and
27+
a user's Python process: nearly every defect is observed as "the service does
28+
not come up", and the local substitute for whatever did not resolve is always
29+
within reach.
30+
31+
- **A gap in the tooling is a bug in the tooling — never a reason to reach
32+
around it.** When something needs a step `codefly`, `uv` or a companion image
33+
does not perform, the answer is a capability fixed in whichever tool owns it,
34+
and named in the PR. Not a hand-assembled substitute — not as a "workaround",
35+
not "just this once", not "until the capability lands".
36+
- **Never hack. Provide the best fix, even when it spans repos.** The fix
37+
living in `core` or `service-python` is not a reason to work around it here.
38+
Open the PR there and consume the reviewed result. When it genuinely cannot
39+
be fixed now, the deliverable is a precise issue against that owner plus an
40+
explicitly labelled stopgap — never an unlabelled one.
41+
- **Classify every change that makes something work**, in the PR body: a *fix*
42+
at the place that owns the behaviour, or a *hack*. A hack does not become a
43+
fix by working, by being small, by being local, or by the real fix belonging
44+
elsewhere.
45+
- **Never hardcode what the system resolves.** Ports, endpoint addresses,
46+
injected environment and credentials are resolved *for* this agent and then
47+
published by it: `Init` takes the port out of the network mappings, and the
48+
environment the Python process receives is assembled by core's
49+
`EnvironmentVariableManager` from endpoints and workspace configurations.
50+
Typing one of those values in encodes something true only on one machine for
51+
ten minutes — and it fails *quietly*: a runtime missing a credential can skip
52+
registration silently, so the service boots, serves, and is simply absent.
53+
- **Diagnose, do not pattern-match.** "It started working when I set X" is not
54+
a diagnosis — set X back and confirm it breaks. Do not trust an error message
55+
before checking its claim. `TestCreateToRunDocker` failing with `too many
56+
tries` is the standing example: it reports a readiness timeout and usually
57+
means the shared Docker daemon is contended, not that the diff broke startup.
58+
- **Say what you did not verify.** Unverified is not the same as working. The
59+
Docker-backed and image-scanning tests are the slow, real ones; if you did not
60+
run them, the PR says so.
61+
62+
## Build and test
63+
64+
Derived from `.github/workflows/ci.yml`, which delegates to core's shared
65+
`go-service-ci.yml` and adds a `race` job of its own. Go comes from `go.mod`
66+
(1.27.0). All of it runs locally:
67+
68+
```bash
69+
go build ./... # CI: build job
70+
go vet ./... # CI: build job
71+
go mod tidy -diff # CI: build job
72+
go test ./... # CI: test job (runs it with -v)
73+
go test -race ./... # CI: race job
74+
```
75+
76+
The `race` job exists because the runner lifecycle (Init / Start / Stop /
77+
Destroy) is concurrent gRPC handlers over shared state: a lost lock is a data
78+
race, not a failed assertion, so the plain `go test` above passes on code with
79+
a mutex removed. Touch `runtime.go`'s locking and `-race` is the run that
80+
matters. The lock order is documented on the struct fields — follow it.
81+
82+
**Nothing is mocked.** The suite drives a real uvicorn through `uv`, real
83+
containers through the Docker daemon, and a real `syft` scan of an image built
84+
from the recipe this agent emits. Tests skip rather than fake when a tool is
85+
absent, so a green run on a bare machine has proved much less than it looks:
86+
87+
| Missing | Silently skips |
88+
| --- | --- |
89+
| `uv` | the real uvicorn lifecycle |
90+
| `docker` | destroy-under-docker |
91+
| `syft` (+ `docker`, `uv`) | the image SBOM inventory |
92+
93+
CI installs `uv` and pins `syft` v1.48.0 in `setup-run` for exactly that
94+
reason. Get them locally too, or say which halves you did not exercise.
95+
96+
The suite is slow because it is real — CI finishes inside `go test`'s default
97+
10-minute timeout, but a contended local Docker daemon pushes the container
98+
cases past it. A local `panic: test timed out` in the lifecycle matrix is that,
99+
not a finding; re-run the suite with `-timeout` raised before concluding
100+
anything from it.
101+
102+
## Where things live
103+
104+
| Path | Owns |
105+
| --- | --- |
106+
| `main.go` | settings, agent advertisement, composition of the generic Python layer |
107+
| `runtime.go` | Init/Start/Stop/Destroy, uvicorn supervision, hot reload, the lock order |
108+
| `builder.go` | `Create` scaffolding, OpenAPI endpoint loading, Docker + kustomize recipes |
109+
| `probes.go`, `probe.py` | endpoint health translated to Kubernetes probes / a local probe |
110+
| `templates/` | **what ships** — embedded via `go:embed`, rendered on `Create` |
111+
| `base/` | a checked-in sample service, embedded nowhere |
112+
113+
`templates/` and `base/` look interchangeable and are not. Only `templates/` is
114+
embedded in the binary, so **editing `base/` changes nothing a user receives**;
115+
it has already drifted from `templates/factory/`. Dependabot watches
116+
`/base/code` for pip updates and `/templates/builder` for the base image, so a
117+
dependency bump landing in `base/code` is not a bump of the scaffold — carry it
118+
into `templates/factory/code/pyproject.toml.tmpl` yourself.
119+
120+
## Rules that bite
121+
122+
- **Endpoint contracts are read from fixed locations in the *generated
123+
service*** (not paths in this repo): `openapi/api.swagger.json` for REST and
124+
`proto/api.proto` for gRPC, the latter being core's `standards.ProtoPath`.
125+
Both Builder and Runtime re-derive endpoints from exactly there, and so does
126+
any dependent service. The swagger file does not exist at `Create` time — the
127+
service generates it at runtime from its own `src/openapi.py` — so absent is
128+
a normal state there, not an error to code around.
129+
- **Readiness is the endpoint's declared health check, never an open port.**
130+
Probe intents (readiness / startup / liveness) stay separate through
131+
`resources.PlanEndpointProbes`; collapsing them silently drops a predicate.
132+
- **The runtime image is pinned.** `docker-image` rejects a bare name and
133+
`:latest`. The default `codeflydev/python` companion is built by core and
134+
repinned on release — prefer it.
135+
- **A user's `flake.nix` is never overwritten.** The embedded flake is written
136+
only when the source dir has none.
137+
- **Release needs cgo.** The binary links core's tree-sitter grammars, so
138+
releases go through the `goreleaser-cross` image; `conformance_test.go` is
139+
what keeps `.goreleaser.yaml` and the release workflow honest about it.
140+
141+
## Procedures
142+
143+
Step-by-step procedures live in `.claude/skills/`, loaded on demand rather than
144+
carried here:
145+
146+
- `lifecycle-test-triage` — a lifecycle test is red or hanging, and you need to
147+
tell a real regression from daemon contention, a silent skip, or the timeout.
148+
149+
## Workflow
150+
151+
- Branch and PR; never commit to `main`. Conventional Commits for the title.
152+
- Bumping a dependency: `go get` does not tidy. Run `go mod tidy` afterwards,
153+
or `ci / build` goes red on a change that passed every other local check.
154+
- `agent.codefly.yaml` carries the agent version; releases are tag-driven
155+
(`.github/workflows/releaser.yml`), never hand-built.
156+
- Keep this file under ~150 lines (hard cap 200). Push depth into a nested
157+
`AGENTS.md` beside what it describes, or into `.claude/skills/`.
158+
- There is no `CLAUDE.md`. If one is added, it is a one-line `@AGENTS.md`
159+
pointer — one canonical source, never a second copy to drift.
160+
- Treat this file as code: the PR that changes a process updates it.

0 commit comments

Comments
 (0)