@thebassclef/lite 1.0.0 → 1.0.2
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 +240 -23
- package/dist/cli.js +242 -25
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/artifact-ingestion-gate.sh +357 -0
- package/dist/lite/.claude/hooks/assert-verify-steering.sh +77 -0
- package/dist/lite/.claude/hooks/bassclef-source-config-validate.sh +215 -0
- package/dist/lite/.claude/hooks/bassclef-sync.sh +716 -0
- package/dist/lite/.claude/hooks/compound-noun-scrub.sh +292 -0
- package/dist/lite/.claude/hooks/kiss-expansion-inject.sh +69 -0
- package/dist/lite/.claude/hooks/longrun-prep-compounding-sequence-check.sh +492 -0
- package/dist/lite/.claude/hooks/plain-english-steering.sh +156 -0
- package/dist/lite/.claude/hooks/post-skill-friction-check.sh +177 -0
- package/dist/lite/.claude/hooks/post-skill-telemetry.sh +62 -0
- package/dist/lite/.claude/hooks/pre-build-gate.sh +511 -0
- package/dist/lite/.claude/hooks/pre-commit-gate.sh +451 -0
- package/dist/lite/.claude/hooks/session-end.sh +433 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +303 -0
- package/dist/lite/.claude/hooks/skill-body-grade-gate.sh +219 -0
- package/dist/lite/.claude/hooks/skill-body-intent-drift.sh +107 -0
- package/dist/lite/.claude/hooks/state-validate.sh +271 -0
- package/dist/lite/.claude/hooks/substrate-clarity-gate.sh +1110 -0
- package/dist/lite/.claude/hooks/temperance-gate.sh +147 -0
- package/dist/lite/.claude/hooks/testing-tier-enforce.sh +233 -0
- package/dist/lite/.claude/hooks/turn-prose-grade-measure.sh +219 -0
- package/dist/lite/.claude/hooks/turn-prose-kiss-check.sh +463 -0
- package/dist/lite/.claude/hooks/vocabulary-migration-check.sh +171 -0
- package/dist/lite/.claude/hooks/whereami-utc-gate.sh +142 -0
- package/dist/lite/CLAUDE.md +2 -2
- package/dist/lite/whereami.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
# Session End hook — fires on Stop event.
|
|
6
|
+
# Requires Claude to run session-end protocol, then auto-saves state.
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
source "$SCRIPT_DIR/trace-helper.sh"
|
|
10
|
+
trace_log "session-end" "stop event"
|
|
11
|
+
|
|
12
|
+
# Compute deterministic session timing
|
|
13
|
+
CWD=$(pwd)
|
|
14
|
+
CWD_HASH=$(echo -n "$CWD" | md5 -q 2>/dev/null || echo -n "$CWD" | md5sum 2>/dev/null | cut -c1-8 || echo "default")
|
|
15
|
+
SESSION_TIMING_FILE="/tmp/claude-session-timing-${CWD_HASH}"
|
|
16
|
+
ENDED_AT=$(date +"%Y-%m-%dT%H:%M:%S%z")
|
|
17
|
+
|
|
18
|
+
if [ -f "$SESSION_TIMING_FILE" ]; then
|
|
19
|
+
STARTED_AT=$(cat "$SESSION_TIMING_FILE")
|
|
20
|
+
START_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$STARTED_AT" +%s 2>/dev/null || date -d "$STARTED_AT" +%s 2>/dev/null || echo "")
|
|
21
|
+
END_EPOCH=$(date +%s)
|
|
22
|
+
if [ -n "$START_EPOCH" ]; then
|
|
23
|
+
DURATION_SEC=$((END_EPOCH - START_EPOCH))
|
|
24
|
+
DURATION_MIN=$((DURATION_SEC / 60))
|
|
25
|
+
fi
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
cat <<END
|
|
29
|
+
============================================
|
|
30
|
+
SESSION END — RUN /session-end
|
|
31
|
+
============================================
|
|
32
|
+
|
|
33
|
+
### Session Timing (deterministic — from hooks)
|
|
34
|
+
- started_at: ${STARTED_AT:-unknown}
|
|
35
|
+
- ended_at: ${ENDED_AT}
|
|
36
|
+
- duration_minutes: ${DURATION_MIN:-unknown}
|
|
37
|
+
|
|
38
|
+
Use these EXACT values in the chronicle frontmatter. Do NOT estimate.
|
|
39
|
+
============================================
|
|
40
|
+
|
|
41
|
+
Run the /session-end skill now. It handles tiered obligations:
|
|
42
|
+
|
|
43
|
+
**Must** (local, no network — always complete these):
|
|
44
|
+
1. Clean working tree (commit or stash)
|
|
45
|
+
2. Chronicle entry (chronicle/ or docs/chronicle/)
|
|
46
|
+
3. Memory update
|
|
47
|
+
4. Remove session lock
|
|
48
|
+
|
|
49
|
+
**Should** (network needed — log failures, don't block):
|
|
50
|
+
5. Git push
|
|
51
|
+
6. Update whereami.md
|
|
52
|
+
7. LinkedIn draft (if a decision, trade-off, or insight shipped)
|
|
53
|
+
|
|
54
|
+
**May** (conditional — skip without guilt if time is short):
|
|
55
|
+
8. Danger mode summary (if auto-complete used)
|
|
56
|
+
9. Release notes (if user-facing changes deployed)
|
|
57
|
+
|
|
58
|
+
Do NOT skip the Must tier. The save-state hook runs automatically
|
|
59
|
+
after this prompt, but the chronicle and memory are YOUR job.
|
|
60
|
+
============================================
|
|
61
|
+
END
|
|
62
|
+
|
|
63
|
+
# Auto-save state on session end
|
|
64
|
+
# Log failures visibly per standards/hook-idempotency.md — don't swallow
|
|
65
|
+
#
|
|
66
|
+
# === Guard 0: SESSION_LOCK precedence for save-state (bet 27b Step 2; ADR-046) ===
|
|
67
|
+
#
|
|
68
|
+
# When an autonomous /longrun session is active, SESSION_LOCK is held.
|
|
69
|
+
# Stop hook fires many times per session — not just at true session-end.
|
|
70
|
+
# Calling save-state.sh on every Stop pollutes feature branches with
|
|
71
|
+
# unwanted commits (bet 27a Way 2 mechanism).
|
|
72
|
+
#
|
|
73
|
+
# The autonomous session owns its own commits via explicit /session-end
|
|
74
|
+
# invocation. When SESSION_LOCK is held by another process group (foreign
|
|
75
|
+
# PGID), skip save-state here. Same-PGID lock is our own — proceed normally
|
|
76
|
+
# (save-state.sh clears it at the end).
|
|
77
|
+
#
|
|
78
|
+
# Held-by-self case is intentional: at real session-end, the autonomous
|
|
79
|
+
# session explicitly invokes /session-end which reaches this hook via a
|
|
80
|
+
# process in our own PGID. Guard 0 lets that path through.
|
|
81
|
+
LOCK_FILE_GUARD0="${CWD:-.}/.claude/SESSION_LOCK"
|
|
82
|
+
if [ -f "$LOCK_FILE_GUARD0" ]; then
|
|
83
|
+
G0_PID=$(grep -E '^pid:' "$LOCK_FILE_GUARD0" 2>/dev/null | head -1 | sed -E 's/^pid:[[:space:]]*//' | tr -d '[:space:]')
|
|
84
|
+
if [ -n "$G0_PID" ]; then
|
|
85
|
+
G0_PGID=$(ps -o pgid= -p "$G0_PID" 2>/dev/null | tr -d ' ')
|
|
86
|
+
G0_OUR_PGID=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')
|
|
87
|
+
if [ -n "$G0_PGID" ] && [ -n "$G0_OUR_PGID" ] && [ "$G0_PGID" != "$G0_OUR_PGID" ]; then
|
|
88
|
+
echo "SESSION_LOCK held by pid $G0_PID (foreign PGID) — skipping save-state per ADR-046 (autonomous session owns own commits)"
|
|
89
|
+
SKIP_SAVE_STATE_G0=1
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if [ "${SKIP_SAVE_STATE_G0:-0}" = "0" ]; then
|
|
95
|
+
"$SCRIPT_DIR/save-state.sh" --quiet 2>&1 || echo "WARNING: save-state failed (non-blocking)"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# === Session-rescue on abrupt stop (bassclef #226 WS-3) ===
|
|
99
|
+
#
|
|
100
|
+
# If the agent hit Stop without fulfilling MUST-tier session-end
|
|
101
|
+
# obligations (chronicle for today, whereami fresh, git clean), leave
|
|
102
|
+
# a forward-pointer deferred-action entry so the next capable session
|
|
103
|
+
# picks up the unfinished work. See .claude/rules/session-artifacts.md
|
|
104
|
+
# and docs/deferred-actions/README.md.
|
|
105
|
+
#
|
|
106
|
+
# Detection is conservative — false positives (extra rescue entry on a
|
|
107
|
+
# clean session-end) are harmless (next session sees the entry, checks
|
|
108
|
+
# obligations are met, `git mv`'s to completed/). False negatives
|
|
109
|
+
# (missing rescue when obligations were actually unmet) are the
|
|
110
|
+
# failure mode we're trying to avoid.
|
|
111
|
+
|
|
112
|
+
TODAY=$(date +"%Y-%m-%d")
|
|
113
|
+
NEEDS_RESCUE=0
|
|
114
|
+
RESCUE_REASONS=""
|
|
115
|
+
|
|
116
|
+
# Chronicle check — session-relative, NOT UTC-calendar-date.
|
|
117
|
+
#
|
|
118
|
+
# Prior shape ("chronicle for ${TODAY} exists?") produced the
|
|
119
|
+
# 2026-04-21 self-amplifying rescue loop: prior session ended
|
|
120
|
+
# 2026-04-21T00:40 local (2026-04-20T23:40 UTC) and wrote a chronicle
|
|
121
|
+
# dated 2026-04-20; next Stop firing at 00:10 UTC next day saw "no
|
|
122
|
+
# chronicle for 2026-04-21" → wrote rescue → auto-save → Stop →
|
|
123
|
+
# loop. The session-relative shape: did the most recent chronicle
|
|
124
|
+
# capture work from within this session's window? "Last 24 hours of
|
|
125
|
+
# mtime" is the simplest correct approximation — crosses midnight
|
|
126
|
+
# freely, still fires when chronicles are genuinely missing.
|
|
127
|
+
CHRONICLE_DIR=""
|
|
128
|
+
if [ -d "$CWD/chronicle" ]; then
|
|
129
|
+
CHRONICLE_DIR="$CWD/chronicle"
|
|
130
|
+
elif [ -d "$CWD/docs/chronicle" ]; then
|
|
131
|
+
CHRONICLE_DIR="$CWD/docs/chronicle"
|
|
132
|
+
fi
|
|
133
|
+
if [ -n "$CHRONICLE_DIR" ]; then
|
|
134
|
+
if ! find "$CHRONICLE_DIR" -maxdepth 1 -name "*.md" -type f -mtime -1 2>/dev/null | grep -q .; then
|
|
135
|
+
NEEDS_RESCUE=1
|
|
136
|
+
RESCUE_REASONS="${RESCUE_REASONS}- no chronicle written or updated in last 24h\n"
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Uncommitted-changes check (from CWD) — with self-output suppression.
|
|
141
|
+
#
|
|
142
|
+
# Without the guard, the rescue hook's own writes (entries dropped
|
|
143
|
+
# into docs/deferred-actions/*-session-rescue.md) count as
|
|
144
|
+
# "uncommitted changes" on the NEXT firing. auto-save commits them,
|
|
145
|
+
# next Stop sees chronicle-missing or other signal, writes ANOTHER
|
|
146
|
+
# rescue → auto-save → ... (2026-04-21 overnight: 16 entries).
|
|
147
|
+
#
|
|
148
|
+
# Fix: when the only uncommitted paths are rescue entries this hook
|
|
149
|
+
# itself produces, treat working tree as clean. Non-rescue content
|
|
150
|
+
# (real work, other deferred-action types) still fires rescue.
|
|
151
|
+
if [ -d "$CWD/.git" ] || git -C "$CWD" rev-parse --git-dir >/dev/null 2>&1; then
|
|
152
|
+
# -u (untracked-files=all) expands untracked-directory entries into
|
|
153
|
+
# their individual files. Default mode collapses e.g. an untracked
|
|
154
|
+
# docs/deferred-actions/ to "?? docs/", which would defeat the path-
|
|
155
|
+
# level filter below.
|
|
156
|
+
DIRTY_STATUS=$(git -C "$CWD" status --porcelain -u 2>/dev/null)
|
|
157
|
+
if [ -n "$DIRTY_STATUS" ]; then
|
|
158
|
+
# Strip porcelain prefix (first 3 chars = "XY ") to get paths.
|
|
159
|
+
# Keep only lines whose path is NOT a session-rescue entry.
|
|
160
|
+
NON_RESCUE=$(echo "$DIRTY_STATUS" | awk '{print substr($0, 4)}' | grep -v '^docs/deferred-actions/[^/]*-session-rescue\.md$' || true)
|
|
161
|
+
if [ -n "$NON_RESCUE" ]; then
|
|
162
|
+
NEEDS_RESCUE=1
|
|
163
|
+
RESCUE_REASONS="${RESCUE_REASONS}- uncommitted changes in working tree\n"
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
# Whereami freshness check (last_updated within last 24h).
|
|
169
|
+
#
|
|
170
|
+
# Read the FIRST ISO date-prefix line in the `## Last updated` section,
|
|
171
|
+
# not `tail -1` across the whole file. The changelog under `## Last
|
|
172
|
+
# updated` grows over time with newest entries prepended at top —
|
|
173
|
+
# `tail -1` grabbed the OLDEST entry and falsely flagged the file as
|
|
174
|
+
# stale on every Stop, producing self-amplifying rescue loops (observed
|
|
175
|
+
# 2026-04-24: 24d session-end at 12:36 → false rescue at 12:39 against
|
|
176
|
+
# a freshly-written whereami).
|
|
177
|
+
#
|
|
178
|
+
# awk scopes to the section: enter on `## Last updated`, exit on the
|
|
179
|
+
# next `## ` header, emit the first matching date-prefix line, stop.
|
|
180
|
+
# Whereami freshness detection — two tiers per ADR-054 D3 (fallback discipline):
|
|
181
|
+
# Tier 0 (primary): state_whereami_get accessor — reads docs/whereami.json
|
|
182
|
+
# (migration bootstrap per goal 2026-09-06d Step 3)
|
|
183
|
+
# Tier 1 (fallback): YAML frontmatter last_updated — handles 3 shapes
|
|
184
|
+
# (nested {at, session}, flat scalar, missing field).
|
|
185
|
+
# Added 2026-09-05 per bassclef-upstream#1481 diagnose.
|
|
186
|
+
# Legacy `## Last updated` section reader retired per goal 2026-09-06d Cure 3.
|
|
187
|
+
# Adopters mid-migration still fall through dispatcher Try 2 (YAML frontmatter).
|
|
188
|
+
WA_UPDATED=""
|
|
189
|
+
|
|
190
|
+
# Tier 0 — accessor
|
|
191
|
+
if [ -f "$CWD/lib/state.sh" ]; then
|
|
192
|
+
# shellcheck source=/dev/null
|
|
193
|
+
source "$CWD/lib/state.sh" 2>/dev/null && \
|
|
194
|
+
WA_UPDATED=$(state_whereami_get 2>/dev/null | jq -r '.last_updated.at // empty' 2>/dev/null | cut -dT -f1)
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
WHEREAMI="$CWD/docs/whereami.md"
|
|
198
|
+
|
|
199
|
+
# Tier 1 — YAML frontmatter (nested last_updated.at OR flat scalar last_updated)
|
|
200
|
+
if [ -z "$WA_UPDATED" ] && [ -f "$WHEREAMI" ]; then
|
|
201
|
+
WA_UPDATED=$(awk '
|
|
202
|
+
/^---$/ && !in_fm { in_fm=1; next }
|
|
203
|
+
/^---$/ && in_fm { exit }
|
|
204
|
+
in_fm && /^last_updated:[[:space:]]+[0-9]{4}-[0-9]{2}-[0-9]{2}/ {
|
|
205
|
+
match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/)
|
|
206
|
+
print substr($0, RSTART, RLENGTH); exit
|
|
207
|
+
}
|
|
208
|
+
in_fm && /^last_updated:[[:space:]]*$/ { nested=1; next }
|
|
209
|
+
in_fm && nested && /^[[:space:]]+at:[[:space:]]+[0-9]{4}-[0-9]{2}-[0-9]{2}/ {
|
|
210
|
+
match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/)
|
|
211
|
+
print substr($0, RSTART, RLENGTH); exit
|
|
212
|
+
}
|
|
213
|
+
in_fm && nested && /^[a-z_]+:/ { nested=0 }
|
|
214
|
+
' "$WHEREAMI" 2>/dev/null)
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
if [ -n "$WA_UPDATED" ] && [ "$WA_UPDATED" != "$TODAY" ]; then
|
|
218
|
+
# Allow yesterday as fresh (just-before-midnight sessions)
|
|
219
|
+
YESTERDAY=$(date -v-1d +"%Y-%m-%d" 2>/dev/null || date -d 'yesterday' +"%Y-%m-%d" 2>/dev/null || echo "")
|
|
220
|
+
if [ "$WA_UPDATED" != "$YESTERDAY" ]; then
|
|
221
|
+
NEEDS_RESCUE=1
|
|
222
|
+
RESCUE_REASONS="${RESCUE_REASONS}- whereami last_updated is ${WA_UPDATED} (stale, not today/yesterday)\n"
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# === Sweep resolved session-rescue entries (bet 2026-07-31e) ===
|
|
227
|
+
#
|
|
228
|
+
# Pair for the write path at L318 below. When all MUST-tier checks
|
|
229
|
+
# passed (NEEDS_RESCUE=0) and rescue entries exist in the active dir,
|
|
230
|
+
# they are the result of prior Stop events whose obligations are now
|
|
231
|
+
# satisfied. Move them to completed/ so the banner reads clean.
|
|
232
|
+
#
|
|
233
|
+
# Deep module hides soft cap, tracked-only check, git-mv, banner. See
|
|
234
|
+
# lib/session-rescue.sh + docs/diagnoses/2026-07-31e-session-rescue-stuck.md.
|
|
235
|
+
if [ "$NEEDS_RESCUE" = "0" ] && [ -f "$CWD/lib/session-rescue.sh" ]; then
|
|
236
|
+
# shellcheck source=/dev/null
|
|
237
|
+
source "$CWD/lib/session-rescue.sh" 2>/dev/null && \
|
|
238
|
+
sweep_resolved_rescue_entries "$CWD" 2>/dev/null || true
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
# === Guard 4: active-session SESSION_LOCK check (bassclef#836) ===
|
|
242
|
+
#
|
|
243
|
+
# Suppress rescue-write when another session's SESSION_LOCK is present
|
|
244
|
+
# and fresh. THAT session is still running and will write its own
|
|
245
|
+
# whereami at its own Stop; the current Stop should not pre-emptively
|
|
246
|
+
# write a rescue pointer against the in-progress work.
|
|
247
|
+
#
|
|
248
|
+
# Per bassclef#836 (observed 2026-06-02): rescue fired against
|
|
249
|
+
# whereami last_updated read as stale even though same-session work
|
|
250
|
+
# had updated it ~5h prior. The accessor returned old data; the
|
|
251
|
+
# defensive net here is "if a session is actively running, defer."
|
|
252
|
+
#
|
|
253
|
+
# Held-by-self lock is normal at our own Stop time (save-state.sh
|
|
254
|
+
# clears it at the very end). We only suppress when the lock is held
|
|
255
|
+
# by ANOTHER process group (PGID mismatch).
|
|
256
|
+
if [ "$NEEDS_RESCUE" = "1" ]; then
|
|
257
|
+
LOCK_FILE_GUARD4="$CWD/.claude/SESSION_LOCK"
|
|
258
|
+
if [ -f "$LOCK_FILE_GUARD4" ]; then
|
|
259
|
+
G4_PID=$(grep -E '^pid:' "$LOCK_FILE_GUARD4" 2>/dev/null | head -1 | sed -E 's/^pid:[[:space:]]*//' | tr -d '[:space:]')
|
|
260
|
+
G4_TS=$(grep -E '^timestamp:' "$LOCK_FILE_GUARD4" 2>/dev/null | head -1 | sed -E 's/^timestamp:[[:space:]]*//')
|
|
261
|
+
if [ -n "$G4_PID" ] && [ -n "$G4_TS" ]; then
|
|
262
|
+
G4_PGID=$(ps -o pgid= -p "$G4_PID" 2>/dev/null | tr -d ' ')
|
|
263
|
+
G4_OUR_PGID=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')
|
|
264
|
+
if [ -n "$G4_PGID" ] && [ -n "$G4_OUR_PGID" ] && [ "$G4_PGID" != "$G4_OUR_PGID" ]; then
|
|
265
|
+
# Held by ANOTHER session — check freshness against TTL
|
|
266
|
+
G4_LOCK_TTL=${LOCK_TTL_SECONDS:-1800}
|
|
267
|
+
G4_EPOCH=""
|
|
268
|
+
if G4_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$G4_TS" +%s 2>/dev/null); then
|
|
269
|
+
:
|
|
270
|
+
elif G4_EPOCH=$(date -d "$G4_TS" +%s 2>/dev/null); then
|
|
271
|
+
:
|
|
272
|
+
fi
|
|
273
|
+
if [ -n "$G4_EPOCH" ]; then
|
|
274
|
+
G4_NOW=$(date +%s)
|
|
275
|
+
G4_AGE=$((G4_NOW - G4_EPOCH))
|
|
276
|
+
if [ "$G4_AGE" -lt "$G4_LOCK_TTL" ]; then
|
|
277
|
+
echo ""
|
|
278
|
+
echo "============================================"
|
|
279
|
+
echo "SESSION-RESCUE SUPPRESSED (Guard 4)"
|
|
280
|
+
echo "============================================"
|
|
281
|
+
echo ""
|
|
282
|
+
echo "Another session is actively running (pid $G4_PID, age ${G4_AGE}s)."
|
|
283
|
+
echo "That session's Stop will write its own whereami; this Stop"
|
|
284
|
+
echo "skips the rescue write per bassclef#836."
|
|
285
|
+
NEEDS_RESCUE=""
|
|
286
|
+
fi
|
|
287
|
+
fi
|
|
288
|
+
fi
|
|
289
|
+
fi
|
|
290
|
+
fi
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
if [ "$NEEDS_RESCUE" = "1" ] && [ -d "$CWD/docs/deferred-actions" ]; then
|
|
294
|
+
# === Guard 3: write-step dedup (bassclef #260) ===
|
|
295
|
+
#
|
|
296
|
+
# #239 closed the dirty-tree self-amplifying loop via Guard 1
|
|
297
|
+
# (self-output-suppression) and normalized the chronicle check via
|
|
298
|
+
# Guard 2 (session-relative mtime, not UTC calendar date). A third
|
|
299
|
+
# path survived both: when chronicle is genuinely missing AND Stop
|
|
300
|
+
# fires repeatedly before the agent writes it, N firings produce N
|
|
301
|
+
# identical rescue entries. Observed 2026-04-23: 22 entries in a
|
|
302
|
+
# 90-min window, all cite `no chronicle written or updated in last
|
|
303
|
+
# 24h`, payload identical. Filename is timestamped so every firing
|
|
304
|
+
# creates a fresh path.
|
|
305
|
+
#
|
|
306
|
+
# The fix: if an un-resolved rescue entry with mtime < 24h already
|
|
307
|
+
# exists in the flat directory, skip the write. Payload is
|
|
308
|
+
# idempotent; one outstanding entry is sufficient signal to the
|
|
309
|
+
# next session. Multiple entries add redundancy, not safety.
|
|
310
|
+
#
|
|
311
|
+
# Multi-session semantics (desktop + mobile ephemeral coexisting,
|
|
312
|
+
# see PR body for full walkthrough):
|
|
313
|
+
# - Auto-save (save-state.sh, line 64) runs BEFORE this block on
|
|
314
|
+
# every Stop, committing + pushing uncommitted work regardless of
|
|
315
|
+
# whether rescue-write is skipped. Work preservation does NOT
|
|
316
|
+
# depend on the rescue pointer.
|
|
317
|
+
# - Dedup is local to $CWD/docs/deferred-actions/. Distinct working
|
|
318
|
+
# copies (e.g., desktop clone vs mobile sandbox) each check their
|
|
319
|
+
# own tree, so they don't suppress each other's pointers until
|
|
320
|
+
# they pull.
|
|
321
|
+
# - Once they pull, the shared pointer is sufficient: /session-end
|
|
322
|
+
# resolves all outstanding rescue entries uniformly (chronicle,
|
|
323
|
+
# whereami, tree-clean are shared-file concerns — one fix closes
|
|
324
|
+
# N pointers).
|
|
325
|
+
#
|
|
326
|
+
# Dedup-skip emits a visible trace block (symmetric with the
|
|
327
|
+
# rescue-write block below) so stuck-signal diagnostics can detect
|
|
328
|
+
# pathological cases — e.g., "skipped 200 times in a month" means
|
|
329
|
+
# /session-end hasn't run in a month and the banner is stuck for a
|
|
330
|
+
# real reason.
|
|
331
|
+
EXISTING_RECENT=$(find "$CWD/docs/deferred-actions" -maxdepth 1 -name "*-session-rescue.md" -type f -mtime -1 2>/dev/null | head -1)
|
|
332
|
+
if [ -n "$EXISTING_RECENT" ]; then
|
|
333
|
+
echo ""
|
|
334
|
+
echo "============================================"
|
|
335
|
+
echo "SESSION-RESCUE ENTRY SKIPPED (dedup)"
|
|
336
|
+
echo "============================================"
|
|
337
|
+
echo ""
|
|
338
|
+
echo "Stop fired with unfinished MUST-tier obligations. An un-"
|
|
339
|
+
echo "resolved rescue entry already covers them:"
|
|
340
|
+
echo " ${EXISTING_RECENT#$CWD/}"
|
|
341
|
+
echo ""
|
|
342
|
+
echo "No new entry written. Next session resolves the existing one."
|
|
343
|
+
echo "If this skip fires repeatedly across many sessions without"
|
|
344
|
+
echo "resolution, /session-end is the path to clear it."
|
|
345
|
+
echo "============================================"
|
|
346
|
+
echo ""
|
|
347
|
+
exit 0
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
TS=$(date -u +"%Y-%m-%dT%H-%M-%SZ")
|
|
351
|
+
RESCUE_FILE="$CWD/docs/deferred-actions/${TS}-session-rescue.md"
|
|
352
|
+
cat > "$RESCUE_FILE" <<RESCUE
|
|
353
|
+
---
|
|
354
|
+
id: ${TS}-session-rescue
|
|
355
|
+
created_by_session: session-end-hook-${TS}
|
|
356
|
+
created_in: desktop
|
|
357
|
+
created_at: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
358
|
+
pending_action: resume-unfinished-session
|
|
359
|
+
requires_capability: [git-push, network]
|
|
360
|
+
priority: high
|
|
361
|
+
origin_skill: /session-end
|
|
362
|
+
resolves_when: |
|
|
363
|
+
MUST-tier session-end obligations listed below are satisfied
|
|
364
|
+
(chronicle written, whereami current, working tree clean), and
|
|
365
|
+
this file has been git mv'd to docs/deferred-actions/completed/.
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Context
|
|
369
|
+
|
|
370
|
+
Stop-hook fired before MUST-tier session-end obligations completed.
|
|
371
|
+
This is the forward-pointer deferred-action entry per bassclef #226
|
|
372
|
+
WS-3 — the next capable session picks up the unfinished work instead
|
|
373
|
+
of silently losing it. Reasons flagged:
|
|
374
|
+
|
|
375
|
+
$(echo -e "$RESCUE_REASONS")
|
|
376
|
+
|
|
377
|
+
Common triggers: context exhaustion, OS-level interruption, sandbox
|
|
378
|
+
teardown, agent completed work but didn't run /session-end.
|
|
379
|
+
|
|
380
|
+
## Command to execute
|
|
381
|
+
|
|
382
|
+
Next session, run /session-end to complete each flagged obligation in
|
|
383
|
+
order. The skill's procedure enumerates the MUST tier; resolve each
|
|
384
|
+
before moving on. If any flagged obligation turns out to be satisfied
|
|
385
|
+
already (hook false-positive), note it in the resolution entry and
|
|
386
|
+
proceed.
|
|
387
|
+
|
|
388
|
+
## On completion
|
|
389
|
+
|
|
390
|
+
- [ ] All flagged obligations resolved
|
|
391
|
+
- [ ] \`git mv "docs/deferred-actions/${TS}-session-rescue.md" "docs/deferred-actions/completed/${TS}-session-rescue.md"\`
|
|
392
|
+
- [ ] \`git commit -m "chore: resolve session-rescue ${TS}"\`
|
|
393
|
+
|
|
394
|
+
## Cross-refs
|
|
395
|
+
|
|
396
|
+
- bassclef #231 — WS-3 issue that shipped this pattern
|
|
397
|
+
- bassclef #226 — parent bet
|
|
398
|
+
- \`.claude/rules/session-artifacts.md\`
|
|
399
|
+
- \`.claude/hooks/session-end.sh\` — the hook that wrote this entry
|
|
400
|
+
RESCUE
|
|
401
|
+
echo ""
|
|
402
|
+
echo "============================================"
|
|
403
|
+
echo "SESSION-RESCUE ENTRY WRITTEN"
|
|
404
|
+
echo "============================================"
|
|
405
|
+
echo ""
|
|
406
|
+
echo "Stop fired with unfinished MUST-tier obligations."
|
|
407
|
+
echo "Forward-pointer entry: ${RESCUE_FILE}"
|
|
408
|
+
echo ""
|
|
409
|
+
echo "Next session-start will surface it as BLOCKED:"
|
|
410
|
+
echo "deferred-action. Resolve per /session-end."
|
|
411
|
+
echo "============================================"
|
|
412
|
+
echo ""
|
|
413
|
+
fi
|
|
414
|
+
|
|
415
|
+
# === Emit session_end telemetry event (bet 2026-07-31f Step 4) ===
|
|
416
|
+
#
|
|
417
|
+
# Silent-fail per @luminary michael-nygard R1 — never crashes Stop.
|
|
418
|
+
# Only fires when adopter has opted in via lib/telemetry.sh enable.
|
|
419
|
+
{
|
|
420
|
+
if [ -f "$CWD/lib/telemetry.sh" ]; then
|
|
421
|
+
# shellcheck source=/dev/null
|
|
422
|
+
source "$CWD/lib/telemetry.sh" 2>/dev/null
|
|
423
|
+
if telemetry_is_enabled 2>/dev/null; then
|
|
424
|
+
_sid=$(telemetry_session_id 2>/dev/null)
|
|
425
|
+
_uid=$(uuidgen 2>/dev/null | tr '[:upper:]' '[:lower:]' || echo "00000000-0000-4000-8000-000000000003")
|
|
426
|
+
_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
427
|
+
telemetry_emit "{\"event_type\":\"session_end\",\"event_id\":\"$_uid\",\"session_id\":\"$_sid\",\"timestamp\":\"$_ts\",\"tier\":\"lite\",\"payload\":{}}" 2>/dev/null
|
|
428
|
+
telemetry_reset_session 2>/dev/null
|
|
429
|
+
fi
|
|
430
|
+
fi
|
|
431
|
+
} 2>/dev/null || true
|
|
432
|
+
|
|
433
|
+
exit 0
|