home / skills / mcouthon / agents / makefile

This skill helps you create and manage Makefiles for reliable AI agent process lifecycle with PID tracking and logging.

npx playbooks add skill mcouthon/agents --skill makefile

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
13.2 KB
---
name: makefile
description: "Use when creating Makefiles for process lifecycle management with PID tracking, logging, and status monitoring. Triggers on: 'use makefile mode', 'makefile', 'create makefile', 'process management', 'background jobs', 'start/stop services'. Full access mode - can create/modify Makefiles."
allowed-tools: [Read, Edit, Write, Bash, Grep, Glob]
---

# Makefile Mode

Create and manage Makefiles optimized for AI agent interaction and process lifecycle management.

## Core Philosophy

> "Start clean. Stop clean. Log everything. Know your state."

**Principles**:

- **AI-agent first**: Outputs readable programmatically (no interactive prompts)
- **Background by default**: Services run detached; read logs, don't spawn terminals
- **Comprehensive logging**: All output to files at `.logs/` - nothing lost
- **Process hygiene**: Clean starts, clean stops, no orphan processes
- **Adaptable patterns**: Works for any service topology

## Pre-Implementation Discovery

Before creating a Makefile, determine:

### Service Topology

- [ ] What services exist? (backend, frontend, workers, etc.)
- [ ] Do any services depend on others? (start order)
- [ ] Are there external dependencies? (databases, emulators, etc.)

### Startup Requirements

- [ ] What commands start each service?
- [ ] What environment variables are needed?
- [ ] What ports are used? (must be unique per-service)
- [ ] Any initialization steps? (migrations, seeds, etc.)

### Testing & Quality

- [ ] What test commands exist? (unit, integration, e2e)
- [ ] What prerequisites for tests? (docker, emulators, etc.)
- [ ] What linting/formatting tools? (eslint, ruff, mypy, etc.)

### Project Context

- [ ] Language/framework? (affects conventions)
- [ ] Development vs Production behavior?
- [ ] Team conventions? (existing practices to preserve)

## Makefile Architecture

Standard structure (in order):

```makefile
# 1. Configuration Variables
# 2. Directory Setup
# 3. Service Lifecycle Targets (run-*, stop-*)
# 4. Combined Operations (run, stop, restart)
# 5. Testing & Quality (test, lint)
# 6. Utility Targets (logs, status, help)
# 7. .PHONY declarations
```

## Core Patterns Library

### A. Starting a Service (Background with PID Tracking)

```makefile
run-backend:
	@mkdir -p .pids .logs
	@if lsof -ti:$(BACKEND_PORT) > /dev/null 2>&1; then \
		echo "โŒ Backend already running on port $(BACKEND_PORT)"; \
		exit 1; \
	fi
	@echo "๐Ÿš€ Starting backend on port $(BACKEND_PORT)..."
	@nohup $(BACKEND_CMD) > .logs/backend.log 2>&1 & echo $$! > .pids/backend.pid
	@echo "โœ… Backend started (PID: $$(cat .pids/backend.pid))"
```

### B. Stopping a Service (Process Group Cleanup)

```makefile
stop-backend:
	@if [ -f .pids/backend.pid ]; then \
		PID=$$(cat .pids/backend.pid); \
		if ps -p $$PID > /dev/null 2>&1; then \
			echo "๐Ÿ›‘ Stopping backend (PID: $$PID)..."; \
			kill -TERM -- -$$PID 2>/dev/null || kill $$PID; \
			rm .pids/backend.pid; \
			echo "โœ… Backend stopped"; \
		else \
			echo "โš ๏ธ  Backend process not found, cleaning up PID file"; \
			rm .pids/backend.pid; \
		fi \
	else \
		echo "โ„น๏ธ  Backend not running"; \
	fi
```

### C. Status Checking

```makefile
status:
	@echo "๐Ÿ“Š Service Status:"
	@echo ""
	@for service in backend frontend; do \
		if [ -f .pids/$$service.pid ]; then \
			PID=$$(cat .pids/$$service.pid); \
			if ps -p $$PID > /dev/null 2>&1; then \
				echo "โœ… $$service: running (PID: $$PID)"; \
			else \
				echo "โŒ $$service: stopped (stale PID file)"; \
			fi \
		else \
			echo "โšช $$service: not running"; \
		fi; \
	done
```

