@biffo/cli 0.243.2 → 0.244.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.
@@ -53,6 +53,47 @@ cd "$root" || exit 0
53
53
  # cannot run, that is a reason to stop, not to wave the push through.
54
54
  sh scripts/biffo.sh rewrite-scope-check || exit 1
55
55
 
56
+ # --- Claim guard (#1231 instance 2) -------------------------------------------
57
+ #
58
+ # `scripts/claim.sh` (packaged inside `@biffo/cli`, run through the bridge
59
+ # above) has asked the estate's four-signal question about "is this issue
60
+ # taken?" since #1209 -- but nothing ever RAN it. It shipped advisory-only, so
61
+ # the fix for the collisions it exists to catch was itself subject to the same
62
+ # failure: a collision happens precisely when someone does not think to check.
63
+ #
64
+ # `--guard <branch>` is the enforced form. It derives the issue number from the
65
+ # branch name (`<type>/<number>-slug>`) and, when the branch names one, checks
66
+ # for another remote branch or another OPEN PR naming the same issue -- NEVER
67
+ # the `in-progress` label (three of four real collisions were "work exists,
68
+ # label does not") and NEVER identity (a claim on #1109 was recorded as
69
+ # `github-actions[bot]` because of a repo-local `user.email` override). It
70
+ # excludes the branch being pushed and any PR whose head IS that branch, so
71
+ # pushing your own branch a second time cannot block you on your own work.
72
+ #
73
+ # A branch that names no issue returns before any network call -- most
74
+ # branches are fine, and noise kills a gate. Measured on a branch that DOES
75
+ # name one: the added latency is the cost of two `gh` lookups, similar in
76
+ # order of magnitude to `rewrite-scope-check` above.
77
+ branch=$(git symbolic-ref --quiet --short HEAD) || branch=""
78
+
79
+ if [ -n "$branch" ]; then
80
+ sh scripts/biffo.sh claim --guard "$branch"
81
+ claim_status=$?
82
+
83
+ if [ "$claim_status" -eq 1 ]; then
84
+ exit 1
85
+ fi
86
+
87
+ # $claim_status -eq 2 is "cannot tell" -- no network, gh unauthenticated, an
88
+ # API error -- and claim.sh has already printed a warning. Deliberately NOT
89
+ # blocked here: unlike every other gate in this file, this is a COORDINATION
90
+ # gate, not a correctness one. A false block costs the ability to push at
91
+ # all; a miss costs a recoverable collision caught early (every collision in
92
+ # #1209's own incident was caught before duplicate work merged). Set
93
+ # BIFFO_CLAIM_STRICT=1 to make claim.sh itself collapse cannot-tell into
94
+ # exit 1 instead, if you would rather block than guess.
95
+ fi
96
+
56
97
  # Through the version-pinned CLI since #1109, so a repo cannot run a gate two
57
98
  # versions old -- eight of them did, and tabsii-crm checked ONE thing in eight
58
99
  # on a 700-line change and printed `verify passed` (#855). There is one copy of
@@ -53,6 +53,47 @@ cd "$root" || exit 0
53
53
  # cannot run, that is a reason to stop, not to wave the push through.
54
54
  sh scripts/biffo.sh rewrite-scope-check || exit 1
55
55
 
