@thebassclef/lite 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/cli.cjs +240 -23
  2. package/dist/cli.js +242 -25
  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/artifact-ingestion-gate.sh +357 -0
  7. package/dist/lite/.claude/hooks/assert-verify-steering.sh +77 -0
  8. package/dist/lite/.claude/hooks/bassclef-source-config-validate.sh +215 -0
  9. package/dist/lite/.claude/hooks/bassclef-sync.sh +634 -0
  10. package/dist/lite/.claude/hooks/compound-noun-scrub.sh +292 -0
  11. package/dist/lite/.claude/hooks/kiss-expansion-inject.sh +69 -0
  12. package/dist/lite/.claude/hooks/longrun-prep-compounding-sequence-check.sh +492 -0
  13. package/dist/lite/.claude/hooks/plain-english-steering.sh +156 -0
  14. package/dist/lite/.claude/hooks/post-skill-friction-check.sh +177 -0
  15. package/dist/lite/.claude/hooks/post-skill-telemetry.sh +62 -0
  16. package/dist/lite/.claude/hooks/pre-build-gate.sh +511 -0
  17. package/dist/lite/.claude/hooks/pre-commit-gate.sh +451 -0
  18. package/dist/lite/.claude/hooks/session-end.sh +433 -0
  19. package/dist/lite/.claude/hooks/session-reflection.sh +303 -0
  20. package/dist/lite/.claude/hooks/skill-body-grade-gate.sh +219 -0
  21. package/dist/lite/.claude/hooks/skill-body-intent-drift.sh +107 -0
  22. package/dist/lite/.claude/hooks/state-validate.sh +271 -0
  23. package/dist/lite/.claude/hooks/substrate-clarity-gate.sh +1110 -0
  24. package/dist/lite/.claude/hooks/temperance-gate.sh +147 -0
  25. package/dist/lite/.claude/hooks/testing-tier-enforce.sh +233 -0
  26. package/dist/lite/.claude/hooks/turn-prose-grade-measure.sh +219 -0
  27. package/dist/lite/.claude/hooks/turn-prose-kiss-check.sh +463 -0
  28. package/dist/lite/.claude/hooks/vocabulary-migration-check.sh +171 -0
  29. package/dist/lite/.claude/hooks/whereami-utc-gate.sh +142 -0
  30. package/dist/lite/CLAUDE.md +2 -2
  31. package/dist/lite/whereami.md +1 -1
  32. package/package.json +1 -1
