@biffo/cli 0.298.19 → 0.298.20

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/claim.sh +124 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.298.19",
3
+ "version": "0.298.20",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/claim.sh CHANGED
@@ -101,9 +101,12 @@
101
101
  # morning, three were "work exists, label does not" — so a gate keyed on
102
102
  # the label would have caught one of four. This checks only what git and
103
103
  # GitHub cannot help but know: an open PR, a remote branch.
104
- # - **Never compares identity.** A claim on #1109 was recorded as
105
- # `github-actions[bot]` because of a repo-local `user.email` override —
106
- # usernames are not a trustworthy signal here.
104
+ # - **Never compares identity by GitHub username.** A claim on #1109 was
105
+ # recorded as `github-actions[bot]` because of a repo-local `user.email`
106
+ # override — usernames are not a trustworthy signal here. What #1698 adds
107
+ # below is not a username comparison: it is the claim record's own
108
+ # `claim-branch` field, written by `--as` at claim time, checked against
109
+ # the branch actually being pushed.
107
110
  # - **Excludes the branch being pushed, and any PR whose head IS that
108
111
  # branch**, from counting as a conflict. Without this, pushing your own
109
112
  # branch a second time blocks you on your own work.
@@ -122,6 +125,18 @@
122
125
  # waved through the most ordinary rival there is. Note the discount applies
123
126
  # to the BRANCH signal only: an OPEN PR is a live claim regardless of
124
127
  # lineage and is never discounted.
128
+ # - **The discount above is ALSO gated on the claim record (#1698).** Patch
129
+ # equivalence alone cannot tell "my own rebase of my own predecessor" from
130
+ # "an independent rival's rebase of the same starting point" — two sessions
131
+ # rebasing the same predecessor onto the same tip produce identical
132
+ # patches, with zero merge commits, and no open PR on either side for the
133
+ # first several minutes. `branch_is_absorbed` therefore only DISCOUNTS a
134
+ # candidate when the issue's claim record — `claim_guard_branch`, read from
135
+ # the same durable comment `--as` already writes, never a new store —
136
+ # names the branch being pushed as the CURRENT claim. Missing, unreadable,
137
+ # written before this field existed, or naming a different branch: all
138
+ # fail CLOSED into a conflict, same as an ordinary rival, with a message
139
+ # explaining why rather than a silent pass.
125
140
  # - **A real conflict — another branch or another open PR naming the same
126
141
  # issue — exits 1**, naming what was found. AGENTS.md permits stealing a
127
142
  # claim that is over an hour stale, with a comment; the message points
@@ -141,6 +156,7 @@ REPO=""
141
156
  HOLDER=""
142
157
  RELEASE=""
143
158
  HOLDER_MARK="claim-holder:"
159
+ BRANCH_MARK="claim-branch:"
144
160
  CHECK_ONLY=""
145
161
  GUARD_BRANCH=""
146
162
 
@@ -280,6 +296,36 @@ claim_held_by() {
280
296
  esac
281
297
  }
282
298
 
299
+ # claim_guard_branch <issue> -- tri-state read of the RECORDED branch on
300
+ # $issue's most recent claim comment (the `claim-branch:` field the write
301
+ # side below adds, #1698). Prints the branch name and returns 0 when a claim
302
+ # comment carries one; returns 1 when the comment list is readable but gives
303
+ # no confirmed branch (no claim comment at all, or one written before this
304
+ # field existed); returns 2 when the comment list itself could not be read.
305
+ #
306
+ # 1 and 2 are BOTH "not confirmed", never "confirmed free" -- the same
307
+ # fail-closed shape `claim_held_by` already uses for the holder token, for the
308
+ # same reason: a caller that collapsed them would treat an unclaimed issue or
309
+ # a flaky API call as proof nobody else is here, which is exactly the
310
+ # fail-open `--guard`'s own lineage discount shipped with (#1696) and this
311
+ # function exists to close.
312
+ claim_guard_branch() {
313
+ _c=$(gh_issue view "$1" --json comments \
314
+ --jq "[.comments[]? | select(.body | contains(\"$HOLDER_MARK\"))] | last | .body // \"\"" \
315
+ 2>/dev/null) || return 2
316
+ [ -n "$_c" ] || return 1
317
+ case "$_c" in
318
+ *"$BRANCH_MARK"*)
319
+ _gb_rest=${_c#*"$BRANCH_MARK"}
320
+ _gb_branch=${_gb_rest%% *}
321
+ [ -n "$_gb_branch" ] || return 1
322
+ printf '%s\n' "$_gb_branch"
323
+ return 0
324
+ ;;
325
+ *) return 1 ;;
326
+ esac
327
+ }
328
+
283
329
  # This repo as `owner/name`, so a closing reference can be checked against it.
