@thebassclef/lite 1.9.8 → 1.9.9

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 (32) 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 +18 -6
  8. package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +10 -6
  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/trace-helper.sh +29 -0
  12. package/dist/lite/.claude/rules/lite-manifest-schema-change-discipline.md +30 -0
  13. package/dist/lite/.claude/skills/build/SKILL.md +19 -7
  14. package/dist/lite/.claude/skills/onboard-repo/SKILL.md +41 -0
  15. package/dist/lite/.claude/skills/release-notes/SKILL.md +24 -0
  16. package/dist/lite/bassclef-version.json +5 -5
  17. package/dist/lite/gitignore +8 -0
  18. package/dist/lite/lib/deferred-actions.sh +4 -4
  19. package/dist/lite/lib/fixture-builder.sh +9 -9
  20. package/dist/lite/lib/identifier-leak-check.sh +85 -0
  21. package/dist/lite/lib/preview-state.sh +20 -20
  22. package/dist/lite/lib/skip-env-inline-check.sh +61 -0
  23. package/dist/lite/lib/state.sh +31 -28
  24. package/dist/lite/lib/tier-check.sh +9 -9
  25. package/dist/lite/lib/tier-dependency-audit.sh +23 -23
  26. package/dist/lite/presence/install/schedule-auto-save.taskscheduler.md +1 -1
  27. package/dist/lite/standards/lite-manifest-schema-changes.md +38 -0
  28. package/dist/lite/standards/lite-manifest.json +38 -38
  29. package/dist/lite/templates/spec-template.md +10 -6
  30. package/package.json +1 -1
  31. package/dist/lite/.claude/hooks/longrun-prep-compounding-axis-check.sh +0 -492
  32. package/dist/lite/.claude/rules/compounding-axis-fresh-analysis.md +0 -173
