@thebassclef/lite 1.1.1 → 1.2.0-alpha.1
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 +23 -6
- package/dist/cli.js +23 -6
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/bassclef-configs.jsonc +56 -0
- package/dist/lite/.claude/bassclef-orientation.md +120 -0
- package/dist/lite/.claude/hooks/adr-discipline-check.sh +132 -0
- package/dist/lite/.claude/hooks/atomic-pr-check.sh +169 -0
- package/dist/lite/.claude/hooks/auto-save-idle.sh +51 -0
- package/dist/lite/.claude/hooks/bassclef-sync.sh +51 -13
- package/dist/lite/.claude/hooks/bet-doc-gate.sh +283 -0
- package/dist/lite/.claude/hooks/bug-diagnosis.sh +64 -0
- package/dist/lite/.claude/hooks/destructive-command-guard.sh +136 -0
- package/dist/lite/.claude/hooks/file-rename-discipline-check.sh +147 -0
- package/dist/lite/.claude/hooks/hook-classifier-canary.sh +158 -0
- package/dist/lite/.claude/hooks/kiss-default-check.sh +129 -0
- package/dist/lite/.claude/hooks/multi-tenancy-check.sh +83 -0
- package/dist/lite/.claude/hooks/oo-ad-entry-point-check.sh +216 -0
- package/dist/lite/.claude/hooks/operator-private-path-check.sh +203 -0
- package/dist/lite/.claude/hooks/pattern-annotation-validate.sh +142 -0
- package/dist/lite/.claude/hooks/plain-english-steering.sh +15 -1
- package/dist/lite/.claude/hooks/post-write-trace.sh +66 -0
- package/dist/lite/.claude/hooks/pr-body-scrub-check.sh +446 -0
- package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +137 -0
- package/dist/lite/.claude/hooks/pre-gh-pr-body-scrub.sh +192 -0
- package/dist/lite/.claude/hooks/pre-gh-pr-title-scrub.sh +172 -0
- package/dist/lite/.claude/hooks/role-entry-gate.sh +115 -0
- package/dist/lite/.claude/hooks/save-state.sh +268 -0
- package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +8 -1
- package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +52 -0
- package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +40 -4
- package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +12 -5
- package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +18 -2
- package/dist/lite/.claude/hooks/session-reflection.d/91-npm-version-drift.sh +54 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/10-abrupt-stop-recovery.test.sh +95 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +62 -15
- package/dist/lite/.claude/hooks/tool-failure-diagnosis.sh +50 -0
- package/dist/lite/.claude/luminaries/andrej-karpathy.md +165 -0
- package/dist/lite/.claude/luminaries/dave-gray.md +155 -0
- package/dist/lite/.claude/luminaries/gary-klein.md +166 -0
- package/dist/lite/.claude/luminaries/kaoru-ishikawa.md +162 -0
- package/dist/lite/.claude/luminaries/lean-product-canvas.md +33 -0
- package/dist/lite/.claude/luminaries/luke-wroblewski.md +224 -0
- package/dist/lite/.claude/luminaries/ryan-singer.md +113 -0
- package/dist/lite/.claude/luminaries/steve-krug.md +168 -0
- package/dist/lite/.claude/luminaries/tony-ulwick.md +134 -0
- package/dist/lite/.claude/rules/arc-level-orientation-before-leaf.md +60 -0
- package/dist/lite/.claude/rules/architect-review-discipline.md +71 -0
- package/dist/lite/.claude/rules/auto-fix-criteria.md +126 -0
- package/dist/lite/.claude/rules/bassclef-hook-drift-blocker.md +144 -0
- package/dist/lite/.claude/rules/bassclef-models-discovery.md +107 -0
- package/dist/lite/.claude/rules/bassclef-workflow-parity.md +104 -0
- package/dist/lite/.claude/rules/cost-governance.md +154 -0
- package/dist/lite/.claude/rules/file-rename-discipline.md +95 -0
- package/dist/lite/.claude/rules/mechanism-fidelity.md +88 -0
- package/dist/lite/.claude/rules/prototype-production-coupling.md +125 -0
- package/dist/lite/.claude/rules/quick-fix-criteria.md +90 -0
- package/dist/lite/.claude/rules/session-artifacts.md +30 -0
- package/dist/lite/.claude/rules/sibling-smoke-after-substrate-change.md +107 -0
- package/dist/lite/.claude/rules/substrate-as-system.md +67 -0
- package/dist/lite/.claude/rules/usability.md +22 -0
- package/dist/lite/.claude/settings.json +20 -0
- package/dist/lite/.claude/skills/agent-research-spawn/SKILL.md +100 -0
- package/dist/lite/.claude/skills/architect-review/SKILL.md +201 -0
- package/dist/lite/.claude/skills/auto-fix/SKILL.md +297 -0
- package/dist/lite/.claude/skills/autonomous/SKILL.md +300 -0
- package/dist/lite/.claude/skills/bind-subdomain/SKILL.md +232 -0
- package/dist/lite/.claude/skills/canvas/SKILL.md +41 -0
- package/dist/lite/.claude/skills/deploy-prod/SKILL.md +106 -0
- package/dist/lite/.claude/skills/docs-sync/SKILL.md +188 -0
- package/dist/lite/.claude/skills/eisenhower/SKILL.md +273 -0
- package/dist/lite/.claude/skills/empathy-map/SKILL.md +168 -0
- package/dist/lite/.claude/skills/frontend-design/SKILL.md +53 -0
- package/dist/lite/.claude/skills/interview-me/SKILL.md +171 -0
- package/dist/lite/.claude/skills/journal-export/SKILL.md +293 -0
- package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +203 -0
- package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +256 -0
- package/dist/lite/.claude/skills/pick-luminaries/SKILL.md +100 -0
- package/dist/lite/.claude/skills/pre-mortem/SKILL.md +180 -0
- package/dist/lite/.claude/skills/prototype-gallery/SKILL.md +264 -0
- package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +257 -0
- package/dist/lite/.claude/skills/quick-fix/SKILL.md +273 -0
- package/dist/lite/.claude/skills/release/SKILL.md +311 -0
- package/dist/lite/.claude/skills/release-notes/SKILL.md +202 -0
- package/dist/lite/.claude/skills/rfc/SKILL.md +222 -0
- package/dist/lite/.claude/skills/riff-prototypes/SKILL.md +637 -0
- package/dist/lite/.claude/skills/secrets-bootstrap/SKILL.md +209 -0
- package/dist/lite/.claude/skills/skill/SKILL.md +304 -0
- package/dist/lite/.claude/skills/smoke-test/SKILL.md +100 -0
- package/dist/lite/.claude/skills/state/SKILL.md +204 -0
- package/dist/lite/.claude/skills/substrate-check/SKILL.md +218 -0
- package/dist/lite/.claude/skills/synthetic-user/SKILL.md +381 -0
- package/dist/lite/.claude/skills/task-scenarios/SKILL.md +105 -0
- package/dist/lite/.claude/skills/tier-dependency-audit/SKILL.md +139 -0
- package/dist/lite/.claude/skills/ux-migration/SKILL.md +424 -0
- package/dist/lite/.claude/skills/value-prop-canvas/SKILL.md +297 -0
- package/dist/lite/README.md +5 -1
- package/dist/lite/gitignore +6 -0
- package/dist/lite/lib/bassclef-dir-resolver.sh +85 -0
- package/dist/lite/lib/clone-failure-classifier.sh +59 -0
- package/dist/lite/lib/fixture-builder.sh +190 -0
- package/dist/lite/lib/fresh-install-check.sh +49 -0
- package/dist/lite/lib/hook-heartbeat.sh +0 -1
- package/dist/lite/lib/mechanism-fidelity.sh +569 -0
- package/dist/lite/lib/npm-version-check.sh +112 -0
- package/dist/lite/lib/preview-state.sh +229 -0
- package/dist/lite/lib/session-rescue.sh +0 -1
- package/dist/lite/scripts/generate-lite-manifest.sh +145 -4
- package/dist/lite/scripts/lite-manifest-drift-check.sh +5 -1
- package/dist/lite/standards/architect-review-discipline.md +222 -0
- package/dist/lite/standards/bassclef-platform.md +249 -0
- package/dist/lite/standards/bassclef-test-repos.md +97 -0
- package/dist/lite/standards/bassclef-wiring-manifest.json +25 -1
- package/dist/lite/standards/cold-adopter-harness.md +205 -0
- package/dist/lite/standards/data-management.md +135 -0
- package/dist/lite/standards/frontend-stack/nextjs-prisma.md +124 -0
- package/dist/lite/standards/lite-manifest-schema-changes.md +27 -0
- package/dist/lite/standards/lite-manifest.json +1456 -14
- package/dist/lite/standards/mechanism-fidelity.md +205 -0
- package/dist/lite/standards/operator-private-discipline.md +200 -0
- package/dist/lite/standards/operator-to-public-flow.md +195 -0
- package/dist/lite/standards/prototype-gallery.md +251 -0
- package/dist/lite/standards/pseudonym-key-schema.md +109 -0
- package/dist/lite/standards/sibling-validation-checklist.md +181 -0
- package/dist/lite/standards/slack-notifications.md +161 -0
- package/dist/lite/standards/synthetic-user-testing.md +173 -0
- package/dist/lite/standards/vendor-bindings.md +165 -0
- package/dist/lite/standards/vendor-plugins/frontend-design.md +66 -0
- package/dist/lite/standards/vpc-fit-validation.md +167 -0
- package/dist/lite/templates/gallery-yml.md +113 -0
- package/dist/lite/templates/product-canvas.md +169 -0
- package/dist/lite/templates/value-prop-canvas.md +180 -0
- package/package.json +3 -3
- /package/dist/lite/{whereami.md → docs/whereami.md} +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
# Save State — graceful checkpoint of all work in progress.
|
|
6
|
+
# Called by: session-end hook, idle auto-save loop, or manually.
|
|
7
|
+
#
|
|
8
|
+
# What it does:
|
|
9
|
+
# 1. Commits any uncommitted changes with a checkpoint message
|
|
10
|
+
# 2. Pushes to remote
|
|
11
|
+
# 3. Writes LAST_SAVE with timestamp + machine info
|
|
12
|
+
# 4. Removes SESSION_LOCK
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# .claude/hooks/save-state.sh # from repo root, interactive
|
|
16
|
+
# .claude/hooks/save-state.sh --quiet # suppress output (session-end)
|
|
17
|
+
# .claude/hooks/save-state.sh --quiet --auto # launchd auto-save; refuses
|
|
18
|
+
# # on feature/fix branches per
|
|
19
|
+
# # bassclef#581 atomic-PR rule
|
|
20
|
+
|
|
21
|
+
QUIET=""
|
|
22
|
+
AUTO=""
|
|
23
|
+
for arg in "$@"; do
|
|
24
|
+
case "$arg" in
|
|
25
|
+
--quiet) QUIET="true" ;;
|
|
26
|
+
--auto) AUTO="true" ;;
|
|
27
|
+
esac
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
log() {
|
|
31
|
+
# Note: must always return 0 so `set -e` does not terminate the script
|
|
32
|
+
# when --quiet is set ($QUIET="true" means the test fails and && skips
|
|
33
|
+
# echo, which would otherwise return 1). Closes bassclef#1054.
|
|
34
|
+
if [ -z "$QUIET" ]; then
|
|
35
|
+
echo "$1"
|
|
36
|
+
fi
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Trace log
|
|
41
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
42
|
+
if [ -f "$SCRIPT_DIR/trace-helper.sh" ]; then
|
|
43
|
+
source "$SCRIPT_DIR/trace-helper.sh"
|
|
44
|
+
trace_log "save-state" "checkpoint"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Check AUTO_SAVE_DEFER marker — respect agent-declared commit-sequence
|
|
48
|
+
# intent (bassclef#1026 / WU-A1).
|
|
49
|
+
#
|
|
50
|
+
# When the agent (or operator) is mid-sequence on commits with per-file
|
|
51
|
+
# intent (rationale per commit, semantic grouping), it writes a marker
|
|
52
|
+
# at state/auto-save-defer.marker. save-state.sh sees the marker and
|
|
53
|
+
# skips the auto-save checkpoint until the marker is cleared OR the
|
|
54
|
+
# marker is older than DEFER_MARKER_TTL_SECONDS (default 3600 = 1h).
|
|
55
|
+
#
|
|
56
|
+
# Only --auto invocations check the marker. Operator-triggered or
|
|
57
|
+
# session-end saves proceed normally regardless (those are intentional).
|
|
58
|
+
#
|
|
59
|
+
# Marker format: ISO timestamp on the first line; optional reason on
|
|
60
|
+
# subsequent lines. Example:
|
|
61
|
+
# $ cat state/auto-save-defer.marker
|
|
62
|
+
# 2026-06-17T06:35:00Z
|
|
63
|
+
# reason: wiki layer-consistency pass — per-file commits with rationale
|
|
64
|
+
DEFER_MARKER="state/auto-save-defer.marker"
|
|
65
|
+
DEFER_MARKER_TTL_SECONDS=${DEFER_MARKER_TTL_SECONDS:-3600}
|
|
66
|
+
if [ "$AUTO" = "true" ] && [ -f "$DEFER_MARKER" ]; then
|
|
67
|
+
DEFER_TS=$(head -1 "$DEFER_MARKER" 2>/dev/null | tr -d ' ')
|
|
68
|
+
if [ -n "$DEFER_TS" ]; then
|
|
69
|
+
DEFER_EPOCH=""
|
|
70
|
+
# macOS date doesn't interpret 'Z' as a UTC suffix; force TZ=UTC so
|
|
71
|
+
# the suffix-literal parse computes the correct epoch.
|
|
72
|
+
if DEFER_EPOCH=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$DEFER_TS" +%s 2>/dev/null); then
|
|
73
|
+
:
|
|
74
|
+
elif DEFER_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$DEFER_TS" +%s 2>/dev/null); then
|
|
75
|
+
:
|
|
76
|
+
elif DEFER_EPOCH=$(date -d "$DEFER_TS" +%s 2>/dev/null); then
|
|
77
|
+
:
|
|
78
|
+
fi
|
|
79
|
+
if [ -n "$DEFER_EPOCH" ]; then
|
|
80
|
+
NOW_DEFER=$(date +%s)
|
|
81
|
+
DEFER_AGE=$((NOW_DEFER - DEFER_EPOCH))
|
|
82
|
+
if [ "$DEFER_AGE" -lt "$DEFER_MARKER_TTL_SECONDS" ]; then
|
|
83
|
+
log "AUTO_SAVE_DEFER marker present (age ${DEFER_AGE}s) — skipping auto-save per bassclef#1026"
|
|
84
|
+
exit 0
|
|
85
|
+
else
|
|
86
|
+
log "AUTO_SAVE_DEFER marker is stale (age ${DEFER_AGE}s > ttl ${DEFER_MARKER_TTL_SECONDS}s) — clearing and proceeding"
|
|
87
|
+
rm -f "$DEFER_MARKER"
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Check SESSION_LOCK — respect locks held by other sessions (bassclef#736 / WU-A3)
|
|
94
|
+
# Behavior:
|
|
95
|
+
# - SAVE_STATE_FORCE=1: bypass entire lock check (logged via trace); per bassclef-upstream#402
|
|
96
|
+
# - No lock: proceed normally
|
|
97
|
+
# - Malformed lock or unparseable timestamp: proceed defensively (fail open)
|
|
98
|
+
# - Lock held by our pid ($$): proceed (it's our own lock)
|
|
99
|
+
# - Lock held by same pgid as $$: proceed (subshell inheritance — Claude harness writes lock; descendant runs save-state)
|
|
100
|
+
# - Lock held by an ancestor pid of $$: proceed (per bassclef-upstream#402; Bash-tool self-close case where pgid differs but harness pid is our ancestor)
|
|
101
|
+
# - Lock held by other pid + fresh (≤ LOCK_TTL_SECONDS): refuse, preserve lock, exit 0
|
|
102
|
+
# - Lock held by other pid + stale (> LOCK_TTL_SECONDS): clear lock with notice, proceed
|
|
103
|
+
LOCK_FILE=".claude/SESSION_LOCK"
|
|
104
|
+
LOCK_TTL_SECONDS=${LOCK_TTL_SECONDS:-1800} # 30 min default per mobile-ephemeral-session.md
|
|
105
|
+
LOCK_ANCESTOR_WALK_MAX_DEPTH=${LOCK_ANCESTOR_WALK_MAX_DEPTH:-20} # caps ancestor walk; mirrors thread-walk pattern
|
|
106
|
+
|
|
107
|
+
# Walk $$'s ancestor pid chain via repeated ps -o ppid=.
|
|
108
|
+
# Returns 0 if target_pid found in chain; 1 otherwise.
|
|
109
|
+
# Caps at LOCK_ANCESTOR_WALK_MAX_DEPTH and breaks at pid 1 (init) or cycle.
|
|
110
|
+
is_pid_ancestor_of_self() {
|
|
111
|
+
local target_pid="$1"
|
|
112
|
+
[ -z "$target_pid" ] && return 1
|
|
113
|
+
local current_pid=$$
|
|
114
|
+
local depth=0
|
|
115
|
+
while [ "$depth" -lt "$LOCK_ANCESTOR_WALK_MAX_DEPTH" ] \
|
|
116
|
+
&& [ -n "$current_pid" ] \
|
|
117
|
+
&& [ "$current_pid" -gt 1 ]; do
|
|
118
|
+
[ "$current_pid" = "$target_pid" ] && return 0
|
|
119
|
+
local next_pid
|
|
120
|
+
next_pid=$(ps -o ppid= -p "$current_pid" 2>/dev/null | tr -d ' ')
|
|
121
|
+
[ -z "$next_pid" ] && break
|
|
122
|
+
[ "$next_pid" = "$current_pid" ] && break # cycle guard
|
|
123
|
+
current_pid="$next_pid"
|
|
124
|
+
depth=$((depth + 1))
|
|
125
|
+
done
|
|
126
|
+
return 1
|
|
127
|
+
}
|
|
128
|
+
# Refuse --auto saves on every named branch in branching.md (bassclef#581 / WU-A4).
|
|
129
|
+
# Why: launchd auto-save fires every 15 min regardless of operator activity.
|
|
130
|
+
# - On main/master: violates 'no direct commit to main' (guardrails.md + branching.md).
|
|
131
|
+
# The push protection is separate; the commit still pollutes local main.
|
|
132
|
+
# - On feature/*, fix/*, spike/*: pollutes the atomic-PR rule with unrelated commits.
|
|
133
|
+
# Session-end and operator-triggered saves don't pass --auto; they proceed normally.
|
|
134
|
+
if [ "$AUTO" = "true" ]; then
|
|
135
|
+
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
|
|
136
|
+
case "$CURRENT_BRANCH" in
|
|
137
|
+
main|master|feature/*|fix/*|spike/*)
|
|
138
|
+
log "Refusing --auto save on $CURRENT_BRANCH — operator-owned branch (bassclef#581). Direct commits violate branching.md + guardrails.md. Save when intentional."
|
|
139
|
+
exit 0
|
|
140
|
+
;;
|
|
141
|
+
esac
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
if [ -f "$LOCK_FILE" ]; then
|
|
145
|
+
LOCK_PID=$(grep -E '^pid:' "$LOCK_FILE" 2>/dev/null | head -1 | sed -E 's/^pid:[[:space:]]*//' | tr -d '[:space:]')
|
|
146
|
+
LOCK_TS=$(grep -E '^timestamp:' "$LOCK_FILE" 2>/dev/null | head -1 | sed -E 's/^timestamp:[[:space:]]*//')
|
|
147
|
+
|
|
148
|
+
if [ "${SAVE_STATE_FORCE:-0}" = "1" ]; then
|
|
149
|
+
# Explicit operator/skill override. Used when the agent has verified
|
|
150
|
+
# the lock is theirs but the pgid/ancestor checks below mis-classify
|
|
151
|
+
# (e.g., Bash-tool spawn that detaches from harness process tree).
|
|
152
|
+
# Logged to trace per defensive-bash.md.
|
|
153
|
+
log "SAVE_STATE_FORCE=1 — bypassing SESSION_LOCK check (LOCK_PID=$LOCK_PID)"
|
|
154
|
+
if command -v trace_log >/dev/null 2>&1; then
|
|
155
|
+
trace_log "save-state.sh SAVE_STATE_FORCE override (LOCK_PID=$LOCK_PID)"
|
|
156
|
+
fi
|
|
157
|
+
rm -f "$LOCK_FILE"
|
|
158
|
+
elif [ -z "$LOCK_PID" ] || [ -z "$LOCK_TS" ]; then
|
|
159
|
+
log "Note: malformed SESSION_LOCK — proceeding defensively"
|
|
160
|
+
elif LOCK_PGID=$(ps -o pgid= -p "$LOCK_PID" 2>/dev/null | tr -d ' ') \
|
|
161
|
+
&& OUR_PGID=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ') \
|
|
162
|
+
&& [ -n "$LOCK_PGID" ] && [ -n "$OUR_PGID" ] \
|
|
163
|
+
&& [ "$LOCK_PGID" = "$OUR_PGID" ]; then
|
|
164
|
+
# Same process group — the lock was written by our session
|
|
165
|
+
# (Claude harness writes the lock; save-state.sh runs as descendant
|
|
166
|
+
# in the same job/pgid). Proceed and clear at end.
|
|
167
|
+
log "SESSION_LOCK is ours (same pgid $OUR_PGID) — proceeding"
|
|
168
|
+
elif is_pid_ancestor_of_self "$LOCK_PID"; then
|
|
169
|
+
# Different pgid but LOCK_PID IS in our process-ancestor chain —
|
|
170
|
+
# the lock is legitimately ours. This catches the Bash-tool
|
|
171
|
+
# self-close case where the tool spawns a shell in a fresh pgid
|
|
172
|
+
# but the harness pid is still our process-tree ancestor.
|
|
173
|
+
# Per bassclef-upstream#402.
|
|
174
|
+
log "SESSION_LOCK is ours (LOCK_PID $LOCK_PID is ancestor of $$) — proceeding"
|
|
175
|
+
else
|
|
176
|
+
# Different pid — determine if stale
|
|
177
|
+
LOCK_EPOCH=""
|
|
178
|
+
if LOCK_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$LOCK_TS" +%s 2>/dev/null); then
|
|
179
|
+
:
|
|
180
|
+
elif LOCK_EPOCH=$(date -d "$LOCK_TS" +%s 2>/dev/null); then
|
|
181
|
+
:
|
|
182
|
+
else
|
|
183
|
+
LOCK_EPOCH=""
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
if [ -z "$LOCK_EPOCH" ]; then
|
|
187
|
+
log "Note: SESSION_LOCK timestamp unparseable ($LOCK_TS) — proceeding defensively"
|
|
188
|
+
else
|
|
189
|
+
NOW_EPOCH=$(date +%s)
|
|
190
|
+
AGE_SECONDS=$((NOW_EPOCH - LOCK_EPOCH))
|
|
191
|
+
if [ "$AGE_SECONDS" -gt "$LOCK_TTL_SECONDS" ]; then
|
|
192
|
+
log "Stale SESSION_LOCK (age ${AGE_SECONDS}s, pid $LOCK_PID) — clearing and proceeding"
|
|
193
|
+
rm -f "$LOCK_FILE"
|
|
194
|
+
else
|
|
195
|
+
log "SESSION_LOCK held by other session (pid $LOCK_PID, age ${AGE_SECONDS}s) — refusing to commit; lock preserved"
|
|
196
|
+
exit 0
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
fi
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# 1. Commit uncommitted changes
|
|
203
|
+
# Skip if agent has files staged — avoid racing over the same files
|
|
204
|
+
ALREADY_STAGED=$(git diff --cached --name-only 2>/dev/null)
|
|
205
|
+
if [ -n "$ALREADY_STAGED" ]; then
|
|
206
|
+
log "Skipping auto-save — agent has staged files (avoiding race condition)"
|
|
207
|
+
else
|
|
208
|
+
DIRTY=$(git status --porcelain 2>/dev/null | grep -v '\.DS_Store' | grep -v 'SESSION_LOCK' | head -20)
|
|
209
|
+
if [ -n "$DIRTY" ]; then
|
|
210
|
+
# Stage tracked modified files + any docs/ or chronicle/ or runtime state
|
|
211
|
+
git add -u 2>/dev/null || true
|
|
212
|
+
git add docs/ chronicle/ specs/ 2>/dev/null || true
|
|
213
|
+
# state/ except state/markers/ — markers are runtime gate evidence that
|
|
214
|
+
# trigger session-rescue false-positives if committed (bassclef#341 / WU-A2).
|
|
215
|
+
# Negative pathspec syntax requires git >= 2.13 (stable since 2017).
|
|
216
|
+
git add 'state/' ':!state/markers' ':!state/markers/**' 2>/dev/null || true
|
|
217
|
+
git add .claude/projects/*/memory/ 2>/dev/null || true
|
|
218
|
+
|
|
219
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null)
|
|
220
|
+
if [ -n "$STAGED" ]; then
|
|
221
|
+
TIMESTAMP=$(date +%Y-%m-%dT%H:%M:%S)
|
|
222
|
+
FILE_COUNT=$(echo "$STAGED" | wc -l | tr -d ' ')
|
|
223
|
+
|
|
224
|
+
# No hostname in commit message per sunj-labs/bassclef#1471 + CCF-3.
|
|
225
|
+
# Branch name + file count + timestamp cover debug needs without leaking
|
|
226
|
+
# operator machine identifier.
|
|
227
|
+
BRANCH_NAME=$(git branch --show-current 2>/dev/null || echo "detached")
|
|
228
|
+
git commit -m "chore: auto-save checkpoint ($FILE_COUNT files on $BRANCH_NAME at $TIMESTAMP)" 2>/dev/null
|
|
229
|
+
log "Committed $FILE_COUNT files"
|
|
230
|
+
fi
|
|
231
|
+
fi
|
|
232
|
+
fi # end ALREADY_STAGED guard
|
|
233
|
+
|
|
234
|
+
# 2. Push to remote (NEVER push to protected branches)
|
|
235
|
+
BRANCH=$(git branch --show-current 2>/dev/null || echo "")
|
|
236
|
+
PROTECTED_BRANCHES="main master"
|
|
237
|
+
REMOTE_EXISTS=$(git remote 2>/dev/null | head -1)
|
|
238
|
+
|
|
239
|
+
if [ -z "$BRANCH" ]; then
|
|
240
|
+
log "Skipping push — detached HEAD or unknown branch"
|
|
241
|
+
elif echo "$PROTECTED_BRANCHES" | grep -qw "$BRANCH"; then
|
|
242
|
+
log "WARNING: on protected branch '$BRANCH' — skipping push. Commit saved locally."
|
|
243
|
+
elif [ -n "$REMOTE_EXISTS" ]; then
|
|
244
|
+
git push origin "$BRANCH" 2>/dev/null && log "Pushed to origin/$BRANCH" || log "Push failed (offline?)"
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
# 3. Write LAST_SAVE (local-only per .gitignore — never git-tracked;
|
|
248
|
+
# per sunj-labs/bassclef#1471 + CCF-3 + .claude/rules/identifier-leak-prevention.md).
|
|
249
|
+
# machine field kept locally for operator debug; file itself gitignored.
|
|
250
|
+
LAST_SAVE_FILE=".claude/LAST_SAVE"
|
|
251
|
+
TIMESTAMP=$(date +%Y-%m-%dT%H:%M:%S%z)
|
|
252
|
+
MACHINE=$(hostname -s 2>/dev/null || echo "unknown")
|
|
253
|
+
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
|
254
|
+
LAST_COMMIT=$(git log --oneline -1 2>/dev/null || echo "none")
|
|
255
|
+
|
|
256
|
+
cat > "$LAST_SAVE_FILE" <<SAVE
|
|
257
|
+
timestamp: $TIMESTAMP
|
|
258
|
+
machine: $MACHINE
|
|
259
|
+
branch: $BRANCH
|
|
260
|
+
last_commit: $LAST_COMMIT
|
|
261
|
+
SAVE
|
|
262
|
+
|
|
263
|
+
log "State saved locally at $TIMESTAMP"
|
|
264
|
+
|
|
265
|
+
# 4. Remove session lock
|
|
266
|
+
rm -f .claude/SESSION_LOCK 2>/dev/null
|
|
267
|
+
|
|
268
|
+
exit 0
|
|
@@ -27,7 +27,14 @@ fi
|
|
|
27
27
|
# Source the shared wirings accessor (per decomposition § Interface Registry).
|
|
28
28
|
__sd_wirings_lib="${BASSCLEF_DIR:-${PWD}}/.claude/hooks/_lib/wirings.sh"
|
|
29
29
|
if [ ! -f "$__sd_wirings_lib" ]; then
|
|
30
|
-
#
|
|
30
|
+
# Audible-fail cure per sunj-labs/bassclef-cli#105 — prior version dropped
|
|
31
|
+
# only a trace line nobody read. Fragment now names itself + the expected
|
|
32
|
+
# path on stderr so the adopter can act.
|
|
33
|
+
if declare -f emit_missing_dep_notice >/dev/null 2>&1; then
|
|
34
|
+
emit_missing_dep_notice "settings-drift" "$__sd_wirings_lib"
|
|
35
|
+
else
|
|
36
|
+
printf 'settings-drift: skip — wirings lib not at %s\n' "$__sd_wirings_lib" >&2
|
|
37
|
+
fi
|
|
31
38
|
trace_log "skip" "settings-drift" "wirings lib not yet synced" 2>/dev/null || true
|
|
32
39
|
unset __sd_wirings_lib
|
|
33
40
|
return 0 2>/dev/null || true
|
|
@@ -40,6 +40,25 @@
|
|
|
40
40
|
# so the per-file allowlist match below can recognize each path.
|
|
41
41
|
# Without -uall, a new file inside a previously empty marker dir would
|
|
42
42
|
# appear as "?? state/" (dir summary), bypassing the allowlist filter.
|
|
43
|
+
# Fresh-install guard — cures sunj-labs/bassclef-cli#108.
|
|
44
|
+
# On the first session after `bassclef init` in an empty repo, `git status
|
|
45
|
+
# --porcelain` sees hundreds of untracked files from init and the banner
|
|
46
|
+
# fires with "ABRUPT STOP" advice against a session that never existed.
|
|
47
|
+
# Skip when the repo has no commits AND no session lock — no prior session
|
|
48
|
+
# to stop abruptly.
|
|
49
|
+
{
|
|
50
|
+
_fi_sd="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
51
|
+
for _fi_c in "${_fi_sd}/../../../lib/fresh-install-check.sh" "${BASSCLEF_DIR:-.}/lib/fresh-install-check.sh"; do
|
|
52
|
+
# shellcheck source=/dev/null
|
|
53
|
+
[ -f "$_fi_c" ] && source "$_fi_c" && break
|
|
54
|
+
done
|
|
55
|
+
unset _fi_sd _fi_c
|
|
56
|
+
} 2>/dev/null || true
|
|
57
|
+
|
|
58
|
+
if declare -f is_fresh_install >/dev/null 2>&1 && is_fresh_install; then
|
|
59
|
+
return 0 2>/dev/null || exit 0
|
|
60
|
+
fi
|
|
61
|
+
|
|
43
62
|
DIRTY_TREE_RAW=$(git status --porcelain -uall 2>/dev/null)
|
|
44
63
|
|
|
45
64
|
# Strip the 3-char porcelain status prefix (XY + space) before matching
|
|
@@ -63,6 +82,39 @@ DIRTY_TREE=$(echo "$DIRTY_TREE_RAW" | awk '
|
|
|
63
82
|
print
|
|
64
83
|
}' | head -5)
|
|
65
84
|
|
|
85
|
+
# Class D (bassclef-upstream#1719): no session has ever ended here.
|
|
86
|
+
#
|
|
87
|
+
# A dirty tree is a proxy for "the last session ended without saving". That
|
|
88
|
+
# holds for a repo with history. It is wrong for a repo whose only prior
|
|
89
|
+
# event was an install: `bassclef init` copies 400+ files and commits none
|
|
90
|
+
# of them, so an adopter's very first session start reported an abrupt stop
|
|
91
|
+
# and told them recovery was needed. Nothing had stopped.
|
|
92
|
+
#
|
|
93
|
+
# Same reasoning as Class 0 above, which bassclef#888 added because a banner
|
|
94
|
+
# that fires when nothing is wrong trains the reader to skip the one that
|
|
95
|
+
# matters.
|
|
96
|
+
#
|
|
97
|
+
# Three signals that a session ran here before. Any one is enough.
|
|
98
|
+
PRIOR_SESSION=""
|
|
99
|
+
[ -f ".claude/LAST_SAVE" ] && PRIOR_SESSION="last-save"
|
|
100
|
+
if [ -z "$PRIOR_SESSION" ]; then
|
|
101
|
+
for d in chronicle docs/chronicle; do
|
|
102
|
+
if [ -d "$d" ] && [ -n "$(ls -A "$d" 2>/dev/null)" ]; then
|
|
103
|
+
PRIOR_SESSION="session-log"; break
|
|
104
|
+
fi
|
|
105
|
+
done
|
|
106
|
+
fi
|
|
107
|
+
if [ -z "$PRIOR_SESSION" ]; then
|
|
108
|
+
COMMIT_COUNT=$(git rev-list --count HEAD 2>/dev/null || echo 0)
|
|
109
|
+
[ "${COMMIT_COUNT:-0}" -gt 1 ] && PRIOR_SESSION="commit-history"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
if [ -n "$DIRTY_TREE" ] && [ -z "$PRIOR_SESSION" ]; then
|
|
113
|
+
# Fresh install. Uncommitted files are what the installer left, not what a
|
|
114
|
+
# crash left. Say nothing rather than raise a false alarm.
|
|
115
|
+
DIRTY_TREE=""
|
|
116
|
+
fi
|
|
117
|
+
|
|
66
118
|
if [ -n "$DIRTY_TREE" ]; then
|
|
67
119
|
echo "### ABRUPT STOP DETECTED — RECOVERY NEEDED"
|
|
68
120
|
echo ""
|
|
@@ -51,6 +51,28 @@ if [ "${SKIP_ORIENTATION_GATE:-0}" = "1" ]; then
|
|
|
51
51
|
return 0 2>/dev/null || exit 0
|
|
52
52
|
fi
|
|
53
53
|
|
|
54
|
+
# Fresh-install guard — cures sunj-labs/bassclef-cli#102.
|
|
55
|
+
# Silence when the repo is fresh-install AND no goal docs on disk. The gate
|
|
56
|
+
# needs an orientation target; a cold repo has none. Firing a red BLOCKED
|
|
57
|
+
# in that state reads as a broken install. Established repos and repos with
|
|
58
|
+
# staged goal docs still fire the gate as before.
|
|
59
|
+
{
|
|
60
|
+
_fi_sd="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
61
|
+
for _fi_c in "${_fi_sd}/../../../lib/fresh-install-check.sh" "${BASSCLEF_DIR:-.}/lib/fresh-install-check.sh"; do
|
|
62
|
+
# shellcheck source=/dev/null
|
|
63
|
+
[ -f "$_fi_c" ] && source "$_fi_c" && break
|
|
64
|
+
done
|
|
65
|
+
unset _fi_sd _fi_c
|
|
66
|
+
} 2>/dev/null || true
|
|
67
|
+
|
|
68
|
+
if declare -f is_fresh_install >/dev/null 2>&1 && is_fresh_install; then
|
|
69
|
+
# Also require no goal docs AND no whereami.md on disk. An operator who
|
|
70
|
+
# staged either before their first commit is oriented enough for the gate.
|
|
71
|
+
if ! ls docs/iteration-bets/*.md >/dev/null 2>&1 && [ ! -f "docs/whereami.md" ]; then
|
|
72
|
+
return 0 2>/dev/null || exit 0
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
|
|
54
76
|
# Branch detection. Silent only when git is missing.
|
|
55
77
|
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
|
|
56
78
|
if [ -z "$BRANCH" ]; then
|
|
@@ -133,13 +155,27 @@ if [ -f "substrate.config.md" ]; then
|
|
|
133
155
|
fi
|
|
134
156
|
fi
|
|
135
157
|
|
|
136
|
-
#
|
|
158
|
+
# RFC-0008 F3 — this gate already knows its own state. Line 118 branches on
|
|
159
|
+
# whether a goal exists. With a goal, not walking it is a real gap. With no
|
|
160
|
+
# goal there is nothing to walk, and a first-run adopter who has done nothing
|
|
161
|
+
# wrong should not open their first session to a stop sign. Same content,
|
|
162
|
+
# severity that matches what happened.
|
|
163
|
+
if [ -n "$ACTIVE_BET" ] && [ "$ACTIVE_BET" != "(none in whereami)" ]; then
|
|
164
|
+
GATE_HEAD="🛑 BLOCKED: orientation-gate"
|
|
165
|
+
GATE_LEAD="Before /sprint or /longrun prep proposes work, read the bigger picture first."
|
|
166
|
+
GATE_CLOSE="Silence is not deferral. Default is read first."
|
|
167
|
+
else
|
|
168
|
+
GATE_HEAD="NOTE: orientation-gate"
|
|
169
|
+
GATE_LEAD="No goal is in flight, so there is nothing to read first. This is how the gate will look once one is."
|
|
170
|
+
GATE_CLOSE="Nothing to do yet. This becomes a gate once a goal is in flight."
|
|
171
|
+
fi
|
|
172
|
+
|
|
137
173
|
echo ""
|
|
138
174
|
echo "============================================"
|
|
139
|
-
echo "
|
|
175
|
+
echo "${GATE_HEAD}"
|
|
140
176
|
echo "============================================"
|
|
141
177
|
echo ""
|
|
142
|
-
echo "
|
|
178
|
+
echo "${GATE_LEAD}"
|
|
143
179
|
echo ""
|
|
144
180
|
echo "Active iteration_bet: ${ACTIVE_BET}"
|
|
145
181
|
echo ""
|
|
@@ -178,7 +214,7 @@ echo ""
|
|
|
178
214
|
echo "OR explicitly defer per .claude/rules/blocked-items.md:"
|
|
179
215
|
echo " \"skip orientation-gate this session\""
|
|
180
216
|
echo ""
|
|
181
|
-
echo "
|
|
217
|
+
echo "${GATE_CLOSE}"
|
|
182
218
|
echo ""
|
|
183
219
|
echo "Override (logged): SKIP_ORIENTATION_GATE=1"
|
|
184
220
|
echo "Per bassclef#1183 + bassclef#1266 + bassclef#1275 + bassclef#1347."
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# tier: lite
|
|
3
|
+
# install-class: dual
|
|
3
4
|
# Hook heartbeat check — reads standards/hook-cadence.json, calls
|
|
4
5
|
# heartbeat_check per named hook, emits BLOCKED banner for any hook
|
|
5
6
|
# silent past its expected max_age.
|
|
@@ -19,12 +20,18 @@
|
|
|
19
20
|
# Anchors: @luminary michael-nygard (steady-state), @luminary
|
|
20
21
|
# jerome-saltzer-and-michael-schroeder (complete mediation).
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# Pattern matches session-reflection.sh:13.
|
|
23
|
+
# Probe bundled path first (dist/lite/ ship path per ADR-056 D3), then
|
|
24
|
+
# $BASSCLEF_DIR fallback for synced adopters. Pattern matches
|
|
25
|
+
# session-reflection.sh:11-17. Cures #1732 Phase A silent-fail at operator
|
|
26
|
+
# scope where $BASSCLEF_DIR resolves to $HOME with no standards/ or lib/.
|
|
27
27
|
_sd="$(dirname "${BASH_SOURCE[0]:-$0}")"
|
|
28
|
+
|
|
29
|
+
MANIFEST=""
|
|
30
|
+
for _m in "${_sd}/../../../standards/hook-cadence.json" \
|
|
31
|
+
"${BASSCLEF_DIR:-.}/standards/hook-cadence.json"; do
|
|
32
|
+
[ -f "$_m" ] && MANIFEST="$_m" && break
|
|
33
|
+
done
|
|
34
|
+
|
|
28
35
|
HB_LIB=""
|
|
29
36
|
for _c in "${_sd}/../../../lib/hook-heartbeat.sh" \
|
|
30
37
|
"${BASSCLEF_DIR:-.}/lib/hook-heartbeat.sh"; do
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# tier: lite
|
|
3
|
+
# install-class: dual
|
|
3
4
|
# Hook manifest staleness — flags standards/hook-cadence.json when it
|
|
4
5
|
# has not been updated since a new .claude/hooks/*.sh file landed.
|
|
5
6
|
# Nygard "who monitors the monitor?" reflex: the backstop itself can
|
|
@@ -10,8 +11,23 @@
|
|
|
10
11
|
# works cleanly both sourced AND standalone. Silent-fail on missing
|
|
11
12
|
# manifest or missing jq.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
# Probe bundled path first (dist/lite/ ship path per ADR-056 D3), then
|
|
15
|
+
# $BASSCLEF_DIR fallback for synced adopters. Pattern matches
|
|
16
|
+
# session-reflection.sh:11-17. Cures #1732 Phase A silent-fail at operator
|
|
17
|
+
# scope where $BASSCLEF_DIR resolves to $HOME with no standards/ or .claude/hooks/.
|
|
18
|
+
_sd="$(dirname "${BASH_SOURCE[0]:-$0}")"
|
|
19
|
+
|
|
20
|
+
MANIFEST=""
|
|
21
|
+
for _m in "${_sd}/../../../standards/hook-cadence.json" \
|
|
22
|
+
"${BASSCLEF_DIR:-.}/standards/hook-cadence.json"; do
|
|
23
|
+
[ -f "$_m" ] && MANIFEST="$_m" && break
|
|
24
|
+
done
|
|
25
|
+
|
|
26
|
+
HOOKS_DIR=""
|
|
27
|
+
for _h in "${_sd}/.." \
|
|
28
|
+
"${BASSCLEF_DIR:-.}/.claude/hooks"; do
|
|
29
|
+
[ -d "$_h" ] && HOOKS_DIR="$_h" && break
|
|
30
|
+
done
|
|
15
31
|
|
|
16
32
|
if [ ! -f "$MANIFEST" ]; then
|
|
17
33
|
return 0 2>/dev/null || exit 0
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# shellcheck shell=bash disable=SC1090
|
|
3
|
+
# tier: lite
|
|
4
|
+
# install-class: dual
|
|
5
|
+
# 91-npm-version-drift.sh — SessionStart fragment that surfaces npm-version
|
|
6
|
+
# drift for @thebassclef/lite. Sources lib/npm-version-check.sh + calls
|
|
7
|
+
# check_lite_version_drift. Silent-return on every failure class per
|
|
8
|
+
# ADR-056 D3 fail-soft discipline.
|
|
9
|
+
#
|
|
10
|
+
# Guarded by is_fresh_install — no drift banner on the first session of
|
|
11
|
+
# a brand-new repo (cli#102 + cli#108 companion pattern).
|
|
12
|
+
#
|
|
13
|
+
# @luminary alan-cooper — adopter hears drift banner; acts on `bassclef sync`
|
|
14
|
+
# @luminary michael-nygard — silent-pass on every failure class
|
|
15
|
+
|
|
16
|
+
# Source lib probe — bundled path first (ADR-056 D3), then $HOME/lib fallback.
|
|
17
|
+
{
|
|
18
|
+
_nd_sd="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
19
|
+
for _nd_c in "${_nd_sd}/../../../lib/npm-version-check.sh" \
|
|
20
|
+
"${BASSCLEF_DIR:-.}/lib/npm-version-check.sh" \
|
|
21
|
+
"${HOME:-/}/lib/npm-version-check.sh"; do
|
|
22
|
+
[ -f "$_nd_c" ] && source "$_nd_c" && break
|
|
23
|
+
done
|
|
24
|
+
unset _nd_sd _nd_c
|
|
25
|
+
} 2>/dev/null || true
|
|
26
|
+
|
|
27
|
+
# Source fresh-install helper (from Sub-feature 2).
|
|
28
|
+
{
|
|
29
|
+
_fi_sd="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
30
|
+
for _fi_c in "${_fi_sd}/../../../lib/fresh-install-check.sh" \
|
|
31
|
+
"${BASSCLEF_DIR:-.}/lib/fresh-install-check.sh" \
|
|
32
|
+
"${HOME:-/}/lib/fresh-install-check.sh"; do
|
|
33
|
+
[ -f "$_fi_c" ] && source "$_fi_c" && break
|
|
34
|
+
done
|
|
35
|
+
unset _fi_sd _fi_c
|
|
36
|
+
} 2>/dev/null || true
|
|
37
|
+
|
|
38
|
+
# Silence on fresh install per Nygard N5 cure — the adopter has bigger
|
|
39
|
+
# things to look at on turn 1 than a drift notice.
|
|
40
|
+
if declare -f is_fresh_install >/dev/null 2>&1 && is_fresh_install; then
|
|
41
|
+
return 0 2>/dev/null || exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Silent-pass if the lib is not present (defensive).
|
|
45
|
+
if ! declare -f check_lite_version_drift >/dev/null 2>&1; then
|
|
46
|
+
return 0 2>/dev/null || exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Fire the check. Banner (if any) goes to stdout, then normal fragment flow
|
|
50
|
+
# continues. Wrap in a subshell so the function's return code cannot break
|
|
51
|
+
# the parent session-reflection chain under set -e.
|
|
52
|
+
( check_lite_version_drift ) 2>/dev/null || true
|
|
53
|
+
|
|
54
|
+
return 0 2>/dev/null || exit 0
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: standard
|
|
3
|
+
# fixtures: live-substrate
|
|
4
|
+
# Tier 0 test for .claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh
|
|
5
|
+
#
|
|
6
|
+
# Closes sunj-labs/bassclef-upstream#1719 — a fresh install leaves 400+
|
|
7
|
+
# uncommitted files, so the fragment's dirty-tree condition fires on an
|
|
8
|
+
# adopter's very first session and tells them an abrupt stop was detected.
|
|
9
|
+
# Nothing stopped. They installed thirty seconds ago.
|
|
10
|
+
#
|
|
11
|
+
# Test list (per .claude/rules/test-list-discipline.md):
|
|
12
|
+
# [x] 1. dirty tree WITH prior-session evidence -> banner fires (unchanged)
|
|
13
|
+
# [x] 2. dirty tree with NO prior session -> silent (#1719)
|
|
14
|
+
# [x] 3. clean tree -> silent (unchanged)
|
|
15
|
+
# [x] 4. sourced under set -e: parent survives (criterion 12)
|
|
16
|
+
# [x] 5. standalone: no "return: can only" error (criterion 12)
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
20
|
+
FRAG="$REPO_ROOT/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh"
|
|
21
|
+
PASS=0; FAIL=0
|
|
22
|
+
pass() { PASS=$((PASS+1)); echo " PASS: $1"; }
|
|
23
|
+
fail() { FAIL=$((FAIL+1)); echo " FAIL: $1"; }
|
|
24
|
+
|
|
25
|
+
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
|
26
|
+
|
|
27
|
+
# Build a repo in one of two shapes: with or without a prior session.
|
|
28
|
+
make_repo() {
|
|
29
|
+
local dir="$1" prior="$2"
|
|
30
|
+
rm -rf "$dir"; mkdir -p "$dir"; cd "$dir" || return 1
|
|
31
|
+
git init -q .; git config user.email t@t.test; git config user.name t
|
|
32
|
+
echo x > README.md; git add -A; git commit -qm "initial"
|
|
33
|
+
if [ "$prior" = "with-prior" ]; then
|
|
34
|
+
mkdir -p .claude; printf 'timestamp: 2026-09-01T00:00:00Z\n' > .claude/LAST_SAVE
|
|
35
|
+
git add -A; git commit -qm "a real session ended here"
|
|
36
|
+
fi
|
|
37
|
+
# the install: many uncommitted files, as bassclef init leaves them
|
|
38
|
+
mkdir -p .claude/skills/temperance
|
|
39
|
+
echo "---" > .claude/skills/temperance/SKILL.md
|
|
40
|
+
echo "untracked" > some-file.md
|
|
41
|
+
cd - >/dev/null || return 1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
run_frag() { # $1 = repo dir
|
|
45
|
+
( cd "$1" && bash "$FRAG" 2>&1 )
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# --- Test 1: dirty tree, prior session existed -> banner still fires ---
|
|
49
|
+
make_repo "$TMP/with" with-prior
|
|
50
|
+
OUT=$(run_frag "$TMP/with")
|
|
51
|
+
if echo "$OUT" | grep -q "ABRUPT STOP DETECTED"; then
|
|
52
|
+
pass "Test 1: dirty tree with a prior session still reports an abrupt stop"
|
|
53
|
+
else
|
|
54
|
+
fail "Test 1: banner went silent where it should still fire"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# --- Test 2: dirty tree, no prior session -> silent (#1719) ---
|
|
58
|
+
make_repo "$TMP/fresh" no-prior
|
|
59
|
+
OUT=$(run_frag "$TMP/fresh")
|
|
60
|
+
if echo "$OUT" | grep -q "ABRUPT STOP DETECTED"; then
|
|
61
|
+
fail "Test 2: fresh install reports an abrupt stop that never happened"
|
|
62
|
+
else
|
|
63
|
+
pass "Test 2: fresh install stays silent"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# --- Test 3: clean tree -> silent ---
|
|
67
|
+
rm -rf "$TMP/clean"; mkdir -p "$TMP/clean"
|
|
68
|
+
( cd "$TMP/clean" && git init -q . && git config user.email t@t.test && git config user.name t \
|
|
69
|
+
&& echo x > README.md && git add -A && git commit -qm init ) >/dev/null 2>&1
|
|
70
|
+
OUT=$(run_frag "$TMP/clean")
|
|
71
|
+
if echo "$OUT" | grep -q "ABRUPT STOP DETECTED"; then
|
|
72
|
+
fail "Test 3: clean tree reported an abrupt stop"
|
|
73
|
+
else
|
|
74
|
+
pass "Test 3: clean tree stays silent"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# --- Test 4 + 5: invocation-mode variation (test-sufficiency criterion 12) ---
|
|
78
|
+
# shellcheck source=/dev/null # $FRAG is the fragment under test; path is a fixture
|
|
79
|
+
OUT=$( set -e; . "$FRAG" >/dev/null 2>&1; echo "parent-survived" )
|
|
80
|
+
if echo "$OUT" | grep -q "parent-survived"; then
|
|
81
|
+
pass "Test 4: sourced under set -e, the parent survives"
|
|
82
|
+
else
|
|
83
|
+
fail "Test 4: sourcing killed the parent"
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
ERR=$(bash "$FRAG" 2>&1 >/dev/null)
|
|
87
|
+
if echo "$ERR" | grep -q "return: can only"; then
|
|
88
|
+
fail "Test 5: standalone run emits a return-outside-function error"
|
|
89
|
+
else
|
|
90
|
+
pass "Test 5: standalone run is clean"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
echo ""
|
|
94
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
95
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|