Ctrl K
ComunidadGuías…

Makefile

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

Archivo original ↗Preguntar a KiBot
.PHONY: dev dev-lan prod stop restart logs down check _check load-test bundle-budget fmt deploy doctor hero build adr adr-check adr-index adr-renumber adr-import verifier

BUN := $(HOME)/.bun/bin/bun
TSX := packages/server/node_modules/.bin/tsx
AUTONOMOS := $(TSX) packages/cli/src/index.ts
DEPLOY_HOST ?= $(or $(HOST),$(shell grep -s '^DEPLOY_HOST=' .env | cut -d= -f2))
DEPLOY_PATH ?= ~/autonomOS
PROD_PORT ?= 3100
NO_MIGRATE ?=
# Interface the prod server binds, for a ONE-OFF override:
#   make deploy BIND_HOST=127.0.0.1
# Empty = the server's default: all interfaces (reachable over the network —
# Tailscale / IAP / SSH — which is how this is normally deployed). The auth
# token gates what you can do once connected; the network layer gates who
# reaches the port. Set BIND_HOST=127.0.0.1 only to RESTRICT to loopback, e.g.
# a box you reach exclusively through an SSH tunnel.
#
# For a persistent restriction, prefer `AUTONOMOS_HOST=127.0.0.1` in THAT box's
# .env instead of this. The service wrapper runs `tsx --env-file=<repo>/.env`,
# so the server reads it on every start — it survives `install-service --force`,
# whereas BIND_HOST bakes `--host` into the service file, which a later reinstall
# without it would drop.
#
# Deliberately NOT named HOST: $(HOST) is already an alias for DEPLOY_HOST above,
# so `HOST=x make deploy` would try to deploy TO a host named "x".
#
# Strip an inline `# comment` and any padding: this value lands in command-prefix
# position below (`HOST=$(BIND_HOST) bash …`), where a stray `#` would comment
# out the rest of the recipe line and make `make prod` a SILENT no-op — a deploy
# that reports success and changes nothing. `-f2-` keeps values containing `=`.
# (the \# is escaped for make — an unescaped # would comment out the rest of
# this line, including the closing paren.)
BIND_HOST ?= $(strip $(shell grep -s '^BIND_HOST=' .env | cut -d= -f2- | cut -d'\#' -f1))

# ── dev: isolated per worktree ───────────────────
# Ports are derived from the directory path hash so each worktree gets unique ports.
# Override manually: make dev DEV_API_PORT=3101 DEV_VITE_PORT=5173
DEV_PORT_HASH := $(shell printf '%s' "$(CURDIR)" | cksum | cut -d' ' -f1)
DEV_API_PORT ?= $(shell echo $$(( 3200 + $(DEV_PORT_HASH) % 800 )))
DEV_VITE_PORT ?= $(shell echo $$(( 5200 + $(DEV_PORT_HASH) % 800 )))
DEV_CONFIG_DIR ?= $(CURDIR)/.autonomos-dev
# THIS MACHINE ONLY by default (Terry, 2026-10-01). The dev API server always
# binds loopback (vite proxies to it); DEV_HOST is what vite listens on. Reach
# the dev dashboard from another device only by opting in: `make dev-lan`.
# Anyone on the network who can reach the vite port gets the dev server's
# files and its proxy to the API, so do that only on a network you trust.
DEV_HOST ?= 127.0.0.1

dev:
	@mkdir -p $(DEV_CONFIG_DIR)
	@lsof -ti:$(DEV_API_PORT) -sTCP:LISTEN | xargs kill -9 2>/dev/null || true
	@lsof -ti:$(DEV_VITE_PORT) -sTCP:LISTEN | xargs kill -9 2>/dev/null || true
	@echo "Dev server: API=:$(DEV_API_PORT) Vite=:$(DEV_VITE_PORT) Config=$(DEV_CONFIG_DIR)"
	@# Loud whenever vite isn't on loopback, however DEV_HOST was set (make
	@# dev-lan, the command line, or a DEV_HOST exported in the shell).
	@case "$(DEV_HOST)" in 127.0.0.1|localhost|::1) ;; *) echo "⚠ DEV_HOST=$(DEV_HOST): the dev dashboard (and its proxy to the API) is reachable by anyone on this network." ;; esac
	@cd packages/server && PORT=$(DEV_API_PORT) AUTONOMOS_HOST=127.0.0.1 AUTONOMOS_CONFIG_DIR=$(DEV_CONFIG_DIR) CORS_ORIGIN=http://localhost:$(DEV_VITE_PORT) ../../$(TSX) watch --env-file=../../.env src/index.ts &
	@sleep 2
	@cd packages/dashboard && DEV_HOST=$(DEV_HOST) VITE_API_PORT=$(DEV_API_PORT) $(BUN) vite --host $(DEV_HOST) --port $(DEV_VITE_PORT)

