@biffo/cli 0.229.0 → 0.230.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.
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Is anyone already working this issue? Ask git, not a label.
4
+ #
5
+ # ## Why this exists
6
+ #
7
+ # Several agent sessions run against this estate at once, and on 2026-08-03
8
+ # **four** of them collided in one morning:
9
+ #
10
+ # - #1165 — another session built AND MERGED a PR for it while this one was
11
+ # claiming it. Three minutes, start to merge.
12
+ # - #1174 — a live worktree on `fix/1174-…` existed; the issue was unlabelled.
13
+ # - #621, #956 — live worktrees, no labels. Labelled on their behalf.
14
+ # - #1188 — the reverse: a label with no work, while another session built it
15
+ # and opened a PR.
16
+ #
17
+ # Three of the four were "work exists, label does not". That is the shape this
18
+ # script is for.
19
+ #
20
+ # ## The rule it encodes
21
+ #
22
+ # **The `in-progress` label is a hand-maintained second copy of something git
23
+ # already knows.** A branch exists. A PR exists. Those are automatic — you
24
+ # cannot do the work without creating them — whereas the label is a separate
25
+ # action a human or agent has to remember, in a workflow that may never have
26
+ # been told to. Second copies of a decision drift; this estate says so about
27
+ # `_extract_detail`, about AGENTS.md, and about the commit-msg type list.
28
+ #
29
+ # So this checks FOUR signals and reports all of them, rather than trusting the
30
+ # one that is easiest to forget.
31
+ #
32
+ # ## What it cannot do
33
+ #
34
+ # Prevent a race. GitHub has no locking, two sessions can start in the same
35
+ # second, and #1165 went from branch to merged in three minutes — no protocol
36
+ # would have caught that. The goal is early, cheap detection, not exclusion.
37
+ # Every collision that morning was caught before duplicate work merged; the
38
+ # cost was minutes, not shipped rework.
39
+ #
40
+ # ## Usage
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
+ #
46
+ # 0 free — and claimed, unless --check
47
+ # 1 taken, or already closed — the reason is printed
48
+ # 2 cannot tell — issue unreadable, gh unauthenticated
49
+ #
50
+ # 2 is deliberately not 0, matching `wait-for-checks.sh` and `branch-health.sh`.
51
+ # A check that cannot see its input must not report "free".
52
+ #
53
+ # Requires `gh`, authenticated. Uses gh's embedded jq, so no jq binary needed.
54
+
55
+ set -u
56
+
57
+ ISSUE=""
58
+ REPO=""
59
+ CHECK_ONLY=""
60
+
61
+ usage() {
62
+ sed -n '2,58p' "$0" | sed 's/^# \{0,1\}//'
63
+ exit 2
64
+ }
65
+
66
+ while [ $# -gt 0 ]; do
67
+ case "$1" in
68
+ -R | --repo)
69
+ REPO="${2:-}"
70
+ shift 2
71
+ ;;
72
+ --check) CHECK_ONLY=1; shift ;;
73
+ -h | --help) usage ;;
74
+ *)
75
+ ISSUE="$1"
76
+ shift
77
+ ;;
78
+ esac
79
+ done
80
+
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
+ RED=$(printf '\033[31m')
89
+ GREEN=$(printf '\033[32m')
90
+ YELLOW=$(printf '\033[33m')
91
+ DIM=$(printf '\033[90m')
92
+ OFF=$(printf '\033[0m')
93
+
94
+ gh_issue() { if [ -n "$REPO" ]; then gh issue "$@" --repo "$REPO"; else gh issue "$@"; fi; }
95
+ gh_pr() { if [ -n "$REPO" ]; then gh pr "$@" --repo "$REPO"; else gh pr "$@"; fi; }
96
+
97
+ LABEL=in-progress
98
+ TAKEN=0
99
+ REASONS=""
100
+
101
+ note() { REASONS="${REASONS} $1\n"; TAKEN=1; }
102
+
103
+ # --- 0. Does the issue exist, and is it still open? --------------------------
104
+
105
+ meta=$(gh_issue view "$ISSUE" --json state,title,labels \
106
+ --jq '"\(.state)\t\(.title)\t\((.labels|map(.name)|join(",")))"' 2>/dev/null) || {
107
+ echo "${RED}claim: cannot read issue #$ISSUE${OFF} — wrong repo, or gh not authenticated." >&2
108
+ echo "${DIM} That is 'cannot tell', not 'free'.${OFF}" >&2
109
+ exit 2
110
+ }
111
+
112
+ state=$(printf '%s' "$meta" | cut -f1)
113
+ title=$(printf '%s' "$meta" | cut -f2)
114
+ labels=$(printf '%s' "$meta" | cut -f3)
115
+
116
+ echo "${DIM}#$ISSUE — $title${OFF}"
117
+ echo
118
+
119
+ if [ "$state" != "OPEN" ]; then
120
+ echo "${RED}Already $state.${OFF} Nothing to claim."
121
+ exit 1
122
+ fi
123
+
124
+ # --- 1. The label. Easiest to check, easiest to forget. ----------------------
125
+
126
+ case ",$labels," in
127
+ *",$LABEL,"*)
128
+ updated=$(gh_issue view "$ISSUE" --json updatedAt --jq .updatedAt 2>/dev/null)
129
+ note "${YELLOW}label${OFF} carries '$LABEL' (issue last updated $updated)"
130
+ ;;
131
+ esac
132
+
133
+ # --- 2. An open PR that references it ----------------------------------------
134
+ #
135
+ # The strongest signal, because a PR cannot be opened without the work existing.
136
+ # Matches the issue number in the title or body as a whole number, so #118 does
137
+ # not match #1188.
138
+
139
+ open_prs=$(gh_pr list --state open --limit 100 --json number,title,body,headRefName \
140
+ --jq "[.[] | select(((.title + \" \" + .body) | test(\"(^|[^0-9])#$ISSUE([^0-9]|\$)\")) or (.headRefName | test(\"(^|[^0-9])$ISSUE([^0-9]|\$)\")))] | .[] | \"#\(.number) \(.headRefName)\"" 2>/dev/null)
141
+
142
+ if [ -n "$open_prs" ]; then
143
+ printf '%s\n' "$open_prs" | while IFS= read -r pr; do
144
+ [ -n "$pr" ] && echo " ${RED}open PR${OFF} $pr"
145
+ done
146
+ note "${RED}open PR${OFF} see above — someone has working code"
147
+ fi
148
+
149
+ # --- 3. A remote branch naming it --------------------------------------------
150
+ #
151
+ # Catches work that has been pushed but has no PR yet. Whole-number match again.
152
+
153
+ branches=$(git ls-remote --heads "${REPO:+https://github.com/$REPO.git}" 2>/dev/null |
154
+ sed 's|.*refs/heads/||' |
155
+ grep -E "(^|[^0-9])$ISSUE([^0-9]|$)" 2>/dev/null)
156
+
157
+ if [ -n "$branches" ]; then
158
+ printf '%s\n' "$branches" | while IFS= read -r b; do
159
+ [ -n "$b" ] && echo " ${RED}branch${OFF} $b"
160
+ done
161
+ note "${RED}branch${OFF} a remote branch names this issue"
162
+ fi
163
+
164
+ # --- 4. A recently merged PR that already closed it --------------------------
165
+ #
166
+ # Not "taken" — "possibly already done". #1165 was built and merged in three
167
+ # minutes; the only trace afterwards is a merged PR.
168
+
169
+ merged=$(gh_pr list --state merged --limit 30 --json number,title,body,mergedAt \
170
+ --jq "[.[] | select((.title + \" \" + .body) | test(\"(^|[^0-9])#$ISSUE([^0-9]|\$)\"))] | .[0] | select(. != null) | \"#\(.number) merged \(.mergedAt[0:16])\"" 2>/dev/null)
171
+
172
+ if [ -n "$merged" ]; then
173
+ echo " ${YELLOW}merged${OFF} $merged"
174
+ echo " ${DIM} the issue is still open, but work referencing it has landed —${OFF}"
175
+ echo " ${DIM} read it before rebuilding.${OFF}"
176
+ fi
177
+
178
+ # --- Verdict ------------------------------------------------------------------
179
+
180
+ echo
181
+ if [ "$TAKEN" -eq 1 ]; then
182
+ printf '%b' "${RED}Taken.${OFF} Signals:\n$REASONS"
183
+ echo
184
+ echo "${DIM}If you believe it is abandoned, check how old the work is and say so in a${OFF}"
185
+ echo "${DIM}comment before taking it. Never steal a fresh claim.${OFF}"
186
+ exit 1
187
+ fi
188
+
189
+ if [ -n "$CHECK_ONLY" ]; then
190
+ echo "${GREEN}Free.${OFF} ${DIM}(--check: nothing changed)${OFF}"
191
+ exit 0
192
+ fi
193
+
194
+ # Claim it. Label AND comment together: the label is what other sessions filter
195
+ # on, the comment is what dates it so a stale claim can be recognised later.
196
+ gh_issue edit "$ISSUE" --add-label "$LABEL" >/dev/null 2>&1 || {
197
+ echo "${RED}claim: could not apply the '$LABEL' label.${OFF}" >&2
198
+ echo "${DIM} Not claimed. Do not start work on the assumption that it worked.${OFF}" >&2
199
+ exit 2
200
+ }
201
+ gh_issue comment "$ISSUE" \
202
+ --body "Claimed at $(date -u +%FT%TZ) by \`$(git config user.name 2>/dev/null || echo agent)\`. Release it — remove the label — on merge, or if you stop." \
203
+ >/dev/null 2>&1
204
+
205
+ echo "${GREEN}Claimed.${OFF}"
206
+ echo "${DIM}Push your branch as soon as it exists: a claim is a reservation, the branch${OFF}"
207
+ echo "${DIM}is the evidence, and the window between them is where collisions happen.${OFF}"
208
+ exit 0
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Is anyone already working this issue? Ask git, not a label.
4
+ #
5
+ # ## Why this exists
6
+ #
7
+ # Several agent sessions run against this estate at once, and on 2026-08-03
8
+ # **four** of them collided in one morning:
9
+ #
10
+ # - #1165 — another session built AND MERGED a PR for it while this one was
11
+ # claiming it. Three minutes, start to merge.
12
+ # - #1174 — a live worktree on `fix/1174-…` existed; the issue was unlabelled.
13
+ # - #621, #956 — live worktrees, no labels. Labelled on their behalf.
14
+ # - #1188 — the reverse: a label with no work, while another session built it
15
+ # and opened a PR.
16
+ #
17
+ # Three of the four were "work exists, label does not". That is the shape this
18
+ # script is for.
19
+ #
20
+ # ## The rule it encodes
21
+ #
22
+ # **The `in-progress` label is a hand-maintained second copy of something git
23
+ # already knows.** A branch exists. A PR exists. Those are automatic — you
24
+ # cannot do the work without creating them — whereas the label is a separate
25
+ # action a human or agent has to remember, in a workflow that may never have
26
+ # been told to. Second copies of a decision drift; this estate says so about
27
+ # `_extract_detail`, about AGENTS.md, and about the commit-msg type list.
28
+ #
29
+ # So this checks FOUR signals and reports all of them, rather than trusting the
30
+ # one that is easiest to forget.
31
+ #
32
+ # ## What it cannot do
33
+ #
34
+ # Prevent a race. GitHub has no locking, two sessions can start in the same
35
+ # second, and #1165 went from branch to merged in three minutes — no protocol
36
+ # would have caught that. The goal is early, cheap detection, not exclusion.
37
+ # Every collision that morning was caught before duplicate work merged; the
38
+ # cost was minutes, not shipped rework.
39
+ #
40
+ # ## Usage
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
+ #
46
+ # 0 free — and claimed, unless --check
47
+ # 1 taken, or already closed — the reason is printed
48
+ # 2 cannot tell — issue unreadable, gh unauthenticated
49
+ #
50
+ # 2 is deliberately not 0, matching `wait-for-checks.sh` and `branch-health.sh`.
51
+ # A check that cannot see its input must not report "free".
52
+ #
53
+ # Requires `gh`, authenticated. Uses gh's embedded jq, so no jq binary needed.
54
+
55
+ set -u
56
+
57
+ ISSUE=""
58
+ REPO=""
59
+ CHECK_ONLY=""
60
+
61
+ usage() {
62
+ sed -n '2,58p' "$0" | sed 's/^# \{0,1\}//'
63
+ exit 2
64
+ }
65
+
66
+ while [ $# -gt 0 ]; do
67
+ case "$1" in
68
+ -R | --repo)
69
+ REPO="${2:-}"
70
+ shift 2
71
+ ;;
72
+ --check) CHECK_ONLY=1; shift ;;
73
+ -h | --help) usage ;;
74
+ *)
75
+ ISSUE="$1"
76
+ shift
77
+ ;;
78
+ esac
79
+ done
80
+
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
+ RED=$(printf '\033[31m')
89
+ GREEN=$(printf '\033[32m')
90
+ YELLOW=$(printf '\033[33m')
91
+ DIM=$(printf '\033[90m')
92
+ OFF=$(printf '\033[0m')
93
+
94
+ gh_issue() { if [ -n "$REPO" ]; then gh issue "$@" --repo "$REPO"; else gh issue "$@"; fi; }
95
+ gh_pr() { if [ -n "$REPO" ]; then gh pr "$@" --repo "$REPO"; else gh pr "$@"; fi; }
96
+
97
+ LABEL=in-progress
98
+ TAKEN=0
99
+ REASONS=""
100
+
101
+ note() { REASONS="${REASONS} $1\n"; TAKEN=1; }
102
+
103
+ # --- 0. Does the issue exist, and is it still open? --------------------------
104
+
105
+ meta=$(gh_issue view "$ISSUE" --json state,title,labels \
106
+ --jq '"\(.state)\t\(.title)\t\((.labels|map(.name)|join(",")))"' 2>/dev/null) || {
107
+ echo "${RED}claim: cannot read issue #$ISSUE${OFF} — wrong repo, or gh not authenticated." >&2
108
+ echo "${DIM} That is 'cannot tell', not 'free'.${OFF}" >&2
109
+ exit 2
110
+ }
111
+
112
+ state=$(printf '%s' "$meta" | cut -f1)
113
+ title=$(printf '%s' "$meta" | cut -f2)
114
+ labels=$(printf '%s' "$meta" | cut -f3)
115
+
116
+ echo "${DIM}#$ISSUE — $title${OFF}"
117
+ echo
118
+
119
+ if [ "$state" != "OPEN" ]; then
120
+ echo "${RED}Already $state.${OFF} Nothing to claim."
121
+ exit 1
122
+ fi
123
+
124
+ # --- 1. The label. Easiest to check, easiest to forget. ----------------------
125
+
126
+ case ",$labels," in
127
+ *",$LABEL,"*)
128
+ updated=$(gh_issue view "$ISSUE" --json updatedAt --jq .updatedAt 2>/dev/null)
129
+ note "${YELLOW}label${OFF} carries '$LABEL' (issue last updated $updated)"
130
+ ;;
131
+ esac
132
+
133
+ # --- 2. An open PR that references it ----------------------------------------
134
+ #
135
+ # The strongest signal, because a PR cannot be opened without the work existing.
136
+ # Matches the issue number in the title or body as a whole number, so #118 does
137
+ # not match #1188.
138
+
139
+ open_prs=$(gh_pr list --state open --limit 100 --json number,title,body,headRefName \
140
+ --jq "[.[] | select(((.title + \" \" + .body) | test(\"(^|[^0-9])#$ISSUE([^0-9]|\$)\")) or (.headRefName | test(\"(^|[^0-9])$ISSUE([^0-9]|\$)\")))] | .[] | \"#\(.number) \(.headRefName)\"" 2>/dev/null)
141
+
142
+ if [ -n "$open_prs" ]; then
143
+ printf '%s\n' "$open_prs" | while IFS= read -r pr; do
144
+ [ -n "$pr" ] && echo " ${RED}open PR${OFF} $pr"
145
+ done
146
+ note "${RED}open PR${OFF} see above — someone has working code"
147
+ fi
148
+
149
+ # --- 3. A remote branch naming it --------------------------------------------
150
+ #
151
+ # Catches work that has been pushed but has no PR yet. Whole-number match again.
152
+
153
+ branches=$(git ls-remote --heads "${REPO:+https://github.com/$REPO.git}" 2>/dev/null |
154
+ sed 's|.*refs/heads/||' |
155
+ grep -E "(^|[^0-9])$ISSUE([^0-9]|$)" 2>/dev/null)
156
+
157
+ if [ -n "$branches" ]; then
158
+ printf '%s\n' "$branches" | while IFS= read -r b; do
159
+ [ -n "$b" ] && echo " ${RED}branch${OFF} $b"
160
+ done
161
+ note "${RED}branch${OFF} a remote branch names this issue"
162
+ fi
163
+
164
+ # --- 4. A recently merged PR that already closed it --------------------------
165
+ #
166
+ # Not "taken" — "possibly already done". #1165 was built and merged in three
167
+ # minutes; the only trace afterwards is a merged PR.
168
+
169
+ merged=$(gh_pr list --state merged --limit 30 --json number,title,body,mergedAt \
170
+ --jq "[.[] | select((.title + \" \" + .body) | test(\"(^|[^0-9])#$ISSUE([^0-9]|\$)\"))] | .[0] | select(. != null) | \"#\(.number) merged \(.mergedAt[0:16])\"" 2>/dev/null)
171
+
172
+ if [ -n "$merged" ]; then
173
+ echo " ${YELLOW}merged${OFF} $merged"
174
+ echo " ${DIM} the issue is still open, but work referencing it has landed —${OFF}"
175
+ echo " ${DIM} read it before rebuilding.${OFF}"
176
+ fi
177
+
178
+ # --- Verdict ------------------------------------------------------------------
179
+
180
+ echo
181
+ if [ "$TAKEN" -eq 1 ]; then
182
+ printf '%b' "${RED}Taken.${OFF} Signals:\n$REASONS"
183
+ echo
184
+ echo "${DIM}If you believe it is abandoned, check how old the work is and say so in a${OFF}"
185
+ echo "${DIM}comment before taking it. Never steal a fresh claim.${OFF}"
186
+ exit 1
187
+ fi
188
+
189
+ if [ -n "$CHECK_ONLY" ]; then
190
+ echo "${GREEN}Free.${OFF} ${DIM}(--check: nothing changed)${OFF}"
191
+ exit 0
192
+ fi
193
+
194
+ # Claim it. Label AND comment together: the label is what other sessions filter
195
+ # on, the comment is what dates it so a stale claim can be recognised later.
196
+ gh_issue edit "$ISSUE" --add-label "$LABEL" >/dev/null 2>&1 || {
197
+ echo "${RED}claim: could not apply the '$LABEL' label.${OFF}" >&2
198
+ echo "${DIM} Not claimed. Do not start work on the assumption that it worked.${OFF}" >&2
199
+ exit 2
200
+ }
201
+ gh_issue comment "$ISSUE" \
202
+ --body "Claimed at $(date -u +%FT%TZ) by \`$(git config user.name 2>/dev/null || echo agent)\`. Release it — remove the label — on merge, or if you stop." \
203
+ >/dev/null 2>&1
204
+
205
+ echo "${GREEN}Claimed.${OFF}"
206
+ echo "${DIM}Push your branch as soon as it exists: a claim is a reservation, the branch${OFF}"
207
+ echo "${DIM}is the evidence, and the window between them is where collisions happen.${OFF}"
208
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.229.0",
3
+ "version": "0.230.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",