@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,61 @@
1
+ # tier: lite
2
+ # Metrics-staleness check: version tags, iteration closeouts, metrics
3
+ # dashboard, DORA freshness, changelog_doc_id. Surfaces as one BLOCKED
4
+ # with all gaps enumerated.
5
+
6
+ METRICS_SCRIPT="scripts/metrics.sh"
7
+ if [ -f "$METRICS_SCRIPT" ] || [ -f "$BASSCLEF_DIR/$METRICS_SCRIPT" ]; then
8
+ METRICS_GAPS=""
9
+
10
+ # Version tags
11
+ TAG_COUNT=$(git tag 2>/dev/null | wc -l | tr -d '[:space:]')
12
+ if [ "$TAG_COUNT" -eq 0 ]; then
13
+ TOTAL_COMMITS=$(git log --oneline 2>/dev/null | wc -l | tr -d '[:space:]')
14
+ if [ "$TOTAL_COMMITS" -gt 20 ]; then
15
+ METRICS_GAPS="${METRICS_GAPS} - NO VERSION TAGS: $TOTAL_COMMITS commits but zero tags. Create retroactive tags at major milestones.\n"
16
+ fi
17
+ fi
18
+
19
+ # Iteration goal closeouts
20
+ BET_DIR="docs/iteration-bets"
21
+ if [ -d "$BET_DIR" ]; then
22
+ TOTAL_BETS=$(ls -1 "$BET_DIR"/*.md 2>/dev/null | wc -l | tr -d '[:space:]')
23
+ CLOSED_BETS=$(grep -rl "## Closeout" "$BET_DIR"/*.md 2>/dev/null | wc -l | tr -d '[:space:]')
24
+ MISSING=$((TOTAL_BETS - CLOSED_BETS))
25
+ if [ "$MISSING" -gt 0 ]; then
26
+ METRICS_GAPS="${METRICS_GAPS} - MISSING CLOSEOUTS: $MISSING of $TOTAL_BETS iteration goals have no closeout. Backfill from chronicles + git log.\n"
27
+ fi
28
+ fi
29
+
30
+ # Metrics dashboard
31
+ if [ ! -f "docs/metrics/latest.md" ]; then
32
+ ACTUAL_METRICS="$( [ -f "$METRICS_SCRIPT" ] && echo "$METRICS_SCRIPT" || echo "$BASSCLEF_DIR/$METRICS_SCRIPT" )"
33
+ METRICS_GAPS="${METRICS_GAPS} - NO METRICS GENERATED: Run $ACTUAL_METRICS --all > docs/metrics/latest.md\n"
34
+ fi
35
+
36
+ # DORA staleness
37
+ ACTUAL_METRICS_PATH="$( [ -f "$METRICS_SCRIPT" ] && echo "$METRICS_SCRIPT" || echo "$BASSCLEF_DIR/$METRICS_SCRIPT" )"
38
+ if [ -f "docs/metrics/latest.md" ]; then
39
+ METRICS_MTIME=$(stat -f "%m" "docs/metrics/latest.md" 2>/dev/null || stat -c "%Y" "docs/metrics/latest.md" 2>/dev/null || echo "0")
40
+ COMMITS_SINCE=$(git log --after="@${METRICS_MTIME}" --oneline 2>/dev/null | wc -l | tr -d '[:space:]')
41
+ if [ "$COMMITS_SINCE" -gt 20 ]; then
42
+ METRICS_GAPS="${METRICS_GAPS} - DORA METRICS STALE: ${COMMITS_SINCE} commits since last run. Run $ACTUAL_METRICS_PATH --all to refresh (includes lead time, deploy frequency, MTTR, change failure rate).\n"
43
+ fi
44
+ fi
45
+
46
+ # changelog_doc_id — bassclef-only check.
47
+ # The weekly changelog is bassclef's own release-notes push (per
48
+ # .claude/skills/release-notes/SKILL.md). Adopter repos typically
49
+ # do not publish a weekly community changelog. Skip the check when
50
+ # .bassclef-source.json is present (marker of an adopter repo that
51
+ # syncs from bassclef rather than being bassclef itself).
52
+ if [ ! -f ".bassclef-source.json" ] && [ -f "substrate.config.md" ]; then
53
+ if ! grep -q "changelog_doc_id:" substrate.config.md 2>/dev/null; then
54
+ METRICS_GAPS="${METRICS_GAPS} - NO CHANGELOG DOC: Add changelog_doc_id to substrate.config.md for weekly changelog push.\n"
55
+ fi
56
+ fi
57
+
58
+ if [ -n "$METRICS_GAPS" ]; then
59
+ blocked_banner "$(printf 'metrics — resolve before other work:\n%b' "$METRICS_GAPS")"
60
+ fi
61
+ fi
@@ -0,0 +1,180 @@
1
+ # tier: lite
2
+ # Post-merge whereami self-heal (bassclef#505 Option A — LR7 WU-2).
3
+ #
4
+ # /session-end writes whereami BEFORE PR merges happen — correct, because
5
+ # the chronicle + whereami land on the WU-N branch that's about to be
6
+ # merged. After all PRs in the stack merge, whereami still says
7
+ # "SHIPPED-PENDING-MERGE" on the active iteration_bet line. Operator
8
+ # previously hand-edited each session to flip this. This hook closes
9
+ # the gap automatically.
10
+ #
11
+ # Triggers: SessionStart (via session-reflection.d/* discovery).
12
+ # Mutates: docs/whereami.md (Shape B sed-in-place) OR docs/whereami.json
13
+ # (Shape A via state_whereami_set accessor).
14
+ # Detects: "SHIPPED-PENDING-MERGE" substring on the active iteration_bet
15
+ # line/field.
16
+ # Verifies: no open PRs match the goal slug AND ≥1 merged PR matches.
17
+ # Resolves: replace "SHIPPED-PENDING-MERGE" → "CLOSED+MERGED YYYY-MM-DD".
18
+ # Skips: gh unavailable, slug unparseable, mixed-state PRs (some open).
19
+ #
20
+ # Override: SKIP_WHEREAMI_SELFHEAL=1 disables the hook for the session.
21
+ # Override: WHEREAMI_SELFHEAL_DRY_RUN=1 emits the suggestion without mutating.
22
+
23
+ if [ "${SKIP_WHEREAMI_SELFHEAL:-0}" = "1" ]; then
24
+ return 0 2>/dev/null || exit 0
25
+ fi
26
+
27
+ WHEREAMI_MD="docs/whereami.md"
28
+ WHEREAMI_JSON="docs/whereami.json"
29
+
30
+ # Resolve goal slug + status line. Two shapes:
31
+ # - Shape A: docs/whereami.json with .iteration.bet (or top-level iteration_bet) string field
32
+ # - Shape B: docs/whereami.md with line `iteration_bet: <slug> (STATUS — narrative...)`
33
+ SHAPE=""
34
+ ACTIVE_LINE=""
35
+ ACTIVE_SLUG=""
36
+
37
+ if [ -f "$WHEREAMI_JSON" ]; then
38
+ SHAPE="A"
39
+ if command -v jq >/dev/null 2>&1; then
40
+ ACTIVE_LINE="$(jq -r '.iteration.bet // .iteration_bet // empty' "$WHEREAMI_JSON" 2>/dev/null || true)"
41
+ fi
42
+ elif [ -f "$WHEREAMI_MD" ]; then
43
+ SHAPE="B"
44
+ # Find the FIRST line starting `iteration_bet:` (active bet, not previous_iteration_bet:).
45
+ ACTIVE_LINE="$(grep -m1 -E '^iteration_bet:' "$WHEREAMI_MD" 2>/dev/null || true)"
46
+ else
47
+ # No whereami at all; nothing to self-heal.
48
+ return 0 2>/dev/null || exit 0
49
+ fi
50
+
51
+ if [ -z "$ACTIVE_LINE" ]; then
52
+ return 0 2>/dev/null || exit 0
53
+ fi
54
+
55
+ # Quick exit: no PENDING-MERGE marker = nothing to flip.
56
+ case "$ACTIVE_LINE" in
57
+ *"SHIPPED-PENDING-MERGE"*) ;;
58
+ *) return 0 2>/dev/null || exit 0 ;;
59
+ esac
60
+
61
+ # Extract goal slug. Format: `iteration_bet: <slug> (...)` (Shape B) or
62
+ # the JSON field value directly (Shape A). Slug is the first
63
+ # whitespace-delimited token after `iteration_bet: ` in Shape B, OR the
64
+ # first whitespace-delimited token in Shape A's value.
65
+ if [ "$SHAPE" = "B" ]; then
66
+ ACTIVE_SLUG="$(echo "$ACTIVE_LINE" | sed -E 's/^iteration_bet:[[:space:]]+//' | awk '{print $1}')"
67
+ else
68
+ ACTIVE_SLUG="$(echo "$ACTIVE_LINE" | awk '{print $1}')"
69
+ fi
70
+
71
+ if [ -z "$ACTIVE_SLUG" ]; then
72
+ echo "post-merge whereami self-heal: skipped — could not parse goal slug from active iteration_bet line" >&2
73
+ return 0 2>/dev/null || exit 0
74
+ fi
75
+
76
+ # gh unavailable → soft-skip.
77
+ if ! command -v gh >/dev/null 2>&1; then
78
+ echo "post-merge whereami self-heal: skipped — gh not available; manual whereami refresh recommended" >&2
79
+ return 0 2>/dev/null || exit 0
80
+ fi
81
+
82
+ # Query merged + open PRs that mention the slug in title OR body. Bassclef's
83
+ # PR title convention is `<type>(bassclef#NNN): ...` — the slug appears in
84
+ # bodies (iteration-goal doc paths, chronicle citations) more than titles.
85
+ # Defaulting to title+body search (no `in:title` modifier) catches both
86
+ # conventions; downstream "no merged matches" branch still covers the case
87
+ # where the slug lives nowhere greppable in PR metadata.
88
+ # Caught at LR7 closeout 2026-05-05: `in:title` filter never matched because
89
+ # bassclef PR titles cite bassclef#NNN not the goal slug — defect surfaced
90
+ # only at first live eat-our-own-dogfood run.
91
+ MERGED_COUNT=$(gh pr list --state merged --search "$ACTIVE_SLUG" --json number --limit 30 2>/dev/null \
92
+ | jq 'length' 2>/dev/null || echo 0)
93
+ OPEN_COUNT=$(gh pr list --state open --search "$ACTIVE_SLUG" --json number --limit 30 2>/dev/null \
94
+ | jq 'length' 2>/dev/null || echo 0)
95
+
96
+ # Also check for the bassclef-NNN-stack-N branch shape (PRs may not include
97
+ # the slug verbatim in titles — branch names + WU citations do). Use the
98
+ # slug's date + LR-N prefix as a coarser match for safety. Skip this fallback
99
+ # if jq broke above.
100
+ if [ -z "$MERGED_COUNT" ] || [ "$MERGED_COUNT" = "null" ]; then MERGED_COUNT=0; fi
101
+ if [ -z "$OPEN_COUNT" ] || [ "$OPEN_COUNT" = "null" ]; then OPEN_COUNT=0; fi
102
+
103
+ # Decision matrix:
104
+ # - OPEN > 0: bet partially shipped; do not flip; surface informational note
105
+ # - MERGED ≥ 1 AND OPEN = 0: safe to flip
106
+ # - MERGED = 0 AND OPEN = 0: title-substring match found nothing; skip with note (slug may be
107
+ # non-bassclef or PRs cite by issue number not slug)
108
+ if [ "$OPEN_COUNT" -gt 0 ]; then
109
+ echo "post-merge whereami self-heal: detected SHIPPED-PENDING-MERGE on $ACTIVE_SLUG, but $OPEN_COUNT PR(s) still open. Skipping flip; resolve PRs first."
110
+ return 0 2>/dev/null || exit 0
111
+ fi
112
+
113
+ if [ "$MERGED_COUNT" -lt 1 ]; then
114
+ echo "post-merge whereami self-heal: skipped for $ACTIVE_SLUG — no merged PRs match by title substring; manual whereami refresh recommended"
115
+ return 0 2>/dev/null || exit 0
116
+ fi
117
+
118
+ # Safe to flip. Compute today's date (UTC) for the marker.
119
+ TODAY="$(date -u +%Y-%m-%d)"
120
+ NEW_STATUS="CLOSED+MERGED $TODAY"
121
+
122
+ # Dry-run mode: emit suggestion without mutating.
123
+ if [ "${WHEREAMI_SELFHEAL_DRY_RUN:-0}" = "1" ]; then
124
+ echo "post-merge whereami self-heal (DRY-RUN): would flip $ACTIVE_SLUG SHIPPED-PENDING-MERGE → $NEW_STATUS"
125
+ echo " shape=$SHAPE; merged=$MERGED_COUNT; open=$OPEN_COUNT"
126
+ echo " override: WHEREAMI_SELFHEAL_DRY_RUN=0 to apply, OR SKIP_WHEREAMI_SELFHEAL=1 to disable"
127
+ return 0 2>/dev/null || exit 0
128
+ fi
129
+
130
+ # Apply the mutation.
131
+ APPLIED=0
132
+ if [ "$SHAPE" = "A" ] && [ -f "lib/state.sh" ]; then
133
+ # Use the accessor — it handles atomic write + schema validation.
134
+ # shellcheck disable=SC1091
135
+ source lib/state.sh
136
+ # Replace the substring within the iteration.bet field. We have to read +
137
+ # mutate + write because state_whereami_set takes a full value.
138
+ CURRENT_VALUE="$(echo "$ACTIVE_LINE")"
139
+ # Eat trailing YYYY-MM-DD if present so we don't double up the date.
140
+ # Pattern: "SHIPPED-PENDING-MERGE 2026-05-05" → "CLOSED+MERGED 2026-05-05"
141
+ # rather than "CLOSED+MERGED 2026-05-05 2026-05-05".
142
+ NEW_VALUE="$(echo "$CURRENT_VALUE" | sed -E "s/SHIPPED-PENDING-MERGE[[:space:]]+[0-9]{4}-[0-9]{2}-[0-9]{2}/$NEW_STATUS/" | sed "s/SHIPPED-PENDING-MERGE/$NEW_STATUS/")"
143
+ # Try .iteration.bet first; fall back to top-level iteration_bet.
144
+ if state_whereami_set "iteration.bet" "$NEW_VALUE" 2>/dev/null; then
145
+ APPLIED=1
146
+ elif state_whereami_set "iteration_bet" "$NEW_VALUE" 2>/dev/null; then
147
+ APPLIED=1
148
+ fi
149
+ elif [ "$SHAPE" = "B" ]; then
150
+ # Shape B: in-place sed on the FIRST iteration_bet: line only. Match
151
+ # the SHIPPED-PENDING-MERGE substring on that line specifically.
152
+ # Use awk to limit the change to the first matching line — sed's `0,/pat/`
153
+ # is GNU-only. BSD-portable awk path:
154
+ TMP="$(mktemp)"
155
+ awk -v new="$NEW_STATUS" '
156
+ /^iteration_bet:/ && !done {
157
+ # Eat trailing YYYY-MM-DD if present so we do not double up the date.
158
+ # Pattern: "SHIPPED-PENDING-MERGE 2026-05-05" → "CLOSED+MERGED 2026-05-05"
159
+ # rather than "CLOSED+MERGED 2026-05-05 2026-05-05".
160
+ if (sub(/SHIPPED-PENDING-MERGE[ \t]+[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/, new) == 0) {
161
+ sub(/SHIPPED-PENDING-MERGE/, new)
162
+ }
163
+ done = 1
164
+ }
165
+ { print }
166
+ ' "$WHEREAMI_MD" > "$TMP"
167
+ if [ -s "$TMP" ] && ! cmp -s "$TMP" "$WHEREAMI_MD"; then
168
+ mv "$TMP" "$WHEREAMI_MD"
169
+ APPLIED=1
170
+ else
171
+ rm -f "$TMP"
172
+ fi
173
+ fi
174
+
175
+ if [ "$APPLIED" = "1" ]; then
176
+ echo "post-merge whereami self-heal: flipped $ACTIVE_SLUG SHIPPED-PENDING-MERGE → $NEW_STATUS (shape=$SHAPE; $MERGED_COUNT PR(s) merged, 0 open)"
177
+ echo " Commit when ready: git add docs/whereami.{md,json} && git commit -m 'chore: post-merge whereami self-heal for $ACTIVE_SLUG'"
178
+ else
179
+ echo "post-merge whereami self-heal: detected $ACTIVE_SLUG ready to flip but mutation did not apply (shape=$SHAPE)"
180
+ fi
@@ -0,0 +1,117 @@
1
+ # tier: lite
2
+ # Whereami size check + auto-rotate (bassclef-upstream#1284).
3
+ #
4
+ # docs/whereami.md grows past Claude Code's Read tool 256KB ceiling within
5
+ # days on active bassclef sessions (~110 turns per session; 3-5 recap blocks
6
+ # per day). When the file exceeds 256KB, session-start Step 0.4 mandatory
7
+ # read fails per .claude/skills/longrun/SKILL.md. This hook surfaces the
8
+ # risk + auto-rotates via scripts/rotate-whereami.py.
9
+ #
10
+ # Thresholds:
11
+ # < 200KB (204800 bytes) — silent (healthy)
12
+ # 200-250KB (up to 256000) — INFORM approaching (rotation due soon)
13
+ # > 250KB (256000 bytes) — auto-rotate via `rotate-whereami.py --days 3`
14
+ # + INFORM before/after size
15
+ # Skips rotation if whereami is uncommitted
16
+ # (operator may be mid-edit).
17
+ #
18
+ # Fires: SessionStart via session-reflection.d/* discovery.
19
+ # Mutates: docs/whereami.md + docs/whereami-archive/YYYY-MM.md (when rotating).
20
+ # Depends: scripts/rotate-whereami.py + python3. Silent skip if either missing.
21
+ #
22
+ # Override:
23
+ # SKIP_WHEREAMI_SIZE_CHECK=1 disable hook for the session
24
+ # WHEREAMI_PATH=<path> override whereami location (test hook)
25
+ # ROTATE_SCRIPT=<path> override rotate-whereami.py path (test hook)
26
+ # ARCHIVE_DIR=<path> override archive dir (test hook)
27
+ # WHEREAMI_MODIFIED=1 treat whereami as uncommitted (test hook)
28
+ #
29
+ # Anchor luminaries:
30
+ # michael-nygard — fail-safe defaults + stability pattern
31
+ # kent-beck — Tier 0 strict TDD
32
+ # michael-feathers — characterization of existing rotate-whereami.py
33
+ # jerome-saltzer-and-michael-schroeder — complete mediation per session
34
+ # donald-norman — INFORM line surfaces state change
35
+
36
+ if [ "${SKIP_WHEREAMI_SIZE_CHECK:-0}" = "1" ]; then
37
+ return 0 2>/dev/null || exit 0
38
+ fi
39
+
40
+ WHEREAMI_PATH="${WHEREAMI_PATH:-docs/whereami.md}"
41
+ ROTATE_SCRIPT="${ROTATE_SCRIPT:-scripts/rotate-whereami.py}"
42
+ ARCHIVE_DIR="${ARCHIVE_DIR:-docs/whereami-archive}"
43
+
44
+ # Skip silently if whereami doesn't exist (fresh repo case)
45
+ if [ ! -f "$WHEREAMI_PATH" ]; then
46
+ return 0 2>/dev/null || exit 0
47
+ fi
48
+
49
+ # Size check via wc -c (portable across BSD + GNU; avoids stat -f/-c ambiguity
50
+ # where BSD stat -f=file-format vs GNU stat -f=filesystem-info — Linux CI ran
51
+ # stat -f %z and got filesystem block info, breaking numeric comparison).
52
+ SIZE=$(wc -c <"$WHEREAMI_PATH" | tr -d ' ')
53
+
54
+ THRESHOLD_APPROACH=204800 # 200KB — approaching ceiling
55
+ THRESHOLD_ROTATE=256000 # 250KB — auto-rotate now
56
+
57
+ # Below approaching threshold — silent
58
+ if [ "$SIZE" -lt "$THRESHOLD_APPROACH" ]; then
59
+ return 0 2>/dev/null || exit 0
60
+ fi
61
+
62
+ # Between approaching + rotate — INFORM only
63
+ if [ "$SIZE" -lt "$THRESHOLD_ROTATE" ]; then
64
+ echo "[whereami-size] approaching Read tool ceiling — file $SIZE bytes; rotation due soon (see bassclef-upstream#1284)" >&2
65
+ return 0 2>/dev/null || exit 0
66
+ fi
67
+
68
+ # Above rotate threshold — check for uncommitted changes first
69
+ # Only check `git diff` when file is tracked; untracked paths (test fixtures,
70
+ # fresh repos) return 0/0 exit codes we cannot distinguish, so we default to
71
+ # NOT modified for those cases.
72
+ IS_MODIFIED=0
73
+ if [ "${WHEREAMI_MODIFIED:-0}" = "1" ]; then
74
+ IS_MODIFIED=1
75
+ elif command -v git >/dev/null 2>&1; then
76
+ # git ls-files --error-unmatch returns 0 iff path is tracked
77
+ if git ls-files --error-unmatch "$WHEREAMI_PATH" >/dev/null 2>&1; then
78
+ if ! git diff --quiet "$WHEREAMI_PATH" 2>/dev/null; then
79
+ IS_MODIFIED=1
80
+ fi
81
+ fi
82
+ fi
83
+
84
+ if [ "$IS_MODIFIED" = "1" ]; then
85
+ echo "[whereami-size] file $SIZE bytes past ceiling BUT modified — skipping auto-rotate (commit or stash first)" >&2
86
+ return 0 2>/dev/null || exit 0
87
+ fi
88
+
89
+ # Skip if rotate script missing (adopter case)
90
+ if [ ! -f "$ROTATE_SCRIPT" ]; then
91
+ echo "[whereami-size] file $SIZE bytes past ceiling but $ROTATE_SCRIPT not found — skipping (adopter path)" >&2
92
+ return 0 2>/dev/null || exit 0
93
+ fi
94
+
95
+ if ! command -v python3 >/dev/null 2>&1; then
96
+ echo "[whereami-size] file $SIZE bytes past ceiling but python3 not found — skipping" >&2
97
+ return 0 2>/dev/null || exit 0
98
+ fi
99
+
100
+ # Fire rotation with tighter --days 3 default (7-day window overshoots per
101
+ # 2026-08-19 diagnostic in bassclef-upstream#1284).
102
+ SIZE_BEFORE="$SIZE"
103
+ python3 "$ROTATE_SCRIPT" --days 3 --file "$WHEREAMI_PATH" --archive-dir "$ARCHIVE_DIR" >/dev/null 2>&1 || {
104
+ echo "[whereami-size] auto-rotate FAILED — file $SIZE bytes past ceiling; run manually: python3 $ROTATE_SCRIPT --days 3" >&2
105
+ return 0 2>/dev/null || exit 0
106
+ }
107
+
108
+ SIZE_AFTER=$(wc -c <"$WHEREAMI_PATH" | tr -d ' ')
109
+
110
+ echo "[whereami-size] rotated — before $SIZE_BEFORE bytes; after $SIZE_AFTER bytes; archive at $ARCHIVE_DIR" >&2
111
+
112
+ # Follow-on inform when rotation left file still past ceiling (dense session week)
113
+ if [ "$SIZE_AFTER" -ge "$THRESHOLD_ROTATE" ]; then
114
+ echo "[whereami-size] file still past ceiling after 3-day rotation — consider byte-cap strategy per bassclef-upstream#1284" >&2
115
+ fi
116
+
117
+ return 0 2>/dev/null || exit 0
@@ -0,0 +1,108 @@
1
+ # tier: lite
2
+ # Gate-compliance check: if /verify or /temperance fire below 80% across
3
+ # the last 5 chronicles, flag as BLOCKED. Uses extract-session-telemetry.sh.
4
+ #
5
+ # Spine v0 note (WU-6): this hook reads gate-compliance state INDIRECTLY
6
+ # via scripts/extract-session-telemetry.sh, which is itself accessor-based
7
+ # post-WU-5. So the hook conforms to accessor-library-discipline.md by
8
+ # composition: telemetry script → lib/state.sh accessors → JSON state.
9
+ # Direct lib/state.sh integration here is deferred to spine v1 (would
10
+ # duplicate the rate-calculation logic that lives in the telemetry script).
11
+ #
12
+ # When the signal is STUCK (same rate across ≥3 consecutive sessions),
13
+ # the BLOCKED banner includes mechanism paths so the agent reads the
14
+ # hook + telemetry script rather than trying harder on the surface.
15
+ # See .claude/rules/stuck-signal-diagnostic.md.
16
+
17
+ TELEMETRY_SCRIPT="scripts/extract-session-telemetry.sh"
18
+ TELEMETRY_PATH=""
19
+ if [ -f "$TELEMETRY_SCRIPT" ]; then
20
+ TELEMETRY_PATH="$TELEMETRY_SCRIPT"
21
+ elif [ -f "$BASSCLEF_DIR/$TELEMETRY_SCRIPT" ]; then
22
+ TELEMETRY_PATH="$BASSCLEF_DIR/$TELEMETRY_SCRIPT"
23
+ fi
24
+
25
+ # History surface used to detect "stuck" — the last 3 rate values for
26
+ # each gate. Lives under state/markers/compliance-history/ so it
27
+ # survives sandbox teardown (per mobile-ephemeral-session.md markers).
28
+ HISTORY_DIR="state/markers/compliance-history"
29
+ mkdir -p "$HISTORY_DIR" 2>/dev/null || true
30
+
31
+ # Given a gate name and current rate, update the rolling history file
32
+ # and echo "stuck" if the last 3 entries (including current) are identical.
33
+ check_stuck() {
34
+ local gate="$1"
35
+ local rate="$2"
36
+ local hist_file="$HISTORY_DIR/${gate}.history"
37
+
38
+ # Append current rate; keep last 3 entries
39
+ echo "$rate" >> "$hist_file"
40
+ local tmp
41
+ tmp=$(tail -3 "$hist_file")
42
+ echo "$tmp" > "$hist_file"
43
+
44
+ # If we have 3 entries and all match, signal is stuck
45
+ local count
46
+ count=$(wc -l < "$hist_file" | tr -d ' ')
47
+ if [ "$count" = "3" ]; then
48
+ local uniq
49
+ uniq=$(sort -u "$hist_file" | wc -l | tr -d ' ')
50
+ if [ "$uniq" = "1" ]; then
51
+ echo "stuck"
52
+ return
53
+ fi
54
+ fi
55
+ echo "moving"
56
+ }
57
+
58
+ # Build the mechanism-paths block appended to BLOCKED banner when stuck.
59
+ mechanism_block() {
60
+ local telemetry="$1"
61
+ cat <<MECH
62
+
63
+ STUCK SIGNAL — rate unchanged across ≥3 sessions. Per
64
+ .claude/rules/stuck-signal-diagnostic.md, stop retrying the surface
65
+ and read the mechanism. The counter's update path lives in:
66
+
67
+ - ${telemetry} (rate calculation — how the % is computed)
68
+ - .claude/hooks/session-reflection.d/40-gate-compliance.sh (banner emitter + history surface)
69
+ - state/markers/compliance-history/ (rolling rate file this hook reads)
70
+ - chronicle/ frontmatter gates_fired / gate-evidence block (what the telemetry script consumes)
71
+
72
+ Read those files. Trace where the gate marker is written → where the
73
+ telemetry script reads it → why the rate isn't moving despite work
74
+ being done. If you find a substrate defect, fire /promote with the
75
+ substrate-defect classifier (see blocked-items.md §"When resolution
76
+ reveals a substrate defect").
77
+ MECH
78
+ }
79
+
80
+ if [ -n "$TELEMETRY_PATH" ]; then
81
+ GATE_OUTPUT=$(bash "$TELEMETRY_PATH" --recent 5 --gates 2>/dev/null || true)
82
+ if [ -n "$GATE_OUTPUT" ]; then
83
+ VERIFY_LINE=$(echo "$GATE_OUTPUT" | grep -i "Verify" | head -1 || true)
84
+ if [ -n "$VERIFY_LINE" ]; then
85
+ VERIFY_RATE=$(echo "$VERIFY_LINE" | grep -oE '[0-9]+%' | tr -d '%' || echo "")
86
+ if [ -n "$VERIFY_RATE" ] && [ "$VERIFY_RATE" -lt 80 ] 2>/dev/null; then
87
+ STUCK=$(check_stuck "verify" "$VERIFY_RATE")
88
+ if [ "$STUCK" = "stuck" ]; then
89
+ blocked_banner "verify-compliance — verify gate fired ${VERIFY_RATE}% in last 5 sessions (threshold: 80%). $(mechanism_block "$TELEMETRY_PATH")"
90
+ else
91
+ blocked_banner "verify-compliance — verify gate fired ${VERIFY_RATE}% in last 5 sessions (threshold: 80%). Run /verify after each task this session."
92
+ fi
93
+ fi
94
+ fi
95
+ TEMP_LINE=$(echo "$GATE_OUTPUT" | grep -i "Temperance" | head -1 || true)
96
+ if [ -n "$TEMP_LINE" ]; then
97
+ TEMP_RATE=$(echo "$TEMP_LINE" | grep -oE '[0-9]+%' | tr -d '%' || echo "")
98
+ if [ -n "$TEMP_RATE" ] && [ "$TEMP_RATE" -lt 80 ] 2>/dev/null; then
99
+ STUCK=$(check_stuck "temperance" "$TEMP_RATE")
100
+ if [ "$STUCK" = "stuck" ]; then
101
+ blocked_banner "temperance-compliance — temperance gate fired ${TEMP_RATE}% in last 5 sessions (threshold: 80%). $(mechanism_block "$TELEMETRY_PATH")"
102
+ else
103
+ blocked_banner "temperance-compliance — temperance gate fired ${TEMP_RATE}% in last 5 sessions (threshold: 80%). Run /temperance before first edit."
104
+ fi
105
+ fi
106
+ fi
107
+ fi
108
+ fi
@@ -0,0 +1,99 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # 45-auto-sync-status.sh — Always-on version status line
4
+ #
5
+ # V3 refactor (bassclef bet 2026-06-15b — Phase B Trust Busters):
6
+ # - Always-on session-start version-status line replacing the earned-banner
7
+ # Layer 2 surface (per operator decision — always-on subsumes earned)
8
+ # - Critical-fix entries from adopter-notable-releases.json fire warnings
9
+ # BYPASSING age threshold (no 30-day gate)
10
+ # - Backward-compat: silent when .bassclef-source.json absent (bassclef's
11
+ # own repo) or when V1 installed_sha/installed_at fields not yet populated
12
+ #
13
+ # Behavior:
14
+ # - When .bassclef-source.json present + has installed_sha + installed_at:
15
+ # emit "### bassclef-sync — version status" header + one-line status
16
+ # showing installed SHA + days-behind + auto-sync state
17
+ # - When critical-fix entries published_at > installed_at:
18
+ # emit additional warning section listing the entries
19
+ #
20
+ # Override: SKIP_AUTO_SYNC_STATUS=1 in env.
21
+ #
22
+ # Per bassclef#939 — adopter sync opt-in default.
23
+ # Per bet 2026-06-15b V3 — Layer 2 collapse to always-on.
24
+
25
+ [ "${SKIP_AUTO_SYNC_STATUS:-0}" = "1" ] && { return 0 2>/dev/null || exit 0; }
26
+
27
+ CWD="${CLAUDE_PROJECT_DIR:-$PWD}"
28
+ SOURCE_FILE="$CWD/.bassclef-source.json"
29
+ ADOPTER_NOTABLE_FILE="$CWD/state/adopter-notable-releases.json"
30
+
31
+ # Silent if no .bassclef-source.json (bassclef's own repo path).
32
+ [ -f "$SOURCE_FILE" ] || { return 0 2>/dev/null || exit 0; }
33
+
34
+ # Parse V1 fields. Silent if installed_sha + installed_at not yet populated
35
+ # (adopter hasn't migrated to V1 schema).
36
+ INSTALLED_SHA=$(jq -r '.installed_sha // empty' "$SOURCE_FILE" 2>/dev/null)
37
+ INSTALLED_AT=$(jq -r '.installed_at // empty' "$SOURCE_FILE" 2>/dev/null)
38
+ AUTO_SYNC=$(jq -r '.auto_sync // false' "$SOURCE_FILE" 2>/dev/null)
39
+
40
+ if [ -z "$INSTALLED_SHA" ] || [ -z "$INSTALLED_AT" ]; then
41
+ # Pre-V1 adopter — silent to avoid noise during gradual schema rollout.
42
+ return 0 2>/dev/null || exit 0
43
+ fi
44
+
45
+ # Compute days-behind from installed_at. Cross-platform date parsing.
46
+ NOW_EPOCH=$(date +%s)
47
+ INSTALLED_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$INSTALLED_AT" +%s 2>/dev/null \
48
+ || date -d "$INSTALLED_AT" +%s 2>/dev/null || echo "$NOW_EPOCH")
49
+ AGE_SECONDS=$((NOW_EPOCH - INSTALLED_EPOCH))
50
+ DAYS_BEHIND=$((AGE_SECONDS / 86400))
51
+
52
+ # Auto-sync state surfacing.
53
+ if [ "$AUTO_SYNC" = "true" ]; then
54
+ AUTO_LINE="Auto-update is ON — next session-start fetches latest."
55
+ else
56
+ AUTO_LINE="Auto-update is OFF — say \"enable auto-sync\" to flip the default."
57
+ fi
58
+
59
+ # Days-behind phrasing.
60
+ if [ "$DAYS_BEHIND" -eq 0 ]; then
61
+ AGE_PHRASE="today"
62
+ elif [ "$DAYS_BEHIND" -eq 1 ]; then
63
+ AGE_PHRASE="1 day ago"
64
+ else
65
+ AGE_PHRASE="$DAYS_BEHIND days ago"
66
+ fi
67
+
68
+ # Compose status line + emit. Always fires when reaching this point.
69
+ cat <<HEADER
70
+
71
+ ### bassclef-sync — version status
72
+
73
+ Installed: $INSTALLED_SHA ($AGE_PHRASE)
74
+ $AUTO_LINE
75
+ HEADER
76
+
77
+ # Check for critical-fix entries published_at > installed_at.
78
+ # Critical-fixes BYPASS the age threshold (no 30-day gate).
79
+ if [ -f "$ADOPTER_NOTABLE_FILE" ]; then
80
+ CRITICAL_COUNT=$(jq --arg cutoff "$INSTALLED_AT" \
81
+ '[.releases[]? | select(.published_at > $cutoff) | select(.kind == "critical_fix")] | length' \
82
+ "$ADOPTER_NOTABLE_FILE" 2>/dev/null || echo 0)
83
+ if [ "$CRITICAL_COUNT" -gt 0 ]; then
84
+ CRITICAL_SUMMARY=$(jq --arg cutoff "$INSTALLED_AT" -r \
85
+ '[.releases[]? | select(.published_at > $cutoff) | select(.kind == "critical_fix") | " • " + .summary] | join("\n")' \
86
+ "$ADOPTER_NOTABLE_FILE" 2>/dev/null || echo "")
87
+ cat <<CRITICAL
88
+
89
+ ⚠ $CRITICAL_COUNT critical-fix update(s) released since your install:
90
+ $CRITICAL_SUMMARY
91
+
92
+ Pull now with:
93
+
94
+ bash .claude/hooks/bassclef-sync.sh
95
+ CRITICAL
96
+ fi
97
+ fi
98
+
99
+ return 0 2>/dev/null || exit 0
@@ -0,0 +1,44 @@
1
+ # tier: lite
2
+ # Bassclef-evolution promotion scan: in bassclef sessions only, list
3
+ # open bassclef-evolution issues in bassclef and known consumer repos.
4
+ #
5
+ # Consumer list is read from substrate.config.md field
6
+ # `bassclef_evolution_child_repos` (comma-separated owner/name list).
7
+ # Falls back to sunj-labs/poa only when the field is absent — safe
8
+ # default for old consumer checkouts predating the config field.
9
+ #
10
+ # Keep the field in sync with .github/workflows/bassclef-template-sync.yml
11
+ # matrix.consumer list.
12
+
13
+ REPO_NAME_CHECK=$(gh repo view --json name -q '.name' 2>/dev/null || echo "")
14
+ if [ "$REPO_NAME_CHECK" = "platform-docs" ] || [ "$REPO_NAME_CHECK" = "bassclef" ]; then
15
+ CHILD_REPOS_RAW=$(grep "^bassclef_evolution_child_repos:" substrate.config.md 2>/dev/null | sed 's/^bassclef_evolution_child_repos:[[:space:]]*//' || echo "")
16
+ if [ -n "$CHILD_REPOS_RAW" ]; then
17
+ CHILD_REPOS=$(echo "$CHILD_REPOS_RAW" | tr ',' '\n' | tr -d ' ' | grep -v '^$')
18
+ else
19
+ CHILD_REPOS="sunj-labs/poa"
20
+ fi
21
+
22
+ ALL_EVOLUTION=""
23
+
24
+ BASSCLEF_ISSUES=$(gh issue list -R "sunj-labs/bassclef" --label "bassclef-evolution" --state open --limit 10 2>/dev/null)
25
+ if [ -n "$BASSCLEF_ISSUES" ]; then
26
+ ALL_EVOLUTION="${ALL_EVOLUTION}\n From bassclef:\n${BASSCLEF_ISSUES}\n"
27
+ fi
28
+
29
+ for CHILD in $CHILD_REPOS; do
30
+ CHILD_ISSUES=$(gh issue list -R "$CHILD" --label "bassclef-evolution" --state open --limit 10 2>/dev/null)
31
+ if [ -n "$CHILD_ISSUES" ]; then
32
+ ALL_EVOLUTION="${ALL_EVOLUTION}\n From ${CHILD}:\n${CHILD_ISSUES}\n"
33
+ fi
34
+ done
35
+
36
+ if [ -n "$ALL_EVOLUTION" ]; then
37
+ echo "### BASSCLEF EVOLUTION — PENDING PROMOTIONS"
38
+ echo ""
39
+ echo -e "$ALL_EVOLUTION"
40
+ echo "Review and resolve these. Child repo proposals need ingestion"
41
+ echo "into bassclef if globally applicable."
42
+ echo ""
43
+ fi
44
+ fi