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
- Run stackctl through the package manager:
pnpm stackctl …(ornpx,yarn,bunx). It is a dev dependency, not on yourPATH. The recipes here writestackctlfor short. - 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 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. - Ask stackctl:
stackctl statussays whether the mode's session runs, each service's state and readiness, and the last commands that acted on it.stackctl status --jsonhas the same forjq:.status.services,.status.ready. Seestackctl status.
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. |
No services yet 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-state","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.
Run tests
Tests that call the services need them running, in the test mode: its own ports and its own database, so a test never writes to the data you develop with.
- Find the test mode: a
.env.testbesidestackctl.config.ts. Without one, ask. - Start a test session of the services the tests call, as in Start a stack:
stackctl --mode test --headless -s api. It runs beside a development session when the services it starts have ports of their own in the test mode. - Run the tests in its environment:
stackctl --mode test run api test. - Stop it:
POST /command/quitto the port innode_modules/.stackctl/modes/test/running.json.
A suite that calls startStack() starts its own session: just run it. Never run tests against the development session.
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.
A service is down
- Read its state, as in Is it running?:
stopped, orrunningbut not ready. A dev script that watches its files, such asnode --watch, outlives its app: when the app exits it staysrunningand waits for an edit. - Find why it stopped:
stackctl logs | grep '\[api *\]' | grep -iE 'error|exception'. A crash prints its error first, then its stack and anything it dumps, so its last lines rarely say why. - Find what acted on it. The event log records every command that changed the stack, by a key, the control API or a stackctl command, and who ran it.
stackctl statuslists the last ones;stackctl events -n 50has them all, with each service's state changes: an infra service restarted just before the crash is the likely cause. - Restart it:
stackctl service restart -s apiwaits until it is ready again. - Read its lines again, to see it come up or why it failed again.
Don't run the service's command yourself while the session runs it: your copy takes its port.
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 start # 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 | grep -i error
stackctl logs --path # its path, to read or tailPipe stackctl logs rather than substituting $(stackctl logs --path): a harness can't check a command substitution before it runs, so it asks the user.
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.