nobrainer-tech-flow 2.4.0 → 2.5.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/README.md +9 -6
- package/assets/nobrainer-tech-logo.svg +1 -1
- package/assets/plugin-logo.png +0 -0
- package/docs/INSTALL.md +9 -6
- package/docs/MIGRATION_TO_FLOW.md +14 -0
- package/docs/RELEASING.md +24 -0
- package/docs/SKILL_CURATION.md +8 -2
- package/docs/releases/v2.4.1.md +63 -0
- package/docs/releases/v2.5.0.md +190 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install.py +13 -6
- package/scripts/install.sh +137 -16
- package/scripts/install_skills.py +34 -5
- package/scripts/recommend_flow_setup.py +126 -21
- package/scripts/validate_skills.py +2 -1
- package/skills/nobrainer-context/SKILL.md +113 -0
- package/skills/nobrainer-context/references/clients.md +115 -0
- package/skills/nobrainer-context/references/consolidate.md +66 -0
- package/skills/nobrainer-context/scripts/context_inventory.py +425 -0
- package/skills/nobrainer-skill-doctor/SKILL.md +4 -4
- package/skills/nobrainer-tech-flow/VERSION +1 -1
- package/skills/nobrainer-tech-flow/references/optional-decisions.md +5 -3
- package/skills/nobrainer-tech-flow/references/routing.md +1 -1
- package/skills/nobrainer-tech-flow/scripts/typed_decisions.py +20 -9
- package/skills/nobrainer-codex-context/SKILL.md +0 -111
- /package/skills/{nobrainer-codex-context → nobrainer-context}/scripts/codex_context.py +0 -0
package/scripts/install.sh
CHANGED
|
@@ -19,7 +19,7 @@ unset CDPATH
|
|
|
19
19
|
# The eighteen skills, and the names earlier layouts used. Tests keep these lists
|
|
20
20
|
# equal to the ones in scripts/install_skills.py.
|
|
21
21
|
SKILLS='nobrainer-auto-fine-tune nobrainer-autoimprove nobrainer-browser nobrainer-build
|
|
22
|
-
nobrainer-
|
|
22
|
+
nobrainer-context nobrainer-decide nobrainer-dispatcher nobrainer-rca
|
|
23
23
|
nobrainer-research nobrainer-review nobrainer-security nobrainer-sessions
|
|
24
24
|
nobrainer-skill-doctor nobrainer-spec-driven-development nobrainer-team
|
|
25
25
|
nobrainer-tech-flow nobrainer-wiki nobrainer-writing'
|
|
@@ -28,13 +28,13 @@ deep-audit deep-autoresearch deep-autoreview deep-bugs-finder deep-code-review
|
|
|
28
28
|
deep-decide deep-rca dispatching-parallel-agents engineering-standards
|
|
29
29
|
karpathy-auto-improver karpathy-llm-wiki llm-wiki nb-add nb-dispatcher nb-flow nb-get
|
|
30
30
|
nb-multi nb-tidy nb-workflow nb-write nobrainer-autopilot nobrainer-capture-lesson
|
|
31
|
-
nobrainer-continuous-improvement nobrainer-fast-audit nobrainer-human-like
|
|
31
|
+
nobrainer-codex-context nobrainer-continuous-improvement nobrainer-fast-audit nobrainer-human-like
|
|
32
32
|
nobrainer-memory nobrainer-memory-memsearch nobrainer-npm-secure nobrainer-reddit
|
|
33
33
|
nobrainer-simplifier nobrainer-skill-browser nobrainer-starter nobrainer-style
|
|
34
34
|
nobrainer-team-builder nobrainer-ultra nobrainer-ultracode-workflow nobrainer-wiki-add
|
|
35
35
|
nobrainer-wiki-get nobrainer-wiki-tidy playwright-cli security-review session-handoff
|
|
36
36
|
wiki-add wiki-get wiki-tidy'
|
|
37
|
-
RETIRED='nobrainer-reddit'
|
|
37
|
+
RETIRED='nobrainer-codex-context nobrainer-reddit'
|
|
38
38
|
START='<!-- NOBRAINER-TECH-FLOW:START -->'
|
|
39
39
|
END='<!-- NOBRAINER-TECH-FLOW:END -->'
|
|
40
40
|
LEGACY_TEXT='(^|[^[:alnum:]_])nobrainer-ultra([^[:alnum:]_]|$)|^[[:space:]]*#{1,6}[[:space:]]+nobrainer([ .]?tech)?[[:space:]]+flow([^[:alnum:]_]|$)'
|
|
@@ -169,7 +169,9 @@ physical() {
|
|
|
169
169
|
return
|
|
170
170
|
fi
|
|
171
171
|
done
|
|
172
|
-
|
|
172
|
+
# The slash keeps a trailing line break of a folder name across the substitution.
|
|
173
|
+
_p_head=$(cd -P "$_p_head" 2>/dev/null && printf '%s/' "$PWD") || _p_head=$1/ _p_tail=
|
|
174
|
+
_p_head=${_p_head%/}
|
|
173
175
|
printf '%s%s\n' "${_p_head%/}" "$_p_tail"
|
|
174
176
|
}
|
|
175
177
|
|
|
@@ -283,6 +285,38 @@ is_our_link() {
|
|
|
283
285
|
[ "$_o_resolved" = "$2" ]
|
|
284
286
|
}
|
|
285
287
|
|
|
288
|
+
# Put a claimed entry ($1) back at its name ($2) unless something new stands there; the
|
|
289
|
+
# private folder it was moved into goes too. A blocked restore keeps the claim and says where.
|
|
290
|
+
unclaim() {
|
|
291
|
+
# Restore through the same native no-replace primitive as the Python installer.
|
|
292
|
+
# Normal shell installs need no Python; without it, a recovery claim stays intact.
|
|
293
|
+
if command -v python3 >/dev/null 2>&1 && python3 - "$root/scripts" "$1" "$2" <<'PYTHON'
|
|
294
|
+
import sys
|
|
295
|
+
from pathlib import Path
|
|
296
|
+
sys.path.insert(0, sys.argv[1])
|
|
297
|
+
from install_skills import restore_claim
|
|
298
|
+
try:
|
|
299
|
+
restore_claim(Path(sys.argv[3]), Path(sys.argv[2]))
|
|
300
|
+
except (OSError, RuntimeError) as exc:
|
|
301
|
+
print(exc, file=sys.stderr)
|
|
302
|
+
sys.exit(1)
|
|
303
|
+
PYTHON
|
|
304
|
+
then
|
|
305
|
+
return 0
|
|
306
|
+
fi
|
|
307
|
+
err "PRESERVED: atomic restore unavailable or blocked; could not put $2 back; it is kept at $1"
|
|
308
|
+
return 1
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# A recorded skill link this checkout made: $1 is the link, $2 the skill name. The link of
|
|
312
|
+
# a retired module dangles, so it is compared as written.
|
|
313
|
+
owns_link() {
|
|
314
|
+
case " $RETIRED " in
|
|
315
|
+
*" $2 "*) is_exact_legacy_link "$1" "$2" ;;
|
|
316
|
+
*) is_our_link "$1" "$root/skills/$2" ;;
|
|
317
|
+
esac
|
|
318
|
+
}
|
|
319
|
+
|
|
286
320
|
# Resolve links before "..", including dangling final links. A trailing slash
|
|
287
321
|
# preserves a pathname's trailing newlines across command substitution.
|
|
288
322
|
resolve_legacy_path() (
|
|
@@ -408,7 +442,8 @@ read_state() {
|
|
|
408
442
|
' ]' | ' ],') _r_in= ;;
|
|
409
443
|
*)
|
|
410
444
|
json_field "\": ${_r_line# }" || return 1
|
|
411
|
-
|
|
445
|
+
# A record from an earlier release may hold a module since retired.
|
|
446
|
+
case " $(printf '%s' "$SKILLS $RETIRED" | tr '\n' ' ') " in *" $json_value "*) ;; *) return 1 ;; esac
|
|
412
447
|
case " $st_created " in *" $json_value "*) return 1 ;; esac
|
|
413
448
|
st_created="$st_created $json_value" ;;
|
|
414
449
|
esac
|
|
@@ -543,7 +578,9 @@ under_link() {
|
|
|
543
578
|
# The folders a record lists must be ones the setup can have made: each an absolute,
|
|
544
579
|
# plain path listed once, that is, or lies above, the skills folder, the folder of the
|
|
545
580
|
# instruction file or the folder of the record. Anything else is a damaged record that an undo must not act on. A path
|
|
546
|
-
# may be spelled with or without the links on the way, so both spellings count.
|
|
581
|
+
# may be spelled with or without the links on the way, so both spellings count. Two entries
|
|
582
|
+
# may still lead to one folder: a link put in place of one of them since the setup does that.
|
|
583
|
+
# The undo removes no folder of such a group.
|
|
547
584
|
above() {
|
|
548
585
|
case "$2/" in "$1"/*) return 0 ;; esac
|
|
549
586
|
return 1
|
|
@@ -591,9 +628,9 @@ check_dirs() {
|
|
|
591
628
|
fi
|
|
592
629
|
[ -n "$_k_ok" ] || exit 1
|
|
593
630
|
case $_k_seen in *"
|
|
594
|
-
$
|
|
631
|
+
$_k_dir
|
|
595
632
|
"*) exit 1 ;; esac
|
|
596
|
-
_k_seen="$_k_seen$
|
|
633
|
+
_k_seen="$_k_seen$_k_dir
|
|
597
634
|
"
|
|
598
635
|
done
|
|
599
636
|
}
|
|
@@ -677,7 +714,7 @@ check_record() {
|
|
|
677
714
|
}
|
|
678
715
|
|
|
679
716
|
plan_skills() {
|
|
680
|
-
to_link= link_count=0 kept_count=0 problems=
|
|
717
|
+
to_link= link_count=0 kept_count=0 problems= to_retire=
|
|
681
718
|
for _p_name in $SKILLS; do
|
|
682
719
|
_p_source=$root/skills/$_p_name
|
|
683
720
|
[ -f "$_p_source/SKILL.md" ] || die 2 "this checkout is incomplete: $_p_source/SKILL.md is missing"
|
|
@@ -704,7 +741,13 @@ CONFLICT: $_p_name: $_p_source -> $_p_target"
|
|
|
704
741
|
if ! is_exact_legacy_link "$_p_target" "$_p_name"; then
|
|
705
742
|
err "PRESERVED_FOREIGN: $_p_name: retired from nobrainer-tech-flow; left unchanged: $_p_target"
|
|
706
743
|
continue
|
|
707
|
-
fi
|
|
744
|
+
fi
|
|
745
|
+
# This setup's record lists the link, as an undo would remove it.
|
|
746
|
+
case " $st_created " in
|
|
747
|
+
*" $_p_name "*)
|
|
748
|
+
to_retire="$to_retire $_p_name"
|
|
749
|
+
continue ;;
|
|
750
|
+
esac ;;
|
|
708
751
|
esac
|
|
709
752
|
problems="$problems
|
|
710
753
|
LEGACY: $_p_name: $_p_target"
|
|
@@ -815,6 +858,9 @@ variable_note() {
|
|
|
815
858
|
preview() {
|
|
816
859
|
say "$source_line for $label"
|
|
817
860
|
say "SKILLS: $link_count to link into $skills_dir ($kept_count already linked)"
|
|
861
|
+
for _v_name in $to_retire; do
|
|
862
|
+
say "REMOVE_RETIRED: $_v_name: made by this setup, now renamed: $skills_dir/$_v_name"
|
|
863
|
+
done
|
|
818
864
|
if [ "$inherit" = 1 ]; then
|
|
819
865
|
say "INHERITS_CODEX: $target imports $codex_file; no duplicate block added"
|
|
820
866
|
else
|
|
@@ -910,6 +956,10 @@ write_instructions() {
|
|
|
910
956
|
}
|
|
911
957
|
|
|
912
958
|
install() {
|
|
959
|
+
# plan_skills needs the retired links the record lists; check_record judges the record
|
|
960
|
+
# after the skills, as before, so a record that cannot be read only lists none here.
|
|
961
|
+
st_created=
|
|
962
|
+
if [ -f "$state" ] && [ ! -L "$state" ]; then read_state "$state" || st_created=; fi
|
|
913
963
|
plan_skills
|
|
914
964
|
plan_instructions
|
|
915
965
|
check_record
|
|
@@ -922,6 +972,16 @@ install() {
|
|
|
922
972
|
sha256 /dev/null >/dev/null || die 2 "this script needs sha256sum, shasum or openssl to record the setup"
|
|
923
973
|
|
|
924
974
|
created= dirs_to_make=
|
|
975
|
+
# A folder this run makes is recorded as it really is, links resolved. A line break or a
|
|
976
|
+
# control character in that form would leave a record no undo can read, so such a run
|
|
977
|
+
# stops before anything is written.
|
|
978
|
+
note_dirs "$home"
|
|
979
|
+
if [ "$link_count" -gt 0 ]; then note_dirs "$skills_dir"; fi
|
|
980
|
+
if [ "$file_status" != present ] && [ "$inherit" = 0 ]; then note_dirs "${target%/*}"; fi
|
|
981
|
+
printf '%s' "$dirs_to_make" | while IFS= read -r _a_dir; do
|
|
982
|
+
reject_control "$(physical "$_a_dir")" "the real path of $_a_dir"
|
|
983
|
+
done || exit 2
|
|
984
|
+
dirs_to_make=
|
|
925
985
|
# The record's folder is made before anything else is written: when it cannot be made,
|
|
926
986
|
# the run stops here instead of leaving links and a block that no record describes. It
|
|
927
987
|
# counts among the folders the setup made: a home that does not exist yet is made for
|
|
@@ -972,8 +1032,43 @@ install() {
|
|
|
972
1032
|
elif [ "$wrote" = 1 ]; then
|
|
973
1033
|
new_target=$target new_backup=$made_backup new_hash=$(sha256 "$target")
|
|
974
1034
|
fi
|
|
975
|
-
|
|
976
|
-
|
|
1035
|
+
# The retired links the record lists go last, after every other write. Each is moved into
|
|
1036
|
+
# a private folder and checked there; one that changed after the check goes back as it is.
|
|
1037
|
+
# They come back if the record cannot be saved, and are deleted only once it is.
|
|
1038
|
+
retired_claims= retirement_error= retirement_restore_error=
|
|
1039
|
+
for _a_name in $to_retire; do
|
|
1040
|
+
_a_link=$skills_dir/$_a_name
|
|
1041
|
+
is_exact_legacy_link "$_a_link" "$_a_name" || continue
|
|
1042
|
+
_a_text=$(readlink "$_a_link") || continue
|
|
1043
|
+
if ! _a_claim=$(mktemp -d "$skills_dir/.nobrainer-retire-XXXXXX"); then
|
|
1044
|
+
retirement_error=1
|
|
1045
|
+
break
|
|
1046
|
+
fi
|
|
1047
|
+
if ! mv "$_a_link" "$_a_claim/$_a_name" 2>/dev/null; then
|
|
1048
|
+
rmdir "$_a_claim"
|
|
1049
|
+
continue
|
|
1050
|
+
fi
|
|
1051
|
+
if [ -L "$_a_claim/$_a_name" ] && [ "$(readlink "$_a_claim/$_a_name")" = "$_a_text" ]; then
|
|
1052
|
+
retired_claims="$retired_claims$_a_claim/$_a_name
|
|
1053
|
+
"
|
|
1054
|
+
else
|
|
1055
|
+
if ! unclaim "$_a_claim/$_a_name" "$_a_link"; then
|
|
1056
|
+
retirement_error=1 retirement_restore_error=1
|
|
1057
|
+
break
|
|
1058
|
+
fi
|
|
1059
|
+
err "PRESERVED: $_a_link changed during the setup and is left as it is"
|
|
1060
|
+
fi
|
|
1061
|
+
done
|
|
1062
|
+
if [ -n "$created" ] || [ -n "$to_retire" ] || [ "$wrote" = 1 ] || { [ "$record" = 1 ] && [ "$new_hash" != "$st_hash" ]; }; then
|
|
1063
|
+
# A retired module's link is gone once migrated; then the record drops its name.
|
|
1064
|
+
_k_created=
|
|
1065
|
+
for _k_name in $st_created; do
|
|
1066
|
+
case " $RETIRED " in
|
|
1067
|
+
*" $_k_name "*) owns_link "$skills_dir/$_k_name" "$_k_name" || continue ;;
|
|
1068
|
+
esac
|
|
1069
|
+
_k_created="$_k_created $_k_name"
|
|
1070
|
+
done
|
|
1071
|
+
new_created=$(printf '%s\n' $_k_created $created | LC_ALL=C sort -u | tr '\n' ' ')
|
|
977
1072
|
new_created=${new_created# }
|
|
978
1073
|
new_created=${new_created% }
|
|
979
1074
|
# Of the folders this run meant to create, the ones that exist now, with the ones an
|
|
@@ -997,7 +1092,7 @@ install() {
|
|
|
997
1092
|
# makes a folder, and its undo keeps the rule it was made under.
|
|
998
1093
|
want_dirs=1
|
|
999
1094
|
if [ "$record" = 1 ] && [ -z "$st_has_dirs" ] && [ -z "$new_dirs" ]; then want_dirs=; fi
|
|
1000
|
-
if ! write_state; then
|
|
1095
|
+
if [ -n "$retirement_error" ] || ! write_state; then
|
|
1001
1096
|
for _a_done in $created; do remove_link "$skills_dir/$_a_done"; done
|
|
1002
1097
|
if [ "$wrote" = 1 ]; then
|
|
1003
1098
|
# Through a copy beside it and a rename, so a read-only file comes back as well.
|
|
@@ -1008,9 +1103,21 @@ install() {
|
|
|
1008
1103
|
rm -f "$target"
|
|
1009
1104
|
fi
|
|
1010
1105
|
fi
|
|
1106
|
+
if ! printf '%s' "$retired_claims" | while IFS= read -r _a_claim; do
|
|
1107
|
+
unclaim "$_a_claim" "$skills_dir/${_a_claim##*/}" || exit 1
|
|
1108
|
+
done; then
|
|
1109
|
+
die 4 "could not save the setup record $state; rollback incomplete; recovery claims were preserved"
|
|
1110
|
+
fi
|
|
1111
|
+
if [ -n "$retirement_restore_error" ]; then
|
|
1112
|
+
die 4 "retired link restore failed; rollback incomplete; recovery claims were preserved"
|
|
1113
|
+
fi
|
|
1011
1114
|
die 4 "could not save the setup record $state; the changes of this run were reversed"
|
|
1012
1115
|
fi
|
|
1013
1116
|
fi
|
|
1117
|
+
printf '%s' "$retired_claims" | while IFS= read -r _a_claim; do
|
|
1118
|
+
remove_link "$_a_claim" && rmdir "${_a_claim%/*}" 2>/dev/null
|
|
1119
|
+
say "REMOVED_RETIRED: $skills_dir/${_a_claim##*/}"
|
|
1120
|
+
done
|
|
1014
1121
|
|
|
1015
1122
|
# A home made for a record that was not needed goes again.
|
|
1016
1123
|
if [ ! -e "$state" ] && [ -n "$made_home" ]; then
|
|
@@ -1019,7 +1126,7 @@ install() {
|
|
|
1019
1126
|
|
|
1020
1127
|
# New links and a written instruction file are what a client has to be restarted for.
|
|
1021
1128
|
changed=0
|
|
1022
|
-
if [ "$link_count" -gt 0 ] || [ "$wrote" = 1 ]; then changed=1; fi
|
|
1129
|
+
if [ "$link_count" -gt 0 ] || [ "$wrote" = 1 ] || [ -n "$retired_claims" ]; then changed=1; fi
|
|
1023
1130
|
if [ "$changed" = 1 ]; then
|
|
1024
1131
|
say "INSTALLED: $source_line for $label"
|
|
1025
1132
|
else
|
|
@@ -1201,7 +1308,7 @@ undo() {
|
|
|
1201
1308
|
for _u_name in $st_created; do
|
|
1202
1309
|
_u_link=$st_dest/$_u_name
|
|
1203
1310
|
if [ -L "$_u_link" ]; then
|
|
1204
|
-
if !
|
|
1311
|
+
if ! owns_link "$_u_link" "$_u_name"; then
|
|
1205
1312
|
say "PRESERVED: changed target $_u_link"
|
|
1206
1313
|
exit 3
|
|
1207
1314
|
fi
|
|
@@ -1238,7 +1345,7 @@ undo() {
|
|
|
1238
1345
|
_u_removed=0
|
|
1239
1346
|
for _u_name in $st_created; do
|
|
1240
1347
|
_u_link=$st_dest/$_u_name
|
|
1241
|
-
if
|
|
1348
|
+
if owns_link "$_u_link" "$_u_name"; then
|
|
1242
1349
|
remove_link "$_u_link" || die 4 "could not remove $_u_link"
|
|
1243
1350
|
_u_removed=$((_u_removed + 1))
|
|
1244
1351
|
fi
|
|
@@ -1283,9 +1390,23 @@ undo() {
|
|
|
1283
1390
|
rm -f "$state"
|
|
1284
1391
|
# The record is gone, so the folder it sat in may be empty now. A folder that was there
|
|
1285
1392
|
# before the setup is not listed, and a folder that holds anything stays.
|
|
1393
|
+
# Two entries that lead to one folder cannot both be what the setup made, and the record
|
|
1394
|
+
# cannot say which one is, so no entry of such a group is removed. The groups are found
|
|
1395
|
+
# before any folder goes.
|
|
1286
1396
|
_u_gone=
|
|
1287
1397
|
if [ -n "$st_dirs" ]; then
|
|
1398
|
+
_u_dupes=$(printf '%s' "$st_dirs" | while IFS= read -r _u_dir; do physical "$_u_dir"; done | LC_ALL=C sort | uniq -d)
|
|
1399
|
+
_u_shared=
|
|
1400
|
+
if [ -n "$_u_dupes" ]; then
|
|
1401
|
+
_u_shared=$(printf '%s' "$st_dirs" | while IFS= read -r _u_dir; do
|
|
1402
|
+
if printf '%s\n' "$_u_dupes" | grep -q -x -F -e "$(physical "$_u_dir")"; then printf '%s\n' "$_u_dir"; fi
|
|
1403
|
+
done)
|
|
1404
|
+
fi
|
|
1288
1405
|
_u_gone=$(printf '%s' "$st_dirs" | LC_ALL=C sort -r | while IFS= read -r _u_dir; do
|
|
1406
|
+
if [ -n "$_u_shared" ] && printf '%s\n' "$_u_shared" | grep -q -x -F -e "$_u_dir"; then
|
|
1407
|
+
printf 'PRESERVED: %s: another folder in the record leads to the same folder, so it is left as it is\n' "$_u_dir"
|
|
1408
|
+
continue
|
|
1409
|
+
fi
|
|
1289
1410
|
if [ ! -d "$_u_dir" ] || [ -L "$_u_dir" ]; then continue; fi
|
|
1290
1411
|
if under_link "$_u_dir"; then
|
|
1291
1412
|
printf 'PRESERVED: %s: a folder above it is a link now, so it is left as it is\n' "$_u_dir"
|
|
@@ -73,7 +73,7 @@ except ValueError:
|
|
|
73
73
|
|
|
74
74
|
CURATED_SKILLS = frozenset(
|
|
75
75
|
{
|
|
76
|
-
"nobrainer-
|
|
76
|
+
"nobrainer-context",
|
|
77
77
|
"nobrainer-skill-doctor",
|
|
78
78
|
"nobrainer-autoimprove",
|
|
79
79
|
"nobrainer-auto-fine-tune",
|
|
@@ -104,6 +104,7 @@ LEGACY_TO_CANONICAL = {
|
|
|
104
104
|
"agents-restraint": "nobrainer-tech-flow",
|
|
105
105
|
"codex-in-claude-code": "nobrainer-tech-flow",
|
|
106
106
|
"code-autoresearch": "nobrainer-autoimprove",
|
|
107
|
+
"nobrainer-codex-context": "nobrainer-context",
|
|
107
108
|
"deep-audit": "nobrainer-review",
|
|
108
109
|
"deep-autoreview": "nobrainer-review",
|
|
109
110
|
"deep-autoresearch": "nobrainer-autoimprove",
|
|
@@ -149,9 +150,11 @@ LEGACY_TO_CANONICAL = {
|
|
|
149
150
|
"wiki-get": "nobrainer-wiki",
|
|
150
151
|
"wiki-tidy": "nobrainer-wiki",
|
|
151
152
|
}
|
|
152
|
-
# Reddit integration was removed,
|
|
153
|
-
#
|
|
154
|
-
|
|
153
|
+
# Names Flow no longer ships: Reddit integration was removed, and
|
|
154
|
+
# nobrainer-codex-context was renamed to nobrainer-context in 2.5.0 without an
|
|
155
|
+
# alias. Keep their owned-link migration, but another source may legitimately
|
|
156
|
+
# use these names, so a foreign target there is preserved, never removed.
|
|
157
|
+
RETIRED_LEGACY = frozenset({"nobrainer-codex-context", "nobrainer-reddit"})
|
|
155
158
|
UNMAPPED_LEGACY = frozenset({"nobrainer-fast-audit"})
|
|
156
159
|
|
|
157
160
|
|
|
@@ -455,6 +458,10 @@ def legacy_name_for(target: Path, canonical: Path) -> str | None:
|
|
|
455
458
|
return None
|
|
456
459
|
|
|
457
460
|
|
|
461
|
+
class ClaimRestoreError(RuntimeError):
|
|
462
|
+
"""A changed entry was claimed but could not be restored to its original name."""
|
|
463
|
+
|
|
464
|
+
|
|
458
465
|
def claim_legacy_link(
|
|
459
466
|
target: Path, legacy_name: str
|
|
460
467
|
) -> tuple[Path, EntryFingerprint]:
|
|
@@ -484,7 +491,7 @@ def claim_legacy_link(
|
|
|
484
491
|
try:
|
|
485
492
|
restore_claim(target, claim)
|
|
486
493
|
except RuntimeError as exc:
|
|
487
|
-
raise
|
|
494
|
+
raise ClaimRestoreError(
|
|
488
495
|
f"legacy link changed after preflight; {exc}"
|
|
489
496
|
) from exc
|
|
490
497
|
raise RuntimeError(
|
|
@@ -683,6 +690,17 @@ def parse_args() -> argparse.Namespace:
|
|
|
683
690
|
"root-level layout and renamed aliases."
|
|
684
691
|
),
|
|
685
692
|
)
|
|
693
|
+
parser.add_argument(
|
|
694
|
+
"--recorded-retired",
|
|
695
|
+
action="append",
|
|
696
|
+
default=[],
|
|
697
|
+
metavar="NAME",
|
|
698
|
+
help=(
|
|
699
|
+
"A retired name that the calling setup's record lists as created by it: "
|
|
700
|
+
"its exact link into this checkout no longer blocks installation and is "
|
|
701
|
+
"left in place for that setup to retire once its whole run has succeeded."
|
|
702
|
+
),
|
|
703
|
+
)
|
|
686
704
|
return parser.parse_args()
|
|
687
705
|
|
|
688
706
|
|
|
@@ -748,6 +766,14 @@ def main() -> int:
|
|
|
748
766
|
if legacy_name in RETIRED_LEGACY
|
|
749
767
|
else "conflict"
|
|
750
768
|
)
|
|
769
|
+
if (
|
|
770
|
+
state == "legacy"
|
|
771
|
+
and legacy_name in RETIRED_LEGACY
|
|
772
|
+
and legacy_name in args.recorded_retired
|
|
773
|
+
):
|
|
774
|
+
# The setup record proves the setup made this link. The setup retires it
|
|
775
|
+
# after its own run succeeds; this helper never deletes it.
|
|
776
|
+
state = "recorded"
|
|
751
777
|
alias_plan.append((legacy_name, canonical_name, target, state))
|
|
752
778
|
if state == "conflict" or (state == "legacy" and not args.migrate_legacy):
|
|
753
779
|
conflicts.append(target)
|
|
@@ -774,6 +800,9 @@ def main() -> int:
|
|
|
774
800
|
print(f"{action}: {name}: {source} -> {target}")
|
|
775
801
|
|
|
776
802
|
for legacy_name, canonical_name, target, state in alias_plan:
|
|
803
|
+
if state == "recorded":
|
|
804
|
+
print(f"RETIRED_RECORDED: {legacy_name}: made by this setup; renamed {canonical_name}: {target}")
|
|
805
|
+
continue
|
|
777
806
|
if state == "preserved":
|
|
778
807
|
print(
|
|
779
808
|
f"PRESERVED_FOREIGN: {legacy_name}: retired from nobrainer-tech-flow; "
|