### D. Log Tailing

```makefile
logs:
	@if [ -f .logs/backend.log ] || [ -f .logs/frontend.log ]; then \
		tail -n 50 .logs/*.log 2>/dev/null; \
	else \
		echo "No logs found"; \
	fi

logs-follow:
	@tail -f .logs/*.log 2>/dev/null
```

### E. Combined Operations

```makefile
run: run-backend run-frontend
stop: stop-frontend stop-backend  # Reverse order for clean shutdown
restart: stop run
```

### F. Testing with Prerequisites

```makefile
test: test-setup
	@echo "๐Ÿงช Running tests..."
	@$(TEST_CMD)

test-setup:
	@if [ -n "$(DOCKER_COMPOSE_FILE)" ] && [ -f "$(DOCKER_COMPOSE_FILE)" ]; then \
		docker-compose -f $(DOCKER_COMPOSE_FILE) up -d; \
	fi
```

### G. Help Target (Self-Documenting)

```makefile
.DEFAULT_GOAL := help

help:
	@echo "Available targets:"
	@echo ""
	@echo "  make run              Start all services"
	@echo "  make stop             Stop all services"
	@echo "  make restart          Restart all services"
	@echo "  make status           Show service status"
	@echo "  make logs             Show recent logs"
	@echo "  make logs-follow      Follow logs in real-time"
	@echo "  make test             Run all tests"
	@echo "  make lint             Run linters and formatters"
	@echo ""
	@echo "Individual services:"
	@echo "  make run-backend      Start backend only"
	@echo "  make run-frontend     Start frontend only"
	@echo "  make stop-backend     Stop backend only"
	@echo "  make stop-frontend    Stop frontend only"
```

## Adaptation Patterns

| Scenario            | Adaptation                                                    |
| ------------------- | ------------------------------------------------------------- |
| Multiple backends   | Use suffix naming: `run-api`, `run-worker`, etc.              |
| Database migrations | Add `migrate` target, make `run-backend` depend on it         |
| Emulators           | Treat like any other service with PID tracking                |
| Docker Compose      | Wrap docker-compose commands, track container IDs             |
| Monorepo            | Use subdirectory variables: `cd $(API_DIR) && ...`            |
| Multiple test types | Separate targets: `test-unit`, `test-integration`, `test-e2e` |
| Watch modes         | Use separate watch targets, don't mix with regular run        |

## Best Practices Checklist

Before completing a Makefile, verify:

- [ ] All targets are `.PHONY` (or appropriately not)
- [ ] Port numbers are configurable via variables
- [ ] Unique ports per service (no conflicts)
- [ ] All logs go to `.logs/` directory
- [ ] All PIDs go to `.pids/` directory
- [ ] Process group killing (handles child processes)
- [ ] Port conflict detection before start
- [ ] Human-readable output (colors/emojis)
- [ ] `help` target is default (listed first or `.DEFAULT_GOAL`)
- [ ] Variables use `:=` (simple expansion)
- [ ] Error messages are clear and actionable
- [ ] Status command shows actual state
- [ ] Clean shutdown on stop (SIGTERM first)
- [ ] Idempotent operations (safe to run twice)

## Common Issues & Solutions

| Problem                              | Solution                                    |
| ------------------------------------ | ------------------------------------------- |
| PID file exists but process dead     | Check `ps -p $PID` before using PID file    |
| Child processes survive parent kill  | Use `kill -TERM -- -$PID` (process group)   |
| Port already in use                  | Check with `lsof -ti:$PORT` before start    |
| Logs interleaved/unreadable          | Separate log files per service              |
| Service starts but immediately exits | Redirect stderr: `2>&1`, check `.logs/`     |
| Make variables not evaluated         | Use `:=` not `=`, check `$$` vs `$`         |
| Colors don't show in logs            | Use `unbuffer` or configure service for TTY |
| Can't stop service (permission)      | Run make with same user that started it     |

## Implementation Workflow

### Creating a New Makefile

