@biffo/cli 0.223.1 → 0.223.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.223.1",
3
+ "version": "0.223.2",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,541 +0,0 @@
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
- # ## Rehearsal: why a distribution run is two phases
33
- #
34
- # Measured on 2026-07-29: **84 `chore(shared): sync template-shared files` PRs
35
- # merged across 12 satellites in one day** — 7 rounds, where one would have done.
36
- # That is 33.5% of the estate's entire merge volume for the day, and every one of
37
- # those PRs is classified `toil` by the practices collector, which read 60.2%.
38
- # Six of the seven rounds carried `scripts/verify.sh` **alone**: the gate was
39
- # being iterated downstream, one estate-wide lap per defect found (root-only
40
- # checks, `--no-cov` in a repo without pytest-cov, the pytest opt-in, `--list`
41
- # disagreeing with the gate).
42
- #
43
- # The reason a lap cost 12 PRs rather than 1 is that this script used to be a
44
- # single pass: for each repo in turn, stage -> push -> open a PR. A defect
45
- # discovered while staging repo 7 left 6 PRs already open, so fixing it meant a
46
- # whole new round rather than a correction.
47
- #
48
- # So: **rehearse every target before touching any of them.** Phase 1 stages the
49
- # candidate files into each repo and runs that repo's own gate against them,
50
- # locally, with no push and no PR. Phase 2 ships — and only if phase 1 was clean
51
- # everywhere. All 14 satellite clones are already on disk under `--estate`, so
52
- # proving the estate costs seconds; proving it through GitHub costs 12 PRs and 12
53
- # CI runs per lap.
54
- #
55
- # **It is not enough to let the target repo's pre-push hook catch this**, which
56
- # is what the push in phase 2 relies on. That hook is exactly the thing that was
57
- # silently DEAD in every fresh worktree until #845, and `scripts/hook-audit.sh`
58
- # exists because the failure was invisible. A repo whose hook is dead pushes
59
- # happily and opens a PR carrying an unproven gate — the fail-open shape this
60
- # whole file set exists to remove. Rehearsal runs the gate itself and reads its
61
- # exit code, so it does not depend on the hook being armed.
62
- #
63
- # Usage:
64
- # sh scripts/shared-sync.sh --check --estate ~/code # report drift, exit 1 if any
65
- # sh scripts/shared-sync.sh --rehearse --estate ~/code # prove the candidates, ship nothing
66
- # sh scripts/shared-sync.sh --estate ~/code # rehearse, then open a PR per repo
67
- # sh scripts/shared-sync.sh --estate ~/code --repo tabsii-crm
68
- # sh scripts/shared-sync.sh --estate ~/code --no-rehearse # ship unproven, loudly
69
-
70
- set -u
71
- # `pipefail` is not POSIX, and this file is documented and invoked as
72
- # `sh scripts/shared-sync.sh` -- including by scripts/practices-daily.sh.
73
- #
74
- # It carried a bare `set -uo pipefail` for as long as it only ever ran on the
75
- # workstation, whose /bin/sh is dash 0.5.12, a version that happens to accept
76
- # `-o pipefail`. The first time anything executed it elsewhere -- a test, on a
77
- # CI runner with an older sh -- it died at this line with
78
- # `set: Illegal option -o pipefail`, before parsing a single argument. Nothing
79
- # had noticed, because nothing had ever run it anywhere else.
80
- #
81
- # That is the third instance of this class in the corpus: the same one-version
82
- # difference in the same shell made js-dependency-audit.sh mangle every advisory
83
- # payload while exiting 0 (#883). Enable it where it exists, carry on where it
84
- # does not.
85
- (set -o pipefail) 2>/dev/null && set -o pipefail || true
86
-
87
- TEMPLATE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
88
- MANIFEST="$TEMPLATE_ROOT/shared-files.json"
89
- [ -f "$MANIFEST" ] || { echo "no shared-files.json beside $0" >&2; exit 2; }
90
-
91
- CHECK=""
92
- ESTATE=""
93
- ONLY=""
94
- REHEARSE_ONLY=""
95
- NO_REHEARSE=""
96
- while [ $# -gt 0 ]; do
97
- case "$1" in
98
- --check) CHECK=1; shift ;;
99
- --rehearse) REHEARSE_ONLY=1; shift ;;
100
- # In the open, per AGENTS.md section 7: a gate that can be skipped silently
101
- # is not a gate. This prints the reason on every line it lets through, so a
102
- # transcript shows what was shipped unproven.
103
- --no-rehearse) NO_REHEARSE=1; shift ;;
104
- --estate) ESTATE="$2"; shift 2 ;;
105
- --repo) ONLY="$2"; shift 2 ;;
106
- *) echo "unknown argument: $1" >&2; exit 2 ;;
107
- esac
108
- done
109
- [ -n "$ESTATE" ] || { echo "--estate <dir> is required" >&2; exit 2; }
110
- [ -n "$REHEARSE_ONLY" ] && [ -n "$NO_REHEARSE" ] && {
111
- echo "--rehearse and --no-rehearse are opposites" >&2; exit 2; }
112
-
113
- FILES=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).files.join('\n'))")
114
- MARKERS=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).appliesTo.join(' '))")
115
-
116
- drifted=0
117
- synced=0
118
- current=0
119
- failed=0
120
-
121
- # Field separator for the two state files below. A literal tab in a `grep`
122
- # pattern or a `${var%%...}` expansion is invisible in a diff and one editor
123
- # away from becoming spaces.
124
- TAB=$(printf '\t')
125
-
126
- applies() {
127
- # Instances are NOT in scope: they carry biffo.core.json and a
128
- # core-manifest.json, so `biffo core upgrade` three-way-merges these paths
129
- # into them. Two mechanisms writing the same files would fight, and the
130
- # core-ownership guard would refuse this script's commit anyway -- correctly,
131
- # since these are template-owned paths in an instance.
132
- [ -f "$1/biffo.core.json" ] && return 1
133
- for m in $MARKERS; do [ -f "$1/$m" ] && return 0; done
134
- # Also: any repo already carrying the gate. The runner repos have neither
135
- # marker but did receive verify.sh, and a mechanism that distributes a file
136
- # once and then stops tracking it is the drift this script exists to end --
137
- # it would have recreated the exact hole in the exact repos nobody watches.
138
- [ -f "$1/scripts/verify.sh" ] && return 0
139
- return 1
140
- }
141
-
142
- # Which of the shared files differ in this repo. Missing counts as drifted: a
143
- # repo that never received a file is exactly as unprotected as one holding a
144
- # stale copy, and reporting them differently invites triaging only the second.
145
- diff_files() {
146
- d="$1"
147
- out=""
148
- # Compare against the REPO's integration branch, not the local working copy.
149
- #
150
- # The first version read "$d/$f" straight off disk, so a clone that had not
151
- # been pulled reported DRIFTED for twelve repos that were entirely current --
152
- # right after their sync PRs merged. The question is "is this repository
153
- # current", not "is my laptop current", and a drift detector that fires on a
154
- # stale checkout is one you learn to ignore.
155
- git -C "$d" fetch origin --quiet 2>/dev/null
156
- # `dev` first, per AGENTS.md section 2: it is the integration branch in every
157
- # Biffo repo. origin/HEAD is NOT a substitute -- it points at `main` in
158
- # several clones, and `main` is a stale release branch that legitimately does
159
- # not carry these files, so resolving through it reported three repos as
160
- # missing everything.
161
- if git -C "$d" rev-parse --verify --quiet origin/dev >/dev/null 2>&1; then
162
- base=dev
163
- else
164
- base=$(git -C "$d" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
165
- [ -n "$base" ] || base=$(git -C "$d" rev-parse --abbrev-ref HEAD 2>/dev/null)
166
- fi
167
- for f in $FILES; do
168
- remote=$(git -C "$d" show "origin/$base:$f" 2>/dev/null)
169
- if [ -z "$remote" ]; then
170
- out="$out $f(missing)"
171
- elif [ "$remote" != "$(cat "$TEMPLATE_ROOT/$f")" ]; then
172
- out="$out $f"
173
- fi
174
- done
175
- echo "$out"
176
- }
177
-
178
- repo_slug() {
179
- git -C "$1" remote get-url origin | sed -E 's#.*[:/]([^/]+/[^/]+)$#\1#; s#\.git$##'
180
- }
181
-
182
- # The absolute path of a working tree's shared git directory, which is the same
183
- # for a primary checkout and every worktree linked to it -- i.e. an identity for
184
- # the REPOSITORY rather than for one of its working trees.
185
- repo_dir() {
186
- (cd "$1" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd)
187
- }
188
-
189
- TEMPLATE_REPO=$(repo_dir "$TEMPLATE_ROOT")
190
- [ -n "$TEMPLATE_REPO" ] || { echo "$TEMPLATE_ROOT is not a git repository" >&2; exit 2; }
191
-
192
- # Phase 1: put the candidate files in place and make the repo ready to run its
193
- # own gate against them. Deliberately stops short of committing anything -- a
194
- # staged worktree is a question ("would this land?"), and until phase 2 it has no
195
- # commit, no push and no PR.
196
- stage_repo() {
197
- d="$1"
198
- label="$2"
199
- base="$3"
200
-
201
- git -C "$d" fetch origin --quiet || return 1
202
- wt="$d/.worktrees/shared-sync"
203
- git -C "$d" worktree remove --force "$wt" 2>/dev/null
204
- # `branch -D` reports on STDOUT, so a quiet run printed "Deleted branch
205
- # chore/sync-shared" in the middle of the rehearsal table.
206
- git -C "$d" branch -D chore/sync-shared >/dev/null 2>&1
207
- git -C "$d" worktree add -q "$wt" -b chore/sync-shared "origin/$base" || return 1
208
-
209
- for f in $FILES; do
210
- mkdir -p "$wt/$(dirname "$f")"
211
- cp "$TEMPLATE_ROOT/$f" "$wt/$f"
212
- chmod +x "$wt/$f" 2>/dev/null
213
- done
214
-
215
- # Stamp the version these files CAME FROM, so a repo can say which template
216
- # its gate is, without a template clone or a network call (#869, H5 gap 1).
217
- #
218
- # Read from $TEMPLATE_ROOT, never from $wt. H5 pre-registered this as the most
219
- # likely way to make the whole thing meaningless: a stamp generated from the
220
- # RECEIVING repo always matches itself and reports perfect health forever --
221
- # the shape of every instrument defect found on 2026-07-29. The test asserts
222
- # the two differ.
223
- _tv=$(git -C "$TEMPLATE_ROOT" describe --tags --match 'core-v*' --abbrev=0 2>/dev/null || echo unknown)
224
- printf '%s\n' "$_tv" > "$wt/.biffo-shared-version"
225
-
226
- # Install the JS/Python deps the gate will now check, or its own pre-push
227
- # correctly refuses this push -- and reaching for BIFFO_SKIP_VERIFY to ship a
228
- # gate would be the counter-metric H4 pre-registered as refuting itself.
229
- # The ROOT package too, not only the nested ones. The first version matched
230
- # `--dir` paths only, so a repo whose package.json is at the root -- tabsii-map
231
- # -- got no install and the gate refused the push with `tsc: not found`. It
232
- # then reported GATE REFUSED, which was true and useless: the gate was right,
233
- # the installer had skipped the one layout it could not see.
234
- [ -f "$wt/package.json" ] && (cd "$wt" && pnpm install --frozen-lockfile >/dev/null 2>&1 || true)
235
- [ -f "$wt/pyproject.toml" ] && (cd "$wt" && uv sync --all-groups >/dev/null 2>&1 || true)
236
- 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
237
- (cd "$wt/$p" 2>/dev/null && { pnpm install --frozen-lockfile >/dev/null 2>&1 ||
238
- pnpm install --frozen-lockfile --ignore-workspace >/dev/null 2>&1 ||
239
- uv sync --all-groups >/dev/null 2>&1; }) || true
240
- done
241
-
242
- git -C "$wt" add -A
243
- if git -C "$wt" diff --cached --quiet; then
244
- git -C "$d" worktree remove --force "$wt" 2>/dev/null
245
- return 2
246
- fi
247
- return 0
248
- }
249
-
250
- # Phase 1's actual question: with the candidate files in place, does this repo's
251
- # gate still work HERE?
252
- #
253
- # This is the check the estate did not have. `--check` compares bytes, and
254
- # `gate-coverage.sh --estate` measures repos as they ARE -- neither can say
255
- # anything about a file that has not been distributed yet. Six of the seven
256
- # rounds on 2026-07-29 were fixing defects that only exist in a repo that is not
257
- # this one: a check list tuned to the template's layout, a pytest-cov flag a
258
- # plugin repo rejects, a package.json that is not at the root.
259
- rehearse_repo() {
260
- wt="$1"
261
-
262
- # `sh`, not `bash`: this is exactly how `.githooks/pre-push` invokes it
263
- # (`exec sh scripts/verify.sh`), and /bin/sh is dash on every machine in this
264
- # estate. A gate proven under one shell and run under another is not the same
265
- # gate -- `js-dependency-audit.sh` reported INCONCLUSIVE on every invocation
266
- # while exiting 0 for exactly that reason, because dash's `echo` interprets
267
- # backslash escapes and bash's does not (#883).
268
- _out=$( (cd "$wt" && sh scripts/verify.sh 2>&1) )
269
- _rc=$?
270
- _checks=$(printf '%s' "$_out" | sed -n 's/.*verify passed[^-]*- *//p' | head -1)
271
-
272
- # `gate-coverage.sh` reads `--list` rather than running anything, so it is
273
- # cheap, and it answers the question `verify.sh` cannot: the gate ran without
274
- # error, but how much of THIS repo's CI did it mirror? A gate can exit 0
275
- # having covered 1 kind in 8 -- that IS #855, and it is invisible in an exit
276
- # code. Reported, not enforced: what coverage number should block a
277
- # distribution is H5's call to make with numbers, not this script's to assume.
278
- #
279
- # Match its three verdicts, not just the fraction. Reading only `N/M` reported
280
- # a bare `?` for tabsii-map, which has no ci.yml at all -- so the one repo
281
- # where the coverage question does not apply looked like the one repo where the
282
- # measurement had failed. "Not applicable" and "could not tell" are different
283
- # answers, and this file exists because conflating them is expensive.
284
- _cov=$( (cd "$wt" && sh scripts/gate-coverage.sh 2>&1) |
285
- sed 's/\x1b\[[0-9;]*m//g' |
286
- grep -oE '([0-9]+/[0-9]+|no CI to mirror|NO GATE)' | head -1)
287
- [ -n "$_cov" ] || _cov='coverage unknown'
288
- case "$_cov" in
289
- */*) _cov="covers $_cov" ;;
290
- esac
291
-
292
- if [ "$_rc" -eq 0 ]; then
293
- case "$_out" in
294
- *"verify ran NOTHING"*)
295
- # verify.sh has already established this is not the #855 bug: no
296
- # `.github/workflows/ci.yml`, so this repo has no CI for the gate to
297
- # mirror and no shift-left obligation. Reporting it as a pass would be
298
- # the exact conflation the gate itself refuses to make, so it gets its
299
- # own verdict.
300
- printf '%s\t%s\n' NO-CI 'gate ran nothing; repo has no ci.yml' ;;
301
- *)
302
- printf '%s\t%s\n' PASS "$(printf '%s (%s)' "$_checks" "$_cov")" ;;
303
- esac
304
- return 0
305
- fi
306
- _why=$(printf '%s' "$_out" | grep -E 'verify failed|verify ran NOTHING' | head -1)
307
- [ -n "$_why" ] || _why=$(printf '%s' "$_out" | tail -1)
308
- printf '%s\t%s\n' FAIL "$(printf '%s' "$_why" | sed 's/\x1b\[[0-9;]*m//g')"
309
- return 1
310
- }
311
-
312
- # Phase 2: commit the already-staged worktree, push it, open the PR. Reached only
313
- # when phase 1 came back clean for EVERY repo in this run.
314
- ship_repo() {
315
- d="$1"
316
- label="$2"
317
- slug="$3"
318
- base="$4"
319
- wt="$d/.worktrees/shared-sync"
320
-
321
- git -C "$wt" -c commit.gpgsign=false commit -q --no-verify -m "chore(shared): sync template-shared files
322
-
323
- Distributed by biffo-template's scripts/shared-sync.sh. These files are held
324
- verbatim from the template; see shared-files.json there for the list and why
325
- this mechanism exists.
326
-
327
- Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>" >/dev/null 2>&1
328
-
329
- # --force-with-lease: this branch is regenerated from origin/<base> on every
330
- # run, so a previous sync's branch is legitimately replaced -- but the lease
331
- # still refuses if someone else has pushed to it.
332
- #
333
- # The output is CAPTURED and classified rather than discarded. The first
334
- # version printed "PUSH REFUSED (run the gate there and look)" for every
335
- # failure, and the first real cause was a plain non-fast-forward against a
336
- # previous run's branch. The gate was green in that repo. A diagnostic that
337
- # names the wrong cause sends you to read a passing log, which is the same
338
- # class of defect as everything else this gate exists to catch.
339
- push_out=$(git -C "$wt" push --force-with-lease -u origin HEAD 2>&1)
340
- push_rc=$?
341
- if [ "$push_rc" -ne 0 ]; then
342
- case "$push_out" in
343
- *"verify failed"*|*"verify ran NOTHING"*)
344
- printf '%-26s \033[31mGATE REFUSED THE PUSH\033[0m - run scripts/verify.sh there\n' "$label" ;;
345
- *"stale info"*|*"non-fast-forward"*)
346
- printf '%-26s \033[31mbranch diverged\033[0m - someone else pushed to chore/sync-shared\n' "$label" ;;
347
- *)
348
- printf '%-26s \033[31mpush failed\033[0m: %s\n' "$label" "$(echo "$push_out" | tail -1)" ;;
349
- esac
350
- return 1
351
- fi
352
- url=$(gh pr create --repo "$slug" --base "$base" --head chore/sync-shared \
353
- --title "chore(shared): sync template-shared files" \
354
- --body "Distributed by \`biffo-template\`'s \`scripts/shared-sync.sh\`.
355
-
356
- 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.
357
-
358
- Files synced verbatim from the template (see \`shared-files.json\` there):
359
-
360
- $(for f in $FILES; do echo "- \`$f\`"; done)
361
-
362
- Run \`sh scripts/gate-coverage.sh\` after merging to see this repo's gate measured against its own CI.
363
-
364
- 🤖 Generated with [Claude Code](https://claude.com/claude-code)" 2>&1 | tail -1)
365
- printf '%-26s %s\n' "$label" "$url"
366
- return 0
367
- }
368
-
369
- # The list of repos this run will touch, one `label<TAB>dir<TAB>slug<TAB>base`
370
- # per line. Written in the survey pass and read twice afterwards, so both phases
371
- # work from the same set: a rehearsal that proved a different list of repos than
372
- # the one that ships is worth nothing.
373
- TARGETS=$(mktemp)
374
- VERDICTS=$(mktemp)
375
- trap 'rm -f "$TARGETS" "$VERDICTS"' EXIT
376
-
377
- printf '\nshared-file sync - template -> repos core upgrade cannot reach\n\n'
378
- for d in "$ESTATE"/*/; do
379
- d="${d%/}"
380
- label=$(basename "$d")
381
- [ -e "$d/.git" ] || continue
382
- # The template is the source, not a target. It matched only because it carries
383
- # scripts/verify.sh, and comparing it to itself through origin/<base> reported
384
- # it as missing every file whenever its own dev was ahead of the checkout.
385
- #
386
- # Compare REPOSITORIES, not working-tree paths. `[ "$d" = "$TEMPLATE_ROOT" ]`
387
- # held only when this script was run from the primary checkout -- and AGENTS.md
388
- # section 1 mandates that all work happens in a worktree, where TEMPLATE_ROOT
389
- # is `.worktrees/<name>` and the primary checkout beside it looks like just
390
- # another satellite carrying scripts/verify.sh. It has no biffo.core.json and
391
- # no sibling/plugin marker, so nothing else excluded it either.
392
- #
393
- # It never fired because a primary checkout is normally byte-identical to
394
- # origin/dev, so `diff_files` reported `current` and skipped it. It fires the
395
- # moment the candidate files differ from origin/dev -- which is the only
396
- # situation this script is ever run in while iterating on a shared file. The
397
- # first `--rehearse` from a worktree would have staged the template as a
398
- # target of its own distribution and opened a sync PR against its own dev.
399
- [ "$(repo_dir "$d")" = "$TEMPLATE_REPO" ] && continue
400
- [ -n "$ONLY" ] && [ "$label" != "$ONLY" ] && continue
401
- applies "$d" || continue
402
- delta=$(diff_files "$d")
403
- if [ -z "$delta" ]; then
404
- current=$((current + 1))
405
- printf '%-26s \033[32mcurrent\033[0m\n' "$label"
406
- continue
407
- fi
408
- drifted=$((drifted + 1))
409
- if [ -n "$CHECK" ]; then
410
- printf '%-26s \033[31mDRIFTED\033[0m%s\n' "$label" "$delta"
411
- continue
412
- fi
413
- slug=$(repo_slug "$d")
414
- base=$(gh repo view "$slug" --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null)
415
- if [ -z "$base" ]; then
416
- printf '%-26s \033[31mcannot resolve default branch\033[0m\n' "$label"
417
- failed=$((failed + 1))
418
- continue
419
- fi
420
- printf '%s\t%s\t%s\t%s\n' "$label" "$d" "$slug" "$base" >> "$TARGETS"
421
- printf '%-26s \033[33mdrifted\033[0m%s\n' "$label" "$delta"
422
- done
423
-
424
- if [ -n "$CHECK" ]; then
425
- printf '\n%s current, %s drifted\n' "$current" "$drifted"
426
- if [ "$drifted" -gt 0 ]; then
427
- printf '\033[31mShared files have drifted.\033[0m Run without --check to open sync PRs.\n\n'
428
- exit 1
429
- fi
430
- printf '\n'
431
- exit 0
432
- fi
433
-
434
- if [ ! -s "$TARGETS" ]; then
435
- printf '\n%s current, %s drifted\n\n' "$current" "$drifted"
436
- [ "${failed:-0}" -gt 0 ] && exit 1
437
- exit 0
438
- fi
439
-
440
- # ---- Phase 1: rehearse -------------------------------------------------------
441
- #
442
- # Every target, before any of them ships. The order matters and it is the whole
443
- # point of the change: staging repo 7 and finding the gate broken there must not
444
- # leave six PRs already open in repos 1-6.
445
- if [ -n "$NO_REHEARSE" ]; then
446
- printf '\n\033[31m--no-rehearse: shipping %s repos unproven.\033[0m ' "$(wc -l < "$TARGETS" | tr -d ' ')"
447
- printf 'Nothing has run the gate against these candidates.\n'
448
- else
449
- printf '\nrehearsing %s repos - staging the candidates and running each gate\n\n' \
450
- "$(wc -l < "$TARGETS" | tr -d ' ')"
451
- rehearsal_failures=0
452
- while IFS="$TAB" read -r label d slug base; do
453
- # Read the status IMMEDIATELY. `if ! stage_repo ...` would have collapsed
454
- # "could not stage" (1) and "nothing to sync" (2) into one branch, and
455
- # reported a repo that was already current as a staging failure.
456
- stage_repo "$d" "$label" "$base"
457
- stage_rc=$?
458
- if [ "$stage_rc" -eq 2 ]; then
459
- printf '%-26s \033[32mnothing to sync\033[0m\n' "$label"
460
- printf '%s%s%s%s%s\n' "$label" "$TAB" SKIP "$TAB" 'nothing to sync' >> "$VERDICTS"
461
- continue
462
- fi
463
- if [ "$stage_rc" -ne 0 ]; then
464
- printf '%-26s \033[31mCANNOT STAGE\033[0m - fetch or worktree failed\n' "$label"
465
- printf '%s%s%s%s%s\n' "$label" "$TAB" FAIL "$TAB" 'could not stage' >> "$VERDICTS"
466
- rehearsal_failures=$((rehearsal_failures + 1))
467
- continue
468
- fi
469
- verdict_line=$(rehearse_repo "$d/.worktrees/shared-sync")
470
- verdict=$(printf '%s' "$verdict_line" | cut -f1)
471
- detail=$(printf '%s' "$verdict_line" | cut -f2-)
472
- case "$verdict" in
473
- PASS) printf '%-26s \033[32mPASS\033[0m %s\n' "$label" "$detail" ;;
474
- NO-CI) printf '%-26s \033[90mNO-CI\033[0m %s\n' "$label" "$detail" ;;
475
- *)
476
- printf '%-26s \033[31mFAIL\033[0m %s\n' "$label" "$detail"
477
- printf '%-26s staged tree left at %s\n' '' "$d/.worktrees/shared-sync"
478
- rehearsal_failures=$((rehearsal_failures + 1)) ;;
479
- esac
480
- printf '%s%s%s%s%s\n' "$label" "$TAB" "$verdict" "$TAB" "$detail" >> "$VERDICTS"
481
- done < "$TARGETS"
482
-
483
- if [ "$rehearsal_failures" -gt 0 ]; then
484
- printf '\n\033[31mrehearsal failed in %s repo(s) - NOTHING was pushed and no PR was opened.\033[0m\n' \
485
- "$rehearsal_failures"
486
- printf 'Fix the candidate files here in the template, then run this again. Each\n'
487
- printf 'round that ships before it is proven costs one PR per satellite: there were\n'
488
- printf '84 of them on 2026-07-29, in 7 rounds, and six of those rounds carried\n'
489
- printf 'scripts/verify.sh alone.\n'
490
- printf 'The failing repos keep their staged worktree so the gate can be run there;\n'
491
- printf 'the clean ones were removed.\n\n'
492
- while IFS="$TAB" read -r label verdict detail; do
493
- [ "$verdict" = FAIL ] || continue
494
- printf ' %-24s %s\n' "$label" "$detail"
495
- done < "$VERDICTS"
496
- printf '\n'
497
- # Reap the worktrees of the repos that passed. They staged cleanly and are
498
- # not evidence of anything; leaving 11 of them behind after a refusal is the
499
- # orphan-worktree accumulation AGENTS.md section 1 exists to prevent.
500
- while IFS="$TAB" read -r label d slug base; do
501
- grep -q "^$label${TAB}FAIL${TAB}" "$VERDICTS" && continue
502
- git -C "$d" worktree remove --force "$d/.worktrees/shared-sync" 2>/dev/null
503
- git -C "$d" branch -D chore/sync-shared 2>/dev/null
504
- done < "$TARGETS"
505
- exit 1
506
- fi
507
- printf '\nrehearsal clean in every repo\n'
508
- fi
509
-
510
- if [ -n "$REHEARSE_ONLY" ]; then
511
- # Staged worktrees are left in place deliberately: --rehearse answers "would
512
- # this land?", and the tree that answered it is the thing to go and look at.
513
- printf '\n--rehearse: nothing pushed, no PRs opened. Staged trees are at\n'
514
- printf '<repo>/.worktrees/shared-sync; re-run without --rehearse to ship.\n\n'
515
- exit 0
516
- fi
517
-
518
- # ---- Phase 2: ship -----------------------------------------------------------
519
- printf '\nopening PRs\n\n'
520
- while IFS="$TAB" read -r label d slug base; do
521
- grep -q "^$label${TAB}SKIP${TAB}" "$VERDICTS" 2>/dev/null && continue
522
- # --no-rehearse skips phase 1 entirely, so nothing has staged these yet.
523
- if [ -n "$NO_REHEARSE" ]; then
524
- stage_repo "$d" "$label" "$base"
525
- case $? in
526
- 2) printf '%-26s \033[32mnothing to sync\033[0m\n' "$label"; continue ;;
527
- 1) printf '%-26s \033[31mCANNOT STAGE\033[0m\n' "$label"; failed=$((failed + 1)); continue ;;
528
- esac
529
- fi
530
- if ship_repo "$d" "$label" "$slug" "$base"; then
531
- synced=$((synced + 1))
532
- else
533
- failed=$((failed + 1))
534
- fi
535
- done < "$TARGETS"
536
-
537
- printf '\n%s current, %s drifted, %s PR(s) opened' "$current" "$drifted" "$synced"
538
- [ "${failed:-0}" -gt 0 ] && printf ', \033[31m%s failed\033[0m' "$failed"
539
- printf '\n\n'
540
- [ "${failed:-0}" -gt 0 ] && exit 1
541
- exit 0
@@ -1,541 +0,0 @@
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
- # ## Rehearsal: why a distribution run is two phases
33
- #
34
- # Measured on 2026-07-29: **84 `chore(shared): sync template-shared files` PRs
35
- # merged across 12 satellites in one day** — 7 rounds, where one would have done.
36
- # That is 33.5% of the estate's entire merge volume for the day, and every one of
37
- # those PRs is classified `toil` by the practices collector, which read 60.2%.
38
- # Six of the seven rounds carried `scripts/verify.sh` **alone**: the gate was
39
- # being iterated downstream, one estate-wide lap per defect found (root-only
40
- # checks, `--no-cov` in a repo without pytest-cov, the pytest opt-in, `--list`
41
- # disagreeing with the gate).
42
- #
43
- # The reason a lap cost 12 PRs rather than 1 is that this script used to be a
44
- # single pass: for each repo in turn, stage -> push -> open a PR. A defect
45
- # discovered while staging repo 7 left 6 PRs already open, so fixing it meant a
46
- # whole new round rather than a correction.
47
- #
48
- # So: **rehearse every target before touching any of them.** Phase 1 stages the
49
- # candidate files into each repo and runs that repo's own gate against them,
50
- # locally, with no push and no PR. Phase 2 ships — and only if phase 1 was clean
51
- # everywhere. All 14 satellite clones are already on disk under `--estate`, so
52
- # proving the estate costs seconds; proving it through GitHub costs 12 PRs and 12
53
- # CI runs per lap.
54
- #
55
- # **It is not enough to let the target repo's pre-push hook catch this**, which
56
- # is what the push in phase 2 relies on. That hook is exactly the thing that was
57
- # silently DEAD in every fresh worktree until #845, and `scripts/hook-audit.sh`
58
- # exists because the failure was invisible. A repo whose hook is dead pushes
59
- # happily and opens a PR carrying an unproven gate — the fail-open shape this
60
- # whole file set exists to remove. Rehearsal runs the gate itself and reads its
61
- # exit code, so it does not depend on the hook being armed.
62
- #
63
- # Usage:
64
- # sh scripts/shared-sync.sh --check --estate ~/code # report drift, exit 1 if any
65
- # sh scripts/shared-sync.sh --rehearse --estate ~/code # prove the candidates, ship nothing
66
- # sh scripts/shared-sync.sh --estate ~/code # rehearse, then open a PR per repo
67
- # sh scripts/shared-sync.sh --estate ~/code --repo tabsii-crm
68
- # sh scripts/shared-sync.sh --estate ~/code --no-rehearse # ship unproven, loudly
69
-
70
- set -u
71
- # `pipefail` is not POSIX, and this file is documented and invoked as
72
- # `sh scripts/shared-sync.sh` -- including by scripts/practices-daily.sh.
73
- #
74
- # It carried a bare `set -uo pipefail` for as long as it only ever ran on the
75
- # workstation, whose /bin/sh is dash 0.5.12, a version that happens to accept
76
- # `-o pipefail`. The first time anything executed it elsewhere -- a test, on a
77
- # CI runner with an older sh -- it died at this line with
78
- # `set: Illegal option -o pipefail`, before parsing a single argument. Nothing
79
- # had noticed, because nothing had ever run it anywhere else.
80
- #
81
- # That is the third instance of this class in the corpus: the same one-version
82
- # difference in the same shell made js-dependency-audit.sh mangle every advisory
83
- # payload while exiting 0 (#883). Enable it where it exists, carry on where it
84
- # does not.
85
- (set -o pipefail) 2>/dev/null && set -o pipefail || true
86
-
87
- TEMPLATE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
88
- MANIFEST="$TEMPLATE_ROOT/shared-files.json"
89
- [ -f "$MANIFEST" ] || { echo "no shared-files.json beside $0" >&2; exit 2; }
90
-
91
- CHECK=""
92
- ESTATE=""
93
- ONLY=""
94
- REHEARSE_ONLY=""
95
- NO_REHEARSE=""
96
- while [ $# -gt 0 ]; do
97
- case "$1" in
98
- --check) CHECK=1; shift ;;
99
- --rehearse) REHEARSE_ONLY=1; shift ;;
100
- # In the open, per AGENTS.md section 7: a gate that can be skipped silently
101
- # is not a gate. This prints the reason on every line it lets through, so a
102
- # transcript shows what was shipped unproven.
103
- --no-rehearse) NO_REHEARSE=1; shift ;;
104
- --estate) ESTATE="$2"; shift 2 ;;
105
- --repo) ONLY="$2"; shift 2 ;;
106
- *) echo "unknown argument: $1" >&2; exit 2 ;;
107
- esac
108
- done
109
- [ -n "$ESTATE" ] || { echo "--estate <dir> is required" >&2; exit 2; }
110
- [ -n "$REHEARSE_ONLY" ] && [ -n "$NO_REHEARSE" ] && {
111
- echo "--rehearse and --no-rehearse are opposites" >&2; exit 2; }
112
-
113
- FILES=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).files.join('\n'))")
114
- MARKERS=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).appliesTo.join(' '))")
115
-
116
- drifted=0
117
- synced=0
118
- current=0
119
- failed=0
120
-
121
- # Field separator for the two state files below. A literal tab in a `grep`
122
- # pattern or a `${var%%...}` expansion is invisible in a diff and one editor
123
- # away from becoming spaces.
124
- TAB=$(printf '\t')
125
-
126
- applies() {
127
- # Instances are NOT in scope: they carry biffo.core.json and a
128
- # core-manifest.json, so `biffo core upgrade` three-way-merges these paths
129
- # into them. Two mechanisms writing the same files would fight, and the
130
- # core-ownership guard would refuse this script's commit anyway -- correctly,
131
- # since these are template-owned paths in an instance.
132
- [ -f "$1/biffo.core.json" ] && return 1
133
- for m in $MARKERS; do [ -f "$1/$m" ] && return 0; done
134
- # Also: any repo already carrying the gate. The runner repos have neither
135
- # marker but did receive verify.sh, and a mechanism that distributes a file
136
- # once and then stops tracking it is the drift this script exists to end --
137
- # it would have recreated the exact hole in the exact repos nobody watches.
138
- [ -f "$1/scripts/verify.sh" ] && return 0
139
- return 1
140
- }
141
-
142
- # Which of the shared files differ in this repo. Missing counts as drifted: a
143
- # repo that never received a file is exactly as unprotected as one holding a
144
- # stale copy, and reporting them differently invites triaging only the second.
145
- diff_files() {
146
- d="$1"
147
- out=""
148
- # Compare against the REPO's integration branch, not the local working copy.
149
- #
150
- # The first version read "$d/$f" straight off disk, so a clone that had not
151
- # been pulled reported DRIFTED for twelve repos that were entirely current --
152
- # right after their sync PRs merged. The question is "is this repository
153
- # current", not "is my laptop current", and a drift detector that fires on a
154
- # stale checkout is one you learn to ignore.
155
- git -C "$d" fetch origin --quiet 2>/dev/null
156
- # `dev` first, per AGENTS.md section 2: it is the integration branch in every
157
- # Biffo repo. origin/HEAD is NOT a substitute -- it points at `main` in
158
- # several clones, and `main` is a stale release branch that legitimately does
159
- # not carry these files, so resolving through it reported three repos as
160
- # missing everything.
161
- if git -C "$d" rev-parse --verify --quiet origin/dev >/dev/null 2>&1; then
162
- base=dev
163
- else
164
- base=$(git -C "$d" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
165
- [ -n "$base" ] || base=$(git -C "$d" rev-parse --abbrev-ref HEAD 2>/dev/null)
166
- fi
167
- for f in $FILES; do
168
- remote=$(git -C "$d" show "origin/$base:$f" 2>/dev/null)
169
- if [ -z "$remote" ]; then
170
- out="$out $f(missing)"
171
- elif [ "$remote" != "$(cat "$TEMPLATE_ROOT/$f")" ]; then
172
- out="$out $f"
173
- fi
174
- done
175
- echo "$out"
176
- }
177
-
178
- repo_slug() {
179
- git -C "$1" remote get-url origin | sed -E 's#.*[:/]([^/]+/[^/]+)$#\1#; s#\.git$##'
180
- }
181
-
182
- # The absolute path of a working tree's shared git directory, which is the same
183
- # for a primary checkout and every worktree linked to it -- i.e. an identity for
184
- # the REPOSITORY rather than for one of its working trees.
185
- repo_dir() {
186
- (cd "$1" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd)
187
- }
188
-
189
- TEMPLATE_REPO=$(repo_dir "$TEMPLATE_ROOT")
190
- [ -n "$TEMPLATE_REPO" ] || { echo "$TEMPLATE_ROOT is not a git repository" >&2; exit 2; }
191
-
192
- # Phase 1: put the candidate files in place and make the repo ready to run its
193
- # own gate against them. Deliberately stops short of committing anything -- a
194
- # staged worktree is a question ("would this land?"), and until phase 2 it has no
195
- # commit, no push and no PR.
196
- stage_repo() {
197
- d="$1"
198
- label="$2"
199
- base="$3"
200
-
201
- git -C "$d" fetch origin --quiet || return 1
202
- wt="$d/.worktrees/shared-sync"
203
- git -C "$d" worktree remove --force "$wt" 2>/dev/null
204
- # `branch -D` reports on STDOUT, so a quiet run printed "Deleted branch
205
- # chore/sync-shared" in the middle of the rehearsal table.
206
- git -C "$d" branch -D chore/sync-shared >/dev/null 2>&1
207
- git -C "$d" worktree add -q "$wt" -b chore/sync-shared "origin/$base" || return 1
208
-
209
- for f in $FILES; do
210
- mkdir -p "$wt/$(dirname "$f")"
211
- cp "$TEMPLATE_ROOT/$f" "$wt/$f"
212
- chmod +x "$wt/$f" 2>/dev/null
213
- done
214
-
215
- # Stamp the version these files CAME FROM, so a repo can say which template
216
- # its gate is, without a template clone or a network call (#869, H5 gap 1).
217
- #
218
- # Read from $TEMPLATE_ROOT, never from $wt. H5 pre-registered this as the most
219
- # likely way to make the whole thing meaningless: a stamp generated from the
220
- # RECEIVING repo always matches itself and reports perfect health forever --
221
- # the shape of every instrument defect found on 2026-07-29. The test asserts
222
- # the two differ.
223
- _tv=$(git -C "$TEMPLATE_ROOT" describe --tags --match 'core-v*' --abbrev=0 2>/dev/null || echo unknown)
224
- printf '%s\n' "$_tv" > "$wt/.biffo-shared-version"
225
-
226
- # Install the JS/Python deps the gate will now check, or its own pre-push
227
- # correctly refuses this push -- and reaching for BIFFO_SKIP_VERIFY to ship a
228
- # gate would be the counter-metric H4 pre-registered as refuting itself.
229
- # The ROOT package too, not only the nested ones. The first version matched
230
- # `--dir` paths only, so a repo whose package.json is at the root -- tabsii-map
231
- # -- got no install and the gate refused the push with `tsc: not found`. It
232
- # then reported GATE REFUSED, which was true and useless: the gate was right,
233
- # the installer had skipped the one layout it could not see.
234
- [ -f "$wt/package.json" ] && (cd "$wt" && pnpm install --frozen-lockfile >/dev/null 2>&1 || true)
235
- [ -f "$wt/pyproject.toml" ] && (cd "$wt" && uv sync --all-groups >/dev/null 2>&1 || true)
236
- 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
237
- (cd "$wt/$p" 2>/dev/null && { pnpm install --frozen-lockfile >/dev/null 2>&1 ||
238
- pnpm install --frozen-lockfile --ignore-workspace >/dev/null 2>&1 ||
239
- uv sync --all-groups >/dev/null 2>&1; }) || true
240
- done
241
-
242
- git -C "$wt" add -A
243
- if git -C "$wt" diff --cached --quiet; then
244
- git -C "$d" worktree remove --force "$wt" 2>/dev/null
245
- return 2
246
- fi
247
- return 0
248
- }
249
-
250
- # Phase 1's actual question: with the candidate files in place, does this repo's
251
- # gate still work HERE?
252
- #
253
- # This is the check the estate did not have. `--check` compares bytes, and
254
- # `gate-coverage.sh --estate` measures repos as they ARE -- neither can say
255
- # anything about a file that has not been distributed yet. Six of the seven
256
- # rounds on 2026-07-29 were fixing defects that only exist in a repo that is not
257
- # this one: a check list tuned to the template's layout, a pytest-cov flag a
258
- # plugin repo rejects, a package.json that is not at the root.
259
- rehearse_repo() {
260
- wt="$1"
261
-
262
- # `sh`, not `bash`: this is exactly how `.githooks/pre-push` invokes it
263
- # (`exec sh scripts/verify.sh`), and /bin/sh is dash on every machine in this
264
- # estate. A gate proven under one shell and run under another is not the same
265
- # gate -- `js-dependency-audit.sh` reported INCONCLUSIVE on every invocation
266
- # while exiting 0 for exactly that reason, because dash's `echo` interprets
267
- # backslash escapes and bash's does not (#883).
268
- _out=$( (cd "$wt" && sh scripts/verify.sh 2>&1) )
269
- _rc=$?
270
- _checks=$(printf '%s' "$_out" | sed -n 's/.*verify passed[^-]*- *//p' | head -1)
271
-
272
- # `gate-coverage.sh` reads `--list` rather than running anything, so it is
273
- # cheap, and it answers the question `verify.sh` cannot: the gate ran without
274
- # error, but how much of THIS repo's CI did it mirror? A gate can exit 0
275
- # having covered 1 kind in 8 -- that IS #855, and it is invisible in an exit
276
- # code. Reported, not enforced: what coverage number should block a
277
- # distribution is H5's call to make with numbers, not this script's to assume.
278
- #
279
- # Match its three verdicts, not just the fraction. Reading only `N/M` reported
280
- # a bare `?` for tabsii-map, which has no ci.yml at all -- so the one repo
281
- # where the coverage question does not apply looked like the one repo where the
282
- # measurement had failed. "Not applicable" and "could not tell" are different
283
- # answers, and this file exists because conflating them is expensive.
284
- _cov=$( (cd "$wt" && sh scripts/gate-coverage.sh 2>&1) |
285
- sed 's/\x1b\[[0-9;]*m//g' |
286
- grep -oE '([0-9]+/[0-9]+|no CI to mirror|NO GATE)' | head -1)
287
- [ -n "$_cov" ] || _cov='coverage unknown'
288
- case "$_cov" in
289
- */*) _cov="covers $_cov" ;;
290
- esac
291
-
292
- if [ "$_rc" -eq 0 ]; then
293
- case "$_out" in
294
- *"verify ran NOTHING"*)
295
- # verify.sh has already established this is not the #855 bug: no
296
- # `.github/workflows/ci.yml`, so this repo has no CI for the gate to
297
- # mirror and no shift-left obligation. Reporting it as a pass would be
298
- # the exact conflation the gate itself refuses to make, so it gets its
299
- # own verdict.
300
- printf '%s\t%s\n' NO-CI 'gate ran nothing; repo has no ci.yml' ;;
301
- *)
302
- printf '%s\t%s\n' PASS "$(printf '%s (%s)' "$_checks" "$_cov")" ;;
303
- esac
304
- return 0
305
- fi
306
- _why=$(printf '%s' "$_out" | grep -E 'verify failed|verify ran NOTHING' | head -1)
307
- [ -n "$_why" ] || _why=$(printf '%s' "$_out" | tail -1)
308
- printf '%s\t%s\n' FAIL "$(printf '%s' "$_why" | sed 's/\x1b\[[0-9;]*m//g')"
309
- return 1
310
- }
311
-
312
- # Phase 2: commit the already-staged worktree, push it, open the PR. Reached only
313
- # when phase 1 came back clean for EVERY repo in this run.
314
- ship_repo() {
315
- d="$1"
316
- label="$2"
317
- slug="$3"
318
- base="$4"
319
- wt="$d/.worktrees/shared-sync"
320
-
321
- git -C "$wt" -c commit.gpgsign=false commit -q --no-verify -m "chore(shared): sync template-shared files
322
-
323
- Distributed by biffo-template's scripts/shared-sync.sh. These files are held
324
- verbatim from the template; see shared-files.json there for the list and why
325
- this mechanism exists.
326
-
327
- Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>" >/dev/null 2>&1
328
-
329
- # --force-with-lease: this branch is regenerated from origin/<base> on every
330
- # run, so a previous sync's branch is legitimately replaced -- but the lease
331
- # still refuses if someone else has pushed to it.
332
- #
333
- # The output is CAPTURED and classified rather than discarded. The first
334
- # version printed "PUSH REFUSED (run the gate there and look)" for every
335
- # failure, and the first real cause was a plain non-fast-forward against a
336
- # previous run's branch. The gate was green in that repo. A diagnostic that
337
- # names the wrong cause sends you to read a passing log, which is the same
338
- # class of defect as everything else this gate exists to catch.
339
- push_out=$(git -C "$wt" push --force-with-lease -u origin HEAD 2>&1)
340
- push_rc=$?
341
- if [ "$push_rc" -ne 0 ]; then
342
- case "$push_out" in
343
- *"verify failed"*|*"verify ran NOTHING"*)
344
- printf '%-26s \033[31mGATE REFUSED THE PUSH\033[0m - run scripts/verify.sh there\n' "$label" ;;
345
- *"stale info"*|*"non-fast-forward"*)
346
- printf '%-26s \033[31mbranch diverged\033[0m - someone else pushed to chore/sync-shared\n' "$label" ;;
347
- *)
348
- printf '%-26s \033[31mpush failed\033[0m: %s\n' "$label" "$(echo "$push_out" | tail -1)" ;;
349
- esac
350
- return 1
351
- fi
352
- url=$(gh pr create --repo "$slug" --base "$base" --head chore/sync-shared \
353
- --title "chore(shared): sync template-shared files" \
354
- --body "Distributed by \`biffo-template\`'s \`scripts/shared-sync.sh\`.
355
-
356
- 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.
357
-
358
- Files synced verbatim from the template (see \`shared-files.json\` there):
359
-
360
- $(for f in $FILES; do echo "- \`$f\`"; done)
361
-
362
- Run \`sh scripts/gate-coverage.sh\` after merging to see this repo's gate measured against its own CI.
363
-
364
- 🤖 Generated with [Claude Code](https://claude.com/claude-code)" 2>&1 | tail -1)
365
- printf '%-26s %s\n' "$label" "$url"
366
- return 0
367
- }
368
-
369
- # The list of repos this run will touch, one `label<TAB>dir<TAB>slug<TAB>base`
370
- # per line. Written in the survey pass and read twice afterwards, so both phases
371
- # work from the same set: a rehearsal that proved a different list of repos than
372
- # the one that ships is worth nothing.
373
- TARGETS=$(mktemp)
374
- VERDICTS=$(mktemp)
375
- trap 'rm -f "$TARGETS" "$VERDICTS"' EXIT
376
-
377
- printf '\nshared-file sync - template -> repos core upgrade cannot reach\n\n'
378
- for d in "$ESTATE"/*/; do
379
- d="${d%/}"
380
- label=$(basename "$d")
381
- [ -e "$d/.git" ] || continue
382
- # The template is the source, not a target. It matched only because it carries
383
- # scripts/verify.sh, and comparing it to itself through origin/<base> reported
384
- # it as missing every file whenever its own dev was ahead of the checkout.
385
- #
386
- # Compare REPOSITORIES, not working-tree paths. `[ "$d" = "$TEMPLATE_ROOT" ]`
387
- # held only when this script was run from the primary checkout -- and AGENTS.md
388
- # section 1 mandates that all work happens in a worktree, where TEMPLATE_ROOT
389
- # is `.worktrees/<name>` and the primary checkout beside it looks like just
390
- # another satellite carrying scripts/verify.sh. It has no biffo.core.json and
391
- # no sibling/plugin marker, so nothing else excluded it either.
392
- #
393
- # It never fired because a primary checkout is normally byte-identical to
394
- # origin/dev, so `diff_files` reported `current` and skipped it. It fires the
395
- # moment the candidate files differ from origin/dev -- which is the only
396
- # situation this script is ever run in while iterating on a shared file. The
397
- # first `--rehearse` from a worktree would have staged the template as a
398
- # target of its own distribution and opened a sync PR against its own dev.
399
- [ "$(repo_dir "$d")" = "$TEMPLATE_REPO" ] && continue
400
- [ -n "$ONLY" ] && [ "$label" != "$ONLY" ] && continue
401
- applies "$d" || continue
402
- delta=$(diff_files "$d")
403
- if [ -z "$delta" ]; then
404
- current=$((current + 1))
405
- printf '%-26s \033[32mcurrent\033[0m\n' "$label"
406
- continue
407
- fi
408
- drifted=$((drifted + 1))
409
- if [ -n "$CHECK" ]; then
410
- printf '%-26s \033[31mDRIFTED\033[0m%s\n' "$label" "$delta"
411
- continue
412
- fi
413
- slug=$(repo_slug "$d")
414
- base=$(gh repo view "$slug" --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null)
415
- if [ -z "$base" ]; then
416
- printf '%-26s \033[31mcannot resolve default branch\033[0m\n' "$label"
417
- failed=$((failed + 1))
418
- continue
419
- fi
420
- printf '%s\t%s\t%s\t%s\n' "$label" "$d" "$slug" "$base" >> "$TARGETS"
421
- printf '%-26s \033[33mdrifted\033[0m%s\n' "$label" "$delta"
422
- done
423
-
424
- if [ -n "$CHECK" ]; then
425
- printf '\n%s current, %s drifted\n' "$current" "$drifted"
426
- if [ "$drifted" -gt 0 ]; then
427
- printf '\033[31mShared files have drifted.\033[0m Run without --check to open sync PRs.\n\n'
428
- exit 1
429
- fi
430
- printf '\n'
431
- exit 0
432
- fi
433
-
434
- if [ ! -s "$TARGETS" ]; then
435
- printf '\n%s current, %s drifted\n\n' "$current" "$drifted"
436
- [ "${failed:-0}" -gt 0 ] && exit 1
437
- exit 0
438
- fi
439
-
440
- # ---- Phase 1: rehearse -------------------------------------------------------
441
- #
442
- # Every target, before any of them ships. The order matters and it is the whole
443
- # point of the change: staging repo 7 and finding the gate broken there must not
444
- # leave six PRs already open in repos 1-6.
445
- if [ -n "$NO_REHEARSE" ]; then
446
- printf '\n\033[31m--no-rehearse: shipping %s repos unproven.\033[0m ' "$(wc -l < "$TARGETS" | tr -d ' ')"
447
- printf 'Nothing has run the gate against these candidates.\n'
448
- else
449
- printf '\nrehearsing %s repos - staging the candidates and running each gate\n\n' \
450
- "$(wc -l < "$TARGETS" | tr -d ' ')"
451
- rehearsal_failures=0
452
- while IFS="$TAB" read -r label d slug base; do
453
- # Read the status IMMEDIATELY. `if ! stage_repo ...` would have collapsed
454
- # "could not stage" (1) and "nothing to sync" (2) into one branch, and
455
- # reported a repo that was already current as a staging failure.
456
- stage_repo "$d" "$label" "$base"
457
- stage_rc=$?
458
- if [ "$stage_rc" -eq 2 ]; then
459
- printf '%-26s \033[32mnothing to sync\033[0m\n' "$label"
460
- printf '%s%s%s%s%s\n' "$label" "$TAB" SKIP "$TAB" 'nothing to sync' >> "$VERDICTS"
461
- continue
462
- fi
463
- if [ "$stage_rc" -ne 0 ]; then
464
- printf '%-26s \033[31mCANNOT STAGE\033[0m - fetch or worktree failed\n' "$label"
465
- printf '%s%s%s%s%s\n' "$label" "$TAB" FAIL "$TAB" 'could not stage' >> "$VERDICTS"
466
- rehearsal_failures=$((rehearsal_failures + 1))
467
- continue
468
- fi
469
- verdict_line=$(rehearse_repo "$d/.worktrees/shared-sync")
470
- verdict=$(printf '%s' "$verdict_line" | cut -f1)
471
- detail=$(printf '%s' "$verdict_line" | cut -f2-)
472
- case "$verdict" in
473
- PASS) printf '%-26s \033[32mPASS\033[0m %s\n' "$label" "$detail" ;;
474
- NO-CI) printf '%-26s \033[90mNO-CI\033[0m %s\n' "$label" "$detail" ;;
475
- *)
476
- printf '%-26s \033[31mFAIL\033[0m %s\n' "$label" "$detail"
477
- printf '%-26s staged tree left at %s\n' '' "$d/.worktrees/shared-sync"
478
- rehearsal_failures=$((rehearsal_failures + 1)) ;;
479
- esac
480
- printf '%s%s%s%s%s\n' "$label" "$TAB" "$verdict" "$TAB" "$detail" >> "$VERDICTS"
481
- done < "$TARGETS"
482
-
483
- if [ "$rehearsal_failures" -gt 0 ]; then
484
- printf '\n\033[31mrehearsal failed in %s repo(s) - NOTHING was pushed and no PR was opened.\033[0m\n' \
485
- "$rehearsal_failures"
486
- printf 'Fix the candidate files here in the template, then run this again. Each\n'
487
- printf 'round that ships before it is proven costs one PR per satellite: there were\n'
488
- printf '84 of them on 2026-07-29, in 7 rounds, and six of those rounds carried\n'
489
- printf 'scripts/verify.sh alone.\n'
490
- printf 'The failing repos keep their staged worktree so the gate can be run there;\n'
491
- printf 'the clean ones were removed.\n\n'
492
- while IFS="$TAB" read -r label verdict detail; do
493
- [ "$verdict" = FAIL ] || continue
494
- printf ' %-24s %s\n' "$label" "$detail"
495
- done < "$VERDICTS"
496
- printf '\n'
497
- # Reap the worktrees of the repos that passed. They staged cleanly and are
498
- # not evidence of anything; leaving 11 of them behind after a refusal is the
499
- # orphan-worktree accumulation AGENTS.md section 1 exists to prevent.
500
- while IFS="$TAB" read -r label d slug base; do
501
- grep -q "^$label${TAB}FAIL${TAB}" "$VERDICTS" && continue
502
- git -C "$d" worktree remove --force "$d/.worktrees/shared-sync" 2>/dev/null
503
- git -C "$d" branch -D chore/sync-shared 2>/dev/null
504
- done < "$TARGETS"
505
- exit 1
506
- fi
507
- printf '\nrehearsal clean in every repo\n'
508
- fi
509
-
510
- if [ -n "$REHEARSE_ONLY" ]; then
511
- # Staged worktrees are left in place deliberately: --rehearse answers "would
512
- # this land?", and the tree that answered it is the thing to go and look at.
513
- printf '\n--rehearse: nothing pushed, no PRs opened. Staged trees are at\n'
514
- printf '<repo>/.worktrees/shared-sync; re-run without --rehearse to ship.\n\n'
515
- exit 0
516
- fi
517
-
518
- # ---- Phase 2: ship -----------------------------------------------------------
519
- printf '\nopening PRs\n\n'
520
- while IFS="$TAB" read -r label d slug base; do
521
- grep -q "^$label${TAB}SKIP${TAB}" "$VERDICTS" 2>/dev/null && continue
522
- # --no-rehearse skips phase 1 entirely, so nothing has staged these yet.
523
- if [ -n "$NO_REHEARSE" ]; then
524
- stage_repo "$d" "$label" "$base"
525
- case $? in
526
- 2) printf '%-26s \033[32mnothing to sync\033[0m\n' "$label"; continue ;;
527
- 1) printf '%-26s \033[31mCANNOT STAGE\033[0m\n' "$label"; failed=$((failed + 1)); continue ;;
528
- esac
529
- fi
530
- if ship_repo "$d" "$label" "$slug" "$base"; then
531
- synced=$((synced + 1))
532
- else
533
- failed=$((failed + 1))
534
- fi
535
- done < "$TARGETS"
536
-
537
- printf '\n%s current, %s drifted, %s PR(s) opened' "$current" "$drifted" "$synced"
538
- [ "${failed:-0}" -gt 0 ] && printf ', \033[31m%s failed\033[0m' "$failed"
539
- printf '\n\n'
540
- [ "${failed:-0}" -gt 0 ] && exit 1
541
- exit 0