Ctrl K
ComunidadGuías…

Makefile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
.PHONY: install start start-build start-prod stop logs clean build dev-backend run-backend dev-frontend setup check check-diff docs-reanchor test-backend test-backend-cov test-backend-lib install-dev-tools require-nextest test-shell lint-backend .env kiro-login bump check-version desktop desktop-dev desktop-target

# Doc-skippers type `make` bare (or `make install`) before reading anything.
# Bare `make` used to run the FIRST target — `_gen-override`, an internal Docker
# helper — a baffling near-no-op. Default to help instead; it points at the
# guided entry (`./kronn start`).
.DEFAULT_GOAL := help

# ─── Configuration ───────────────────────────────────────────────────────────
APP_NAME    := kronn
VERSION     := $(shell cat VERSION 2>/dev/null || echo "0.0.0")
PORT        := 3140
DOCKER_COMP := docker compose

# In-place sed differs between GNU (`sed -i`) and BSD/macOS (`sed -i ''`).
# Detected once so `make bump` works on both — the GNU-only syntax used to
# abort mid-bump on macOS (observed on the 0.8.10 bump).
SEDI := $(shell sed --version >/dev/null 2>&1 && echo "-i" || echo "-i ''")

# ─── Colors ──────────────────────────────────────────────────────────────────
RED    := \033[0;31m
GREEN  := \033[0;32m
YELLOW := \033[0;33m
CYAN   := \033[0;36m
RESET  := \033[0m

# ─── Main Commands ───────────────────────────────────────────────────────────

## Auto-detect host environment and write .env for docker compose
.env:
	@echo "# Auto-generated by Kronn — do not edit" > .env
	@uname_s=$$(uname -s 2>/dev/null); \
	if [ "$$uname_s" = "Darwin" ]; then \
		echo "KRONN_HOST_OS=macOS" >> .env; \
		echo "KRONN_MACOS_SSH_SOCK=/run/host-services/ssh-auth.sock" >> .env; \
		if [ -d /opt/homebrew/bin ]; then \
			echo "KRONN_GLOBAL_BIN=/opt/homebrew/bin" >> .env; \
			echo "$(CYAN)  Detected macOS (Apple Silicon)$(RESET)"; \
		elif [ -d /usr/local/bin ]; then \
			echo "KRONN_GLOBAL_BIN=/usr/local/bin" >> .env; \
			echo "$(CYAN)  Detected macOS (Intel)$(RESET)"; \
		fi; \
	elif grep -qiE "microsoft|wsl" /proc/version 2>/dev/null; then \
		echo "KRONN_HOST_OS=WSL" >> .env; \
		echo "$(CYAN)  Detected WSL2$(RESET)"; \
	else \
		echo "KRONN_HOST_OS=Linux" >> .env; \
		echo "$(CYAN)  Detected Linux$(RESET)"; \
	fi
	@# Detect npm global bin directory (where `npm install -g` puts binaries)
	@npm_bin=$$(npm bin -g 2>/dev/null || npm config get prefix 2>/dev/null | xargs -I{} echo "{}/bin"); \
	if [ -d "$$npm_bin" ]; then \
		echo "KRONN_NPM_BIN=$$npm_bin" >> .env; \
		echo "$(CYAN)  npm global bin: $$npm_bin$(RESET)"; \
	fi
	@echo "KRONN_HOST_UID=$$(id -u)" >> .env
	@echo "KRONN_HOST_GID=$$(id -g)" >> .env
	@echo "UID=$$(id -u)" >> .env
	@echo "GID=$$(id -g)" >> .env
	@# Confirm repos dir on first run; inference remains the default answer.
	@# Edge case (TD-20260507-makefile-repos-rel-home-edge): when Kronn lives
	@# directly under $$HOME, the parent IS $$HOME, which collides with the
	@# read-only `${HOME}:/host-home:ro` mount in docker-compose.yml. Refuse
	@# rather than silently produce a broken mount layout — the operator
	@# either moves Kronn under a sub-dir OR sets KRONN_REPOS_DIR explicitly.
	@home_dir=$$(cd "$$HOME" && pwd -P); \
	repos_dir=$$(cd "$(CURDIR)/.." && pwd -P); \
	if [ -n "$$KRONN_REPOS_DIR" ]; then repos_dir="$$KRONN_REPOS_DIR"; \
	elif [ -t 0 ]; then \
		echo "$(CYAN)Kronn must know where it may write project files (.mcp.json, agent instructions, worktrees).$(RESET)"; \
		printf "$(CYAN)Primary repositories directory [$$repos_dir]: $(RESET)"; \
		read -r confirmed_repos; \
		if [ -n "$$confirmed_repos" ]; then repos_dir="$$confirmed_repos"; fi; \
	fi; \
	if [ -d "$$repos_dir" ]; then repos_dir=$$(cd "$$repos_dir" && pwd -P); fi; \
	if [ -z "$$KRONN_REPOS_DIR" ] && [ "$$repos_dir" = "$$home_dir" ]; then \
		echo "$(RED)KRONN_REPOS_DIR auto-detect picked $$HOME, which collides with the read-only /host-home mount.$(RESET)"; \
		echo "$(YELLOW)The parent of Kronn becomes the rw repositories directory: move Kronn into the directory that already contains your repos.$(RESET)"; \
		echo "$(YELLOW)Or set KRONN_REPOS_DIR for the primary directory; use KRONN_EXTRA_REPOS for additional directories.$(RESET)"; \
		exit 1; \
	fi; \
	case "$$repos_dir" in \
		"$$home_dir"/*) repos_rel=$${repos_dir#$$home_dir/} ;; \
		*) repos_rel=.kronn-external-repos ;; \
	esac; \
	echo "KRONN_REPOS_DIR=$$repos_dir" >> .env; \
	echo "KRONN_REPOS_REL=$$repos_rel" >> .env; \
	echo "$(CYAN)  Repos dir confirmed: $$repos_dir (rw mount)$(RESET)"; \
	case "$$repos_dir" in /mnt/[a-zA-Z]/*) \
		echo "$(YELLOW)  WSL warning: /mnt/c-style repositories are much slower for Docker/agents. Prefer the Linux filesystem under $$HOME.$(RESET)" ;; \
	esac; \
	extra_repos="$${KRONN_EXTRA_REPOS:-}"; \
	if [ -z "$$extra_repos" ] && [ -t 0 ]; then \
		printf "$(CYAN)Additional repositories directories (colon-separated, optional): $(RESET)"; \
		read -r extra_repos; \
	fi; \
	if [ -n "$$extra_repos" ]; then \
		echo "KRONN_EXTRA_REPOS=$$extra_repos" >> .env; \
		echo "$(CYAN)  Additional repos: $$extra_repos (rw mounts)$(RESET)"; \
	fi
	@# Docker socket GID (for docker compose access from container)
	@if [ -S /var/run/docker.sock ]; then \
		docker_gid=$$(stat -c '%g' /var/run/docker.sock 2>/dev/null || stat -f '%g' /var/run/docker.sock 2>/dev/null || echo ""); \
		if [ -n "$$docker_gid" ]; then \
			echo "KRONN_DOCKER_GID=$$docker_gid" >> .env; \
			echo "$(CYAN)  Docker socket GID: $$docker_gid$(RESET)"; \
		fi; \
	fi
	@# 0.8.2 — Auto-detect version-manager bin paths so workflow Exec
	@# steps can call `cargo`, `bun`, etc. without manual config. Each
	@# path is written ONLY if the directory exists; missing → silent skip
	@# so users without rustup/Bun aren't penalized.
	@if [ -d "$$HOME/.cargo/bin" ]; then \
		echo "KRONN_CARGO_BIN=$$HOME/.cargo/bin" >> .env; \
		echo "$(CYAN)  Cargo bin: $$HOME/.cargo/bin$(RESET)"; \
	fi
	@if [ -d "$$HOME/.rustup" ]; then \
		echo "KRONN_RUSTUP_HOME=$$HOME/.rustup" >> .env; \
		echo "$(CYAN)  Rustup home: $$HOME/.rustup$(RESET)"; \
	fi
	@if [ -d "$$HOME/.bun/bin" ]; then \
		echo "KRONN_BUN_BIN=$$HOME/.bun/bin" >> .env; \
		echo "$(CYAN)  Bun bin: $$HOME/.bun/bin$(RESET)"; \
	fi
	@# Auto-detect host system binary paths for terminal access
	@sysbin=""; \
	for dir in /usr/bin /usr/sbin /snap/bin; do \
		[ -d "$$dir" ] && sysbin="$${sysbin:+$$sysbin:}$$dir"; \
	done; \
	if [ -n "$$sysbin" ]; then \
		echo "KRONN_HOST_SYSBIN=$$sysbin" >> .env; \
		echo "$(CYAN)  Host system binaries: $$sysbin$(RESET)"; \
	fi
	@# Auto-detect host network IPs for multi-user identity display
	@host_ips=$$(ip -4 addr show 2>/dev/null | awk '/^[0-9]/{split($$2,a,/[:@]/); iface=a[1]} /inet /{split($$2,a,"/"); ip=a[1]; if(ip~/^127\./)next; if(iface~/^(lo|docker|br-|veth)/)next; printf "%s%s:%s",sep,iface,ip; sep=","}'); \
	if [ -n "$$host_ips" ]; then \
		echo "KRONN_HOST_IPS=$$host_ips" >> .env; \
		echo "$(CYAN)  Host IPs: $$host_ips$(RESET)"; \
	fi
	@echo "# Add extra repo dirs (colon-separated) for rw access:" >> .env
	@echo "# KRONN_EXTRA_REPOS=$$HOME/Desktop/projects:$$HOME/Work" >> .env
	@# Ensure .claude.json exists as a file (Docker creates a directory if missing)
	@if [ ! -e "$$HOME/.claude.json" ]; then \
		echo '{}' > "$$HOME/.claude.json"; \
		echo "$(CYAN)  Created ~/.claude.json (empty file)$(RESET)"; \
	elif [ -d "$$HOME/.claude.json" ]; then \
		echo "$(YELLOW)  WARNING: ~/.claude.json is a directory — fixing$(RESET)"; \
		rmdir "$$HOME/.claude.json" 2>/dev/null && echo '{}' > "$$HOME/.claude.json" || \
		echo "$(RED)  ERROR: Cannot fix ~/.claude.json — remove it manually$(RESET)"; \
	fi

## Create the ${HOME} bind-mount sources as the user before Docker does it as
## root (scripts/docker-bind-sources.sh).
.PHONY: _ensure-bind-sources
_ensure-bind-sources:
	@./scripts/docker-bind-sources.sh docker-compose.yml "$$HOME"

## Generate docker-compose.override.yml: empty read-only mounts over the host's
## secrets (scripts/docker-secret-masks.sh), extra repo dirs (KRONN_EXTRA_REPOS)
## and, only when KRONN_DOCKER_SOCKET=1, the host's Docker socket and group.
.PHONY: _gen-override
_gen-override:
	@extra_repos=$$(sed -n 's/^KRONN_EXTRA_REPOS=//p' .env | tail -n 1); \
	docker_socket=$$(sed -n 's/^KRONN_DOCKER_SOCKET=//p' .env | tail -n 1); \
	docker_gid=$$(sed -n 's/^KRONN_DOCKER_GID=//p' .env | tail -n 1); \
	masks=$$(./scripts/docker-secret-masks.sh "$$HOME" ./.docker/empty ./.docker/empty-file); \
	if [ -z "$$masks" ] && [ -z "$$extra_repos" ] && [ "$$docker_socket" != "1" ]; then \
		rm -f docker-compose.override.yml; \
	else \
		echo "# Auto-generated — host secret masks, KRONN_EXTRA_REPOS rw mounts, opt-in Docker socket" > docker-compose.override.yml; \
		echo "services:" >> docker-compose.override.yml; \
		echo "  backend:" >> docker-compose.override.yml; \
		if [ "$$docker_socket" = "1" ]; then \
			echo "    group_add:" >> docker-compose.override.yml; \
			echo "      - \"$${docker_gid:-989}\"" >> docker-compose.override.yml; \
			echo "$(YELLOW)  Docker socket enabled (KRONN_DOCKER_SOCKET=1): agents can control Docker, and so this machine.$(RESET)"; \
		fi; \
		echo "    volumes:" >> docker-compose.override.yml; \
		if [ "$$docker_socket" = "1" ]; then echo "      - /var/run/docker.sock:/var/run/docker.sock" >> docker-compose.override.yml; fi; \
		if [ -n "$$masks" ]; then printf '%s\n' "$$masks" >> docker-compose.override.yml; fi; \
		if [ -n "$$extra_repos" ]; then \
			IFS=':'; for dir in $$extra_repos; do \
				case "$$dir" in \
					"$$HOME"/*) rel=$${dir#$$HOME/}; target="/host-home/$$rel" ;; \
					*) target="$$dir" ;; \
				esac; \
				echo "      - $$dir:$$target:rw" >> docker-compose.override.yml; \
				echo "$(CYAN)  Extra rw mount: $$dir$(RESET)"; \
			done; \
		fi; \
	fi

## Write/remove KRONN_RUST_LOG in .env based on DEBUG=1.
## docker-compose.yml reads `RUST_LOG=${KRONN_RUST_LOG:-}`, so setting it
## here enables verbose backend logs for the next `up -d` without touching
## the persisted `config.server.debug_mode`.
_apply-debug-flag:
	@sed -i.bak '/^KRONN_RUST_LOG=/d' .env 2>/dev/null || true
	@rm -f .env.bak
	@if [ "$(DEBUG)" = "1" ]; then \
		echo "KRONN_RUST_LOG=kronn=debug,tower_http=debug" >> .env; \
		echo "$(CYAN)  Debug mode: verbose logs enabled (DEBUG=1)$(RESET)"; \
	fi

## Snapshot the machine hosts file for the backend container. A direct bind of
## `/etc/hosts` is unreliable on Docker Desktop and OrbStack: it resolves to the
## Linux VM's file instead of the macOS host file.
.PHONY: _snapshot-host-hosts
_snapshot-host-hosts: .env
	@mkdir -p .docker/runtime
	@hosts_source="$${KRONN_HOSTS_FILE:-/etc/hosts}"; \
	 snapshot_tmp=".docker/runtime/hosts.tmp"; \
	 env_tmp=".env.hosts.tmp"; \
	 snapshot="./.docker/host-hosts-unavailable"; \
	 if [ -f "$$hosts_source" ] && cp "$$hosts_source" "$$snapshot_tmp"; then \
		mv "$$snapshot_tmp" .docker/runtime/hosts; \
		snapshot="./.docker/runtime/hosts"; \
	 else \
		echo "$(YELLOW)  Host mappings unavailable: $$hosts_source$(RESET)"; \
	 fi; \
	 grep -v '^KRONN_HOSTS_SNAPSHOT=' .env > "$$env_tmp" || true; \
	 echo "KRONN_HOSTS_SNAPSHOT=$$snapshot" >> "$$env_tmp"; \
	 mv "$$env_tmp" .env

# The universal "I didn't read the docs" entry point. Delegates to the guided
# CLI (`./kronn start`), which handles the macOS Docker warning, prerequisites,
# and points at `./kronn start-dev` (native) where appropriate. NOTE: `start`
# below must NOT delegate to the CLI — kronn's cmd_web calls `make start`
# internally, so that would recurse.
install:
	@echo "$(CYAN)Launching the guided Kronn setup (./kronn start)...$(RESET)"
	@./kronn start

## Start everything with Docker. Pulls the published images of VERSION and starts
## them without compiling (KT-1468: the backend crate needs ~19 GB of RAM to
## build). On main, VERSION is the last release: this runs its image, not local edits.
## A pull failure (VERSION not published, offline) falls back to a source build behind
## a memory check (scripts/docker-up.sh). `make start-build` forces the source build.
## Pass DEBUG=1 to force verbose logs (kronn=debug,tower_http=debug) for this run.
## Without DEBUG=1, the backend picks the level from config.server.debug_mode.
start: .env _snapshot-host-hosts _ensure-bind-sources _gen-override _apply-debug-flag
	@if [ "$$(uname -s)" = "Darwin" ]; then \
		echo "$(YELLOW)  macOS note: Docker can't run your host agents (Claude, Codex, …) or read the Keychain.$(RESET)"; \
		echo "$(YELLOW)  For native agent execution prefer: ./kronn start-dev$(RESET)"; \
	fi
	@./scripts/docker-up.sh
	@$(MAKE) --no-print-directory _own-claude-volumes
	@echo ""
	@echo "  $(CYAN)╭──╮$(RESET)"
	@echo "  $(CYAN)│$(GREEN)⚡$(CYAN)│$(RESET) $(GREEN)Kronn v$(VERSION)$(RESET)"
	@echo "  $(CYAN)╰──╯$(RESET) Services running."
	@echo ""
	@echo "       → $(GREEN)http://localhost:$(PORT)$(RESET)"
	@echo ""
	@echo "       $(CYAN)kronn stop$(RESET)     Stop"
	@echo "       $(CYAN)kronn logs$(RESET)     Logs"
	@echo "       $(CYAN)kronn restart$(RESET)  Restart"
	@echo ""

## Same as `make start`, but always builds the images from source (skips the
## pull). Needs ~20 GB of memory for Docker: the memory check stops a smaller
## machine before it compiles; KRONN_FORCE_BUILD=1 overrides it.
start-build: export KRONN_BUILD_FROM_SOURCE := 1
start-build: start

## Production build (Docker, release with LTO — slow but optimized binary)
start-prod: .env _snapshot-host-hosts _ensure-bind-sources _gen-override
	@echo "$(GREEN)▸ Building $(APP_NAME) (production release)...$(RESET)"
	@KRONN_BUILD_FROM_SOURCE=1 ./scripts/docker-up.sh
	@$(MAKE) --no-print-directory _own-claude-volumes

.PHONY: _own-claude-volumes
# Volumes created before the image pre-created their mount points are owned by
# root, so Claude Code cannot save a session and every resumed turn fails.
_own-claude-volumes:
	@$(DOCKER_COMP) exec -T -u 0 backend chown kronn:kronn /home/kronn/.claude/projects /home/kronn/.claude/sessions

## Stop all services
stop:
	@echo "$(YELLOW)▸ Stopping $(APP_NAME)...$(RESET)"
	@$(DOCKER_COMP) down
	@echo ""
	@echo "  $(CYAN)╭──╮$(RESET)"
	@echo "  $(CYAN)│$(RED)⚡$(CYAN)│$(RESET) $(RED)Kronn v$(VERSION)$(RESET)"
	@echo "  $(CYAN)╰──╯$(RESET) Services stopped."
	@echo ""

## Tail logs
logs:
	@$(DOCKER_COMP) logs -f

## Login to Kiro from the backend container (headless-safe OAuth device flow)
kiro-login: .env _gen-override
	@echo "$(CYAN)▸ Kiro login (device flow)...$(RESET)"
	@$(DOCKER_COMP) exec -u kronn backend /bin/sh -lc 'set -e; PATH="$$HOME/.local/bin:$$PATH"; command -v kiro-cli >/dev/null 2>&1 || (command -v unzip >/dev/null 2>&1 || { echo "Missing unzip in backend image. Rebuild with make start."; exit 1; } ; echo "Installing kiro-cli in container..." ; curl -fsSL https://cli.kiro.dev/install | bash); kiro-cli login --use-device-flow'

## Clean everything
clean:
	@echo "$(YELLOW)▸ Cleaning up...$(RESET)"
	@$(DOCKER_COMP) down -v --remove-orphans
	@rm -rf target backend/target frontend/node_modules frontend/dist
	@rm -f docker-compose.override.yml

## Production build (no Docker)
build:
	@echo "$(GREEN)▸ Building backend...$(RESET)"
	cd backend && cargo build --release
	@echo "$(GREEN)▸ Building frontend...$(RESET)"
	cd frontend && pnpm install && pnpm build
	@echo "$(GREEN)▸ Done. Binary at target/release/$(APP_NAME)$(RESET)"

# ─── Development ─────────────────────────────────────────────────────────────

## Backend dev with hot reload
dev-backend:
	@echo "$(GREEN)▸ Starting backend (watch mode)...$(RESET)"
	./scripts/dev-backend-supervisor.sh

# Exploitation mode: NO watcher. Dev mode now keeps the old backend serving
# while it compiles, but a successful build still swaps the process and would
# interrupt live agents/audits. Use this whenever runs must survive repo edits.
# NB: .cargo/config.toml shares target-dir at the repo root — the binary
# lands HERE, not in backend/target/.
run-backend:
	@echo "$(GREEN)▸ Building backend...$(RESET)"
	cd backend && cargo build
	@echo "$(GREEN)▸ Starting backend (bare binary — no watcher)...$(RESET)"
	./target/debug/kronn

## Frontend dev server
dev-frontend:
	@echo "$(GREEN)▸ Starting frontend dev server...$(RESET)"
	cd frontend && pnpm install && pnpm dev

## Run both in dev mode (requires tmux or two terminals)
dev:
	@echo "$(CYAN)One command runs both (backend hot-reload + Vite):$(RESET)"
	@echo "  ./kronn start-dev"
	@echo "$(YELLOW)Or manually, in two terminals:$(RESET)"
	@echo "  make dev-backend"
	@echo "  make dev-frontend"

# ─── Utilities ───────────────────────────────────────────────────────────────

## Check prerequisites
check:
	@echo "$(CYAN)Checking prerequisites...$(RESET)"
	@command -v docker >/dev/null 2>&1 && echo "  ✓ Docker" || echo "  ✗ Docker (required)"
	@command -v cargo  >/dev/null 2>&1 && echo "  ✓ Rust/Cargo" || echo "  ✗ Rust/Cargo (optional, for dev)"
	@command -v node   >/dev/null 2>&1 && echo "  ✓ Node.js" || echo "  ✗ Node.js (optional, for dev)"
	@command -v pnpm   >/dev/null 2>&1 && echo "  ✓ pnpm" || echo "  ✗ pnpm (optional, for dev)"

## Generate TypeScript types from Rust models
typegen:
	@echo "$(GREEN)▸ Generating ts-rs bindings from Rust models...$(RESET)"
	cd backend && cargo test --lib export_bindings -- --nocapture
	@echo "$(GREEN)▸ Assembling frontend/src/types/generated.ts from the bindings...$(RESET)"
	@node frontend/scripts/assemble-generated-types.mjs
	@echo "$(GREEN)▸ generated.ts regenerated — commit it with your Rust model change.$(RESET)"

## Run the backend suite CI runs, with the runner CI uses (cargo-nextest,
## backend/.config/nextest.toml): every test in its own process, then the
## doctests nextest does not run. The ts-rs exports are filtered out by the
## default profile so bindings stay put. No `cargo test` fallback: the
## integration tests share one binary and assume a process per test.
test-backend: require-nextest
	@echo "$(CYAN)▸ Running backend tests with cargo-nextest (as CI)...$(RESET)"
	cd backend && cargo nextest run --workspace $(NEXTEST_ARGS)
	cd backend && cargo test --doc

require-nextest:
	@cargo nextest --version >/dev/null 2>&1 \
	  || { echo "$(YELLOW)▸ cargo-nextest is required (the runner CI uses): run 'make install-dev-tools'.$(RESET)"; exit 1; }

## The CI coverage gate, locally: the instrumented nextest run with the same
## floors, then the key-management per-file floors from the same data. The
## profile pool keeps one process per test from writing ~50 GB of profraw.
test-backend-cov:
	@cargo nextest --version >/dev/null 2>&1 && cargo llvm-cov --version >/dev/null 2>&1 \
	  || { echo "$(YELLOW)▸ cargo-nextest and cargo-llvm-cov are required: run 'make install-dev-tools'.$(RESET)"; exit 1; }
	@echo "$(CYAN)▸ Running the backend coverage gate (as CI)...$(RESET)"
	cd backend && NEXTEST_PROFILE=ci LLVM_PROFILE_FILE_NAME='kronn-%8m.profraw' cargo llvm-cov nextest --workspace --summary-only \
	  --fail-under-lines 83 --fail-under-functions 83 --fail-under-regions 83
	scripts/check-keymgmt-coverage.sh

## Library unit tests only: a quick loop, not the gate.
test-backend-lib: require-nextest
	@echo "$(CYAN)▸ Running backend library tests...$(RESET)"
	cd backend && cargo nextest run --lib $(NEXTEST_ARGS)

## Install the backend test tools CI uses: cargo-nextest and cargo-llvm-cov
## (prebuilt through cargo-binstall when present, built from source otherwise).
install-dev-tools:
	@if command -v cargo-binstall >/dev/null 2>&1; then \
	  cargo binstall --no-confirm cargo-nextest cargo-llvm-cov; \
	else \
	  cargo install --locked cargo-nextest cargo-llvm-cov; \
	fi
	rustup component add llvm-tools-preview

## Run Python helper tests (MCP bridge auto-inheritance contract).
## Stdlib `unittest` only — no extra dev deps. The MCP script
## (`disc-introspection-mcp.py`) drives the kronn-internal MCP server
## that 5+ agent CLIs talk to, so its helpers MUST stay green.
test-python:
	@echo "$(CYAN)▸ Running Python helper tests...$(RESET)"
	python3 -m unittest discover -s backend/scripts -p 'test_*.py' -v
	cd backend/sidecars/docs && python3 -m unittest test_build_bundle.py -v

## Run frontend unit tests (vitest)
test-frontend:
	@echo "$(CYAN)▸ Running frontend unit tests (vitest)...$(RESET)"
	cd frontend && pnpm test

## Orchestration validation compatibility: keep these stable target names for
## persisted TaskExecution gates, even though the frontend now uses pnpm.
typecheck:
	@echo "$(CYAN)▸ Running frontend TypeScript parity checks...$(RESET)"
	cd frontend && pnpm typecheck:native
	cd frontend && pnpm typecheck:legacy

yarn-test-changed:
	@echo "$(CYAN)▸ Running frontend unit tests for persisted validation gates...$(RESET)"
	cd frontend && pnpm test

## Run frontend E2E tests (Playwright). Pre-req: `make dev` running (Vite + backend).
test-e2e:
	@echo "$(CYAN)▸ Running E2E tests (Playwright)...$(RESET)"
	cd frontend && pnpm test:e2e

## Run frontend E2E tests with the Playwright UI (visual debug).
test-e2e-ui:
	@echo "$(CYAN)▸ Launching Playwright UI...$(RESET)"
	cd frontend && pnpm test:e2e:ui

## Run clippy lints in Docker (same toolchain as CI)
lint-backend:
	@echo "$(CYAN)▸ Running cargo clippy in Docker...$(RESET)"
	@docker build --target linter -f backend/Dockerfile backend/

## Run clippy locally with the ts-rs 9.x noise filtered out.
## ts-rs's proc macro emits "failed to parse serde attribute" for any
## #[serde] attribute combination it doesn't recognize (skip_serializing_if,
## deserialize_with, ...). These don't affect the generated TypeScript and
## aren't actionable on our side — wait for the ts-rs 10 release. Until
## then, this target makes `cargo clippy` actually scannable for real
## warnings.
lint-backend-local:
	@echo "$(CYAN)▸ Running cargo clippy locally (ts-rs noise filtered)...$(RESET)"
	@cd backend && cargo clippy --all-targets 2>&1 \
	  | awk 'BEGIN { skip=0 } \
	         /^warning: failed to parse serde attribute/ { skip=6; next } \
	         skip>0 { skip--; next } \
	         { print }' \
	  | tee /dev/stderr | grep -qE "^(warning|error):" && exit 1 || exit 0

## Run shell script tests (bats)
test-shell:
	@echo "$(CYAN)▸ Running shell tests...$(RESET)"
	@./tests/bats/run.sh

## ─── Sidecars (Python helpers) ──────────────────────────────────────────────

## Install the kronn-docs sidecar (document generation: PDF/DOCX/XLSX/CSV/PPTX).
## Creates a dedicated venv at ~/.kronn/venv/docs and installs the pinned deps.
## Idempotent — reruns just upgrade deps in place.
docs-setup:
	@echo "$(CYAN)▸ Setting up kronn-docs sidecar...$(RESET)"
	@command -v python3 >/dev/null 2>&1 || { echo "$(RED)python3 is required$(RESET)"; exit 1; }
	@mkdir -p $$HOME/.kronn/venv
	@if [ ! -d $$HOME/.kronn/venv/docs ]; then \
		echo "  · Creating venv at ~/.kronn/venv/docs"; \
		python3 -m venv $$HOME/.kronn/venv/docs; \
	fi
	@echo "  · Installing kronn-docs + deps (this may take ~1-2 min on first run)"
	@$$HOME/.kronn/venv/docs/bin/pip install --quiet --upgrade pip
	@$$HOME/.kronn/venv/docs/bin/pip install --quiet -e backend/sidecars/docs
	@echo "$(GREEN)▸ kronn-docs sidecar ready.$(RESET)"
	@echo "  WeasyPrint needs cairo/pango/gdk-pixbuf system libs on your OS —"
	@echo "  if PDF generation fails with a 'library not found' error, install them:"
	@echo "    apt:  sudo apt install libcairo2 libpango-1.0-0 libpangoft2-1.0-0 libgdk-pixbuf-2.0-0"
	@echo "    brew: brew install cairo pango gdk-pixbuf libffi"
	@echo "    win:  use the GTK+ runtime (see weasyprint.readthedocs.io)"

## ─── Desktop app (Tauri) ────────────────────────────────────────────────────

## Build the desktop app for the current platform
desktop:
	@echo "$(CYAN)▸ Building desktop app...$(RESET)"
	@cd desktop && cargo tauri build
	@echo "$(GREEN)✓ Desktop app built$(RESET)"
	@echo "  Output: desktop/src-tauri/target/release/bundle/"

## Build only the standalone document exporter embedded in desktop installers.
docs-bundle:
	@echo "$(CYAN)▸ Building bundled kronn-docs exporter...$(RESET)"
	@cd desktop && node scripts/build-docs-sidecar.mjs

## Run the desktop app in dev mode (hot reload)
desktop-dev:
	@echo "$(CYAN)▸ Starting desktop app (dev)...$(RESET)"
	@cd desktop && cargo tauri dev

## Build desktop app for a specific target (cross-compilation)
## Usage: make desktop-target T=x86_64-pc-windows-msvc
desktop-target:
ifndef T
	$(error Usage: make desktop-target T=<rust-target-triple>  Ex: x86_64-pc-windows-msvc, aarch64-apple-darwin)
endif
	@echo "$(CYAN)▸ Building desktop app for $(T)...$(RESET)"
	@cd frontend && pnpm build
	@cd desktop/src-tauri && cargo build --release --target $(T)
	@echo "$(GREEN)✓ Desktop app built for $(T)$(RESET)"

help:
	@echo ""
	@echo "$(CYAN)$(APP_NAME) — Enter the grid. Command your agents.$(RESET)"
	@echo ""
	@echo "$(GREEN)New here? One command:$(RESET)"
	@echo "  ./kronn start       Guided setup & launch (recommended)"
	@echo "  ./kronn start-dev   Native mode — REQUIRED on macOS for host agents"
	@echo ""
	@echo "$(GREEN)Usage:$(RESET)"
	@echo "  make install        Same as ./kronn start (guided)"
	@echo "  make start          Pull the published image & launch (Docker); builds only if the pull fails"
	@echo "  make start-build    Build from source & launch (Docker; needs ~20 GB for Docker, checked first)"
	@echo "  make start-prod     Build & launch (Docker, release + LTO)"
	@echo "  make stop           Stop services"
	@echo "  make logs           Tail logs"
	@echo "  make clean          Remove containers & data"
	@echo "  make build          Production build (native)"
	@echo "  make dev-backend    Rust hot reload"
	@echo "  make run-backend    Bare binary, no watcher (audits/agents survive edits)"
	@echo "  make dev-frontend   Vite dev server"
	@echo "  make check          Verify prerequisites"
	@echo "  make kiro-login     Kiro OAuth login (device flow in container)"
	@echo "  make typegen        Sync Rust → TS types"
	@echo "  make test-backend   Backend tests (cargo-nextest, as CI)"
	@echo "  make test-backend-cov  Backend coverage gate (as CI)"
	@echo "  make install-dev-tools  Install cargo-nextest and cargo-llvm-cov"
	@echo "  make test-shell     Run shell tests (bats)"
	@echo "  make bump V=x.y.z  Bump version everywhere"
	@echo "  make check-version  Verify release version consistency"
	@echo "  make check-diff     Whitespace/conflict markers in the branch diff"
	@echo "  make docs-reanchor [BASE=<rev>]  Re-anchor moved doc citations, then check them all"
	@echo "  make desktop        Build desktop app (current platform)"
	@echo "  make desktop-dev    Run desktop app (dev mode)"
	@echo "  make desktop-target T=<triple>  Cross-compile desktop app"
	@echo ""

## ─── Version bump ────────────────────────────────────────────────────────────
## Usage: make bump V=0.2.0
bump:
ifndef V
	$(error Usage: make bump V=x.y.z)
endif
	@echo "$(YELLOW)▸ Bumping version to $(V)...$(RESET)"
	@echo "$(V)" > VERSION
	@sed $(SEDI) 's/^version = ".*"/version = "$(V)"/' backend/Cargo.toml
	@sed $(SEDI) 's/^version = ".*"/version = "$(V)"/' desktop/src-tauri/Cargo.toml
	@sed $(SEDI) 's/"version": ".*"/"version": "$(V)"/' frontend/package.json
	@sed $(SEDI) 's/"version": ".*"/"version": "$(V)"/' desktop/package.json
	@sed $(SEDI) 's/"version": ".*"/"version": "$(V)"/' desktop/src-tauri/tauri.conf.json
	@sed $(SEDI) -E \
		-e 's|^> \*\*Status:.*|> **Status: $(V) (current release).** Functional but pre-1.0. Breaking changes happen between minor versions; patch versions are safe.|' \
		-e 's|(git clone --branch )[0-9]+\.[0-9]+\.[0-9]+|\1$(V)|g' \
		-e 's|Kronn v[0-9]+\.[0-9]+\.[0-9]+|Kronn v$(V)|g' README.md
	@sed $(SEDI) -E \
		-e 's|^> \*\*Statut :.*|> **Statut : $(V) (version actuelle).** Fonctionnel mais pré-1.0. Les versions mineures peuvent introduire des breaking changes ; les patch versions sont safe.|' \
		-e 's|(git clone --branch )[0-9]+\.[0-9]+\.[0-9]+|\1$(V)|g' \
		-e 's|Kronn v[0-9]+\.[0-9]+\.[0-9]+|Kronn v$(V)|g' README.fr.md
	@sed $(SEDI) -E 's|^## Current release: [0-9]+\.[0-9]+\.[0-9]+|## Current release: $(V)|' docs/index.md
	@sed $(SEDI) -E 's|(git clone --branch )[0-9]+\.[0-9]+\.[0-9]+|\1$(V)|g' docs/install.md
	@# The published images `make start` pulls (KT-1468) are tagged with VERSION.
	@sed $(SEDI) -E 's|(\$$\{KRONN_IMAGE_TAG:-)[0-9]+\.[0-9]+\.[0-9]+(\})|\1$(V)\2|g' docker-compose.yml
	@# 0.8.6 — also bump the hardcoded version in the public site (FR/EN/ES).
	@# Pre-fix `make bump` skipped these and we shipped 0.8.6 with the site
	@# still claiming v0.8.5 on the early-access disclaimer + credits line.
	@# Pattern: any "v<semver>" occurrence in site/*.html is the Kronn version
	@# (no other tokens use that prefix today).
	@sed $(SEDI) -E 's/v[0-9]+\.[0-9]+\.[0-9]+/v$(V)/g' site/index.html site/en.html site/es.html
	@# Sync Cargo.lock workspace entries so `cargo check --locked` stays green in CI.
	@# `cargo update --workspace --offline` only touches local package versions in the
	@# lock file — no network, no dep bumps. Required after editing a workspace
	@# member's own `version` in Cargo.toml.
	@echo "$(YELLOW)▸ Syncing Cargo.lock files...$(RESET)"
	@cd backend && cargo update --workspace --offline >/dev/null 2>&1 || \
		{ echo "$(YELLOW)  backend: offline failed, retrying online$(RESET)"; cd ../backend && cargo update --workspace >/dev/null; }
	@cd desktop/src-tauri && cargo update --workspace --offline >/dev/null 2>&1 || \
		{ echo "$(YELLOW)  desktop: offline failed, retrying online$(RESET)"; cd ../../desktop/src-tauri && cargo update --workspace >/dev/null; }
	@echo "$(GREEN)✓ Version bumped to $(V) in all files$(RESET)"
	@echo "  Files updated: VERSION, backend/Cargo.toml, desktop/src-tauri/Cargo.toml,"
	@echo "  frontend/package.json, desktop/package.json, desktop/src-tauri/tauri.conf.json,"
	@echo "  README.md, README.fr.md,"
	@echo "  docs/index.md, docs/install.md, docker-compose.yml (published image tags),"
	@echo "  site/index.html, site/en.html, site/es.html (v-prefixed mentions only — historical (0.8.x) refs preserved)"
	@echo "  + Cargo.lock (backend, desktop/src-tauri) synced via cargo update --workspace"
	@$(MAKE) --no-print-directory check-version

check-version:
	@./scripts/check-version-sync.sh

## Whitespace errors and conflict markers in what the branch changes
## (BASE defaults to the merge base with origin/main). CI runs the same script.
check-diff:
	@./scripts/check-diff.sh $(BASE)

## Re-points a doc citation whose #symbol left its range: unique symbol first,
## then a safe line map through `git diff -U0 BASE HEAD`. In a pull-request CI
## checkout BASE is its base parent; locally it defaults to the merge-base with
## origin/HEAD (then origin/main or origin/master, then a local main or master).
## During an unfinished merge (`git merge --no-commit <target>`) both sides of
## the merge are the evidence and no BASE is needed. With no base at all it still
## re-points the unique-symbol citations instead of refusing to run (a worker
## worktree has no origin refs to find one). DOCS="docs/a.md docs/b.md" limits
## both the rewrite and the strict citation check to those docs.
docs-reanchor:
	@base="$(BASE)"; \
	if [ -z "$$base" ] && git rev-parse --verify --quiet MERGE_HEAD >/dev/null; then \
		base=merge; \
	fi; \
	if [ -z "$$base" ]; then \
		if [ "$${GITHUB_EVENT_NAME:-}" = pull_request ] && git rev-parse --verify --quiet HEAD^2 >/dev/null; then \
			base=HEAD^1; \
		else \
			target="$$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)"; \
			for candidate in origin/main origin/master main master; do \
				if [ -z "$$target" ] && git rev-parse --verify --quiet "$$candidate" >/dev/null; then target="$$candidate"; fi; \
			done; \
			if [ -n "$$target" ]; then base="$$(git merge-base "$$target" HEAD 2>/dev/null || true)"; fi; \
		fi; \
	fi; \
	if [ -z "$$base" ]; then echo "docs-reanchor: no target-branch merge-base found; re-anchoring unique #symbol moves only (pass BASE=<rev> for the Git line map)" >&2; fi; \
	cd backend && KRONN_DOCS_REANCHOR="$(or $(DOCS),all)" KRONN_DOCS_REANCHOR_BASE="$$base" cargo test --lib core::docs_citations::tests::docs_citations_resolve_in_the_repo -- --nocapture