@@ -49,14 +49,14 @@ ps_exists() {
49
49
 
50
50
  ps_create() {
51
51
  local slug="$1"
52
- local path
53
- path="$(ps_path "$slug")"
54
- mkdir -p "$(dirname "$path")"
55
- if [[ -f "$path" ]]; then
56
- echo "ps_create: already exists: $path" >&2
52
+ local p
53
+ p="$(ps_path "$slug")"
54
+ mkdir -p "$(dirname "$p")"
55
+ if [[ -f "$p" ]]; then
56
+ echo "ps_create: already exists: $p" >&2
57
57
  return 1
58
58
  fi
59
- cat > "$path" <<EOF
59
+ cat > "$p" <<EOF
60
60
  slug: ${slug}
61
61
  created: $(date -u +'%Y-%m-%dT%H:%M:%S+0000')
62
62
  current_tier: pending
@@ -73,16 +73,16 @@ EOF
73
73
  ps_get() {
74
74
  local slug="$1"
75
75
  local field="$2"
76
- local path
77
- path="$(ps_path "$slug")"
78
- [[ -f "$path" ]] || { echo "ps_get: not-found: $path" >&2; return 1; }
76
+ local p
77
+ p="$(ps_path "$slug")"
78
+ [[ -f "$p" ]] || { echo "ps_get: not-found: $p" >&2; return 1; }
79
79
  awk -v f="$field" '
80
80
  $0 ~ "^" f ":" {
81
81
  sub("^" f ":[[:space:]]*", "")
82
82
  print
83
83
  exit
84
84
  }
85
- ' "$path"
85
+ ' "$p"
86
86
  }
87
87
 
88
88
  ps_set() {
@@ -90,9 +90,9 @@ ps_set() {
90
90
  local field="$2"
91
91
  shift 2
92
92
  local value="$*"
93
- local path
94
- path="$(ps_path "$slug")"
95
- [[ -f "$path" ]] || { echo "ps_set: not-found: $path" >&2; return 1; }
93
+ local p
94
+ p="$(ps_path "$slug")"
95
+ [[ -f "$p" ]] || { echo "ps_set: not-found: $p" >&2; return 1; }
96
96
  local tmp
97
97
  tmp="$(mktemp)"
98
98
  awk -v f="$field" -v v="$value" '
@@ -108,8 +108,8 @@ ps_set() {
108
108
  print f ": " v
109
109
  }
110
110
  }
111
- ' "$path" > "$tmp"
112
- mv "$tmp" "$path"
111
+ ' "$p" > "$tmp"
112
+ mv "$tmp" "$p"
113
113
  }
114
114
 
115
115
  ps_status() {
@@ -146,9 +146,9 @@ ps_archive() {
146
146
  ps_reset() {
147
147
  local slug="$1"
148
148
  local reason="${2:-no reason given}"
149
- local path
150
- path="$(ps_path "$slug")"
151
- [[ -f "$path" ]] || { echo "ps_reset: not-found: $path" >&2; return 1; }
149
+ local p
150
+ p="$(ps_path "$slug")"
151
+ [[ -f "$p" ]] || { echo "ps_reset: not-found: $p" >&2; return 1; }
152
152
 
153
153
  local now
154
154
  now="$(date -u +'%Y-%m-%dT%H:%M:%S+0000')"
@@ -188,8 +188,8 @@ EOF
188
188
  next
189
189
  }
190
190
  { print }
191
- ' "$path" > "$tmp"
192
- mv "$tmp" "$path"
191
+ ' "$p" > "$tmp"
192
+ mv "$tmp" "$p"
193
193
  rm -f "$entry_file"
194
194
 
195
195
  # Reset cycle-state fields
@@ -0,0 +1,61 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # SKIP_* env-reach check for PreToolUse Bash hooks.
4
+ #
5
+ # Per #2036 Finding #4 — adopter report 2026-10-02.
6
+ #
7
+ # PreToolUse Bash hooks run in Claude Code's process env. The agent's
8
+ # `SKIP_FOO=1 git commit ...` recipe sets FOO in the git subshell's env,
9
+ # not the hook's env. Hooks that only read `${SKIP_FOO:-0}` miss the
10
+ # inline override and the recipe lies to the adopter.
11
+ #
12
+ # This helper parses tool_input.command for inline `SKIP_FOO=1` and
13
+ # falls back to process env. The adopter's natural recipe
14
+ # `SKIP_FOO=1 cmd` now works regardless of harness env propagation.
15
+ #
16
+ # Retirement path: once Claude Code harness guarantees inline-env
17
+ # propagation to hook subprocesses, this helper becomes redundant.
18
+ # Comment stays as the audit trail.
19
+ #
20
+ # Usage:
21
+ # source "$CLAUDE_PROJECT_DIR/lib/skip-env-inline-check.sh"
22
+ # COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
23
+ # if skip_var_set "SKIP_FOO" "$COMMAND"; then
24
+ # # bypass
25
+ # exit 0
26
+ # fi
27
+ #
28
+ # Anchors:
29
+ # @luminary john-ousterhout — deep module; narrow interface
30
+ # @luminary david-parnas — information hiding; consumer-free of mechanism
31
+ # @luminary alan-cooper — adopter-goal reliable override recipe
32
+
33
+ # skip_var_set — return 0 if the named SKIP_* var is set to "1".
34
+ # Checks two sources:
35
+ # 1. Inline prefix in command string: `SKIP_FOO=1 cmd...`
36
+ # 2. Fallback: process env `$SKIP_FOO`
37
+ # Returns 1 otherwise.
38
+ #
39
+ # Args:
40
+ # $1 — var name (e.g., "SKIP_OPERATOR_PATHS")
41
+ # $2 — command string (from tool_input.command)
42
+ skip_var_set() {
43
+ local var_name="$1"
44
+ local cmd="$2"
45
+
46
+ # Source 1 — inline prefix. The pattern anchors on word boundary (start
47
+ # of string or whitespace) and requires =1 followed by whitespace or
48
+ # end-of-string. This matches bash's own parsing of inline env prefix.
49
+ if [ -n "$cmd" ]; then
50
+ if echo "$cmd" | grep -qE "(^|[[:space:]])${var_name}=1([[:space:]]|$)"; then
51
+ return 0
52
+ fi
53
+ fi
54
+
55
+ # Source 2 — process env fallback (works where harness propagates env).
56
+ if [ "$(eval "echo \"\${${var_name}:-0}\"")" = "1" ]; then
57
+ return 0
58
+ fi
59
+
60
+ return 1
61
+ }
@@ -186,17 +186,19 @@ __state_atomic_write() {
186
186
  # crash caller).
187
187
  __state_yaml_write() {
188
188
  __state_check_binary_deps || return 4
189
- local path="$1"
189
+ # Finding #6 (#2036): `local path` is a zsh-magic variable name — ties to
190
+ # $PATH when sourced from zsh. Renamed to `local p` throughout this file.
191
+ local p="$1"
190
192
  local field="$2"
191
193
  local value="$3"
192
- if [ -z "$path" ] || [ -z "$field" ]; then
194
+ if [ -z "$p" ] || [ -z "$field" ]; then
193
195
  echo "__state_yaml_write: requires <path> <field> <value>" >&2
194
196
  return 2
195
197
  fi
196
198
  local dir
197
- dir="$(dirname "$path")"
199
+ dir="$(dirname "$p")"
198
200
  [ -d "$dir" ] || mkdir -p "$dir" || return 4
199
- local tmp="${path}.tmp.$$"
201
+ local tmp="${p}.tmp.$$"
200
202
  BASSCLEF_YAML_FIELD="$field" \
201
203
  BASSCLEF_YAML_VALUE="$value" \
202
204
  BASSCLEF_YAML_TMP="$tmp" \
@@ -213,7 +215,7 @@ except Exception as e:
213
215
  print(f"__state_yaml_write: safe_dump failed: {e}", file=sys.stderr)
214
216
  sys.exit(3)
215
217
  ' 2>&1 || { rm -f "$tmp"; return 3; }
216
- mv "$tmp" "$path" || { rm -f "$tmp"; return 4; }
218
+ mv "$tmp" "$p" || { rm -f "$tmp"; return 4; }
217
219
  }
218
220
 
219
221
  # === Whereami (Shape A — JSON authoritative + markdown rendered) ===
@@ -239,21 +241,22 @@ state_whereami_get() {
239
241
  state_whereami_set() {
240
242
  local field="$1"
241
243
  local value="$2"
242
- local path="docs/whereami.json"
244
+ local p="docs/whereami.json"
243
245
  if [ -z "$field" ] || [ -z "$value" ]; then
244
246
  echo "state_whereami_set: requires <field> <value>" >&2
245
247
  return 2
246
248
  fi
247
- if [ ! -f "$path" ]; then
249
+ if [ ! -f "$p" ]; then
248
250
  echo "state_whereami_set: docs/whereami.json does not exist" >&2
249
251
  return 4
250
252
  fi
251
253
  local updated
252
- # Use jq's setpath for nested field paths (e.g., 'phase.value')
254
+ # Use jq's setpath for nested field paths (e.g., 'phase.value').
255
+ # The jq `--arg path` names a jq variable (not shell) — safe to keep.
253
256
  updated="$(jq --arg path "$field" --arg val "$value" '
254
257
  setpath($path | split("."); $val)
255
- ' "$path")" || return 3
256
- __state_atomic_write "$path" "$updated"
258
+ ' "$p")" || return 3
259
+ __state_atomic_write "$p" "$updated"
257
260
  }
258
261
 
259
262
  # === IterationBet (Shape B — markdown + YAML frontmatter + body) ===
@@ -264,11 +267,11 @@ state_iteration_bet_get() {
264
267
  echo "state_iteration_bet_get: requires <slug>" >&2
265
268
  return 2
266
269
  fi
267
- local path="docs/iteration-bets/${slug}.md"
268
- if [ ! -f "$path" ]; then
270
+ local p="docs/iteration-bets/${slug}.md"
271
+ if [ ! -f "$p" ]; then
269
272
  return 1
270
273
  fi
271
- __state_yq2json "$path"
274
+ __state_yq2json "$p"
272
275
  }
273
276
 
274
277
  # Special-purpose: returns the single active iteration bet (status=active),
@@ -333,25 +336,25 @@ state_roadmap_get() {
333
336
  echo "state_roadmap_get: requires <slug>" >&2
334
337
  return 2
335
338
  fi
336
- local path="docs/roadmaps/${slug}.md"
337
- if [ ! -f "$path" ]; then
339
+ local p="docs/roadmaps/${slug}.md"
340
+ if [ ! -f "$p" ]; then
338
341
  return 1
339
342
  fi
340
- __state_yq2json "$path"
343
+ __state_yq2json "$p"
341
344
  }
342
345
 
343
346
  # Internal: rewrite a roadmap's frontmatter from JSON, preserving the body.
344
347
  __state_roadmap_write_frontmatter() {
345
348
  local slug="$1"
346
349
  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
350
+ local p="docs/roadmaps/${slug}.md"
351
+ if [ ! -f "$p" ]; then
352
+ echo "__state_roadmap_write_frontmatter: roadmap $p not found" >&2
350
353
  return 1
351
354
  fi
352
355
  python3 -c "
353
356
  import sys, yaml, json
354
- path = '$path'
357
+ path = '$p'
355
358
  fm = json.loads('''$frontmatter_json''')
356
359
  with open(path, 'r') as f:
357
360
  content = f.read()
@@ -726,15 +729,15 @@ state_chronicle_gate_evidence() {
726
729
  return 2
727
730
  fi
728
731
  # Try bassclef (chronicle/) and consumer-repo (docs/chronicle/) paths
729
- local path=""
732
+ local p=""
730
733
  if [ -f "chronicle/${id}.md" ]; then
731
- path="chronicle/${id}.md"
734
+ p="chronicle/${id}.md"
732
735
  elif [ -f "docs/chronicle/${id}.md" ]; then
733
- path="docs/chronicle/${id}.md"
736
+ p="docs/chronicle/${id}.md"
734
737
  else
735
738
  return 1
736
739
  fi
737
- __state_yq2json "$path" | jq -c '.gates_fired // []'
740
+ __state_yq2json "$p" | jq -c '.gates_fired // []'
738
741
  }
739
742
 
740
743
  # List chronicle IDs (filename without .md extension), optionally limited
@@ -1081,16 +1084,16 @@ __state_platform_resolve_path() {
1081
1084
 
1082
1085
  state_platform_get() {
1083
1086
  local key="${1:-}"
1084
- local path
1085
- path="$(__state_platform_resolve_path)"
1086
- if [ ! -f "$path" ]; then
1087
+ local p
1088
+ p="$(__state_platform_resolve_path)"
1089
+ if [ ! -f "$p" ]; then
1087
1090
  return 1
1088
1091
  fi
1089
1092
  local json
1090
1093
  json="$(python3 -c "
1091
1094
  import sys, yaml, json
1092
1095
  try:
1093
- with open('$path') as f:
1096
+ with open('$p') as f:
1094
1097
  data = yaml.safe_load(f) or {}
1095
1098
  print(json.dumps(data, default=str))
1096
1099
  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,44 @@ 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
+ ### #1777 — friction hook pattern adds stage|launch new skill vocabulary (v1.14.2 → v1.14.3, 2026-10-01)
38
+
39
+ - **Change type:** content-change (1 file: `post-skill-friction-check` hook)
40
+ - **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).
41
+ - **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.
42
+ - **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).
43
+ - **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.
44
+ - **Old shape retired:** old pattern retired `2026-10-31` (grace window end for ADR-040 D1 vocabulary); current pattern keeps both until then.
45
+
46
+ ### #523 — retirement discipline + fixture allowlist pattern (v1.14.1 → v1.14.2, 2026-10-01)
47
+
48
+ - **Change type:** content-change (2 files: `rule/lite-manifest-schema-change-discipline` + `standard/lite-manifest-schema-changes`)
49
+ - **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).
50
+ - **Version bump:** v1.14.1 → v1.14.2 (patch; content-only change on 2 slugs; no schema shape shift; no entry count change).
51
+ - **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.
52
+ - **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).
53
+ - **Old shape retired:** none — additive discipline extension.
54
+ - **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.
55
+
56
+ ### #1992 — compounding-axis compat shim retirement (v1.13.2 → v1.14.0, 2026-09-28)
57
+
58
+ - **Change type:** content-remove (2 entries dropped)
59
+ - **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).
60
+ - **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).
61
+ - **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.
62
+ - **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.
63
+ - **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.
64
+ - **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).
65
+
66
+ ### #1777 — friction hook pattern adds stage|launch new skill vocabulary (v1.14.0 → v1.14.1, 2026-10-01)
67
+
68
+ - **Change type:** content-change (1 file: `post-skill-friction-check` hook)
69
+ - **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).
70
+ - **Version bump:** v1.14.0 → v1.14.1 (patch — content-only change; no field add/remove; no shape shift).
71
+ - **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).
72
+ - **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.
73
+ - **Old shape retired:** old pattern retired `2026-10-31` (grace window end for ADR-040 D1 vocabulary); current pattern keeps both until then.
74
+
37
75
  ### #1961 — persistent statusline for bassclef version + sync status (v1.12.2 → v1.13.0, 2026-09-27)
38
76
 
39
77
  - **Change type:** content-add (1 new entry: `statusline-version` lib) + content-change (2 files: `bassclef-sync` hook + `bassclef-orientation`)