@thebassclef/lite 1.0.1 → 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.
- package/dist/cli.cjs +24 -1
- package/dist/cli.js +24 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/bassclef-sync.sh +98 -16
- package/dist/lite/.claude/hooks/longrun-prep-compounding-sequence-check.sh +3 -3
- 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,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# 15-substrate-config-validate.sh
|
|
4
|
+
#
|
|
5
|
+
# bassclef#591 — substrate.config.md presence + basic-coherence check.
|
|
6
|
+
#
|
|
7
|
+
# Today: thin presence check + required-section presence verification.
|
|
8
|
+
# Full JSON Schema validation deferred per bassclef#643 (migration of
|
|
9
|
+
# substrate.config.md to YAML frontmatter; schema already exists at
|
|
10
|
+
# standards/state-spine/schemas/substrate-config.schema.json).
|
|
11
|
+
#
|
|
12
|
+
# Fires at session-start as part of session-reflection.d/* chain.
|
|
13
|
+
# Surfaces BLOCKED if substrate.config.md is missing OR missing key
|
|
14
|
+
# sections that downstream hooks/skills depend on.
|
|
15
|
+
#
|
|
16
|
+
# Override: SKIP_SUBSTRATE_CONFIG_VALIDATE=1
|
|
17
|
+
#
|
|
18
|
+
# Per bassclef#639 — operates ONLY on $CWD; no cross-repo execution.
|
|
19
|
+
|
|
20
|
+
set -u
|
|
21
|
+
|
|
22
|
+
if [ "${SKIP_SUBSTRATE_CONFIG_VALIDATE:-0}" = "1" ]; then
|
|
23
|
+
return 0 2>/dev/null || exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Some sessions provide CWD via stdin JSON; fall back to $PWD if not.
|
|
27
|
+
CWD="${CWD:-$PWD}"
|
|
28
|
+
CONFIG_FILE="$CWD/substrate.config.md"
|
|
29
|
+
|
|
30
|
+
# Not all repos have substrate.config.md (fresh consumer pre-onboarding).
|
|
31
|
+
# Skip silently if absent; bassclef-sync's onboard path prompts when needed.
|
|
32
|
+
if [ ! -f "$CONFIG_FILE" ]; then
|
|
33
|
+
return 0 2>/dev/null || exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Required sections (header style: `## SectionName`).
|
|
37
|
+
# Downstream hooks (session-reflection.sh + others) grep for these section
|
|
38
|
+
# fields; absence is observable behavior, not crash.
|
|
39
|
+
REQUIRED_SECTIONS="Configuration"
|
|
40
|
+
MISSING=""
|
|
41
|
+
|
|
42
|
+
for section in $REQUIRED_SECTIONS; do
|
|
43
|
+
if ! grep -q "^## $section" "$CONFIG_FILE" 2>/dev/null; then
|
|
44
|
+
MISSING="$MISSING $section"
|
|
45
|
+
fi
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
# Required fields by line-prefix (legacy markdown style — pre-YAML-frontmatter).
|
|
49
|
+
REQUIRED_FIELDS="gating execution_mode"
|
|
50
|
+
for field in $REQUIRED_FIELDS; do
|
|
51
|
+
if ! grep -q "^$field:" "$CONFIG_FILE" 2>/dev/null; then
|
|
52
|
+
MISSING="$MISSING $field:"
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
if [ -n "$MISSING" ]; then
|
|
57
|
+
cat >&2 <<EOF
|
|
58
|
+
|
|
59
|
+
================================================================
|
|
60
|
+
⚠️ SUBSTRATE.CONFIG DRIFT — INFORM (Class B cure per #1157)
|
|
61
|
+
================================================================
|
|
62
|
+
|
|
63
|
+
File: $CONFIG_FILE
|
|
64
|
+
Missing:$MISSING
|
|
65
|
+
|
|
66
|
+
substrate.config.md is the single source of truth for external
|
|
67
|
+
resource references (per standards/substrate-config-schema.md).
|
|
68
|
+
Missing sections/fields above are expected by downstream hooks.
|
|
69
|
+
Session continues — this WARN does not block SessionStart.
|
|
70
|
+
|
|
71
|
+
Resolution (do at your convenience):
|
|
72
|
+
- If field/section was intentionally removed, restore it (set to a
|
|
73
|
+
placeholder value if not yet decided)
|
|
74
|
+
- If file is being bootstrapped fresh, see presence/install/
|
|
75
|
+
for the substrate-config.md template
|
|
76
|
+
|
|
77
|
+
Override (logged): SKIP_SUBSTRATE_CONFIG_VALIDATE=1
|
|
78
|
+
|
|
79
|
+
Full JSON Schema validation (against
|
|
80
|
+
standards/state-spine/schemas/substrate-config.schema.json) is
|
|
81
|
+
deferred — substrate.config.md needs migration to YAML frontmatter
|
|
82
|
+
first; tracked at bassclef#643.
|
|
83
|
+
|
|
84
|
+
Closes bassclef#1157 (BLOCK→WARN severity ladder).
|
|
85
|
+
Prior contract: bassclef#591 (thin-validator half; full-schema half deferred).
|
|
86
|
+
================================================================
|
|
87
|
+
EOF
|
|
88
|
+
return 0 2>/dev/null || exit 0
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
return 0 2>/dev/null || exit 0
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# tier: lite
|
|
2
|
+
# Artifact-staleness checks: chronicle, danger-mode summary,
|
|
3
|
+
# architect review, release notes. Any that are overdue fire BLOCKED.
|
|
4
|
+
|
|
5
|
+
# === Chronicle ===
|
|
6
|
+
# Freshness signal reads TWO sources per bassclef-upstream#1438:
|
|
7
|
+
# 1. Chronicle filename date (YYYY-MM-DD from `chronicle/YYYY-MM-DDx-slug.md`)
|
|
8
|
+
# 2. Latest `## Amendment YYYY-MM-DD` header inside chronicle bodies
|
|
9
|
+
# LAST_CHRONICLE = MAX of the two. Cross-day marathon sessions land
|
|
10
|
+
# amendments on prior-day chronicles instead of creating same-day files;
|
|
11
|
+
# reading only the filename produced false-positive blocks. Similarly,
|
|
12
|
+
# TODAY_CHRONICLE counts same-day filename OR any today amendment header.
|
|
13
|
+
for CHRONICLE_DIR in "chronicle" "docs/chronicle"; do
|
|
14
|
+
if [ -d "$CHRONICLE_DIR" ]; then
|
|
15
|
+
TODAY=$(date +%Y-%m-%d)
|
|
16
|
+
LAST_CHRONICLE_FILENAME=$(ls -1 "$CHRONICLE_DIR"/*.md 2>/dev/null | sort | tail -1 | xargs basename 2>/dev/null | cut -c1-10)
|
|
17
|
+
|
|
18
|
+
# Path A (bassclef-upstream#1438): scan chronicle bodies for
|
|
19
|
+
# `^## Amendment YYYY-MM-DD` headers. Take the latest by date-sort.
|
|
20
|
+
LATEST_AMENDMENT_DATE=$(grep -h -E '^## Amendment [0-9]{4}-[0-9]{2}-[0-9]{2}' "$CHRONICLE_DIR"/*.md 2>/dev/null \
|
|
21
|
+
| sed -E 's/^## Amendment ([0-9]{4}-[0-9]{2}-[0-9]{2}).*/\1/' \
|
|
22
|
+
| sort | tail -1)
|
|
23
|
+
|
|
24
|
+
# LAST_CHRONICLE = MAX(filename-date, amendment-date). Empty inputs
|
|
25
|
+
# drop out of grep -v; sort picks max lex which equals max date for
|
|
26
|
+
# ISO YYYY-MM-DD format.
|
|
27
|
+
LAST_CHRONICLE=$(printf '%s\n%s\n' "$LAST_CHRONICLE_FILENAME" "$LATEST_AMENDMENT_DATE" \
|
|
28
|
+
| grep -v '^$' | sort | tail -1)
|
|
29
|
+
|
|
30
|
+
if [ -n "$LAST_CHRONICLE" ]; then
|
|
31
|
+
COMMITS_SINCE_CHRONICLE=$(git log --oneline --since="$LAST_CHRONICLE" 2>/dev/null | wc -l | tr -d ' ')
|
|
32
|
+
|
|
33
|
+
# TODAY_CHRONICLE: same-day filename OR any today amendment header
|
|
34
|
+
TODAY_CHRONICLE_FILENAME=$(ls -1 "$CHRONICLE_DIR"/${TODAY}*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
35
|
+
TODAY_AMENDMENT=$(grep -h -E "^## Amendment ${TODAY}" "$CHRONICLE_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
36
|
+
TODAY_CHRONICLE=$((TODAY_CHRONICLE_FILENAME + TODAY_AMENDMENT))
|
|
37
|
+
|
|
38
|
+
if [ "$COMMITS_SINCE_CHRONICLE" -ge 3 ] && [ "$TODAY_CHRONICLE" -eq 0 ]; then
|
|
39
|
+
blocked_banner "chronicle — last entry $LAST_CHRONICLE, $COMMITS_SINCE_CHRONICLE commits since. Write the missing chronicle."
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
break
|
|
43
|
+
fi
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
# === Danger mode summary ===
|
|
47
|
+
DANGER_DIR="docs/danger-mode-summaries"
|
|
48
|
+
if [ -d "$DANGER_DIR" ]; then
|
|
49
|
+
TODAY=$(date +%Y-%m-%d)
|
|
50
|
+
COMMITS_TODAY=$(git log --oneline --since="$TODAY" 2>/dev/null | wc -l | tr -d ' ')
|
|
51
|
+
TODAY_DANGER=$(ls -1 "$DANGER_DIR"/${TODAY}.md 2>/dev/null | wc -l | tr -d ' ')
|
|
52
|
+
|
|
53
|
+
if [ "$COMMITS_TODAY" -ge 3 ] && [ "$TODAY_DANGER" -eq 0 ]; then
|
|
54
|
+
echo "### DANGER MODE SUMMARY MAY BE MISSING"
|
|
55
|
+
echo ""
|
|
56
|
+
echo "$COMMITS_TODAY commits today but no danger mode summary."
|
|
57
|
+
echo "If the previous session used auto-complete, write the summary."
|
|
58
|
+
echo ""
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# === Architect review ===
|
|
63
|
+
# Per #1486: scan all three known review-output paths (audits/ was added
|
|
64
|
+
# via bet 27c; reader was blind to it). Take the LATEST date across paths
|
|
65
|
+
# — the write side may land in any of them.
|
|
66
|
+
LAST_REVIEW=""
|
|
67
|
+
for REVIEW_DIR in "docs/architecture/reviews" "architecture/reviews" "architecture/audits"; do
|
|
68
|
+
if [ -d "$REVIEW_DIR" ]; then
|
|
69
|
+
CANDIDATE=$(ls -1 "$REVIEW_DIR"/*.md 2>/dev/null | sort | tail -1 | xargs basename 2>/dev/null | cut -c1-10)
|
|
70
|
+
if [ -n "$CANDIDATE" ] && [[ "$CANDIDATE" > "$LAST_REVIEW" ]]; then
|
|
71
|
+
LAST_REVIEW="$CANDIDATE"
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
if [ -n "$LAST_REVIEW" ]; then
|
|
76
|
+
COMMITS_SINCE=$(git log --oneline --since="$LAST_REVIEW" 2>/dev/null | wc -l | tr -d ' ')
|
|
77
|
+
if [ "$COMMITS_SINCE" -ge 10 ]; then
|
|
78
|
+
blocked_banner "architect-review — $COMMITS_SINCE commits since last review ($LAST_REVIEW). Run /architect-review."
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# === Release notes ===
|
|
83
|
+
NOTES_DIR="docs/release-notes"
|
|
84
|
+
if [ -d "$NOTES_DIR" ]; then
|
|
85
|
+
LAST_NOTES=$(ls -1 "$NOTES_DIR"/*.md 2>/dev/null | grep -v .gitkeep | sort | tail -1)
|
|
86
|
+
if [ -n "$LAST_NOTES" ]; then
|
|
87
|
+
LAST_NOTES_DATE=$(basename "$LAST_NOTES" .md | cut -c1-10)
|
|
88
|
+
DAYS_SINCE=$(( ( $(date +%s) - $(date -j -f "%Y-%m-%d" "$LAST_NOTES_DATE" +%s 2>/dev/null || echo "0") ) / 86400 ))
|
|
89
|
+
if [ "$DAYS_SINCE" -gt 7 ]; then
|
|
90
|
+
blocked_banner "release-notes — last entry $(basename "$LAST_NOTES"), ${DAYS_SINCE} days ago. Run /release-notes."
|
|
91
|
+
fi
|
|
92
|
+
else
|
|
93
|
+
TOTAL_COMMITS=$(git log --oneline 2>/dev/null | wc -l | tr -d ' ')
|
|
94
|
+
if [ "$TOTAL_COMMITS" -gt 10 ]; then
|
|
95
|
+
blocked_banner "release-notes — $TOTAL_COMMITS commits, zero changelogs. Run /release-notes to backfill."
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
@@ -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
|