@thebassclef/lite 1.1.1 → 1.2.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 (135) hide show
  1. package/dist/cli.cjs +23 -6
  2. package/dist/cli.js +23 -6
  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/bassclef-configs.jsonc +56 -0
  7. package/dist/lite/.claude/bassclef-orientation.md +120 -0
  8. package/dist/lite/.claude/hooks/adr-discipline-check.sh +132 -0
  9. package/dist/lite/.claude/hooks/atomic-pr-check.sh +169 -0
  10. package/dist/lite/.claude/hooks/auto-save-idle.sh +51 -0
  11. package/dist/lite/.claude/hooks/bassclef-sync.sh +51 -13
  12. package/dist/lite/.claude/hooks/bet-doc-gate.sh +283 -0
  13. package/dist/lite/.claude/hooks/bug-diagnosis.sh +64 -0
  14. package/dist/lite/.claude/hooks/destructive-command-guard.sh +136 -0
  15. package/dist/lite/.claude/hooks/file-rename-discipline-check.sh +147 -0
  16. package/dist/lite/.claude/hooks/hook-classifier-canary.sh +158 -0
  17. package/dist/lite/.claude/hooks/kiss-default-check.sh +129 -0
  18. package/dist/lite/.claude/hooks/multi-tenancy-check.sh +83 -0
  19. package/dist/lite/.claude/hooks/oo-ad-entry-point-check.sh +216 -0
  20. package/dist/lite/.claude/hooks/operator-private-path-check.sh +203 -0
  21. package/dist/lite/.claude/hooks/pattern-annotation-validate.sh +142 -0
  22. package/dist/lite/.claude/hooks/plain-english-steering.sh +15 -1
  23. package/dist/lite/.claude/hooks/post-write-trace.sh +66 -0
  24. package/dist/lite/.claude/hooks/pr-body-scrub-check.sh +446 -0
  25. package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +137 -0
  26. package/dist/lite/.claude/hooks/pre-gh-pr-body-scrub.sh +192 -0
  27. package/dist/lite/.claude/hooks/pre-gh-pr-title-scrub.sh +172 -0
  28. package/dist/lite/.claude/hooks/role-entry-gate.sh +115 -0
  29. package/dist/lite/.claude/hooks/save-state.sh +268 -0
  30. package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +8 -1
  31. package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +52 -0
  32. package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +40 -4
  33. package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +12 -5
  34. package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +18 -2
  35. package/dist/lite/.claude/hooks/session-reflection.d/91-npm-version-drift.sh +54 -0
  36. package/dist/lite/.claude/hooks/session-reflection.d/tests/10-abrupt-stop-recovery.test.sh +95 -0
  37. package/dist/lite/.claude/hooks/session-reflection.sh +62 -15
  38. package/dist/lite/.claude/hooks/tool-failure-diagnosis.sh +50 -0
  39. package/dist/lite/.claude/luminaries/andrej-karpathy.md +165 -0
  40. package/dist/lite/.claude/luminaries/dave-gray.md +155 -0
  41. package/dist/lite/.claude/luminaries/gary-klein.md +166 -0
  42. package/dist/lite/.claude/luminaries/kaoru-ishikawa.md +162 -0
  43. package/dist/lite/.claude/luminaries/lean-product-canvas.md +33 -0
  44. package/dist/lite/.claude/luminaries/luke-wroblewski.md +224 -0
  45. package/dist/lite/.claude/luminaries/ryan-singer.md +113 -0
  46. package/dist/lite/.claude/luminaries/steve-krug.md +168 -0
  47. package/dist/lite/.claude/luminaries/tony-ulwick.md +134 -0
  48. package/dist/lite/.claude/rules/arc-level-orientation-before-leaf.md +60 -0
  49. package/dist/lite/.claude/rules/architect-review-discipline.md +71 -0
  50. package/dist/lite/.claude/rules/auto-fix-criteria.md +126 -0
  51. package/dist/lite/.claude/rules/bassclef-hook-drift-blocker.md +144 -0
  52. package/dist/lite/.claude/rules/bassclef-models-discovery.md +107 -0
  53. package/dist/lite/.claude/rules/bassclef-workflow-parity.md +104 -0
  54. package/dist/lite/.claude/rules/cost-governance.md +154 -0
  55. package/dist/lite/.claude/rules/file-rename-discipline.md +95 -0
  56. package/dist/lite/.claude/rules/mechanism-fidelity.md +88 -0
  57. package/dist/lite/.claude/rules/prototype-production-coupling.md +125 -0
  58. package/dist/lite/.claude/rules/quick-fix-criteria.md +90 -0
  59. package/dist/lite/.claude/rules/session-artifacts.md +30 -0
  60. package/dist/lite/.claude/rules/sibling-smoke-after-substrate-change.md +107 -0
  61. package/dist/lite/.claude/rules/substrate-as-system.md +67 -0
  62. package/dist/lite/.claude/rules/usability.md +22 -0
  63. package/dist/lite/.claude/settings.json +20 -0
  64. package/dist/lite/.claude/skills/agent-research-spawn/SKILL.md +100 -0
  65. package/dist/lite/.claude/skills/architect-review/SKILL.md +201 -0
  66. package/dist/lite/.claude/skills/auto-fix/SKILL.md +297 -0
  67. package/dist/lite/.claude/skills/autonomous/SKILL.md +300 -0
  68. package/dist/lite/.claude/skills/bind-subdomain/SKILL.md +232 -0
  69. package/dist/lite/.claude/skills/canvas/SKILL.md +41 -0
  70. package/dist/lite/.claude/skills/deploy-prod/SKILL.md +106 -0
  71. package/dist/lite/.claude/skills/docs-sync/SKILL.md +188 -0
  72. package/dist/lite/.claude/skills/eisenhower/SKILL.md +273 -0
  73. package/dist/lite/.claude/skills/empathy-map/SKILL.md +168 -0
  74. package/dist/lite/.claude/skills/frontend-design/SKILL.md +53 -0
  75. package/dist/lite/.claude/skills/interview-me/SKILL.md +171 -0
  76. package/dist/lite/.claude/skills/journal-export/SKILL.md +293 -0
  77. package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +203 -0
  78. package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +256 -0
  79. package/dist/lite/.claude/skills/pick-luminaries/SKILL.md +100 -0
  80. package/dist/lite/.claude/skills/pre-mortem/SKILL.md +180 -0
  81. package/dist/lite/.claude/skills/prototype-gallery/SKILL.md +264 -0
  82. package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +257 -0
  83. package/dist/lite/.claude/skills/quick-fix/SKILL.md +273 -0
  84. package/dist/lite/.claude/skills/release/SKILL.md +311 -0
  85. package/dist/lite/.claude/skills/release-notes/SKILL.md +202 -0
  86. package/dist/lite/.claude/skills/rfc/SKILL.md +222 -0
  87. package/dist/lite/.claude/skills/riff-prototypes/SKILL.md +637 -0
  88. package/dist/lite/.claude/skills/secrets-bootstrap/SKILL.md +209 -0
  89. package/dist/lite/.claude/skills/skill/SKILL.md +304 -0
  90. package/dist/lite/.claude/skills/smoke-test/SKILL.md +100 -0
  91. package/dist/lite/.claude/skills/state/SKILL.md +204 -0
  92. package/dist/lite/.claude/skills/substrate-check/SKILL.md +218 -0
  93. package/dist/lite/.claude/skills/synthetic-user/SKILL.md +381 -0
  94. package/dist/lite/.claude/skills/task-scenarios/SKILL.md +105 -0
  95. package/dist/lite/.claude/skills/tier-dependency-audit/SKILL.md +139 -0
  96. package/dist/lite/.claude/skills/ux-migration/SKILL.md +424 -0
  97. package/dist/lite/.claude/skills/value-prop-canvas/SKILL.md +297 -0
  98. package/dist/lite/README.md +5 -1
  99. package/dist/lite/gitignore +6 -0
  100. package/dist/lite/lib/bassclef-dir-resolver.sh +85 -0
  101. package/dist/lite/lib/clone-failure-classifier.sh +59 -0
  102. package/dist/lite/lib/fixture-builder.sh +190 -0
  103. package/dist/lite/lib/fresh-install-check.sh +49 -0
  104. package/dist/lite/lib/hook-heartbeat.sh +0 -1
  105. package/dist/lite/lib/mechanism-fidelity.sh +569 -0
  106. package/dist/lite/lib/npm-version-check.sh +112 -0
  107. package/dist/lite/lib/preview-state.sh +229 -0
  108. package/dist/lite/lib/session-rescue.sh +0 -1
  109. package/dist/lite/scripts/generate-lite-manifest.sh +145 -4
  110. package/dist/lite/scripts/lite-manifest-drift-check.sh +5 -1
  111. package/dist/lite/standards/architect-review-discipline.md +222 -0
  112. package/dist/lite/standards/bassclef-platform.md +249 -0
  113. package/dist/lite/standards/bassclef-test-repos.md +97 -0
  114. package/dist/lite/standards/bassclef-wiring-manifest.json +25 -1
  115. package/dist/lite/standards/cold-adopter-harness.md +205 -0
  116. package/dist/lite/standards/data-management.md +135 -0
  117. package/dist/lite/standards/frontend-stack/nextjs-prisma.md +124 -0
  118. package/dist/lite/standards/lite-manifest-schema-changes.md +27 -0
  119. package/dist/lite/standards/lite-manifest.json +1456 -14
  120. package/dist/lite/standards/mechanism-fidelity.md +205 -0
  121. package/dist/lite/standards/operator-private-discipline.md +200 -0
  122. package/dist/lite/standards/operator-to-public-flow.md +195 -0
  123. package/dist/lite/standards/prototype-gallery.md +251 -0
  124. package/dist/lite/standards/pseudonym-key-schema.md +109 -0
  125. package/dist/lite/standards/sibling-validation-checklist.md +181 -0
  126. package/dist/lite/standards/slack-notifications.md +161 -0
  127. package/dist/lite/standards/synthetic-user-testing.md +173 -0
  128. package/dist/lite/standards/vendor-bindings.md +165 -0
  129. package/dist/lite/standards/vendor-plugins/frontend-design.md +66 -0
  130. package/dist/lite/standards/vpc-fit-validation.md +167 -0
  131. package/dist/lite/templates/gallery-yml.md +113 -0
  132. package/dist/lite/templates/product-canvas.md +169 -0
  133. package/dist/lite/templates/value-prop-canvas.md +180 -0
  134. package/package.json +3 -3
  135. /package/dist/lite/{whereami.md → docs/whereami.md} +0 -0
