@biffo/cli 0.243.1 → 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.
@@ -9,6 +9,19 @@
9
9
  # own reporting. CI jobs that push (notably the core-v* tag workflow) have no
10
10
  # `uv` on PATH, so firing here made tag pushes fail with `uv: not found` (code
11
11
  # 127) and blocked releases. GitHub Actions and every mainstream CI set $CI.
12
+ # git EXPORTS GIT_DIR (and friends) into every hook. In a worktree that points
13
+ # at `<main>/.git/worktrees/<name>` -- a real directory, where an ordinary
14
+ # checkout has `.git` as a FILE -- and turbo reads it expecting the latter,
15
+ # dying with `I/O error: Is a directory (os error 21)` before a single test
16
+ # runs. Reproduced directly: `GIT_DIR=... pnpm turbo run test` fails, the same
17
+ # command without it passes 3/3.
18
+ #
19
+ # That made the gate unpassable from any worktree, which AGENTS.md section 1
20
+ # mandates as the place all work happens. Unsetting is safe: every git command
21
+ # below discovers the repo from the working directory, which is the worktree
22
+ # root and is what they should be operating on anyway.
23
+ unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
24
+
12
25
  [ -n "${CI:-}" ] && exit 0
13
26
 
14
27
  # Escape hatch, deliberately explicit rather than a flag on git. `--no-verify`
@@ -40,6 +53,47 @@ cd "$root" || exit 0
40
53
  # cannot run, that is a reason to stop, not to wave the push through.
41
54
  sh scripts/biffo.sh rewrite-scope-check || exit 1
42
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
+
43
97
  # Through the version-pinned CLI since #1109, so a repo cannot run a gate two
44
98
  # versions old -- eight of them did, and tabsii-crm checked ONE thing in eight
45
99
  # on a 700-line change and printed `verify passed` (#855). There is one copy of
@@ -9,6 +9,19 @@
9
9
  # own reporting. CI jobs that push (notably the core-v* tag workflow) have no
10
10
  # `uv` on PATH, so firing here made tag pushes fail with `uv: not found` (code
11
11
  # 127) and blocked releases. GitHub Actions and every mainstream CI set $CI.
12
+ # git EXPORTS GIT_DIR (and friends) into every hook. In a worktree that points
13
+ # at `<main>/.git/worktrees/<name>` -- a real directory, where an ordinary
14
+ # checkout has `.git` as a FILE -- and turbo reads it expecting the latter,
15
+ # dying with `I/O error: Is a directory (os error 21)` before a single test
16
+ # runs. Reproduced directly: `GIT_DIR=... pnpm turbo run test` fails, the same
17
+ # command without it passes 3/3.
18
+ #
19
+ # That made the gate unpassable from any worktree, which AGENTS.md section 1
20
+ # mandates as the place all work happens. Unsetting is safe: every git command
21
+ # below discovers the repo from the working directory, which is the worktree
22
+ # root and is what they should be operating on anyway.
23
+ unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
24
+
12
25
  [ -n "${CI:-}" ] && exit 0
13
26
 
14
27
  # Escape hatch, deliberately explicit rather than a flag on git. `--no-verify`
@@ -40,6 +53,47 @@ cd "$root" || exit 0
40
53
  # cannot run, that is a reason to stop, not to wave the push through.
41
54
  sh scripts/biffo.sh rewrite-scope-check || exit 1
42
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
+
43
97
  # Through the version-pinned CLI since #1109, so a repo cannot run a gate two
44
98
  # versions old -- eight of them did, and tabsii-crm checked ONE thing in eight
45
99
  # on a 700-line change and printed `verify passed` (#855). There is one copy of
@@ -30,7 +30,7 @@
30
30
  "@vitejs/plugin-react": "^4.7.0",
31
31
  "eslint": "^9.18.0",
32
32
  "eslint-config-next": "^15.5.22",
33
- "jsdom": "^29.1.1",
33
+ "jsdom": "^30.0.1",
34
34
  "prettier": "^3.4.2",
35
35
  "typescript": "^5.7.3",
