@thebassclef/lite 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cli.cjs +24 -1
  2. package/dist/cli.js +24 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/lite/.claude/hooks/session-reflection.d/00-bassclef-hook-connect.sh +63 -0
  7. package/dist/lite/.claude/hooks/session-reflection.d/05-active-bet.sh +113 -0
  8. package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +62 -0
  9. package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +78 -0
  10. package/dist/lite/.claude/hooks/session-reflection.d/12-session-lock.sh +63 -0
  11. package/dist/lite/.claude/hooks/session-reflection.d/15-substrate-config-validate.sh +91 -0
  12. package/dist/lite/.claude/hooks/session-reflection.d/20-artifact-staleness.sh +98 -0
  13. package/dist/lite/.claude/hooks/session-reflection.d/30-metrics-staleness.sh +61 -0
  14. package/dist/lite/.claude/hooks/session-reflection.d/35-post-merge-whereami-self-heal.sh +180 -0
  15. package/dist/lite/.claude/hooks/session-reflection.d/36-whereami-size-check.sh +117 -0
  16. package/dist/lite/.claude/hooks/session-reflection.d/40-gate-compliance.sh +108 -0
  17. package/dist/lite/.claude/hooks/session-reflection.d/45-auto-sync-status.sh +99 -0
  18. package/dist/lite/.claude/hooks/session-reflection.d/50-evolution-issues.sh +44 -0
  19. package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +187 -0
  20. package/dist/lite/.claude/hooks/session-reflection.d/56-roadmap-reconcile-gate.sh +88 -0
  21. package/dist/lite/.claude/hooks/session-reflection.d/60-deferred-actions.sh +212 -0
  22. package/dist/lite/.claude/hooks/session-reflection.d/70-recent-strategic-artifacts.sh +101 -0
  23. package/dist/lite/.claude/hooks/session-reflection.d/75-prior-r8-findings.sh +100 -0
  24. package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +87 -0
  25. package/dist/lite/.claude/hooks/session-reflection.d/80-workflow-staleness.sh +141 -0
  26. package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +67 -0
  27. package/dist/lite/.claude/hooks/session-reflection.d/90-release-backlog.sh +140 -0
  28. package/dist/lite/.claude/hooks/session-reflection.d/90-secrets-expiry.sh +104 -0
  29. package/dist/lite/.claude/hooks/session-reflection.d/95-settings-hook-verify.sh +131 -0
  30. package/dist/lite/.claude/hooks/session-reflection.d/95-telemetry-emit.sh +66 -0
  31. package/dist/lite/.claude/hooks/session-reflection.d/97-install-class-dispatch.sh +57 -0
  32. package/dist/lite/.claude/hooks/session-reflection.d/tests/00-bassclef-hook-connect.test.sh +492 -0
  33. package/dist/lite/.claude/hooks/session-reflection.d/tests/05-active-bet.test.sh +336 -0
  34. package/dist/lite/.claude/hooks/session-reflection.d/tests/12-session-lock.test.sh +216 -0
  35. package/dist/lite/.claude/hooks/session-reflection.d/tests/15-substrate-config-validate.test.sh +50 -0
  36. package/dist/lite/.claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh +348 -0
  37. package/dist/lite/.claude/hooks/session-reflection.d/tests/36-whereami-size-check.test.sh +189 -0
  38. package/dist/lite/.claude/hooks/session-reflection.d/tests/45-auto-sync-status.test.sh +263 -0
  39. package/dist/lite/.claude/hooks/session-reflection.d/tests/55-orientation-gate.test.sh +595 -0
  40. package/dist/lite/.claude/hooks/session-reflection.d/tests/56-roadmap-reconcile-gate.test.sh +227 -0
  41. package/dist/lite/.claude/hooks/session-reflection.d/tests/75-prior-r8-findings.test.sh +142 -0
  42. package/dist/lite/.claude/hooks/session-reflection.d/tests/90-release-backlog.test.sh +44 -0
  43. package/dist/lite/.claude/hooks/trace-helper.sh +22 -0
  44. package/package.json +1 -1
