Skip to content

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, pkill or lsof … | kill a stackctl process or its port. Stop a session with POST /command/quit or stackctl stop, and restart a service with stackctl 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-config prints the resolved ones.
  • Don't pass --kill-others or killOthers: true unless the user said to.
  • Don't use stackctl start --detach. It is for CI and is refused for an agent.
  • stackctl infra up --reset and stackctl infra destroy delete data. Only on request.
  • Use stackctl infra docker-compose …, never a bare docker compose …, which talks to a different Compose project.
  • Write stackctl, never its alias sctl. Guide pages type sctl; permission rules and scripts match the full name.

Is it running? ​

  1. Is the repo managed by stackctl? Look for stackctl.config.ts at or above the app's directory. If there is none, see Apps stackctl doesn't manage.
  2. Is the app one of its services? stackctl show-config | jq '.services | keys'. The key is the name -s and the control API take. It need not match the directory.
  3. 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.
  4. Read the service's state from GET /query/status:
bash
curl -s "http://127.0.0.1:$port/query/status" | jq '{ services, ready, sessionReady }'
services.<name>Meaning
runningIts process is live. ready.<name> says whether it passes its healthcheck.
waitingHeld back by a service it waits for.
restartingOn its way back up.
stoppedNot running: it exited, crashed or was never started.
not a keyConfigured, 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 ​

bash
stackctl show-config | jq '.services | map_values({ port, publicOrigin, healthcheck })'

No session is needed. Pass the --mode the session runs in.

  • publicOrigin is where a browser reaches the service, and the address the app itself is configured for. Through the reverse proxy, a service that is down answers 503 with its status, not a connection error.
  • port is the service itself: curl http://localhost:<port>/…. It works in every setup, while the service runs.
  • *.localhost names resolve in browsers, not in curl. 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:

bash
stackctl --headless -s web
  1. Run it with your harness's background-task tool, so the session ends when the task or your conversation does. Never with &, nohup or setsid.
  2. Find its port in node_modules/.stackctl/modes/<mode>/running.json, which appears as soon as the session starts.
  3. Wait until it is healthy on the event stream:
bash
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:

bash
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 without

Or 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.watchesAfter an edit to its code
trueNothing. Its dev script reloads itself.
falsestackctl 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.

bash
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 environment

See One-off commands. From a script, use exec(), run() or resolveEnv().

Infra ​

bash
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 db

A session starts its infra itself. In a running session, restart one infra service with POST /command/infra/restart/<name>. See Infra.

Read logs ​

bash
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:

  1. Give it a dev entry point. A dev script in its package.json, or for a service without one, a Makefile target (recipe lines indented with a tab). If it already has an equivalent script under another name (start:dev, serve), rename it to dev and fix the references, or set command.
  2. Add a key to services in stackctl.config.ts. The key is its name, and its directory is <serviceRootDir>/<key>:
ts
'my-service': {
  port: 3001,
  healthcheck: '/health',
  deps: [{ service: 'other-service', wait: true }],
},
  1. Fill in what is true of it, per Services:
    • port: the port it listens on. It reads it from PORT.
    • healthcheck: a path, if it has a health endpoint.
    • deps: the services it needs, with wait: true for 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.
  2. Prefer paths to full URLs ('/health', '/admin'), and a callback only for a value that depends on the environment. Config strings are not interpolated.
  3. 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

bash
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.

Released under the MIT License.