36
36
  "vite": "^6.4.3",
@@ -40,7 +40,8 @@
40
40
  "overrides": {
41
41
  "postcss@<8.5.18": ">=8.5.18",
42
42
  "sharp@<0.35.0": ">=0.35.0",
43
- "brace-expansion@<5.0.9": ">=5.0.9"
43
+ "brace-expansion@<5.0.9": ">=5.0.9",
44
+ "undici@<7.29.0": ">=7.29.0"
44
45
  }
45
46
  }
46
47
  }
@@ -8,6 +8,7 @@ overrides:
8
8
  postcss@<8.5.18: '>=8.5.18'
9
9
  sharp@<0.35.0: '>=0.35.0'
10
10
  brace-expansion@<5.0.9: '>=5.0.9'
11
+ undici@<7.29.0: '>=7.29.0'
11
12
 
12
13
  importers:
13
14
 
@@ -60,8 +61,8 @@ importers:
60
61
  specifier: ^15.5.22
61
62
  version: 15.5.22(eslint@9.39.4)(typescript@5.9.3)
62
63
  jsdom:
63
- specifier: ^29.1.1
64
- version: 29.1.1
64
+ specifier: ^30.0.1
65
+ version: 30.0.1
65
66
  prettier:
66
67
  specifier: ^3.4.2
67
68
  version: 3.9.4
@@ -73,27 +74,20 @@ importers:
73
74
  version: 6.4.3(@types/node@22.20.0)
74
75
  vitest:
75
76
  specifier: ^4.1.0
76
- version: 4.1.9(@types/node@22.20.0)(jsdom@29.1.1)(vite@6.4.3(@types/node@22.20.0))
77
+ version: 4.1.9(@types/node@22.20.0)(jsdom@30.0.1)(vite@6.4.3(@types/node@22.20.0))
77
78
 
78
79
  packages:
79
80
 
80
81
  '@adobe/css-tools@4.5.0':
81
82
  resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
82
83
 
83
- '@asamuzakjp/css-color@5.1.11':
84
- resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==}
85
- engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
86
-
87
- '@asamuzakjp/dom-selector@7.1.1':
88
- resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==}
89
- engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
90
-
91
- '@asamuzakjp/generational-cache@1.0.1':
92
- resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==}
93
- engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
84
+ '@asamuzakjp/css-color@6.0.5':
85
+ resolution: {integrity: sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==}
86
+ engines: {node: ^22.13.0 || >=24.0.0}
94
87
 
95
- '@asamuzakjp/nwsapi@2.3.9':
96
- resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
88
+ '@asamuzakjp/dom-selector@8.3.2':
89
+ resolution: {integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==}
90
+ engines: {node: ^22.13.0 || >=24.0.0}
97
91
 
98
92
  '@aws-crypto/sha256-js@1.2.2':
99
93
  resolution: {integrity: sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==}
@@ -223,8 +217,8 @@ packages:
223
217
  peerDependencies:
224
218
  '@csstools/css-tokenizer': ^4.0.0
225
219
 
226
- '@csstools/css-syntax-patches-for-csstree@1.1.6':
227
- resolution: {integrity: sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==}
220
+ '@csstools/css-syntax-patches-for-csstree@1.1.7':
221
+ resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==}
228
222
  peerDependencies:
229
223
  css-tree: ^3.2.1
230
224
  peerDependenciesMeta:
@@ -1875,11 +1869,11 @@ packages:
1875
1869
  resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
1876
1870
  hasBin: true
1877
1871
 
1878
- jsdom@29.1.1:
1879
- resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==}
1880
- engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
1872
+ jsdom@30.0.1:
1873
+ resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==}
1874
+ engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
1881
1875
  peerDependencies:
1882
- canvas: ^3.0.0
1876
+ canvas: ^3.2.3
1883
1877
  peerDependenciesMeta:
1884
1878
  canvas:
1885
1879
  optional: true
@@ -1936,8 +1930,8 @@ packages:
1936
1930
  resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
1937
1931
  hasBin: true
1938
1932
 