1. **Discovery**: Ask questions (see Discovery section)
2. **Configuration**: Set up variables (ports, commands, paths)
3. **Core services**: Implement run/stop for each service
4. **Combined ops**: Add run/stop/restart for all services
5. **Utilities**: Add status, logs, help
6. **Testing**: Add test targets with prerequisites
7. **Quality**: Add lint/format targets
8. **Validation**: Test each target, verify idempotency
9. **Documentation**: Ensure help is complete and accurate

### Amending an Existing Makefile

1. **Read current Makefile**: Understand existing structure
2. **Identify gaps**: Compare against best practices checklist
3. **Plan changes**: Determine what to add/modify
4. **Preserve conventions**: Keep existing naming/style
5. **Incremental changes**: Add features one at a time
6. **Test each change**: Verify nothing breaks
7. **Update help**: Reflect new targets

## Complete Template

A minimal working template for a full-stack app:

```makefile
# =============================================================================
# Configuration
# =============================================================================
BACKEND_PORT := 3001
FRONTEND_PORT := 3000
BACKEND_CMD := npm run dev --prefix backend
FRONTEND_CMD := npm run dev --prefix frontend
TEST_CMD := npm test

# =============================================================================
# Directory Setup
# =============================================================================
$(shell mkdir -p .pids .logs)

# =============================================================================
# Service Lifecycle
# =============================================================================
run-backend:
	@if lsof -ti:$(BACKEND_PORT) > /dev/null 2>&1; then \
		echo "โŒ Backend already running on port $(BACKEND_PORT)"; \
		exit 1; \
	fi
	@echo "๐Ÿš€ Starting backend on port $(BACKEND_PORT)..."
	@nohup $(BACKEND_CMD) > .logs/backend.log 2>&1 & echo $$! > .pids/backend.pid
	@echo "โœ… Backend started (PID: $$(cat .pids/backend.pid))"

run-frontend:
	@if lsof -ti:$(FRONTEND_PORT) > /dev/null 2>&1; then \
		echo "โŒ Frontend already running on port $(FRONTEND_PORT)"; \
		exit 1; \
	fi
	@echo "๐Ÿš€ Starting frontend on port $(FRONTEND_PORT)..."
	@nohup $(FRONTEND_CMD) > .logs/frontend.log 2>&1 & echo $$! > .pids/frontend.pid
	@echo "โœ… Frontend started (PID: $$(cat .pids/frontend.pid))"

stop-backend:
	@if [ -f .pids/backend.pid ]; then \
		PID=$$(cat .pids/backend.pid); \
		if ps -p $$PID > /dev/null 2>&1; then \
			echo "๐Ÿ›‘ Stopping backend (PID: $$PID)..."; \
			kill -TERM -- -$$PID 2>/dev/null || kill $$PID; \
			rm .pids/backend.pid; \
			echo "โœ… Backend stopped"; \
		else \
			echo "โš ๏ธ  Backend not found, cleaning up PID file"; \
			rm .pids/backend.pid; \
		fi \
	else \
		echo "โ„น๏ธ  Backend not running"; \
	fi

stop-frontend:
	@if [ -f .pids/frontend.pid ]; then \
		PID=$$(cat .pids/frontend.pid); \
		if ps -p $$PID > /dev/null 2>&1; then \
			echo "๐Ÿ›‘ Stopping frontend (PID: $$PID)..."; \
			kill -TERM -- -$$PID 2>/dev/null || kill $$PID; \
			rm .pids/frontend.pid; \
			echo "โœ… Frontend stopped"; \
		else \
			echo "โš ๏ธ  Frontend not found, cleaning up PID file"; \
			rm .pids/frontend.pid; \
		fi \
	else \
		echo "โ„น๏ธ  Frontend not running"; \
	fi

# =============================================================================
# Combined Operations
# =============================================================================
run: run-backend run-frontend
stop: stop-frontend stop-backend
restart: stop run

# =============================================================================
# Testing & Quality
# =============================================================================
test:
	@echo "๐Ÿงช Running tests..."
	@$(TEST_CMD)

lint:
	@echo "๐Ÿ” Running linters..."
	@npm run lint 2>&1 || true

# =============================================================================
# Utilities
# =============================================================================
status:
	@echo "๐Ÿ“Š Service Status:"
	@echo ""
	@for service in backend frontend; do \
		if [ -f .pids/$$service.pid ]; then \
			PID=$$(cat .pids/$$service.pid); \
			if ps -p $$PID > /dev/null 2>&1; then \
				echo "โœ… $$service: running (PID: $$PID)"; \
			else \
				echo "โŒ $$service: stopped (stale PID file)"; \
			fi \
		else \
			echo "โšช $$service: not running"; \
		fi; \
	done

logs:
	@tail -n 50 .logs/*.log 2>/dev/null || echo "No logs found"

logs-follow:
	@tail -f .logs/*.log 2>/dev/null

clean:
	@rm -rf .pids .logs
	@echo "๐Ÿงน Cleaned up PID and log files"

# =============================================================================
# Help
# =============================================================================
.DEFAULT_GOAL := help

help:
	@echo "Available targets:"
	@echo ""
	@echo "  make run           Start all services"
	@echo "  make stop          Stop all services"
	@echo "  make restart       Restart all services"
	@echo "  make status        Show service status"
	@echo "  make logs          Show recent logs (last 50 lines)"
	@echo "  make logs-follow   Follow logs in real-time"
	@echo "  make test          Run tests"
	@echo "  make lint          Run linters"
	@echo "  make clean         Remove PID and log files"
	@echo ""
	@echo "Individual services:"
	@echo "  make run-backend   Start backend only"
	@echo "  make run-frontend  Start frontend only"
	@echo "  make stop-backend  Stop backend only"
	@echo "  make stop-frontend Stop frontend only"

# =============================================================================
# .PHONY
# =============================================================================
.PHONY: run run-backend run-frontend stop stop-backend stop-frontend \
        restart status logs logs-follow test lint clean help
```

