@thebassclef/lite 0.0.1 → 0.1.0

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 (182) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +85 -4
  3. package/dist/cli.cjs +1610 -0
  4. package/dist/cli.js +1587 -0
  5. package/dist/index.cjs +4 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +4 -0
  8. package/package.json +51 -3
  9. package/substrate/.bassclef/lite-manifest.json +1794 -0
  10. package/substrate/.claude/agents/architect.md +70 -0
  11. package/substrate/.claude/agents/builder.md +114 -0
  12. package/substrate/.claude/agents/designer.md +156 -0
  13. package/substrate/.claude/agents/reviewer.md +88 -0
  14. package/substrate/.claude/hooks/artifact-ingestion-gate.sh +357 -0
  15. package/substrate/.claude/hooks/assert-verify-steering.sh +77 -0
  16. package/substrate/.claude/hooks/bassclef-source-config-validate.sh +215 -0
  17. package/substrate/.claude/hooks/bassclef-sync.sh +634 -0
  18. package/substrate/.claude/hooks/compound-noun-scrub.sh +292 -0
  19. package/substrate/.claude/hooks/kiss-expansion-inject.sh +69 -0
  20. package/substrate/.claude/hooks/plain-english-steering.sh +156 -0
  21. package/substrate/.claude/hooks/post-skill-friction-check.sh +177 -0
  22. package/substrate/.claude/hooks/post-skill-telemetry.sh +62 -0
  23. package/substrate/.claude/hooks/pre-build-gate.sh +511 -0
  24. package/substrate/.claude/hooks/pre-commit-gate.sh +452 -0
  25. package/substrate/.claude/hooks/session-end.sh +433 -0
  26. package/substrate/.claude/hooks/session-reflection.sh +303 -0
  27. package/substrate/.claude/hooks/skill-body-grade-gate.sh +219 -0
  28. package/substrate/.claude/hooks/skill-body-intent-drift.sh +107 -0
  29. package/substrate/.claude/hooks/skill-step-list-check.sh +171 -0
  30. package/substrate/.claude/hooks/state-validate.sh +271 -0
  31. package/substrate/.claude/hooks/substrate-clarity-gate.sh +1110 -0
  32. package/substrate/.claude/hooks/temperance-gate.sh +147 -0
  33. package/substrate/.claude/hooks/testing-tier-enforce.sh +233 -0
  34. package/substrate/.claude/hooks/turn-prose-grade-measure.sh +219 -0
  35. package/substrate/.claude/hooks/turn-prose-kiss-check.sh +434 -0
  36. package/substrate/.claude/hooks/vocabulary-migration-check.sh +171 -0
  37. package/substrate/.claude/hooks/whereami-utc-gate.sh +142 -0
  38. package/substrate/.claude/luminaries/alan-cooper.md +170 -0
  39. package/substrate/.claude/luminaries/alistair-cockburn.md +140 -0
  40. package/substrate/.claude/luminaries/amazon-pr-faq.md +34 -0
  41. package/substrate/.claude/luminaries/ash-maurya.md +121 -0
  42. package/substrate/.claude/luminaries/bill-buxton.md +210 -0
  43. package/substrate/.claude/luminaries/charles-sanders-peirce.md +150 -0
  44. package/substrate/.claude/luminaries/david-ogilvy.md +192 -0
  45. package/substrate/.claude/luminaries/don-norman.md +173 -0
  46. package/substrate/.claude/luminaries/edward-tufte.md +179 -0
  47. package/substrate/.claude/luminaries/eric-evans.md +160 -0
  48. package/substrate/.claude/luminaries/frederick-brooks.md +50 -0
  49. package/substrate/.claude/luminaries/gang-of-four.md +157 -0
  50. package/substrate/.claude/luminaries/jason-fried-dhh.md +46 -0
  51. package/substrate/.claude/luminaries/jesse-james-garrett.md +154 -0
  52. package/substrate/.claude/luminaries/john-ousterhout.md +94 -0
  53. package/substrate/.claude/luminaries/karl-popper.md +132 -0
  54. package/substrate/.claude/luminaries/kent-beck.md +168 -0
  55. package/substrate/.claude/luminaries/linus-torvalds.md +218 -0
  56. package/substrate/.claude/luminaries/martin-fowler.md +164 -0
  57. package/substrate/.claude/luminaries/michael-feathers.md +125 -0
  58. package/substrate/.claude/luminaries/michael-nygard.md +118 -0
  59. package/substrate/.claude/luminaries/robert-c-martin.md +164 -0
  60. package/substrate/.claude/luminaries/saltzer-schroeder.md +194 -0
  61. package/substrate/.claude/luminaries/sophia-prater.md +193 -0
  62. package/substrate/.claude/luminaries/stephen-toulmin.md +131 -0
  63. package/substrate/.claude/rules/artifact-ingestion.md +179 -0
  64. package/substrate/.claude/rules/assert-only-after-verify.md +137 -0
  65. package/substrate/.claude/rules/blocked-items.md +146 -0
  66. package/substrate/.claude/rules/branching.md +28 -0
  67. package/substrate/.claude/rules/commit-conventions.md +22 -0
  68. package/substrate/.claude/rules/context-engineering.md +202 -0
  69. package/substrate/.claude/rules/context-management.md +85 -0
  70. package/substrate/.claude/rules/destructive-operations.md +69 -0
  71. package/substrate/.claude/rules/diagnosis.md +38 -0
  72. package/substrate/.claude/rules/guardrails.md +73 -0
  73. package/substrate/.claude/rules/identifier-leak-prevention.md +104 -0
  74. package/substrate/.claude/rules/lite-manifest-schema-change-discipline.md +98 -0
  75. package/substrate/.claude/rules/oo-ad-entry-point.md +117 -0
  76. package/substrate/.claude/rules/operator-facing-prose.md +196 -0
  77. package/substrate/.claude/rules/plain-english-discipline.md +154 -0
  78. package/substrate/.claude/rules/pr-body-shape.md +317 -0
  79. package/substrate/.claude/rules/pr-strategy.md +167 -0
  80. package/substrate/.claude/rules/sdlc-gates.md +149 -0
  81. package/substrate/.claude/rules/session-artifacts.md +236 -0
  82. package/substrate/.claude/rules/state-schema-validation.md +162 -0
  83. package/substrate/.claude/rules/whereami-load-bearing.md +202 -0
  84. package/substrate/.claude/rules/writing-craft-discipline.md +92 -0
  85. package/substrate/.claude/skills/build/SKILL.md +640 -0
  86. package/substrate/.claude/skills/decompose/SKILL.md +280 -0
  87. package/substrate/.claude/skills/diagnose/SKILL.md +297 -0
  88. package/substrate/.claude/skills/howdoi/SKILL.md +105 -0
  89. package/substrate/.claude/skills/ia-model/SKILL.md +108 -0
  90. package/substrate/.claude/skills/interaction-design/SKILL.md +112 -0
  91. package/substrate/.claude/skills/interpret-input/SKILL.md +180 -0
  92. package/substrate/.claude/skills/kiss/SKILL.md +449 -0
  93. package/substrate/.claude/skills/launch/SKILL.md +924 -0
  94. package/substrate/.claude/skills/lean-canvas/SKILL.md +332 -0
  95. package/substrate/.claude/skills/longrun/SKILL.md +426 -0
  96. package/substrate/.claude/skills/luminary/SKILL.md +481 -0
  97. package/substrate/.claude/skills/ogilvy-writing-audit/SKILL.md +177 -0
  98. package/substrate/.claude/skills/onboard-repo/SKILL.md +2190 -0
  99. package/substrate/.claude/skills/pattern-review/SKILL.md +99 -0
  100. package/substrate/.claude/skills/personas/SKILL.md +207 -0
  101. package/substrate/.claude/skills/promote/SKILL.md +283 -0
  102. package/substrate/.claude/skills/requirement/SKILL.md +98 -0
  103. package/substrate/.claude/skills/retro/SKILL.md +117 -0
  104. package/substrate/.claude/skills/riff/SKILL.md +114 -0
  105. package/substrate/.claude/skills/roadmap-reconcile/SKILL.md +163 -0
  106. package/substrate/.claude/skills/session-end/SKILL.md +309 -0
  107. package/substrate/.claude/skills/session-log/SKILL.md +299 -0
  108. package/substrate/.claude/skills/skills/SKILL.md +228 -0
  109. package/substrate/.claude/skills/spec/SKILL.md +105 -0
  110. package/substrate/.claude/skills/sprint/SKILL.md +392 -0
  111. package/substrate/.claude/skills/stage/SKILL.md +384 -0
  112. package/substrate/.claude/skills/state-a-problem/SKILL.md +185 -0
  113. package/substrate/.claude/skills/temperance/SKILL.md +108 -0
  114. package/substrate/.claude/skills/use-case/SKILL.md +417 -0
  115. package/substrate/.claude/skills/user-stories/SKILL.md +268 -0
  116. package/substrate/.claude/skills/value-prop/SKILL.md +251 -0
  117. package/substrate/.claude/skills/verify/SKILL.md +160 -0
  118. package/substrate/.claude/skills/whereami/SKILL.md +307 -0
  119. package/substrate/AGENTS.md +79 -0
  120. package/substrate/CLAUDE-lite.md +85 -0
  121. package/substrate/CODE_OF_CONDUCT.md +28 -0
  122. package/substrate/CONTRIBUTING.md +177 -0
  123. package/substrate/README.md +173 -0
  124. package/substrate/SECURITY.md +19 -0
  125. package/substrate/architecture/decisions/ADR-029-release-pipeline.md +79 -0
  126. package/substrate/architecture/decisions/ADR-031-non-breaking-changes-adopter-discipline.md +139 -0
  127. package/substrate/architecture/decisions/ADR-032-adopter-sync-dispatcher-architecture.md +192 -0
  128. package/substrate/architecture/decisions/ADR-039-release-tagging-scheme.md +145 -0
  129. package/substrate/architecture/decisions/ADR-040-planning-skill-vocabulary-and-lite-profile.md +155 -0
  130. package/substrate/architecture/decisions/ADR-044-unified-skill-body-template.md +162 -0
  131. package/substrate/lib/clean-artifacts-sweep.sh +112 -0
  132. package/substrate/lib/code-comment-discipline.sh +144 -0
  133. package/substrate/lib/composer-preflight.sh +459 -0
  134. package/substrate/lib/hook-inject.sh +255 -0
  135. package/substrate/lib/luminary-pick.sh +96 -0
  136. package/substrate/lib/output-discipline.sh +143 -0
  137. package/substrate/lib/rewrite-check.sh +214 -0
  138. package/substrate/lib/state.sh +1372 -0
  139. package/substrate/lib/telemetry.sh +205 -0
  140. package/substrate/lib/tier-check.sh +138 -0
  141. package/substrate/lib/tier-dependency-audit.sh +933 -0
  142. package/substrate/presence/install/bassclef-hook-connect.sh +178 -0
  143. package/substrate/presence/install/bassclef-sync.dispatcher.template.sh +841 -0
  144. package/substrate/presence/install/bassclef-sync.template.sh +2076 -0
  145. package/substrate/presence/install/schedule-auto-save.cron.sh +88 -0
  146. package/substrate/presence/install/schedule-auto-save.taskscheduler.md +122 -0
  147. package/substrate/scripts/aggregate-telemetry.sh +217 -0
  148. package/substrate/scripts/analyze-tier-dependencies.sh +239 -0
  149. package/substrate/scripts/generate-lite-manifest.sh +489 -0
  150. package/substrate/scripts/generate-tier-manifest.sh +28 -0
  151. package/substrate/scripts/intent-drift-check.sh +456 -0
  152. package/substrate/scripts/lite-manifest-drift-check.sh +146 -0
  153. package/substrate/scripts/render-lite-manifest-doc.sh +150 -0
  154. package/substrate/standards/bassclef-configs-schema.md +232 -0
  155. package/substrate/standards/bassclef-evolution.md +143 -0
  156. package/substrate/standards/bassclef-internal-jargon.md +244 -0
  157. package/substrate/standards/bassclef-managed-sentinel.md +96 -0
  158. package/substrate/standards/bassclef-source-config.md +228 -0
  159. package/substrate/standards/deferred-actions-schema.md +204 -0
  160. package/substrate/standards/docs-sync-allowlist.md +76 -0
  161. package/substrate/standards/graceful-exit.md +227 -0
  162. package/substrate/standards/hook-idempotency.md +102 -0
  163. package/substrate/standards/lite-manifest-schema-changes.md +75 -0
  164. package/substrate/standards/luminary-problem-patterns.md +481 -0
  165. package/substrate/standards/ogilvy-writing-rules.md +225 -0
  166. package/substrate/standards/opener-discipline.md +96 -0
  167. package/substrate/standards/operator-facing-prose-discipline.md +201 -0
  168. package/substrate/standards/project-directory-layout.md +276 -0
  169. package/substrate/standards/release-tagging.md +137 -0
  170. package/substrate/standards/sdlc-compliance.md +286 -0
  171. package/substrate/standards/secrets-lifecycle.md +210 -0
  172. package/substrate/standards/security-scanner-adapter.md +145 -0
  173. package/substrate/standards/skill-output-discipline.md +90 -0
  174. package/substrate/standards/state-spine.md +511 -0
  175. package/substrate/standards/tech-stack-config.md +109 -0
  176. package/substrate/standards/tier-dependency-analysis.md +167 -0
  177. package/substrate/standards/tier-runtime-deps/lite.md +57 -0
  178. package/substrate/standards/tier-tag-schema.md +155 -0
  179. package/substrate/standards/whereami-schema.md +301 -0
  180. package/substrate/standards/writing-guide.md +213 -0
  181. package/substrate/templates/deferred-action-template.md +45 -0
  182. package/substrate/templates/whereami-template.md +101 -0
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ # tier: lite
3
+ # temperance-gate.sh — mechanical /temperance firing at decision boundaries
4
+ # (bassclef#1147 T15)
5
+ #
6
+ # Fires on PreToolUse Edit|Write|MultiEdit. MVP scope: ONE trigger active —
7
+ # "load-bearing path edit" (catches edits to .claude/, standards/, lib/state.sh,
8
+ # SKILL.md when no fresh temperance marker exists for the session).
9
+ #
10
+ # Other triggers from bet 2026-06-18b WU-7 acceptance — explain-mode entry,
11
+ # chain-anchor entry, non-stacked-branch first edit, post-compaction recovery —
12
+ # are scaffolded as stubs below; design + implementation deferred to follow-on
13
+ # tickets pending operator review of the trigger taxonomy.
14
+ #
15
+ # Other triggers from ticket #1147 — first-source-edit per session, N
16
+ # consecutive edits without temperance, substrate proposal without catalog
17
+ # browse, diagnose-repro marker absence — also deferred to follow-on.
18
+ #
19
+ # Coordinates with pre-build-gate.sh — that hook already handles
20
+ # non-stacked-branch first edit temperance. This hook adds the load-bearing
21
+ # trigger which pre-build-gate does not cover.
22
+ #
23
+ # Exit codes:
24
+ # 0 — pass (override, skip, marker present, non-trigger)
25
+ # 3 — BLOCK (load-bearing edit without temperance marker)
26
+ #
27
+ # Override: SKIP_TEMPERANCE_GATE=1 — logged via trace-helper (bassclef idiom).
28
+
29
+ set -u
30
+
31
+ # --- Override path ---
32
+ if [ "${SKIP_TEMPERANCE_GATE:-0}" = "1" ]; then
33
+ echo "SKIP_TEMPERANCE_GATE=1 — temperance gate bypassed" >&2
34
+ exit 0
35
+ fi
36
+
37
+ # --- Read stdin (fail-soft on malformed JSON) ---
38
+ INPUT=$(cat)
39
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
40
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
41
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null || echo "unknown")
42
+ CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
43
+
44
+ if [ -z "$TOOL_NAME" ]; then
45
+ exit 0
46
+ fi
47
+
48
+ # --- Tool-name matcher ---
49
+ case "$TOOL_NAME" in
50
+ Edit|Write|MultiEdit) ;;
51
+ *) exit 0 ;;
52
+ esac
53
+
54
+ # --- Trigger 1: Load-bearing path edit (MVP — only active trigger) ---
55
+ # Detects edits to substrate paths whose changes carry blast-radius beyond a
56
+ # normal source edit. The temperance question forces the agent to articulate
57
+ # "right thing" + "right way" before the edit lands.
58
+
59
+ is_load_bearing() {
60
+ local path="$1"
61
+ case "$path" in
62
+ */.claude/*) return 0 ;;
63
+ */standards/*) return 0 ;;
64
+ */lib/state.sh) return 0 ;;
65
+ */SKILL.md) return 0 ;;
66
+ .claude/*) return 0 ;;
67
+ standards/*) return 0 ;;
68
+ lib/state.sh) return 0 ;;
69
+ *) return 1 ;;
70
+ esac
71
+ }
72
+
73
+ # --- Trigger stubs (deferred to follow-on tickets) ---
74
+ # Each stub is a function that always returns "no trigger" — the design +
75
+ # implementation lands in a follow-on ticket. Listed here so reviewers can
76
+ # see the planned trigger surface without reading the goal doc.
77
+
78
+ trigger_explain_mode() { return 1; } # bet 2026-06-18b WU-7
79
+ trigger_chain_anchor() { return 1; } # bet 2026-06-18b WU-7
80
+ trigger_non_stacked_first_edit() { return 1; } # bet 2026-06-18b WU-7 (overlaps pre-build-gate; routing TBD)
81
+ trigger_post_compaction() { return 1; } # bet 2026-06-18b WU-7
82
+ trigger_n_consecutive_edits() { return 1; } # ticket #1147 trigger 3
83
+ trigger_substrate_proposal() { return 1; } # ticket #1147 trigger 5
84
+ trigger_diagnose_repro() { return 1; } # ticket #1147 trigger 6
85
+
86
+ # --- Marker check for active trigger ---
87
+ if ! is_load_bearing "$FILE_PATH"; then
88
+ exit 0
89
+ fi
90
+
91
+ # Marker convention shared with pre-build-gate.sh — same temperance markers.
92
+ # A fresh temperance marker (any branch this session) clears all triggers.
93
+ MARKER_DIR="${CWD}/state/markers/temperance"
94
+ SESSION_MARKER="${MARKER_DIR}/${SESSION_ID}.marker"
95
+
96
+ if [ -f "$SESSION_MARKER" ]; then
97
+ exit 0
98
+ fi
99
+
100
+ # Also clear when any temperance marker exists for the current branch (the
101
+ # pre-build-gate convention writes branch-slug markers).
102
+ if [ -d "$MARKER_DIR" ] && [ -n "$(ls -A "$MARKER_DIR" 2>/dev/null)" ]; then
103
+ # Marker exists for some branch this session — pass. (Reviewer: this is
104
+ # the loose check; tighter "matching branch" check is a follow-on.)
105
+ exit 0
106
+ fi
107
+
108
+ # --- BLOCK ---
109
+ cat >&2 <<BLOCK
110
+ ============================================
111
+ 🛑 temperance-gate — BLOCKED (essential path)
112
+ ============================================
113
+
114
+ About to edit an essential substrate path without a fresh temperance marker:
115
+ ${FILE_PATH}
116
+
117
+ The temperance question:
118
+ Am I building the RIGHT THING?
119
+ Am I building it the RIGHT WAY?
120
+
121
+ Essential paths (.claude/, standards/, lib/state.sh, SKILL.md) carry
122
+ more impact than a normal source edit. Pause and answer the two
123
+ questions before the edit lands.
124
+
125
+ To proceed:
126
+ 1. State the answers (right thing / right way / what could break).
127
+ 2. Write a temperance marker:
128
+ mkdir -p ${MARKER_DIR}
129
+ cat > ${SESSION_MARKER} <<EOF
130
+ {"session_id":"${SESSION_ID}","entered_at":"\$(date -u +%Y-%m-%dT%H:%M:%SZ)","question_answered":"yes"}
131
+ EOF
132
+ 3. Re-run the edit — the marker now satisfies the gate.
133
+
134
+ Override (logged):
135
+ SKIP_TEMPERANCE_GATE=1 <command>
136
+
137
+ Note: this is the MVP trigger (load-bearing path). Other triggers
138
+ (explain-mode, chain-anchor, post-compaction, non-stacked first edit,
139
+ N consecutive edits, substrate proposal, diagnose-repro) are scaffolded
140
+ in this hook but inactive — they ship in follow-on tickets.
141
+
142
+ Per bassclef#1147 T15 (this hook) and .claude/skills/temperance/SKILL.md
143
+ (the methodology this gate enforces mechanically).
144
+ ============================================
145
+ BLOCK
146
+
147
+ exit 3
@@ -0,0 +1,233 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # testing-tier-enforce.sh — per-path testing-tier enforcement (bassclef#1037)
4
+ #
5
+ # Reads tier config from .claude/bassclef-configs.jsonc (testing.global_floor
6
+ # + testing.path_matchers); per-file overrides from .claude/hooks/testing-tier-
7
+ # enforce-allowlist.txt (grandfather grace) and state/testing-tier-overrides.jsonl
8
+ # (operator-filed per-file tier flip — future; not yet read in v1).
9
+ #
10
+ # Tier semantics (per .claude/rules/testing-tier-config.md):
11
+ # 0 strict TDD — BLOCK when sibling test absent OR test mtime < source mtime
12
+ # 1 test-with — WARN when sibling test absent
13
+ # 2 smoke — silent (advisory only)
14
+ # 3 manual verify — silent
15
+ #
16
+ # Path-matcher precedence: array order is most-specific to least-specific; first
17
+ # match wins. When no entry matches, testing.global_floor applies (default 1).
18
+ #
19
+ # Sibling test convention (Tier 0 only):
20
+ # .claude/hooks/<name>.sh -> .claude/hooks/tests/<name>.test.sh
21
+ # standards/.../schemas/<name>.json -> standards/.../schemas/tests/<name>.test.sh
22
+ # lib/<name>.sh -> lib/tests/<name>.test.sh
23
+ #
24
+ # Override: SKIP_TESTING_TIER_ENFORCE=1 — bypass entire hook for one command.
25
+ # Logged via trace-helper.sh per bassclef's existing override idiom.
26
+ #
27
+ # Wired into pre-commit-gate.sh chain. Also invokable standalone for tests
28
+ # (set TESTING_TIER_FILES env var with newline-separated paths to check
29
+ # instead of reading staged files from git).
30
+
31
+ set -u
32
+
33
+ REPO_ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
34
+ CONFIG_FILE="$REPO_ROOT/.claude/bassclef-configs.jsonc"
35
+ ALLOWLIST_FILE="$REPO_ROOT/.claude/hooks/testing-tier-enforce-allowlist.txt"
36
+
37
+ # Override path
38
+ if [ "${SKIP_TESTING_TIER_ENFORCE:-0}" = "1" ]; then
39
+ if [ -f "$REPO_ROOT/.claude/hooks/trace-helper.sh" ]; then
40
+ # shellcheck disable=SC1091
41
+ source "$REPO_ROOT/.claude/hooks/trace-helper.sh" 2>/dev/null || true
42
+ type trace_log >/dev/null 2>&1 && trace_log "testing-tier-enforce" "SKIP_TESTING_TIER_ENFORCE=1 bypass"
43
+ fi
44
+ exit 0
45
+ fi
46
+
47
+ # resolve_tier_for_path <relative-path> -> echoes tier int (0/1/2/3)
48
+ resolve_tier_for_path() {
49
+ local path="$1"
50
+ if [ ! -f "$CONFIG_FILE" ]; then
51
+ echo 1
52
+ return
53
+ fi
54
+ local json
55
+ json=$(perl -pe 's,^\s*//.*$,,; s,/\*.*?\*/,,g' "$CONFIG_FILE" 2>/dev/null)
56
+ local global_floor
57
+ global_floor=$(echo "$json" | jq -r '.testing.global_floor // 1' 2>/dev/null)
58
+ [ -z "$global_floor" ] || [ "$global_floor" = "null" ] && global_floor=1
59
+
60
+ # Iterate matchers in array order; first shell-glob match wins.
61
+ local count
62
+ count=$(echo "$json" | jq -r '.testing.path_matchers | length // 0' 2>/dev/null)
63
+ [ -z "$count" ] || [ "$count" = "null" ] && count=0
64
+
65
+ local i=0
66
+ while [ "$i" -lt "$count" ]; do
67
+ local pattern tier
68
+ pattern=$(echo "$json" | jq -r ".testing.path_matchers[$i].match" 2>/dev/null)
69
+ tier=$(echo "$json" | jq -r ".testing.path_matchers[$i].tier" 2>/dev/null)
70
+ # Shell glob via case statement; ** treated as * (bash glob limitation OK for v1)
71
+ # Replace ** with * for case matching
72
+ local case_pattern="${pattern//\*\*/\*}"
73
+ case "$path" in
74
+ $case_pattern)
75
+ echo "$tier"
76
+ return
77
+ ;;
78
+ esac
79
+ i=$((i + 1))
80
+ done
81
+
82
+ echo "$global_floor"
83
+ }
84
+
85
+ # is_allowlisted_in_grace <relative-path> -> exit 0 if grace covers
86
+ is_allowlisted_in_grace() {
87
+ local path="$1"
88
+ [ ! -f "$ALLOWLIST_FILE" ] && return 1
89
+ local now
90
+ now=$(date -u +%s)
91
+ while IFS='|' read -r entry_path grace_until; do
92
+ [ -z "${entry_path:-}" ] && continue
93
+ case "$entry_path" in '#'*|'') continue ;; esac
94
+ # Trim whitespace
95
+ entry_path="${entry_path# }"; entry_path="${entry_path% }"
96
+ grace_until="${grace_until# }"; grace_until="${grace_until% }"
97
+ [ "$entry_path" != "$path" ] && continue
98
+ local grace_epoch=""
99
+ grace_epoch=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$grace_until" +%s 2>/dev/null) || \
100
+ grace_epoch=$(date -u -d "$grace_until" +%s 2>/dev/null) || \
101
+ grace_epoch=""
102
+ [ -z "$grace_epoch" ] && return 1
103
+ [ "$now" -lt "$grace_epoch" ] && return 0
104
+ return 1
105
+ done < "$ALLOWLIST_FILE"
106
+ return 1
107
+ }
108
+
109
+ # sibling_test_path <source-relative-path> -> echoes expected test path
110
+ sibling_test_path() {
111
+ local src="$1"
112
+ local dir base stem
113
+ dir=$(dirname "$src")
114
+ base=$(basename "$src")
115
+ stem="${base%.*}"
116
+ echo "${dir}/tests/${stem}.test.sh"
117
+ }
118
+
119
+ # Get file mtime as epoch seconds (portable Mac + Linux).
120
+ # GNU stat is tried first because BSD-style `stat -f %m` on Linux GNU
121
+ # does NOT fail — it succeeds with mount-point output (%m means mount
122
+ # point under GNU's -f filesystem mode), which would silently corrupt
123
+ # the mtime comparison. GNU-first; BSD fallback for macOS.
124
+ file_mtime() {
125
+ local f="$1"
126
+ stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null
127
+ }
128
+
129
+ # check_tier_0_violation <relative-path> -> exit 0 if ok, 1 if violation
130
+ # Sets REASON var with violation description.
131
+ check_tier_0_violation() {
132
+ local src="$1"
133
+ local test_rel test_abs src_abs
134
+ test_rel=$(sibling_test_path "$src")
135
+ test_abs="$REPO_ROOT/$test_rel"
136
+ src_abs="$REPO_ROOT/$src"
137
+
138
+ if [ ! -f "$test_abs" ]; then
139
+ REASON="Tier 0 source has no sibling test at $test_rel — write the test first per TDD discipline"
140
+ return 1
141
+ fi
142
+ local src_mtime test_mtime
143
+ src_mtime=$(file_mtime "$src_abs")
144
+ test_mtime=$(file_mtime "$test_abs")
145
+ if [ -z "$src_mtime" ] || [ -z "$test_mtime" ]; then
146
+ REASON="Tier 0 mtime check failed: could not read mtimes (src=$src test=$test_rel)"
147
+ return 1
148
+ fi
149
+ if [ "$test_mtime" -lt "$src_mtime" ]; then
150
+ REASON="Tier 0 source modified ($src) but test ($test_rel) mtime did not advance — write the test before the source per TDD discipline"
151
+ return 1
152
+ fi
153
+ return 0
154
+ }
155
+
156
+ main() {
157
+ local files
158
+ if [ -n "${TESTING_TIER_FILES:-}" ]; then
159
+ files="$TESTING_TIER_FILES"
160
+ else
161
+ # Read PreToolUse hook payload (jsonpath .tool_input.command) from stdin if present
162
+ if [ ! -t 0 ]; then
163
+ local input command
164
+ input=$(cat 2>/dev/null || echo '')
165
+ if [ -n "$input" ]; then
166
+ command=$(echo "$input" | jq -r '.tool_input.command // ""' 2>/dev/null || echo '')
167
+ if ! echo "$command" | grep -q 'git commit'; then
168
+ exit 0
169
+ fi
170
+ fi
171
+ fi
172
+ files=$(git -C "$REPO_ROOT" diff --cached --name-only --diff-filter=AM 2>/dev/null || echo '')
173
+ fi
174
+
175
+ [ -z "$files" ] && exit 0
176
+
177
+ local blocks=()
178
+ local warnings=()
179
+
180
+ while IFS= read -r f; do
181
+ [ -z "$f" ] && continue
182
+ local tier
183
+ tier=$(resolve_tier_for_path "$f")
184
+ case "$tier" in
185
+ 0)
186
+ if is_allowlisted_in_grace "$f"; then
187
+ continue
188
+ fi
189
+ REASON=""
190
+ if ! check_tier_0_violation "$f"; then
191
+ blocks+=("$f: $REASON")
192
+ fi
193
+ ;;
194
+ 1)
195
+ local exp_test
196
+ exp_test=$(sibling_test_path "$f")
197
+ if [ ! -f "$REPO_ROOT/$exp_test" ]; then
198
+ warnings+=("$f: Tier 1 — no sibling test at $exp_test (expected; not BLOCKing)")
199
+ fi
200
+ ;;
201
+ 2|3)
202
+ : ;;
203
+ esac
204
+ done <<< "$files"
205
+
206
+ if [ "${#warnings[@]}" -gt 0 ]; then
207
+ # Advisory only — write to stdout so the Claude Code PreToolUse
208
+ # wrapper does not treat non-empty stderr as an error and block
209
+ # the git commit. BLOCK output below stays on stderr (real error).
210
+ echo ""
211
+ echo "TESTING-TIER WARN (Tier 1):"
212
+ for w in "${warnings[@]}"; do echo " $w"; done
213
+ fi
214
+
215
+ if [ "${#blocks[@]}" -gt 0 ]; then
216
+ echo "" >&2
217
+ echo "============================================" >&2
218
+ echo "🛑 TESTING-TIER ENFORCE — BLOCKED 🛑" >&2
219
+ echo "============================================" >&2
220
+ for b in "${blocks[@]}"; do echo " $b" >&2; done
221
+ echo "" >&2
222
+ echo "Per .claude/rules/testing-tier-config.md (bassclef#1037)." >&2
223
+ echo "Tier framework: 0 strict TDD / 1 WARN / 2 smoke / 3 manual verify." >&2
224
+ echo "Resolution: write the sibling test FIRST, then re-stage and commit." >&2
225
+ echo "Override (logged): SKIP_TESTING_TIER_ENFORCE=1 <command>" >&2
226
+ echo "============================================" >&2
227
+ exit 1
228
+ fi
229
+
230
+ exit 0
231
+ }
232
+
233
+ main
@@ -0,0 +1,219 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # install-class: dual
4
+ # Turn-prose grade measure — Stop hook that reads the last assistant message
5
+ # from the transcript, computes its Flesch-Kincaid grade level, and writes
6
+ # the number to a marker file at state/markers/turn-grade/last-turn.json.
7
+ #
8
+ # The pre-turn plain-english-steering.sh reads the marker and injects the
9
+ # number into the steering block on the next prompt. Turns a soft "aim for
10
+ # grade 10" reminder into a concrete number the agent sees each turn.
11
+ #
12
+ # Complements turn-prose-kiss-check.sh — that hook scans for BLOCK words.
13
+ # This hook measures reading level.
14
+ #
15
+ # Input (stdin): JSON with transcript_path + session_id
16
+ # Output: writes state/markers/turn-grade/last-turn.json
17
+ # Exit: 0 always (measurement only, no gate)
18
+ #
19
+ # Override: SKIP_TURN_PROSE_GRADE=1
20
+
21
+ set +e
22
+
23
+ # === Override path ===
24
+ if [ "${SKIP_TURN_PROSE_GRADE:-0}" = "1" ]; then
25
+ exit 0
26
+ fi
27
+
28
+ # === Parse stdin JSON ===
29
+ STDIN_JSON=$(cat 2>/dev/null || echo '{}')
30
+ TRANSCRIPT_PATH=$(echo "$STDIN_JSON" | jq -r '.transcript_path // ""' 2>/dev/null)
31
+
32
+ [ -z "$TRANSCRIPT_PATH" ] && exit 0
33
+ [ ! -f "$TRANSCRIPT_PATH" ] && exit 0
34
+
35
+ # === Extract last assistant text ===
36
+ LAST_ASSISTANT_LINE=$(grep '"type":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1)
37
+ [ -z "$LAST_ASSISTANT_LINE" ] && exit 0
38
+
39
+ MESSAGE_TEXT=$(echo "$LAST_ASSISTANT_LINE" \
40
+ | jq -r '.message.content[]? | select(.type == "text") | .text' 2>/dev/null)
41
+ [ -z "$MESSAGE_TEXT" ] && exit 0
42
+
43
+ # === Strip code blocks + inline backticks + URLs (prose only) ===
44
+ SCRUBBED=$(echo "$MESSAGE_TEXT" | awk '
45
+ BEGIN { in_code = 0 }
46
+ /^```/ { in_code = !in_code; next }
47
+ in_code { next }
48
+ { print }
49
+ ' | sed -E 's/`[^`]*`//g; s|https?://[^ )]+||g')
50
+
51
+ # === Compute Flesch-Kincaid grade via python ===
52
+ # Try textstat first (accurate; well-tested syllable counter). Fall back to
53
+ # hand-rolled formula when textstat is not installed. Zero break for adopters
54
+ # who have not installed the library.
55
+ # Per Ship 1 #1312: also compute per-sentence grade + top 3 offenders that
56
+ # exceed grade 10. Output as JSON for bash to inject into marker.
57
+ GRADE_OUTPUT=$(printf '%s' "$SCRUBBED" | python3 -c '
58
+ import sys, re, json
59
+
60
+ text = sys.stdin.read()
61
+
62
+ sentences = [s for s in re.split(r"[.!?]+", text) if s.strip()]
63
+ words = re.findall(r"\b[a-zA-Z][a-zA-Z\x27]*\b", text)
64
+
65
+ if len(words) < 20 or not sentences:
66
+ print("SKIP")
67
+ sys.exit(0)
68
+
69
+ # Fallback syllable counter — always defined so per-sentence path works
70
+ def count_syllables(word):
71
+ word = re.sub(r"[^a-z]", "", word.lower())
72
+ if not word:
73
+ return 0
74
+ vowels = "aeiouy"
75
+ count = 0
76
+ prev_was_vowel = False
77
+ for c in word:
78
+ is_vowel = c in vowels
79
+ if is_vowel and not prev_was_vowel:
80
+ count += 1
81
+ prev_was_vowel = is_vowel
82
+ if word.endswith("e") and count > 1:
83
+ count -= 1
84
+ return max(1, count)
85
+
86
+ # Preferred path — textstat library
87
+ try:
88
+ from textstat import flesch_kincaid_grade, syllable_count
89
+ grade = flesch_kincaid_grade(text)
90
+ syllables = syllable_count(text)
91
+ source = "TEXTSTAT"
92
+ def sentence_grade(s):
93
+ return flesch_kincaid_grade(s)
94
+ except ImportError:
95
+ syllables = sum(count_syllables(w) for w in words)
96
+ grade = 0.39 * (len(words) / len(sentences)) + 11.8 * (syllables / len(words)) - 15.59
97
+ source = "FALLBACK"
98
+ def sentence_grade(s):
99
+ s_words = re.findall(r"\b[a-zA-Z][a-zA-Z\x27]*\b", s)
100
+ if not s_words:
101
+ return 0
102
+ s_syllables = sum(count_syllables(w) for w in s_words)
103
+ # For a single sentence, sentence-count is 1
104
+ return 0.39 * len(s_words) + 11.8 * (s_syllables / len(s_words)) - 15.59
105
+
106
+ # Per-sentence offenders — Ship 1 #1312
107
+ offenders = []
108
+ for s in sentences:
109
+ s_clean = s.strip()
110
+ s_words = re.findall(r"\b[a-zA-Z][a-zA-Z\x27]*\b", s_clean)
111
+ if len(s_words) < 5:
112
+ continue # skip very short sentences
113
+ try:
114
+ s_grade = sentence_grade(s_clean)
115
+ except Exception:
116
+ continue
117
+ if s_grade > 10:
118
+ preview = s_clean[:80]
119
+ if len(s_clean) > 80:
120
+ preview += "..."
121
+ offenders.append({
122
+ "grade": round(s_grade, 1),
123
+ "words": len(s_words),
124
+ "preview": preview,
125
+ })
126
+ # Sort by grade descending; take top 3
127
+ offenders.sort(key=lambda x: -x["grade"])
128
+ offenders = offenders[:3]
129
+
130
+ # Emit first line = original space-separated shape for backward compat
131
+ print(f"{grade:.1f} {len(words)} {len(sentences)} {syllables} {source}")
132
+ # Emit second line = JSON array of offenders (may be empty)
133
+ print(json.dumps(offenders))
134
+ ' 2>/dev/null)
135
+
136
+ # Parse output — either "SKIP" or "grade words sentences syllables SOURCE" + offenders JSON
137
+ if [ -z "$GRADE_OUTPUT" ] || [ "$GRADE_OUTPUT" = "SKIP" ]; then
138
+ exit 0
139
+ fi
140
+
141
+ # First line = grade metrics; second line = offenders JSON
142
+ GRADE_LINE=$(echo "$GRADE_OUTPUT" | head -1)
143
+ OFFENDERS_JSON=$(echo "$GRADE_OUTPUT" | sed -n '2p')
144
+ [ -z "$OFFENDERS_JSON" ] && OFFENDERS_JSON="[]"
145
+
146
+ GRADE=$(echo "$GRADE_LINE" | awk '{print $1}')
147
+ WORDS=$(echo "$GRADE_LINE" | awk '{print $2}')
148
+ SENTENCES=$(echo "$GRADE_LINE" | awk '{print $3}')
149
+ SOURCE=$(echo "$GRADE_LINE" | awk '{print $5}')
150
+
151
+ [ -z "$GRADE" ] && exit 0
152
+
153
+ # === Write grade marker ===
154
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
155
+ MARKER_DIR="${REPO_ROOT}/state/markers/turn-grade"
156
+ mkdir -p "$MARKER_DIR" 2>/dev/null
157
+
158
+ cat > "${MARKER_DIR}/last-turn.json" 2>/dev/null <<EOF
159
+ {
160
+ "grade": $GRADE,
161
+ "words": $WORDS,
162
+ "sentences": $SENTENCES,
163
+ "measured_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
164
+ "target_ceiling": 10,
165
+ "target_aspiration": 8,
166
+ "source": "${SOURCE:-UNKNOWN}",
167
+ "top_offenders": ${OFFENDERS_JSON}
168
+ }
169
+ EOF
170
+
171
+ # === Rewrite-check hits marker (ticket #935 extension) ===
172
+ # Run lib/rewrite-check.sh on the scrubbed text; count hits per category.
173
+ # Write to state/markers/turn-prose-flags/last-turn.json for plain-english-steering.sh
174
+ # to read on the next turn and inject as first-person compose reminders.
175
+ FLAGS_DIR="${REPO_ROOT}/state/markers/turn-prose-flags"
176
+ REWRITE_LIB="${REPO_ROOT}/lib/rewrite-check.sh"
177
+
178
+ if [ -f "$REWRITE_LIB" ]; then
179
+ mkdir -p "$FLAGS_DIR" 2>/dev/null
180
+ TMP_TEXT=$(mktemp)
181
+ printf '%s' "$SCRUBBED" > "$TMP_TEXT"
182
+
183
+ # shellcheck disable=SC1090
184
+ source "$REWRITE_LIB" 2>/dev/null
185
+
186
+ # Count hits per check function (each prints one match per line)
187
+ PASSIVE_HITS=0
188
+ HYPHEN_HITS=0
189
+ LONG_SENTENCE_HITS=0
190
+ ADVERB_HITS=0
191
+
192
+ if declare -f check_passive_voice >/dev/null 2>&1; then
193
+ PASSIVE_HITS=$(check_passive_voice "$TMP_TEXT" 2>/dev/null | wc -l | tr -d ' ')
194
+ fi
195
+ if declare -f check_hyphen_words >/dev/null 2>&1; then
196
+ HYPHEN_HITS=$(check_hyphen_words "$TMP_TEXT" 2>/dev/null | wc -l | tr -d ' ')
197
+ fi
198
+ if declare -f check_sentence_length >/dev/null 2>&1; then
199
+ LONG_SENTENCE_HITS=$(check_sentence_length "$TMP_TEXT" 25 2>/dev/null | wc -l | tr -d ' ')
200
+ fi
201
+ if declare -f check_adverb_density >/dev/null 2>&1; then
202
+ ADVERB_HITS=$(check_adverb_density "$TMP_TEXT" 2>/dev/null | wc -l | tr -d ' ')
203
+ fi
204
+
205
+ rm -f "$TMP_TEXT"
206
+
207
+ cat > "${FLAGS_DIR}/last-turn.json" 2>/dev/null <<EOF
208
+ {
209
+ "total_words": $WORDS,
210
+ "long_sentences": $LONG_SENTENCE_HITS,
211
+ "passive_voice": $PASSIVE_HITS,
212
+ "hyphen_words": $HYPHEN_HITS,
213
+ "adverbs": $ADVERB_HITS,
214
+ "measured_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
215
+ }
216
+ EOF
217
+ fi
218
+
219
+ exit 0