@thebassclef/lite 1.0.0 → 1.0.2

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 (32) hide show
  1. package/dist/cli.cjs +240 -23
  2. package/dist/cli.js +242 -25
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/lite/.claude/hooks/artifact-ingestion-gate.sh +357 -0
  7. package/dist/lite/.claude/hooks/assert-verify-steering.sh +77 -0
  8. package/dist/lite/.claude/hooks/bassclef-source-config-validate.sh +215 -0
  9. package/dist/lite/.claude/hooks/bassclef-sync.sh +716 -0
  10. package/dist/lite/.claude/hooks/compound-noun-scrub.sh +292 -0
  11. package/dist/lite/.claude/hooks/kiss-expansion-inject.sh +69 -0
  12. package/dist/lite/.claude/hooks/longrun-prep-compounding-sequence-check.sh +492 -0
  13. package/dist/lite/.claude/hooks/plain-english-steering.sh +156 -0
  14. package/dist/lite/.claude/hooks/post-skill-friction-check.sh +177 -0
  15. package/dist/lite/.claude/hooks/post-skill-telemetry.sh +62 -0
  16. package/dist/lite/.claude/hooks/pre-build-gate.sh +511 -0
  17. package/dist/lite/.claude/hooks/pre-commit-gate.sh +451 -0
  18. package/dist/lite/.claude/hooks/session-end.sh +433 -0
  19. package/dist/lite/.claude/hooks/session-reflection.sh +303 -0
  20. package/dist/lite/.claude/hooks/skill-body-grade-gate.sh +219 -0
  21. package/dist/lite/.claude/hooks/skill-body-intent-drift.sh +107 -0
  22. package/dist/lite/.claude/hooks/state-validate.sh +271 -0
  23. package/dist/lite/.claude/hooks/substrate-clarity-gate.sh +1110 -0
  24. package/dist/lite/.claude/hooks/temperance-gate.sh +147 -0
  25. package/dist/lite/.claude/hooks/testing-tier-enforce.sh +233 -0
  26. package/dist/lite/.claude/hooks/turn-prose-grade-measure.sh +219 -0
  27. package/dist/lite/.claude/hooks/turn-prose-kiss-check.sh +463 -0
  28. package/dist/lite/.claude/hooks/vocabulary-migration-check.sh +171 -0
  29. package/dist/lite/.claude/hooks/whereami-utc-gate.sh +142 -0
  30. package/dist/lite/CLAUDE.md +2 -2
  31. package/dist/lite/whereami.md +1 -1
  32. package/package.json +1 -1
