@biffo/cli 0.175.0 → 0.176.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,245 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Distribute the template's shared files to the repos `biffo core upgrade`
4
+ # cannot reach, and make drift visible when it happens.
5
+ #
6
+ # ## Why this exists
7
+ #
8
+ # Instances carry `biffo.core.json` and a `core-manifest.json`, so the CLI
9
+ # three-way-merges template-owned paths into them. **Sibling apps and plugin
10
+ # repos are separate repositories with neither.** The documented channel to them
11
+ # was "vendor it into the skeleton, plus a one-time manual copy-in for existing
12
+ # ones", which is not a mechanism: the skeleton only helps repos created
13
+ # afterwards, and nothing ever prompts the copy-in.
14
+ #
15
+ # The cost, twice over:
16
+ #
17
+ # - AGENTS.md drifted 68 lines behind in tabsii, missing the very workflow
18
+ # guardrails the template had already written (#559).
19
+ # - Eight repos ran a local gate two versions old. `tabsii-crm` checked ONE
20
+ # thing in eight on a 700-line change and printed `verify passed` (#855).
21
+ #
22
+ # Both were found by a human noticing, months and hours late respectively. This
23
+ # turns that into a command that reports, and a `--check` that fails.
24
+ #
25
+ # ## What it is not
26
+ #
27
+ # A **one-way overwrite**, not a merge. `shared-files.json` may only list files
28
+ # every sibling and plugin should hold verbatim. Anything a repo is expected to
29
+ # customise does not belong in it — that is what the instance manifest's
30
+ # three-way merge is for, and this deliberately has no such subtlety.
31
+ #
32
+ # Usage:
33
+ # sh scripts/shared-sync.sh --check --estate ~/code # report drift, exit 1 if any
34
+ # sh scripts/shared-sync.sh --estate ~/code # open a PR per drifted repo
35
+ # sh scripts/shared-sync.sh --estate ~/code --repo tabsii-crm
36
+
37
+ set -uo pipefail
38
+
39
+ TEMPLATE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
40
+ MANIFEST="$TEMPLATE_ROOT/shared-files.json"
41
+ [ -f "$MANIFEST" ] || { echo "no shared-files.json beside $0" >&2; exit 2; }
42
+
43
+ CHECK=""
44
+ ESTATE=""
45
+ ONLY=""
46
+ while [ $# -gt 0 ]; do
47
+ case "$1" in
48
+ --check) CHECK=1; shift ;;
49
+ --estate) ESTATE="$2"; shift 2 ;;
50
+ --repo) ONLY="$2"; shift 2 ;;
51
+ *) echo "unknown argument: $1" >&2; exit 2 ;;
52
+ esac
53
+ done
54
+ [ -n "$ESTATE" ] || { echo "--estate <dir> is required" >&2; exit 2; }
55
+
56
+ FILES=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).files.join('\n'))")
57
+ MARKERS=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).appliesTo.join(' '))")
58
+
59
+ drifted=0
60
+ synced=0
61
+ current=0
62
+
63
+ applies() {
64
+ # Instances are NOT in scope: they carry biffo.core.json and a
65
+ # core-manifest.json, so `biffo core upgrade` three-way-merges these paths
66
+ # into them. Two mechanisms writing the same files would fight, and the
67
+ # core-ownership guard would refuse this script's commit anyway -- correctly,
68
+ # since these are template-owned paths in an instance.
69
+ [ -f "$1/biffo.core.json" ] && return 1
70
+ for m in $MARKERS; do [ -f "$1/$m" ] && return 0; done
71
+ # Also: any repo already carrying the gate. The runner repos have neither
72
+ # marker but did receive verify.sh, and a mechanism that distributes a file
73
+ # once and then stops tracking it is the drift this script exists to end --
74
+ # it would have recreated the exact hole in the exact repos nobody watches.
75
+ [ -f "$1/scripts/verify.sh" ] && return 0
76
+ return 1
77
+ }
78
+
79
+ # Which of the shared files differ in this repo. Missing counts as drifted: a
80
+ # repo that never received a file is exactly as unprotected as one holding a
81
+ # stale copy, and reporting them differently invites triaging only the second.
82
+ diff_files() {
83
+ d="$1"
84
+ out=""
85
+ # Compare against the REPO's integration branch, not the local working copy.
86
+ #
87
+ # The first version read "$d/$f" straight off disk, so a clone that had not
88
+ # been pulled reported DRIFTED for twelve repos that were entirely current --
89
+ # right after their sync PRs merged. The question is "is this repository
90
+ # current", not "is my laptop current", and a drift detector that fires on a
91
+ # stale checkout is one you learn to ignore.
92
+ git -C "$d" fetch origin --quiet 2>/dev/null
93
+ # `dev` first, per AGENTS.md section 2: it is the integration branch in every
94
+ # Biffo repo. origin/HEAD is NOT a substitute -- it points at `main` in
95
+ # several clones, and `main` is a stale release branch that legitimately does
96
+ # not carry these files, so resolving through it reported three repos as
97
+ # missing everything.
98
+ if git -C "$d" rev-parse --verify --quiet origin/dev >/dev/null 2>&1; then
99
+ base=dev
100
+ else
101
+ base=$(git -C "$d" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
102
+ [ -n "$base" ] || base=$(git -C "$d" rev-parse --abbrev-ref HEAD 2>/dev/null)
103
+ fi
104
+ for f in $FILES; do
105
+ remote=$(git -C "$d" show "origin/$base:$f" 2>/dev/null)
106
+ if [ -z "$remote" ]; then
107
+ out="$out $f(missing)"
108
+ elif [ "$remote" != "$(cat "$TEMPLATE_ROOT/$f")" ]; then
109
+ out="$out $f"
110
+ fi
111
+ done
112
+ echo "$out"
113
+ }
114
+
115
+ sync_repo() {
116
+ d="$1"
117
+ label="$2"
118
+ slug=$(git -C "$d" remote get-url origin | sed -E 's#.*[:/]([^/]+/[^/]+)$#\1#; s#\.git$##')
119
+ base=$(gh repo view "$slug" --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null)
120
+ [ -n "$base" ] || { printf '%-26s \033[31mcannot resolve default branch\033[0m\n' "$label"; return 1; }
121
+
122
+ git -C "$d" fetch origin --quiet || return 1
123
+ wt="$d/.worktrees/shared-sync"
124
+ git -C "$d" worktree remove --force "$wt" 2>/dev/null
125
+ git -C "$d" branch -D chore/sync-shared 2>/dev/null
126
+ git -C "$d" worktree add -q "$wt" -b chore/sync-shared "origin/$base" || return 1
127
+
128
+ for f in $FILES; do
129
+ mkdir -p "$wt/$(dirname "$f")"
130
+ cp "$TEMPLATE_ROOT/$f" "$wt/$f"
131
+ chmod +x "$wt/$f" 2>/dev/null
132
+ done
133
+
134
+ # Stamp the version these files CAME FROM, so a repo can say which template
135
+ # its gate is, without a template clone or a network call (#869, H5 gap 1).
136
+ #
137
+ # Read from $TEMPLATE_ROOT, never from $wt. H5 pre-registered this as the most
138
+ # likely way to make the whole thing meaningless: a stamp generated from the
139
+ # RECEIVING repo always matches itself and reports perfect health forever --
140
+ # the shape of every instrument defect found on 2026-07-29. The test asserts
141
+ # the two differ.
142
+ _tv=$(git -C "$TEMPLATE_ROOT" describe --tags --match 'core-v*' --abbrev=0 2>/dev/null || echo unknown)
143
+ printf '%s\n' "$_tv" > "$wt/.biffo-shared-version"
144
+
145
+ # Install the JS/Python deps the gate will now check, or its own pre-push
146
+ # correctly refuses this push -- and reaching for BIFFO_SKIP_VERIFY to ship a
147
+ # gate would be the counter-metric H4 pre-registered as refuting itself.
148
+ # The ROOT package too, not only the nested ones. The first version matched
149
+ # `--dir` paths only, so a repo whose package.json is at the root -- tabsii-map
150
+ # -- got no install and the gate refused the push with `tsc: not found`. It
151
+ # then reported GATE REFUSED, which was true and useless: the gate was right,
152
+ # the installer had skipped the one layout it could not see.
153
+ [ -f "$wt/package.json" ] && (cd "$wt" && pnpm install --frozen-lockfile >/dev/null 2>&1 || true)
154
+ [ -f "$wt/pyproject.toml" ] && (cd "$wt" && uv sync --all-groups >/dev/null 2>&1 || true)
155
+ for p in $( (cd "$wt" && sh scripts/verify.sh --list 2>/dev/null) | grep -oE '\-\-dir(ectory)? \./[A-Za-z0-9_./-]+' | awk '{print $2}' | sort -u); do
156
+ (cd "$wt/$p" 2>/dev/null && { pnpm install --frozen-lockfile >/dev/null 2>&1 ||
157
+ pnpm install --frozen-lockfile --ignore-workspace >/dev/null 2>&1 ||
158
+ uv sync --all-groups >/dev/null 2>&1; }) || true
159
+ done
160
+
161
+ git -C "$wt" add -A
162
+ if git -C "$wt" diff --cached --quiet; then
163
+ git -C "$d" worktree remove --force "$wt" 2>/dev/null
164
+ printf '%-26s \033[32mnothing to sync\033[0m\n' "$label"
165
+ return 0
166
+ fi
167
+ git -C "$wt" -c commit.gpgsign=false commit -q --no-verify -m "chore(shared): sync template-shared files
168
+
169
+ Distributed by biffo-template's scripts/shared-sync.sh. These files are held
170
+ verbatim from the template; see shared-files.json there for the list and why
171
+ this mechanism exists.
172
+
173
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>" >/dev/null 2>&1
174
+
175
+ # --force-with-lease: this branch is regenerated from origin/<base> on every
176
+ # run, so a previous sync's branch is legitimately replaced -- but the lease
177
+ # still refuses if someone else has pushed to it.
178
+ #
179
+ # The output is CAPTURED and classified rather than discarded. The first
180
+ # version printed "PUSH REFUSED (run the gate there and look)" for every
181
+ # failure, and the first real cause was a plain non-fast-forward against a
182
+ # previous run's branch. The gate was green in that repo. A diagnostic that
183
+ # names the wrong cause sends you to read a passing log, which is the same
184
+ # class of defect as everything else this gate exists to catch.
185
+ push_out=$(git -C "$wt" push --force-with-lease -u origin HEAD 2>&1)
186
+ push_rc=$?
187
+ if [ "$push_rc" -ne 0 ]; then
188
+ case "$push_out" in
189
+ *"verify failed"*|*"verify ran NOTHING"*)
190
+ printf '%-26s \033[31mGATE REFUSED THE PUSH\033[0m - run scripts/verify.sh there\n' "$label" ;;
191
+ *"stale info"*|*"non-fast-forward"*)
192
+ printf '%-26s \033[31mbranch diverged\033[0m - someone else pushed to chore/sync-shared\n' "$label" ;;
193
+ *)
194
+ printf '%-26s \033[31mpush failed\033[0m: %s\n' "$label" "$(echo "$push_out" | tail -1)" ;;
195
+ esac
196
+ return 1
197
+ fi
198
+ url=$(gh pr create --repo "$slug" --base "$base" --head chore/sync-shared \
199
+ --title "chore(shared): sync template-shared files" \
200
+ --body "Distributed by \`biffo-template\`'s \`scripts/shared-sync.sh\`.
201
+
202
+ Sibling and plugin repos are separate repositories with no \`core-manifest.json\`, so \`biffo core upgrade\` cannot reach them. The documented channel was \"vendor into the skeleton plus a one-time manual copy-in\", which only ever helped repos created afterwards — and is why this repo was running a local gate two versions old.
203
+
204
+ Files synced verbatim from the template (see \`shared-files.json\` there):
205
+
206
+ $(for f in $FILES; do echo "- \`$f\`"; done)
207
+
208
+ Run \`sh scripts/gate-coverage.sh\` after merging to see this repo's gate measured against its own CI.
209
+
210
+ 🤖 Generated with [Claude Code](https://claude.com/claude-code)" 2>&1 | tail -1)
211
+ printf '%-26s %s\n' "$label" "$url"
212
+ return 0
213
+ }
214
+
215
+ printf '\nshared-file sync - template -> repos core upgrade cannot reach\n\n'
216
+ for d in "$ESTATE"/*/; do
217
+ d="${d%/}"
218
+ label=$(basename "$d")
219
+ [ -e "$d/.git" ] || continue
220
+ # The template is the source, not a target. It matched only because it carries
221
+ # scripts/verify.sh, and comparing it to itself through origin/<base> reported
222
+ # it as missing every file whenever its own dev was ahead of the checkout.
223
+ [ "$d" = "$TEMPLATE_ROOT" ] && continue
224
+ [ -n "$ONLY" ] && [ "$label" != "$ONLY" ] && continue
225
+ applies "$d" || continue
226
+ delta=$(diff_files "$d")
227
+ if [ -z "$delta" ]; then
228
+ current=$((current + 1))
229
+ printf '%-26s \033[32mcurrent\033[0m\n' "$label"
230
+ continue
231
+ fi
232
+ drifted=$((drifted + 1))
233
+ if [ -n "$CHECK" ]; then
234
+ printf '%-26s \033[31mDRIFTED\033[0m%s\n' "$label" "$delta"
235
+ else
236
+ sync_repo "$d" "$label" && synced=$((synced + 1))
237
+ fi
238
+ done
239
+
240
+ printf '\n%s current, %s drifted\n' "$current" "$drifted"
241
+ if [ -n "$CHECK" ] && [ "$drifted" -gt 0 ]; then
242
+ printf '\033[31mShared files have drifted.\033[0m Run without --check to open sync PRs.\n\n'
243
+ exit 1
244
+ fi
245
+ printf '\n'
@@ -74,8 +74,50 @@ FAILED=""
74
74
  PASSED=""
75
75
  SKIPPED=""
76
76
 
77
+ # pytest is included where the suite is FAST ENOUGH TO PAY, measured rather than
78
+ # opted into (#869, H5 gap 4).
79
+ #
80
+ # The old rule was a blanket exclusion with a manual opt-in nobody ever issued,
81
+ # so the fastest suites in the estate were the ones not being run:
82
+ #
83
+ # tabsii-marketplace 1.7s tabsii-geo 2.1s tabsii-intake 2.5s tabsii-crm 2.7s
84
+ # biffo-template 51.2s biffo-platform 57.4s tabsii-platform 85.6s
85
+ #
86
+ # The exclusion was right for the three repos it was written against and wrong
87
+ # for the four it was applied to. The arithmetic: ~2.5s on every push against a
88
+ # ~14 min sibling CI round trip to discover and confirm a Python test failure --
89
+ # break-even at one catch per 336 pushes, against an observed rate of roughly
90
+ # one per 165.
91
+ #
92
+ # BIFFO_VERIFY_PYTEST overrides in BOTH directions: 1 forces it in, 0 forces it
93
+ # out. An override that only forces on would leave no way to escape a suite that
94
+ # has quietly grown past the threshold.
95
+ PYTEST_BUDGET_SECONDS="${BIFFO_VERIFY_PYTEST_BUDGET:-15}"
77
96
  PYTEST="${BIFFO_VERIFY_PYTEST:-}"
78
97
 
98
+ # Cached per directory, because timing the suite to decide whether to run the
99
+ # suite would cost exactly what it is trying to save. The cache lives with the
100
+ # repo, not in $HOME, so it cannot leak a fast verdict from one repo to another.
101
+ pytest_is_fast() {
102
+ _d="$1"
103
+ _cache="$_d/.pytest-duration"
104
+ if [ -f "$_cache" ]; then
105
+ _secs=$(cat "$_cache" 2>/dev/null)
106
+ else
107
+ # First run in a repo: time it once, then decide from then on. A timeout
108
+ # means "too slow", which is the correct verdict rather than a hang.
109
+ _start=$(date +%s)
110
+ if [ "$_d" = "." ]; then
111
+ timeout "$((PYTEST_BUDGET_SECONDS * 4))" uv run pytest -q --no-cov >/dev/null 2>&1 || true
112
+ else
113
+ timeout "$((PYTEST_BUDGET_SECONDS * 4))" uv run --directory "$_d" pytest -q --no-cov >/dev/null 2>&1 || true
114
+ fi
115
+ _secs=$(($(date +%s) - _start))
116
+ echo "$_secs" > "$_cache" 2>/dev/null || true
117
+ fi
118
+ [ "${_secs:-9999}" -le "$PYTEST_BUDGET_SECONDS" ]
119
+ }
120
+
79
121
  # Does THIS repo's CI run a check of this kind?
80
122
  #
81
123
  # ## Why every check is gated on this (#861)
@@ -200,7 +242,14 @@ skip() {
200
242
  printf ' \033[90m-- %-16s n/a - %s\033[0m\n' "$1" "$2"
201
243
  }
202
244
 
203
- [ -n "$LIST" ] || printf '\nverify - the checks CI runs, before the push\n\n'
245
+ if [ -z "$LIST" ]; then
246
+ # State which template this gate came from. A gate two versions old is the
247
+ # condition that let tabsii-crm print `verify passed` on a 700-line change
248
+ # while running one check, and nothing in the repo said so (#869, H5 gap 1).
249
+ _stamp=""
250
+ [ -f .biffo-shared-version ] && _stamp=" (template $(cat .biffo-shared-version))"
251
+ printf '\nverify - the checks CI runs, before the push%s\n\n' "$_stamp"
252
+ fi
204
253
 
205
254
  # Python first: ruff is near-instant, so the cheapest feedback on the largest
206
255
  # single class of failure comes back immediately.
@@ -232,20 +281,24 @@ if [ -n "$PY_DIRS" ]; then
232
281
  [ -z "$bandit_paths" ] && [ -d src ] && bandit_paths="src"
233
282
  # shellcheck disable=SC2086
234
283
  [ -n "$bandit_paths" ] && ci_has "bandit" && run_check "bandit$suffix" uv run bandit -r $bandit_paths -ll -q
235
- if [ -n "$PYTEST" ]; then
236
- run_check "pytest$suffix" uv run pytest -q
284
+ if [ "$PYTEST" = "0" ]; then
285
+ skip "pytest$suffix" "excluded by BIFFO_VERIFY_PYTEST=0"
286
+ elif [ -n "$PYTEST" ] || { [ -z "$LIST" ] && pytest_is_fast "."; }; then
287
+ ci_has "pytest" && run_check "pytest$suffix" uv run pytest -q --no-cov
237
288
  else
238
- skip "pytest$suffix" "excluded - set BIFFO_VERIFY_PYTEST=1 where the suite is fast"
289
+ skip "pytest$suffix" "suite is slower than ${PYTEST_BUDGET_SECONDS}s - CI keeps it"
239
290
  fi
240
291
  else
241
292
  ci_has "ruff check" && run_check "ruff-check$suffix" uv run --directory "$d" ruff check .
242
293
  ci_has "ruff format" && run_check "ruff-format$suffix" uv run --directory "$d" ruff format --check .
243
294
  ci_has "pyright" && run_check "pyright$suffix" uv run --directory "$d" pyright
244
295
  ci_has "bandit" && run_check "bandit$suffix" uv run --directory "$d" bandit -r src -ll -q
245
- if [ -n "$PYTEST" ]; then
246
- run_check "pytest$suffix" uv run --directory "$d" pytest -q
296
+ if [ "$PYTEST" = "0" ]; then
297
+ skip "pytest$suffix" "excluded by BIFFO_VERIFY_PYTEST=0"
298
+ elif [ -n "$PYTEST" ] || { [ -z "$LIST" ] && pytest_is_fast "$d"; }; then
299
+ ci_has "pytest" && run_check "pytest$suffix" uv run --directory "$d" pytest -q --no-cov
247
300
  else
248
- skip "pytest$suffix" "excluded - set BIFFO_VERIFY_PYTEST=1 where the suite is fast"
301
+ skip "pytest$suffix" "suite is slower than ${PYTEST_BUDGET_SECONDS}s - CI keeps it"
249
302
  fi
250
303
  fi
251
304
  done
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Distribute the template's shared files to the repos `biffo core upgrade`
4
+ # cannot reach, and make drift visible when it happens.
5
+ #
6
+ # ## Why this exists
7
+ #
8
+ # Instances carry `biffo.core.json` and a `core-manifest.json`, so the CLI
9
+ # three-way-merges template-owned paths into them. **Sibling apps and plugin
10
+ # repos are separate repositories with neither.** The documented channel to them
11
+ # was "vendor it into the skeleton, plus a one-time manual copy-in for existing
12
+ # ones", which is not a mechanism: the skeleton only helps repos created
13
+ # afterwards, and nothing ever prompts the copy-in.
14
+ #
15
+ # The cost, twice over:
16
+ #
17
+ # - AGENTS.md drifted 68 lines behind in tabsii, missing the very workflow
18
+ # guardrails the template had already written (#559).
19
+ # - Eight repos ran a local gate two versions old. `tabsii-crm` checked ONE
20
+ # thing in eight on a 700-line change and printed `verify passed` (#855).
21
+ #
22
+ # Both were found by a human noticing, months and hours late respectively. This
23
+ # turns that into a command that reports, and a `--check` that fails.
24
+ #
25
+ # ## What it is not
26
+ #
27
+ # A **one-way overwrite**, not a merge. `shared-files.json` may only list files
28
+ # every sibling and plugin should hold verbatim. Anything a repo is expected to
29
+ # customise does not belong in it — that is what the instance manifest's
30
+ # three-way merge is for, and this deliberately has no such subtlety.
31
+ #
32
+ # Usage:
33
+ # sh scripts/shared-sync.sh --check --estate ~/code # report drift, exit 1 if any
34
+ # sh scripts/shared-sync.sh --estate ~/code # open a PR per drifted repo
35
+ # sh scripts/shared-sync.sh --estate ~/code --repo tabsii-crm
36
+
37
+ set -uo pipefail
38
+
39
+ TEMPLATE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
40
+ MANIFEST="$TEMPLATE_ROOT/shared-files.json"
41
+ [ -f "$MANIFEST" ] || { echo "no shared-files.json beside $0" >&2; exit 2; }
42
+
43
+ CHECK=""
44
+ ESTATE=""
45
+ ONLY=""
46
+ while [ $# -gt 0 ]; do
47
+ case "$1" in
48
+ --check) CHECK=1; shift ;;
49
+ --estate) ESTATE="$2"; shift 2 ;;
50
+ --repo) ONLY="$2"; shift 2 ;;
51
+ *) echo "unknown argument: $1" >&2; exit 2 ;;
52
+ esac
53
+ done
54
+ [ -n "$ESTATE" ] || { echo "--estate <dir> is required" >&2; exit 2; }
55
+
56
+ FILES=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).files.join('\n'))")
57
+ MARKERS=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).appliesTo.join(' '))")
58
+
59
+ drifted=0
60
+ synced=0
61
+ current=0
62
+
63
+ applies() {
64
+ # Instances are NOT in scope: they carry biffo.core.json and a
65
+ # core-manifest.json, so `biffo core upgrade` three-way-merges these paths
66
+ # into them. Two mechanisms writing the same files would fight, and the
67
+ # core-ownership guard would refuse this script's commit anyway -- correctly,
68
+ # since these are template-owned paths in an instance.
69
+ [ -f "$1/biffo.core.json" ] && return 1
70
+ for m in $MARKERS; do [ -f "$1/$m" ] && return 0; done
71
+ # Also: any repo already carrying the gate. The runner repos have neither
72
+ # marker but did receive verify.sh, and a mechanism that distributes a file
73
+ # once and then stops tracking it is the drift this script exists to end --
74
+ # it would have recreated the exact hole in the exact repos nobody watches.
75
+ [ -f "$1/scripts/verify.sh" ] && return 0
76
+ return 1
77
+ }
78
+
79
+ # Which of the shared files differ in this repo. Missing counts as drifted: a
80
+ # repo that never received a file is exactly as unprotected as one holding a
81
+ # stale copy, and reporting them differently invites triaging only the second.
82
+ diff_files() {
83
+ d="$1"
84
+ out=""
85
+ # Compare against the REPO's integration branch, not the local working copy.
86
+ #
87
+ # The first version read "$d/$f" straight off disk, so a clone that had not
88
+ # been pulled reported DRIFTED for twelve repos that were entirely current --
89
+ # right after their sync PRs merged. The question is "is this repository
90
+ # current", not "is my laptop current", and a drift detector that fires on a
91
+ # stale checkout is one you learn to ignore.
92
+ git -C "$d" fetch origin --quiet 2>/dev/null
93
+ # `dev` first, per AGENTS.md section 2: it is the integration branch in every
94
+ # Biffo repo. origin/HEAD is NOT a substitute -- it points at `main` in
95
+ # several clones, and `main` is a stale release branch that legitimately does
96
+ # not carry these files, so resolving through it reported three repos as
97
+ # missing everything.
98
+ if git -C "$d" rev-parse --verify --quiet origin/dev >/dev/null 2>&1; then
99
+ base=dev
100
+ else
101
+ base=$(git -C "$d" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
102
+ [ -n "$base" ] || base=$(git -C "$d" rev-parse --abbrev-ref HEAD 2>/dev/null)
103
+ fi
104
+ for f in $FILES; do
105
+ remote=$(git -C "$d" show "origin/$base:$f" 2>/dev/null)
106
+ if [ -z "$remote" ]; then
107
+ out="$out $f(missing)"
108
+ elif [ "$remote" != "$(cat "$TEMPLATE_ROOT/$f")" ]; then
109
+ out="$out $f"
110
+ fi
111
+ done
112
+ echo "$out"
113
+ }
114
+
115
+ sync_repo() {
116
+ d="$1"
117
+ label="$2"
118
+ slug=$(git -C "$d" remote get-url origin | sed -E 's#.*[:/]([^/]+/[^/]+)$#\1#; s#\.git$##')
119
+ base=$(gh repo view "$slug" --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null)
120
+ [ -n "$base" ] || { printf '%-26s \033[31mcannot resolve default branch\033[0m\n' "$label"; return 1; }
121
+
122
+ git -C "$d" fetch origin --quiet || return 1
123
+ wt="$d/.worktrees/shared-sync"
124
+ git -C "$d" worktree remove --force "$wt" 2>/dev/null
125
+ git -C "$d" branch -D chore/sync-shared 2>/dev/null
126
+ git -C "$d" worktree add -q "$wt" -b chore/sync-shared "origin/$base" || return 1
127
+
128
+ for f in $FILES; do
129
+ mkdir -p "$wt/$(dirname "$f")"
130
+ cp "$TEMPLATE_ROOT/$f" "$wt/$f"
131
+ chmod +x "$wt/$f" 2>/dev/null
132
+ done
133
+
134
+ # Stamp the version these files CAME FROM, so a repo can say which template
135
+ # its gate is, without a template clone or a network call (#869, H5 gap 1).
136
+ #
137
+ # Read from $TEMPLATE_ROOT, never from $wt. H5 pre-registered this as the most
138
+ # likely way to make the whole thing meaningless: a stamp generated from the
139
+ # RECEIVING repo always matches itself and reports perfect health forever --
140
+ # the shape of every instrument defect found on 2026-07-29. The test asserts
141
+ # the two differ.
142
+ _tv=$(git -C "$TEMPLATE_ROOT" describe --tags --match 'core-v*' --abbrev=0 2>/dev/null || echo unknown)
143
+ printf '%s\n' "$_tv" > "$wt/.biffo-shared-version"
144
+
145
+ # Install the JS/Python deps the gate will now check, or its own pre-push
146
+ # correctly refuses this push -- and reaching for BIFFO_SKIP_VERIFY to ship a
147
+ # gate would be the counter-metric H4 pre-registered as refuting itself.
148
+ # The ROOT package too, not only the nested ones. The first version matched
149
+ # `--dir` paths only, so a repo whose package.json is at the root -- tabsii-map
150
+ # -- got no install and the gate refused the push with `tsc: not found`. It
151
+ # then reported GATE REFUSED, which was true and useless: the gate was right,
152
+ # the installer had skipped the one layout it could not see.
153
+ [ -f "$wt/package.json" ] && (cd "$wt" && pnpm install --frozen-lockfile >/dev/null 2>&1 || true)
154
+ [ -f "$wt/pyproject.toml" ] && (cd "$wt" && uv sync --all-groups >/dev/null 2>&1 || true)
155
+ for p in $( (cd "$wt" && sh scripts/verify.sh --list 2>/dev/null) | grep -oE '\-\-dir(ectory)? \./[A-Za-z0-9_./-]+' | awk '{print $2}' | sort -u); do
156
+ (cd "$wt/$p" 2>/dev/null && { pnpm install --frozen-lockfile >/dev/null 2>&1 ||
157
+ pnpm install --frozen-lockfile --ignore-workspace >/dev/null 2>&1 ||
158
+ uv sync --all-groups >/dev/null 2>&1; }) || true
159
+ done
160
+
161
+ git -C "$wt" add -A
162
+ if git -C "$wt" diff --cached --quiet; then
163
+ git -C "$d" worktree remove --force "$wt" 2>/dev/null
164
+ printf '%-26s \033[32mnothing to sync\033[0m\n' "$label"
165
+ return 0
166
+ fi
167
+ git -C "$wt" -c commit.gpgsign=false commit -q --no-verify -m "chore(shared): sync template-shared files
168
+
169
+ Distributed by biffo-template's scripts/shared-sync.sh. These files are held
170
+ verbatim from the template; see shared-files.json there for the list and why
171
+ this mechanism exists.
172
+
173
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>" >/dev/null 2>&1
174
+
175
+ # --force-with-lease: this branch is regenerated from origin/<base> on every
176
+ # run, so a previous sync's branch is legitimately replaced -- but the lease
177
+ # still refuses if someone else has pushed to it.
178
+ #
179
+ # The output is CAPTURED and classified rather than discarded. The first
180
+ # version printed "PUSH REFUSED (run the gate there and look)" for every
181
+ # failure, and the first real cause was a plain non-fast-forward against a
182
+ # previous run's branch. The gate was green in that repo. A diagnostic that
183
+ # names the wrong cause sends you to read a passing log, which is the same
184
+ # class of defect as everything else this gate exists to catch.
185
+ push_out=$(git -C "$wt" push --force-with-lease -u origin HEAD 2>&1)
186
+ push_rc=$?
187
+ if [ "$push_rc" -ne 0 ]; then
188
+ case "$push_out" in
189
+ *"verify failed"*|*"verify ran NOTHING"*)
190
+ printf '%-26s \033[31mGATE REFUSED THE PUSH\033[0m - run scripts/verify.sh there\n' "$label" ;;
191
+ *"stale info"*|*"non-fast-forward"*)
192
+ printf '%-26s \033[31mbranch diverged\033[0m - someone else pushed to chore/sync-shared\n' "$label" ;;
193
+ *)
194
+ printf '%-26s \033[31mpush failed\033[0m: %s\n' "$label" "$(echo "$push_out" | tail -1)" ;;
195
+ esac
196
+ return 1
197
+ fi
198
+ url=$(gh pr create --repo "$slug" --base "$base" --head chore/sync-shared \
199
+ --title "chore(shared): sync template-shared files" \
200
+ --body "Distributed by \`biffo-template\`'s \`scripts/shared-sync.sh\`.
201
+
202
+ Sibling and plugin repos are separate repositories with no \`core-manifest.json\`, so \`biffo core upgrade\` cannot reach them. The documented channel was \"vendor into the skeleton plus a one-time manual copy-in\", which only ever helped repos created afterwards — and is why this repo was running a local gate two versions old.
203
+
204
+ Files synced verbatim from the template (see \`shared-files.json\` there):
205
+
206
+ $(for f in $FILES; do echo "- \`$f\`"; done)
207
+
208
+ Run \`sh scripts/gate-coverage.sh\` after merging to see this repo's gate measured against its own CI.
209
+
210
+ 🤖 Generated with [Claude Code](https://claude.com/claude-code)" 2>&1 | tail -1)
211
+ printf '%-26s %s\n' "$label" "$url"
212
+ return 0
213
+ }
214
+
215
+ printf '\nshared-file sync - template -> repos core upgrade cannot reach\n\n'
216
+ for d in "$ESTATE"/*/; do
217
+ d="${d%/}"
218
+ label=$(basename "$d")
219
+ [ -e "$d/.git" ] || continue
220
+ # The template is the source, not a target. It matched only because it carries
221
+ # scripts/verify.sh, and comparing it to itself through origin/<base> reported
222
+ # it as missing every file whenever its own dev was ahead of the checkout.
223
+ [ "$d" = "$TEMPLATE_ROOT" ] && continue
224
+ [ -n "$ONLY" ] && [ "$label" != "$ONLY" ] && continue
225
+ applies "$d" || continue
226
+ delta=$(diff_files "$d")
227
+ if [ -z "$delta" ]; then
228
+ current=$((current + 1))
229
+ printf '%-26s \033[32mcurrent\033[0m\n' "$label"
230
+ continue
231
+ fi
232
+ drifted=$((drifted + 1))
233
+ if [ -n "$CHECK" ]; then
234
+ printf '%-26s \033[31mDRIFTED\033[0m%s\n' "$label" "$delta"
235
+ else
236
+ sync_repo "$d" "$label" && synced=$((synced + 1))
237
+ fi
238
+ done
239
+
240
+ printf '\n%s current, %s drifted\n' "$current" "$drifted"
241
+ if [ -n "$CHECK" ] && [ "$drifted" -gt 0 ]; then
242
+ printf '\033[31mShared files have drifted.\033[0m Run without --check to open sync PRs.\n\n'
243
+ exit 1
244
+ fi
245
+ printf '\n'
@@ -74,8 +74,50 @@ FAILED=""
74
74
  PASSED=""
75
75
  SKIPPED=""
76
76
 
77
+ # pytest is included where the suite is FAST ENOUGH TO PAY, measured rather than
78
+ # opted into (#869, H5 gap 4).
79
+ #
80
+ # The old rule was a blanket exclusion with a manual opt-in nobody ever issued,
81
+ # so the fastest suites in the estate were the ones not being run:
82
+ #
83
+ # tabsii-marketplace 1.7s tabsii-geo 2.1s tabsii-intake 2.5s tabsii-crm 2.7s
84
+ # biffo-template 51.2s biffo-platform 57.4s tabsii-platform 85.6s
85
+ #
86
+ # The exclusion was right for the three repos it was written against and wrong
87
+ # for the four it was applied to. The arithmetic: ~2.5s on every push against a
88
+ # ~14 min sibling CI round trip to discover and confirm a Python test failure --
89
+ # break-even at one catch per 336 pushes, against an observed rate of roughly
90
+ # one per 165.
91
+ #
92
+ # BIFFO_VERIFY_PYTEST overrides in BOTH directions: 1 forces it in, 0 forces it
93
+ # out. An override that only forces on would leave no way to escape a suite that
94
+ # has quietly grown past the threshold.
95
+ PYTEST_BUDGET_SECONDS="${BIFFO_VERIFY_PYTEST_BUDGET:-15}"
77
96
  PYTEST="${BIFFO_VERIFY_PYTEST:-}"
78
97
 
98
+ # Cached per directory, because timing the suite to decide whether to run the
99
+ # suite would cost exactly what it is trying to save. The cache lives with the
100
+ # repo, not in $HOME, so it cannot leak a fast verdict from one repo to another.
101
+ pytest_is_fast() {
102
+ _d="$1"
103
+ _cache="$_d/.pytest-duration"
104
+ if [ -f "$_cache" ]; then
105
+ _secs=$(cat "$_cache" 2>/dev/null)
106
+ else
107
+ # First run in a repo: time it once, then decide from then on. A timeout
108
+ # means "too slow", which is the correct verdict rather than a hang.
109
+ _start=$(date +%s)
110
+ if [ "$_d" = "." ]; then
111
+ timeout "$((PYTEST_BUDGET_SECONDS * 4))" uv run pytest -q --no-cov >/dev/null 2>&1 || true
112
+ else
113
+ timeout "$((PYTEST_BUDGET_SECONDS * 4))" uv run --directory "$_d" pytest -q --no-cov >/dev/null 2>&1 || true
114
+ fi
115
+ _secs=$(($(date +%s) - _start))
116
+ echo "$_secs" > "$_cache" 2>/dev/null || true
117
+ fi
118
+ [ "${_secs:-9999}" -le "$PYTEST_BUDGET_SECONDS" ]
119
+ }
120
+
79
121
  # Does THIS repo's CI run a check of this kind?
80
122
  #
81
123
  # ## Why every check is gated on this (#861)
@@ -200,7 +242,14 @@ skip() {
200
242
  printf ' \033[90m-- %-16s n/a - %s\033[0m\n' "$1" "$2"
201
243
  }
202
244
 
203
- [ -n "$LIST" ] || printf '\nverify - the checks CI runs, before the push\n\n'
245
+ if [ -z "$LIST" ]; then
246
+ # State which template this gate came from. A gate two versions old is the
247
+ # condition that let tabsii-crm print `verify passed` on a 700-line change
248
+ # while running one check, and nothing in the repo said so (#869, H5 gap 1).
249
+ _stamp=""
250
+ [ -f .biffo-shared-version ] && _stamp=" (template $(cat .biffo-shared-version))"
251
+ printf '\nverify - the checks CI runs, before the push%s\n\n' "$_stamp"
252
+ fi
204
253
 
205
254
  # Python first: ruff is near-instant, so the cheapest feedback on the largest
206
255
  # single class of failure comes back immediately.
@@ -232,20 +281,24 @@ if [ -n "$PY_DIRS" ]; then
232
281
  [ -z "$bandit_paths" ] && [ -d src ] && bandit_paths="src"
233
282
  # shellcheck disable=SC2086
234
283
  [ -n "$bandit_paths" ] && ci_has "bandit" && run_check "bandit$suffix" uv run bandit -r $bandit_paths -ll -q
235
- if [ -n "$PYTEST" ]; then
236
- run_check "pytest$suffix" uv run pytest -q
284
+ if [ "$PYTEST" = "0" ]; then
285
+ skip "pytest$suffix" "excluded by BIFFO_VERIFY_PYTEST=0"
286
+ elif [ -n "$PYTEST" ] || { [ -z "$LIST" ] && pytest_is_fast "."; }; then
287
+ ci_has "pytest" && run_check "pytest$suffix" uv run pytest -q --no-cov
237
288
  else
238
- skip "pytest$suffix" "excluded - set BIFFO_VERIFY_PYTEST=1 where the suite is fast"
289
+ skip "pytest$suffix" "suite is slower than ${PYTEST_BUDGET_SECONDS}s - CI keeps it"
239
290
  fi
240
291
  else
241
292
  ci_has "ruff check" && run_check "ruff-check$suffix" uv run --directory "$d" ruff check .
242
293
  ci_has "ruff format" && run_check "ruff-format$suffix" uv run --directory "$d" ruff format --check .
243
294
  ci_has "pyright" && run_check "pyright$suffix" uv run --directory "$d" pyright
244
295
  ci_has "bandit" && run_check "bandit$suffix" uv run --directory "$d" bandit -r src -ll -q
245
- if [ -n "$PYTEST" ]; then
246
- run_check "pytest$suffix" uv run --directory "$d" pytest -q
296
+ if [ "$PYTEST" = "0" ]; then
297
+ skip "pytest$suffix" "excluded by BIFFO_VERIFY_PYTEST=0"
298
+ elif [ -n "$PYTEST" ] || { [ -z "$LIST" ] && pytest_is_fast "$d"; }; then
299
+ ci_has "pytest" && run_check "pytest$suffix" uv run --directory "$d" pytest -q --no-cov
247
300
  else
248
- skip "pytest$suffix" "excluded - set BIFFO_VERIFY_PYTEST=1 where the suite is fast"
301
+ skip "pytest$suffix" "suite is slower than ${PYTEST_BUDGET_SECONDS}s - CI keeps it"
249
302
  fi
250
303
  fi
251
304
  done
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.175.0",
3
+ "version": "0.176.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",