56
+ # --- Claim guard (#1231 instance 2) -------------------------------------------
57
+ #
58
+ # `scripts/claim.sh` (packaged inside `@biffo/cli`, run through the bridge
59
+ # above) has asked the estate's four-signal question about "is this issue
60
+ # taken?" since #1209 -- but nothing ever RAN it. It shipped advisory-only, so
61
+ # the fix for the collisions it exists to catch was itself subject to the same
62
+ # failure: a collision happens precisely when someone does not think to check.
63
+ #
64
+ # `--guard <branch>` is the enforced form. It derives the issue number from the
65
+ # branch name (`<type>/<number>-slug>`) and, when the branch names one, checks
66
+ # for another remote branch or another OPEN PR naming the same issue -- NEVER
67
+ # the `in-progress` label (three of four real collisions were "work exists,
68
+ # label does not") and NEVER identity (a claim on #1109 was recorded as
69
+ # `github-actions[bot]` because of a repo-local `user.email` override). It
70
+ # excludes the branch being pushed and any PR whose head IS that branch, so
71
+ # pushing your own branch a second time cannot block you on your own work.
72
+ #
73
+ # A branch that names no issue returns before any network call -- most
74
+ # branches are fine, and noise kills a gate. Measured on a branch that DOES
75
+ # name one: the added latency is the cost of two `gh` lookups, similar in
76
+ # order of magnitude to `rewrite-scope-check` above.
77
+ branch=$(git symbolic-ref --quiet --short HEAD) || branch=""
78
+
79
+ if [ -n "$branch" ]; then
80
+ sh scripts/biffo.sh claim --guard "$branch"
81
+ claim_status=$?
82
+
83
+ if [ "$claim_status" -eq 1 ]; then
84
+ exit 1
85
+ fi
86
+
87
+ # $claim_status -eq 2 is "cannot tell" -- no network, gh unauthenticated, an
88
+ # API error -- and claim.sh has already printed a warning. Deliberately NOT
89
+ # blocked here: unlike every other gate in this file, this is a COORDINATION
90
+ # gate, not a correctness one. A false block costs the ability to push at
91
+ # all; a miss costs a recoverable collision caught early (every collision in
92
+ # #1209's own incident was caught before duplicate work merged). Set
93
+ # BIFFO_CLAIM_STRICT=1 to make claim.sh itself collapse cannot-tell into
94
+ # exit 1 instead, if you would rather block than guess.
95
+ fi
96
+
56
97
  # Through the version-pinned CLI since #1109, so a repo cannot run a gate two
57
98
  # versions old -- eight of them did, and tabsii-crm checked ONE thing in eight
58
99
  # on a 700-line change and printed `verify passed` (#855). There is one copy of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.243.2",
3
+ "version": "0.244.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/claim.sh CHANGED
@@ -42,6 +42,7 @@
42
42
  # sh scripts/claim.sh 1234 # check, and claim if free
43
43
  # sh scripts/claim.sh 1234 --check # report only, change nothing
44
44
  # sh scripts/claim.sh 1234 -R owner/repo
45
+ # sh scripts/claim.sh --guard <branch> # pre-push gate — see below
45
46
  #
46
47
  # 0 free — and claimed, unless --check
47
48
  # 1 taken, or already closed — the reason is printed
@@ -51,15 +52,50 @@
51
52
  # A check that cannot see its input must not report "free".
52
53
  #
53
54
  # Requires `gh`, authenticated. Uses gh's embedded jq, so no jq binary needed.
55
+ #
56
+ # ## `--guard <branch>` — the pre-push gate (#1231 instance 2)
57
+ #
58
+ # The four-signal check above is advisory: nothing ever runs it for you, so a
59
+ # collision happens precisely when someone does not think to check. `--guard`
60
+ # is the enforced half, called from `.githooks/pre-push` on every push.
61
+ #
62
+ # It answers a narrower question than the plain form above, on purpose:
63
+ #
64
+ # - **Derives the issue from the branch name.** Pattern `<type>/<number>-…`,
65
+ # e.g. `feat/1234-thing`. A branch that does not name an issue (most of
66
+ # them) is skipped SILENTLY, before any network call — most branches are
67
+ # fine and noise kills a gate.
68
+ # - **Never checks the `in-progress` label.** Of four real collisions in one
69
+ # morning, three were "work exists, label does not" — so a gate keyed on
70
+ # the label would have caught one of four. This checks only what git and
71
+ # GitHub cannot help but know: an open PR, a remote branch.
72
+ # - **Never compares identity.** A claim on #1109 was recorded as
73
+ # `github-actions[bot]` because of a repo-local `user.email` override —
74
+ # usernames are not a trustworthy signal here.
75
+ # - **Excludes the branch being pushed, and any PR whose head IS that
76
+ # branch**, from counting as a conflict. Without this, pushing your own
77
+ # branch a second time blocks you on your own work.
78
+ # - **A real conflict — another branch or another open PR naming the same
79
+ # issue — exits 1**, naming what was found. AGENTS.md permits stealing a
80
+ # claim that is over an hour stale, with a comment; the message points
81
+ # there rather than being a dead end.
82
+ # - **"Cannot tell" (no network, `gh` unauthenticated, an API error) warns
83
+ # and exits 2 — never 1.** By default the caller should treat 2 as a pass:
84
+ # this is a COORDINATION gate, not a correctness one, and a false block
85
+ # costs the ability to push at all while a miss costs a recoverable
86
+ # collision caught early (see "What it cannot do" above). Set
87
+ # `BIFFO_CLAIM_STRICT=1` to make this script itself collapse cannot-tell
88
+ # into exit 1 instead, for anyone who would rather block than guess.
54
89
 
