@thebassclef/lite 1.9.8 → 1.9.10

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 (44) hide show
  1. package/README.md +6 -2
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/lite/.claude/hooks/git-stash-u-guard.sh +4 -1
  6. package/dist/lite/.claude/hooks/post-skill-friction-check.sh +6 -1
  7. package/dist/lite/.claude/hooks/pre-commit-gate.sh +49 -6
  8. package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +68 -8
  9. package/dist/lite/.claude/hooks/pre-commit-manifest-autoregen.sh +4 -2
  10. package/dist/lite/.claude/hooks/save-state.sh +24 -0
  11. package/dist/lite/.claude/hooks/session-reflection.d/83-install-written-paths-rebuild.sh +111 -0
  12. package/dist/lite/.claude/hooks/state-validate.sh +8 -0
  13. package/dist/lite/.claude/hooks/trace-helper.sh +29 -0
  14. package/dist/lite/.claude/rules/install-written-paths-mediation.md +74 -0
  15. package/dist/lite/.claude/rules/lite-manifest-schema-change-discipline.md +30 -0
  16. package/dist/lite/.claude/skills/autonomous/SKILL.md +53 -7
  17. package/dist/lite/.claude/skills/build/SKILL.md +53 -11
  18. package/dist/lite/.claude/skills/deploy-prod/SKILL.md +10 -0
  19. package/dist/lite/.claude/skills/launch/SKILL.md +26 -0
  20. package/dist/lite/.claude/skills/onboard-repo/SKILL.md +59 -18
  21. package/dist/lite/.claude/skills/personas/SKILL.md +1 -1
  22. package/dist/lite/.claude/skills/release-notes/SKILL.md +24 -0
  23. package/dist/lite/bassclef-version.json +5 -5
  24. package/dist/lite/gitignore +8 -0
  25. package/dist/lite/lib/capability-probe.sh +39 -1
  26. package/dist/lite/lib/composer-preflight.sh +8 -4
  27. package/dist/lite/lib/deferred-actions.sh +4 -4
  28. package/dist/lite/lib/fixture-builder.sh +9 -9
  29. package/dist/lite/lib/identifier-leak-check.sh +85 -0
  30. package/dist/lite/lib/install-written-paths.sh +369 -0
  31. package/dist/lite/lib/luminary-pick.sh +35 -13
  32. package/dist/lite/lib/preview-state.sh +20 -20
  33. package/dist/lite/lib/skip-env-inline-check.sh +61 -0
  34. package/dist/lite/lib/state.sh +102 -39
  35. package/dist/lite/lib/tier-check.sh +9 -9
  36. package/dist/lite/lib/tier-dependency-audit.sh +23 -23
  37. package/dist/lite/presence/install/schedule-auto-save.taskscheduler.md +1 -1
  38. package/dist/lite/standards/lite-manifest-schema-changes.md +83 -0
  39. package/dist/lite/standards/lite-manifest.json +70 -47
  40. package/dist/lite/standards/state-spine.md +1 -0
  41. package/dist/lite/templates/spec-template.md +10 -6
  42. package/package.json +1 -1
  43. package/dist/lite/.claude/hooks/longrun-prep-compounding-axis-check.sh +0 -492
  44. package/dist/lite/.claude/rules/compounding-axis-fresh-analysis.md +0 -173
