@thebassclef/lite 1.0.1 → 1.0.3

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 (46) hide show
  1. package/dist/cli.cjs +24 -1
  2. package/dist/cli.js +24 -1
  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/bassclef-sync.sh +98 -16
  7. package/dist/lite/.claude/hooks/longrun-prep-compounding-sequence-check.sh +3 -3
  8. package/dist/lite/.claude/hooks/session-reflection.d/00-bassclef-hook-connect.sh +63 -0
  9. package/dist/lite/.claude/hooks/session-reflection.d/05-active-bet.sh +113 -0
  10. package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +62 -0
  11. package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +78 -0
  12. package/dist/lite/.claude/hooks/session-reflection.d/12-session-lock.sh +63 -0
  13. package/dist/lite/.claude/hooks/session-reflection.d/15-substrate-config-validate.sh +91 -0
  14. package/dist/lite/.claude/hooks/session-reflection.d/20-artifact-staleness.sh +98 -0
  15. package/dist/lite/.claude/hooks/session-reflection.d/30-metrics-staleness.sh +61 -0
  16. package/dist/lite/.claude/hooks/session-reflection.d/35-post-merge-whereami-self-heal.sh +180 -0
  17. package/dist/lite/.claude/hooks/session-reflection.d/36-whereami-size-check.sh +117 -0
  18. package/dist/lite/.claude/hooks/session-reflection.d/40-gate-compliance.sh +108 -0
  19. package/dist/lite/.claude/hooks/session-reflection.d/45-auto-sync-status.sh +99 -0
  20. package/dist/lite/.claude/hooks/session-reflection.d/50-evolution-issues.sh +44 -0
  21. package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +187 -0
  22. package/dist/lite/.claude/hooks/session-reflection.d/56-roadmap-reconcile-gate.sh +88 -0
  23. package/dist/lite/.claude/hooks/session-reflection.d/60-deferred-actions.sh +212 -0
  24. package/dist/lite/.claude/hooks/session-reflection.d/70-recent-strategic-artifacts.sh +101 -0
  25. package/dist/lite/.claude/hooks/session-reflection.d/75-prior-r8-findings.sh +100 -0
  26. package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +87 -0
  27. package/dist/lite/.claude/hooks/session-reflection.d/80-workflow-staleness.sh +141 -0
  28. package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +67 -0
  29. package/dist/lite/.claude/hooks/session-reflection.d/90-release-backlog.sh +140 -0
  30. package/dist/lite/.claude/hooks/session-reflection.d/90-secrets-expiry.sh +104 -0
  31. package/dist/lite/.claude/hooks/session-reflection.d/95-settings-hook-verify.sh +131 -0
  32. package/dist/lite/.claude/hooks/session-reflection.d/95-telemetry-emit.sh +66 -0
  33. package/dist/lite/.claude/hooks/session-reflection.d/97-install-class-dispatch.sh +57 -0
  34. package/dist/lite/.claude/hooks/session-reflection.d/tests/00-bassclef-hook-connect.test.sh +492 -0
  35. package/dist/lite/.claude/hooks/session-reflection.d/tests/05-active-bet.test.sh +336 -0
  36. package/dist/lite/.claude/hooks/session-reflection.d/tests/12-session-lock.test.sh +216 -0
  37. package/dist/lite/.claude/hooks/session-reflection.d/tests/15-substrate-config-validate.test.sh +50 -0
  38. package/dist/lite/.claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh +348 -0
  39. package/dist/lite/.claude/hooks/session-reflection.d/tests/36-whereami-size-check.test.sh +189 -0
  40. package/dist/lite/.claude/hooks/session-reflection.d/tests/45-auto-sync-status.test.sh +263 -0
  41. package/dist/lite/.claude/hooks/session-reflection.d/tests/55-orientation-gate.test.sh +595 -0
  42. package/dist/lite/.claude/hooks/session-reflection.d/tests/56-roadmap-reconcile-gate.test.sh +227 -0
  43. package/dist/lite/.claude/hooks/session-reflection.d/tests/75-prior-r8-findings.test.sh +142 -0
  44. package/dist/lite/.claude/hooks/session-reflection.d/tests/90-release-backlog.test.sh +44 -0
  45. package/dist/lite/.claude/hooks/trace-helper.sh +22 -0
  46. package/package.json +1 -1
