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.
Files changed (33) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +14 -8
  5. package/docs/INSTALL.md +36 -22
  6. package/docs/MIGRATION_TO_FLOW.md +14 -0
  7. package/docs/SKILL_CURATION.md +8 -2
  8. package/docs/releases/v2.5.0.md +190 -0
  9. package/docs/releases/v2.6.0.md +73 -0
  10. package/gemini-extension.json +1 -1
  11. package/package.json +1 -1
  12. package/plugin.json +1 -1
  13. package/scripts/install.py +16 -7
  14. package/scripts/install.sh +129 -17
  15. package/scripts/install_personalization.py +51 -16
  16. package/scripts/install_skills.py +34 -5
  17. package/scripts/recommend_flow_setup.py +102 -12
  18. package/scripts/validate_skills.py +2 -1
  19. package/skills/nobrainer-build/SKILL.md +6 -2
  20. package/skills/nobrainer-context/SKILL.md +113 -0
  21. package/skills/nobrainer-context/references/clients.md +115 -0
  22. package/skills/nobrainer-context/references/consolidate.md +66 -0
  23. package/skills/nobrainer-context/scripts/context_inventory.py +425 -0
  24. package/skills/nobrainer-skill-doctor/SKILL.md +4 -4
  25. package/skills/nobrainer-tech-flow/SKILL.md +3 -3
  26. package/skills/nobrainer-tech-flow/VERSION +1 -1
  27. package/skills/nobrainer-tech-flow/references/daily-update.md +5 -2
  28. package/skills/nobrainer-tech-flow/references/delivery.md +17 -2
  29. package/skills/nobrainer-tech-flow/references/optional-decisions.md +5 -3
  30. package/skills/nobrainer-tech-flow/references/routing.md +1 -1
  31. package/skills/nobrainer-tech-flow/scripts/typed_decisions.py +20 -9
  32. package/skills/nobrainer-codex-context/SKILL.md +0 -111
  33. /package/skills/{nobrainer-codex-context → nobrainer-context}/scripts/codex_context.py +0 -0
@@ -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, and no standing authorization is ever granted. A managed block that is
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-codex-context nobrainer-decide nobrainer-dispatcher nobrainer-rca
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
- - 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.
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
- case " $(printf '%s' "$SKILLS" | tr '\n' ' ') " in *" $json_value "*) ;; *) return 1 ;; esac
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: CHECK_AND_NOTIFY"
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: CHECK_AND_NOTIFY"
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
- if [ -n "$created" ] || [ "$wrote" = 1 ] || { [ "$record" = 1 ] && [ "$new_hash" != "$st_hash" ]; }; then
990
- new_created=$(printf '%s\n' $st_created $created | LC_ALL=C sort -u | tr '\n' ' ')
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 ! is_our_link "$_u_link" "$root/skills/$_u_name"; then
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 is_our_link "$_u_link" "$root/skills/$_u_name"; then
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 two lines that carry a standing authorization. --keep-options recognises a grant
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 = "- 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."
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.add_argument(
384
+ updates = parser.add_mutually_exclusive_group()
385
+ updates.add_argument(
360
386
  "--auto-update",
361
387
  action="store_true",
362
- help="authorize safe, verified nobrainer-tech-flow-only updates; default is check and notify",
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 auto-update, session-restart and wiki-root settings already in 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.auto_update
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
- if not auto_update and earlier_grant(
437
- existing, AUTO_UPDATE_RULE, "grants standing authorization to apply", "auto-update", "--auto-update"
438
- ):
439
- auto_update = True
440
- kept.append("auto-update")
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-codex-context",
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, not absorbed into a canonical skill. Keep its
153
- # owned-link migration, but a different source may legitimately use this name.
154
- RETIRED_LEGACY = frozenset({"nobrainer-reddit"})
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 RuntimeError(
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; "