nobrainer-tech-flow 2.4.1 → 2.6.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/README.md +14 -8
- package/docs/INSTALL.md +36 -22
- package/docs/MIGRATION_TO_FLOW.md +14 -0
- package/docs/SKILL_CURATION.md +8 -2
- package/docs/releases/v2.5.0.md +190 -0
- package/docs/releases/v2.6.0.md +73 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install.py +16 -7
- package/scripts/install.sh +129 -17
- package/scripts/install_personalization.py +51 -16
- package/scripts/install_skills.py +34 -5
- package/scripts/recommend_flow_setup.py +102 -12
- package/scripts/validate_skills.py +2 -1
- package/skills/nobrainer-build/SKILL.md +6 -2
- 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/SKILL.md +3 -3
- package/skills/nobrainer-tech-flow/VERSION +1 -1
- package/skills/nobrainer-tech-flow/references/daily-update.md +5 -2
- package/skills/nobrainer-tech-flow/references/delivery.md +17 -2
- 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
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
# Same flags, locations, conflict rule and setup record as scripts/install.py, so
|
|
5
5
|
# either one can undo what the other did. Every run previews until --apply. The
|
|
6
6
|
# skills are linked to this checkout (never copied), one managed instruction block
|
|
7
|
-
# is added
|
|
7
|
+
# is added. Its only standing authorization is the default daily update of Flow itself;
|
|
8
|
+
# an existing block keeps notifying unless it already held that line. A managed block that is
|
|
8
9
|
# already there is replaced by this version's text when it differs, and everything
|
|
9
10
|
# outside the two markers stays byte for byte, as install.py does. One difference: a
|
|
10
11
|
# block that carries options (a standing authorization, a wiki root or a preference)
|
|
@@ -19,7 +20,7 @@ unset CDPATH
|
|
|
19
20
|
# The eighteen skills, and the names earlier layouts used. Tests keep these lists
|
|
20
21
|
# equal to the ones in scripts/install_skills.py.
|
|
21
22
|
SKILLS='nobrainer-auto-fine-tune nobrainer-autoimprove nobrainer-browser nobrainer-build
|
|
22
|
-
nobrainer-
|
|
23
|
+
nobrainer-context nobrainer-decide nobrainer-dispatcher nobrainer-rca
|
|
23
24
|
nobrainer-research nobrainer-review nobrainer-security nobrainer-sessions
|
|
24
25
|
nobrainer-skill-doctor nobrainer-spec-driven-development nobrainer-team
|
|
25
26
|
nobrainer-tech-flow nobrainer-wiki nobrainer-writing'
|
|
@@ -28,13 +29,13 @@ deep-audit deep-autoresearch deep-autoreview deep-bugs-finder deep-code-review
|
|
|
28
29
|
deep-decide deep-rca dispatching-parallel-agents engineering-standards
|
|
29
30
|
karpathy-auto-improver karpathy-llm-wiki llm-wiki nb-add nb-dispatcher nb-flow nb-get
|
|
30
31
|
nb-multi nb-tidy nb-workflow nb-write nobrainer-autopilot nobrainer-capture-lesson
|
|
31
|
-
nobrainer-continuous-improvement nobrainer-fast-audit nobrainer-human-like
|
|
32
|
+
nobrainer-codex-context nobrainer-continuous-improvement nobrainer-fast-audit nobrainer-human-like
|
|
32
33
|
nobrainer-memory nobrainer-memory-memsearch nobrainer-npm-secure nobrainer-reddit
|
|
33
34
|
nobrainer-simplifier nobrainer-skill-browser nobrainer-starter nobrainer-style
|
|
34
35
|
nobrainer-team-builder nobrainer-ultra nobrainer-ultracode-workflow nobrainer-wiki-add
|
|
35
36
|
nobrainer-wiki-get nobrainer-wiki-tidy playwright-cli security-review session-handoff
|
|
36
37
|
wiki-add wiki-get wiki-tidy'
|
|
37
|
-
RETIRED='nobrainer-reddit'
|
|
38
|
+
RETIRED='nobrainer-codex-context nobrainer-reddit'
|
|
38
39
|
START='<!-- NOBRAINER-TECH-FLOW:START -->'
|
|
39
40
|
END='<!-- NOBRAINER-TECH-FLOW:END -->'
|
|
40
41
|
LEGACY_TEXT='(^|[^[:alnum:]_])nobrainer-ultra([^[:alnum:]_]|$)|^[[:space:]]*#{1,6}[[:space:]]+nobrainer([ .]?tech)?[[:space:]]+flow([^[:alnum:]_]|$)'
|
|
@@ -47,6 +48,12 @@ SEPARATORS=$(printf '\342\200[\250\251]')
|
|
|
47
48
|
TAB=$(printf '\t')
|
|
48
49
|
CR=$(printf '\r')
|
|
49
50
|
|
|
51
|
+
# The update line of the block: on by default; an existing block that only notifies
|
|
52
|
+
# (written with --no-auto-update, or the default before 2.6.0) keeps notifying.
|
|
53
|
+
AUTO_UPDATE_LINE='- Once a day on first use, update nobrainer-tech-flow to its latest verified stable release from the canonical source, with a backup; change nothing else and ask if unsure.'
|
|
54
|
+
NOTIFY_UPDATE_LINE='- Once a day on first use, tell the owner when a newer stable nobrainer-tech-flow release exists; do not update it yourself.'
|
|
55
|
+
update_line=$AUTO_UPDATE_LINE
|
|
56
|
+
|
|
50
57
|
# The block scripts/install_personalization.py writes without options, byte for byte
|
|
51
58
|
# (a test compares them).
|
|
52
59
|
block() {
|
|
@@ -61,7 +68,9 @@ block() {
|
|
|
61
68
|
- Derive short-term goals from the user's long-term direction (LDD) and define observable completion criteria. At project start, inspect the existing structure and layers, then recommend a fitting approach.
|
|
62
69
|
- At project start, discover whether a relevant wiki exists. If one exists, read its `WIKI.md`, locate relevant `index.md` entries with a targeted search, then read only the relevant pages. Ask before creating a wiki unless project rules or prior authorization already settle it.
|
|
63
70
|
- Assess context and checkpoint at appropriate milestones. Recommend session rotation when it would help; do not restart or archive automatically. When rotation is authorized, use the supported lifecycle, verify exact successor takeover by ID/readback, and archive the old session only after that readback. Do not create recursive visible workers or claim a restart when unsupported.
|
|
64
|
-
|
|
71
|
+
EOF
|
|
72
|
+
printf '%s\n' "$update_line"
|
|
73
|
+
cat <<'EOF'
|
|
65
74
|
- Use `nobrainer-writing` for material writing when available; make technical documentation concrete, source-backed, and technically verified. Preserve facts, use the user's language, and verify at the actual delivery layer.
|
|
66
75
|
<!-- NOBRAINER-TECH-FLOW:END -->
|
|
67
76
|
EOF
|
|
@@ -285,6 +294,38 @@ is_our_link() {
|
|
|
285
294
|
[ "$_o_resolved" = "$2" ]
|
|
286
295
|
}
|
|
287
296
|
|
|
297
|
+
# Put a claimed entry ($1) back at its name ($2) unless something new stands there; the
|
|
298
|
+
# private folder it was moved into goes too. A blocked restore keeps the claim and says where.
|
|
299
|
+
unclaim() {
|
|
300
|
+
# Restore through the same native no-replace primitive as the Python installer.
|
|
301
|
+
# Normal shell installs need no Python; without it, a recovery claim stays intact.
|
|
302
|
+
if command -v python3 >/dev/null 2>&1 && python3 - "$root/scripts" "$1" "$2" <<'PYTHON'
|
|
303
|
+
import sys
|
|
304
|
+
from pathlib import Path
|
|
305
|
+
sys.path.insert(0, sys.argv[1])
|
|
306
|
+
from install_skills import restore_claim
|
|
307
|
+
try:
|
|
308
|
+
restore_claim(Path(sys.argv[3]), Path(sys.argv[2]))
|
|
309
|
+
except (OSError, RuntimeError) as exc:
|
|
310
|
+
print(exc, file=sys.stderr)
|
|
311
|
+
sys.exit(1)
|
|
312
|
+
PYTHON
|
|
313
|
+
then
|
|
314
|
+
return 0
|
|
315
|
+
fi
|
|
316
|
+
err "PRESERVED: atomic restore unavailable or blocked; could not put $2 back; it is kept at $1"
|
|
317
|
+
return 1
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# A recorded skill link this checkout made: $1 is the link, $2 the skill name. The link of
|
|
321
|
+
# a retired module dangles, so it is compared as written.
|
|
322
|
+
owns_link() {
|
|
323
|
+
case " $RETIRED " in
|
|
324
|
+
*" $2 "*) is_exact_legacy_link "$1" "$2" ;;
|
|
325
|
+
*) is_our_link "$1" "$root/skills/$2" ;;
|
|
326
|
+
esac
|
|
327
|
+
}
|
|
328
|
+
|
|
288
329
|
# Resolve links before "..", including dangling final links. A trailing slash
|
|
289
330
|
# preserves a pathname's trailing newlines across command substitution.
|
|
290
331
|
resolve_legacy_path() (
|
|
@@ -410,7 +451,8 @@ read_state() {
|
|
|
410
451
|
' ]' | ' ],') _r_in= ;;
|
|
411
452
|
*)
|
|
412
453
|
json_field "\": ${_r_line# }" || return 1
|
|
413
|
-
|
|
454
|
+
# A record from an earlier release may hold a module since retired.
|
|
455
|
+
case " $(printf '%s' "$SKILLS $RETIRED" | tr '\n' ' ') " in *" $json_value "*) ;; *) return 1 ;; esac
|
|
414
456
|
case " $st_created " in *" $json_value "*) return 1 ;; esac
|
|
415
457
|
st_created="$st_created $json_value" ;;
|
|
416
458
|
esac
|
|
@@ -681,7 +723,7 @@ check_record() {
|
|
|
681
723
|
}
|
|
682
724
|
|
|
683
725
|
plan_skills() {
|
|
684
|
-
to_link= link_count=0 kept_count=0 problems=
|
|
726
|
+
to_link= link_count=0 kept_count=0 problems= to_retire=
|
|
685
727
|
for _p_name in $SKILLS; do
|
|
686
728
|
_p_source=$root/skills/$_p_name
|
|
687
729
|
[ -f "$_p_source/SKILL.md" ] || die 2 "this checkout is incomplete: $_p_source/SKILL.md is missing"
|
|
@@ -708,7 +750,13 @@ CONFLICT: $_p_name: $_p_source -> $_p_target"
|
|
|
708
750
|
if ! is_exact_legacy_link "$_p_target" "$_p_name"; then
|
|
709
751
|
err "PRESERVED_FOREIGN: $_p_name: retired from nobrainer-tech-flow; left unchanged: $_p_target"
|
|
710
752
|
continue
|
|
711
|
-
fi
|
|
753
|
+
fi
|
|
754
|
+
# This setup's record lists the link, as an undo would remove it.
|
|
755
|
+
case " $st_created " in
|
|
756
|
+
*" $_p_name "*)
|
|
757
|
+
to_retire="$to_retire $_p_name"
|
|
758
|
+
continue ;;
|
|
759
|
+
esac ;;
|
|
712
760
|
esac
|
|
713
761
|
problems="$problems
|
|
714
762
|
LEGACY: $_p_name: $_p_target"
|
|
@@ -767,6 +815,12 @@ plan_instructions() {
|
|
|
767
815
|
die 3 "malformed or duplicated nobrainer-tech-flow managed markers in $target"
|
|
768
816
|
fi
|
|
769
817
|
file_status=present
|
|
818
|
+
# An existing block keeps updating only when it holds the update line as its own line;
|
|
819
|
+
# a notify-only, edited or unknown line keeps notifying, so no grant is ever inferred.
|
|
820
|
+
if block_lines "$target" && ! head -n "$block_to" "$target" | tail -n "+$block_from" |
|
|
821
|
+
LC_ALL=C sed 's/[[:space:]]*$//' | grep -a -q -x -F -e "$AUTO_UPDATE_LINE"; then
|
|
822
|
+
update_line=$NOTIFY_UPDATE_LINE
|
|
823
|
+
fi
|
|
770
824
|
# A block of an earlier version is brought up to date, but only one that can be replaced
|
|
771
825
|
# without losing anything: its markers stand on lines of their own, and it holds no option.
|
|
772
826
|
if ! block_is_default && block_lines "$target" && ! block_has_options; then
|
|
@@ -788,6 +842,10 @@ block_lines() {
|
|
|
788
842
|
[ "$block_from" -lt "$block_to" ]
|
|
789
843
|
}
|
|
790
844
|
|
|
845
|
+
update_status() {
|
|
846
|
+
if [ "$update_line" = "$NOTIFY_UPDATE_LINE" ]; then echo CHECK_AND_NOTIFY; else echo AUTHORIZED_SAFE_VERIFIED_ONLY; fi
|
|
847
|
+
}
|
|
848
|
+
|
|
791
849
|
block_is_default() {
|
|
792
850
|
block_lines "$target" || return 1
|
|
793
851
|
_b_text=$(head -n "$block_to" "$target" | tail -n "+$block_from")
|
|
@@ -797,7 +855,7 @@ block_is_default() {
|
|
|
797
855
|
}
|
|
798
856
|
|
|
799
857
|
# block_has_options: the block block_lines found in $target carries something the owner
|
|
800
|
-
# chose: a standing authorization, a wiki root or a preference. Only install.py and
|
|
858
|
+
# chose: a pre-2.6 standing authorization, a wiki root or a preference. Only install.py and
|
|
801
859
|
# scripts/install_personalization.py --keep-options carry those over to a new block.
|
|
802
860
|
block_has_options() {
|
|
803
861
|
head -n "$block_to" "$target" | tail -n "+$block_from" |
|
|
@@ -819,6 +877,9 @@ variable_note() {
|
|
|
819
877
|
preview() {
|
|
820
878
|
say "$source_line for $label"
|
|
821
879
|
say "SKILLS: $link_count to link into $skills_dir ($kept_count already linked)"
|
|
880
|
+
for _v_name in $to_retire; do
|
|
881
|
+
say "REMOVE_RETIRED: $_v_name: made by this setup, now renamed: $skills_dir/$_v_name"
|
|
882
|
+
done
|
|
822
883
|
if [ "$inherit" = 1 ]; then
|
|
823
884
|
say "INHERITS_CODEX: $target imports $codex_file; no duplicate block added"
|
|
824
885
|
else
|
|
@@ -831,7 +892,7 @@ preview() {
|
|
|
831
892
|
elif [ "$file_status" = refresh ]; then
|
|
832
893
|
block_lines "$target"
|
|
833
894
|
say "UPDATE: nobrainer-tech-flow personalization block"
|
|
834
|
-
say "AUTO_UPDATE:
|
|
895
|
+
say "AUTO_UPDATE: $(update_status)"
|
|
835
896
|
say "AUTO_SESSION_RESTART: ASSESS_CHECKPOINT_RECOMMEND"
|
|
836
897
|
say "PRESERVED: content outside the managed block"
|
|
837
898
|
say "DIFF:"
|
|
@@ -839,7 +900,7 @@ preview() {
|
|
|
839
900
|
block | sed 's/^/+/'
|
|
840
901
|
else
|
|
841
902
|
say "MISSING: nobrainer-tech-flow personalization block"
|
|
842
|
-
say "AUTO_UPDATE:
|
|
903
|
+
say "AUTO_UPDATE: $(update_status)"
|
|
843
904
|
say "AUTO_SESSION_RESTART: ASSESS_CHECKPOINT_RECOMMEND"
|
|
844
905
|
say "DIFF:"
|
|
845
906
|
block | sed 's/^/+/'
|
|
@@ -914,6 +975,10 @@ write_instructions() {
|
|
|
914
975
|
}
|
|
915
976
|
|
|
916
977
|
install() {
|
|
978
|
+
# plan_skills needs the retired links the record lists; check_record judges the record
|
|
979
|
+
# after the skills, as before, so a record that cannot be read only lists none here.
|
|
980
|
+
st_created=
|
|
981
|
+
if [ -f "$state" ] && [ ! -L "$state" ]; then read_state "$state" || st_created=; fi
|
|
917
982
|
plan_skills
|
|
918
983
|
plan_instructions
|
|
919
984
|
check_record
|
|
@@ -986,8 +1051,43 @@ install() {
|
|
|
986
1051
|
elif [ "$wrote" = 1 ]; then
|
|
987
1052
|
new_target=$target new_backup=$made_backup new_hash=$(sha256 "$target")
|
|
988
1053
|
fi
|
|
989
|
-
|
|
990
|
-
|
|
1054
|
+
# The retired links the record lists go last, after every other write. Each is moved into
|
|
1055
|
+
# a private folder and checked there; one that changed after the check goes back as it is.
|
|
1056
|
+
# They come back if the record cannot be saved, and are deleted only once it is.
|
|
1057
|
+
retired_claims= retirement_error= retirement_restore_error=
|
|
1058
|
+
for _a_name in $to_retire; do
|
|
1059
|
+
_a_link=$skills_dir/$_a_name
|
|
1060
|
+
is_exact_legacy_link "$_a_link" "$_a_name" || continue
|
|
1061
|
+
_a_text=$(readlink "$_a_link") || continue
|
|
1062
|
+
if ! _a_claim=$(mktemp -d "$skills_dir/.nobrainer-retire-XXXXXX"); then
|
|
1063
|
+
retirement_error=1
|
|
1064
|
+
break
|
|
1065
|
+
fi
|
|
1066
|
+
if ! mv "$_a_link" "$_a_claim/$_a_name" 2>/dev/null; then
|
|
1067
|
+
rmdir "$_a_claim"
|
|
1068
|
+
continue
|
|
1069
|
+
fi
|
|
1070
|
+
if [ -L "$_a_claim/$_a_name" ] && [ "$(readlink "$_a_claim/$_a_name")" = "$_a_text" ]; then
|
|
1071
|
+
retired_claims="$retired_claims$_a_claim/$_a_name
|
|
1072
|
+
"
|
|
1073
|
+
else
|
|
1074
|
+
if ! unclaim "$_a_claim/$_a_name" "$_a_link"; then
|
|
1075
|
+
retirement_error=1 retirement_restore_error=1
|
|
1076
|
+
break
|
|
1077
|
+
fi
|
|
1078
|
+
err "PRESERVED: $_a_link changed during the setup and is left as it is"
|
|
1079
|
+
fi
|
|
1080
|
+
done
|
|
1081
|
+
if [ -n "$created" ] || [ -n "$to_retire" ] || [ "$wrote" = 1 ] || { [ "$record" = 1 ] && [ "$new_hash" != "$st_hash" ]; }; then
|
|
1082
|
+
# A retired module's link is gone once migrated; then the record drops its name.
|
|
1083
|
+
_k_created=
|
|
1084
|
+
for _k_name in $st_created; do
|
|
1085
|
+
case " $RETIRED " in
|
|
1086
|
+
*" $_k_name "*) owns_link "$skills_dir/$_k_name" "$_k_name" || continue ;;
|
|
1087
|
+
esac
|
|
1088
|
+
_k_created="$_k_created $_k_name"
|
|
1089
|
+
done
|
|
1090
|
+
new_created=$(printf '%s\n' $_k_created $created | LC_ALL=C sort -u | tr '\n' ' ')
|
|
991
1091
|
new_created=${new_created# }
|
|
992
1092
|
new_created=${new_created% }
|
|
993
1093
|
# Of the folders this run meant to create, the ones that exist now, with the ones an
|
|
@@ -1011,7 +1111,7 @@ install() {
|
|
|
1011
1111
|
# makes a folder, and its undo keeps the rule it was made under.
|
|
1012
1112
|
want_dirs=1
|
|
1013
1113
|
if [ "$record" = 1 ] && [ -z "$st_has_dirs" ] && [ -z "$new_dirs" ]; then want_dirs=; fi
|
|
1014
|
-
if ! write_state; then
|
|
1114
|
+
if [ -n "$retirement_error" ] || ! write_state; then
|
|
1015
1115
|
for _a_done in $created; do remove_link "$skills_dir/$_a_done"; done
|
|
1016
1116
|
if [ "$wrote" = 1 ]; then
|
|
1017
1117
|
# Through a copy beside it and a rename, so a read-only file comes back as well.
|
|
@@ -1022,9 +1122,21 @@ install() {
|
|
|
1022
1122
|
rm -f "$target"
|
|
1023
1123
|
fi
|
|
1024
1124
|
fi
|
|
1125
|
+
if ! printf '%s' "$retired_claims" | while IFS= read -r _a_claim; do
|
|
1126
|
+
unclaim "$_a_claim" "$skills_dir/${_a_claim##*/}" || exit 1
|
|
1127
|
+
done; then
|
|
1128
|
+
die 4 "could not save the setup record $state; rollback incomplete; recovery claims were preserved"
|
|
1129
|
+
fi
|
|
1130
|
+
if [ -n "$retirement_restore_error" ]; then
|
|
1131
|
+
die 4 "retired link restore failed; rollback incomplete; recovery claims were preserved"
|
|
1132
|
+
fi
|
|
1025
1133
|
die 4 "could not save the setup record $state; the changes of this run were reversed"
|
|
1026
1134
|
fi
|
|
1027
1135
|
fi
|
|
1136
|
+
printf '%s' "$retired_claims" | while IFS= read -r _a_claim; do
|
|
1137
|
+
remove_link "$_a_claim" && rmdir "${_a_claim%/*}" 2>/dev/null
|
|
1138
|
+
say "REMOVED_RETIRED: $skills_dir/${_a_claim##*/}"
|
|
1139
|
+
done
|
|
1028
1140
|
|
|
1029
1141
|
# A home made for a record that was not needed goes again.
|
|
1030
1142
|
if [ ! -e "$state" ] && [ -n "$made_home" ]; then
|
|
@@ -1033,7 +1145,7 @@ install() {
|
|
|
1033
1145
|
|
|
1034
1146
|
# New links and a written instruction file are what a client has to be restarted for.
|
|
1035
1147
|
changed=0
|
|
1036
|
-
if [ "$link_count" -gt 0 ] || [ "$wrote" = 1 ]; then changed=1; fi
|
|
1148
|
+
if [ "$link_count" -gt 0 ] || [ "$wrote" = 1 ] || [ -n "$retired_claims" ]; then changed=1; fi
|
|
1037
1149
|
if [ "$changed" = 1 ]; then
|
|
1038
1150
|
say "INSTALLED: $source_line for $label"
|
|
1039
1151
|
else
|
|
@@ -1215,7 +1327,7 @@ undo() {
|
|
|
1215
1327
|
for _u_name in $st_created; do
|
|
1216
1328
|
_u_link=$st_dest/$_u_name
|
|
1217
1329
|
if [ -L "$_u_link" ]; then
|
|
1218
|
-
if !
|
|
1330
|
+
if ! owns_link "$_u_link" "$_u_name"; then
|
|
1219
1331
|
say "PRESERVED: changed target $_u_link"
|
|
1220
1332
|
exit 3
|
|
1221
1333
|
fi
|
|
@@ -1252,7 +1364,7 @@ undo() {
|
|
|
1252
1364
|
_u_removed=0
|
|
1253
1365
|
for _u_name in $st_created; do
|
|
1254
1366
|
_u_link=$st_dest/$_u_name
|
|
1255
|
-
if
|
|
1367
|
+
if owns_link "$_u_link" "$_u_name"; then
|
|
1256
1368
|
remove_link "$_u_link" || die 4 "could not remove $_u_link"
|
|
1257
1369
|
_u_removed=$((_u_removed + 1))
|
|
1258
1370
|
fi
|
|
@@ -27,10 +27,17 @@ LEGACY_FLOW_INSTRUCTIONS = re.compile(
|
|
|
27
27
|
r"\bnobrainer-ultra\b|^\s*#{1,6}\s+NoBrainer(?:[ .]?Tech)?\s+Flow\b",
|
|
28
28
|
re.IGNORECASE | re.MULTILINE,
|
|
29
29
|
)
|
|
30
|
-
# The
|
|
30
|
+
# The lines that carry a standing authorization. --keep-options recognises a grant
|
|
31
31
|
# only as one of these exact lines, never as a phrase somewhere in the block, so free
|
|
32
32
|
# text in a preference cannot forge one.
|
|
33
|
-
AUTO_UPDATE_RULE = "-
|
|
33
|
+
AUTO_UPDATE_RULE = "- Once a day on first use, update nobrainer-tech-flow to its latest verified stable release from the canonical source, with a backup; change nothing else and ask if unsure."
|
|
34
|
+
# The opt-out written by --no-auto-update.
|
|
35
|
+
NOTIFY_UPDATE_RULE = "- Once a day on first use, tell the owner when a newer stable nobrainer-tech-flow release exists; do not update it yourself."
|
|
36
|
+
# Lines written before 2.6.0. --keep-options maps them to the short rules above and
|
|
37
|
+
# keeps the owner's choice: the old grant stays a grant, the old notify-only default
|
|
38
|
+
# stays notify-only.
|
|
39
|
+
LEGACY_AUTO_UPDATE_RULE = "- On the first `nobrainer-tech-flow` use each calendar day, check for a safe verified update when this client exposes a supported check. This setting grants standing authorization to apply a `nobrainer-tech-flow`-only update after verifying the canonical source/version, reviewing the exact changes, and making a recoverable backup. Never apply destructive, unrelated, or uncertain changes; ask the owner first. If the client is inactive or cannot check, do not claim a check occurred."
|
|
40
|
+
LEGACY_NOTIFY_UPDATE_RULE = "- On the first `nobrainer-tech-flow` use each calendar day, check for available updates when this client exposes a supported check and notify the owner; do not apply them automatically. If the client is inactive or cannot check, do not claim a check occurred."
|
|
34
41
|
AUTO_SESSION_RULE = "- This setting grants standing authorization for session rotation only when the host supports creating a fresh successor, context or checkpoint evidence warrants rotation, and no write is in flight. Create the successor, verify exact takeover by ID/readback, and archive the old session only after that readback. Do not create recursive visible workers or claim a restart when unsupported."
|
|
35
42
|
|
|
36
43
|
|
|
@@ -57,6 +64,28 @@ def earlier_grant(existing: str, rule: str, phrase: str, label: str, flag: str)
|
|
|
57
64
|
return False
|
|
58
65
|
|
|
59
66
|
|
|
67
|
+
# Wording of an update grant, current or pre-2.6, used only to warn that an edited one
|
|
68
|
+
# was not kept.
|
|
69
|
+
UPDATE_GRANT_PHRASES = (
|
|
70
|
+
"update nobrainer-tech-flow to its latest verified stable release",
|
|
71
|
+
"grants standing authorization to apply a `nobrainer-tech-flow`-only update",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def saved_update_choice(existing: str) -> bool:
|
|
76
|
+
"""True only when an existing block holds an update grant as its own line.
|
|
77
|
+
|
|
78
|
+
Anything else, a notify-only line, an edited or unknown one, keeps notifying:
|
|
79
|
+
an update grant is never inferred. Trailing spaces on the line are ignored, and
|
|
80
|
+
a preference that quotes the wording is not a line of its own.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
return any(
|
|
84
|
+
re.search(r"(?m)^" + re.escape(rule) + r"[ \t]*\r?$", existing) is not None
|
|
85
|
+
for rule in (AUTO_UPDATE_RULE, LEGACY_AUTO_UPDATE_RULE)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
60
89
|
def unsafe_line(value: str) -> bool:
|
|
61
90
|
"""True when ``value`` cannot be one plain Markdown line (or could split into two)."""
|
|
62
91
|
|
|
@@ -85,11 +114,7 @@ def build_block(
|
|
|
85
114
|
if wiki_root is not None
|
|
86
115
|
else "- At project start, discover whether a relevant wiki exists. If one exists, read its `WIKI.md`, locate relevant `index.md` entries with a targeted search, then read only the relevant pages. Ask before creating a wiki unless project rules or prior authorization already settle it."
|
|
87
116
|
)
|
|
88
|
-
update_rule =
|
|
89
|
-
AUTO_UPDATE_RULE
|
|
90
|
-
if auto_update
|
|
91
|
-
else "- On the first `nobrainer-tech-flow` use each calendar day, check for available updates when this client exposes a supported check and notify the owner; do not apply them automatically. If the client is inactive or cannot check, do not claim a check occurred."
|
|
92
|
-
)
|
|
117
|
+
update_rule = AUTO_UPDATE_RULE if auto_update else NOTIFY_UPDATE_RULE
|
|
93
118
|
session_rule = (
|
|
94
119
|
AUTO_SESSION_RULE
|
|
95
120
|
if auto_session_restart
|
|
@@ -356,10 +381,16 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
356
381
|
type=Path,
|
|
357
382
|
help="project wiki root containing WIKI.md; its resolved path is added to the managed instructions",
|
|
358
383
|
)
|
|
359
|
-
parser.
|
|
384
|
+
updates = parser.add_mutually_exclusive_group()
|
|
385
|
+
updates.add_argument(
|
|
360
386
|
"--auto-update",
|
|
361
387
|
action="store_true",
|
|
362
|
-
help="
|
|
388
|
+
help="update nobrainer-tech-flow once a day from its verified stable releases (the default)",
|
|
389
|
+
)
|
|
390
|
+
updates.add_argument(
|
|
391
|
+
"--no-auto-update",
|
|
392
|
+
action="store_true",
|
|
393
|
+
help="only tell the owner about a newer stable release; never update automatically",
|
|
363
394
|
)
|
|
364
395
|
parser.add_argument(
|
|
365
396
|
"--auto-session-restart",
|
|
@@ -374,7 +405,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
374
405
|
"--keep-options",
|
|
375
406
|
action="store_true",
|
|
376
407
|
help=(
|
|
377
|
-
"keep the
|
|
408
|
+
"keep the update, session-restart and wiki-root settings already in the "
|
|
378
409
|
"managed block; flags given on this run are added to them, never removed"
|
|
379
410
|
),
|
|
380
411
|
)
|
|
@@ -429,15 +460,19 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
429
460
|
saved = SAVED_PREFERENCE.search(existing)
|
|
430
461
|
if saved:
|
|
431
462
|
preferences = saved.group(1)
|
|
432
|
-
auto_update = args.
|
|
463
|
+
auto_update = not args.no_auto_update
|
|
433
464
|
auto_session_restart = args.auto_session_restart
|
|
434
465
|
if args.keep_options and existing:
|
|
435
466
|
kept: list[str] = []
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
467
|
+
saved_update = saved_update_choice(existing)
|
|
468
|
+
if not (args.auto_update or args.no_auto_update) and not saved_update:
|
|
469
|
+
auto_update = False
|
|
470
|
+
kept.append("notify-only updates")
|
|
471
|
+
if any(phrase in existing for phrase in UPDATE_GRANT_PHRASES):
|
|
472
|
+
print(
|
|
473
|
+
"NOTE: an earlier update authorization was not kept because its line no longer "
|
|
474
|
+
"matches what this installer writes; pass --auto-update to grant it again"
|
|
475
|
+
)
|
|
441
476
|
if not auto_session_restart and earlier_grant(
|
|
442
477
|
existing,
|
|
443
478
|
AUTO_SESSION_RULE,
|
|
@@ -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; "
|