@@ -0,0 +1,336 @@
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/05-active-bet.sh
5
+ #
6
+ # Per architecture/audits/2026-06-30-session-orientation-blind-spot.md cure 1:
7
+ # surface in_flight_bet from whereami prominently at SessionStart. The audit's
8
+ # root cause was the agent narrowing scope to "bet 29e wrap" from a stale
9
+ # branch name; whereami + commits had bet 30a as in_flight. This hook puts
10
+ # the in_flight_bet front of the SessionStart output so the agent cannot
11
+ # narrow scope without seeing it.
12
+ #
13
+ # Tier 0 strict-TDD per testing-tier-config.md; test list + 11 sufficiency
14
+ # criteria per test-sufficiency.md.
15
+ #
16
+ # Run: bash .claude/hooks/session-reflection.d/tests/05-active-bet.test.sh
17
+
18
+ set -u
19
+
20
+ # test-list:
21
+ # [ ] Branch: emits ### ACTIVE BET header when whereami has in_flight_bet field
22
+ # [ ] Branch: emits "no in-flight bet" message when whereami absent
23
+ # [ ] Branch: emits "no in-flight bet" message when whereami present but no in_flight_bet field
24
+ # [ ] Branch: extracts bet slug from in_flight_bet.bet: field (Shape B markdown frontmatter)
25
+ # [ ] Exit: exit 0 on all paths (SessionStart hooks are informational)
26
+ # [ ] Exit: exit 0 when SKIP_ACTIVE_BET=1 override is set
27
+ # [ ] Exit: exit 0 on missing binaries (graceful)
28
+ # [ ] Output: header includes "ACTIVE BET" + "read before scoping"
29
+ # [ ] Output: bet slug appears literally in output when present
30
+ # [ ] Output: status field appears when present in whereami
31
+ # [ ] Output: remaining_wus appears when present in whereami
32
+ # [ ] Output: includes one-line pointer to bet doc path
33
+ # [ ] Output: cites the cure source
34
+ # [ ] State-file: handles missing docs/whereami.md gracefully
35
+ # [ ] State-file: handles whereami.md with malformed YAML gracefully
36
+ # [ ] Override: SKIP_ACTIVE_BET=1 silences output entirely
37
+ # [~] Trap: no tmpfiles created in hook; no trap needed. Reason: hook is read-only.
38
+ # [ ] Install-class: hook resolves from CLAUDE_PROJECT_DIR under project install (criterion 11)
39
+ # [ ] Install-class: hook resolves from operator install layout (criterion 11)
40
+
41
+ REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
42
+ HOOK="${REPO_ROOT}/.claude/hooks/session-reflection.d/05-active-bet.sh"
43
+ PASS=0
44
+ FAIL=0
45
+ FAILED_TESTS=()
46
+
47
+ assert() {
48
+ local name="$1"
49
+ local result="$2"
50
+ if [ "$result" = "ok" ]; then
51
+ PASS=$((PASS + 1))
52
+ else
53
+ FAIL=$((FAIL + 1))
54
+ FAILED_TESTS+=("$name: $result")
55
+ fi
56
+ }
57
+
58
+ run_hook_in() {
59
+ local tmpdir="$1"
60
+ (cd "$tmpdir" && bash "$HOOK" 2>&1)
61
+ }
62
+
63
+ write_whereami_with_bet() {
64
+ local tmpdir="$1"
65
+ local bet_slug="$2"
66
+ local status="${3:-in_flight}"
67
+ local remaining="${4:-}"
68
+ mkdir -p "$tmpdir/docs"
69
+ cat > "$tmpdir/docs/whereami.md" <<WHEREAMI
70
+ ---
71
+ project: test-repo
72
+ phase: Construction
73
+ in_flight_bet:
74
+ bet: docs/iteration-bets/${bet_slug}.md
75
+ status: ${status}
76
+ remaining_wus: [${remaining}]
77
+ ---
78
+
79
+ # whereami — test-repo
80
+
81
+ ## Role
82
+ test
83
+ WHEREAMI
84
+ }
85
+
86
+ # --- Test 1
87
+ TMP=$(mktemp -d)
88
+ write_whereami_with_bet "$TMP" "2026-06-30b-substrate-anchoring-cure" "in_flight" "C, D, E, F"
89
+ out=$(run_hook_in "$TMP")
90
+ rm -rf "$TMP"
91
+ if echo "$out" | grep -q "### ACTIVE BET"; then
92
+ assert "01-header-when-bet-present" "ok"
93
+ else
94
+ assert "01-header-when-bet-present" "missing header; got: $out"
95
+ fi
96
+
97
+ # --- Test 2
98
+ TMP=$(mktemp -d)
99
+ out=$(run_hook_in "$TMP")
100
+ rm -rf "$TMP"
101
+ if echo "$out" | grep -qi "no in-flight bet\|whereami absent\|whereami missing"; then
102
+ assert "02-no-bet-message-when-whereami-absent" "ok"
103
+ else
104
+ assert "02-no-bet-message-when-whereami-absent" "missing no-bet message; got: $out"
105
+ fi
106
+
107
+ # --- Test 3
108
+ TMP=$(mktemp -d)
109
+ mkdir -p "$TMP/docs"
110
+ cat > "$TMP/docs/whereami.md" <<'EOF'
111
+ ---
112
+ project: test-repo
113
+ phase: Construction
114
+ ---
115
+
116
+ # whereami — test-repo
117
+ EOF
118
+ out=$(run_hook_in "$TMP")
119
+ rm -rf "$TMP"
120
+ if echo "$out" | grep -qi "no in-flight bet"; then
121
+ assert "03-no-bet-message-when-field-absent" "ok"
122
+ else
123
+ assert "03-no-bet-message-when-field-absent" "missing no-bet message; got: $out"
124
+ fi
125
+
126
+ # --- Test 4
127
+ TMP=$(mktemp -d)
128
+ write_whereami_with_bet "$TMP" "2026-06-30b-substrate-anchoring-cure"
129
+ out=$(run_hook_in "$TMP")
130
+ rm -rf "$TMP"
131
+ if echo "$out" | grep -q "2026-06-30b-substrate-anchoring-cure"; then
132
+ assert "04-extracts-bet-slug" "ok"
133
+ else
134
+ assert "04-extracts-bet-slug" "slug not in output; got: $out"
135
+ fi
136
+
137
+ # --- Test 5
138
+ TMP=$(mktemp -d)
139
+ write_whereami_with_bet "$TMP" "test-slug"
140
+ (cd "$TMP" && bash "$HOOK" >/dev/null 2>&1)
141
+ ec=$?
142
+ rm -rf "$TMP"
143
+ if [ "$ec" = "0" ]; then
144
+ assert "05-exit-0-when-bet-present" "ok"
145
+ else
146
+ assert "05-exit-0-when-bet-present" "expected exit 0; got $ec"
147
+ fi
148
+
149
+ # --- Test 6
150
+ TMP=$(mktemp -d)
151
+ write_whereami_with_bet "$TMP" "test-slug"
152
+ (cd "$TMP" && SKIP_ACTIVE_BET=1 bash "$HOOK" >/dev/null 2>&1)
153
+ ec=$?
154
+ rm -rf "$TMP"
155
+ if [ "$ec" = "0" ]; then
156
+ assert "06-exit-0-when-override-set" "ok"
157
+ else
158
+ assert "06-exit-0-when-override-set" "expected exit 0; got $ec"
159
+ fi
160
+
161
+ # --- Test 7
162
+ TMP=$(mktemp -d)
163
+ write_whereami_with_bet "$TMP" "test-slug"
164
+ (cd "$TMP" && PATH=/usr/bin:/bin bash "$HOOK" >/dev/null 2>&1)
165
+ ec=$?
166
+ rm -rf "$TMP"
167
+ if [ "$ec" = "0" ]; then
168
+ assert "07-exit-0-on-missing-binaries" "ok"
169
+ else
170
+ assert "07-exit-0-on-missing-binaries" "expected exit 0; got $ec"
171
+ fi
172
+
173
+ # --- Test 8
174
+ TMP=$(mktemp -d)
175
+ write_whereami_with_bet "$TMP" "test-slug"
176
+ out=$(run_hook_in "$TMP")
177
+ rm -rf "$TMP"
178
+ if echo "$out" | grep -qi "read before scoping work\|read before scope"; then
179
+ assert "08-tagline-present" "ok"
180
+ else
181
+ assert "08-tagline-present" "missing tagline; got: $out"
182
+ fi
183
+
184
+ # --- Test 9
185
+ TMP=$(mktemp -d)
186
+ write_whereami_with_bet "$TMP" "2026-06-30b-substrate-anchoring-cure"
187
+ out=$(run_hook_in "$TMP")
188
+ rm -rf "$TMP"
189
+ if echo "$out" | grep -q "2026-06-30b-substrate-anchoring-cure"; then
190
+ assert "09-bet-slug-literal" "ok"
191
+ else
192
+ assert "09-bet-slug-literal" "slug not literal in output; got: $out"
193
+ fi
194
+
195
+ # --- Test 10
196
+ TMP=$(mktemp -d)
197
+ write_whereami_with_bet "$TMP" "test-slug" "in_flight"
198
+ out=$(run_hook_in "$TMP")
199
+ rm -rf "$TMP"
200
+ if echo "$out" | grep -qi "in_flight"; then
201
+ assert "10-status-field-in-output" "ok"
202
+ else
203
+ assert "10-status-field-in-output" "status not in output; got: $out"
204
+ fi
205
+
206
+ # --- Test 11
207
+ TMP=$(mktemp -d)
208
+ write_whereami_with_bet "$TMP" "test-slug" "in_flight" "C, D, E, F"
209
+ out=$(run_hook_in "$TMP")
210
+ rm -rf "$TMP"
211
+ if echo "$out" | grep -q "remaining_wus\|C, D, E, F\|C,D,E,F"; then
212
+ assert "11-remaining-wus-in-output" "ok"
213
+ else
214
+ assert "11-remaining-wus-in-output" "remaining_wus not in output; got: $out"
215
+ fi
216
+
217
+ # --- Test 12
218
+ TMP=$(mktemp -d)
219
+ write_whereami_with_bet "$TMP" "test-slug"
220
+ out=$(run_hook_in "$TMP")
221
+ rm -rf "$TMP"
222
+ if echo "$out" | grep -q "docs/iteration-bets/test-slug.md"; then
223
+ assert "12-bet-doc-path-pointer" "ok"
224
+ else
225
+ assert "12-bet-doc-path-pointer" "bet path not in output; got: $out"
226
+ fi
227
+
228
+ # --- Test 13
229
+ TMP=$(mktemp -d)
230
+ write_whereami_with_bet "$TMP" "test-slug"
231
+ out=$(run_hook_in "$TMP")
232
+ rm -rf "$TMP"
233
+ if echo "$out" | grep -q "2026-06-30-session-orientation-blind-spot\|bassclef-upstream#540"; then
234
+ assert "13-cure-source-cited" "ok"
235
+ else
236
+ assert "13-cure-source-cited" "cure source not cited; got: $out"
237
+ fi
238
+
239
+ # --- Test 14
240
+ TMP=$(mktemp -d)
241
+ out=$(run_hook_in "$TMP" 2>&1)
242
+ ec=$?
243
+ rm -rf "$TMP"
244
+ if [ "$ec" = "0" ]; then
245
+ assert "14-graceful-missing-whereami" "ok"
246
+ else
247
+ assert "14-graceful-missing-whereami" "expected exit 0; got $ec"
248
+ fi
249
+
250
+ # --- Test 15
251
+ TMP=$(mktemp -d)
252
+ mkdir -p "$TMP/docs"
253
+ cat > "$TMP/docs/whereami.md" <<'EOF'
254
+ ---
255
+ project: broken
256
+ in_flight_bet:
257
+ bet: docs/iteration-bets/broken
258
+ status: in_flight
259
+ unterminated: "string
260
+ ---
261
+
262
+ EOF
263
+ out=$(run_hook_in "$TMP" 2>&1)
264
+ ec=$?
265
+ rm -rf "$TMP"
266
+ if [ "$ec" = "0" ]; then
267
+ assert "15-graceful-malformed-yaml" "ok"
268
+ else
269
+ assert "15-graceful-malformed-yaml" "expected exit 0; got $ec"
270
+ fi
271
+
272
+ # --- Test 16
273
+ TMP=$(mktemp -d)
274
+ write_whereami_with_bet "$TMP" "test-slug"
275
+ out=$(cd "$TMP" && SKIP_ACTIVE_BET=1 bash "$HOOK" 2>&1)
276
+ rm -rf "$TMP"
277
+ if [ -z "$out" ]; then
278
+ assert "16-skip-silences-output" "ok"
279
+ else
280
+ assert "16-skip-silences-output" "expected empty output; got: $out"
281
+ fi
282
+
283
+ # --- Test 17
284
+ TMP=$(mktemp -d)
285
+ write_whereami_with_bet "$TMP" "test-slug"
286
+ out=$(cd "$TMP" && CLAUDE_PROJECT_DIR="$TMP" bash "$HOOK" 2>&1)
287
+ ec=$?
288
+ rm -rf "$TMP"
289
+ if [ "$ec" = "0" ] && echo "$out" | grep -q "test-slug"; then
290
+ assert "17-install-class-project" "ok"
291
+ else
292
+ assert "17-install-class-project" "expected exit 0 + bet visible; got ec=$ec out=$out"
293
+ fi
294
+
295
+ # --- Test 18
296
+ TMP=$(mktemp -d)
297
+ HOME_FAKE=$(mktemp -d)
298
+ write_whereami_with_bet "$TMP" "operator-install-test-slug"
299
+ out=$(cd "$TMP" && HOME="$HOME_FAKE" bash "$HOOK" 2>&1)
300
+ ec=$?
301
+ rm -rf "$TMP" "$HOME_FAKE"
302
+ if [ "$ec" = "0" ] && echo "$out" | grep -q "operator-install-test-slug"; then
303
+ assert "18-install-class-operator" "ok"
304
+ else
305
+ assert "18-install-class-operator" "expected exit 0 + bet visible; got ec=$ec out=$out"
306
+ fi
307
+
308
+ # --- Test 19: fixture-driven coverage (rides under run-with-fixtures.sh wrapper)
309
+ # When FIXTURE_DIR is exported by the wrapper, the test runs against a real
310
+ # fixture-shaped adopter repo. Per #545 retroactive backfill — every cure
311
+ # test on substrate-affecting paths declares fixtures it rides AND uses
312
+ # FIXTURE_DIR in at least one assertion to exercise the fixture's environment.
313
+ if [ -n "${FIXTURE_DIR:-}" ]; then
314
+ write_whereami_with_bet "$FIXTURE_DIR" "fixture-driven-bet-slug" "in_flight" "X, Y, Z"
315
+ fixture_out=$(cd "$FIXTURE_DIR" && bash "$HOOK" 2>&1)
316
+ fixture_name="${FIXTURE_DIR##*/}"
317
+ if echo "$fixture_out" | grep -q "fixture-driven-bet-slug"; then
318
+ assert "19-fixture-coverage-${fixture_name}" "ok"
319
+ else
320
+ assert "19-fixture-coverage-${fixture_name}" "fixture bet not surfaced; got: $fixture_out"
321
+ fi
322
+ fi
323
+
324
+ # --- Summary
325
+ TOTAL=$((PASS + FAIL))
326
+ echo ""
327
+ echo "=== 05-active-bet.sh tests ==="
328
+ echo "Passed: $PASS / $TOTAL"
329
+ if [ "$FAIL" -gt 0 ]; then
330
+ echo "Failed:"
331
+ for t in "${FAILED_TESTS[@]}"; do
332
+ echo " - $t"
333
+ done
334
+ exit 1
335
+ fi
336
+ exit 0
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env bash
2
+ # tier: standard
3
+ # Tests for .claude/hooks/session-reflection.d/12-session-lock.sh
4
+ #
5
+ # WU-1 of bet 2026-06-18a (bassclef#1155). Back-ports pgid self-
6
+ # recognition pattern from save-state.sh L123-130 + session-end.sh
7
+ # Guard 4 into the session-start hook. Self-held locks emit an
8
+ # informational "is ours" line; foreign or stale locks keep the
9
+ # current SESSION_LOCK warning.
10
+ #
11
+ # Run: bash .claude/hooks/session-reflection.d/tests/12-session-lock.test.sh
12
+ #
13
+ # test-list:
14
+ # [x] No lock file: hook is silent, exit 0
15
+ # [x] Lock present + our pid (same pgid): emits "is ours" line, NOT the warning
16
+ # [x] Lock present + foreign pid (alive, different pgid: init/PID 1): emits warning
17
+ # [x] Lock present + dead pid (ps lookup fails): emits warning
18
+ # [x] Lock present + malformed (no pid field): emits warning (fail-soft)
19
+ # [x] Lock present + readable age (mtime fresh): age label appears in warning
20
+ # [x] Lock content lines indented under "Content:" in warning output
21
+
22
+ set -u
23
+
24
+ REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
25
+ HOOK="${REPO_ROOT}/.claude/hooks/session-reflection.d/12-session-lock.sh"
26
+ PASS=0
27
+ FAIL=0
28
+ FAILED_TESTS=()
29
+
30
+ assert() {
31
+ local name="$1"
32
+ local result="$2"
33
+ if [ "$result" = "ok" ]; then
34
+ PASS=$((PASS + 1))
35
+ echo " PASS $name"
36
+ else
37
+ FAIL=$((FAIL + 1))
38
+ FAILED_TESTS+=("$name")
39
+ echo " FAIL $name — $result"
40
+ fi
41
+ }
42
+
43
+ setup_tmpdir() {
44
+ TMPDIR_TEST="$(mktemp -d)"
45
+ mkdir -p "$TMPDIR_TEST/.claude"
46
+ }
47
+
48
+ teardown_tmpdir() {
49
+ rm -rf "$TMPDIR_TEST"
50
+ }
51
+
52
+ # Run hook from $TMPDIR_TEST so its relative-path LOCK_FILE check sees
53
+ # the test-controlled .claude/SESSION_LOCK, not the real repo's one.
54
+ run_hook() {
55
+ HOOK_STDOUT=$(cd "$TMPDIR_TEST" && bash "$HOOK" 2>&1)
56
+ HOOK_EXIT=$?
57
+ }
58
+
59
+ if [ ! -x "$HOOK" ]; then
60
+ chmod +x "$HOOK"
61
+ fi
62
+
63
+ echo "Testing $HOOK"
64
+ echo "---"
65
+
66
+ # ============================================================
67
+ # 1. No lock file → silent, exit 0
68
+ # ============================================================
69
+ setup_tmpdir
70
+ run_hook
71
+ if [ "$HOOK_EXIT" = "0" ] && [ -z "$HOOK_STDOUT" ]; then
72
+ assert "no lock file → silent, exit 0" "ok"
73
+ else
74
+ assert "no lock file → silent, exit 0" "exit=$HOOK_EXIT stdout=$HOOK_STDOUT"
75
+ fi
76
+ teardown_tmpdir
77
+
78
+ # ============================================================
79
+ # 2. Lock present + our pid (same pgid) → "is ours" line, NOT the warning
80
+ # ============================================================
81
+ setup_tmpdir
82
+ NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%S+0000")
83
+ OUR_PID=$$
84
+ cat > "$TMPDIR_TEST/.claude/SESSION_LOCK" <<EOF
85
+ machine: TestMachine
86
+ timestamp: ${NOW_ISO}
87
+ pid: ${OUR_PID}
88
+ EOF
89
+ run_hook
90
+ if [ "$HOOK_EXIT" = "0" ] \
91
+ && echo "$HOOK_STDOUT" | grep -q "is ours" \
92
+ && ! echo "$HOOK_STDOUT" | grep -q "PRESENT — auto-save suppressed"; then
93
+ assert "own-pid lock → 'is ours' line, no warning" "ok"
94
+ else
95
+ assert "own-pid lock → 'is ours' line, no warning" "exit=$HOOK_EXIT stdout=$HOOK_STDOUT"
96
+ fi
97
+ teardown_tmpdir
98
+
99
+ # ============================================================
100
+ # 3. Lock present + foreign pid (alive, different pgid) → warning fires
101
+ # ============================================================
102
+ setup_tmpdir
103
+ NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%S+0000")
104
+ # init / PID 1 — guaranteed alive AND in a different pgid than this test
105
+ cat > "$TMPDIR_TEST/.claude/SESSION_LOCK" <<EOF
106
+ machine: OtherMachine
107
+ timestamp: ${NOW_ISO}
108
+ pid: 1
109
+ EOF
110
+ run_hook
111
+ if [ "$HOOK_EXIT" = "0" ] \
112
+ && echo "$HOOK_STDOUT" | grep -q "PRESENT — auto-save suppressed" \
113
+ && ! echo "$HOOK_STDOUT" | grep -q "is ours"; then
114
+ assert "foreign-pid alive → warning, no 'is ours'" "ok"
115
+ else
116
+ assert "foreign-pid alive → warning, no 'is ours'" "exit=$HOOK_EXIT stdout=$HOOK_STDOUT"
117
+ fi
118
+ teardown_tmpdir
119
+
120
+ # ============================================================
121
+ # 4. Lock present + dead pid (ps lookup fails) → warning fires
122
+ # ============================================================
123
+ setup_tmpdir
124
+ NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%S+0000")
125
+ # Pick a PID very unlikely to exist (max + 1 territory is platform-specific;
126
+ # use 999999 which exceeds default pid_max on most macOS/Linux configurations)
127
+ DEAD_PID=999999
128
+ # Confirm it's actually dead; if not, skip this assertion gracefully
129
+ if ps -p "$DEAD_PID" >/dev/null 2>&1; then
130
+ echo " SKIP dead-pid → warning (PID $DEAD_PID happens to be alive)"
131
+ else
132
+ cat > "$TMPDIR_TEST/.claude/SESSION_LOCK" <<EOF
133
+ machine: ZombieMachine
134
+ timestamp: ${NOW_ISO}
135
+ pid: ${DEAD_PID}
136
+ EOF
137
+ run_hook
138
+ if [ "$HOOK_EXIT" = "0" ] \
139
+ && echo "$HOOK_STDOUT" | grep -q "PRESENT — auto-save suppressed" \
140
+ && ! echo "$HOOK_STDOUT" | grep -q "is ours"; then
141
+ assert "dead-pid lock → warning, no 'is ours'" "ok"
142
+ else
143
+ assert "dead-pid lock → warning, no 'is ours'" "exit=$HOOK_EXIT stdout=$HOOK_STDOUT"
144
+ fi
145
+ fi
146
+ teardown_tmpdir
147
+
148
+ # ============================================================
149
+ # 5. Lock present + malformed (no pid field) → warning fires (fail-soft)
150
+ # ============================================================
151
+ setup_tmpdir
152
+ NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%S+0000")
153
+ cat > "$TMPDIR_TEST/.claude/SESSION_LOCK" <<EOF
154
+ machine: TestMachine
155
+ timestamp: ${NOW_ISO}
156
+ EOF
157
+ run_hook
158
+ if [ "$HOOK_EXIT" = "0" ] \
159
+ && echo "$HOOK_STDOUT" | grep -q "PRESENT — auto-save suppressed" \
160
+ && ! echo "$HOOK_STDOUT" | grep -q "is ours"; then
161
+ assert "malformed lock (no pid) → warning, fail-soft" "ok"
162
+ else
163
+ assert "malformed lock (no pid) → warning, fail-soft" "exit=$HOOK_EXIT stdout=$HOOK_STDOUT"
164
+ fi
165
+ teardown_tmpdir
166
+
167
+ # ============================================================
168
+ # 6. Lock present + foreign pid → age label appears in warning
169
+ # ============================================================
170
+ setup_tmpdir
171
+ NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%S+0000")
172
+ cat > "$TMPDIR_TEST/.claude/SESSION_LOCK" <<EOF
173
+ machine: OtherMachine
174
+ timestamp: ${NOW_ISO}
175
+ pid: 1
176
+ EOF
177
+ run_hook
178
+ if echo "$HOOK_STDOUT" | grep -qE "Age:[[:space:]]+[0-9]+(s|m|h)"; then
179
+ assert "age label appears (seconds/minutes/hours)" "ok"
180
+ else
181
+ assert "age label appears (seconds/minutes/hours)" "stdout=$HOOK_STDOUT"
182
+ fi
183
+ teardown_tmpdir
184
+
185
+ # ============================================================
186
+ # 7. Lock present + foreign pid → content block indented in warning
187
+ # ============================================================
188
+ setup_tmpdir
189
+ NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%S+0000")
190
+ cat > "$TMPDIR_TEST/.claude/SESSION_LOCK" <<EOF
191
+ machine: OtherMachine
192
+ timestamp: ${NOW_ISO}
193
+ pid: 1
194
+ EOF
195
+ run_hook
196
+ if echo "$HOOK_STDOUT" | grep -q "Content:" \
197
+ && echo "$HOOK_STDOUT" | grep -q " machine: OtherMachine"; then
198
+ assert "content lines indented under 'Content:'" "ok"
199
+ else
200
+ assert "content lines indented under 'Content:'" "stdout=$HOOK_STDOUT"
201
+ fi
202
+ teardown_tmpdir
203
+
204
+ # ============================================================
205
+ # Summary
206
+ # ============================================================
207
+ echo "---"
208
+ echo "Results: $PASS passed, $FAIL failed"
209
+ if [ "$FAIL" -gt 0 ]; then
210
+ echo "Failed:"
211
+ for t in "${FAILED_TESTS[@]}"; do
212
+ echo " - $t"
213
+ done
214
+ exit 1
215
+ fi
216
+ exit 0
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # tier: standard
3
+ # Minimal Tier 1 sibling test for 15-substrate-config-validate.sh.
4
+ # Coordinator-level integration coverage lives at:
5
+ # .claude/hooks/tests/session-reflection-coordinator.test.sh
6
+ # (fixture 4 pins the exit-1 → return-2 abort semantic for this module).
7
+ # This stub asserts the module file exists and is syntactically valid.
8
+
9
+ set -u
10
+
11
+ REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
12
+ MODULE="${REPO_ROOT}/.claude/hooks/session-reflection.d/15-substrate-config-validate.sh"
13
+
14
+ PASS=0
15
+ FAIL=0
16
+
17
+ assert() {
18
+ if [ "$2" = "ok" ]; then PASS=$((PASS+1)); echo " PASS $1"; else FAIL=$((FAIL+1)); echo " FAIL $1 — $2"; fi
19
+ }
20
+
21
+ echo "Testing $MODULE (minimal stub — full coverage via coordinator test)"
22
+
23
+ if [ -f "$MODULE" ]; then
24
+ assert "module file present" "ok"
25
+ else
26
+ assert "module file present" "not found at $MODULE"
27
+ fi
28
+
29
+ if bash -n "$MODULE" 2>/dev/null; then
30
+ assert "syntax valid" "ok"
31
+ else
32
+ assert "syntax valid" "bash -n reported syntax error"
33
+ fi
34
+
35
+ # #1157 Class B cure (2026-08-11 bet 2026-08-08b WU-2) softened the module
36
+ # from BLOCK (return 2) to WARN (return 0) on adopter drift. Prior
37
+ # characterization pinned the return-2 pattern; assertion updated to
38
+ # pin the new return-0 dual pattern. Intent preserved: module must use
39
+ # the dual return/exit shape (not raw exit), so sourced dispatcher path
40
+ # works AND standalone bash invocation works.
41
+ if grep -q "return 0 2>/dev/null || exit 0" "$MODULE"; then
42
+ assert "return-0 dual pattern preserved (#1157 Class B cure)" "ok"
43
+ else
44
+ assert "return-0 dual pattern preserved (#1157 Class B cure)" "expected 'return 0 2>/dev/null || exit 0' in module"
45
+ fi
46
+
47
+ echo ""
48
+ echo "Results: $PASS passed, $FAIL failed"
49
+ [ "$FAIL" -gt 0 ] && exit 1
50
+ exit 0