@biffo/cli 0.283.4 → 0.284.1

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.283.4",
3
+ "version": "0.284.1",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/claim.sh CHANGED
@@ -39,20 +39,52 @@
39
39
  #
40
40
  # ## Usage
41
41
  #
42
- # sh scripts/claim.sh 1234 # check, and claim if free
43
- # sh scripts/claim.sh 1234 --check # report only, change nothing
44
- # sh scripts/claim.sh 1234 -R owner/repo
45
- # sh scripts/claim.sh --guard <branch> # pre-push gate see below
42
+ # sh scripts/claim.sh 1234 --as <token> # check, and claim if free
43
+ # sh scripts/claim.sh 1234 --as <token> --check # report only, change nothing
44
+ # sh scripts/claim.sh 1234 --as <token> -R owner/repo
45
+ # sh scripts/claim.sh 1234 --release <token> # only the holder may clear it
46
+ # sh scripts/claim.sh --guard <branch> # pre-push gate — see below
46
47
  #
47
48
  # 0 free — and claimed, unless --check
48
49
  # 1 taken, or already closed — the reason is printed
49
- # 2 cannot tell — issue unreadable, gh unauthenticated
50
+ # 2 cannot tell — issue unreadable, gh unauthenticated, or no --as token
50
51
  #
51
52
  # 2 is deliberately not 0, matching `wait-for-checks.sh` and `branch-health.sh`.
52
53
  # A check that cannot see its input must not report "free".
53
54
  #
54
55
  # Requires `gh`, authenticated. Uses gh's embedded jq, so no jq binary needed.
55
56
  #
57
+ # ## `--as <token>` is REQUIRED on the claim and `--check` paths (#1562)
58
+ #
59
+ # It used to be optional, and `${HOLDER:+ …}` in the claim comment simply
60
+ # omitted the slug when it was absent — no flag, no slug, no warning, exit 0.
61
+ # So the safe form existed and nothing steered anyone to it: measured on
62
+ # 2026-08-13, `--as` appeared **zero** times in the AGENTS.md of every satellite
63
+ # in the estate, because the flag shipped in #1279 reached the template's own
64
+ # ruleset and neither skeleton. Two concurrent sessions in one plugin repo then
65
+ # produced four claims that read `Claimed at … by \`Keiran Holloway\`` and could
66
+ # not be told apart — ownership had to be reconstructed from a local command log,
67
+ # and for one pair could not be established at all.
68
+ #
69
+ # An optional flag that records the deciding information is a fail-open: the
70
+ # default loses exactly what the mechanism exists to keep. So a claim that
71
+ # cannot be proved to be yours is now refused rather than made.
72
+ #
73
+ # **`--guard` and `--release` are deliberately exempt.** `--guard` is invoked by
74
+ # `.githooks/pre-push` on EVERY push in every repo, with no token and no issue
75
+ # argument, and it never compares identity by design (see below) — requiring one
76
+ # there would break `git push` estate-wide to enforce a rule that path does not
77
+ # use. `--release` carries the token in its own value, so it already cannot run
78
+ # untokened.
79
+ #
80
+ # **The token must identify a session, not a role.** A mandatory field that
81
+ # everybody satisfies with `--as agent` is worse than an optional one, because it
82
+ # manufactures a column that looks authoritative and distinguishes nobody — the
83
+ # 2026-08-13 measurement above is what that looks like. So the shape is checked
84
+ # (at least two `-`-separated parts, 6+ characters), and the refusal prints a
85
+ # ready-made suggestion derived from the branch — which is already unique per
86
+ # unit of work — so the cheapest thing to type is also a good token.
87
+ #
56
88
  # ## `--guard <branch>` — the pre-push gate (#1231 instance 2)
57
89
  #
58
90
  # The four-signal check above is advisory: nothing ever runs it for you, so a
@@ -98,7 +130,12 @@ CHECK_ONLY=""
98
130
  GUARD_BRANCH=""
99
131
 
100
132
  usage() {
101
- sed -n '2,84p' "$0" | sed 's/^# \{0,1\}//'
133
+ # The whole header comment, found rather than hardcoded: the previous fixed
134
+ # `2,84p` was already truncating the last five lines of the `--guard` section
135
+ # mid-sentence, and any edit to the header silently moves where the real end
136
+ # is. Derived from `set -u`, which is always the first line of code.
137
+ _usage_end=$(grep -n '^set -u' "$0" | head -1 | cut -d: -f1)
138
+ sed -n "2,$((_usage_end - 2))p" "$0" | sed 's/^# \{0,1\}//'
102
139
  exit 2
103
140
  }
104
141
 
@@ -429,11 +466,77 @@ fi
429
466
 
430
467
  case "$ISSUE" in
431
468
  '' | *[!0-9]*)
432
- echo "claim: give an issue number, e.g. sh scripts/claim.sh 1234" >&2
469
+ echo "claim: give an issue number, e.g. sh scripts/claim.sh 1234 --as <token>" >&2
433
470
  exit 2
434
471
  ;;
435
472
  esac
436
473
 
