@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.
Files changed (32) hide show
  1. package/dist/cli.cjs +240 -23
  2. package/dist/cli.js +242 -25
  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/artifact-ingestion-gate.sh +357 -0
  7. package/dist/lite/.claude/hooks/assert-verify-steering.sh +77 -0
  8. package/dist/lite/.claude/hooks/bassclef-source-config-validate.sh +215 -0
  9. package/dist/lite/.claude/hooks/bassclef-sync.sh +716 -0
  10. package/dist/lite/.claude/hooks/compound-noun-scrub.sh +292 -0
  11. package/dist/lite/.claude/hooks/kiss-expansion-inject.sh +69 -0
  12. package/dist/lite/.claude/hooks/longrun-prep-compounding-sequence-check.sh +492 -0
  13. package/dist/lite/.claude/hooks/plain-english-steering.sh +156 -0
  14. package/dist/lite/.claude/hooks/post-skill-friction-check.sh +177 -0
  15. package/dist/lite/.claude/hooks/post-skill-telemetry.sh +62 -0
  16. package/dist/lite/.claude/hooks/pre-build-gate.sh +511 -0
  17. package/dist/lite/.claude/hooks/pre-commit-gate.sh +451 -0
  18. package/dist/lite/.claude/hooks/session-end.sh +433 -0
  19. package/dist/lite/.claude/hooks/session-reflection.sh +303 -0
  20. package/dist/lite/.claude/hooks/skill-body-grade-gate.sh +219 -0
  21. package/dist/lite/.claude/hooks/skill-body-intent-drift.sh +107 -0
  22. package/dist/lite/.claude/hooks/state-validate.sh +271 -0
  23. package/dist/lite/.claude/hooks/substrate-clarity-gate.sh +1110 -0
  24. package/dist/lite/.claude/hooks/temperance-gate.sh +147 -0
  25. package/dist/lite/.claude/hooks/testing-tier-enforce.sh +233 -0
  26. package/dist/lite/.claude/hooks/turn-prose-grade-measure.sh +219 -0
  27. package/dist/lite/.claude/hooks/turn-prose-kiss-check.sh +463 -0
  28. package/dist/lite/.claude/hooks/vocabulary-migration-check.sh +171 -0
  29. package/dist/lite/.claude/hooks/whereami-utc-gate.sh +142 -0
  30. package/dist/lite/CLAUDE.md +2 -2
  31. package/dist/lite/whereami.md +1 -1
  32. 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