@@ -0,0 +1,492 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # install-class: dual
4
+ # longrun-prep-compounding-sequence-check.sh — Stop hook that fires when
5
+ # state/markers/turn-prose-surface/longrun_prep_proposal.marker is
6
+ # present. Scans the latest assistant message for the 5 prescribed
7
+ # column headers from /longrun SKILL Step 1.5 (compounding-sequence
8
+ # analysis). Default V2 = strict (exit 2 on missing axes); explicit
9
+ # advisory toggle preserved for backward compat.
10
+ #
11
+ # Per bassclef-upstream#426 V2 flip. Calibration cycle (one /longrun
12
+ # prep after V1 ship) demonstrated the visibility gap: V1 advisory
13
+ # emitted to stderr; agent could not see its own advisory, missed
14
+ # 2-of-5 column at next prep. Flip closes the loop per ADR-035
15
+ # substrate-as-system.
16
+ #
17
+ # Per bet 28a WU-5d (V1 ship) + bet 30b extension (V2 flip with
18
+ # tonight's prep as eat-own-dogfood evidence).
19
+ #
20
+ # Sister hook pattern to turn-prose-kiss-check.sh (Stop hook scanning
21
+ # marker-gated transcript).
22
+ #
23
+ # Override per-call: SKIP_COMPOUNDING_AXIS_CHECK=1
24
+ # Backward compat: COMPOUNDING_AXIS_TOGGLE=advisory — exit 0 + finding
25
+ # COMPOUNDING_AXIS_TOGGLE=strict — exit 2 (default; explicit opt-in)
26
+ #
27
+ # Frozen stderr contract (per bassclef-upstream#1536 Hyrum finding):
28
+ # SECTION-EMPTY guard emits exactly:
29
+ # "🛑 COMPOUNDING-AXIS — BLOCKED (SECTION-EMPTY per-shape parse boundary; V2 strict default)"
30
+ # "🛑 COMPOUNDING-AXIS — ADVISORY (SECTION-EMPTY per-shape parse boundary; backward-compat opt-out)"
31
+ # Per-shape line prefix: " 🔍 <shape>"
32
+ # Any change to these strings updates this header comment as the observable contract.
33
+ #
34
+ # Tier 0 strict TDD: .claude/hooks/tests/longrun-prep-compounding-sequence-check.test.sh
35
+
36
+ set +e
37
+
38
+ INPUT=$(cat)
39
+ TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
40
+ CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
41
+
42
+ # Override
43
+ if [ "${SKIP_COMPOUNDING_AXIS_CHECK:-0}" = "1" ]; then
44
+ exit 0
45
+ fi
46
+
47
+ # Marker gate — only fire when /longrun prep proposal is being authored
48
+ MARKER_PATH="$CWD/state/markers/turn-prose-surface/longrun_prep_proposal.marker"
49
+ if [ ! -f "$MARKER_PATH" ]; then
50
+ exit 0
51
+ fi
52
+
53
+ # Auto-clean gate (Cure A per ticket #1555) — if a prior Stop event already
54
+ # fired this hook cleanly (exit 0) during this prep session, skip subsequent
55
+ # firings. The consumed sibling marker signals "primary marker's job is done".
56
+ # Operator manually re-touches the primary marker for the next prep proposal.
57
+ # Closes the doom loop where post-prep status turns kept tripping this hook.
58
+ CONSUMED_MARKER="$CWD/state/markers/turn-prose-surface/longrun_prep_proposal.marker.consumed"
59
+ if [ -f "$CONSUMED_MARKER" ]; then
60
+ exit 0
61
+ fi
62
+
63
+ # Need transcript to scan
64
+ if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
65
+ exit 0
66
+ fi
67
+
68
+ # Extract last assistant message text (per turn-prose-kiss-check pattern)
69
+ LAST_ASSISTANT_LINE=$(grep '"type":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1)
70
+ [ -z "$LAST_ASSISTANT_LINE" ] && exit 0
71
+
72
+ MESSAGE_TEXT=$(echo "$LAST_ASSISTANT_LINE" | jq -r '.message.content[]? | select(.type == "text") | .text' 2>/dev/null)
73
+ [ -z "$MESSAGE_TEXT" ] && exit 0
74
+
75
+ # === Check compounding-sequence column headers ===
76
+ # Per /longrun SKILL Step 1.5: option table must include columns for the
77
+ # axis set. Hook accepts EITHER of two shapes during the ADR-031 grace
78
+ # window through 2026-10-31 (bassclef#1555 PR-1):
79
+ #
80
+ # 5-axis LEGACY (original, per bet 28a WU-5d):
81
+ # Compounding surface | Compounding rate | Foundation prereq |
82
+ # Inverse-dependency | Risk class
83
+ #
84
+ # 6-axis NEW (per plan doc 2026-09-09-compounding-axis-refactor):
85
+ # Deliverable | Problem | Value prop | Turns | Risk | Shipping priority
86
+ #
87
+ # Try each axis set. The hook passes if EITHER set matches. If neither
88
+ # matches, BLOCK naming the axes missing from the closest shape (fewest
89
+ # missing entries).
90
+
91
+ check_axis_any() {
92
+ # Returns 0 if any variant appears in MESSAGE_TEXT; else 1
93
+ local variant
94
+ for variant in "$@"; do
95
+ if echo "$MESSAGE_TEXT" | grep -qi "$variant"; then
96
+ return 0
97
+ fi
98
+ done
99
+ return 1
100
+ }
101
+
102
+ # --- 5-axis LEGACY set ---
103
+ FIVE_MISSING=()
104
+ check_axis_any "Compounding surface" "Compounds at" "Where the payoff shows up" \
105
+ || FIVE_MISSING+=("Compounding surface")
106
+ check_axis_any "Compounding rate" "| *Rate" "How often it fires" \
107
+ || FIVE_MISSING+=("Compounding rate")
108
+ check_axis_any "Foundation prereq" "What must be true first" \
109
+ || FIVE_MISSING+=("Foundation prereq")
110
+ check_axis_any "Inverse-dependency" "Does this teach" \
111
+ || FIVE_MISSING+=("Inverse-dependency")
112
+ check_axis_any "Risk class" "| *Risk" "What breaks if" \
113
+ || FIVE_MISSING+=("Risk class")
114
+
115
+ # --- 6-axis NEW set (bassclef#1555 PR-1) ---
116
+ SIX_MISSING=()
117
+ check_axis_any "Deliverable" "Ships when" \
118
+ || SIX_MISSING+=("Deliverable")
119
+ check_axis_any "Problem" "Pain" \
120
+ || SIX_MISSING+=("Problem")
121
+ check_axis_any "Value prop" "value proposition" \
122
+ || SIX_MISSING+=("Value prop")
123
+ check_axis_any "Turns" "Turn count" "Turn range" "Turn estimate" \
124
+ || SIX_MISSING+=("Turns")
125
+ check_axis_any "Risk" "| *Risk" "🟢" "🟡" "🔴" \
126
+ || SIX_MISSING+=("Risk")
127
+ check_axis_any "Shipping priority" "Priority:" "\bQ1\b" "\bQ2\b" "\bQ3\b" "\bQ4\b" \
128
+ || SIX_MISSING+=("Shipping priority")
129
+
130
+ # Pick the shape with fewer missing axes; if neither set is complete, that
131
+ # shape is what MISSING reports. If either set is complete (empty missing
132
+ # array), the message passes the header check.
133
+ if [ "${#FIVE_MISSING[@]}" -eq 0 ] || [ "${#SIX_MISSING[@]}" -eq 0 ]; then
134
+ MISSING=()
135
+ elif [ "${#FIVE_MISSING[@]}" -le "${#SIX_MISSING[@]}" ]; then
136
+ MISSING=("${FIVE_MISSING[@]}")
137
+ else
138
+ MISSING=("${SIX_MISSING[@]}")
139
+ fi
140
+
141
+ # === Per-shape distinct-value check (bassclef-upstream#564) ===
142
+ # When 2+ shape section headers appear (**Shape a**, ### Shape a, etc.),
143
+ # the rule postcondition per Hoare requires each shape section to render
144
+ # all 5 axes with distinct values. A single header row with axis names
145
+ # but no per-shape values is R8 pattern — label without work.
146
+ #
147
+ # Per luminary consultation: Ousterhout (deep modules; SKILL + rule +
148
+ # hook = one contract with matching depth), Hoare (strong postcondition),
149
+ # Feathers (characterization tests use tonight's prep messages), Saltzer
150
+ # & Schroeder (complete mediation; every axis cell mediates), Beck (TDD).
151
+ #
152
+ # Falsifies R8 flavor A (aggregate cells collapse axes into one).
153
+ # This check only fires when message-level axis names ARE present
154
+ # (avoids double-firing on pure message-level miss).
155
+
156
+ # === Per-WU compounding-sequence check (bassclef-upstream#597) ===
157
+ # When prep proposal contains a produces/consumes WU table with 2+ WU
158
+ # rows (multi-step bet), the message must also render per-step compounding
159
+ # axes — either via "Compounding value per step" heading OR per-step axis
160
+ # rendering that names each step with its 5 axes.
161
+ #
162
+ # Convention worked for weeks (bet-doc convention). Tonight's PR #567
163
+ # tightened Step 1.5 to require 5 distinct axes per shape and the
164
+ # attention shift crowded the per-step habit out. Per ADR-035
165
+ # substrate-as-system tenet, mechanize the convention so it survives
166
+ # sibling-discipline tightening.
167
+ #
168
+ # Fires only when the message passes the shape-level checks first —
169
+ # avoids double-firing when the message-level column check already
170
+ # BLOCKED.
171
+
172
+ if [ "${#MISSING[@]}" -eq 0 ]; then
173
+ # Count WU rows in produces/consumes tables. WU row markers include:
174
+ # | **1** | ... or | **WU-1** | ... or **WU-1** in bullets.
175
+ # Use a permissive regex to catch canonical bet-doc formats.
176
+ WU_ROW_COUNT=$(echo "$MESSAGE_TEXT" | grep -cE '^\|[[:space:]]*\*\*(WU-)?[0-9]+\*\*' 2>/dev/null || echo 0)
177
+ # Sanitize (grep -c can print trailing content on some macOS bash)
178
+ WU_ROW_COUNT=$(echo "$WU_ROW_COUNT" | tr -cd '0-9')
179
+ [ -z "$WU_ROW_COUNT" ] && WU_ROW_COUNT=0
180
+
181
+ if [ "$WU_ROW_COUNT" -ge 2 ]; then
182
+ # Look for either the canonical section heading OR per-step axis rendering.
183
+ HAS_PER_WU_HEADING=$(echo "$MESSAGE_TEXT" | grep -ciE '(compounding value per step|per-step compounding|compounding sequence per step|compounding.axis.*wu)' 2>/dev/null | tr -cd '0-9')
184
+ [ -z "$HAS_PER_WU_HEADING" ] && HAS_PER_WU_HEADING=0
185
+
186
+ if [ "$HAS_PER_WU_HEADING" -eq 0 ]; then
187
+ TOGGLE="${COMPOUNDING_AXIS_TOGGLE:-strict}"
188
+ if [ "$TOGGLE" = "advisory" ]; then
189
+ LABEL="ADVISORY (per-step; backward-compat opt-out)"
190
+ else
191
+ LABEL="BLOCKED (per-step compounding columns missing; V1 strict default)"
192
+ fi
193
+
194
+ echo "" >&2
195
+ echo "🛑 COMPOUNDING-AXIS — $LABEL" >&2
196
+ echo "" >&2
197
+ echo " /longrun prep proposal has $WU_ROW_COUNT WU rows in the" >&2
198
+ echo " produces/consumes table but no per-step compounding-sequence" >&2
199
+ echo " rendering (R8 pattern — convention displaced by sibling" >&2
200
+ echo " tightening in PR #567)." >&2
201
+ echo "" >&2
202
+ echo " Per bassclef-upstream#597 + ADR-035 substrate-as-system:" >&2
203
+ echo " each step section must carry the 5 axes (plain-English preferred):" >&2
204
+ echo " - Where the payoff shows up (was: Compounding surface)" >&2
205
+ echo " - How often it fires (was: Compounding rate)" >&2
206
+ echo " - What must be true first (was: Foundation prereq)" >&2
207
+ echo " - Does this teach a shape later work reuses (was: Inverse-dependency)" >&2
208
+ echo " - What breaks if we ship this half-done (was: Risk class)" >&2
209
+ echo " Both label forms accepted through 2026-10-31 per ADR-040 D4." >&2
210
+ echo "" >&2
211
+ echo " Cure: add a '## Compounding value per step' section OR render" >&2
212
+ echo " each step with its 5 axes as bullets. See" >&2
213
+ echo " .claude/rules/compounding-sequence-fresh-analysis.md for shape." >&2
214
+ echo "" >&2
215
+ echo " Override: SKIP_COMPOUNDING_AXIS_CHECK=1 (logged)" >&2
216
+ echo " Backward compat: COMPOUNDING_AXIS_TOGGLE=advisory (exit 0)" >&2
217
+ echo "" >&2
218
+
219
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
220
+ if [ -f "$SCRIPT_DIR/trace-helper.sh" ]; then
221
+ # shellcheck disable=SC1091
222
+ source "$SCRIPT_DIR/trace-helper.sh"
223
+ trace_log "compounding-sequence-check" "$LABEL per-step-missing count=$WU_ROW_COUNT"
224
+ fi
225
+
226
+ [ "$TOGGLE" = "advisory" ] && exit 0
227
+ exit 2
228
+ fi
229
+ fi
230
+
231
+ # Per bassclef-upstream#1193 — SKILL rename #830 replaced "Shape a-c"
232
+ # with "Option a-e". Regex accepts both during the grace window
233
+ # through 2026-10-31 per .claude/rules/we-dont-break-adopters.md.
234
+ # After that date, only "Option [a-z]" matches.
235
+ SHAPES=$(echo "$MESSAGE_TEXT" | grep -Eo '(Shape [a-c]|Option [a-z])\b' | sort -u)
236
+
237
+ # Per bassclef-upstream#1376 — accept per-recommended-only shape.
238
+ # When the message opens with `**Recommend:** Option X` (or Shape X),
239
+ # the reader lands the pick in the first 15 lines. Non-recommended
240
+ # options carry their axes as one-liner cells in the scan-table row,
241
+ # not as full sections. The hook trusts the Recommend line: it treats
242
+ # the labeled option as REQUIRED (section + 5 axes) and treats every
243
+ # other option as OPTIONAL (a missing section stops SECTION-EMPTY from
244
+ # firing on that option).
245
+ #
246
+ # Per Tufte data-ink (drop fields that do not change the pick) +
247
+ # Prater progressive disclosure (default minimum; expand on ask).
248
+ # Sister #959 lands the SKILL body trim; this hook amend unblocks it.
249
+ RECOMMENDED_OPTION=$(echo "$MESSAGE_TEXT" | grep -Eo '\*\*Recommend[^*]*\*\*[[:space:]]*(Shape [a-c]|Option [a-z])' | head -1 | grep -Eo '(Shape [a-c]|Option [a-z])' | head -1)
250
+ if [ -z "$SHAPES" ]; then
251
+ SHAPE_COUNT=0
252
+ else
253
+ SHAPE_COUNT=$(echo "$SHAPES" | wc -l | tr -d ' ')
254
+ fi
255
+
256
+ # Per-shape check fires when 2+ options AND for recommended-only shape
257
+ # even with 1 option (bassclef-upstream#1376) — the recommended option's
258
+ # section must carry all 5 axes with distinct values.
259
+ if [ "$SHAPE_COUNT" -ge 2 ] || [ -n "$RECOMMENDED_OPTION" ]; then
260
+ PER_SHAPE_MISSING=""
261
+ SECTION_EMPTY_SHAPES=""
262
+ while IFS= read -r shape; do
263
+ [ -z "$shape" ] && continue
264
+ # Section boundaries require a heading anchor at line start
265
+ # (H2+ heading like `## Option a`, `### Option a`, `#### Option a`,
266
+ # or bold prefix `**Option a`) so mid-body cross-references like
267
+ # "same end-state as Option b" do not trigger a false section
268
+ # boundary. Cure per bassclef-upstream#1421 Option (a). H2+ range
269
+ # per bassclef-upstream#1532 — options nested under a container H3
270
+ # anchor (e.g., `### Compounding value per option`) resolve cleanly
271
+ # regardless of nesting depth. H1 excluded by design.
272
+ SECTION=$(echo "$MESSAGE_TEXT" | awk -v want="$shape" '
273
+ BEGIN { in_section=0 }
274
+ /^(##+ |\*\*)(Shape [a-c]|Option [a-z])/ {
275
+ if ($0 ~ want) { in_section=1; print; next }
276
+ else if (in_section) { in_section=0 }
277
+ }
278
+ in_section { print }
279
+ ')
280
+
281
+ # Hoare precondition guard (bassclef-upstream#1536) — when shape name
282
+ # matches via grep-Eo but SECTION returns empty, the heading boundary
283
+ # is missing. Fail-safe per Saltzer-Schroeder: capture + emit distinct
284
+ # diagnostic under the existing banner + exit 2. Advisory toggle preserved.
285
+ #
286
+ # Recommended-only shape carve-out (bassclef-upstream#1376) — when the
287
+ # message declares a recommended option via **Recommend:** ..., non-
288
+ # recommended options may legitimately lack a section (they carry axes
289
+ # as scan-table cells only). Skip SECTION-EMPTY for those; still fire
290
+ # SECTION-EMPTY if the RECOMMENDED option has no section.
291
+ if [ -z "$SECTION" ]; then
292
+ if [ -n "$RECOMMENDED_OPTION" ] && [ "$shape" != "$RECOMMENDED_OPTION" ]; then
293
+ continue
294
+ fi
295
+ SECTION_EMPTY_SHAPES="${SECTION_EMPTY_SHAPES} 🔍 ${shape}\n"
296
+ continue
297
+ fi
298
+
299
+ # Recommended-only shape carve-out (bassclef-upstream#1376) —
300
+ # only the recommended option must carry all 5 axes distinctly in
301
+ # per-recommended-only mode. Non-recommended options with a partial
302
+ # section skip the per-axis check.
303
+ if [ -n "$RECOMMENDED_OPTION" ] && [ "$shape" != "$RECOMMENDED_OPTION" ]; then
304
+ continue
305
+ fi
306
+
307
+ # Grace window per bet 2026-07-16b WU-2 (#734) + bassclef#1555 PR-1 —
308
+ # per-shape check accepts EITHER 5-axis LEGACY or 6-axis NEW shape.
309
+ # Determine which axis set the section uses, then verify all axes of
310
+ # that set are present. If neither set is complete for this section,
311
+ # report the set with fewer missing axes.
312
+
313
+ # Try 5-axis set on this section
314
+ FIVE_SECTION_MISSING=()
315
+ echo "$SECTION" | grep -qiE "Compounding surface|Compounds at|Where the payoff shows up" \
316
+ || FIVE_SECTION_MISSING+=("Compounding surface")
317
+ echo "$SECTION" | grep -qiE "Compounding rate|\| *Rate|How often it fires" \
318
+ || FIVE_SECTION_MISSING+=("Compounding rate")
319
+ echo "$SECTION" | grep -qiE "Foundation prereq|What must be true first" \
320
+ || FIVE_SECTION_MISSING+=("Foundation prereq")
321
+ echo "$SECTION" | grep -qiE "Inverse-dependency|Does this teach" \
322
+ || FIVE_SECTION_MISSING+=("Inverse-dependency")
323
+ echo "$SECTION" | grep -qiE "Risk class|\| *Risk|What breaks if" \
324
+ || FIVE_SECTION_MISSING+=("Risk class")
325
+
326
+ # Try 6-axis set on this section
327
+ SIX_SECTION_MISSING=()
328
+ echo "$SECTION" | grep -qiE "Deliverable|Ships when" \
329
+ || SIX_SECTION_MISSING+=("Deliverable")
330
+ echo "$SECTION" | grep -qiE "Problem|Pain" \
331
+ || SIX_SECTION_MISSING+=("Problem")
332
+ echo "$SECTION" | grep -qiE "Value prop|value proposition" \
333
+ || SIX_SECTION_MISSING+=("Value prop")
334
+ echo "$SECTION" | grep -qiE "Turns|Turn count|Turn range|Turn estimate" \
335
+ || SIX_SECTION_MISSING+=("Turns")
336
+ echo "$SECTION" | grep -qiE "Risk|🟢|🟡|🔴" \
337
+ || SIX_SECTION_MISSING+=("Risk")
338
+ echo "$SECTION" | grep -qiE "Shipping priority|Priority:|\bQ1\b|\bQ2\b|\bQ3\b|\bQ4\b" \
339
+ || SIX_SECTION_MISSING+=("Shipping priority")
340
+
341
+ # Pick the shape with fewer missing per this section. If either set is
342
+ # complete for this section, that section passes.
343
+ if [ "${#FIVE_SECTION_MISSING[@]}" -eq 0 ] || [ "${#SIX_SECTION_MISSING[@]}" -eq 0 ]; then
344
+ : # section passes — one of the two shapes is complete
345
+ elif [ "${#FIVE_SECTION_MISSING[@]}" -le "${#SIX_SECTION_MISSING[@]}" ]; then
346
+ for axis_name in "${FIVE_SECTION_MISSING[@]}"; do
347
+ PER_SHAPE_MISSING="${PER_SHAPE_MISSING} - ${shape}: missing '${axis_name}'\n"
348
+ done
349
+ else
350
+ for axis_name in "${SIX_SECTION_MISSING[@]}"; do
351
+ PER_SHAPE_MISSING="${PER_SHAPE_MISSING} - ${shape}: missing '${axis_name}'\n"
352
+ done
353
+ fi
354
+ done <<< "$SHAPES"
355
+
356
+ # Hoare precondition guard emit — SECTION-EMPTY takes precedence over
357
+ # "distinct values missing" because a failed parse is a different
358
+ # failure class than missing content. Per bassclef-upstream#1536.
359
+ if [ -n "$SECTION_EMPTY_SHAPES" ]; then
360
+ TOGGLE="${COMPOUNDING_AXIS_TOGGLE:-strict}"
361
+ if [ "$TOGGLE" = "advisory" ]; then
362
+ LABEL="ADVISORY (SECTION-EMPTY per-shape parse boundary; backward-compat opt-out)"
363
+ else
364
+ LABEL="BLOCKED (SECTION-EMPTY per-shape parse boundary; V2 strict default)"
365
+ fi
366
+ echo "" >&2
367
+ echo "🛑 COMPOUNDING-AXIS — $LABEL" >&2
368
+ echo "" >&2
369
+ echo " /longrun prep proposal references shapes but SECTION extraction" >&2
370
+ echo " returned empty. Precondition failure — heading boundary not found." >&2
371
+ echo " Per Hoare precondition contract (bassclef-upstream#1536)." >&2
372
+ echo "" >&2
373
+ echo " SECTION-EMPTY shapes:" >&2
374
+ printf "$SECTION_EMPTY_SHAPES" >&2
375
+ echo "" >&2
376
+ echo " Cure: give each shape a heading anchor at H2 through H6" >&2
377
+ echo " (e.g., '#### Option a') or bold prefix ('**Option a')." >&2
378
+ echo " See fixture 2026-09-08-step-1-4c-worked-example/ for the shape." >&2
379
+ echo "" >&2
380
+ echo " Override: SKIP_COMPOUNDING_AXIS_CHECK=1 (logged)" >&2
381
+ echo " Backward compat: COMPOUNDING_AXIS_TOGGLE=advisory (exit 0)" >&2
382
+ echo "" >&2
383
+
384
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
385
+ if [ -f "$SCRIPT_DIR/trace-helper.sh" ]; then
386
+ # shellcheck disable=SC1091
387
+ source "$SCRIPT_DIR/trace-helper.sh"
388
+ trace_log "compounding-sequence-check" "$LABEL section-empty"
389
+ fi
390
+
391
+ [ "$TOGGLE" = "advisory" ] && exit 0
392
+ exit 2
393
+ fi
394
+
395
+ if [ -n "$PER_SHAPE_MISSING" ]; then
396
+ TOGGLE="${COMPOUNDING_AXIS_TOGGLE:-strict}"
397
+ if [ "$TOGGLE" = "advisory" ]; then
398
+ LABEL="ADVISORY (per-shape; backward-compat opt-out)"
399
+ else
400
+ LABEL="BLOCKED (per-shape distinct values missing; V2 strict default)"
401
+ fi
402
+
403
+ echo "" >&2
404
+ echo "🛑 COMPOUNDING-AXIS — $LABEL" >&2
405
+ echo "" >&2
406
+ echo " /longrun prep proposal has $SHAPE_COUNT shape sections but" >&2
407
+ echo " the axes do not render distinctly per shape (R8 pattern)." >&2
408
+ echo "" >&2
409
+ echo " Per bassclef-upstream#564 + luminary consultation" >&2
410
+ echo " (Ousterhout/Hoare/Feathers/Saltzer-Schroeder/Beck):" >&2
411
+ echo " each shape section must carry all 5 axes as distinct values." >&2
412
+ echo "" >&2
413
+ echo " Missing per-shape values:" >&2
414
+ printf "$PER_SHAPE_MISSING" >&2
415
+ echo "" >&2
416
+ echo " Cure: for each shape (a, b, c...), render all 5 axes with" >&2
417
+ echo " their distinct values under that shape's section." >&2
418
+ echo "" >&2
419
+ echo " Override: SKIP_COMPOUNDING_AXIS_CHECK=1 (logged)" >&2
420
+ echo " Backward compat: COMPOUNDING_AXIS_TOGGLE=advisory (exit 0)" >&2
421
+ echo "" >&2
422
+
423
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
424
+ if [ -f "$SCRIPT_DIR/trace-helper.sh" ]; then
425
+ # shellcheck disable=SC1091
426
+ source "$SCRIPT_DIR/trace-helper.sh"
427
+ trace_log "compounding-sequence-check" "$LABEL per-shape-missing"
428
+ fi
429
+
430
+ [ "$TOGGLE" = "advisory" ] && exit 0
431
+ exit 2
432
+ fi
433
+ fi
434
+ # True-clean pass — all message-level axes present, per-WU + per-shape
435
+ # checks passed. Write consumed sibling marker per Cure A (ticket #1555)
436
+ # so subsequent Stop events skip the scan for this prep session.
437
+ touch "$CONSUMED_MARKER" 2>/dev/null || true
438
+ exit 0
439
+ fi
440
+
441
+ # === Emit finding ===
442
+ # Default V2 = strict. Explicit "advisory" toggle preserves backward compat.
443
+ TOGGLE="${COMPOUNDING_AXIS_TOGGLE:-strict}"
444
+
445
+ if [ "$TOGGLE" = "advisory" ]; then
446
+ LABEL="ADVISORY (backward-compat opt-out)"
447
+ else
448
+ LABEL="BLOCKED (V2 strict default)"
449
+ fi
450
+
451
+ echo "" >&2
452
+ echo "🛑 COMPOUNDING-AXIS — $LABEL" >&2
453
+ echo "" >&2
454
+ echo " /longrun prep proposal detected (marker present)." >&2
455
+ echo " Per /longrun SKILL Step 1.5 + bet 28a WU-5d:" >&2
456
+ echo " shape proposal table must include compounding-sequence columns." >&2
457
+ echo "" >&2
458
+ echo " Missing axes in latest assistant message (plain-English preferred):" >&2
459
+ for col in "${MISSING[@]}"; do
460
+ case "$col" in
461
+ "Compounding surface") echo " - Where the payoff shows up (was: Compounding surface)" >&2 ;;
462
+ "Compounding rate") echo " - How often it fires (was: Compounding rate)" >&2 ;;
463
+ "Foundation prereq") echo " - What must be true first (was: Foundation prereq)" >&2 ;;
464
+ "Inverse-dependency") echo " - Does this teach a shape later work reuses (was: Inverse-dependency)" >&2 ;;
465
+ "Risk class") echo " - What breaks if we ship this half-done (was: Risk class)" >&2 ;;
466
+ *) echo " - $col" >&2 ;;
467
+ esac
468
+ done
469
+ echo "" >&2
470
+ echo " Cure: add these axes to the option table before operator picks." >&2
471
+ echo " Each option row carries a value per axis. Both label forms accepted" >&2
472
+ echo " through 2026-10-31 per ADR-040 D4." >&2
473
+ echo "" >&2
474
+ echo " Override: SKIP_COMPOUNDING_AXIS_CHECK=1 (logged via trace)" >&2
475
+ echo " Backward compat: COMPOUNDING_AXIS_TOGGLE=advisory (exit 0)" >&2
476
+ echo "" >&2
477
+
478
+ # Trace log
479
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
480
+ if [ -f "$SCRIPT_DIR/trace-helper.sh" ]; then
481
+ # shellcheck disable=SC1091
482
+ source "$SCRIPT_DIR/trace-helper.sh"
483
+ trace_log "compounding-sequence-check" "$LABEL missing=${MISSING[*]}"
484
+ fi
485
+
486
+ # Advisory mode (backward compat): exit 0
487
+ if [ "$TOGGLE" = "advisory" ]; then
488
+ exit 0
489
+ fi
490
+
491
+ # Strict mode (default V2): exit 2 to block the stop
492
+ exit 2
@@ -0,0 +1,156 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # install-class: dual
4
+ # plain-english-steering.sh — UserPromptSubmit hook that injects
5
+ # additionalContext steering the agent toward grade-10 plain English
6
+ # at source. Per bassclef-upstream#443 + bet 28a WU-5c.
7
+ #
8
+ # Adds Layer 0 to the 5-layer prose-discipline defense
9
+ # (per .claude/rules/operator-facing-prose.md):
10
+ #
11
+ # Layer 0 (this hook, UserPromptSubmit) — steer at source
12
+ # Layer 1 (substrate-clarity-gate, PreToolUse Edit on substrate paths)
13
+ # Layer 2 (pre-gh-pr-body-scrub, PreToolUse Bash for gh pr create/edit)
14
+ # Layer 3 (pre-gh-issue-body-scrub + pre-git-commit-msg-scrub)
15
+ # Layer 4 (turn-prose-kiss-check, Stop hook scanning assistant message)
16
+ # Layer 5 (CI bassclef-pr-ci.yml jobs as backstop)
17
+ #
18
+ # Override per-call: SKIP_PLAIN_ENGLISH_STEERING=1
19
+ #
20
+ # Tier 0 strict TDD: .claude/hooks/tests/plain-english-steering.test.sh
21
+
22
+ set +e
23
+
24
+ # === Source install-class-aware lib (per kiss cure pattern) ===
25
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ for _candidate in \
27
+ "${SCRIPT_DIR}/../../lib/hook-inject.sh" \
28
+ "${HOME_OVERRIDE:-${HOME:-/}}/lib/hook-inject.sh"; do
29
+ if [ -f "$_candidate" ]; then
30
+ # shellcheck disable=SC1090
31
+ source "$_candidate"
32
+ break
33
+ fi
34
+ done
35
+
36
+ # Defensive: if lib not loaded, exit silent
37
+ if ! declare -f emit_additional_context >/dev/null 2>&1; then
38
+ exit 0
39
+ fi
40
+
41
+ # Override
42
+ if [ "${SKIP_PLAIN_ENGLISH_STEERING:-0}" = "1" ]; then
43
+ exit 0
44
+ fi
45
+
46
+ # === Read last-turn grade marker (if present) ===
47
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
48
+ GRADE_MARKER="${REPO_ROOT}/state/markers/turn-grade/last-turn.json"
49
+ FLAGS_MARKER="${REPO_ROOT}/state/markers/turn-prose-flags/last-turn.json"
50
+ GRADE_LINE=""
51
+ FALLBACK_WARNING=""
52
+ FLAGS_LINE=""
53
+
54
+ if [ -f "$GRADE_MARKER" ]; then
55
+ LAST_GRADE=$(jq -r '.grade // ""' "$GRADE_MARKER" 2>/dev/null)
56
+ LAST_WORDS=$(jq -r '.words // ""' "$GRADE_MARKER" 2>/dev/null)
57
+ SOURCE=$(jq -r '.source // "UNKNOWN"' "$GRADE_MARKER" 2>/dev/null)
58
+ if [ -n "$LAST_GRADE" ] && [ "$LAST_GRADE" != "null" ]; then
59
+ # Classify per Norman + Ogilvy guidance (ticket #935 target shift):
60
+ # - Aim grade 8 for 80%+ of prose (was grade 8-9 pre-#935)
61
+ # - Grade 10 is the ceiling for simple technical sentences
62
+ # - Sentences above 10 need EXPLICIT justification
63
+ GRADE_STATUS=$(python3 -c "
64
+ g = float('$LAST_GRADE')
65
+ if g <= 8: print('ON TARGET (grade 8 or below)')
66
+ elif g <= 9: print('SLIGHTLY ABOVE (target grade 8; ceiling 10) — trim one clause or drop a compound noun')
67
+ elif g <= 10: print('AT THE GRADE-10 CEILING — tighten sentences and drop compound nouns to land at 8')
68
+ else: print('ABOVE THE GRADE-10 CEILING — rewrite in short SVO. Any sentence above 10 needs a clear reason grades 8-10 could not say it')
69
+ " 2>/dev/null)
70
+
71
+ # Ship 1 #1312: extract top 3 offender sentences that broke grade 10.
72
+ # Named offenders replace aggregate score as actionable signal.
73
+ OFFENDERS_LINE=""
74
+ OFFENDERS_COUNT=$(jq -r '.top_offenders | length' "$GRADE_MARKER" 2>/dev/null)
75
+ if [ -n "$OFFENDERS_COUNT" ] && [ "$OFFENDERS_COUNT" != "null" ] && [ "$OFFENDERS_COUNT" -gt 0 ] 2>/dev/null; then
76
+ OFFENDERS_BULLETS=$(jq -r '.top_offenders[] | "- Grade \(.grade), \(.words) words: \"\(.preview)\""' "$GRADE_MARKER" 2>/dev/null)
77
+ OFFENDERS_LINE="
78
+
79
+ **Top ${OFFENDERS_COUNT} sentence(s) from LAST turn above grade 10 — rewrite these first this turn:**
80
+ ${OFFENDERS_BULLETS}"
81
+ fi
82
+
83
+ GRADE_LINE="
84
+
85
+ **From your LAST turn (not this one): grade ${LAST_GRADE} (${LAST_WORDS} words). Target: grade 8 for 80%+ of prose. Ceiling: 10 for simple technical sentences. ${GRADE_STATUS}. THIS turn is not measured yet — self-check each sentence before I ship it.** (Per #1044 Option B: post-turn measurement + pre-turn injection means the number I see is one turn old by design.)${OFFENDERS_LINE}"
86
+ if [ "$SOURCE" = "FALLBACK" ]; then
87
+ FALLBACK_WARNING="
88
+
89
+ **textstat not installed; grade uses the hand-rolled fallback. Numbers are approximate. Run \`pip install textstat\` for the sharper score.**"
90
+ fi
91
+ fi
92
+ fi
93
+
94
+ # === Read rewrite-check flags marker (ticket #935 extension) ===
95
+ if [ -f "$FLAGS_MARKER" ]; then
96
+ FL_WORDS=$(jq -r '.total_words // 0' "$FLAGS_MARKER" 2>/dev/null)
97
+ FL_LONG=$(jq -r '.long_sentences // 0' "$FLAGS_MARKER" 2>/dev/null)
98
+ FL_PASSIVE=$(jq -r '.passive_voice // 0' "$FLAGS_MARKER" 2>/dev/null)
99
+ FL_HYPHEN=$(jq -r '.hyphen_words // 0' "$FLAGS_MARKER" 2>/dev/null)
100
+ # Only inject the flags line when something caught the writer's attention
101
+ if [ "$FL_LONG" != "0" ] || [ "$FL_PASSIVE" != "0" ] || [ "$FL_HYPHEN" != "0" ]; then
102
+ FLAGS_LINE="
103
+
104
+ **From your LAST turn (not this one) — hit counts.** Long sentences past 25 words: ${FL_LONG}. Passive voice: ${FL_PASSIVE}. Hyphen words: ${FL_HYPHEN}. THIS turn is not measured yet — I check each sentence before I ship it."
105
+ fi
106
+ fi
107
+
108
+ # === Emit steering content ===
109
+ # Extension per ticket #935 — first-person Ogilvy compose tests
110
+ # grounded in standards/ogilvy-writing-rules.md (from #934 PR #938)
111
+ STEERING_CONTENT="**Plain English for anything the operator or an adopter reads:**
112
+
113
+ For turn output, PR body, issue body, session log, journal entry, and skill output:
114
+
115
+ - Bassclef aims for grade 8 in most prose. Grade 10 is the top limit for simple technical sentences.
116
+ - Above grade 10, give a clear reason grades 8-10 could not say it. A technical topic on its own is not a reason.
117
+ - Short sentences. Subject-verb-object.
118
+ - If I want to use a hyphen word (X-side, X-as-Y, N-layer X, X gate), I write what it does instead.
119
+ - No more than two nouns in a row without a verb between them.
120
+ - Drop words that only bassclef readers know — see standards/bassclef-internal-jargon.md.
121
+ - Keep standard tech words: API, hook, PR, schema, ORM.
122
+
123
+ **Vocabulary rename active (per ADR-040 D1; grace window through 2026-10-31):**
124
+
125
+ When I author NEW prose for the operator or an adopter, I use the new terms:
126
+
127
+ - Say **goal** not **bet**. (Historical goal IDs like \`bet 08-11a\` stay as-is.)
128
+ - Say **step** not **WU** or **workunit**. (Historical \`WU-2\` refs stay.)
129
+ - Say **in_flight_goal** not **in_flight_bet** for new whereami updates.
130
+ - Say **time budget** not **appetite** for new prose.
131
+ - Say **parent goal** and **thread** not **parent bet** and **arc** for new prose.
132
+
133
+ **Discrimination cue.** Two situations, two different rules:
134
+
135
+ 1. When I AUTHOR — writing my own sentence about work in flight or scope decisions — I use the new terms (Step-N, goal, time budget). Never introduce \`WU-N\` or \`bet\` or \`appetite\` in new prose.
136
+ 2. When I QUOTE a past goal ID or reference (\`bet 27c WU-5\`, \`bet 2026-06-20a WU-1\`) — I preserve the exact historical string verbatim inside backticks. The backticks mark the historical boundary. The reader sees \"quote\" not \"active vocabulary.\"
137
+
138
+ Rule of thumb: if I am about to type \`WU-\` in a sentence, ask — am I quoting a historical goal ID? If yes, wrap in backticks. If no, use Step-N instead.
139
+
140
+ Historical mentions (past goal IDs, closed PR bodies, archived session logs) keep the old words — read-through via \`standards/vocabulary-migration.json\`.
141
+
142
+ **Compose check this turn (per Ogilvy + Roman-Raphaelson via standards/ogilvy-writing-rules.md).**
143
+ Before I write each sentence, I ask:
144
+
145
+ - Natural voice — would a real person say this to a peer? If it sounds like corporate throat-clearing, rewrite.
146
+ - Short units — is this sentence past 25 words? If yes, split.
147
+ - Read-aloud — when I read this in my head, does it stumble? If yes, rewrite until it flows.
148
+ - Completeness — is the ONE thing the reader needs to take from this actually written down in plain language?
149
+ - Honesty — am I saying what I mean or hiding behind hedge words and euphemisms? Say it straight.
150
+ - Facts with context — for each number or fact, did I say why it matters?
151
+
152
+ Per bassclef#790 + .claude/rules/operator-facing-prose.md + .claude/rules/writing-craft-discipline.md.${GRADE_LINE}${FLAGS_LINE}${FALLBACK_WARNING}"
153
+
154
+ # Use emit_additional_context to wrap as JSON envelope
155
+ emit_additional_context "UserPromptSubmit" "$STEERING_CONTENT"
156
+ exit 0