@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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/lite/.claude/hooks/artifact-ingestion-gate.sh +9 -3
  6. package/dist/lite/.claude/hooks/pre-commit-gate.sh +31 -0
  7. package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +58 -2
  8. package/dist/lite/.claude/hooks/save-state.sh +6 -1
  9. package/dist/lite/.claude/hooks/session-reflection.d/83-install-written-paths-rebuild.sh +111 -0
  10. package/dist/lite/.claude/hooks/session-reflection.d/97-last-block-count.sh +53 -0
  11. package/dist/lite/.claude/hooks/session-reflection.d/tests/97-last-block-count.test.sh +131 -0
  12. package/dist/lite/.claude/hooks/session-reflection.sh +22 -0
  13. package/dist/lite/.claude/hooks/state-validate.sh +8 -0
  14. package/dist/lite/.claude/rules/install-written-paths-mediation.md +74 -0
  15. package/dist/lite/.claude/rules/last-block-marker.md +79 -0
  16. package/dist/lite/.claude/skills/autonomous/SKILL.md +53 -7
  17. package/dist/lite/.claude/skills/build/SKILL.md +41 -5
  18. package/dist/lite/.claude/skills/deploy-prod/SKILL.md +10 -0
  19. package/dist/lite/.claude/skills/ia-model/SKILL.md +8 -0
  20. package/dist/lite/.claude/skills/interaction-design/SKILL.md +8 -0
  21. package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +9 -1
  22. package/dist/lite/.claude/skills/last-block/SKILL.md +99 -0
  23. package/dist/lite/.claude/skills/launch/SKILL.md +44 -12
  24. package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +5 -17
  25. package/dist/lite/.claude/skills/onboard-repo/SKILL.md +18 -18
  26. package/dist/lite/.claude/skills/personas/SKILL.md +1 -1
  27. package/dist/lite/.claude/skills/use-case/SKILL.md +5 -4
  28. package/dist/lite/bassclef-version.json +5 -5
  29. package/dist/lite/lib/capability-probe.sh +39 -1
  30. package/dist/lite/lib/composer-preflight.sh +20 -4
  31. package/dist/lite/lib/install-written-paths.sh +369 -0
  32. package/dist/lite/lib/last-block.sh +153 -0
  33. package/dist/lite/lib/luminary-pick.sh +35 -13
  34. package/dist/lite/lib/state.sh +82 -11
  35. package/dist/lite/scripts/interpret-input.sh +19 -4
  36. package/dist/lite/standards/lite-manifest-schema-changes.md +115 -0
  37. package/dist/lite/standards/lite-manifest.json +80 -26
  38. package/dist/lite/standards/state-spine.md +1 -0
  39. 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.9<!-- version-end -->
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
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const version = "1.9.9";
3
+ const version = "1.9.11";
4
4
  exports.version = version;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const version: "1.9.9";
1
+ export declare const version: "1.9.11";
2
2
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const version = "1.9.9";
1
+ const version = "1.9.11";
2
2
  export {
3
3
  version
4
4
  };
@@ -143,9 +143,15 @@ if [ "$HAS_SOURCES_READ" -eq 1 ]; then
143
143
  exit
144
144
  }
145
145
  ')
146
- if [ -z "$PARENT_VALUE" ]; then
147
- continue
148
- fi
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
- STAGED_DIFF=$(git diff --cached 2>/dev/null | grep '^+' | grep -v '^+++' || echo "")
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. Override (rare — for legitimate cases like DON'T-do-this documentation):
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
- git add docs/ chronicle/ specs/ 2>/dev/null || true
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