@thebassclef/lite 1.1.0 → 1.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +258 -63
- package/dist/cli.js +258 -63
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/bassclef-configs.jsonc +56 -0
- package/dist/lite/.claude/bassclef-orientation.md +120 -0
- package/dist/lite/.claude/hooks/adr-discipline-check.sh +132 -0
- package/dist/lite/.claude/hooks/atomic-pr-check.sh +169 -0
- package/dist/lite/.claude/hooks/auto-save-idle.sh +51 -0
- package/dist/lite/.claude/hooks/bassclef-sync.sh +51 -13
- package/dist/lite/.claude/hooks/bet-doc-gate.sh +283 -0
- package/dist/lite/.claude/hooks/bug-diagnosis.sh +64 -0
- package/dist/lite/.claude/hooks/destructive-command-guard.sh +136 -0
- package/dist/lite/.claude/hooks/file-rename-discipline-check.sh +147 -0
- package/dist/lite/.claude/hooks/hook-classifier-canary.sh +158 -0
- package/dist/lite/.claude/hooks/kiss-default-check.sh +129 -0
- package/dist/lite/.claude/hooks/multi-tenancy-check.sh +83 -0
- package/dist/lite/.claude/hooks/oo-ad-entry-point-check.sh +216 -0
- package/dist/lite/.claude/hooks/operator-private-path-check.sh +203 -0
- package/dist/lite/.claude/hooks/pattern-annotation-validate.sh +142 -0
- package/dist/lite/.claude/hooks/plain-english-steering.sh +15 -1
- package/dist/lite/.claude/hooks/post-write-trace.sh +66 -0
- package/dist/lite/.claude/hooks/pr-body-scrub-check.sh +446 -0
- package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +137 -0
- package/dist/lite/.claude/hooks/pre-gh-pr-body-scrub.sh +192 -0
- package/dist/lite/.claude/hooks/pre-gh-pr-title-scrub.sh +172 -0
- package/dist/lite/.claude/hooks/role-entry-gate.sh +115 -0
- package/dist/lite/.claude/hooks/save-state.sh +268 -0
- package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +8 -1
- package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +52 -0
- package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +40 -4
- package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +12 -5
- package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +18 -2
- package/dist/lite/.claude/hooks/session-reflection.d/91-npm-version-drift.sh +54 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/10-abrupt-stop-recovery.test.sh +95 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +62 -15
- package/dist/lite/.claude/hooks/tool-failure-diagnosis.sh +50 -0
- package/dist/lite/.claude/luminaries/andrej-karpathy.md +165 -0
- package/dist/lite/.claude/luminaries/dave-gray.md +155 -0
- package/dist/lite/.claude/luminaries/gary-klein.md +166 -0
- package/dist/lite/.claude/luminaries/kaoru-ishikawa.md +162 -0
- package/dist/lite/.claude/luminaries/lean-product-canvas.md +33 -0
- package/dist/lite/.claude/luminaries/luke-wroblewski.md +224 -0
- package/dist/lite/.claude/luminaries/ryan-singer.md +113 -0
- package/dist/lite/.claude/luminaries/steve-krug.md +168 -0
- package/dist/lite/.claude/luminaries/tony-ulwick.md +134 -0
- package/dist/lite/.claude/rules/arc-level-orientation-before-leaf.md +60 -0
- package/dist/lite/.claude/rules/architect-review-discipline.md +71 -0
- package/dist/lite/.claude/rules/auto-fix-criteria.md +126 -0
- package/dist/lite/.claude/rules/bassclef-hook-drift-blocker.md +144 -0
- package/dist/lite/.claude/rules/bassclef-models-discovery.md +107 -0
- package/dist/lite/.claude/rules/bassclef-workflow-parity.md +104 -0
- package/dist/lite/.claude/rules/cost-governance.md +154 -0
- package/dist/lite/.claude/rules/file-rename-discipline.md +95 -0
- package/dist/lite/.claude/rules/mechanism-fidelity.md +88 -0
- package/dist/lite/.claude/rules/prototype-production-coupling.md +125 -0
- package/dist/lite/.claude/rules/quick-fix-criteria.md +90 -0
- package/dist/lite/.claude/rules/session-artifacts.md +30 -0
- package/dist/lite/.claude/rules/sibling-smoke-after-substrate-change.md +107 -0
- package/dist/lite/.claude/rules/substrate-as-system.md +67 -0
- package/dist/lite/.claude/rules/usability.md +22 -0
- package/dist/lite/.claude/settings.json +20 -0
- package/dist/lite/.claude/skills/agent-research-spawn/SKILL.md +100 -0
- package/dist/lite/.claude/skills/architect-review/SKILL.md +201 -0
- package/dist/lite/.claude/skills/auto-fix/SKILL.md +297 -0
- package/dist/lite/.claude/skills/autonomous/SKILL.md +300 -0
- package/dist/lite/.claude/skills/bind-subdomain/SKILL.md +232 -0
- package/dist/lite/.claude/skills/canvas/SKILL.md +41 -0
- package/dist/lite/.claude/skills/deploy-prod/SKILL.md +106 -0
- package/dist/lite/.claude/skills/docs-sync/SKILL.md +188 -0
- package/dist/lite/.claude/skills/eisenhower/SKILL.md +273 -0
- package/dist/lite/.claude/skills/empathy-map/SKILL.md +168 -0
- package/dist/lite/.claude/skills/frontend-design/SKILL.md +53 -0
- package/dist/lite/.claude/skills/interview-me/SKILL.md +171 -0
- package/dist/lite/.claude/skills/journal-export/SKILL.md +293 -0
- package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +203 -0
- package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +256 -0
- package/dist/lite/.claude/skills/pick-luminaries/SKILL.md +100 -0
- package/dist/lite/.claude/skills/pre-mortem/SKILL.md +180 -0
- package/dist/lite/.claude/skills/prototype-gallery/SKILL.md +264 -0
- package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +257 -0
- package/dist/lite/.claude/skills/quick-fix/SKILL.md +273 -0
- package/dist/lite/.claude/skills/release/SKILL.md +311 -0
- package/dist/lite/.claude/skills/release-notes/SKILL.md +202 -0
- package/dist/lite/.claude/skills/rfc/SKILL.md +222 -0
- package/dist/lite/.claude/skills/riff-prototypes/SKILL.md +637 -0
- package/dist/lite/.claude/skills/secrets-bootstrap/SKILL.md +209 -0
- package/dist/lite/.claude/skills/skill/SKILL.md +304 -0
- package/dist/lite/.claude/skills/smoke-test/SKILL.md +100 -0
- package/dist/lite/.claude/skills/state/SKILL.md +204 -0
- package/dist/lite/.claude/skills/substrate-check/SKILL.md +218 -0
- package/dist/lite/.claude/skills/synthetic-user/SKILL.md +381 -0
- package/dist/lite/.claude/skills/task-scenarios/SKILL.md +105 -0
- package/dist/lite/.claude/skills/tier-dependency-audit/SKILL.md +139 -0
- package/dist/lite/.claude/skills/ux-migration/SKILL.md +424 -0
- package/dist/lite/.claude/skills/value-prop-canvas/SKILL.md +297 -0
- package/dist/lite/README.md +5 -1
- package/dist/lite/gitignore +6 -0
- package/dist/lite/lib/bassclef-dir-resolver.sh +85 -0
- package/dist/lite/lib/clone-failure-classifier.sh +59 -0
- package/dist/lite/lib/fixture-builder.sh +190 -0
- package/dist/lite/lib/fresh-install-check.sh +49 -0
- package/dist/lite/lib/hook-heartbeat.sh +0 -1
- package/dist/lite/lib/mechanism-fidelity.sh +569 -0
- package/dist/lite/lib/npm-version-check.sh +112 -0
- package/dist/lite/lib/preview-state.sh +229 -0
- package/dist/lite/lib/session-rescue.sh +0 -1
- package/dist/lite/scripts/generate-lite-manifest.sh +145 -4
- package/dist/lite/scripts/lite-manifest-drift-check.sh +5 -1
- package/dist/lite/standards/architect-review-discipline.md +222 -0
- package/dist/lite/standards/bassclef-platform.md +249 -0
- package/dist/lite/standards/bassclef-test-repos.md +97 -0
- package/dist/lite/standards/bassclef-wiring-manifest.json +25 -1
- package/dist/lite/standards/cold-adopter-harness.md +205 -0
- package/dist/lite/standards/data-management.md +135 -0
- package/dist/lite/standards/frontend-stack/nextjs-prisma.md +124 -0
- package/dist/lite/standards/lite-manifest-schema-changes.md +27 -0
- package/dist/lite/standards/lite-manifest.json +1456 -14
- package/dist/lite/standards/mechanism-fidelity.md +205 -0
- package/dist/lite/standards/operator-private-discipline.md +200 -0
- package/dist/lite/standards/operator-to-public-flow.md +195 -0
- package/dist/lite/standards/prototype-gallery.md +251 -0
- package/dist/lite/standards/pseudonym-key-schema.md +109 -0
- package/dist/lite/standards/sibling-validation-checklist.md +181 -0
- package/dist/lite/standards/slack-notifications.md +161 -0
- package/dist/lite/standards/synthetic-user-testing.md +173 -0
- package/dist/lite/standards/vendor-bindings.md +165 -0
- package/dist/lite/standards/vendor-plugins/frontend-design.md +66 -0
- package/dist/lite/standards/vpc-fit-validation.md +167 -0
- package/dist/lite/templates/gallery-yml.md +113 -0
- package/dist/lite/templates/product-canvas.md +169 -0
- package/dist/lite/templates/value-prop-canvas.md +180 -0
- package/package.json +12 -5
- /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
|