@thebassclef/lite 1.9.16 → 1.9.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/agents/builder.md +7 -4
- package/dist/lite/.claude/bassclef-orientation.md +22 -0
- package/dist/lite/.claude/hooks/artifact-ingestion-gate.sh +11 -3
- package/dist/lite/.claude/hooks/assert-verify-steering.sh +1 -19
- package/dist/lite/.claude/hooks/plain-english-steering.sh +86 -41
- package/dist/lite/.claude/hooks/pre-commit-gate.sh +10 -2
- package/dist/lite/.claude/hooks/pre-push-shellcheck.sh +196 -0
- package/dist/lite/.claude/hooks/reviewer-dispatch-check.sh +153 -0
- package/dist/lite/.claude/hooks/session-reflection.d/83-install-written-paths-rebuild.sh +15 -32
- package/dist/lite/.claude/hooks/turn-prose-density-measure.sh +109 -0
- package/dist/lite/.claude/hooks/turn-prose-kiss-check.sh +28 -0
- package/dist/lite/.claude/rules/branching.md +2 -0
- package/dist/lite/.claude/rules/identifier-leak-prevention.md +2 -1
- package/dist/lite/.claude/rules/install-written-paths-mediation.md +7 -0
- package/dist/lite/.claude/rules/lite-manifest-schema-change-discipline.md +23 -0
- package/dist/lite/.claude/rules/reviewer-dispatch.md +95 -0
- package/dist/lite/.claude/settings.json +10 -0
- package/dist/lite/.claude/skills/build/SKILL.md +81 -6
- package/dist/lite/.claude/skills/eisenhower/SKILL.md +26 -0
- package/dist/lite/.claude/skills/launch/SKILL.md +45 -2
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +208 -40
- package/dist/lite/.claude/skills/onboard-repo/claude-oauth-probe.sh +60 -0
- package/dist/lite/.claude/skills/onboard-repo/concise-prompt.sh +73 -0
- package/dist/lite/.claude/skills/onboard-repo/write-state-marker.sh +81 -0
- package/dist/lite/.claude/skills/pre-mortem/SKILL.md +56 -0
- package/dist/lite/.claude/skills/sprint/SKILL.md +24 -3
- package/dist/lite/.claude/skills/temperance/SKILL.md +15 -4
- package/dist/lite/README.md +2 -0
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/lib/install-written-paths.sh +250 -3
- package/dist/lite/lib/prose-marker.sh +203 -0
- package/dist/lite/lib/tier-dependency-audit.sh +52 -25
- package/dist/lite/presence/install/bassclef-sync.template.sh +86 -8
- package/dist/lite/presence/install/schedule-auto-save.cron.sh +30 -2
- package/dist/lite/scripts/generate-lite-manifest.sh +15 -0
- package/dist/lite/scripts/launch/local-serve.sh +149 -8
- package/dist/lite/standards/bassclef-wiring-manifest.json +12 -0
- package/dist/lite/standards/lite-manifest-schema-changes.md +84 -0
- package/dist/lite/standards/lite-manifest.json +100 -29
- package/dist/lite/standards/prose-marker-format.md +105 -0
- package/package.json +1 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
#
|
|
4
|
+
# lib/prose-marker.sh — shared marker write + read helpers for the bassclef
|
|
5
|
+
# prose scorer stack. Pins the marker format from
|
|
6
|
+
# standards/prose-marker-format.md v1.0.
|
|
7
|
+
#
|
|
8
|
+
# Producers (Stop hooks that score prose dimensions):
|
|
9
|
+
# - turn-prose-density-measure.sh (Step 2b of Longrun 2)
|
|
10
|
+
# - turn-prose-kiss-check.sh (Step 2c extension)
|
|
11
|
+
# - turn-prose-grade-measure.sh (future retrofit; writes raw today)
|
|
12
|
+
#
|
|
13
|
+
# Consumer (UserPromptSubmit hook that reads markers + injects numbers):
|
|
14
|
+
# - plain-english-steering.sh (Step 2d extension)
|
|
15
|
+
#
|
|
16
|
+
# Per @luminary john-ousterhout — deep module hides JSON shape + atomic
|
|
17
|
+
# write + expiry logic behind a narrow two-function interface.
|
|
18
|
+
# Per @luminary tony-hoare — pre/postcondition contract per
|
|
19
|
+
# standards/prose-marker-format.md §Hoare.
|
|
20
|
+
# Per @luminary michael-nygard — fail-soft on write permission denied.
|
|
21
|
+
|
|
22
|
+
set +e
|
|
23
|
+
|
|
24
|
+
# === BASSCLEF_STATE_ROOT resolution ===
|
|
25
|
+
# When tests isolate state via BASSCLEF_STATE_ROOT env var, honor it.
|
|
26
|
+
# Otherwise resolve to the repo root (walk up from this file's location).
|
|
27
|
+
_prose_marker_resolve_state_root() {
|
|
28
|
+
if [ -n "${BASSCLEF_STATE_ROOT:-}" ]; then
|
|
29
|
+
echo "$BASSCLEF_STATE_ROOT"
|
|
30
|
+
return
|
|
31
|
+
fi
|
|
32
|
+
local script_dir
|
|
33
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
34
|
+
echo "$(cd "$script_dir/.." && pwd)"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# === write_prose_marker <scorer-name> <value> [notes] ===
|
|
38
|
+
# Preconditions:
|
|
39
|
+
# - scorer-name is non-empty and matches [a-z0-9-]+
|
|
40
|
+
# - value is non-empty
|
|
41
|
+
# Postconditions:
|
|
42
|
+
# - file at state/markers/<scorer-name>/last-turn.json exists
|
|
43
|
+
# - JSON matches standards/prose-marker-format.md shape
|
|
44
|
+
# - atomic write (temp file + mv)
|
|
45
|
+
# Returns: 0 on success; 1 on invalid input; 2 on write failure (fail-soft).
|
|
46
|
+
write_prose_marker() {
|
|
47
|
+
local scorer="$1"
|
|
48
|
+
local value="$2"
|
|
49
|
+
local notes="${3:-}"
|
|
50
|
+
|
|
51
|
+
# Validate scorer-name
|
|
52
|
+
if [ -z "$scorer" ]; then
|
|
53
|
+
return 1
|
|
54
|
+
fi
|
|
55
|
+
if ! echo "$scorer" | grep -qE '^[a-z0-9-]+$'; then
|
|
56
|
+
return 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Validate value
|
|
60
|
+
if [ -z "$value" ]; then
|
|
61
|
+
return 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
local state_root
|
|
65
|
+
state_root="$(_prose_marker_resolve_state_root)"
|
|
66
|
+
local marker_dir="${state_root}/state/markers/${scorer}"
|
|
67
|
+
local marker_path="${marker_dir}/last-turn.json"
|
|
68
|
+
|
|
69
|
+
if ! mkdir -p "$marker_dir" 2>/dev/null; then
|
|
70
|
+
return 2
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
local timestamp
|
|
74
|
+
timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
75
|
+
|
|
76
|
+
local tmp
|
|
77
|
+
tmp="$(mktemp "${marker_dir}/last-turn.XXXXXX.tmp" 2>/dev/null)" || return 2
|
|
78
|
+
|
|
79
|
+
# Build JSON. Use jq when available for proper escaping; fall back to raw.
|
|
80
|
+
if command -v jq >/dev/null 2>&1; then
|
|
81
|
+
if [ -n "$notes" ]; then
|
|
82
|
+
jq -n \
|
|
83
|
+
--arg metric "$scorer" \
|
|
84
|
+
--argjson value "$value" \
|
|
85
|
+
--arg measured_at "$timestamp" \
|
|
86
|
+
--arg source "lib/prose-marker.sh" \
|
|
87
|
+
--arg notes "$notes" \
|
|
88
|
+
'{metric: $metric, value: $value, measured_at: $measured_at, source: $source, notes: $notes}' \
|
|
89
|
+
> "$tmp" 2>/dev/null
|
|
90
|
+
else
|
|
91
|
+
jq -n \
|
|
92
|
+
--arg metric "$scorer" \
|
|
93
|
+
--argjson value "$value" \
|
|
94
|
+
--arg measured_at "$timestamp" \
|
|
95
|
+
--arg source "lib/prose-marker.sh" \
|
|
96
|
+
'{metric: $metric, value: $value, measured_at: $measured_at, source: $source}' \
|
|
97
|
+
> "$tmp" 2>/dev/null
|
|
98
|
+
fi
|
|
99
|
+
local rc=$?
|
|
100
|
+
if [ "$rc" -ne 0 ]; then
|
|
101
|
+
rm -f "$tmp" 2>/dev/null
|
|
102
|
+
return 2
|
|
103
|
+
fi
|
|
104
|
+
else
|
|
105
|
+
# Fallback: raw JSON. Scorer + notes get basic escaping (no quotes in input).
|
|
106
|
+
if [ -n "$notes" ]; then
|
|
107
|
+
printf '{"metric":"%s","value":%s,"measured_at":"%s","source":"lib/prose-marker.sh","notes":"%s"}\n' \
|
|
108
|
+
"$scorer" "$value" "$timestamp" "$notes" > "$tmp" 2>/dev/null
|
|
109
|
+
else
|
|
110
|
+
printf '{"metric":"%s","value":%s,"measured_at":"%s","source":"lib/prose-marker.sh"}\n' \
|
|
111
|
+
"$scorer" "$value" "$timestamp" > "$tmp" 2>/dev/null
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Atomic rename
|
|
116
|
+
if ! mv -f "$tmp" "$marker_path" 2>/dev/null; then
|
|
117
|
+
rm -f "$tmp" 2>/dev/null
|
|
118
|
+
return 2
|
|
119
|
+
fi
|
|
120
|
+
return 0
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# === read_prose_markers [--max-age-minutes N] <scorer-name> [<scorer-name> ...] ===
|
|
124
|
+
# Preconditions:
|
|
125
|
+
# - zero or more scorer-name arguments
|
|
126
|
+
# - optional --max-age-minutes N (default 10)
|
|
127
|
+
# Postconditions:
|
|
128
|
+
# - stdout emits one JSON object mapping scorer-name → marker object OR null
|
|
129
|
+
# - null when: marker missing, older than max-age, or malformed JSON
|
|
130
|
+
# - fail-soft: never crashes on bad input
|
|
131
|
+
# Returns: 0 always (fail-soft).
|
|
132
|
+
read_prose_markers() {
|
|
133
|
+
local max_age_min=10
|
|
134
|
+
local scorers=()
|
|
135
|
+
|
|
136
|
+
while [ "$#" -gt 0 ]; do
|
|
137
|
+
case "$1" in
|
|
138
|
+
--max-age-minutes)
|
|
139
|
+
shift
|
|
140
|
+
max_age_min="${1:-10}"
|
|
141
|
+
shift
|
|
142
|
+
;;
|
|
143
|
+
*)
|
|
144
|
+
scorers+=("$1")
|
|
145
|
+
shift
|
|
146
|
+
;;
|
|
147
|
+
esac
|
|
148
|
+
done
|
|
149
|
+
|
|
150
|
+
local state_root
|
|
151
|
+
state_root="$(_prose_marker_resolve_state_root)"
|
|
152
|
+
|
|
153
|
+
# Build JSON object. If jq missing, emit raw.
|
|
154
|
+
if command -v jq >/dev/null 2>&1; then
|
|
155
|
+
# Start with empty object
|
|
156
|
+
local result='{}'
|
|
157
|
+
local scorer
|
|
158
|
+
for scorer in "${scorers[@]}"; do
|
|
159
|
+
local marker_path="${state_root}/state/markers/${scorer}/last-turn.json"
|
|
160
|
+
local marker_json="null"
|
|
161
|
+
if [ -f "$marker_path" ]; then
|
|
162
|
+
# Age check
|
|
163
|
+
local file_age_sec
|
|
164
|
+
if stat -f %m "$marker_path" >/dev/null 2>&1; then
|
|
165
|
+
# BSD stat (macOS)
|
|
166
|
+
file_age_sec=$(( $(date +%s) - $(stat -f %m "$marker_path") ))
|
|
167
|
+
else
|
|
168
|
+
# GNU stat (Linux)
|
|
169
|
+
file_age_sec=$(( $(date +%s) - $(stat -c %Y "$marker_path") ))
|
|
170
|
+
fi
|
|
171
|
+
local max_age_sec=$(( max_age_min * 60 ))
|
|
172
|
+
if [ "$file_age_sec" -le "$max_age_sec" ]; then
|
|
173
|
+
# Parse marker — null on malformed JSON
|
|
174
|
+
marker_json=$(jq -c '.' "$marker_path" 2>/dev/null)
|
|
175
|
+
if [ -z "$marker_json" ]; then
|
|
176
|
+
marker_json="null"
|
|
177
|
+
fi
|
|
178
|
+
fi
|
|
179
|
+
fi
|
|
180
|
+
result=$(echo "$result" | jq --arg k "$scorer" --argjson v "$marker_json" '. + {($k): $v}' 2>/dev/null)
|
|
181
|
+
done
|
|
182
|
+
echo "$result"
|
|
183
|
+
else
|
|
184
|
+
# Fallback without jq — emit a flat null map
|
|
185
|
+
printf '{'
|
|
186
|
+
local first=1
|
|
187
|
+
local scorer
|
|
188
|
+
for scorer in "${scorers[@]}"; do
|
|
189
|
+
if [ "$first" -eq 0 ]; then
|
|
190
|
+
printf ','
|
|
191
|
+
fi
|
|
192
|
+
printf '"%s":null' "$scorer"
|
|
193
|
+
first=0
|
|
194
|
+
done
|
|
195
|
+
printf '}\n'
|
|
196
|
+
fi
|
|
197
|
+
return 0
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Export functions when sourced
|
|
201
|
+
export -f write_prose_marker 2>/dev/null
|
|
202
|
+
export -f read_prose_markers 2>/dev/null
|
|
203
|
+
export -f _prose_marker_resolve_state_root 2>/dev/null
|
|
@@ -814,37 +814,64 @@ build_reverse_index() {
|
|
|
814
814
|
walk_graph_bfs_inbound() {
|
|
815
815
|
local seeds_file="$1"
|
|
816
816
|
local rev_index="$2"
|
|
817
|
-
local visited_file="" queue_file=""
|
|
818
|
-
visited_file="$(mktemp)"
|
|
819
|
-
queue_file="$(mktemp)"
|
|
820
|
-
# shellcheck disable=SC2064
|
|
821
|
-
trap "rm -f '$visited_file' '$queue_file'" RETURN
|
|
822
817
|
|
|
823
818
|
if [ ! -s "$seeds_file" ]; then
|
|
824
|
-
: > "$visited_file"
|
|
825
|
-
sort -u "$visited_file"
|
|
826
819
|
return 0
|
|
827
820
|
fi
|
|
828
|
-
cp "$seeds_file" "$queue_file"
|
|
829
821
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
[ -z "$p" ] && continue
|
|
836
|
-
if grep -Fxq "$p" "$visited_file" 2>/dev/null; then continue; fi
|
|
837
|
-
echo "$p" >> "$visited_file"
|
|
838
|
-
# Look up callers of $p in the reverse index (path is column 1; caller is column 2)
|
|
839
|
-
awk -F'\t' -v p="$p" '$1 == p { print $2 }' "$rev_index" | while IFS= read -r caller; do
|
|
840
|
-
[ -n "$caller" ] && echo "$caller" >> "$next_file"
|
|
841
|
-
done
|
|
842
|
-
done < "$queue_file"
|
|
843
|
-
mv "$next_file" "$queue_file"
|
|
844
|
-
sort -u -o "$queue_file" "$queue_file"
|
|
845
|
-
done
|
|
822
|
+
if [ ! -s "$rev_index" ]; then
|
|
823
|
+
# No edges — output just the seeds, deduped
|
|
824
|
+
sort -u "$seeds_file"
|
|
825
|
+
return 0
|
|
826
|
+
fi
|
|
846
827
|
|
|
847
|
-
|
|
828
|
+
# Single-pass awk: build reverse-index hash during Phase 1 (reading
|
|
829
|
+
# rev_index), then run BFS entirely in-memory during Phase 2 (reading
|
|
830
|
+
# seeds_file via END block). Visited set prevents re-visiting; queue
|
|
831
|
+
# grows as new callers are found.
|
|
832
|
+
#
|
|
833
|
+
# Complexity: O(N + M) where N = seeds + M = edges in rev_index.
|
|
834
|
+
# Prior per-item awk was O(N × M) — unusable at 1.1MB index (ticket
|
|
835
|
+
# bassclef-upstream#2109 observed 15+ min hang during /release).
|
|
836
|
+
#
|
|
837
|
+
# Per @luminary john-ousterhout — deep module: one awk hides BFS.
|
|
838
|
+
# Per @luminary michael-feathers — behavior pinned by Tier 0 tests
|
|
839
|
+
# at lib/tests/tier-dependency-audit-inbound.test.sh.
|
|
840
|
+
awk -F'\t' '
|
|
841
|
+
# Phase 1: load reverse index into callers map
|
|
842
|
+
# Each line: <path>\t<caller>; multiple callers per path concatenated
|
|
843
|
+
# with SUBSEP as the separator (safe since SUBSEP is non-printable).
|
|
844
|
+
NR == FNR {
|
|
845
|
+
if (NF >= 2 && $1 != "") {
|
|
846
|
+
callers[$1] = (callers[$1] == "" ? $2 : callers[$1] SUBSEP $2)
|
|
847
|
+
}
|
|
848
|
+
next
|
|
849
|
+
}
|
|
850
|
+
# Phase 2: enqueue seeds (file 2)
|
|
851
|
+
{
|
|
852
|
+
if ($0 != "" && !($0 in visited)) {
|
|
853
|
+
queue[++qlen] = $0
|
|
854
|
+
visited[$0] = 1
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
# Phase 3: BFS, draining the queue
|
|
858
|
+
END {
|
|
859
|
+
head = 1
|
|
860
|
+
while (head <= qlen) {
|
|
861
|
+
p = queue[head++]
|
|
862
|
+
if (p in callers) {
|
|
863
|
+
n = split(callers[p], c, SUBSEP)
|
|
864
|
+
for (i = 1; i <= n; i++) {
|
|
865
|
+
if (c[i] != "" && !(c[i] in visited)) {
|
|
866
|
+
visited[c[i]] = 1
|
|
867
|
+
queue[++qlen] = c[i]
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
for (p in visited) print p
|
|
873
|
+
}
|
|
874
|
+
' "$rev_index" "$seeds_file" | sort -u
|
|
848
875
|
return 0
|
|
849
876
|
}
|
|
850
877
|
|
|
@@ -256,6 +256,64 @@ bassclef_sync_parse_whereami_ts() {
|
|
|
256
256
|
return 0
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
# === #2130 — scope-aware install-class resolution + prior-sync check ===
|
|
260
|
+
# Reads a hook source file's `# install-class:` header. Returns one of:
|
|
261
|
+
# project | operator | dual | unknown
|
|
262
|
+
# Fails soft — missing file, missing header, or invalid value → "unknown".
|
|
263
|
+
#
|
|
264
|
+
# Anchor luminaries — @luminary linus-torvalds (adopter contract; project-class
|
|
265
|
+
# hooks must not report scope=operator), @luminary jerome-saltzer-and-michael-schroeder
|
|
266
|
+
# (every scope mediated per source declaration), @luminary michael-feathers
|
|
267
|
+
# (characterization anchor — pin the install-class header shape before cure).
|
|
268
|
+
_template_resolve_install_class() {
|
|
269
|
+
local hook_src="$1"
|
|
270
|
+
if [ ! -f "$hook_src" ]; then
|
|
271
|
+
echo "unknown"
|
|
272
|
+
return 0
|
|
273
|
+
fi
|
|
274
|
+
local header_val
|
|
275
|
+
header_val=$(head -10 "$hook_src" 2>/dev/null | grep -E \
|
|
276
|
+
'^# install-class:[[:space:]]*(operator|project|dual)[[:space:]]*$' \
|
|
277
|
+
| head -1 | sed -E 's/^# install-class:[[:space:]]*//;s/[[:space:]]*$//')
|
|
278
|
+
case "$header_val" in
|
|
279
|
+
project|operator|dual) echo "$header_val" ;;
|
|
280
|
+
*) echo "unknown" ;;
|
|
281
|
+
esac
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# Returns 0 (true) if the hook is installed at the scope its install-class names.
|
|
285
|
+
# For `dual`, either project or operator location satisfies.
|
|
286
|
+
# For `unknown`, defaults to operator check per ADR-058 C2 grace window.
|
|
287
|
+
#
|
|
288
|
+
# $1 — hook basename (e.g., "pre-commit-gate.sh")
|
|
289
|
+
# $2 — scope value (project | operator | dual | unknown)
|
|
290
|
+
# Reads $CWD and $HOME. $CWD falls back to $(pwd) when unset.
|
|
291
|
+
_template_hook_present_at_scope() {
|
|
292
|
+
local hook_name="$1"
|
|
293
|
+
local scope="$2"
|
|
294
|
+
local cwd="${CWD:-$(pwd)}"
|
|
295
|
+
case "$scope" in
|
|
296
|
+
project)
|
|
297
|
+
[ -f "$cwd/.claude/hooks/$hook_name" ] && return 0
|
|
298
|
+
return 1
|
|
299
|
+
;;
|
|
300
|
+
operator)
|
|
301
|
+
[ -f "$HOME/.claude/hooks/$hook_name" ] && return 0
|
|
302
|
+
return 1
|
|
303
|
+
;;
|
|
304
|
+
dual)
|
|
305
|
+
[ -f "$cwd/.claude/hooks/$hook_name" ] && return 0
|
|
306
|
+
[ -f "$HOME/.claude/hooks/$hook_name" ] && return 0
|
|
307
|
+
return 1
|
|
308
|
+
;;
|
|
309
|
+
*)
|
|
310
|
+
# unknown — grace-window; check operator per legacy behavior
|
|
311
|
+
[ -f "$HOME/.claude/hooks/$hook_name" ] && return 0
|
|
312
|
+
return 1
|
|
313
|
+
;;
|
|
314
|
+
esac
|
|
315
|
+
}
|
|
316
|
+
|
|
259
317
|
# === #1929 — postcondition emitter (RESTORED/DEGRADED + JSON) ===
|
|
260
318
|
# Mirrors .claude/hooks/bassclef-sync.sh L215-243 (idempotent-complete-or-fail-loud
|
|
261
319
|
# pattern shipped under #1894 Cure 1 of #1691). Reads globals INSTALLED_COUNT,
|
|
@@ -1022,9 +1080,16 @@ if [ -d "$BASSCLEF_DIR/.claude/hooks" ]; then
|
|
|
1022
1080
|
fi
|
|
1023
1081
|
if [ -n "$_WALKER_MISS" ]; then
|
|
1024
1082
|
echo "$(date -u +%FT%TZ) install-fail-missing-dep $HOOK_NAME" >> "$BASSCLEF_TRACE_LOG"
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1083
|
+
# #2130 — resolve install-class scope + check if hook is already present
|
|
1084
|
+
# at scoped target from a prior sync. Prior-sync-present counts as installed.
|
|
1085
|
+
_HOOK_SCOPE=$(_template_resolve_install_class "$HOOK_SRC")
|
|
1086
|
+
if _template_hook_present_at_scope "$HOOK_NAME" "$_HOOK_SCOPE"; then
|
|
1087
|
+
INSTALLED_COUNT=$((INSTALLED_COUNT + 1))
|
|
1088
|
+
else
|
|
1089
|
+
FAILED_COUNT=$((FAILED_COUNT + 1))
|
|
1090
|
+
missing_paths+=(".claude/hooks/$HOOK_NAME")
|
|
1091
|
+
missing_scopes+=("$_HOOK_SCOPE")
|
|
1092
|
+
fi
|
|
1028
1093
|
continue
|
|
1029
1094
|
fi
|
|
1030
1095
|
|
|
@@ -1038,9 +1103,15 @@ if [ -d "$BASSCLEF_DIR/.claude/hooks" ]; then
|
|
|
1038
1103
|
cp -p "$HOOK_SRC" "$TARGET_USER" 2>/dev/null || true
|
|
1039
1104
|
chmod +x "$TARGET_USER" 2>/dev/null || true
|
|
1040
1105
|
echo "$(date -u +%FT%TZ) dispatch-fail $HOOK_NAME" >> "$BASSCLEF_TRACE_LOG"
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1106
|
+
# #2130 — scope-aware missing[] + prior-sync pass-through
|
|
1107
|
+
_HOOK_SCOPE=$(_template_resolve_install_class "$HOOK_SRC")
|
|
1108
|
+
if _template_hook_present_at_scope "$HOOK_NAME" "$_HOOK_SCOPE"; then
|
|
1109
|
+
INSTALLED_COUNT=$((INSTALLED_COUNT + 1))
|
|
1110
|
+
else
|
|
1111
|
+
FAILED_COUNT=$((FAILED_COUNT + 1))
|
|
1112
|
+
missing_paths+=(".claude/hooks/$HOOK_NAME")
|
|
1113
|
+
missing_scopes+=("$_HOOK_SCOPE")
|
|
1114
|
+
fi
|
|
1044
1115
|
fi
|
|
1045
1116
|
elif [ "$HAS_HEADER" = "1" ] && [ "$DISPATCH_AVAILABLE" = "0" ]; then
|
|
1046
1117
|
# S-F1 MED — dispatch lib missing; trace explicitly (not silent)
|
|
@@ -1059,8 +1130,15 @@ if [ -d "$BASSCLEF_DIR/.claude/hooks" ]; then
|
|
|
1059
1130
|
else
|
|
1060
1131
|
echo "$(date -u +%FT%TZ) skipped-invalid-header $HOOK_NAME" >> "$BASSCLEF_TRACE_LOG"
|
|
1061
1132
|
SKIPPED_INVALID=$((SKIPPED_INVALID + 1))
|
|
1062
|
-
|
|
1063
|
-
|
|
1133
|
+
# #2130 — invalid header → scope resolves to "unknown"; prior-sync still passes
|
|
1134
|
+
if _template_hook_present_at_scope "$HOOK_NAME" "unknown"; then
|
|
1135
|
+
# Operator-scope fallback cp above landed; treat as installed under grace
|
|
1136
|
+
INSTALLED_COUNT=$((INSTALLED_COUNT + 1))
|
|
1137
|
+
SKIPPED_INVALID=$((SKIPPED_INVALID - 1))
|
|
1138
|
+
else
|
|
1139
|
+
missing_paths+=(".claude/hooks/$HOOK_NAME")
|
|
1140
|
+
missing_scopes+=("unknown")
|
|
1141
|
+
fi
|
|
1064
1142
|
fi
|
|
1065
1143
|
fi
|
|
1066
1144
|
done
|
|
@@ -28,8 +28,21 @@
|
|
|
28
28
|
|
|
29
29
|
set -euo pipefail
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
# Hook path resolution — closes bassclef-upstream#2257.
|
|
32
|
+
# Cold adopters do not have $HOME/src/sunj-labs/bassclef cloned; they get
|
|
33
|
+
# the hook installed at $CLAUDE_PROJECT_DIR/.claude/hooks/ via `bassclef init`.
|
|
34
|
+
# Probe adopter project dir first, then dev sibling. Positional arg and
|
|
35
|
+
# BASSCLEF_HOOK_PATH env override still win over the probe.
|
|
36
|
+
_probe_hook_path() {
|
|
37
|
+
local proj="${CLAUDE_PROJECT_DIR:-}"
|
|
38
|
+
if [ -n "$proj" ] && [ -f "$proj/.claude/hooks/auto-save-idle.sh" ]; then
|
|
39
|
+
echo "$proj/.claude/hooks/auto-save-idle.sh"
|
|
40
|
+
return 0
|
|
41
|
+
fi
|
|
42
|
+
echo "$HOME/src/sunj-labs/bassclef/.claude/hooks/auto-save-idle.sh"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
HOOK_PATH="${1:-${BASSCLEF_HOOK_PATH:-$(_probe_hook_path)}}"
|
|
33
46
|
CRON_MARKER="# bassclef auto-save (ADR-046)"
|
|
34
47
|
CRON_ENTRY="*/15 * * * * bash ${HOOK_PATH} >/dev/null 2>&1"
|
|
35
48
|
|
|
@@ -54,6 +67,21 @@ fi
|
|
|
54
67
|
# Check hook file exists
|
|
55
68
|
if [ ! -f "$HOOK_PATH" ]; then
|
|
56
69
|
echo "ERROR: hook file not found at ${HOOK_PATH}"
|
|
70
|
+
echo ""
|
|
71
|
+
echo "Resolution order (per bassclef-upstream#2257):"
|
|
72
|
+
echo " 1. positional arg \$1 — not set"
|
|
73
|
+
if [ -n "${BASSCLEF_HOOK_PATH:-}" ]; then
|
|
74
|
+
echo " 2. BASSCLEF_HOOK_PATH env — set to ${BASSCLEF_HOOK_PATH}"
|
|
75
|
+
else
|
|
76
|
+
echo " 2. BASSCLEF_HOOK_PATH env — not set"
|
|
77
|
+
fi
|
|
78
|
+
if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
79
|
+
echo " 3. CLAUDE_PROJECT_DIR probe — tried ${CLAUDE_PROJECT_DIR}/.claude/hooks/auto-save-idle.sh"
|
|
80
|
+
else
|
|
81
|
+
echo " 3. CLAUDE_PROJECT_DIR probe — unset; skipped"
|
|
82
|
+
fi
|
|
83
|
+
echo " 4. Dev sibling fallback — tried ${HOME}/src/sunj-labs/bassclef/.claude/hooks/auto-save-idle.sh"
|
|
84
|
+
echo ""
|
|
57
85
|
echo " Pass the path as an argument OR set BASSCLEF_HOOK_PATH env var."
|
|
58
86
|
echo " Example: bash $0 /path/to/bassclef/.claude/hooks/auto-save-idle.sh"
|
|
59
87
|
exit 2
|
|
@@ -497,6 +497,18 @@ while IFS= read -r f; do
|
|
|
497
497
|
done < <(find "$REPO_ROOT/.claude/hooks" -mindepth 2 -type f -name '*.sh' \
|
|
498
498
|
-not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
|
|
499
499
|
|
|
500
|
+
# Skill-helper scripts (bash) — per bassclef-upstream#2140. Skills (e.g.,
|
|
501
|
+
# onboard-repo) dispatch helper .sh files from their SKILL.md Procedure
|
|
502
|
+
# steps. Lite adopters that run those skills need the helpers to land
|
|
503
|
+
# in their bundle. Shipped as type=script (reuses existing enum; no
|
|
504
|
+
# schema shape change). Only lite-tagged helpers end up in lite-manifest;
|
|
505
|
+
# standard-tagged helpers inherit into standard-manifest + above.
|
|
506
|
+
SKILL_HELPER_FILES=()
|
|
507
|
+
while IFS= read -r f; do
|
|
508
|
+
SKILL_HELPER_FILES+=("$f")
|
|
509
|
+
done < <(find "$REPO_ROOT/.claude/skills" -mindepth 2 -type f -name '*.sh' \
|
|
510
|
+
-not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
|
|
511
|
+
|
|
500
512
|
# Top-level .claude/*.md (bassclef-orientation.md) — vendored via @import from adopter
|
|
501
513
|
# CLAUDE.md; ships as root-doc. Per goal 2026-09-16a.
|
|
502
514
|
CLAUDE_DOC_FILES=()
|
|
@@ -635,6 +647,9 @@ ENTRIES_JSON=$(
|
|
|
635
647
|
if [[ ${#SCRIPT_FILES[@]} -gt 0 ]]; then
|
|
636
648
|
emit_entries "script" extract_hook_tier extract_hook_description basename "${SCRIPT_FILES[@]}"
|
|
637
649
|
fi
|
|
650
|
+
if [[ ${#SKILL_HELPER_FILES[@]} -gt 0 ]]; then
|
|
651
|
+
emit_entries "script" extract_hook_tier extract_hook_description basename "${SKILL_HELPER_FILES[@]}"
|
|
652
|
+
fi
|
|
638
653
|
if [[ ${#PRESENCE_TEMPLATE_SH_FILES[@]} -gt 0 ]]; then
|
|
639
654
|
emit_entries "presence-template" extract_hook_tier extract_hook_description basename "${PRESENCE_TEMPLATE_SH_FILES[@]}"
|
|
640
655
|
fi
|
|
@@ -30,16 +30,157 @@
|
|
|
30
30
|
# Cleanup: rely on the operator running scripts/launch/local-serve-stop.sh
|
|
31
31
|
# or just killing the PID. State file lists the PID for reference.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
# === #2131 — testable helpers for arg parsing + usage ===
|
|
34
|
+
# Anchor luminaries — @luminary linus-torvalds (default bind unchanged;
|
|
35
|
+
# --lan is opt-in; no silent regression), @luminary alan-cooper (honesty
|
|
36
|
+
# default — handoff text matches bind reality), @luminary michael-feathers
|
|
37
|
+
# (characterization anchor via LOCAL_SERVE_TEST_MODE guard).
|
|
38
|
+
|
|
39
|
+
_local_serve_print_usage() {
|
|
40
|
+
cat <<'USAGE'
|
|
41
|
+
usage: local-serve.sh [--lan] <gallery-dir> [port-hint]
|
|
42
|
+
|
|
43
|
+
Serves a /launch local-mode prototype gallery via python http.server.
|
|
44
|
+
|
|
45
|
+
Positional:
|
|
46
|
+
gallery-dir path to the gallery directory (must contain index.html)
|
|
47
|
+
port-hint optional starting port (default 8000)
|
|
48
|
+
|
|
49
|
+
Flags:
|
|
50
|
+
--lan bind to 0.0.0.0 (all interfaces) and PRINT the detected
|
|
51
|
+
LAN IP so phones/iPads on the same Wi-Fi can reach the
|
|
52
|
+
URL directly. Default is 127.0.0.1 (this computer only).
|
|
53
|
+
Prints a warning: any device on this Wi-Fi can open the URL.
|
|
54
|
+
Firewall note: macOS firewall may need to be off for the
|
|
55
|
+
port, or the OS will prompt on first bind.
|
|
56
|
+
--help print this usage
|
|
57
|
+
|
|
58
|
+
Exit codes:
|
|
59
|
+
0 — server started OR idempotent re-run returns existing URL
|
|
60
|
+
1 — python3 missing, gallery dir missing, port exhaustion, server crash
|
|
61
|
+
2 — bad args (missing gallery-dir, unknown flag)
|
|
62
|
+
USAGE
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Detects the machine's LAN IP so --lan mode can print a dialable URL
|
|
66
|
+
# instead of literal "0.0.0.0". Tries mac first (ipconfig getifaddr en0),
|
|
67
|
+
# then linux (hostname -I | awk '{print $1}'). Returns empty on both
|
|
68
|
+
# failures — the caller falls back to 127.0.0.1 so the --lan path
|
|
69
|
+
# degrades to "this computer only" rather than crashing.
|
|
70
|
+
# Per bassclef-upstream#2143 — the prior shape printed 0.0.0.0 which
|
|
71
|
+
# phones cannot dial.
|
|
72
|
+
_local_serve_detect_lan_ip() {
|
|
73
|
+
local ip
|
|
74
|
+
# macOS primary: en0 is the typical Wi-Fi interface
|
|
75
|
+
if command -v ipconfig >/dev/null 2>&1; then
|
|
76
|
+
ip=$(ipconfig getifaddr en0 2>/dev/null || true)
|
|
77
|
+
if [ -n "$ip" ]; then
|
|
78
|
+
echo "$ip"
|
|
79
|
+
return 0
|
|
80
|
+
fi
|
|
81
|
+
# macOS fallback: try en1 (secondary Wi-Fi on some models)
|
|
82
|
+
ip=$(ipconfig getifaddr en1 2>/dev/null || true)
|
|
83
|
+
if [ -n "$ip" ]; then
|
|
84
|
+
echo "$ip"
|
|
85
|
+
return 0
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
# Linux: hostname -I returns space-separated IPs; first is usually primary LAN
|
|
89
|
+
if command -v hostname >/dev/null 2>&1; then
|
|
90
|
+
ip=$(hostname -I 2>/dev/null | awk '{print $1}' || true)
|
|
91
|
+
if [ -n "$ip" ]; then
|
|
92
|
+
echo "$ip"
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
fi
|
|
96
|
+
# Fallback: empty — caller uses 127.0.0.1
|
|
97
|
+
return 0
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Parses CLI args. Sets globals BIND_ADDR, DISPLAY_HOST, LAN_WARNING,
|
|
101
|
+
# GALLERY_DIR_PARSED, PORT_HINT_PARSED. Returns 0 on success. Returns
|
|
102
|
+
# non-zero on --help, unknown flag, or missing gallery-dir.
|
|
103
|
+
#
|
|
104
|
+
# BIND_ADDR is what python's http.server --bind sees (127.0.0.1 default,
|
|
105
|
+
# 0.0.0.0 under --lan so it listens on all interfaces).
|
|
106
|
+
# DISPLAY_HOST is what the printed URL + state file carries (127.0.0.1
|
|
107
|
+
# default, detected LAN IP under --lan). Separate so the printed URL is
|
|
108
|
+
# always dialable: 127.0.0.1 for the host, LAN IP for a phone on the
|
|
109
|
+
# same Wi-Fi. Per bassclef-upstream#2143.
|
|
110
|
+
_local_serve_parse_args() {
|
|
111
|
+
BIND_ADDR="127.0.0.1"
|
|
112
|
+
DISPLAY_HOST="127.0.0.1"
|
|
113
|
+
LAN_WARNING=""
|
|
114
|
+
GALLERY_DIR_PARSED=""
|
|
115
|
+
PORT_HINT_PARSED="8000"
|
|
116
|
+
local positional=()
|
|
117
|
+
local arg
|
|
118
|
+
local lan_mode=0
|
|
119
|
+
for arg in "$@"; do
|
|
120
|
+
case "$arg" in
|
|
121
|
+
--lan)
|
|
122
|
+
lan_mode=1
|
|
123
|
+
BIND_ADDR="0.0.0.0"
|
|
124
|
+
LAN_WARNING="WARN: --lan set; any device on this Wi-Fi can open this URL. Unset --lan to bind 127.0.0.1 only."
|
|
125
|
+
;;
|
|
126
|
+
--help|-h)
|
|
127
|
+
_local_serve_print_usage
|
|
128
|
+
return 1
|
|
129
|
+
;;
|
|
130
|
+
--*)
|
|
131
|
+
echo "ERROR: unknown flag: $arg" >&2
|
|
132
|
+
_local_serve_print_usage >&2
|
|
133
|
+
return 2
|
|
134
|
+
;;
|
|
135
|
+
*)
|
|
136
|
+
positional+=("$arg")
|
|
137
|
+
;;
|
|
138
|
+
esac
|
|
139
|
+
done
|
|
140
|
+
if [ "$lan_mode" -eq 1 ]; then
|
|
141
|
+
local detected
|
|
142
|
+
detected=$(_local_serve_detect_lan_ip)
|
|
143
|
+
if [ -n "$detected" ]; then
|
|
144
|
+
DISPLAY_HOST="$detected"
|
|
145
|
+
else
|
|
146
|
+
# Detection failed; fall back to 127.0.0.1 so the printed URL is still
|
|
147
|
+
# dialable from the host at least. Warn so the operator knows the
|
|
148
|
+
# phone-preview path won't work.
|
|
149
|
+
DISPLAY_HOST="127.0.0.1"
|
|
150
|
+
LAN_WARNING="${LAN_WARNING}
|
|
151
|
+
WARN: --lan set but LAN IP detection failed (ipconfig / hostname). Printed URL falls back to 127.0.0.1; phone preview will not work."
|
|
152
|
+
fi
|
|
153
|
+
fi
|
|
154
|
+
if [ "${#positional[@]}" -ge 1 ]; then
|
|
155
|
+
GALLERY_DIR_PARSED="${positional[0]}"
|
|
156
|
+
fi
|
|
157
|
+
if [ "${#positional[@]}" -ge 2 ]; then
|
|
158
|
+
PORT_HINT_PARSED="${positional[1]}"
|
|
159
|
+
fi
|
|
160
|
+
if [ -z "$GALLERY_DIR_PARSED" ]; then
|
|
161
|
+
echo "ERROR: gallery-dir required" >&2
|
|
162
|
+
_local_serve_print_usage >&2
|
|
163
|
+
return 2
|
|
164
|
+
fi
|
|
165
|
+
return 0
|
|
166
|
+
}
|
|
34
167
|
|
|
35
|
-
|
|
36
|
-
|
|
168
|
+
# === Test-mode guard — tests source + exercise helpers without running main ===
|
|
169
|
+
if [ "${LOCAL_SERVE_TEST_MODE:-}" = "1" ]; then
|
|
170
|
+
return 0 2>/dev/null || exit 0
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
set -e
|
|
37
174
|
|
|
38
|
-
if
|
|
39
|
-
echo "ERROR: gallery-dir required" >&2
|
|
40
|
-
echo "usage: $0 <gallery-dir> [port-hint]" >&2
|
|
175
|
+
if ! _local_serve_parse_args "$@"; then
|
|
41
176
|
exit 2
|
|
42
177
|
fi
|
|
178
|
+
GALLERY_DIR="$GALLERY_DIR_PARSED"
|
|
179
|
+
PORT_HINT="$PORT_HINT_PARSED"
|
|
180
|
+
|
|
181
|
+
if [ -n "$LAN_WARNING" ]; then
|
|
182
|
+
echo "$LAN_WARNING" >&2
|
|
183
|
+
fi
|
|
43
184
|
|
|
44
185
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
45
186
|
echo "ERROR: python3 not on PATH. Install python3 first." >&2
|
|
@@ -92,7 +233,7 @@ fi
|
|
|
92
233
|
|
|
93
234
|
# Start http.server in background, bound to 127.0.0.1 only (no external exposure)
|
|
94
235
|
LOG_FILE="$STATE_DIR/${GALLERY_SLUG}.log"
|
|
95
|
-
nohup python3 -m http.server "$PORT" --bind
|
|
236
|
+
nohup python3 -m http.server "$PORT" --bind "$BIND_ADDR" --directory "$GALLERY_DIR" \
|
|
96
237
|
> "$LOG_FILE" 2>&1 &
|
|
97
238
|
SERVER_PID=$!
|
|
98
239
|
|
|
@@ -103,7 +244,7 @@ if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
|
103
244
|
exit 1
|
|
104
245
|
fi
|
|
105
246
|
|
|
106
|
-
URL="http
|
|
247
|
+
URL="http://${DISPLAY_HOST}:${PORT}/"
|
|
107
248
|
|
|
108
249
|
# Write state file
|
|
109
250
|
cat > "$STATE_FILE" <<EOF
|