@@ -0,0 +1,49 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ #
4
+ # fresh-install-check.sh — is this a fresh-install repo or an established one?
5
+ # Cures sunj-labs/bassclef-cli#102 + sunj-labs/bassclef-cli#108 where two
6
+ # session-start fragments (55-orientation-gate + 10-abrupt-stop-recovery)
7
+ # false-fire on the first session after `bassclef init` in an empty repo.
8
+ #
9
+ # A repo is fresh-install when BOTH conditions hold:
10
+ # 1. No git commits (git rev-parse HEAD fails), AND
11
+ # 2. No session lock at .claude/SESSION_LOCK
12
+ #
13
+ # The composite catches the cli#108 case (init leaves 379 untracked files
14
+ # but no commits, and no prior session) while preserving the genuine-stop
15
+ # case (a real session stops abruptly and leaves a lock — that lock alone
16
+ # fires the check even before any commit).
17
+ #
18
+ # Contract: exit 0 = fresh, exit 1 = established (or defensive fallback).
19
+ # Silent — no stderr output on any path. Never crashes the parent.
20
+ #
21
+ # Anchor luminaries:
22
+ # @luminary john-ousterhout — deep module (composite predicate hidden
23
+ # behind zero-arg function)
24
+ # @luminary tony-hoare — postcondition (exit code contract)
25
+ # @luminary michael-nygard — fail-soft (missing git = defensive-1;
26
+ # assume not fresh; skip false-silence)
27
+
28
+ # @pattern patterns/code/gof/strategy.md
29
+ # Composite strategy — two discriminators combined via short-circuit AND.
30
+ is_fresh_install() {
31
+ # Defensive: if git is not on PATH, assume not-fresh — better a false
32
+ # BLOCKED banner an operator can dismiss than a silent-skip that hides
33
+ # a real state.
34
+ command -v git >/dev/null 2>&1 || return 1
35
+
36
+ # Discriminator 1: git HEAD does not resolve → no commits
37
+ if git rev-parse HEAD >/dev/null 2>&1; then
38
+ return 1
39
+ fi
40
+
41
+ # Discriminator 2: session lock present → interrupted session
42
+ if [ -f ".claude/SESSION_LOCK" ]; then
43
+ return 1
44
+ fi
45
+
46
+ return 0
47
+ }
48
+
49
+ export -f is_fresh_install 2>/dev/null || true
@@ -6,7 +6,6 @@
6
6
  # hides the marker path convention, atomic write pattern, format
