1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
|
# `-l` highlight + `-R` review loop — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Highlight hint rows whose version equals the SBo `.info` version in `mkhint -l`, and add a `-R`/`--review` flag that walks each matched hint with a side-by-side diff and a `[K]eep/[D]elete/[S]kip` prompt.
**Architecture:** Two independent flags (`SHOW_LIST`, `RUN_REVIEW`) replace the single `COMMAND="list"`. `list_hint_files` gains exact-match highlighting (TTY-only color, plus a test-only `MKHINT_FORCE_COLOR` knob) and collects matched packages into a global array `MATCHED_PKGS`. A new `review_hint_files` consumes that array. Removal is factored into `_remove_hint` shared with `delete_hint_file`.
**Tech Stack:** bash, getopt, tput, git/diff, existing `tests/mkhint_test.sh` harness.
---
## File Structure
- Modify `mkhint`:
- `show_help` — document `--review`/`-R`.
- `list_hint_files` — highlight matches, populate `MATCHED_PKGS`.
- `delete_hint_file` — extract removal into `_remove_hint`.
- new `review_hint_files`.
- `main` — flag parsing + dispatch for `-l`/`-R`/`-lR`.
- Modify `mkhint.bash-completion` — add `--review -R` to `all_flags`.
- Modify `tests/mkhint_test.sh` — add T36–T41.
- Modify `CLAUDE.md` — docs.
Note on existing test IDs: T32–T35 already exist (nvchecker quoting/path). New tests are **T36–T41**.
---
## Task 1: Extract `_remove_hint` helper
**Files:**
- Modify: `mkhint` (`delete_hint_file`, ~lines 590–613)
- [ ] **Step 1: Add `_remove_hint` and call it from `delete_hint_file`**
Replace the body of `delete_hint_file` (lines 590–613) with:
```bash
# Remove a hint file and its .bak if present. No existence guard, no exit —
# safe to call inside loops. Echoes what was removed.
_remove_hint() {
local full_path="$1"
rm "$full_path"
echo "Deleted: $full_path"
local bak_path="${full_path}.bak"
if [[ -f "$bak_path" ]]; then
rm "$bak_path"
echo "Deleted: $bak_path"
fi
}
delete_hint_file() {
local file="$1"
local normalized_file="${file}"
if [[ "$file" != *.hint ]]; then
normalized_file="${file}.hint"
fi
local full_path="${HINT_DIR}/${normalized_file}"
if [[ ! -f "$full_path" ]]; then
echo "Error: Hint file does not exist: $full_path" >&2
exit 2
fi
_remove_hint "$full_path"
}
```
- [ ] **Step 2: Run existing delete tests to confirm no regression**
Run: `bash tests/mkhint_test.sh 2>&1 | grep -E "T11|T12|hint deleted|bak deleted|delete missing"`
Expected: those PASS lines present, no new FAIL.
- [ ] **Step 3: Commit**
```bash
git add mkhint
git commit -m "refactor: extract _remove_hint helper for reuse in review loop"
```
---
## Task 2: Highlight matched rows in `list_hint_files`
**Files:**
- Modify: `mkhint` (`list_hint_files`, lines 82–119)
- Test: `tests/mkhint_test.sh`
- [ ] **Step 1: Write the failing test (T36)**
Insert before the `# ─── SUMMARY` block (after T35, ~line 746):
```bash
# ── T36: -l highlights rows where hint version == SBo version ─────────────────
echo ""
echo "T36: -l highlights matching version rows, plain row for mismatch"
rm -f "$MOCK_HINT"/*.hint "$MOCK_HINT"/*.bak 2>/dev/null
# curl matches its .info (8.5.0); clion differs (hint 9.9.9 vs .info 2025.3)
cat > "$MOCK_HINT/curl.hint" << 'EOF'
VERSION="8.5.0"
ARCH="x86_64"
DOWNLOAD="https://curl.se/download/curl-8.5.0.tar.gz"
MD5SUM="abc123def456abc123def456abc123de"
EOF
cat > "$MOCK_HINT/clion.hint" << 'EOF'
VERSION="9.9.9"
ARCH="x86_64"
DOWNLOAD="UNSUPPORTED"
MD5SUM=""
EOF
# force color so the highlight is observable through the $(...) pipe
out=$(MKHINT_FORCE_COLOR=1 run_mkhint -l 2>&1)
# matched curl row carries the color start code (ESC[33m); legend present
echo "$out" | grep -q $'\033\[33m.*curl.hint' \
&& { echo " PASS: curl row highlighted"; (( PASS++ )); } \
|| { echo " FAIL: curl row not highlighted"; echo "$out" | cat -v | sed 's/^/ /'; (( FAIL++ )); ERRORS+=("T36 highlight"); }
echo "$out" | grep -q "highlighted = hint version matches" \
&& { echo " PASS: legend shown"; (( PASS++ )); } \
|| { echo " FAIL: legend missing"; (( FAIL++ )); ERRORS+=("T36 legend"); }
# clion row must NOT carry the color start code
echo "$out" | grep "clion.hint" | grep -q $'\033\[33m' \
&& { echo " FAIL: clion wrongly highlighted"; (( FAIL++ )); ERRORS+=("T36 false hl"); } \
|| { echo " PASS: clion row plain"; (( PASS++ )); }
```
- [ ] **Step 2: Run it to verify it fails**
Run: `bash tests/mkhint_test.sh 2>&1 | grep -A6 "^T36:"`
Expected: FAIL on "curl row highlighted" / "legend shown" (no color/legend yet).
- [ ] **Step 3: Implement highlighting in `list_hint_files`**
Replace lines 82–119 (whole function) with:
```bash
list_hint_files() {
if [[ ! -d "$HINT_DIR" ]]; then
echo "Error: Hint directory does not exist: $HINT_DIR" >&2
exit 2
fi
# Color only on a TTY, or when forced for tests. tput optional.
local c_on="" c_off=""
if [[ -n "$MKHINT_FORCE_COLOR" || -t 1 ]]; then
if command -v tput &>/dev/null && tput setaf 3 &>/dev/null; then
c_on=$(tput setaf 3); c_off=$(tput sgr0)
else
c_on=$'\033[33m'; c_off=$'\033[0m'
fi
fi
echo "Hint files in: $HINT_DIR"
echo "======================================================="
printf "%-40s %10s %10s %-20s %s\n" "File" "HintVer" "SBOVer" "Category" "Created"
echo "-------------------------------------------------------"
MATCHED_PKGS=()
local count=0 matched=0
for file in "$HINT_DIR"/*.hint; do
if [[ -f "$file" ]]; then
local VER=$(grep "^VERSION" "$file" |cut -d '"' -f2)
local name=$(basename "$file")
local pkg="${name%.hint}"
local info_file
info_file=$(find "$REPO_DIR" -mindepth 2 -name "${pkg}.info" 2>/dev/null | head -1)
local SBO_VER=""
local category=""
if [[ -f "$info_file" ]]; then
SBO_VER=$(grep "^VERSION" "$info_file" | cut -d '"' -f2)
category=$(basename "$(dirname "$(dirname "$info_file")")")
fi
local date=$(stat -c "%y" "$file" | cut -d'.' -f1)
local row
row=$(printf "%-40s %10s %10s %-20s %s" "$name" "$VER" "$SBO_VER" "$category" "$date")
if [[ -n "$VER" && "$VER" == "$SBO_VER" ]]; then
printf "%s%s%s\n" "$c_on" "$row" "$c_off"
MATCHED_PKGS+=("$pkg")
matched=$((matched + 1))
else
printf "%s\n" "$row"
fi
count=$((count + 1))
fi
done
if [[ $count -eq 0 ]]; then
echo " (no hint files found)"
fi
echo "======================================================="
echo "Total: $count file(s)"
if [[ $matched -gt 0 && -n "$c_on" ]]; then
echo "(highlighted = hint version matches SBo .info)"
fi
}
```
- [ ] **Step 4: Run T36 to verify it passes**
Run: `bash tests/mkhint_test.sh 2>&1 | grep -A6 "^T36:"`
Expected: all T36 lines PASS.
- [ ] **Step 5: Declare `MATCHED_PKGS` global near other state**
Find the global declarations near the top (after the path config block) and add:
```bash
MATCHED_PKGS=()
```
(Search for an existing `DELETE_HINT_FILES=()` or similar array declaration and place it alongside. If none exists at top, the in-function reset is sufficient — skip this step.)
- [ ] **Step 6: Commit**
```bash
git add mkhint tests/mkhint_test.sh
git commit -m "feat: highlight hint rows whose version matches SBo .info in -l"
```
---
## Task 3: Add `review_hint_files` and `-R`/`--review` flag
**Files:**
- Modify: `mkhint` (new function; `main` parsing/dispatch lines 752–849; `show_help`)
- Test: `tests/mkhint_test.sh`
- [ ] **Step 1: Add `review_hint_files` function**
Insert immediately after `list_hint_files` (after its closing `}`):
```bash
# Show each matched hint side-by-side with its .info, prompt Keep/Delete/Skip.
# Relies on MATCHED_PKGS populated by list_hint_files.
review_hint_files() {
if [[ ${#MATCHED_PKGS[@]} -eq 0 ]]; then
echo "No hints match their SBo version; nothing to review."
return 0
fi
local deleted=0 kept=0
local pkg
for pkg in "${MATCHED_PKGS[@]}"; do
local hint="${HINT_DIR}/${pkg}.hint"
local info
info=$(find "$REPO_DIR" -mindepth 2 -name "${pkg}.info" 2>/dev/null | head -1)
[[ -f "$hint" ]] || continue
echo ""
echo "=== $pkg ==="
if command -v git &>/dev/null; then
git diff --no-index --color=auto "$hint" "$info" || true
else
diff -y --width="${COLUMNS:-160}" "$hint" "$info" || true
fi
local ans
read -r -p "Review $pkg: [K]eep / [D]elete / [S]kip (default Keep): " ans
case "$ans" in
[Dd])
_remove_hint "$hint"
deleted=$((deleted + 1))
;;
[Ss]|[Kk]|"")
echo "Kept: $pkg"
kept=$((kept + 1))
;;
*)
echo "Unrecognised answer; keeping $pkg"
kept=$((kept + 1))
;;
esac
done
echo ""
echo "Reviewed ${#MATCHED_PKGS[@]} hint(s): deleted $deleted, kept $kept."
}
```
- [ ] **Step 2: Add the failing tests (T37–T41)**
Insert after T36 in `tests/mkhint_test.sh`:
```bash
# ── T37: -R delete a matched hint ─────────────────────────────────────────────
echo ""
echo "T37: -R answer D → matched hint and .bak removed"
rm -f "$MOCK_HINT"/*.hint "$MOCK_HINT"/*.bak 2>/dev/null
cat > "$MOCK_HINT/curl.hint" << 'EOF'
VERSION="8.5.0"
ARCH="x86_64"
DOWNLOAD="https://curl.se/download/curl-8.5.0.tar.gz"
MD5SUM="abc123def456abc123def456abc123de"
EOF
touch "$MOCK_HINT/curl.hint.bak"
out=$(run_mkhint -R < <(printf 'D\n') 2>&1)
assert_file_not_exists "curl hint deleted" "$MOCK_HINT/curl.hint"
assert_file_not_exists "curl bak deleted" "$MOCK_HINT/curl.hint.bak"
echo "$out" | grep -q "deleted 1" \
&& { echo " PASS: summary reports deleted 1"; (( PASS++ )); } \
|| { echo " FAIL: summary wrong"; echo "$out" | sed 's/^/ /'; (( FAIL++ )); ERRORS+=("T37 summary"); }
# ── T38: -R keep a matched hint ───────────────────────────────────────────────
echo ""
echo "T38: -R answer K → matched hint unchanged"
rm -f "$MOCK_HINT"/*.hint "$MOCK_HINT"/*.bak 2>/dev/null
cat > "$MOCK_HINT/curl.hint" << 'EOF'
VERSION="8.5.0"
ARCH="x86_64"
DOWNLOAD="https://curl.se/download/curl-8.5.0.tar.gz"
MD5SUM="abc123def456abc123def456abc123de"
EOF
run_mkhint -R < <(printf 'K\n') >/dev/null 2>&1
assert_file_exists "curl hint kept" "$MOCK_HINT/curl.hint"
# ── T39: -R empty answer = keep ───────────────────────────────────────────────
echo ""
echo "T39: -R empty answer → kept (default)"
run_mkhint -R < <(printf '\n') >/dev/null 2>&1
assert_file_exists "curl hint kept on empty" "$MOCK_HINT/curl.hint"
# ── T40: -R skip a matched hint ───────────────────────────────────────────────
echo ""
echo "T40: -R answer S → matched hint unchanged"
run_mkhint -R < <(printf 'S\n') >/dev/null 2>&1
assert_file_exists "curl hint kept on skip" "$MOCK_HINT/curl.hint"
# ── T41: -R with no matches → nothing to review, exit 0 ───────────────────────
echo ""
echo "T41: -R no matched rows → nothing to review"
rm -f "$MOCK_HINT"/*.hint "$MOCK_HINT"/*.bak 2>/dev/null
# clion hint version differs from its .info → no match
cat > "$MOCK_HINT/clion.hint" << 'EOF'
VERSION="9.9.9"
ARCH="x86_64"
DOWNLOAD="UNSUPPORTED"
MD5SUM=""
EOF
set +e
out=$(run_mkhint -R < <(printf '\n') 2>&1)
code=$?
set -e
assert_exit_code "no-match review exits 0" 0 "$code"
echo "$out" | grep -q "nothing to review" \
&& { echo " PASS: nothing-to-review message"; (( PASS++ )); } \
|| { echo " FAIL: message missing"; echo "$out" | sed 's/^/ /'; (( FAIL++ )); ERRORS+=("T41 msg"); }
```
- [ ] **Step 3: Run tests to verify T37–T41 fail**
Run: `bash tests/mkhint_test.sh 2>&1 | grep -A3 -E "^T3[789]:|^T4[01]:"`
Expected: FAIL — `-R` not yet a known option (getopt error / unknown option).
- [ ] **Step 4: Wire up `-R`/`--review` in `main`**
In the getopt line (line 755), add `R` to short opts and `review` to long opts:
```bash
parsed=$(getopt -o v:f:n:lcCdNhR \
--long version:,hintfile:,new:,list,clean,check,delete,no-dl,help,review \
-n 'mkhint' -- "$@") || { show_help; exit 1; }
```
Replace the `--list|-l` case (lines 774–777) with two independent toggles:
```bash
--list|-l)
SHOW_LIST=1
shift
;;
--review|-R)
RUN_REVIEW=1
shift
;;
```
- [ ] **Step 5: Add dispatch for the list/review flags**
In the `if [[ -z "$COMMAND" ]]` block (lines 816–825), the new flags are not `COMMAND`s — handle them after that block. Add, right before the `case "$COMMAND" in` (line 837):
```bash
if [[ -n "$SHOW_LIST" || -n "$RUN_REVIEW" ]]; then
[[ -n "$SHOW_LIST" ]] && list_hint_files
if [[ -n "$RUN_REVIEW" ]]; then
# ensure MATCHED_PKGS is populated even when -l was not given
[[ -z "$SHOW_LIST" ]] && list_hint_files >/dev/null
review_hint_files
fi
exit $?
fi
```
Remove the now-dead `list)` case from the `case "$COMMAND"` block (lines 841–843), since `-l` no longer sets `COMMAND`. Leave the rest of the cases intact.
- [ ] **Step 6: Initialize the flag globals**
Near the top globals (where `NO_DL=0` / `DELETE_HINT_FILES=()` live), add:
```bash
SHOW_LIST=""
RUN_REVIEW=""
```
- [ ] **Step 7: Run the new tests to verify they pass**
Run: `bash tests/mkhint_test.sh 2>&1 | grep -A3 -E "^T3[6789]:|^T4[01]:"`
Expected: all T36–T41 lines PASS.
- [ ] **Step 8: Run the full suite**
Run: `bash tests/mkhint_test.sh 2>&1 | tail -5`
Expected: `Results: N passed, 0 failed`.
- [ ] **Step 9: Document `--review` in `show_help`**
Add to the Usage block (after the `--list` line, ~line 49):
```
./mkhint --review Review hints matching SBo version, keep/delete each
```
Add to the Options block (after `--list, -l`, ~line 59):
```
--review, -R Review hints whose version matches the SBo .info; diff + keep/delete
```
- [ ] **Step 10: Commit**
```bash
git add mkhint tests/mkhint_test.sh
git commit -m "feat: add -R/--review loop with side-by-side diff and keep/delete prompt"
```
---
## Task 4: Bash completion + CLAUDE.md docs
**Files:**
- Modify: `mkhint.bash-completion` (line 11)
- Modify: `CLAUDE.md`
- [ ] **Step 1: Add `--review -R` to completion flags**
In `mkhint.bash-completion` line 11, append to `all_flags`:
```bash
local all_flags="--version -v --hintfile -f --new -n --list -l --review -R --clean -c --check -C --delete -d --no-dl -N --help -h"
```
- [ ] **Step 2: Update CLAUDE.md — Running/Testing**
Add under the run examples (after the `--list` group / near line 42):
```bash
bash mkhint --list # highlight hints whose version matches SBo .info
bash mkhint --review # review matched hints: diff + [K]eep/[D]elete/[S]kip
bash mkhint --list --review # show highlighted table, then review
```
- [ ] **Step 3: Update CLAUDE.md — test table**
Append rows to the coverage table:
```
| T36 | `-l` highlights rows where hint version == SBo version; mismatch plain |
| T37 | `-R` answer D — matched hint and .bak removed, summary reports deleted 1 |
| T38 | `-R` answer K — matched hint unchanged |
| T39 | `-R` empty answer — kept (default) |
| T40 | `-R` answer S — matched hint unchanged |
| T41 | `-R` no matched rows — "nothing to review", exit 0 |
```
- [ ] **Step 4: Update CLAUDE.md — Key Behaviors**
Add bullets:
```
- `--list` / `-l`: lists hints with `HintVer`/`SBOVer`; rows where the two are byte-equal are highlighted (color only on a TTY; plain when piped). Adds a legend when any row matched.
- `--review` / `-R`: iterates only the matched (highlighted) hints. For each, shows the hint side-by-side with its `.info` (`git diff --no-index` if git present, else `diff -y`), then prompts `[K]eep / [D]elete / [S]kip` (default Keep). Delete removes the hint and its `.bak` via `_remove_hint`. Prints a deleted/kept summary. `-l` and `-R` combine: `-lR` shows the table first, then reviews. No matches → "nothing to review", exit 0.
```
- [ ] **Step 5: Run full suite one final time**
Run: `bash tests/mkhint_test.sh 2>&1 | tail -3`
Expected: `Results: N passed, 0 failed`.
- [ ] **Step 6: Commit**
```bash
git add mkhint.bash-completion CLAUDE.md
git commit -m "docs: document -l highlight and -R review; add to bash completion"
```
---
## Self-Review Notes
- **Spec coverage:** flag table (Task 3 dispatch), exact match (Task 2 `==`), TTY-only color + non-TTY plain (Task 2 `c_on` guard), legend (Task 2), git/diff fallback (Task 3), Keep/Delete/Skip + default + re-prompt (Task 3 `case`), `_remove_hint` refactor preserving `--delete` exit-2 (Task 1), no-match exit 0 (Task 3 / T41), all docs (Task 4). All covered.
- **Type consistency:** `MATCHED_PKGS` populated in `list_hint_files`, consumed in `review_hint_files`; `SHOW_LIST`/`RUN_REVIEW` set in parsing, read in dispatch; `_remove_hint` defined Task 1, used Tasks 1 & 3.
- **Test IDs:** T36–T41 (T32–T35 already taken by nvchecker tests).
- **Note for executor:** `-t 1` is false under the `$(...)` test pipe, so T36 uses `MKHINT_FORCE_COLOR=1`. Real interactive use needs no env var.
|