284
330
  # GitHub records a CROSS-REPO closing reference in the same list as a local one
285
331
  # -- tabsii-lms#43 closes tabsii-platform#553 -- so the number alone is not an
@@ -743,6 +789,39 @@ if [ -n "$GUARD_BRANCH" ]; then
743
789
  findings="${findings} ${RED}open PR${OFF} $(printf '%s' "$open_prs" | head -1)\n"
744
790
  fi
745
791
 
792
+ # --- the claim record's own idea of who currently holds $guard_issue --------
793
+ #
794
+ # #1698: `branch_is_absorbed` below answers "does the candidate carry work I
795
+ # lack?" -- a content test, not an identity one. Two independent sessions
796
+ # rebasing the same predecessor onto the same tip produce patch-id-equivalent
797
+ # commits, so content alone cannot tell "my own rebase of my own predecessor"
798
+ # from a genuine rival's rebase of the same starting point. Read once, used
799
+ # below to gate every absorption on this push -- not by comparing $2's shape
800
+ # the way `claim_held_by` does (`--guard` runs with no token, by design, see
801
+ # the header), but by checking whether the record's own `claim-branch` names
802
+ # THIS branch.
803
+ _guard_confirmed_branch=$(claim_guard_branch "$guard_issue")
804
+ _guard_confirmed_rc=$?
805
+ case "$_guard_confirmed_rc" in
806
+ 0)
807
+ if [ "$_guard_confirmed_branch" = "$GUARD_BRANCH" ]; then
808
+ _guard_identity_ok=1
809
+ _guard_identity_reason=""
810
+ else
811
+ _guard_identity_ok=0
812
+ _guard_identity_reason="the claim record for #$guard_issue names \`$_guard_confirmed_branch\`, not this branch"
813
+ fi
814
+ ;;
815
+ 1)
816
+ _guard_identity_ok=0
817
+ _guard_identity_reason="#$guard_issue carries no claim record naming a branch (unclaimed, or claimed before #1698's fix)"
818
+ ;;
819
+ *)
820
+ _guard_identity_ok=0
821
+ _guard_identity_reason="the claim record for #$guard_issue could not be read"
822
+ ;;
823
+ esac
824
+
746
825
  # --- a remote branch naming the issue, excluding our own branch -------------
747
826
  branch_err=$(mktemp)
748
827
  raw_branches=$(remote_branches 2>"$branch_err")
@@ -759,11 +838,15 @@ if [ -n "$GUARD_BRANCH" ]; then
759
838
  # substring search for $guard_issue inside the candidate's raw text. See
760
839
  # `derive_branch_issue` above for why that asymmetry was the defect.
761
840
  # Each candidate that names the same issue is then classified by LINEAGE
762
- # (see `branch_is_absorbed`): `rival` is a conflict, `absorbed` is this
763
- # pusher's own superseded predecessor and is reported but not blocked.
841
+ # (see `branch_is_absorbed`) AND, when lineage says absorbed, by IDENTITY
842
+ # (`$_guard_identity_ok`, above): a genuine rival is always `rival`; a
843
+ # content-superseded predecessor is only actually `absorbed` when the
844
+ # claim record also confirms this push as the current claim -- otherwise
845
+ # it is `denied` and treated as a conflict too (#1698 fails this closed,
846
+ # rather than the silent pass it replaces), with a message explaining why.
764
847
  # The candidate's SHA -- `git ls-remote`'s first, tab-separated field -- is
765
- # what makes that question answerable, so it is no longer thrown away by a
766
- # `sed` that kept only the name.
848
+ # what makes lineage answerable, so it is no longer thrown away by a `sed`
849
+ # that kept only the name.
767
850
  _classified=$(printf '%s\n' "$raw_branches" |
768
851
  while IFS= read -r _line; do
769
852
  case "$_line" in
@@ -778,27 +861,39 @@ if [ -n "$GUARD_BRANCH" ]; then
778
861
  [ "$_cand_issue" = "$guard_issue" ] || continue
779
862
  _cand_sha=$(printf '%s\n' "$_line" | cut -f1)
780
863
  if branch_is_absorbed "$_cand_sha" "$guard_tip"; then
781
- printf 'absorbed %s\n' "$_cand"
864
+ if [ "$_guard_identity_ok" -eq 1 ]; then
865
+ printf 'absorbed %s\n' "$_cand"
866
+ else
867
+ printf 'denied %s\n' "$_cand"
868
+ fi
782
869
  else
783
870
  printf 'rival %s\n' "$_cand"
784
871
  fi
785
872
  done)
786
873
 
787
874
  other_branch=$(printf '%s\n' "$_classified" | sed -n 's/^rival //p' | head -1)
