.DEFAULT_GOAL := help PYTHON ?= $(firstword $(wildcard .venv/bin/python3) python3) .PHONY: help help-check hooks check mcp-config mcp-check mcp-install-user \ rules-sync rules-check tenancy-check omniroute-health omniroute-start omniroute-stop \ omniroute-restart omniroute-status clone-descriptions clone-description-suggest dashboard \ docs-config-sync docs-config-check docs-build docs-serve ci-docs \ docs-update-check workspace open-lab close-lab voice voice-setup code-library \ code-library-search sandbox layout-plan layout-apply layout-check \ anytype-scope-check mcp-smoke browser-smoke browser-test-setup config-migrate capabilities-check library-index research-sync research research-staged \ patterns-index anytype-gate-install anytype-gate-check anytype-grant anytype-revoke \ projects-check claude-local-check cursor-project-check \ automations-run automations-list automations-runs automations-pause automations-resume \ automations-plist automations-check automations-defs-check automations-install automations-uninstall automations-digest \ automations-key-store automations-key-check automations-key-delete automations-health automations-clone-check \ automations-arxiv-poll \ automations-notify-key-store automations-notify-key-check automations-notify-key-delete \ security-attack claude-sandbox-install claude-sandbox-check claude-sandbox-uninstall \ anytype-key-store anytype-key-check anytype-key-delete canary-init redteam-build redteam-parity \ redteam-spaces redteam-setup redteam-reset redteam-serve redteam-launch redteam-judge \ redteam-p2-preflight redteam-p2-next redteam-p2-finish redteam-p2-purge redteam-p2-status ## List every documented target (default) help: $(PYTHON) scripts/makefile_help.py ## Fail if a .PHONY target is missing a ## catalog line help-check: $(PYTHON) scripts/makefile_help.py --check ## Run every drift gate (pre-commit) check: mcp-check rules-check docs-config-check tenancy-check help-check capabilities-check automations-defs-check claude-sandbox-check ## Point git at the versioned hooks in scripts/hooks/ hooks: git config core.hooksPath scripts/hooks @echo "git hooks enabled from scripts/hooks/" ## Regenerate .mcp.json and .cursor/mcp.json from tool-chest.toml mcp-config: $(PYTHON) scripts/gen_mcp_config.py ## Fail if MCP client configs drifted from tool-chest.toml mcp-check: $(PYTHON) scripts/gen_mcp_config.py --check ## Start every [mcp.*] server and require tools/list (not in check; slow) mcp-smoke: $(PYTHON) scripts/mcp_smoke.py $(MCP_SMOKE_ARGS) ## Fail if [capability.*] drifted: MCP owned by one area, references exist capabilities-check: $(PYTHON) scripts/capabilities.py --check ## Index in-scope Calibre books for the reference library library-index: uv run --directory library library index ## Crawl, archive and index Anthropic research (network; slow) research-sync: uv run --directory research research sync ## Run any research command in its own environment (`research` is not on PATH): make research ARGS="staged list" research: uv run --directory research research $(ARGS) ## List the research entries held back for review (flagged by the scan, or agent-suggested) research-staged: uv run --directory research research staged list ## Index the architecture patterns corpus patterns-index: uv run --directory patterns patterns index ## Write the user-level MCP clients (claude-user, cursor-user) mcp-install-user: $(PYTHON) scripts/gen_mcp_config.py --install-user ## Re-render .cursor/rules/ from shared/rules/ per rules-manifest.yaml rules-sync: $(PYTHON) scripts/sync_rules.py ## Fail if .cursor/rules/ drifted from rules-manifest.yaml rules-check: $(PYTHON) scripts/sync_rules.py --check ## Create the private names list (/canary.txt) from a commented template, if missing (#94) canary-init: $(PYTHON) scripts/name_canary.py --init ## Smoke-test the workbench shell in a real Chromium; a missing browser FAILS (CI job browser-smoke) browser-smoke: REQUIRE_BROWSER=1 $(PYTHON) scripts/browser_smoke.py ## Move per-user settings out of tool-chest.toml (and ~/.config/{research,library}) into /config; DRY=1 to preview config-migrate: $(PYTHON) scripts/migrate_config.py $(if $(DRY),--dry-run,) ## Install the browser-test dependency (Playwright + Chromium, ~100 MB download; CI skips those tests without it) browser-test-setup: $(PYTHON) -m pip install --quiet -r requirements-dev.txt $(PYTHON) -m playwright install chromium ## Validate tenancy schemas and Wave 0–2 script tests tenancy-check: export TOOL_CHEST_ANYTYPE_KEY_SERVICE = tool-chest.anytype.tests-never-stored # Tests never read or write the real private settings: the data root is a scratch directory for the whole run. tenancy-check: export TOOL_CHEST_DATA_ROOT = $(shell mktemp -d) tenancy-check: $(PYTHON) scripts/tenancy.py --check $(PYTHON) scripts/test_tenancy.py $(PYTHON) scripts/test_omniroute.py $(PYTHON) scripts/test_omniroute_ctl.py $(PYTHON) scripts/test_code_library.py $(PYTHON) scripts/test_library_repos.py $(PYTHON) scripts/test_voice_capture.py $(PYTHON) scripts/test_sandbox_lab.py $(PYTHON) scripts/test_dashboard.py $(PYTHON) scripts/test_project_dirs.py $(PYTHON) scripts/test_project_state.py $(PYTHON) scripts/test_privacy.py $(PYTHON) scripts/test_name_canary.py $(PYTHON) scripts/test_redteam.py $(PYTHON) scripts/test_redteam_observe.py $(PYTHON) scripts/test_redteam_fixtures.py $(PYTHON) scripts/test_redteam_session.py $(PYTHON) scripts/test_redteam_phase2.py $(PYTHON) scripts/test_patterns_private_tier.py $(PYTHON) scripts/test_viewer_corpora.py $(PYTHON) scripts/test_library_search.py $(PYTHON) scripts/test_research_search.py $(PYTHON) scripts/test_claude_local.py $(PYTHON) scripts/test_cursor_project.py $(PYTHON) scripts/test_project_connections.py $(PYTHON) scripts/test_project_toggle.py $(PYTHON) scripts/test_project_create.py $(PYTHON) scripts/test_mcp_smoke.py $(PYTHON) scripts/test_view_isolation.py $(PYTHON) scripts/test_capabilities.py $(PYTHON) scripts/test_health.py $(PYTHON) scripts/test_client_connect.py $(PYTHON) scripts/test_area_config.py $(PYTHON) scripts/test_workspace_config.py $(PYTHON) scripts/test_config_inventory.py $(PYTHON) scripts/test_migrate_config.py $(PYTHON) scripts/test_security_checks.py $(PYTHON) scripts/test_check_runner.py $(PYTHON) scripts/test_project_launch.py $(PYTHON) scripts/test_inbox.py $(PYTHON) scripts/test_glance.py $(PYTHON) scripts/test_harnesses.py $(PYTHON) scripts/test_clone_sync.py $(PYTHON) scripts/test_no_git_refs_in_tests.py $(PYTHON) scripts/test_gen_mcp_config.py $(PYTHON) scripts/test_workspace_layout.py $(PYTHON) scripts/test_anytype_scope.py $(PYTHON) scripts/test_anytype_write_gate.py $(PYTHON) scripts/test_threat_model.py $(PYTHON) scripts/test_makefile_help.py $(PYTHON) scripts/test_automation_http.py $(PYTHON) scripts/test_automation_install.py $(PYTHON) scripts/test_automations.py $(PYTHON) scripts/test_automation_anytype.py $(PYTHON) scripts/test_automation_health.py $(PYTHON) scripts/test_automation_notify.py $(PYTHON) scripts/test_automation_autopause.py $(PYTHON) scripts/test_automation_keychain.py $(PYTHON) scripts/test_automation_pushover_key.py $(PYTHON) scripts/test_automation_clone_check.py $(PYTHON) scripts/test_automation_arxiv_poll.py $(PYTHON) scripts/test_dashboard_automations.py $(PYTHON) scripts/test_automation_digest.py $(PYTHON) scripts/test_task_due.py $(PYTHON) scripts/test_clone_descriptions.py $(PYTHON) scripts/test_project_object.py $(PYTHON) scripts/test_task_browser.py $(PYTHON) -m unittest discover -s config-kit/tests -t config-kit/tests $(PYTHON) scripts/test_config_registry.py $(PYTHON) scripts/test_run_index.py $(PYTHON) scripts/test_next_ui.py $(PYTHON) scripts/test_next_js.py $(PYTHON) scripts/test_security_attack.py $(PYTHON) scripts/test_claude_sandbox_install.py $(PYTHON) scripts/test_anytype_key.py $(PYTHON) scripts/test_tests_are_wired.py ## Fail if the Anytype key can see a space marked agent_read=false (needs Anytype running) anytype-scope-check: $(PYTHON) scripts/anytype_scope.py ## Store the AnyType API key in the Keychain, every read prompting (run it yourself in a terminal; checked against the API first) anytype-key-store: $(PYTHON) scripts/anytype_key.py --store ## Is the AnyType key stored in the Keychain? (reads no value, shows no dialog) anytype-key-check: $(PYTHON) scripts/anytype_key.py --check ## Remove the AnyType key from the Keychain anytype-key-delete: $(PYTHON) scripts/anytype_key.py --delete ## Install the AnyType write gate into ~/.claude/hooks/tool-chest/ and register it in ~/.claude/settings.json (idempotent, backs up first) anytype-gate-install: $(PYTHON) scripts/anytype_gate_install.py --install ## Fail if the installed AnyType write gate or its registration differs from the repo (machine state, not in `make check`) anytype-gate-check: $(PYTHON) scripts/anytype_gate_install.py --check ## Let the AnyType write gate skip prompts for one space in one Claude session: make anytype-grant SPACE=work [SESSION=] [TTL=30m, max 2h]; no SESSION takes the one session just prompted anytype-grant: @test -n "$(SPACE)" || { echo "usage: make anytype-grant SPACE=work [SESSION=] [TTL=30m]"; exit 2; } $(PYTHON) scripts/hooks/anytype_grants.py grant "$(SPACE)" "$(if $(SESSION),$(SESSION),--latest)" $(TTL) ## End an AnyType grant early: make anytype-revoke SESSION= [SPACE=work] anytype-revoke: @test -n "$(SESSION)" || { echo "usage: make anytype-revoke SESSION= [SPACE=work]"; exit 2; } $(PYTHON) scripts/hooks/anytype_grants.py revoke "$(SESSION)" $(SPACE) ## Install the Claude Code Bash sandbox from [claude_sandbox] into ~/.claude/settings.json (EVERY session on this machine; backs up first) claude-sandbox-install: $(PYTHON) scripts/claude_sandbox_install.py --install ## Fail if the installed sandbox block or ask rules drifted from tool-chest.toml (skips where ~/.claude/settings.json is absent, e.g. CI) claude-sandbox-check: $(PYTHON) scripts/claude_sandbox_install.py --check ## Roll back: remove the sandbox block and our ask rules from ~/.claude/settings.json (backs up first) claude-sandbox-uninstall: $(PYTHON) scripts/claude_sandbox_install.py --uninstall ## Drive `claude -p` through attack probes, judged by effect (calls a model; local only, never in check/CI): make security-attack [ATTACK_ARGS="--only env-secrets"] security-attack: $(PYTHON) scripts/security_attack.py $(ATTACK_ARGS) ## Build the red-team scratch gate, grant state and settings under /redteam/scratch (touches nothing real), for the Work-Decoy id redteam-setup resolved [DECOY= to override] (#97) redteam-build: $(PYTHON) scripts/redteam.py build $(if $(DECOY),--decoy-space-id "$(DECOY)",) ## Fail unless the red-team scratch copy is the installed gate and sandbox plus the decoy and its own grant state (#97) redteam-parity: $(PYTHON) scripts/redteam.py parity ## List the spaces (name and id) the decoy key can see (#97; one decoy-key Keychain dialog) redteam-spaces: $(PYTHON) scripts/redteam_session.py spaces ## Fresh red-team markers, fixtures and marker objects in the Work-like decoy, by space name: make redteam-setup WORK= FORBIDDEN= (#97; one decoy-key Keychain dialog) redteam-setup: @test -n "$(WORK)" -a -n "$(FORBIDDEN)" || { echo "usage: make redteam-setup WORK= FORBIDDEN="; exit 2; } $(PYTHON) scripts/redteam_session.py setup --work-space "$(WORK)" --forbidden-space "$(FORBIDDEN)" ## Restore the red-team starting position between attempts (#97; one decoy-key Keychain dialog) redteam-reset: $(PYTHON) scripts/redteam_session.py reset ## Serve the hostile page and listen for egress until Ctrl-C (#97) redteam-serve: $(PYTHON) scripts/redteam_session.py serve ## Print the claude command for a red-team phase, without running it: make redteam-launch PHASE=1 (tier-failed) or 2 (strict reader) (#97) redteam-launch: @test -n "$(PHASE)" || { echo "usage: make redteam-launch PHASE=1|2"; exit 2; } $(PYTHON) scripts/redteam_session.py launch --phase "$(PHASE)" ## Judge a red-team session: make redteam-judge [TRACE=] [JUDGE_ARGS=--no-key] (#97; default: the newest fresh transcript; exit 1 a finding, 2 inconclusive) redteam-judge: $(PYTHON) scripts/redteam_session.py judge $(if $(TRACE),--trace "$(TRACE)") $(JUDGE_ARGS) ## Phase 2 pre-attempt checks (serve running, no allow rule, strict launch line, parity, gate, sandbox, env); no Keychain dialog (#98) redteam-p2-preflight: $(PYTHON) scripts/redteam_phase2.py preflight ## Phase 2: preflight, reset (one decoy-key dialog), then print the next attempt's launch line and prompt [NO_RESET=1 skips the reset] (#98) redteam-p2-next: $(PYTHON) scripts/redteam_phase2.py next $(if $(NO_RESET),--no-reset) ## Phase 2: judge and audit the attempt just run, check Work-Decoy for extras, append the log block [ATTEMPT=P2-n] [TRACE=] [ACCEPT=] [JUDGE_ARGS=--no-key] (#98; one dialog) redteam-p2-finish: $(PYTHON) scripts/redteam_phase2.py finish $(if $(ATTEMPT),--attempt "$(ATTEMPT)") $(if $(TRACE),--trace "$(TRACE)") $(if $(ACCEPT),--accept-extras "$(ACCEPT)") $(JUDGE_ARGS) ## Phase 2: archive every untracked object in Work-Decoy, recreate the tracked ones, clear the baseline [FORGET=P2-1,... marks those attempts not-run] (#98; one dialog) redteam-p2-purge: $(PYTHON) scripts/redteam_phase2.py purge $(if $(FORGET),--forget "$(FORGET)") ## Phase 2: which attempts are done for this setup and with what verdict (#98) redteam-p2-status: $(PYTHON) scripts/redteam_phase2.py status ## Ping OmniRoute (optional; down must not block commits) omniroute-health: $(PYTHON) scripts/omniroute_health.py ## Start OmniRoute detached from its project directory (does nothing if it is already running) omniroute-start: $(PYTHON) scripts/omniroute_ctl.py start ## Stop OmniRoute gracefully: only an OmniRoute process, never force-killed omniroute-stop: $(PYTHON) scripts/omniroute_ctl.py stop ## Restart OmniRoute: stop it, then start it detached omniroute-restart: $(PYTHON) scripts/omniroute_ctl.py restart ## Show whether OmniRoute is running: pid, uptime, port and directory omniroute-status: $(PYTHON) scripts/omniroute_ctl.py status ## List the reference clones that need a description drafted (none yet, or the README changed), with a README excerpt clone-descriptions: $(PYTHON) scripts/clone_descriptions.py todo ## Record a drafted description for a clone: make clone-description-suggest KEY=ai/some-repo TEXT="what it is, what it is for" clone-description-suggest: $(PYTHON) scripts/clone_descriptions.py suggest "$(KEY)" "$(TEXT)" ## Run one automation by name and record the run: make automations-run NAME=heartbeat automations-run: @test -n "$(NAME)" || { echo "usage: make automations-run NAME="; exit 2; } $(PYTHON) scripts/automations.py run "$(NAME)" ## Store an automation's own AnyType key in the Keychain (run it yourself in a terminal; the key is checked against the definition first): make automations-key-store NAME=task-digest automations-key-store: @test -n "$(NAME)" || { echo "usage: make automations-key-store NAME="; exit 2; } $(PYTHON) scripts/automation_anytype.py store "$(NAME)" ## Show what the stored AnyType key reaches, against the definition's declaration: make automations-key-check NAME=task-digest automations-key-check: @test -n "$(NAME)" || { echo "usage: make automations-key-check NAME="; exit 2; } $(PYTHON) scripts/automation_anytype.py check "$(NAME)" ## Remove an automation's AnyType key from the Keychain: make automations-key-delete NAME=task-digest automations-key-delete: @test -n "$(NAME)" || { echo "usage: make automations-key-delete NAME="; exit 2; } $(PYTHON) scripts/automation_anytype.py delete "$(NAME)" ## Health of each automation from the run log, healthy or degraded with the reasons (exit 1 if any is degraded): make automations-health [NAME=task-digest] automations-health: $(PYTHON) scripts/automation_health.py $(NAME) ## Store the shared Pushover credentials in the Keychain (run it yourself in a terminal; checked with Pushover before storing): make automations-notify-key-store automations-notify-key-store: $(PYTHON) scripts/automation_pushover_key.py store ## Confirm the stored Pushover credentials still validate (no notification sent): make automations-notify-key-check automations-notify-key-check: $(PYTHON) scripts/automation_pushover_key.py check ## Remove the stored Pushover credentials from the Keychain: make automations-notify-key-delete automations-notify-key-delete: $(PYTHON) scripts/automation_pushover_key.py delete ## Fetch every reference clone and count how many are behind upstream, recording the run (never updates one): make automations-clone-check automations-clone-check: $(PYTHON) scripts/automations.py run clone-check ## Poll the saved arXiv feeds and ingest new papers, recording the run (needs /research/feeds.toml): make automations-arxiv-poll automations-arxiv-poll: $(PYTHON) scripts/automations.py run arxiv-poll ## Run the task digest by hand and record the run (needs its key stored first, see automations-key-store): make automations-digest automations-digest: $(PYTHON) scripts/automations.py run task-digest ## List the defined automations, whether each is paused, and its last run automations-list: $(PYTHON) scripts/automations.py list ## Show recent automation runs (never their output): make automations-runs [NAME=heartbeat] automations-runs: $(PYTHON) scripts/automations.py runs $(NAME) ## Stop an automation from running until resumed: make automations-pause NAME=heartbeat REASON="why" automations-pause: @test -n "$(NAME)" || { echo "usage: make automations-pause NAME= [REASON=...]"; exit 2; } $(PYTHON) scripts/automations.py pause "$(NAME)" --reason "$(or $(REASON),paused by hand)" ## Let a paused automation run again: make automations-resume NAME=heartbeat automations-resume: @test -n "$(NAME)" || { echo "usage: make automations-resume NAME="; exit 2; } $(PYTHON) scripts/automations.py resume "$(NAME)" ## Print the launchd agent generated from a definition (changes nothing): make automations-plist NAME=heartbeat automations-plist: @test -n "$(NAME)" || { echo "usage: make automations-plist NAME="; exit 2; } $(PYTHON) scripts/automations.py plist "$(NAME)" ## Fail if this machine's installed launchd agents drifted from the definitions (local; not part of `make check`) automations-check: $(PYTHON) scripts/automations.py check ## Validate every automation definition: schema, and nothing private in them (part of `make check`) automations-defs-check: $(PYTHON) scripts/automation_defs.py --check ## Install an automation as a launchd agent; run it yourself in a terminal, it asks first (DRY=1 previews): make automations-install NAME=heartbeat automations-install: @test -n "$(NAME)" || { echo "usage: make automations-install NAME= [DRY=1]"; exit 2; } $(PYTHON) scripts/automations.py install "$(NAME)" $(if $(DRY),--dry-run) ## Uninstall an automation's launchd agent; run it yourself in a terminal, it asks first (DRY=1 previews): make automations-uninstall NAME=heartbeat automations-uninstall: @test -n "$(NAME)" || { echo "usage: make automations-uninstall NAME= [DRY=1]"; exit 2; } $(PYTHON) scripts/automations.py uninstall "$(NAME)" $(if $(DRY),--dry-run) ## Check the per-project state file: orphaned entries and leaked names (local; skips when absent) projects-check: $(PYTHON) scripts/project_state.py --check ## Compare the per-project state file with Claude Code's local scope (local; skips when absent) claude-local-check: $(PYTHON) scripts/claude_local.py --check ## Compare the per-project state file with each project's Cursor MCP file (local; skips when absent) cursor-project-check: $(PYTHON) scripts/cursor_project.py --check ## Open the localhost dashboard (127.0.0.1:8787) dashboard: PATH="/opt/homebrew/bin:/usr/local/bin:$$PATH" $(PYTHON) scripts/dashboard.py ## Ensure whisper-cli + ggml-base.en model are available for voice capture voice-setup: @command -v whisper-cli >/dev/null || (echo "install: brew install whisper.cpp"; exit 1) @command -v ffmpeg >/dev/null || (echo "install: brew install ffmpeg (needed for webm→wav)"; exit 1) @mkdir -p "$(HOME)/tool-chest-data/models/whisper" @test -f "$(HOME)/tool-chest-data/models/whisper/ggml-base.en.bin" || \ curl -L --fail -o "$(HOME)/tool-chest-data/models/whisper/ggml-base.en.bin" \ "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin" @echo "voice ready: $$(command -v whisper-cli)" @echo "model: $(HOME)/tool-chest-data/models/whisper/ggml-base.en.bin" ## Transcribe a local audio file with Whisper (AUDIO=/path) voice: @[ -n "$(AUDIO)" ] || (echo "usage: make voice AUDIO=/path/to.wav"; exit 1) PATH="/opt/homebrew/bin:/usr/local/bin:$$PATH" $(PYTHON) scripts/voice_capture.py --audio "$(AUDIO)" ## Scaffold a hybrid workspace folder + pointer-only wiki stub workspace: @[ -n "$(NAME)" ] && [ -n "$(DOMAIN)" ] && [ -n "$(CLASSIFICATION)" ] || \ (echo "usage: make workspace NAME=… DOMAIN=personal|work|career CLASSIFICATION=… [TAGS='a b']"; exit 1) $(PYTHON) scripts/workspace_fs.py new-workspace \ --name "$(NAME)" --domain "$(DOMAIN)" --classification "$(CLASSIFICATION)" \ $(foreach tag,$(TAGS),--tag $(tag)) ## Open a host-worktree lab (read/analyze only) open-lab: @[ -n "$(ID)" ] && [ -n "$(SCOPE)" ] || \ (echo "usage: make open-lab ID=… SCOPE=own|oss|employer-ephemeral [SOURCE=…]"; exit 1) $(PYTHON) scripts/workspace_fs.py open-lab --id "$(ID)" --scope "$(SCOPE)" \ $(if $(SOURCE),--source "$(SOURCE)",) ## Delete a host-worktree lab close-lab: @[ -n "$(ID)" ] || (echo "usage: make close-lab ID=…"; exit 1) $(PYTHON) scripts/workspace_fs.py close-lab --id "$(ID)" ## List files in enabled code-library categories (empty if none enabled) code-library: $(PYTHON) scripts/code_library.py list ## Lexical search of enabled code-library categories (Q='query') code-library-search: @[ -n "$(Q)" ] || (echo "usage: make code-library-search Q='tokenizer'"; exit 1) $(PYTHON) scripts/code_library.py search "$(Q)" ## Dry-run the ~/Documents/workspace reorganization from its workspace.toml layout-plan: $(PYTHON) scripts/workspace_layout.py plan ## Apply workspace.toml moves (quit Claude Code/Cursor first) layout-apply: $(PYTHON) scripts/workspace_layout.py apply ## Fail on workspace layout drift (unplaced dirs, wrong buckets, CLAUDE.md) layout-check: $(PYTHON) scripts/workspace_layout.py check ## Print the copy-in, network-deny sandbox plan (IMAGE=… optional) sandbox: $(PYTHON) scripts/sandbox_lab.py $(if $(IMAGE),--image $(IMAGE),) \ $(if $(FINDINGS),--findings $(FINDINGS),) ## Regenerate zensical.toml nav from every tracked markdown file docs-config-sync: $(PYTHON) scripts/gen_mkdocs_config.py ## Fail if zensical.toml drifted from tracked markdown docs-config-check: $(PYTHON) scripts/gen_mkdocs_config.py --check ## Export tracked files and build the static docs site docs-build: scripts/export_docs.sh zensical build ## Export tracked files and serve docs at http://127.0.0.1:8000 docs-serve: scripts/export_docs.sh zensical serve ## CI docs job: drift gates plus a real site build ci-docs: check docs-build ## Install latest zensical and confirm the docs build still passes docs-update-check: scripts/docs_update_check.sh