55
90
  set -u
56
91
 
57
92
  ISSUE=""
58
93
  REPO=""
59
94
  CHECK_ONLY=""
95
+ GUARD_BRANCH=""
60
96
 
61
97
  usage() {
62
- sed -n '2,58p' "$0" | sed 's/^# \{0,1\}//'
98
+ sed -n '2,84p' "$0" | sed 's/^# \{0,1\}//'
63
99
  exit 2
64
100
  }
65
101
 
@@ -70,6 +106,10 @@ while [ $# -gt 0 ]; do
70
106
  shift 2
71
107
  ;;
72
108
  --check) CHECK_ONLY=1; shift ;;
109
+ --guard)
110
+ GUARD_BRANCH="${2:-}"
111
+ shift 2
112
+ ;;
73
113
  -h | --help) usage ;;
74
114
  *)
75
115
  ISSUE="$1"
@@ -78,13 +118,6 @@ while [ $# -gt 0 ]; do
78
118
  esac
79
119
  done
80
120
 
81
- case "$ISSUE" in
82
- '' | *[!0-9]*)
83
- echo "claim: give an issue number, e.g. sh scripts/claim.sh 1234" >&2
84
- exit 2
85
- ;;
86
- esac
87
-
88
121
  RED=$(printf '\033[31m')
89
122
  GREEN=$(printf '\033[32m')
90
123
  YELLOW=$(printf '\033[33m')
@@ -94,12 +127,117 @@ OFF=$(printf '\033[0m')
94
127
  gh_issue() { if [ -n "$REPO" ]; then gh issue "$@" --repo "$REPO"; else gh issue "$@"; fi; }
95
128
  gh_pr() { if [ -n "$REPO" ]; then gh pr "$@" --repo "$REPO"; else gh pr "$@"; fi; }
96
129
 
130
+ # `git ls-remote --heads ""` fails outright ("fatal: bad repository ''") rather
131
+ # than falling back to the default remote — `${REPO:+url}` expands to an empty
132
+ # STRING ARGUMENT when $REPO is unset, not to no argument at all. That silently
133
+ # broke signal 3 below for every caller that does not pass -R, which is nearly
134
+ # all of them: the branch check never fired, and nothing noticed because the
135
+ # label and open-PR signals usually catch a collision first. Found while
136
+ # building `--guard`, which depends on this signal actually working.
137
+ remote_branches() {
138
+ if [ -n "$REPO" ]; then
139
+ git ls-remote --heads "https://github.com/$REPO.git"
140
+ else
141
+ git ls-remote --heads
142
+ fi
143
+ }
144
+
97
145
  LABEL=in-progress
98
146
  TAKEN=0
99
147
  REASONS=""
100
148
 
101
149
  note() { REASONS="${REASONS} $1\n"; TAKEN=1; }
102
150
 