# The explicit opt-in for using the dev dashboard from another device.
dev-lan:
	@$(MAKE) dev DEV_HOST=0.0.0.0

# ── prod: build + OS-native daemon on :3100 ───────
#   Supervised by launchd (macOS) / systemd-user (Linux) via
#   scripts/install-prod-service.sh — NOT pm2. The script auto-migrates an
#   existing pm2-managed autonomos (set NO_MIGRATE=1 to skip) and is idempotent,
#   so re-running picks up new source.
#
#   Runs synchronously and verifies the daemon came up. (Don't run `make prod`
#   from inside an agent session the daemon spawned — the supervisor restart
#   would kill that PTY mid-run. Use a plain shell or `make deploy`.)
prod: build
	@echo "Handing off to OS-native supervisor (launchd/systemd-user)..."
	@PORT=$(PROD_PORT) NO_MIGRATE=$(NO_MIGRATE) HOST='$(BIND_HOST)' bash scripts/install-prod-service.sh

# ── build: deps + channel server + dashboard, NO service operations ──
#   Factored out of `prod` so the source-mode updater (ADR-077) can rebuild a
#   managed clone after a tag checkout without touching the supervisor — the
#   updater owns the restart (out-of-process, health-gated). Everything here
#   must stay side-effect-free with respect to the running daemon.
build:
	@$(BUN) install
	@# Security floors (ADR-137): a plain install keeps a nested copy the new
	@# lockfile no longer lists, so a security bump can miss the package that
	@# loads it (measured, V12b: an upgraded clone still served ws 8.19 through
	@# @hono/node-ws). Relinks with --force --frozen-lockfile ONLY when a floor
	@# is unmet, then fails the build if it still is. Every upgrade path runs
	@# this target.
	@BUN=$(BUN) $(TSX) scripts/check-security-floors.ts
	@bash scripts/ensure-node-pty.sh
	@echo "Building channel server..."
	@# Deps INLINED (no --packages=external): the release tarball carries no
	@# node_modules resolvable from channel-server/, so external specifiers
	@# crashed the bridge with ERR_MODULE_NOT_FOUND before the MCP initialize
	@# response — every agent's autonomos MCP dead fleet-wide on bundle
	@# installs while the daemon (whose own bundle inlines the same deps)
	@# looked healthy (#376). Only ws's OPTIONAL native accelerators stay
	@# external: ws require()s bufferutil/utf-8-validate in try/catch and
	@# falls back to its JS implementations when absent — inlining them would
	@# fail the build; leaving them external is ws's supported shape.
	@# The createRequire banner is load-bearing: inlined CJS deps (ws) call
	@# require() for node builtins, and esbuild's ESM output otherwise shims
	@# require to a throw ("Dynamic require of events is not supported").
	@bunx esbuild packages/server/src/channel-server/index.ts --bundle --platform=node --format=esm --outfile=packages/server/src/channel-server/dist.mjs --external:bufferutil --external:utf-8-validate --banner:js="import { createRequire as __csCreateRequire } from 'node:module'; const require = __csCreateRequire(import.meta.url);" --log-level=warning
	@echo "Removing any stale embedded dashboard (hosted server serves packages/dashboard/dist; _embedded_dashboard is a binary-build artifact only)..."
	@rm -rf packages/server/src/_embedded_dashboard
	@echo "Building dashboard..."
	@# Build into dist.next and swap (ADR-105): on a source install the OLD
	@# daemon keeps serving packages/dashboard/dist throughout `autonomos
	@# upgrade`'s minutes-long rebuild, and vite empties its outDir first — a
	@# browser refresh mid-build would 404 every asset. The swap window is two
	@# renames.
	@cd packages/dashboard && rm -rf dist.next dist.prev && $(BUN) vite build --outDir dist.next --emptyOutDir && { [ ! -d dist ] || mv dist dist.prev; } && mv dist.next dist && rm -rf dist.prev

# ── doctor: preflight checks (node-pty ABI vs runtime node) ──
#   Run standalone to diagnose/repair a crash-loop after a node upgrade.
doctor:
	@bash scripts/ensure-node-pty.sh

# ── stop / restart / logs ─────────────────────────
#   stop: service-aware — stops via the supervisor so launchd KeepAlive /
#   systemd Restart don't immediately revive it (a bare SIGTERM would bounce).
stop:
	@$(AUTONOMOS) stop

restart:
	@$(AUTONOMOS) restart

logs:
	@$(AUTONOMOS) logs --lines 50

