@biffo/cli 0.170.0 → 0.172.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env sh
2
+ # Conventional Commits, and the core-ownership boundary.
3
+ # See docs/practices/standards/local-gates.md
4
+ root=$(git rev-parse --show-toplevel) || exit 0
5
+ cd "$root" || exit 0
6
+
7
+ # commitlint only where it is configured. The release derives its version bump
8
+ # from the squash-merge subject (ADR-0006), so this matters most in repos that
9
+ # cut releases -- but a repo without the config must not be blocked by it.
10
+ if [ -f commitlint.config.js ] || [ -f commitlint.config.mjs ] || [ -f .commitlintrc.json ]; then
11
+ pnpm exec commitlint --edit "$1" || exit 1
12
+ else
13
+ echo "commit-msg: no commitlint config — subject not checked (see local-gates.md)" >&2
14
+ fi
15
+
16
+ # Refuse commits that edit template-owned paths (#370). Inert in the template
17
+ # and absent in sibling/plugin repos, which own all of their own paths.
18
+ # CI runs the same check, which --no-verify cannot skip.
19
+ if [ -f scripts/biffo.sh ]; then
20
+ sh scripts/biffo.sh check ownership --staged "$1" || exit 1
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env sh
2
+ # Per-file, auto-fixing, sub-second. See docs/practices/standards/local-gates.md
3
+ #
4
+ # Defensive by design: these three hooks are dropped verbatim into every repo in
5
+ # the estate, and they differ in what they have. A hook that fails because the
6
+ # repo has no lint-staged config would block all work there, which is how a
7
+ # safety measure gets ripped out. Absent tooling is reported, never fatal;
8
+ # tooling that IS present and fails is fatal.
9
+ root=$(git rev-parse --show-toplevel) || exit 0
10
+ cd "$root" || exit 0
11
+
12
+ if [ ! -f package.json ]; then
13
+ echo "pre-commit: no package.json — nothing staged-file-level to run" >&2
14
+ exit 0
15
+ fi
16
+ if ! node -e "const p=require('./package.json');process.exit(p['lint-staged']||p.config?.['lint-staged']?0:1)" 2>/dev/null \
17
+ && [ ! -f .lintstagedrc ] && [ ! -f .lintstagedrc.json ] && [ ! -f lint-staged.config.js ]; then
18
+ echo "pre-commit: no lint-staged config — skipping (see local-gates.md)" >&2
19
+ exit 0
20
+ fi
21
+ exec pnpm exec lint-staged
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env sh
2
+ # Run the checks CI runs, before paying for the round trip that would find them.
3
+ #
4
+ # Over the 30 days to 2026-07-29 the estate had 373 failed CI runs, 211 of 342
5
+ # failing steps locally catchable. scripts/verify.sh documents the breakdown and
6
+ # what it deliberately leaves out.
7
+ #
8
+ # Skip in CI. This is a *local* guard; CI runs each check in its own job with its
9
+ # own reporting. CI jobs that push (notably the core-v* tag workflow) have no
10
+ # `uv` on PATH, so firing here made tag pushes fail with `uv: not found` (code
11
+ # 127) and blocked releases. GitHub Actions and every mainstream CI set $CI.
12
+ [ -n "${CI:-}" ] && exit 0
13
+
14
+ # Escape hatch, deliberately explicit rather than a flag on git. `--no-verify`
15
+ # would skip this too, but it also skips every other hook silently; this leaves a
16
+ # decision in the shell history. Its usage rate is an H4 counter-metric.
17
+ if [ -n "${BIFFO_SKIP_VERIFY:-}" ]; then
18
+ echo "pre-push: verify skipped (BIFFO_SKIP_VERIFY set) — CI will run it anyway." >&2
19
+ exit 0
20
+ fi
21
+
22
+ root=$(git rev-parse --show-toplevel) || exit 0
23
+ cd "$root" || exit 0
24
+ if [ ! -f scripts/verify.sh ]; then
25
+ echo "pre-push: no scripts/verify.sh — NO checks ran (see local-gates.md)" >&2
26
+ exit 0
27
+ fi
28
+ exec sh scripts/verify.sh
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Is a git hook actually going to execute here?
4
+ #
5
+ # ## Why this exists
6
+ #
7
+ # A configured hook that does not run is worse than no hook, because it is
8
+ # assumed to be protecting you. On 2026-07-29 the estate was in exactly that
9
+ # state: `core.hooksPath` pointed at `.husky/_`, a **gitignored** directory
10
+ # created only by `prepare: husky` on `pnpm install` — and git resolves that
11
+ # relative path against *each worktree's* root. Every fresh worktree therefore
12
+ # had no hooks, and git said nothing: no warning, no error, no output.
13
+ #
14
+ # AGENTS.md §1 mandates a fresh worktree per unit of work, so the required
15
+ # workflow disarmed its own gates. 6 of 32 working trees were armed. The pre-push
16
+ # pyright, the pre-commit lint-staged and commitlint had all been silently
17
+ # skipped in the other 26 for as long as anyone had been using worktrees.
18
+ #
19
+ # Nothing detected it because nothing looked. This is the thing that looks.
20
+ #
21
+ # ## Verdicts
22
+ #
23
+ # ARMED git will execute a hook here.
24
+ # DEAD core.hooksPath is set and its target is missing or carries no
25
+ # hooks. Git skips silently. **This is the state that lies to you**,
26
+ # and the only one that makes this script exit non-zero.
27
+ # NO-HOOKS no hooks configured at all. Honest, and visible in this report.
28
+ #
29
+ # Usage:
30
+ # sh scripts/hook-audit.sh # this repo and its worktrees
31
+ # sh scripts/hook-audit.sh --estate ~/code # every repo under a directory
32
+ # sh scripts/hook-audit.sh --quiet # verdict counts only
33
+
34
+ set -uo pipefail
35
+
36
+ ESTATE=""
37
+ QUIET=""
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ --estate) ESTATE="$2"; shift 2 ;;
41
+ --quiet) QUIET=1; shift ;;
42
+ *) echo "unknown argument: $1" >&2; exit 2 ;;
43
+ esac
44
+ done
45
+
46
+ armed=0
47
+ dead=0
48
+ nohooks=0
49
+ dead_list=""
50
+
51
+ # The three hooks this standard cares about. A hooksPath directory containing
52
+ # none of them is not armed for our purposes even if it holds something else.
53
+ WANTED='^(pre-commit|pre-push|commit-msg)$'
54
+
55
+ report() {
56
+ tree="$1"
57
+ label="$2"
58
+ hp=$(git -C "$tree" config core.hooksPath 2>/dev/null)
59
+
60
+ if [ -z "$hp" ]; then
61
+ # The default hooks directory is in the **common** git dir, which linked
62
+ # worktrees share. Reading "$tree/.git/hooks" is wrong for exactly the trees
63
+ # this audit exists to check: in a linked worktree `.git` is a *file*
64
+ # containing a gitdir pointer, so that path does not exist and every armed
65
+ # worktree was about to be reported NO-HOOKS. Ask git where it actually is.
66
+ #
67
+ # Git ships .sample files there that never execute, so counting the
68
+ # directory as armed merely for being non-empty would be exactly the false
69
+ # comfort this script exists to remove.
70
+ common=$(git -C "$tree" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
71
+ real=$(ls "${common:-$tree/.git}/hooks" 2>/dev/null | grep -vc '\.sample$' || true)
72
+ if [ "${real:-0}" -gt 0 ]; then
73
+ armed=$((armed + 1))
74
+ [ -n "$QUIET" ] || printf '%-56s %-14s \033[32mARMED\033[0m %s\n' "$label" "(default)" "$real hook(s) in .git/hooks"
75
+ else
76
+ nohooks=$((nohooks + 1))
77
+ [ -n "$QUIET" ] || printf '%-56s %-14s \033[33mNO-HOOKS\033[0m %s\n' "$label" "(default)" "no hooks configured"
78
+ fi
79
+ return
80
+ fi
81
+
82
+ # Relative hooksPath resolves against the working tree root — the whole bug.
83
+ case "$hp" in
84
+ /*) dir="$hp" ;;
85
+ *) dir="$tree/$hp" ;;
86
+ esac
87
+
88
+ present=$(ls "$dir" 2>/dev/null | grep -E "$WANTED" | tr '\n' ',' || true)
89
+ if [ -z "$present" ]; then
90
+ dead=$((dead + 1))
91
+ dead_list="$dead_list $label ($hp)
92
+ "
93
+ [ -n "$QUIET" ] || printf '%-56s %-14s \033[31mDEAD\033[0m %s\n' "$label" "$hp" "$hp missing or holds no hooks — git skips ALL hooks silently"
94
+ else
95
+ armed=$((armed + 1))
96
+ [ -n "$QUIET" ] || printf '%-56s %-14s \033[32mARMED\033[0m %s\n' "$label" "$hp" "${present%,}"
97
+ fi
98
+ }
99
+
100
+ walk_repo() {
101
+ root="${1%/}"
102
+ name="$2"
103
+ # Every working tree, not just the primary — the primary is usually the one
104
+ # that IS armed, which is how this went unnoticed for so long.
105
+ git -C "$root" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2}' | while read -r t; do
106
+ [ -d "$t" ] || continue
107
+ if [ "$t" = "$root" ]; then echo "$t|$name"; else echo "$t|$name${t#$root}"; fi
108
+ done
109
+ }
110
+
111
+ [ -n "$QUIET" ] || printf '%-56s %-14s %-8s %s\n' "WORKING TREE" "hooksPath" "VERDICT" "detail"
112
+
113
+ if [ -n "$ESTATE" ]; then
114
+ targets=$(for d in "$ESTATE"/*/; do
115
+ [ -e "$d/.git" ] || continue
116
+ walk_repo "$d" "$(basename "${d%/}")"
117
+ done)
118
+ else
119
+ root=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "not a git repo" >&2; exit 2; }
120
+ # From inside a worktree, --show-toplevel gives the worktree; walk from the
121
+ # common repo so sibling worktrees are audited too.
122
+ common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
123
+ root=$(dirname "$common")
124
+ targets=$(walk_repo "$root" "$(basename "$root")")
125
+ fi
126
+
127
+ while IFS='|' read -r tree label; do
128
+ [ -n "$tree" ] || continue
129
+ report "$tree" "$label"
130
+ done <<EOF
131
+ $targets
132
+ EOF
133
+
134
+ total=$((armed + dead + nohooks))
135
+ printf '\n%s working trees — \033[32m%s armed\033[0m, \033[31m%s dead\033[0m, %s without hooks' "$total" "$armed" "$dead" "$nohooks"
136
+ [ "$total" -gt 0 ] && printf ' (%s%% armed)' "$((100 * armed / total))"
137
+ printf '\n'
138
+
139
+ if [ "$dead" -gt 0 ]; then
140
+ printf '\n\033[31mDEAD working trees — hooks are configured here and are NOT running:\033[0m\n%s' "$dead_list"
141
+ printf 'Every commit and push made in these is unguarded, and nothing says so.\n'
142
+ printf 'Fix: run `pnpm install` there, or move the repo to tracked .githooks/.\n'
143
+ exit 1
144
+ fi
145
+ exit 0
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Install hook dispatchers into the repository's **shared** hooks directory, so
4
+ # every worktree is guarded — including ones that already exist.
5
+ #
6
+ # ## Why not core.hooksPath
7
+ #
8
+ # #838 moved the hooks into a tracked `.githooks/` and pointed
9
+ # `core.hooksPath` there. That fixed the original bug (a gitignored runner
10
+ # directory that only `pnpm install` created), but it leaves a gap that matters
11
+ # for reaching full coverage:
12
+ #
13
+ # `core.hooksPath` is a **relative** path resolved against each worktree's root,
14
+ # and it lives in the shared config. So the moment it is set, every worktree
15
+ # checked out on a branch that predates `.githooks/` has the config but not the
16
+ # directory — and git is back to skipping silently. AGENTS.md forbids modifying
17
+ # a worktree you did not create, so those cannot simply be rebased.
18
+ #
19
+ # Git's own default is better than the override. With `core.hooksPath` **unset**,
20
+ # a linked worktree runs the hooks in the **common** `.git/hooks` directory —
21
+ # verified 2026-07-29: a dispatcher installed once in the main checkout fired in
22
+ # a pre-existing linked worktree and in one created afterwards, blocking the
23
+ # commit in both, with the hook's `pwd` set to the worktree.
24
+ #
25
+ # So the shared directory is the delivery mechanism and `.githooks/` stays the
26
+ # source of truth for the logic. One install per clone arms every worktree that
27
+ # clone will ever have.
28
+ #
29
+ # ## What a dispatcher does when there is nothing to dispatch to
30
+ #
31
+ # It warns and exits 0. A branch without `.githooks/` has no repo-defined hooks,
32
+ # which is the state it was already in — blocking every commit there would be
33
+ # inventing a gate that branch never had, and would break other agents
34
+ # mid-flight. But it says so, every time, because "no checks ran" being
35
+ # invisible is the whole defect (#838, #839).
36
+ #
37
+ # Run by `prepare`, so `pnpm install` is still what arms a fresh clone. There is
38
+ # no way to ship an armed hook in a repository — that is git's design, not an
39
+ # oversight — so the goal is to need it once per clone rather than once per
40
+ # worktree.
41
+
42
+ set -eu
43
+
44
+ HOOKS_DIR="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)/hooks"
45
+ mkdir -p "$HOOKS_DIR"
46
+
47
+ # An inherited core.hooksPath would make every dispatcher below unreachable, and
48
+ # the failure would be silent. Clearing it is the entire point of the exercise.
49
+ if [ -n "$(git config --local core.hooksPath 2>/dev/null || true)" ]; then
50
+ git config --local --unset core.hooksPath
51
+ fi
52
+
53
+ for hook in pre-commit pre-push commit-msg; do
54
+ cat > "$HOOKS_DIR/$hook" <<EOF
55
+ #!/usr/bin/env sh
56
+ # Generated by scripts/install-hooks.sh — do not edit.
57
+ # The logic lives in the tracked .githooks/ of whichever worktree is running.
58
+ root=\$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
59
+ if [ -x "\$root/.githooks/$hook" ]; then
60
+ exec "\$root/.githooks/$hook" "\$@"
61
+ fi
62
+ if [ -f "\$root/.githooks/$hook" ]; then
63
+ exec sh "\$root/.githooks/$hook" "\$@"
64
+ fi
65
+ # Nothing to run. Say so — an unguarded commit that looks guarded is the defect
66
+ # this whole mechanism exists to remove (#839).
67
+ echo "warning: no .githooks/$hook in \$root — this tree predates the local-gate" >&2
68
+ echo " standard, so NO checks ran. See docs/practices/standards/local-gates.md" >&2
69
+ exit 0
70
+ EOF
71
+ chmod +x "$HOOKS_DIR/$hook"
72
+ done
73
+
74
+ echo "hooks armed: $HOOKS_DIR (shared — every worktree of this clone, now and later)"
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Run the checks CI runs, here, before the push that would have found them.
4
+ #
5
+ # ## Why this exists
6
+ #
7
+ # Until 2026-07-29 the only local gate was a whole-project `pyright` in a
8
+ # pre-push hook -- in the three repos that had hooks at all. Everything else
9
+ # (eslint, prettier, tsc, vitest, ruff, terraform fmt, the plugin guards) ran for
10
+ # the first time on a GitHub runner, after a push, after a PR, after the merge
11
+ # race.
12
+ #
13
+ # Over the 30 days to 2026-07-29, across the twelve repos in the estate that run
14
+ # CI: 373 failed runs, and 211 of 342 failing steps (62%) were locally
15
+ # catchable -- deterministic, offline, no credentials. By kind: tests 49,
16
+ # format 53, typecheck 20, lint 16, terraform fmt 12, and the core ownership
17
+ # guard 11 -- a check already wired as a commit hook, being discovered in the
18
+ # pipeline because the hook was not running.
19
+ #
20
+ # The same file failed the same check across consecutive runs -- e.g.
21
+ # services/api/src/api/routing/crud_handlers.py failing `ruff format --check` on
22
+ # four separate runs. That is the signature of a round trip being used as the
23
+ # check: push, wait for CI, read the failure, fix, push again.
24
+ #
25
+ # ## Why it adapts instead of being tailored
26
+ #
27
+ # This one file runs in the template, in instances, in sibling apps and in
28
+ # plugin repos, whose CI check sets differ. It could have been forked per repo;
29
+ # forks drift, and a gate that has drifted from CI reports a green CI will not
30
+ # honour.
31
+ #
32
+ # So every check is conditional on the repo actually having it, and an
33
+ # inapplicable check prints `n/a` rather than being silently absent. Absence and
34
+ # inapplicability look identical in a summary that omits both, and telling them
35
+ # apart is the entire point (docs/practices/standards/local-gates.md).
36
+ #
37
+ # ## What is deliberately excluded
38
+ #
39
+ # - pytest -- 56s in the template, more than the rest of the gate combined,
40
+ # and it failed once there in 30 days. Opt in per repo with
41
+ # BIFFO_VERIFY_PYTEST=1 where the suite is fast.
42
+ # - app/portal build -- a full Next build.
43
+ # - dependency audits, pip-audit, pnpm audit -- network.
44
+ # - gitleaks -- scans history, not the working tree.
45
+ #
46
+ # cli/src/lib/verify-parity.test.ts fails if the template's CI grows a check
47
+ # that is neither here nor in that written exclusion list.
48
+ #
49
+ # Usage:
50
+ # sh scripts/verify.sh # everything applicable to this repo
51
+ # pnpm run verify # same
52
+ # BIFFO_SKIP_VERIFY=1 git push # escape hatch, for when you mean it
53
+
54
+ set -u
55
+
56
+ FAILED=""
57
+ PASSED=""
58
+ SKIPPED=""
59
+
60
+ PYTEST="${BIFFO_VERIFY_PYTEST:-}"
61
+
62
+ have_script() {
63
+ [ -f package.json ] || return 1
64
+ node -e "process.exit(JSON.parse(require('fs').readFileSync('package.json','utf8')).scripts?.['$1']?0:1)" 2>/dev/null
65
+ }
66
+
67
+ run_check() {
68
+ name="$1"
69
+ shift
70
+ start=$(date +%s)
71
+ if "$@" >"/tmp/biffo-verify.$$" 2>&1; then
72
+ PASSED="$PASSED $name"
73
+ printf ' \033[32mOK\033[0m %-16s %ss\n' "$name" "$(($(date +%s) - start))"
74
+ else
75
+ FAILED="$FAILED $name"
76
+ printf ' \033[31mFAIL\033[0m %-16s %ss\n' "$name" "$(($(date +%s) - start))"
77
+ sed 's/^/ /' "/tmp/biffo-verify.$$" | tail -25
78
+ fi
79
+ rm -f "/tmp/biffo-verify.$$"
80
+ }
81
+
82
+ skip() {
83
+ SKIPPED="$SKIPPED $1"
84
+ printf ' \033[90m-- %-16s n/a - %s\033[0m\n' "$1" "$2"
85
+ }
86
+
87
+ printf '\nverify - the checks CI runs, before the push\n\n'
88
+
89
+ # Python first: ruff is near-instant, so the cheapest feedback on the largest
90
+ # single class of failure comes back immediately.
91
+ if [ -f pyproject.toml ]; then
92
+ if command -v uv >/dev/null 2>&1; then
93
+ run_check ruff-check uv run ruff check .
94
+ run_check ruff-format uv run ruff format --check .
95
+ run_check pyright uv run pyright
96
+ if [ -n "$PYTEST" ]; then
97
+ run_check pytest uv run pytest -q
98
+ else
99
+ skip pytest "excluded - set BIFFO_VERIFY_PYTEST=1 where the suite is fast"
100
+ fi
101
+ else
102
+ skip python "uv not installed"
103
+ fi
104
+ else
105
+ skip python "no pyproject.toml in this repo"
106
+ fi
107
+
108
+ # Terraform, wherever this repo keeps it: modules/ in the template and
109
+ # instances, infra/ and modules/ in siblings.
110
+ if command -v terraform >/dev/null 2>&1; then
111
+ # Scope must match this repo's CI, not exceed it. The template and instances
112
+ # deliberately fmt-check modules/ ONLY: infra/environments/ is user-owned, and
113
+ # a template-shipped check asserting over paths the template does not own is
114
+ # the #325 trap -- it reds an instance on content it neither wrote nor can
115
+ # repair. Siblings own their whole infra/ and their CI checks it, so they get
116
+ # both. biffo.sibling.json is what tells them apart.
117
+ tf_dirs=""
118
+ [ -d modules ] && tf_dirs="$tf_dirs modules/"
119
+ [ -f biffo.sibling.json ] && [ -d infra ] && tf_dirs="$tf_dirs infra/"
120
+ if [ -n "$tf_dirs" ]; then
121
+ # shellcheck disable=SC2086
122
+ run_check terraform-fmt terraform fmt -check -recursive $tf_dirs
123
+ else
124
+ skip terraform-fmt "no terraform in this repo"
125
+ fi
126
+ else
127
+ skip terraform-fmt "terraform not installed"
128
+ fi
129
+
130
+ # The Biffo guards, where the dispatcher exists. Cheap, and two of them
131
+ # (ownership, plugin-terraform) were being caught in CI.
132
+ if [ -f scripts/biffo.sh ]; then
133
+ run_check plugin-tf sh scripts/biffo.sh check plugin-terraform
134
+ run_check plugin-names sh scripts/biffo.sh check plugin-collisions
135
+ else
136
+ skip biffo-guards "no scripts/biffo.sh in this repo"
137
+ fi
138
+
139
+ # JS, cheapest first; `test` last because it is slowest and the most likely to
140
+ # be interrupted by an impatient reader.
141
+ if [ -f package.json ]; then
142
+ skip build "excluded - a full app build is too slow for a push gate"
143
+ for s in lint typecheck format:check test; do
144
+ label=$(printf '%s' "$s" | tr -d ':')
145
+ if have_script "$s"; then
146
+ run_check "$label" pnpm run "$s"
147
+ else
148
+ skip "$label" "no \"$s\" script in package.json"
149
+ fi
150
+ done
151
+ else
152
+ skip javascript "no package.json in this repo"
153
+ fi
154
+
155
+ printf '\n'
156
+ if [ -n "$FAILED" ]; then
157
+ printf '\033[31mverify failed:\033[0m%s\n' "$FAILED"
158
+ printf 'Fix these here - CI will find them anyway, three minutes and a merge race later.\n'
159
+ printf 'Most format failures are one command: pnpm run format\n\n'
160
+ exit 1
161
+ fi
162
+ printf '\033[32mverify passed\033[0m -%s\n' "$PASSED"
163
+ [ -n "$SKIPPED" ] && printf '\033[90mnot applicable here:%s\033[0m\n' "$SKIPPED"
164
+ printf '\n'
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env sh
2
+ # Conventional Commits, and the core-ownership boundary.
3
+ # See docs/practices/standards/local-gates.md
4
+ root=$(git rev-parse --show-toplevel) || exit 0
5
+ cd "$root" || exit 0
6
+
7
+ # commitlint only where it is configured. The release derives its version bump
8
+ # from the squash-merge subject (ADR-0006), so this matters most in repos that
9
+ # cut releases -- but a repo without the config must not be blocked by it.
10
+ if [ -f commitlint.config.js ] || [ -f commitlint.config.mjs ] || [ -f .commitlintrc.json ]; then
11
+ pnpm exec commitlint --edit "$1" || exit 1
12
+ else
13
+ echo "commit-msg: no commitlint config — subject not checked (see local-gates.md)" >&2
14
+ fi
15
+
16
+ # Refuse commits that edit template-owned paths (#370). Inert in the template
17
+ # and absent in sibling/plugin repos, which own all of their own paths.
18
+ # CI runs the same check, which --no-verify cannot skip.
19
+ if [ -f scripts/biffo.sh ]; then
20
+ sh scripts/biffo.sh check ownership --staged "$1" || exit 1
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env sh
2
+ # Per-file, auto-fixing, sub-second. See docs/practices/standards/local-gates.md
3
+ #
4
+ # Defensive by design: these three hooks are dropped verbatim into every repo in
5
+ # the estate, and they differ in what they have. A hook that fails because the
6
+ # repo has no lint-staged config would block all work there, which is how a
7
+ # safety measure gets ripped out. Absent tooling is reported, never fatal;
8
+ # tooling that IS present and fails is fatal.
9
+ root=$(git rev-parse --show-toplevel) || exit 0
10
+ cd "$root" || exit 0
11
+
12
+ if [ ! -f package.json ]; then
13
+ echo "pre-commit: no package.json — nothing staged-file-level to run" >&2
14
+ exit 0
15
+ fi
16
+ if ! node -e "const p=require('./package.json');process.exit(p['lint-staged']||p.config?.['lint-staged']?0:1)" 2>/dev/null \
17
+ && [ ! -f .lintstagedrc ] && [ ! -f .lintstagedrc.json ] && [ ! -f lint-staged.config.js ]; then
18
+ echo "pre-commit: no lint-staged config — skipping (see local-gates.md)" >&2
19
+ exit 0
20
+ fi
21
+ exec pnpm exec lint-staged
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env sh
2
+ # Run the checks CI runs, before paying for the round trip that would find them.
3
+ #
4
+ # Over the 30 days to 2026-07-29 the estate had 373 failed CI runs, 211 of 342
5
+ # failing steps locally catchable. scripts/verify.sh documents the breakdown and
6
+ # what it deliberately leaves out.
7
+ #
8
+ # Skip in CI. This is a *local* guard; CI runs each check in its own job with its
9
+ # own reporting. CI jobs that push (notably the core-v* tag workflow) have no
10
+ # `uv` on PATH, so firing here made tag pushes fail with `uv: not found` (code
11
+ # 127) and blocked releases. GitHub Actions and every mainstream CI set $CI.
12
+ [ -n "${CI:-}" ] && exit 0
13
+
14
+ # Escape hatch, deliberately explicit rather than a flag on git. `--no-verify`
15
+ # would skip this too, but it also skips every other hook silently; this leaves a
16
+ # decision in the shell history. Its usage rate is an H4 counter-metric.
17
+ if [ -n "${BIFFO_SKIP_VERIFY:-}" ]; then
18
+ echo "pre-push: verify skipped (BIFFO_SKIP_VERIFY set) — CI will run it anyway." >&2
19
+ exit 0
20
+ fi
21
+
22
+ root=$(git rev-parse --show-toplevel) || exit 0
23
+ cd "$root" || exit 0
24
+ if [ ! -f scripts/verify.sh ]; then
25
+ echo "pre-push: no scripts/verify.sh — NO checks ran (see local-gates.md)" >&2
26
+ exit 0
27
+ fi
28
+ exec sh scripts/verify.sh
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Is a git hook actually going to execute here?
4
+ #
5
+ # ## Why this exists
6
+ #
7
+ # A configured hook that does not run is worse than no hook, because it is
8
+ # assumed to be protecting you. On 2026-07-29 the estate was in exactly that
9
+ # state: `core.hooksPath` pointed at `.husky/_`, a **gitignored** directory
10
+ # created only by `prepare: husky` on `pnpm install` — and git resolves that
11
+ # relative path against *each worktree's* root. Every fresh worktree therefore
12
+ # had no hooks, and git said nothing: no warning, no error, no output.
13
+ #
14
+ # AGENTS.md §1 mandates a fresh worktree per unit of work, so the required
15
+ # workflow disarmed its own gates. 6 of 32 working trees were armed. The pre-push
16
+ # pyright, the pre-commit lint-staged and commitlint had all been silently
17
+ # skipped in the other 26 for as long as anyone had been using worktrees.
18
+ #
19
+ # Nothing detected it because nothing looked. This is the thing that looks.
20
+ #
21
+ # ## Verdicts
22
+ #
23
+ # ARMED git will execute a hook here.
24
+ # DEAD core.hooksPath is set and its target is missing or carries no
25
+ # hooks. Git skips silently. **This is the state that lies to you**,
26
+ # and the only one that makes this script exit non-zero.
27
+ # NO-HOOKS no hooks configured at all. Honest, and visible in this report.
28
+ #
29
+ # Usage:
30
+ # sh scripts/hook-audit.sh # this repo and its worktrees
31
+ # sh scripts/hook-audit.sh --estate ~/code # every repo under a directory
32
+ # sh scripts/hook-audit.sh --quiet # verdict counts only
33
+
34
+ set -uo pipefail
35
+
36
+ ESTATE=""
37
+ QUIET=""
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ --estate) ESTATE="$2"; shift 2 ;;
41
+ --quiet) QUIET=1; shift ;;
42
+ *) echo "unknown argument: $1" >&2; exit 2 ;;
43
+ esac
44
+ done
45
+
46
+ armed=0
47
+ dead=0
48
+ nohooks=0
49
+ dead_list=""
50
+
51
+ # The three hooks this standard cares about. A hooksPath directory containing
52
+ # none of them is not armed for our purposes even if it holds something else.
53
+ WANTED='^(pre-commit|pre-push|commit-msg)$'
54
+
55
+ report() {
56
+ tree="$1"
57
+ label="$2"
58
+ hp=$(git -C "$tree" config core.hooksPath 2>/dev/null)
59
+
60
+ if [ -z "$hp" ]; then
61
+ # The default hooks directory is in the **common** git dir, which linked
62
+ # worktrees share. Reading "$tree/.git/hooks" is wrong for exactly the trees
63
+ # this audit exists to check: in a linked worktree `.git` is a *file*
64
+ # containing a gitdir pointer, so that path does not exist and every armed
65
+ # worktree was about to be reported NO-HOOKS. Ask git where it actually is.
66
+ #
67
+ # Git ships .sample files there that never execute, so counting the
68
+ # directory as armed merely for being non-empty would be exactly the false
69
+ # comfort this script exists to remove.
70
+ common=$(git -C "$tree" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
71
+ real=$(ls "${common:-$tree/.git}/hooks" 2>/dev/null | grep -vc '\.sample$' || true)
72
+ if [ "${real:-0}" -gt 0 ]; then
73
+ armed=$((armed + 1))
74
+ [ -n "$QUIET" ] || printf '%-56s %-14s \033[32mARMED\033[0m %s\n' "$label" "(default)" "$real hook(s) in .git/hooks"
75
+ else
76
+ nohooks=$((nohooks + 1))
77
+ [ -n "$QUIET" ] || printf '%-56s %-14s \033[33mNO-HOOKS\033[0m %s\n' "$label" "(default)" "no hooks configured"
78
+ fi
79
+ return
80
+ fi
81
+
82
+ # Relative hooksPath resolves against the working tree root — the whole bug.
83
+ case "$hp" in
84
+ /*) dir="$hp" ;;
85
+ *) dir="$tree/$hp" ;;
86
+ esac
87
+
88
+ present=$(ls "$dir" 2>/dev/null | grep -E "$WANTED" | tr '\n' ',' || true)
89
+ if [ -z "$present" ]; then
90
+ dead=$((dead + 1))
91
+ dead_list="$dead_list $label ($hp)
92
+ "
93
+ [ -n "$QUIET" ] || printf '%-56s %-14s \033[31mDEAD\033[0m %s\n' "$label" "$hp" "$hp missing or holds no hooks — git skips ALL hooks silently"
94
+ else
95
+ armed=$((armed + 1))
96
+ [ -n "$QUIET" ] || printf '%-56s %-14s \033[32mARMED\033[0m %s\n' "$label" "$hp" "${present%,}"
97
+ fi
98
+ }
99
+
100
+ walk_repo() {
101
+ root="${1%/}"
102
+ name="$2"
103
+ # Every working tree, not just the primary — the primary is usually the one
104
+ # that IS armed, which is how this went unnoticed for so long.
105
+ git -C "$root" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2}' | while read -r t; do
106
+ [ -d "$t" ] || continue
107
+ if [ "$t" = "$root" ]; then echo "$t|$name"; else echo "$t|$name${t#$root}"; fi
108
+ done
109
+ }
110
+
111
+ [ -n "$QUIET" ] || printf '%-56s %-14s %-8s %s\n' "WORKING TREE" "hooksPath" "VERDICT" "detail"
112
+
113
+ if [ -n "$ESTATE" ]; then
114
+ targets=$(for d in "$ESTATE"/*/; do
115
+ [ -e "$d/.git" ] || continue
116
+ walk_repo "$d" "$(basename "${d%/}")"
117
+ done)
118
+ else
119
+ root=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "not a git repo" >&2; exit 2; }
120
+ # From inside a worktree, --show-toplevel gives the worktree; walk from the
121
+ # common repo so sibling worktrees are audited too.
122
+ common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
123
+ root=$(dirname "$common")
124
+ targets=$(walk_repo "$root" "$(basename "$root")")
125
+ fi
126
+
127
+ while IFS='|' read -r tree label; do
128
+ [ -n "$tree" ] || continue
129
+ report "$tree" "$label"
130
+ done <<EOF
131
+ $targets
132
+ EOF
133
+
134
+ total=$((armed + dead + nohooks))
135
+ printf '\n%s working trees — \033[32m%s armed\033[0m, \033[31m%s dead\033[0m, %s without hooks' "$total" "$armed" "$dead" "$nohooks"
136
+ [ "$total" -gt 0 ] && printf ' (%s%% armed)' "$((100 * armed / total))"
137
+ printf '\n'
138
+
139
+ if [ "$dead" -gt 0 ]; then
140
+ printf '\n\033[31mDEAD working trees — hooks are configured here and are NOT running:\033[0m\n%s' "$dead_list"
141
+ printf 'Every commit and push made in these is unguarded, and nothing says so.\n'
142
+ printf 'Fix: run `pnpm install` there, or move the repo to tracked .githooks/.\n'
143
+ exit 1
144
+ fi
145
+ exit 0
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Install hook dispatchers into the repository's **shared** hooks directory, so
4
+ # every worktree is guarded — including ones that already exist.
5
+ #
6
+ # ## Why not core.hooksPath
7
+ #
8
+ # #838 moved the hooks into a tracked `.githooks/` and pointed
9
+ # `core.hooksPath` there. That fixed the original bug (a gitignored runner
10
+ # directory that only `pnpm install` created), but it leaves a gap that matters
11
+ # for reaching full coverage:
12
+ #
13
+ # `core.hooksPath` is a **relative** path resolved against each worktree's root,
14
+ # and it lives in the shared config. So the moment it is set, every worktree
15
+ # checked out on a branch that predates `.githooks/` has the config but not the
16
+ # directory — and git is back to skipping silently. AGENTS.md forbids modifying
17
+ # a worktree you did not create, so those cannot simply be rebased.
18
+ #
19
+ # Git's own default is better than the override. With `core.hooksPath` **unset**,
20
+ # a linked worktree runs the hooks in the **common** `.git/hooks` directory —
21
+ # verified 2026-07-29: a dispatcher installed once in the main checkout fired in
22
+ # a pre-existing linked worktree and in one created afterwards, blocking the
23
+ # commit in both, with the hook's `pwd` set to the worktree.
24
+ #
25
+ # So the shared directory is the delivery mechanism and `.githooks/` stays the
26
+ # source of truth for the logic. One install per clone arms every worktree that
27
+ # clone will ever have.
28
+ #
29
+ # ## What a dispatcher does when there is nothing to dispatch to
30
+ #
31
+ # It warns and exits 0. A branch without `.githooks/` has no repo-defined hooks,
32
+ # which is the state it was already in — blocking every commit there would be
33
+ # inventing a gate that branch never had, and would break other agents
34
+ # mid-flight. But it says so, every time, because "no checks ran" being
35
+ # invisible is the whole defect (#838, #839).
36
+ #
37
+ # Run by `prepare`, so `pnpm install` is still what arms a fresh clone. There is
38
+ # no way to ship an armed hook in a repository — that is git's design, not an
39
+ # oversight — so the goal is to need it once per clone rather than once per
40
+ # worktree.
41
+
42
+ set -eu
43
+
44
+ HOOKS_DIR="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)/hooks"
45
+ mkdir -p "$HOOKS_DIR"
46
+
47
+ # An inherited core.hooksPath would make every dispatcher below unreachable, and
48
+ # the failure would be silent. Clearing it is the entire point of the exercise.
49
+ if [ -n "$(git config --local core.hooksPath 2>/dev/null || true)" ]; then
50
+ git config --local --unset core.hooksPath
51
+ fi
52
+
53
+ for hook in pre-commit pre-push commit-msg; do
54
+ cat > "$HOOKS_DIR/$hook" <<EOF
55
+ #!/usr/bin/env sh
56
+ # Generated by scripts/install-hooks.sh — do not edit.
57
+ # The logic lives in the tracked .githooks/ of whichever worktree is running.
58
+ root=\$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
59
+ if [ -x "\$root/.githooks/$hook" ]; then
60
+ exec "\$root/.githooks/$hook" "\$@"
61
+ fi
62
+ if [ -f "\$root/.githooks/$hook" ]; then
63
+ exec sh "\$root/.githooks/$hook" "\$@"
64
+ fi
65
+ # Nothing to run. Say so — an unguarded commit that looks guarded is the defect
66
+ # this whole mechanism exists to remove (#839).
67
+ echo "warning: no .githooks/$hook in \$root — this tree predates the local-gate" >&2
68
+ echo " standard, so NO checks ran. See docs/practices/standards/local-gates.md" >&2
69
+ exit 0
70
+ EOF
71
+ chmod +x "$HOOKS_DIR/$hook"
72
+ done
73
+
74
+ echo "hooks armed: $HOOKS_DIR (shared — every worktree of this clone, now and later)"
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Run the checks CI runs, here, before the push that would have found them.
4
+ #
5
+ # ## Why this exists
6
+ #
7
+ # Until 2026-07-29 the only local gate was a whole-project `pyright` in a
8
+ # pre-push hook -- in the three repos that had hooks at all. Everything else
9
+ # (eslint, prettier, tsc, vitest, ruff, terraform fmt, the plugin guards) ran for
10
+ # the first time on a GitHub runner, after a push, after a PR, after the merge
11
+ # race.
12
+ #
13
+ # Over the 30 days to 2026-07-29, across the twelve repos in the estate that run
14
+ # CI: 373 failed runs, and 211 of 342 failing steps (62%) were locally
15
+ # catchable -- deterministic, offline, no credentials. By kind: tests 49,
16
+ # format 53, typecheck 20, lint 16, terraform fmt 12, and the core ownership
17
+ # guard 11 -- a check already wired as a commit hook, being discovered in the
18
+ # pipeline because the hook was not running.
19
+ #
20
+ # The same file failed the same check across consecutive runs -- e.g.
21
+ # services/api/src/api/routing/crud_handlers.py failing `ruff format --check` on
22
+ # four separate runs. That is the signature of a round trip being used as the
23
+ # check: push, wait for CI, read the failure, fix, push again.
24
+ #
25
+ # ## Why it adapts instead of being tailored
26
+ #
27
+ # This one file runs in the template, in instances, in sibling apps and in
28
+ # plugin repos, whose CI check sets differ. It could have been forked per repo;
29
+ # forks drift, and a gate that has drifted from CI reports a green CI will not
30
+ # honour.
31
+ #
32
+ # So every check is conditional on the repo actually having it, and an
33
+ # inapplicable check prints `n/a` rather than being silently absent. Absence and
34
+ # inapplicability look identical in a summary that omits both, and telling them
35
+ # apart is the entire point (docs/practices/standards/local-gates.md).
36
+ #
37
+ # ## What is deliberately excluded
38
+ #
39
+ # - pytest -- 56s in the template, more than the rest of the gate combined,
40
+ # and it failed once there in 30 days. Opt in per repo with
41
+ # BIFFO_VERIFY_PYTEST=1 where the suite is fast.
42
+ # - app/portal build -- a full Next build.
43
+ # - dependency audits, pip-audit, pnpm audit -- network.
44
+ # - gitleaks -- scans history, not the working tree.
45
+ #
46
+ # cli/src/lib/verify-parity.test.ts fails if the template's CI grows a check
47
+ # that is neither here nor in that written exclusion list.
48
+ #
49
+ # Usage:
50
+ # sh scripts/verify.sh # everything applicable to this repo
51
+ # pnpm run verify # same
52
+ # BIFFO_SKIP_VERIFY=1 git push # escape hatch, for when you mean it
53
+
54
+ set -u
55
+
56
+ FAILED=""
57
+ PASSED=""
58
+ SKIPPED=""
59
+
60
+ PYTEST="${BIFFO_VERIFY_PYTEST:-}"
61
+
62
+ have_script() {
63
+ [ -f package.json ] || return 1
64
+ node -e "process.exit(JSON.parse(require('fs').readFileSync('package.json','utf8')).scripts?.['$1']?0:1)" 2>/dev/null
65
+ }
66
+
67
+ run_check() {
68
+ name="$1"
69
+ shift
70
+ start=$(date +%s)
71
+ if "$@" >"/tmp/biffo-verify.$$" 2>&1; then
72
+ PASSED="$PASSED $name"
73
+ printf ' \033[32mOK\033[0m %-16s %ss\n' "$name" "$(($(date +%s) - start))"
74
+ else
75
+ FAILED="$FAILED $name"
76
+ printf ' \033[31mFAIL\033[0m %-16s %ss\n' "$name" "$(($(date +%s) - start))"
77
+ sed 's/^/ /' "/tmp/biffo-verify.$$" | tail -25
78
+ fi
79
+ rm -f "/tmp/biffo-verify.$$"
80
+ }
81
+
82
+ skip() {
83
+ SKIPPED="$SKIPPED $1"
84
+ printf ' \033[90m-- %-16s n/a - %s\033[0m\n' "$1" "$2"
85
+ }
86
+
87
+ printf '\nverify - the checks CI runs, before the push\n\n'
88
+
89
+ # Python first: ruff is near-instant, so the cheapest feedback on the largest
90
+ # single class of failure comes back immediately.
91
+ if [ -f pyproject.toml ]; then
92
+ if command -v uv >/dev/null 2>&1; then
93
+ run_check ruff-check uv run ruff check .
94
+ run_check ruff-format uv run ruff format --check .
95
+ run_check pyright uv run pyright
96
+ if [ -n "$PYTEST" ]; then
97
+ run_check pytest uv run pytest -q
98
+ else
99
+ skip pytest "excluded - set BIFFO_VERIFY_PYTEST=1 where the suite is fast"
100
+ fi
101
+ else
102
+ skip python "uv not installed"
103
+ fi
104
+ else
105
+ skip python "no pyproject.toml in this repo"
106
+ fi
107
+
108
+ # Terraform, wherever this repo keeps it: modules/ in the template and
109
+ # instances, infra/ and modules/ in siblings.
110
+ if command -v terraform >/dev/null 2>&1; then
111
+ # Scope must match this repo's CI, not exceed it. The template and instances
112
+ # deliberately fmt-check modules/ ONLY: infra/environments/ is user-owned, and
113
+ # a template-shipped check asserting over paths the template does not own is
114
+ # the #325 trap -- it reds an instance on content it neither wrote nor can
115
+ # repair. Siblings own their whole infra/ and their CI checks it, so they get
116
+ # both. biffo.sibling.json is what tells them apart.
117
+ tf_dirs=""
118
+ [ -d modules ] && tf_dirs="$tf_dirs modules/"
119
+ [ -f biffo.sibling.json ] && [ -d infra ] && tf_dirs="$tf_dirs infra/"
120
+ if [ -n "$tf_dirs" ]; then
121
+ # shellcheck disable=SC2086
122
+ run_check terraform-fmt terraform fmt -check -recursive $tf_dirs
123
+ else
124
+ skip terraform-fmt "no terraform in this repo"
125
+ fi
126
+ else
127
+ skip terraform-fmt "terraform not installed"
128
+ fi
129
+
130
+ # The Biffo guards, where the dispatcher exists. Cheap, and two of them
131
+ # (ownership, plugin-terraform) were being caught in CI.
132
+ if [ -f scripts/biffo.sh ]; then
133
+ run_check plugin-tf sh scripts/biffo.sh check plugin-terraform
134
+ run_check plugin-names sh scripts/biffo.sh check plugin-collisions
135
+ else
136
+ skip biffo-guards "no scripts/biffo.sh in this repo"
137
+ fi
138
+
139
+ # JS, cheapest first; `test` last because it is slowest and the most likely to
140
+ # be interrupted by an impatient reader.
141
+ if [ -f package.json ]; then
142
+ skip build "excluded - a full app build is too slow for a push gate"
143
+ for s in lint typecheck format:check test; do
144
+ label=$(printf '%s' "$s" | tr -d ':')
145
+ if have_script "$s"; then
146
+ run_check "$label" pnpm run "$s"
147
+ else
148
+ skip "$label" "no \"$s\" script in package.json"
149
+ fi
150
+ done
151
+ else
152
+ skip javascript "no package.json in this repo"
153
+ fi
154
+
155
+ printf '\n'
156
+ if [ -n "$FAILED" ]; then
157
+ printf '\033[31mverify failed:\033[0m%s\n' "$FAILED"
158
+ printf 'Fix these here - CI will find them anyway, three minutes and a merge race later.\n'
159
+ printf 'Most format failures are one command: pnpm run format\n\n'
160
+ exit 1
161
+ fi
162
+ printf '\033[32mverify passed\033[0m -%s\n' "$PASSED"
163
+ [ -n "$SKIPPED" ] && printf '\033[90mnot applicable here:%s\033[0m\n' "$SKIPPED"
164
+ printf '\n'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.170.0",
3
+ "version": "0.172.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",