@thebassclef/lite 1.9.9 → 1.9.11
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 +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/artifact-ingestion-gate.sh +9 -3
- package/dist/lite/.claude/hooks/pre-commit-gate.sh +31 -0
- package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +58 -2
- package/dist/lite/.claude/hooks/save-state.sh +6 -1
- package/dist/lite/.claude/hooks/session-reflection.d/83-install-written-paths-rebuild.sh +111 -0
- package/dist/lite/.claude/hooks/session-reflection.d/97-last-block-count.sh +53 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/97-last-block-count.test.sh +131 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +22 -0
- package/dist/lite/.claude/hooks/state-validate.sh +8 -0
- package/dist/lite/.claude/rules/install-written-paths-mediation.md +74 -0
- package/dist/lite/.claude/rules/last-block-marker.md +79 -0
- package/dist/lite/.claude/skills/autonomous/SKILL.md +53 -7
- package/dist/lite/.claude/skills/build/SKILL.md +41 -5
- package/dist/lite/.claude/skills/deploy-prod/SKILL.md +10 -0
- package/dist/lite/.claude/skills/ia-model/SKILL.md +8 -0
- package/dist/lite/.claude/skills/interaction-design/SKILL.md +8 -0
- package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +9 -1
- package/dist/lite/.claude/skills/last-block/SKILL.md +99 -0
- package/dist/lite/.claude/skills/launch/SKILL.md +44 -12
- package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +5 -17
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +18 -18
- package/dist/lite/.claude/skills/personas/SKILL.md +1 -1
- package/dist/lite/.claude/skills/use-case/SKILL.md +5 -4
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/lib/capability-probe.sh +39 -1
- package/dist/lite/lib/composer-preflight.sh +20 -4
- package/dist/lite/lib/install-written-paths.sh +369 -0
- package/dist/lite/lib/last-block.sh +153 -0
- package/dist/lite/lib/luminary-pick.sh +35 -13
- package/dist/lite/lib/state.sh +82 -11
- package/dist/lite/scripts/interpret-input.sh +19 -4
- package/dist/lite/standards/lite-manifest-schema-changes.md +115 -0
- package/dist/lite/standards/lite-manifest.json +80 -26
- package/dist/lite/standards/state-spine.md +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ your browser.
|
|
|
67
67
|
|
|
68
68
|
## Current release
|
|
69
69
|
|
|
70
|
-
<!-- version-start -->1.9.
|
|
70
|
+
<!-- version-start -->1.9.11<!-- version-end -->
|
|
71
71
|
|
|
72
72
|
See [CHANGELOG.md](CHANGELOG.md) for the full changelog. Adopter-facing
|
|
73
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.11";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -143,9 +143,15 @@ if [ "$HAS_SOURCES_READ" -eq 1 ]; then
|
|
|
143
143
|
exit
|
|
144
144
|
}
|
|
145
145
|
')
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
# cli#311 cure — treat null / ~ / empty as "no parent declared" (Peirce three-case).
|
|
147
|
+
# YAML null shorthand is `null`, `~`, or an empty value. Prior code caught empty
|
|
148
|
+
# only; `parent_bet: null` tripped the gate on root goals that genuinely have no
|
|
149
|
+
# parent. Mirrors the #70 cure in bet-doc-gate.sh.
|
|
150
|
+
case "$PARENT_VALUE" in
|
|
151
|
+
"" | "null" | "Null" | "NULL" | "~" | "None")
|
|
152
|
+
continue
|
|
153
|
+
;;
|
|
154
|
+
esac
|
|
149
155
|
|
|
150
156
|
# Check if the parent path (or its basename without extension) appears
|
|
151
157
|
# in the Sources-read section. Accepts either:
|
|
@@ -201,6 +201,37 @@ if ! { declare -F skip_var_set >/dev/null && skip_var_set "SKIP_LEAD_LENS_SIGNOF
|
|
|
201
201
|
-e '^\.github/workflows/bassclef-.*\.yml$' \
|
|
202
202
|
-e '^scripts/[^/]+\.sh$' \
|
|
203
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
|
|
204
235
|
if [ -n "$STAGED_SUBSTRATE_LL" ]; then
|
|
205
236
|
LL_BRANCH_SLUG=$(git branch --show-current 2>/dev/null | tr '/' '-')
|
|
206
237
|
PREM_M="state/markers/pre-mortem/${LL_BRANCH_SLUG}.marker"
|
|
@@ -97,8 +97,36 @@ fi
|
|
|
97
97
|
# Filter to '+' lines only so a commit that REMOVES a pre-existing leak
|
|
98
98
|
# does not trip on the removed-content line. `+++` file headers stripped.
|
|
99
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
|
+
)
|
|
100
112
|
if [ -z "$BLOCK_REASON" ]; then
|
|
101
|
-
|
|
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"
|
|
102
130
|
if [ -n "$STAGED_DIFF" ]; then
|
|
103
131
|
MATCH=$(echo "$STAGED_DIFF" | grep -oE "$IDENTIFIER_PATTERNS" | head -1)
|
|
104
132
|
if [ -n "$MATCH" ]; then
|
|
@@ -108,6 +136,29 @@ if [ -z "$BLOCK_REASON" ]; then
|
|
|
108
136
|
fi
|
|
109
137
|
fi
|
|
110
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
|
+
|
|
111
162
|
# BLOCK if any pattern matched
|
|
112
163
|
if [ -n "$BLOCK_REASON" ]; then
|
|
113
164
|
cat >&2 <<EOF
|
|
@@ -128,7 +179,12 @@ Resolution paths:
|
|
|
128
179
|
1. Rewrite the commit message or staged content to drop the identifier
|
|
129
180
|
- Replace machine names with placeholders like <machine> or the branch
|
|
130
181
|
- Replace absolute paths with relative paths or \$HOME references
|
|
131
|
-
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):
|
|
132
188
|
SKIP_IDENTIFIER_LEAK_SCRUB=1 git commit ...
|
|
133
189
|
|
|
134
190
|
See .claude/rules/identifier-leak-prevention.md for full discipline.
|
|
@@ -209,7 +209,12 @@ DIRTY=$(git status --porcelain 2>/dev/null | grep -v '\.DS_Store' | grep -v 'SES
|
|
|
209
209
|
if [ -n "$DIRTY" ]; then
|
|
210
210
|
# Stage tracked modified files + any docs/ or chronicle/ or runtime state
|
|
211
211
|
git add -u 2>/dev/null || true
|
|
212
|
-
|
|
212
|
+
# docs/ except docs/prototypes/ — prototypes are review-gated mocks per
|
|
213
|
+
# .claude/rules/prototype-workflow.md; auto-save must not commit them
|
|
214
|
+
# before the operator confirms the rendered output (cli#323).
|
|
215
|
+
# Same pathspec pattern used for state/markers below.
|
|
216
|
+
git add 'docs/' ':!docs/prototypes' ':!docs/prototypes/**' 2>/dev/null || true
|
|
217
|
+
git add chronicle/ specs/ 2>/dev/null || true
|
|
213
218
|
# state/ except state/markers/ — markers are runtime gate evidence that
|
|
214
219
|
# trigger session-rescue false-positives if committed (bassclef#341 / WU-A2).
|
|
215
220
|
# Negative pathspec syntax requires git >= 2.13 (stable since 2017).
|
|
@@ -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
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# install-class: dual
|
|
4
|
+
# Session-reflection fragment — surface last-block marker count.
|
|
5
|
+
#
|
|
6
|
+
# Per @luminary don-norman signifier: when BLOCK banners from prior
|
|
7
|
+
# sessions sit in state/markers/last-block/, this fragment tells the
|
|
8
|
+
# adopter they exist and names /last-block as the retrieval path.
|
|
9
|
+
#
|
|
10
|
+
# Fires once at session-start. Reads marker count. Prints one line to
|
|
11
|
+
# stdout when count > 0. Silent on empty.
|
|
12
|
+
#
|
|
13
|
+
# Composition — sourced by session-reflection.sh under set -e. Must use
|
|
14
|
+
# `return`, not `exit`, else the coordinator dies. Per test-sufficiency
|
|
15
|
+
# criterion 12 (invocation-mode variation).
|
|
16
|
+
#
|
|
17
|
+
# Per bassclef-upstream#2036 Finding #10 + UC-skill-last-block.md.
|
|
18
|
+
|
|
19
|
+
# Resolve directory (same convention as lib/last-block.sh).
|
|
20
|
+
_last_block_count_dir() {
|
|
21
|
+
if [ -n "${LAST_BLOCK_DIR:-}" ]; then
|
|
22
|
+
echo "$LAST_BLOCK_DIR"
|
|
23
|
+
else
|
|
24
|
+
echo "${CLAUDE_PROJECT_DIR:-.}/state/markers/last-block"
|
|
25
|
+
fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
LBC_DIR="$(_last_block_count_dir)"
|
|
29
|
+
|
|
30
|
+
if [ ! -d "$LBC_DIR" ]; then
|
|
31
|
+
# Fresh install or dir never created. Silent.
|
|
32
|
+
# shellcheck disable=SC2317 # dual-use sourced-or-standalone exit path
|
|
33
|
+
return 0 2>/dev/null || exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
LBC_COUNT=$(find "$LBC_DIR" -maxdepth 1 -name '*.marker' -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
37
|
+
|
|
38
|
+
if [ "$LBC_COUNT" -gt 0 ]; then
|
|
39
|
+
if [ "$LBC_COUNT" = "1" ]; then
|
|
40
|
+
echo "### LAST BLOCK"
|
|
41
|
+
echo ""
|
|
42
|
+
echo "1 BLOCK banner stored. Read it with /last-block."
|
|
43
|
+
echo ""
|
|
44
|
+
else
|
|
45
|
+
echo "### LAST BLOCKS"
|
|
46
|
+
echo ""
|
|
47
|
+
echo "${LBC_COUNT} BLOCK banners stored. Read the newest with /last-block, or list all with /last-block --list."
|
|
48
|
+
echo ""
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# shellcheck disable=SC2317 # dual-use sourced-or-standalone exit path
|
|
53
|
+
return 0 2>/dev/null || exit 0
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# fixtures: bassclef-upstream-self
|
|
4
|
+
# shellcheck disable=SC2034
|
|
5
|
+
# Tier 0 test for .claude/hooks/session-reflection.d/97-last-block-count.sh
|
|
6
|
+
#
|
|
7
|
+
# Covers invocation-mode variation (criterion 12 of test-sufficiency rubric):
|
|
8
|
+
# both standalone and sourced-under-set-e paths.
|
|
9
|
+
#
|
|
10
|
+
# Ticket: bassclef-upstream#2036 Finding #10
|
|
11
|
+
#
|
|
12
|
+
# test-list:
|
|
13
|
+
# [x] T01: standalone + no dir -> silent, exit 0, no return error
|
|
14
|
+
# [x] T02: standalone + empty dir -> silent, exit 0
|
|
15
|
+
# [x] T03: standalone + 1 marker -> prints "1 BLOCK banner stored"
|
|
16
|
+
# [x] T04: standalone + 3 markers -> prints "3 BLOCK banners stored"
|
|
17
|
+
# [x] T05: sourced under set -e -> parent survives (no exit)
|
|
18
|
+
# [x] T06: honors LAST_BLOCK_DIR env override
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
FRAG_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
24
|
+
FRAG="$FRAG_DIR/97-last-block-count.sh"
|
|
25
|
+
|
|
26
|
+
if [ ! -f "$FRAG" ]; then
|
|
27
|
+
echo "RED: $FRAG does not exist yet" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
FIXTURE_ROOT="$(mktemp -d)"
|
|
32
|
+
RESULTS_FILE="$FIXTURE_ROOT/results.log"
|
|
33
|
+
touch "$RESULTS_FILE"
|
|
34
|
+
trap 'rm -rf "$FIXTURE_ROOT"' EXIT
|
|
35
|
+
|
|
36
|
+
pass() { echo "PASS" >> "$RESULTS_FILE"; echo "PASS: $1"; }
|
|
37
|
+
fail() { echo "FAIL" >> "$RESULTS_FILE"; echo "FAIL: $1"; }
|
|
38
|
+
|
|
39
|
+
# T01: standalone + no dir
|
|
40
|
+
( LAST_BLOCK_DIR="$FIXTURE_ROOT/nonexistent"
|
|
41
|
+
export LAST_BLOCK_DIR
|
|
42
|
+
OUT=$(bash "$FRAG" 2>&1)
|
|
43
|
+
RC=$?
|
|
44
|
+
if [ "$RC" = "0" ] && [ -z "$OUT" ]; then
|
|
45
|
+
pass "T01: standalone + no dir -> silent"
|
|
46
|
+
else
|
|
47
|
+
fail "T01: standalone + no dir -> silent (rc=$RC out=$OUT)"
|
|
48
|
+
fi
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# T02: standalone + empty dir
|
|
52
|
+
( LAST_BLOCK_DIR="$(mktemp -d)"
|
|
53
|
+
export LAST_BLOCK_DIR
|
|
54
|
+
OUT=$(bash "$FRAG" 2>&1)
|
|
55
|
+
RC=$?
|
|
56
|
+
if [ "$RC" = "0" ] && [ -z "$OUT" ]; then
|
|
57
|
+
pass "T02: standalone + empty dir -> silent"
|
|
58
|
+
else
|
|
59
|
+
fail "T02: standalone + empty dir -> silent (rc=$RC out=$OUT)"
|
|
60
|
+
fi
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# T03: standalone + 1 marker
|
|
64
|
+
( DIR="$(mktemp -d)"
|
|
65
|
+
LAST_BLOCK_DIR="$DIR"
|
|
66
|
+
export LAST_BLOCK_DIR
|
|
67
|
+
echo "hook: a" > "$DIR/a.marker"
|
|
68
|
+
OUT=$(bash "$FRAG" 2>&1)
|
|
69
|
+
if echo "$OUT" | grep -qF "1 BLOCK banner stored"; then
|
|
70
|
+
pass "T03: 1 marker -> prints singular"
|
|
71
|
+
else
|
|
72
|
+
fail "T03: 1 marker (got: $OUT)"
|
|
73
|
+
fi
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# T04: standalone + 3 markers
|
|
77
|
+
( DIR="$(mktemp -d)"
|
|
78
|
+
LAST_BLOCK_DIR="$DIR"
|
|
79
|
+
export LAST_BLOCK_DIR
|
|
80
|
+
echo "hook: a" > "$DIR/a.marker"
|
|
81
|
+
echo "hook: b" > "$DIR/b.marker"
|
|
82
|
+
echo "hook: c" > "$DIR/c.marker"
|
|
83
|
+
OUT=$(bash "$FRAG" 2>&1)
|
|
84
|
+
if echo "$OUT" | grep -qF "3 BLOCK banners stored"; then
|
|
85
|
+
pass "T04: 3 markers -> prints plural with count"
|
|
86
|
+
else
|
|
87
|
+
fail "T04: 3 markers (got: $OUT)"
|
|
88
|
+
fi
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# T05: sourced under set -e -> parent survives (criterion 12)
|
|
92
|
+
( DIR="$(mktemp -d)"
|
|
93
|
+
LAST_BLOCK_DIR="$DIR"
|
|
94
|
+
export LAST_BLOCK_DIR
|
|
95
|
+
echo "hook: a" > "$DIR/a.marker"
|
|
96
|
+
# Simulate coordinator: set -e wrapper + source
|
|
97
|
+
CANARY_FILE="$FIXTURE_ROOT/canary-t05"
|
|
98
|
+
(
|
|
99
|
+
set -e
|
|
100
|
+
# shellcheck source=/dev/null
|
|
101
|
+
source "$FRAG"
|
|
102
|
+
# If frag used `exit` instead of `return`, this line never runs.
|
|
103
|
+
touch "$CANARY_FILE"
|
|
104
|
+
) >/dev/null 2>&1 || true
|
|
105
|
+
if [ -f "$CANARY_FILE" ]; then
|
|
106
|
+
pass "T05: sourced under set -e survives (canary ran)"
|
|
107
|
+
else
|
|
108
|
+
fail "T05: sourced under set -e survives (canary missing)"
|
|
109
|
+
fi
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# T06: honors LAST_BLOCK_DIR env override
|
|
113
|
+
( DIR="$(mktemp -d)"
|
|
114
|
+
LAST_BLOCK_DIR="$DIR"
|
|
115
|
+
export LAST_BLOCK_DIR
|
|
116
|
+
echo "hook: a" > "$DIR/a.marker"
|
|
117
|
+
OUT=$(bash "$FRAG" 2>&1)
|
|
118
|
+
if echo "$OUT" | grep -qF "1 BLOCK banner stored"; then
|
|
119
|
+
pass "T06: honors LAST_BLOCK_DIR override"
|
|
120
|
+
else
|
|
121
|
+
fail "T06: honors LAST_BLOCK_DIR override (got: $OUT)"
|
|
122
|
+
fi
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
echo
|
|
126
|
+
echo "=== Results ==="
|
|
127
|
+
PASS=$(awk '/^PASS$/ {c++} END {print c+0}' "$RESULTS_FILE")
|
|
128
|
+
FAIL=$(awk '/^FAIL$/ {c++} END {print c+0}' "$RESULTS_FILE")
|
|
129
|
+
echo "PASS: $PASS"
|
|
130
|
+
echo "FAIL: $FAIL"
|
|
131
|
+
if [ "$FAIL" -gt 0 ]; then exit 1; fi
|
|
@@ -74,6 +74,14 @@ else
|
|
|
74
74
|
fi
|
|
75
75
|
export BASSCLEF_DIR
|
|
76
76
|
|
|
77
|
+
# Source lib/last-block.sh so banner calls persist markers. Per Finding
|
|
78
|
+
# #10 — adopters need a first-class way to retrieve BLOCK text they saw.
|
|
79
|
+
# The lib fails soft; missing lib leaves the banners untouched.
|
|
80
|
+
# shellcheck source=/dev/null
|
|
81
|
+
if [ -f "${BASSCLEF_DIR}/lib/last-block.sh" ]; then
|
|
82
|
+
source "${BASSCLEF_DIR}/lib/last-block.sh"
|
|
83
|
+
fi
|
|
84
|
+
|
|
77
85
|
# High-contrast BLOCKED banner; used by every module that raises a gap.
|
|
78
86
|
# RFC-0008 F3 — "Silence is not deferral" is a commitment device. It works
|
|
79
87
|
# because it forecloses the easy out, and it is consumable: spend it on a
|
|
@@ -81,9 +89,13 @@ export BASSCLEF_DIR
|
|
|
81
89
|
# Callers that can prove their own state is benign pass a second argument
|
|
82
90
|
# and get a softer close. Callers that pass one argument behave exactly as
|
|
83
91
|
# before, so the other 27 sites are untouched.
|
|
92
|
+
#
|
|
93
|
+
# Per Finding #10 — each call now also writes a marker so the adopter can
|
|
94
|
+
# retrieve the exact banner text later via /last-block.
|
|
84
95
|
blocked_banner() {
|
|
85
96
|
local msg="$1"
|
|
86
97
|
local severity="${2:-blocking}"
|
|
98
|
+
local hook_name="${BASSCLEF_CALLING_HOOK:-session-reflection}"
|
|
87
99
|
echo ""
|
|
88
100
|
case "$severity" in
|
|
89
101
|
info)
|
|
@@ -106,9 +118,14 @@ blocked_banner() {
|
|
|
106
118
|
echo "────────────────────────────────────────────"
|
|
107
119
|
echo "ACTION: resolve OR explicitly defer (per .claude/rules/blocked-items.md)."
|
|
108
120
|
echo "Silence is not deferral. Propose this as item #1 in your session plan."
|
|
121
|
+
echo "(Retrieve this banner later with: /last-block ${hook_name})"
|
|
109
122
|
;;
|
|
110
123
|
esac
|
|
111
124
|
echo ""
|
|
125
|
+
# Persist marker for /last-block retrieval. Fail-soft.
|
|
126
|
+
if type -t record_last_block >/dev/null 2>&1; then
|
|
127
|
+
record_last_block "$hook_name" "$severity" "$msg" 2>/dev/null || true
|
|
128
|
+
fi
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
# Vendor-swap constant — the AI in the harness running bassclef. Adopter
|
|
@@ -131,7 +148,12 @@ guardrail_banner() {
|
|
|
131
148
|
echo "────────────────────────────────────────────"
|
|
132
149
|
echo "bassclef is steering $AI_NAME to $step — $retry"
|
|
133
150
|
echo "────────────────────────────────────────────"
|
|
151
|
+
echo "(Retrieve this banner later with: /last-block ${hook_name})"
|
|
134
152
|
echo ""
|
|
153
|
+
# Persist marker for /last-block retrieval. Fail-soft.
|
|
154
|
+
if type -t record_last_block >/dev/null 2>&1; then
|
|
155
|
+
record_last_block "$hook_name" "guardrail" "$step — $retry" 2>/dev/null || true
|
|
156
|
+
fi
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
# RFC-0008 F6 — this block is steering aimed at the agent, but the adopter
|
|
@@ -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
|
|
|
@@ -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
|