@thebassclef/lite 1.8.0 → 1.9.1
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/bassclef-sync.sh +43 -6
- package/dist/lite/.claude/hooks/git-stash-u-guard.sh +84 -0
- package/dist/lite/.claude/hooks/session-reflection.d/01-adopter-message-channel.sh +49 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/fresh-install-state-gate.test.sh +242 -0
- package/dist/lite/.claude/settings.json +5 -0
- package/dist/lite/.claude/skills/build/SKILL.md +57 -1
- package/dist/lite/.claude/skills/launch/SKILL.md +43 -1
- package/dist/lite/.claude/skills/riff/SKILL.md +55 -13
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/lib/capability-probe.sh +140 -0
- package/dist/lite/presence/install/bassclef-sync.template.sh +85 -19
- package/dist/lite/standards/bassclef-wiring-manifest.json +6 -0
- package/dist/lite/standards/lite-manifest-schema-changes.md +9 -0
- package/dist/lite/standards/lite-manifest.json +36 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ See [`standards/os-support.md`](standards/os-support.md) for the full policy.
|
|
|
35
35
|
|
|
36
36
|
## Current release
|
|
37
37
|
|
|
38
|
-
<!-- version-start -->1.
|
|
38
|
+
<!-- version-start -->1.9.1<!-- version-end -->
|
|
39
39
|
|
|
40
40
|
See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
41
41
|
|
package/dist/index.cjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const version: "1.
|
|
1
|
+
export declare const version: "1.9.1";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# tier:
|
|
2
|
+
# tier: upstream
|
|
3
|
+
# install-class: operator
|
|
3
4
|
# Bassclef Substrate Sync — fires on SessionStart.
|
|
4
5
|
# Operator-class hook: lives on bassclef-upstream operators' machines only.
|
|
6
|
+
# Ship 1 (#1828) — install-class: operator retag; adopters use the dispatcher
|
|
7
|
+
# (presence/install/bassclef-sync.dispatcher.template.sh) instead.
|
|
5
8
|
# Adopter consumers receive presence/install/bassclef-sync.template.sh instead
|
|
6
9
|
# (sourced from canonical until the WU-0 absorb gap is closed).
|
|
7
10
|
# Ensures bassclef standards are available regardless of device.
|
|
@@ -127,6 +130,9 @@ _sync_install_hooks() {
|
|
|
127
130
|
local skipped_invalid=0
|
|
128
131
|
local skipped_lib_missing=0
|
|
129
132
|
local failed=0
|
|
133
|
+
# #1894 — capture missing-hook paths for structured status line
|
|
134
|
+
local -a missing_paths=()
|
|
135
|
+
local -a missing_scopes=()
|
|
130
136
|
|
|
131
137
|
# S-F2 LOW — dir-exists guard before loop
|
|
132
138
|
if [ ! -d "$bassclef_dir/.claude/hooks" ]; then
|
|
@@ -161,6 +167,9 @@ _sync_install_hooks() {
|
|
|
161
167
|
chmod +x "$target" 2>/dev/null || true
|
|
162
168
|
echo "$(date -u +%FT%TZ) dispatch-fail $hook_name" >> "$BASSCLEF_TRACE_LOG"
|
|
163
169
|
failed=$((failed + 1))
|
|
170
|
+
# #1894 — capture path for structured status line
|
|
171
|
+
missing_paths+=(".claude/hooks/$hook_name")
|
|
172
|
+
missing_scopes+=("project")
|
|
164
173
|
fi
|
|
165
174
|
elif [ "$has_header" = "1" ] && [ "$dispatch_available" = "0" ]; then
|
|
166
175
|
# S-F1 MED — dispatch lib missing; trace explicitly (not silent)
|
|
@@ -179,6 +188,9 @@ _sync_install_hooks() {
|
|
|
179
188
|
else
|
|
180
189
|
echo "$(date -u +%FT%TZ) skipped-invalid-header $hook_name" >> "$BASSCLEF_TRACE_LOG"
|
|
181
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")
|
|
182
194
|
fi
|
|
183
195
|
fi
|
|
184
196
|
done
|
|
@@ -195,13 +207,36 @@ _sync_install_hooks() {
|
|
|
195
207
|
breakdown="${breakdown#, }"
|
|
196
208
|
# RFC-0008 F7 — three bare numbers read three ways. Say what they mean
|
|
197
209
|
# together, and say plainly whether the install is usable.
|
|
198
|
-
if [ "$failed" = "0" ]; then
|
|
210
|
+
if [ "$failed" = "0" ] && [ "$skipped_invalid" = "0" ]; then
|
|
199
211
|
echo "bassclef-sync: $installed hooks ready. $skipped_total skipped on purpose ($breakdown). Nothing failed — your install is complete."
|
|
200
212
|
else
|
|
201
213
|
echo "bassclef-sync: $installed hooks ready. $skipped_total skipped on purpose ($breakdown)."
|
|
202
|
-
|
|
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
|
|
203
217
|
fi
|
|
204
218
|
fi
|
|
219
|
+
|
|
220
|
+
# #1894 — idempotent-complete-or-fail-loud postcondition
|
|
221
|
+
# (pre-mortem R2 fold: RESTORED iff failed==0 && skipped_invalid==0;
|
|
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
|
|
205
240
|
}
|
|
206
241
|
|
|
207
242
|
# === Test-mode guard (#915) ===
|
|
@@ -719,9 +754,11 @@ fi
|
|
|
719
754
|
MISSING=""
|
|
720
755
|
|
|
721
756
|
# Google service account (needed for journal → Google Docs)
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
757
|
+
# Ship 1 (#1816): env var lookup; operator sets BASSCLEF_GCLOUD_KEY in their shell profile.
|
|
758
|
+
# Unset = feature disabled (no adopter-visible path leak).
|
|
759
|
+
SA_KEY="${BASSCLEF_GCLOUD_KEY:-}"
|
|
760
|
+
if [ -n "$SA_KEY" ] && [ ! -f "$SA_KEY" ]; then
|
|
761
|
+
MISSING="${MISSING}\n- Google Docs service account key missing at \$BASSCLEF_GCLOUD_KEY (needed for /journal → Google Doc push)"
|
|
725
762
|
fi
|
|
726
763
|
|
|
727
764
|
# gh CLI auth (needed for /promote, issue creation)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# install-class: dual
|
|
4
|
+
# PreToolUse: Bash
|
|
5
|
+
#
|
|
6
|
+
# git-stash-u guard (bassclef-upstream#1691 Cure 2)
|
|
7
|
+
#
|
|
8
|
+
# Fires ADVISORY banner when a `git stash -u` or `git stash --include-untracked`
|
|
9
|
+
# command runs in a repo where any bassclef substrate directory is gitignored.
|
|
10
|
+
# Warns the operator (or their agent) that `-u` would stash bassclef's own
|
|
11
|
+
# untracked files, causing subsequent hook calls to fail with "No such file
|
|
12
|
+
# or directory" per .claude/bassclef-orientation.md L120-134.
|
|
13
|
+
#
|
|
14
|
+
# ADVISORY tone (exit 0; never blocks). Silent when trap conditions do not
|
|
15
|
+
# match — bare `git status`, plain `git stash` (no -u), stash subcommands
|
|
16
|
+
# (pop, apply, list), or a repo where substrate is committed.
|
|
17
|
+
#
|
|
18
|
+
# Anchor luminaries — @luminary donald-norman (signifier match: ⚠ tone),
|
|
19
|
+
# @luminary alan-cooper (Sam+Louis plain language), @luminary linus-torvalds
|
|
20
|
+
# (adopter contract; additive-only + skippable + gitignore-conditional).
|
|
21
|
+
#
|
|
22
|
+
# Override: SKIP_GIT_STASH_U_GUARD=1 bypasses for one call (logged via trace).
|
|
23
|
+
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
|
|
26
|
+
# --- Read JSON stdin ---
|
|
27
|
+
INPUT=""
|
|
28
|
+
if [ -t 0 ]; then
|
|
29
|
+
# No stdin (interactive test) — nothing to check
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
INPUT=$(cat 2>/dev/null || true)
|
|
33
|
+
[ -z "$INPUT" ] && exit 0
|
|
34
|
+
|
|
35
|
+
# --- Extract tool_input.command ---
|
|
36
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
37
|
+
# Fail-soft on missing jq (Nygard stability)
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
41
|
+
[ -z "$CMD" ] && exit 0
|
|
42
|
+
|
|
43
|
+
# --- Regex — match ONLY git stash with -u OR --include-untracked (Norman N2 fold) ---
|
|
44
|
+
# Anchor on "git stash" followed by args that include -u or --include-untracked.
|
|
45
|
+
# Excludes: bare `git stash`, `git stash pop`, `git stash apply`, `git stash list`, etc.
|
|
46
|
+
# The -u flag can appear anywhere after the `stash` verb (e.g., `git stash -u`,
|
|
47
|
+
# `git stash push -u`, `git stash -u -m "msg"`).
|
|
48
|
+
if ! echo "$CMD" | grep -qE '(^|[[:space:]])git[[:space:]]+stash([[:space:]]+push)?([[:space:]]+.*)?(-u([[:space:]]|$)|--include-untracked([[:space:]]|$))'; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# --- Override — SKIP_GIT_STASH_U_GUARD=1 bypasses (logged) ---
|
|
53
|
+
if [ "${SKIP_GIT_STASH_U_GUARD:-0}" = "1" ]; then
|
|
54
|
+
TRACE_LOG="${BASSCLEF_TRACE_LOG:-${BASSCLEF_TRACE_FILE:-$HOME/.claude/state/trace.log}}"
|
|
55
|
+
mkdir -p "$(dirname "$TRACE_LOG")" 2>/dev/null || true
|
|
56
|
+
echo "$(date -u +%FT%TZ) SKIP_GIT_STASH_U_GUARD $CMD" >> "$TRACE_LOG" 2>/dev/null || true
|
|
57
|
+
exit 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# --- Gitignore state check (Torvalds T2 fold) — fire only if substrate is gitignored ---
|
|
61
|
+
GITIGNORE=""
|
|
62
|
+
if [ -f "./.gitignore" ]; then
|
|
63
|
+
GITIGNORE=$(cat ./.gitignore 2>/dev/null || true)
|
|
64
|
+
fi
|
|
65
|
+
# Trap conditions: any of the substrate dirs listed in .gitignore
|
|
66
|
+
if ! echo "$GITIGNORE" | grep -qE '^\.claude/(hooks|skills|rules|agents|luminaries)/?[[:space:]]*$'; then
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# --- Emit ADVISORY banner (operator pick 2026-09-23c mock 2 v2B) ---
|
|
71
|
+
cat <<'ADVISORY'
|
|
72
|
+
|
|
73
|
+
⚠ Heads up — this would break bassclef.
|
|
74
|
+
|
|
75
|
+
A stash tucks changes out of sight. `-u`
|
|
76
|
+
also stashes untracked files, and bassclef's
|
|
77
|
+
files aren't committed here — so `-u` would
|
|
78
|
+
tuck them away too.
|
|
79
|
+
|
|
80
|
+
Use plain `git stash` instead.
|
|
81
|
+
|
|
82
|
+
ADVISORY
|
|
83
|
+
|
|
84
|
+
exit 0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# install-class: dual
|
|
4
|
+
# SessionStart fragment — adopter message channel (bassclef-upstream#1926)
|
|
5
|
+
#
|
|
6
|
+
# Prints a welcome message on first-run adopters (no state files present).
|
|
7
|
+
# Serves as a channel — future first-touch messages register by adding
|
|
8
|
+
# check-print blocks that read state and print when a gate matches.
|
|
9
|
+
#
|
|
10
|
+
# V1 registers one message — welcome text pointing at /onboard-repo.
|
|
11
|
+
# Fires only when ALL of these are absent: docs/whereami.md,
|
|
12
|
+
# substrate.config.md, .claude/settings.json (fresh install shape).
|
|
13
|
+
#
|
|
14
|
+
# Anchor luminaries — @luminary alan-cooper (Sam+Louis first-touch persona),
|
|
15
|
+
# @luminary donald-norman (INFO tone; no BLOCK glyph), @luminary linus-torvalds
|
|
16
|
+
# (adopter contract; additive-only + silent on onboarded installs).
|
|
17
|
+
#
|
|
18
|
+
# Override: SKIP_ADOPTER_MESSAGES=1 bypasses for one call.
|
|
19
|
+
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
# --- Override — SKIP_ADOPTER_MESSAGES=1 bypasses ---
|
|
23
|
+
if [ "${SKIP_ADOPTER_MESSAGES:-0}" = "1" ]; then
|
|
24
|
+
return 0 2>/dev/null || exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# --- Message: V1 welcome (only registered message so far) ---
|
|
28
|
+
# Gate: fresh install shape — ALL 3 state files absent.
|
|
29
|
+
_welcome_gate_matches() {
|
|
30
|
+
[ ! -f "./docs/whereami.md" ] && \
|
|
31
|
+
[ ! -f "./substrate.config.md" ] && \
|
|
32
|
+
[ ! -f "./.claude/settings.json" ]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_welcome_print() {
|
|
36
|
+
echo ""
|
|
37
|
+
echo "Welcome to bassclef."
|
|
38
|
+
echo ""
|
|
39
|
+
echo "First, run /onboard-repo — it wires hooks, writes config,"
|
|
40
|
+
echo "and creates the state files this repo needs."
|
|
41
|
+
echo ""
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# --- Channel: check each message's gate; print first that matches ---
|
|
45
|
+
if _welcome_gate_matches; then
|
|
46
|
+
_welcome_print
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
return 0 2>/dev/null || exit 0
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: standard
|
|
3
|
+
# fixtures: bassclef-upstream-self
|
|
4
|
+
# Tier 0 test for #1818 Ship 3 — fresh-install state gates on
|
|
5
|
+
# 30-metrics-staleness.sh + 80-hook-heartbeat-check.sh.
|
|
6
|
+
#
|
|
7
|
+
# Per @luminary donald-norman — a banner fires only when a real problem
|
|
8
|
+
# exists. Fresh state (state dir absent) is not broken state.
|
|
9
|
+
# Per @luminary michael-feathers — characterization tests via mktemp
|
|
10
|
+
# fixture repos; source fragment inside wrapper that provides the
|
|
11
|
+
# coordinator's helper stubs (blocked_banner, BASSCLEF_DIR).
|
|
12
|
+
#
|
|
13
|
+
# Test list per .claude/rules/test-list-discipline.md:
|
|
14
|
+
# [x] Test 1 — 30-metrics-staleness.sh: no docs/metrics/ dir → silent (no BLOCKED)
|
|
15
|
+
# [x] Test 2 — 30-metrics-staleness.sh: with docs/metrics/ dir + no latest.md → BLOCKED fires (gate does not over-scope)
|
|
16
|
+
# [x] Test 3 — 80-hook-heartbeat-check.sh: no state/markers/hook-heartbeat/ → silent (no BLOCKED)
|
|
17
|
+
# [x] Test 4 — 80-hook-heartbeat-check.sh: with state/markers/hook-heartbeat/ + aged markers past grace → BLOCKED fires
|
|
18
|
+
# [x] Test 5 — 81-hook-manifest-staleness.sh: no state/markers/hook-heartbeat/ → silent (bassclef-upstream#1896 cure)
|
|
19
|
+
# [x] Test 6 — 81-hook-manifest-staleness.sh: SELF_MODE=1 → silent (bassclef-upstream#1804 sister guard)
|
|
20
|
+
# [x] Test 7 — 80-hook-heartbeat-check.sh: dir exists + no markers → silent (#1895 fresh-install cure — was BLOCKED pre-fix)
|
|
21
|
+
# [x] Test 8 — 80-hook-heartbeat-check.sh: dir exists + fresh markers under 24h grace → silent (#1895 grace window)
|
|
22
|
+
# [x] Test 9 — 80-hook-heartbeat-check.sh: FRESH_INSTALL_GRACE_SECONDS=0 override → BLOCKED fires immediately (#1895 override path)
|
|
23
|
+
|
|
24
|
+
set -u
|
|
25
|
+
|
|
26
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
|
|
27
|
+
METRICS_FRAG="$REPO_ROOT/.claude/hooks/session-reflection.d/30-metrics-staleness.sh"
|
|
28
|
+
HB_FRAG="$REPO_ROOT/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh"
|
|
29
|
+
MS_FRAG="$REPO_ROOT/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh"
|
|
30
|
+
|
|
31
|
+
PASS=0
|
|
32
|
+
FAIL=0
|
|
33
|
+
FAILED_TESTS=()
|
|
34
|
+
|
|
35
|
+
check() {
|
|
36
|
+
local name="$1"
|
|
37
|
+
local cond="$2"
|
|
38
|
+
if eval "$cond"; then
|
|
39
|
+
echo "PASS: $name"
|
|
40
|
+
PASS=$((PASS + 1))
|
|
41
|
+
else
|
|
42
|
+
echo "FAIL: $name"
|
|
43
|
+
FAIL=$((FAIL + 1))
|
|
44
|
+
FAILED_TESTS+=("$name")
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
TMPBASE=$(mktemp -d)
|
|
49
|
+
# shellcheck disable=SC2064
|
|
50
|
+
trap "rm -rf $TMPBASE" EXIT
|
|
51
|
+
|
|
52
|
+
# Wrapper that stubs the coordinator's helpers so the fragment runs
|
|
53
|
+
# under conditions matching real session-reflection.sh invocation.
|
|
54
|
+
# blocked_banner writes to a marker file so tests can detect emission.
|
|
55
|
+
run_fragment_with_stubs() {
|
|
56
|
+
local frag="$1"
|
|
57
|
+
local fixture_dir="$2"
|
|
58
|
+
local marker_file="$3"
|
|
59
|
+
(
|
|
60
|
+
cd "$fixture_dir" || exit 99
|
|
61
|
+
# shellcheck disable=SC2317
|
|
62
|
+
blocked_banner() {
|
|
63
|
+
echo "$*" >> "$marker_file"
|
|
64
|
+
}
|
|
65
|
+
export -f blocked_banner 2>/dev/null || true
|
|
66
|
+
BASSCLEF_DIR="$REPO_ROOT"
|
|
67
|
+
export BASSCLEF_DIR
|
|
68
|
+
# Source the fragment
|
|
69
|
+
# shellcheck disable=SC1090
|
|
70
|
+
source "$frag" 2>/dev/null || true
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# === Test 1 — metrics fragment: no docs/metrics/ → silent ===
|
|
75
|
+
FIXTURE1="$TMPBASE/fixture1-no-metrics"
|
|
76
|
+
mkdir -p "$FIXTURE1"
|
|
77
|
+
cd "$FIXTURE1" && git init -q -b main && git config user.email test@bassclef.local && git config user.name test
|
|
78
|
+
mkdir -p "$FIXTURE1/scripts"
|
|
79
|
+
touch "$FIXTURE1/scripts/metrics.sh"
|
|
80
|
+
BANNER1="$TMPBASE/banner1.out"
|
|
81
|
+
run_fragment_with_stubs "$METRICS_FRAG" "$FIXTURE1" "$BANNER1"
|
|
82
|
+
if [ ! -s "$BANNER1" ]; then
|
|
83
|
+
check "Test 1 — metrics fragment silent when no docs/metrics/ dir" "true"
|
|
84
|
+
else
|
|
85
|
+
echo " banner content: $(cat $BANNER1)"
|
|
86
|
+
check "Test 1 — metrics fragment silent when no docs/metrics/ dir" "false"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# === Test 2 — metrics fragment: docs/metrics/ present + no latest.md → BLOCKED fires ===
|
|
90
|
+
FIXTURE2="$TMPBASE/fixture2-with-metrics-no-latest"
|
|
91
|
+
mkdir -p "$FIXTURE2/docs/metrics" "$FIXTURE2/scripts"
|
|
92
|
+
cd "$FIXTURE2" && git init -q -b main && git config user.email test@bassclef.local && git config user.name test
|
|
93
|
+
touch "$FIXTURE2/scripts/metrics.sh"
|
|
94
|
+
BANNER2="$TMPBASE/banner2.out"
|
|
95
|
+
run_fragment_with_stubs "$METRICS_FRAG" "$FIXTURE2" "$BANNER2"
|
|
96
|
+
if [ -s "$BANNER2" ] && grep -q "NO METRICS GENERATED" "$BANNER2"; then
|
|
97
|
+
check "Test 2 — metrics fragment fires BLOCKED when docs/metrics/ exists + no latest.md" "true"
|
|
98
|
+
else
|
|
99
|
+
echo " banner content: $(cat $BANNER2 2>/dev/null || echo empty)"
|
|
100
|
+
check "Test 2 — metrics fragment fires BLOCKED when docs/metrics/ exists + no latest.md" "false"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# === Test 3 — heartbeat fragment: no state/markers/hook-heartbeat/ → silent ===
|
|
104
|
+
FIXTURE3="$TMPBASE/fixture3-no-heartbeat-dir"
|
|
105
|
+
mkdir -p "$FIXTURE3/standards" "$FIXTURE3/lib"
|
|
106
|
+
cd "$FIXTURE3" && git init -q -b main && git config user.email test@bassclef.local && git config user.name test
|
|
107
|
+
# Copy the manifest + lib so the fragment finds them but state dir absent
|
|
108
|
+
cp "$REPO_ROOT/standards/hook-cadence.json" "$FIXTURE3/standards/" 2>/dev/null
|
|
109
|
+
cp "$REPO_ROOT/lib/hook-heartbeat.sh" "$FIXTURE3/lib/" 2>/dev/null
|
|
110
|
+
BANNER3="$TMPBASE/banner3.out"
|
|
111
|
+
SELF_MODE=0 run_fragment_with_stubs "$HB_FRAG" "$FIXTURE3" "$BANNER3"
|
|
112
|
+
if [ ! -s "$BANNER3" ]; then
|
|
113
|
+
check "Test 3 — heartbeat fragment silent when no state/markers/hook-heartbeat/ dir" "true"
|
|
114
|
+
else
|
|
115
|
+
echo " banner content: $(cat $BANNER3)"
|
|
116
|
+
check "Test 3 — heartbeat fragment silent when no state/markers/hook-heartbeat/ dir" "false"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# === Test 4 — heartbeat fragment: dir + aged markers past grace → BLOCKED fires ===
|
|
120
|
+
# Fixture: dir exists AND has an aged marker (48h old) — install is past
|
|
121
|
+
# the 24h grace window AND has real staleness (marker's referenced hook
|
|
122
|
+
# has max_age < 48h so it counts as stale).
|
|
123
|
+
FIXTURE4="$TMPBASE/fixture4-heartbeat-aged-markers"
|
|
124
|
+
mkdir -p "$FIXTURE4/standards" "$FIXTURE4/lib" "$FIXTURE4/state/markers/hook-heartbeat"
|
|
125
|
+
cd "$FIXTURE4" && git init -q -b main && git config user.email test@bassclef.local && git config user.name test
|
|
126
|
+
cp "$REPO_ROOT/standards/hook-cadence.json" "$FIXTURE4/standards/" 2>/dev/null
|
|
127
|
+
cp "$REPO_ROOT/lib/hook-heartbeat.sh" "$FIXTURE4/lib/" 2>/dev/null
|
|
128
|
+
# Write an anchor marker aged 48h old so the grace window has expired.
|
|
129
|
+
# Use bassclef-sync as the aged marker; max_age is 86400s (24h) so it counts as stale.
|
|
130
|
+
echo "2026-09-20T00:00:00Z" > "$FIXTURE4/state/markers/hook-heartbeat/bassclef-sync.marker"
|
|
131
|
+
# Portable mtime setup: BSD `date -u -v-48H` first, fallback to GNU `date -u -d`
|
|
132
|
+
old_stamp=$(date -u -v-48H +%Y%m%d%H%M 2>/dev/null || date -u -d '48 hours ago' +%Y%m%d%H%M)
|
|
133
|
+
touch -t "$old_stamp" "$FIXTURE4/state/markers/hook-heartbeat/bassclef-sync.marker"
|
|
134
|
+
BANNER4="$TMPBASE/banner4.out"
|
|
135
|
+
SELF_MODE=0 run_fragment_with_stubs "$HB_FRAG" "$FIXTURE4" "$BANNER4"
|
|
136
|
+
if [ -s "$BANNER4" ] && grep -q "hook liveness" "$BANNER4"; then
|
|
137
|
+
check "Test 4 — heartbeat fragment fires BLOCKED when dir + aged markers past grace" "true"
|
|
138
|
+
else
|
|
139
|
+
echo " banner content: $(cat $BANNER4 2>/dev/null || echo empty)"
|
|
140
|
+
check "Test 4 — heartbeat fragment fires BLOCKED when dir + aged markers past grace" "false"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# === Test 5 — 81 fragment: no state/markers/hook-heartbeat/ → silent (bassclef-upstream#1896 cure) ===
|
|
144
|
+
FIXTURE5="$TMPBASE/fixture5-no-heartbeat-dir-ms"
|
|
145
|
+
mkdir -p "$FIXTURE5/standards" "$FIXTURE5/.claude/hooks"
|
|
146
|
+
cd "$FIXTURE5" && git init -q -b main && git config user.email test@bassclef.local && git config user.name test
|
|
147
|
+
# Copy manifest + a new hook that would flag as "not in manifest" without the gate
|
|
148
|
+
cp "$REPO_ROOT/standards/hook-cadence.json" "$FIXTURE5/standards/" 2>/dev/null
|
|
149
|
+
touch "$FIXTURE5/.claude/hooks/some-uncatalogued-hook.sh"
|
|
150
|
+
BANNER5="$TMPBASE/banner5.out"
|
|
151
|
+
SELF_MODE=0 run_fragment_with_stubs "$MS_FRAG" "$FIXTURE5" "$BANNER5"
|
|
152
|
+
if [ ! -s "$BANNER5" ]; then
|
|
153
|
+
check "Test 5 — 81 fragment silent when no state/markers/hook-heartbeat/ dir (#1896 cure)" "true"
|
|
154
|
+
else
|
|
155
|
+
echo " banner content: $(cat $BANNER5)"
|
|
156
|
+
check "Test 5 — 81 fragment silent when no state/markers/hook-heartbeat/ dir (#1896 cure)" "false"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# === Test 6 — 81 fragment: SELF_MODE=1 → silent (bassclef-upstream#1804 sister guard) ===
|
|
160
|
+
FIXTURE6="$TMPBASE/fixture6-selfmode"
|
|
161
|
+
mkdir -p "$FIXTURE6/standards" "$FIXTURE6/.claude/hooks" "$FIXTURE6/state/markers/hook-heartbeat"
|
|
162
|
+
cd "$FIXTURE6" && git init -q -b main && git config user.email test@bassclef.local && git config user.name test
|
|
163
|
+
cp "$REPO_ROOT/standards/hook-cadence.json" "$FIXTURE6/standards/" 2>/dev/null
|
|
164
|
+
touch "$FIXTURE6/.claude/hooks/some-uncatalogued-hook.sh"
|
|
165
|
+
BANNER6="$TMPBASE/banner6.out"
|
|
166
|
+
SELF_MODE=1 run_fragment_with_stubs "$MS_FRAG" "$FIXTURE6" "$BANNER6"
|
|
167
|
+
if [ ! -s "$BANNER6" ]; then
|
|
168
|
+
check "Test 6 — 81 fragment silent under SELF_MODE=1 (#1804 sister guard)" "true"
|
|
169
|
+
else
|
|
170
|
+
echo " banner content: $(cat $BANNER6)"
|
|
171
|
+
check "Test 6 — 81 fragment silent under SELF_MODE=1 (#1804 sister guard)" "false"
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
# === Test 7 — heartbeat fragment: dir exists + no markers → silent (#1895 fresh-install cure) ===
|
|
175
|
+
# Fixture: dir mkdir'd but no producer has ever fired heartbeat_mark. This is
|
|
176
|
+
# the exact self-defeat class from #1895 — dir gets created by first hook fire,
|
|
177
|
+
# but at 4 min old install with only 1 fire, remaining 6 hooks show "never fired."
|
|
178
|
+
# Cure: grace window (24h default) suppresses banner on fresh install.
|
|
179
|
+
FIXTURE7="$TMPBASE/fixture7-heartbeat-dir-no-markers"
|
|
180
|
+
mkdir -p "$FIXTURE7/standards" "$FIXTURE7/lib" "$FIXTURE7/state/markers/hook-heartbeat"
|
|
181
|
+
cd "$FIXTURE7" && git init -q -b main && git config user.email test@bassclef.local && git config user.name test
|
|
182
|
+
cp "$REPO_ROOT/standards/hook-cadence.json" "$FIXTURE7/standards/" 2>/dev/null
|
|
183
|
+
cp "$REPO_ROOT/lib/hook-heartbeat.sh" "$FIXTURE7/lib/" 2>/dev/null
|
|
184
|
+
# dir EXISTS but no markers — pre-cure this triggers BLOCKED; post-cure silent under grace
|
|
185
|
+
BANNER7="$TMPBASE/banner7.out"
|
|
186
|
+
SELF_MODE=0 run_fragment_with_stubs "$HB_FRAG" "$FIXTURE7" "$BANNER7"
|
|
187
|
+
if [ ! -s "$BANNER7" ]; then
|
|
188
|
+
check "Test 7 — heartbeat fragment silent when dir exists + no markers (#1895 cure)" "true"
|
|
189
|
+
else
|
|
190
|
+
echo " banner content: $(cat $BANNER7)"
|
|
191
|
+
check "Test 7 — heartbeat fragment silent when dir exists + no markers (#1895 cure)" "false"
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# === Test 8 — heartbeat fragment: dir + fresh markers under 24h grace → silent (#1895 grace window) ===
|
|
195
|
+
# Fixture: dir exists AND has 1 fresh marker (just written) — install is
|
|
196
|
+
# actively booting. Grace window should suppress banner even though other
|
|
197
|
+
# critical hooks show "never fired."
|
|
198
|
+
FIXTURE8="$TMPBASE/fixture8-heartbeat-fresh-marker"
|
|
199
|
+
mkdir -p "$FIXTURE8/standards" "$FIXTURE8/lib" "$FIXTURE8/state/markers/hook-heartbeat"
|
|
200
|
+
cd "$FIXTURE8" && git init -q -b main && git config user.email test@bassclef.local && git config user.name test
|
|
201
|
+
cp "$REPO_ROOT/standards/hook-cadence.json" "$FIXTURE8/standards/" 2>/dev/null
|
|
202
|
+
cp "$REPO_ROOT/lib/hook-heartbeat.sh" "$FIXTURE8/lib/" 2>/dev/null
|
|
203
|
+
# Write a fresh marker (now) — grace window should hold.
|
|
204
|
+
date -u +%Y-%m-%dT%H:%M:%SZ > "$FIXTURE8/state/markers/hook-heartbeat/bassclef-sync.marker"
|
|
205
|
+
BANNER8="$TMPBASE/banner8.out"
|
|
206
|
+
SELF_MODE=0 run_fragment_with_stubs "$HB_FRAG" "$FIXTURE8" "$BANNER8"
|
|
207
|
+
if [ ! -s "$BANNER8" ]; then
|
|
208
|
+
check "Test 8 — heartbeat fragment silent when dir + fresh markers under 24h grace (#1895)" "true"
|
|
209
|
+
else
|
|
210
|
+
echo " banner content: $(cat $BANNER8)"
|
|
211
|
+
check "Test 8 — heartbeat fragment silent when dir + fresh markers under 24h grace (#1895)" "false"
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# === Test 9 — heartbeat fragment: FRESH_INSTALL_GRACE_SECONDS=0 override → BLOCKED fires ===
|
|
215
|
+
# Fixture: same shape as Test 7 (dir + no markers) but grace window disabled.
|
|
216
|
+
# Override path exercised — operator can bypass grace to test the check itself.
|
|
217
|
+
FIXTURE9="$TMPBASE/fixture9-heartbeat-grace-override"
|
|
218
|
+
mkdir -p "$FIXTURE9/standards" "$FIXTURE9/lib" "$FIXTURE9/state/markers/hook-heartbeat"
|
|
219
|
+
cd "$FIXTURE9" && git init -q -b main && git config user.email test@bassclef.local && git config user.name test
|
|
220
|
+
cp "$REPO_ROOT/standards/hook-cadence.json" "$FIXTURE9/standards/" 2>/dev/null
|
|
221
|
+
cp "$REPO_ROOT/lib/hook-heartbeat.sh" "$FIXTURE9/lib/" 2>/dev/null
|
|
222
|
+
BANNER9="$TMPBASE/banner9.out"
|
|
223
|
+
FRESH_INSTALL_GRACE_SECONDS=0 SELF_MODE=0 run_fragment_with_stubs "$HB_FRAG" "$FIXTURE9" "$BANNER9"
|
|
224
|
+
if [ -s "$BANNER9" ] && grep -q "hook liveness" "$BANNER9"; then
|
|
225
|
+
check "Test 9 — heartbeat fragment fires BLOCKED when FRESH_INSTALL_GRACE_SECONDS=0 (#1895 override)" "true"
|
|
226
|
+
else
|
|
227
|
+
echo " banner content: $(cat $BANNER9 2>/dev/null || echo empty)"
|
|
228
|
+
check "Test 9 — heartbeat fragment fires BLOCKED when FRESH_INSTALL_GRACE_SECONDS=0 (#1895 override)" "false"
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
# --- Summary ---
|
|
232
|
+
echo ""
|
|
233
|
+
echo "Tests passed: $PASS"
|
|
234
|
+
echo "Tests failed: $FAIL"
|
|
235
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
236
|
+
echo "Failed tests:"
|
|
237
|
+
for t in "${FAILED_TESTS[@]}"; do
|
|
238
|
+
echo " - $t"
|
|
239
|
+
done
|
|
240
|
+
exit 1
|
|
241
|
+
fi
|
|
242
|
+
exit 0
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
"type": "command",
|
|
48
48
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-commit-identifier-leak-scrub.sh",
|
|
49
49
|
"timeout": 10
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/git-stash-u-guard.sh",
|
|
54
|
+
"timeout": 5
|
|
50
55
|
}
|
|
51
56
|
]
|
|
52
57
|
},
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
tier: lite
|
|
3
3
|
name: build
|
|
4
|
-
description: "Take a
|
|
4
|
+
description: "Take a plan from /launch and ship it. One builder per user story. Runs /verify. Opens PRs. Gates prod on a human. Refuses auth, schema, security, and tenant work. Two modes — full and degrade-with-note."
|
|
5
5
|
problem: "A buildable plan sits on disk. Nothing ships until someone writes code and PRs and gates prod."
|
|
6
6
|
value: "Reads the plan and ships it. Runs the whole cycle so you can watch instead of type."
|
|
7
7
|
inputs: [A goal slug pointing to a spec on disk]
|
|
8
8
|
outputs: [One PR per user story, Verify report, Reviewer sign-off, Prod deploy gated on a human]
|
|
9
9
|
user_invocable: true
|
|
10
10
|
disable_model_invocation: false
|
|
11
|
+
modes: [full, degrade-with-note]
|
|
11
12
|
requires:
|
|
12
13
|
cli: [git, gh]
|
|
13
14
|
network: true
|
|
@@ -104,8 +105,63 @@ Open every /build invocation with:
|
|
|
104
105
|
|
|
105
106
|
Walls of structured Phase 0-7 detail come AFTER this block.
|
|
106
107
|
|
|
108
|
+
### Phase −1a — Capability probe (per bassclef-upstream#1909)
|
|
109
|
+
|
|
110
|
+
/build gets two modes: **full** and **degrade-with-note**. Per RFC B1 folded (`docs/rfcs/2026-09-23-graceful-degrade-adversarial-review.md`), a third `scratch` mode would collide with the Phase 0 blast-radius floor — /build must never run in a repo it cannot audit for auth, schema, security, or tenant work.
|
|
111
|
+
|
|
112
|
+
- **full** — git repo + gh auth + Playwright MCP + network. Full chain runs through Phase 7.
|
|
113
|
+
- **degrade-with-note** — one or more capabilities missing. /build emits the spec and a plain-language note ("cd into a git repo with gh auth before /build") and exits. No PR attempt.
|
|
114
|
+
|
|
115
|
+
The blast-radius floor at Phase 0 is not overridable by mode dispatch (spec Req 10 per `docs/specs/2026-09-23-graceful-degrade-skills.md`). The floor holds forever.
|
|
116
|
+
|
|
117
|
+
The mode picker short-circuits `probe_network` when other probes already downgrade the mode (RFC C2 folded).
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
source "${BASSCLEF_DIR:-$REPO_ROOT}/lib/capability-probe.sh"
|
|
121
|
+
|
|
122
|
+
probe_git; GIT=$?
|
|
123
|
+
probe_mcp_playwright; MCP=$?
|
|
124
|
+
probe_gh_auth; GH=$?
|
|
125
|
+
|
|
126
|
+
if [ $GIT -eq 0 ] && [ $MCP -eq 0 ] && [ $GH -eq 0 ]; then
|
|
127
|
+
probe_network; NET=$?
|
|
128
|
+
else
|
|
129
|
+
NET=1
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
G=$([ $GIT -eq 0 ] && echo 1 || echo 0)
|
|
133
|
+
M=$([ $MCP -eq 0 ] && echo 1 || echo 0)
|
|
134
|
+
H=$([ $GH -eq 0 ] && echo 1 || echo 0)
|
|
135
|
+
N=$([ $NET -eq 0 ] && echo 1 || echo 0)
|
|
136
|
+
|
|
137
|
+
# /build collapses the three-value probe into its two modes:
|
|
138
|
+
# full requires all four; anything else is degrade-with-note.
|
|
139
|
+
LIB_MODE=$(resolve_mode "$G" "$H" "$M" "$N")
|
|
140
|
+
if [ "$LIB_MODE" = "full" ]; then
|
|
141
|
+
MODE="full"
|
|
142
|
+
else
|
|
143
|
+
MODE="degrade-with-note"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
announce_mode "$MODE" "/build"
|
|
147
|
+
|
|
148
|
+
if [ "$MODE" = "degrade-with-note" ]; then
|
|
149
|
+
echo "" >&2
|
|
150
|
+
echo "/build: environment does not support the full chain." >&2
|
|
151
|
+
echo " - git present? $([ $GIT -eq 0 ] && echo yes || echo no)" >&2
|
|
152
|
+
echo " - gh authed? $([ $GH -eq 0 ] && echo yes || echo no)" >&2
|
|
153
|
+
echo " - Playwright MCP loaded? $([ $MCP -eq 0 ] && echo yes || echo no)" >&2
|
|
154
|
+
echo " - network reachable? $([ $NET -eq 0 ] && echo yes || echo no)" >&2
|
|
155
|
+
echo "" >&2
|
|
156
|
+
echo "Emitting the spec at docs/specs/<slug>.md. cd into a git repo with gh auth to run the full chain." >&2
|
|
157
|
+
exit 0
|
|
158
|
+
fi
|
|
159
|
+
```
|
|
160
|
+
|
|
107
161
|
### Phase -1 — Pre-flight (per bassclef#408)
|
|
108
162
|
|
|
163
|
+
Fires only when Phase −1a picked `full` mode. In `degrade-with-note` mode the skill already exited at Phase −1a.
|
|
164
|
+
|
|
109
165
|
Before Phase 0 (which is /build's blast-radius floor refusal), source
|
|
110
166
|
the shared pre-flight helper to surface workflow prerequisites:
|
|
111
167
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
tier: lite
|
|
3
3
|
name: launch
|
|
4
|
-
description: "Compare variants of an idea.
|
|
4
|
+
description: "Compare variants of an idea. Get a mock gallery. Pick a direction. Get a spec, user stories, and a plan to build. Two sizes \u2014 medium and full. Three modes \u2014 full, local, scratch."
|
|
5
5
|
problem: "An idea is a doc. A working feature is code. The gap in between is where projects stall."
|
|
6
6
|
value: "Turns an idea into a buildable plan \u2014 mock gallery and spec and INVEST stories. Ready for /build."
|
|
7
7
|
inputs: [A rough idea or ticket or canvas]
|
|
@@ -9,6 +9,7 @@ outputs: [Clickable mock gallery, Buildable spec, INVEST user stories, GRASP res
|
|
|
9
9
|
user_invocable: true
|
|
10
10
|
disable_model_invocation: false
|
|
11
11
|
sizes: [medium, full]
|
|
12
|
+
modes: [full, local, scratch]
|
|
12
13
|
composes_baseline: anthropic-frontend-design
|
|
13
14
|
---
|
|
14
15
|
|
|
@@ -97,8 +98,49 @@ Per `.claude/rules/adr-discipline.md`. Mechanism: `adr-discipline-check.sh` fire
|
|
|
97
98
|
|
|
98
99
|
## Procedure
|
|
99
100
|
|
|
101
|
+
### Phase −1a — Capability probe (per bassclef-upstream#1909)
|
|
102
|
+
|
|
103
|
+
Source `lib/capability-probe.sh` and pick a mode. Three modes: **full**, **local**, **scratch**. Modes are orthogonal to the existing `--brownfield`, `--reuse-artifact`, and `--local` deploy flags (RFC H3 folded per `docs/rfcs/2026-09-23-graceful-degrade-adversarial-review.md`).
|
|
104
|
+
|
|
105
|
+
- **full** — git repo + gh auth + Playwright MCP + network. Full chain runs.
|
|
106
|
+
- **local** — git repo present. No PR, no screenshots. Composer-preflight skips the deploy-host check.
|
|
107
|
+
- **scratch** — no git repo. Writes to a tmp dir. Composer-preflight skips entirely; only Phase 1 (interpret-input) and Phase 4 (riff-prototypes) run.
|
|
108
|
+
|
|
109
|
+
The mode picker short-circuits `probe_network` when other probes already downgrade the mode below `full` (RFC C2 folded).
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
source "${BASSCLEF_DIR:-$REPO_ROOT}/lib/capability-probe.sh"
|
|
113
|
+
|
|
114
|
+
probe_git; GIT=$?
|
|
115
|
+
probe_mcp_playwright; MCP=$?
|
|
116
|
+
probe_gh_auth; GH=$?
|
|
117
|
+
|
|
118
|
+
if [ $GIT -eq 0 ] && [ $MCP -eq 0 ] && [ $GH -eq 0 ]; then
|
|
119
|
+
probe_network; NET=$?
|
|
120
|
+
else
|
|
121
|
+
NET=1
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
G=$([ $GIT -eq 0 ] && echo 1 || echo 0)
|
|
125
|
+
M=$([ $MCP -eq 0 ] && echo 1 || echo 0)
|
|
126
|
+
H=$([ $GH -eq 0 ] && echo 1 || echo 0)
|
|
127
|
+
N=$([ $NET -eq 0 ] && echo 1 || echo 0)
|
|
128
|
+
|
|
129
|
+
MODE="${OVERRIDE_MODE:-$(resolve_mode "$G" "$H" "$M" "$N")}"
|
|
130
|
+
validate_mode "$MODE" "$G" "$H" "$M" "$N" || exit 1
|
|
131
|
+
announce_mode "$MODE" "/launch"
|
|
132
|
+
|
|
133
|
+
if [ "$MODE" = "scratch" ]; then
|
|
134
|
+
echo "launch: scratch mode — Phase −1 (composer prerequisites) skipped; no git repo, no deploy chain" >&2
|
|
135
|
+
fi
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Operator override reads `--mode=<name>` from argv. The parser validates the requested mode against the environment before dispatch. Requested `full` without gh auth exits with a plain-language error.
|
|
139
|
+
|
|
100
140
|
### Phase -1 — Pre-flight (per bassclef#408 + bassclef#449/#450)
|
|
101
141
|
|
|
142
|
+
Runs only when Phase −1a picked `full` or `local`. Skipped entirely in `scratch` mode.
|
|
143
|
+
|
|
102
144
|
Source the shared pre-flight helper and surface all 6 prerequisites
|
|
103
145
|
upfront. Pass `--no-launch` if the dispatch is variants-only (no
|
|
104
146
|
`/launch-preview` chain expected); the deploy-host check (Check 6)
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: riff
|
|
3
|
-
description: "Produce 2
|
|
4
|
-
problem: "Design
|
|
5
|
-
value: "Two or three
|
|
3
|
+
description: "Produce 2 or 3 clickable mock variants. Each pins a luminary lens. Three modes — full, local, scratch. Full uses git, gh, and screenshots. Local writes to disk only. Scratch uses a tmp dir with no git."
|
|
4
|
+
problem: "Design work gets one variant. No comparison. The operator picks blind. Rework is heavy."
|
|
5
|
+
value: "Two or three mocks per surface. Each mock pins a luminary lens. Cold adopters see a result in five minutes."
|
|
6
6
|
inputs: [A UI surface intent or design goal]
|
|
7
7
|
outputs: [2-3 clickable mock variants, Luminary lens per variant]
|
|
8
8
|
tier: lite
|
|
9
9
|
user_invocable: true
|
|
10
10
|
disable_model_invocation: false
|
|
11
|
+
modes: [full, local, scratch]
|
|
11
12
|
composes_with: [interpret-input, luminary, visual-review]
|
|
12
13
|
---
|
|
13
14
|
|
|
@@ -70,15 +71,55 @@ Standard adopters can bypass `/riff`. They invoke `/stage` directly for the full
|
|
|
70
71
|
|
|
71
72
|
Per canvas `docs/canvases/2026-08-02-porcelain-plumbing-reframe.md` — 10 of 10 luminaries agreed on the reframe.
|
|
72
73
|
|
|
74
|
+
## Modes (per bassclef-upstream#1909)
|
|
75
|
+
|
|
76
|
+
Three modes let the skill run in any environment:
|
|
77
|
+
|
|
78
|
+
- **full** — git repo + gh auth + Playwright MCP + network. Ships PR + screenshots.
|
|
79
|
+
- **local** — git repo present. No PR. No screenshots. Artifacts land on disk.
|
|
80
|
+
- **scratch** — no git. Writes to a tmp dir. No PR. No screenshots. Fastest path for cold adopters.
|
|
81
|
+
|
|
82
|
+
Cold adopters typing `/riff` on a fresh Saturday hit **local** or **scratch** by default. They see a viewable result. No BLOCK on missing MCP.
|
|
83
|
+
|
|
84
|
+
The mode picker sits at Phase −1. It reads four probes from `lib/capability-probe.sh`. It picks the highest-fidelity mode the environment supports. Operator can override with `--mode=<name>`. The picker validates the requested mode's requirements. If a required capability is missing, the parser exits with a plain-language error.
|
|
85
|
+
|
|
73
86
|
## Procedure
|
|
74
87
|
|
|
88
|
+
0. **Phase −1 — pick the mode.** Source the probe lib. Read four capability probes. Resolve a mode. Announce it. This step short-circuits `probe_network` when the other three probes already downgrade the mode below `full` (RFC C2 folded).
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
source "${BASSCLEF_DIR:-$REPO_ROOT}/lib/capability-probe.sh"
|
|
92
|
+
|
|
93
|
+
probe_git; GIT=$?
|
|
94
|
+
probe_mcp_playwright; MCP=$?
|
|
95
|
+
probe_gh_auth; GH=$?
|
|
96
|
+
|
|
97
|
+
# Short-circuit: only probe network when other capabilities support full mode
|
|
98
|
+
if [ $GIT -eq 0 ] && [ $MCP -eq 0 ] && [ $GH -eq 0 ]; then
|
|
99
|
+
probe_network; NET=$?
|
|
100
|
+
else
|
|
101
|
+
NET=1
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# Convert exit codes to booleans (0 = present)
|
|
105
|
+
G=$([ $GIT -eq 0 ] && echo 1 || echo 0)
|
|
106
|
+
M=$([ $MCP -eq 0 ] && echo 1 || echo 0)
|
|
107
|
+
H=$([ $GH -eq 0 ] && echo 1 || echo 0)
|
|
108
|
+
N=$([ $NET -eq 0 ] && echo 1 || echo 0)
|
|
109
|
+
|
|
110
|
+
# Operator override reads --mode=<name>; parser validates before dispatch
|
|
111
|
+
MODE="${OVERRIDE_MODE:-$(resolve_mode "$G" "$H" "$M" "$N")}"
|
|
112
|
+
validate_mode "$MODE" "$G" "$H" "$M" "$N" || exit 1
|
|
113
|
+
announce_mode "$MODE" "/riff"
|
|
114
|
+
```
|
|
115
|
+
|
|
75
116
|
1. Parse the operator's input via `/interpret-input`. Output: normalized InputArtifact.
|
|
76
117
|
2. Pick 2-3 luminary lenses. Either operator-named or via `/luminary` picker.
|
|
77
|
-
3. For each lens: author a static HTML mock. Path: `docs/prototypes/YYYY-MM-DD-riff-<slug>-variant-N/index.html`. Stubbed data. Include a `<h2>` naming the lens.
|
|
78
|
-
4.
|
|
79
|
-
5. Assemble a gallery page. Link every variant. Show screenshots inline. Show luminary tags.
|
|
80
|
-
6. Run `/visual-review`
|
|
81
|
-
7. Present the gallery URL to the operator.
|
|
118
|
+
3. For each lens: author a static HTML mock. Path in `full` and `local` modes: `docs/prototypes/YYYY-MM-DD-riff-<slug>-variant-N/index.html`. Path in `scratch` mode: `${TMPDIR:-/tmp}/riff-<slug>-variant-N/index.html`. Stubbed data. Include a `<h2>` naming the lens.
|
|
119
|
+
4. **Screenshots — mode-gated.** In `full` mode, screenshot each variant via Playwright MCP and save under `.playwright-mcp/`. In `local` and `scratch` modes, skip this step and note in the gallery output that screenshots are absent because the environment did not have Playwright MCP.
|
|
120
|
+
5. Assemble a gallery page. Link every variant. Show screenshots inline when present. Show luminary tags in every mode.
|
|
121
|
+
6. Run `/visual-review` in `full` mode. In `local` and `scratch` modes, run the self-review checklist against each variant instead. Report findings per variant.
|
|
122
|
+
7. Present the gallery URL to the operator. In `full` mode, open a PR. In `local` mode, print the disk path. In `scratch` mode, print the tmp path and a note that the gallery does not survive reboot. Picked variant flows into `/build` or `/launch` at standard.
|
|
82
123
|
|
|
83
124
|
## Baseline composition
|
|
84
125
|
|
|
@@ -93,11 +134,12 @@ Per canvas `docs/canvases/2026-08-02-porcelain-plumbing-reframe.md` — 10 of 10
|
|
|
93
134
|
|
|
94
135
|
## Rules
|
|
95
136
|
|
|
96
|
-
- MUST produce a PR. Push to a feature branch, not main.
|
|
97
|
-
- MUST run on a feature branch.
|
|
98
|
-
- MUST screenshot every variant via Playwright MCP.
|
|
99
|
-
- MUST tag each variant with its luminary lens in the HTML.
|
|
100
|
-
- MUST report per-variant `/visual-review` findings
|
|
137
|
+
- MUST produce a PR when running in `full` mode. Push to a feature branch, not main.
|
|
138
|
+
- MUST run on a feature branch when git is present. Scratch mode has no branch.
|
|
139
|
+
- MUST screenshot every variant via Playwright MCP when running in `full` mode. In `local` and `scratch` modes, skip screenshots and note the reason in the gallery output. (Per bassclef-upstream#1909 — the prior BLOCK broke cold adopters.)
|
|
140
|
+
- MUST tag each variant with its luminary lens in the HTML in every mode.
|
|
141
|
+
- MUST report per-variant `/visual-review` findings in `full` mode. In `local` and `scratch` modes, run the self-review checklist and report findings the same way.
|
|
142
|
+
- MUST print the mode announcement line at Phase −1. STDOUT stays untouched; the line prints to STDERR so parsers stay clean (RFC R1 folded per `docs/risk-ledgers/2026-09-23-graceful-degrade.md`).
|
|
101
143
|
|
|
102
144
|
## Refs
|
|
103
145
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "v1.
|
|
3
|
-
"marker_tag": "release-2026-09-
|
|
4
|
-
"release_date": "2026-09-
|
|
5
|
-
"release_sha": "
|
|
6
|
-
"release_tag": "release-2026-09-
|
|
2
|
+
"version": "v1.5.1",
|
|
3
|
+
"marker_tag": "release-2026-09-24-c4fa9e65",
|
|
4
|
+
"release_date": "2026-09-24T16:59:30Z",
|
|
5
|
+
"release_sha": "c4fa9e65",
|
|
6
|
+
"release_tag": "release-2026-09-24-c4fa9e65"
|
|
7
7
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# lib/capability-probe.sh — capability probe deep module.
|
|
4
|
+
#
|
|
5
|
+
# Per @luminary john-ousterhout deep module: hides several probe strategies
|
|
6
|
+
# behind a narrow interface. Downstream SKILLs source this lib and ask what
|
|
7
|
+
# mode fits the environment. Probe strategies can change without touching
|
|
8
|
+
# any SKILL.
|
|
9
|
+
#
|
|
10
|
+
# Exposed functions:
|
|
11
|
+
# probe_git() — 0 inside git repo, 1 otherwise
|
|
12
|
+
# probe_gh_auth() — 0 when authenticated to GitHub, 1 otherwise
|
|
13
|
+
# probe_mcp_playwright() — 0 when Playwright MCP loaded, 1 otherwise
|
|
14
|
+
# probe_network() — 0 when network reachable, 1 otherwise (or OFFLINE=1)
|
|
15
|
+
# resolve_mode g gh m n — echoes "full" | "local" | "scratch"
|
|
16
|
+
# validate_mode m g gh m n — 0 if mode requirements met, 1 otherwise
|
|
17
|
+
# announce_mode m skill — prints "<skill>: running in <m> mode — <reason>" to stderr
|
|
18
|
+
#
|
|
19
|
+
# Per bassclef-upstream#1909 + spec docs/specs/2026-09-23-graceful-degrade-skills.md.
|
|
20
|
+
# Every probe fail-soft. Never throws.
|
|
21
|
+
|
|
22
|
+
# --- Individual probes -------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
probe_git() {
|
|
25
|
+
# Short-circuit if git binary absent
|
|
26
|
+
command -v git >/dev/null 2>&1 || return 1
|
|
27
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
28
|
+
return 0
|
|
29
|
+
fi
|
|
30
|
+
return 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
probe_gh_auth() {
|
|
34
|
+
# Fast path: token env vars indicate auth intent + presence.
|
|
35
|
+
if [ -n "${GH_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then
|
|
36
|
+
return 0
|
|
37
|
+
fi
|
|
38
|
+
# Slow path: gh CLI must be present + authenticated.
|
|
39
|
+
command -v gh >/dev/null 2>&1 || return 1
|
|
40
|
+
if gh auth status >/dev/null 2>&1; then
|
|
41
|
+
return 0
|
|
42
|
+
fi
|
|
43
|
+
return 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
probe_mcp_playwright() {
|
|
47
|
+
# V1 heuristic — env var contract. V2 may read tool inventory.
|
|
48
|
+
[ -n "${CLAUDE_MCP_PLAYWRIGHT:-}" ] && return 0
|
|
49
|
+
return 1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
probe_network() {
|
|
53
|
+
# Short-circuit: OFFLINE=1 skips the wire call entirely.
|
|
54
|
+
[ -n "${OFFLINE:-}" ] && return 1
|
|
55
|
+
command -v curl >/dev/null 2>&1 || return 1
|
|
56
|
+
if curl -sSf --max-time 2 "${BASSCLEF_NETWORK_PROBE_URL:-https://api.github.com/zen}" >/dev/null 2>&1; then
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
return 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# --- Mode resolution ---------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
# resolve_mode <git> <gh> <mcp> <network>
|
|
65
|
+
# Each arg is "0" or "1" per the probe result convention (1 = capability present).
|
|
66
|
+
# Prints one of: "full" | "local" | "scratch".
|
|
67
|
+
resolve_mode() {
|
|
68
|
+
local git="${1:-0}" gh="${2:-0}" mcp="${3:-0}" net="${4:-0}"
|
|
69
|
+
if [ "$git" = "1" ] && [ "$gh" = "1" ] && [ "$mcp" = "1" ] && [ "$net" = "1" ]; then
|
|
70
|
+
echo "full"
|
|
71
|
+
elif [ "$git" = "1" ]; then
|
|
72
|
+
echo "local"
|
|
73
|
+
else
|
|
74
|
+
echo "scratch"
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# validate_mode <mode> <git> <gh> <mcp> <net>
|
|
79
|
+
# Returns 0 when the mode's requirements are met, 1 otherwise.
|
|
80
|
+
# Stderr names the missing capability on failure.
|
|
81
|
+
validate_mode() {
|
|
82
|
+
local mode="${1:-}" git="${2:-0}" gh="${3:-0}" mcp="${4:-0}" net="${5:-0}"
|
|
83
|
+
case "$mode" in
|
|
84
|
+
full)
|
|
85
|
+
[ "$git" = "1" ] || { echo "validate_mode: full requires git (not present)" >&2; return 1; }
|
|
86
|
+
[ "$gh" = "1" ] || { echo "validate_mode: full requires gh auth (not present)" >&2; return 1; }
|
|
87
|
+
[ "$mcp" = "1" ] || { echo "validate_mode: full requires MCP (CLAUDE_MCP_PLAYWRIGHT not set)" >&2; return 1; }
|
|
88
|
+
[ "$net" = "1" ] || { echo "validate_mode: full requires network (probe failed)" >&2; return 1; }
|
|
89
|
+
return 0
|
|
90
|
+
;;
|
|
91
|
+
local)
|
|
92
|
+
[ "$git" = "1" ] || { echo "validate_mode: local requires git (not present)" >&2; return 1; }
|
|
93
|
+
return 0
|
|
94
|
+
;;
|
|
95
|
+
scratch)
|
|
96
|
+
return 0
|
|
97
|
+
;;
|
|
98
|
+
*)
|
|
99
|
+
echo "validate_mode: '$mode' is not a valid mode (expected: full | local | scratch)" >&2
|
|
100
|
+
return 1
|
|
101
|
+
;;
|
|
102
|
+
esac
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# --- Announcement ------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
# announce_mode <mode> <skill-name>
|
|
108
|
+
# Prints one line to stderr naming the picked mode and the reason.
|
|
109
|
+
# Honors SKIP_MODE_ANNOUNCE=1 to suppress the line (useful for scripted runs).
|
|
110
|
+
# STDOUT is untouched — parsers relying on STDOUT stay clean (R1 folded).
|
|
111
|
+
announce_mode() {
|
|
112
|
+
[ -n "${SKIP_MODE_ANNOUNCE:-}" ] && return 0
|
|
113
|
+
local mode="${1:-}" skill="${2:-}"
|
|
114
|
+
local reason
|
|
115
|
+
case "$mode" in
|
|
116
|
+
full) reason="all capabilities present" ;;
|
|
117
|
+
local) reason="one or more of gh, MCP, or network missing; disk-only" ;;
|
|
118
|
+
scratch) reason="no git repo detected; using scratch dir" ;;
|
|
119
|
+
*) reason="unknown mode" ;;
|
|
120
|
+
esac
|
|
121
|
+
echo "${skill}: running in \`${mode}\` mode — ${reason}. Say \`--mode=<other>\` to override." >&2
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# --- Lib self-check on source ------------------------------------------------
|
|
125
|
+
# Per R13 folded (Nygard stability): a broken lib should not silently return
|
|
126
|
+
# FALSE for everything. Emit a WARN if any required function is missing.
|
|
127
|
+
__bassclef_capability_probe_self_check() {
|
|
128
|
+
local missing=""
|
|
129
|
+
local f
|
|
130
|
+
for f in probe_git probe_gh_auth probe_mcp_playwright probe_network resolve_mode validate_mode announce_mode; do
|
|
131
|
+
type -t "$f" >/dev/null 2>&1 || missing="$missing $f"
|
|
132
|
+
done
|
|
133
|
+
if [ -n "$missing" ]; then
|
|
134
|
+
echo "WARN: lib/capability-probe.sh is missing functions:$missing" >&2
|
|
135
|
+
return 1
|
|
136
|
+
fi
|
|
137
|
+
return 0
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
__bassclef_capability_probe_self_check || true
|
|
@@ -256,6 +256,42 @@ bassclef_sync_parse_whereami_ts() {
|
|
|
256
256
|
return 0
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
# === #1929 — postcondition emitter (RESTORED/DEGRADED + JSON) ===
|
|
260
|
+
# Mirrors .claude/hooks/bassclef-sync.sh L215-243 (idempotent-complete-or-fail-loud
|
|
261
|
+
# pattern shipped under #1894 Cure 1 of #1691). Reads globals INSTALLED_COUNT,
|
|
262
|
+
# FAILED_COUNT, SKIPPED_INVALID, SKIPPED_TOTAL, missing_paths[], missing_scopes[]
|
|
263
|
+
# set by the install loop below. Emits exactly one human line + one JSON status
|
|
264
|
+
# line. Defined ABOVE the BASSCLEF_SYNC_TEST_MODE guard so tests can source and
|
|
265
|
+
# invoke directly.
|
|
266
|
+
#
|
|
267
|
+
# Anchor luminaries — @luminary linus-torvalds (adopter contract), @luminary
|
|
268
|
+
# michael-nygard (fail-loud stability), @luminary jerome-saltzer-and-michael-schroeder
|
|
269
|
+
# (complete mediation across both callers).
|
|
270
|
+
_template_emit_install_postcondition() {
|
|
271
|
+
local installed="${INSTALLED_COUNT:-0}"
|
|
272
|
+
local failed="${FAILED_COUNT:-0}"
|
|
273
|
+
local skipped_invalid="${SKIPPED_INVALID:-0}"
|
|
274
|
+
local skipped_total="${SKIPPED_TOTAL:-0}"
|
|
275
|
+
local total=$((installed + skipped_total + failed))
|
|
276
|
+
local defect_total=$((failed + skipped_invalid))
|
|
277
|
+
|
|
278
|
+
if [ "$defect_total" = "0" ]; then
|
|
279
|
+
# RESTORED — human line + minimal JSON status:complete
|
|
280
|
+
echo "bassclef-sync: RESTORED — $installed of $total hooks resolved at their scoped targets."
|
|
281
|
+
printf 'bassclef-sync: {"status":"complete","installed":%d}\n' "$installed"
|
|
282
|
+
else
|
|
283
|
+
# DEGRADED — human line + JSON with missing[] carrying path + scope + cure per entry
|
|
284
|
+
echo "bassclef-sync: DEGRADED — $defect_total of $total hooks did not resolve after self-heal."
|
|
285
|
+
local i missing_json="["
|
|
286
|
+
for i in "${!missing_paths[@]}"; do
|
|
287
|
+
[ "$i" -gt 0 ] && missing_json="$missing_json,"
|
|
288
|
+
missing_json="$missing_json{\"path\":\"${missing_paths[$i]}\",\"scope\":\"${missing_scopes[$i]}\",\"cure\":\"bassclef init --force\"}"
|
|
289
|
+
done
|
|
290
|
+
missing_json="$missing_json]"
|
|
291
|
+
printf 'bassclef-sync: {"status":"degraded","installed":%d,"missing":%s}\n' "$installed" "$missing_json"
|
|
292
|
+
fi
|
|
293
|
+
}
|
|
294
|
+
|
|
259
295
|
# === Test-mode early return (#897 Cure A) ===
|
|
260
296
|
# Tests source this template to access helper functions (see
|
|
261
297
|
# .claude/hooks/tests/bassclef-sync-*.test.sh). Early return under
|
|
@@ -528,25 +564,40 @@ if [ "$FETCH_DONE" = "0" ]; then
|
|
|
528
564
|
SYNC_STATUS="freshly cloned"
|
|
529
565
|
FETCH_DONE=1
|
|
530
566
|
else
|
|
531
|
-
#
|
|
532
|
-
#
|
|
533
|
-
#
|
|
534
|
-
#
|
|
535
|
-
#
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
567
|
+
# v1.5.1 refactor (bassclef-upstream#1935 epic + peer bassclef-cli-aa diagnostic on
|
|
568
|
+
# @thebassclef/lite@1.9.0 2026-09-24): the pre-v1.5.1 stdout banner ("### Bassclef sync
|
|
569
|
+
# — full-tier not synced" + Optional-auth prose + Paths attempted list) was fired on
|
|
570
|
+
# every cold-adopter SessionStart. Norman lens (adversarial RFC 2026-09-24): a loud
|
|
571
|
+
# banner on a working state trains adopters to ignore banners.
|
|
572
|
+
#
|
|
573
|
+
# New shape per operator direction 2026-09-24:
|
|
574
|
+
# stdout — one-line RESTORED acknowledgment + JSON contract (peer #1510 harness reads)
|
|
575
|
+
# stderr — [bassclef-sync] diagnostic lines (advanced adopters + operator trace)
|
|
576
|
+
#
|
|
577
|
+
# Cures the #1929 tail defect (postcondition emit unreachable on this branch — see
|
|
578
|
+
# peer's docker-smoke against v1.9.0) in the same edit.
|
|
579
|
+
{
|
|
580
|
+
echo "[bassclef-sync] Strategy A (peer $CWD/../bassclef): not present"
|
|
581
|
+
if [ "$IS_EPHEMERAL" = "1" ]; then
|
|
582
|
+
if [ -f "$CWD/.bassclef-source.json" ]; then
|
|
583
|
+
echo "[bassclef-sync] Strategy D (.bassclef-source.json): config present but fetch fell through (see prior [bassclef-sync] stderr lines)"
|
|
584
|
+
else
|
|
585
|
+
echo "[bassclef-sync] Strategy D (.bassclef-source.json): config missing"
|
|
586
|
+
fi
|
|
587
|
+
if [ -f "$CWD/.bassclef-url" ]; then
|
|
588
|
+
echo "[bassclef-sync] Strategy B (.bassclef-url): URL file present but fetch fell through"
|
|
589
|
+
else
|
|
590
|
+
echo "[bassclef-sync] Strategy B (.bassclef-url): URL file missing"
|
|
591
|
+
fi
|
|
592
|
+
fi
|
|
593
|
+
echo "[bassclef-sync] Strategy C (git clone $BASSCLEF_REPO): failed (no network, no git, or auth required)"
|
|
594
|
+
} >&2
|
|
595
|
+
|
|
596
|
+
_clone_fail_hook_count=$(find "$CWD/.claude/hooks" -maxdepth 1 -name '*.sh' 2>/dev/null | wc -l | tr -d ' ')
|
|
597
|
+
echo "bassclef-sync: RESTORED — lite bundle in place."
|
|
598
|
+
printf 'bassclef-sync: {"status":"complete","installed":%d}\n' "$_clone_fail_hook_count"
|
|
599
|
+
unset _clone_fail_hook_count
|
|
600
|
+
|
|
550
601
|
exit 0
|
|
551
602
|
fi
|
|
552
603
|
fi
|
|
@@ -898,6 +949,9 @@ SKIPPED_NO_HEADER=0
|
|
|
898
949
|
SKIPPED_INVALID=0
|
|
899
950
|
SKIPPED_LIB_MISSING=0
|
|
900
951
|
FAILED_COUNT=0
|
|
952
|
+
# #1929 — missing[] capture for RESTORED/DEGRADED postcondition
|
|
953
|
+
missing_paths=()
|
|
954
|
+
missing_scopes=()
|
|
901
955
|
|
|
902
956
|
mkdir -p "$HOME/.claude/hooks" 2>/dev/null || true
|
|
903
957
|
|
|
@@ -933,6 +987,8 @@ if [ -d "$BASSCLEF_DIR/.claude/hooks" ]; then
|
|
|
933
987
|
if [ -n "$_WALKER_MISS" ]; then
|
|
934
988
|
echo "$(date -u +%FT%TZ) install-fail-missing-dep $HOOK_NAME" >> "$BASSCLEF_TRACE_LOG"
|
|
935
989
|
FAILED_COUNT=$((FAILED_COUNT + 1))
|
|
990
|
+
missing_paths+=(".claude/hooks/$HOOK_NAME")
|
|
991
|
+
missing_scopes+=("operator")
|
|
936
992
|
continue
|
|
937
993
|
fi
|
|
938
994
|
|
|
@@ -947,6 +1003,8 @@ if [ -d "$BASSCLEF_DIR/.claude/hooks" ]; then
|
|
|
947
1003
|
chmod +x "$TARGET_USER" 2>/dev/null || true
|
|
948
1004
|
echo "$(date -u +%FT%TZ) dispatch-fail $HOOK_NAME" >> "$BASSCLEF_TRACE_LOG"
|
|
949
1005
|
FAILED_COUNT=$((FAILED_COUNT + 1))
|
|
1006
|
+
missing_paths+=(".claude/hooks/$HOOK_NAME")
|
|
1007
|
+
missing_scopes+=("operator")
|
|
950
1008
|
fi
|
|
951
1009
|
elif [ "$HAS_HEADER" = "1" ] && [ "$DISPATCH_AVAILABLE" = "0" ]; then
|
|
952
1010
|
# S-F1 MED — dispatch lib missing; trace explicitly (not silent)
|
|
@@ -965,6 +1023,8 @@ if [ -d "$BASSCLEF_DIR/.claude/hooks" ]; then
|
|
|
965
1023
|
else
|
|
966
1024
|
echo "$(date -u +%FT%TZ) skipped-invalid-header $HOOK_NAME" >> "$BASSCLEF_TRACE_LOG"
|
|
967
1025
|
SKIPPED_INVALID=$((SKIPPED_INVALID + 1))
|
|
1026
|
+
missing_paths+=(".claude/hooks/$HOOK_NAME")
|
|
1027
|
+
missing_scopes+=("operator")
|
|
968
1028
|
fi
|
|
969
1029
|
fi
|
|
970
1030
|
done
|
|
@@ -1005,6 +1065,12 @@ fi
|
|
|
1005
1065
|
|
|
1006
1066
|
# N-F1 MED — install summary with reason breakdown when non-zero
|
|
1007
1067
|
SKIPPED_TOTAL=$((SKIPPED_NO_HEADER + SKIPPED_INVALID + SKIPPED_LIB_MISSING))
|
|
1068
|
+
|
|
1069
|
+
# #1929 — RESTORED/DEGRADED + JSON postcondition (mirrors operator hook L215-243).
|
|
1070
|
+
# Retains the legacy breakdown line for adopters parsing the old shape.
|
|
1071
|
+
_template_emit_install_postcondition
|
|
1072
|
+
|
|
1073
|
+
# RFC-0008 F7 legacy — plain-English breakdown line preserved for parser contract.
|
|
1008
1074
|
if [ "$SKIPPED_TOTAL" = "0" ] && [ "$FAILED_COUNT" = "0" ]; then
|
|
1009
1075
|
echo "bassclef-sync: installed $INSTALLED_COUNT hooks; skipped 0; failed 0"
|
|
1010
1076
|
else
|
|
@@ -165,6 +165,12 @@
|
|
|
165
165
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-commit-identifier-leak-scrub.sh",
|
|
166
166
|
"timeout": 10,
|
|
167
167
|
"tier": "lite"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"type": "command",
|
|
171
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/git-stash-u-guard.sh",
|
|
172
|
+
"timeout": 5,
|
|
173
|
+
"tier": "lite"
|
|
168
174
|
}
|
|
169
175
|
]
|
|
170
176
|
},
|
|
@@ -34,6 +34,15 @@ Every entry stays in this file for the life of the manifest. Never pruned. When
|
|
|
34
34
|
|
|
35
35
|
## Entries (newest first)
|
|
36
36
|
|
|
37
|
+
### Track 1 v1.5.1 — bassclef-sync clone-fail postcondition emit + banner-to-stderr (v1.11.0 → v1.11.1, 2026-09-24)
|
|
38
|
+
|
|
39
|
+
- **Change type:** content-change (1 file: `bassclef-sync-template`)
|
|
40
|
+
- **Fields:** 434 entries (unchanged from v1.11.0). Content hash shifts on the `bassclef-sync-template` slug — `presence/install/bassclef-sync.template.sh` gains a postcondition emit block before the L586 clone-fail exit, plus moves the prior multi-line stdout banner to stderr with `[bassclef-sync]` prefix.
|
|
41
|
+
- **Version bump:** v1.11.0 → v1.11.1 (content_hash change only → patch bump per policy).
|
|
42
|
+
- **Rationale:** #1929 tail cure — postcondition emit was unreachable on the clone-fail branch (peer bassclef-cli-aa docker-smoke evidence 2026-09-24 against `@thebassclef/lite@1.9.0`). See PR #1936 + `docs/theses/2026-09-24-bassclef-sync-class-cure-thesis.md` + `docs/rfcs/RFC-2026-09-24-parallel-sync-adversarial.md`.
|
|
43
|
+
- **Downstream cure:** none required. Adopters see one-line `bassclef-sync: RESTORED — lite bundle in place.` on stdout on cold-adopter runs (was multi-line "### Bassclef sync — full-tier not synced" banner). Paths-attempted debug moves to stderr with `[bassclef-sync]` prefix — accessible via `2>&1`. Peer #1510 harness can now grep `RESTORED` on the cold-adopter path to verify cure landing.
|
|
44
|
+
- **Old shape retired:** immediately. The pre-v1.5.1 banner had a factual error (called the public bassclef repo "private" at L584) — no adopters had a stable contract to break.
|
|
45
|
+
|
|
37
46
|
### Q1 /longrun Slice D — session-start banner-tone discipline (v1.9.10 → v1.9.11, 2026-09-22)
|
|
38
47
|
|
|
39
48
|
- **Change type:** content-add (1 new standard) + content-change (session-reflection.sh + 3 fragments)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"tier": "lite",
|
|
3
|
-
"manifest_version": "1.
|
|
4
|
-
"generated_at": "2026-09-
|
|
3
|
+
"manifest_version": "1.11.1",
|
|
4
|
+
"generated_at": "2026-09-24T15:21:28Z",
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
7
7
|
"slug": "029-release-pipeline",
|
|
@@ -91,6 +91,14 @@
|
|
|
91
91
|
"content_hash": "sha256:732f8c5a9c2e206f55246aafbdd7fd5c2fa2a154b37b7281b238f0b1175e1ba0",
|
|
92
92
|
"description": "00-bassclef-hook-connect.sh — Auto-connect bassclef hooks to adopter settings.json."
|
|
93
93
|
},
|
|
94
|
+
{
|
|
95
|
+
"slug": "01-adopter-message-channel",
|
|
96
|
+
"type": "hook",
|
|
97
|
+
"path": ".claude/hooks/session-reflection.d/01-adopter-message-channel.sh",
|
|
98
|
+
"tier": "lite",
|
|
99
|
+
"content_hash": "sha256:c2831a9400ef8cacc4d79ed4bb3c8696a1b89826c7a9ece52e373a2c30f36e8c",
|
|
100
|
+
"description": "SessionStart fragment — adopter message channel (bassclef-upstream#1926)"
|
|
101
|
+
},
|
|
94
102
|
{
|
|
95
103
|
"slug": "05-active-bet",
|
|
96
104
|
"type": "hook",
|
|
@@ -387,6 +395,14 @@
|
|
|
387
395
|
"content_hash": "sha256:c923c8081ce4055060ac9d3e2f0a6a7f245087b96098a5c610196d74fc5ba97b",
|
|
388
396
|
"description": "file-rename-discipline-check — PreToolUse hook that blocks `git commit`"
|
|
389
397
|
},
|
|
398
|
+
{
|
|
399
|
+
"slug": "git-stash-u-guard",
|
|
400
|
+
"type": "hook",
|
|
401
|
+
"path": ".claude/hooks/git-stash-u-guard.sh",
|
|
402
|
+
"tier": "lite",
|
|
403
|
+
"content_hash": "sha256:23e45017aa058d6110a327e5e74a70233333b64a0d9c09c5f03549fb58c42add",
|
|
404
|
+
"description": "PreToolUse: Bash"
|
|
405
|
+
},
|
|
390
406
|
{
|
|
391
407
|
"slug": "hook-classifier-canary",
|
|
392
408
|
"type": "hook",
|
|
@@ -723,6 +739,14 @@
|
|
|
723
739
|
"content_hash": "sha256:10d42c54a64415f7e9b4f2a6701d57ffc89cc9d59ec0fec9ddc2af3c178855a2",
|
|
724
740
|
"description": "bassclef-dir-resolver.sh — resolve BASSCLEF_DIR across install layouts."
|
|
725
741
|
},
|
|
742
|
+
{
|
|
743
|
+
"slug": "capability-probe",
|
|
744
|
+
"type": "lib",
|
|
745
|
+
"path": "lib/capability-probe.sh",
|
|
746
|
+
"tier": "lite",
|
|
747
|
+
"content_hash": "sha256:12d4d7cfa78bd07e1f67a405a88b960ac62ac637297698d8ca43f217b74f4de0",
|
|
748
|
+
"description": "lib/capability-probe.sh — capability probe deep module."
|
|
749
|
+
},
|
|
726
750
|
{
|
|
727
751
|
"slug": "clean-artifacts-sweep",
|
|
728
752
|
"type": "lib",
|
|
@@ -1295,7 +1319,7 @@
|
|
|
1295
1319
|
"type": "presence-template",
|
|
1296
1320
|
"path": "presence/install/bassclef-sync.template.sh",
|
|
1297
1321
|
"tier": "lite",
|
|
1298
|
-
"content_hash": "sha256:
|
|
1322
|
+
"content_hash": "sha256:d79a9a6499c1bcaa368e0472f7a9cd76611f2dbdb8ab2b42533c3f85de2323c4",
|
|
1299
1323
|
"description": "description: BASSCLEF_SYNC_VERSION=2026-09-06-1496-sessionstart-heal-v1"
|
|
1300
1324
|
},
|
|
1301
1325
|
{
|
|
@@ -2146,8 +2170,8 @@
|
|
|
2146
2170
|
"type": "skill",
|
|
2147
2171
|
"path": ".claude/skills/build/SKILL.md",
|
|
2148
2172
|
"tier": "lite",
|
|
2149
|
-
"content_hash": "sha256:
|
|
2150
|
-
"description": "Take a
|
|
2173
|
+
"content_hash": "sha256:ef8f6b7872a29f74e8a9c181ab0b7c24f7c93bfc76b05baaf4cee4703318b10c",
|
|
2174
|
+
"description": "Take a plan from /launch and ship it. One builder per user story. Runs /verify. Opens PRs. Gates prod on a human. Refuses auth, schema, security, and tenant work. Two modes — full and degrade-with-note.",
|
|
2151
2175
|
"inputs": [
|
|
2152
2176
|
"A goal slug pointing to a spec on disk"
|
|
2153
2177
|
],
|
|
@@ -2534,8 +2558,8 @@
|
|
|
2534
2558
|
"type": "skill",
|
|
2535
2559
|
"path": ".claude/skills/launch/SKILL.md",
|
|
2536
2560
|
"tier": "lite",
|
|
2537
|
-
"content_hash": "sha256:
|
|
2538
|
-
"description": "Compare variants of an idea.
|
|
2561
|
+
"content_hash": "sha256:35763f40fe046500da02f8d37ff4e6984e18ab08b64ed0eaa3c4095d90a22225",
|
|
2562
|
+
"description": "Compare variants of an idea. Get a mock gallery. Pick a direction. Get a spec, user stories, and a plan to build. Two sizes — medium and full. Three modes — full, local, scratch.",
|
|
2539
2563
|
"inputs": [
|
|
2540
2564
|
"A rough idea or ticket or canvas"
|
|
2541
2565
|
],
|
|
@@ -2910,8 +2934,8 @@
|
|
|
2910
2934
|
"type": "skill",
|
|
2911
2935
|
"path": ".claude/skills/riff/SKILL.md",
|
|
2912
2936
|
"tier": "lite",
|
|
2913
|
-
"content_hash": "sha256:
|
|
2914
|
-
"description": "Produce 2
|
|
2937
|
+
"content_hash": "sha256:c481ce52be01c6d5e62e8ea07389e7612362b77721228fb59cd1c1c469f327ff",
|
|
2938
|
+
"description": "Produce 2 or 3 clickable mock variants. Each pins a luminary lens. Three modes — full, local, scratch. Full uses git, gh, and screenshots. Local writes to disk only. Scratch uses a tmp dir with no git.",
|
|
2915
2939
|
"composes_with": [
|
|
2916
2940
|
"interpret-input",
|
|
2917
2941
|
"luminary",
|
|
@@ -2924,8 +2948,8 @@
|
|
|
2924
2948
|
"2-3 clickable mock variants",
|
|
2925
2949
|
"Luminary lens per variant"
|
|
2926
2950
|
],
|
|
2927
|
-
"problem": "Design
|
|
2928
|
-
"value": "Two or three
|
|
2951
|
+
"problem": "Design work gets one variant. No comparison. The operator picks blind. Rework is heavy.",
|
|
2952
|
+
"value": "Two or three mocks per surface. Each mock pins a luminary lens. Cold adopters see a result in five minutes."
|
|
2929
2953
|
},
|
|
2930
2954
|
{
|
|
2931
2955
|
"slug": "riff-prototypes",
|
|
@@ -3737,7 +3761,7 @@
|
|
|
3737
3761
|
"type": "standard",
|
|
3738
3762
|
"path": "standards/lite-manifest-schema-changes.md",
|
|
3739
3763
|
"tier": "lite",
|
|
3740
|
-
"content_hash": "sha256:
|
|
3764
|
+
"content_hash": "sha256:bccea2ce3acc450f35729690e7c24706391c87186ce99ece91e574a8f4686c58",
|
|
3741
3765
|
"description": "Chronological ledger of every lite-manifest schema change. Adopters read this before sync to know what shifted."
|
|
3742
3766
|
},
|
|
3743
3767
|
{
|