@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.
Files changed (135) hide show
  1. package/dist/cli.cjs +23 -6
  2. package/dist/cli.js +23 -6
  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/bassclef-configs.jsonc +56 -0
  7. package/dist/lite/.claude/bassclef-orientation.md +120 -0
  8. package/dist/lite/.claude/hooks/adr-discipline-check.sh +132 -0
  9. package/dist/lite/.claude/hooks/atomic-pr-check.sh +169 -0
  10. package/dist/lite/.claude/hooks/auto-save-idle.sh +51 -0
  11. package/dist/lite/.claude/hooks/bassclef-sync.sh +51 -13
  12. package/dist/lite/.claude/hooks/bet-doc-gate.sh +283 -0
  13. package/dist/lite/.claude/hooks/bug-diagnosis.sh +64 -0
  14. package/dist/lite/.claude/hooks/destructive-command-guard.sh +136 -0
  15. package/dist/lite/.claude/hooks/file-rename-discipline-check.sh +147 -0
  16. package/dist/lite/.claude/hooks/hook-classifier-canary.sh +158 -0
  17. package/dist/lite/.claude/hooks/kiss-default-check.sh +129 -0
  18. package/dist/lite/.claude/hooks/multi-tenancy-check.sh +83 -0
  19. package/dist/lite/.claude/hooks/oo-ad-entry-point-check.sh +216 -0
  20. package/dist/lite/.claude/hooks/operator-private-path-check.sh +203 -0
  21. package/dist/lite/.claude/hooks/pattern-annotation-validate.sh +142 -0
  22. package/dist/lite/.claude/hooks/plain-english-steering.sh +15 -1
  23. package/dist/lite/.claude/hooks/post-write-trace.sh +66 -0
  24. package/dist/lite/.claude/hooks/pr-body-scrub-check.sh +446 -0
  25. package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +137 -0
  26. package/dist/lite/.claude/hooks/pre-gh-pr-body-scrub.sh +192 -0
  27. package/dist/lite/.claude/hooks/pre-gh-pr-title-scrub.sh +172 -0
  28. package/dist/lite/.claude/hooks/role-entry-gate.sh +115 -0
  29. package/dist/lite/.claude/hooks/save-state.sh +268 -0
  30. package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +8 -1
  31. package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +52 -0
  32. package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +40 -4
  33. package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +12 -5
  34. package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +18 -2
  35. package/dist/lite/.claude/hooks/session-reflection.d/91-npm-version-drift.sh +54 -0
  36. package/dist/lite/.claude/hooks/session-reflection.d/tests/10-abrupt-stop-recovery.test.sh +95 -0
  37. package/dist/lite/.claude/hooks/session-reflection.sh +62 -15
  38. package/dist/lite/.claude/hooks/tool-failure-diagnosis.sh +50 -0
  39. package/dist/lite/.claude/luminaries/andrej-karpathy.md +165 -0
  40. package/dist/lite/.claude/luminaries/dave-gray.md +155 -0
  41. package/dist/lite/.claude/luminaries/gary-klein.md +166 -0
  42. package/dist/lite/.claude/luminaries/kaoru-ishikawa.md +162 -0
  43. package/dist/lite/.claude/luminaries/lean-product-canvas.md +33 -0
  44. package/dist/lite/.claude/luminaries/luke-wroblewski.md +224 -0
  45. package/dist/lite/.claude/luminaries/ryan-singer.md +113 -0
  46. package/dist/lite/.claude/luminaries/steve-krug.md +168 -0
  47. package/dist/lite/.claude/luminaries/tony-ulwick.md +134 -0
  48. package/dist/lite/.claude/rules/arc-level-orientation-before-leaf.md +60 -0
  49. package/dist/lite/.claude/rules/architect-review-discipline.md +71 -0
  50. package/dist/lite/.claude/rules/auto-fix-criteria.md +126 -0
  51. package/dist/lite/.claude/rules/bassclef-hook-drift-blocker.md +144 -0
  52. package/dist/lite/.claude/rules/bassclef-models-discovery.md +107 -0
  53. package/dist/lite/.claude/rules/bassclef-workflow-parity.md +104 -0
  54. package/dist/lite/.claude/rules/cost-governance.md +154 -0
  55. package/dist/lite/.claude/rules/file-rename-discipline.md +95 -0
  56. package/dist/lite/.claude/rules/mechanism-fidelity.md +88 -0
  57. package/dist/lite/.claude/rules/prototype-production-coupling.md +125 -0
  58. package/dist/lite/.claude/rules/quick-fix-criteria.md +90 -0
  59. package/dist/lite/.claude/rules/session-artifacts.md +30 -0
  60. package/dist/lite/.claude/rules/sibling-smoke-after-substrate-change.md +107 -0
  61. package/dist/lite/.claude/rules/substrate-as-system.md +67 -0
  62. package/dist/lite/.claude/rules/usability.md +22 -0
  63. package/dist/lite/.claude/settings.json +20 -0
  64. package/dist/lite/.claude/skills/agent-research-spawn/SKILL.md +100 -0
  65. package/dist/lite/.claude/skills/architect-review/SKILL.md +201 -0
  66. package/dist/lite/.claude/skills/auto-fix/SKILL.md +297 -0
  67. package/dist/lite/.claude/skills/autonomous/SKILL.md +300 -0
  68. package/dist/lite/.claude/skills/bind-subdomain/SKILL.md +232 -0
  69. package/dist/lite/.claude/skills/canvas/SKILL.md +41 -0
  70. package/dist/lite/.claude/skills/deploy-prod/SKILL.md +106 -0
  71. package/dist/lite/.claude/skills/docs-sync/SKILL.md +188 -0
  72. package/dist/lite/.claude/skills/eisenhower/SKILL.md +273 -0
  73. package/dist/lite/.claude/skills/empathy-map/SKILL.md +168 -0
  74. package/dist/lite/.claude/skills/frontend-design/SKILL.md +53 -0
  75. package/dist/lite/.claude/skills/interview-me/SKILL.md +171 -0
  76. package/dist/lite/.claude/skills/journal-export/SKILL.md +293 -0
  77. package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +203 -0
  78. package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +256 -0
  79. package/dist/lite/.claude/skills/pick-luminaries/SKILL.md +100 -0
  80. package/dist/lite/.claude/skills/pre-mortem/SKILL.md +180 -0
  81. package/dist/lite/.claude/skills/prototype-gallery/SKILL.md +264 -0
  82. package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +257 -0
  83. package/dist/lite/.claude/skills/quick-fix/SKILL.md +273 -0
  84. package/dist/lite/.claude/skills/release/SKILL.md +311 -0
  85. package/dist/lite/.claude/skills/release-notes/SKILL.md +202 -0
  86. package/dist/lite/.claude/skills/rfc/SKILL.md +222 -0
  87. package/dist/lite/.claude/skills/riff-prototypes/SKILL.md +637 -0
  88. package/dist/lite/.claude/skills/secrets-bootstrap/SKILL.md +209 -0
  89. package/dist/lite/.claude/skills/skill/SKILL.md +304 -0
  90. package/dist/lite/.claude/skills/smoke-test/SKILL.md +100 -0
  91. package/dist/lite/.claude/skills/state/SKILL.md +204 -0
  92. package/dist/lite/.claude/skills/substrate-check/SKILL.md +218 -0
  93. package/dist/lite/.claude/skills/synthetic-user/SKILL.md +381 -0
  94. package/dist/lite/.claude/skills/task-scenarios/SKILL.md +105 -0
  95. package/dist/lite/.claude/skills/tier-dependency-audit/SKILL.md +139 -0
  96. package/dist/lite/.claude/skills/ux-migration/SKILL.md +424 -0
  97. package/dist/lite/.claude/skills/value-prop-canvas/SKILL.md +297 -0
  98. package/dist/lite/README.md +5 -1
  99. package/dist/lite/gitignore +6 -0
  100. package/dist/lite/lib/bassclef-dir-resolver.sh +85 -0
  101. package/dist/lite/lib/clone-failure-classifier.sh +59 -0
  102. package/dist/lite/lib/fixture-builder.sh +190 -0
  103. package/dist/lite/lib/fresh-install-check.sh +49 -0
  104. package/dist/lite/lib/hook-heartbeat.sh +0 -1
  105. package/dist/lite/lib/mechanism-fidelity.sh +569 -0
  106. package/dist/lite/lib/npm-version-check.sh +112 -0
  107. package/dist/lite/lib/preview-state.sh +229 -0
  108. package/dist/lite/lib/session-rescue.sh +0 -1
  109. package/dist/lite/scripts/generate-lite-manifest.sh +145 -4
  110. package/dist/lite/scripts/lite-manifest-drift-check.sh +5 -1
  111. package/dist/lite/standards/architect-review-discipline.md +222 -0
  112. package/dist/lite/standards/bassclef-platform.md +249 -0
  113. package/dist/lite/standards/bassclef-test-repos.md +97 -0
  114. package/dist/lite/standards/bassclef-wiring-manifest.json +25 -1
  115. package/dist/lite/standards/cold-adopter-harness.md +205 -0
  116. package/dist/lite/standards/data-management.md +135 -0
  117. package/dist/lite/standards/frontend-stack/nextjs-prisma.md +124 -0
  118. package/dist/lite/standards/lite-manifest-schema-changes.md +27 -0
  119. package/dist/lite/standards/lite-manifest.json +1456 -14
  120. package/dist/lite/standards/mechanism-fidelity.md +205 -0
  121. package/dist/lite/standards/operator-private-discipline.md +200 -0
  122. package/dist/lite/standards/operator-to-public-flow.md +195 -0
  123. package/dist/lite/standards/prototype-gallery.md +251 -0
  124. package/dist/lite/standards/pseudonym-key-schema.md +109 -0
  125. package/dist/lite/standards/sibling-validation-checklist.md +181 -0
  126. package/dist/lite/standards/slack-notifications.md +161 -0
  127. package/dist/lite/standards/synthetic-user-testing.md +173 -0
  128. package/dist/lite/standards/vendor-bindings.md +165 -0
  129. package/dist/lite/standards/vendor-plugins/frontend-design.md +66 -0
  130. package/dist/lite/standards/vpc-fit-validation.md +167 -0
  131. package/dist/lite/templates/gallery-yml.md +113 -0
  132. package/dist/lite/templates/product-canvas.md +169 -0
  133. package/dist/lite/templates/value-prop-canvas.md +180 -0
  134. package/package.json +3 -3
  135. /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
- # Substrate not fully synced; sister modules surface the larger issue.
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
- # Emit BLOCKED block
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 "🛑 BLOCKED: orientation-gate"
175
+ echo "${GATE_HEAD}"
140
176
  echo "============================================"
141
177
  echo ""
142
- echo "Before /sprint or /longrun prep proposes work, read the bigger picture first."
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 "Silence is not deferral. Default is read first."
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
- MANIFEST="${BASSCLEF_DIR:-.}/standards/hook-cadence.json"
23
-
24
- # Probe bundled lib path first (dist/lite/ ship path per #1631 goal
25
- # 2026-09-14b), then $BASSCLEF_DIR fallback for synced adopters.
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
- MANIFEST="${BASSCLEF_DIR:-.}/standards/hook-cadence.json"
14
- HOOKS_DIR="${BASSCLEF_DIR:-.}/.claude/hooks"
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