1939
- lru-cache@11.5.1:
1940
- resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==}
1933
+ lru-cache@11.5.2:
1934
+ resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
1941
1935
  engines: {node: 20 || >=22}
1942
1936
 
1943
1937
  lru-cache@5.1.1:
@@ -2395,8 +2389,8 @@ packages:
2395
2389
  resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
2396
2390
  engines: {node: '>=8.0'}
2397
2391
 
2398
- tough-cookie@6.0.1:
2399
- resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
2392
+ tough-cookie@6.0.2:
2393
+ resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==}
2400
2394
  engines: {node: '>=16'}
2401
2395
 
2402
2396
  tr46@0.0.3:
@@ -2453,9 +2447,9 @@ packages:
2453
2447
  undici-types@6.21.0:
2454
2448
  resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
2455
2449
 
2456
- undici@7.28.0:
2457
- resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
2458
- engines: {node: '>=20.18.1'}
2450
+ undici@8.10.0:
2451
+ resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==}
2452
+ engines: {node: '>=22.19.0'}
2459
2453
 
2460
2454
  unfetch@4.2.0:
2461
2455
  resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==}
@@ -2572,6 +2566,10 @@ packages:
2572
2566
  resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
2573
2567
  engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
2574
2568
 
2569
+ whatwg-url@17.1.0:
2570
+ resolution: {integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==}
2571
+ engines: {node: ^22.14.0 || >=24.0.0}
2572
+
2575
2573
  whatwg-url@5.0.0:
2576
2574
  resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
2577
2575
 
@@ -2623,25 +2621,20 @@ snapshots:
2623
2621
 
2624
2622
  '@adobe/css-tools@4.5.0': {}
2625
2623
 
2626
- '@asamuzakjp/css-color@5.1.11':
2624
+ '@asamuzakjp/css-color@6.0.5':
2627
2625
  dependencies:
2628
- '@asamuzakjp/generational-cache': 1.0.1
2629
2626
  '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
2630
2627
  '@csstools/css-color-parser': 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
2631
2628
  '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
2632
2629
  '@csstools/css-tokenizer': 4.0.0
2630
+ lru-cache: 11.5.2
2633
2631
 
2634
- '@asamuzakjp/dom-selector@7.1.1':
2632
+ '@asamuzakjp/dom-selector@8.3.2':
2635
2633
  dependencies:
2636
- '@asamuzakjp/generational-cache': 1.0.1
2637
- '@asamuzakjp/nwsapi': 2.3.9
2638
2634
  bidi-js: 1.0.3
2639
2635
  css-tree: 3.2.1
2640
2636
  is-potential-custom-element-name: 1.0.1
2641
-
2642
- '@asamuzakjp/generational-cache@1.0.1': {}
2643
-
2644
- '@asamuzakjp/nwsapi@2.3.9': {}
2637
+ lru-cache: 11.5.2
2645
2638
 
2646
2639
  '@aws-crypto/sha256-js@1.2.2':
2647
2640
  dependencies:
@@ -2800,7 +2793,7 @@ snapshots:
2800
2793
  dependencies:
2801
2794
  '@csstools/css-tokenizer': 4.0.0
2802
2795
 
2803
- '@csstools/css-syntax-patches-for-csstree@1.1.6(css-tree@3.2.1)':
2796
+ '@csstools/css-syntax-patches-for-csstree@1.1.7(css-tree@3.2.1)':
2804
2797
  optionalDependencies:
2805
2798
  css-tree: 3.2.1
2806
2799
 
@@ -4483,28 +4476,28 @@ snapshots:
4483
4476
  dependencies:
4484
4477
  argparse: 2.0.1
4485
4478
 
4486
- jsdom@29.1.1:
4479
+ jsdom@30.0.1:
4487
4480
  dependencies:
4488
- '@asamuzakjp/css-color': 5.1.11
4489
- '@asamuzakjp/dom-selector': 7.1.1
4481
+ '@asamuzakjp/css-color': 6.0.5
4482
+ '@asamuzakjp/dom-selector': 8.3.2
4490
4483
  '@bramus/specificity': 2.4.2
