@biffo/cli 0.228.7 → 0.228.9

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.
@@ -78,6 +78,162 @@ set -u
78
78
  LIST=""
79
79
  [ "${1:-}" = "--list" ] && LIST=1
80
80
 
81
+ # Checkout health -- is the tree a command is about to trust stale or dirty in
82
+ # the ONE place that matters: the PRIMARY checkout. (#1196)
83
+ #
84
+ # AGENTS.md SS1/SS2 already say the primary must stay on the integration
85
+ # branch, no more than a `git fetch` behind, and that real work happens in
86
+ # worktrees instead -- but nothing checked it, and it happened again. An agent
87
+ # asked to migrate tabsii-crm's `auth.ts` read an 80-line file exporting five
88
+ # extra functions and built a whole, internally consistent, WRONG analysis on
89
+ # it: `origin/dev` held 36 lines and one export. The primary checkout it read
90
+ # was 16 commits behind, 1 ahead, dirty. The agent's reasoning was sound; its
91
+ # input was not, and nothing said so.
92
+ #
93
+ # This does not naturally belong to a PRE-PUSH file -- the incident above
94
+ # never touched git at all, it was a pure read, so a gate that only runs at
95
+ # push time could never have caught it by running later. It is here anyway,
96
+ # not in `scripts/hook-audit.sh` as the issue itself proposed, for a
97
+ # mechanical reason recorded rather than argued away: #1194 (gitleaks scope)
98
+ # and this were required to land in the same PR, in this file. Two things
99
+ # follow from that:
100
+ #
101
+ # `--checkout-health` is the part meant to answer #1196's actual question --
102
+ # run it BEFORE trusting a read, standalone, any time:
103
+ # sh scripts/verify.sh --checkout-health
104
+ #
105
+ # The WARN folded into the ordinary run further down is defence in depth for
106
+ # the one case an ordinary PUSH-TIME run can still see: a push attempted
107
+ # FROM the primary, which SS1 forbids outright regardless of staleness.
108
+ # It does not, and cannot, cover the read-only case the issue was filed for.
109
+ #
110
+ # Scope is deliberately narrow. A WORKTREE being behind `origin/dev` is normal
111
+ # mid-work -- that is what branching for a unit of work looks like -- so this
112
+ # never evaluates one. It only evaluates the ONE checkout AGENTS.md says must
113
+ # always mirror the integration branch: a non-worktree working tree whose
114
+ # CURRENT branch IS that branch. A worktree, a detached HEAD, or some other
115
+ # branch checked out at a repo root are all out of scope, and reported as
116
+ # such (`n/a`) rather than silently read as healthy for a question they were
117
+ # never asked.
118
+ CHECKOUT_HEALTH_BRANCH="${BIFFO_INTEGRATION_BRANCH:-dev}"
119
+
120
+ # A linked worktree's git-dir lives under the primary's `.git/worktrees/`; the
121
+ # primary's own git-dir does not. That is the one property distinguishing them
122
+ # regardless of where either happens to be checked out on disk.
123
+ _is_linked_worktree() {
124
+ case "$(git rev-parse --git-dir 2>/dev/null)" in
125
+ */worktrees/*) return 0 ;;
126
+ *) return 1 ;;
127
+ esac
128
+ }
129
+
130
+ # Verdict in CHECKOUT_HEALTH_VERDICT: healthy | stale | n/a | detached | unknown.
131
+ # Detail (the "why", for stale/unknown) in CHECKOUT_HEALTH_DETAIL.
132
+ #
133
+ # Sets globals rather than echoing the verdict for `$(...)` capture on
134
+ # purpose: `_v=$(checkout_health)` runs the function in a SUBSHELL, and any
135
+ # variable it assigns -- CHECKOUT_HEALTH_DETAIL included -- dies with that
136
+ # subshell. The first version did exactly that and every non-trivial verdict
137
+ # crashed the caller under `set -u` reading a detail that command substitution
138
+ # had already thrown away. Caught by the fail-first rehearsal below, not by
139
+ # the happy-path `healthy` case, which never touches DETAIL and looked fine.
140
+ checkout_health() {
141
+ CHECKOUT_HEALTH_VERDICT=""
142
+ CHECKOUT_HEALTH_DETAIL=""
143
+ if _is_linked_worktree; then
144
+ CHECKOUT_HEALTH_VERDICT="n/a"
145
+ return 0
146
+ fi
147
+ _ch_branch=$(git symbolic-ref --short -q HEAD) || {
148
+ CHECKOUT_HEALTH_VERDICT="detached"
149
+ return 0
150
+ }
151
+ if [ "$_ch_branch" != "$CHECKOUT_HEALTH_BRANCH" ]; then
152
+ CHECKOUT_HEALTH_VERDICT="n/a"
153
+ return 0
154
+ fi
155
+ _ch_dirty=""
156
+ [ -n "$(git status --porcelain 2>/dev/null)" ] && _ch_dirty=1
157
+
158
+ if ! git remote get-url origin >/dev/null 2>&1; then
159
+ CHECKOUT_HEALTH_VERDICT="unknown"
160
+ CHECKOUT_HEALTH_DETAIL="no 'origin' remote configured"
161
+ return 0
162
+ fi
163
+
164
+ # A fetch may be the only way to know staleness -- do not hard-fail an
165
+ # offline machine over a question it structurally cannot answer. Bounded so
166
+ # a dead network cannot hang the gate the way an unbounded fetch could; a
167
+ # stale-but-cached remote-tracking ref is still evidence, just older.
168
+ _ch_fetched=1
169
+ timeout 10 git fetch --quiet origin "$CHECKOUT_HEALTH_BRANCH" 2>/dev/null || _ch_fetched=""
170
+ if [ -z "$_ch_fetched" ] && ! git rev-parse -q --verify "origin/$CHECKOUT_HEALTH_BRANCH" >/dev/null 2>&1; then
171
+ CHECKOUT_HEALTH_VERDICT="unknown"
172
+ CHECKOUT_HEALTH_DETAIL="could not reach origin, and no cached origin/$CHECKOUT_HEALTH_BRANCH to fall back on"
173
+ return 0
174
+ fi
175
+
176
+ _ch_behind=$(git rev-list --count "HEAD..origin/$CHECKOUT_HEALTH_BRANCH" 2>/dev/null || echo 0)
177
+ _ch_ahead=$(git rev-list --count "origin/$CHECKOUT_HEALTH_BRANCH..HEAD" 2>/dev/null || echo 0)
178
+
179
+ if [ -n "$_ch_dirty" ] || [ "${_ch_behind:-0}" -gt 0 ] || [ "${_ch_ahead:-0}" -gt 0 ]; then
180
+ CHECKOUT_HEALTH_VERDICT="stale"
181
+ _ch_bits=""
182
+ [ "${_ch_behind:-0}" -gt 0 ] && _ch_bits="$_ch_bits ${_ch_behind} behind"
183
+ [ "${_ch_ahead:-0}" -gt 0 ] && _ch_bits="$_ch_bits ${_ch_ahead} ahead"
184
+ [ -n "$_ch_dirty" ] && _ch_bits="$_ch_bits dirty"
185
+ CHECKOUT_HEALTH_DETAIL="${_ch_bits# }"
186
+ return 0
187
+ fi
188
+
189
+ CHECKOUT_HEALTH_VERDICT="healthy"
190
+ return 0
191
+ }
192
+
193
+ # Standalone entry point. Exits before any of the slower checks below run --
194
+ # answering "can I trust this tree" is the whole job, not a side effect of a
195
+ # full gate run. FAILS CLOSED on a confirmed-stale tree (exit 1) and on a tree
196
+ # it could not evaluate (exit 2) -- "cannot tell" is never a pass, the same
197
+ # convention `wait-for-checks.sh` and `branch-health.sh` already use for
198
+ # exactly this reason. It does not fail merely for BEING the primary: a clean,
199
+ # up-to-date primary on the integration branch is the correct, expected state,
200
+ # not a violation -- only staleness and dirt are.
201
+ if [ "${1:-}" = "--checkout-health" ]; then
202
+ checkout_health
203
+ case "$CHECKOUT_HEALTH_VERDICT" in
204
+ healthy)
205
+ printf 'checkout-health: OK - on %s, clean, matches origin/%s\n' \
206
+ "$CHECKOUT_HEALTH_BRANCH" "$CHECKOUT_HEALTH_BRANCH"
207
+ exit 0
208
+ ;;
209
+ n/a)
210
+ printf 'checkout-health: n/a - not the primary checkout parked on %s (worktree, detached HEAD, or another branch)\n' \
211
+ "$CHECKOUT_HEALTH_BRANCH"
212
+ exit 0
213
+ ;;
214
+ detached)
215
+ printf 'checkout-health: n/a - detached HEAD, not evaluated\n'
216
+ exit 0
217
+ ;;
218
+ stale)
219
+ printf 'checkout-health: STALE - this checkout is %s\n' "$CHECKOUT_HEALTH_DETAIL"
220
+ printf 'AGENTS.md SS1/SS2: keep the primary on %s, no more than a git fetch\n' "$CHECKOUT_HEALTH_BRANCH"
221
+ printf 'behind, and do real work in a worktree instead. Do not trust anything\n'
222
+ printf 'read from this tree until: git fetch origin && git status\n'
223
+ exit 1
224
+ ;;
225
+ unknown)
226
+ printf 'checkout-health: CANNOT TELL - %s\n' "$CHECKOUT_HEALTH_DETAIL"
227
+ printf 'Not a pass -- reconnect and re-run before trusting this tree.\n'
228
+ exit 2
229
+ ;;
230
+ *)
231
+ printf 'checkout-health: CANNOT TELL - unexpected verdict "%s"\n' "$CHECKOUT_HEALTH_VERDICT"
232
+ exit 2
233
+ ;;
234
+ esac
235
+ fi
236
+
81
237
  FAILED=""
82
238
  PASSED=""
83
239
  SKIPPED=""
@@ -339,6 +495,25 @@ if [ -z "$LIST" ]; then
339
495
  _stamp=""
340
496
  [ -f .biffo-shared-version ] && _stamp=" (template $(cat .biffo-shared-version))"
341
497
  printf '\nverify - the checks CI runs, before the push%s\n\n' "$_stamp"
498
+
499
+ # Defence in depth (#1196): the ordinary run only reaches the ONE case it
500
+ # can, a push attempted straight from the primary checkout -- see the long
501
+ # comment above `checkout_health` for why this is a WARN, not a FAIL, and
502
+ # why it does not (cannot) cover the read-only case the issue was filed for.
503
+ # A worktree -- the normal place this script runs -- returns `n/a` here
504
+ # before any git command runs, so this costs nothing on the common path.
505
+ checkout_health
506
+ case "$CHECKOUT_HEALTH_VERDICT" in
507
+ stale)
508
+ printf ' \033[33mWARN\033[0m %-16s this is the primary checkout on %s and it is %s\n' \
509
+ "checkout-health" "$CHECKOUT_HEALTH_BRANCH" "$CHECKOUT_HEALTH_DETAIL"
510
+ printf ' \033[33mAGENTS.md SS1/SS2: never edit or push from the primary -- do the work in a worktree instead.\033[0m\n'
511
+ NOT_RUN="$NOT_RUN checkout-health"
512
+ ;;
513
+ unknown)
514
+ printf ' \033[33mWARN\033[0m %-16s could not verify - %s\n' "checkout-health" "$CHECKOUT_HEALTH_DETAIL"
515
+ ;;
516
+ esac
342
517
  fi
343
518
 
344
519
  # Python first: ruff is near-instant, so the cheapest feedback on the largest
@@ -640,6 +815,59 @@ fi
640
815
  # does nothing and lets the gate print `verify passed` is the precise failure this
641
816
  # whole file exists to prevent, and it would be worse here than elsewhere: the
642
817
  # thing not being checked is credentials.
818
+ #
819
+ # Scoped to TRACKED files only (#1194). `--no-git` walks the filesystem, not the
820
+ # index, and does not honour `.gitignore` -- so anything a build leaves behind
821
+ # gets scanned too. An agent in tabsii-crm ran `pnpm run build`, then hit this
822
+ # gate scanning 218MB of `.next/`/`out/` and got 30 phantom leaks, none in a
823
+ # tracked file, none of them committable. A scanner that cries wolf is worse
824
+ # than a slow one: the second time 30 leaks turn out to be bundle noise, people
825
+ # stop reading gitleaks output, which is exactly the day a real one hides in it.
826
+ #
827
+ # The fix is not a broader `.gitleaks.toml` allowlist -- that has to be kept in
828
+ # step with `.gitignore` by hand, in every repo this file runs in, forever, and
829
+ # it is the wrong shape besides: AGENTS.md SS7 says never fix a scan failure by
830
+ # editing the allowlist, and a path-list that grows to cover every build tool's
831
+ # output directory is that same fix wearing a different hat. It is narrowed to
832
+ # what the gate is actually FOR instead: nothing untracked can reach the remote
833
+ # a push sends to, so nothing untracked needs to be able to fail a push.
834
+ # `gitleaks_tracked_only` (below) mirrors `git ls-files` into a scratch
835
+ # directory -- current on-disk content, not HEAD, so a secret staged into an
836
+ # already-tracked file is still caught before the commit that would push it --
837
+ # and scans that copy. Relative paths inside the copy match the repo, so the
838
+ # existing path-based `.gitleaks.toml` allowlist entries keep working unchanged.
839
+ gitleaks_tracked_only() {
840
+ _gl_dir=$(mktemp -d "${TMPDIR:-/tmp}/biffo-gitleaks.XXXXXX") || return 1
841
+ _gl_root=$(git rev-parse --show-toplevel) || {
842
+ rm -rf "$_gl_dir"
843
+ return 1
844
+ }
845
+ # Newline-delimited, not `git ls-files -z` + `read -d ''`: `-d` is a bashism
846
+ # dash does not implement, and this file runs under `sh`
847
+ # (shell-portability.test.ts enforces it). The `plan_artefacts` loop above
848
+ # already made this exact call for the same reason.
849
+ ( cd "$_gl_root" && git ls-files ) | while IFS= read -r _f; do
850
+ [ -f "$_gl_root/$_f" ] || continue
851
+ mkdir -p "$_gl_dir/$(dirname "$_f")"
852
+ cp -p "$_gl_root/$_f" "$_gl_dir/$_f" 2>/dev/null
853
+ done
854
+ # No explicit `--config`, deliberately. gitleaks' own default resolution
855
+ # looks for `.gitleaks.toml` at "(target path)", which is `--source` -- i.e.
856
+ # the mirrored copy, where the file already sits at its usual relative path
857
+ # if this repo tracks one. A first cut passed `--config "$_gl_root/.gitleaks.toml"`
858
+ # to remove any doubt about that resolution, and it was a regression:
859
+ # against a repo with NO `.gitleaks.toml` at all -- a real, valid state,
860
+ # since gitleaks otherwise falls back to its built-in default ruleset --
861
+ # a literal `--config` path that does not exist is FATAL ("unable to load
862
+ # gitleaks config"), where the flagless form degrades gracefully to
863
+ # defaults, identical to what this repo ran before #1194. Verified by
864
+ # running both forms against a `--source` with no config file present.
865
+ gitleaks detect --no-git --redact --exit-code=2 --source "$_gl_dir"
866
+ _gl_status=$?
867
+ rm -rf "$_gl_dir"
868
+ return $_gl_status
869
+ }
870
+
643
871
  if ci_has "gitleaks"; then
644
872
  # The installation check gates EXECUTION only, never `--list`.
645
873
  #
@@ -653,7 +881,7 @@ if ci_has "gitleaks"; then
653
881
  # `terraform-fmt` above has the same shape and is only unexposed because
654
882
  # terraform happens to be installed here. Recorded, not fixed in this change.
655
883
  if [ -n "$LIST" ] || command -v gitleaks >/dev/null 2>&1; then
656
- run_check gitleaks gitleaks detect --no-git --redact --exit-code=2
884
+ run_check gitleaks gitleaks_tracked_only
657
885
  else
658
886
  # Say how to close it, pinned to the version ci.yml installs. A skip that
659
887
  # only reports its own absence stays skipped: this one sat `n/a` long
@@ -78,6 +78,162 @@ set -u
78
78
  LIST=""
79
79
  [ "${1:-}" = "--list" ] && LIST=1
80
80
 
81
+ # Checkout health -- is the tree a command is about to trust stale or dirty in
82
+ # the ONE place that matters: the PRIMARY checkout. (#1196)
83
+ #
84
+ # AGENTS.md SS1/SS2 already say the primary must stay on the integration
85
+ # branch, no more than a `git fetch` behind, and that real work happens in
86
+ # worktrees instead -- but nothing checked it, and it happened again. An agent
87
+ # asked to migrate tabsii-crm's `auth.ts` read an 80-line file exporting five
88
+ # extra functions and built a whole, internally consistent, WRONG analysis on
89
+ # it: `origin/dev` held 36 lines and one export. The primary checkout it read
90
+ # was 16 commits behind, 1 ahead, dirty. The agent's reasoning was sound; its
91
+ # input was not, and nothing said so.
92
+ #
93
+ # This does not naturally belong to a PRE-PUSH file -- the incident above
94
+ # never touched git at all, it was a pure read, so a gate that only runs at
95
+ # push time could never have caught it by running later. It is here anyway,
96
+ # not in `scripts/hook-audit.sh` as the issue itself proposed, for a
97
+ # mechanical reason recorded rather than argued away: #1194 (gitleaks scope)
98
+ # and this were required to land in the same PR, in this file. Two things
99
+ # follow from that:
100
+ #
101
+ # `--checkout-health` is the part meant to answer #1196's actual question --
102
+ # run it BEFORE trusting a read, standalone, any time:
103
+ # sh scripts/verify.sh --checkout-health
104
+ #
105
+ # The WARN folded into the ordinary run further down is defence in depth for
106
+ # the one case an ordinary PUSH-TIME run can still see: a push attempted
107
+ # FROM the primary, which SS1 forbids outright regardless of staleness.
108
+ # It does not, and cannot, cover the read-only case the issue was filed for.
109
+ #
110
+ # Scope is deliberately narrow. A WORKTREE being behind `origin/dev` is normal
111
+ # mid-work -- that is what branching for a unit of work looks like -- so this
112
+ # never evaluates one. It only evaluates the ONE checkout AGENTS.md says must
113
+ # always mirror the integration branch: a non-worktree working tree whose
114
+ # CURRENT branch IS that branch. A worktree, a detached HEAD, or some other
115
+ # branch checked out at a repo root are all out of scope, and reported as
116
+ # such (`n/a`) rather than silently read as healthy for a question they were
117
+ # never asked.
118
+ CHECKOUT_HEALTH_BRANCH="${BIFFO_INTEGRATION_BRANCH:-dev}"
119
+
120
+ # A linked worktree's git-dir lives under the primary's `.git/worktrees/`; the
121
+ # primary's own git-dir does not. That is the one property distinguishing them
122
+ # regardless of where either happens to be checked out on disk.
123
+ _is_linked_worktree() {
124
+ case "$(git rev-parse --git-dir 2>/dev/null)" in
125
+ */worktrees/*) return 0 ;;
126
+ *) return 1 ;;
127
+ esac
128
+ }
129
+
130
+ # Verdict in CHECKOUT_HEALTH_VERDICT: healthy | stale | n/a | detached | unknown.
131
+ # Detail (the "why", for stale/unknown) in CHECKOUT_HEALTH_DETAIL.
132
+ #
133
+ # Sets globals rather than echoing the verdict for `$(...)` capture on
134
+ # purpose: `_v=$(checkout_health)` runs the function in a SUBSHELL, and any
135
+ # variable it assigns -- CHECKOUT_HEALTH_DETAIL included -- dies with that
136
+ # subshell. The first version did exactly that and every non-trivial verdict
137
+ # crashed the caller under `set -u` reading a detail that command substitution
138
+ # had already thrown away. Caught by the fail-first rehearsal below, not by
139
+ # the happy-path `healthy` case, which never touches DETAIL and looked fine.
140
+ checkout_health() {
141
+ CHECKOUT_HEALTH_VERDICT=""
142
+ CHECKOUT_HEALTH_DETAIL=""
143
+ if _is_linked_worktree; then
144
+ CHECKOUT_HEALTH_VERDICT="n/a"
145
+ return 0
146
+ fi
147
+ _ch_branch=$(git symbolic-ref --short -q HEAD) || {
148
+ CHECKOUT_HEALTH_VERDICT="detached"
149
+ return 0
150
+ }
151
+ if [ "$_ch_branch" != "$CHECKOUT_HEALTH_BRANCH" ]; then
152
+ CHECKOUT_HEALTH_VERDICT="n/a"
153
+ return 0
154
+ fi
155
+ _ch_dirty=""
156
+ [ -n "$(git status --porcelain 2>/dev/null)" ] && _ch_dirty=1
157
+
158
+ if ! git remote get-url origin >/dev/null 2>&1; then
159
+ CHECKOUT_HEALTH_VERDICT="unknown"
160
+ CHECKOUT_HEALTH_DETAIL="no 'origin' remote configured"
161
+ return 0
162
+ fi
163
+
164
+ # A fetch may be the only way to know staleness -- do not hard-fail an
165
+ # offline machine over a question it structurally cannot answer. Bounded so
166
+ # a dead network cannot hang the gate the way an unbounded fetch could; a
167
+ # stale-but-cached remote-tracking ref is still evidence, just older.
168
+ _ch_fetched=1
169
+ timeout 10 git fetch --quiet origin "$CHECKOUT_HEALTH_BRANCH" 2>/dev/null || _ch_fetched=""
170
+ if [ -z "$_ch_fetched" ] && ! git rev-parse -q --verify "origin/$CHECKOUT_HEALTH_BRANCH" >/dev/null 2>&1; then
171
+ CHECKOUT_HEALTH_VERDICT="unknown"
172
+ CHECKOUT_HEALTH_DETAIL="could not reach origin, and no cached origin/$CHECKOUT_HEALTH_BRANCH to fall back on"
173
+ return 0
174
+ fi
175
+
176
+ _ch_behind=$(git rev-list --count "HEAD..origin/$CHECKOUT_HEALTH_BRANCH" 2>/dev/null || echo 0)
177
+ _ch_ahead=$(git rev-list --count "origin/$CHECKOUT_HEALTH_BRANCH..HEAD" 2>/dev/null || echo 0)
178
+
179
+ if [ -n "$_ch_dirty" ] || [ "${_ch_behind:-0}" -gt 0 ] || [ "${_ch_ahead:-0}" -gt 0 ]; then
180
+ CHECKOUT_HEALTH_VERDICT="stale"
181
+ _ch_bits=""
182
+ [ "${_ch_behind:-0}" -gt 0 ] && _ch_bits="$_ch_bits ${_ch_behind} behind"
183
+ [ "${_ch_ahead:-0}" -gt 0 ] && _ch_bits="$_ch_bits ${_ch_ahead} ahead"
184
+ [ -n "$_ch_dirty" ] && _ch_bits="$_ch_bits dirty"
185
+ CHECKOUT_HEALTH_DETAIL="${_ch_bits# }"
186
+ return 0
187
+ fi
188
+
189
+ CHECKOUT_HEALTH_VERDICT="healthy"
190
+ return 0
191
+ }
192
+
193
+ # Standalone entry point. Exits before any of the slower checks below run --
194
+ # answering "can I trust this tree" is the whole job, not a side effect of a
195
+ # full gate run. FAILS CLOSED on a confirmed-stale tree (exit 1) and on a tree
196
+ # it could not evaluate (exit 2) -- "cannot tell" is never a pass, the same
197
+ # convention `wait-for-checks.sh` and `branch-health.sh` already use for
198
+ # exactly this reason. It does not fail merely for BEING the primary: a clean,
199
+ # up-to-date primary on the integration branch is the correct, expected state,
200
+ # not a violation -- only staleness and dirt are.
201
+ if [ "${1:-}" = "--checkout-health" ]; then
202
+ checkout_health
203
+ case "$CHECKOUT_HEALTH_VERDICT" in
204
+ healthy)
205
+ printf 'checkout-health: OK - on %s, clean, matches origin/%s\n' \
206
+ "$CHECKOUT_HEALTH_BRANCH" "$CHECKOUT_HEALTH_BRANCH"
207
+ exit 0
208
+ ;;
209
+ n/a)
210
+ printf 'checkout-health: n/a - not the primary checkout parked on %s (worktree, detached HEAD, or another branch)\n' \
211
+ "$CHECKOUT_HEALTH_BRANCH"
212
+ exit 0
213
+ ;;
214
+ detached)
215
+ printf 'checkout-health: n/a - detached HEAD, not evaluated\n'
216
+ exit 0
217
+ ;;
218
+ stale)
219
+ printf 'checkout-health: STALE - this checkout is %s\n' "$CHECKOUT_HEALTH_DETAIL"
220
+ printf 'AGENTS.md SS1/SS2: keep the primary on %s, no more than a git fetch\n' "$CHECKOUT_HEALTH_BRANCH"
221
+ printf 'behind, and do real work in a worktree instead. Do not trust anything\n'
222
+ printf 'read from this tree until: git fetch origin && git status\n'
223
+ exit 1
224
+ ;;
225
+ unknown)
226
+ printf 'checkout-health: CANNOT TELL - %s\n' "$CHECKOUT_HEALTH_DETAIL"
227
+ printf 'Not a pass -- reconnect and re-run before trusting this tree.\n'
228
+ exit 2
229
+ ;;
230
+ *)
231
+ printf 'checkout-health: CANNOT TELL - unexpected verdict "%s"\n' "$CHECKOUT_HEALTH_VERDICT"
232
+ exit 2
233
+ ;;
234
+ esac
235
+ fi
236
+
81
237
  FAILED=""
82
238
  PASSED=""
83
239
  SKIPPED=""
@@ -339,6 +495,25 @@ if [ -z "$LIST" ]; then
339
495
  _stamp=""
340
496
  [ -f .biffo-shared-version ] && _stamp=" (template $(cat .biffo-shared-version))"
341
497
  printf '\nverify - the checks CI runs, before the push%s\n\n' "$_stamp"
498
+
499
+ # Defence in depth (#1196): the ordinary run only reaches the ONE case it
500
+ # can, a push attempted straight from the primary checkout -- see the long
501
+ # comment above `checkout_health` for why this is a WARN, not a FAIL, and
502
+ # why it does not (cannot) cover the read-only case the issue was filed for.
503
+ # A worktree -- the normal place this script runs -- returns `n/a` here
504
+ # before any git command runs, so this costs nothing on the common path.
505
+ checkout_health
506
+ case "$CHECKOUT_HEALTH_VERDICT" in
507
+ stale)
508
+ printf ' \033[33mWARN\033[0m %-16s this is the primary checkout on %s and it is %s\n' \
509
+ "checkout-health" "$CHECKOUT_HEALTH_BRANCH" "$CHECKOUT_HEALTH_DETAIL"
510
+ printf ' \033[33mAGENTS.md SS1/SS2: never edit or push from the primary -- do the work in a worktree instead.\033[0m\n'
511
+ NOT_RUN="$NOT_RUN checkout-health"
512
+ ;;
513
+ unknown)
514
+ printf ' \033[33mWARN\033[0m %-16s could not verify - %s\n' "checkout-health" "$CHECKOUT_HEALTH_DETAIL"
515
+ ;;
516
+ esac
342
517
  fi
343
518
 
344
519
  # Python first: ruff is near-instant, so the cheapest feedback on the largest
@@ -640,6 +815,59 @@ fi
640
815
  # does nothing and lets the gate print `verify passed` is the precise failure this
641
816
  # whole file exists to prevent, and it would be worse here than elsewhere: the
642
817
  # thing not being checked is credentials.
818
+ #
819
+ # Scoped to TRACKED files only (#1194). `--no-git` walks the filesystem, not the
820
+ # index, and does not honour `.gitignore` -- so anything a build leaves behind
821
+ # gets scanned too. An agent in tabsii-crm ran `pnpm run build`, then hit this
822
+ # gate scanning 218MB of `.next/`/`out/` and got 30 phantom leaks, none in a
823
+ # tracked file, none of them committable. A scanner that cries wolf is worse
824
+ # than a slow one: the second time 30 leaks turn out to be bundle noise, people
825
+ # stop reading gitleaks output, which is exactly the day a real one hides in it.
826
+ #
827
+ # The fix is not a broader `.gitleaks.toml` allowlist -- that has to be kept in
828
+ # step with `.gitignore` by hand, in every repo this file runs in, forever, and
829
+ # it is the wrong shape besides: AGENTS.md SS7 says never fix a scan failure by
830
+ # editing the allowlist, and a path-list that grows to cover every build tool's
831
+ # output directory is that same fix wearing a different hat. It is narrowed to
832
+ # what the gate is actually FOR instead: nothing untracked can reach the remote
833
+ # a push sends to, so nothing untracked needs to be able to fail a push.
834
+ # `gitleaks_tracked_only` (below) mirrors `git ls-files` into a scratch
835
+ # directory -- current on-disk content, not HEAD, so a secret staged into an
836
+ # already-tracked file is still caught before the commit that would push it --
837
+ # and scans that copy. Relative paths inside the copy match the repo, so the
838
+ # existing path-based `.gitleaks.toml` allowlist entries keep working unchanged.
839
+ gitleaks_tracked_only() {
840
+ _gl_dir=$(mktemp -d "${TMPDIR:-/tmp}/biffo-gitleaks.XXXXXX") || return 1
841
+ _gl_root=$(git rev-parse --show-toplevel) || {
842
+ rm -rf "$_gl_dir"
843
+ return 1
844
+ }
845
+ # Newline-delimited, not `git ls-files -z` + `read -d ''`: `-d` is a bashism
846
+ # dash does not implement, and this file runs under `sh`
847
+ # (shell-portability.test.ts enforces it). The `plan_artefacts` loop above
848
+ # already made this exact call for the same reason.
849
+ ( cd "$_gl_root" && git ls-files ) | while IFS= read -r _f; do
850
+ [ -f "$_gl_root/$_f" ] || continue
851
+ mkdir -p "$_gl_dir/$(dirname "$_f")"
852
+ cp -p "$_gl_root/$_f" "$_gl_dir/$_f" 2>/dev/null
853
+ done
854
+ # No explicit `--config`, deliberately. gitleaks' own default resolution
855
+ # looks for `.gitleaks.toml` at "(target path)", which is `--source` -- i.e.
856
+ # the mirrored copy, where the file already sits at its usual relative path
857
+ # if this repo tracks one. A first cut passed `--config "$_gl_root/.gitleaks.toml"`
858
+ # to remove any doubt about that resolution, and it was a regression:
859
+ # against a repo with NO `.gitleaks.toml` at all -- a real, valid state,
860
+ # since gitleaks otherwise falls back to its built-in default ruleset --
861
+ # a literal `--config` path that does not exist is FATAL ("unable to load
862
+ # gitleaks config"), where the flagless form degrades gracefully to
863
+ # defaults, identical to what this repo ran before #1194. Verified by
864
+ # running both forms against a `--source` with no config file present.
865
+ gitleaks detect --no-git --redact --exit-code=2 --source "$_gl_dir"
866
+ _gl_status=$?
867
+ rm -rf "$_gl_dir"
868
+ return $_gl_status
869
+ }
870
+
643
871
  if ci_has "gitleaks"; then
644
872
  # The installation check gates EXECUTION only, never `--list`.
645
873
  #
@@ -653,7 +881,7 @@ if ci_has "gitleaks"; then
653
881
  # `terraform-fmt` above has the same shape and is only unexposed because
654
882
  # terraform happens to be installed here. Recorded, not fixed in this change.
655
883
  if [ -n "$LIST" ] || command -v gitleaks >/dev/null 2>&1; then
656
- run_check gitleaks gitleaks detect --no-git --redact --exit-code=2
884
+ run_check gitleaks gitleaks_tracked_only
657
885
  else
658
886
  # Say how to close it, pinned to the version ci.yml installs. A skip that
659
887
  # only reports its own absence stays skipped: this one sat `n/a` long
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.228.7",
3
+ "version": "0.228.9",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",