@thebassclef/lite 1.9.9 → 1.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/hooks/artifact-ingestion-gate.sh +9 -3
- package/dist/lite/.claude/hooks/pre-commit-gate.sh +31 -0
- package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +58 -2
- package/dist/lite/.claude/hooks/save-state.sh +6 -1
- package/dist/lite/.claude/hooks/session-reflection.d/83-install-written-paths-rebuild.sh +111 -0
- package/dist/lite/.claude/hooks/session-reflection.d/97-last-block-count.sh +53 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/97-last-block-count.test.sh +131 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +22 -0
- package/dist/lite/.claude/hooks/state-validate.sh +8 -0
- package/dist/lite/.claude/rules/install-written-paths-mediation.md +74 -0
- package/dist/lite/.claude/rules/last-block-marker.md +79 -0
- package/dist/lite/.claude/skills/autonomous/SKILL.md +53 -7
- package/dist/lite/.claude/skills/build/SKILL.md +41 -5
- package/dist/lite/.claude/skills/deploy-prod/SKILL.md +10 -0
- package/dist/lite/.claude/skills/ia-model/SKILL.md +8 -0
- package/dist/lite/.claude/skills/interaction-design/SKILL.md +8 -0
- package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +9 -1
- package/dist/lite/.claude/skills/last-block/SKILL.md +99 -0
- package/dist/lite/.claude/skills/launch/SKILL.md +44 -12
- package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +5 -17
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +18 -18
- package/dist/lite/.claude/skills/personas/SKILL.md +1 -1
- package/dist/lite/.claude/skills/use-case/SKILL.md +5 -4
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/lib/capability-probe.sh +39 -1
- package/dist/lite/lib/composer-preflight.sh +20 -4
- package/dist/lite/lib/install-written-paths.sh +369 -0
- package/dist/lite/lib/last-block.sh +153 -0
- package/dist/lite/lib/luminary-pick.sh +35 -13
- package/dist/lite/lib/state.sh +82 -11
- package/dist/lite/scripts/interpret-input.sh +19 -4
- package/dist/lite/standards/lite-manifest-schema-changes.md +115 -0
- package/dist/lite/standards/lite-manifest.json +80 -26
- package/dist/lite/standards/state-spine.md +1 -0
- package/package.json +2 -2
|
@@ -44,8 +44,46 @@ probe_gh_auth() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
probe_mcp_playwright() {
|
|
47
|
-
#
|
|
47
|
+
# Fast path — explicit opt-in via env var. Adopters who want to force the
|
|
48
|
+
# probe TRUE set CLAUDE_MCP_PLAYWRIGHT=1 in their shell init. Documented at
|
|
49
|
+
# docs/use-cases/UC-lib-capability-probe-mcp-fallback.md.
|
|
48
50
|
[ -n "${CLAUDE_MCP_PLAYWRIGHT:-}" ] && return 0
|
|
51
|
+
|
|
52
|
+
# Fallback — look for a Playwright MCP server in known MCP config files.
|
|
53
|
+
# jq is required for parsing. When absent, the probe fails soft to 1.
|
|
54
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
55
|
+
echo "probe_mcp_playwright: jq not found; env-var fast path only" >&2
|
|
56
|
+
return 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
local project_dir="${CLAUDE_PROJECT_DIR:-.}"
|
|
60
|
+
local home_dir="${HOME:-}"
|
|
61
|
+
|
|
62
|
+
# Check paths in precedence order. First hit wins.
|
|
63
|
+
_probe_mcp_json_has_playwright "${project_dir}/.mcp.json" && return 0
|
|
64
|
+
_probe_mcp_json_has_playwright "${project_dir}/.claude/settings.json" && return 0
|
|
65
|
+
if [ -n "$home_dir" ]; then
|
|
66
|
+
_probe_mcp_json_has_playwright "${home_dir}/.claude/settings.json" && return 0
|
|
67
|
+
_probe_mcp_json_has_playwright "${home_dir}/.claude.json" && return 0
|
|
68
|
+
fi
|
|
69
|
+
return 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# _probe_mcp_json_has_playwright <path>
|
|
73
|
+
# Returns 0 when the JSON file names a Playwright MCP server. Checks two
|
|
74
|
+
# shapes:
|
|
75
|
+
# 1. Top-level "playwright" key (used by `.mcp.json` on some adopters)
|
|
76
|
+
# 2. Nested "mcpServers.playwright" (used by `settings.json` + `~/.claude.json`)
|
|
77
|
+
# Fail-soft on missing file + malformed JSON — returns 1 silently.
|
|
78
|
+
# Per bassclef-upstream Finding #6 (#2036): `local path` is a zsh-magic variable
|
|
79
|
+
# name — ties to $PATH when sourced from zsh. Renamed to `local p`.
|
|
80
|
+
_probe_mcp_json_has_playwright() {
|
|
81
|
+
local p="$1"
|
|
82
|
+
[ -f "$p" ] || return 1
|
|
83
|
+
# Shape 1 — top-level playwright
|
|
84
|
+
jq -e '.playwright // empty' "$p" >/dev/null 2>&1 && return 0
|
|
85
|
+
# Shape 2 — nested mcpServers.playwright
|
|
86
|
+
jq -e '.mcpServers.playwright // empty' "$p" >/dev/null 2>&1 && return 0
|
|
49
87
|
return 1
|
|
50
88
|
}
|
|
51
89
|
|
|
@@ -178,10 +178,14 @@ composer_preflight_check() {
|
|
|
178
178
|
echo "check_5_recommendation="
|
|
179
179
|
pass=$((pass + 1))
|
|
180
180
|
elif [ -f "$repo_skill" ] || [ -L "$repo_skill" ]; then
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
# Per cli#321 — Claude Code's harness loads project .claude/skills/
|
|
182
|
+
# directly; user-level symlinks are not required. Repo-level IS a
|
|
183
|
+
# valid discovery path. Previously flagged as WARN, trained adopters
|
|
184
|
+
# to ignore warnings. Now PASS with informational message.
|
|
185
|
+
echo "check_5_status=pass"
|
|
186
|
+
echo "check_5_message=Skill resolves at repo-level: $repo_skill (harness loads project .claude/skills directly)"
|
|
187
|
+
echo "check_5_recommendation="
|
|
188
|
+
pass=$((pass + 1))
|
|
185
189
|
elif [ -n "${BASSCLEF_DIR:-}" ] && [ -f "$bassclef_skill" ]; then
|
|
186
190
|
echo "check_5_status=warn"
|
|
187
191
|
echo "check_5_message=Skill at bassclef only; consumer-side links missing — substrate sync incomplete"
|
|
@@ -265,6 +269,18 @@ composer_preflight_check() {
|
|
|
265
269
|
echo "check_6_message=hosting_platform=none + --no-launch (variants-only run)"
|
|
266
270
|
echo "check_6_recommendation="
|
|
267
271
|
pass=$((pass + 1))
|
|
272
|
+
elif [ "$skill_name" = "build" ]; then
|
|
273
|
+
# cli#309 cure — /build has no --no-launch flag and its Phase 7 deploy
|
|
274
|
+
# is a documented stub (see .claude/skills/build/SKILL.md Phase 7). Warn
|
|
275
|
+
# (not fail) so dispatch proceeds; Phase 7 will emit a TODO rather than
|
|
276
|
+
# run a real deploy. Adopter can set a real hosting_platform when ready
|
|
277
|
+
# to use Phase 7 live.
|
|
278
|
+
# Anchor: @luminary tony-hoare (postcondition per skill) +
|
|
279
|
+
# @luminary michael-nygard (fail-soft when it does not matter).
|
|
280
|
+
echo "check_6_status=warn"
|
|
281
|
+
echo "check_6_message=hosting_platform=none — /build Phase 7 deploy is a stub; dispatch proceeds, deploy emits TODO instead of running"
|
|
282
|
+
echo "check_6_recommendation=Set hosting_platform to a deploy provider (amplify) when ready to live-deploy; see .claude/skills/build/SKILL.md Phase 7"
|
|
283
|
+
warn=$((warn + 1))
|
|
268
284
|
else
|
|
269
285
|
echo "check_6_status=fail"
|
|
270
286
|
echo "check_6_message=hosting_platform=none — composer would generate variants that can't launch"
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# lib/last-block.sh — persist and read BLOCK banners.
|
|
4
|
+
#
|
|
5
|
+
# Per @luminary john-ousterhout deep module: three narrow functions hide
|
|
6
|
+
# the plaintext marker format. Callers persist, read, and list without
|
|
7
|
+
# touching file layout.
|
|
8
|
+
#
|
|
9
|
+
# Exposed functions:
|
|
10
|
+
# record_last_block <hook> <severity> <message> — write a marker
|
|
11
|
+
# read_last_block [<hook>] — read one marker (newest if no arg)
|
|
12
|
+
# list_last_blocks — list every marker newest first
|
|
13
|
+
#
|
|
14
|
+
# Marker format (plaintext, grep-friendly):
|
|
15
|
+
# hook: <hook-name>
|
|
16
|
+
# severity: blocking|advisory|info
|
|
17
|
+
# fired_at: <ISO-8601 UTC>
|
|
18
|
+
# session: <session-id or "unknown">
|
|
19
|
+
# ---
|
|
20
|
+
# <verbatim banner text, truncated to 8KB with [truncated] marker>
|
|
21
|
+
#
|
|
22
|
+
# Fail-soft: every write path returns 0 even on failure; stderr WARN
|
|
23
|
+
# describes the problem. Per @luminary michael-nygard — a persistence lib
|
|
24
|
+
# must never crash its caller.
|
|
25
|
+
#
|
|
26
|
+
# Per bassclef-upstream#2036 Finding #10 + UC-skill-last-block.md.
|
|
27
|
+
|
|
28
|
+
# Resolve the marker directory. Caller overrides via LAST_BLOCK_DIR for
|
|
29
|
+
# tests. Default is state/markers/last-block/ under CLAUDE_PROJECT_DIR
|
|
30
|
+
# (fallback to current dir when unset).
|
|
31
|
+
_last_block_dir() {
|
|
32
|
+
if [ -n "${LAST_BLOCK_DIR:-}" ]; then
|
|
33
|
+
echo "$LAST_BLOCK_DIR"
|
|
34
|
+
else
|
|
35
|
+
echo "${CLAUDE_PROJECT_DIR:-.}/state/markers/last-block"
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# record_last_block <hook> <severity> <message>
|
|
40
|
+
# Writes a marker. Fail-soft on every error (returns 0; WARN to stderr).
|
|
41
|
+
record_last_block() {
|
|
42
|
+
local hook="${1:-unknown}"
|
|
43
|
+
local severity="${2:-blocking}"
|
|
44
|
+
local message="${3:-}"
|
|
45
|
+
local dir
|
|
46
|
+
dir="$(_last_block_dir)"
|
|
47
|
+
|
|
48
|
+
# mkdir -p fail-soft — read-only disk, permission denied, etc.
|
|
49
|
+
if ! mkdir -p "$dir" 2>/dev/null; then
|
|
50
|
+
echo "record_last_block: cannot create '$dir' — marker not written" >&2
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Truncate banner text to 8192 bytes to bound marker size.
|
|
55
|
+
local max_bytes=8192
|
|
56
|
+
local truncated=""
|
|
57
|
+
local body_bytes
|
|
58
|
+
body_bytes=$(printf '%s' "$message" | wc -c | tr -d ' ')
|
|
59
|
+
if [ "$body_bytes" -gt "$max_bytes" ]; then
|
|
60
|
+
message="$(printf '%s' "$message" | head -c "$max_bytes")"
|
|
61
|
+
truncated=$'\n[truncated — original was '"$body_bytes"' bytes]'
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
local ts
|
|
65
|
+
ts="$(date -u +'%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo 'unknown')"
|
|
66
|
+
local session_id="${CLAUDE_SESSION_ID:-unknown}"
|
|
67
|
+
local marker="$dir/${hook}.marker"
|
|
68
|
+
local tmp="${marker}.tmp.$$"
|
|
69
|
+
|
|
70
|
+
{
|
|
71
|
+
printf 'hook: %s\n' "$hook"
|
|
72
|
+
printf 'severity: %s\n' "$severity"
|
|
73
|
+
printf 'fired_at: %s\n' "$ts"
|
|
74
|
+
printf 'session: %s\n' "$session_id"
|
|
75
|
+
printf '%s\n' '---'
|
|
76
|
+
printf '%s%s\n' "$message" "$truncated"
|
|
77
|
+
} > "$tmp" 2>/dev/null || {
|
|
78
|
+
echo "record_last_block: write to '$tmp' failed — marker not written" >&2
|
|
79
|
+
rm -f "$tmp" 2>/dev/null
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if ! mv "$tmp" "$marker" 2>/dev/null; then
|
|
84
|
+
echo "record_last_block: rename to '$marker' failed — marker not written" >&2
|
|
85
|
+
rm -f "$tmp" 2>/dev/null
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
return 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# read_last_block [<hook>]
|
|
93
|
+
# Prints a marker's banner. With a hook arg, reads that marker. With no
|
|
94
|
+
# args, reads the newest marker by mtime. Returns 1 when no marker found.
|
|
95
|
+
read_last_block() {
|
|
96
|
+
local dir
|
|
97
|
+
dir="$(_last_block_dir)"
|
|
98
|
+
|
|
99
|
+
if [ ! -d "$dir" ]; then
|
|
100
|
+
echo "no block recorded (no markers directory at $dir)" >&2
|
|
101
|
+
return 1
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
local marker
|
|
105
|
+
if [ $# -ge 1 ] && [ -n "$1" ]; then
|
|
106
|
+
marker="$dir/${1}.marker"
|
|
107
|
+
if [ ! -f "$marker" ]; then
|
|
108
|
+
echo "no block recorded for '$1'" >&2
|
|
109
|
+
return 1
|
|
110
|
+
fi
|
|
111
|
+
else
|
|
112
|
+
# Newest by mtime. ls -t is intentional (sort-by-mtime is its purpose).
|
|
113
|
+
# shellcheck disable=SC2012
|
|
114
|
+
marker="$(ls -t "$dir"/*.marker 2>/dev/null | head -1)"
|
|
115
|
+
if [ -z "$marker" ]; then
|
|
116
|
+
echo "no block recorded this session or prior" >&2
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
cat "$marker"
|
|
122
|
+
return 0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# list_last_blocks
|
|
126
|
+
# Lists every marker newest first — one line per marker with hook name,
|
|
127
|
+
# severity, timestamp. Returns 1 when the directory is empty or absent.
|
|
128
|
+
list_last_blocks() {
|
|
129
|
+
local dir
|
|
130
|
+
dir="$(_last_block_dir)"
|
|
131
|
+
|
|
132
|
+
if [ ! -d "$dir" ]; then
|
|
133
|
+
echo "no blocks recorded (no markers directory at $dir)" >&2
|
|
134
|
+
return 1
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
local markers
|
|
138
|
+
# shellcheck disable=SC2012
|
|
139
|
+
markers="$(ls -t "$dir"/*.marker 2>/dev/null)"
|
|
140
|
+
if [ -z "$markers" ]; then
|
|
141
|
+
echo "no blocks recorded (directory empty)" >&2
|
|
142
|
+
return 1
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
local m hook severity fired_at
|
|
146
|
+
echo "$markers" | while IFS= read -r m; do
|
|
147
|
+
hook="$(awk -F': ' '/^hook:/{print $2; exit}' "$m" 2>/dev/null)"
|
|
148
|
+
severity="$(awk -F': ' '/^severity:/{print $2; exit}' "$m" 2>/dev/null)"
|
|
149
|
+
fired_at="$(awk -F': ' '/^fired_at:/{print $2; exit}' "$m" 2>/dev/null)"
|
|
150
|
+
printf '%s\t%s\t%s\n' "$hook" "$severity" "$fired_at"
|
|
151
|
+
done
|
|
152
|
+
return 0
|
|
153
|
+
}
|
|
@@ -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.
|