875
+ denied_branch=$(printf '%s\n' "$_classified" | sed -n 's/^denied //p' | head -1)
788
876
  absorbed_branches=$(printf '%s\n' "$_classified" | sed -n 's/^absorbed //p')
789
877
  if [ -n "$other_branch" ]; then
790
878
  conflict=1
791
879
  findings="${findings} ${RED}branch${OFF} $other_branch\n"
880
+ elif [ -n "$denied_branch" ]; then
881
+ conflict=1
882
+ findings="${findings} ${RED}branch${OFF} $denied_branch ${DIM}(content looks like a superseded predecessor, but $_guard_identity_reason)${OFF}\n"
792
883
  fi
793
884
  fi
794
885
 
795
886
  # A discount a guard grants silently is a guard nobody can audit, so say so
796
- # -- on stderr, only when it actually fired, and whether or not a real rival
797
- # was also found.
887
+ # -- on stderr, only when it actually fired. One line per branch (#1698):
888
+ # $absorbed_branches can hold more than one name, and explaining several in
889
+ # the singular sent whoever read it looking for the wrong branch.
798
890
  if [ -n "$absorbed_branches" ]; then
799
- printf '%b' "${DIM}claim --guard: discounted $(printf '%s' "$absorbed_branches" | tr '\n' ' ') ${OFF}" >&2
800
- echo "${DIM}-- every commit on it is already carried by $GUARD_BRANCH, so it is a${OFF}" >&2
801
- echo "${DIM}superseded predecessor rather than a rival claim (#1112).${OFF}" >&2
891
+ printf '%s\n' "$absorbed_branches" | while IFS= read -r _ab; do
892
+ [ -n "$_ab" ] || continue
893
+ echo "${DIM}claim --guard: discounted $_ab -- every commit on it is already carried by${OFF}" >&2
894
+ echo "${DIM}$GUARD_BRANCH, and the claim record for #$guard_issue confirms $GUARD_BRANCH, so${OFF}" >&2
895
+ echo "${DIM}it is a superseded predecessor rather than a rival claim (#1112).${OFF}" >&2
896
+ done
802
897
  fi
803
898
 
804
899
  if [ "$conflict" -eq 1 ]; then
@@ -1156,13 +1251,28 @@ gh_label create "$LABEL" \
1156
1251
  # which is why a claim could be written with nothing to identify it. That branch
1157
1252
  # is now unreachable (the requirement above exits 2 first), so the conditional
1158
1253
  # would only be a place for the old behaviour to come back.
1254
+ #
1255
+ # `claim-branch` (#1698) is the fact `--guard` reads back via
1256
+ # `claim_guard_branch` to confirm a discount is safe -- this comment is that
1257
+ # mechanism's one durable, non-worktree record, so nothing new is invented,
1258
+ # only this field added to it. AGENTS.md's own sequence puts worktree-and-
1259
+ # branch creation before this claim runs, so the branch normally already
1260
+ # exists; when it genuinely does not (detached HEAD, or `HEAD` itself, which
1261
+ # `--abbrev-ref` returns rather than a real name), the field is OMITTED
1262
+ # entirely rather than written empty -- an empty `claim-branch:` would match
1263
+ # any `case *"$BRANCH_MARK"*` probe as "present" while naming nothing, the
1264
+ # same empty-token trap `claim_held_by` already guards against for $2 (#826).
1265
+ _claim_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')
1266
+ case "$_claim_branch" in
1267
+ '' | HEAD) _claim_branch='' ;;
1268
+ esac
1159
1269
  gh_issue edit "$ISSUE" --add-label "$LABEL" >/dev/null 2>&1 || {
1160
1270
  echo "${RED}claim: could not apply the '$LABEL' label.${OFF}" >&2
1161
1271
  echo "${DIM} Not claimed. Do not start work on the assumption that it worked.${OFF}" >&2
1162
1272
  exit 2
1163
1273
  }
1164
1274
  gh_issue comment "$ISSUE" \
1165
- --body "Claimed at $(date -u +%FT%TZ) by \`$(git config user.name 2>/dev/null || echo agent)\`. ${HOLDER_MARK}${HOLDER} Release it — remove the label — on merge, or if you stop." \
1275
+ --body "Claimed at $(date -u +%FT%TZ) by \`$(git config user.name 2>/dev/null || echo agent)\`. ${HOLDER_MARK}${HOLDER}${_claim_branch:+ ${BRANCH_MARK}${_claim_branch}} Release it — remove the label — on merge, or if you stop." \
1166
1276
  >/dev/null 2>&1
1167
1277
 
1168
1278
  echo "${GREEN}Claimed.${OFF}"