@thebassclef/lite 1.9.8 → 1.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/git-stash-u-guard.sh +4 -1
- package/dist/lite/.claude/hooks/post-skill-friction-check.sh +6 -1
- package/dist/lite/.claude/hooks/pre-commit-gate.sh +49 -6
- package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +68 -8
- package/dist/lite/.claude/hooks/pre-commit-manifest-autoregen.sh +4 -2
- package/dist/lite/.claude/hooks/save-state.sh +24 -0
- package/dist/lite/.claude/hooks/session-reflection.d/83-install-written-paths-rebuild.sh +111 -0
- package/dist/lite/.claude/hooks/state-validate.sh +8 -0
- package/dist/lite/.claude/hooks/trace-helper.sh +29 -0
- package/dist/lite/.claude/rules/install-written-paths-mediation.md +74 -0
- package/dist/lite/.claude/rules/lite-manifest-schema-change-discipline.md +30 -0
- package/dist/lite/.claude/skills/autonomous/SKILL.md +53 -7
- package/dist/lite/.claude/skills/build/SKILL.md +53 -11
- package/dist/lite/.claude/skills/deploy-prod/SKILL.md +10 -0
- package/dist/lite/.claude/skills/launch/SKILL.md +26 -0
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +59 -18
- package/dist/lite/.claude/skills/personas/SKILL.md +1 -1
- package/dist/lite/.claude/skills/release-notes/SKILL.md +24 -0
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/gitignore +8 -0
- package/dist/lite/lib/capability-probe.sh +39 -1
- package/dist/lite/lib/composer-preflight.sh +8 -4
- package/dist/lite/lib/deferred-actions.sh +4 -4
- package/dist/lite/lib/fixture-builder.sh +9 -9
- package/dist/lite/lib/identifier-leak-check.sh +85 -0
- package/dist/lite/lib/install-written-paths.sh +369 -0
- package/dist/lite/lib/luminary-pick.sh +35 -13
- package/dist/lite/lib/preview-state.sh +20 -20
- package/dist/lite/lib/skip-env-inline-check.sh +61 -0
- package/dist/lite/lib/state.sh +102 -39
- package/dist/lite/lib/tier-check.sh +9 -9
- package/dist/lite/lib/tier-dependency-audit.sh +23 -23
- package/dist/lite/presence/install/schedule-auto-save.taskscheduler.md +1 -1
- package/dist/lite/standards/lite-manifest-schema-changes.md +83 -0
- package/dist/lite/standards/lite-manifest.json +70 -47
- package/dist/lite/standards/state-spine.md +1 -0
- package/dist/lite/templates/spec-template.md +10 -6
- package/package.json +1 -1
- package/dist/lite/.claude/hooks/longrun-prep-compounding-axis-check.sh +0 -492
- package/dist/lite/.claude/rules/compounding-axis-fresh-analysis.md +0 -173
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# Install-written-paths manifest + accessor lib.
|
|
4
|
+
#
|
|
5
|
+
# Records paths written by bassclef install writers so discipline hooks
|
|
6
|
+
# can skip them. Per bassclef-upstream#2036 Finding #8 structural cure
|
|
7
|
+
# (Kunal Slot 1). See docs/decompositions/2026-10-03-install-written-paths-manifest.md
|
|
8
|
+
# for design + patterns applied (Repository + Deep Module + Null Object + Guard Clause).
|
|
9
|
+
#
|
|
10
|
+
# @pattern patterns/code/fowler/repository.md
|
|
11
|
+
# @pattern patterns/code/ousterhout/deep-module.md
|
|
12
|
+
#
|
|
13
|
+
# Public API (5 verbs):
|
|
14
|
+
# install_written_paths_register <path> <writer>
|
|
15
|
+
# is_install_written_path <path>
|
|
16
|
+
# install_written_paths_rename <old-path> <new-path>
|
|
17
|
+
# install_written_paths_rebuild
|
|
18
|
+
# install_written_paths_verify
|
|
19
|
+
#
|
|
20
|
+
# Override: SKIP_INSTALL_WRITTEN_PATHS=1 — is_install_written_path returns 1 (false)
|
|
21
|
+
# so hooks fire discipline normally (don't skip).
|
|
22
|
+
#
|
|
23
|
+
# Variable naming: internal locals use `tgt` (target path), `arg_*` prefix,
|
|
24
|
+
# or `_iwp_*` namespace to avoid shadowing shell builtins. Reserved names
|
|
25
|
+
# like `PATH`, `CDPATH`, `FPATH`, `MANPATH` are not used as locals per
|
|
26
|
+
# shell-portability discipline (zsh treats lowercase `path` as array-tied
|
|
27
|
+
# to PATH; renaming avoids the kill).
|
|
28
|
+
|
|
29
|
+
set -u
|
|
30
|
+
|
|
31
|
+
# ---- Constants ----
|
|
32
|
+
|
|
33
|
+
_IWP_SCHEMA_VERSION="1.0.0"
|
|
34
|
+
|
|
35
|
+
_IWP_WRITERS="bassclef-hook-connect bassclef-sync-template bassclef-sync-dispatcher schedule-auto-save bassclef-init-npm"
|
|
36
|
+
|
|
37
|
+
# ---- Internal helpers ----
|
|
38
|
+
|
|
39
|
+
_iwp_manifest_path() {
|
|
40
|
+
local root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
41
|
+
echo "$root/state/install-written-paths.json"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_iwp_log() {
|
|
45
|
+
if command -v trace_log >/dev/null 2>&1; then
|
|
46
|
+
trace_log "install-written-paths" "$*"
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_iwp_sha256() {
|
|
51
|
+
local file="$1"
|
|
52
|
+
if [ ! -f "$file" ]; then
|
|
53
|
+
echo ""
|
|
54
|
+
return 1
|
|
55
|
+
fi
|
|
56
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
57
|
+
sha256sum "$file" 2>/dev/null | awk '{print "sha256:" $1}'
|
|
58
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
59
|
+
shasum -a 256 "$file" 2>/dev/null | awk '{print "sha256:" $1}'
|
|
60
|
+
else
|
|
61
|
+
echo ""
|
|
62
|
+
return 1
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_iwp_valid_writer() {
|
|
67
|
+
local w="$1"
|
|
68
|
+
for valid in $_IWP_WRITERS; do
|
|
69
|
+
[ "$w" = "$valid" ] && return 0
|
|
70
|
+
done
|
|
71
|
+
return 1
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_iwp_validate_path() {
|
|
75
|
+
local p="$1"
|
|
76
|
+
# Reject path traversal (any component equal to .. OR path exactly ..)
|
|
77
|
+
case "$p" in
|
|
78
|
+
*../*|..) return 1 ;;
|
|
79
|
+
esac
|
|
80
|
+
# Reject absolute paths outside $CLAUDE_PROJECT_DIR
|
|
81
|
+
case "$p" in
|
|
82
|
+
/*)
|
|
83
|
+
local root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
84
|
+
case "$p" in
|
|
85
|
+
"$root"/*) ;;
|
|
86
|
+
*) return 1 ;;
|
|
87
|
+
esac
|
|
88
|
+
;;
|
|
89
|
+
esac
|
|
90
|
+
return 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_iwp_ensure_manifest() {
|
|
94
|
+
local mpath
|
|
95
|
+
mpath=$(_iwp_manifest_path)
|
|
96
|
+
if [ ! -f "$mpath" ]; then
|
|
97
|
+
mkdir -p "$(dirname "$mpath")"
|
|
98
|
+
cat > "$mpath" <<EOF
|
|
99
|
+
{
|
|
100
|
+
"schema_version": "$_IWP_SCHEMA_VERSION",
|
|
101
|
+
"entries": []
|
|
102
|
+
}
|
|
103
|
+
EOF
|
|
104
|
+
fi
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_iwp_manifest_valid() {
|
|
108
|
+
local mpath
|
|
109
|
+
mpath=$(_iwp_manifest_path)
|
|
110
|
+
[ -f "$mpath" ] || return 1
|
|
111
|
+
jq -e . "$mpath" >/dev/null 2>&1 || return 1
|
|
112
|
+
return 0
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# ---- Public API ----
|
|
116
|
+
|
|
117
|
+
# install_written_paths_register <path> <writer>
|
|
118
|
+
# Records a path as install-written. Idempotent on path.
|
|
119
|
+
# Returns 0 on success, 1 on validation failure.
|
|
120
|
+
install_written_paths_register() {
|
|
121
|
+
local tgt="${1:-}"
|
|
122
|
+
local writer="${2:-}"
|
|
123
|
+
|
|
124
|
+
if [ -z "$tgt" ] || [ -z "$writer" ]; then
|
|
125
|
+
echo "install-written-paths: register requires <path> <writer>" >&2
|
|
126
|
+
return 1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
if ! _iwp_validate_path "$tgt"; then
|
|
130
|
+
echo "install-written-paths: path '$tgt' rejected (traversal or outside project)" >&2
|
|
131
|
+
_iwp_log "register-rejected path=$tgt reason=traversal"
|
|
132
|
+
return 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if ! _iwp_valid_writer "$writer"; then
|
|
136
|
+
echo "install-written-paths: writer '$writer' not in allowed enum" >&2
|
|
137
|
+
_iwp_log "register-rejected path=$tgt writer=$writer reason=unknown-writer"
|
|
138
|
+
return 1
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
142
|
+
echo "install-written-paths: jq not available; cannot register" >&2
|
|
143
|
+
return 1
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
_iwp_ensure_manifest
|
|
147
|
+
local mpath
|
|
148
|
+
mpath=$(_iwp_manifest_path)
|
|
149
|
+
|
|
150
|
+
local root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
151
|
+
local full_tgt
|
|
152
|
+
case "$tgt" in
|
|
153
|
+
/*) full_tgt="$tgt" ;;
|
|
154
|
+
*) full_tgt="$root/$tgt" ;;
|
|
155
|
+
esac
|
|
156
|
+
|
|
157
|
+
local hash
|
|
158
|
+
hash=$(_iwp_sha256 "$full_tgt")
|
|
159
|
+
if [ -z "$hash" ]; then
|
|
160
|
+
echo "install-written-paths: unable to compute hash for '$full_tgt'" >&2
|
|
161
|
+
return 1
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
local now
|
|
165
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
166
|
+
|
|
167
|
+
# Use flock to serialize writes; fall back to no-lock on BSD without flock
|
|
168
|
+
local tmp
|
|
169
|
+
tmp=$(mktemp "$mpath.tmp.XXXXXX")
|
|
170
|
+
|
|
171
|
+
if command -v flock >/dev/null 2>&1; then
|
|
172
|
+
(
|
|
173
|
+
flock -x 9
|
|
174
|
+
jq --arg path "$tgt" --arg writer "$writer" --arg ts "$now" --arg hash "$hash" \
|
|
175
|
+
'.entries |= (map(select(.path != $path)) + [{"path": $path, "writer": $writer, "written_at": $ts, "content_hash": $hash}])' \
|
|
176
|
+
"$mpath" > "$tmp" && mv "$tmp" "$mpath"
|
|
177
|
+
) 9>"$mpath.lock"
|
|
178
|
+
else
|
|
179
|
+
jq --arg path "$tgt" --arg writer "$writer" --arg ts "$now" --arg hash "$hash" \
|
|
180
|
+
'.entries |= (map(select(.path != $path)) + [{"path": $path, "writer": $writer, "written_at": $ts, "content_hash": $hash}])' \
|
|
181
|
+
"$mpath" > "$tmp" && mv "$tmp" "$mpath"
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
rm -f "$tmp" 2>/dev/null || true
|
|
185
|
+
_iwp_log "register path=$tgt writer=$writer"
|
|
186
|
+
return 0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# is_install_written_path <path>
|
|
190
|
+
# Returns 0 (true) when path in manifest AND content hash matches.
|
|
191
|
+
# Returns 1 (false) otherwise.
|
|
192
|
+
# Override: SKIP_INSTALL_WRITTEN_PATHS=1 forces return 1 (hooks fire normally).
|
|
193
|
+
is_install_written_path() {
|
|
194
|
+
if [ -n "${SKIP_INSTALL_WRITTEN_PATHS:-}" ]; then
|
|
195
|
+
_iwp_log "check override SKIP_INSTALL_WRITTEN_PATHS=1 path=${1:-}"
|
|
196
|
+
return 1
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
local tgt="${1:-}"
|
|
200
|
+
if [ -z "$tgt" ]; then
|
|
201
|
+
return 1
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
local mpath
|
|
205
|
+
mpath=$(_iwp_manifest_path)
|
|
206
|
+
|
|
207
|
+
# Fail-soft: manifest missing or malformed → not install-written
|
|
208
|
+
if ! _iwp_manifest_valid; then
|
|
209
|
+
_iwp_log "check path=$tgt result=false reason=no-manifest-or-malformed"
|
|
210
|
+
[ -f "$mpath" ] && echo "install-written-paths: manifest malformed; treating as not-install-written" >&2
|
|
211
|
+
return 1
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
215
|
+
_iwp_log "check path=$tgt result=false reason=no-jq"
|
|
216
|
+
return 1
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
# Look up entry
|
|
220
|
+
local recorded_hash
|
|
221
|
+
recorded_hash=$(jq -r --arg path "$tgt" '.entries[] | select(.path == $path) | .content_hash' "$mpath" 2>/dev/null)
|
|
222
|
+
if [ -z "$recorded_hash" ] || [ "$recorded_hash" = "null" ]; then
|
|
223
|
+
_iwp_log "check path=$tgt result=false reason=not-in-manifest"
|
|
224
|
+
return 1
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
# Compute current hash
|
|
228
|
+
local root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
229
|
+
local full_tgt
|
|
230
|
+
case "$tgt" in
|
|
231
|
+
/*) full_tgt="$tgt" ;;
|
|
232
|
+
*) full_tgt="$root/$tgt" ;;
|
|
233
|
+
esac
|
|
234
|
+
|
|
235
|
+
if [ ! -f "$full_tgt" ]; then
|
|
236
|
+
_iwp_log "check path=$tgt result=false reason=file-missing"
|
|
237
|
+
return 1
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
local current_hash
|
|
241
|
+
current_hash=$(_iwp_sha256 "$full_tgt")
|
|
242
|
+
if [ -z "$current_hash" ]; then
|
|
243
|
+
_iwp_log "check path=$tgt result=false reason=hash-fail"
|
|
244
|
+
return 1
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
if [ "$recorded_hash" = "$current_hash" ]; then
|
|
248
|
+
_iwp_log "check path=$tgt result=true reason=match"
|
|
249
|
+
return 0
|
|
250
|
+
else
|
|
251
|
+
_iwp_log "check path=$tgt result=false reason=hash-mismatch"
|
|
252
|
+
return 1
|
|
253
|
+
fi
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# install_written_paths_rename <old-path> <new-path>
|
|
257
|
+
# Moves entry; preserves writer; bumps written_at.
|
|
258
|
+
install_written_paths_rename() {
|
|
259
|
+
local old="${1:-}"
|
|
260
|
+
local new="${2:-}"
|
|
261
|
+
|
|
262
|
+
if [ -z "$old" ] || [ -z "$new" ]; then
|
|
263
|
+
echo "install-written-paths: rename requires <old-path> <new-path>" >&2
|
|
264
|
+
return 1
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
if ! _iwp_validate_path "$new"; then
|
|
268
|
+
echo "install-written-paths: new path '$new' rejected" >&2
|
|
269
|
+
return 1
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
if ! _iwp_manifest_valid; then
|
|
273
|
+
echo "install-written-paths: manifest missing or malformed" >&2
|
|
274
|
+
return 1
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
278
|
+
echo "install-written-paths: jq not available" >&2
|
|
279
|
+
return 1
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
local mpath
|
|
283
|
+
mpath=$(_iwp_manifest_path)
|
|
284
|
+
|
|
285
|
+
# Check old exists
|
|
286
|
+
local writer
|
|
287
|
+
writer=$(jq -r --arg path "$old" '.entries[] | select(.path == $path) | .writer' "$mpath" 2>/dev/null)
|
|
288
|
+
if [ -z "$writer" ] || [ "$writer" = "null" ]; then
|
|
289
|
+
echo "install-written-paths: '$old' not in manifest" >&2
|
|
290
|
+
return 1
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
local now
|
|
294
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
295
|
+
|
|
296
|
+
# Compute hash from new location if it exists; else keep old hash
|
|
297
|
+
local root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
298
|
+
local full_new
|
|
299
|
+
case "$new" in
|
|
300
|
+
/*) full_new="$new" ;;
|
|
301
|
+
*) full_new="$root/$new" ;;
|
|
302
|
+
esac
|
|
303
|
+
|
|
304
|
+
local new_hash
|
|
305
|
+
if [ -f "$full_new" ]; then
|
|
306
|
+
new_hash=$(_iwp_sha256 "$full_new")
|
|
307
|
+
else
|
|
308
|
+
new_hash=$(jq -r --arg path "$old" '.entries[] | select(.path == $path) | .content_hash' "$mpath")
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
local tmp
|
|
312
|
+
tmp=$(mktemp "$mpath.tmp.XXXXXX")
|
|
313
|
+
|
|
314
|
+
jq --arg old "$old" --arg new "$new" --arg writer "$writer" --arg ts "$now" --arg hash "$new_hash" \
|
|
315
|
+
'.entries |= (map(select(.path != $old)) + [{"path": $new, "writer": $writer, "written_at": $ts, "content_hash": $hash}])' \
|
|
316
|
+
"$mpath" > "$tmp" && mv "$tmp" "$mpath"
|
|
317
|
+
|
|
318
|
+
rm -f "$tmp" 2>/dev/null || true
|
|
319
|
+
_iwp_log "rename old=$old new=$new"
|
|
320
|
+
return 0
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# install_written_paths_rebuild
|
|
324
|
+
# Regenerates the manifest. V1: creates empty manifest when missing.
|
|
325
|
+
# Future: walks install writers' known output locations.
|
|
326
|
+
install_written_paths_rebuild() {
|
|
327
|
+
local mpath
|
|
328
|
+
mpath=$(_iwp_manifest_path)
|
|
329
|
+
if [ -f "$mpath" ] && _iwp_manifest_valid; then
|
|
330
|
+
return 0
|
|
331
|
+
fi
|
|
332
|
+
rm -f "$mpath" 2>/dev/null || true
|
|
333
|
+
_iwp_ensure_manifest
|
|
334
|
+
_iwp_log "rebuild manifest=$mpath"
|
|
335
|
+
return 0
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# install_written_paths_verify
|
|
339
|
+
# Returns 0 on well-formed manifest, 1 on schema violation.
|
|
340
|
+
install_written_paths_verify() {
|
|
341
|
+
local mpath
|
|
342
|
+
mpath=$(_iwp_manifest_path)
|
|
343
|
+
|
|
344
|
+
if [ ! -f "$mpath" ]; then
|
|
345
|
+
echo "install-written-paths: manifest file missing at $mpath" >&2
|
|
346
|
+
return 1
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
350
|
+
echo "install-written-paths: jq not available" >&2
|
|
351
|
+
return 1
|
|
352
|
+
fi
|
|
353
|
+
|
|
354
|
+
if ! jq -e . "$mpath" >/dev/null 2>&1; then
|
|
355
|
+
echo "install-written-paths: manifest JSON malformed" >&2
|
|
356
|
+
return 1
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
# Basic shape check — required top-level keys present
|
|
360
|
+
local has_version has_entries
|
|
361
|
+
has_version=$(jq -r 'has("schema_version")' "$mpath" 2>/dev/null)
|
|
362
|
+
has_entries=$(jq -r 'has("entries")' "$mpath" 2>/dev/null)
|
|
363
|
+
if [ "$has_version" != "true" ] || [ "$has_entries" != "true" ]; then
|
|
364
|
+
echo "install-written-paths: manifest missing required keys" >&2
|
|
365
|
+
return 1
|
|
366
|
+
fi
|
|
367
|
+
|
|
368
|
+
return 0
|
|
369
|
+
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
#
|
|
26
26
|
# Usage from a caller SKILL body:
|
|
27
27
|
#
|
|
28
|
-
# source "${BASSCLEF_DIR:-$
|
|
28
|
+
# source "${BASSCLEF_DIR:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}/lib/luminary-pick.sh"
|
|
29
29
|
# CATALOG=$(luminary_pick_catalog)
|
|
30
30
|
# # then Claude reasons over $CATALOG + intent, writes JSON in-turn
|
|
31
31
|
#
|
|
@@ -44,22 +44,44 @@ set -euo pipefail
|
|
|
44
44
|
# Sort: alphabetical by slug.
|
|
45
45
|
# Return: 0 on success (including empty catalog); non-zero if dir missing.
|
|
46
46
|
luminary_pick_catalog() {
|
|
47
|
-
|
|
47
|
+
# Resolve basedir: BASSCLEF_DIR → CLAUDE_PROJECT_DIR → git root → pwd.
|
|
48
|
+
# cli#313 — the former fallback $HOME/src/sunj-labs/bassclef was the
|
|
49
|
+
# engineering team's own home path and failed for every adopter.
|
|
50
|
+
local basedir="${BASSCLEF_DIR:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}"
|
|
48
51
|
local catalog_dir="${basedir}/state/luminary-implementations"
|
|
52
|
+
local lite_catalog="${basedir}/.claude/luminaries"
|
|
49
53
|
|
|
50
|
-
if [
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
if [ -d "$catalog_dir" ]; then
|
|
55
|
+
(
|
|
56
|
+
shopt -s nullglob
|
|
57
|
+
set +e
|
|
58
|
+
for f in "$catalog_dir"/*.json; do
|
|
59
|
+
jq -r '"\(.luminary_slug) (\(.primary_domain // "unknown")) — \(.name // .luminary_slug)"' "$f" 2>/dev/null || true
|
|
60
|
+
done | sort -u
|
|
61
|
+
)
|
|
62
|
+
return 0
|
|
53
63
|
fi
|
|
54
64
|
|
|
55
|
-
(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
# Lite-tier fallback (cli#313): read frontmatter from .claude/luminaries/*.md.
|
|
66
|
+
# Every tier ships this directory; state/luminary-implementations/ is upstream-only.
|
|
67
|
+
if [ -d "$lite_catalog" ]; then
|
|
68
|
+
(
|
|
69
|
+
shopt -s nullglob
|
|
70
|
+
set +e
|
|
71
|
+
for f in "$lite_catalog"/*.md; do
|
|
72
|
+
local slug domain name
|
|
73
|
+
slug=$(awk '/^slug:[[:space:]]/{sub(/^slug:[[:space:]]*/,""); print; exit}' "$f")
|
|
74
|
+
domain=$(awk '/^primary_domain:[[:space:]]/{sub(/^primary_domain:[[:space:]]*/,""); print; exit}' "$f")
|
|
75
|
+
name=$(awk '/^name:[[:space:]]/{sub(/^name:[[:space:]]*/,""); print; exit}' "$f")
|
|
76
|
+
[ -z "$slug" ] && continue
|
|
77
|
+
echo "${slug} (${domain:-unknown}) — ${name:-$slug}"
|
|
78
|
+
done | sort -u
|
|
79
|
+
)
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
echo "[luminary-pick-catalog] catalog dir missing: $catalog_dir (and no .claude/luminaries/ fallback at $lite_catalog)" >&2
|
|
84
|
+
return 2
|
|
63
85
|
}
|
|
64
86
|
|
|
65
87
|
# Grace-window stub preserving the old function signature.
|
|
@@ -49,14 +49,14 @@ ps_exists() {
|
|
|
49
49
|
|
|
50
50
|
ps_create() {
|
|
51
51
|
local slug="$1"
|
|
52
|
-
local
|
|
53
|
-
|
|
54
|
-
mkdir -p "$(dirname "$
|
|
55
|
-
if [[ -f "$
|
|
56
|
-
echo "ps_create: already exists: $
|
|
52
|
+
local p
|
|
53
|
+
p="$(ps_path "$slug")"
|
|
54
|
+
mkdir -p "$(dirname "$p")"
|
|
55
|
+
if [[ -f "$p" ]]; then
|
|
56
|
+
echo "ps_create: already exists: $p" >&2
|
|
57
57
|
return 1
|
|
58
58
|
fi
|
|
59
|
-
cat > "$
|
|
59
|
+
cat > "$p" <<EOF
|
|
60
60
|
slug: ${slug}
|
|
61
61
|
created: $(date -u +'%Y-%m-%dT%H:%M:%S+0000')
|
|
62
62
|
current_tier: pending
|
|
@@ -73,16 +73,16 @@ EOF
|
|
|
73
73
|
ps_get() {
|
|
74
74
|
local slug="$1"
|
|
75
75
|
local field="$2"
|
|
76
|
-
local
|
|
77
|
-
|
|
78
|
-
[[ -f "$
|
|
76
|
+
local p
|
|
77
|
+
p="$(ps_path "$slug")"
|
|
78
|
+
[[ -f "$p" ]] || { echo "ps_get: not-found: $p" >&2; return 1; }
|
|
79
79
|
awk -v f="$field" '
|
|
80
80
|
$0 ~ "^" f ":" {
|
|
81
81
|
sub("^" f ":[[:space:]]*", "")
|
|
82
82
|
print
|
|
83
83
|
exit
|
|
84
84
|
}
|
|
85
|
-
' "$
|
|
85
|
+
' "$p"
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
ps_set() {
|
|
@@ -90,9 +90,9 @@ ps_set() {
|
|
|
90
90
|
local field="$2"
|
|
91
91
|
shift 2
|
|
92
92
|
local value="$*"
|
|
93
|
-
local
|
|
94
|
-
|
|
95
|
-
[[ -f "$
|
|
93
|
+
local p
|
|
94
|
+
p="$(ps_path "$slug")"
|
|
95
|
+
[[ -f "$p" ]] || { echo "ps_set: not-found: $p" >&2; return 1; }
|
|
96
96
|
local tmp
|
|
97
97
|
tmp="$(mktemp)"
|
|
98
98
|
awk -v f="$field" -v v="$value" '
|
|
@@ -108,8 +108,8 @@ ps_set() {
|
|
|
108
108
|
print f ": " v
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
' "$
|
|
112
|
-
mv "$tmp" "$
|
|
111
|
+
' "$p" > "$tmp"
|
|
112
|
+
mv "$tmp" "$p"
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
ps_status() {
|
|
@@ -146,9 +146,9 @@ ps_archive() {
|
|
|
146
146
|
ps_reset() {
|
|
147
147
|
local slug="$1"
|
|
148
148
|
local reason="${2:-no reason given}"
|
|
149
|
-
local
|
|
150
|
-
|
|
151
|
-
[[ -f "$
|
|
149
|
+
local p
|
|
150
|
+
p="$(ps_path "$slug")"
|
|
151
|
+
[[ -f "$p" ]] || { echo "ps_reset: not-found: $p" >&2; return 1; }
|
|
152
152
|
|
|
153
153
|
local now
|
|
154
154
|
now="$(date -u +'%Y-%m-%dT%H:%M:%S+0000')"
|
|
@@ -188,8 +188,8 @@ EOF
|
|
|
188
188
|
next
|
|
189
189
|
}
|
|
190
190
|
{ print }
|
|
191
|
-
' "$
|
|
192
|
-
mv "$tmp" "$
|
|
191
|
+
' "$p" > "$tmp"
|
|
192
|
+
mv "$tmp" "$p"
|
|
193
193
|
rm -f "$entry_file"
|
|
194
194
|
|
|
195
195
|
# Reset cycle-state fields
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# SKIP_* env-reach check for PreToolUse Bash hooks.
|
|
4
|
+
#
|
|
5
|
+
# Per #2036 Finding #4 — adopter report 2026-10-02.
|
|
6
|
+
#
|
|
7
|
+
# PreToolUse Bash hooks run in Claude Code's process env. The agent's
|
|
8
|
+
# `SKIP_FOO=1 git commit ...` recipe sets FOO in the git subshell's env,
|
|
9
|
+
# not the hook's env. Hooks that only read `${SKIP_FOO:-0}` miss the
|
|
10
|
+
# inline override and the recipe lies to the adopter.
|
|
11
|
+
#
|
|
12
|
+
# This helper parses tool_input.command for inline `SKIP_FOO=1` and
|
|
13
|
+
# falls back to process env. The adopter's natural recipe
|
|
14
|
+
# `SKIP_FOO=1 cmd` now works regardless of harness env propagation.
|
|
15
|
+
#
|
|
16
|
+
# Retirement path: once Claude Code harness guarantees inline-env
|
|
17
|
+
# propagation to hook subprocesses, this helper becomes redundant.
|
|
18
|
+
# Comment stays as the audit trail.
|
|
19
|
+
#
|
|
20
|
+
# Usage:
|
|
21
|
+
# source "$CLAUDE_PROJECT_DIR/lib/skip-env-inline-check.sh"
|
|
22
|
+
# COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
|
23
|
+
# if skip_var_set "SKIP_FOO" "$COMMAND"; then
|
|
24
|
+
# # bypass
|
|
25
|
+
# exit 0
|
|
26
|
+
# fi
|
|
27
|
+
#
|
|
28
|
+
# Anchors:
|
|
29
|
+
# @luminary john-ousterhout — deep module; narrow interface
|
|
30
|
+
# @luminary david-parnas — information hiding; consumer-free of mechanism
|
|
31
|
+
# @luminary alan-cooper — adopter-goal reliable override recipe
|
|
32
|
+
|
|
33
|
+
# skip_var_set — return 0 if the named SKIP_* var is set to "1".
|
|
34
|
+
# Checks two sources:
|
|
35
|
+
# 1. Inline prefix in command string: `SKIP_FOO=1 cmd...`
|
|
36
|
+
# 2. Fallback: process env `$SKIP_FOO`
|
|
37
|
+
# Returns 1 otherwise.
|
|
38
|
+
#
|
|
39
|
+
# Args:
|
|
40
|
+
# $1 — var name (e.g., "SKIP_OPERATOR_PATHS")
|
|
41
|
+
# $2 — command string (from tool_input.command)
|
|
42
|
+
skip_var_set() {
|
|
43
|
+
local var_name="$1"
|
|
44
|
+
local cmd="$2"
|
|
45
|
+
|
|
46
|
+
# Source 1 — inline prefix. The pattern anchors on word boundary (start
|
|
47
|
+
# of string or whitespace) and requires =1 followed by whitespace or
|
|
48
|
+
# end-of-string. This matches bash's own parsing of inline env prefix.
|
|
49
|
+
if [ -n "$cmd" ]; then
|
|
50
|
+
if echo "$cmd" | grep -qE "(^|[[:space:]])${var_name}=1([[:space:]]|$)"; then
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Source 2 — process env fallback (works where harness propagates env).
|
|
56
|
+
if [ "$(eval "echo \"\${${var_name}:-0}\"")" = "1" ]; then
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
return 1
|
|
61
|
+
}
|