.PHONY: install install-cli dashboard-install dashboard-check db db-tools migrate seed api dashboard check test test-python clean register-project register-codex-project register-mcp bootstrap-env validate-adr add-domain rename-domain add-repo list-repos register-path cleanup-stale tunnels-up tunnels-status tunnels-check bridges install-hooks install-hooks-all gitea-inventory token-reconcile COMPOSE = docker compose -f infra/docker-compose.yml --env-file .env PYTHON ?= python3 # Codex/WSL non-login shells may not source ~/.profile; keep uv discoverable. UV ?= $(shell command -v uv 2>/dev/null || if [ -x "$$HOME/.local/bin/uv" ]; then printf "%s" "$$HOME/.local/bin/uv"; else printf "%s" "uv"; fi) start: @echo "# run in different terminals" @echo "make db # docker compose up postgres" @echo "make api # start backend api" @echo "make mcp-http # start state-hub mcp service" @echo "make dashboard # Observable dev server on :3000" @echo "make bridges # Set up ssh bridges for cross machines access" install: $(UV) sync dashboard/node_modules/.bin/observable: dashboard/package.json dashboard/package-lock.json cd dashboard && npm ci dashboard-install: dashboard/node_modules/.bin/observable dashboard-check: dashboard-install cd dashboard && npm run build ## Symlink the custodian CLI into ~/.local/bin so it's on PATH system-wide install-cli: install mkdir -p ~/.local/bin ln -sf "$(shell pwd)/.venv/bin/custodian" ~/.local/bin/custodian ln -sf "$(shell pwd)/.venv/bin/statehub" ~/.local/bin/statehub @echo "Installed: custodian → $$(readlink -f ~/.local/bin/custodian)" @echo "Installed: statehub → $$(readlink -f ~/.local/bin/statehub)" @echo "Make sure ~/.local/bin is on your PATH:" @echo " echo 'export PATH=\"\$$HOME/.local/bin:\$$PATH\"' >> ~/.bashrc && source ~/.bashrc" db: $(COMPOSE) up -d postgres db-tools: $(COMPOSE) --profile tools up -d migrate: $(UV) run alembic upgrade head seed: $(UV) run python scripts/seed.py ## Start (or restart) the MCP SSE server on :8001 — primary transport for Claude Code. ## Remote clients (e.g. COULOMBCORE) connect via the ops-bridge tunnel (port 18001). ## Registration: claude mcp add-json -s user state-hub '{"type":"sse","url":"http://127.0.0.1:8001/sse"}' mcp-http: @fuser -k 8001/tcp 2>/dev/null && echo "Stopped running MCP server" || true MCP_TRANSPORT=sse MCP_PORT=8001 $(UV) run python mcp_server/server.py dashboard: @fuser -k 3000/tcp 2>/dev/null && echo "Stopped running dashboard" || true $(MAKE) dashboard-install cd dashboard && npm run dev check: curl -sf http://127.0.0.1:8000/state/health | python3 -m json.tool test: test-python dashboard-check test-python: TEST_DATABASE_URL=postgresql+asyncpg://custodian:changeme@127.0.0.1:5432/custodian_test \ $(UV) run pytest -x -q ## ops-bridge managed tunnels ## Requires ops-bridge: bridge is at /home/worsch/.local/bin/bridge tunnels-up: bridge up tunnels-status: bridge status ## End-to-end check: verifies SSH process alive + remote port listening on COULOMBCORE. ## Exits non-zero if any tunnel is not fully operational. tunnels-check: bridge check ## Ensure all ops-bridge tunnels are up and healthy. ## Brings up any stopped/stale tunnels, shows final status, exits non-zero if anything is still down. bridges: @echo "==> Bringing up all tunnels..." bridge up @echo "" @echo "==> Tunnel status:" bridge status @echo "" @echo "==> Checking tunnel health..." bridge check ## Start (or restart) the full backend — db + migrate + uvicorn. ## Stops uvicorn on :8000 if already running, then starts fresh. api: db @echo "Waiting for postgres..."; \ for i in 1 2 3 4 5 6 7 8 9 10; do \ nc -z 127.0.0.1 5432 2>/dev/null && break; \ sleep 1; \ done $(MAKE) migrate @fuser -k 8000/tcp 2>/dev/null && echo "Stopped running API" || true $(UV) run uvicorn api.main:app --reload --reload-dir api --reload-dir mcp_server --reload-dir task_flow_engine --host 127.0.0.1 --port 8000 ## Register a project (Claude Code): make register-project DOMAIN=railiance PROJECT_PATH=/home/worsch/railiance register-project: @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-project DOMAIN= PROJECT_PATH="; exit 1) @test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1) scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)" ## Register a Codex project (AGENTS.md + HTTP API): make register-codex-project DOMAIN=capabilities PROJECT_PATH=/home/worsch/my-repo register-codex-project: @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-codex-project DOMAIN= PROJECT_PATH="; exit 1) @test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1) scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)" --codex ## Register State Hub MCP for Claude Code. Optional: make register-mcp MCP_URL=http://127.0.0.1:18001/sse register-mcp: scripts/register-mcp.sh \ $(if $(MCP_URL),--url "$(MCP_URL)",) \ $(if $(API_BASE),--api-base "$(API_BASE)",) \ $(if $(DRY_RUN),--dry-run,) ## Bootstrap a new operator/collaborator environment. Optional: make bootstrap-env ARGS="--install-missing" bootstrap-env: scripts/bootstrap-env.sh $(ARGS) ## Add a second repo to an existing domain: make add-repo DOMAIN=railiance REPO_PATH=/home/worsch/railiance-infra add-repo: @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1) @test -n "$(REPO_PATH)" || (echo "ERROR: REPO_PATH is required."; exit 1) scripts/register_project.sh "$(DOMAIN)" "$(REPO_PATH)" --additional ## Create a new domain: make add-domain DOMAIN=my_domain NAME="My Domain" add-domain: @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required (slug)."; exit 1) @test -n "$(NAME)" || (echo "ERROR: NAME is required (display name)."; exit 1) curl -sf -X POST http://127.0.0.1:8000/domains/ \ -H "Content-Type: application/json" \ -d "{\"slug\": \"$(DOMAIN)\", \"name\": \"$(NAME)\"}" | python3 -m json.tool ## Rename a domain: make rename-domain DOMAIN=old_slug NEW_SLUG=new_slug NEW_NAME="New Name" rename-domain: @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN (old slug) is required."; exit 1) @test -n "$(NEW_SLUG)" || (echo "ERROR: NEW_SLUG is required."; exit 1) @test -n "$(NEW_NAME)" || (echo "ERROR: NEW_NAME is required."; exit 1) curl -sf -X PATCH http://127.0.0.1:8000/domains/$(DOMAIN)/rename \ -H "Content-Type: application/json" \ -d "{\"new_slug\": \"$(NEW_SLUG)\", \"new_name\": \"$(NEW_NAME)\"}" | python3 -m json.tool ## Register this machine's local path for a repo: make register-path REPO=marki-docx PATH=/home/tegwick/marki-docx register-path: @test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make register-path REPO= PATH="; exit 1) @test -n "$(PATH)" || (echo "ERROR: PATH is required. Usage: make register-path REPO= PATH="; exit 1) curl -sf -X POST "http://127.0.0.1:8000/repos/$(REPO)/paths" \ -H "Content-Type: application/json" \ -d "{\"host\": \"$$(hostname)\", \"path\": \"$(PATH)\"}" | python3 -m json.tool ## List repos for a domain: make list-repos DOMAIN=railiance list-repos: @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1) curl -sf "http://127.0.0.1:8000/repos/?domain=$(DOMAIN)" | python3 -m json.tool ## Ingest SBOM data for a repo (all mechanisms: lockfiles + ansible + sbom-tools.yaml). ## Auto-detect all sources: make ingest-sbom REPO=the-custodian REPO_PATH=/home/worsch/the-custodian ## Single lockfile (explicit): make ingest-sbom REPO=the-custodian LOCKFILE=/path/to/uv.lock ## Dry-run (no submit): make ingest-sbom REPO=the-custodian REPO_PATH=... DRY_RUN=1 ## Tip: run capture-tools first for repos with system-level tool dependencies. ingest-sbom: @test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1) $(UV) run python scripts/ingest_sbom.py --repo "$(REPO)" \ $(if $(LOCKFILE),--lockfile "$(LOCKFILE)") \ $(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \ $(if $(DRY_RUN),--dry-run) ## Ingest capability declarations from SCOPE.md into the catalog. ## Usage: make ingest-capabilities REPO=the-custodian [REPO_PATH=/home/worsch/the-custodian] ## Or: make ingest-capabilities-all ## Add DRY_RUN=1 to preview without writing. ingest-capabilities: @test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1) $(UV) run python scripts/ingest_capabilities.py --repo "$(REPO)" \ $(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \ $(if $(DRY_RUN),--dry-run) ingest-capabilities-all: $(UV) run python scripts/ingest_capabilities.py --all \ $(if $(DRY_RUN),--dry-run) ## Check Repository Definition of Integrated (DoI) criteria for a repo. ## Usage: make check-doi REPO=llm-connect ## Or: make check-doi-all ## Add JSON=1 for machine-readable output. check-doi: @test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1) $(UV) run python scripts/check_doi.py --repo "$(REPO)" $(if $(JSON),--json) check-doi-all: $(UV) run python scripts/check_doi.py --all $(if $(JSON),--json) ## Ingest tpsc.yaml service declarations from a repo into the TPSC catalog. ## Usage: make ingest-tpsc REPO=llm-connect ## Or: make ingest-tpsc-all ## Add DRY_RUN=1 to preview without writing. ingest-tpsc: @test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1) $(UV) run python scripts/ingest_tpsc.py --repo "$(REPO)" \ $(if $(DRY_RUN),--dry-run) ingest-tpsc-all: $(UV) run python scripts/ingest_tpsc.py --all \ $(if $(DRY_RUN),--dry-run) ## Run SBOM capture agent for a repo — generates/updates sbom-tools.yaml. ## Usage: make capture-tools REPO=railiance-infra [REPO_PATH=/home/worsch/railiance-infra] ## Add DRY_RUN=1 to preview without writing. capture-tools: @test -n "$(REPO)" || (echo "ERROR: REPO is required."; exit 1) $(UV) run python scripts/capture_sbom_tools.py --repo "$(REPO)" \ $(if $(REPO_PATH),--repo-path "$(REPO_PATH)") \ $(if $(DRY_RUN),--dry-run) ## Check a repo for ADR-001 compliance: make validate-adr REPO=/path/to/repo [DOMAIN=custodian] validate-adr: @test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make validate-adr REPO= [DOMAIN=]"; exit 1) $(UV) run python scripts/validate_repo_adr.py "$(REPO)" $(if $(DOMAIN),--domain "$(DOMAIN)",) ## Consistency exit contract: ## - Direct scripts/consistency_check.py: 0 clean, 2 warnings-only, 1 failures. ## - Agent/operator Make wrappers below normalize warning-only 2 to shell success ## while preserving visible WARN output and keeping real failures non-zero. ## Check a single repo for ADR-001 consistency: make check-consistency REPO=the-custodian [REPO_PATH=/override] ## Exit 0 = clean or warnings-only (warnings stay visible), exit 1 = failures check-consistency: @test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make check-consistency REPO="; exit 1) $(UV) run python scripts/consistency_check.py --repo "$(REPO)" \ $(if $(API_BASE),--api-base "$(API_BASE)",) \ $(if $(REPO_PATH),--repo-path "$(REPO_PATH)",); \ e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e ## Check and auto-fix a single repo: make fix-consistency REPO=the-custodian [REPO_PATH=/override] ## Exit 0 = clean or warnings-only (warnings stay visible), exit 1 = failures fix-consistency: @test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make fix-consistency REPO="; exit 1) $(UV) run python scripts/consistency_check.py --repo "$(REPO)" --fix \ $(if $(API_BASE),--api-base "$(API_BASE)",) \ $(if $(REPO_PATH),--repo-path "$(REPO_PATH)",); \ e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e ## Reconcile measured token sources against State Hub. ## Usage: make token-reconcile [SINCE=2026-05-19] [APPLY=1] [ZERO_FALLBACKS=1] token-reconcile: $(PYTHON) scripts/token_reconcile.py \ $(if $(SINCE),--since "$(SINCE)",) \ $(if $(API_BASE),--api-base "$(API_BASE)",) \ $(if $(CODEX_HOME),--codex-home "$(CODEX_HOME)",) \ $(if $(CLAUDE_HOME),--claude-home "$(CLAUDE_HOME)",) \ $(if $(APPLY),--apply,) \ $(if $(ZERO_FALLBACKS),--zero-superseded-fallbacks,) ## Pull then fix: single repo or all repos if REPO omitted ## make fix-consistency-remote — smart pull+fix all repos that need it ## make fix-consistency-remote REPO=slug — pull+fix one repo fix-consistency-remote: $(UV) run python scripts/consistency_check.py \ $(if $(REPO),--repo "$(REPO)",--all) \ --remote \ $(if $(API_BASE),--api-base "$(API_BASE)",) \ $(if $(NO_WRITEBACK),--no-writeback,); \ e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e ## Infer repo slug from git remote URL and check: make check-consistency-here [REPO_PATH=/path/to/repo] ## Omit REPO_PATH to use the Python script's CWD (i.e. pass an empty --here flag). check-consistency-here: $(UV) run python scripts/consistency_check.py \ --here $(if $(REPO_PATH),"$(REPO_PATH)",) \ $(if $(API_BASE),--api-base "$(API_BASE)",); \ e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e ## Infer repo slug from git remote URL and fix: make fix-consistency-here [REPO_PATH=/path/to/repo] fix-consistency-here: $(UV) run python scripts/consistency_check.py \ --here $(if $(REPO_PATH),"$(REPO_PATH)",) \ --fix \ $(if $(API_BASE),--api-base "$(API_BASE)",); \ e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e ## Check all registered repos for ADR-001 consistency check-consistency-all: $(UV) run python scripts/consistency_check.py --all $(if $(API_BASE),--api-base "$(API_BASE)",); \ e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e ## Check and auto-fix all registered repos fix-consistency-all: $(UV) run python scripts/consistency_check.py --all --fix $(if $(API_BASE),--api-base "$(API_BASE)",); \ e=$$?; [ $$e -eq 2 ] && exit 0 || exit $$e ## Cancel open tasks belonging to completed/archived workstreams. ## Safe to run at any time; also suitable for a daily cron job. ## Cron example: 0 3 * * * cd ~/state-hub && make cleanup-stale cleanup-stale: $(UV) run python scripts/cleanup_stale_tasks.py ## Install custodian post-commit sync hook into one repo: make install-hooks REPO=marki-docx install-hooks: @test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make install-hooks REPO="; exit 1) bash scripts/install_hooks.sh --repo "$(REPO)" ## Install custodian post-commit sync hook into all active registered repos install-hooks-all: bash scripts/install_hooks.sh --all ## Remove custodian post-commit sync hook from one repo: make remove-hooks REPO=marki-docx remove-hooks: @test -n "$(REPO)" || (echo "ERROR: REPO is required. Usage: make remove-hooks REPO="; exit 1) bash scripts/install_hooks.sh --repo "$(REPO)" --remove ## Compare Gitea coulomb org repos against state-hub registered repos ## Requires GITEA_TOKEN in env or .env: make gitea-inventory GITEA_TOKEN= gitea-inventory: $(UV) run python scripts/gitea_inventory.py $(if $(JSON),--json) clean: $(COMPOSE) down -v