@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,271 @@
1
+ #!/usr/bin/env bash
2
+ # tier: lite
3
+ # state-validate.sh — PreToolUse hook that validates state-spine entity
4
+ # writes against their JSON schemas.
5
+ #
6
+ # Wires per .claude/settings.json under hooks.PreToolUse with matcher
7
+ # "Edit|Write". Reads the tool's event JSON from stdin, detects whether
8
+ # the target file is a state-spine entity, extracts its data per the
9
+ # entity's storage shape, validates against the schema, and BLOCKs
10
+ # (exit non-zero) on failure.
11
+ #
12
+ # Per ADR-015: JSON Schema only for v0. Validator: ajv-cli draft-2020-12.
13
+ # Per state-spine.md: three storage shapes (A/B/C) handled via path-based
14
+ # entity-type detection.
15
+ # Per accessor-library-discipline.md: this is the write-side gate.
16
+ #
17
+ # Override: SKIP_STATE_VALIDATE=1 (logged to stderr).
18
+ #
19
+ # Pairs with: .claude/rules/state-schema-validation.md
20
+
21
+ set -u
22
+
23
+ # === Hook liveness heartbeat (WU-3 of bet 2026-07-31d; closes #1002) ===
24
+ # Silent-fail — a missing lib never crashes the hook.
25
+ {
26
+ _hb_sd="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ for _hb_c in "${_hb_sd}/../../lib/hook-heartbeat.sh" "${HOME:-/}/lib/hook-heartbeat.sh"; do
28
+ [ -f "$_hb_c" ] && source "$_hb_c" && heartbeat_mark "state-validate" && break
29
+ done
30
+ unset _hb_sd _hb_c
31
+ } 2>/dev/null || true
32
+
33
+ # === Override path ===
34
+
35
+ if [ "${SKIP_STATE_VALIDATE:-}" = "1" ]; then
36
+ echo "[state-validate.sh] SKIP_STATE_VALIDATE=1 — bypassing validation" >&2
37
+ exit 0
38
+ fi
39
+
40
+ # === Read PreToolUse event ===
41
+
42
+ EVENT_JSON="$(cat)"
43
+ TOOL_NAME="$(echo "$EVENT_JSON" | jq -r '.tool_name // empty')"
44
+ TARGET_PATH="$(echo "$EVENT_JSON" | jq -r '.tool_input.file_path // empty')"
45
+
46
+ # Tool filter: only Edit and Write trigger validation
47
+ case "$TOOL_NAME" in
48
+ Edit|Write|MultiEdit) ;;
49
+ *) exit 0 ;;
50
+ esac
51
+
52
+ # Empty path → pass-through
53
+ if [ -z "$TARGET_PATH" ]; then
54
+ exit 0
55
+ fi
56
+
57
+ # === Resolve project dir + schema dir ===
58
+
59
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
60
+ SCHEMA_DIR="${PROJECT_DIR}/standards/state-spine/schemas"
61
+
62
+ # If schema dir doesn't exist, this repo doesn't have spine v0 yet — pass through
63
+ if [ ! -d "$SCHEMA_DIR" ]; then
64
+ exit 0
65
+ fi
66
+
67
+ # === Path-based entity detection ===
68
+ #
69
+ # Returns the entity slug (matching <slug>.schema.json) if path is a
70
+ # state-spine entity, empty otherwise.
71
+ detect_entity_type() {
72
+ local path="$1"
73
+ # Strip project_dir prefix if present
74
+ local rel="${path#${PROJECT_DIR}/}"
75
+ rel="${rel#./}"
76
+
77
+ local result=""
78
+ case "$rel" in
79
+ docs/whereami.json) result="whereami" ;;
80
+ substrate.config.json) result="substrate-config" ;;
81
+ docs/preview-state/*.json) result="preview-state" ;;
82
+ docs/iteration-bets/*.md) result="iteration-bet" ;;
83
+ docs/roadmaps/*.md) result="roadmap" ;;
84
+ docs/stacks/*.md) result="stack" ;;
85
+ docs/decompositions/*.md) result="decomposition" ;;
86
+ docs/personas/*.md) result="persona" ;;
87
+ docs/deferred-actions/*.md) result="deferred-action" ;;
88
+ chronicle/*.md|docs/chronicle/*.md) result="chronicle" ;;
89
+ state/markers/*/*.json) result="marker" ;;
90
+ docs/sessions/*.json) result="session" ;;
91
+ docs/sessions/*.md) result="session" ;;
92
+ docs/input-artifacts/*.json) result="input-artifact" ;;
93
+ state/template-instances/*.json) result="template-instance" ;;
94
+ state/inbox/*.json) result="inbox-work-item" ;;
95
+ state/stack-templates/*.json) result="stack-template" ;;
96
+ bassclef-platform.yml|bassclef-platform.yaml) result="bassclef-platform" ;;
97
+ esac
98
+
99
+ # bassclef-platform.yml lives at the root of a SIBLING repo (e.g.,
100
+ # sunj-labs/platform-config). When edited from outside PROJECT_DIR,
101
+ # the absolute path doesn't strip cleanly — match by basename suffix
102
+ # as a fallback. The file MUST be at a repo root (not nested).
103
+ if [ -z "$result" ]; then
104
+ case "$path" in
105
+ */bassclef-platform.yml|*/bassclef-platform.yaml) result="bassclef-platform" ;;
106
+ esac
107
+ fi
108
+
109
+ echo "$result"
110
+ }
111
+
112
+ ENTITY="$(detect_entity_type "$TARGET_PATH")"
113
+
114
+ # Non-state file → pass-through
115
+ if [ -z "$ENTITY" ]; then
116
+ exit 0
117
+ fi
118
+
119
+ # === Resolve schema file ===
120
+
121
+ SCHEMA_FILE="${SCHEMA_DIR}/${ENTITY}.schema.json"
122
+ if [ ! -f "$SCHEMA_FILE" ]; then
123
+ echo "[state-validate.sh] schema for entity '$ENTITY' not found at $SCHEMA_FILE — pass-through" >&2
124
+ exit 0
125
+ fi
126
+
127
+ # === Storage shape detection ===
128
+ #
129
+ # A = pure JSON (whole file validates)
130
+ # B = markdown + YAML frontmatter (extract frontmatter, validate)
131
+ # C = pure JSON (whole file validates) — same as A but for collection entities
132
+ #
133
+ # In practice for the validator: anything ending in .json validates the
134
+ # whole file; anything ending in .md extracts YAML frontmatter and
135
+ # validates that.
136
+
137
+ extract_data_for_validation() {
138
+ local path="$1"
139
+ if [ ! -f "$path" ]; then
140
+ echo "[state-validate.sh] file does not exist at write time: $path" >&2
141
+ return 4
142
+ fi
143
+ case "$path" in
144
+ *.json)
145
+ cat "$path"
146
+ ;;
147
+ *.md)
148
+ # Extract YAML frontmatter, convert to JSON
149
+ awk '/^---$/{c++; next} c==1' "$path" | python3 -c "
150
+ import sys, yaml, json
151
+ try:
152
+ data = yaml.safe_load(sys.stdin) or {}
153
+ print(json.dumps(data, default=str))
154
+ except Exception as e:
155
+ print(f'YAML parse failed: {e}', file=sys.stderr)
156
+ sys.exit(3)
157
+ "
158
+ ;;
159
+ *.yml|*.yaml)
160
+ # Pure YAML file (Shape D — entity is the whole file). Convert
161
+ # to JSON and validate. Used by bassclef-platform.yml.
162
+ python3 -c "
163
+ import sys, yaml, json
164
+ try:
165
+ with open('$path') as f:
166
+ data = yaml.safe_load(f) or {}
167
+ print(json.dumps(data, default=str))
168
+ except Exception as e:
169
+ print(f'YAML parse failed: {e}', file=sys.stderr)
170
+ sys.exit(3)
171
+ "
172
+ ;;
173
+ *)
174
+ echo "[state-validate.sh] unsupported extension for path: $path" >&2
175
+ return 5
176
+ ;;
177
+ esac
178
+ }
179
+
180
+ # Hoare precondition cure (#1001): on new-file Write the target does not
181
+ # yet exist on disk. Read tool_input.content from the event JSON and
182
+ # materialize a temp file so the extract function can read it. Preserves
183
+ # the disk-read path for Edit and existing-file Write. Silent no-op when
184
+ # content is absent (matches prior file-missing behavior).
185
+ NEW_FILE_TMP=""
186
+ if [ ! -f "$TARGET_PATH" ] && [ "$TOOL_NAME" = "Write" ]; then
187
+ WRITE_CONTENT="$(echo "$EVENT_JSON" | jq -r '.tool_input.content // empty')"
188
+ if [ -n "$WRITE_CONTENT" ]; then
189
+ NEW_FILE_TMP="$(mktemp -t state-validate-newfile-XXXXXX)${TARGET_PATH##*.}"
190
+ # Add trailing dot if the extension detection above collapsed
191
+ case "$TARGET_PATH" in
192
+ *.*) NEW_FILE_TMP="${NEW_FILE_TMP%.*}.${TARGET_PATH##*.}" ;;
193
+ esac
194
+ printf '%s' "$WRITE_CONTENT" > "$NEW_FILE_TMP"
195
+ trap 'rm -f "$NEW_FILE_TMP"' EXIT
196
+ DATA_JSON="$(extract_data_for_validation "$NEW_FILE_TMP")"
197
+ EXTRACT_EXIT=$?
198
+ else
199
+ exit 0
200
+ fi
201
+ else
202
+ DATA_JSON="$(extract_data_for_validation "$TARGET_PATH")"
203
+ EXTRACT_EXIT=$?
204
+ fi
205
+
206
+ if [ "$EXTRACT_EXIT" -ne 0 ]; then
207
+ exit "$EXTRACT_EXIT"
208
+ fi
209
+
210
+ # Legacy pass-through: pre-spine artifacts have no YAML frontmatter at
211
+ # all (e.g., bets authored before schemas existed). Skip validation
212
+ # with WARN to nudge migration without BLOCKing routine work. WU-4
213
+ # migration brings these into conformance progressively.
214
+ if [ -z "$DATA_JSON" ] || [ "$DATA_JSON" = "{}" ] || [ "$DATA_JSON" = "null" ]; then
215
+ echo "[state-validate.sh] WARN: $TARGET_PATH ($ENTITY) has no YAML frontmatter — legacy pass-through. Migrate via WU-4 to add minimal metadata." >&2
216
+ exit 0
217
+ fi
218
+
219
+ # === Validate via ajv-cli ===
220
+
221
+ # Stage data as a temp JSON file for ajv -d.
222
+ # Note: ajv-cli detects format by file extension, and macOS mktemp -t
223
+ # appends random suffix AFTER the template, so .json must be explicitly
224
+ # tacked on via mv to ensure it's the trailing extension.
225
+ DATA_TMP_RAW="$(mktemp -t state-validate-XXXXXX)"
226
+ DATA_TMP="${DATA_TMP_RAW}.json"
227
+ mv "$DATA_TMP_RAW" "$DATA_TMP"
228
+ trap 'rm -f "$DATA_TMP"' EXIT
229
+ echo "$DATA_JSON" > "$DATA_TMP"
230
+
231
+ # Stage all schemas as -r refs so $ref resolution works (e.g., _reference, _value_prop, _appetite_actuals, step)
232
+ REF_ARGS=""
233
+ for ref_schema in "$SCHEMA_DIR"/_*.schema.json "$SCHEMA_DIR"/step.schema.json; do
234
+ if [ -f "$ref_schema" ] && [ "$ref_schema" != "$SCHEMA_FILE" ]; then
235
+ REF_ARGS="$REF_ARGS -r $ref_schema"
236
+ fi
237
+ done
238
+
239
+ # Run ajv-cli; silence its noisy npm warnings, capture only validation result
240
+ VALIDATION_OUTPUT="$(npx -y -p ajv-cli@5 -p ajv-formats@2 ajv validate \
241
+ --spec=draft2020 \
242
+ -s "$SCHEMA_FILE" \
243
+ $REF_ARGS \
244
+ -d "$DATA_TMP" \
245
+ -c ajv-formats 2>&1)"
246
+ VALIDATION_EXIT=$?
247
+
248
+ if [ "$VALIDATION_EXIT" -eq 0 ]; then
249
+ exit 0
250
+ fi
251
+
252
+ # === BLOCK with structured error ===
253
+
254
+ echo "" >&2
255
+ echo "🛑 STATE-VALIDATE — BLOCKED 🛑" >&2
256
+ echo "" >&2
257
+ echo "Entity: $ENTITY" >&2
258
+ echo "Schema: $SCHEMA_FILE" >&2
259
+ echo "Path: $TARGET_PATH" >&2
260
+ echo "" >&2
261
+ echo "Validation error:" >&2
262
+ # Strip noisy npm warnings; surface only ajv's actual error lines
263
+ echo "$VALIDATION_OUTPUT" | grep -v "^npm warn" | grep -v "^$" >&2
264
+ echo "" >&2
265
+ echo "Per .claude/rules/state-schema-validation.md: state-spine entity" >&2
266
+ echo "writes must conform to their schema. Either:" >&2
267
+ echo " 1. Fix the data to match the schema (preferred)" >&2
268
+ echo " 2. Update the schema if it's wrong (requires PR review)" >&2
269
+ echo " 3. Override with SKIP_STATE_VALIDATE=1 (logged; rare; not for normal work)" >&2
270
+ echo "" >&2
271
+ exit 3