@thebassclef/lite 1.0.2 → 1.0.4

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 (44) hide show
  1. package/dist/cli.cjs +51 -7
  2. package/dist/cli.js +51 -7
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/lite/.claude/hooks/session-reflection.d/00-bassclef-hook-connect.sh +63 -0
  7. package/dist/lite/.claude/hooks/session-reflection.d/05-active-bet.sh +113 -0
  8. package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +62 -0
  9. package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +78 -0
  10. package/dist/lite/.claude/hooks/session-reflection.d/12-session-lock.sh +63 -0
  11. package/dist/lite/.claude/hooks/session-reflection.d/15-substrate-config-validate.sh +91 -0
  12. package/dist/lite/.claude/hooks/session-reflection.d/20-artifact-staleness.sh +98 -0
  13. package/dist/lite/.claude/hooks/session-reflection.d/30-metrics-staleness.sh +61 -0
  14. package/dist/lite/.claude/hooks/session-reflection.d/35-post-merge-whereami-self-heal.sh +180 -0
  15. package/dist/lite/.claude/hooks/session-reflection.d/36-whereami-size-check.sh +117 -0
  16. package/dist/lite/.claude/hooks/session-reflection.d/40-gate-compliance.sh +108 -0
  17. package/dist/lite/.claude/hooks/session-reflection.d/45-auto-sync-status.sh +99 -0
  18. package/dist/lite/.claude/hooks/session-reflection.d/50-evolution-issues.sh +44 -0
  19. package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +187 -0
  20. package/dist/lite/.claude/hooks/session-reflection.d/56-roadmap-reconcile-gate.sh +88 -0
  21. package/dist/lite/.claude/hooks/session-reflection.d/60-deferred-actions.sh +212 -0
  22. package/dist/lite/.claude/hooks/session-reflection.d/70-recent-strategic-artifacts.sh +101 -0
  23. package/dist/lite/.claude/hooks/session-reflection.d/75-prior-r8-findings.sh +100 -0
  24. package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +87 -0
  25. package/dist/lite/.claude/hooks/session-reflection.d/80-workflow-staleness.sh +141 -0
  26. package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +67 -0
  27. package/dist/lite/.claude/hooks/session-reflection.d/90-release-backlog.sh +140 -0
  28. package/dist/lite/.claude/hooks/session-reflection.d/90-secrets-expiry.sh +104 -0
  29. package/dist/lite/.claude/hooks/session-reflection.d/95-settings-hook-verify.sh +131 -0
  30. package/dist/lite/.claude/hooks/session-reflection.d/95-telemetry-emit.sh +66 -0
  31. package/dist/lite/.claude/hooks/session-reflection.d/97-install-class-dispatch.sh +57 -0
  32. package/dist/lite/.claude/hooks/session-reflection.d/tests/00-bassclef-hook-connect.test.sh +492 -0
  33. package/dist/lite/.claude/hooks/session-reflection.d/tests/05-active-bet.test.sh +336 -0
  34. package/dist/lite/.claude/hooks/session-reflection.d/tests/12-session-lock.test.sh +216 -0
  35. package/dist/lite/.claude/hooks/session-reflection.d/tests/15-substrate-config-validate.test.sh +50 -0
  36. package/dist/lite/.claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh +348 -0
  37. package/dist/lite/.claude/hooks/session-reflection.d/tests/36-whereami-size-check.test.sh +189 -0
  38. package/dist/lite/.claude/hooks/session-reflection.d/tests/45-auto-sync-status.test.sh +263 -0
  39. package/dist/lite/.claude/hooks/session-reflection.d/tests/55-orientation-gate.test.sh +595 -0
  40. package/dist/lite/.claude/hooks/session-reflection.d/tests/56-roadmap-reconcile-gate.test.sh +227 -0
  41. package/dist/lite/.claude/hooks/session-reflection.d/tests/75-prior-r8-findings.test.sh +142 -0
  42. package/dist/lite/.claude/hooks/session-reflection.d/tests/90-release-backlog.test.sh +44 -0
  43. package/dist/lite/.claude/hooks/trace-helper.sh +22 -0
  44. package/package.json +1 -1