474
+ # --- --as <token> is required from here on (#1562) ---------------------------
475
+ #
476
+ # Placed AFTER the `--guard` and `--release` short-circuits above, which is the
477
+ # whole exemption: those two paths exit before reaching this line. Everything
478
+ # below is the four-signal check and the claim it writes — the two things that
479
+ # need to know whose session is asking.
480
+ #
481
+ # A suggestion, not a lecture. An agent that hits this must be able to fix it by
482
+ # copying one line, so the refusal derives a token instead of describing one:
483
+ #
484
+ # <slug>-<MMDD>-<pid>
485
+ #
486
+ # `<slug>` comes from the current branch when it names the work, because a
487
+ # branch is already unique per unit of work (AGENTS.md: one worktree per unit).
488
+ # Claiming usually happens BEFORE the worktree exists, though — you claim from
489
+ # the primary checkout, on `dev` — so on an integration branch the slug falls
490
+ # back to the issue, and the pid keeps two sessions that both did that apart.
491
+ suggest_token() {
492
+ _b=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')
493
+ case "$_b" in
494
+ '' | HEAD | dev | main | master | staging) _slug="issue$ISSUE" ;;
495
+ */*) _slug=$(printf '%s' "${_b#*/}" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9-' '-' | cut -c1-24) ;;
496
+ *) _slug=$(printf '%s' "$_b" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9-' '-' | cut -c1-24) ;;
497
+ esac
498
+ _slug=$(printf '%s' "$_slug" | sed 's/-*$//')
499
+ printf '%s-%s-%04x' "$_slug" "$(date -u +%m%d)" "$$"
500
+ }
501
+
502
+ # Two `-`-separated parts, 6+ characters. Deliberately shape-only: the token is
503
+ # opaque by design, so there is nothing else to validate — but `agent`, `me`,
504
+ # `bot`, `session` and every other role word a whole estate would share fail it,
505
+ # and that is the failure this rule is for. See the header.
506
+ token_is_identifying() {
507
+ case "$1" in
508
+ *[!A-Za-z0-9._-]*) return 1 ;; # opaque-token characters only
509
+ -* | *-) return 1 ;; # no leading or trailing separator
510
+ *-*) [ "${#1}" -ge 6 ] ;; # two parts, long enough to be a session
511
+ *) return 1 ;; # a single word: 'agent', 'bot', 'me'
512
+ esac
513
+ }
514
+
515
+ if [ -z "$HOLDER" ]; then
516
+ _s=$(suggest_token)
517
+ echo "${RED}claim: --as <token> is required.${OFF}" >&2
518
+ echo "${DIM} A claim with no token cannot be proved to be yours: every session on this${OFF}" >&2
519
+ echo "${DIM} workstation claims under the same GitHub actor, so --release has nothing to${OFF}" >&2
520
+ echo "${DIM} check and a delegated agent cannot tell your reservation from a stranger's.${OFF}" >&2
521
+ echo >&2
522
+ echo " sh scripts/biffo.sh claim $ISSUE --as $_s" >&2
523
+ echo >&2
524
+ echo "${DIM} Give that same token to every agent you dispatch onto #$ISSUE, and release it${OFF}" >&2
525
+ echo "${DIM} with: sh scripts/biffo.sh claim $ISSUE --release $_s${OFF}" >&2
526
+ exit 2
527
+ fi
528
+
529
+ if ! token_is_identifying "$HOLDER"; then
530
+ _s=$(suggest_token)
531
+ echo "${RED}claim: --as '$HOLDER' does not identify a session.${OFF}" >&2
532
+ echo "${DIM} Needs two '-'-separated parts and 6+ characters, e.g. <what>-<MMDD>-<unique>.${OFF}" >&2
533
+ echo "${DIM} A token every session shares — 'agent', 'bot', 'me' — is worse than none:${OFF}" >&2
534
+ echo "${DIM} it fills the field that decides ownership with a value that decides nothing.${OFF}" >&2
535
+ echo >&2
536
+ echo " sh scripts/biffo.sh claim $ISSUE --as $_s" >&2
537
+ exit 2
538
+ fi
539
+
437
540
  # --- 0. Does the issue exist, and is it still open? --------------------------
438
541
 
439
542
  meta=$(gh_issue view "$ISSUE" --json state,title,labels \
@@ -578,14 +681,21 @@ gh_label create "$LABEL" \
578
681
  >/dev/null 2>&1 || true
579
682
 
580
683
  # Claim it. Label AND comment together: the label is what other sessions filter
581
- # on, the comment is what dates it so a stale claim can be recognised later.
684
+ # on, the comment is what dates it so a stale claim can be recognised later, and
685
+ # the holder token is what makes it answerable.
686
+ #
687
+ # The token is interpolated unconditionally (#1562). It used to be
688
+ # `${HOLDER:+ ${HOLDER_MARK}${HOLDER}}` — present only when `--as` was passed —
689
+ # which is why a claim could be written with nothing to identify it. That branch
690
+ # is now unreachable (the requirement above exits 2 first), so the conditional
691
+ # would only be a place for the old behaviour to come back.
582
692
  gh_issue edit "$ISSUE" --add-label "$LABEL" >/dev/null 2>&1 || {
583
693
  echo "${RED}claim: could not apply the '$LABEL' label.${OFF}" >&2
584
694
  echo "${DIM} Not claimed. Do not start work on the assumption that it worked.${OFF}" >&2
585
695
  exit 2
586
696
  }
587
697
  gh_issue comment "$ISSUE" \
588
- --body "Claimed at $(date -u +%FT%TZ) by \`$(git config user.name 2>/dev/null || echo agent)\`.${HOLDER:+ ${HOLDER_MARK}${HOLDER}} Release it — remove the label — on merge, or if you stop." \
698
+ --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." \
589
699
  >/dev/null 2>&1
590
700
 
591
701
  echo "${GREEN}Claimed.${OFF}"