@thebassclef/lite 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +240 -23
- package/dist/cli.js +242 -25
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/artifact-ingestion-gate.sh +357 -0
- package/dist/lite/.claude/hooks/assert-verify-steering.sh +77 -0
- package/dist/lite/.claude/hooks/bassclef-source-config-validate.sh +215 -0
- package/dist/lite/.claude/hooks/bassclef-sync.sh +716 -0
- package/dist/lite/.claude/hooks/compound-noun-scrub.sh +292 -0
- package/dist/lite/.claude/hooks/kiss-expansion-inject.sh +69 -0
- package/dist/lite/.claude/hooks/longrun-prep-compounding-sequence-check.sh +492 -0
- package/dist/lite/.claude/hooks/plain-english-steering.sh +156 -0
- package/dist/lite/.claude/hooks/post-skill-friction-check.sh +177 -0
- package/dist/lite/.claude/hooks/post-skill-telemetry.sh +62 -0
- package/dist/lite/.claude/hooks/pre-build-gate.sh +511 -0
- package/dist/lite/.claude/hooks/pre-commit-gate.sh +451 -0
- package/dist/lite/.claude/hooks/session-end.sh +433 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +303 -0
- package/dist/lite/.claude/hooks/skill-body-grade-gate.sh +219 -0
- package/dist/lite/.claude/hooks/skill-body-intent-drift.sh +107 -0
- package/dist/lite/.claude/hooks/state-validate.sh +271 -0
- package/dist/lite/.claude/hooks/substrate-clarity-gate.sh +1110 -0
- package/dist/lite/.claude/hooks/temperance-gate.sh +147 -0
- package/dist/lite/.claude/hooks/testing-tier-enforce.sh +233 -0
- package/dist/lite/.claude/hooks/turn-prose-grade-measure.sh +219 -0
- package/dist/lite/.claude/hooks/turn-prose-kiss-check.sh +463 -0
- package/dist/lite/.claude/hooks/vocabulary-migration-check.sh +171 -0
- package/dist/lite/.claude/hooks/whereami-utc-gate.sh +142 -0
- package/dist/lite/CLAUDE.md +2 -2
- package/dist/lite/whereami.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# Bassclef Substrate Sync — fires on SessionStart.
|
|
4
|
+
# Operator-class hook: lives on bassclef-upstream operators' machines only.
|
|
5
|
+
# Adopter consumers receive presence/install/bassclef-sync.template.sh instead
|
|
6
|
+
# (sourced from canonical until the WU-0 absorb gap is closed).
|
|
7
|
+
# Ensures bassclef standards are available regardless of device.
|
|
8
|
+
#
|
|
9
|
+
# 1. Clones/pulls bassclef repo
|
|
10
|
+
# 2. Symlinks bassclef skills into project .claude/skills/ (auto-discovery)
|
|
11
|
+
# 3. Installs general hooks + agents to ~/.claude/
|
|
12
|
+
# 4. Writes ~/.claude/settings.json for user-level hook wiring (cloud)
|
|
13
|
+
# 5. Sets env var for rules/CLAUDE.md inheritance
|
|
14
|
+
|
|
15
|
+
# === Section 0a: SELF_MODE detection (#915) ===
|
|
16
|
+
# Sister to Cure 1 in #901 (commit 4b6c258d) which added SELF_MODE
|
|
17
|
+
# detection to the adopter template at presence/install/bassclef-sync.template.sh.
|
|
18
|
+
# This is the operator-class SessionStart hook — it needs the same guard.
|
|
19
|
+
#
|
|
20
|
+
# Detection: git remote origin URL matches sunj-labs/(bassclef|bassclef-upstream)(\.git)?$
|
|
21
|
+
# on both HTTPS and SSH URL shapes. When SELF_MODE=1, hook exits 0 early
|
|
22
|
+
# to prevent the sync flow from re-symlinking tracked hooks in the
|
|
23
|
+
# substrate source repo.
|
|
24
|
+
#
|
|
25
|
+
# Live proof of the class this cures: session 2026-07-26d WU-0 reverted 84
|
|
26
|
+
# T + 1 M hooks caused by this SessionStart re-symlink in bassclef-upstream.
|
|
27
|
+
#
|
|
28
|
+
# Env override SKIP_SELF_MODE_DETECTION=1 forces SELF_MODE=0 regardless
|
|
29
|
+
# of remote — for adopter forks of bassclef-upstream that legitimately
|
|
30
|
+
# want the sync flow.
|
|
31
|
+
#
|
|
32
|
+
# Function is defined ABOVE the main-body INPUT read so tests can source
|
|
33
|
+
# the hook with BASSCLEF_SYNC_TEST_MODE=1 and access the function without
|
|
34
|
+
# triggering the sync flow.
|
|
35
|
+
bassclef_detect_self_mode() {
|
|
36
|
+
local cwd="${1:-.}"
|
|
37
|
+
|
|
38
|
+
# Env override wins
|
|
39
|
+
if [ "${SKIP_SELF_MODE_DETECTION:-0}" = "1" ]; then
|
|
40
|
+
echo "0"
|
|
41
|
+
return 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# No git → not substrate source
|
|
45
|
+
command -v git >/dev/null 2>&1 || { echo "0"; return 0; }
|
|
46
|
+
|
|
47
|
+
# Get remote origin URL
|
|
48
|
+
local remote_url
|
|
49
|
+
remote_url=$(git -C "$cwd" config --get remote.origin.url 2>/dev/null)
|
|
50
|
+
if [ -z "$remote_url" ]; then
|
|
51
|
+
echo "0"
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Match sunj-labs/(bassclef|bassclef-upstream) with optional .git suffix
|
|
56
|
+
# HTTPS: https://github.com/sunj-labs/bassclef.git
|
|
57
|
+
# SSH: git@github.com:sunj-labs/bassclef-upstream.git
|
|
58
|
+
if echo "$remote_url" | grep -qE 'sunj-labs/(bassclef|bassclef-upstream)(\.git)?$'; then
|
|
59
|
+
echo "1"
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
echo "0"
|
|
64
|
+
return 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# === _sync_install_hooks (goal 14c Step 5) ===
|
|
68
|
+
# Install a hard-coded list of substrate hooks to the operator's
|
|
69
|
+
# ~/.claude/hooks/. For each name: if the source hook declares a
|
|
70
|
+
# # install-class: header, route via lib/hook-installer.sh's
|
|
71
|
+
# install_by_class (which honors operator|project|dual). If no header,
|
|
72
|
+
# fall back to legacy operator-scope copy per ADR-058 c2 grace window
|
|
73
|
+
# (through 2026-10-31).
|
|
74
|
+
#
|
|
75
|
+
# Args:
|
|
76
|
+
# $1 — BASSCLEF_DIR (source of hooks + lib)
|
|
77
|
+
# $2 — user-scope target dir (default $HOME/.claude/hooks)
|
|
78
|
+
#
|
|
79
|
+
# Discipline (per ADR-058 constraints 1-4):
|
|
80
|
+
# c1 — install_by_class wrapped in subshell; lib's strict mode
|
|
81
|
+
# stays contained; caller's strict-mode state preserved
|
|
82
|
+
# c2 — unheadered hooks install where they install today +
|
|
83
|
+
# one trace line `skipped-no-header <hook>` fires
|
|
84
|
+
# c3 — CLAUDE_PROJECT_DIR resolved (git rev-parse fallback) and
|
|
85
|
+
# exported before the loop so project/dual class routing works
|
|
86
|
+
# c4 — settings.json command strings unchanged; wiring self-heal
|
|
87
|
+
# continues at Section 4 (unrelated to this function)
|
|
88
|
+
_sync_install_hooks() {
|
|
89
|
+
local bassclef_dir="$1"
|
|
90
|
+
local user_target_dir="${2:-$HOME/.claude/hooks}"
|
|
91
|
+
|
|
92
|
+
local hook_names=(
|
|
93
|
+
bug-diagnosis.sh
|
|
94
|
+
tool-failure-diagnosis.sh
|
|
95
|
+
trace-helper.sh
|
|
96
|
+
deploy-guard.sh
|
|
97
|
+
memory-write-audit.sh
|
|
98
|
+
turn-prose-kiss-check.sh
|
|
99
|
+
session-end.sh
|
|
100
|
+
bet-doc-gate.sh
|
|
101
|
+
substrate-clarity-gate.sh
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
mkdir -p "$user_target_dir" 2>/dev/null || true
|
|
105
|
+
|
|
106
|
+
local installer_lib="$bassclef_dir/lib/hook-installer.sh"
|
|
107
|
+
local dispatch_available=0
|
|
108
|
+
[ -f "$installer_lib" ] && dispatch_available=1
|
|
109
|
+
|
|
110
|
+
# ADR-058 c3 — resolve CLAUDE_PROJECT_DIR before loop
|
|
111
|
+
if [ -z "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
112
|
+
if command -v git >/dev/null 2>&1; then
|
|
113
|
+
CLAUDE_PROJECT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
114
|
+
fi
|
|
115
|
+
if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
116
|
+
export CLAUDE_PROJECT_DIR
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
for hook_name in "${hook_names[@]}"; do
|
|
121
|
+
local hook_src="$bassclef_dir/.claude/hooks/$hook_name"
|
|
122
|
+
if [ ! -f "$hook_src" ]; then
|
|
123
|
+
continue
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
local has_header=0
|
|
127
|
+
if head -10 "$hook_src" 2>/dev/null | grep -qE '^# install-class:[[:space:]]*(operator|project|dual)[[:space:]]*$'; then
|
|
128
|
+
has_header=1
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
if [ "$has_header" = "1" ] && [ "$dispatch_available" = "1" ]; then
|
|
132
|
+
# ADR-058 c1 — subshell wraps install_by_class so lib strict
|
|
133
|
+
# mode never leaks into this script. On subshell failure, fall
|
|
134
|
+
# back to legacy operator-scope copy to preserve adopter
|
|
135
|
+
# contract per c4 (settings.json command strings byte-identical).
|
|
136
|
+
# shellcheck disable=SC1090 # dynamic source path
|
|
137
|
+
if ! ( source "$installer_lib" 2>/dev/null && install_by_class "$hook_src" ) >/dev/null 2>&1; then
|
|
138
|
+
cp "$hook_src" "$user_target_dir/" 2>/dev/null || true
|
|
139
|
+
chmod +x "$user_target_dir/$hook_name" 2>/dev/null || true
|
|
140
|
+
fi
|
|
141
|
+
else
|
|
142
|
+
# ADR-058 c2 fallback — grace window through 2026-10-31.
|
|
143
|
+
cp "$hook_src" "$user_target_dir/" 2>/dev/null || true
|
|
144
|
+
chmod +x "$user_target_dir/$hook_name" 2>/dev/null || true
|
|
145
|
+
if [ "$has_header" = "0" ] && command -v trace_log >/dev/null 2>&1; then
|
|
146
|
+
trace_log "ok" "sync-install-hooks-skipped-no-header" "$hook_name"
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
done
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# === Test-mode guard (#915) ===
|
|
153
|
+
# Tests source this hook to access helpers without triggering main sync.
|
|
154
|
+
# Early return under BASSCLEF_SYNC_TEST_MODE=1. Helpers defined ABOVE.
|
|
155
|
+
if [ "${BASSCLEF_SYNC_TEST_MODE:-0}" = "1" ]; then
|
|
156
|
+
return 0 2>/dev/null || exit 0
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# === Hook liveness heartbeat (WU-3 of bet 2026-07-31d; closes #1002) ===
|
|
160
|
+
# Silent-fail — a missing lib never crashes the hook. Discipline: ADR-048 +
|
|
161
|
+
# standards/hook-cadence.json. Session-start reflection module (WU-4.5) checks
|
|
162
|
+
# marker mtime against expected cadence.
|
|
163
|
+
{
|
|
164
|
+
_hb_sd="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
165
|
+
for _hb_c in "${_hb_sd}/../../lib/hook-heartbeat.sh" "${HOME:-/}/lib/hook-heartbeat.sh"; do
|
|
166
|
+
[ -f "$_hb_c" ] && source "$_hb_c" && heartbeat_mark "bassclef-sync" && break
|
|
167
|
+
done
|
|
168
|
+
unset _hb_sd _hb_c
|
|
169
|
+
} 2>/dev/null || true
|
|
170
|
+
|
|
171
|
+
INPUT=$(cat)
|
|
172
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // "."')
|
|
173
|
+
cd "$CWD"
|
|
174
|
+
|
|
175
|
+
# === Section 0a fires: exit early if this repo is substrate source ===
|
|
176
|
+
SELF_MODE=$(bassclef_detect_self_mode "$CWD")
|
|
177
|
+
if [ "$SELF_MODE" = "1" ]; then
|
|
178
|
+
echo "SELF_MODE detected — skipping sync in substrate source repo." >&2
|
|
179
|
+
echo " (Override with SKIP_SELF_MODE_DETECTION=1 for adopter forks.)" >&2
|
|
180
|
+
exit 0
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# Bassclef substrate location — peer clone at ../bassclef (post-rename).
|
|
184
|
+
# Pre-rename ../canonical fallback removed 2026-06-24 per bassclef-upstream#317
|
|
185
|
+
# Phase 0: zero adopters carry the canonical/ path; operator's local
|
|
186
|
+
# ~/src/sunj-labs/canonical was removed before this cleanup.
|
|
187
|
+
BASSCLEF_DIR="$CWD/../bassclef"
|
|
188
|
+
BASSCLEF_REPO="https://github.com/sunj-labs/bassclef.git"
|
|
189
|
+
SYNC_STATUS="up-to-date"
|
|
190
|
+
SYNC_NEW=""
|
|
191
|
+
|
|
192
|
+
# === 1. Clone or pull bassclef ===
|
|
193
|
+
if [ ! -d "$BASSCLEF_DIR/.claude" ]; then
|
|
194
|
+
if git clone "$BASSCLEF_REPO" "$BASSCLEF_DIR"; then
|
|
195
|
+
SYNC_STATUS="freshly cloned"
|
|
196
|
+
else
|
|
197
|
+
echo "### Bassclef sync — full-tier not synced"
|
|
198
|
+
echo ""
|
|
199
|
+
echo "Optional: set up GitHub auth to sync the full bassclef substrate."
|
|
200
|
+
echo "Lite tier works without it — your session is fine."
|
|
201
|
+
echo "Skills / rules / agents beyond lite become available once sync succeeds."
|
|
202
|
+
echo ""
|
|
203
|
+
exit 0
|
|
204
|
+
fi
|
|
205
|
+
else
|
|
206
|
+
BEFORE=$(git -C "$BASSCLEF_DIR" rev-parse HEAD 2>/dev/null)
|
|
207
|
+
git -C "$BASSCLEF_DIR" pull --quiet 2>/dev/null || true
|
|
208
|
+
AFTER=$(git -C "$BASSCLEF_DIR" rev-parse HEAD 2>/dev/null)
|
|
209
|
+
|
|
210
|
+
if [ "$BEFORE" != "$AFTER" ]; then
|
|
211
|
+
SYNC_STATUS="updated"
|
|
212
|
+
SYNC_NEW=$(git -C "$BASSCLEF_DIR" log --oneline "$BEFORE".."$AFTER" 2>/dev/null)
|
|
213
|
+
fi
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
# === 2. Symlink bassclef skills into project for auto-discovery ===
|
|
217
|
+
mkdir -p "$CWD/.claude/skills" 2>/dev/null || true
|
|
218
|
+
for SKILL_DIR in "$BASSCLEF_DIR/.claude/skills/"*/; do
|
|
219
|
+
SKILL_NAME=$(basename "$SKILL_DIR")
|
|
220
|
+
TARGET="$CWD/.claude/skills/$SKILL_NAME"
|
|
221
|
+
# Only symlink if not already an app-specific skill (real directory, not symlink)
|
|
222
|
+
if [ -L "$TARGET" ]; then
|
|
223
|
+
# Existing symlink — update it
|
|
224
|
+
rm "$TARGET"
|
|
225
|
+
ln -s "$SKILL_DIR" "$TARGET"
|
|
226
|
+
elif [ ! -d "$TARGET" ]; then
|
|
227
|
+
# Doesn't exist — create symlink
|
|
228
|
+
ln -s "$SKILL_DIR" "$TARGET"
|
|
229
|
+
fi
|
|
230
|
+
# If it's a real directory (app-specific), leave it alone
|
|
231
|
+
done
|
|
232
|
+
|
|
233
|
+
# === 2b. Park orphan ~/.claude/skills + agents + hooks symlinks (widened per ADR-033 + bassclef#278) ===
|
|
234
|
+
# Uses the shared orphan-parking helper. Closes the skill / agent / hook
|
|
235
|
+
# duplication-in-available-skills bug. The helper parks user-level
|
|
236
|
+
# symlinks AND cp'd files whose basename appears in project-level —
|
|
237
|
+
# preserves real directories untouched + EXCLUDES hooks wired in
|
|
238
|
+
# settings.json at $HOME paths (per #294); backs up moved entries to
|
|
239
|
+
# dated dirs that survive at least 30 days per Hyrum + Linus discipline.
|
|
240
|
+
PARKING_HELPER="$BASSCLEF_DIR/scripts/bassclef-orphan-parking.sh"
|
|
241
|
+
USER_SETTINGS="$HOME/.claude/settings.json"
|
|
242
|
+
ORPHAN_COUNT=0
|
|
243
|
+
if [ -f "$PARKING_HELPER" ]; then
|
|
244
|
+
for surface in skills agents hooks; do
|
|
245
|
+
USER_SURFACE="$HOME/.claude/$surface"
|
|
246
|
+
PROJECT_SURFACE="$CWD/.claude/$surface"
|
|
247
|
+
if [ -d "$USER_SURFACE" ] && [ -d "$PROJECT_SURFACE" ]; then
|
|
248
|
+
mkdir -p "$HOME/.claude" 2>/dev/null
|
|
249
|
+
# Pass user settings.json as 5th arg so wired hooks are excluded
|
|
250
|
+
# from parking. Per #294 — broke the Stop hook on this machine when
|
|
251
|
+
# parking removed session-end.sh that settings.json wires.
|
|
252
|
+
PARKED_LINE=$(bash "$PARKING_HELPER" "$USER_SURFACE" "$PROJECT_SURFACE" "$HOME/.claude" "$surface" "$USER_SETTINGS" 2>/dev/null || echo "")
|
|
253
|
+
if [ -n "$PARKED_LINE" ]; then
|
|
254
|
+
# Extract the count from "parked N <surface> orphans → <dir>"
|
|
255
|
+
COUNT_N=$(echo "$PARKED_LINE" | grep -oE 'parked [0-9]+' | grep -oE '[0-9]+' | head -1)
|
|
256
|
+
[ -n "$COUNT_N" ] && ORPHAN_COUNT=$((ORPHAN_COUNT + COUNT_N))
|
|
257
|
+
fi
|
|
258
|
+
fi
|
|
259
|
+
done
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
# === 2c. Adopter self-heal — auto-migrate stale canonical references ===
|
|
263
|
+
# Per ADR-031 we-dont-break-adopters + Nygard's Steady State pattern.
|
|
264
|
+
# When this hook runs in an adopter repo (CWD is NOT bassclef-upstream and
|
|
265
|
+
# does NOT have architecture/dual-repo-flow.md), invoke the migration script
|
|
266
|
+
# to repair any stale ../canonical/ references in symlinks + settings.json
|
|
267
|
+
# + dispatcher + statusline. Idempotent — no-op on already-migrated repos.
|
|
268
|
+
#
|
|
269
|
+
# Banner surfaces per-surface info (not just count) so adopters who
|
|
270
|
+
# customized any of the migrated surfaces see exactly what changed.
|
|
271
|
+
# Closes Yellow 2 of the 2026-06-22 release-readiness review.
|
|
272
|
+
MIGRATE_SCRIPT="$BASSCLEF_DIR/scripts/migrate-adopter-references.sh"
|
|
273
|
+
SUMMARY_HELPER="$BASSCLEF_DIR/scripts/bassclef-migrate-summary.sh"
|
|
274
|
+
if [ -f "$MIGRATE_SCRIPT" ] && [ ! -f "$CWD/architecture/dual-repo-flow.md" ]; then
|
|
275
|
+
MIGRATE_OUT=$(bash "$MIGRATE_SCRIPT" --apply "$CWD" 2>&1)
|
|
276
|
+
MIGRATE_CHANGES=$(echo "$MIGRATE_OUT" | awk -F: '/^changes:/ {print $2}' | tr -d ' ')
|
|
277
|
+
if [ -n "$MIGRATE_CHANGES" ] && [ "$MIGRATE_CHANGES" -gt 0 ]; then
|
|
278
|
+
PER_SURFACE=""
|
|
279
|
+
# Check -f (file exists), not -x (executable). We invoke via `bash $HELPER`
|
|
280
|
+
# so the executable bit is irrelevant; the bit may not survive tarball
|
|
281
|
+
# fetches across adopter clones (per reviewer note on PR #291).
|
|
282
|
+
if [ -f "$SUMMARY_HELPER" ]; then
|
|
283
|
+
PER_SURFACE=$(echo "$MIGRATE_OUT" | bash "$SUMMARY_HELPER" 2>/dev/null)
|
|
284
|
+
fi
|
|
285
|
+
SYNC_NEW="${SYNC_NEW}
|
|
286
|
+
Adopter self-heal: $MIGRATE_CHANGES surface(s) migrated. Old setup keeps working; new setup matches current bassclef substrate. Per ADR-031."
|
|
287
|
+
if [ -n "$PER_SURFACE" ]; then
|
|
288
|
+
SYNC_NEW="${SYNC_NEW}
|
|
289
|
+
${PER_SURFACE}"
|
|
290
|
+
fi
|
|
291
|
+
fi
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
# === 3. Install general hooks to ~/.claude/ ===
|
|
295
|
+
# Delegates to _sync_install_hooks (goal 14c Step 5 per ADR-058).
|
|
296
|
+
# The hard-coded 9-name list that lived here through v0.41.1 moved
|
|
297
|
+
# into the function above. Each hook now routes by its own
|
|
298
|
+
# # install-class: header when declared; unheadered hooks fall back
|
|
299
|
+
# to legacy operator-scope copy per ADR-058 c2 grace window
|
|
300
|
+
# (through 2026-10-31). Section 4 self-heal continues to preserve
|
|
301
|
+
# adopter contract per c4 (settings.json command strings unchanged).
|
|
302
|
+
_sync_install_hooks "$BASSCLEF_DIR" "$HOME/.claude/hooks"
|
|
303
|
+
|
|
304
|
+
# === Install agents to ~/.claude/ ===
|
|
305
|
+
mkdir -p ~/.claude/agents 2>/dev/null || true
|
|
306
|
+
cp "$BASSCLEF_DIR/.claude/agents/"*.md ~/.claude/agents/ 2>/dev/null || true
|
|
307
|
+
|
|
308
|
+
# === 4. Versioned self-heal of ~/.claude/settings.json ===
|
|
309
|
+
# WU-3 of bet 2026-06-24a-settings-hardening (#313). Replaces the
|
|
310
|
+
# brittle multi-grep + destructive heredoc rewrite with:
|
|
311
|
+
# 1. Version-field comparison (read_settings_version).
|
|
312
|
+
# 2. Additive merge that preserves operator's top-level keys (theme,
|
|
313
|
+
# statusLine, permissions, alwaysThinkingEnabled, etc.) and
|
|
314
|
+
# operator-only event types — bassclef wins per-event for events
|
|
315
|
+
# it ships.
|
|
316
|
+
# 3. Hoare postcondition: after self-heal, drift detector MUST
|
|
317
|
+
# return clean (compare_wirings .ok == true). Else exit 1 with
|
|
318
|
+
# structured stderr — failure compounds across every adopter,
|
|
319
|
+
# so failing loud beats failing silent.
|
|
320
|
+
# Closes the bug class diagnosed 2026-06-24 (operator's settings.json
|
|
321
|
+
# never picked up substrate's new hook wirings).
|
|
322
|
+
WIRINGS_LIB="$BASSCLEF_DIR/.claude/hooks/_lib/wirings.sh"
|
|
323
|
+
PROJECT_SETTINGS="$BASSCLEF_DIR/.claude/settings.json"
|
|
324
|
+
USER_SETTINGS="$HOME/.claude/settings.json"
|
|
325
|
+
|
|
326
|
+
sync_self_heal() {
|
|
327
|
+
if [ ! -f "$WIRINGS_LIB" ] || [ ! -f "$PROJECT_SETTINGS" ]; then
|
|
328
|
+
# Substrate not fully synced yet OR project settings.json not
|
|
329
|
+
# checked out on this branch. Fall back silently — drift detector
|
|
330
|
+
# at next session-start will surface the gap.
|
|
331
|
+
return 0
|
|
332
|
+
fi
|
|
333
|
+
# shellcheck source=/dev/null
|
|
334
|
+
source "$WIRINGS_LIB"
|
|
335
|
+
|
|
336
|
+
local required_version user_version
|
|
337
|
+
required_version=$(read_settings_version "$PROJECT_SETTINGS")
|
|
338
|
+
user_version=$(read_settings_version "$USER_SETTINGS")
|
|
339
|
+
|
|
340
|
+
if [ ! -f "$USER_SETTINGS" ] || [ "$user_version" -lt "$required_version" ]; then
|
|
341
|
+
additive_merge_settings "$USER_SETTINGS" "$PROJECT_SETTINGS"
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
# Hoare postcondition: required ⊆ loaded after self-heal.
|
|
345
|
+
local required loaded diff ok missing
|
|
346
|
+
required=$(read_required_wirings "$PROJECT_SETTINGS")
|
|
347
|
+
loaded=$(read_loaded_wirings)
|
|
348
|
+
diff=$(compare_wirings "$required" "$loaded")
|
|
349
|
+
ok=$(echo "$diff" | jq -r '.ok')
|
|
350
|
+
if [ "$ok" != "true" ]; then
|
|
351
|
+
missing=$(echo "$diff" | jq -c '.missing')
|
|
352
|
+
echo "🛑 bassclef-sync postcondition FAILED — required wirings missing after self-heal:" >&2
|
|
353
|
+
echo "$missing" | jq -r '.[] | " - " + .event + " → " + .command' >&2
|
|
354
|
+
echo "" >&2
|
|
355
|
+
echo "Resolve: edit ~/.claude/settings.json to include the missing wirings, then re-run bassclef-sync." >&2
|
|
356
|
+
if command -v trace_log >/dev/null 2>&1; then
|
|
357
|
+
trace_log "fail" "bassclef-sync-postcondition" "$missing"
|
|
358
|
+
fi
|
|
359
|
+
return 1
|
|
360
|
+
fi
|
|
361
|
+
if command -v trace_log >/dev/null 2>&1; then
|
|
362
|
+
trace_log "ok" "bassclef-sync-postcondition" "version=$required_version"
|
|
363
|
+
fi
|
|
364
|
+
return 0
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
sync_self_heal || exit 1
|
|
368
|
+
|
|
369
|
+
# === 4b. PreToolUse template embedding — operator settings.json self-heal ===
|
|
370
|
+
# Per bassclef-upstream#336 — ensure operator's ~/.claude/settings.json wires
|
|
371
|
+
# the bassclef-managed PreToolUse hooks (substrate-clarity-gate + bet-doc-gate).
|
|
372
|
+
# Mirrors §3c-hooks in presence/install/bassclef-sync.template.sh (consumer-side)
|
|
373
|
+
# but targets ~/.claude/settings.json with $HOME-prefixed commands.
|
|
374
|
+
#
|
|
375
|
+
# Why this lives here even though §4 sync_self_heal runs additive-merge:
|
|
376
|
+
# wirings.sh merges bassclef's project settings.json into operator's user
|
|
377
|
+
# settings.json — but during a sync where wirings.sh itself is being upgraded,
|
|
378
|
+
# OR when the operator's settings.json predates the wirings library entirely,
|
|
379
|
+
# the merge may not fire. This §4b is the literal-text fallback: greppable
|
|
380
|
+
# guards re-emit the PreToolUse template when entries are missing, regardless
|
|
381
|
+
# of wirings.sh state. Bootstrap-pair discipline (per
|
|
382
|
+
# .claude/rules/bootstrap-pair-discipline.md) — wiring ships in the same place
|
|
383
|
+
# as the hook reference.
|
|
384
|
+
#
|
|
385
|
+
# Template (operator settings.json fragment, "PreToolUse": event):
|
|
386
|
+
# {
|
|
387
|
+
# "hooks": {
|
|
388
|
+
# "PreToolUse": [
|
|
389
|
+
# {
|
|
390
|
+
# "matcher": "Edit|Write|MultiEdit",
|
|
391
|
+
# "hooks": [
|
|
392
|
+
# {"type": "command", "command": "$HOME/.claude/hooks/substrate-clarity-gate.sh"},
|
|
393
|
+
# {"type": "command", "command": "$HOME/.claude/hooks/bet-doc-gate.sh"}
|
|
394
|
+
# ]
|
|
395
|
+
# }
|
|
396
|
+
# ]
|
|
397
|
+
# }
|
|
398
|
+
# }
|
|
399
|
+
#
|
|
400
|
+
# Idempotent via jq additive merge — re-runs detect existing entries and skip.
|
|
401
|
+
# Skip paths:
|
|
402
|
+
# - SKIP_PRETOOLUSE_EMBED=1 — operator override (logged)
|
|
403
|
+
# - jq missing — graceful skip with stderr notice
|
|
404
|
+
# - ~/.claude/settings.json missing — skip (sync_self_heal handles creation)
|
|
405
|
+
if [ "${SKIP_PRETOOLUSE_EMBED:-0}" != "1" ] && \
|
|
406
|
+
command -v jq >/dev/null 2>&1 && \
|
|
407
|
+
[ -f "$USER_SETTINGS" ]; then
|
|
408
|
+
|
|
409
|
+
# Guard re-emit when bet-doc-gate missing (existing-adopter self-heal)
|
|
410
|
+
if ! grep -q 'bet-doc-gate' ~/.claude/settings.json 2>/dev/null; then
|
|
411
|
+
BET_DOC_GATE_MISSING=1
|
|
412
|
+
else
|
|
413
|
+
BET_DOC_GATE_MISSING=0
|
|
414
|
+
fi
|
|
415
|
+
# Guard re-emit when substrate-clarity-gate missing
|
|
416
|
+
if ! grep -q 'substrate-clarity-gate' ~/.claude/settings.json 2>/dev/null; then
|
|
417
|
+
SUBSTRATE_CLARITY_GATE_MISSING=1
|
|
418
|
+
else
|
|
419
|
+
SUBSTRATE_CLARITY_GATE_MISSING=0
|
|
420
|
+
fi
|
|
421
|
+
|
|
422
|
+
if [ "$BET_DOC_GATE_MISSING" = "1" ] || [ "$SUBSTRATE_CLARITY_GATE_MISSING" = "1" ]; then
|
|
423
|
+
# Operator-class PreToolUse template — note the $HOME prefix (left literal
|
|
424
|
+
# in JSON; Claude Code expands at hook-fire time per presence/install
|
|
425
|
+
# consumer-side §4 line 820 precedent).
|
|
426
|
+
OPERATOR_PRETOOLUSE_JSON='[
|
|
427
|
+
{"matcher": "Edit|Write|MultiEdit", "command": "$HOME/.claude/hooks/substrate-clarity-gate.sh"},
|
|
428
|
+
{"matcher": "Edit|Write|MultiEdit", "command": "$HOME/.claude/hooks/bet-doc-gate.sh"}
|
|
429
|
+
]'
|
|
430
|
+
|
|
431
|
+
# Additive jq merge — mirrors §3c-hooks logic but writes to USER_SETTINGS:
|
|
432
|
+
# 1. Ensure .hooks.PreToolUse array exists
|
|
433
|
+
# 2. For each bassclef hook:
|
|
434
|
+
# - If matcher exists, ensure command is in the matcher's hooks array
|
|
435
|
+
# - If matcher missing, create new matcher entry with the command
|
|
436
|
+
# Operator-managed entries (other matchers, other commands) stay untouched.
|
|
437
|
+
NEW_OPERATOR_SETTINGS=$(jq --argjson bassclef "$OPERATOR_PRETOOLUSE_JSON" '
|
|
438
|
+
.hooks //= {} |
|
|
439
|
+
.hooks.PreToolUse //= [] |
|
|
440
|
+
reduce $bassclef[] as $hook (
|
|
441
|
+
.;
|
|
442
|
+
if (.hooks.PreToolUse | map(.matcher) | index($hook.matcher)) != null then
|
|
443
|
+
.hooks.PreToolUse |= map(
|
|
444
|
+
if .matcher == $hook.matcher then
|
|
445
|
+
if (.hooks | map(.command) | index($hook.command)) == null then
|
|
446
|
+
.hooks += [{"type": "command", "command": $hook.command}]
|
|
447
|
+
else . end
|
|
448
|
+
else . end
|
|
449
|
+
)
|
|
450
|
+
else
|
|
451
|
+
.hooks.PreToolUse += [{
|
|
452
|
+
"matcher": $hook.matcher,
|
|
453
|
+
"hooks": [{"type": "command", "command": $hook.command}]
|
|
454
|
+
}]
|
|
455
|
+
end
|
|
456
|
+
)
|
|
457
|
+
' "$USER_SETTINGS" 2>/dev/null)
|
|
458
|
+
|
|
459
|
+
if [ -n "$NEW_OPERATOR_SETTINGS" ]; then
|
|
460
|
+
echo "$NEW_OPERATOR_SETTINGS" > "$USER_SETTINGS.tmp" && \
|
|
461
|
+
mv "$USER_SETTINGS.tmp" "$USER_SETTINGS"
|
|
462
|
+
if command -v trace_log >/dev/null 2>&1; then
|
|
463
|
+
trace_log "ok" "bassclef-sync-pretooluse-embed" \
|
|
464
|
+
"added_bet_doc=$BET_DOC_GATE_MISSING added_substrate_clarity=$SUBSTRATE_CLARITY_GATE_MISSING"
|
|
465
|
+
fi
|
|
466
|
+
fi
|
|
467
|
+
fi
|
|
468
|
+
fi
|
|
469
|
+
|
|
470
|
+
# === 4c. Stop template embedding — operator settings.json self-heal ===
|
|
471
|
+
# Same pattern as §4b but for the "Stop": event — wires
|
|
472
|
+
# $HOME/.claude/hooks/turn-prose-kiss-check.sh so the per-turn /kiss scan
|
|
473
|
+
# fires at session-stop boundaries. Per bassclef-upstream#336 sister gap
|
|
474
|
+
# (Stop template embedding missing alongside PreToolUse).
|
|
475
|
+
#
|
|
476
|
+
# Template (operator settings.json fragment, "Stop": event):
|
|
477
|
+
# {
|
|
478
|
+
# "hooks": {
|
|
479
|
+
# "Stop": [
|
|
480
|
+
# {
|
|
481
|
+
# "matcher": "",
|
|
482
|
+
# "hooks": [
|
|
483
|
+
# {"type": "command", "command": "$HOME/.claude/hooks/turn-prose-kiss-check.sh", "timeout": 30}
|
|
484
|
+
# ]
|
|
485
|
+
# }
|
|
486
|
+
# ]
|
|
487
|
+
# }
|
|
488
|
+
# }
|
|
489
|
+
#
|
|
490
|
+
# Timeout 30s: turn-prose-kiss-check scans the most-recent assistant message
|
|
491
|
+
# against the BLOCK wordlist; finishes well under 30s on any realistic
|
|
492
|
+
# transcript. ≥5s minimum per session-artifacts rule (Stop hooks need
|
|
493
|
+
# adequate timeout or they get killed mid-write).
|
|
494
|
+
if [ "${SKIP_STOP_EMBED:-0}" != "1" ] && \
|
|
495
|
+
command -v jq >/dev/null 2>&1 && \
|
|
496
|
+
[ -f "$USER_SETTINGS" ]; then
|
|
497
|
+
|
|
498
|
+
# Guard re-emit when turn-prose entry missing
|
|
499
|
+
if ! grep -q 'turn-prose' ~/.claude/settings.json 2>/dev/null; then
|
|
500
|
+
TURN_PROSE_MISSING=1
|
|
501
|
+
else
|
|
502
|
+
TURN_PROSE_MISSING=0
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
if [ "$TURN_PROSE_MISSING" = "1" ]; then
|
|
506
|
+
# Stop-event entry — empty matcher (Stop fires once per session-end;
|
|
507
|
+
# no per-tool matcher applies). Command uses $HOME-prefixed path
|
|
508
|
+
# (Claude Code expands at hook-fire time). Timeout 30 per above.
|
|
509
|
+
OPERATOR_STOP_JSON='[
|
|
510
|
+
{"matcher": "", "command": "$HOME/.claude/hooks/turn-prose-kiss-check.sh", "timeout": 30}
|
|
511
|
+
]'
|
|
512
|
+
|
|
513
|
+
NEW_STOP_SETTINGS=$(jq --argjson bassclef "$OPERATOR_STOP_JSON" '
|
|
514
|
+
.hooks //= {} |
|
|
515
|
+
.hooks.Stop //= [] |
|
|
516
|
+
reduce $bassclef[] as $hook (
|
|
517
|
+
.;
|
|
518
|
+
if (.hooks.Stop | map(.matcher) | index($hook.matcher)) != null then
|
|
519
|
+
.hooks.Stop |= map(
|
|
520
|
+
if .matcher == $hook.matcher then
|
|
521
|
+
if (.hooks | map(.command) | index($hook.command)) == null then
|
|
522
|
+
.hooks += [{"type": "command", "command": $hook.command, "timeout": $hook.timeout}]
|
|
523
|
+
else . end
|
|
524
|
+
else . end
|
|
525
|
+
)
|
|
526
|
+
else
|
|
527
|
+
.hooks.Stop += [{
|
|
528
|
+
"matcher": $hook.matcher,
|
|
529
|
+
"hooks": [{"type": "command", "command": $hook.command, "timeout": $hook.timeout}]
|
|
530
|
+
}]
|
|
531
|
+
end
|
|
532
|
+
)
|
|
533
|
+
' "$USER_SETTINGS" 2>/dev/null)
|
|
534
|
+
|
|
535
|
+
if [ -n "$NEW_STOP_SETTINGS" ]; then
|
|
536
|
+
echo "$NEW_STOP_SETTINGS" > "$USER_SETTINGS.tmp" && \
|
|
537
|
+
mv "$USER_SETTINGS.tmp" "$USER_SETTINGS"
|
|
538
|
+
if command -v trace_log >/dev/null 2>&1; then
|
|
539
|
+
trace_log "ok" "bassclef-sync-stop-embed" \
|
|
540
|
+
"added_turn_prose=$TURN_PROSE_MISSING"
|
|
541
|
+
fi
|
|
542
|
+
fi
|
|
543
|
+
fi
|
|
544
|
+
fi
|
|
545
|
+
|
|
546
|
+
# === 5. Set env var for rules/CLAUDE.md inheritance ===
|
|
547
|
+
if [ -n "$CLAUDE_ENV_FILE" ]; then
|
|
548
|
+
echo "CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1" >> "$CLAUDE_ENV_FILE"
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
# === Summary ===
|
|
552
|
+
SKILL_COUNT=$(ls "$BASSCLEF_DIR/.claude/skills/" 2>/dev/null | wc -l | tr -d ' ')
|
|
553
|
+
RULE_COUNT=$(ls "$BASSCLEF_DIR/.claude/rules/" 2>/dev/null | wc -l | tr -d ' ')
|
|
554
|
+
AGENT_COUNT=$(ls ~/.claude/agents/ 2>/dev/null | wc -l | tr -d ' ')
|
|
555
|
+
HOOK_COUNT=$(ls ~/.claude/hooks/*.sh 2>/dev/null | wc -l | tr -d ' ')
|
|
556
|
+
|
|
557
|
+
echo ""
|
|
558
|
+
echo "### BASSCLEF SUBSTRATE — SYNCED"
|
|
559
|
+
echo ""
|
|
560
|
+
echo "Status: $SYNC_STATUS"
|
|
561
|
+
echo "Skills: ${SKILL_COUNT} (symlinked to .claude/skills/ for auto-discovery)"
|
|
562
|
+
echo "Rules: ${RULE_COUNT} (loaded via additionalDirectories + env var)"
|
|
563
|
+
echo "Agents: ${AGENT_COUNT} (installed to ~/.claude/agents/)"
|
|
564
|
+
echo "Hooks: ${HOOK_COUNT} user-level (bug-diagnosis, tool-failure, trace-helper)"
|
|
565
|
+
echo ""
|
|
566
|
+
|
|
567
|
+
if [ -n "$SYNC_NEW" ]; then
|
|
568
|
+
echo "### NEW SINCE LAST SYNC — REVIEW BEFORE PROCEEDING"
|
|
569
|
+
echo ""
|
|
570
|
+
echo "$SYNC_NEW"
|
|
571
|
+
echo ""
|
|
572
|
+
echo "→ New bassclef standards detected. Ask the user to confirm"
|
|
573
|
+
echo " before proceeding. These may change how this session operates."
|
|
574
|
+
echo ""
|
|
575
|
+
fi
|
|
576
|
+
|
|
577
|
+
if [ "$ORPHAN_COUNT" -gt 0 ]; then
|
|
578
|
+
echo "### USER-LEVEL SUBSTRATE CLEANUP (per ADR-033 + bassclef#278)"
|
|
579
|
+
echo ""
|
|
580
|
+
echo "Parked $ORPHAN_COUNT user-level entries (skills + agents + hooks; symlinks AND cp'd files)"
|
|
581
|
+
echo "whose basenames duplicated project-level entries. Backup dirs:"
|
|
582
|
+
echo " ~/.claude/skills.orphans.$(date +%Y%m%d)/"
|
|
583
|
+
echo " ~/.claude/agents.orphans.$(date +%Y%m%d)/"
|
|
584
|
+
echo " ~/.claude/hooks.orphans.$(date +%Y%m%d)/"
|
|
585
|
+
echo ""
|
|
586
|
+
echo "Project-level entries now load without dedupe conflict."
|
|
587
|
+
echo "Backups survive 30 days minimum; restore if a script you wrote"
|
|
588
|
+
echo "depends on the user-level location."
|
|
589
|
+
echo ""
|
|
590
|
+
fi
|
|
591
|
+
|
|
592
|
+
echo "YOU MUST confirm to the user:"
|
|
593
|
+
echo " 'Bassclef substrate synced: ${SKILL_COUNT} skills, ${RULE_COUNT} rules,"
|
|
594
|
+
echo " ${AGENT_COUNT} agents, ${HOOK_COUNT} hooks. Status: ${SYNC_STATUS}.'"
|
|
595
|
+
echo ""
|
|
596
|
+
|
|
597
|
+
# === 6. Worktree cleanup — prune stale worktrees from crashed parallel sessions ===
|
|
598
|
+
STALE_WORKTREES=$(git -C "$CWD" worktree list --porcelain 2>/dev/null | grep "^worktree " | grep -v "$CWD$" | wc -l | tr -d ' ')
|
|
599
|
+
if [ "$STALE_WORKTREES" -gt 0 ]; then
|
|
600
|
+
# Prune worktrees whose directories no longer exist
|
|
601
|
+
git -C "$CWD" worktree prune 2>/dev/null || true
|
|
602
|
+
|
|
603
|
+
# Clean up .claude/worktrees/ directory if it exists
|
|
604
|
+
if [ -d "$CWD/.claude/worktrees" ]; then
|
|
605
|
+
rm -rf "$CWD/.claude/worktrees" 2>/dev/null || true
|
|
606
|
+
fi
|
|
607
|
+
|
|
608
|
+
REMAINING=$(git -C "$CWD" worktree list --porcelain 2>/dev/null | grep "^worktree " | grep -v "$CWD$" | wc -l | tr -d ' ')
|
|
609
|
+
if [ "$REMAINING" -gt 0 ]; then
|
|
610
|
+
echo "### STALE WORKTREES DETECTED"
|
|
611
|
+
echo ""
|
|
612
|
+
echo "$REMAINING worktree(s) from a previous session still exist."
|
|
613
|
+
echo "These may hold branch locks. Review and remove:"
|
|
614
|
+
echo ""
|
|
615
|
+
git -C "$CWD" worktree list 2>/dev/null | grep -v "$CWD "
|
|
616
|
+
echo ""
|
|
617
|
+
echo "To remove: git worktree remove <path>"
|
|
618
|
+
echo ""
|
|
619
|
+
fi
|
|
620
|
+
fi
|
|
621
|
+
|
|
622
|
+
# === 7. Boot check — only report what's missing ===
|
|
623
|
+
MISSING=""
|
|
624
|
+
|
|
625
|
+
# Google service account (needed for journal → Google Docs)
|
|
626
|
+
SA_KEY="$HOME/.config/gcloud/sunjay-google-ops.json"
|
|
627
|
+
if [ ! -f "$SA_KEY" ]; then
|
|
628
|
+
MISSING="${MISSING}\n- Google Docs service account key missing at $SA_KEY (needed for /journal → Google Doc push)"
|
|
629
|
+
fi
|
|
630
|
+
|
|
631
|
+
# gh CLI auth (needed for /promote, issue creation)
|
|
632
|
+
if ! command -v gh &>/dev/null; then
|
|
633
|
+
MISSING="${MISSING}\n- gh CLI not installed (needed for /promote, issue creation)"
|
|
634
|
+
elif ! gh auth status &>/dev/null 2>&1; then
|
|
635
|
+
MISSING="${MISSING}\n- gh CLI not authenticated (run: gh auth login)"
|
|
636
|
+
fi
|
|
637
|
+
|
|
638
|
+
if [ -n "$MISSING" ]; then
|
|
639
|
+
echo "### SUBSTRATE DEPENDENCIES — OPTIONAL SETUP NEEDED"
|
|
640
|
+
echo ""
|
|
641
|
+
echo "The following are not blocking but limit some skills:"
|
|
642
|
+
echo -e "$MISSING"
|
|
643
|
+
echo ""
|
|
644
|
+
echo "Run: bassclef/scripts/boot-bassclef.sh for full setup guide."
|
|
645
|
+
echo "These are informational — proceed with your work."
|
|
646
|
+
echo ""
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
# === 8. journal draft staleness check ===
|
|
650
|
+
# Check both bassclef and the current repo for stale drafts.
|
|
651
|
+
#
|
|
652
|
+
# Fetch origin first (per bassclef-upstream#403): the staleness check
|
|
653
|
+
# below reads local filesystem state + local git log, which diverges
|
|
654
|
+
# from origin when another session has pushed since the last pull
|
|
655
|
+
# (e.g., a prior session-end's commit). Without the fetch, the banner
|
|
656
|
+
# shows wrong-now data (observed 2026-06-26c — local saw 2026-06-19
|
|
657
|
+
# while origin had 2026-06-26.md from #401).
|
|
658
|
+
#
|
|
659
|
+
# Best-effort: || true keeps the check working when offline / no remote.
|
|
660
|
+
CURRENT_BRANCH_FOR_FETCH=$(git -C "$CWD" symbolic-ref --short HEAD 2>/dev/null)
|
|
661
|
+
if [ -n "$CURRENT_BRANCH_FOR_FETCH" ]; then
|
|
662
|
+
git -C "$CWD" fetch origin "$CURRENT_BRANCH_FOR_FETCH" --quiet 2>/dev/null || true
|
|
663
|
+
fi
|
|
664
|
+
|
|
665
|
+
DRAFT_DIRS=""
|
|
666
|
+
if [ -d "$CWD/docs/operator-private/journals" ]; then
|
|
667
|
+
DRAFT_DIRS="$CWD/docs/operator-private/journals"
|
|
668
|
+
elif [ -d "$CWD/docs/journals" ]; then
|
|
669
|
+
# Pre-2026-06-27 path — migrated to operator-private per bassclef-upstream#405
|
|
670
|
+
DRAFT_DIRS="$CWD/docs/journals"
|
|
671
|
+
elif [ -d "$CWD/docs/linkedin-drafts" ]; then
|
|
672
|
+
# Legacy path — pre-/linkedin-→-/journal rename
|
|
673
|
+
DRAFT_DIRS="$CWD/docs/linkedin-drafts"
|
|
674
|
+
fi
|
|
675
|
+
if [ -d "$BASSCLEF_DIR/docs/operator-private/journals" ] && [ "$BASSCLEF_DIR" != "$CWD" ]; then
|
|
676
|
+
DRAFT_DIRS="$DRAFT_DIRS $BASSCLEF_DIR/docs/operator-private/journals"
|
|
677
|
+
elif [ -d "$BASSCLEF_DIR/docs/journals" ] && [ "$BASSCLEF_DIR" != "$CWD" ]; then
|
|
678
|
+
DRAFT_DIRS="$DRAFT_DIRS $BASSCLEF_DIR/docs/journals"
|
|
679
|
+
fi
|
|
680
|
+
|
|
681
|
+
if [ -n "$DRAFT_DIRS" ]; then
|
|
682
|
+
LATEST_DRAFT=""
|
|
683
|
+
for DIR in $DRAFT_DIRS; do
|
|
684
|
+
LAST=$(ls -1 "$DIR"/*.md 2>/dev/null | sort | tail -1)
|
|
685
|
+
if [ -n "$LAST" ]; then
|
|
686
|
+
DRAFT_DATE=$(basename "$LAST" .md | head -c 10)
|
|
687
|
+
if [ -z "$LATEST_DRAFT" ] || [ "$DRAFT_DATE" \> "$LATEST_DRAFT" ]; then
|
|
688
|
+
LATEST_DRAFT="$DRAFT_DATE"
|
|
689
|
+
fi
|
|
690
|
+
fi
|
|
691
|
+
done
|
|
692
|
+
|
|
693
|
+
if [ -n "$LATEST_DRAFT" ]; then
|
|
694
|
+
TODAY=$(date +%Y-%m-%d)
|
|
695
|
+
# Count commits since last draft
|
|
696
|
+
COMMITS_SINCE=$(git -C "$CWD" log --oneline --since="$LATEST_DRAFT" 2>/dev/null | wc -l | tr -d ' ')
|
|
697
|
+
|
|
698
|
+
if [ "$LATEST_DRAFT" != "$TODAY" ] && [ "$COMMITS_SINCE" -gt 3 ]; then
|
|
699
|
+
DAYS_AGO=$(( ( $(date +%s) - $(date -j -f "%Y-%m-%d" "$LATEST_DRAFT" +%s 2>/dev/null || echo 0) ) / 86400 ))
|
|
700
|
+
echo "### JOURNAL DRAFTS OVERDUE (SHOULD — backfill before other work)"
|
|
701
|
+
echo ""
|
|
702
|
+
echo "Last draft: $LATEST_DRAFT ($DAYS_AGO days ago, $COMMITS_SINCE commits since)"
|
|
703
|
+
echo ""
|
|
704
|
+
echo "→ BACKFILL NOW — before starting new work."
|
|
705
|
+
echo " 1. Read chronicles since $LATEST_DRAFT"
|
|
706
|
+
echo " 2. Write drafts for each session with a notable decision or insight"
|
|
707
|
+
echo " 3. Push to Google Doc (oldest first)"
|
|
708
|
+
echo " 4. Then proceed with session work"
|
|
709
|
+
echo ""
|
|
710
|
+
echo "This is a SHOULD gate. Do not skip without documented rationale."
|
|
711
|
+
echo ""
|
|
712
|
+
fi
|
|
713
|
+
fi
|
|
714
|
+
fi
|
|
715
|
+
|
|
716
|
+
exit 0
|