@thebassclef/lite 1.0.2 → 1.0.4
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/dist/cli.cjs +51 -7
- package/dist/cli.js +51 -7
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/session-reflection.d/00-bassclef-hook-connect.sh +63 -0
- package/dist/lite/.claude/hooks/session-reflection.d/05-active-bet.sh +113 -0
- package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +62 -0
- package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +78 -0
- package/dist/lite/.claude/hooks/session-reflection.d/12-session-lock.sh +63 -0
- package/dist/lite/.claude/hooks/session-reflection.d/15-substrate-config-validate.sh +91 -0
- package/dist/lite/.claude/hooks/session-reflection.d/20-artifact-staleness.sh +98 -0
- package/dist/lite/.claude/hooks/session-reflection.d/30-metrics-staleness.sh +61 -0
- package/dist/lite/.claude/hooks/session-reflection.d/35-post-merge-whereami-self-heal.sh +180 -0
- package/dist/lite/.claude/hooks/session-reflection.d/36-whereami-size-check.sh +117 -0
- package/dist/lite/.claude/hooks/session-reflection.d/40-gate-compliance.sh +108 -0
- package/dist/lite/.claude/hooks/session-reflection.d/45-auto-sync-status.sh +99 -0
- package/dist/lite/.claude/hooks/session-reflection.d/50-evolution-issues.sh +44 -0
- package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +187 -0
- package/dist/lite/.claude/hooks/session-reflection.d/56-roadmap-reconcile-gate.sh +88 -0
- package/dist/lite/.claude/hooks/session-reflection.d/60-deferred-actions.sh +212 -0
- package/dist/lite/.claude/hooks/session-reflection.d/70-recent-strategic-artifacts.sh +101 -0
- package/dist/lite/.claude/hooks/session-reflection.d/75-prior-r8-findings.sh +100 -0
- package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +87 -0
- package/dist/lite/.claude/hooks/session-reflection.d/80-workflow-staleness.sh +141 -0
- package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +67 -0
- package/dist/lite/.claude/hooks/session-reflection.d/90-release-backlog.sh +140 -0
- package/dist/lite/.claude/hooks/session-reflection.d/90-secrets-expiry.sh +104 -0
- package/dist/lite/.claude/hooks/session-reflection.d/95-settings-hook-verify.sh +131 -0
- package/dist/lite/.claude/hooks/session-reflection.d/95-telemetry-emit.sh +66 -0
- package/dist/lite/.claude/hooks/session-reflection.d/97-install-class-dispatch.sh +57 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/00-bassclef-hook-connect.test.sh +492 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/05-active-bet.test.sh +336 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/12-session-lock.test.sh +216 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/15-substrate-config-validate.test.sh +50 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh +348 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/36-whereami-size-check.test.sh +189 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/45-auto-sync-status.test.sh +263 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/55-orientation-gate.test.sh +595 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/56-roadmap-reconcile-gate.test.sh +227 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/75-prior-r8-findings.test.sh +142 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/90-release-backlog.test.sh +44 -0
- package/dist/lite/.claude/hooks/trace-helper.sh +22 -0
- 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"
|