@@ -0,0 +1,595 @@
1
+ #!/usr/bin/env bash
2
+ # tier: standard
3
+ # fixtures: post-rename-adopter bassclef-upstream-self fully-cured-adopter
4
+ # Tests for .claude/hooks/session-reflection.d/55-orientation-gate.sh
5
+ #
6
+ # Per bassclef#1183 + bassclef#1266: mechanical enforcement that fires
7
+ # BLOCKED at SessionStart when the agent is in bassclef repo and no
8
+ # orientation marker exists for the current branch. /sprint + /longrun
9
+ # prep procedures (WU-3/WU-4) then refuse to propose work until the
10
+ # operator + agent have oriented at the arc level (active bet's
11
+ # parent_drivers + open bassclef-evolution issues + open audit-drops).
12
+ #
13
+ # Tier 0 strict-TDD per bassclef#1037; test list block + 10 sufficiency
14
+ # criteria per bassclef#1090 + bassclef#1098.
15
+ #
16
+ # Run: bash .claude/hooks/session-reflection.d/tests/55-orientation-gate.test.sh
17
+
18
+ set -u
19
+
20
+ # test-list:
21
+ # [x] Branch: fires BLOCKED in consumer repo too (bassclef#1275 — drop bassclef-only guard)
22
+ # [x] Branch: silent when in bassclef repo AND orientation marker exists for current branch
23
+ # [x] Branch: fires BLOCKED when in bassclef repo AND no orientation marker for current branch
24
+ # [x] Branch: fires BLOCKED on main when no main marker (cure 2 — 2026-06-30 audit)
25
+ # [x] Branch: silent on main when state/markers/orientation-gate/main.marker exists
26
+ # [x] Branch: silent on main when SKIP_ORIENTATION_GATE_ON_MAIN=1 (adopter compat per ADR-031)
27
+ # [x] Exit: exit 0 on all paths (SessionStart hooks emit informational BLOCKED, not blocking exit)
28
+ # [x] Exit: exit 0 when override SKIP_ORIENTATION_GATE=1 is set
29
+ # [x] Exit: exit 0 on missing binaries (graceful)
30
+ # [x] BLOCKED content: emits "🛑 BLOCKED: orientation-gate" header
31
+ # [x] BLOCKED content: includes active iteration_bet from docs/whereami.md
32
+ # [x] BLOCKED content: includes resolution path naming the marker file pattern
33
+ # [x] BLOCKED content: includes override path (SKIP_ORIENTATION_GATE=1)
34
+ # [x] BLOCKED content: cites bassclef#1183 + bassclef#1266
35
+ # [x] Override: SKIP_ORIENTATION_GATE=1 silences output entirely
36
+ # [x] Marker: marker at state/markers/orientation-gate/<branch-slug>.marker silences hook
37
+ # [x] Marker: marker on a DIFFERENT branch does NOT silence (per-branch scope)
38
+ # [x] Marker: handles branch slug with slash (feature/foo-bar → feature-foo-bar)
39
+ # [x] State-file: handles missing docs/whereami.md gracefully (silent, no crash)
40
+ # [x] State-file: handles whereami.md without iteration_bet field gracefully
41
+ # [x] External-binary: silent when git binary missing (no crash)
42
+ # [x] Config: substrate.config.md with bassclef_evolution_child_repos → BLOCKED scan includes each child repo
43
+ # [x] Config: substrate.config.md absent → BLOCKED scan defaults to current repo only
44
+ # [x] Config: bassclef_evolution_label override → BLOCKED scan uses configured label
45
+ # [x] Recursive walk: hook walks parent_bet chain when bet files exist (bassclef#1347)
46
+ # [x] Recursive walk: single bet (no parent_bet) does not crash
47
+ # [x] Recursive walk: missing bet file degrades gracefully
48
+ # [x] Recursive walk: cycle in chain (A→B→A) does not loop infinitely
49
+ # [x] Recursive walk: BLOCKED message labels the ancestor chain explicitly
50
+ # [x] Recursive walk: BLOCKED cites bassclef#1347
51
+ # [~] Trap: no tmpfiles created in hook; no trap needed. Reason: hook is read-only on filesystem.
52
+
53
+ REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
54
+ HOOK="${REPO_ROOT}/.claude/hooks/session-reflection.d/55-orientation-gate.sh"
55
+ PASS=0
56
+ FAIL=0
57
+ FAILED_TESTS=()
58
+
59
+ assert() {
60
+ local name="$1"
61
+ local result="$2"
62
+ if [ "$result" = "ok" ]; then
63
+ PASS=$((PASS + 1))
64
+ echo " PASS $name"
65
+ else
66
+ FAIL=$((FAIL + 1))
67
+ FAILED_TESTS+=("$name")
68
+ echo " FAIL $name — $result"
69
+ fi
70
+ }
71
+
72
+ setup_tmpdir() {
73
+ TMPDIR_TEST="$(mktemp -d)"
74
+ mkdir -p "$TMPDIR_TEST/.claude/hooks" "$TMPDIR_TEST/docs" "$TMPDIR_TEST/state/markers/orientation-gate"
75
+ # Set up a git repo so `git symbolic-ref --short HEAD` works
76
+ (cd "$TMPDIR_TEST" && git init -q && git config user.email t@t && git config user.name t \
77
+ && git checkout -q -b feature/test-branch 2>/dev/null \
78
+ && touch README.md && git add README.md && git commit -q -m init 2>/dev/null) || true
79
+ }
80
+
81
+ teardown_tmpdir() {
82
+ rm -rf "$TMPDIR_TEST"
83
+ }
84
+
85
+ # Inject a stub `gh` binary at the front of PATH that pretends we're in
86
+ # the bassclef repo (or not, per arg). Real gh isn't called in tests.
87
+ stub_gh_bassclef() {
88
+ cat > "$TMPDIR_TEST/gh" <<'EOF'
89
+ #!/usr/bin/env bash
90
+ # Test stub for gh CLI. Behavior controlled by GH_STUB_REPO_NAME env var.
91
+ if [ "$1" = "repo" ] && [ "$2" = "view" ]; then
92
+ echo "${GH_STUB_REPO_NAME:-bassclef}"
93
+ exit 0
94
+ fi
95
+ if [ "$1" = "issue" ] && [ "$2" = "list" ]; then
96
+ # Return a fixed line so the hook can count
97
+ echo "1268 Adopter-input prompts need plain English"
98
+ echo "1266 Dependency-aware /longrun + /sprint"
99
+ echo "1265 Wiki-sync mechanism not inherited"
100
+ exit 0
101
+ fi
102
+ exit 0
103
+ EOF
104
+ chmod +x "$TMPDIR_TEST/gh"
105
+ }
106
+
107
+ # Run hook with CWD=$TMPDIR_TEST + PATH including stub gh
108
+ run_hook() {
109
+ HOOK_STDOUT=$(cd "$TMPDIR_TEST" \
110
+ && PATH="$TMPDIR_TEST:$PATH" \
111
+ && CLAUDE_PROJECT_DIR="$TMPDIR_TEST" \
112
+ bash "$HOOK" 2>&1)
113
+ HOOK_EXIT=$?
114
+ }
115
+
116
+ if [ ! -x "$HOOK" ]; then
117
+ chmod +x "$HOOK" 2>/dev/null || true
118
+ fi
119
+
120
+ echo "Testing $HOOK"
121
+ echo "---"
122
+
123
+ # =====================================================================
124
+ # Branch coverage: adopter-too (bassclef#1275 — drop bassclef-only guard)
125
+ # =====================================================================
126
+
127
+ # Fires BLOCKED in consumer repo too — adopter inherits the discipline
128
+ setup_tmpdir
129
+ stub_gh_bassclef
130
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
131
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
132
+ EOF
133
+ GH_STUB_REPO_NAME=poa run_hook
134
+ [ "$HOOK_EXIT" = "0" ] && echo "$HOOK_STDOUT" | grep -q "orientation-gate" && \
135
+ assert "fires BLOCKED in consumer repo too (bassclef#1275)" "ok" || \
136
+ assert "fires BLOCKED in consumer repo too (bassclef#1275)" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
137
+ teardown_tmpdir
138
+
139
+ # Fires BLOCKED when in bassclef repo + no marker
140
+ setup_tmpdir
141
+ stub_gh_bassclef
142
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
143
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
144
+ EOF
145
+ run_hook
146
+ [ "$HOOK_EXIT" = "0" ] && echo "$HOOK_STDOUT" | grep -q "orientation-gate" && \
147
+ assert "fires BLOCKED in bassclef repo with no marker" "ok" || \
148
+ assert "fires BLOCKED in bassclef repo with no marker" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
149
+ teardown_tmpdir
150
+
151
+ # Fires BLOCKED on main when no main marker (cure 2 — 2026-06-30-session-orientation-blind-spot audit)
152
+ setup_tmpdir
153
+ stub_gh_bassclef
154
+ (cd "$TMPDIR_TEST" && git checkout -q -b main 2>/dev/null || git checkout -q main 2>/dev/null)
155
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
156
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
157
+ EOF
158
+ run_hook
159
+ [ "$HOOK_EXIT" = "0" ] && echo "$HOOK_STDOUT" | grep -q "orientation-gate" && \
160
+ assert "fires BLOCKED on main when no main marker (cure 2)" "ok" || \
161
+ assert "fires BLOCKED on main when no main marker (cure 2)" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
162
+ teardown_tmpdir
163
+
164
+ # Silent on main when main marker exists (sister of feature-branch-marker-silences)
165
+ setup_tmpdir
166
+ stub_gh_bassclef
167
+ (cd "$TMPDIR_TEST" && git checkout -q -b main 2>/dev/null || git checkout -q main 2>/dev/null)
168
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
169
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
170
+ EOF
171
+ touch "$TMPDIR_TEST/state/markers/orientation-gate/main.marker"
172
+ run_hook
173
+ [ "$HOOK_EXIT" = "0" ] && [ -z "$HOOK_STDOUT" ] && \
174
+ assert "silent on main when main marker exists" "ok" || \
175
+ assert "silent on main when main marker exists" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
176
+ teardown_tmpdir
177
+
178
+ # Silent on main when SKIP_ORIENTATION_GATE_ON_MAIN=1 (adopter compat per ADR-031)
179
+ setup_tmpdir
180
+ stub_gh_bassclef
181
+ (cd "$TMPDIR_TEST" && git checkout -q -b main 2>/dev/null || git checkout -q main 2>/dev/null)
182
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
183
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
184
+ EOF
185
+ HOOK_STDOUT=$(cd "$TMPDIR_TEST" \
186
+ && PATH="$TMPDIR_TEST:$PATH" \
187
+ && CLAUDE_PROJECT_DIR="$TMPDIR_TEST" \
188
+ && SKIP_ORIENTATION_GATE_ON_MAIN=1 \
189
+ bash "$HOOK" 2>&1)
190
+ HOOK_EXIT=$?
191
+ [ "$HOOK_EXIT" = "0" ] && [ -z "$HOOK_STDOUT" ] && \
192
+ assert "silent on main when SKIP_ORIENTATION_GATE_ON_MAIN=1 (adopter compat)" "ok" || \
193
+ assert "silent on main when SKIP_ORIENTATION_GATE_ON_MAIN=1 (adopter compat)" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
194
+ teardown_tmpdir
195
+
196
+ # =====================================================================
197
+ # BLOCKED content
198
+ # =====================================================================
199
+
200
+ # Header
201
+ setup_tmpdir
202
+ stub_gh_bassclef
203
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
204
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
205
+ EOF
206
+ run_hook
207
+ echo "$HOOK_STDOUT" | grep -q "🛑 BLOCKED: orientation-gate" && \
208
+ assert "BLOCKED header emitted" "ok" || \
209
+ assert "BLOCKED header emitted" "output=$HOOK_STDOUT"
210
+ teardown_tmpdir
211
+
212
+ # Active bet surfaced
213
+ setup_tmpdir
214
+ stub_gh_bassclef
215
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
216
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
217
+ EOF
218
+ run_hook
219
+ echo "$HOOK_STDOUT" | grep -q "2026-06-20a-longrun-sprint-orientation-cure" && \
220
+ assert "active iteration_bet name surfaced" "ok" || \
221
+ assert "active iteration_bet name surfaced" "output=$HOOK_STDOUT"
222
+ teardown_tmpdir
223
+
224
+ # Resolution path with marker pattern
225
+ setup_tmpdir
226
+ stub_gh_bassclef
227
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
228
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
229
+ EOF
230
+ run_hook
231
+ echo "$HOOK_STDOUT" | grep -qE "state/markers/orientation-gate" && \
232
+ assert "resolution path names marker file pattern" "ok" || \
233
+ assert "resolution path names marker file pattern" "output=$HOOK_STDOUT"
234
+ teardown_tmpdir
235
+
236
+ # Override path in BLOCKED content
237
+ setup_tmpdir
238
+ stub_gh_bassclef
239
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
240
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
241
+ EOF
242
+ run_hook
243
+ echo "$HOOK_STDOUT" | grep -q "SKIP_ORIENTATION_GATE=1" && \
244
+ assert "override path documented in BLOCKED" "ok" || \
245
+ assert "override path documented in BLOCKED" "output=$HOOK_STDOUT"
246
+ teardown_tmpdir
247
+
248
+ # Cites tickets
249
+ setup_tmpdir
250
+ stub_gh_bassclef
251
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
252
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
253
+ EOF
254
+ run_hook
255
+ echo "$HOOK_STDOUT" | grep -q "bassclef#1183" && echo "$HOOK_STDOUT" | grep -q "bassclef#1266" && \
256
+ assert "BLOCKED cites bassclef#1183 + bassclef#1266" "ok" || \
257
+ assert "BLOCKED cites bassclef#1183 + bassclef#1266" "output=$HOOK_STDOUT"
258
+ teardown_tmpdir
259
+
260
+ # =====================================================================
261
+ # Override
262
+ # =====================================================================
263
+
264
+ # SKIP_ORIENTATION_GATE=1 silences
265
+ setup_tmpdir
266
+ stub_gh_bassclef
267
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
268
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
269
+ EOF
270
+ HOOK_STDOUT=$(cd "$TMPDIR_TEST" && PATH="$TMPDIR_TEST:$PATH" CLAUDE_PROJECT_DIR="$TMPDIR_TEST" SKIP_ORIENTATION_GATE=1 bash "$HOOK" 2>&1)
271
+ HOOK_EXIT=$?
272
+ [ "$HOOK_EXIT" = "0" ] && [ -z "$HOOK_STDOUT" ] && \
273
+ assert "SKIP_ORIENTATION_GATE=1 silences hook" "ok" || \
274
+ assert "SKIP_ORIENTATION_GATE=1 silences hook" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
275
+ teardown_tmpdir
276
+
277
+ # =====================================================================
278
+ # Marker
279
+ # =====================================================================
280
+
281
+ # Marker for current branch silences
282
+ setup_tmpdir
283
+ stub_gh_bassclef
284
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
285
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
286
+ EOF
287
+ touch "$TMPDIR_TEST/state/markers/orientation-gate/feature-test-branch.marker"
288
+ run_hook
289
+ [ "$HOOK_EXIT" = "0" ] && [ -z "$HOOK_STDOUT" ] && \
290
+ assert "marker for current branch silences hook" "ok" || \
291
+ assert "marker for current branch silences hook" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
292
+ teardown_tmpdir
293
+
294
+ # Marker on different branch does NOT silence
295
+ setup_tmpdir
296
+ stub_gh_bassclef
297
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
298
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
299
+ EOF
300
+ touch "$TMPDIR_TEST/state/markers/orientation-gate/feature-OTHER-branch.marker"
301
+ run_hook
302
+ [ "$HOOK_EXIT" = "0" ] && echo "$HOOK_STDOUT" | grep -q "orientation-gate" && \
303
+ assert "marker on other branch does NOT silence" "ok" || \
304
+ assert "marker on other branch does NOT silence" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
305
+ teardown_tmpdir
306
+
307
+ # Branch slug with slash translates to dash
308
+ setup_tmpdir
309
+ stub_gh_bassclef
310
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
311
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
312
+ EOF
313
+ # Current branch in setup_tmpdir is "feature/test-branch" → should map to "feature-test-branch.marker"
314
+ touch "$TMPDIR_TEST/state/markers/orientation-gate/feature-test-branch.marker"
315
+ run_hook
316
+ [ "$HOOK_EXIT" = "0" ] && [ -z "$HOOK_STDOUT" ] && \
317
+ assert "slash-in-branch-name handled (feature/foo → feature-foo)" "ok" || \
318
+ assert "slash-in-branch-name handled (feature/foo → feature-foo)" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
319
+ teardown_tmpdir
320
+
321
+ # =====================================================================
322
+ # State-file graceful handling
323
+ # =====================================================================
324
+
325
+ # Missing whereami.md → silent (can't determine bet, gracefully decline)
326
+ setup_tmpdir
327
+ stub_gh_bassclef
328
+ # No whereami.md created
329
+ run_hook
330
+ [ "$HOOK_EXIT" = "0" ] && \
331
+ assert "missing whereami.md does not crash" "ok" || \
332
+ assert "missing whereami.md does not crash" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
333
+ teardown_tmpdir
334
+
335
+ # whereami.md without iteration_bet field → emits BLOCKED but with "—" or "none active" for bet
336
+ setup_tmpdir
337
+ stub_gh_bassclef
338
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
339
+ phase: Construction
340
+ note: some note
341
+ EOF
342
+ run_hook
343
+ [ "$HOOK_EXIT" = "0" ] && \
344
+ assert "whereami without iteration_bet field handled gracefully" "ok" || \
345
+ assert "whereami without iteration_bet field handled gracefully" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
346
+ teardown_tmpdir
347
+
348
+ # =====================================================================
349
+ # External binary graceful handling
350
+ # =====================================================================
351
+
352
+ # Missing gh binary → silent (consumer-repo detection fails-soft)
353
+ setup_tmpdir
354
+ # No stub_gh_bassclef — PATH won't contain gh in tmpdir
355
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
356
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
357
+ EOF
358
+ HOOK_STDOUT=$(cd "$TMPDIR_TEST" && PATH="/usr/bin:/bin" CLAUDE_PROJECT_DIR="$TMPDIR_TEST" bash "$HOOK" 2>&1)
359
+ HOOK_EXIT=$?
360
+ [ "$HOOK_EXIT" = "0" ] && \
361
+ assert "missing gh binary does not crash" "ok" || \
362
+ assert "missing gh binary does not crash" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
363
+ teardown_tmpdir
364
+
365
+ # =====================================================================
366
+ # Config — adopter-configurable scan (bassclef#1275)
367
+ # =====================================================================
368
+
369
+ # substrate.config.md with bassclef_evolution_child_repos → BLOCKED scan includes children
370
+ setup_tmpdir
371
+ stub_gh_bassclef
372
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
373
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
374
+ EOF
375
+ cat > "$TMPDIR_TEST/substrate.config.md" <<'EOF'
376
+ bassclef_evolution_child_repos: sunj-labs/poa, sunj-labs/twodo
377
+ EOF
378
+ run_hook
379
+ echo "$HOOK_STDOUT" | grep -q "sunj-labs/poa" && echo "$HOOK_STDOUT" | grep -q "sunj-labs/twodo" && \
380
+ assert "scan command includes child repos from substrate.config.md" "ok" || \
381
+ assert "scan command includes child repos from substrate.config.md" "output=$HOOK_STDOUT"
382
+ teardown_tmpdir
383
+
384
+ # substrate.config.md absent → defaults to current repo only (no -R flag)
385
+ setup_tmpdir
386
+ stub_gh_bassclef
387
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
388
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
389
+ EOF
390
+ # No substrate.config.md
391
+ run_hook
392
+ # Default scan: gh issue list --label bassclef-evolution (no -R for current repo)
393
+ echo "$HOOK_STDOUT" | grep -qE "gh issue list[^-]*--label .?bassclef-evolution" && \
394
+ assert "scan defaults to current repo + bassclef-evolution label when config absent" "ok" || \
395
+ assert "scan defaults to current repo + bassclef-evolution label when config absent" "output=$HOOK_STDOUT"
396
+ teardown_tmpdir
397
+
398
+ # bassclef_evolution_label override → BLOCKED scan uses configured label
399
+ setup_tmpdir
400
+ stub_gh_bassclef
401
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
402
+ iteration_bet: 2026-06-20a-longrun-sprint-orientation-cure
403
+ EOF
404
+ cat > "$TMPDIR_TEST/substrate.config.md" <<'EOF'
405
+ bassclef_evolution_label: substrate-evolution
406
+ EOF
407
+ run_hook
408
+ echo "$HOOK_STDOUT" | grep -q "substrate-evolution" && \
409
+ assert "scan command uses configured label override" "ok" || \
410
+ assert "scan command uses configured label override" "output=$HOOK_STDOUT"
411
+ teardown_tmpdir
412
+
413
+ # =====================================================================
414
+ # Recursive parent_bet walk (bassclef#1347)
415
+ # =====================================================================
416
+
417
+ # Hook walks parent_bet chain recursively and surfaces ancestor lineage
418
+ setup_tmpdir
419
+ stub_gh_bassclef
420
+ mkdir -p "$TMPDIR_TEST/docs/iteration-bets"
421
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
422
+ iteration_bet: 2026-06-20h-marathon
423
+ EOF
424
+ cat > "$TMPDIR_TEST/docs/iteration-bets/2026-06-20h-marathon.md" <<'EOF'
425
+ ---
426
+ slug: 2026-06-20h-marathon
427
+ parent_bet: 2026-06-20g-validate
428
+ ---
429
+ EOF
430
+ cat > "$TMPDIR_TEST/docs/iteration-bets/2026-06-20g-validate.md" <<'EOF'
431
+ ---
432
+ slug: 2026-06-20g-validate
433
+ parent_bet: 2026-06-20f-rename
434
+ ---
435
+ EOF
436
+ cat > "$TMPDIR_TEST/docs/iteration-bets/2026-06-20f-rename.md" <<'EOF'
437
+ ---
438
+ slug: 2026-06-20f-rename
439
+ ---
440
+ EOF
441
+ run_hook
442
+ echo "$HOOK_STDOUT" | grep -q "2026-06-20g-validate" && \
443
+ echo "$HOOK_STDOUT" | grep -q "2026-06-20f-rename" && \
444
+ assert "recursive walk surfaces 3-deep ancestor chain" "ok" || \
445
+ assert "recursive walk surfaces 3-deep ancestor chain" "output=$HOOK_STDOUT"
446
+ teardown_tmpdir
447
+
448
+ # Single bet (no parent_bet) — chain is just the active bet
449
+ setup_tmpdir
450
+ stub_gh_bassclef
451
+ mkdir -p "$TMPDIR_TEST/docs/iteration-bets"
452
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
453
+ iteration_bet: 2026-06-20-alone
454
+ EOF
455
+ cat > "$TMPDIR_TEST/docs/iteration-bets/2026-06-20-alone.md" <<'EOF'
456
+ ---
457
+ slug: 2026-06-20-alone
458
+ ---
459
+ EOF
460
+ run_hook
461
+ [ "$HOOK_EXIT" = "0" ] && \
462
+ assert "single bet (no parent_bet) does not crash recursive walk" "ok" || \
463
+ assert "single bet (no parent_bet) does not crash recursive walk" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
464
+ teardown_tmpdir
465
+
466
+ # Missing bet file — recursive walk degrades gracefully
467
+ setup_tmpdir
468
+ stub_gh_bassclef
469
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
470
+ iteration_bet: 2026-06-20-missing
471
+ EOF
472
+ # Note: no bet file at docs/iteration-bets/2026-06-20-missing.md
473
+ run_hook
474
+ [ "$HOOK_EXIT" = "0" ] && \
475
+ assert "missing bet file does not crash recursive walk" "ok" || \
476
+ assert "missing bet file does not crash recursive walk" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
477
+ teardown_tmpdir
478
+
479
+ # Cycle protection — chain with bet A → B → A doesn't loop infinitely
480
+ setup_tmpdir
481
+ stub_gh_bassclef
482
+ mkdir -p "$TMPDIR_TEST/docs/iteration-bets"
483
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
484
+ iteration_bet: bet-A
485
+ EOF
486
+ cat > "$TMPDIR_TEST/docs/iteration-bets/bet-A.md" <<'EOF'
487
+ ---
488
+ parent_bet: bet-B
489
+ ---
490
+ EOF
491
+ cat > "$TMPDIR_TEST/docs/iteration-bets/bet-B.md" <<'EOF'
492
+ ---
493
+ parent_bet: bet-A
494
+ ---
495
+ EOF
496
+ # Cycle protection lives in the hook itself (max_depth bound); test runs normally.
497
+ # If the hook is buggy and loops, the test hangs — that surfaces the bug.
498
+ run_hook
499
+ [ "$HOOK_EXIT" = "0" ] && \
500
+ assert "cycle in parent_bet chain does not infinite-loop" "ok" || \
501
+ assert "cycle in parent_bet chain does not infinite-loop" "exit=$HOOK_EXIT output=$HOOK_STDOUT"
502
+ teardown_tmpdir
503
+
504
+ # BLOCKED message includes section labeling the ancestor chain
505
+ setup_tmpdir
506
+ stub_gh_bassclef
507
+ mkdir -p "$TMPDIR_TEST/docs/iteration-bets"
508
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
509
+ iteration_bet: 2026-06-20h-marathon
510
+ EOF
511
+ cat > "$TMPDIR_TEST/docs/iteration-bets/2026-06-20h-marathon.md" <<'EOF'
512
+ ---
513
+ slug: 2026-06-20h-marathon
514
+ parent_bet: 2026-06-20g-validate
515
+ ---
516
+ EOF
517
+ cat > "$TMPDIR_TEST/docs/iteration-bets/2026-06-20g-validate.md" <<'EOF'
518
+ ---
519
+ slug: 2026-06-20g-validate
520
+ ---
521
+ EOF
522
+ run_hook
523
+ echo "$HOOK_STDOUT" | grep -qE "[Aa]ncestor|[Cc]hain|[Ll]ineage" && \
524
+ assert "BLOCKED message labels the chain explicitly" "ok" || \
525
+ assert "BLOCKED message labels the chain explicitly" "output=$HOOK_STDOUT"
526
+ teardown_tmpdir
527
+
528
+ # BLOCKED cites bassclef#1347 (recursive walk source ticket)
529
+ setup_tmpdir
530
+ stub_gh_bassclef
531
+ cat > "$TMPDIR_TEST/docs/whereami.md" <<'EOF'
532
+ iteration_bet: 2026-06-20a
533
+ EOF
534
+ run_hook
535
+ echo "$HOOK_STDOUT" | grep -q "bassclef#1347" && \
536
+ assert "BLOCKED cites bassclef#1347 (recursive walk)" "ok" || \
537
+ assert "BLOCKED cites bassclef#1347 (recursive walk)" "output=$HOOK_STDOUT"
538
+ teardown_tmpdir
539
+
540
+ # =====================================================================
541
+ # Fixture-driven coverage (per #545 retroactive backfill)
542
+ # =====================================================================
543
+ # When FIXTURE_DIR is exported by run-with-fixtures.sh wrapper, the test
544
+ # runs once per declared fixture against a real adopter-shaped repo.
545
+ # Verifies the gate fires on main inside an adopter environment when no
546
+ # main marker exists and whereami is present.
547
+
548
+ if [ -n "${FIXTURE_DIR:-}" ]; then
549
+ fixture_name="${FIXTURE_DIR##*/}"
550
+ # Set up the fixture as a git repo on main + write a whereami file
551
+ (cd "$FIXTURE_DIR" && git init -q 2>/dev/null && git config user.email t@t && git config user.name t \
552
+ && git checkout -q -b main 2>/dev/null || git checkout -q main 2>/dev/null) || true
553
+ mkdir -p "$FIXTURE_DIR/docs" "$FIXTURE_DIR/.claude/hooks"
554
+ cat > "$FIXTURE_DIR/docs/whereami.md" <<'EOF'
555
+ iteration_bet: fixture-driven-test-bet
556
+ EOF
557
+ # Stub gh binary at fixture root so the hook doesn't crash on a real query
558
+ cat > "$FIXTURE_DIR/gh" <<'EOF'
559
+ #!/usr/bin/env bash
560
+ if [ "$1" = "repo" ] && [ "$2" = "view" ]; then echo "bassclef"; exit 0; fi
561
+ if [ "$1" = "issue" ] && [ "$2" = "list" ]; then exit 0; fi
562
+ exit 0
563
+ EOF
564
+ chmod +x "$FIXTURE_DIR/gh"
565
+
566
+ FIXTURE_STDOUT=$(cd "$FIXTURE_DIR" \
567
+ && PATH="$FIXTURE_DIR:$PATH" \
568
+ && CLAUDE_PROJECT_DIR="$FIXTURE_DIR" \
569
+ bash "$HOOK" 2>&1)
570
+ FIXTURE_EXIT=$?
571
+
572
+ if [ "$FIXTURE_EXIT" = "0" ] && echo "$FIXTURE_STDOUT" | grep -q "orientation-gate"; then
573
+ assert "fixture-${fixture_name}-fires-on-main-with-no-marker" "ok"
574
+ else
575
+ assert "fixture-${fixture_name}-fires-on-main-with-no-marker" "exit=$FIXTURE_EXIT output=$FIXTURE_STDOUT"
576
+ fi
577
+ fi
578
+
579
+ # =====================================================================
580
+ # Summary
581
+ # =====================================================================
582
+
583
+ echo "---"
584
+ echo "Results: $PASS passed, $FAIL failed"
585
+
586
+ if [ "$FAIL" -gt 0 ]; then
587
+ echo ""
588
+ echo "Failed tests:"
589
+ for t in "${FAILED_TESTS[@]}"; do
590
+ echo " - $t"
591
+ done
592
+ exit 1
593
+ fi
594
+
595
+ exit 0