makefile

SKILL.md

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

# 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)

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)

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

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

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

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

F. Testing with Prerequisites

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)

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

# =============================================================================
# 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/
Weekly Installs
11
Repository
mcouthon/agents
GitHub Stars
37
First Seen
Jan 28, 2026
Installed on
mcpjam11
roo11
kilo11
claude-code11
junie11
windsurf11