ai-crew-sync 0.7.1

MCP server that lets a team's AI coding agents (Claude Code, Codex, Cursor or any MCP client) exchange messages, coordinate tasks, share presence and keep shared notes, backed by Postgres
Documentation
# Makefile for ai-crew-sync — MCP coordination bus for AI coding agent teams.
#
# Everything that reaches a real environment is explicit and named as such:
# `deploy` deploys to a swarm, `up`/`down` act on the local compose stack,
# and images are published by CI on version tags — nothing here pushes one.
# `make check` is the pre-push gate and runs offline; `make test` needs
# docker for its throwaway Postgres.

# --project-directory keeps the repo root as the project dir, so compose
# reads ./.env and names the project from COMPOSE_PROJECT_NAME there even
# though the files live under Docker/.
COMPOSE      = docker compose --project-directory . -f Docker/docker-compose.yml
STACK       ?= crew
# The reverse proxy's network the bus attaches to (external in the compose
# file). `up` creates it locally when missing; `deploy` requires it to exist.
TRAEFIK_NETWORK ?= edge

TEST_PG_NAME  = ai-crew-sync-test-pg
# The JetStream fixture. Required from phase 4 of ADR 0001: a broker test
# that skips itself proves nothing and reads like a pass, so a missing
# broker is a visible failure rather than a silent one.
TEST_NATS_NAME  = ai-crew-sync-test-nats
TEST_NATS_PORT ?= 14222
TEST_NATS_IMAGE = nats:2.12-alpine
TEST_PG_PORT ?= 55432
TEST_PG_IMAGE ?= postgres:18-alpine

.DEFAULT_GOAL := all

.PHONY: all
all: check ## Default: the pre-push gate

.PHONY: help
help: ## List every target
	@grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
		| sort \
		| awk 'BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-14s\033[0m %s\n", $$1, $$2}'

# --- verification -----------------------------------------------------------

.PHONY: check
check: fmt-check lint validate config-check hooks-check recipes-check ## Pre-push gate: format, clippy, compose renders, config documented, hooks sane, commands match recipes. Offline.

.PHONY: pre-push
pre-push: check ## Alias for check

.PHONY: fmt
fmt: ## Format the Rust sources
	cargo fmt

.PHONY: fmt-check
fmt-check: ## Fail if a source file is unformatted
	cargo fmt --check

.PHONY: lint
lint: ## Clippy with warnings as errors, all targets
	cargo clippy --all-targets -- -D warnings

.PHONY: lint-fix
lint-fix: ## Apply the clippy fixes that are machine-applicable
	cargo clippy --fix --all-targets --allow-dirty -- -D warnings

# The one compose file must render from its defaults alone (a bare `make up`
# on a machine with no .env), and its build context must be the crate root:
# `config -q` validates shape but never resolves a build context, so a context
# pointing outside the repo renders fine and fails at build time.
.PHONY: validate
validate: ## Render the compose file from defaults and check its build context
	@env -u POSTGRES_PASSWORD -u BUS_VERSION -u BUS_IMAGE -u BUS_ALLOWED_HOSTS \
		-u BUS_DASHBOARD_SECRET -u BUS_PUBLIC_HOST $(COMPOSE) --env-file /dev/null config -q \
		&& echo "  ok  Docker/docker-compose.yml renders from defaults"
	@ctx=$$($(COMPOSE) config --format json 2>/dev/null \
		| python3 -c 'import json,sys; print(json.load(sys.stdin)["services"]["bus"]["build"]["context"])'); \
	if [ -d "$$ctx" ] && [ -f "$$ctx/Cargo.toml" ]; then \
		echo "  ok  + build context resolves to the crate root"; \
	else \
		echo "  FAIL  build context '$$ctx' is not the crate root"; exit 1; \
	fi

# A knob nobody can discover is a knob nobody can set. Every variable the
# server, the compose files, the client or the plugin reads must appear in
# .env.example — commented out is fine, absent is not. (Make's own variables
# are not user configuration; they are documented by `make help`.)
.PHONY: config-check
config-check: ## Fail if a configuration variable is missing from .env.example
	@missing=""; \
	for v in $$(grep -rhoE '\b(BUS|POSTGRES|DATABASE|RUST)_[A-Z_]+' \
			src Docker plugin 2>/dev/null | sort -u); do \
		case "$$v" in POSTGRES_USER|POSTGRES_DB) continue ;; esac; \
		grep -q "^#\? *$$v[=:]\?" .env.example || missing="$$missing $$v"; \
	done; \
	if [ -n "$$missing" ]; then \
		echo "undocumented in .env.example:$$missing"; exit 1; \
	fi; \
	echo "config: every variable is documented"

.PHONY: hooks-check
hooks-check: ## Plugin hook regression tests (no bus, no network)
	@sh plugin/scripts/test-hooks.sh

.PHONY: recipes
recipes: ## Regenerate plugin/commands/*.md from recipes/*.md (the recipes are the source)
	@python3 plugin/scripts/recipes.py generate

.PHONY: recipes-check
recipes-check: ## Fail if a slash command drifted from its recipe
	@python3 plugin/scripts/recipes.py check