@@ -40,8 +40,9 @@ __state_check_binary_deps() {
40
40
  if [ "${__STATE_DEPS_CHECKED:-}" = "1" ]; then
41
41
  return 0
42
42
  fi
43
+ # Baseline deps — awk + jq required unconditionally
43
44
  local missing=""
44
- for bin in awk python3 jq; do
45
+ for bin in awk jq; do
45
46
  command -v "$bin" >/dev/null 2>&1 || missing="$missing $bin"
46
47
  done
47
48
  if [ -n "$missing" ]; then
@@ -49,21 +50,63 @@ __state_check_binary_deps() {
49
50
  STATE ACCESSOR FAILURE
50
51
  Required binary missing from PATH:$missing
51
52
 
52
- These binaries are load-bearing for lib/state.sh accessors (Shape B
53
- YAML frontmatter extraction + JSON tooling).
53
+ awk + jq are load-bearing for lib/state.sh accessors.
54
54
 
55
55
  Resolution:
56
- macOS: brew install gawk python3 jq
57
- Debian: sudo apt-get install gawk python3 jq
58
-
59
- Closes the bassclef#598 silent-failure class.
56
+ macOS: brew install gawk jq
57
+ Debian: sudo apt-get install gawk jq
60
58
  EOF
61
59
  return 4
62
60
  fi
61
+
62
+ # Per cli#308: YAML parser needs EITHER yq OR python3+PyYAML.
63
+ # Stock macOS has python3 but not PyYAML. Pre-cli#308, that mismatch
64
+ # caused silent failures where __state_check_binary_deps passed but
65
+ # the actual yaml parse failed with ModuleNotFoundError. Now we check
66
+ # that at least one working parser exists.
67
+ __state_check_yaml_parser || return 6
63
68
  export __STATE_DEPS_CHECKED=1
64
69
  return 0
65
70
  }
66
71
 
72
+ # YAML parser check (per cli#308). Returns 0 if yq available OR
73
+ # python3+yaml-module importable. Returns 6 if neither.
74
+ __state_check_yaml_parser() {
75
+ # Cache the result per shell process.
76
+ if [ "${__STATE_YAML_PARSER:-}" = "yq" ] || [ "${__STATE_YAML_PARSER:-}" = "python3" ]; then
77
+ return 0
78
+ fi
79
+ # Prefer yq (faster + simpler contract).
80
+ if command -v yq >/dev/null 2>&1; then
81
+ export __STATE_YAML_PARSER=yq
82
+ return 0
83
+ fi
84
+ # Fall back to python3+PyYAML.
85
+ if command -v python3 >/dev/null 2>&1 && python3 -c "import yaml" >/dev/null 2>&1; then
86
+ export __STATE_YAML_PARSER=python3
87
+ return 0
88
+ fi
89
+ cat >&2 <<EOF
90
+ STATE ACCESSOR FAILURE — no YAML parser available (per cli#308)
91
+
92
+ lib/state.sh needs ONE of:
93
+ - yq (preferred — faster, no Python module needed)
94
+ - python3 + PyYAML module
95
+
96
+ Both are missing on this machine.
97
+
98
+ Resolution:
99
+ macOS (preferred): brew install yq
100
+ macOS (alt): python3 -m pip install pyyaml
101
+ Debian: sudo apt-get install yq OR python3 -m pip install pyyaml
102
+
103
+ Per cli#308 — this check closes the silent-failure class where
104
+ python3 existed but PyYAML did not, and callers reported "goal not
105
+ found" (wrong diagnosis).
106
+ EOF
107
+ return 6
108
+ }
109
+
67
110
  # yq2json — extract YAML frontmatter from markdown file, emit JSON.
68
111
  # Used by Shape B (markdown + YAML frontmatter + body) entity accessors.
69
112
  #
@@ -86,7 +129,7 @@ EOF
86
129
  # on invalid input) + @luminary michael-feathers (characterization
87
130
  # tests RED-first via fixtures/2026-09-06-silent-empty/).
88
131
  __state_yq2json() {
89
- __state_check_binary_deps || return 4
132
+ __state_check_binary_deps || return $?
90
133
  local file="$1"
91
134
  if [ ! -f "$file" ]; then
92
135
  return 1
@@ -127,7 +170,23 @@ __state_yq2json() {
127
170
 
128
171
  local out
129
172
  local ec
130
- out=$(awk '/^---$/{c++; next} c==1' "$file" | python3 -c "
173
+ # Per cli#308 — prefer yq when available, fall back to python3+PyYAML.
174
+ # __state_check_binary_deps (called at top of this function via return 4
175
+ # path) already resolved __STATE_YAML_PARSER to one of the two.
176
+ if [ "${__STATE_YAML_PARSER:-}" = "yq" ]; then
177
+ out=$(awk '/^---$/{c++; next} c==1' "$file" | yq -o=json '.' 2>&1)
178
+ ec=$?
179
+ if [ "$ec" -ne 0 ]; then
180
+ printf 'yaml parse failed (yq): %s\n' "$out" >&2
181
+ return 3
182
+ fi
183
+ # yq returns `null` for empty input; coerce to `{}` for consistency
184
+ # with python3 path.
185
+ if [ "$out" = "null" ]; then
186
+ out="{}"
187
+ fi
188
+ else
189
+ out=$(awk '/^---$/{c++; next} c==1' "$file" | python3 -c "
131
190
  import sys, yaml, json
132
191
  try:
133
192
  data = yaml.safe_load(sys.stdin) or {}
@@ -136,7 +195,8 @@ except Exception as e:
136
195
  print(f'yaml parse failed: {e}', file=sys.stderr)
137
196
  sys.exit(3)
138
197
  ")
139
- ec=$?
198
+ ec=$?
199
+ fi
140
200
  # Instrument real-parse count for #1519 tests + observability.
141
201
  __STATE_YQ2JSON_PARSE_COUNT=$(( ${__STATE_YQ2JSON_PARSE_COUNT:-0} + 1 ))
142
202
  if [ "$ec" -ne 0 ]; then
@@ -185,18 +245,20 @@ __state_atomic_write() {
185
245
  # output) + @luminary michael-nygard (silent-fail wrap; write does not
186
246
  # crash caller).
187
247
  __state_yaml_write() {
188
- __state_check_binary_deps || return 4
189
- local path="$1"
248
+ __state_check_binary_deps || return $?
249
+ # Finding #6 (#2036): `local path` is a zsh-magic variable name — ties to
250
+ # $PATH when sourced from zsh. Renamed to `local p` throughout this file.
251
+ local p="$1"
190
252
  local field="$2"
191
253
  local value="$3"
192
- if [ -z "$path" ] || [ -z "$field" ]; then
254
+ if [ -z "$p" ] || [ -z "$field" ]; then
193
255
  echo "__state_yaml_write: requires <path> <field> <value>" >&2
194
256
  return 2
195
257
  fi
196
258
  local dir
197
- dir="$(dirname "$path")"
259
+ dir="$(dirname "$p")"
198
260
  [ -d "$dir" ] || mkdir -p "$dir" || return 4
199
- local tmp="${path}.tmp.$$"
261
+ local tmp="${p}.tmp.$$"
200
262
  BASSCLEF_YAML_FIELD="$field" \
201
263
  BASSCLEF_YAML_VALUE="$value" \
202
264
  BASSCLEF_YAML_TMP="$tmp" \
@@ -213,7 +275,7 @@ except Exception as e:
213
275
  print(f"__state_yaml_write: safe_dump failed: {e}", file=sys.stderr)
214
276
  sys.exit(3)
215
277
  ' 2>&1 || { rm -f "$tmp"; return 3; }
216
- mv "$tmp" "$path" || { rm -f "$tmp"; return 4; }
278
+ mv "$tmp" "$p" || { rm -f "$tmp"; return 4; }
217
279
  }
218
280
 
219
281
  # === Whereami (Shape A — JSON authoritative + markdown rendered) ===
@@ -239,21 +301,22 @@ state_whereami_get() {
239
301
  state_whereami_set() {
240
302
  local field="$1"
241
303
  local value="$2"
242
- local path="docs/whereami.json"
304
+ local p="docs/whereami.json"
243
305
  if [ -z "$field" ] || [ -z "$value" ]; then
244
306
  echo "state_whereami_set: requires <field> <value>" >&2
245
307
  return 2
246
308
  fi
247
- if [ ! -f "$path" ]; then
309
+ if [ ! -f "$p" ]; then
248
310
  echo "state_whereami_set: docs/whereami.json does not exist" >&2
249
311
  return 4
250
312
  fi
251
313
  local updated
252
- # Use jq's setpath for nested field paths (e.g., 'phase.value')
314
+ # Use jq's setpath for nested field paths (e.g., 'phase.value').
315
+ # The jq `--arg path` names a jq variable (not shell) — safe to keep.
253
316
  updated="$(jq --arg path "$field" --arg val "$value" '
254
317
  setpath($path | split("."); $val)
255
- ' "$path")" || return 3
256
- __state_atomic_write "$path" "$updated"
318
+ ' "$p")" || return 3
319
+ __state_atomic_write "$p" "$updated"
257
320
  }
258
321
 
259
322
  # === IterationBet (Shape B — markdown + YAML frontmatter + body) ===
@@ -264,11 +327,11 @@ state_iteration_bet_get() {
264
327
  echo "state_iteration_bet_get: requires <slug>" >&2
265
328
  return 2
266
329
  fi
267
- local path="docs/iteration-bets/${slug}.md"
268
- if [ ! -f "$path" ]; then
330
+ local p="docs/iteration-bets/${slug}.md"
331
+ if [ ! -f "$p" ]; then
269
332
  return 1
270
333
  fi
271
- __state_yq2json "$path"
334
+ __state_yq2json "$p"
272
335
  }
273
336
 
274
337
  # Special-purpose: returns the single active iteration bet (status=active),
@@ -333,25 +396,25 @@ state_roadmap_get() {
333
396
  echo "state_roadmap_get: requires <slug>" >&2
334
397
  return 2
335
398
  fi
336
- local path="docs/roadmaps/${slug}.md"
337
- if [ ! -f "$path" ]; then
399
+ local p="docs/roadmaps/${slug}.md"
400
+ if [ ! -f "$p" ]; then
338
401
  return 1
339
402
  fi
340
- __state_yq2json "$path"
403
+ __state_yq2json "$p"
341
404
  }
342
405
 
343
406
  # Internal: rewrite a roadmap's frontmatter from JSON, preserving the body.
344
407
  __state_roadmap_write_frontmatter() {
345
408
  local slug="$1"
346
409
  local frontmatter_json="$2"
347
- local path="docs/roadmaps/${slug}.md"
348
- if [ ! -f "$path" ]; then
349
- echo "__state_roadmap_write_frontmatter: roadmap $path not found" >&2
410
+ local p="docs/roadmaps/${slug}.md"
411
+ if [ ! -f "$p" ]; then
412
+ echo "__state_roadmap_write_frontmatter: roadmap $p not found" >&2
350
413
  return 1
351
414
  fi
352
415
  python3 -c "
353
416
  import sys, yaml, json
354
- path = '$path'
417
+ path = '$p'
355
418
  fm = json.loads('''$frontmatter_json''')
356
419
  with open(path, 'r') as f:
357
420
  content = f.read()
@@ -726,15 +789,15 @@ state_chronicle_gate_evidence() {
726
789
  return 2
727
790
  fi
728
791
  # Try bassclef (chronicle/) and consumer-repo (docs/chronicle/) paths
729
- local path=""
792
+ local p=""
730
793
  if [ -f "chronicle/${id}.md" ]; then
731
- path="chronicle/${id}.md"
794
+ p="chronicle/${id}.md"
732
795
  elif [ -f "docs/chronicle/${id}.md" ]; then
733
- path="docs/chronicle/${id}.md"
796
+ p="docs/chronicle/${id}.md"
734
797
  else
735
798
  return 1
736
799
  fi
737
- __state_yq2json "$path" | jq -c '.gates_fired // []'
800
+ __state_yq2json "$p" | jq -c '.gates_fired // []'
738
801
  }
739
802
 
740
803
  # List chronicle IDs (filename without .md extension), optionally limited
@@ -1081,16 +1144,16 @@ __state_platform_resolve_path() {
1081
1144
 
1082
1145
  state_platform_get() {
1083
1146
  local key="${1:-}"
1084
- local path
1085
- path="$(__state_platform_resolve_path)"
1086
- if [ ! -f "$path" ]; then
1147
+ local p
1148
+ p="$(__state_platform_resolve_path)"
1149
+ if [ ! -f "$p" ]; then
1087
1150
  return 1
1088
1151
  fi
1089
1152
  local json
1090
1153
  json="$(python3 -c "
1091
1154
  import sys, yaml, json
1092
1155
  try:
1093
- with open('$path') as f:
1156
+ with open('$p') as f:
1094
1157
  data = yaml.safe_load(f) or {}
1095
1158
  print(json.dumps(data, default=str))
1096
1159
  except Exception as e:
@@ -131,35 +131,35 @@ PYEOF
131
131
  # Mirrors detection shape from .claude/hooks/tier-tag-required-at-write.sh so
132
132
  # the reader half and the writer half stay consistent.
133
133
  resolve_file_tier() {
134
- local path="$1"
135
- if [ ! -f "$path" ]; then
134
+ local p="$1"
135
+ if [ ! -f "$p" ]; then
136
136
  return 0
137
137
  fi
138
138
 
139
139
  local tier=""
140
- case "$path" in
140
+ case "$p" in
141
141
  *.md)
142
142
  # Frontmatter between --- markers, first 30 lines
143
143
  tier=$(awk '
144
144
  /^---/ { in_fm = !in_fm; if (!in_fm) exit; next }
145
145
  in_fm && /^tier:[[:space:]]/ { sub(/^tier:[[:space:]]*/, ""); sub(/[[:space:]]*$/, ""); print; exit }
146
146
  NR > 30 { exit }
147
- ' "$path" 2>/dev/null)
147
+ ' "$p" 2>/dev/null)
148
148
  ;;
149
149
  *.sh|*.py)
150
- tier=$(head -10 "$path" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]' | head -1 | sed -E 's/^#[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
150
+ tier=$(head -10 "$p" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]' | head -1 | sed -E 's/^#[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
151
151
  ;;
152
152
  *.yml|*.yaml)
153
- tier=$(head -30 "$path" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]|^tier:[[:space:]]' | head -1 | sed -E 's/^#?[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
153
+ tier=$(head -30 "$p" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]|^tier:[[:space:]]' | head -1 | sed -E 's/^#?[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
154
154
  ;;
155
155
  *.json|*.jsonc)
156
- tier=$(head -30 "$path" 2>/dev/null | grep -oE '"(_?tier)"[[:space:]]*:[[:space:]]*"[a-z]+"' | head -1 | sed -E 's/.*:[[:space:]]*"([a-z]+)"/\1/')
156
+ tier=$(head -30 "$p" 2>/dev/null | grep -oE '"(_?tier)"[[:space:]]*:[[:space:]]*"[a-z]+"' | head -1 | sed -E 's/.*:[[:space:]]*"([a-z]+)"/\1/')
157
157
  if [ -z "$tier" ]; then
158
- tier=$(head -30 "$path" 2>/dev/null | grep -oE '//[[:space:]]*tier:[[:space:]]*[a-z]+' | head -1 | sed -E 's/.*tier:[[:space:]]*//')
158
+ tier=$(head -30 "$p" 2>/dev/null | grep -oE '//[[:space:]]*tier:[[:space:]]*[a-z]+' | head -1 | sed -E 's/.*tier:[[:space:]]*//')
159
159
  fi
160
160
  ;;
161
161
  *.txt)
162
- tier=$(head -1 "$path" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]' | sed -E 's/^#[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
162
+ tier=$(head -1 "$p" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]' | sed -E 's/^#[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
163
163
  ;;
164
164
  esac
165
165
 
@@ -364,15 +364,15 @@ walk_graph_bfs() {
364
364
  while [ -s "$queue_file" ] && [ "$iter" -lt "$max_iter" ]; do
365
365
  iter=$((iter + 1))
366
366
  local next_file; next_file="$(mktemp)"
367
- while IFS= read -r path; do
368
- [ -z "$path" ] && continue
367
+ while IFS= read -r p; do
368
+ [ -z "$p" ] && continue
369
369
  # Skip if visited
370
- if grep -Fxq "$path" "$visited_file" 2>/dev/null; then
370
+ if grep -Fxq "$p" "$visited_file" 2>/dev/null; then
371
371
  continue
372
372
  fi
373
- echo "$path" >> "$visited_file"
373
+ echo "$p" >> "$visited_file"
374
374
  # Extract references from this file
375
- "$extractor" "$path" 2>/dev/null | while IFS= read -r ref; do
375
+ "$extractor" "$p" 2>/dev/null | while IFS= read -r ref; do
376
376
  [ -n "$ref" ] && echo "$ref" >> "$next_file"
377
377
  done
378
378
  done < "$queue_file"
@@ -504,17 +504,17 @@ classify_files() {
504
504
  # tag == target tier → bucket 1 (ships correctly in this tier)
505
505
  # no tag at all → bucket 2a (safe mechanical tag)
506
506
  # tag == other tier → bucket 2b (chain-snap risk; per-file cure)
507
- comm -12 "$intended_sorted" "$actual_sorted" | while IFS= read -r path; do
507
+ comm -12 "$intended_sorted" "$actual_sorted" | while IFS= read -r p; do
508
508
  # Look up tag for path in tags file. Returns "" for untagged (no tab)
509
509
  # or missing entry; returns tier value when tagged.
510
510
  local file_tag
511
- file_tag="$(awk -F'\t' -v p="$path" '$1 == p { print ($2 ? $2 : ""); exit }' "$tags")"
511
+ file_tag="$(awk -F'\t' -v p="$p" '$1 == p { print ($2 ? $2 : ""); exit }' "$tags")"
512
512
  if [ "$file_tag" = "$tier" ]; then
513
- echo "$path" >> "$bucket_1"
513
+ echo "$p" >> "$bucket_1"
514
514
  elif [ -z "$file_tag" ]; then
515
- echo "$path" >> "$bucket_2a"
515
+ echo "$p" >> "$bucket_2a"
516
516
  else
517
- echo "$path" >> "$bucket_2b"
517
+ echo "$p" >> "$bucket_2b"
518
518
  fi
519
519
  done
520
520
 
@@ -557,13 +557,13 @@ classify_files_combined() {
557
557
 
558
558
  # Bucket 1 + Bucket 2 — split intersection by tag presence.
559
559
  # In combined mode, ANY non-empty tag places the file in bucket 1.
560
- comm -12 "$intended_sorted" "$actual_sorted" | while IFS= read -r path; do
560
+ comm -12 "$intended_sorted" "$actual_sorted" | while IFS= read -r p; do
561
561
  local file_tag
562
- file_tag="$(awk -F'\t' -v p="$path" '$1 == p { print ($2 ? $2 : ""); exit }' "$tags")"
562
+ file_tag="$(awk -F'\t' -v p="$p" '$1 == p { print ($2 ? $2 : ""); exit }' "$tags")"
563
563
  if [ -n "$file_tag" ]; then
564
- echo "$path" >> "$bucket_1"
564
+ echo "$p" >> "$bucket_1"
565
565
  else
566
- echo "$path" >> "$bucket_2"
566
+ echo "$p" >> "$bucket_2"
567
567
  fi
568
568
  done
569
569
 
@@ -738,12 +738,12 @@ write_manifest_combined() {
738
738
  # - SDLC trace logs
739
739
  # ---------------------------------------------------------------------------
740
740
  _is_substrate_relevant_path() {
741
- local path="$1"
741
+ local p="$1"
742
742
  # Normalize leading ./ prefix so patterns match consistently.
743
743
  # Closure files mix "./foo" (from find) with "foo" (from git grep).
744
744
  # Per #1385 walker cure — extends G3 filter beyond initial pattern set.
745
- path="${path#./}"
746
- case "$path" in
745
+ p="${p#./}"
746
+ case "$p" in
747
747
  /*|../*|.git/*|*/.git/*) return 1 ;;
748
748
  # Runtime state — walker outputs + gate markers + session state + analysis.
749
749
  # None ship to adopters regardless of tier tag. Per #1385.
@@ -831,12 +831,12 @@ walk_graph_bfs_inbound() {
831
831
  while [ -s "$queue_file" ] && [ "$iter" -lt "$max_iter" ]; do
832
832
  iter=$((iter + 1))
833
833
  local next_file; next_file="$(mktemp)"
834
- while IFS= read -r path; do
835
- [ -z "$path" ] && continue
836
- if grep -Fxq "$path" "$visited_file" 2>/dev/null; then continue; fi
837
- echo "$path" >> "$visited_file"
838
- # Look up callers of $path in the reverse index (path is column 1; caller is column 2)
839
- awk -F'\t' -v p="$path" '$1 == p { print $2 }' "$rev_index" | while IFS= read -r caller; do
834
+ while IFS= read -r p; do
835
+ [ -z "$p" ] && continue
836
+ if grep -Fxq "$p" "$visited_file" 2>/dev/null; then continue; fi
837
+ echo "$p" >> "$visited_file"
838
+ # Look up callers of $p in the reverse index (path is column 1; caller is column 2)
839
+ awk -F'\t' -v p="$p" '$1 == p { print $2 }' "$rev_index" | while IFS= read -r caller; do
840
840
  [ -n "$caller" ] && echo "$caller" >> "$next_file"
841
841
  done
842
842
  done < "$queue_file"
@@ -110,7 +110,7 @@ Unregister-ScheduledTask -TaskName "BassclefAutoSave" -Confirm:$false
110
110
 
111
111
  ## Alternatives
112
112
 
113
- - **WSL2 available?** Skip this doc; use `presence/install/schedule-auto-save.cron.sh` inside WSL. Cron there watches your Windows filesystem via `/mnt/c/Users/...`.
113
+ - **WSL2 available?** Skip this doc; use `presence/install/schedule-auto-save.cron.sh` inside WSL. Cron there watches your Windows filesystem via the mounted `/mnt/c/` tree under your user profile.
114
114
  - **Prefer a .ps1 installer?** File a ticket. The current ship stays doc-only to avoid installer complexity for a population not yet observed. Path B (per-OS installers) upgrade is a follow-on.
115
115
 
116
116
  ## References
@@ -34,6 +34,89 @@ Every entry stays in this file for the life of the manifest. Never pruned. When
34
34
 
35
35
  ## Entries (newest first)
36
36
 
37
+ ### cli#305 — identifier-leak scrub self-exclude + commit author check (v1.15.3 → v1.15.4, 2026-10-04)
38
+
39
+ - **Change type:** content-change (1 file: `.claude/hooks/pre-commit-identifier-leak-scrub.sh`). Check 2 now filters out hunks from the hook's own files, its test, the rule, and the pattern source. Check 3 added — scans `git var GIT_AUTHOR_IDENT` for the pattern set plus the `.local` suffix. BLOCK banner adds a resolution path pointing at the GitHub no-reply address.
40
+ - **Fields:** 444 entries (unchanged). Content hash shifts on the `pre-commit-identifier-leak-scrub` hook slug.
41
+ - **Version bump:** v1.15.3 → v1.15.4 (patch — content-only change on 1 slug; no entry count change). Rides on top of the Finding #7 bump (v1.15.3) when PRs land in order.
42
+ - **Rationale:** Kunal cli#305 — first commit after `bassclef init` blocked on the check's own files; author identity leaked silently when `user.email` was unset on Mac. Class A identity leak sister to cli#313 + cli#320 shipped in v1.15.2. Per PR #2062 (planned).
43
+ - **Downstream cure:** no action for adopters at exact-pin. Self-exclude paths are PATH-scoped, not pattern-scoped — leaks moved into non-self files still block. Author check is additive; adopters with clean emails see no change. Adopters whose git author ends in `.local` or carries a hostname pattern see the new BLOCK with a recipe pointing at `<username>@users.noreply.github.com`.
44
+ - **Old shape retired:** nothing retired. Check 1 (message) + Check 2 (staged content, now filtered) + Check 3 (author) chain is additive.
45
+
46
+ ### #2036 Finding #7 — Playwright probe file-based fallback (v1.15.4 → v1.15.5, 2026-10-05)
47
+
48
+ - **Change type:** content-change (1 file: `lib/capability-probe.sh`). `probe_mcp_playwright()` extended with four MCP config file fallback paths after the `CLAUDE_MCP_PLAYWRIGHT` env-var fast path. Fail-soft on missing `jq` + malformed JSON.
49
+ - **Fields:** 444 entries (unchanged). Content hash shifts on the `capability-probe` lib slug.
50
+ - **Version bump:** v1.15.4 → v1.15.5 (patch — content-change on existing lib slug per policy). Rebased on top of cli#305 which landed first.
51
+ - **Rationale:** Kunal v1.6.4 adopter run installed Playwright MCP but did not set `CLAUDE_MCP_PLAYWRIGHT`; probe returned 1; `/riff` + `/build` skipped screenshots. Fallback now reads `.mcp.json`, project + user `.claude/settings.json`, and `~/.claude.json`. Per bassclef-upstream#2036 Finding #7 + PR #2060.
52
+ - **Downstream cure:** nothing — byte-for-byte unchanged when `CLAUDE_MCP_PLAYWRIGHT` is set (fast path first). Adopters without the env var set gain file-based detection on the fly.
53
+ - **Old shape retired:** immediately (new behavior is additive).
54
+
55
+ ### cli#313 + cli#320 — identity-leak sweep in luminary-pick + personas (v1.15.1 → v1.15.2, 2026-10-04)
56
+
57
+ - **Change type:** content-change (2 files: `lib/luminary-pick.sh` + `.claude/skills/personas/SKILL.md`). luminary-pick basedir fallback moves from `$HOME/src/sunj-labs/bassclef` to `$CLAUDE_PROJECT_DIR` → `git rev-parse` → `pwd` chain, plus adds `.claude/luminaries/*.md` catalog fallback for lite tier. personas SKILL default slug changes from email-derived (`operator-[local-part]`) to plain `operator` (never from email).
58
+ - **Fields:** 444 entries (unchanged). Content hash shifts on the `luminary-pick` lib slug + `personas` skill slug.
59
+ - **Version bump:** v1.15.1 → v1.15.2 (patch; content-only on 2 slugs; no schema shape shift; no entry count change).
60
+ - **Rationale:** bassclef-cli#313 (luminary-pick hardcoded eng path; adopters hit "catalog dir missing" every /launch Phase 3.5) + bassclef-cli#320 (operator slug derived from git email leaked GitHub account ID into committed docs). Kunal cold-adopter smoke caught both; shared class is "engineering assumption in cold paths" (per plan doc Slot 3).
61
+ - **Downstream cure:** no action for adopters at exact-pin. Both changes are additive — lite tier adopters now get a working picker catalog; email-derived slugs never fire again. Operators wanting a display name beyond `operator` get an in-turn prompt instead.
62
+ - **Old shape retired:** nothing retired. Both old shapes stay viable (BASSCLEF_DIR env var still honored first; manual slug entry still allowed via operator reply).
63
+
64
+ ### cli#307 — /build Phase 0 awk matches Step-N + WU-N (v1.15.0 → v1.15.1, 2026-10-04)
65
+
66
+ - **Change type:** content-change (1 file: `.claude/skills/build/SKILL.md`). Phase 0 safety floor awk pattern + prose at L276 updated to recognize both `### Step-N` (new vocab per ADR-040 D1) and `### WU-N` (legacy).
67
+ - **Fields:** 444 entries (unchanged). Content hash shifts on the `build` skill slug.
68
+ - **Version bump:** v1.15.0 → v1.15.1 (patch; content-only change on 1 slug; no schema shape shift; no entry count change).
69
+ - **Rationale:** bassclef-cli#307 — Kunal cold-adopter smoke caught the Phase 0 awk reading zero acceptance lines on Step-format specs. Partial-rename regression from ADR-040 D1 (Phase 1 grep got the rename, Phase 0 awk was missed).
70
+ - **Downstream cure:** no action for adopters at exact-pin — the fix is additive (both heading forms match). Step-format specs now trigger the floor as designed. WU-format specs continue to work (characterization).
71
+ - **Old shape retired:** nothing retired. The WU-only pattern leaves. The `(Step|WU)-` pattern stays indefinitely.
72
+
73
+ ### #2036 — install-written-paths manifest + lib + rule (v1.14.4 → v1.15.0, 2026-10-03)
74
+
75
+ - **Change type:** content-add (3 new entries: `lib/install-written-paths.sh`, `.claude/rules/install-written-paths-mediation.md`, `.claude/hooks/session-reflection.d/83-install-written-paths-rebuild.sh`); content-change on `.claude/hooks/pre-commit-gate.sh` (filter block added for Kunal Finding #8 cure).
76
+ - **Fields:** 441 entries → 444 entries (3 adds, 1 content_hash shift).
77
+ - **Version bump:** v1.14.4 → v1.15.0 (content-add MINOR per discipline policy).
78
+ - **Rationale:** bassclef-upstream#2036 Finding #8 structural cure. See `architecture/reviews/2026-10-03.md` + plan doc `docs/next-session-plan-2026-10-04-kunal-slots-5-6.md` Slot 5.
79
+ - **Downstream cure:** no action for adopters at exact-pin — the new entries are additive and the lib's accessor API is new. Adopters pulling at `@thebassclef/lite@^1.15.0` get the cure automatically; the session-start fragment regenerates the install-written-paths manifest on first fresh-install session.
80
+ - **Old shape retired:** nothing retired. All existing entries preserved.
81
+
82
+ ### #1777 — friction hook pattern adds stage|launch new skill vocabulary (v1.14.2 → v1.14.3, 2026-10-01)
83
+
84
+ - **Change type:** content-change (1 file: `post-skill-friction-check` hook)
85
+ - **Fields:** 439 entries → 439 entries (no add/remove). Content hash shifts on `post-skill-friction-check` slug (MAJOR_SKILLS_PATTERN regex extended with stage|launch per ADR-040 D1 rename grace window).
86
+ - **Version bump:** v1.14.2 → v1.14.3 (patch — content-only change; no field add/remove; no shape shift). Rebased after #523's retirement-discipline cure landed as v1.14.2.
87
+ - **Rationale:** PR #2028 closes #1777 (friction log never created because hook pattern matched old skill names only). Additive regex extension; grace-window compat preserved (preview|preview-build still match).
88
+ - **Downstream cure:** none — adopters re-sync and the hook starts firing on current-vocabulary chain dispatches. Lazy-create of `docs/session-friction-log.md` happens on first real friction case.
89
+ - **Old shape retired:** old pattern retired `2026-10-31` (grace window end for ADR-040 D1 vocabulary); current pattern keeps both until then.
90
+
91
+ ### #523 — retirement discipline + fixture allowlist pattern (v1.14.1 → v1.14.2, 2026-10-01)
92
+
93
+ - **Change type:** content-change (2 files: `rule/lite-manifest-schema-change-discipline` + `standard/lite-manifest-schema-changes`)
94
+ - **Fields:** 439 entries (unchanged). Content hashes shift on `.claude/rules/lite-manifest-schema-change-discipline.md` (gains a `## Retirements` section covering retirement-class discipline alongside additions) and `standards/lite-manifest-schema-changes.md` (gains the v1.14.0 entry for #1992 that this PR backfilled plus this v1.14.2 entry).
95
+ - **Version bump:** v1.14.1 → v1.14.2 (patch; content-only change on 2 slugs; no schema shape shift; no entry count change).
96
+ - **Rationale:** sunj-labs/bassclef-upstream#523 closed. 4 consecutive main-CI failures 2026-09-29 to 09-30 (SHAs `3c997584`, `02da6b8d`, `924d489f` on `substrate-check-phase`) traced to Test 16 — the Feathers v1.6.1 snapshot guard — seeing 2 entries drop after PR #1992's compat shim retirement. Prior addition-class discipline fired a mechanical ledger-pre-flight gate; retirement-class did not. This PR extends the existing rule with a `## Retirements` section (per Ousterhout contrarian: one rule, two sections) + ships a fixture allowlist pattern (`.claude/hooks/tests/fixtures/2026-09-16-lite-catalog-closure/retired-entries.txt`) + updates Test 16 to subtract allowlist rows before asserting zero drops + backfills the v1.14.0 ledger entry #1992 missed.
97
+ - **Downstream cure:** none — adopters at `~1.14` get the rule content update at next sync. Any adopter authoring a retirement PR going forward follows the `## Retirements` section. Mechanical pre-commit hook (`pre-retire-discipline-check.sh`) deferred to a follow-on PR per Ousterhout (keep this PR scope-tight).
98
+ - **Old shape retired:** none — additive discipline extension.
99
+ - **Protects:** 4 upcoming retirement tickets (#266 S3 → npm move, #108 ADR-020 prototype deprecation, #1142 structural rename sweep, #1999 launch cadence rules) ship under the extended discipline.
100
+
101
+ ### #1992 — compounding-axis compat shim retirement (v1.13.2 → v1.14.0, 2026-09-28)
102
+
103
+ - **Change type:** content-remove (2 entries dropped)
104
+ - **Fields:** 441 entries → 439 entries. Removed entries: `hook/longrun-prep-compounding-axis-check` (`.claude/hooks/longrun-prep-compounding-axis-check.sh`) + `rule/compounding-axis-fresh-analysis` (`.claude/rules/compounding-axis-fresh-analysis.md`). Both were compat shim symlinks from the 2026-07-03 compounding-axis → compounding-sequence rename per PR #611 (bet 2026-07-03b WU-2).
105
+ - **Version bump:** v1.13.2 → v1.14.0 (MINOR — content-remove per bump policy; adopters pinning major stay green, adopters pinning exact version must update).
106
+ - **Rationale:** sunj-labs/bassclef-upstream#1989 — compat shim grace window (ADR-031 default) elapsed on 2026-10-03; harness scanned each symlink twice inflating the rule set at session start. Grep across `standards/bassclef-source-consumers.json` returned zero adopter pins on the old slug, so the shim was defensive dogma rather than adopter-driven. Both symlinks + the duplicate embedding JSON deleted at SHA `9884278f`. Rename targets (`compounding-sequence-fresh-analysis.md` + `longrun-prep-compounding-sequence-check.sh`) stay live under their post-rename names.
107
+ - **Downstream cure:** none for adopters at `~1.13` or `~1.14`. Adopters pinning exact `manifest_version` (bassclef-web#256 + bassclef-cli#51) update pins at the next sync. Any adopter code reading `.entries[]` by legacy `compounding-axis` slug finds nothing — switch reads to `compounding-sequence` sister slugs.
108
+ - **Old shape retired:** immediately. ADR-031 grace window on compat shim ran its full term (2026-07-03 → 2026-10-03); no further compat preservation owed.
109
+ - **Retirement allowlist:** `.claude/hooks/tests/fixtures/2026-09-16-lite-catalog-closure/retired-entries.txt` carries both removed slugs so Test 16 (Feathers v1.6.1 snapshot guard) subtracts them before asserting zero drops. Backfilled by follow-on PR #<this PR> (sunj-labs/bassclef-upstream#523).
110
+
111
+ ### #1777 — friction hook pattern adds stage|launch new skill vocabulary (v1.14.0 → v1.14.1, 2026-10-01)
112
+
113
+ - **Change type:** content-change (1 file: `post-skill-friction-check` hook)
114
+ - **Fields:** 439 entries → 439 entries (no add/remove). Content hash shifts on `post-skill-friction-check` slug (MAJOR_SKILLS_PATTERN regex extended with stage|launch per ADR-040 D1 rename grace window).
115
+ - **Version bump:** v1.14.0 → v1.14.1 (patch — content-only change; no field add/remove; no shape shift).
116
+ - **Rationale:** PR #2028 closes #1777 (friction log never created because hook pattern matched old skill names only). Additive regex extension; grace-window compat preserved (preview|preview-build still match).
117
+ - **Downstream cure:** none — adopters re-sync and the hook starts firing on current-vocabulary chain dispatches. Lazy-create of `docs/session-friction-log.md` happens on first real friction case.
118
+ - **Old shape retired:** old pattern retired `2026-10-31` (grace window end for ADR-040 D1 vocabulary); current pattern keeps both until then.
119
+
37
120
  ### #1961 — persistent statusline for bassclef version + sync status (v1.12.2 → v1.13.0, 2026-09-27)
38
121
 
39
122
  - **Change type:** content-add (1 new entry: `statusline-version` lib) + content-change (2 files: `bassclef-sync` hook + `bassclef-orientation`)