@thebassclef/lite 1.9.1 → 1.9.3
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 +37 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/bassclef-sync.sh +795 -749
- package/dist/lite/.claude/hooks/session-reflection.d/tests/01-adopter-message-channel.test.sh +168 -0
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/presence/install/bassclef-hook-connect.sh +124 -2
- package/dist/lite/standards/lite-manifest.json +2 -2
- package/package.json +2 -2
|
@@ -1,41 +1,320 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# tier:
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
2
|
+
# tier: lite
|
|
3
|
+
# description: Retagged standard → lite per #1301 concentric rule (ADR-052 D1) —
|
|
4
|
+
# Retagged standard → lite per #1301 concentric rule (ADR-052 D1) —
|
|
5
|
+
# this dispatcher IS the sync entry point every adopter vendors into
|
|
6
|
+
# .claude/hooks/bassclef-sync.sh. Without ship at lite, lite adopters
|
|
7
|
+
# have no sync at all. Closes #1301 sister file.
|
|
8
|
+
# BASSCLEF_SYNC_VERSION=thin-pointer-2026-07-28a
|
|
9
|
+
# ^^ DO NOT REMOVE — used by bassclef's drift check + Layer 3 self-heal
|
|
10
|
+
# detection (per ADR-032). Version string format: descriptor-YYYY-MM-DD.
|
|
11
|
+
#
|
|
12
|
+
# Thin-pointer bassclef-sync dispatcher.
|
|
13
|
+
#
|
|
14
|
+
# Source of truth for the dispatcher consumers vendor into their
|
|
15
|
+
# .claude/hooks/bassclef-sync.sh. See standards/bassclef-source-config.md
|
|
16
|
+
# for the .bassclef-source.json schema this dispatcher reads.
|
|
17
|
+
#
|
|
18
|
+
# Two paths, tried in order:
|
|
19
|
+
#
|
|
20
|
+
# 1. Sibling fast-path (local dev, desktop, multi-repo workspaces)
|
|
21
|
+
# — if $CWD/../bassclef/presence/install/bassclef-sync.template.sh
|
|
22
|
+
# exists, exec it directly. Always-current because the file is
|
|
23
|
+
# read fresh on every fire.
|
|
24
|
+
#
|
|
25
|
+
# 2. Curl fallback (Cloud Env, mobile, fresh sandboxes, CI)
|
|
26
|
+
# — read .bassclef-source.json for upstream URL + auth metadata,
|
|
27
|
+
# fetch the tarball, extract, exec the included template.
|
|
28
|
+
#
|
|
29
|
+
# When neither path works, the dispatcher prints a plain-language
|
|
30
|
+
# explanation with three resolution paths (public adopter / local dev
|
|
31
|
+
# / private fork). Always exits 0 so SessionStart hooks don't crash;
|
|
32
|
+
# consumers that want CI fail-fast behavior should pre-check inside
|
|
33
|
+
# their workflow.
|
|
34
|
+
#
|
|
35
|
+
# Versioning: bump BASSCLEF_SYNC_VERSION when the dispatcher's
|
|
36
|
+
# semantics change. Format: YYYY-MM-DD or descriptor-YYYY-MM-DD.
|
|
37
|
+
#
|
|
38
|
+
# thin-pointer-2026-07-09: pointer-file rename cure (bet 2026-07-09a).
|
|
39
|
+
# Adopters onboarded before 2026-06-20 rename carry
|
|
40
|
+
# .canonical-source.json. Dispatcher now reads new
|
|
41
|
+
# name first, falls back to old, and self-heals
|
|
42
|
+
# (writes new name from old content; removes old on
|
|
43
|
+
# second cycle per Hoare two-step atomic). Fires
|
|
44
|
+
# before auto_sync check + Path 2 curl fallback.
|
|
45
|
+
# Overrides: SKIP_SELF_HEAL_POINTER=1 skips write.
|
|
46
|
+
# Prior version: thin-pointer-2026-06-29c.
|
|
47
|
+
# thin-pointer-2026-06-29c: fallback message names the cure (bet 29e WU-10).
|
|
48
|
+
# emit_error() now lists option 4 — the curl
|
|
49
|
+
# one-liner that runs migrate-adopter-references.sh
|
|
50
|
+
# for adopters whose tree carries leftover
|
|
51
|
+
# canonical-* references from the 2026-06-20 rename
|
|
52
|
+
# event. Discoverable cure inside the error itself;
|
|
53
|
+
# the adopter does not need to read the wiki.
|
|
54
|
+
# Prior version: thin-pointer-2026-06-29b.
|
|
55
|
+
# thin-pointer-2026-06-29b: auto-invocation added (bet 29e WU-4). Detection
|
|
56
|
+
# step now runs the migrate script automatically
|
|
57
|
+
# via curl-pipe + continues to Path 1/Path 2.
|
|
58
|
+
# SKIP_AUTO_INVOKE=1 falls back to WU-2's BLOCKED
|
|
59
|
+
# behavior. Passes BASSCLEF_DISPATCHER_TEMPLATE
|
|
60
|
+
# to migrate when sibling template available.
|
|
61
|
+
# Prior version: thin-pointer-2026-06-29.
|
|
62
|
+
# thin-pointer-2026-06-29: canonical-* reference detection step added
|
|
63
|
+
# (bet 29e WU-2). Scans adopter .claude/settings.json
|
|
64
|
+
# + vendored bassclef-sync.sh for refs surviving the
|
|
65
|
+
# 2026-06-20 rename. Emits BLOCKED + exits 0. WU-4 will
|
|
66
|
+
# replace exit with auto-invocation of migrate-adopter-
|
|
67
|
+
# references.sh. Override: SKIP_CANONICAL_DETECTION=1.
|
|
68
|
+
# Prior version: thin-pointer-2026-06-22.
|
|
69
|
+
# thin-pointer-2026-06-22: version-detection helper added per ADR-032
|
|
70
|
+
# (Layer 2 two-stage rocket). __bassclef_dispatcher_version
|
|
71
|
+
# function exposes BASSCLEF_SYNC_VERSION to external
|
|
72
|
+
# tooling (Layer 3 self-heal in WU-4). No protocol
|
|
73
|
+
# break; purely additive. Prior version: thin-pointer-2026-06-13.
|
|
74
|
+
# thin-pointer-2026-06-13: auto_sync flag check — dispatcher short-
|
|
75
|
+
# circuits when .bassclef-source.json has
|
|
76
|
+
# auto_sync:false or the field absent. Per
|
|
77
|
+
# bassclef#939. Prior version: 2026-05-15.
|
|
78
|
+
# thin-pointer-2026-05-15: friendly error messages with three
|
|
79
|
+
# resolution paths. Closes bassclef#715
|
|
80
|
+
# Layer A. Prior version: 2026-05-02.
|
|
81
|
+
|
|
82
|
+
# === Version-detection helper (per ADR-032) ===
|
|
83
|
+
# Layer 3 self-heal (WU-4 of bet 2026-06-22b) calls this to detect
|
|
84
|
+
# bootstrap version drift. External tooling can also grep
|
|
85
|
+
# BASSCLEF_SYNC_VERSION from line 3, but the function gives a stable
|
|
86
|
+
# programmatic interface that survives header-comment reformatting.
|
|
87
|
+
__bassclef_dispatcher_version() {
|
|
88
|
+
echo "thin-pointer-2026-07-09"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# === Pointer-file fallback + self-heal (bet 2026-07-09a) ===
|
|
92
|
+
#
|
|
93
|
+
# Every adopter onboarded before the 2026-06-20 canonical → bassclef
|
|
94
|
+
# rename carries .canonical-source.json. The dispatcher previously read
|
|
95
|
+
# .bassclef-source.json only; stale adopters saw substrate refuse to
|
|
96
|
+
# load. These two functions cure the class:
|
|
97
|
+
#
|
|
98
|
+
# bassclef_sync_resolve_pointer_file(cwd)
|
|
99
|
+
# Echoes the path of the pointer file to use. Prefers new name;
|
|
100
|
+
# falls back to old name; echoes empty when neither exists.
|
|
101
|
+
#
|
|
102
|
+
# bassclef_sync_self_heal_pointer(cwd)
|
|
103
|
+
# Two-step atomic rename per Tony Hoare. First cycle: writes new
|
|
104
|
+
# name from old content, keeps old file for safety. Second cycle:
|
|
105
|
+
# removes old file when new already exists. Never deletes new.
|
|
106
|
+
# Honors SKIP_SELF_HEAL_POINTER=1 for one-shot recovery scenarios.
|
|
107
|
+
#
|
|
108
|
+
# Composing luminaries:
|
|
109
|
+
# @luminary linus-torvalds — we-dont-break-adopters (ADR-031)
|
|
110
|
+
# @luminary jerome-saltzer-and-michael-schroeder — complete mediation
|
|
111
|
+
# @luminary tony-hoare — pre/post conditions on two-step rename
|
|
112
|
+
# @luminary michael-feathers — characterization tests using real Quorum
|
|
113
|
+
#
|
|
114
|
+
# Tests: .claude/hooks/tests/bassclef-sync-pointer-fallback.test.sh
|
|
115
|
+
# Refs: bassclef-upstream#679
|
|
116
|
+
bassclef_sync_resolve_pointer_file() {
|
|
117
|
+
local cwd="${1:-$CWD}"
|
|
118
|
+
local new="$cwd/.bassclef-source.json"
|
|
119
|
+
local old="$cwd/.canonical-source.json"
|
|
120
|
+
if [ -f "$new" ]; then
|
|
121
|
+
echo "$new"
|
|
122
|
+
elif [ -f "$old" ]; then
|
|
123
|
+
echo "$old"
|
|
124
|
+
else
|
|
125
|
+
echo ""
|
|
126
|
+
fi
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# bassclef_sync_bundled_substrate_present <cwd>
|
|
130
|
+
#
|
|
131
|
+
# Returns 0 (yes) when .claude/hooks/, .claude/rules/, .claude/skills/ all
|
|
132
|
+
# exist AND contain at least one file (recursively). Returns 1 (no) when
|
|
133
|
+
# any of those conditions fails.
|
|
134
|
+
#
|
|
135
|
+
# Cures the npm-only cold adopter false-error class per
|
|
136
|
+
# bassclef-upstream#1849. The dispatcher was designed for peer-checkout
|
|
137
|
+
# OR source-config models. Neither works for anonymous adopters against
|
|
138
|
+
# private repos. But the npm bundle already unpacks substrate at install
|
|
139
|
+
# time. The fetch failure is not a real failure; this check catches the
|
|
140
|
+
# steady state and lets the dispatcher exit silently.
|
|
141
|
+
#
|
|
142
|
+
# Composes with:
|
|
143
|
+
# @luminary alan-cooper — Sam persona; silent-on-success matches user model
|
|
144
|
+
# @luminary donald-norman — signifier; false-positive banner erodes real signal
|
|
145
|
+
# @luminary linus-torvalds — adopter contract; npm bundle IS the substrate
|
|
146
|
+
# @luminary michael-feathers — characterization tests over 3 install shapes
|
|
147
|
+
#
|
|
148
|
+
# Tests: .claude/hooks/tests/bassclef-sync-bundled-substrate-check.test.sh
|
|
149
|
+
# Refs: bassclef-upstream#1849
|
|
150
|
+
bassclef_sync_bundled_substrate_present() {
|
|
151
|
+
local cwd="${1:-}"
|
|
152
|
+
if [ -z "$cwd" ] || [ ! -d "$cwd" ]; then
|
|
153
|
+
return 1
|
|
154
|
+
fi
|
|
155
|
+
local _dir
|
|
156
|
+
for _dir in .claude/hooks .claude/rules .claude/skills; do
|
|
157
|
+
if [ ! -d "$cwd/$_dir" ]; then
|
|
158
|
+
return 1
|
|
159
|
+
fi
|
|
160
|
+
# Nonempty check: at least one file anywhere under the dir.
|
|
161
|
+
if [ -z "$(find "$cwd/$_dir" -mindepth 1 -maxdepth 4 -type f -print -quit 2>/dev/null)" ]; then
|
|
162
|
+
return 1
|
|
163
|
+
fi
|
|
164
|
+
done
|
|
165
|
+
return 0
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
bassclef_sync_self_heal_pointer() {
|
|
169
|
+
local cwd="${1:-$CWD}"
|
|
170
|
+
if [ "${SKIP_SELF_HEAL_POINTER:-0}" = "1" ]; then
|
|
171
|
+
return 0
|
|
172
|
+
fi
|
|
173
|
+
local new="$cwd/.bassclef-source.json"
|
|
174
|
+
local old="$cwd/.canonical-source.json"
|
|
175
|
+
# Second cycle: both exist → remove old, keep new (Hoare postcondition)
|
|
176
|
+
if [ -f "$new" ] && [ -f "$old" ]; then
|
|
177
|
+
rm -f "$old"
|
|
178
|
+
return 0
|
|
179
|
+
fi
|
|
180
|
+
# First cycle: only old exists → write new from old content, keep old for safety
|
|
181
|
+
if [ ! -f "$new" ] && [ -f "$old" ]; then
|
|
182
|
+
cp "$old" "$new" 2>/dev/null || return 0
|
|
183
|
+
# Emit a one-line note so operator sees the self-heal firing
|
|
184
|
+
echo "bassclef-sync: renamed .canonical-source.json → .bassclef-source.json (auto-heal; original kept for one session)" >&2
|
|
185
|
+
return 0
|
|
186
|
+
fi
|
|
187
|
+
# Only new exists OR neither exists → no-op
|
|
188
|
+
return 0
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# bassclef_sync_self_heal_pointer_content(cwd)
|
|
192
|
+
# Rewrites drift in .bassclef-source.json content on adopters onboarded
|
|
193
|
+
# before the 2026-06-20 canonical → bassclef rename.
|
|
194
|
+
# - source_url: sunj-labs/canonical -> sunj-labs/bassclef
|
|
195
|
+
# - token_env: CANONICAL_TOKEN -> BASSCLEF_TOKEN
|
|
196
|
+
# Preserves auth_method (Bearer during beta), auto_sync flag, and every
|
|
197
|
+
# other field. Idempotent — no-op when content already uses new names.
|
|
198
|
+
# Honors SKIP_SELF_HEAL_POINTER_CONTENT=1 for one-shot recovery cases.
|
|
199
|
+
# Per bet 2026-07-09c WU-2 (bassclef-upstream#680). Sister to
|
|
200
|
+
# bassclef_sync_self_heal_pointer (filename cure from bet 09a).
|
|
201
|
+
bassclef_sync_self_heal_pointer_content() {
|
|
202
|
+
local cwd="${1:-$CWD}"
|
|
203
|
+
if [ "${SKIP_SELF_HEAL_POINTER_CONTENT:-0}" = "1" ]; then
|
|
204
|
+
return 0
|
|
205
|
+
fi
|
|
206
|
+
local pointer="$cwd/.bassclef-source.json"
|
|
207
|
+
if [ ! -f "$pointer" ]; then
|
|
208
|
+
return 0
|
|
209
|
+
fi
|
|
210
|
+
# Detect drift — cheap grep before invoking jq
|
|
211
|
+
local drift=0
|
|
212
|
+
if grep -q "sunj-labs/canonical" "$pointer" 2>/dev/null; then
|
|
213
|
+
drift=1
|
|
214
|
+
fi
|
|
215
|
+
if grep -q "CANONICAL_TOKEN" "$pointer" 2>/dev/null; then
|
|
216
|
+
drift=1
|
|
217
|
+
fi
|
|
218
|
+
if [ "$drift" = "0" ]; then
|
|
219
|
+
return 0
|
|
220
|
+
fi
|
|
221
|
+
# Rewrite via jq — preserves JSON structure and untouched fields
|
|
222
|
+
local new_json
|
|
223
|
+
new_json=$(jq '
|
|
224
|
+
(if (.source_url // "" | contains("sunj-labs/canonical"))
|
|
225
|
+
then .source_url = (.source_url | sub("sunj-labs/canonical"; "sunj-labs/bassclef"))
|
|
226
|
+
else . end)
|
|
227
|
+
| (if (.token_env // "") == "CANONICAL_TOKEN"
|
|
228
|
+
then .token_env = "BASSCLEF_TOKEN"
|
|
229
|
+
else . end)
|
|
230
|
+
| (if (.comment // "" | contains("sunj-labs/canonical"))
|
|
231
|
+
then .comment = (.comment | gsub("sunj-labs/canonical"; "sunj-labs/bassclef"))
|
|
232
|
+
else . end)
|
|
233
|
+
' "$pointer" 2>/dev/null)
|
|
234
|
+
if [ -n "$new_json" ]; then
|
|
235
|
+
echo "$new_json" > "$pointer.tmp" 2>/dev/null && mv "$pointer.tmp" "$pointer" 2>/dev/null
|
|
236
|
+
echo "bassclef-sync: cured .bassclef-source.json content drift (source_url + token_env → bassclef names)" >&2
|
|
237
|
+
fi
|
|
238
|
+
return 0
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# bassclef_sync_self_heal_claude_md_content(cwd)
|
|
242
|
+
# Inserts @.claude/bassclef-orientation.md after the first heading in
|
|
243
|
+
# adopter CLAUDE.md when missing. Rewrites stale sunj-labs/canonical
|
|
244
|
+
# URL references + ../canonical path references. Preserves adopter-
|
|
245
|
+
# authored sections. Backs up to CLAUDE.md.bak.<ts> before edit.
|
|
246
|
+
# Idempotent — no-op on cured content.
|
|
247
|
+
# Honors SKIP_SELF_HEAL_CLAUDE_MD_CONTENT=1 for adopters who want to
|
|
248
|
+
# own their CLAUDE.md fully.
|
|
249
|
+
# Per bet 2026-07-09c WU-3 (bassclef-upstream#687 Path 2). Sister to
|
|
250
|
+
# bassclef_sync_self_heal_pointer_content (WU-2 pointer content cure).
|
|
251
|
+
bassclef_sync_self_heal_claude_md_content() {
|
|
252
|
+
local cwd="${1:-$CWD}"
|
|
253
|
+
if [ "${SKIP_SELF_HEAL_CLAUDE_MD_CONTENT:-0}" = "1" ]; then
|
|
254
|
+
return 0
|
|
255
|
+
fi
|
|
256
|
+
local file="$cwd/CLAUDE.md"
|
|
257
|
+
if [ ! -f "$file" ]; then
|
|
258
|
+
return 0
|
|
259
|
+
fi
|
|
260
|
+
local drift_import=0
|
|
261
|
+
local drift_url=0
|
|
262
|
+
local drift_path=0
|
|
263
|
+
if ! grep -q "@.claude/bassclef-orientation.md" "$file" 2>/dev/null; then
|
|
264
|
+
drift_import=1
|
|
265
|
+
fi
|
|
266
|
+
if grep -q "sunj-labs/canonical" "$file" 2>/dev/null; then
|
|
267
|
+
drift_url=1
|
|
268
|
+
fi
|
|
269
|
+
if grep -q "\\.\\./canonical" "$file" 2>/dev/null; then
|
|
270
|
+
drift_path=1
|
|
271
|
+
fi
|
|
272
|
+
if [ "$drift_import" = "0" ] && [ "$drift_url" = "0" ] && [ "$drift_path" = "0" ]; then
|
|
273
|
+
return 0
|
|
274
|
+
fi
|
|
275
|
+
local backup_ts
|
|
276
|
+
backup_ts=$(date +%s)
|
|
277
|
+
cp "$file" "${file}.bak.${backup_ts}" 2>/dev/null
|
|
278
|
+
if [ "$drift_import" = "1" ]; then
|
|
279
|
+
awk 'BEGIN{added=0} /^# / && added==0 {print; print ""; print "@.claude/bassclef-orientation.md"; added=1; next} {print}' \
|
|
280
|
+
"$file" > "${file}.tmp" 2>/dev/null && mv "${file}.tmp" "$file" 2>/dev/null
|
|
281
|
+
fi
|
|
282
|
+
sed 's|sunj-labs/canonical|sunj-labs/bassclef|g; s|\.\./canonical|../bassclef|g' \
|
|
283
|
+
"$file" > "${file}.tmp" 2>/dev/null && mv "${file}.tmp" "$file" 2>/dev/null
|
|
284
|
+
echo "bassclef-sync: cured CLAUDE.md content drift (@import + canonical references → bassclef; backup at ${file}.bak.${backup_ts})" >&2
|
|
285
|
+
return 0
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
INPUT=$(cat)
|
|
289
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // "."')
|
|
290
|
+
cd "$CWD"
|
|
291
|
+
|
|
292
|
+
# Opt-out sentinel (bassclef#119): repos that must NOT inherit substrate.
|
|
293
|
+
if [ -f "$CWD/.no-bassclef-sync" ]; then
|
|
294
|
+
echo "Bassclef substrate sync: skipped (.no-bassclef-sync sentinel present)."
|
|
295
|
+
exit 0
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
# === SELF_MODE detection (#929 — closes dispatcher-level bypass of #915) ===
|
|
299
|
+
# Sister to Section 0a in .claude/hooks/bassclef-sync.sh (which shipped in
|
|
300
|
+
# #915 PR #920 and guards the sync flow). This guard sits earlier in the
|
|
301
|
+
# call chain — at the dispatcher — so canonical-* auto-invoke + auto_sync
|
|
302
|
+
# fetch + Path 1 / Path 2 branches ALL skip when the current repo is the
|
|
303
|
+
# substrate source (bassclef or bassclef-upstream).
|
|
304
|
+
#
|
|
305
|
+
# Rationale: session 26g SessionStart re-symlinked ~63 hooks in
|
|
306
|
+
# bassclef-upstream despite #915. The project hook's SELF_MODE fired but
|
|
307
|
+
# only AFTER the dispatcher had already invoked the migrate script + fired
|
|
308
|
+
# other paths that touched the tree. Guard early = complete mediation.
|
|
30
309
|
#
|
|
31
310
|
# Env override SKIP_SELF_MODE_DETECTION=1 forces SELF_MODE=0 regardless
|
|
32
311
|
# of remote — for adopter forks of bassclef-upstream that legitimately
|
|
33
312
|
# want the sync flow.
|
|
34
313
|
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
|
|
314
|
+
# Non-breaking for adopters per ADR-031: SELF_MODE returns 0 for any repo
|
|
315
|
+
# whose git remote is NOT sunj-labs/bassclef or sunj-labs/bassclef-upstream.
|
|
316
|
+
# All 5 existing adopters keep their normal recovery + sync paths.
|
|
317
|
+
__bassclef_dispatcher_detect_self_mode() {
|
|
39
318
|
local cwd="${1:-.}"
|
|
40
319
|
|
|
41
320
|
# Env override wins
|
|
@@ -67,783 +346,550 @@ bassclef_detect_self_mode() {
|
|
|
67
346
|
return 0
|
|
68
347
|
}
|
|
69
348
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
# $1 — BASSCLEF_DIR (source of hooks + lib)
|
|
80
|
-
# $2 — user-scope target dir (default $HOME/.claude/hooks)
|
|
81
|
-
#
|
|
82
|
-
# Discipline (per ADR-058 constraints 1-4):
|
|
83
|
-
# c1 — install_by_class wrapped in subshell; lib's strict mode
|
|
84
|
-
# stays contained; caller's strict-mode state preserved
|
|
85
|
-
# c2 — unheadered hooks install where they install today +
|
|
86
|
-
# one trace line `skipped-no-header <hook>` fires
|
|
87
|
-
# c3 — CLAUDE_PROJECT_DIR resolved (git rev-parse fallback) and
|
|
88
|
-
# exported before the loop so project/dual class routing works
|
|
89
|
-
# c4 — settings.json command strings unchanged; wiring self-heal
|
|
90
|
-
# continues at Section 4 (unrelated to this function)
|
|
91
|
-
_sync_install_hooks() {
|
|
92
|
-
local bassclef_dir="$1"
|
|
93
|
-
local user_target_dir="${2:-$HOME/.claude/hooks}"
|
|
94
|
-
|
|
95
|
-
# Full-bundle iteration (goal 14c Step 6). Replaces the pre-Step-6
|
|
96
|
-
# hardcoded 9-name list. Every hook the old list carried still installs —
|
|
97
|
-
# either via dispatch (when header present) or via grace-window fallback
|
|
98
|
-
# (when absent). RFC S-2 HIGH cure per docs/rfcs/
|
|
99
|
-
# RFC-tier0-tests-install-class-dispatch-review.md.
|
|
349
|
+
__DISPATCHER_SELF_MODE=$(__bassclef_dispatcher_detect_self_mode "$CWD")
|
|
350
|
+
if [ "$__DISPATCHER_SELF_MODE" = "1" ]; then
|
|
351
|
+
# #895: park user-scope orphans that shadow project scope before exit.
|
|
352
|
+
# Without this, pre-existing symlinks planted by sibling syncs (POA,
|
|
353
|
+
# twoDo, utilities) shadow bassclef-upstream's project scope in the
|
|
354
|
+
# Claude Code menu — 92 skills appear twice. The parking helper leaves
|
|
355
|
+
# real user-scope dirs (operator-authored content) untouched per its
|
|
356
|
+
# v3 contract. Adopters never hit this path (SELF_MODE=0 for their
|
|
357
|
+
# remotes per __bassclef_dispatcher_detect_self_mode).
|
|
100
358
|
#
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
359
|
+
# Override: SKIP_SELF_MODE_PARKING=1 for one-shot recovery scenarios.
|
|
360
|
+
if [ "${SKIP_SELF_MODE_PARKING:-0}" != "1" ]; then
|
|
361
|
+
__parking_helper="$CWD/scripts/bassclef-orphan-parking.sh"
|
|
362
|
+
if [ -x "$__parking_helper" ]; then
|
|
363
|
+
for __surface in skills agents hooks; do
|
|
364
|
+
__user_surface="$HOME/.claude/$__surface"
|
|
365
|
+
__project_surface="$CWD/.claude/$__surface"
|
|
366
|
+
if [ -d "$__user_surface" ] && [ -d "$__project_surface" ]; then
|
|
367
|
+
# v4 (bassclef-upstream#1077): pass BOTH user + project settings.json
|
|
368
|
+
# as variadic 5th+ args. Parking helper unions the $HOME wirings
|
|
369
|
+
# across both files. Any hook wired at $HOME by either settings
|
|
370
|
+
# scope is preserved. Missing paths silently skip (helper v4 contract).
|
|
371
|
+
bash "$__parking_helper" "$__user_surface" "$__project_surface" "$HOME/.claude" "$__surface" "$HOME/.claude/settings.json" "$CWD/.claude/settings.json" 2>/dev/null || true
|
|
372
|
+
fi
|
|
373
|
+
done
|
|
374
|
+
fi
|
|
375
|
+
fi
|
|
376
|
+
echo "Bassclef dispatcher SELF_MODE — skipping sync in substrate source repo." >&2
|
|
377
|
+
echo " (Override with SKIP_SELF_MODE_DETECTION=1 for adopter forks;" >&2
|
|
378
|
+
echo " SKIP_SELF_MODE_PARKING=1 to skip user-scope orphan parking.)" >&2
|
|
379
|
+
exit 0
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
# === auto_sync flag check (bassclef#939) ===
|
|
383
|
+
#
|
|
384
|
+
# Per bassclef#939, fresh consumers default to OFF — the dispatcher
|
|
385
|
+
# only fetches when the consumer explicitly opts in via auto_sync: true
|
|
386
|
+
# in .bassclef-source.json. Cold adopters get a one-line note + the
|
|
387
|
+
# opt-in path instead of opaque auto-fetch.
|
|
388
|
+
#
|
|
389
|
+
# Default behavior on absence (file or field): OFF.
|
|
390
|
+
#
|
|
391
|
+
# Self-heal first (bet 2026-07-09a): stale adopters carrying
|
|
392
|
+
# .canonical-source.json get the file renamed to .bassclef-source.json
|
|
393
|
+
# so this pre-check sees the correct file. Second cycle drops the old.
|
|
394
|
+
bassclef_sync_self_heal_pointer "$CWD"
|
|
395
|
+
# Content-drift cure runs AFTER filename cure so the file we rewrite exists at the new name.
|
|
396
|
+
bassclef_sync_self_heal_pointer_content "$CWD"
|
|
397
|
+
# CLAUDE.md content cure runs at same lifecycle point — before Path 1 / Path 2 pick.
|
|
398
|
+
bassclef_sync_self_heal_claude_md_content "$CWD"
|
|
399
|
+
SOURCE_FILE_PRECHECK=$(bassclef_sync_resolve_pointer_file "$CWD")
|
|
400
|
+
if [ -n "$SOURCE_FILE_PRECHECK" ] && [ -f "$SOURCE_FILE_PRECHECK" ]; then
|
|
401
|
+
AUTO_SYNC=$(jq -r '.auto_sync // false' "$SOURCE_FILE_PRECHECK" 2>/dev/null)
|
|
402
|
+
if [ "$AUTO_SYNC" != "true" ]; then
|
|
403
|
+
cat <<NOTE
|
|
404
|
+
### bassclef-sync — auto-update is OFF
|
|
104
405
|
|
|
105
|
-
|
|
406
|
+
Bassclef can keep your skills, rules, and agents current at every
|
|
407
|
+
session start by pulling its latest substrate. It's off by default
|
|
408
|
+
for fresh repos so nothing fetches without your say-so.
|
|
106
409
|
|
|
107
|
-
|
|
108
|
-
local dispatch_available=0
|
|
109
|
-
[ -f "$installer_lib" ] && dispatch_available=1
|
|
410
|
+
Turn on anytime by saying "enable auto-sync" or by setting:
|
|
110
411
|
|
|
111
|
-
|
|
112
|
-
if [ -z "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
113
|
-
if command -v git >/dev/null 2>&1; then
|
|
114
|
-
CLAUDE_PROJECT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
115
|
-
fi
|
|
116
|
-
if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
117
|
-
export CLAUDE_PROJECT_DIR
|
|
118
|
-
fi
|
|
119
|
-
fi
|
|
412
|
+
"auto_sync": true
|
|
120
413
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
local skipped_no_header=0
|
|
130
|
-
local skipped_invalid=0
|
|
131
|
-
local skipped_lib_missing=0
|
|
132
|
-
local failed=0
|
|
133
|
-
# #1894 — capture missing-hook paths for structured status line
|
|
134
|
-
local -a missing_paths=()
|
|
135
|
-
local -a missing_scopes=()
|
|
136
|
-
|
|
137
|
-
# S-F2 LOW — dir-exists guard before loop
|
|
138
|
-
if [ ! -d "$bassclef_dir/.claude/hooks" ]; then
|
|
139
|
-
return 0
|
|
414
|
+
in .bassclef-source.json. Run a manual sync once with:
|
|
415
|
+
|
|
416
|
+
bash .claude/hooks/bassclef-sync.sh
|
|
417
|
+
|
|
418
|
+
More: https://github.com/sunj-labs/bassclef/blob/main/standards/bassclef-source-config.md
|
|
419
|
+
|
|
420
|
+
NOTE
|
|
421
|
+
exit 0
|
|
140
422
|
fi
|
|
423
|
+
fi
|
|
141
424
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
425
|
+
# === Plain-language error printer ===
|
|
426
|
+
#
|
|
427
|
+
# Args: $1 = short heading, $2 = context paragraph (one or more sentences).
|
|
428
|
+
# Always exits 0 to keep SessionStart hooks non-blocking.
|
|
429
|
+
emit_error() {
|
|
430
|
+
local heading="$1"
|
|
431
|
+
local context="$2"
|
|
432
|
+
cat <<HEADER
|
|
145
433
|
|
|
146
|
-
|
|
147
|
-
target="$user_target_dir/$hook_name"
|
|
434
|
+
### bassclef-sync — $heading
|
|
148
435
|
|
|
149
|
-
|
|
150
|
-
[ -L "$target" ] && rm -f "$target"
|
|
436
|
+
$context
|
|
151
437
|
|
|
152
|
-
|
|
153
|
-
if head -10 "$hook_src" 2>/dev/null | grep -qE '^# install-class:[[:space:]]*(operator|project|dual)$'; then
|
|
154
|
-
has_header=1
|
|
155
|
-
fi
|
|
438
|
+
Three ways to fix this:
|
|
156
439
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# mode never leaks into this script. On subshell failure, fall
|
|
160
|
-
# back to legacy operator-scope copy to preserve adopter
|
|
161
|
-
# contract per c4 (settings.json command strings byte-identical).
|
|
162
|
-
# shellcheck disable=SC1090 # dynamic source path
|
|
163
|
-
if ( source "$installer_lib" 2>/dev/null && install_by_class "$hook_src" ) >/dev/null 2>&1; then
|
|
164
|
-
installed=$((installed + 1))
|
|
165
|
-
else
|
|
166
|
-
cp -p "$hook_src" "$target" 2>/dev/null || true
|
|
167
|
-
chmod +x "$target" 2>/dev/null || true
|
|
168
|
-
echo "$(date -u +%FT%TZ) dispatch-fail $hook_name" >> "$BASSCLEF_TRACE_LOG"
|
|
169
|
-
failed=$((failed + 1))
|
|
170
|
-
# #1894 — capture path for structured status line
|
|
171
|
-
missing_paths+=(".claude/hooks/$hook_name")
|
|
172
|
-
missing_scopes+=("project")
|
|
173
|
-
fi
|
|
174
|
-
elif [ "$has_header" = "1" ] && [ "$dispatch_available" = "0" ]; then
|
|
175
|
-
# S-F1 MED — dispatch lib missing; trace explicitly (not silent)
|
|
176
|
-
cp -p "$hook_src" "$target" 2>/dev/null || true
|
|
177
|
-
chmod +x "$target" 2>/dev/null || true
|
|
178
|
-
echo "$(date -u +%FT%TZ) dispatch-lib-missing $hook_name" >> "$BASSCLEF_TRACE_LOG"
|
|
179
|
-
skipped_lib_missing=$((skipped_lib_missing + 1))
|
|
180
|
-
else
|
|
181
|
-
# ADR-058 c2 fallback — grace window through 2026-10-31.
|
|
182
|
-
# L-F2 MED — additive-only during grace; do NOT rm legacy user-scope copies.
|
|
183
|
-
cp -p "$hook_src" "$target" 2>/dev/null || true
|
|
184
|
-
chmod +x "$target" 2>/dev/null || true
|
|
185
|
-
if [ "$has_header" = "0" ]; then
|
|
186
|
-
echo "$(date -u +%FT%TZ) skipped-no-header $hook_name" >> "$BASSCLEF_TRACE_LOG"
|
|
187
|
-
skipped_no_header=$((skipped_no_header + 1))
|
|
188
|
-
else
|
|
189
|
-
echo "$(date -u +%FT%TZ) skipped-invalid-header $hook_name" >> "$BASSCLEF_TRACE_LOG"
|
|
190
|
-
skipped_invalid=$((skipped_invalid + 1))
|
|
191
|
-
# #1894 — invalid-header is a real defect (not grace-window); capture for status line
|
|
192
|
-
missing_paths+=(".claude/hooks/$hook_name")
|
|
193
|
-
missing_scopes+=("project")
|
|
194
|
-
fi
|
|
195
|
-
fi
|
|
196
|
-
done
|
|
440
|
+
1) PUBLIC adopters (most common)
|
|
441
|
+
Edit .bassclef-source.json to point at the public release tarball:
|
|
197
442
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
[ "$skipped_no_header" -gt 0 ] && breakdown="$breakdown, $skipped_no_header grace-window"
|
|
205
|
-
[ "$skipped_invalid" -gt 0 ] && breakdown="$breakdown, $skipped_invalid invalid-header"
|
|
206
|
-
[ "$skipped_lib_missing" -gt 0 ] && breakdown="$breakdown, $skipped_lib_missing dispatch-lib-missing"
|
|
207
|
-
breakdown="${breakdown#, }"
|
|
208
|
-
# RFC-0008 F7 — three bare numbers read three ways. Say what they mean
|
|
209
|
-
# together, and say plainly whether the install is usable.
|
|
210
|
-
if [ "$failed" = "0" ] && [ "$skipped_invalid" = "0" ]; then
|
|
211
|
-
echo "bassclef-sync: $installed hooks ready. $skipped_total skipped on purpose ($breakdown). Nothing failed — your install is complete."
|
|
212
|
-
else
|
|
213
|
-
echo "bassclef-sync: $installed hooks ready. $skipped_total skipped on purpose ($breakdown)."
|
|
214
|
-
if [ "$failed" -gt 0 ]; then
|
|
215
|
-
echo "bassclef-sync: $failed could not be installed. Those hooks will not fire until that is fixed."
|
|
216
|
-
fi
|
|
217
|
-
fi
|
|
218
|
-
fi
|
|
443
|
+
{
|
|
444
|
+
"source_url": "https://github.com/sunj-labs/bassclef/releases/latest/download/substrate.tar.gz",
|
|
445
|
+
"auth_method": "none",
|
|
446
|
+
"token_env": "",
|
|
447
|
+
"comment": "Public bassclef substrate. No auth needed."
|
|
448
|
+
}
|
|
219
449
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
# grace-window and dispatch-lib-missing count as OK per ADR-058 c2)
|
|
223
|
-
local total=$((installed + skipped_total + failed))
|
|
224
|
-
local defect_total=$((failed + skipped_invalid))
|
|
225
|
-
if [ "$defect_total" = "0" ]; then
|
|
226
|
-
# RESTORED — human line + minimal JSON status:complete (Beck B1 fold: minimal JSON in V1)
|
|
227
|
-
echo "bassclef-sync: RESTORED — $installed of $total hooks resolved at their scoped targets."
|
|
228
|
-
printf 'bassclef-sync: {"status":"complete","installed":%d}\n' "$installed"
|
|
229
|
-
else
|
|
230
|
-
# DEGRADED — human line + JSON with missing[] carrying path + scope + cure per entry
|
|
231
|
-
echo "bassclef-sync: DEGRADED — $defect_total of $total hooks did not resolve after self-heal."
|
|
232
|
-
local i missing_json="["
|
|
233
|
-
for i in "${!missing_paths[@]}"; do
|
|
234
|
-
[ "$i" -gt 0 ] && missing_json="$missing_json,"
|
|
235
|
-
missing_json="$missing_json{\"path\":\"${missing_paths[$i]}\",\"scope\":\"${missing_scopes[$i]}\",\"cure\":\"bassclef init --force\"}"
|
|
236
|
-
done
|
|
237
|
-
missing_json="$missing_json]"
|
|
238
|
-
printf 'bassclef-sync: {"status":"degraded","installed":%d,"missing":%s}\n' "$installed" "$missing_json"
|
|
239
|
-
fi
|
|
240
|
-
}
|
|
450
|
+
Pre-launch note: while bassclef is still private, the public
|
|
451
|
+
release URL above may not yet exist. Until it does, use option 3.
|
|
241
452
|
|
|
242
|
-
|
|
243
|
-
# Tests source this hook to access helpers without triggering main sync.
|
|
244
|
-
# Early return under BASSCLEF_SYNC_TEST_MODE=1. Helpers defined ABOVE.
|
|
245
|
-
if [ "${BASSCLEF_SYNC_TEST_MODE:-0}" = "1" ]; then
|
|
246
|
-
return 0 2>/dev/null || exit 0
|
|
247
|
-
fi
|
|
453
|
+
2) LOCAL DEV — clone bassclef alongside this repo
|
|
248
454
|
|
|
249
|
-
|
|
250
|
-
# Silent-fail — a missing lib never crashes the hook. Discipline: ADR-048 +
|
|
251
|
-
# standards/hook-cadence.json. Session-start reflection module (WU-4.5) checks
|
|
252
|
-
# marker mtime against expected cadence.
|
|
253
|
-
{
|
|
254
|
-
_hb_sd="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
255
|
-
for _hb_c in "${_hb_sd}/../../lib/hook-heartbeat.sh" "${HOME:-/}/lib/hook-heartbeat.sh"; do
|
|
256
|
-
[ -f "$_hb_c" ] && source "$_hb_c" && heartbeat_mark "bassclef-sync" && break
|
|
257
|
-
done
|
|
258
|
-
unset _hb_sd _hb_c
|
|
259
|
-
} 2>/dev/null || true
|
|
455
|
+
cd ../ && git clone https://github.com/sunj-labs/bassclef.git
|
|
260
456
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
457
|
+
Re-run your session. The dispatcher uses the local checkout via
|
|
458
|
+
the sibling fast-path; no auth needed.
|
|
459
|
+
|
|
460
|
+
3) PRIVATE / pre-launch — create a fine-grained GitHub PAT
|
|
461
|
+
|
|
462
|
+
a) GitHub -> Settings -> Developer settings -> Personal access tokens
|
|
463
|
+
-> Fine-grained -> Generate new token
|
|
464
|
+
b) Resource owner: sunj-labs
|
|
465
|
+
c) Repository access: select sunj-labs/bassclef only
|
|
466
|
+
d) Permissions -> Repository -> Contents -> Read
|
|
467
|
+
e) Set the token as BASSCLEF_TOKEN in your environment:
|
|
468
|
+
CI: gh secret set BASSCLEF_TOKEN -R <your-org/your-repo>
|
|
469
|
+
Local: export BASSCLEF_TOKEN=<paste>
|
|
470
|
+
Cloud Code: set BASSCLEF_TOKEN in your environment config
|
|
471
|
+
|
|
472
|
+
4) STALE REFERENCES — your repo installed bassclef before 2026-06-29
|
|
264
473
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
474
|
+
If your tree carries leftover canonical-* references from the
|
|
475
|
+
2026-06-20 rename event, refresh them in place by running the
|
|
476
|
+
migrate script directly:
|
|
477
|
+
|
|
478
|
+
curl -sL https://raw.githubusercontent.com/sunj-labs/bassclef/main/scripts/migrate-adopter-references.sh \\
|
|
479
|
+
| bash -s -- --apply "\$CWD"
|
|
480
|
+
|
|
481
|
+
Open a fresh Claude Code session afterward. The fresh dispatcher
|
|
482
|
+
carries detection that runs the cure automatically when needed.
|
|
483
|
+
|
|
484
|
+
More: https://github.com/sunj-labs/bassclef/blob/main/standards/bassclef-source-config.md
|
|
485
|
+
https://github.com/sunj-labs/bassclef/wiki/Troubleshooting#substrate-fails-to-load-and-you-do-not-see-the-self-heal-banner
|
|
486
|
+
|
|
487
|
+
HEADER
|
|
270
488
|
exit 0
|
|
271
|
-
|
|
489
|
+
}
|
|
272
490
|
|
|
273
|
-
#
|
|
274
|
-
#
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
#
|
|
282
|
-
|
|
283
|
-
BASSCLEF_REPO="https://github.com/sunj-labs/bassclef.git"
|
|
284
|
-
SYNC_STATUS="up-to-date"
|
|
285
|
-
SYNC_NEW=""
|
|
286
|
-
|
|
287
|
-
# === 1. Clone or pull bassclef ===
|
|
288
|
-
# When BASSCLEF_DIR points at a valid bundle with .claude/hooks/ AND is NOT a
|
|
289
|
-
# git repo, skip clone/pull — env-supplied bundle (test fixture) is authoritative.
|
|
290
|
-
if [ -d "$BASSCLEF_DIR/.claude/hooks" ] && [ ! -d "$BASSCLEF_DIR/.git" ]; then
|
|
291
|
-
# env-override fixture path; skip clone/pull
|
|
292
|
-
:
|
|
293
|
-
elif [ ! -d "$BASSCLEF_DIR/.claude" ]; then
|
|
294
|
-
# Capture stderr so classify_clone_failure can name the real cause.
|
|
295
|
-
# Prior version blamed GitHub auth on every failure — cli#106 body showed
|
|
296
|
-
# this misleads adopters whose auth is fine but who can't see a private repo.
|
|
297
|
-
# Classifier emits per-cause hints — for auth failures, the hint reads
|
|
298
|
-
# "Optional: set up GitHub auth to unlock full-tier sync — run: gh auth login".
|
|
299
|
-
# (This literal phrase kept here so #1631 Amendment C test greps still match.)
|
|
300
|
-
__sync_clone_stderr=$(git clone "$BASSCLEF_REPO" "$BASSCLEF_DIR" 2>&1)
|
|
301
|
-
__sync_clone_exit=$?
|
|
302
|
-
if [ "$__sync_clone_exit" -eq 0 ]; then
|
|
303
|
-
SYNC_STATUS="freshly cloned"
|
|
304
|
-
else
|
|
305
|
-
# Source the classifier lib; probe bundled path first, then $HOME/lib fallback.
|
|
306
|
-
{
|
|
307
|
-
_cf_sd="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
308
|
-
for _cf_c in "${_cf_sd}/../../lib/clone-failure-classifier.sh" "${HOME:-/}/lib/clone-failure-classifier.sh"; do
|
|
309
|
-
[ -f "$_cf_c" ] && source "$_cf_c" && break
|
|
310
|
-
done
|
|
311
|
-
unset _cf_sd _cf_c
|
|
312
|
-
} 2>/dev/null || true
|
|
491
|
+
# === Recovery script resolver + checksum verifier ===
|
|
492
|
+
#
|
|
493
|
+
# Per goal 2026-08-27 step 7 PR #1 — cures external security review HIGH
|
|
494
|
+
# finding on the self-heal curl|bash fallback (source audit tracked in
|
|
495
|
+
# operator-only artifact; not shipped to public bassclef).
|
|
496
|
+
#
|
|
497
|
+
# Two functions cover Adam's Options A (bundle local) + B (checksum on curl).
|
|
498
|
+
#
|
|
499
|
+
# @pattern strategy (RecoveryScriptResolver picks one of three sources)
|
|
500
|
+
# @pattern guard-clause (verify_checksum fails loud on mismatch)
|
|
313
501
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
exit 0
|
|
502
|
+
# resolve_recovery_script — prints one of:
|
|
503
|
+
# LOCAL:<path> env override set + file exists
|
|
504
|
+
# SIBLING:<path> sibling checkout has the script
|
|
505
|
+
# CURL:<url>|<sha> neither available; use curl with checksum
|
|
506
|
+
#
|
|
507
|
+
# Priority chain per docs/decompositions/2026-08-27-step-7-pr-1-dispatcher-cure-responsibilities.md
|
|
508
|
+
resolve_recovery_script() {
|
|
509
|
+
# Priority 1 — env override
|
|
510
|
+
if [ -n "${BASSCLEF_RECOVERY_SCRIPT_PATH:-}" ] && [ -f "$BASSCLEF_RECOVERY_SCRIPT_PATH" ]; then
|
|
511
|
+
printf 'LOCAL:%s\n' "$BASSCLEF_RECOVERY_SCRIPT_PATH"
|
|
512
|
+
return 0
|
|
326
513
|
fi
|
|
327
|
-
unset __sync_clone_stderr __sync_clone_exit
|
|
328
|
-
else
|
|
329
|
-
BEFORE=$(git -C "$BASSCLEF_DIR" rev-parse HEAD 2>/dev/null)
|
|
330
|
-
git -C "$BASSCLEF_DIR" pull --quiet 2>/dev/null || true
|
|
331
|
-
AFTER=$(git -C "$BASSCLEF_DIR" rev-parse HEAD 2>/dev/null)
|
|
332
514
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
515
|
+
# Priority 2 — sibling checkout (matches SIBLING_TEMPLATE pattern at L594)
|
|
516
|
+
local sibling_script="${CWD:-$(pwd)}/../bassclef/scripts/migrate-adopter-references.sh"
|
|
517
|
+
if [ -f "$sibling_script" ]; then
|
|
518
|
+
printf 'SIBLING:%s\n' "$sibling_script"
|
|
519
|
+
return 0
|
|
336
520
|
fi
|
|
337
|
-
fi
|
|
338
521
|
|
|
339
|
-
#
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if [ -L "$TARGET" ]; then
|
|
346
|
-
# Existing symlink — update it
|
|
347
|
-
rm "$TARGET"
|
|
348
|
-
ln -s "$SKILL_DIR" "$TARGET"
|
|
349
|
-
elif [ ! -d "$TARGET" ]; then
|
|
350
|
-
# Doesn't exist — create symlink
|
|
351
|
-
ln -s "$SKILL_DIR" "$TARGET"
|
|
352
|
-
fi
|
|
353
|
-
# If it's a real directory (app-specific), leave it alone
|
|
354
|
-
done
|
|
355
|
-
|
|
356
|
-
# === 2b. Park orphan ~/.claude/skills + agents + hooks symlinks (widened per ADR-033 + bassclef#278) ===
|
|
357
|
-
# Uses the shared orphan-parking helper. Closes the skill / agent / hook
|
|
358
|
-
# duplication-in-available-skills bug. The helper parks user-level
|
|
359
|
-
# symlinks AND cp'd files whose basename appears in project-level —
|
|
360
|
-
# preserves real directories untouched + EXCLUDES hooks wired in
|
|
361
|
-
# settings.json at $HOME paths (per #294); backs up moved entries to
|
|
362
|
-
# dated dirs that survive at least 30 days per Hyrum + Linus discipline.
|
|
363
|
-
PARKING_HELPER="$BASSCLEF_DIR/scripts/bassclef-orphan-parking.sh"
|
|
364
|
-
USER_SETTINGS="$HOME/.claude/settings.json"
|
|
365
|
-
ORPHAN_COUNT=0
|
|
366
|
-
if [ -f "$PARKING_HELPER" ]; then
|
|
367
|
-
for surface in skills agents hooks; do
|
|
368
|
-
USER_SURFACE="$HOME/.claude/$surface"
|
|
369
|
-
PROJECT_SURFACE="$CWD/.claude/$surface"
|
|
370
|
-
if [ -d "$USER_SURFACE" ] && [ -d "$PROJECT_SURFACE" ]; then
|
|
371
|
-
mkdir -p "$HOME/.claude" 2>/dev/null
|
|
372
|
-
# Pass user settings.json as 5th arg so wired hooks are excluded
|
|
373
|
-
# from parking. Per #294 — broke the Stop hook on this machine when
|
|
374
|
-
# parking removed session-end.sh that settings.json wires.
|
|
375
|
-
PARKED_LINE=$(bash "$PARKING_HELPER" "$USER_SURFACE" "$PROJECT_SURFACE" "$HOME/.claude" "$surface" "$USER_SETTINGS" 2>/dev/null || echo "")
|
|
376
|
-
if [ -n "$PARKED_LINE" ]; then
|
|
377
|
-
# Extract the count from "parked N <surface> orphans → <dir>"
|
|
378
|
-
COUNT_N=$(echo "$PARKED_LINE" | grep -oE 'parked [0-9]+' | grep -oE '[0-9]+' | head -1)
|
|
379
|
-
[ -n "$COUNT_N" ] && ORPHAN_COUNT=$((ORPHAN_COUNT + COUNT_N))
|
|
380
|
-
fi
|
|
381
|
-
fi
|
|
382
|
-
done
|
|
383
|
-
fi
|
|
522
|
+
# Priority 3 — curl fallback with checksum sidecar
|
|
523
|
+
local recovery_url="https://raw.githubusercontent.com/sunj-labs/bassclef/main/scripts/migrate-adopter-references.sh"
|
|
524
|
+
local sha_url="${recovery_url}.sha256"
|
|
525
|
+
printf 'CURL:%s|%s\n' "$recovery_url" "$sha_url"
|
|
526
|
+
return 0
|
|
527
|
+
}
|
|
384
528
|
|
|
385
|
-
#
|
|
386
|
-
#
|
|
387
|
-
#
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
PER_SURFACE=""
|
|
402
|
-
# Check -f (file exists), not -x (executable). We invoke via `bash $HELPER`
|
|
403
|
-
# so the executable bit is irrelevant; the bit may not survive tarball
|
|
404
|
-
# fetches across adopter clones (per reviewer note on PR #291).
|
|
405
|
-
if [ -f "$SUMMARY_HELPER" ]; then
|
|
406
|
-
PER_SURFACE=$(echo "$MIGRATE_OUT" | bash "$SUMMARY_HELPER" 2>/dev/null)
|
|
407
|
-
fi
|
|
408
|
-
SYNC_NEW="${SYNC_NEW}
|
|
409
|
-
Adopter self-heal: $MIGRATE_CHANGES surface(s) migrated. Old setup keeps working; new setup matches current bassclef substrate. Per ADR-031."
|
|
410
|
-
if [ -n "$PER_SURFACE" ]; then
|
|
411
|
-
SYNC_NEW="${SYNC_NEW}
|
|
412
|
-
${PER_SURFACE}"
|
|
413
|
-
fi
|
|
529
|
+
# verify_checksum <file> <expected_sha256>
|
|
530
|
+
# Returns 0 on match; non-zero + loud stderr on mismatch or missing tool.
|
|
531
|
+
# @luminary saltzer-schroeder — complete mediation; no silent pass.
|
|
532
|
+
verify_checksum() {
|
|
533
|
+
local file="$1"
|
|
534
|
+
local expected="$2"
|
|
535
|
+
local computed=""
|
|
536
|
+
|
|
537
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
538
|
+
computed=$(shasum -a 256 "$file" 2>/dev/null | awk '{print $1}')
|
|
539
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
540
|
+
computed=$(sha256sum "$file" 2>/dev/null | awk '{print $1}')
|
|
541
|
+
else
|
|
542
|
+
echo "verify_checksum: neither shasum nor sha256sum found in PATH" >&2
|
|
543
|
+
echo "verify_checksum: install one before continuing" >&2
|
|
544
|
+
return 2
|
|
414
545
|
fi
|
|
415
|
-
fi
|
|
416
546
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
# into the function above. Each hook now routes by its own
|
|
421
|
-
# # install-class: header when declared; unheadered hooks fall back
|
|
422
|
-
# to legacy operator-scope copy per ADR-058 c2 grace window
|
|
423
|
-
# (through 2026-10-31). Section 4 self-heal continues to preserve
|
|
424
|
-
# adopter contract per c4 (settings.json command strings unchanged).
|
|
425
|
-
_sync_install_hooks "$BASSCLEF_DIR" "$HOME/.claude/hooks"
|
|
426
|
-
|
|
427
|
-
# === Install agents to ~/.claude/ ===
|
|
428
|
-
mkdir -p ~/.claude/agents 2>/dev/null || true
|
|
429
|
-
cp "$BASSCLEF_DIR/.claude/agents/"*.md ~/.claude/agents/ 2>/dev/null || true
|
|
430
|
-
|
|
431
|
-
# === 4. Versioned self-heal of ~/.claude/settings.json ===
|
|
432
|
-
# WU-3 of bet 2026-06-24a-settings-hardening (#313). Replaces the
|
|
433
|
-
# brittle multi-grep + destructive heredoc rewrite with:
|
|
434
|
-
# 1. Version-field comparison (read_settings_version).
|
|
435
|
-
# 2. Additive merge that preserves operator's top-level keys (theme,
|
|
436
|
-
# statusLine, permissions, alwaysThinkingEnabled, etc.) and
|
|
437
|
-
# operator-only event types — bassclef wins per-event for events
|
|
438
|
-
# it ships.
|
|
439
|
-
# 3. Hoare postcondition: after self-heal, drift detector MUST
|
|
440
|
-
# return clean (compare_wirings .ok == true). Else exit 1 with
|
|
441
|
-
# structured stderr — failure compounds across every adopter,
|
|
442
|
-
# so failing loud beats failing silent.
|
|
443
|
-
# Closes the bug class diagnosed 2026-06-24 (operator's settings.json
|
|
444
|
-
# never picked up substrate's new hook wirings).
|
|
445
|
-
WIRINGS_LIB="$BASSCLEF_DIR/.claude/hooks/_lib/wirings.sh"
|
|
446
|
-
PROJECT_SETTINGS="$BASSCLEF_DIR/.claude/settings.json"
|
|
447
|
-
USER_SETTINGS="$HOME/.claude/settings.json"
|
|
448
|
-
|
|
449
|
-
sync_self_heal() {
|
|
450
|
-
if [ ! -f "$WIRINGS_LIB" ] || [ ! -f "$PROJECT_SETTINGS" ]; then
|
|
451
|
-
# Substrate not fully synced yet OR project settings.json not
|
|
452
|
-
# checked out on this branch. Fall back silently — drift detector
|
|
453
|
-
# at next session-start will surface the gap.
|
|
454
|
-
return 0
|
|
547
|
+
if [ -z "$computed" ]; then
|
|
548
|
+
echo "verify_checksum: could not compute SHA-256 of $file" >&2
|
|
549
|
+
return 3
|
|
455
550
|
fi
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
local required_version user_version
|
|
460
|
-
required_version=$(read_settings_version "$PROJECT_SETTINGS")
|
|
461
|
-
user_version=$(read_settings_version "$USER_SETTINGS")
|
|
462
|
-
|
|
463
|
-
if [ ! -f "$USER_SETTINGS" ] || [ "$user_version" -lt "$required_version" ]; then
|
|
464
|
-
additive_merge_settings "$USER_SETTINGS" "$PROJECT_SETTINGS"
|
|
465
|
-
fi
|
|
466
|
-
|
|
467
|
-
# Hoare postcondition: required ⊆ loaded after self-heal.
|
|
468
|
-
local required loaded diff ok missing
|
|
469
|
-
required=$(read_required_wirings "$PROJECT_SETTINGS")
|
|
470
|
-
loaded=$(read_loaded_wirings)
|
|
471
|
-
diff=$(compare_wirings "$required" "$loaded")
|
|
472
|
-
ok=$(echo "$diff" | jq -r '.ok')
|
|
473
|
-
if [ "$ok" != "true" ]; then
|
|
474
|
-
missing=$(echo "$diff" | jq -c '.missing')
|
|
475
|
-
# cli#1742 cure B — guard banner emit on non-empty missing list.
|
|
476
|
-
# Prior version printed the header + Fix line even when missing was
|
|
477
|
-
# empty (extras-only case), leaving adopters with a header pointing at
|
|
478
|
-
# nothing. Silence when nothing is genuinely missing.
|
|
479
|
-
__sync_missing_count=$(echo "$missing" | jq 'length' 2>/dev/null || echo 0)
|
|
480
|
-
if [ "${__sync_missing_count:-0}" -gt 0 ]; then
|
|
481
|
-
echo "🛑 bassclef-sync — these hooks should fire and are not connected:" >&2
|
|
482
|
-
echo "$missing" | jq -r '.[] | " - " + .event + " → " + .command' >&2
|
|
483
|
-
echo "" >&2
|
|
484
|
-
echo "Fix: add the lines above to ~/.claude/settings.json, then run bassclef-sync again." >&2
|
|
485
|
-
fi
|
|
486
|
-
unset __sync_missing_count
|
|
487
|
-
if command -v trace_log >/dev/null 2>&1; then
|
|
488
|
-
trace_log "fail" "bassclef-sync-postcondition" "$missing"
|
|
489
|
-
fi
|
|
490
|
-
return 1
|
|
491
|
-
fi
|
|
492
|
-
if command -v trace_log >/dev/null 2>&1; then
|
|
493
|
-
trace_log "ok" "bassclef-sync-postcondition" "version=$required_version"
|
|
551
|
+
|
|
552
|
+
if [ "$computed" = "$expected" ]; then
|
|
553
|
+
return 0
|
|
494
554
|
fi
|
|
495
|
-
|
|
555
|
+
|
|
556
|
+
echo "verify_checksum: SHA-256 mismatch on $file" >&2
|
|
557
|
+
echo "verify_checksum: expected: $expected" >&2
|
|
558
|
+
echo "verify_checksum: computed: $computed" >&2
|
|
559
|
+
return 1
|
|
496
560
|
}
|
|
497
561
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
#
|
|
501
|
-
#
|
|
502
|
-
#
|
|
503
|
-
#
|
|
504
|
-
#
|
|
505
|
-
#
|
|
506
|
-
#
|
|
507
|
-
#
|
|
508
|
-
#
|
|
509
|
-
#
|
|
510
|
-
#
|
|
511
|
-
#
|
|
512
|
-
#
|
|
513
|
-
#
|
|
514
|
-
#
|
|
515
|
-
#
|
|
516
|
-
#
|
|
517
|
-
#
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
#
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
# }
|
|
529
|
-
# }
|
|
530
|
-
#
|
|
531
|
-
# Idempotent via jq additive merge — re-runs detect existing entries and skip.
|
|
532
|
-
# Skip paths:
|
|
533
|
-
# - SKIP_PRETOOLUSE_EMBED=1 — operator override (logged)
|
|
534
|
-
# - jq missing — graceful skip with stderr notice
|
|
535
|
-
# - ~/.claude/settings.json missing — skip (sync_self_heal handles creation)
|
|
536
|
-
if [ "${SKIP_PRETOOLUSE_EMBED:-0}" != "1" ] && \
|
|
537
|
-
command -v jq >/dev/null 2>&1 && \
|
|
538
|
-
[ -f "$USER_SETTINGS" ]; then
|
|
539
|
-
|
|
540
|
-
# Guard re-emit when bet-doc-gate missing (existing-adopter self-heal)
|
|
541
|
-
if ! grep -q 'bet-doc-gate' ~/.claude/settings.json 2>/dev/null; then
|
|
542
|
-
BET_DOC_GATE_MISSING=1
|
|
543
|
-
else
|
|
544
|
-
BET_DOC_GATE_MISSING=0
|
|
562
|
+
# === Canonical-* reference detection (per bet 29e WU-2) ===
|
|
563
|
+
#
|
|
564
|
+
# Self-heal for adopters carrying canonical-* references from the
|
|
565
|
+
# 2026-06-20 rename. Scans the adopter .claude/ tree at session-start
|
|
566
|
+
# BEFORE Path 1 / Path 2. When canonical-* refs are found, emits a
|
|
567
|
+
# BLOCKED block naming the recovery command (curl one-liner shipped
|
|
568
|
+
# in WU-3 of bet 29e) and exits 0 so SessionStart stays non-blocking.
|
|
569
|
+
#
|
|
570
|
+
# WU-2 emits BLOCKED + exits. WU-4 will rewrite this block to auto-
|
|
571
|
+
# invoke the recovery and continue the dispatcher chain.
|
|
572
|
+
#
|
|
573
|
+
# Override: SKIP_CANONICAL_DETECTION=1 bypasses the scan. Reserved for
|
|
574
|
+
# migration scripts; not for routine adopter use.
|
|
575
|
+
#
|
|
576
|
+
# Primary lens: @luminary michael-nygard — self-healing integration
|
|
577
|
+
# point per Release It! Stability Patterns. Composing lenses:
|
|
578
|
+
# @luminary jerome-saltzer-and-michael-schroeder (complete mediation —
|
|
579
|
+
# every session-start sees the scan), @luminary tony-hoare (when no
|
|
580
|
+
# canonical refs found, scan is silent; postcondition holds), @luminary
|
|
581
|
+
# john-ousterhout (deep dispatcher hides migration complexity).
|
|
582
|
+
if [ "${SKIP_CANONICAL_DETECTION:-0}" != "1" ]; then
|
|
583
|
+
CANONICAL_FINDINGS=()
|
|
584
|
+
|
|
585
|
+
# Surface 2: settings.json additionalDirectories contains "canonical"
|
|
586
|
+
SETTINGS_PATH="$CWD/.claude/settings.json"
|
|
587
|
+
if [ -f "$SETTINGS_PATH" ]; then
|
|
588
|
+
STALE_DIRS=$(jq -r '.permissions.additionalDirectories // [] | .[] | select(test("canonical"))' "$SETTINGS_PATH" 2>/dev/null || true)
|
|
589
|
+
if [ -n "$STALE_DIRS" ]; then
|
|
590
|
+
CANONICAL_FINDINGS+=(".claude/settings.json additionalDirectories contains canonical")
|
|
591
|
+
fi
|
|
545
592
|
fi
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
{"matcher": "Edit|Write|MultiEdit", "command": "$HOME/.claude/hooks/bet-doc-gate.sh"}
|
|
560
|
-
]'
|
|
561
|
-
|
|
562
|
-
# Additive jq merge — mirrors §3c-hooks logic but writes to USER_SETTINGS:
|
|
563
|
-
# 1. Ensure .hooks.PreToolUse array exists
|
|
564
|
-
# 2. For each bassclef hook:
|
|
565
|
-
# - If matcher exists, ensure command is in the matcher's hooks array
|
|
566
|
-
# - If matcher missing, create new matcher entry with the command
|
|
567
|
-
# Operator-managed entries (other matchers, other commands) stay untouched.
|
|
568
|
-
NEW_OPERATOR_SETTINGS=$(jq --argjson bassclef "$OPERATOR_PRETOOLUSE_JSON" '
|
|
569
|
-
.hooks //= {} |
|
|
570
|
-
.hooks.PreToolUse //= [] |
|
|
571
|
-
reduce $bassclef[] as $hook (
|
|
572
|
-
.;
|
|
573
|
-
if (.hooks.PreToolUse | map(.matcher) | index($hook.matcher)) != null then
|
|
574
|
-
.hooks.PreToolUse |= map(
|
|
575
|
-
if .matcher == $hook.matcher then
|
|
576
|
-
if (.hooks | map(.command) | index($hook.command)) == null then
|
|
577
|
-
.hooks += [{"type": "command", "command": $hook.command}]
|
|
578
|
-
else . end
|
|
579
|
-
else . end
|
|
580
|
-
)
|
|
581
|
-
else
|
|
582
|
-
.hooks.PreToolUse += [{
|
|
583
|
-
"matcher": $hook.matcher,
|
|
584
|
-
"hooks": [{"type": "command", "command": $hook.command}]
|
|
585
|
-
}]
|
|
586
|
-
end
|
|
587
|
-
)
|
|
588
|
-
' "$USER_SETTINGS" 2>/dev/null)
|
|
589
|
-
|
|
590
|
-
if [ -n "$NEW_OPERATOR_SETTINGS" ]; then
|
|
591
|
-
echo "$NEW_OPERATOR_SETTINGS" > "$USER_SETTINGS.tmp" && \
|
|
592
|
-
mv "$USER_SETTINGS.tmp" "$USER_SETTINGS"
|
|
593
|
-
if command -v trace_log >/dev/null 2>&1; then
|
|
594
|
-
trace_log "ok" "bassclef-sync-pretooluse-embed" \
|
|
595
|
-
"added_bet_doc=$BET_DOC_GATE_MISSING added_substrate_clarity=$SUBSTRATE_CLARITY_GATE_MISSING"
|
|
593
|
+
|
|
594
|
+
# Surface 3: vendored dispatcher BASSCLEF_SYNC_VERSION drift
|
|
595
|
+
# (per ADR-032 Layer 3 self-heal; mirrors migrate-adopter-references.sh Surface 3)
|
|
596
|
+
# Read the current version from the helper to avoid drift between the
|
|
597
|
+
# comment header, the function, and this constant.
|
|
598
|
+
CURRENT_VERSION=$(__bassclef_dispatcher_version)
|
|
599
|
+
for CANDIDATE in "$CWD/.claude/hooks/bassclef-sync.sh" "$CWD/.claude/hooks/canonical-sync.sh"; do
|
|
600
|
+
if [ -f "$CANDIDATE" ] && [ ! -L "$CANDIDATE" ]; then
|
|
601
|
+
VENDORED_VERSION=$(grep -m1 'BASSCLEF_SYNC_VERSION=' "$CANDIDATE" 2>/dev/null | sed 's/.*BASSCLEF_SYNC_VERSION=\([^ ]*\).*/\1/' | head -1)
|
|
602
|
+
if [ -z "$VENDORED_VERSION" ] || [[ "$VENDORED_VERSION" < "$CURRENT_VERSION" ]]; then
|
|
603
|
+
BASENAME=$(basename "$CANDIDATE")
|
|
604
|
+
DISPLAY_VERSION="${VENDORED_VERSION:-<missing>}"
|
|
605
|
+
CANONICAL_FINDINGS+=(".claude/hooks/$BASENAME stale (BASSCLEF_SYNC_VERSION=$DISPLAY_VERSION; current=$CURRENT_VERSION)")
|
|
596
606
|
fi
|
|
607
|
+
break
|
|
597
608
|
fi
|
|
598
|
-
|
|
599
|
-
fi
|
|
609
|
+
done
|
|
600
610
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
#
|
|
605
|
-
#
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
#
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
# }
|
|
617
|
-
# ]
|
|
618
|
-
# }
|
|
619
|
-
# }
|
|
620
|
-
#
|
|
621
|
-
# Timeout 30s: turn-prose-kiss-check scans the most-recent assistant message
|
|
622
|
-
# against the BLOCK wordlist; finishes well under 30s on any realistic
|
|
623
|
-
# transcript. ≥5s minimum per session-artifacts rule (Stop hooks need
|
|
624
|
-
# adequate timeout or they get killed mid-write).
|
|
625
|
-
if [ "${SKIP_STOP_EMBED:-0}" != "1" ] && \
|
|
626
|
-
command -v jq >/dev/null 2>&1 && \
|
|
627
|
-
[ -f "$USER_SETTINGS" ]; then
|
|
628
|
-
|
|
629
|
-
# Guard re-emit when turn-prose entry missing
|
|
630
|
-
if ! grep -q 'turn-prose' ~/.claude/settings.json 2>/dev/null; then
|
|
631
|
-
TURN_PROSE_MISSING=1
|
|
632
|
-
else
|
|
633
|
-
TURN_PROSE_MISSING=0
|
|
634
|
-
fi
|
|
635
|
-
|
|
636
|
-
if [ "$TURN_PROSE_MISSING" = "1" ]; then
|
|
637
|
-
# Stop-event entry — empty matcher (Stop fires once per session-end;
|
|
638
|
-
# no per-tool matcher applies). Command uses $HOME-prefixed path
|
|
639
|
-
# (Claude Code expands at hook-fire time). Timeout 30 per above.
|
|
640
|
-
OPERATOR_STOP_JSON='[
|
|
641
|
-
{"matcher": "", "command": "$HOME/.claude/hooks/turn-prose-kiss-check.sh", "timeout": 30}
|
|
642
|
-
]'
|
|
643
|
-
|
|
644
|
-
NEW_STOP_SETTINGS=$(jq --argjson bassclef "$OPERATOR_STOP_JSON" '
|
|
645
|
-
.hooks //= {} |
|
|
646
|
-
.hooks.Stop //= [] |
|
|
647
|
-
reduce $bassclef[] as $hook (
|
|
648
|
-
.;
|
|
649
|
-
if (.hooks.Stop | map(.matcher) | index($hook.matcher)) != null then
|
|
650
|
-
.hooks.Stop |= map(
|
|
651
|
-
if .matcher == $hook.matcher then
|
|
652
|
-
if (.hooks | map(.command) | index($hook.command)) == null then
|
|
653
|
-
.hooks += [{"type": "command", "command": $hook.command, "timeout": $hook.timeout}]
|
|
654
|
-
else . end
|
|
655
|
-
else . end
|
|
656
|
-
)
|
|
657
|
-
else
|
|
658
|
-
.hooks.Stop += [{
|
|
659
|
-
"matcher": $hook.matcher,
|
|
660
|
-
"hooks": [{"type": "command", "command": $hook.command, "timeout": $hook.timeout}]
|
|
661
|
-
}]
|
|
662
|
-
end
|
|
663
|
-
)
|
|
664
|
-
' "$USER_SETTINGS" 2>/dev/null)
|
|
665
|
-
|
|
666
|
-
if [ -n "$NEW_STOP_SETTINGS" ]; then
|
|
667
|
-
echo "$NEW_STOP_SETTINGS" > "$USER_SETTINGS.tmp" && \
|
|
668
|
-
mv "$USER_SETTINGS.tmp" "$USER_SETTINGS"
|
|
669
|
-
if command -v trace_log >/dev/null 2>&1; then
|
|
670
|
-
trace_log "ok" "bassclef-sync-stop-embed" \
|
|
671
|
-
"added_turn_prose=$TURN_PROSE_MISSING"
|
|
611
|
+
if [ "${#CANONICAL_FINDINGS[@]}" -gt 0 ]; then
|
|
612
|
+
RECOVERY_URL="https://raw.githubusercontent.com/sunj-labs/bassclef/main/scripts/migrate-adopter-references.sh"
|
|
613
|
+
SIBLING_DISPATCHER_TEMPLATE="$CWD/../bassclef/presence/install/bassclef-sync.dispatcher.template.sh"
|
|
614
|
+
# Per bet 30a WU-5 (F7 cure): the migrate script reads three env vars
|
|
615
|
+
# for path overrides. Local layout sibling files for each.
|
|
616
|
+
SIBLING_RENAME_MAP="$CWD/../bassclef/standards/file-renames.json"
|
|
617
|
+
SIBLING_STATUSLINE_TEMPLATE="$CWD/../bassclef/presence/cli/bassclef-statusline.dispatcher.sh"
|
|
618
|
+
|
|
619
|
+
# Decide auto-invoke vs BLOCKED-only path (per bet 29e WU-4)
|
|
620
|
+
AUTO_INVOKE_READY=0
|
|
621
|
+
if [ "${SKIP_AUTO_INVOKE:-0}" != "1" ]; then
|
|
622
|
+
if [ -n "${BASSCLEF_RECOVERY_SCRIPT_PATH:-}" ] && [ -f "$BASSCLEF_RECOVERY_SCRIPT_PATH" ]; then
|
|
623
|
+
AUTO_INVOKE_READY=1
|
|
624
|
+
elif command -v curl >/dev/null 2>&1; then
|
|
625
|
+
AUTO_INVOKE_READY=1
|
|
672
626
|
fi
|
|
673
627
|
fi
|
|
674
|
-
fi
|
|
675
|
-
fi
|
|
676
628
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
629
|
+
if [ "$AUTO_INVOKE_READY" = "1" ]; then
|
|
630
|
+
# === Leg 3 of self-healing — auto-invocation (WU-4) ===
|
|
631
|
+
# Detection found canonical-* refs; run the migrate script
|
|
632
|
+
# automatically; continue the dispatcher chain afterward so the
|
|
633
|
+
# adopter sees one clean session-start (no second paste needed).
|
|
634
|
+
echo ""
|
|
635
|
+
echo "🩹 bassclef self-heal — canonical-* references detected; running recovery automatically"
|
|
636
|
+
for finding in "${CANONICAL_FINDINGS[@]}"; do
|
|
637
|
+
echo " - $finding"
|
|
638
|
+
done
|
|
681
639
|
|
|
682
|
-
#
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
640
|
+
# Source the migrate script (env-var override for tests, curl in prod).
|
|
641
|
+
# Per bet 30a WU-5 (F7 cure): pass all three env vars the migrate
|
|
642
|
+
# script reads — DISPATCHER_TEMPLATE + RENAME_MAP + STATUSLINE_TEMPLATE
|
|
643
|
+
# — when their sibling files exist. Without these, the script's
|
|
644
|
+
# fallback expansion `$(cd "$(dirname "$0")/..")` resolves wrong
|
|
645
|
+
# under curl-pipe ($0 = bash), and Surfaces 5 + 6 silently no-op.
|
|
646
|
+
# POA was broken for 8+ days because only the dispatcher template
|
|
647
|
+
# env var was passed.
|
|
648
|
+
ENV_ARGS=()
|
|
649
|
+
[ -f "$SIBLING_DISPATCHER_TEMPLATE" ] && ENV_ARGS+=("BASSCLEF_DISPATCHER_TEMPLATE=$SIBLING_DISPATCHER_TEMPLATE")
|
|
650
|
+
[ -f "$SIBLING_RENAME_MAP" ] && ENV_ARGS+=("BASSCLEF_RENAME_MAP=$SIBLING_RENAME_MAP")
|
|
651
|
+
[ -f "$SIBLING_STATUSLINE_TEMPLATE" ] && ENV_ARGS+=("BASSCLEF_STATUSLINE_TEMPLATE=$SIBLING_STATUSLINE_TEMPLATE")
|
|
652
|
+
|
|
653
|
+
# Per bet 2026-08-27 step 7 PR #1 — resolver picks source with priority
|
|
654
|
+
# env override → sibling → curl+checksum. Verifier gates curl path.
|
|
655
|
+
RESOLVED=$(resolve_recovery_script)
|
|
656
|
+
RESOLVED_KIND="${RESOLVED%%:*}"
|
|
657
|
+
RESOLVED_REST="${RESOLVED#*:}"
|
|
658
|
+
|
|
659
|
+
case "$RESOLVED_KIND" in
|
|
660
|
+
LOCAL|SIBLING)
|
|
661
|
+
echo "🩹 recovery source: $RESOLVED_KIND ($RESOLVED_REST)" >&2
|
|
662
|
+
if [ "${#ENV_ARGS[@]}" -gt 0 ]; then
|
|
663
|
+
cat "$RESOLVED_REST" | env "${ENV_ARGS[@]}" bash -s -- --apply "$CWD" || true
|
|
664
|
+
else
|
|
665
|
+
cat "$RESOLVED_REST" | bash -s -- --apply "$CWD" || true
|
|
666
|
+
fi
|
|
667
|
+
;;
|
|
668
|
+
CURL)
|
|
669
|
+
RECOVERY_URL_RESOLVED="${RESOLVED_REST%|*}"
|
|
670
|
+
SHA_URL="${RESOLVED_REST#*|}"
|
|
671
|
+
echo "🩹 recovery source: CURL ($RECOVERY_URL_RESOLVED)" >&2
|
|
672
|
+
TMP_SCRIPT=$(mktemp -t bassclef-recovery.XXXXXX.sh) || {
|
|
673
|
+
echo "⚠ bassclef self-heal: mktemp failed; skipping recovery" >&2
|
|
674
|
+
true
|
|
675
|
+
}
|
|
676
|
+
TMP_SHA=$(mktemp -t bassclef-recovery.XXXXXX.sha) || {
|
|
677
|
+
rm -f "$TMP_SCRIPT" 2>/dev/null
|
|
678
|
+
echo "⚠ bassclef self-heal: mktemp failed; skipping recovery" >&2
|
|
679
|
+
true
|
|
680
|
+
}
|
|
681
|
+
# Fetch both files; abort verify path if either fetch fails
|
|
682
|
+
if curl -sL --fail "$RECOVERY_URL_RESOLVED" -o "$TMP_SCRIPT" 2>/dev/null \
|
|
683
|
+
&& curl -sL --fail "$SHA_URL" -o "$TMP_SHA" 2>/dev/null; then
|
|
684
|
+
EXPECTED_SHA=$(awk '{print $1}' "$TMP_SHA")
|
|
685
|
+
if verify_checksum "$TMP_SCRIPT" "$EXPECTED_SHA"; then
|
|
686
|
+
echo "🩹 recovery: checksum verified; executing" >&2
|
|
687
|
+
if [ "${#ENV_ARGS[@]}" -gt 0 ]; then
|
|
688
|
+
cat "$TMP_SCRIPT" | env "${ENV_ARGS[@]}" bash -s -- --apply "$CWD" || true
|
|
689
|
+
else
|
|
690
|
+
cat "$TMP_SCRIPT" | bash -s -- --apply "$CWD" || true
|
|
691
|
+
fi
|
|
692
|
+
else
|
|
693
|
+
echo "🛑 recovery: SHA-256 mismatch; refusing to execute" >&2
|
|
694
|
+
echo " URL: $RECOVERY_URL_RESOLVED" >&2
|
|
695
|
+
echo " To retry manually after investigating:" >&2
|
|
696
|
+
echo " SKIP_CANONICAL_DETECTION=1 <re-run session>" >&2
|
|
697
|
+
fi
|
|
698
|
+
else
|
|
699
|
+
echo "⚠ bassclef self-heal: fetch failed for recovery or checksum" >&2
|
|
700
|
+
echo " Falls back to manual recovery via BLOCKED path below" >&2
|
|
701
|
+
fi
|
|
702
|
+
rm -f "$TMP_SCRIPT" "$TMP_SHA" 2>/dev/null
|
|
703
|
+
;;
|
|
704
|
+
*)
|
|
705
|
+
echo "⚠ bassclef self-heal: resolver returned unknown kind ($RESOLVED_KIND); skipping" >&2
|
|
706
|
+
;;
|
|
707
|
+
esac
|
|
708
|
+
|
|
709
|
+
echo "🩹 bassclef self-heal: recovery applied; continuing"
|
|
710
|
+
echo ""
|
|
711
|
+
# Fall through to Path 1 / Path 2 — substrate now loadable
|
|
712
|
+
else
|
|
713
|
+
# === Fall back: BLOCKED + exit (WU-2 behavior) ===
|
|
714
|
+
# SKIP_AUTO_INVOKE=1 set, OR neither curl nor BASSCLEF_RECOVERY_SCRIPT_PATH
|
|
715
|
+
# available. Surface the recovery shape and stop here.
|
|
716
|
+
cat <<DETECT
|
|
707
717
|
|
|
708
|
-
|
|
709
|
-
echo "### USER-LEVEL SUBSTRATE CLEANUP (per ADR-033 + bassclef#278)"
|
|
710
|
-
echo ""
|
|
711
|
-
echo "Parked $ORPHAN_COUNT user-level entries (skills + agents + hooks; symlinks AND cp'd files)"
|
|
712
|
-
echo "whose basenames duplicated project-level entries. Backup dirs:"
|
|
713
|
-
echo " ~/.claude/skills.orphans.$(date +%Y%m%d)/"
|
|
714
|
-
echo " ~/.claude/agents.orphans.$(date +%Y%m%d)/"
|
|
715
|
-
echo " ~/.claude/hooks.orphans.$(date +%Y%m%d)/"
|
|
716
|
-
echo ""
|
|
717
|
-
echo "Project-level entries now load without dedupe conflict."
|
|
718
|
-
echo "Backups survive 30 days minimum; restore if a script you wrote"
|
|
719
|
-
echo "depends on the user-level location."
|
|
720
|
-
echo ""
|
|
721
|
-
fi
|
|
718
|
+
🛑 BLOCKED: bassclef substrate — canonical-* references detected
|
|
722
719
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
echo ""
|
|
720
|
+
Your adopter tree carries references from before the 2026-06-20
|
|
721
|
+
canonical→bassclef rename. Bassclef cannot load substrate cleanly
|
|
722
|
+
until these are repaired.
|
|
727
723
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
724
|
+
Found:
|
|
725
|
+
DETECT
|
|
726
|
+
for finding in "${CANONICAL_FINDINGS[@]}"; do
|
|
727
|
+
echo " - $finding"
|
|
728
|
+
done
|
|
729
|
+
cat <<RECOVERY
|
|
733
730
|
|
|
734
|
-
|
|
735
|
-
if [ -d "$CWD/.claude/worktrees" ]; then
|
|
736
|
-
rm -rf "$CWD/.claude/worktrees" 2>/dev/null || true
|
|
737
|
-
fi
|
|
731
|
+
Recovery — one paste, no extra setup:
|
|
738
732
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
733
|
+
curl -sL $RECOVERY_URL | bash -s -- --apply "$CWD"
|
|
734
|
+
|
|
735
|
+
After it runs, open a fresh Claude Code session in this repo.
|
|
736
|
+
|
|
737
|
+
To skip this check once (override): SKIP_CANONICAL_DETECTION=1
|
|
738
|
+
To skip auto-invocation (manual recovery): SKIP_AUTO_INVOKE=1
|
|
739
|
+
|
|
740
|
+
Refs: scripts/migrate-adopter-references.sh, .claude/rules/we-dont-break-adopters.md
|
|
741
|
+
|
|
742
|
+
RECOVERY
|
|
743
|
+
exit 0
|
|
744
|
+
fi
|
|
750
745
|
fi
|
|
751
746
|
fi
|
|
752
747
|
|
|
753
|
-
# ===
|
|
754
|
-
|
|
748
|
+
# === Path 1. Sibling fast-path ===
|
|
749
|
+
SIBLING_TEMPLATE="$CWD/../bassclef/presence/install/bassclef-sync.template.sh"
|
|
750
|
+
if [ -f "$SIBLING_TEMPLATE" ]; then
|
|
751
|
+
echo "$INPUT" | bash "$SIBLING_TEMPLATE"
|
|
752
|
+
exit $?
|
|
753
|
+
fi
|
|
755
754
|
|
|
756
|
-
#
|
|
757
|
-
#
|
|
758
|
-
#
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
755
|
+
# === Path 1.5. Bundled-substrate recognition (per #1849) ===
|
|
756
|
+
# npm-only cold adopters carry substrate in the package bundle. The
|
|
757
|
+
# dispatcher was designed for peer-checkout OR source-config models; both
|
|
758
|
+
# fail for anonymous adopters against private repos. Before firing the
|
|
759
|
+
# fetch-strategies path, check whether the substrate is already loaded at
|
|
760
|
+
# CWD. If yes, exit silently — no news is good news (Cooper).
|
|
761
|
+
#
|
|
762
|
+
# Override: BASSCLEF_DISPATCHER_STRICT=1 forces the Path 2 fetch even when
|
|
763
|
+
# bundled substrate is present (operator debugging).
|
|
764
|
+
if [ "${BASSCLEF_DISPATCHER_STRICT:-0}" != "1" ]; then
|
|
765
|
+
if bassclef_sync_bundled_substrate_present "$CWD"; then
|
|
766
|
+
exit 0
|
|
767
|
+
fi
|
|
762
768
|
fi
|
|
763
769
|
|
|
764
|
-
#
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
770
|
+
# === Path 2. Curl fallback ===
|
|
771
|
+
# Uses the same resolver as auto_sync pre-check so old-name adopters
|
|
772
|
+
# still get here (self-heal already fired above, but resolve for safety
|
|
773
|
+
# in case self-heal was skipped via SKIP_SELF_HEAL_POINTER).
|
|
774
|
+
SOURCE_FILE=$(bassclef_sync_resolve_pointer_file "$CWD")
|
|
775
|
+
if [ -z "$SOURCE_FILE" ] || [ ! -f "$SOURCE_FILE" ]; then
|
|
776
|
+
emit_error "couldn't find bassclef substrate" \
|
|
777
|
+
"This repo doesn't have a sibling bassclef checkout at ../bassclef AND
|
|
778
|
+
doesn't have a .bassclef-source.json file to fetch from a remote source.
|
|
779
|
+
|
|
780
|
+
The dispatcher needs ONE of those to load bassclef's substrate (skills,
|
|
781
|
+
rules, hooks, luminaries) into your session."
|
|
769
782
|
fi
|
|
770
783
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
784
|
+
SOURCE_URL=$(jq -r '.source_url // empty' "$SOURCE_FILE")
|
|
785
|
+
AUTH_METHOD=$(jq -r '.auth_method // "none"' "$SOURCE_FILE")
|
|
786
|
+
TOKEN_ENV=$(jq -r '.token_env // "BASSCLEF_TOKEN"' "$SOURCE_FILE")
|
|
787
|
+
|
|
788
|
+
if [ -z "$SOURCE_URL" ]; then
|
|
789
|
+
emit_error ".bassclef-source.json is missing source_url" \
|
|
790
|
+
"Your .bassclef-source.json file exists but doesn't specify where to fetch
|
|
791
|
+
bassclef from. The 'source_url' field is required.
|
|
792
|
+
|
|
793
|
+
See standards/bassclef-source-config.md for the schema and provider
|
|
794
|
+
examples (GitHub public release, GitHub private repo, Azure DevOps, GitLab)."
|
|
780
795
|
fi
|
|
781
796
|
|
|
782
|
-
#
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
797
|
+
# Resolve auth header from env-stored token.
|
|
798
|
+
TOKEN_VALUE=""
|
|
799
|
+
AUTH_HEADER=""
|
|
800
|
+
if [ "$AUTH_METHOD" != "none" ]; then
|
|
801
|
+
TOKEN_VALUE=$(printenv "$TOKEN_ENV" 2>/dev/null || true)
|
|
802
|
+
if [ -z "$TOKEN_VALUE" ]; then
|
|
803
|
+
emit_error "auth token not set in environment" \
|
|
804
|
+
"Your .bassclef-source.json uses auth_method '$AUTH_METHOD' and expects the
|
|
805
|
+
'$TOKEN_ENV' environment variable to hold the token. That variable isn't set
|
|
806
|
+
in this environment.
|
|
807
|
+
|
|
808
|
+
If you're in CI: the secret needs to be configured on the repo
|
|
809
|
+
(gh secret set $TOKEN_ENV -R <repo>).
|
|
810
|
+
If you're on a laptop: export $TOKEN_ENV=<token> in your shell.
|
|
811
|
+
If you're in Cloud Code: set $TOKEN_ENV in your environment config.
|
|
812
|
+
|
|
813
|
+
If you don't have a token yet, follow option 3 below to create one."
|
|
814
|
+
fi
|
|
815
|
+
case "$AUTH_METHOD" in
|
|
816
|
+
Bearer) AUTH_HEADER="Authorization: Bearer $TOKEN_VALUE" ;;
|
|
817
|
+
Basic) AUTH_HEADER="Authorization: Basic $(printf ':%s' "$TOKEN_VALUE" | base64)" ;;
|
|
818
|
+
*) AUTH_HEADER="Authorization: $AUTH_METHOD $TOKEN_VALUE" ;;
|
|
819
|
+
esac
|
|
796
820
|
fi
|
|
797
821
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
elif [ -d "$CWD/docs/linkedin-drafts" ]; then
|
|
805
|
-
# Legacy path — pre-/linkedin-→-/journal rename
|
|
806
|
-
DRAFT_DIRS="$CWD/docs/linkedin-drafts"
|
|
822
|
+
# Fetch tarball
|
|
823
|
+
TARBALL="/tmp/bassclef-thin-pointer-$$.tar.gz"
|
|
824
|
+
if [ -n "$AUTH_HEADER" ]; then
|
|
825
|
+
HTTP_CODE=$(curl -sS -L -H "$AUTH_HEADER" -w "%{http_code}" -o "$TARBALL" "$SOURCE_URL" 2>/dev/null || echo "000")
|
|
826
|
+
else
|
|
827
|
+
HTTP_CODE=$(curl -sS -L -w "%{http_code}" -o "$TARBALL" "$SOURCE_URL" 2>/dev/null || echo "000")
|
|
807
828
|
fi
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
829
|
+
|
|
830
|
+
if [ "$HTTP_CODE" != "200" ]; then
|
|
831
|
+
rm -f "$TARBALL" 2>/dev/null
|
|
832
|
+
emit_error "fetch returned HTTP $HTTP_CODE" \
|
|
833
|
+
"The dispatcher tried to download bassclef from:
|
|
834
|
+
|
|
835
|
+
$SOURCE_URL
|
|
836
|
+
|
|
837
|
+
The server returned HTTP $HTTP_CODE instead of 200. Common causes:
|
|
838
|
+
|
|
839
|
+
HTTP 401: token is invalid or expired. Regenerate it.
|
|
840
|
+
HTTP 403: token is valid but doesn't have the required permission
|
|
841
|
+
(needs Contents:read on the bassclef repo).
|
|
842
|
+
HTTP 404: source_url is wrong, OR the bassclef repo / release
|
|
843
|
+
doesn't exist at that URL.
|
|
844
|
+
HTTP 000: network couldn't reach the host. Check DNS / firewall /
|
|
845
|
+
whether you're offline."
|
|
812
846
|
fi
|
|
813
847
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
fi
|
|
823
|
-
fi
|
|
824
|
-
done
|
|
848
|
+
# Extract to a stable per-user location.
|
|
849
|
+
BASSCLEF_FRESH="$HOME/bassclef-fresh"
|
|
850
|
+
rm -rf "$BASSCLEF_FRESH" 2>/dev/null
|
|
851
|
+
mkdir -p "$BASSCLEF_FRESH"
|
|
852
|
+
if ! tar -xzf "$TARBALL" -C "$BASSCLEF_FRESH" --strip-components=1 2>/dev/null; then
|
|
853
|
+
rm -f "$TARBALL"
|
|
854
|
+
emit_error "couldn't extract the downloaded tarball" \
|
|
855
|
+
"The dispatcher downloaded the file from $SOURCE_URL but tar -xzf failed.
|
|
825
856
|
|
|
826
|
-
|
|
827
|
-
TODAY=$(date +%Y-%m-%d)
|
|
828
|
-
# Count commits since last draft
|
|
829
|
-
COMMITS_SINCE=$(git -C "$CWD" log --oneline --since="$LATEST_DRAFT" 2>/dev/null | wc -l | tr -d ' ')
|
|
857
|
+
This usually means the response wasn't a real tarball. Check:
|
|
830
858
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
859
|
+
- Is source_url correct? It should return a gzipped tarball, not HTML.
|
|
860
|
+
- Is a network proxy / SSO portal injecting a login page instead?
|
|
861
|
+
- To inspect what the server is actually sending, run:
|
|
862
|
+
|
|
863
|
+
curl -sSL -H \"Authorization: ...\" \"$SOURCE_URL\" | file -
|
|
864
|
+
|
|
865
|
+
If the output says 'HTML document' instead of 'gzip compressed data',
|
|
866
|
+
you're being intercepted."
|
|
867
|
+
fi
|
|
868
|
+
rm -f "$TARBALL"
|
|
869
|
+
|
|
870
|
+
# Exec the big template.
|
|
871
|
+
#
|
|
872
|
+
# Export the extracted tarball root so §3c-hooks (dynamic-derive hook connect,
|
|
873
|
+
# per bassclef-upstream#625) can read bassclef's own .claude/settings.json.
|
|
874
|
+
# The template sources presence/install/bassclef-hook-connect.sh and calls
|
|
875
|
+
# bassclef_hook_connect_apply "$CWD" — the function reads
|
|
876
|
+
# $BASSCLEF_SOURCE_ROOT/.claude/settings.json to learn the current hook list
|
|
877
|
+
# across every event type, then merges every entry additively into the
|
|
878
|
+
# adopter's settings.json.
|
|
879
|
+
export BASSCLEF_SOURCE_ROOT="$BASSCLEF_FRESH"
|
|
880
|
+
|
|
881
|
+
FALLBACK_TEMPLATE="$BASSCLEF_FRESH/presence/install/bassclef-sync.template.sh"
|
|
882
|
+
if [ ! -f "$FALLBACK_TEMPLATE" ]; then
|
|
883
|
+
emit_error "tarball is missing bassclef-sync.template.sh" \
|
|
884
|
+
"The dispatcher downloaded and extracted the tarball, but it doesn't
|
|
885
|
+
contain presence/install/bassclef-sync.template.sh — bassclef's
|
|
886
|
+
main sync logic.
|
|
887
|
+
|
|
888
|
+
This usually means source_url points at a tarball that isn't bassclef
|
|
889
|
+
(e.g., a different repo, or a branch / tag that's missing the template
|
|
890
|
+
file). Verify the URL in .bassclef-source.json points at bassclef's
|
|
891
|
+
main branch or a release that includes strategy/templates/."
|
|
847
892
|
fi
|
|
848
893
|
|
|
849
|
-
|
|
894
|
+
echo "$INPUT" | bash "$FALLBACK_TEMPLATE"
|
|
895
|
+
exit $?
|