@@ -0,0 +1,716 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # Bassclef Substrate Sync — fires on SessionStart.
4
+ # Operator-class hook: lives on bassclef-upstream operators' machines only.
5
+ # Adopter consumers receive presence/install/bassclef-sync.template.sh instead
6
+ # (sourced from canonical until the WU-0 absorb gap is closed).
7
+ # Ensures bassclef standards are available regardless of device.
8
+ #
9
+ # 1. Clones/pulls bassclef repo
10
+ # 2. Symlinks bassclef skills into project .claude/skills/ (auto-discovery)
11
+ # 3. Installs general hooks + agents to ~/.claude/
12
+ # 4. Writes ~/.claude/settings.json for user-level hook wiring (cloud)
13
+ # 5. Sets env var for rules/CLAUDE.md inheritance
14
+
15
+ # === Section 0a: SELF_MODE detection (#915) ===
16
+ # Sister to Cure 1 in #901 (commit 4b6c258d) which added SELF_MODE
17
+ # detection to the adopter template at presence/install/bassclef-sync.template.sh.
18
+ # This is the operator-class SessionStart hook — it needs the same guard.
19
+ #
20
+ # Detection: git remote origin URL matches sunj-labs/(bassclef|bassclef-upstream)(\.git)?$
21
+ # on both HTTPS and SSH URL shapes. When SELF_MODE=1, hook exits 0 early
22
+ # to prevent the sync flow from re-symlinking tracked hooks in the
23
+ # substrate source repo.
24
+ #
25
+ # Live proof of the class this cures: session 2026-07-26d WU-0 reverted 84
26
+ # T + 1 M hooks caused by this SessionStart re-symlink in bassclef-upstream.
27
+ #
28
+ # Env override SKIP_SELF_MODE_DETECTION=1 forces SELF_MODE=0 regardless
29
+ # of remote — for adopter forks of bassclef-upstream that legitimately
30
+ # want the sync flow.
31
+ #
32
+ # Function is defined ABOVE the main-body INPUT read so tests can source
33
+ # the hook with BASSCLEF_SYNC_TEST_MODE=1 and access the function without
34
+ # triggering the sync flow.
35
+ bassclef_detect_self_mode() {
36
+ local cwd="${1:-.}"
37
+
38
+ # Env override wins
39
+ if [ "${SKIP_SELF_MODE_DETECTION:-0}" = "1" ]; then
40
+ echo "0"
41
+ return 0
42
+ fi
43
+
44
+ # No git → not substrate source
45
+ command -v git >/dev/null 2>&1 || { echo "0"; return 0; }
46
+
47
+ # Get remote origin URL
48
+ local remote_url
49
+ remote_url=$(git -C "$cwd" config --get remote.origin.url 2>/dev/null)
50
+ if [ -z "$remote_url" ]; then
51
+ echo "0"
52
+ return 0
53
+ fi
54
+
55
+ # Match sunj-labs/(bassclef|bassclef-upstream) with optional .git suffix
56
+ # HTTPS: https://github.com/sunj-labs/bassclef.git
57
+ # SSH: git@github.com:sunj-labs/bassclef-upstream.git
58
+ if echo "$remote_url" | grep -qE 'sunj-labs/(bassclef|bassclef-upstream)(\.git)?$'; then
59
+ echo "1"
60
+ return 0
61
+ fi
62
+
63
+ echo "0"
64
+ return 0
65
+ }
66
+
67
+ # === _sync_install_hooks (goal 14c Step 5) ===
68
+ # Install a hard-coded list of substrate hooks to the operator's
69
+ # ~/.claude/hooks/. For each name: if the source hook declares a
70
+ # # install-class: header, route via lib/hook-installer.sh's
71
+ # install_by_class (which honors operator|project|dual). If no header,
72
+ # fall back to legacy operator-scope copy per ADR-058 c2 grace window
73
+ # (through 2026-10-31).
74
+ #
75
+ # Args:
76
+ # $1 — BASSCLEF_DIR (source of hooks + lib)
77
+ # $2 — user-scope target dir (default $HOME/.claude/hooks)
78
+ #
79
+ # Discipline (per ADR-058 constraints 1-4):
80
+ # c1 — install_by_class wrapped in subshell; lib's strict mode
81
+ # stays contained; caller's strict-mode state preserved
82
+ # c2 — unheadered hooks install where they install today +
83
+ # one trace line `skipped-no-header <hook>` fires
84
+ # c3 — CLAUDE_PROJECT_DIR resolved (git rev-parse fallback) and
85
+ # exported before the loop so project/dual class routing works
86
+ # c4 — settings.json command strings unchanged; wiring self-heal
87
+ # continues at Section 4 (unrelated to this function)
88
+ _sync_install_hooks() {
89
+ local bassclef_dir="$1"
90
+ local user_target_dir="${2:-$HOME/.claude/hooks}"
91
+
92
+ local hook_names=(
93
+ bug-diagnosis.sh
94
+ tool-failure-diagnosis.sh
95
+ trace-helper.sh
96
+ deploy-guard.sh
97
+ memory-write-audit.sh
98
+ turn-prose-kiss-check.sh
99
+ session-end.sh
100
+ bet-doc-gate.sh
101
+ substrate-clarity-gate.sh
102
+ )
103
+
104
+ mkdir -p "$user_target_dir" 2>/dev/null || true
105
+
106
+ local installer_lib="$bassclef_dir/lib/hook-installer.sh"
107
+ local dispatch_available=0
108
+ [ -f "$installer_lib" ] && dispatch_available=1
109
+
110
+ # ADR-058 c3 — resolve CLAUDE_PROJECT_DIR before loop
111
+ if [ -z "${CLAUDE_PROJECT_DIR:-}" ]; then
112
+ if command -v git >/dev/null 2>&1; then
113
+ CLAUDE_PROJECT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || true)"
114
+ fi
115
+ if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
116
+ export CLAUDE_PROJECT_DIR
117
+ fi
118
+ fi
119
+
120
+ for hook_name in "${hook_names[@]}"; do
121
+ local hook_src="$bassclef_dir/.claude/hooks/$hook_name"
122
+ if [ ! -f "$hook_src" ]; then
123
+ continue
124
+ fi
125
+
126
+ local has_header=0
127
+ if head -10 "$hook_src" 2>/dev/null | grep -qE '^# install-class:[[:space:]]*(operator|project|dual)[[:space:]]*$'; then
128
+ has_header=1
129
+ fi
130
+
131
+ if [ "$has_header" = "1" ] && [ "$dispatch_available" = "1" ]; then
132
+ # ADR-058 c1 — subshell wraps install_by_class so lib strict
133
+ # mode never leaks into this script. On subshell failure, fall
134
+ # back to legacy operator-scope copy to preserve adopter
135
+ # contract per c4 (settings.json command strings byte-identical).
136
+ # shellcheck disable=SC1090 # dynamic source path
137
+ if ! ( source "$installer_lib" 2>/dev/null && install_by_class "$hook_src" ) >/dev/null 2>&1; then
138
+ cp "$hook_src" "$user_target_dir/" 2>/dev/null || true
139
+ chmod +x "$user_target_dir/$hook_name" 2>/dev/null || true
140
+ fi
141
+ else
142
+ # ADR-058 c2 fallback — grace window through 2026-10-31.
143
+ cp "$hook_src" "$user_target_dir/" 2>/dev/null || true
144
+ chmod +x "$user_target_dir/$hook_name" 2>/dev/null || true
145
+ if [ "$has_header" = "0" ] && command -v trace_log >/dev/null 2>&1; then
146
+ trace_log "ok" "sync-install-hooks-skipped-no-header" "$hook_name"
147
+ fi
148
+ fi
149
+ done
150
+ }
151
+
152
+ # === Test-mode guard (#915) ===
153
+ # Tests source this hook to access helpers without triggering main sync.
154
+ # Early return under BASSCLEF_SYNC_TEST_MODE=1. Helpers defined ABOVE.
155
+ if [ "${BASSCLEF_SYNC_TEST_MODE:-0}" = "1" ]; then
156
+ return 0 2>/dev/null || exit 0
157
+ fi
158
+
159
+ # === Hook liveness heartbeat (WU-3 of bet 2026-07-31d; closes #1002) ===
160
+ # Silent-fail — a missing lib never crashes the hook. Discipline: ADR-048 +
161
+ # standards/hook-cadence.json. Session-start reflection module (WU-4.5) checks
162
+ # marker mtime against expected cadence.
163
+ {
164
+ _hb_sd="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
165
+ for _hb_c in "${_hb_sd}/../../lib/hook-heartbeat.sh" "${HOME:-/}/lib/hook-heartbeat.sh"; do
166
+ [ -f "$_hb_c" ] && source "$_hb_c" && heartbeat_mark "bassclef-sync" && break
167
+ done
168
+ unset _hb_sd _hb_c
169
+ } 2>/dev/null || true
170
+
171
+ INPUT=$(cat)
172
+ CWD=$(echo "$INPUT" | jq -r '.cwd // "."')
173
+ cd "$CWD"
174
+
175
+ # === Section 0a fires: exit early if this repo is substrate source ===
176
+ SELF_MODE=$(bassclef_detect_self_mode "$CWD")
177
+ if [ "$SELF_MODE" = "1" ]; then
178
+ echo "SELF_MODE detected — skipping sync in substrate source repo." >&2
179
+ echo " (Override with SKIP_SELF_MODE_DETECTION=1 for adopter forks.)" >&2
180
+ exit 0
181
+ fi
182
+
183
+ # Bassclef substrate location — peer clone at ../bassclef (post-rename).
184
+ # Pre-rename ../canonical fallback removed 2026-06-24 per bassclef-upstream#317
185
+ # Phase 0: zero adopters carry the canonical/ path; operator's local
186
+ # ~/src/sunj-labs/canonical was removed before this cleanup.
187
+ BASSCLEF_DIR="$CWD/../bassclef"
188
+ BASSCLEF_REPO="https://github.com/sunj-labs/bassclef.git"
189
+ SYNC_STATUS="up-to-date"
190
+ SYNC_NEW=""
191
+
192
+ # === 1. Clone or pull bassclef ===
193
+ if [ ! -d "$BASSCLEF_DIR/.claude" ]; then
194
+ if git clone "$BASSCLEF_REPO" "$BASSCLEF_DIR"; then
195
+ SYNC_STATUS="freshly cloned"
196
+ else
197
+ echo "### Bassclef sync — full-tier not synced"
198
+ echo ""
199
+ echo "Optional: set up GitHub auth to sync the full bassclef substrate."
200
+ echo "Lite tier works without it — your session is fine."
201
+ echo "Skills / rules / agents beyond lite become available once sync succeeds."
202
+ echo ""
203
+ exit 0
204
+ fi
205
+ else
206
+ BEFORE=$(git -C "$BASSCLEF_DIR" rev-parse HEAD 2>/dev/null)
207
+ git -C "$BASSCLEF_DIR" pull --quiet 2>/dev/null || true
208
+ AFTER=$(git -C "$BASSCLEF_DIR" rev-parse HEAD 2>/dev/null)
209
+
210
+ if [ "$BEFORE" != "$AFTER" ]; then
211
+ SYNC_STATUS="updated"
212
+ SYNC_NEW=$(git -C "$BASSCLEF_DIR" log --oneline "$BEFORE".."$AFTER" 2>/dev/null)
213
+ fi
214
+ fi
215
+
216
+ # === 2. Symlink bassclef skills into project for auto-discovery ===
217
+ mkdir -p "$CWD/.claude/skills" 2>/dev/null || true
218
+ for SKILL_DIR in "$BASSCLEF_DIR/.claude/skills/"*/; do
219
+ SKILL_NAME=$(basename "$SKILL_DIR")
220
+ TARGET="$CWD/.claude/skills/$SKILL_NAME"
221
+ # Only symlink if not already an app-specific skill (real directory, not symlink)
222
+ if [ -L "$TARGET" ]; then
223
+ # Existing symlink — update it
224
+ rm "$TARGET"
225
+ ln -s "$SKILL_DIR" "$TARGET"
226
+ elif [ ! -d "$TARGET" ]; then
227
+ # Doesn't exist — create symlink
228
+ ln -s "$SKILL_DIR" "$TARGET"
229
+ fi
230
+ # If it's a real directory (app-specific), leave it alone
231
+ done
232
+
233
+ # === 2b. Park orphan ~/.claude/skills + agents + hooks symlinks (widened per ADR-033 + bassclef#278) ===
234
+ # Uses the shared orphan-parking helper. Closes the skill / agent / hook
235
+ # duplication-in-available-skills bug. The helper parks user-level
236
+ # symlinks AND cp'd files whose basename appears in project-level —
237
+ # preserves real directories untouched + EXCLUDES hooks wired in
238
+ # settings.json at $HOME paths (per #294); backs up moved entries to
239
+ # dated dirs that survive at least 30 days per Hyrum + Linus discipline.
240
+ PARKING_HELPER="$BASSCLEF_DIR/scripts/bassclef-orphan-parking.sh"
241
+ USER_SETTINGS="$HOME/.claude/settings.json"
242
+ ORPHAN_COUNT=0
243
+ if [ -f "$PARKING_HELPER" ]; then
244
+ for surface in skills agents hooks; do
245
+ USER_SURFACE="$HOME/.claude/$surface"
246
+ PROJECT_SURFACE="$CWD/.claude/$surface"
247
+ if [ -d "$USER_SURFACE" ] && [ -d "$PROJECT_SURFACE" ]; then
248
+ mkdir -p "$HOME/.claude" 2>/dev/null
249
+ # Pass user settings.json as 5th arg so wired hooks are excluded
250
+ # from parking. Per #294 — broke the Stop hook on this machine when
251
+ # parking removed session-end.sh that settings.json wires.
252
+ PARKED_LINE=$(bash "$PARKING_HELPER" "$USER_SURFACE" "$PROJECT_SURFACE" "$HOME/.claude" "$surface" "$USER_SETTINGS" 2>/dev/null || echo "")
253
+ if [ -n "$PARKED_LINE" ]; then
254
+ # Extract the count from "parked N <surface> orphans → <dir>"
255
+ COUNT_N=$(echo "$PARKED_LINE" | grep -oE 'parked [0-9]+' | grep -oE '[0-9]+' | head -1)
256
+ [ -n "$COUNT_N" ] && ORPHAN_COUNT=$((ORPHAN_COUNT + COUNT_N))
257
+ fi
258
+ fi
259
+ done
260
+ fi
261
+
262
+ # === 2c. Adopter self-heal — auto-migrate stale canonical references ===
263
+ # Per ADR-031 we-dont-break-adopters + Nygard's Steady State pattern.
264
+ # When this hook runs in an adopter repo (CWD is NOT bassclef-upstream and
265
+ # does NOT have architecture/dual-repo-flow.md), invoke the migration script
266
+ # to repair any stale ../canonical/ references in symlinks + settings.json
267
+ # + dispatcher + statusline. Idempotent — no-op on already-migrated repos.
268
+ #
269
+ # Banner surfaces per-surface info (not just count) so adopters who
270
+ # customized any of the migrated surfaces see exactly what changed.
271
+ # Closes Yellow 2 of the 2026-06-22 release-readiness review.
272
+ MIGRATE_SCRIPT="$BASSCLEF_DIR/scripts/migrate-adopter-references.sh"
273
+ SUMMARY_HELPER="$BASSCLEF_DIR/scripts/bassclef-migrate-summary.sh"
274
+ if [ -f "$MIGRATE_SCRIPT" ] && [ ! -f "$CWD/architecture/dual-repo-flow.md" ]; then
275
+ MIGRATE_OUT=$(bash "$MIGRATE_SCRIPT" --apply "$CWD" 2>&1)
276
+ MIGRATE_CHANGES=$(echo "$MIGRATE_OUT" | awk -F: '/^changes:/ {print $2}' | tr -d ' ')
277
+ if [ -n "$MIGRATE_CHANGES" ] && [ "$MIGRATE_CHANGES" -gt 0 ]; then
278
+ PER_SURFACE=""
279
+ # Check -f (file exists), not -x (executable). We invoke via `bash $HELPER`
280
+ # so the executable bit is irrelevant; the bit may not survive tarball
281
+ # fetches across adopter clones (per reviewer note on PR #291).
282
+ if [ -f "$SUMMARY_HELPER" ]; then
283
+ PER_SURFACE=$(echo "$MIGRATE_OUT" | bash "$SUMMARY_HELPER" 2>/dev/null)
284
+ fi
285
+ SYNC_NEW="${SYNC_NEW}
286
+ Adopter self-heal: $MIGRATE_CHANGES surface(s) migrated. Old setup keeps working; new setup matches current bassclef substrate. Per ADR-031."
287
+ if [ -n "$PER_SURFACE" ]; then
288
+ SYNC_NEW="${SYNC_NEW}
289
+ ${PER_SURFACE}"
290
+ fi
291
+ fi
292
+ fi
293
+
294
+ # === 3. Install general hooks to ~/.claude/ ===
295
+ # Delegates to _sync_install_hooks (goal 14c Step 5 per ADR-058).
296
+ # The hard-coded 9-name list that lived here through v0.41.1 moved
297
+ # into the function above. Each hook now routes by its own
298
+ # # install-class: header when declared; unheadered hooks fall back
299
+ # to legacy operator-scope copy per ADR-058 c2 grace window
300
+ # (through 2026-10-31). Section 4 self-heal continues to preserve
301
+ # adopter contract per c4 (settings.json command strings unchanged).
302
+ _sync_install_hooks "$BASSCLEF_DIR" "$HOME/.claude/hooks"
303
+
304
+ # === Install agents to ~/.claude/ ===
305
+ mkdir -p ~/.claude/agents 2>/dev/null || true
306
+ cp "$BASSCLEF_DIR/.claude/agents/"*.md ~/.claude/agents/ 2>/dev/null || true
307
+
308
+ # === 4. Versioned self-heal of ~/.claude/settings.json ===
309
+ # WU-3 of bet 2026-06-24a-settings-hardening (#313). Replaces the
310
+ # brittle multi-grep + destructive heredoc rewrite with:
311
+ # 1. Version-field comparison (read_settings_version).
312
+ # 2. Additive merge that preserves operator's top-level keys (theme,
313
+ # statusLine, permissions, alwaysThinkingEnabled, etc.) and
314
+ # operator-only event types — bassclef wins per-event for events
315
+ # it ships.
316
+ # 3. Hoare postcondition: after self-heal, drift detector MUST
317
+ # return clean (compare_wirings .ok == true). Else exit 1 with
318
+ # structured stderr — failure compounds across every adopter,
319
+ # so failing loud beats failing silent.
320
+ # Closes the bug class diagnosed 2026-06-24 (operator's settings.json
321
+ # never picked up substrate's new hook wirings).
322
+ WIRINGS_LIB="$BASSCLEF_DIR/.claude/hooks/_lib/wirings.sh"
323
+ PROJECT_SETTINGS="$BASSCLEF_DIR/.claude/settings.json"
324
+ USER_SETTINGS="$HOME/.claude/settings.json"
325
+
326
+ sync_self_heal() {
327
+ if [ ! -f "$WIRINGS_LIB" ] || [ ! -f "$PROJECT_SETTINGS" ]; then
328
+ # Substrate not fully synced yet OR project settings.json not
329
+ # checked out on this branch. Fall back silently — drift detector
330
+ # at next session-start will surface the gap.
331
+ return 0
332
+ fi
333
+ # shellcheck source=/dev/null
334
+ source "$WIRINGS_LIB"
335
+
336
+ local required_version user_version
337
+ required_version=$(read_settings_version "$PROJECT_SETTINGS")
338
+ user_version=$(read_settings_version "$USER_SETTINGS")
339
+
340
+ if [ ! -f "$USER_SETTINGS" ] || [ "$user_version" -lt "$required_version" ]; then
341
+ additive_merge_settings "$USER_SETTINGS" "$PROJECT_SETTINGS"
342
+ fi
343
+
344
+ # Hoare postcondition: required ⊆ loaded after self-heal.
345
+ local required loaded diff ok missing
346
+ required=$(read_required_wirings "$PROJECT_SETTINGS")
347
+ loaded=$(read_loaded_wirings)
348
+ diff=$(compare_wirings "$required" "$loaded")
349
+ ok=$(echo "$diff" | jq -r '.ok')
350
+ if [ "$ok" != "true" ]; then
351
+ missing=$(echo "$diff" | jq -c '.missing')
352
+ echo "🛑 bassclef-sync postcondition FAILED — required wirings missing after self-heal:" >&2
353
+ echo "$missing" | jq -r '.[] | " - " + .event + " → " + .command' >&2
354
+ echo "" >&2
355
+ echo "Resolve: edit ~/.claude/settings.json to include the missing wirings, then re-run bassclef-sync." >&2
356
+ if command -v trace_log >/dev/null 2>&1; then
357
+ trace_log "fail" "bassclef-sync-postcondition" "$missing"
358
+ fi
359
+ return 1
360
+ fi
361
+ if command -v trace_log >/dev/null 2>&1; then
362
+ trace_log "ok" "bassclef-sync-postcondition" "version=$required_version"
363
+ fi
364
+ return 0
365
+ }
366
+
367
+ sync_self_heal || exit 1
368
+
369
+ # === 4b. PreToolUse template embedding — operator settings.json self-heal ===
370
+ # Per bassclef-upstream#336 — ensure operator's ~/.claude/settings.json wires
371
+ # the bassclef-managed PreToolUse hooks (substrate-clarity-gate + bet-doc-gate).
372
+ # Mirrors §3c-hooks in presence/install/bassclef-sync.template.sh (consumer-side)
373
+ # but targets ~/.claude/settings.json with $HOME-prefixed commands.
374
+ #
375
+ # Why this lives here even though §4 sync_self_heal runs additive-merge:
376
+ # wirings.sh merges bassclef's project settings.json into operator's user
377
+ # settings.json — but during a sync where wirings.sh itself is being upgraded,
378
+ # OR when the operator's settings.json predates the wirings library entirely,
379
+ # the merge may not fire. This §4b is the literal-text fallback: greppable
380
+ # guards re-emit the PreToolUse template when entries are missing, regardless
381
+ # of wirings.sh state. Bootstrap-pair discipline (per
382
+ # .claude/rules/bootstrap-pair-discipline.md) — wiring ships in the same place
383
+ # as the hook reference.
384
+ #
385
+ # Template (operator settings.json fragment, "PreToolUse": event):
386
+ # {
387
+ # "hooks": {
388
+ # "PreToolUse": [
389
+ # {
390
+ # "matcher": "Edit|Write|MultiEdit",
391
+ # "hooks": [
392
+ # {"type": "command", "command": "$HOME/.claude/hooks/substrate-clarity-gate.sh"},
393
+ # {"type": "command", "command": "$HOME/.claude/hooks/bet-doc-gate.sh"}
394
+ # ]
395
+ # }
396
+ # ]
397
+ # }
398
+ # }
399
+ #
400
+ # Idempotent via jq additive merge — re-runs detect existing entries and skip.
401
+ # Skip paths:
402
+ # - SKIP_PRETOOLUSE_EMBED=1 — operator override (logged)
403
+ # - jq missing — graceful skip with stderr notice
404
+ # - ~/.claude/settings.json missing — skip (sync_self_heal handles creation)
405
+ if [ "${SKIP_PRETOOLUSE_EMBED:-0}" != "1" ] && \
406
+ command -v jq >/dev/null 2>&1 && \
407
+ [ -f "$USER_SETTINGS" ]; then
408
+
409
+ # Guard re-emit when bet-doc-gate missing (existing-adopter self-heal)
410
+ if ! grep -q 'bet-doc-gate' ~/.claude/settings.json 2>/dev/null; then
411
+ BET_DOC_GATE_MISSING=1
412
+ else
413
+ BET_DOC_GATE_MISSING=0
414
+ fi
415
+ # Guard re-emit when substrate-clarity-gate missing
416
+ if ! grep -q 'substrate-clarity-gate' ~/.claude/settings.json 2>/dev/null; then
417
+ SUBSTRATE_CLARITY_GATE_MISSING=1
418
+ else
419
+ SUBSTRATE_CLARITY_GATE_MISSING=0
420
+ fi
421
+
422
+ if [ "$BET_DOC_GATE_MISSING" = "1" ] || [ "$SUBSTRATE_CLARITY_GATE_MISSING" = "1" ]; then
423
+ # Operator-class PreToolUse template — note the $HOME prefix (left literal
424
+ # in JSON; Claude Code expands at hook-fire time per presence/install
425
+ # consumer-side §4 line 820 precedent).
426
+ OPERATOR_PRETOOLUSE_JSON='[
427
+ {"matcher": "Edit|Write|MultiEdit", "command": "$HOME/.claude/hooks/substrate-clarity-gate.sh"},
428
+ {"matcher": "Edit|Write|MultiEdit", "command": "$HOME/.claude/hooks/bet-doc-gate.sh"}
429
+ ]'
430
+
431
+ # Additive jq merge — mirrors §3c-hooks logic but writes to USER_SETTINGS:
432
+ # 1. Ensure .hooks.PreToolUse array exists
433
+ # 2. For each bassclef hook:
434
+ # - If matcher exists, ensure command is in the matcher's hooks array
435
+ # - If matcher missing, create new matcher entry with the command
436
+ # Operator-managed entries (other matchers, other commands) stay untouched.
437
+ NEW_OPERATOR_SETTINGS=$(jq --argjson bassclef "$OPERATOR_PRETOOLUSE_JSON" '
438
+ .hooks //= {} |
439
+ .hooks.PreToolUse //= [] |
440
+ reduce $bassclef[] as $hook (
441
+ .;
442
+ if (.hooks.PreToolUse | map(.matcher) | index($hook.matcher)) != null then
443
+ .hooks.PreToolUse |= map(
444
+ if .matcher == $hook.matcher then
445
+ if (.hooks | map(.command) | index($hook.command)) == null then
446
+ .hooks += [{"type": "command", "command": $hook.command}]
447
+ else . end
448
+ else . end
449
+ )
450
+ else
451
+ .hooks.PreToolUse += [{
452
+ "matcher": $hook.matcher,
453
+ "hooks": [{"type": "command", "command": $hook.command}]
454
+ }]
455
+ end
456
+ )
457
+ ' "$USER_SETTINGS" 2>/dev/null)
458
+
459
+ if [ -n "$NEW_OPERATOR_SETTINGS" ]; then
460
+ echo "$NEW_OPERATOR_SETTINGS" > "$USER_SETTINGS.tmp" && \
461
+ mv "$USER_SETTINGS.tmp" "$USER_SETTINGS"
462
+ if command -v trace_log >/dev/null 2>&1; then
463
+ trace_log "ok" "bassclef-sync-pretooluse-embed" \
464
+ "added_bet_doc=$BET_DOC_GATE_MISSING added_substrate_clarity=$SUBSTRATE_CLARITY_GATE_MISSING"
465
+ fi
466
+ fi
467
+ fi
468
+ fi
469
+
470
+ # === 4c. Stop template embedding — operator settings.json self-heal ===
471
+ # Same pattern as §4b but for the "Stop": event — wires
472
+ # $HOME/.claude/hooks/turn-prose-kiss-check.sh so the per-turn /kiss scan
473
+ # fires at session-stop boundaries. Per bassclef-upstream#336 sister gap
474
+ # (Stop template embedding missing alongside PreToolUse).
475
+ #
476
+ # Template (operator settings.json fragment, "Stop": event):
477
+ # {
478
+ # "hooks": {
479
+ # "Stop": [
480
+ # {
481
+ # "matcher": "",
482
+ # "hooks": [
483
+ # {"type": "command", "command": "$HOME/.claude/hooks/turn-prose-kiss-check.sh", "timeout": 30}
484
+ # ]
485
+ # }
486
+ # ]
487
+ # }
488
+ # }
489
+ #
490
+ # Timeout 30s: turn-prose-kiss-check scans the most-recent assistant message
491
+ # against the BLOCK wordlist; finishes well under 30s on any realistic
492
+ # transcript. ≥5s minimum per session-artifacts rule (Stop hooks need
493
+ # adequate timeout or they get killed mid-write).
494
+ if [ "${SKIP_STOP_EMBED:-0}" != "1" ] && \
495
+ command -v jq >/dev/null 2>&1 && \
496
+ [ -f "$USER_SETTINGS" ]; then
497
+
498
+ # Guard re-emit when turn-prose entry missing
499
+ if ! grep -q 'turn-prose' ~/.claude/settings.json 2>/dev/null; then
500
+ TURN_PROSE_MISSING=1
501
+ else
502
+ TURN_PROSE_MISSING=0
503
+ fi
504
+
505
+ if [ "$TURN_PROSE_MISSING" = "1" ]; then
506
+ # Stop-event entry — empty matcher (Stop fires once per session-end;
507
+ # no per-tool matcher applies). Command uses $HOME-prefixed path
508
+ # (Claude Code expands at hook-fire time). Timeout 30 per above.
509
+ OPERATOR_STOP_JSON='[
510
+ {"matcher": "", "command": "$HOME/.claude/hooks/turn-prose-kiss-check.sh", "timeout": 30}
511
+ ]'
512
+
513
+ NEW_STOP_SETTINGS=$(jq --argjson bassclef "$OPERATOR_STOP_JSON" '
514
+ .hooks //= {} |
515
+ .hooks.Stop //= [] |
516
+ reduce $bassclef[] as $hook (
517
+ .;
518
+ if (.hooks.Stop | map(.matcher) | index($hook.matcher)) != null then
519
+ .hooks.Stop |= map(
520
+ if .matcher == $hook.matcher then
521
+ if (.hooks | map(.command) | index($hook.command)) == null then
522
+ .hooks += [{"type": "command", "command": $hook.command, "timeout": $hook.timeout}]
523
+ else . end
524
+ else . end
525
+ )
526
+ else
527
+ .hooks.Stop += [{
528
+ "matcher": $hook.matcher,
529
+ "hooks": [{"type": "command", "command": $hook.command, "timeout": $hook.timeout}]
530
+ }]
531
+ end
532
+ )
533
+ ' "$USER_SETTINGS" 2>/dev/null)
534
+
535
+ if [ -n "$NEW_STOP_SETTINGS" ]; then
536
+ echo "$NEW_STOP_SETTINGS" > "$USER_SETTINGS.tmp" && \
537
+ mv "$USER_SETTINGS.tmp" "$USER_SETTINGS"
538
+ if command -v trace_log >/dev/null 2>&1; then
539
+ trace_log "ok" "bassclef-sync-stop-embed" \
540
+ "added_turn_prose=$TURN_PROSE_MISSING"
541
+ fi
542
+ fi
543
+ fi
544
+ fi
545
+
546
+ # === 5. Set env var for rules/CLAUDE.md inheritance ===
547
+ if [ -n "$CLAUDE_ENV_FILE" ]; then
548
+ echo "CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1" >> "$CLAUDE_ENV_FILE"
549
+ fi
550
+
551
+ # === Summary ===
552
+ SKILL_COUNT=$(ls "$BASSCLEF_DIR/.claude/skills/" 2>/dev/null | wc -l | tr -d ' ')
553
+ RULE_COUNT=$(ls "$BASSCLEF_DIR/.claude/rules/" 2>/dev/null | wc -l | tr -d ' ')
554
+ AGENT_COUNT=$(ls ~/.claude/agents/ 2>/dev/null | wc -l | tr -d ' ')
555
+ HOOK_COUNT=$(ls ~/.claude/hooks/*.sh 2>/dev/null | wc -l | tr -d ' ')
556
+
557
+ echo ""
558
+ echo "### BASSCLEF SUBSTRATE — SYNCED"
559
+ echo ""
560
+ echo "Status: $SYNC_STATUS"
561
+ echo "Skills: ${SKILL_COUNT} (symlinked to .claude/skills/ for auto-discovery)"
562
+ echo "Rules: ${RULE_COUNT} (loaded via additionalDirectories + env var)"
563
+ echo "Agents: ${AGENT_COUNT} (installed to ~/.claude/agents/)"
564
+ echo "Hooks: ${HOOK_COUNT} user-level (bug-diagnosis, tool-failure, trace-helper)"
565
+ echo ""
566
+
567
+ if [ -n "$SYNC_NEW" ]; then
568
+ echo "### NEW SINCE LAST SYNC — REVIEW BEFORE PROCEEDING"
569
+ echo ""
570
+ echo "$SYNC_NEW"
571
+ echo ""
572
+ echo "→ New bassclef standards detected. Ask the user to confirm"
573
+ echo " before proceeding. These may change how this session operates."
574
+ echo ""
575
+ fi
576
+
577
+ if [ "$ORPHAN_COUNT" -gt 0 ]; then
578
+ echo "### USER-LEVEL SUBSTRATE CLEANUP (per ADR-033 + bassclef#278)"
579
+ echo ""
580
+ echo "Parked $ORPHAN_COUNT user-level entries (skills + agents + hooks; symlinks AND cp'd files)"
581
+ echo "whose basenames duplicated project-level entries. Backup dirs:"
582
+ echo " ~/.claude/skills.orphans.$(date +%Y%m%d)/"
583
+ echo " ~/.claude/agents.orphans.$(date +%Y%m%d)/"
584
+ echo " ~/.claude/hooks.orphans.$(date +%Y%m%d)/"
585
+ echo ""
586
+ echo "Project-level entries now load without dedupe conflict."
587
+ echo "Backups survive 30 days minimum; restore if a script you wrote"
588
+ echo "depends on the user-level location."
589
+ echo ""
590
+ fi
591
+
592
+ echo "YOU MUST confirm to the user:"
593
+ echo " 'Bassclef substrate synced: ${SKILL_COUNT} skills, ${RULE_COUNT} rules,"
594
+ echo " ${AGENT_COUNT} agents, ${HOOK_COUNT} hooks. Status: ${SYNC_STATUS}.'"
595
+ echo ""
596
+
597
+ # === 6. Worktree cleanup — prune stale worktrees from crashed parallel sessions ===
598
+ STALE_WORKTREES=$(git -C "$CWD" worktree list --porcelain 2>/dev/null | grep "^worktree " | grep -v "$CWD$" | wc -l | tr -d ' ')
599
+ if [ "$STALE_WORKTREES" -gt 0 ]; then
600
+ # Prune worktrees whose directories no longer exist
601
+ git -C "$CWD" worktree prune 2>/dev/null || true
602
+
603
+ # Clean up .claude/worktrees/ directory if it exists
604
+ if [ -d "$CWD/.claude/worktrees" ]; then
605
+ rm -rf "$CWD/.claude/worktrees" 2>/dev/null || true
606
+ fi
607
+
608
+ REMAINING=$(git -C "$CWD" worktree list --porcelain 2>/dev/null | grep "^worktree " | grep -v "$CWD$" | wc -l | tr -d ' ')
609
+ if [ "$REMAINING" -gt 0 ]; then
610
+ echo "### STALE WORKTREES DETECTED"
611
+ echo ""
612
+ echo "$REMAINING worktree(s) from a previous session still exist."
613
+ echo "These may hold branch locks. Review and remove:"
614
+ echo ""
615
+ git -C "$CWD" worktree list 2>/dev/null | grep -v "$CWD "
616
+ echo ""
617
+ echo "To remove: git worktree remove <path>"
618
+ echo ""
619
+ fi
620
+ fi
621
+
622
+ # === 7. Boot check — only report what's missing ===
623
+ MISSING=""
624
+
625
+ # Google service account (needed for journal → Google Docs)
626
+ SA_KEY="$HOME/.config/gcloud/sunjay-google-ops.json"
627
+ if [ ! -f "$SA_KEY" ]; then
628
+ MISSING="${MISSING}\n- Google Docs service account key missing at $SA_KEY (needed for /journal → Google Doc push)"
629
+ fi
630
+
631
+ # gh CLI auth (needed for /promote, issue creation)
632
+ if ! command -v gh &>/dev/null; then
633
+ MISSING="${MISSING}\n- gh CLI not installed (needed for /promote, issue creation)"
634
+ elif ! gh auth status &>/dev/null 2>&1; then
635
+ MISSING="${MISSING}\n- gh CLI not authenticated (run: gh auth login)"
636
+ fi
637
+
638
+ if [ -n "$MISSING" ]; then
639
+ echo "### SUBSTRATE DEPENDENCIES — OPTIONAL SETUP NEEDED"
640
+ echo ""
641
+ echo "The following are not blocking but limit some skills:"
642
+ echo -e "$MISSING"
643
+ echo ""
644
+ echo "Run: bassclef/scripts/boot-bassclef.sh for full setup guide."
645
+ echo "These are informational — proceed with your work."
646
+ echo ""
647
+ fi
648
+
649
+ # === 8. journal draft staleness check ===
650
+ # Check both bassclef and the current repo for stale drafts.
651
+ #
652
+ # Fetch origin first (per bassclef-upstream#403): the staleness check
653
+ # below reads local filesystem state + local git log, which diverges
654
+ # from origin when another session has pushed since the last pull
655
+ # (e.g., a prior session-end's commit). Without the fetch, the banner
656
+ # shows wrong-now data (observed 2026-06-26c — local saw 2026-06-19
657
+ # while origin had 2026-06-26.md from #401).
658
+ #
659
+ # Best-effort: || true keeps the check working when offline / no remote.
660
+ CURRENT_BRANCH_FOR_FETCH=$(git -C "$CWD" symbolic-ref --short HEAD 2>/dev/null)
661
+ if [ -n "$CURRENT_BRANCH_FOR_FETCH" ]; then
662
+ git -C "$CWD" fetch origin "$CURRENT_BRANCH_FOR_FETCH" --quiet 2>/dev/null || true
663
+ fi
664
+
665
+ DRAFT_DIRS=""
666
+ if [ -d "$CWD/docs/operator-private/journals" ]; then
667
+ DRAFT_DIRS="$CWD/docs/operator-private/journals"
668
+ elif [ -d "$CWD/docs/journals" ]; then
669
+ # Pre-2026-06-27 path — migrated to operator-private per bassclef-upstream#405
670
+ DRAFT_DIRS="$CWD/docs/journals"
671
+ elif [ -d "$CWD/docs/linkedin-drafts" ]; then
672
+ # Legacy path — pre-/linkedin-→-/journal rename
673
+ DRAFT_DIRS="$CWD/docs/linkedin-drafts"
674
+ fi
675
+ if [ -d "$BASSCLEF_DIR/docs/operator-private/journals" ] && [ "$BASSCLEF_DIR" != "$CWD" ]; then
676
+ DRAFT_DIRS="$DRAFT_DIRS $BASSCLEF_DIR/docs/operator-private/journals"
677
+ elif [ -d "$BASSCLEF_DIR/docs/journals" ] && [ "$BASSCLEF_DIR" != "$CWD" ]; then
678
+ DRAFT_DIRS="$DRAFT_DIRS $BASSCLEF_DIR/docs/journals"
679
+ fi
680
+
681
+ if [ -n "$DRAFT_DIRS" ]; then
682
+ LATEST_DRAFT=""
683
+ for DIR in $DRAFT_DIRS; do
684
+ LAST=$(ls -1 "$DIR"/*.md 2>/dev/null | sort | tail -1)
685
+ if [ -n "$LAST" ]; then
686
+ DRAFT_DATE=$(basename "$LAST" .md | head -c 10)
687
+ if [ -z "$LATEST_DRAFT" ] || [ "$DRAFT_DATE" \> "$LATEST_DRAFT" ]; then
688
+ LATEST_DRAFT="$DRAFT_DATE"
689
+ fi
690
+ fi
691
+ done
692
+
693
+ if [ -n "$LATEST_DRAFT" ]; then
694
+ TODAY=$(date +%Y-%m-%d)
695
+ # Count commits since last draft
696
+ COMMITS_SINCE=$(git -C "$CWD" log --oneline --since="$LATEST_DRAFT" 2>/dev/null | wc -l | tr -d ' ')
697
+
698
+ if [ "$LATEST_DRAFT" != "$TODAY" ] && [ "$COMMITS_SINCE" -gt 3 ]; then
699
+ DAYS_AGO=$(( ( $(date +%s) - $(date -j -f "%Y-%m-%d" "$LATEST_DRAFT" +%s 2>/dev/null || echo 0) ) / 86400 ))
700
+ echo "### JOURNAL DRAFTS OVERDUE (SHOULD — backfill before other work)"
701
+ echo ""
702
+ echo "Last draft: $LATEST_DRAFT ($DAYS_AGO days ago, $COMMITS_SINCE commits since)"
703
+ echo ""
704
+ echo "→ BACKFILL NOW — before starting new work."
705
+ echo " 1. Read chronicles since $LATEST_DRAFT"
706
+ echo " 2. Write drafts for each session with a notable decision or insight"
707
+ echo " 3. Push to Google Doc (oldest first)"
708
+ echo " 4. Then proceed with session work"
709
+ echo ""
710
+ echo "This is a SHOULD gate. Do not skip without documented rationale."
711
+ echo ""
712
+ fi
713
+ fi
714
+ fi
715
+
716
+ exit 0