# ── down: stop everything ────────────────────────
#   Removes the OS-native service entirely (stop + delete unit), then frees any
#   dev ports.
down:
	@$(AUTONOMOS) uninstall-service 2>/dev/null || true
	@lsof -ti:$(DEV_API_PORT) -sTCP:LISTEN | xargs kill -9 2>/dev/null || true
	@lsof -ti:$(DEV_VITE_PORT) -sTCP:LISTEN | xargs kill -9 2>/dev/null || true
	@echo "Stopped (service removed; dev API=:$(DEV_API_PORT) Vite=:$(DEV_VITE_PORT))."

# ── deploy: rsync + prod on remote ───────────────
#
#   Configure per machine:
#     .env: DEPLOY_HOST=forge
#   Or inline: make deploy DEPLOY_HOST=forge
#
deploy:
	@[ -n "$(DEPLOY_HOST)" ] || { echo "Error: Set DEPLOY_HOST in .env or pass it: make deploy DEPLOY_HOST=forge"; exit 1; }
	@echo "⚠️  make deploy is a private dev tool, NOT a supported install shape (ADR-077)."
	@echo "   It rsyncs a mutable working tree with no git history — no provenance, no"
	@echo "   'autonomos upgrade', no rollback. For a managed deployment use:"
	@echo "   scripts/install-source.sh on the target (clone-at-tag, upgradeable)."
	@echo "Deploying to $(DEPLOY_HOST):$(DEPLOY_PATH)..."
	rsync -avz --delete \
		--exclude node_modules \
		--exclude .env \
		--exclude dist \
		--exclude .git \
		--exclude _embedded_dashboard \
		--exclude .autonomos-bin \
		./ $(DEPLOY_HOST):$(DEPLOY_PATH)/
	@echo "Installing bun (if needed)..."
	ssh $(DEPLOY_HOST) 'export PATH=$$HOME/.bun/bin:$$PATH && command -v bun >/dev/null || { curl -fsSL https://bun.sh/install | bash && export PATH=$$HOME/.bun/bin:$$PATH; }'
	@echo "Installing dependencies..."
	ssh $(DEPLOY_HOST) 'cd $(DEPLOY_PATH) && export PATH=$$HOME/.bun/bin:$$PATH && bun install'
	@echo "Building and starting on $(DEPLOY_HOST) (launchd/systemd-user, auto-migrates pm2)..."
	@# Forward BIND_HOST only when set locally. Passing an empty BIND_HOST= would
	@# override (and blank out) the remote's own .env value, silently flipping a
	@# deliberately-exposed box back to loopback and cutting off browser access.
	ssh $(DEPLOY_HOST) 'cd $(DEPLOY_PATH) && export PATH=$$HOME/.local/bin:$$HOME/.bun/bin:$$PATH && make prod $(if $(BIND_HOST),BIND_HOST=$(BIND_HOST),)'

# ── fmt: auto-fix lint + formatting ─────────────
fmt:
	npx biome check --write --unsafe packages/

# ── check: lint + typecheck + test ───────────────
# Per-test backstop (OUTSIDE `ifndef CI` on purpose — it must apply in CI,
# which sets CI=true): a test stuck on an await fails at 5 min, NAMED, instead
# of silently holding the run until the CI job timeout. 5 min sits above every
# real-agent suite's own diagnostic budget (agent-spawn-prompt waits 180s in a
# 200s describe), so it never pre-empts their better failure messages. It can
# NOT catch a synchronous block (the event loop is frozen); the CI job's
# timeout-minutes is the backstop for that.
NODE_TEST_TIMEOUT := --test-timeout=300000
# Exit once the last test finishes, even if a test file leaked a live handle
# (socket, server, timer). Without it a finished run can idle forever: a gate's
# runner sat at 0% CPU for 48 min holding the machine-wide slot.
NODE_TEST_FORCE_EXIT := --test-force-exit

# Local runs cap test fan-out at half the cores. Uncapped, one run forks about
# one process per core, and a few agents' gates at once saturated the box (load
# avg 24-35), slowing the live server and causing timing-only flakes. CI sets
# CI=true, so its command line is unchanged.
ifndef CI
LOCAL_TEST_CAP := $(shell node -e "process.stdout.write(String(Math.max(1, Math.floor(require('os').availableParallelism() / 2))))" 2>/dev/null)
NODE_TEST_CONCURRENCY := $(if $(LOCAL_TEST_CAP),--test-concurrency=$(LOCAL_TEST_CAP))
VITEST_MAX_WORKERS := $(if $(LOCAL_TEST_CAP),--maxWorkers=$(LOCAL_TEST_CAP))
endif

# Test suites run with git's location variables STRIPPED. Inside a git hook
# (the pre-push gate) git exports GIT_DIR (for a linked worktree, its
# .git/worktrees/<name>), and any fixture `git init`/`config` then targets the
# REAL repository: one fixture `git init` flipped the shared repo to
# core.bare=true and broke every worktree (#321, again on 2026-10-01). No suite
# needs the hook's repo location; cwd discovery still works without it.
GIT_CLEAN_ENV := env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_COMMON_DIR -u GIT_OBJECT_DIRECTORY -u GIT_ALTERNATE_OBJECT_DIRECTORIES -u GIT_PREFIX -u GIT_NAMESPACE

