@thebassclef/lite 1.1.1 → 1.2.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 (135) hide show
  1. package/dist/cli.cjs +23 -6
  2. package/dist/cli.js +23 -6
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/lite/.claude/bassclef-configs.jsonc +56 -0
  7. package/dist/lite/.claude/bassclef-orientation.md +120 -0
  8. package/dist/lite/.claude/hooks/adr-discipline-check.sh +132 -0
  9. package/dist/lite/.claude/hooks/atomic-pr-check.sh +169 -0
  10. package/dist/lite/.claude/hooks/auto-save-idle.sh +51 -0
  11. package/dist/lite/.claude/hooks/bassclef-sync.sh +51 -13
  12. package/dist/lite/.claude/hooks/bet-doc-gate.sh +283 -0
  13. package/dist/lite/.claude/hooks/bug-diagnosis.sh +64 -0
  14. package/dist/lite/.claude/hooks/destructive-command-guard.sh +136 -0
  15. package/dist/lite/.claude/hooks/file-rename-discipline-check.sh +147 -0
  16. package/dist/lite/.claude/hooks/hook-classifier-canary.sh +158 -0
  17. package/dist/lite/.claude/hooks/kiss-default-check.sh +129 -0
  18. package/dist/lite/.claude/hooks/multi-tenancy-check.sh +83 -0
  19. package/dist/lite/.claude/hooks/oo-ad-entry-point-check.sh +216 -0
  20. package/dist/lite/.claude/hooks/operator-private-path-check.sh +203 -0
  21. package/dist/lite/.claude/hooks/pattern-annotation-validate.sh +142 -0
  22. package/dist/lite/.claude/hooks/plain-english-steering.sh +15 -1
  23. package/dist/lite/.claude/hooks/post-write-trace.sh +66 -0
  24. package/dist/lite/.claude/hooks/pr-body-scrub-check.sh +446 -0
  25. package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +137 -0
  26. package/dist/lite/.claude/hooks/pre-gh-pr-body-scrub.sh +192 -0
  27. package/dist/lite/.claude/hooks/pre-gh-pr-title-scrub.sh +172 -0
  28. package/dist/lite/.claude/hooks/role-entry-gate.sh +115 -0
  29. package/dist/lite/.claude/hooks/save-state.sh +268 -0
  30. package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +8 -1
  31. package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +52 -0
  32. package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +40 -4
  33. package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +12 -5
  34. package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +18 -2
  35. package/dist/lite/.claude/hooks/session-reflection.d/91-npm-version-drift.sh +54 -0
  36. package/dist/lite/.claude/hooks/session-reflection.d/tests/10-abrupt-stop-recovery.test.sh +95 -0
  37. package/dist/lite/.claude/hooks/session-reflection.sh +62 -15
  38. package/dist/lite/.claude/hooks/tool-failure-diagnosis.sh +50 -0
  39. package/dist/lite/.claude/luminaries/andrej-karpathy.md +165 -0
  40. package/dist/lite/.claude/luminaries/dave-gray.md +155 -0
  41. package/dist/lite/.claude/luminaries/gary-klein.md +166 -0
  42. package/dist/lite/.claude/luminaries/kaoru-ishikawa.md +162 -0
  43. package/dist/lite/.claude/luminaries/lean-product-canvas.md +33 -0
  44. package/dist/lite/.claude/luminaries/luke-wroblewski.md +224 -0
  45. package/dist/lite/.claude/luminaries/ryan-singer.md +113 -0
  46. package/dist/lite/.claude/luminaries/steve-krug.md +168 -0
  47. package/dist/lite/.claude/luminaries/tony-ulwick.md +134 -0
  48. package/dist/lite/.claude/rules/arc-level-orientation-before-leaf.md +60 -0
  49. package/dist/lite/.claude/rules/architect-review-discipline.md +71 -0
  50. package/dist/lite/.claude/rules/auto-fix-criteria.md +126 -0
  51. package/dist/lite/.claude/rules/bassclef-hook-drift-blocker.md +144 -0
  52. package/dist/lite/.claude/rules/bassclef-models-discovery.md +107 -0
  53. package/dist/lite/.claude/rules/bassclef-workflow-parity.md +104 -0
  54. package/dist/lite/.claude/rules/cost-governance.md +154 -0
  55. package/dist/lite/.claude/rules/file-rename-discipline.md +95 -0
  56. package/dist/lite/.claude/rules/mechanism-fidelity.md +88 -0
  57. package/dist/lite/.claude/rules/prototype-production-coupling.md +125 -0
  58. package/dist/lite/.claude/rules/quick-fix-criteria.md +90 -0
  59. package/dist/lite/.claude/rules/session-artifacts.md +30 -0
  60. package/dist/lite/.claude/rules/sibling-smoke-after-substrate-change.md +107 -0
  61. package/dist/lite/.claude/rules/substrate-as-system.md +67 -0
  62. package/dist/lite/.claude/rules/usability.md +22 -0
  63. package/dist/lite/.claude/settings.json +20 -0
  64. package/dist/lite/.claude/skills/agent-research-spawn/SKILL.md +100 -0
  65. package/dist/lite/.claude/skills/architect-review/SKILL.md +201 -0
  66. package/dist/lite/.claude/skills/auto-fix/SKILL.md +297 -0
  67. package/dist/lite/.claude/skills/autonomous/SKILL.md +300 -0
  68. package/dist/lite/.claude/skills/bind-subdomain/SKILL.md +232 -0
  69. package/dist/lite/.claude/skills/canvas/SKILL.md +41 -0
  70. package/dist/lite/.claude/skills/deploy-prod/SKILL.md +106 -0
  71. package/dist/lite/.claude/skills/docs-sync/SKILL.md +188 -0
  72. package/dist/lite/.claude/skills/eisenhower/SKILL.md +273 -0
  73. package/dist/lite/.claude/skills/empathy-map/SKILL.md +168 -0
  74. package/dist/lite/.claude/skills/frontend-design/SKILL.md +53 -0
  75. package/dist/lite/.claude/skills/interview-me/SKILL.md +171 -0
  76. package/dist/lite/.claude/skills/journal-export/SKILL.md +293 -0
  77. package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +203 -0
  78. package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +256 -0
  79. package/dist/lite/.claude/skills/pick-luminaries/SKILL.md +100 -0
  80. package/dist/lite/.claude/skills/pre-mortem/SKILL.md +180 -0
  81. package/dist/lite/.claude/skills/prototype-gallery/SKILL.md +264 -0
  82. package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +257 -0
  83. package/dist/lite/.claude/skills/quick-fix/SKILL.md +273 -0
  84. package/dist/lite/.claude/skills/release/SKILL.md +311 -0
  85. package/dist/lite/.claude/skills/release-notes/SKILL.md +202 -0
  86. package/dist/lite/.claude/skills/rfc/SKILL.md +222 -0
  87. package/dist/lite/.claude/skills/riff-prototypes/SKILL.md +637 -0
  88. package/dist/lite/.claude/skills/secrets-bootstrap/SKILL.md +209 -0
  89. package/dist/lite/.claude/skills/skill/SKILL.md +304 -0
  90. package/dist/lite/.claude/skills/smoke-test/SKILL.md +100 -0
  91. package/dist/lite/.claude/skills/state/SKILL.md +204 -0
  92. package/dist/lite/.claude/skills/substrate-check/SKILL.md +218 -0
  93. package/dist/lite/.claude/skills/synthetic-user/SKILL.md +381 -0
  94. package/dist/lite/.claude/skills/task-scenarios/SKILL.md +105 -0
  95. package/dist/lite/.claude/skills/tier-dependency-audit/SKILL.md +139 -0
  96. package/dist/lite/.claude/skills/ux-migration/SKILL.md +424 -0
  97. package/dist/lite/.claude/skills/value-prop-canvas/SKILL.md +297 -0
  98. package/dist/lite/README.md +5 -1
  99. package/dist/lite/gitignore +6 -0
  100. package/dist/lite/lib/bassclef-dir-resolver.sh +85 -0
  101. package/dist/lite/lib/clone-failure-classifier.sh +59 -0
  102. package/dist/lite/lib/fixture-builder.sh +190 -0
  103. package/dist/lite/lib/fresh-install-check.sh +49 -0
  104. package/dist/lite/lib/hook-heartbeat.sh +0 -1
  105. package/dist/lite/lib/mechanism-fidelity.sh +569 -0
  106. package/dist/lite/lib/npm-version-check.sh +112 -0
  107. package/dist/lite/lib/preview-state.sh +229 -0
  108. package/dist/lite/lib/session-rescue.sh +0 -1
  109. package/dist/lite/scripts/generate-lite-manifest.sh +145 -4
  110. package/dist/lite/scripts/lite-manifest-drift-check.sh +5 -1
  111. package/dist/lite/standards/architect-review-discipline.md +222 -0
  112. package/dist/lite/standards/bassclef-platform.md +249 -0
  113. package/dist/lite/standards/bassclef-test-repos.md +97 -0
  114. package/dist/lite/standards/bassclef-wiring-manifest.json +25 -1
  115. package/dist/lite/standards/cold-adopter-harness.md +205 -0
  116. package/dist/lite/standards/data-management.md +135 -0
  117. package/dist/lite/standards/frontend-stack/nextjs-prisma.md +124 -0
  118. package/dist/lite/standards/lite-manifest-schema-changes.md +27 -0
  119. package/dist/lite/standards/lite-manifest.json +1456 -14
  120. package/dist/lite/standards/mechanism-fidelity.md +205 -0
  121. package/dist/lite/standards/operator-private-discipline.md +200 -0
  122. package/dist/lite/standards/operator-to-public-flow.md +195 -0
  123. package/dist/lite/standards/prototype-gallery.md +251 -0
  124. package/dist/lite/standards/pseudonym-key-schema.md +109 -0
  125. package/dist/lite/standards/sibling-validation-checklist.md +181 -0
  126. package/dist/lite/standards/slack-notifications.md +161 -0
  127. package/dist/lite/standards/synthetic-user-testing.md +173 -0
  128. package/dist/lite/standards/vendor-bindings.md +165 -0
  129. package/dist/lite/standards/vendor-plugins/frontend-design.md +66 -0
  130. package/dist/lite/standards/vpc-fit-validation.md +167 -0
  131. package/dist/lite/templates/gallery-yml.md +113 -0
  132. package/dist/lite/templates/product-canvas.md +169 -0
  133. package/dist/lite/templates/value-prop-canvas.md +180 -0
  134. package/package.json +3 -3
  135. /package/dist/lite/{whereami.md → docs/whereami.md} +0 -0
