From 13144f32fb17e12833a4e33016d83c806dd00820 Mon Sep 17 00:00:00 2001 From: lukasWuttke <54042461+LukasWodka@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:10:05 +0200 Subject: [PATCH 1/3] chore(devex): install a `make check` pre-push hook via `make setup` (backend#1606) (#117) Takes the fan-out from 10/16 repos to 16/16. The target is byte-identical to the one already in backend, client-runtime, cli, model-zoo, tracebloc-engine and client -- extracted verbatim rather than retyped, so this repo cannot start out drifted from the fleet. Honest by design: it catches FORGETTING, not defiance (`--no-verify` skips it), refuses to clobber a pre-push hook that is not ours, skips delete/no-op pushes so a red tree cannot block `git push --delete`, and degrades to a no-op when `make` is absent -- GUI git clients launch hooks with a minimal PATH and several do not expose `--no-verify`, so skipping beats hard-blocking every push. Verified by running the target in a real checkout: hook installed, executable, carries our marker, ends in `exec make check`. Mutation-tested both refusals -- a planted foreign hook is preserved, and an all-zeros local sha exits 0. Co-authored-by: Claude Opus 5 --- Makefile | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/Makefile b/Makefile index 6cc4fbf..3a48106 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ help: @echo " check mint broken-links (~2 s) — run this before every push" @echo " check-all check + the SDK install-extras gate (needs network)" @echo " setup npm i -g mint" + @echo " install-hooks (re)install the git pre-push hook that runs 'make check'" @echo @echo " dev mint dev — local preview on http://localhost:3000" @@ -65,6 +66,7 @@ check-sdk-extras: setup: npm i -g mint @echo "==> setup: mint installed; run 'make check'" + @$(MAKE) --no-print-directory install-hooks # ---- individual targets ------------------------------------------ @@ -79,3 +81,71 @@ guard-mint: .PHONY: dev dev: guard-mint $(MINT) dev + +# install-hooks: put a pre-push hook in place that runs `make check`, so the +# canon's "run the tests before you push" is carried by the tooling rather than +# by memory. Factored out of `setup` so it is independently runnable and +# testable, and so a contributor who only wants the hook need not rerun the +# full `make setup`. +# +# Honest by design: the hook catches FORGETTING, not defiance — `git push +# --no-verify` skips it and always will. And it refuses to clobber a pre-push +# hook that is already there and not ours (e.g. one the pre-commit framework +# manages), rather than silently stomping a contributor's setup. +# +# `git rev-parse --git-path hooks` (not a hard-coded `.git/hooks`) so it lands +# in the right place inside a linked worktree or a submodule, where the git dir +# is not `.git`. +.PHONY: install-hooks +install-hooks: + @if ! git rev-parse --git-dir >/dev/null 2>&1; then \ + echo "note: not a git checkout — skipping pre-push hook install"; \ + elif hp="$$(git config --get core.hooksPath 2>/dev/null || true)"; [ -n "$$hp" ] && { \ + hd="$$(git rev-parse --git-path hooks)"; \ + case "$$hd" in /*) hdd="$$hd";; *) hdd="$$PWD/$$hd";; esac; \ + cpar="$$(cd "$$(dirname "$$hdd")" 2>/dev/null && pwd -P || true)"; \ + ctop="$$(cd "$$(git rev-parse --show-toplevel)" && pwd -P)"; \ + [ -z "$$cpar" ] || case "$$cpar/" in "$$ctop/"*) false;; *) true;; esac; \ + }; then \ + echo "note: core.hooksPath is set to '$$hp', outside this repo — skipping."; \ + echo " That is a shared hooks dir; installing here would run 'make check' from every repo you push."; \ + echo " Add 'make check' to that hook by hand if you want it everywhere."; \ + else \ + hook="$$(git rev-parse --git-path hooks)/pre-push"; \ + if [ -e "$$hook" ] && ! grep -q 'tracebloc pre-push hook' "$$hook" 2>/dev/null; then \ + echo "note: $$hook already exists and is not ours — leaving it untouched."; \ + echo " add 'make check' to it, or remove it and re-run 'make install-hooks'."; \ + else \ + mkdir -p "$$(dirname "$$hook")" && \ + printf '%s\n' \ + '#!/bin/sh' \ + '# tracebloc pre-push hook installed by make setup (backend#1606).' \ + '# Runs make check so a push that would be red in CI is caught locally first.' \ + '# It catches forgetting, not defiance: git push --no-verify skips it.' \ + '#' \ + '# Nothing to check on a delete/no-op push: a branch delete streams a' \ + '# local sha of all-zeros on stdin (no new commits). Skip so a red tree' \ + '# cannot block "git push --delete", and cleanup pushes stay free.' \ + 'z=0000000000000000000000000000000000000000' \ + 'had_update=0' \ + 'while read -r _ local_sha _ _; do' \ + ' [ "$$local_sha" != "$$z" ] && had_update=1' \ + 'done' \ + '[ "$$had_update" = 0 ] && exit 0' \ + '#' \ + '# Degrade gracefully when the toolchain is absent: GUI/IDE git clients' \ + '# (Tower, GitKraken, VS Code) launch hooks with a minimal PATH, so make' \ + '# may be missing — and several do not expose --no-verify. Skipping beats' \ + '# hard-blocking every push with "make: command not found".' \ + 'command -v make >/dev/null 2>&1 || exit 0' \ + '#' \ + '# Git exports GIT_DIR/GIT_WORK_TREE/etc into hook processes; a nested git' \ + '# invocation (from a test, tool, or setuptools-scm) then fails in a linked' \ + '# worktree with exit status 128. Clear them so make check runs as from the shell.' \ + 'unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_COMMON_DIR GIT_OBJECT_DIRECTORY' \ + 'exec make check' > "$$hook" && \ + chmod +x "$$hook" && \ + echo "==> pre-push hook installed at $$hook" && \ + echo " 'make check' now runs before each push (skip once with: git push --no-verify)"; \ + fi; \ + fi From bb25bfaf6197e2855e5c18c37ffd728b8c70b004 Mon Sep 17 00:00:00 2001 From: lukasWuttke <54042461+LukasWodka@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:57:39 +0200 Subject: [PATCH 2/3] fix(devex): guard the pre-push hook on the real toolchain, and resolve the hooks dir itself (backend#1995, frontend-app#809) (#119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects in the shared install-hooks block, byte-identical across design-system, docs and frontend-app. Both repos' copies are updated in lockstep. 1. The hook guarded on `make`, then ran a target needing yarn/node/mint. /usr/bin/make ships with the Xcode CLT and sits on the default launchd PATH, while yarn and node (nvm/fnm/volta) and mint (npm -g) are put on PATH by shell rc files a GUI git client's hook shell never sources. So `command -v make` passed and the push then hard-failed on "command not found" — precisely the outcome the skip was written to prevent — and VS Code exposes no --no-verify on push. Rather than add `command -v yarn` to the hook, which would be a fourth copy of the tool list and would drift the next time `check` gains a dependency, `check` now DEPENDS ON a `guard-toolchain` target and the hook asks that target: make guard-toolchain >/dev/null 2>&1 || exit 0 The list then lives in exactly one place — the prerequisite `check` cannot run without satisfying. It gates on TOOLS, not on installed dependencies: "yarn is absent" means this shell cannot run the checks at all and must skip, while "node_modules is absent" means the checks can run and will say so, and must not be swallowed just because it looks similar. `node` is listed alongside the runner because runner-on-PATH-without-node is a real configuration, not a theoretical one: Homebrew puts yarn in /usr/local/bin and nvm puts node elsewhere, and mint's bin is a `#!/usr/bin/env node` script. Guarding only on the runner lets `check` start and die with "env: node: No such file or directory", exit 127. 2. The core.hooksPath guard resolved the hooks directory's PARENT. With core.hooksPath=.githooks, where .githooks is a symlink to a shared dir, `dirname` is the checkout root, which resolves in-repo — so it installed, and the write went THROUGH the symlink into the shared directory. The one path element that can point elsewhere was the only one never resolved. Conversely a linked worktree whose core.hooksPath is the main repo's .git/hooks resolved to a parent outside the worktree toplevel and was skipped, though that directory belongs to the same repository, so the worktree silently got no hook. It now resolves the hooks directory itself when it exists, else the deepest existing ancestor, and treats it as in-repo when it is under either the worktree toplevel or the repo's common git dir. An unresolvable path still skips: "cannot tell" is not evidence that the path is ours. Co-authored-by: Claude Opus 5 --- Makefile | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 3a48106..a03c662 100644 --- a/Makefile +++ b/Makefile @@ -43,8 +43,12 @@ help: # `mint broken-links` is what catches a bad link before pushing. It is # also the check that would have caught the class of problem # preview-page-coverage.yml exists to catch, one step earlier. +# +# The prerequisite is spelled `guard-toolchain` — the uniform name the pre-push +# hook asks for before it runs `make check` (backend#1995) — and in this repo +# that is guard-mint. .PHONY: check -check: guard-mint +check: guard-toolchain $(MINT) broken-links # check-all adds the one gate that needs network: every documented @@ -77,6 +81,38 @@ guard-mint: echo " npm i -g mint (or: make setup)"; \ exit 1; } +# guard-toolchain: is the toolchain `check` needs on PATH at all? +# +# The uniform name every repo exposes, so the pre-push hook can ask one +# question — "can this shell run check at all?" — and skip itself on failure +# rather than hard-failing the push on "mint: command not found" +# (backend#1995). GUI/IDE git clients launch hooks with a minimal PATH, where +# /usr/bin/make is present but `npm i -g mint` output is not. +# +# Reuses guard-mint rather than making a second copy of that check: `check` +# depends on this target, so the mint probe lives in exactly one place and +# cannot drift as `check` changes. (`check-all` additionally needs python3, +# which is a base-system tool and is not gated here; the hook only runs +# `check`.) +# +# node is checked too, because mint IS a node program — its bin is a +# `#!/usr/bin/env node` script. mint on PATH without node on PATH is a REAL +# combination: `npm i -g mint` can land the shim in a prefix a GUI hook shell +# has while nvm/fnm/volta put node somewhere it does not. Then mint is found and +# dies with "env: node: No such file or directory", exit 127 — the same hard +# failure backend#1995 is about. +# +# TOOLS, NOT DEPENDENCIES: it asks whether mint and node are on PATH, never +# whether the docs tree is in a state mint would accept. A broken link is a real +# failure and must NOT be skipped just because "cannot run" and "runs and fails" +# look similar from the outside. +.PHONY: guard-toolchain +guard-toolchain: guard-mint + @command -v node >/dev/null 2>&1 || { \ + echo "node is not on PATH — the Mintlify CLI is a Node program; install Node, then:"; \ + echo " make setup"; \ + exit 1; } + # dev: local preview, per README.md. .PHONY: dev dev: guard-mint @@ -96,6 +132,26 @@ dev: guard-mint # `git rev-parse --git-path hooks` (not a hard-coded `.git/hooks`) so it lands # in the right place inside a linked worktree or a submodule, where the git dir # is not `.git`. +# +# The core.hooksPath guard below resolves the HOOKS DIRECTORY ITSELF, not its +# parent (frontend-app#809). Two cases the parent-based version got wrong: +# +# symlink core.hooksPath=.githooks where .githooks is a symlink to a shared +# dir. `dirname` is the checkout root, which resolves in-repo, so it +# installed — and the write went THROUGH the symlink into the shared +# dir. The one path element that can point elsewhere was the only +# one never resolved. +# worktree a linked worktree whose core.hooksPath is the main repo's +# .git/hooks. The parent is outside the worktree's toplevel, so it +# skipped — even though that directory belongs to the SAME +# repository, and the worktree silently got no hook. +# +# So: resolve the hooks dir itself when it exists, else the deepest existing +# ancestor (there is no symlink left to resolve below that), and count it as +# in-repo if it is under EITHER the worktree toplevel OR the repo's common git +# dir. The common-git-dir arm is what fixes the worktree case without +# re-opening the shared-dir case the guard legitimately exists to catch. An +# unresolvable path still skips: "cannot tell" is not evidence that it is ours. .PHONY: install-hooks install-hooks: @if ! git rev-parse --git-dir >/dev/null 2>&1; then \ @@ -103,11 +159,19 @@ install-hooks: elif hp="$$(git config --get core.hooksPath 2>/dev/null || true)"; [ -n "$$hp" ] && { \ hd="$$(git rev-parse --git-path hooks)"; \ case "$$hd" in /*) hdd="$$hd";; *) hdd="$$PWD/$$hd";; esac; \ - cpar="$$(cd "$$(dirname "$$hdd")" 2>/dev/null && pwd -P || true)"; \ + hdx="$$hdd"; \ + while [ ! -d "$$hdx" ] && [ "$$hdx" != "$$(dirname "$$hdx")" ]; do \ + hdx="$$(dirname "$$hdx")"; \ + done; \ + chd="$$(cd "$$hdx" 2>/dev/null && pwd -P || true)"; \ ctop="$$(cd "$$(git rev-parse --show-toplevel)" && pwd -P)"; \ - [ -z "$$cpar" ] || case "$$cpar/" in "$$ctop/"*) false;; *) true;; esac; \ + cgd="$$(cd "$$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P || true)"; \ + inr=0; \ + case "$$chd/" in "$$ctop/"*) inr=1;; esac; \ + if [ -n "$$cgd" ]; then case "$$chd/" in "$$cgd/"*) inr=1;; esac; fi; \ + [ -z "$$chd" ] || [ "$$inr" = 0 ]; \ }; then \ - echo "note: core.hooksPath is set to '$$hp', outside this repo — skipping."; \ + echo "note: core.hooksPath is set to '$$hp' (resolves to '$$chd'), outside this repo — skipping."; \ echo " That is a shared hooks dir; installing here would run 'make check' from every repo you push."; \ echo " Add 'make check' to that hook by hand if you want it everywhere."; \ else \ @@ -141,8 +205,23 @@ install-hooks: '#' \ '# Git exports GIT_DIR/GIT_WORK_TREE/etc into hook processes; a nested git' \ '# invocation (from a test, tool, or setuptools-scm) then fails in a linked' \ - '# worktree with exit status 128. Clear them so make check runs as from the shell.' \ + '# worktree with exit status 128. Clear them so the make runs below behave' \ + '# as they do from an ordinary shell.' \ 'unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_COMMON_DIR GIT_OBJECT_DIRECTORY' \ + '#' \ + '# Guarding on make alone was not enough (backend#1995): /usr/bin/make ships' \ + '# with the Xcode CLT and sits on the default launchd PATH, while the tools' \ + '# make check actually runs — yarn/node via nvm/fnm/volta, mint via npm -g —' \ + '# are put on PATH by shell rc files this hook shell never sources. So the' \ + '# skip above passed and the push then hard-failed on "command not found":' \ + '# exactly the outcome the skip exists to prevent, and VS Code offers no' \ + '# --no-verify on push.' \ + '#' \ + '# Ask the Makefile rather than restating the tool list here: guard-toolchain' \ + '# is a prerequisite of check itself, so it cannot drift when check gains a' \ + '# dependency. It guards on the TOOLS, not on installed dependencies — a' \ + '# missing node_modules is a real failure and must not be skipped.' \ + 'make guard-toolchain >/dev/null 2>&1 || exit 0' \ 'exec make check' > "$$hook" && \ chmod +x "$$hook" && \ echo "==> pre-push hook installed at $$hook" && \ From c3b04cacf39198903785eacbc1642f36fa608a9b Mon Sep 17 00:00:00 2001 From: lukasWuttke <54042461+LukasWodka@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:25:38 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix(devex):=20unbreak=20`make=20check`=20?= =?UTF-8?q?=E2=80=94=20mint=20MDX-parses=20CLAUDE.md=20(backend#1606)=20(#?= =?UTF-8?q?120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make check` (`mint broken-links`) has been red on develop since the org-standards sync landed, and the new pre-push hook that runs it would have blocked every push: erro Syntax error - Unable to parse CLAUDE.md - 43:2: Unexpected character `!` (U+0021) before name ... `mint broken-links` MDX-parses the repo-meta markdown at the repo root, and MDX cannot represent an HTML comment. Line 43 of CLAUDE.md is ``, written there by the automated sync from tracebloc/.github — so this was never a content mistake anyone could fix in CLAUDE.md, and it will recur every sync. `.mintignore` already documented this exact failure mode for `.github/` ("the dev server still tries to MDX-parse files inside .github (e.g. pull_request_template.md, which uses HTML +# markers into it — and so `make check`, and the pre-push hook that runs +# it, were red on develop from the day they were added. +# +# Measured, not assumed — each candidate was probed by appending an HTML +# comment and re-running `mint broken-links` (2026-08-14, mint 4.2.802): +# +# CLAUDE.md PARSED -> listed below (this is the one that broke) +# AGENTS.md PARSED -> listed below +# TERMINOLOGY.md PARSED -> listed below +# .cursor/BUGBOT.md PARSED -> listed below (via .cursor/) +# README.md skipped -> auto-ignore genuinely holds here +# CONTRIBUTING.md skipped -> auto-ignore genuinely holds here +# .claude/** skipped -> auto-ignore genuinely holds here +# +# None of these are pages: no root markdown file appears in docs.json +# navigation, so ignoring them checks nothing less. They are listed +# pre-emptively rather than reactively — any of them can grow an HTML +# comment (the org-standards sync is automated and will keep writing +# them), and the failure mode is a red run for every contributor, not a +# local nuisance. .github/ .github/** +.cursor/ +.cursor/** +CLAUDE.md +AGENTS.md +TERMINOLOGY.md # Draft content drafts/ diff --git a/Makefile b/Makefile index a03c662..164d4e9 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,17 @@ help: # ---- check ------------------------------------------------------- # -# MEASURED at 1.8 s on the current tree, and green. +# MEASURED at 1.8 s warm (2.9 s cold) on the current tree, and green. +# +# The "and green" half of that claim was false from the day it was +# written, and is only true again as of this commit. `mint broken-links` +# MDX-parses the repo-meta markdown at the root, and the org-standards +# sync had already written HTML comments — which MDX cannot represent — +# into CLAUDE.md on 2026-08-10, the day before this target landed. So +# `make check` was red on develop for its entire life up to here, and the +# pre-push hook that runs it would have blocked every push. Fixed by +# listing those files in .mintignore, which carries the reasoning and the +# per-file measurements. # # This repo has no test suite. The automated checks on a docs PR are the # org-shared ones (gitleaks + house-rules, which need the shared checker