7
7
  # schema, directory creation, and silent-fail behavior.
8
8
  #
9
- # tier: upstream
10
9
  # install-class: dual
11
10
  #
12
11
  # Interface (what callers know):
@@ -0,0 +1,569 @@
1
+ #!/usr/bin/env bash
2
+ # tier: lite
3
+ # install-class: dual
4
+ # lib/mechanism-fidelity.sh — shared scanner for the mechanism-fidelity
5
+ # audit per standards/mechanism-fidelity.md.
6
+ #
7
+ # This library is SOURCED by the audit skill (.claude/skills/audit-rule-
8
+ # mechanism-fidelity/SKILL.md — lands in WU-4) AND by the cold-adopter
9
+ # harness check classes (scripts/cold-adopter-harness-sync.sh extension —
10
+ # lands in WU-5). Single source of truth per @luminary john-ousterhout
11
+ # (deep modules).
12
+ #
13
+ # Authority: ADR-035 (substrate-as-system tenet)
14
+ # Tier 0 strict TDD: lib/tests/mechanism-fidelity.test.sh
15
+ # Anchors: @luminary michael-feathers (legacy code = code without tests),
16
+ # @luminary kent-beck (TDD), @luminary tony-hoare (pre/post).
17
+ #
18
+ # Exposed functions:
19
+ # scan_rule_for_mechanism_claims <rule-path> # extracts mechanism refs
20
+ # verify_hook_exists <hook-path> # step 1 of verification chain
21
+ # verify_hook_wired <hook-name> # step 2 (settings.json wiring)
22
+ # verify_path_resolution <hook-path> # step 4 (Class A check)
23
+ # verify_hook_test_pins_contract <hook-or-lib> # step 3 (Tier 0 test present)
24
+ # classify_hook_install_pattern <hook-name> [root] # detects ci|launchd|lib|standard
25
+ # classify_finding [--flags] # returns 1 of 10 labels
26
+ #
27
+ # Env vars consumed:
28
+ # CLAUDE_PROJECT_DIR — project root
29
+ # HOME_OVERRIDE — test-only $HOME replacement
30
+ # USER_SETTINGS_OVERRIDE — test-only user settings.json replacement
31
+ # CANONICAL_MANIFEST_OVERRIDE — test-only canonical wiring manifest path override
32
+ # (per bet 2026-08-05c: verify_hook_wired reads
33
+ # standards/bassclef-wiring-manifest.json as an
34
+ # added source of hook wiring; shipped 2026-08-04h)
35
+
36
+ # === Resolve $HOME with test-override support ===
37
+ __mech_fidelity_home() {
38
+ echo "${HOME_OVERRIDE:-$HOME}"
39
+ }
40
+
41
+ # === Resolve user settings.json with test-override support ===
42
+ __mech_fidelity_user_settings() {
43
+ if [ -n "${USER_SETTINGS_OVERRIDE:-}" ]; then
44
+ echo "$USER_SETTINGS_OVERRIDE"
45
+ else
46
+ echo "$(__mech_fidelity_home)/.claude/settings.json"
47
+ fi
48
+ }
49
+
50
+ # === Resolve canonical wiring manifest path with test-override support ===
51
+ # Per bet 2026-08-05c: verify_hook_wired reads this file as an added source
52
+ # of hook wiring alongside project + user settings.json. The manifest shipped
53
+ # 2026-08-04h per design/discoveries/2026-08-04-managed-settings-d-as-bassclef-wiring-channel.md.
54
+ __mech_fidelity_canonical_manifest() {
55
+ if [ -n "${CANONICAL_MANIFEST_OVERRIDE:-}" ]; then
56
+ echo "$CANONICAL_MANIFEST_OVERRIDE"
57
+ else
58
+ echo "${CLAUDE_PROJECT_DIR:-}/standards/bassclef-wiring-manifest.json"
59
+ fi
60
+ }
61
+
62
+ # === _is_placeholder_hook_name ===
63
+ # Match candidate hook names against conventional example / placeholder
64
+ # names that rule bodies use as illustrations. Real hook names are
65
+ # descriptive (verb-object shape). Bare placeholders leak as UNWIRED
66
+ # audit noise per sunj-labs/bassclef-upstream#1164.
67
+ #
68
+ # Kept STRICT (exact bare names only) to avoid false negatives on real
69
+ # hooks that happen to prefix/suffix with a placeholder root (e.g.,
70
+ # foo-hook.sh, install-test.sh). Add new entries as new noise leaks
71
+ # into future audits.
72
+ #
73
+ # Returns 0 (true) when candidate is a bare placeholder name.
74
+ _is_placeholder_hook_name() {
75
+ local name="${1:-}"
76
+ case "$name" in
77
+ foo.sh|bar.sh|baz.sh|test.sh) return 0 ;;
78
+ example.sh|template.sh|sample.sh) return 0 ;;
79
+ whatever.sh|myhook.sh) return 0 ;;
80
+ esac
81
+ return 1
82
+ }
83
+
84
+ # === scan_rule_for_mechanism_claims ===
85
+ # Greps a rule body for mechanism claims (hook references, event names,
86
+ # matchers). Returns one claim per line (compact form: hook-name + optional
87
+ # event/matcher hint).
88
+ #
89
+ # Args: $1 — rule file path
90
+ # Stdout: claims, one per line (empty if no claims)
91
+ # Exit: 0 success (even if zero claims); 1 file missing; 2 missing arg
92
+ scan_rule_for_mechanism_claims() {
93
+ local rule="${1:-}"
94
+ local project_root="${2:-${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}}"
95
+ if [ -z "$rule" ]; then
96
+ echo "scan_rule_for_mechanism_claims: missing rule arg" >&2
97
+ return 2
98
+ fi
99
+ if [ ! -f "$rule" ]; then
100
+ echo "scan_rule_for_mechanism_claims: file not found: $rule" >&2
101
+ return 1
102
+ fi
103
+
104
+ # Pattern 1: "<name>-hook.sh" or "<name>.sh" anywhere in body (most direct)
105
+ # Pattern 2: "Mechanical enforcement" section header (formal claim)
106
+ # Pattern 3: "fires at PreToolUse|PostToolUse|Stop|SessionStart|UserPromptSubmit"
107
+
108
+ # Extract candidate .sh references
109
+ # Regex accepts capital letters at match start + inside the name (bassclef-upstream#1423).
110
+ # Prior lowercase-only pattern silently dropped leading capital letters — e.g.,
111
+ # `Bassclef-sync.sh` at sentence start extracted as phantom `assclef-sync.sh`.
112
+ # Normalize to lowercase before dedup so the file lookup below finds the real file.
113
+ local candidates
114
+ candidates=$(grep -oE '[A-Za-z0-9][a-zA-Z0-9-]*-?[a-zA-Z0-9]*\.sh' "$rule" 2>/dev/null \
115
+ | tr '[:upper:]' '[:lower:]' \
116
+ | sort -u)
117
+ [ -z "$candidates" ] && return 0
118
+
119
+ # Filter to reduce false positives (bet 28a WU-4):
120
+ # - Emit candidate IF .sh exists in .claude/hooks/ (true positive — real hook)
121
+ # - Skip candidate IF .sh exists elsewhere in repo (lib/script/test/fixture; not a hook claim)
122
+ # - Emit candidate IF .sh does not exist anywhere AND has hook-context proximity
123
+ # ("hook", "fires at", "wired", "enforced by", "mechanism", "matcher") within ±2 lines
124
+ while IFS= read -r candidate; do
125
+ [ -z "$candidate" ] && continue
126
+ # True positive: real hook file
127
+ if [ -f "$project_root/.claude/hooks/$candidate" ]; then
128
+ echo "$candidate"
129
+ continue
130
+ fi
131
+ # Check if file exists elsewhere in repo (excluding .git/)
132
+ local found_elsewhere=0
133
+ if [ -d "$project_root" ]; then
134
+ if find "$project_root" -name .git -prune -o -type f -name "$candidate" -print 2>/dev/null | grep -q .; then
135
+ found_elsewhere=1
136
+ fi
137
+ fi
138
+ if [ "$found_elsewhere" -eq 1 ]; then
139
+ # File exists but not in .claude/hooks/ — not a hook claim, skip
140
+ continue
141
+ fi
142
+ # (bassclef-upstream#1164) Placeholder-name filter — skip conventional
143
+ # example filenames (foo.sh, test.sh, template.sh, bar.sh, etc.) that
144
+ # rule bodies use as illustrations. Real hook names are descriptive;
145
+ # placeholders leak as UNWIRED audit noise.
146
+ if _is_placeholder_hook_name "$candidate"; then
147
+ continue
148
+ fi
149
+ # File doesn't exist anywhere. Check negative-context patterns first
150
+ # (sibling/example/renamed/library references; bet 28a WU-6 + bet 2026-08-05c WU-4).
151
+ # Skip when candidate appears within ±2 lines of:
152
+ # "sibling of" "alias of" "renamed from/to" "former name"
153
+ # "Example for/of" "e.g.," "such as"
154
+ # "sourced by" "library" "CI hook" "GitHub Actions" "workflow"
155
+ # "launchd" "cron" "scheduled job"
156
+ # Deferred-future patterns (bet 2026-08-05c WU-4 CI cure) — rules that
157
+ # cite hooks planned for later ship are not active claims:
158
+ # "deferred (follow-on|to|until)"
159
+ # "will (add|ship|introduce|land)"
160
+ # "planned (hook|for|to)"
161
+ # "not yet"
162
+ # Dropped "V[0-9]+ (mechanical|...)" — too broad; matches "V2 mechanical
163
+ # layer" heading even when adjacent to an unrelated active claim.
164
+ local nearby
165
+ nearby=$(grep -B2 -A2 -F "$candidate" "$rule" 2>/dev/null)
166
+ if echo "$nearby" | grep -qiE "(sibling of|alias of|renamed (from|to)|former name|Example (for|of)|e\.g\.,|such as|sourced by|library|CI hook|GitHub Actions|workflow|launchd|cron|scheduled job|deferred (follow-on|to|until)|will (add|ship|introduce|land)|planned (hook|for|to)|not yet)"; then
167
+ continue
168
+ fi
169
+ # Check hook-context proximity (within ±2 lines).
170
+ if echo "$nearby" | grep -qiE "(hook|fires at|wired|enforced by|mechanism|matcher)"; then
171
+ echo "$candidate"
172
+ fi
173
+ done <<< "$candidates"
174
+ return 0
175
+ }
176
+
177
+ # === verify_hook_exists ===
178
+ # Step 1 of the 5-step verification chain.
179
+ # Args: $1 — hook file path (absolute or relative to repo)
180
+ # Exit: 0 file exists; 1 file missing; 2 missing arg
181
+ verify_hook_exists() {
182
+ local hook="${1:-}"
183
+ if [ -z "$hook" ]; then
184
+ echo "verify_hook_exists: missing arg" >&2
185
+ return 2
186
+ fi
187
+ if [ -f "$hook" ]; then
188
+ return 0
189
+ fi
190
+ echo "verify_hook_exists: not found: $hook" >&2
191
+ return 1
192
+ }
193
+
194
+ # === verify_hook_wired ===
195
+ # Step 2 of the 5-step chain. Greps project settings, user settings, AND
196
+ # the canonical wiring manifest for the hook name appearing in any command
197
+ # path. Returns 0 on FIRST match (short-circuits per R1 pre-mortem).
198
+ # Args: $1 — hook base name (e.g., "wired-hook.sh")
199
+ # Exit: 0 wired in at least one source; 1 wired nowhere; 2 missing arg
200
+ # Sources checked (in order): project settings.json, user settings.json,
201
+ # bassclef-wiring-manifest.json
202
+ # Per bet 2026-08-05c: added the canonical manifest source. Prior versions
203
+ # missed hooks wired only in the manifest → classified them DEAD-LETTER.
204
+ verify_hook_wired() {
205
+ local hook_name="${1:-}"
206
+ if [ -z "$hook_name" ]; then
207
+ echo "verify_hook_wired: missing arg" >&2
208
+ return 2
209
+ fi
210
+
211
+ local project_settings="${CLAUDE_PROJECT_DIR:-}/.claude/settings.json"
212
+ local user_settings
213
+ user_settings="$(__mech_fidelity_user_settings)"
214
+ local canonical_manifest
215
+ canonical_manifest="$(__mech_fidelity_canonical_manifest)"
216
+
217
+ if [ -f "$project_settings" ] && grep -qF "$hook_name" "$project_settings" 2>/dev/null; then
218
+ return 0
219
+ fi
220
+ if [ -f "$user_settings" ] && grep -qF "$hook_name" "$user_settings" 2>/dev/null; then
221
+ return 0
222
+ fi
223
+ if [ -f "$canonical_manifest" ] && grep -qF "$hook_name" "$canonical_manifest" 2>/dev/null; then
224
+ return 0
225
+ fi
226
+
227
+ echo "verify_hook_wired: '$hook_name' not in project settings, user settings, or canonical manifest" >&2
228
+ return 1
229
+ }
230
+
231
+ # === verify_path_resolution ===
232
+ # Step 4 of the 5-step chain. Checks for Class A pattern: hook uses
233
+ # SCRIPT_DIR/../../ to reach project resources but is declared install-class
234
+ # operator (mismatch). Reads the install-class header.
235
+ # Args: $1 — hook file path
236
+ # Exit: 0 safe; 1 unsafe (Class A) or no install-class declared with risky pattern
237
+ verify_path_resolution() {
238
+ local hook="${1:-}"
239
+ if [ -z "$hook" ] || [ ! -f "$hook" ]; then
240
+ echo "verify_path_resolution: hook missing: $hook" >&2
241
+ return 2
242
+ fi
243
+
244
+ # Check if hook uses SCRIPT_DIR/../../ pattern (the Class A risk pattern)
245
+ local uses_risky_pattern=0
246
+ if grep -qE 'SCRIPT_DIR.*\.\./\.\.' "$hook"; then
247
+ uses_risky_pattern=1
248
+ fi
249
+
250
+ # No risky pattern → safe regardless of install-class
251
+ if [ $uses_risky_pattern -eq 0 ]; then
252
+ return 0
253
+ fi
254
+
255
+ # Risky pattern present — install-class header decides
256
+ local install_class
257
+ install_class=$(grep -m1 '^# install-class:' "$hook" | sed 's/^# install-class:[[:space:]]*//' | tr -d '[:space:]')
258
+
259
+ case "$install_class" in
260
+ dual)
261
+ # Author declared lib/hook-inject.sh resolve-helpers handle both layouts
262
+ return 0
263
+ ;;
264
+ project)
265
+ # Project-class wiring + project-relative reads = safe
266
+ return 0
267
+ ;;
268
+ operator)
269
+ # Class A failure — operator-class wiring + project-relative reads
270
+ echo "verify_path_resolution: Class A failure — '$hook' uses SCRIPT_DIR/../../ but declares install-class: operator" >&2
271
+ echo " Under \$HOME wiring, SCRIPT_DIR/../../<resource> resolves to nonexistent path" >&2
272
+ echo " Fix: declare install-class: dual + use lib/hook-inject.sh resolve-helpers" >&2
273
+ return 1
274
+ ;;
275
+ "")
276
+ # No header — defaults to project; but risky pattern with no declaration warns
277
+ echo "verify_path_resolution: '$hook' uses SCRIPT_DIR/../../ with no install-class header" >&2
278
+ echo " Default is 'project' but the declaration should be explicit per standards/hook-install-class.md" >&2
279
+ return 1
280
+ ;;
281
+ *)
282
+ echo "verify_path_resolution: '$hook' declares unknown install-class: '$install_class'" >&2
283
+ return 1
284
+ ;;
285
+ esac
286
+ }
287
+
288
+ # === verify_hook_test_pins_contract ===
289
+ # Step 3 of the 5-step chain. Checks for sibling Tier 0 test file.
290
+ # Convention: <dir>/<name>.sh has test at <dir>/tests/<name>.test.sh
291
+ # Args: $1 — hook or lib path
292
+ # Exit: 0 sibling test exists; 1 no sibling test; 2 missing arg
293
+ verify_hook_test_pins_contract() {
294
+ local source="${1:-}"
295
+ if [ -z "$source" ]; then
296
+ echo "verify_hook_test_pins_contract: missing arg" >&2
297
+ return 2
298
+ fi
299
+
300
+ local dir base
301
+ dir="$(dirname "$source")"
302
+ base="$(basename "$source" .sh)"
303
+
304
+ if [ -f "$dir/tests/$base.test.sh" ]; then
305
+ return 0
306
+ fi
307
+
308
+ echo "verify_hook_test_pins_contract: no test at $dir/tests/$base.test.sh" >&2
309
+ return 1
310
+ }
311
+
312
+ # === classify_hook_install_pattern ===
313
+ # Determines how a hook is meant to be installed. Some hooks live in
314
+ # .github/workflows/*.yml (CI hooks), some are scheduled by launchd, some
315
+ # are sourced as libraries by other hooks — none of these belong in
316
+ # .claude/settings.json. The audit needs this signal so it does not flag
317
+ # them as DEAD-LETTER when they correctly do not appear in settings.json.
318
+ #
319
+ # Detection priority:
320
+ # 1. CI — hook name appears in any .github/workflows/*.yml
321
+ # 2. launchd — first 10 lines of the hook header mention launchd / cron / scheduled
322
+ # 3. lib — first 10 lines mention "sourced by" (library used by other hooks)
323
+ # 4. standard — default; hook is meant to live in settings.json
324
+ #
325
+ # CI takes precedence over launchd: a hook can be both scheduled locally
326
+ # AND fire in CI; the CI presence is the dominant signal because the
327
+ # workflow IS the wiring.
328
+ #
329
+ # Args: $1 — hook base name (e.g., "pr-scrub.sh")
330
+ # $2 — project root (default: $CLAUDE_PROJECT_DIR or lib parent dir)
331
+ # Stdout: one of "ci", "launchd", "lib", "standard"
332
+ # Exit: 0 always (default "standard" is safe); 2 missing arg
333
+ classify_hook_install_pattern() {
334
+ local hook_name="${1:-}"
335
+ local project_root="${2:-${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}}"
336
+
337
+ if [ -z "$hook_name" ]; then
338
+ echo "classify_hook_install_pattern: missing arg" >&2
339
+ return 2
340
+ fi
341
+
342
+ # Layer 2 — read pattern list from standards/hook-invocation-patterns.json
343
+ # when available (sunj-labs/bassclef-upstream#1163). Falls back to
344
+ # hardcoded case cascade if JSON missing OR jq unavailable — keeps
345
+ # existing tests passing per @luminary michael-feathers characterization.
346
+ local patterns_json="$project_root/standards/hook-invocation-patterns.json"
347
+ if [ -f "$patterns_json" ] && command -v jq >/dev/null 2>&1; then
348
+ local result
349
+ result=$(_classify_via_patterns_json "$hook_name" "$project_root" "$patterns_json") || result=""
350
+ if [ -n "$result" ]; then
351
+ echo "$result"
352
+ return 0
353
+ fi
354
+ fi
355
+
356
+ # Fallback — hardcoded cascade (identical to pre-Layer-2 behavior)
357
+ # 1. CI — workflow reference wins
358
+ if [ -d "$project_root/.github/workflows" ]; then
359
+ if grep -qrlF "$hook_name" "$project_root/.github/workflows/" 2>/dev/null; then
360
+ echo "ci"
361
+ return 0
362
+ fi
363
+ fi
364
+
365
+ # 1.5 Release
366
+ local release_script="$project_root/scripts/release-to-bassclef.sh"
367
+ if [ -f "$release_script" ]; then
368
+ if grep -qF "$hook_name" "$release_script" 2>/dev/null; then
369
+ echo "release"
370
+ return 0
371
+ fi
372
+ fi
373
+
374
+ # 2/3 — header inspection
375
+ local hook_path="$project_root/.claude/hooks/$hook_name"
376
+ if [ -f "$hook_path" ]; then
377
+ local header
378
+ header=$(head -10 "$hook_path" 2>/dev/null)
379
+ if echo "$header" | grep -qiE "(launchd|launch agent|scheduled job|via cron|cron job)"; then
380
+ echo "launchd"
381
+ return 0
382
+ fi
383
+ if echo "$header" | grep -qiE "(sourced by|sourced into|sourced from)"; then
384
+ echo "lib"
385
+ return 0
386
+ fi
387
+ fi
388
+
389
+ # 4 — default
390
+ echo "standard"
391
+ return 0
392
+ }
393
+
394
+ # === _classify_via_patterns_json (private helper for Layer 2) ===
395
+ # Reads standards/hook-invocation-patterns.json + iterates entries
396
+ # in precedence order. First match wins. Returns pattern name on
397
+ # stdout OR empty string when nothing matches (fallback to hardcoded).
398
+ #
399
+ # Args: $1 hook_name, $2 project_root, $3 patterns_json_path
400
+ _classify_via_patterns_json() {
401
+ local hook_name="$1"
402
+ local project_root="$2"
403
+ local patterns_json="$3"
404
+
405
+ # Extract patterns sorted by precedence ASC — each line is TSV:
406
+ # name<TAB>detect.kind<TAB>detect.arg
407
+ local pattern_rows
408
+ pattern_rows=$(jq -r '.patterns | sort_by(.precedence)[] | [.name, .detect.kind, (.detect.arg // "")] | @tsv' "$patterns_json" 2>/dev/null) || return 1
409
+ [ -z "$pattern_rows" ] && return 1
410
+
411
+ local IFS=$'\n'
412
+ local row
413
+ for row in $pattern_rows; do
414
+ local name kind arg
415
+ name=$(echo "$row" | cut -f1)
416
+ kind=$(echo "$row" | cut -f2)
417
+ arg=$(echo "$row" | cut -f3)
418
+
419
+ case "$kind" in
420
+ workflow_grep)
421
+ if [ -d "$project_root/$arg" ]; then
422
+ if grep -qrlF "$hook_name" "$project_root/$arg/" 2>/dev/null; then
423
+ echo "$name"
424
+ return 0
425
+ fi
426
+ fi
427
+ ;;
428
+ script_grep)
429
+ if [ -f "$project_root/$arg" ]; then
430
+ if grep -qF "$hook_name" "$project_root/$arg" 2>/dev/null; then
431
+ echo "$name"
432
+ return 0
433
+ fi
434
+ fi
435
+ ;;
436
+ header_regex)
437
+ local hook_path="$project_root/.claude/hooks/$hook_name"
438
+ if [ -f "$hook_path" ]; then
439
+ local header
440
+ header=$(head -10 "$hook_path" 2>/dev/null)
441
+ if echo "$header" | grep -qiE "$arg"; then
442
+ echo "$name"
443
+ return 0
444
+ fi
445
+ fi
446
+ ;;
447
+ default)
448
+ echo "$name"
449
+ return 0
450
+ ;;
451
+ esac
452
+ done
453
+
454
+ # No match — signal fallback
455
+ return 1
456
+ }
457
+
458
+ # === _install_pattern_is_valid (private helper for Layer 2) ===
459
+ # Returns 0 if pattern is in the JSON pattern list OR in the hardcoded
460
+ # fallback list (ci|launchd|lib|release|standard). Returns 1 otherwise.
461
+ _install_pattern_is_valid() {
462
+ local candidate="${1:-}"
463
+ [ -z "$candidate" ] && return 1
464
+
465
+ # Try JSON first
466
+ local project_root="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
467
+ local patterns_json="$project_root/standards/hook-invocation-patterns.json"
468
+ if [ -f "$patterns_json" ] && command -v jq >/dev/null 2>&1; then
469
+ local names
470
+ names=$(jq -r '.patterns[].name' "$patterns_json" 2>/dev/null) || names=""
471
+ if [ -n "$names" ]; then
472
+ local n
473
+ while IFS= read -r n; do
474
+ [ "$n" = "$candidate" ] && return 0
475
+ done <<< "$names"
476
+ return 1
477
+ fi
478
+ fi
479
+
480
+ # Fallback — hardcoded list
481
+ case "$candidate" in
482
+ ci|launchd|lib|release|standard) return 0 ;;
483
+ *) return 1 ;;
484
+ esac
485
+ }
486
+
487
+ # === classify_finding ===
488
+ # Returns 1 of 10 classification labels per standards/mechanism-fidelity.md.
489
+ # Flags (exit codes from each verification step):
490
+ # --exists=N step 1: verify_hook_exists exit code (0 OK, 1 missing)
491
+ # --wired=N step 2: verify_hook_wired exit code (0 OK, 1 unwired)
492
+ # --path-ok=N step 4: verify_path_resolution exit code (0 OK, 1 broken)
493
+ # --test=N step 3: verify_hook_test_pins_contract exit code (0 OK, 1 missing)
494
+ # --install-pattern=V install pattern (ci|launchd|lib|standard) — bet 29a WU-1
495
+ # Overrides DEAD-LETTER → NOT-WIRED-BY-DESIGN-{CI|LAUNCHD|LIB}
496
+ # when the hook correctly lives outside settings.json
497
+ # --methodology-only short-circuit when the rule names no mechanism
498
+ #
499
+ # Stdout: one of CLEAN / DEAD-LETTER / UNWIRED / PATH-BROKEN / TEST-MISSING /
500
+ # METHODOLOGY-ONLY / SILENT-PASS / NOT-WIRED-BY-DESIGN-CI /
501
+ # NOT-WIRED-BY-DESIGN-LAUNCHD / NOT-WIRED-BY-DESIGN-LIB
502
+ # Exit: 0 success; 2 no args OR unknown --install-pattern value
503
+ classify_finding() {
504
+ if [ $# -eq 0 ]; then
505
+ echo "classify_finding: no args" >&2
506
+ return 2
507
+ fi
508
+
509
+ local exists wired path_ok test_present methodology_only=0 install_pattern="standard"
510
+ for arg in "$@"; do
511
+ case "$arg" in
512
+ --exists=*) exists="${arg#*=}" ;;
513
+ --wired=*) wired="${arg#*=}" ;;
514
+ --path-ok=*) path_ok="${arg#*=}" ;;
515
+ --test=*) test_present="${arg#*=}" ;;
516
+ --install-pattern=*)
517
+ install_pattern="${arg#*=}"
518
+ # Layer 2 (bassclef-upstream#1163) — validate against JSON
519
+ # pattern list when available; fall back to hardcoded enum.
520
+ if ! _install_pattern_is_valid "$install_pattern"; then
521
+ echo "classify_finding: unknown install-pattern value: $install_pattern" >&2
522
+ return 2
523
+ fi
524
+ ;;
525
+ --methodology-only) methodology_only=1 ;;
526
+ *) echo "classify_finding: unknown arg: $arg" >&2; return 2 ;;
527
+ esac
528
+ done
529
+
530
+ # Short-circuit on methodology-only
531
+ if [ "$methodology_only" -eq 1 ]; then
532
+ echo "METHODOLOGY-ONLY"
533
+ return 0
534
+ fi
535
+
536
+ # Classification cascade
537
+ if [ "$exists" = "1" ]; then
538
+ echo "UNWIRED"
539
+ return 0
540
+ fi
541
+ if [ "$wired" = "1" ]; then
542
+ # Hook lives on disk but settings.json wiring absent.
543
+ # If the hook is meant to live elsewhere (CI / launchd / sourced lib),
544
+ # this is by design, not a failure.
545
+ # Layer 2 (bassclef-upstream#1163) — algorithmic label derivation:
546
+ # "standard" → DEAD-LETTER; anything else → NOT-WIRED-BY-DESIGN-<UPPER>.
547
+ # New patterns land in standards/hook-invocation-patterns.json; their
548
+ # classification label flows through this branch without cascade edits.
549
+ if [ "$install_pattern" = "standard" ]; then
550
+ echo "DEAD-LETTER"
551
+ return 0
552
+ fi
553
+ local upper
554
+ upper=$(echo "$install_pattern" | tr '[:lower:]' '[:upper:]')
555
+ echo "NOT-WIRED-BY-DESIGN-${upper}"
556
+ return 0
557
+ fi
558
+ if [ "$path_ok" = "1" ]; then
559
+ echo "PATH-BROKEN"
560
+ return 0
561
+ fi
562
+ if [ "$test_present" = "1" ]; then
563
+ echo "TEST-MISSING"
564
+ return 0
565
+ fi
566
+
567
+ echo "CLEAN"
568
+ return 0
569
+ }