151
+ # --- --guard <branch>: the enforced pre-push gate -----------------------------
152
+ #
153
+ # Deliberately short-circuits before any of the four-signal machinery below —
154
+ # it asks two of those four questions, not all four, and answers a different
155
+ # question ("would this push collide with someone else's live work?" rather
156
+ # than "is this issue free to claim?").
157
+ if [ -n "$GUARD_BRANCH" ]; then
158
+ guard_issue=$(printf '%s' "$GUARD_BRANCH" | sed -n 's#^[^/]*/\([0-9][0-9]*\)-.*#\1#p')
159
+
160
+ # No issue named by the branch — most branches, e.g.
161
+ # `security/brace-expansion-5-0-9`. Skip silently, and — this is the point —
162
+ # before touching the network at all.
163
+ if [ -z "$guard_issue" ]; then
164
+ exit 0
165
+ fi
166
+
167
+ cannot_tell=0
168
+ conflict=0
169
+ findings=""
170
+ cannot_tell_reasons=""
171
+
172
+ # --- an open PR referencing the issue, excluding our own branch's PR --------
173
+ pr_err=$(mktemp)
174
+ open_prs=$(gh_pr list --state open --limit 100 --json number,title,body,headRefName \
175
+ --jq "[.[] | select(((.title + \" \" + .body) | test(\"(^|[^0-9])#$guard_issue([^0-9]|\$)\")) or (.headRefName | test(\"(^|[^0-9])$guard_issue([^0-9]|\$)\")))] | .[] | select(.headRefName != \"$GUARD_BRANCH\") | \"#\(.number) \(.headRefName)\"" \
176
+ 2>"$pr_err")
177
+ pr_status=$?
178
+ pr_err_text=$(cat "$pr_err")
179
+ rm -f "$pr_err"
180
+
181
+ if [ "$pr_status" -ne 0 ]; then
182
+ cannot_tell=1
183
+ cannot_tell_reasons="${cannot_tell_reasons}${pr_err_text} "
184
+ elif [ -n "$open_prs" ]; then
185
+ conflict=1
186
+ findings="${findings} ${RED}open PR${OFF} $(printf '%s' "$open_prs" | head -1)\n"
187
+ fi
188
+
189
+ # --- a remote branch naming the issue, excluding our own branch -------------
190
+ branch_err=$(mktemp)
191
+ raw_branches=$(remote_branches 2>"$branch_err")
192
+ branch_status=$?
193
+ branch_err_text=$(cat "$branch_err")
194
+ rm -f "$branch_err"
195
+
196
+ if [ "$branch_status" -ne 0 ]; then
197
+ cannot_tell=1
198
+ cannot_tell_reasons="${cannot_tell_reasons}${branch_err_text} "
199
+ else
200
+ other_branch=$(printf '%s\n' "$raw_branches" |
201
+ sed 's|.*refs/heads/||' |
202
+ grep -E "(^|[^0-9])$guard_issue([^0-9]|$)" |
203
+ grep -v -x "$GUARD_BRANCH" | head -1)
204
+ if [ -n "$other_branch" ]; then
205
+ conflict=1
206
+ findings="${findings} ${RED}branch${OFF} $other_branch\n"
207
+ fi
208
+ fi
209
+
210
+ if [ "$conflict" -eq 1 ]; then
211
+ printf '%b' "${RED}claim --guard: issue #$guard_issue looks claimed by someone else.${OFF}\n$findings"
212
+ echo
213
+ echo "${DIM}If you believe it is abandoned (no activity for over an hour), AGENTS.md${OFF}"
214
+ echo "${DIM}permits stealing it deliberately — say so in a comment on the issue first,${OFF}"
215
+ echo "${DIM}then push. Never steal a fresh claim.${OFF}"
216
+ exit 1
217
+ fi
218
+
219
+ if [ "$cannot_tell" -eq 1 ]; then
220
+ if [ -n "${BIFFO_CLAIM_STRICT:-}" ]; then
221
+ echo "${RED}claim --guard: cannot tell whether #$guard_issue is claimed elsewhere${OFF} — ${DIM}$cannot_tell_reasons${OFF}" >&2
222
+ echo "${DIM}BIFFO_CLAIM_STRICT=1 is set, so cannot-tell blocks the push.${OFF}" >&2
223
+ exit 1
224
+ fi
225
+ echo "${YELLOW}claim --guard: cannot tell whether #$guard_issue is claimed elsewhere${OFF} — ${DIM}$cannot_tell_reasons${OFF}" >&2
226
+ echo "${DIM}Warning and letting the push through: this is a coordination gate, not a${OFF}" >&2
227
+ echo "${DIM}correctness one. Set BIFFO_CLAIM_STRICT=1 to block instead of guessing.${OFF}" >&2
228
+ exit 2
229
+ fi
230
+
231
+ exit 0
232
+ fi
233
+
234
+ case "$ISSUE" in
235
+ '' | *[!0-9]*)
236
+ echo "claim: give an issue number, e.g. sh scripts/claim.sh 1234" >&2
237
+ exit 2
238
+ ;;
239
+ esac
240
+
103
241
  # --- 0. Does the issue exist, and is it still open? --------------------------
104
242
 
105
243
  meta=$(gh_issue view "$ISSUE" --json state,title,labels \
@@ -150,7 +288,7 @@ fi
150
288
  #
151
289
  # Catches work that has been pushed but has no PR yet. Whole-number match again.
152
290
 
153
- branches=$(git ls-remote --heads "${REPO:+https://github.com/$REPO.git}" 2>/dev/null |
291
+ branches=$(remote_branches 2>/dev/null |
154
292
  sed 's|.*refs/heads/||' |
155
293
  grep -E "(^|[^0-9])$ISSUE([^0-9]|$)" 2>/dev/null)
156
294