## Gitignore Additions

Remind users to add these to `.gitignore`:

```
.pids/
.logs/
```

Overview

This skill helps create Makefiles for process lifecycle management with PID tracking, structured logging, and status monitoring. It generates deterministic, agent-friendly targets to start, stop, restart, inspect, and tail services running in the background. The templates prioritize clean startup/shutdown, per-service logs in .logs/, and PID files in .pids/ for reliable automation.

How this skill works

The skill emits a Makefile scaffold that defines configuration variables, directory setup, per-service run/stop targets, combined operations (run/stop/restart), testing and lint targets, and utility commands (status, logs, logs-follow, clean, help). Each run target checks for port conflicts, creates .pids and .logs, starts the service with nohup, and records the PID. Stop targets validate PID files and kill process groups to avoid orphaned children.

When to use it

  • When you need reproducible, non-interactive service lifecycle control for local development.
  • When multiple services or dependent services must be started and stopped in a defined order.
  • When you want deterministic logs and PID tracking for CI, agent automation, or debugging.
  • When adding lightweight process management without Docker or replacing ad-hoc scripts.
  • When introducing tests and environment-dependent setup steps into project workflows.

Best practices

  • Keep ports configurable via variables and ensure unique ports per service.
  • Always write logs to .logs/ and PID files to .pids/ and add both to .gitignore.
  • Use process-group kills (kill -- -PID) to terminate child processes cleanly.
  • Make run targets idempotent: check lsof -ti:$PORT before starting and error clearly.
  • Provide a help target as the default goal and list individual service targets.
  • Separate watch/dev targets from production-style run targets to avoid mixing behaviors.

Example use cases

  • Full-stack dev repo: run-backend and run-frontend targets that start both services and write logs to .logs/
  • Worker + API: run-api, run-worker, with run depending on migrations or test-setup targets
  • Monorepo: service targets that cd into subdirectories using variables like API_DIR before executing commands
  • CI job: test target that boots required emulators or docker-compose prerequisites before running test suite
  • Debugging: status target to report running/stale PID files and logs-follow to inspect live output

FAQ

What if a PID file exists but the process is dead?

Stop targets check ps -p $PID before killing. If the process is absent the PID file is removed and a warning is emitted.

How are child processes handled when stopping a service?

Stops use process-group termination (kill -TERM -- -PID) first, then fallback to kill PID to ensure children are terminated.