Agent playbook
Recipes for a coding agent working in a repo that uses stackctl. Each one links to the reference page with the details.
Rules
- Never
kill,pkillorlsof … | killa stackctl process or its port. Stop a session withPOST /command/quitorstackctl stop, and restart a service withstackctl service restart -s <name>. A killed session leaves its services running. - Check before you start anything. Starting a session in a mode quits the one already running in it. stackctl refuses that when a person started it.
- Never guess a port or hostname.
stackctl show-configprints the resolved ones. - Don't pass
--kill-othersorkillOthers: trueunless the user said to. - Don't use
stackctl start --detach. It is for CI and is refused for an agent. stackctl infra up --resetandstackctl infra destroydelete data. Only on request.- Use
stackctl infra docker-compose …, never a baredocker compose …, which talks to a different Compose project. - Write
stackctl, never its aliassctl. Guide pages typesctl; permission rules and scripts match the full name.
Is it running?
- Is the repo managed by stackctl? Look for
stackctl.config.tsat or above the app's directory. If there is none, see Apps stackctl doesn't manage. - Is the app one of its services?
stackctl show-config | jq '.services | keys'. The key is the name-sand the control API take. It need not match the directory. - Is a session live? Read the mode's record and check it, as in Finding the port. No record, or a dead pid, means no session.
- Read the service's state from
GET /query/status:
curl -s "http://127.0.0.1:$port/query/status" | jq '{ services, ready, sessionReady }'services.<name> | Meaning |
|---|---|
running | Its process is live. ready.<name> says whether it passes its healthcheck. |
waiting | Held back by a service it waits for. |
restarting | On its way back up. |
stopped | Not running: it exited, crashed or was never started. |
| not a key | Configured, but not part of this session. |
services: null means the session is still starting its infra. Check again in a few seconds.
Reach a service
stackctl show-config | jq '.services | map_values({ port, publicOrigin, healthcheck })'No session is needed. Pass the --mode the session runs in.
publicOriginis where a browser reaches the service, and the address the app itself is configured for. Through the reverse proxy, a service that is down answers503with its status, not a connection error.portis the service itself:curl http://localhost:<port>/…. It works in every setup, while the service runs.*.localhostnames resolve in browsers, not incurl. Pass the host:curl -H 'Host: api.localhost' http://127.0.0.1:<proxy port>/.- Infra services with a port are under
.infra["docker-compose"].services.
Start a stack
Only when no session is running in that mode. There is no TTY, so always pass --headless and name the services:
stackctl --headless -s web- Run it with your harness's background-task tool, so the session ends when the task or your conversation does. Never with
&,nohuporsetsid. - Find its port in
node_modules/.stackctl/modes/<mode>/running.json, which appears as soon as the session starts. - Wait until it is healthy on the event stream:
curl -sN "http://127.0.0.1:$port/events" \
| grep -m1 -E '"sessionReady":true|"type":"session","ready":true|"status":"stopped"'A line with "status":"stopped" means a service crashed while starting. Read the log. 4. Stop it when done: POST /command/quit, or stop the background task.
The config's infra comes up first on its own, and stays up after the session ends. For an e2e suite, call startStack() from its global setup instead. For a CI workflow you are asked to write, use stackctl start --detach.
Drive a running session
From a shell, in the session's mode:
stackctl service restart -s api # waits until it is ready again
stackctl service stop -s worker
stackctl service start -s worker # also adds a service the session was started withoutOr over the control API: POST /command/service/<start|stop|restart>/<name>.
stackctl stop ends the whole session, including one a person started. Run it only when asked.
After editing code
Read the service's command in stackctl show-config:
command.watches | After an edit to its code |
|---|---|
true | Nothing. Its dev script reloads itself. |
false | stackctl service restart -s <name> |
An edit to a .env file restarts the affected services on its own, so an unexpected restart may be an env edit, not a crash. An edit to stackctl.config.ts needs the session restarted.
Run a one-off command
Use the service's own environment. Don't export variables by hand.
stackctl run api seed # a package.json script or Makefile target
stackctl exec api -- node src/migrate.ts # any command
stackctl --mode test exec api -- node --test # in the test mode's environmentSee One-off commands. From a script, use exec(), run() or resolveEnv().
Infra
stackctl infra up # start the databases etc., wait until ready
stackctl infra docker-compose ps # what is running
stackctl infra docker-compose exec db psql -U app
stackctl infra docker-compose logs --tail 100 dbA session starts its infra itself. In a running session, restart one infra service with POST /command/infra/restart/<name>. See Infra.
Read logs
stackctl logs # the current session's whole log
stackctl logs --path # its path, to grep or tail
grep -i error "$(stackctl logs --path)"Each line is <timestamp> [<service>] <text>, the label padded with spaces before its ] so every line's text lines up: match one service's with grep '\[api *\]'. [stackctl] lines are the session's own, [control] lines are commands received over the control API, and [infra:<name>] lines are an infra service's. Earlier sessions' logs are kept beside the current one. See Logs.
Onboard a service
To add a service to stackctl:
- Give it a dev entry point. A
devscript in itspackage.json, or for a service without one, aMakefiletarget (recipe lines indented with a tab). If it already has an equivalent script under another name (start:dev,serve), rename it todevand fix the references, or setcommand. - Add a key to
servicesinstackctl.config.ts. The key is its name, and its directory is<serviceRootDir>/<key>:
'my-service': {
port: 3001,
healthcheck: '/health',
deps: [{ service: 'other-service', wait: true }],
},- Fill in what is true of it, per Services:
port: the port it listens on. It reads it fromPORT.healthcheck: a path, if it has a health endpoint.deps: the services it needs, withwait: truefor one it can't start without.command: ['<script>', { watches: false }]if its dev script doesn't reload on edits.urls: ['']for something a person opens in a browser.
- Prefer paths to full URLs (
'/health','/admin'), and a callback only for a value that depends on the environment. Config strings are not interpolated. - Check it:
stackctl show-config | jq '.services["my-service"]'.
Apps stackctl doesn't manage
If the repo has no stackctl config, or the app isn't in services, say so. That is a complete answer. Then check the app directly: find the port it binds, and
lsof -nP -iTCP:$PORT -sTCP:LISTEN
curl -sS -o /dev/null -w '%{http_code}\n' --max-time 2 "http://localhost:$PORT/"A service stackctl manages can also be running outside it, started by hand. /query/status can't see that, the port check can. Report what you find, and never kill it.