4491
- '@csstools/css-syntax-patches-for-csstree': 1.1.6(css-tree@3.2.1)
4484
+ '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1)
4492
4485
  '@exodus/bytes': 1.15.1
4493
4486
  css-tree: 3.2.1
4494
4487
  data-urls: 7.0.0
4495
4488
  decimal.js: 10.6.0
4496
4489
  html-encoding-sniffer: 6.0.0
4497
4490
  is-potential-custom-element-name: 1.0.1
4498
- lru-cache: 11.5.1
4491
+ lru-cache: 11.5.2
4499
4492
  parse5: 8.0.1
4500
4493
  saxes: 6.0.0
4501
4494
  symbol-tree: 3.2.4
4502
- tough-cookie: 6.0.1
4503
- undici: 7.28.0
4495
+ tough-cookie: 6.0.2
4496
+ undici: 8.10.0
4504
4497
  w3c-xmlserializer: 5.0.0
4505
4498
  webidl-conversions: 8.0.1
4506
4499
  whatwg-mimetype: 5.0.0
4507
- whatwg-url: 16.0.1
4500
+ whatwg-url: 17.1.0
4508
4501
  xml-name-validator: 5.0.0
4509
4502
  transitivePeerDependencies:
4510
4503
  - '@noble/hashes'
@@ -4555,7 +4548,7 @@ snapshots:
4555
4548
  dependencies:
4556
4549
  js-tokens: 4.0.0
4557
4550
 
4558
- lru-cache@11.5.1: {}
4551
+ lru-cache@11.5.2: {}
4559
4552
 
4560
4553
  lru-cache@5.1.1:
4561
4554
  dependencies:
@@ -5080,7 +5073,7 @@ snapshots:
5080
5073
  dependencies:
5081
5074
  is-number: 7.0.0
5082
5075
 
5083
- tough-cookie@6.0.1:
5076
+ tough-cookie@6.0.2:
5084
5077
  dependencies:
5085
5078
  tldts: 7.4.6
5086
5079
 
@@ -5153,7 +5146,7 @@ snapshots:
5153
5146
 
5154
5147
  undici-types@6.21.0: {}
5155
5148
 
5156
- undici@7.28.0: {}
5149
+ undici@8.10.0: {}
5157
5150
 
5158
5151
  unfetch@4.2.0: {}
5159
5152
 
@@ -5206,7 +5199,7 @@ snapshots:
5206
5199
  '@types/node': 22.20.0
5207
5200
  fsevents: 2.3.3
5208
5201
 
5209
- vitest@4.1.9(@types/node@22.20.0)(jsdom@29.1.1)(vite@6.4.3(@types/node@22.20.0)):
5202
+ vitest@4.1.9(@types/node@22.20.0)(jsdom@30.0.1)(vite@6.4.3(@types/node@22.20.0)):
5210
5203
  dependencies:
5211
5204
  '@vitest/expect': 4.1.9
5212
5205
  '@vitest/mocker': 4.1.9(vite@6.4.3(@types/node@22.20.0))
@@ -5230,7 +5223,7 @@ snapshots:
5230
5223
  why-is-node-running: 2.3.0
5231
5224
  optionalDependencies:
5232
5225
  '@types/node': 22.20.0
5233
- jsdom: 29.1.1
5226
+ jsdom: 30.0.1
5234
5227
  transitivePeerDependencies:
5235
5228
  - msw
5236
5229
 
@@ -5252,6 +5245,14 @@ snapshots:
5252
5245
  transitivePeerDependencies:
5253
5246
  - '@noble/hashes'
5254
5247
 
5248
+ whatwg-url@17.1.0:
5249
+ dependencies:
5250
+ '@exodus/bytes': 1.15.1
5251
+ tr46: 6.0.0
5252
+ webidl-conversions: 8.0.1
5253
+ transitivePeerDependencies:
5254
+ - '@noble/hashes'
5255
+
5255
5256
  whatwg-url@5.0.0:
5256
5257
  dependencies:
5257
5258
  tr46: 0.0.3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.243.1",
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