# Every full local run takes the ONE machine-wide slot (scripts/ci-gate-lock.sh),
# whoever starts it: the pre-push gate, an agent's `make check`, an
# AUTONOMOS_INTEGRATION=1 run, `make load-test`. Overlapping full runs saturated
# the box (load 24-35, later 600+ with a load rig on top) and slowed the live
# server. The lock is re-entrant, so the gate (which already holds it) passes
# straight through. CI runs one job per runner and skips it.
check:
ifdef CI
	$(MAKE) _check
else
	scripts/ci-gate-lock.sh $(MAKE) _check
endif

# First step: a worktree installed while a stale node-pty sorted first in the
# store has a 644 spawn-helper, and every PTY test then fails with
# "posix_spawnp failed." ensure-node-pty.sh heals it (idempotent).
_check:
	@bash scripts/ensure-node-pty.sh
	$(TSX) scripts/decisions.ts check
	npx biome check packages/
	packages/dashboard/node_modules/.bin/tsc --build
	$(TSX) scripts/check-dashboard-dist.ts
	$(TSX) scripts/build-verifier.ts --check
	$(GIT_CLEAN_ENV) env -u AUTONOMOS_LOAD_TEST $(TSX) --import ./scripts/test-home-sentinel.ts --test $(NODE_TEST_CONCURRENCY) $(NODE_TEST_TIMEOUT) $(NODE_TEST_FORCE_EXIT) packages/server/src/__tests__/*.test.ts packages/cli/src/__tests__/*.test.ts scripts/*.test.ts
	cd packages/dashboard && $(GIT_CLEAN_ENV) node_modules/.bin/vitest run $(VITEST_MAX_WORKERS)

# N-agent statusline load guard (CI: the `Load` workflow). Locally it takes the
# machine-wide slot, and the test aborts itself if the box's load climbs.
load-test:
	scripts/ci-gate-lock.sh env AUTONOMOS_LOAD_TEST=1 $(GIT_CLEAN_ENV) $(TSX) --test --test-timeout=600000 packages/server/src/__tests__/statusline-load.test.ts

# ── verifier: the single-file provenance verifier install.sh runs (ADR-126) ──
# Rebuilds scripts/verify-provenance.mjs and re-pins its sha256 in install.sh.
# `make check` fails when either is stale — commit both files together.
verifier:
	$(TSX) scripts/build-verifier.ts

# Dashboard bundle-size ratchet (also a CI step): build, then compare the
# gzipped JS/CSS against packages/dashboard/perf-budget.json.
bundle-budget:
	@bun --filter @autonomos/dashboard build >/dev/null
	$(TSX) scripts/check-bundle-budget.ts

# ── adr: architectural decision records, one file each (docs/decisions/) ───────
# `make adr NEW="Title"` allocates the next free number across origin/main AND open
# PRs (via gh, when available) and writes a template. `make adr-index` prints the
# index table; it is never committed (ADR-157).
# See docs/decisions/README.md. Arguments are read by the shell as "$$NEW" (make
# exports command-line variables), so backticks and quotes in a title survive.
adr:
	@test -n "$$NEW" || { echo 'usage: make adr NEW="Short decision title"'; exit 2; }
	$(TSX) scripts/decisions.ts new "$$NEW"

adr-check:
	$(TSX) scripts/decisions.ts check

adr-index:
	@$(TSX) scripts/decisions.ts index

adr-renumber:
	@test -n "$$FILE" || { echo 'usage: make adr-renumber FILE=docs/decisions/ADR-NNN-slug.md'; exit 2; }
	$(TSX) scripts/decisions.ts renumber "$$FILE"

adr-import:
	$(TSX) scripts/decisions.ts import "$${REF:-HEAD}"

# ── hero: regenerate the README hero screenshot (docs/assets/hero.png) ───────────────
# Boots an isolated demo instance (own config dir + fake HOME + ephemeral port,
# never :3100), stages the multi-agent scene, and captures via headless Chrome.
# RE-RUN THIS AFTER ANY DASHBOARD UI CHANGE so the README hero stays accurate.
# See packages/dashboard/scripts/capture-hero.ts for prerequisites + details.
# The README hero capture uses NO real login (#551). On macOS the kernel
# enforces it: the run is sandboxed away from ~/.codex, ~/.gemini, the Claude
# credentials file, the keychain and the `security` CLI.
hero:
	cd packages/dashboard && if [ "$$(uname -s)" = Darwin ]; then \
	  sandbox-exec -D HOME="$$HOME" -f scripts/hero-no-credentials.sb $(BUN) run capture-hero; \
	else $(BUN) run capture-hero; fi