@@ -0,0 +1,446 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # install-class: project
4
+ # PR-body scrub check — Rule 1 (jargon) + Rule 4 (operator-private +
5
+ # strategic-deliberation) + Rule 5 (ADVISE-PHRASE advisory) + Rule 6
6
+ # (Problem-section opener shape, bassclef#1178, advisory in V1).
7
+ # Per bassclef#739 substrate-coherence CI extension.
8
+ #
9
+ # Rule 1: blocks PR if bassclef-internal jargon found in PR body.
10
+ # Jargon list: composer, primitive, tier-preset, load-bearing, blast radius,
11
+ # compose-with, scope-bounded, operationalize.
12
+ # Allowlist (per bassclef#763): substrate, bassclef, cameo, Studio, Band, bandleader.
13
+ #
14
+ # Rule 4: blocks PR if scrub criteria 1+2 violated per bassclef#787:
15
+ # Criterion 1 — no docs/operator-private/ path citations
16
+ # Criterion 2 — no operator-strategic deliberation framing
17
+ # (heuristics: "leap-of-faith", "LF[0-9]", "F-[A-Z][0-9]", "canvas walk surfaced",
18
+ # "audit finding F-", "persona divergence", "strategic pivot reasoning")
19
+ #
20
+ # Shared override: PR labeled `bassclef-internal-prose-ok` downgrades to advisory.
21
+ #
22
+ # Usage:
23
+ # bash pr-body-scrub-check.sh <pr-body-file>
24
+ #
25
+ # Optional env:
26
+ # PR_LABELS=label1,label2,... — labels on the PR (newline OR comma separated)
27
+ # STRICT_MODE=1 — force blocking even with override label (testing)
28
+ #
29
+ # Exit codes:
30
+ # 0 — pass (no findings, OR findings + override label present)
31
+ # 1 — block (findings + no override)
32
+ # 2 — usage error
33
+
34
+ set +e
35
+
36
+ BODY_FILE="${1:-}"
37
+ if [ -z "$BODY_FILE" ] || [ ! -f "$BODY_FILE" ]; then
38
+ echo "usage: $0 <pr-body-file>" >&2
39
+ exit 2
40
+ fi
41
+
42
+ PR_LABELS="${PR_LABELS:-}"
43
+ STRICT_MODE="${STRICT_MODE:-0}"
44
+
45
+ # Normalize labels (newline or comma separated → lines)
46
+ LABELS_NORMALIZED=$(echo "$PR_LABELS" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | grep -v '^$' || true)
47
+
48
+ # Check override label
49
+ HAS_OVERRIDE=0
50
+ if [ "$STRICT_MODE" != "1" ]; then
51
+ if echo "$LABELS_NORMALIZED" | grep -qx "bassclef-internal-prose-ok"; then
52
+ HAS_OVERRIDE=1
53
+ fi
54
+ fi
55
+
56
+ # Strip code blocks + inline backtick spans + URLs + bare file paths so they
57
+ # don't trigger false positives (filenames, function names, etc. are technical
58
+ # refs not prose).
59
+ # Bare file path strip added per sunj-labs/bassclef-upstream#1581 — a file
60
+ # path in a bullet list without backticks (e.g., `- standards/composer-foo.md`)
61
+ # used to trip the jargon scan on substrings within the file name.
62
+ # Path pattern matches known bassclef-substrate trees plus common extensions.
63
+ SCRUBBED=$(awk '
64
+ BEGIN { in_code = 0 }
65
+ /^```/ { in_code = !in_code; next }
66
+ in_code { next }
67
+ { print }
68
+ ' "$BODY_FILE" \
69
+ | sed -E 's/`[^`]*`//g' \
70
+ | sed -E 's|https?://[^ )]+||g' \
71
+ | sed -E 's|\.claude/[A-Za-z0-9_./-]+||g' \
72
+ | sed -E 's|standards/[A-Za-z0-9_./-]+||g' \
73
+ | sed -E 's|lib/[A-Za-z0-9_./-]+||g' \
74
+ | sed -E 's|scripts/[A-Za-z0-9_./-]+||g' \
75
+ | sed -E 's|docs/[A-Za-z0-9_./-]+||g' \
76
+ | sed -E 's|chronicle/[A-Za-z0-9_./-]+||g' \
77
+ | sed -E 's|presence/[A-Za-z0-9_./-]+||g' \
78
+ | sed -E 's|templates/[A-Za-z0-9_./-]+||g' \
79
+ | sed -E 's|architecture/[A-Za-z0-9_./-]+||g' \
80
+ | sed -E 's|state/[A-Za-z0-9_./-]+||g')
81
+
82
+ # === Rule 1 — jargon detection ===
83
+ # Allowlist per bassclef#763: substrate, bassclef, cameo, Studio, Band, bandleader
84
+ # (Excluded from the blocking list even though "substrate" appears in /kiss table.)
85
+ #
86
+ # Per bassclef#907 Part 1: BLOCK terms read from standards/bassclef-internal-jargon.md
87
+ # (single source of truth shared with /kiss skill + pre-push hook). Falls back to
88
+ # hardcoded list if standards file missing (defensive backward compat).
89
+ SCRIPT_DIR_PR_SCRUB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
90
+ STANDARDS_FILE="${KISS_STANDARDS_FILE:-${SCRIPT_DIR_PR_SCRUB}/../../standards/bassclef-internal-jargon.md}"
91
+
92
+ # Surface-context flag (bassclef#1241 WU-2). When set to "title" or "commit",
93
+ # also read BLOCK-title terms (surface-specific blocking). Unset or "body" =
94
+ # legacy behavior (BLOCK terms only, no BLOCK-title).
95
+ SCRUB_SURFACE="${SCRUB_SURFACE:-body}"
96
+
97
+ # awk extractor (shared) — strips parenthetical context ` (...)` from term column
98
+ # at parse time so rows like `seed (verb)` extract as `seed`. Robust against
99
+ # terms ending in `:` (e.g., `substrate-defect:`) via the `:?` in the term regex.
100
+ extract_terms() {
101
+ local tier="$1"
102
+ local file="$2"
103
+ awk -F'|' -v tier="$tier" '
104
+ {
105
+ # Build a regex that matches the tier exactly at the end of line
106
+ tier_pat = " " tier " *\\|$"
107
+ if ($0 ~ "^\\| [a-zA-Z0-9][a-zA-Z0-9 :+./-]* *\\|" && $0 ~ tier_pat) {
108
+ term = $2
109
+ sub(/^ +/, "", term)
110
+ sub(/ +$/, "", term)
111
+ # Strip parenthetical context: "term (note)" → "term"
112
+ sub(/ +\(.*\)$/, "", term)
113
+ print term
114
+ }
115
+ }
116
+ ' "$file"
117
+ }
118
+
119
+ if [ -f "$STANDARDS_FILE" ]; then
120
+ # Read BLOCK-tier terms (fire on all surfaces)
121
+ JARGON_TERMS=()
122
+ while IFS= read -r line; do
123
+ [ -n "$line" ] && JARGON_TERMS+=("$line")
124
+ done < <(extract_terms "BLOCK" "$STANDARDS_FILE")
125
+
126
+ # Read BLOCK-title terms only when surface is title or commit
127
+ if [ "$SCRUB_SURFACE" = "title" ] || [ "$SCRUB_SURFACE" = "commit" ]; then
128
+ while IFS= read -r line; do
129
+ [ -n "$line" ] && JARGON_TERMS+=("$line")
130
+ done < <(extract_terms "BLOCK-title" "$STANDARDS_FILE")
131
+ fi
132
+
133
+ if [ ${#JARGON_TERMS[@]} -eq 0 ]; then
134
+ # Empty result (malformed standards file) → fall through to backward-compat list
135
+ JARGON_TERMS=(
136
+ "composer"
137
+ "primitive"
138
+ "tier-preset"
139
+ "load-bearing"
140
+ "blast radius"
141
+ "compose-with"
142
+ "scope-bounded"
143
+ "operationalize"
144
+ )
145
+ fi
146
+ else
147
+ # Backward compat fallback
148
+ JARGON_TERMS=(
149
+ "composer"
150
+ "primitive"
151
+ "tier-preset"
152
+ "load-bearing"
153
+ "blast radius"
154
+ "compose-with"
155
+ "scope-bounded"
156
+ "operationalize"
157
+ )
158
+ fi
159
+
160
+ JARGON_FINDINGS=""
161
+ JARGON_COUNT=0
162
+ for term in "${JARGON_TERMS[@]}"; do
163
+ # Soft-boundary matcher (bassclef#1241 WU-2):
164
+ # - Terms ending in alphanumeric use word-boundary (\b) — backward compat
165
+ # - Terms ending in non-alphanumeric (e.g., `substrate-defect:`) use a
166
+ # character-class boundary that treats hyphen, underscore, and alnum
167
+ # as "in-word" — so `substrate-defect:` matches at "start of token"
168
+ # contexts (start of line, after whitespace, etc.)
169
+ last_char="${term: -1}"
170
+ first_char="${term:0:1}"
171
+ if [[ "$last_char" =~ [a-zA-Z0-9] ]] && [[ "$first_char" =~ [a-zA-Z0-9] ]]; then
172
+ pat="\\b${term}\\b"
173
+ else
174
+ # At least one boundary is non-alnum — use soft boundary
175
+ prefix='(^|[^[:alnum:]_-])'
176
+ suffix='([^[:alnum:]_-]|$)'
177
+ [[ "$first_char" =~ [a-zA-Z0-9] ]] && prefix='\b'
178
+ [[ "$last_char" =~ [a-zA-Z0-9] ]] && suffix='\b'
179
+ pat="${prefix}${term}${suffix}"
180
+ fi
181
+ count=$(echo "$SCRUBBED" | grep -ioE "$pat" | wc -l | tr -d ' ')
182
+ if [ "$count" -gt 0 ]; then
183
+ JARGON_FINDINGS="${JARGON_FINDINGS}- \`${term}\` (${count}x)"$'\n'
184
+ JARGON_COUNT=$((JARGON_COUNT + count))
185
+ fi
186
+ done
187
+
188
+ # === Rule 4 Criterion 1 — operator-private path citations ===
189
+ # Read from raw body file — SCRUBBED strips bare file paths (bassclef-upstream#1581)
190
+ # which would mask operator-private path citations. Rule 4 checks all citations
191
+ # regardless of code-block or path context — leaks are leaks.
192
+ PATH_FINDINGS=""
193
+ PATH_HITS=$(cat "$BODY_FILE" | grep -oE 'docs/operator-private/[A-Za-z0-9_./-]+' | sort -u || true)
194
+ PATH_COUNT=0
195
+ if [ -n "$PATH_HITS" ]; then
196
+ while IFS= read -r path; do
197
+ [ -z "$path" ] && continue
198
+ PATH_FINDINGS="${PATH_FINDINGS}- \`${path}\`"$'\n'
199
+ PATH_COUNT=$((PATH_COUNT + 1))
200
+ done <<< "$PATH_HITS"
201
+ fi
202
+
203
+ # === Rule 4 Criterion 2 — operator-strategic deliberation framing ===
204
+ # Heuristics that signal strategic deliberation in PR bodies:
205
+ # - "leap-of-faith" / "leap of faith"
206
+ # - "LF[0-9]" / "LF-[0-9]" (e.g., LF1, LF-6)
207
+ # - "F-[A-Z][0-9]" (e.g., F-D9.1) — audit finding IDs are fine to cite
208
+ # by ID, but ONLY by ID without exposition.
209
+ # We don't flag F-X.Y refs themselves; we flag the
210
+ # EXPLANATORY framing around them.
211
+ # - "canvas walk surfaced" (canvas analysis exposition)
212
+ # - "audit finding" (only flagged if appears with multi-sentence framing)
213
+ # - "persona divergence" (operator-strategic analysis)
214
+ # - "strategic pivot" (strategic reasoning)
215
+ # - "IP-protection-vs" (specific pivot vocabulary)
216
+ # - "riskiest assumption" (canvas-derived strategic vocabulary)
217
+ DELIB_PATTERNS=(
218
+ "leap[ -]of[ -]faith"
219
+ "\bLF-?[0-9]+\b"
220
+ "canvas walk surfaced"
221
+ "persona divergence"
222
+ "strategic pivot"
223
+ "IP-protection-vs"
224
+ "riskiest assumption"
225
+ )
226
+
227
+ DELIB_FINDINGS=""
228
+ DELIB_COUNT=0
229
+ for pattern in "${DELIB_PATTERNS[@]}"; do
230
+ hits=$(echo "$SCRUBBED" | grep -iEc "$pattern" || true)
231
+ if [ "$hits" -gt 0 ]; then
232
+ DELIB_FINDINGS="${DELIB_FINDINGS}- pattern \`${pattern}\` (${hits}x)"$'\n'
233
+ DELIB_COUNT=$((DELIB_COUNT + hits))
234
+ fi
235
+ done
236
+
237
+ # === Rule 5 — phrase-shaped substrate-speak (ADVISORY ONLY per bassclef#915) ===
238
+ # Word-shaped Rule 1 catches individual jargon terms. Compound noun-phrases
239
+ # pass Rule 1 (each individual word is plain) but read as substrate-internal.
240
+ # Phrases are read from standards file ADVISE-PHRASE tier. Advisory only —
241
+ # emits CI annotation, does not block. bassclef#915 V2 considers promoting
242
+ # to blocking after operator-feedback cycle.
243
+ PHRASE_TERMS=()
244
+ if [ -f "$STANDARDS_FILE" ]; then
245
+ while IFS= read -r line; do
246
+ PHRASE_TERMS+=("$line")
247
+ done < <(awk -F'|' '
248
+ /^\| [a-zA-Z]/ && /ADVISE-PHRASE *\|$/ {
249
+ gsub(/^ +| +$/, "", $2)
250
+ print $2
251
+ }
252
+ ' "$STANDARDS_FILE")
253
+ fi
254
+
255
+ PHRASE_FINDINGS=""
256
+ PHRASE_COUNT=0
257
+ for phrase in "${PHRASE_TERMS[@]}"; do
258
+ [ -z "$phrase" ] && continue
259
+ # Case-insensitive plain substring match (phrases may include hyphens/spaces)
260
+ count=$(echo "$SCRUBBED" | grep -io -- "${phrase}" | wc -l | tr -d ' ')
261
+ if [ "$count" -gt 0 ]; then
262
+ PHRASE_FINDINGS="${PHRASE_FINDINGS}- \`${phrase}\` (${count}x)"$'\n'
263
+ PHRASE_COUNT=$((PHRASE_COUNT + count))
264
+ fi
265
+ done
266
+
267
+ # === Rule 6 — Problem section opener (bassclef#1178, ADVISORY) ===
268
+ # Per bassclef#1178: PR body should open with **Problem (≤500 chars):**
269
+ # followed by --- and ## Goal. Advisory in V1; may flip to blocking after
270
+ # adopters get used to the new shape.
271
+ PROBLEM_PRESENT=0
272
+ if grep -qE '^\*\*Problem' "$BODY_FILE"; then
273
+ PROBLEM_PRESENT=1
274
+ fi
275
+
276
+ # === Rule 7 — Closes-keyword check (bassclef#974, ADVISORY) ===
277
+ # When title carries feat(#NNN): or fix(#NNN): pattern, body should carry
278
+ # Closes / Fixes / Resolves keyword so GitHub auto-closes the ticket on
279
+ # merge. Ticket state and shipped state can otherwise drift silently.
280
+ # Reads $PR_TITLE env var (passed by pre-gh-pr-body-scrub.sh wrapper).
281
+ # Advisory in V1; may flip to blocking after calibration.
282
+ CLOSES_ADVISORY=""
283
+ PR_TITLE="${PR_TITLE:-}"
284
+ if [ -n "$PR_TITLE" ]; then
285
+ TITLE_TICKET=$(echo "$PR_TITLE" | grep -oE '(feat|fix)\((bassclef)?#[0-9]+\)' | grep -oE '#[0-9]+' | head -1)
286
+ if [ -n "$TITLE_TICKET" ]; then
287
+ if ! grep -qiE "(closes|fixes|resolves)[[:space:]]+$TITLE_TICKET" "$BODY_FILE"; then
288
+ CLOSES_ADVISORY="title carries \`${TITLE_TICKET}\` in feat/fix pattern but body has no \`Closes ${TITLE_TICKET}\` / \`Fixes ${TITLE_TICKET}\` / \`Resolves ${TITLE_TICKET}\` keyword. Without a closing keyword in the body, GitHub will not auto-close the ticket on merge — the ticket stays open even after the PR ships. Add \`Closes ${TITLE_TICKET}\` to the body Refs section, or use \`SKIP_PRE_GH_PR_BODY_SCRUB=1\` if this PR intentionally does not close the ticket."
289
+ fi
290
+ fi
291
+ fi
292
+
293
+ LEGACY_SHAPE=0
294
+ if grep -qE '^\*\*Flash' "$BODY_FILE" && grep -qE '^\*\*Tweet' "$BODY_FILE"; then
295
+ LEGACY_SHAPE=1
296
+ fi
297
+
298
+ SHAPE_ADVISORY=""
299
+ if [ "$PROBLEM_PRESENT" = "0" ]; then
300
+ if [ "$LEGACY_SHAPE" = "1" ]; then
301
+ SHAPE_ADVISORY="legacy Flash + Tweet opener present; bassclef#1178 retired the pair in favor of a single Problem section. Use /state-a-problem brief to draft the new shape (runs /kiss --grade10 inline by default)."
302
+ else
303
+ SHAPE_ADVISORY="no Problem section detected at opener. Per .claude/rules/pr-body-shape.md (bassclef#1178), feature-scope PRs open with **Problem (≤500 chars):** drafted by /state-a-problem brief. Trivial fix PRs (under 3 files, no SKILL.md or rule changes) may skip."
304
+ fi
305
+ fi
306
+
307
+ # === Report ===
308
+ TOTAL_FINDINGS=$((JARGON_COUNT + PATH_COUNT + DELIB_COUNT))
309
+
310
+ if [ "$TOTAL_FINDINGS" -eq 0 ]; then
311
+ if [ "$PHRASE_COUNT" -eq 0 ] && [ -z "$SHAPE_ADVISORY" ] && [ -z "$CLOSES_ADVISORY" ]; then
312
+ echo "PASS — PR body clean (Rule 1 + Rule 4 + Rule 5 + Rule 6 + Rule 7 scrub criteria)"
313
+ exit 0
314
+ fi
315
+ # Rule 1 + Rule 4 clean, but Rule 5 phrase hits or Rule 6 shape note or Rule 7 closes advisory — emit + exit 0
316
+ echo "PASS — PR body clean (Rule 1 + Rule 4). Advisory hits below."
317
+ echo ""
318
+ if [ "$PHRASE_COUNT" -gt 0 ]; then
319
+ echo "## Rule 5 — phrase-shaped substrate-speak (${PHRASE_COUNT} matches, advisory only)"
320
+ echo ""
321
+ echo "$PHRASE_FINDINGS"
322
+ echo ""
323
+ echo "Compound substrate-speak passes word-shaped Rule 1 but reads as substrate-internal."
324
+ echo "Plain alternatives per standards/bassclef-internal-jargon.md ADVISE-PHRASE tier:"
325
+ echo "- \`pipeline altitude\` → \`pipeline\` (drop altitude)"
326
+ echo "- \`atomic discipline\` → \`one-PR-per-concern\` (or \`one concern per PR\`)"
327
+ echo "- \`operator-attention\` / \`operator-attended\` → \`operator review\` / \`operator-reviewed\`"
328
+ echo "- \`honest case\` → \`valid case\` (or omit)"
329
+ echo "- \`forcing function\` → \`what forces the answer\`"
330
+ echo ""
331
+ echo "Advisory only — does not block merge. Per bassclef#915."
332
+ echo ""
333
+ fi
334
+ if [ -n "$SHAPE_ADVISORY" ]; then
335
+ echo "## Rule 6 — Problem-section opener shape (advisory)"
336
+ echo ""
337
+ echo "${SHAPE_ADVISORY}"
338
+ echo ""
339
+ echo "Per .claude/rules/pr-body-shape.md (bassclef#1178). Advisory only in V1."
340
+ echo ""
341
+ fi
342
+ if [ -n "$CLOSES_ADVISORY" ]; then
343
+ echo "## Rule 7 — ticket-close keyword (advisory)"
344
+ echo ""
345
+ echo "${CLOSES_ADVISORY}"
346
+ echo ""
347
+ echo "Per .claude/rules/pr-body-shape.md (bassclef#974). Advisory only in V1."
348
+ echo ""
349
+ fi
350
+ exit 0
351
+ fi
352
+
353
+ # Findings exist — report
354
+ echo "==============================================="
355
+ echo "PR-body scrub findings (bassclef#739 Rule 1 + Rule 4)"
356
+ echo "==============================================="
357
+ echo ""
358
+
359
+ if [ -n "$JARGON_FINDINGS" ]; then
360
+ echo "## Rule 1 — bassclef-internal jargon (${JARGON_COUNT} matches)"
361
+ echo ""
362
+ echo "$JARGON_FINDINGS"
363
+ echo ""
364
+ echo "Plain-English alternatives per /kiss substitution table:"
365
+ echo "- \`composer\` → \`runs\` or \`chains\` or \`combines\`"
366
+ echo "- \`primitive\` → \`building block\`"
367
+ echo "- \`tier-preset\` → \`preset\` or \`size\`"
368
+ echo "- \`load-bearing\` → \`required\` or \`essential\`"
369
+ echo "- \`blast radius\` → \`impact\` or \`what it can break\`"
370
+ echo "- \`compose-with\` → \`uses\` or \`builds on\`"
371
+ echo "- \`scope-bounded\` → \`small\` or \`tight\`"
372
+ echo "- \`operationalize\` → \`do\` or \`ship\`"
373
+ if [ "$SCRUB_SURFACE" = "title" ] || [ "$SCRUB_SURFACE" = "commit" ]; then
374
+ echo "- \`substrate-defect:\` (as title or commit prefix) → \`fix:\` (per commit-conventions.md — use the 8 standard types)"
375
+ fi
376
+ echo ""
377
+ fi
378
+
379
+ if [ -n "$PATH_FINDINGS" ]; then
380
+ echo "## Rule 4 Criterion 1 — operator-private path citations (${PATH_COUNT} matches)"
381
+ echo ""
382
+ echo "$PATH_FINDINGS"
383
+ echo ""
384
+ echo "Operator-private paths are gitignored; citations point at files invisible"
385
+ echo "to public readers. Use stub framing per standards/pr-body-discipline.md:"
386
+ echo " Avoid: \`per docs/operator-private/strategy/...md\`"
387
+ echo " Use: \`per internal strategic decision\`"
388
+ echo ""
389
+ fi
390
+
391
+ if [ -n "$DELIB_FINDINGS" ]; then
392
+ echo "## Rule 4 Criterion 2 — operator-strategic deliberation framing (${DELIB_COUNT} matches)"
393
+ echo ""
394
+ echo "$DELIB_FINDINGS"
395
+ echo ""
396
+ echo "PR bodies frame mechanically: what the PR does + which acceptance criterion"
397
+ echo "it closes + which downstream work it unblocks. Strategic deliberation belongs"
398
+ echo "in operator-private canvas/audit, not public PR body."
399
+ echo " Avoid: \"LF1 Morgan-as-gatekeeper riskiest assumption analysis\""
400
+ echo " Use: \"Closes audit finding F-D9.1 (README hedges audience).\""
401
+ echo ""
402
+ fi
403
+
404
+ if [ -n "$PHRASE_FINDINGS" ]; then
405
+ echo "## Rule 5 — phrase-shaped substrate-speak (${PHRASE_COUNT} matches, ADVISORY)"
406
+ echo ""
407
+ echo "$PHRASE_FINDINGS"
408
+ echo ""
409
+ echo "Compound substrate-speak passes word-shaped Rule 1 but reads as substrate-internal."
410
+ echo "Plain alternatives per standards/bassclef-internal-jargon.md ADVISE-PHRASE tier."
411
+ echo "Advisory only — does not contribute to block decision (per bassclef#915)."
412
+ echo ""
413
+ fi
414
+
415
+ if [ -n "$SHAPE_ADVISORY" ]; then
416
+ echo "## Rule 6 — Problem-section opener shape (advisory)"
417
+ echo ""
418
+ echo "${SHAPE_ADVISORY}"
419
+ echo ""
420
+ echo "Per .claude/rules/pr-body-shape.md (bassclef#1178). Advisory only in V1."
421
+ echo ""
422
+ fi
423
+
424
+ if [ -n "$CLOSES_ADVISORY" ]; then
425
+ echo "## Rule 7 — ticket-close keyword (advisory)"
426
+ echo ""
427
+ echo "${CLOSES_ADVISORY}"
428
+ echo ""
429
+ echo "Per .claude/rules/pr-body-shape.md (bassclef#974). Advisory only in V1."
430
+ echo ""
431
+ fi
432
+
433
+ echo "==============================================="
434
+ echo "Override path: add label \`bassclef-internal-prose-ok\` to the PR."
435
+ echo "This downgrades the check to advisory + lets the PR merge."
436
+ echo "Use sparingly; the label indicates conscious operator choice to keep"
437
+ echo "bassclef-internal vocabulary or operator-private framing in this PR."
438
+ echo "==============================================="
439
+
440
+ if [ "$HAS_OVERRIDE" -eq 1 ]; then
441
+ echo ""
442
+ echo "ADVISORY — \`bassclef-internal-prose-ok\` label present; downgraded from blocking."
443
+ exit 0
444
+ fi
445
+
446
+ exit 1
@@ -0,0 +1,137 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # install-class: dual
4
+ # PreToolUse: Bash
5
+ # andon-allow: [A-Za-z][A-Za-z0-9]*-MacBook-(Air|Pro)
6
+ # andon-allow: /Users/[a-z][a-z0-9._-]+
7
+ # andon-allow: /home/[a-z][a-z0-9._-]+
8
+ #
9
+ # pre-commit-identifier-leak-scrub.sh — Pre-commit hook that BLOCKs commits
10
+ # carrying operator machine identifiers in commit messages or staged content.
11
+ #
12
+ # Pattern classes:
13
+ # 1. Hostname shapes — [A-Za-z]+-MacBook-(Air|Pro), DESKTOP-[A-Z0-9]+,
14
+ # *-server-*, *-desktop-* (macOS + Windows + Linux common patterns)
15
+ # 2. Absolute paths — /Users/<name>, /home/<name>
16
+ #
17
+ # Fires on PreToolUse Bash matcher when tool_input.command starts with
18
+ # `git commit`. Non-git-commit commands pass through cleanly.
19
+ #
20
+ # Failure modes covered:
21
+ # - R-N1 (Nygard) grep missing → fail-soft exit 0 with stderr warning
22
+ # - R-L1 (Linus) legitimate content → SKIP_IDENTIFIER_LEAK_SCRUB=1 bypass
23
+ # - R-F1 (Feathers) git commit -F file → reads assembled COMMIT_EDITMSG
24
+ # (deferred: current hook parses -m arg only; -F support in V2)
25
+ #
26
+ # Closes sunj-labs/bassclef#1471 item 5b.
27
+ #
28
+ # Anchors:
29
+ # @luminary jerome-saltzer-and-michael-schroeder — complete mediation
30
+ # @luminary linus-torvalds — adopter contract per ADR-031
31
+ # @luminary michael-feathers — characterization tests pin behavior
32
+ # @luminary michael-nygard — fail-soft stability
33
+
34
+ set -u
35
+
36
+ # Override path — logged via trace-helper per bassclef discipline
37
+ if [ "${SKIP_IDENTIFIER_LEAK_SCRUB:-0}" = "1" ]; then
38
+ echo " identifier-leak-scrub: SKIP_IDENTIFIER_LEAK_SCRUB=1 — bypassing scrub" >&2
39
+ exit 0
40
+ fi
41
+
42
+ # R-N1 fail-soft: if grep is unavailable, warn to stderr and exit 0
43
+ if ! command -v grep >/dev/null 2>&1; then
44
+ echo " identifier-leak-scrub: warn — grep unavailable; skipping scrub" >&2
45
+ exit 0
46
+ fi
47
+
48
+ # Read the Claude Code Bash tool JSON from stdin
49
+ INPUT=$(cat 2>/dev/null || echo "")
50
+ if [ -z "$INPUT" ]; then
51
+ exit 0
52
+ fi
53
+
54
+ # Extract command (fallback grep if jq unavailable — jq is standard on bassclef machines)
55
+ if command -v jq >/dev/null 2>&1; then
56
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
57
+ else
58
+ # Bash-native extraction: find "command":"..." in the JSON blob
59
+ COMMAND=$(echo "$INPUT" | grep -oE '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/^"command"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
60
+ fi
61
+
62
+ # Pass through non-git-commit commands
63
+ if ! echo "$COMMAND" | grep -qE '^[[:space:]]*git[[:space:]]+commit'; then
64
+ exit 0
65
+ fi
66
+
67
+ # Pattern set — loaded from single source of truth (bassclef-upstream#1461).
68
+ # Same file scripts/release-to-bassclef.sh andon scan reads — both gates share one truth.
69
+ PATTERN_FILE="${PATTERN_FILE_OVERRIDE:-${CLAUDE_PROJECT_DIR:-.}/standards/identifier-leak-patterns.txt}"
70
+ if [ -f "$PATTERN_FILE" ]; then
71
+ # Join non-comment lines with | into one grep -E pattern
72
+ IDENTIFIER_PATTERNS=$(grep -v '^#' "$PATTERN_FILE" 2>/dev/null | sed '/^$/d' | tr '\n' '|' | sed 's/|$//')
73
+ IDENTIFIER_PATTERNS="($IDENTIFIER_PATTERNS)"
74
+ else
75
+ # Fallback if standards file missing (defensive; fail-soft to prior inline pattern)
76
+ IDENTIFIER_PATTERNS='([A-Za-z][A-Za-z0-9]*-MacBook-(Air|Pro)|DESKTOP-[A-Z0-9]{4,}|/Users/[a-z][a-z0-9._-]+|/home/[a-z][a-z0-9._-]+)'
77
+ fi
78
+
79
+ BLOCK_REASON=""
80
+ BLOCK_MATCH=""
81
+
82
+ # Check 1 — commit message text (parse -m "..." or -m '...' argument)
83
+ MSG=$(echo "$COMMAND" | grep -oE '\-m[[:space:]]*("[^"]*"|'"'"'[^'"'"']*'"'"')' | head -1 | sed -E 's/^-m[[:space:]]*["'"'"']//' | sed -E 's/["'"'"']$//')
84
+ if [ -n "$MSG" ]; then
85
+ MATCH=$(echo "$MSG" | grep -oE "$IDENTIFIER_PATTERNS" | head -1)
86
+ if [ -n "$MATCH" ]; then
87
+ BLOCK_REASON="commit message"
88
+ BLOCK_MATCH="$MATCH"
89
+ fi
90
+ fi
91
+
92
+ # Check 2 — staged content (git diff --cached, ADDED lines only)
93
+ # Filter to '+' lines only so a commit that REMOVES a pre-existing leak
94
+ # does not trip on the removed-content line. `+++` file headers stripped.
95
+ # Per sunj-labs/bassclef-upstream#1575.
96
+ if [ -z "$BLOCK_REASON" ]; then
97
+ STAGED_DIFF=$(git diff --cached 2>/dev/null | grep '^+' | grep -v '^+++' || echo "")
98
+ if [ -n "$STAGED_DIFF" ]; then
99
+ MATCH=$(echo "$STAGED_DIFF" | grep -oE "$IDENTIFIER_PATTERNS" | head -1)
100
+ if [ -n "$MATCH" ]; then
101
+ BLOCK_REASON="staged content"
102
+ BLOCK_MATCH="$MATCH"
103
+ fi
104
+ fi
105
+ fi
106
+
107
+ # BLOCK if any pattern matched
108
+ if [ -n "$BLOCK_REASON" ]; then
109
+ cat >&2 <<EOF
110
+ ============================================
111
+ 🛑 IDENTIFIER LEAK — BLOCKED
112
+ ============================================
113
+
114
+ Location: $BLOCK_REASON
115
+ Pattern: $BLOCK_MATCH
116
+
117
+ Bassclef discipline (per CCF-3 + sunj-labs/bassclef#1471) forbids
118
+ operator machine identifiers in committed artifacts. Detected patterns:
119
+
120
+ - Hostname shapes: X-MacBook-Air, X-MacBook-Pro, DESKTOP-XYZ, etc.
121
+ - Absolute paths: /Users/<name>, /home/<name>
122
+
123
+ Resolution paths:
124
+ 1. Rewrite the commit message or staged content to drop the identifier
125
+ - Replace machine names with placeholders like <machine> or the branch
126
+ - Replace absolute paths with relative paths or \$HOME references
127
+ 2. Override (rare — for legitimate cases like DON'T-do-this documentation):
128
+ SKIP_IDENTIFIER_LEAK_SCRUB=1 git commit ...
129
+
130
+ See .claude/rules/identifier-leak-prevention.md for full discipline.
131
+ ============================================
132
+ EOF
133
+ exit 2
134
+ fi
135
+
136
+ # All clean — pass through
137
+ exit 0