.PHONY: test
test: ## Integration tests against a throwaway Postgres (needs docker)
	@docker rm -f $(TEST_PG_NAME) >/dev/null 2>&1 || true
	@docker run -d --name $(TEST_PG_NAME) -p $(TEST_PG_PORT):5432 \
		-e POSTGRES_USER=test -e POSTGRES_PASSWORD=test -e POSTGRES_DB=test \
		$(TEST_PG_IMAGE) >/dev/null
	@until docker exec $(TEST_PG_NAME) pg_isready -U test >/dev/null 2>&1; do sleep 1; done
	@docker rm -f $(TEST_NATS_NAME) >/dev/null 2>&1 || true
	@# Docker/nats-test.conf raises max_payload, which nats-server accepts
	@# only in a config file and whose 1 MiB default refuses a 1 MiB body
	@# once envelope headers are added. Measured — see the adapter test.
	@docker run -d --name $(TEST_NATS_NAME) -p $(TEST_NATS_PORT):4222 \
		-v "$(PWD)/Docker/nats-test.conf:/etc/nats/nats.conf:ro" \
		$(TEST_NATS_IMAGE) -js -c /etc/nats/nats.conf >/dev/null
	@until docker logs $(TEST_NATS_NAME) 2>&1 | grep -q 'Server is ready'; do sleep 1; done
	@AI_CREW_SYNC_REQUIRE_DB=1 AI_CREW_SYNC_REQUIRE_NATS=1 \
		TEST_DATABASE_URL=postgres://test:test@localhost:$(TEST_PG_PORT)/test \
		TEST_NATS_URL=nats://127.0.0.1:$(TEST_NATS_PORT) cargo test; \
		status=$$?; \
		docker rm -f $(TEST_PG_NAME) >/dev/null; \
		docker rm -f $(TEST_NATS_NAME) >/dev/null; \
		exit $$status

# --- build and run ----------------------------------------------------------

.PHONY: build
build: ## Release build of the binary
	cargo build --release

.PHONY: run
run: ## Run the server from source against $$DATABASE_URL
	cargo run -- serve

# --- the stack --------------------------------------------------------------

# The compose file carries a build block for up-dev; `up` must never build,
# so an image that is missing locally is pulled instead.
.PHONY: up
up: network ## Start the local stack (published image), detached
	$(COMPOSE) up -d --no-build

# A local tag, so a checkout build never shadows the published image tag.
.PHONY: up-dev
up-dev: network ## Start the local stack built from this checkout
	BUS_IMAGE=ai-crew-sync BUS_VERSION=dev $(COMPOSE) up -d --build

.PHONY: network
network: ## Create the proxy network locally if it does not exist
	@docker network inspect $(TRAEFIK_NETWORK) >/dev/null 2>&1 \
		|| docker network create $(TRAEFIK_NETWORK) >/dev/null

.PHONY: down
down: ## Stop the stack (volumes are kept)
	$(COMPOSE) down

.PHONY: restart
restart: down up ## Restart the stack

.PHONY: ps
ps: ## Show stack state
	$(COMPOSE) ps

.PHONY: logs
logs: ## Follow stack logs
	$(COMPOSE) logs -f

# Preflight before the cluster, not after. The compose file has a working
# default for everything so it boots on a laptop; these checks are what keeps
# a production stack from coming up with the laptop password, a moving tag
# that makes a rolling restart non-deterministic, or per-replica dashboard
# sessions.
.PHONY: deploy-check
deploy-check: ## Verify production values are present and safe (no cluster contact)
	@fail=0; 	[ -n "$$POSTGRES_PASSWORD" ] || { echo "POSTGRES_PASSWORD is not set"; fail=1; }; 	[ "$$POSTGRES_PASSWORD" != "change-me" ] || { echo "POSTGRES_PASSWORD is still the example value"; fail=1; }; 	[ -n "$$BUS_VERSION" ] || { echo "BUS_VERSION is not set (pin an immutable tag)"; fail=1; }; 	case "$$BUS_VERSION" in latest|"") echo "BUS_VERSION must be immutable, not 'latest'"; fail=1 ;; esac; 	[ -n "$$BUS_DASHBOARD_SECRET" ] || { echo "BUS_DASHBOARD_SECRET is not set (dashboard sessions would not survive a restart or work across replicas)"; fail=1; }; 	if [ -z "$$BUS_ALLOWED_HOSTS" ]; then 		echo "BUS_ALLOWED_HOSTS is not set (use your hostname, or '*' if a proxy validates Host)"; fail=1; 	elif [ "$$BUS_ALLOWED_HOSTS" = "*" ]; then 		echo "note: BUS_ALLOWED_HOSTS=* — only safe behind a proxy that validates the Host header"; 	fi; 	if [ "$$TRAEFIK_ENABLE" = "true" ] && [ -z "$$BUS_PUBLIC_HOST" ]; then 		echo "TRAEFIK_ENABLE=true but BUS_PUBLIC_HOST is not set (the proxy needs the hostname to route)"; fail=1; 	fi; 	[ $$fail -eq 0 ] || { echo ""; echo "refusing to deploy: fix the above, then re-run"; exit 1; }; 	echo "deploy preflight: ok"

.PHONY: deploy
deploy: deploy-check ## Deploy to the current Docker Swarm as stack '$(STACK)' -- REACHES A REAL ENVIRONMENT
	@docker network inspect $(TRAEFIK_NETWORK) >/dev/null 2>&1 \
		|| { echo "network '$(TRAEFIK_NETWORK)' does not exist on this swarm: the proxy's stack must create it (attachable overlay), or set TRAEFIK_NETWORK"; exit 1; }
	docker stack deploy -c Docker/docker-compose.yml $(STACK)

# --- housekeeping -----------------------------------------------------------

.PHONY: clean
clean: ## Stop the stack and remove its volumes -- DESTROYS LOCAL DATA
	$(COMPOSE) down -v

.PHONY: dsstore
dsstore: ## Delete stray .DS_Store files
	@find . -name '.DS_Store' -type f -delete 2>/dev/null || true