@@ -0,0 +1,100 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # install-class: dual
4
+ # 75-prior-r8-findings.sh — SessionStart fragment that surfaces prior R8
5
+ # catches from recent chronicles. Primes the agent on the pattern early
6
+ # in the session so claim-shape prose without cited sources gets caught
7
+ # at authoring time, not just at end-of-turn (Surface 4) or write (Surface 5)
8
+ # or Bash (Surface 6).
9
+ #
10
+ # Per bet 2026-07-03a WU-4 + ADR-035 (surface 7 of 9-hook taxonomy)
11
+ # Sister surfaces: assert-verify-steering.sh (1), kiss-expansion-inject.sh (2),
12
+ # posttooluse-reprime.sh (3), stop-assertion-shape-scan.sh (4),
13
+ # substrate-clarity-gate.sh (5), pre-gh-git-claim-shape-scrub.sh (6)
14
+ #
15
+ # Behavior:
16
+ # - Fires at SessionStart via session-reflection.d dispatcher
17
+ # - Reads chronicle/ or docs/chronicle/ from CWD
18
+ # - Scans last 7 chronicles for R8-shape patterns
19
+ # - When matches found: emits summary block; when zero: silent
20
+ # - Always exit 0
21
+ #
22
+ # Override: SKIP_PRIOR_R8_FINDINGS=1
23
+
24
+ set +e
25
+
26
+ if [ "${SKIP_PRIOR_R8_FINDINGS:-0}" = "1" ]; then
27
+ return 0 2>/dev/null || exit 0
28
+ fi
29
+
30
+ # Locate chronicle dir
31
+ CHRON_DIR=""
32
+ if [ -d "chronicle" ]; then
33
+ CHRON_DIR="chronicle"
34
+ elif [ -d "docs/chronicle" ]; then
35
+ CHRON_DIR="docs/chronicle"
36
+ else
37
+ return 0 2>/dev/null || exit 0
38
+ fi
39
+
40
+ # Recent chronicles (last 7 by name-sort)
41
+ RECENT_LIST=""
42
+ if [ -d "$CHRON_DIR" ]; then
43
+ RECENT_LIST=$(ls -1 "$CHRON_DIR"/*.md 2>/dev/null | sort -r | head -7)
44
+ fi
45
+
46
+ if [ -z "$RECENT_LIST" ]; then
47
+ return 0 2>/dev/null || exit 0
48
+ fi
49
+
50
+ # R8 pattern set
51
+ R8_PATTERN='R8|assertion-shape|claim-shape|claim without source|assert without validation|assert without verification'
52
+
53
+ FINDINGS=""
54
+ TOTAL_MATCHES=0
55
+ FILES_WITH_MATCHES=0
56
+
57
+ while IFS= read -r chron; do
58
+ [ -z "$chron" ] && continue
59
+ # -a treats binary as text; -c counts occurrences
60
+ matches=$(grep -a -c -iE "$R8_PATTERN" "$chron" 2>/dev/null || echo 0)
61
+ matches=${matches//[^0-9]/}
62
+ matches=${matches:-0}
63
+ if [ "$matches" -gt 0 ]; then
64
+ TOTAL_MATCHES=$((TOTAL_MATCHES + matches))
65
+ FILES_WITH_MATCHES=$((FILES_WITH_MATCHES + 1))
66
+ fname=$(basename "$chron")
67
+ FINDINGS="${FINDINGS}
68
+ - ${fname} — ${matches} R8-related mentions"
69
+ fi
70
+ done <<< "$RECENT_LIST"
71
+
72
+ if [ "$TOTAL_MATCHES" -eq 0 ]; then
73
+ return 0 2>/dev/null || exit 0
74
+ fi
75
+
76
+ cat <<EOF
77
+
78
+ ### PRIOR R8 FINDINGS
79
+
80
+ Recent sessions carry ${TOTAL_MATCHES} R8-related mentions across ${FILES_WITH_MATCHES} chronicles:
81
+ ${FINDINGS}
82
+
83
+ R8 = agent asserts without validation. Umbrella: bassclef-upstream#559.
84
+
85
+ Surface coverage now shipped:
86
+ 1. UserPromptSubmit — bet 2026-07-01b
87
+ 2. UserPromptExpansion — bet 2026-07-01e
88
+ 3. PostToolUse re-priming — bet 2026-07-03a WU-2
89
+ 4. Stop assertion-shape scan — bet 2026-07-03a WU-1
90
+ 5. substrate-clarity-gate claim-shape scan — bet 2026-07-01e
91
+ 6. Bash scrub claim-shape extension — bet 2026-07-03a WU-3
92
+ 7. SessionStart prior findings (this fragment) — bet 2026-07-03a WU-4
93
+
94
+ Cure: every operator-facing assertion carries My claim + Evidence + Why the evidence supports the claim (Toulmin's original vocabulary: claim + data + warrant).
95
+ Read the source first; cite it inline (file path, ticket #, ADR ref).
96
+
97
+ Override: SKIP_PRIOR_R8_FINDINGS=1
98
+ EOF
99
+
100
+ return 0 2>/dev/null || exit 0
@@ -0,0 +1,87 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # Hook heartbeat check — reads standards/hook-cadence.json, calls
4
+ # heartbeat_check per named hook, emits BLOCKED banner for any hook
5
+ # silent past its expected max_age.
6
+ #
7
+ # This is the mediator that closes the ADR-048 loop:
8
+ # WU-2: interface (lib/hook-heartbeat.sh + manifest + ADR)
9
+ # WU-3: producers (7 critical hooks call heartbeat_mark on entry)
10
+ # WU-4.5 (this file): consumer (session-start scanner reads state)
11
+ #
12
+ # Sourced by session-reflection.sh coordinator. Uses `return 0 2>/dev/null
13
+ # || exit 0` per bassclef-upstream#992 fragment-coordinator-class cure +
14
+ # test-sufficiency criterion 12 — works cleanly both sourced AND standalone.
15
+ #
16
+ # Rule: .claude/rules/substrate-as-system.md — the mediator closes the
17
+ # runtime-cadence axis (per ADR-035 tenet).
18
+ #
19
+ # Anchors: @luminary michael-nygard (steady-state), @luminary
20
+ # jerome-saltzer-and-michael-schroeder (complete mediation).
21
+
22
+ MANIFEST="${BASSCLEF_DIR:-.}/standards/hook-cadence.json"
23
+
24
+ # Probe bundled lib path first (dist/lite/ ship path per #1631 goal
25
+ # 2026-09-14b), then $BASSCLEF_DIR fallback for synced adopters.
26
+ # Pattern matches session-reflection.sh:13.
27
+ _sd="$(dirname "${BASH_SOURCE[0]:-$0}")"
28
+ HB_LIB=""
29
+ for _c in "${_sd}/../../../lib/hook-heartbeat.sh" \
30
+ "${BASSCLEF_DIR:-.}/lib/hook-heartbeat.sh"; do
31
+ [ -f "$_c" ] && HB_LIB="$_c" && break
32
+ done
33
+
34
+ # Guards — silent-return if either half missing (adopter mid-sync).
35
+ if [ ! -f "$MANIFEST" ]; then
36
+ return 0 2>/dev/null || exit 0
37
+ fi
38
+
39
+ if [ -z "$HB_LIB" ]; then
40
+ return 0 2>/dev/null || exit 0
41
+ fi
42
+
43
+ # jq is required to parse the manifest.
44
+ if ! command -v jq >/dev/null 2>&1; then
45
+ return 0 2>/dev/null || exit 0
46
+ fi
47
+
48
+ # shellcheck source=/dev/null
49
+ source "$HB_LIB"
50
+
51
+ # Iterate critical hooks. Each entry has: name, max_age_seconds.
52
+ STALE_ENTRIES=""
53
+
54
+ while IFS=$'\t' read -r hook_name max_age rationale; do
55
+ [ -z "$hook_name" ] && continue
56
+
57
+ # heartbeat_check returns 1 or 2 for stale/missing. Under set -e in the
58
+ # parent coordinator that would abort the whole session-reflection chain.
59
+ # Wrap in if/then/else so non-zero returns are expected + captured.
60
+ if heartbeat_check "$hook_name" "$max_age"; then
61
+ rc=0
62
+ else
63
+ rc=$?
64
+ fi
65
+
66
+ case "$rc" in
67
+ 0) ;; # fresh; nothing to report
68
+ 1)
69
+ STALE_ENTRIES="${STALE_ENTRIES}\n - ${hook_name}: silent > ${max_age}s (${rationale})"
70
+ ;;
71
+ 2)
72
+ STALE_ENTRIES="${STALE_ENTRIES}\n - ${hook_name}: never fired (${rationale})"
73
+ ;;
74
+ esac
75
+ done < <(jq -r '.critical_hooks[] | [.name, .max_age_seconds, .rationale] | @tsv' "$MANIFEST" 2>/dev/null)
76
+
77
+ if [ -n "$STALE_ENTRIES" ]; then
78
+ NL=$'\n'
79
+ MSG="hook liveness — stale hook(s) detected:$(echo -e "$STALE_ENTRIES")${NL}${NL}Cadence per standards/hook-cadence.json. Discipline per ADR-048 + .claude/rules/substrate-as-system.md.${NL}${NL}Resolve — run each named hook manually (check .claude/settings.json for its trigger event) OR file a substrate-defect ticket if the hook cannot be fired."
80
+ if declare -f blocked_banner >/dev/null 2>&1; then
81
+ blocked_banner "$MSG"
82
+ else
83
+ printf "BLOCKED: %b\n" "$MSG" >&2
84
+ fi
85
+ fi
86
+
87
+ return 0 2>/dev/null || exit 0
@@ -0,0 +1,141 @@
1
+ # tier: lite
2
+ # Workflow-staleness — surfaces silent bassclef workflow failures at
3
+ # session-start so the agent doesn't operate against stale-substrate-state
4
+ # state for days without notice.
5
+ #
6
+ # Triggered by 2026-04-27d operator screenshots that surfaced
7
+ # bassclef-template-sync.yml failing for 6 days (last success
8
+ # 2026-04-20; 2 consecutive failures since 2026-04-26 08:37) — only
9
+ # discovered when operator pursued "why is hook drift this much?" The
10
+ # silent-failure class needs upstream surfacing.
11
+ #
12
+ # Detection: queries `gh run list --workflow=<workflow>.yml --limit 1` for
13
+ # the most recent run. If conclusion is failure AND age >24h, BLOCKED.
14
+ # If failure <24h, WARN (transient flake possible). Otherwise silent.
15
+ #
16
+ # Scope: fires only in the bassclef repo itself (the workflow lives here).
17
+ # Consumer repos rely on bassclef-hook-drift-blocker.md for propagated
18
+ # drift detection — complementary, not redundant.
19
+ #
20
+ # Inherits from session-reflection.sh: blocked_banner, $BASSCLEF_DIR, trace_log.
21
+ #
22
+ # Env overrides:
23
+ # SKIP_WORKFLOW_STALENESS=1 — disable this session
24
+ # WORKFLOW_STALENESS_HOURS=N — failure-age threshold (default 24)
25
+ #
26
+ # Issue: bassclef#NNN (filed at WU-9d ship — see docs/iteration-bets/2026-04-27d)
27
+
28
+ # Skip via env
29
+ if [ "${SKIP_WORKFLOW_STALENESS:-0}" = "1" ]; then
30
+ type trace_log >/dev/null 2>&1 && trace_log "80-workflow-staleness" "SKIP via SKIP_WORKFLOW_STALENESS=1"
31
+ return 0 2>/dev/null || exit 0
32
+ fi
33
+
34
+ # Only fire in bassclef repo (workflow lives here; consumer drift covered
35
+ # by bassclef-hook-drift-blocker.md).
36
+ ORIGIN_URL=$(git remote get-url origin 2>/dev/null || echo "")
37
+ case "$ORIGIN_URL" in
38
+ *sunj-labs/bassclef*) ;; # fire
39
+ *)
40
+ type trace_log >/dev/null 2>&1 && trace_log "80-workflow-staleness" "SKIP non-bassclef repo"
41
+ return 0 2>/dev/null || exit 0
42
+ ;;
43
+ esac
44
+
45
+ # gh CLI required; silent skip when absent or unauthenticated (cleanest
46
+ # graceful-degradation path — consumer repos in CI / fresh clones may
47
+ # lack gh; not their concern).
48
+ if ! command -v gh >/dev/null 2>&1; then
49
+ type trace_log >/dev/null 2>&1 && trace_log "80-workflow-staleness" "SKIP gh not on PATH"
50
+ return 0 2>/dev/null || exit 0
51
+ fi
52
+ if ! gh auth status >/dev/null 2>&1; then
53
+ type trace_log >/dev/null 2>&1 && trace_log "80-workflow-staleness" "SKIP gh not authenticated"
54
+ return 0 2>/dev/null || exit 0
55
+ fi
56
+
57
+ # Workflows monitored. Add to this list as new sync-class workflows ship.
58
+ # bassclef-template-sync.yml retired 2026-05-03 (PR #445, thin-pointer
59
+ # default ships bassclef fetch via .bassclef-source.json — workflow
60
+ # dispatch no longer needed).
61
+ # bassclef-s3-publish.yml retired 2026-05-13 (bassclef#446 LR8.5 WU-2)
62
+ # once every consumer migrated from .bassclef-url to .bassclef-source.json.
63
+ # Hook is now dormant — no workflows to monitor — until a future
64
+ # distribution workflow ships. Consider deleting in a follow-up if the
65
+ # dormant state persists past 30 days.
66
+ WORKFLOWS=()
67
+
68
+ THRESHOLD_HOURS="${WORKFLOW_STALENESS_HOURS:-24}"
69
+ THRESHOLD_SECS=$((THRESHOLD_HOURS * 3600))
70
+ NOW_EPOCH=$(date +%s)
71
+
72
+ ANY_BLOCKED=0
73
+ ANY_WARN=0
74
+ DETAILS=""
75
+
76
+ for workflow in "${WORKFLOWS[@]}"; do
77
+ # Use timeout if available (Linux/coreutils); fall through to direct
78
+ # gh call on macOS without coreutils. gh's own client behavior
79
+ # bounds runtime in practice; this hook isn't on a hot critical path.
80
+ if command -v timeout >/dev/null 2>&1; then
81
+ RUN_JSON=$(timeout 5 gh run list --workflow="$workflow" --limit 1 \
82
+ --json status,conclusion,createdAt,databaseId,url 2>/dev/null || echo "[]")
83
+ else
84
+ RUN_JSON=$(gh run list --workflow="$workflow" --limit 1 \
85
+ --json status,conclusion,createdAt,databaseId,url 2>/dev/null || echo "[]")
86
+ fi
87
+
88
+ # Empty result → workflow doesn't exist in this repo or query failed
89
+ if [ "$RUN_JSON" = "[]" ] || [ -z "$RUN_JSON" ]; then
90
+ continue
91
+ fi
92
+
93
+ STATUS=$(echo "$RUN_JSON" | jq -r '.[0].status // ""' 2>/dev/null)
94
+ CONCLUSION=$(echo "$RUN_JSON" | jq -r '.[0].conclusion // ""' 2>/dev/null)
95
+ CREATED_AT=$(echo "$RUN_JSON" | jq -r '.[0].createdAt // ""' 2>/dev/null)
96
+ RUN_URL=$(echo "$RUN_JSON" | jq -r '.[0].url // ""' 2>/dev/null)
97
+
98
+ # Still running → silent (workflows take time)
99
+ if [ "$STATUS" != "completed" ]; then
100
+ continue
101
+ fi
102
+
103
+ # Success → silent (green path)
104
+ if [ "$CONCLUSION" = "success" ]; then
105
+ continue
106
+ fi
107
+
108
+ # Skipped or cancelled → silent (operator decision, not failure)
109
+ case "$CONCLUSION" in
110
+ skipped|cancelled|neutral) continue ;;
111
+ esac
112
+
113
+ # failure / timed_out / action_required / startup_failure → assess age
114
+ CREATED_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$CREATED_AT" +%s 2>/dev/null || \
115
+ date -d "$CREATED_AT" +%s 2>/dev/null || echo "")
116
+ if [ -z "$CREATED_EPOCH" ]; then
117
+ continue # date parse failed; don't fire false-positive
118
+ fi
119
+ AGE_SECS=$((NOW_EPOCH - CREATED_EPOCH))
120
+ AGE_HOURS=$((AGE_SECS / 3600))
121
+
122
+ if [ "$AGE_SECS" -ge "$THRESHOLD_SECS" ]; then
123
+ ANY_BLOCKED=1
124
+ DETAILS="${DETAILS}- ${workflow}: ${CONCLUSION} ${AGE_HOURS}h ago (${CREATED_AT}) — ${RUN_URL}\n"
125
+ else
126
+ ANY_WARN=1
127
+ DETAILS="${DETAILS}- ${workflow}: ${CONCLUSION} ${AGE_HOURS}h ago (within ${THRESHOLD_HOURS}h grace) — ${RUN_URL}\n"
128
+ fi
129
+ done
130
+
131
+ # Surface findings
132
+ if [ "$ANY_BLOCKED" = "1" ]; then
133
+ MSG="workflow-staleness — bassclef workflow(s) failed >${THRESHOLD_HOURS}h ago without recovery:\n$(echo -e "$DETAILS")\nThis is the silent-failure class WU-9d retires. Read the failed run logs (gh run view <id>); root-cause + fix or operator-confirm deferral. Do not silently ignore."
134
+ blocked_banner "$MSG"
135
+ elif [ "$ANY_WARN" = "1" ]; then
136
+ echo "### WORKFLOW-STALENESS — WARN (within ${THRESHOLD_HOURS}h grace)"
137
+ echo ""
138
+ echo -e "$DETAILS"
139
+ echo "Watch for next run. If it fails again, this becomes BLOCKED next session."
140
+ echo ""
141
+ fi
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # Hook manifest staleness — flags standards/hook-cadence.json when it
4
+ # has not been updated since a new .claude/hooks/*.sh file landed.
5
+ # Nygard "who monitors the monitor?" reflex: the backstop itself can
6
+ # rot silently if the manifest drifts.
7
+ #
8
+ # Sourced by session-reflection.sh coordinator. Uses `return 0 2>/dev/null
9
+ # || exit 0` per bassclef-upstream#992 + test-sufficiency criterion 12 —
10
+ # works cleanly both sourced AND standalone. Silent-fail on missing
11
+ # manifest or missing jq.
12
+
13
+ MANIFEST="${BASSCLEF_DIR:-.}/standards/hook-cadence.json"
14
+ HOOKS_DIR="${BASSCLEF_DIR:-.}/.claude/hooks"
15
+
16
+ if [ ! -f "$MANIFEST" ]; then
17
+ return 0 2>/dev/null || exit 0
18
+ fi
19
+
20
+ if [ ! -d "$HOOKS_DIR" ]; then
21
+ return 0 2>/dev/null || exit 0
22
+ fi
23
+
24
+ if ! command -v jq >/dev/null 2>&1; then
25
+ return 0 2>/dev/null || exit 0
26
+ fi
27
+
28
+ # Read last_manifest_update (YYYY-MM-DD).
29
+ LAST_UPDATE=$(jq -r '.last_manifest_update // ""' "$MANIFEST" 2>/dev/null)
30
+ if [ -z "$LAST_UPDATE" ]; then
31
+ return 0 2>/dev/null || exit 0
32
+ fi
33
+
34
+ # Convert to epoch. Portable macOS + Linux.
35
+ LAST_UPDATE_EPOCH=$(date -j -f "%Y-%m-%d" "$LAST_UPDATE" +%s 2>/dev/null || \
36
+ date -d "$LAST_UPDATE" +%s 2>/dev/null || \
37
+ echo 0)
38
+
39
+ if [ "$LAST_UPDATE_EPOCH" = "0" ]; then
40
+ return 0 2>/dev/null || exit 0
41
+ fi
42
+
43
+ # Find any hook file newer than the manifest update date.
44
+ NEW_HOOKS=""
45
+ while IFS= read -r hook_path; do
46
+ [ -z "$hook_path" ] && continue
47
+ # GNU stat first (Linux default; `stat -f` on GNU is filesystem-info mode).
48
+ hook_mtime=$(stat -c %Y "$hook_path" 2>/dev/null || stat -f %m "$hook_path" 2>/dev/null || echo 0)
49
+ if [ "$hook_mtime" -gt "$LAST_UPDATE_EPOCH" ]; then
50
+ hook_base=$(basename "$hook_path" .sh)
51
+ if ! jq -e --arg name "$hook_base" '.critical_hooks[] | select(.name == $name)' "$MANIFEST" >/dev/null 2>&1; then
52
+ NEW_HOOKS="${NEW_HOOKS}\n - ${hook_base}"
53
+ fi
54
+ fi
55
+ done < <(find "$HOOKS_DIR" -maxdepth 1 -name '*.sh' -type f 2>/dev/null)
56
+
57
+ if [ -n "$NEW_HOOKS" ]; then
58
+ NL=$'\n'
59
+ MSG="hook-cadence manifest staleness — new hook(s) landed since last_manifest_update=${LAST_UPDATE} but manifest does not list them:$(echo -e "$NEW_HOOKS")${NL}${NL}Resolve — decide per hook whether it is critical (add to standards/hook-cadence.json + bump last_manifest_update) OR non-critical (no action; the manifest tracks only critical hooks by design)."
60
+ if declare -f blocked_banner >/dev/null 2>&1; then
61
+ blocked_banner "$MSG"
62
+ else
63
+ printf "BLOCKED: %b\n" "$MSG" >&2
64
+ fi
65
+ fi
66
+
67
+ return 0 2>/dev/null || exit 0
@@ -0,0 +1,140 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # install-class: project
4
+ # 90-release-backlog.sh
5
+ #
6
+ # Per bet 2026-06-29c WU-1. Surfaces release pipeline state at SessionStart.
7
+ # The release pipeline crosses two repos. Substrate ships from bassclef-upstream
8
+ # to public bassclef via /release. The pipeline correctly refuses to run when a
9
+ # prior release PR is open. Before this hook the refusal was silent because
10
+ # /release wasn't being invoked — work could pile up for days unnoticed.
11
+ #
12
+ # Behavior:
13
+ # - Fires only when current repo is bassclef-upstream (remote contains
14
+ # "bassclef-upstream" in URL)
15
+ # - Silent on adopter repos (they don't run /release)
16
+ # - Silent when SKIP_RELEASE_BACKLOG=1
17
+ # - Silent when gh binary missing OR gh API call fails (network failure)
18
+ #
19
+ # - Queries public bassclef for open release PRs (branch starts with "release-")
20
+ # - If any open release PR > 24h old → BLOCKED banner (operator must address)
21
+ # - If open release PR ≤ 24h → silent (still pending, normal)
22
+ #
23
+ # - If no open release PR + commits-ahead > 0:
24
+ # - If active iteration goal in_flight → silent (mid-bet work; don't pressure release)
25
+ # - Else → INFO banner suggesting /release
26
+ #
27
+ # Resolution:
28
+ # 1. Address the stuck release PR (close + re-release, or fix the failure)
29
+ # 2. OR run /release --supersede to ship a fresh release
30
+ # 3. OR explicitly defer: "skip release-backlog this session"
31
+ #
32
+ # Override (logged):
33
+ # SKIP_RELEASE_BACKLOG=1 <command>
34
+ #
35
+ # Closes bet 2026-06-29c WU-1.
36
+
37
+ set -euo pipefail
38
+
39
+ # Override path — silent, log via trace-helper if present
40
+ if [ "${SKIP_RELEASE_BACKLOG:-0}" = "1" ]; then
41
+ if [ -x ".claude/hooks/trace-helper.sh" ]; then
42
+ bash .claude/hooks/trace-helper.sh "release-backlog" "skipped via SKIP_RELEASE_BACKLOG=1" 2>/dev/null || true
43
+ fi
44
+ return 0 2>/dev/null || exit 0
45
+ fi
46
+
47
+ # Detect bassclef-upstream — silent on other repos
48
+ REMOTE_URL=$(git config --get remote.origin.url 2>/dev/null || echo "")
49
+ case "$REMOTE_URL" in
50
+ *bassclef-upstream*) ;;
51
+ *) return 0 2>/dev/null || exit 0 ;;
52
+ esac
53
+
54
+ # Need gh + jq
55
+ command -v gh >/dev/null 2>&1 || { return 0 2>/dev/null || exit 0; }
56
+ command -v jq >/dev/null 2>&1 || { return 0 2>/dev/null || exit 0; }
57
+
58
+ # Query open release PRs on public bassclef
59
+ RELEASE_PRS=$(gh pr list -R sunj-labs/bassclef --state open --json number,title,createdAt,headRefName 2>/dev/null || echo "[]")
60
+ [ -z "$RELEASE_PRS" ] && RELEASE_PRS="[]"
61
+
62
+ # Filter to release- branches
63
+ STALE_PR=$(echo "$RELEASE_PRS" | jq -r '
64
+ .[] | select(.headRefName | startswith("release-")) |
65
+ "\(.number)|\(.createdAt)|\(.title)"
66
+ ' 2>/dev/null | head -1)
67
+
68
+ if [ -n "$STALE_PR" ]; then
69
+ PR_NUM=$(echo "$STALE_PR" | cut -d'|' -f1)
70
+ PR_DATE=$(echo "$STALE_PR" | cut -d'|' -f2)
71
+ PR_TITLE=$(echo "$STALE_PR" | cut -d'|' -f3)
72
+
73
+ # Compute age in hours
74
+ PR_EPOCH=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$PR_DATE" +%s 2>/dev/null || date -u -d "$PR_DATE" +%s 2>/dev/null || echo "")
75
+ NOW_EPOCH=$(date -u +%s)
76
+ if [ -n "$PR_EPOCH" ]; then
77
+ AGE_HOURS=$(( (NOW_EPOCH - PR_EPOCH) / 3600 ))
78
+ if [ "$AGE_HOURS" -gt 24 ]; then
79
+ cat <<EOF
80
+ ============================================
81
+ 🛑 BLOCKED — release pipeline stuck
82
+ ============================================
83
+
84
+ Open release PR on public bassclef has been sitting $AGE_HOURS hours.
85
+
86
+ PR #$PR_NUM: $PR_TITLE
87
+ URL: https://github.com/sunj-labs/bassclef/pull/$PR_NUM
88
+
89
+ Either address it or re-release fresh. Substrate work merged here
90
+ since the PR opened is not reaching adopters until this clears.
91
+
92
+ Three resolution paths:
93
+ 1. Review + merge the PR if CI is green
94
+ 2. Diagnose + fix the failure if CI is red
95
+ 3. Close + run /release --supersede from current HEAD
96
+
97
+ To skip this check for one session:
98
+ SKIP_RELEASE_BACKLOG=1
99
+ ============================================
100
+ EOF
101
+ return 0 2>/dev/null || exit 0
102
+ fi
103
+ fi
104
+ # Fresh release PR — silent
105
+ return 0 2>/dev/null || exit 0
106
+ fi
107
+
108
+ # No open release PR. Check commits-ahead vs last shipped release.
109
+ # Last release SHA lives in public bassclef-version.json
110
+ # COMMITS_AHEAD_OVERRIDE is a test-only knob — when set, skip git rev-list + use the value
111
+ if [ -n "${COMMITS_AHEAD_OVERRIDE:-}" ]; then
112
+ COMMITS_AHEAD="$COMMITS_AHEAD_OVERRIDE"
113
+ else
114
+ LAST_SHA=$(gh api repos/sunj-labs/bassclef/contents/bassclef-version.json --jq '.content' 2>/dev/null | base64 -d 2>/dev/null | jq -r '.release_sha // empty' 2>/dev/null || echo "")
115
+ [ -z "$LAST_SHA" ] && { return 0 2>/dev/null || exit 0; }
116
+ COMMITS_AHEAD=$(git rev-list --count "$LAST_SHA"..HEAD 2>/dev/null || echo "0")
117
+ fi
118
+
119
+ if [ "$COMMITS_AHEAD" -gt 0 ]; then
120
+ # Check if active iteration goal — silent if mid-bet
121
+ # Read whereami in_flight_bet field (best-effort)
122
+ IN_FLIGHT=""
123
+ if [ "${NO_ACTIVE_BET_OVERRIDE:-0}" != "1" ] && [ -f "docs/whereami.md" ]; then
124
+ IN_FLIGHT=$(awk '/^in_flight_bet:/{flag=1; next} flag && /^[a-z_]+:/{exit} flag' docs/whereami.md 2>/dev/null | head -5)
125
+ fi
126
+ if [ -n "$IN_FLIGHT" ]; then
127
+ return 0 2>/dev/null || exit 0
128
+ fi
129
+ SHA_NOTE=""
130
+ if [ -n "${LAST_SHA:-}" ]; then
131
+ SHA_NOTE=" (sha $LAST_SHA)"
132
+ fi
133
+ cat <<EOF
134
+ INFO — release pipeline is current but $COMMITS_AHEAD commits ahead of last
135
+ shipped release$SHA_NOTE. No active iteration goal in flight. Consider
136
+ running /release --supersede when ready to ship to adopters.
137
+ EOF
138
+ fi
139
+
140
+ return 0 2>/dev/null || exit 0
@@ -0,0 +1,104 @@
1
+ # tier: lite
2
+ # Secrets-expiry scan: parse substrate.secrets.md, surface BLOCKED
3
+ # at session start when any secret is within alert_threshold_days.
4
+ #
5
+ # Schema: standards/state-spine/schemas/substrate-secrets.schema.json
6
+ # Standard: standards/secrets-lifecycle.md
7
+ # Rule: (paired with .claude/rules/substrate-config-schema.md cross-reference)
8
+ # Refs: bassclef#381
9
+ #
10
+ # Behavior:
11
+ # - Silent when no substrate.secrets.md (graceful in repos without secrets)
12
+ # - Silent when all secrets in OK state
13
+ # - Surfaces BLOCKED block listing every WARN / ALERT / EXPIRED secret
14
+ # with rotation procedure path
15
+ # - Override per-session: SKIP_SECRETS_EXPIRY_CHECK=1 (logged to stderr)
16
+
17
+ # Per-session override
18
+ if [ "${SKIP_SECRETS_EXPIRY_CHECK:-0}" = "1" ]; then
19
+ echo "[secrets-expiry: SKIP_SECRETS_EXPIRY_CHECK=1, skipping]" >&2
20
+ return 0 2>/dev/null || true
21
+ fi
22
+
23
+ SECRETS_FILE="${CWD:-$(pwd)}/substrate.secrets.md"
24
+
25
+ # No file → graceful exit (consumer repo without secrets is fine)
26
+ if [ ! -f "$SECRETS_FILE" ]; then
27
+ return 0 2>/dev/null || true
28
+ fi
29
+
30
+ # Locate the status script. Prefer consumer-repo path; fall back to
31
+ # bassclef-vendored path if available.
32
+ STATUS_SCRIPT="${CWD:-$(pwd)}/scripts/secrets-status.sh"
33
+ if [ ! -x "$STATUS_SCRIPT" ] && [ -n "${BASSCLEF_DIR:-}" ]; then
34
+ STATUS_SCRIPT="${BASSCLEF_DIR}/scripts/secrets-status.sh"
35
+ fi
36
+
37
+ if [ ! -x "$STATUS_SCRIPT" ]; then
38
+ # Script missing — surface as BLOCKED with installation hint.
39
+ blocked_banner "secrets-expiry — scripts/secrets-status.sh not found
40
+
41
+ substrate.secrets.md exists at $SECRETS_FILE but the status script is
42
+ missing. The hook can't check expiry without it.
43
+
44
+ Resolution:
45
+ - If this is bassclef: ensure scripts/secrets-status.sh is committed
46
+ - If this is a consumer repo: re-run /onboard-repo to vendor the script
47
+
48
+ Override: SKIP_SECRETS_EXPIRY_CHECK=1 <command>
49
+ Standard: standards/secrets-lifecycle.md"
50
+ return 0 2>/dev/null || true
51
+ fi
52
+
53
+ # Run status script in JSON mode for parseable output
54
+ STATUS_JSON="$(SECRETS_FILE="$SECRETS_FILE" bash "$STATUS_SCRIPT" --json 2>/dev/null || echo '')"
55
+
56
+ if [ -z "$STATUS_JSON" ]; then
57
+ return 0 2>/dev/null || true
58
+ fi
59
+
60
+ # Extract WARN / ALERT / EXPIRED entries via python3 (jq optional).
61
+ # Each line of output is "STATE|NAME|EXPIRES|DAYS|PROCEDURE"
62
+ PROBLEM_ENTRIES="$(echo "$STATUS_JSON" | python3 -c "
63
+ import sys, json
64
+ try:
65
+ d = json.load(sys.stdin)
66
+ except Exception:
67
+ sys.exit(0)
68
+ for s in d.get('secrets', []):
69
+ state = s.get('state', '')
70
+ if state in ('WARN', 'ALERT', 'EXPIRED'):
71
+ print(f\"{state}|{s.get('name','')}|{s.get('expires_at','')}|{s.get('days_remaining','')}|{s.get('rotation_procedure','')}\")
72
+ " 2>/dev/null || echo '')"
73
+
74
+ if [ -z "$PROBLEM_ENTRIES" ]; then
75
+ return 0 2>/dev/null || true
76
+ fi
77
+
78
+ # Build BLOCKED banner content
79
+ MSG="secrets-expiry — at least one credential within alert window
80
+
81
+ "
82
+
83
+ while IFS='|' read -r state name expires days proc; do
84
+ case "$state" in
85
+ EXPIRED) glyph="💀 EXPIRED" ;;
86
+ ALERT) glyph="🛑 ALERT " ;;
87
+ WARN) glyph="⚠️ WARN " ;;
88
+ *) glyph="$state" ;;
89
+ esac
90
+ MSG="$MSG $glyph $name expires $expires ($days days)
91
+ rotate: $proc
92
+ "
93
+ done <<< "$PROBLEM_ENTRIES"
94
+
95
+ MSG="$MSG
96
+ Resolution paths:
97
+ 1. Rotate the credential per its rotation_procedure runbook
98
+ 2. Update substrate.secrets.md (last_rotated + expires_at) after rotation
99
+ 3. Verify with: bash scripts/secrets-status.sh
100
+
101
+ Override (per-session): SKIP_SECRETS_EXPIRY_CHECK=1 <command>
102
+ Standard: standards/secrets-lifecycle.md"
103
+
104
+ blocked_banner "$MSG"