@thebassclef/lite 1.9.8 → 1.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/git-stash-u-guard.sh +4 -1
- package/dist/lite/.claude/hooks/post-skill-friction-check.sh +6 -1
- package/dist/lite/.claude/hooks/pre-commit-gate.sh +49 -6
- package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +68 -8
- package/dist/lite/.claude/hooks/pre-commit-manifest-autoregen.sh +4 -2
- package/dist/lite/.claude/hooks/save-state.sh +24 -0
- package/dist/lite/.claude/hooks/session-reflection.d/83-install-written-paths-rebuild.sh +111 -0
- package/dist/lite/.claude/hooks/state-validate.sh +8 -0
- package/dist/lite/.claude/hooks/trace-helper.sh +29 -0
- package/dist/lite/.claude/rules/install-written-paths-mediation.md +74 -0
- package/dist/lite/.claude/rules/lite-manifest-schema-change-discipline.md +30 -0
- package/dist/lite/.claude/skills/autonomous/SKILL.md +53 -7
- package/dist/lite/.claude/skills/build/SKILL.md +53 -11
- package/dist/lite/.claude/skills/deploy-prod/SKILL.md +10 -0
- package/dist/lite/.claude/skills/launch/SKILL.md +26 -0
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +59 -18
- package/dist/lite/.claude/skills/personas/SKILL.md +1 -1
- package/dist/lite/.claude/skills/release-notes/SKILL.md +24 -0
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/gitignore +8 -0
- package/dist/lite/lib/capability-probe.sh +39 -1
- package/dist/lite/lib/composer-preflight.sh +8 -4
- package/dist/lite/lib/deferred-actions.sh +4 -4
- package/dist/lite/lib/fixture-builder.sh +9 -9
- package/dist/lite/lib/identifier-leak-check.sh +85 -0
- package/dist/lite/lib/install-written-paths.sh +369 -0
- package/dist/lite/lib/luminary-pick.sh +35 -13
- package/dist/lite/lib/preview-state.sh +20 -20
- package/dist/lite/lib/skip-env-inline-check.sh +61 -0
- package/dist/lite/lib/state.sh +102 -39
- package/dist/lite/lib/tier-check.sh +9 -9
- package/dist/lite/lib/tier-dependency-audit.sh +23 -23
- package/dist/lite/presence/install/schedule-auto-save.taskscheduler.md +1 -1
- package/dist/lite/standards/lite-manifest-schema-changes.md +83 -0
- package/dist/lite/standards/lite-manifest.json +70 -47
- package/dist/lite/standards/state-spine.md +1 -0
- package/dist/lite/templates/spec-template.md +10 -6
- package/package.json +1 -1
- package/dist/lite/.claude/hooks/longrun-prep-compounding-axis-check.sh +0 -492
- package/dist/lite/.claude/rules/compounding-axis-fresh-analysis.md +0 -173
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @thebassclef/lite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Security posture.** @thebassclef/lite ships with zero install-time scripts, SLSA v1 build provenance, Apache-2.0 license, and zero runtime dependencies. The CLI writes to `~/.claude` and `$CLAUDE_PROJECT_DIR` at runtime, not at install. See [SECURITY.md](SECURITY.md) for disclosure.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Two commands.
|
|
4
8
|
|
|
5
9
|
```bash
|
|
6
10
|
npm install -g @thebassclef/lite
|
|
@@ -63,7 +67,7 @@ your browser.
|
|
|
63
67
|
|
|
64
68
|
## Current release
|
|
65
69
|
|
|
66
|
-
<!-- version-start -->1.9.
|
|
70
|
+
<!-- version-start -->1.9.10<!-- version-end -->
|
|
67
71
|
|
|
68
72
|
See [CHANGELOG.md](CHANGELOG.md) for the full changelog. Adopter-facing
|
|
69
73
|
release notes live at [docs.bassclef.dev/docs/whats-new/releases](https://docs.bassclef.dev/docs/whats-new/releases).
|
package/dist/index.cjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const version: "1.9.
|
|
1
|
+
export declare const version: "1.9.10";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,10 @@ if ! echo "$CMD" | grep -qE '(^|[[:space:]])git[[:space:]]+stash([[:space:]]+pus
|
|
|
50
50
|
fi
|
|
51
51
|
|
|
52
52
|
# --- Override — SKIP_GIT_STASH_U_GUARD=1 bypasses (logged) ---
|
|
53
|
-
|
|
53
|
+
# Honors inline SKIP per #2036 Finding #4 (recipe must reach the hook).
|
|
54
|
+
# shellcheck source=/dev/null
|
|
55
|
+
source "${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}/lib/skip-env-inline-check.sh" 2>/dev/null || true
|
|
56
|
+
if declare -F skip_var_set >/dev/null && skip_var_set "SKIP_GIT_STASH_U_GUARD" "$CMD"; then
|
|
54
57
|
TRACE_LOG="${BASSCLEF_TRACE_LOG:-${BASSCLEF_TRACE_FILE:-$HOME/.claude/state/trace.log}}"
|
|
55
58
|
mkdir -p "$(dirname "$TRACE_LOG")" 2>/dev/null || true
|
|
56
59
|
echo "$(date -u +%FT%TZ) SKIP_GIT_STASH_U_GUARD $CMD" >> "$TRACE_LOG" 2>/dev/null || true
|
|
@@ -45,7 +45,12 @@ SKILL_NAME=$(jq <<< "$TOOL_INPUT" -r '.skill // ""' 2>/dev/null)
|
|
|
45
45
|
|
|
46
46
|
# Major composer skills — these chain other skills or do heavy work.
|
|
47
47
|
# Friction surfaces best at their boundaries.
|
|
48
|
-
|
|
48
|
+
# Per ADR-040 D1 vocabulary rename (grace window through 2026-10-31 per
|
|
49
|
+
# .claude/rules/we-dont-break-adopters.md): new names stage/launch added;
|
|
50
|
+
# old names preview/preview-build preserved for grace-window compat.
|
|
51
|
+
# Fix for bassclef-upstream#1777 (friction log never created because current
|
|
52
|
+
# vocabulary skipped the gate).
|
|
53
|
+
MAJOR_SKILLS_PATTERN="^(preview|preview-build|stage|launch|build|shape|longrun|decompose|spec|architect-review|frontend-design|prototype-variants|prototype-gallery|interpret-input|objectory-decompose|ux-migration|ia-model|interaction-design|use-case|user-stories|jtbd-tasks|task-scenarios)$"
|
|
49
54
|
|
|
50
55
|
if ! echo "$SKILL_NAME" | grep -qE "$MAJOR_SKILLS_PATTERN"; then
|
|
51
56
|
exit 0
|
|
@@ -41,7 +41,16 @@ trace_log "pre-commit" "git commit"
|
|
|
41
41
|
# Exempt: rules/hooks/skills/standards/docs/strategy/chronicle/scripts/tests
|
|
42
42
|
# (places where the policy itself is documented or test fixtures live).
|
|
43
43
|
# Override: SKIP_OPERATOR_PATHS=1 git commit (logged via trace-helper)
|
|
44
|
-
|
|
44
|
+
#
|
|
45
|
+
# Env-reach fix per #2036 Finding #4: the inline recipe sets the var in
|
|
46
|
+
# the git subshell env, not the hook env. Source the helper lib so the
|
|
47
|
+
# recipe reaches the check reliably across harness setups.
|
|
48
|
+
# shellcheck source=/dev/null
|
|
49
|
+
source "${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}/lib/skip-env-inline-check.sh" 2>/dev/null || true
|
|
50
|
+
if declare -F skip_var_set >/dev/null && skip_var_set "SKIP_OPERATOR_PATHS" "$COMMAND"; then
|
|
51
|
+
# Override honored; skip the check.
|
|
52
|
+
:
|
|
53
|
+
else
|
|
45
54
|
STAGED_NONEXEMPT=$(git diff --cached --name-only --diff-filter=AM 2>/dev/null | \
|
|
46
55
|
grep -vE '^\.claude/(rules|hooks|skills|luminaries|agents)/' | \
|
|
47
56
|
grep -vE '^standards/' | \
|
|
@@ -50,7 +59,8 @@ if [ "${SKIP_OPERATOR_PATHS:-0}" != "1" ]; then
|
|
|
50
59
|
grep -vE '^docs/' | \
|
|
51
60
|
grep -vE '^chronicle/' | \
|
|
52
61
|
grep -vE '^strategy/' | \
|
|
53
|
-
grep -vE '^scripts/tests/'
|
|
62
|
+
grep -vE '^scripts/tests/' | \
|
|
63
|
+
grep -vE '^lib/tests/' || true)
|
|
54
64
|
if [ -n "$STAGED_NONEXEMPT" ]; then
|
|
55
65
|
LEAKED=""
|
|
56
66
|
while IFS= read -r f; do
|
|
@@ -110,7 +120,8 @@ fi
|
|
|
110
120
|
# fast-fail layer per Saltzer-Schroeder complete mediation.
|
|
111
121
|
#
|
|
112
122
|
# Override: SKIP_COLD_ADOPTER_HARNESS=1 git commit (logged via trace-helper)
|
|
113
|
-
|
|
123
|
+
# Env-reach via skip_var_set helper per #2036 Finding #4.
|
|
124
|
+
if ! { declare -F skip_var_set >/dev/null && skip_var_set "SKIP_COLD_ADOPTER_HARNESS" "$COMMAND"; }; then
|
|
114
125
|
# The 12-pattern matcher mirrors .claude/rules/cold-adopter-harness-discipline.md
|
|
115
126
|
# § "When this rule fires". Keep these patterns in sync; a future WU may
|
|
116
127
|
# centralize the list via a sourced library function.
|
|
@@ -173,7 +184,8 @@ fi
|
|
|
173
184
|
# state/markers/lead-lens-signoff/<branch-slug>.marker
|
|
174
185
|
# Discipline lives in .claude/rules/loop-discipline.md Step 0.5 + 2a + 5.5.
|
|
175
186
|
# Override: SKIP_LEAD_LENS_SIGNOFF=1 git commit (logged via trace-helper)
|
|
176
|
-
|
|
187
|
+
# Env-reach via skip_var_set helper per #2036 Finding #4.
|
|
188
|
+
if ! { declare -F skip_var_set >/dev/null && skip_var_set "SKIP_LEAD_LENS_SIGNOFF" "$COMMAND"; }; then
|
|
177
189
|
# Reuse the substrate-affecting matcher shape from cold-adopter fast-path.
|
|
178
190
|
STAGED_SUBSTRATE_LL=$(git diff --cached --name-only --diff-filter=AM 2>/dev/null | grep -E \
|
|
179
191
|
-e '^\.claude/hooks/[^/]+\.sh$' \
|
|
@@ -189,6 +201,37 @@ if [ "${SKIP_LEAD_LENS_SIGNOFF:-0}" != "1" ]; then
|
|
|
189
201
|
-e '^\.github/workflows/bassclef-.*\.yml$' \
|
|
190
202
|
-e '^scripts/[^/]+\.sh$' \
|
|
191
203
|
|| true)
|
|
204
|
+
# Install-written-paths filter per bassclef-upstream#2036 Finding #8 (Kunal).
|
|
205
|
+
# Rule: .claude/rules/install-written-paths-mediation.md
|
|
206
|
+
# Any staged substrate path registered as install-written AND unchanged since
|
|
207
|
+
# install is dropped from the author-edit set. Lead-lens gate fires only on
|
|
208
|
+
# author-edited substrate paths.
|
|
209
|
+
if [ -n "$STAGED_SUBSTRATE_LL" ]; then
|
|
210
|
+
IWP_LIB=""
|
|
211
|
+
if [ -f "lib/install-written-paths.sh" ]; then
|
|
212
|
+
IWP_LIB="lib/install-written-paths.sh"
|
|
213
|
+
elif [ -f "$BASSCLEF_DIR/lib/install-written-paths.sh" ]; then
|
|
214
|
+
IWP_LIB="$BASSCLEF_DIR/lib/install-written-paths.sh"
|
|
215
|
+
fi
|
|
216
|
+
if [ -n "$IWP_LIB" ]; then
|
|
217
|
+
# shellcheck source=/dev/null
|
|
218
|
+
. "$IWP_LIB" 2>/dev/null || true
|
|
219
|
+
if declare -F is_install_written_path >/dev/null 2>&1; then
|
|
220
|
+
FILTERED_LL=""
|
|
221
|
+
while IFS= read -r _iwp_f; do
|
|
222
|
+
[ -z "$_iwp_f" ] && continue
|
|
223
|
+
if is_install_written_path "$_iwp_f" 2>/dev/null; then
|
|
224
|
+
trace_log "pre-commit" "lead-lens skip install-written path=$_iwp_f"
|
|
225
|
+
continue
|
|
226
|
+
fi
|
|
227
|
+
FILTERED_LL="${FILTERED_LL}${FILTERED_LL:+$'\n'}$_iwp_f"
|
|
228
|
+
done <<EOF
|
|
229
|
+
$STAGED_SUBSTRATE_LL
|
|
230
|
+
EOF
|
|
231
|
+
STAGED_SUBSTRATE_LL="$FILTERED_LL"
|
|
232
|
+
fi
|
|
233
|
+
fi
|
|
234
|
+
fi
|
|
192
235
|
if [ -n "$STAGED_SUBSTRATE_LL" ]; then
|
|
193
236
|
LL_BRANCH_SLUG=$(git branch --show-current 2>/dev/null | tr '/' '-')
|
|
194
237
|
PREM_M="state/markers/pre-mortem/${LL_BRANCH_SLUG}.marker"
|
|
@@ -305,7 +348,7 @@ fi
|
|
|
305
348
|
|
|
306
349
|
# --- Reserved skill names check (collision with Claude Code built-ins) ---
|
|
307
350
|
# See .claude/rules/reserved-skill-names.md + standards/reserved-skill-names.md
|
|
308
|
-
if
|
|
351
|
+
if ! { declare -F skip_var_set >/dev/null && skip_var_set "SKIP_RESERVED_NAMES" "$COMMAND"; }; then
|
|
309
352
|
STAGED_NEW_SKILLS=$(git diff --cached --name-only --diff-filter=A -- '.claude/skills/*/SKILL.md' 2>/dev/null || true)
|
|
310
353
|
if [ -n "$STAGED_NEW_SKILLS" ]; then
|
|
311
354
|
# Extract reserved names from the standards table (Name column values inside backticks)
|
|
@@ -351,7 +394,7 @@ fi
|
|
|
351
394
|
# counter, silencing the compliance signal (the failure mode that motivated
|
|
352
395
|
# this gate). Block commits to chronicle/*.md or docs/chronicle/*.md that
|
|
353
396
|
# lack the section. Override: SKIP_CHRONICLE_GATE_EVIDENCE=1 git commit.
|
|
354
|
-
if
|
|
397
|
+
if ! { declare -F skip_var_set >/dev/null && skip_var_set "SKIP_CHRONICLE_GATE_EVIDENCE" "$COMMAND"; }; then
|
|
355
398
|
STAGED_CHRONICLES=$(git diff --cached --name-only --diff-filter=AM -- 'chronicle/*.md' 'docs/chronicle/*.md' 2>/dev/null || true)
|
|
356
399
|
# Source spine accessor library when available — preferred frontmatter
|
|
357
400
|
# check route per state-spine-contract.md. Graceful fallback for repos
|
|
@@ -33,12 +33,6 @@
|
|
|
33
33
|
|
|
34
34
|
set -u
|
|
35
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
36
|
# R-N1 fail-soft: if grep is unavailable, warn to stderr and exit 0
|
|
43
37
|
if ! command -v grep >/dev/null 2>&1; then
|
|
44
38
|
echo " identifier-leak-scrub: warn — grep unavailable; skipping scrub" >&2
|
|
@@ -59,6 +53,16 @@ else
|
|
|
59
53
|
COMMAND=$(echo "$INPUT" | grep -oE '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/^"command"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
|
|
60
54
|
fi
|
|
61
55
|
|
|
56
|
+
# Override path — honors inline SKIP_FOO=1 cmd per #2036 Finding #4.
|
|
57
|
+
# PreToolUse hooks run in Claude Code's env, not the git subshell env.
|
|
58
|
+
# The helper lib parses $COMMAND for the inline prefix + falls back to env.
|
|
59
|
+
# shellcheck source=/dev/null
|
|
60
|
+
source "${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}/lib/skip-env-inline-check.sh" 2>/dev/null || true
|
|
61
|
+
if declare -F skip_var_set >/dev/null && skip_var_set "SKIP_IDENTIFIER_LEAK_SCRUB" "$COMMAND"; then
|
|
62
|
+
echo " identifier-leak-scrub: SKIP_IDENTIFIER_LEAK_SCRUB=1 — bypassing scrub" >&2
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
62
66
|
# Pass through non-git-commit commands
|
|
63
67
|
if ! echo "$COMMAND" | grep -qE '^[[:space:]]*git[[:space:]]+commit'; then
|
|
64
68
|
exit 0
|
|
@@ -93,8 +97,36 @@ fi
|
|
|
93
97
|
# Filter to '+' lines only so a commit that REMOVES a pre-existing leak
|
|
94
98
|
# does not trip on the removed-content line. `+++` file headers stripped.
|
|
95
99
|
# Per sunj-labs/bassclef-upstream#1575.
|
|
100
|
+
#
|
|
101
|
+
# Self-exclude per sunj-labs/bassclef-cli#305: the hook, its test, the
|
|
102
|
+
# rule, and the pattern-source file all carry pattern-looking strings
|
|
103
|
+
# as documentation. Scanning them blocks the first commit after a fresh
|
|
104
|
+
# bassclef init and teaches adopters to skip the check on day one.
|
|
105
|
+
# Non-self paths with the same patterns STILL block (Test 19).
|
|
106
|
+
SELF_EXCLUDE_PATHS=(
|
|
107
|
+
".claude/hooks/pre-commit-identifier-leak-scrub.sh"
|
|
108
|
+
".claude/hooks/tests/pre-commit-identifier-leak-scrub.test.sh"
|
|
109
|
+
".claude/rules/identifier-leak-prevention.md"
|
|
110
|
+
"standards/identifier-leak-patterns.txt"
|
|
111
|
+
)
|
|
96
112
|
if [ -z "$BLOCK_REASON" ]; then
|
|
97
|
-
|
|
113
|
+
STAGED_FILES=$(git diff --cached --name-only 2>/dev/null || echo "")
|
|
114
|
+
STAGED_DIFF=""
|
|
115
|
+
while IFS= read -r file; do
|
|
116
|
+
[ -z "$file" ] && continue
|
|
117
|
+
skip=0
|
|
118
|
+
for sp in "${SELF_EXCLUDE_PATHS[@]}"; do
|
|
119
|
+
if [ "$file" = "$sp" ]; then
|
|
120
|
+
skip=1
|
|
121
|
+
break
|
|
122
|
+
fi
|
|
123
|
+
done
|
|
124
|
+
[ "$skip" = "1" ] && continue
|
|
125
|
+
file_diff=$(git diff --cached -- "$file" 2>/dev/null | grep '^+' | grep -v '^+++' || echo "")
|
|
126
|
+
if [ -n "$file_diff" ]; then
|
|
127
|
+
STAGED_DIFF+="$file_diff"$'\n'
|
|
128
|
+
fi
|
|
129
|
+
done <<< "$STAGED_FILES"
|
|
98
130
|
if [ -n "$STAGED_DIFF" ]; then
|
|
99
131
|
MATCH=$(echo "$STAGED_DIFF" | grep -oE "$IDENTIFIER_PATTERNS" | head -1)
|
|
100
132
|
if [ -n "$MATCH" ]; then
|
|
@@ -104,6 +136,29 @@ if [ -z "$BLOCK_REASON" ]; then
|
|
|
104
136
|
fi
|
|
105
137
|
fi
|
|
106
138
|
|
|
139
|
+
# Check 3 — git author identity (per sunj-labs/bassclef-cli#305)
|
|
140
|
+
# On a Mac without user.email set, git builds the author from username +
|
|
141
|
+
# hostname (e.g., `<user>@<host>.local`). The address ships in every commit.
|
|
142
|
+
# Scan GIT_AUTHOR_IDENT against the pattern set plus the `.local` suffix.
|
|
143
|
+
# Fail-soft when git is missing or the ident cannot be resolved.
|
|
144
|
+
if [ -z "$BLOCK_REASON" ] && command -v git >/dev/null 2>&1; then
|
|
145
|
+
AUTHOR_IDENT=$(git var GIT_AUTHOR_IDENT 2>/dev/null || true)
|
|
146
|
+
if [ -n "$AUTHOR_IDENT" ]; then
|
|
147
|
+
# Format: "Name <email> timestamp timezone"
|
|
148
|
+
AUTHOR_EMAIL=$(echo "$AUTHOR_IDENT" | grep -oE '<[^>]+>' | tr -d '<>')
|
|
149
|
+
if [ -n "$AUTHOR_EMAIL" ]; then
|
|
150
|
+
MATCH=$(echo "$AUTHOR_EMAIL" | grep -oE "$IDENTIFIER_PATTERNS" | head -1)
|
|
151
|
+
if [ -z "$MATCH" ] && echo "$AUTHOR_EMAIL" | grep -qE '\.local$'; then
|
|
152
|
+
MATCH="$AUTHOR_EMAIL"
|
|
153
|
+
fi
|
|
154
|
+
if [ -n "$MATCH" ]; then
|
|
155
|
+
BLOCK_REASON="commit author"
|
|
156
|
+
BLOCK_MATCH="$MATCH"
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
fi
|
|
161
|
+
|
|
107
162
|
# BLOCK if any pattern matched
|
|
108
163
|
if [ -n "$BLOCK_REASON" ]; then
|
|
109
164
|
cat >&2 <<EOF
|
|
@@ -124,7 +179,12 @@ Resolution paths:
|
|
|
124
179
|
1. Rewrite the commit message or staged content to drop the identifier
|
|
125
180
|
- Replace machine names with placeholders like <machine> or the branch
|
|
126
181
|
- Replace absolute paths with relative paths or \$HOME references
|
|
127
|
-
2.
|
|
182
|
+
2. If the match is in your commit author identity:
|
|
183
|
+
- Set a stable identity: git config --global user.email \\
|
|
184
|
+
<username>@users.noreply.github.com
|
|
185
|
+
- And: git config --global user.name "<Your Name>"
|
|
186
|
+
- Then retry the commit; the author will no longer include the host.
|
|
187
|
+
3. Override (rare — for legitimate cases like DON'T-do-this documentation):
|
|
128
188
|
SKIP_IDENTIFIER_LEAK_SCRUB=1 git commit ...
|
|
129
189
|
|
|
130
190
|
See .claude/rules/identifier-leak-prevention.md for full discipline.
|
|
@@ -38,8 +38,10 @@ if ! echo "$COMMAND" | grep -q 'git commit'; then
|
|
|
38
38
|
exit 0
|
|
39
39
|
fi
|
|
40
40
|
|
|
41
|
-
# Override path
|
|
42
|
-
|
|
41
|
+
# Override path — honors inline SKIP_FOO=1 cmd per #2036 Finding #4.
|
|
42
|
+
# shellcheck source=/dev/null
|
|
43
|
+
source "${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}/lib/skip-env-inline-check.sh" 2>/dev/null || true
|
|
44
|
+
if declare -F skip_var_set >/dev/null && skip_var_set "SKIP_MANIFEST_AUTOREGEN" "$COMMAND"; then
|
|
43
45
|
# Best-effort trace log; ignore failure so hook stays exit 0
|
|
44
46
|
if command -v bash >/dev/null 2>&1 && [ -f ".claude/hooks/trace-helper.sh" ]; then
|
|
45
47
|
bash .claude/hooks/trace-helper.sh "skip" "manifest-autoregen" "SKIP_MANIFEST_AUTOREGEN=1" 2>/dev/null || true
|
|
@@ -216,6 +216,30 @@ if [ -n "$DIRTY" ]; then
|
|
|
216
216
|
git add 'state/' ':!state/markers' ':!state/markers/**' 2>/dev/null || true
|
|
217
217
|
git add .claude/projects/*/memory/ 2>/dev/null || true
|
|
218
218
|
|
|
219
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null)
|
|
220
|
+
if [ -n "$STAGED" ]; then
|
|
221
|
+
# Operator-identifier leak check per #2036 Finding #1.
|
|
222
|
+
# save-state runs `git commit` directly, bypassing the PreToolUse
|
|
223
|
+
# pre-commit-gate.sh CCF-3 check. Even if CCF-3 fired, its grep
|
|
224
|
+
# exempts docs/ paths — so docs/sdlc-traces/*.log carrying absolute
|
|
225
|
+
# /Users/<name>/ paths would still get committed.
|
|
226
|
+
# Shared lib so this check matches pre-commit-gate CCF-3 shape.
|
|
227
|
+
REPO_ROOT_FOR_LIB="$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
228
|
+
LEAK_LIB="${REPO_ROOT_FOR_LIB}/lib/identifier-leak-check.sh"
|
|
229
|
+
if [ -f "$LEAK_LIB" ]; then
|
|
230
|
+
# shellcheck source=/dev/null
|
|
231
|
+
source "$LEAK_LIB"
|
|
232
|
+
if ! check_staged_for_leaks; then
|
|
233
|
+
log "BLOCKED: auto-save detected operator paths in staged content; resetting stage"
|
|
234
|
+
git reset HEAD -- . 2>/dev/null || true
|
|
235
|
+
# Fall through — SESSION_LOCK cleanup below still fires, push is skipped
|
|
236
|
+
# because STAGED check below sees empty stage.
|
|
237
|
+
STAGED=""
|
|
238
|
+
fi
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# Re-check STAGED after possible reset
|
|
219
243
|
STAGED=$(git diff --cached --name-only 2>/dev/null)
|
|
220
244
|
if [ -n "$STAGED" ]; then
|
|
221
245
|
TIMESTAMP=$(date +%Y-%m-%dT%H:%M:%S)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# install-class: dual
|
|
4
|
+
#
|
|
5
|
+
# Session-start fragment: rebuild install-written-paths manifest when missing/stale.
|
|
6
|
+
# Per bassclef-upstream#2036 Finding #8 (Kunal Slot 1 cure).
|
|
7
|
+
#
|
|
8
|
+
# Walks lite-manifest.json. Registers each shipped file as install-written
|
|
9
|
+
# via the lib. Runs when state/install-written-paths.json is missing OR
|
|
10
|
+
# when any file in lite-manifest is newer than the manifest.
|
|
11
|
+
#
|
|
12
|
+
# Fail-soft per Nygard — logs advisory and continues on error.
|
|
13
|
+
# Override: SKIP_INSTALL_WRITTEN_PATHS_REBUILD=1
|
|
14
|
+
|
|
15
|
+
# Env-reach safe guard per bassclef-upstream#2036 Finding #4.
|
|
16
|
+
# Fragment is sourced by session-reflection.sh; use return (not exit) so parent survives.
|
|
17
|
+
# shellcheck disable=SC2317
|
|
18
|
+
if [ -n "${SKIP_INSTALL_WRITTEN_PATHS_REBUILD:-}" ]; then
|
|
19
|
+
return 0 2>/dev/null
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
_iwp_fragment_log() {
|
|
23
|
+
if command -v trace_log >/dev/null 2>&1; then
|
|
24
|
+
trace_log "install-written-paths-rebuild" "$*"
|
|
25
|
+
fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_iwp_fragment_main() {
|
|
29
|
+
local project_root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
30
|
+
local lite_manifest="$project_root/lite-manifest.json"
|
|
31
|
+
local write_manifest="$project_root/state/install-written-paths.json"
|
|
32
|
+
local lib="$project_root/lib/install-written-paths.sh"
|
|
33
|
+
|
|
34
|
+
# Fallback to BASSCLEF_DIR when CLAUDE_PROJECT_DIR doesn't carry bassclef
|
|
35
|
+
if [ ! -f "$lite_manifest" ] && [ -n "${BASSCLEF_DIR:-}" ]; then
|
|
36
|
+
lite_manifest="$BASSCLEF_DIR/lite-manifest.json"
|
|
37
|
+
lib="$BASSCLEF_DIR/lib/install-written-paths.sh"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Skip when inputs missing (fail-soft)
|
|
41
|
+
if [ ! -f "$lib" ]; then
|
|
42
|
+
_iwp_fragment_log "skip reason=no-lib"
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [ ! -f "$lite_manifest" ]; then
|
|
47
|
+
_iwp_fragment_log "skip reason=no-lite-manifest"
|
|
48
|
+
return 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
52
|
+
_iwp_fragment_log "skip reason=no-jq"
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Decide whether to rebuild: missing OR older than lite-manifest
|
|
57
|
+
local rebuild=0
|
|
58
|
+
if [ ! -f "$write_manifest" ]; then
|
|
59
|
+
rebuild=1
|
|
60
|
+
elif [ "$lite_manifest" -nt "$write_manifest" ]; then
|
|
61
|
+
rebuild=1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
if [ "$rebuild" -eq 0 ]; then
|
|
65
|
+
_iwp_fragment_log "skip reason=manifest-fresh"
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Source lib — subshell so caller scope stays clean
|
|
70
|
+
(
|
|
71
|
+
# shellcheck source=/dev/null
|
|
72
|
+
. "$lib" 2>/dev/null || exit 0
|
|
73
|
+
|
|
74
|
+
if ! declare -F install_written_paths_register >/dev/null 2>&1; then
|
|
75
|
+
_iwp_fragment_log "skip reason=lib-missing-verb"
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Reset manifest to empty, then walk lite-manifest entries
|
|
80
|
+
rm -f "$write_manifest" 2>/dev/null || true
|
|
81
|
+
install_written_paths_rebuild 2>/dev/null || true
|
|
82
|
+
|
|
83
|
+
local registered=0
|
|
84
|
+
local skipped=0
|
|
85
|
+
|
|
86
|
+
# Each lite-manifest entry has .path field — register when file exists on disk
|
|
87
|
+
while IFS= read -r path; do
|
|
88
|
+
[ -z "$path" ] && continue
|
|
89
|
+
local full
|
|
90
|
+
case "$path" in
|
|
91
|
+
/*) full="$path" ;;
|
|
92
|
+
*) full="$project_root/$path" ;;
|
|
93
|
+
esac
|
|
94
|
+
if [ -f "$full" ]; then
|
|
95
|
+
if install_written_paths_register "$path" "bassclef-sync-template" 2>/dev/null; then
|
|
96
|
+
registered=$((registered + 1))
|
|
97
|
+
else
|
|
98
|
+
skipped=$((skipped + 1))
|
|
99
|
+
fi
|
|
100
|
+
else
|
|
101
|
+
skipped=$((skipped + 1))
|
|
102
|
+
fi
|
|
103
|
+
done < <(jq -r '.entries[]?.path // empty' "$lite_manifest" 2>/dev/null)
|
|
104
|
+
|
|
105
|
+
_iwp_fragment_log "rebuild complete registered=$registered skipped=$skipped"
|
|
106
|
+
) || true
|
|
107
|
+
|
|
108
|
+
return 0
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_iwp_fragment_main
|
|
@@ -61,6 +61,14 @@ SCHEMA_DIR="${PROJECT_DIR}/standards/state-spine/schemas"
|
|
|
61
61
|
|
|
62
62
|
# If schema dir doesn't exist, this repo doesn't have spine v0 yet — pass through
|
|
63
63
|
if [ ! -d "$SCHEMA_DIR" ]; then
|
|
64
|
+
# Per cli#316 — honest skip. Previously exited 0 silently; adopters +
|
|
65
|
+
# agents on lite couldn't tell "validated" from "not checked". /launch
|
|
66
|
+
# and /objectory-decompose SKILLs promise this hook will BLOCK bad
|
|
67
|
+
# frontmatter. On lite, where schemas don't ship, this message makes
|
|
68
|
+
# the skip visible so the promise doesn't lie.
|
|
69
|
+
if [ "${STATE_VALIDATE_QUIET:-0}" != "1" ]; then
|
|
70
|
+
echo "state-validate: schemas not available (standards/state-spine/schemas missing at $SCHEMA_DIR); validation skipped at this tier — set STATE_VALIDATE_QUIET=1 to silence" >&2
|
|
71
|
+
fi
|
|
64
72
|
exit 0
|
|
65
73
|
fi
|
|
66
74
|
|
|
@@ -5,9 +5,28 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Usage: source this file, then call:
|
|
7
7
|
# trace_log "hook-name" "trigger-description"
|
|
8
|
+
#
|
|
9
|
+
# Privacy — relative paths (#2036 Finding #1 — adopter report 2026-10-02):
|
|
10
|
+
# The ${trigger} field can carry filesystem paths from caller args
|
|
11
|
+
# (e.g., $FILE_PATH from PreToolUse hooks). We strip repo-root prefix
|
|
12
|
+
# so adopter traces never leak /Users/<name>/ or /home/<name>/ paths
|
|
13
|
+
# to adopter git. Fallback chain: $CLAUDE_PROJECT_DIR → git rev-parse
|
|
14
|
+
# --show-toplevel → no-strip (silent — trigger lands as-is).
|
|
8
15
|
|
|
9
16
|
TRACE_DIR="docs/sdlc-traces"
|
|
10
17
|
|
|
18
|
+
# _trace_repo_root — resolve repo root for prefix-strip.
|
|
19
|
+
# Precondition fallback per @luminary tony-hoare — Hoare R1 risk ledger.
|
|
20
|
+
_trace_repo_root() {
|
|
21
|
+
if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
22
|
+
echo "$CLAUDE_PROJECT_DIR"
|
|
23
|
+
return 0
|
|
24
|
+
fi
|
|
25
|
+
local root
|
|
26
|
+
root=$(git rev-parse --show-toplevel 2>/dev/null) || return 1
|
|
27
|
+
echo "$root"
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
trace_log() {
|
|
12
31
|
local hook="$1"
|
|
13
32
|
local trigger="$2"
|
|
@@ -17,6 +36,16 @@ trace_log() {
|
|
|
17
36
|
today=$(date +%Y-%m-%d)
|
|
18
37
|
local log_file="${TRACE_DIR}/${today}.log"
|
|
19
38
|
|
|
39
|
+
# Strip repo-root prefix from trigger before writing.
|
|
40
|
+
# Only strips when root is a non-empty prefix of trigger — leaves
|
|
41
|
+
# unrelated absolute paths (/opt/anthropic/..., /usr/bin/...) intact.
|
|
42
|
+
local root
|
|
43
|
+
root=$(_trace_repo_root 2>/dev/null) || root=""
|
|
44
|
+
if [ -n "$root" ] && [ "$root" != "/" ]; then
|
|
45
|
+
trigger="${trigger//$root\//}"
|
|
46
|
+
trigger="${trigger//$root/}"
|
|
47
|
+
fi
|
|
48
|
+
|
|
20
49
|
mkdir -p "$TRACE_DIR" 2>/dev/null || true
|
|
21
50
|
echo "${timestamp} | ${hook} | ${trigger} | — | —" >> "$log_file" 2>/dev/null || true
|
|
22
51
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
tier: lite
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Install-Written-Paths — single accessor mediation
|
|
6
|
+
|
|
7
|
+
Discipline hooks that would fire on files written by install writers (`bassclef init`, `bassclef-sync.sh`, `bassclef-hook-connect`, `schedule-auto-save`, template scripts) MUST consult `lib/install-written-paths.sh` via the single accessor `is_install_written_path <path>` before firing on each staged file.
|
|
8
|
+
|
|
9
|
+
The lib + manifest ship per bassclef-upstream#2036 Finding #8 (Kunal cold-adopter report, 2026-10-02). Structural cure for Class A (install-leak surfaces).
|
|
10
|
+
|
|
11
|
+
## When this rule fires
|
|
12
|
+
|
|
13
|
+
Any discipline hook that currently fires on file content without discriminating install-written from author-edited. Candidate hooks:
|
|
14
|
+
|
|
15
|
+
- `.claude/hooks/pre-commit-gate.sh` — lead-lens 3-marker gate section (consumer wired per this cure's V1 PR)
|
|
16
|
+
- `.claude/hooks/pre-commit-identifier-leak-scrub.sh` — identifier-leak sister per Kunal #1 (future adopter per R2 risk ledger)
|
|
17
|
+
- Any new discipline hook authored under the loop discipline (`.claude/rules/loop-discipline.md`) that assumes author-edit
|
|
18
|
+
|
|
19
|
+
## What the rule requires
|
|
20
|
+
|
|
21
|
+
Before firing discipline on a staged path, each discipline hook:
|
|
22
|
+
|
|
23
|
+
1. Sources `lib/install-written-paths.sh`.
|
|
24
|
+
2. Calls `is_install_written_path "$staged_path"`.
|
|
25
|
+
3. When return is 0 (true — path is install-written AND content hash matches), SKIP discipline for that path. Emit a trace line via `trace_log` for audit.
|
|
26
|
+
4. When return is 1 (false — path not in manifest OR hash drifted), FIRE discipline as today.
|
|
27
|
+
|
|
28
|
+
When all staged paths return 0, the hook returns pass without discipline. When at least one returns 1, the hook fires discipline on the author-edited paths.
|
|
29
|
+
|
|
30
|
+
## Anti-patterns
|
|
31
|
+
|
|
32
|
+
### Reimplementing the discrimination
|
|
33
|
+
|
|
34
|
+
Hook authors who grep `.bassclef-source.json` or hard-code install path lists duplicate the lib. Future rename events or new install writers break them silently.
|
|
35
|
+
|
|
36
|
+
INSTEAD: source the lib. Compose with the shared discipline.
|
|
37
|
+
|
|
38
|
+
### Skipping the lib for sister hooks
|
|
39
|
+
|
|
40
|
+
Discipline hooks that fire on install-written content but don't consult the lib produce Kunal-pattern false fires on cold adopters.
|
|
41
|
+
|
|
42
|
+
INSTEAD: add the consultation at the first opportunity. Per @luminary michael-feathers legacy-code retrofit — sister hooks consult the lib when next touched for other work.
|
|
43
|
+
|
|
44
|
+
## Composes with
|
|
45
|
+
|
|
46
|
+
- `.claude/rules/bootstrap-pair-discipline.md` — this rule + lib + schema + hook consumption + session-start fragment ship paired
|
|
47
|
+
- `.claude/rules/state-schema-validation.md` — schema at `standards/state-spine/schemas/install-written-paths.schema.json` validates on write
|
|
48
|
+
- `.claude/rules/we-dont-break-adopters.md` — additive; existing SKIP_* paths preserved
|
|
49
|
+
- `.claude/rules/sibling-smoke-after-substrate-change.md` — post-merge sibling smoke confirms Kunal #8 cured end-to-end
|
|
50
|
+
- @luminary linus-torvalds — adopter contract (manifest IS the adopter-visible surface this cure protects)
|
|
51
|
+
- @luminary michael-feathers — characterization tests (lib tests pin actual install-vs-author discrimination)
|
|
52
|
+
- @luminary john-ousterhout — deep module (narrow 5-verb interface hides schema + hash + I/O complexity)
|
|
53
|
+
- @luminary michael-nygard — fail-soft (missing manifest, malformed JSON, missing hash tool → return "not install-written" + advisory to stderr)
|
|
54
|
+
|
|
55
|
+
## Override path
|
|
56
|
+
|
|
57
|
+
`SKIP_INSTALL_WRITTEN_PATHS=1` — the lib's `is_install_written_path` returns 1 (false) unconditionally when this env var is set. Hooks fire discipline normally. Logged via `trace_log`.
|
|
58
|
+
|
|
59
|
+
INSTEAD of overriding routinely: trust the lib. Overrides are for one-shot debugging where the manifest-based path has misclassified.
|
|
60
|
+
|
|
61
|
+
## V1 scope — this cure's PR
|
|
62
|
+
|
|
63
|
+
- `lib/install-written-paths.sh` + Tier 0 tests
|
|
64
|
+
- `standards/state-spine/schemas/install-written-paths.schema.json` + state-spine.md Shape C entry
|
|
65
|
+
- `.claude/hooks/pre-commit-gate.sh` — lead-lens 3-marker gate section amended to consult the lib
|
|
66
|
+
- `presence/install/bassclef-hook-connect.sh` — registers each path on write
|
|
67
|
+
- `presence/install/bassclef-sync.template.sh` — registers each path on sync
|
|
68
|
+
- `.claude/hooks/session-reflection.d/83-install-written-paths-rebuild.sh` — regenerates manifest when missing at session-start
|
|
69
|
+
|
|
70
|
+
## Deferred to follow-on
|
|
71
|
+
|
|
72
|
+
- `bassclef init` npm-side registration (lands in bassclef-cli; sources the lib via package symlink)
|
|
73
|
+
- `pre-commit-identifier-leak-scrub.sh` consultation (sister hook adoption per Feathers legacy-code retrofit)
|
|
74
|
+
- Any other hook that assumes author-edit — audit at next /architect-review
|
|
@@ -36,11 +36,41 @@ When a PR changes the manifest schema OR output shape:
|
|
|
36
36
|
3. **Downstream cure named** — the entry names what adopters need to do. If nothing (backward-compatible add), say so explicitly. If code change (like the v0.36.0 `upstream_commit` removal), name the pattern.
|
|
37
37
|
4. **Release script pre-flight** — before every `/release`, the script reads the ledger and confirms an entry exists for the outgoing `manifest_version`. Missing entry BLOCKs the cascade.
|
|
38
38
|
|
|
39
|
+
## Retirements
|
|
40
|
+
|
|
41
|
+
Retirement-class changes (content-remove: a file previously shipped in the lite manifest gets deleted intentionally) ship with the same discipline shape as additions. Three paired artifacts, same PR:
|
|
42
|
+
|
|
43
|
+
1. **Ledger entry** under this ledger's `## Entries` section naming the removed slugs, the deletion SHA, the version bump (MINOR per content-remove policy), and the downstream cure (what adopters at an exact pin do).
|
|
44
|
+
2. **Allowlist row** in `.claude/hooks/tests/fixtures/2026-09-16-lite-catalog-closure/retired-entries.txt` naming each removed `<type>/<slug>` + `retired-in=<SHA>` + `reason=<one-line>`. The Feathers v1.6.1 snapshot guard (Test 16 in `.claude/hooks/tests/generate-lite-manifest.test.sh`) subtracts allowlist rows before asserting zero drops. Without the allowlist row the test fails and main CI goes red.
|
|
45
|
+
3. **Discipline amendment** when a new retirement class surfaces that this section does not cover (e.g., tier retirement, field retirement). Extend this `## Retirements` section rather than author a new rule, per Ousterhout's complexity-is-incremental principle.
|
|
46
|
+
|
|
47
|
+
When this rule fires on retirement:
|
|
48
|
+
|
|
49
|
+
- Any PR that deletes a file listed in the current `lite-manifest.json` under `.entries[].path`
|
|
50
|
+
- Any PR that retires a schema field that previously shipped as REQUIRED
|
|
51
|
+
|
|
52
|
+
Skips (same as additions):
|
|
53
|
+
|
|
54
|
+
- Content-only changes where the file stays but its content_hash shifts
|
|
55
|
+
- Comment-only edits
|
|
56
|
+
|
|
57
|
+
**Anchor luminaries for retirement:**
|
|
58
|
+
|
|
59
|
+
- `@luminary linus-torvalds` — retirement of a tier-tagged adopter-observable file is a userspace break; the ledger + allowlist + release-step6 check ship in one PR
|
|
60
|
+
- `@luminary hyrum-wright` — observable behavior becomes dependency with enough adopters; the 2 removed slugs were cited in rule bodies, sister docs, and the fixture snapshot; every retirement needs observable-surface enumeration first
|
|
61
|
+
- `@luminary michael-nygard` — current ledger discipline for additions is methodology at author time; retirement needs the same mechanical floor plus a stability-pattern gate (release-step6 pre-flight)
|
|
62
|
+
- `@luminary michael-feathers` — the fixture snapshot is the characterization test; allowlist is the author-time hook that keeps it green
|
|
63
|
+
- `@luminary vaughn-vernon` — `retired-entries.txt` is the anticorruption layer between the frozen snapshot and the current manifest
|
|
64
|
+
- `@luminary john-ousterhout` — contrarian: extend the existing rule with this section, do not author a new rule; the mechanical pre-commit hook (`pre-retire-discipline-check.sh`) is deferred to a follow-on PR so this one stays scope-tight
|
|
65
|
+
|
|
39
66
|
## Anti-patterns
|
|
40
67
|
|
|
41
68
|
**Schema field added but no ledger entry.** Release script pre-flight BLOCKs.
|
|
42
69
|
INSTEAD: add the entry in the same PR.
|
|
43
70
|
|
|
71
|
+
**File removed from the lite manifest without an allowlist row + ledger entry.** Test 16 (Feathers v1.6.1 snapshot guard) sees the drop and main CI goes red at the next run.
|
|
72
|
+
INSTEAD: append a row to `retired-entries.txt` naming `<type>/<slug> retired-in=<SHA> reason=<one-line>` AND add a `## Retirements`-aware entry to `standards/lite-manifest-schema-changes.md` in the same PR.
|
|
73
|
+
|
|
44
74
|
**Ledger entry written but wrong version bump.** Adopters plan their pin update against the wrong version.
|
|
45
75
|
INSTEAD: correct the bump before merge. AJV validation of the entry format catches inconsistency.
|
|
46
76
|
|