@thebassclef/lite 0.1.2 → 1.0.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 (293) hide show
  1. package/dist/cli.cjs +238 -127
  2. package/dist/cli.js +240 -129
  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/.bassclef-source.json +10 -0
  7. package/dist/lite/.claude/settings.json +212 -0
  8. package/dist/lite/CLAUDE.md +41 -0
  9. package/dist/lite/gitignore +58 -0
  10. package/dist/lite/standards/bassclef-wiring-manifest.json +497 -0
  11. package/dist/lite/whereami.md +24 -0
  12. package/package.json +8 -2
  13. package/substrate/.bassclef/lite-manifest.json +0 -2701
  14. package/substrate/.claude/agents/architect.md +0 -70
  15. package/substrate/.claude/agents/builder.md +0 -114
  16. package/substrate/.claude/agents/designer.md +0 -156
  17. package/substrate/.claude/agents/reviewer.md +0 -88
  18. package/substrate/.claude/hooks/artifact-ingestion-gate.sh +0 -357
  19. package/substrate/.claude/hooks/assert-verify-steering.sh +0 -77
  20. package/substrate/.claude/hooks/bassclef-source-config-validate.sh +0 -215
  21. package/substrate/.claude/hooks/bassclef-sync.sh +0 -634
  22. package/substrate/.claude/hooks/compound-noun-scrub.sh +0 -292
  23. package/substrate/.claude/hooks/kiss-expansion-inject.sh +0 -69
  24. package/substrate/.claude/hooks/longrun-prep-compounding-axis-check.sh +0 -492
  25. package/substrate/.claude/hooks/longrun-prep-compounding-sequence-check.sh +0 -492
  26. package/substrate/.claude/hooks/plain-english-steering.sh +0 -156
  27. package/substrate/.claude/hooks/post-skill-friction-check.sh +0 -177
  28. package/substrate/.claude/hooks/post-skill-telemetry.sh +0 -62
  29. package/substrate/.claude/hooks/pre-build-gate.sh +0 -511
  30. package/substrate/.claude/hooks/pre-commit-gate.sh +0 -451
  31. package/substrate/.claude/hooks/session-end.sh +0 -433
  32. package/substrate/.claude/hooks/session-reflection.sh +0 -303
  33. package/substrate/.claude/hooks/skill-body-grade-gate.sh +0 -219
  34. package/substrate/.claude/hooks/skill-body-intent-drift.sh +0 -107
  35. package/substrate/.claude/hooks/skill-step-list-check.sh +0 -171
  36. package/substrate/.claude/hooks/state-validate.sh +0 -271
  37. package/substrate/.claude/hooks/substrate-clarity-gate.sh +0 -1110
  38. package/substrate/.claude/hooks/temperance-gate.sh +0 -147
  39. package/substrate/.claude/hooks/testing-tier-enforce.sh +0 -233
  40. package/substrate/.claude/hooks/turn-prose-grade-measure.sh +0 -219
  41. package/substrate/.claude/hooks/turn-prose-kiss-check.sh +0 -463
  42. package/substrate/.claude/hooks/vocabulary-migration-check.sh +0 -171
  43. package/substrate/.claude/hooks/whereami-utc-gate.sh +0 -142
  44. package/substrate/.claude/luminaries/alan-cooper.md +0 -170
  45. package/substrate/.claude/luminaries/alistair-cockburn.md +0 -140
  46. package/substrate/.claude/luminaries/amazon-pr-faq.md +0 -34
  47. package/substrate/.claude/luminaries/ash-maurya.md +0 -121
  48. package/substrate/.claude/luminaries/bill-buxton.md +0 -210
  49. package/substrate/.claude/luminaries/charles-sanders-peirce.md +0 -150
  50. package/substrate/.claude/luminaries/david-ogilvy.md +0 -192
  51. package/substrate/.claude/luminaries/don-norman.md +0 -173
  52. package/substrate/.claude/luminaries/edward-tufte.md +0 -179
  53. package/substrate/.claude/luminaries/eric-evans.md +0 -160
  54. package/substrate/.claude/luminaries/frederick-brooks.md +0 -50
  55. package/substrate/.claude/luminaries/gang-of-four.md +0 -157
  56. package/substrate/.claude/luminaries/glenford-myers.md +0 -230
  57. package/substrate/.claude/luminaries/hunt-thomas.md +0 -115
  58. package/substrate/.claude/luminaries/hyrum-wright.md +0 -94
  59. package/substrate/.claude/luminaries/jason-fried-dhh.md +0 -46
  60. package/substrate/.claude/luminaries/jesse-james-garrett.md +0 -154
  61. package/substrate/.claude/luminaries/john-ousterhout.md +0 -94
  62. package/substrate/.claude/luminaries/karl-popper.md +0 -132
  63. package/substrate/.claude/luminaries/kent-beck.md +0 -168
  64. package/substrate/.claude/luminaries/linus-torvalds.md +0 -218
  65. package/substrate/.claude/luminaries/martin-fowler.md +0 -164
  66. package/substrate/.claude/luminaries/michael-feathers.md +0 -125
  67. package/substrate/.claude/luminaries/michael-nygard.md +0 -118
  68. package/substrate/.claude/luminaries/robert-c-martin.md +0 -164
  69. package/substrate/.claude/luminaries/saltzer-schroeder.md +0 -194
  70. package/substrate/.claude/luminaries/sophia-prater.md +0 -193
  71. package/substrate/.claude/luminaries/stephen-toulmin.md +0 -131
  72. package/substrate/.claude/luminaries/tony-hoare.md +0 -170
  73. package/substrate/.claude/luminaries/vaughn-vernon.md +0 -50
  74. package/substrate/.claude/luminaries/w-edwards-deming.md +0 -158
  75. package/substrate/.claude/rules/accessor-library-discipline.md +0 -138
  76. package/substrate/.claude/rules/adr-discipline.md +0 -120
  77. package/substrate/.claude/rules/api-conventions.md +0 -125
  78. package/substrate/.claude/rules/artifact-ingestion.md +0 -179
  79. package/substrate/.claude/rules/assert-only-after-verify.md +0 -137
  80. package/substrate/.claude/rules/blocked-items.md +0 -146
  81. package/substrate/.claude/rules/bootstrap-pair-discipline.md +0 -141
  82. package/substrate/.claude/rules/branching.md +0 -28
  83. package/substrate/.claude/rules/cold-adopter-harness-discipline.md +0 -129
  84. package/substrate/.claude/rules/commit-conventions.md +0 -22
  85. package/substrate/.claude/rules/compounding-axis-fresh-analysis.md +0 -188
  86. package/substrate/.claude/rules/compounding-sequence-fresh-analysis.md +0 -188
  87. package/substrate/.claude/rules/context-engineering.md +0 -202
  88. package/substrate/.claude/rules/context-management.md +0 -85
  89. package/substrate/.claude/rules/defensive-bash.md +0 -68
  90. package/substrate/.claude/rules/deferred-actions.md +0 -233
  91. package/substrate/.claude/rules/destructive-operations.md +0 -69
  92. package/substrate/.claude/rules/diagnosis.md +0 -38
  93. package/substrate/.claude/rules/github-issue-flash-tweet.md +0 -156
  94. package/substrate/.claude/rules/guardrails.md +0 -73
  95. package/substrate/.claude/rules/hook-wire-on-author.md +0 -103
  96. package/substrate/.claude/rules/identifier-leak-prevention.md +0 -104
  97. package/substrate/.claude/rules/iteration-bet-brief-completeness.md +0 -54
  98. package/substrate/.claude/rules/lite-manifest-schema-change-discipline.md +0 -98
  99. package/substrate/.claude/rules/longrun-prep-plan-doc-compression.md +0 -89
  100. package/substrate/.claude/rules/loop-discipline.md +0 -81
  101. package/substrate/.claude/rules/manual-prod-approval.md +0 -100
  102. package/substrate/.claude/rules/marker-enrichment-discipline.md +0 -99
  103. package/substrate/.claude/rules/mobile-ephemeral-session.md +0 -109
  104. package/substrate/.claude/rules/new-dependency-check.md +0 -51
  105. package/substrate/.claude/rules/oo-ad-entry-point.md +0 -117
  106. package/substrate/.claude/rules/operator-facing-prose.md +0 -196
  107. package/substrate/.claude/rules/option-label-discipline.md +0 -108
  108. package/substrate/.claude/rules/pattern-annotation.md +0 -100
  109. package/substrate/.claude/rules/plain-english-discipline.md +0 -156
  110. package/substrate/.claude/rules/plan-enumeration-needs-value-props.md +0 -211
  111. package/substrate/.claude/rules/pr-body-shape.md +0 -317
  112. package/substrate/.claude/rules/pr-strategy.md +0 -167
  113. package/substrate/.claude/rules/pr-title-shape.md +0 -161
  114. package/substrate/.claude/rules/prototype-workflow.md +0 -65
  115. package/substrate/.claude/rules/reserved-skill-names.md +0 -123
  116. package/substrate/.claude/rules/schema-management.md +0 -49
  117. package/substrate/.claude/rules/sdlc-gates.md +0 -149
  118. package/substrate/.claude/rules/security.md +0 -37
  119. package/substrate/.claude/rules/session-artifacts.md +0 -236
  120. package/substrate/.claude/rules/skill-composition-declarations.md +0 -124
  121. package/substrate/.claude/rules/skill-description-clarity.md +0 -247
  122. package/substrate/.claude/rules/skill-procedure-step-list.md +0 -137
  123. package/substrate/.claude/rules/state-schema-validation.md +0 -162
  124. package/substrate/.claude/rules/stuck-signal-diagnostic.md +0 -140
  125. package/substrate/.claude/rules/substrate-config-schema.md +0 -98
  126. package/substrate/.claude/rules/test-list-discipline.md +0 -175
  127. package/substrate/.claude/rules/test-sufficiency.md +0 -210
  128. package/substrate/.claude/rules/testing-tier-config.md +0 -145
  129. package/substrate/.claude/rules/testing.md +0 -38
  130. package/substrate/.claude/rules/turn-estimate-grounding.md +0 -134
  131. package/substrate/.claude/rules/visual-hierarchy.md +0 -437
  132. package/substrate/.claude/rules/we-dont-break-adopters.md +0 -126
  133. package/substrate/.claude/rules/whereami-load-bearing.md +0 -202
  134. package/substrate/.claude/rules/writing-craft-discipline.md +0 -92
  135. package/substrate/.claude/rules/wu-sequencing-compounds.md +0 -145
  136. package/substrate/.claude/skills/build/SKILL.md +0 -640
  137. package/substrate/.claude/skills/chronicle/SKILL.md +0 -55
  138. package/substrate/.claude/skills/clean-artifacts/SKILL.md +0 -249
  139. package/substrate/.claude/skills/decompose/SKILL.md +0 -280
  140. package/substrate/.claude/skills/diagnose/SKILL.md +0 -297
  141. package/substrate/.claude/skills/feynman/SKILL.md +0 -90
  142. package/substrate/.claude/skills/howdoi/SKILL.md +0 -105
  143. package/substrate/.claude/skills/ia-model/SKILL.md +0 -108
  144. package/substrate/.claude/skills/interaction-design/SKILL.md +0 -112
  145. package/substrate/.claude/skills/interpret-input/SKILL.md +0 -180
  146. package/substrate/.claude/skills/journal/SKILL.md +0 -209
  147. package/substrate/.claude/skills/kiss/SKILL.md +0 -449
  148. package/substrate/.claude/skills/launch/SKILL.md +0 -915
  149. package/substrate/.claude/skills/lean-canvas/SKILL.md +0 -332
  150. package/substrate/.claude/skills/longrun/SKILL.md +0 -463
  151. package/substrate/.claude/skills/luminary/SKILL.md +0 -481
  152. package/substrate/.claude/skills/ogilvy-writing-audit/SKILL.md +0 -177
  153. package/substrate/.claude/skills/onboard-repo/SKILL.md +0 -1624
  154. package/substrate/.claude/skills/pattern-review/SKILL.md +0 -99
  155. package/substrate/.claude/skills/personas/SKILL.md +0 -207
  156. package/substrate/.claude/skills/promote/SKILL.md +0 -283
  157. package/substrate/.claude/skills/requirement/SKILL.md +0 -98
  158. package/substrate/.claude/skills/retro/SKILL.md +0 -117
  159. package/substrate/.claude/skills/riff/SKILL.md +0 -114
  160. package/substrate/.claude/skills/roadmap-reconcile/SKILL.md +0 -163
  161. package/substrate/.claude/skills/session-end/SKILL.md +0 -309
  162. package/substrate/.claude/skills/session-log/SKILL.md +0 -299
  163. package/substrate/.claude/skills/skills/SKILL.md +0 -228
  164. package/substrate/.claude/skills/spec/SKILL.md +0 -105
  165. package/substrate/.claude/skills/sprint/SKILL.md +0 -392
  166. package/substrate/.claude/skills/stage/SKILL.md +0 -384
  167. package/substrate/.claude/skills/state-a-problem/SKILL.md +0 -185
  168. package/substrate/.claude/skills/temperance/SKILL.md +0 -108
  169. package/substrate/.claude/skills/use-case/SKILL.md +0 -417
  170. package/substrate/.claude/skills/user-stories/SKILL.md +0 -268
  171. package/substrate/.claude/skills/value-prop/SKILL.md +0 -251
  172. package/substrate/.claude/skills/verify/SKILL.md +0 -160
  173. package/substrate/.claude/skills/visual-review/SKILL.md +0 -503
  174. package/substrate/.claude/skills/whats-the-plan/SKILL.md +0 -202
  175. package/substrate/.claude/skills/whereami/SKILL.md +0 -307
  176. package/substrate/AGENTS.md +0 -79
  177. package/substrate/CLAUDE-lite.md +0 -85
  178. package/substrate/CODE_OF_CONDUCT.md +0 -28
  179. package/substrate/CONTRIBUTING.md +0 -177
  180. package/substrate/README.md +0 -173
  181. package/substrate/SECURITY.md +0 -19
  182. package/substrate/architecture/decisions/ADR-029-release-pipeline.md +0 -79
  183. package/substrate/architecture/decisions/ADR-031-non-breaking-changes-adopter-discipline.md +0 -139
  184. package/substrate/architecture/decisions/ADR-032-adopter-sync-dispatcher-architecture.md +0 -192
  185. package/substrate/architecture/decisions/ADR-039-release-tagging-scheme.md +0 -145
  186. package/substrate/architecture/decisions/ADR-040-planning-skill-vocabulary-and-lite-profile.md +0 -155
  187. package/substrate/architecture/decisions/ADR-044-unified-skill-body-template.md +0 -162
  188. package/substrate/lib/clean-artifacts-sweep.sh +0 -112
  189. package/substrate/lib/code-comment-discipline.sh +0 -144
  190. package/substrate/lib/composer-preflight.sh +0 -459
  191. package/substrate/lib/hook-inject.sh +0 -255
  192. package/substrate/lib/luminary-pick.sh +0 -96
  193. package/substrate/lib/output-discipline.sh +0 -143
  194. package/substrate/lib/prose-scan-boundary.sh +0 -171
  195. package/substrate/lib/rewrite-check.sh +0 -214
  196. package/substrate/lib/state.sh +0 -1372
  197. package/substrate/lib/telemetry.sh +0 -205
  198. package/substrate/lib/tier-check.sh +0 -187
  199. package/substrate/lib/tier-dependency-audit.sh +0 -1088
  200. package/substrate/presence/install/bassclef-hook-connect.sh +0 -178
  201. package/substrate/presence/install/bassclef-sync.dispatcher.template.sh +0 -841
  202. package/substrate/presence/install/bassclef-sync.template.sh +0 -2076
  203. package/substrate/presence/install/schedule-auto-save.cron.sh +0 -88
  204. package/substrate/presence/install/schedule-auto-save.taskscheduler.md +0 -122
  205. package/substrate/scripts/aggregate-telemetry.sh +0 -217
  206. package/substrate/scripts/analyze-tier-dependencies.sh +0 -239
  207. package/substrate/scripts/generate-lite-manifest.sh +0 -505
  208. package/substrate/scripts/generate-tier-manifest.sh +0 -28
  209. package/substrate/scripts/intent-drift-check.sh +0 -456
  210. package/substrate/scripts/lite-manifest-drift-check.sh +0 -146
  211. package/substrate/scripts/render-lite-manifest-doc.sh +0 -150
  212. package/substrate/standards/adr-template.md +0 -86
  213. package/substrate/standards/api-conventions/nextjs.md +0 -84
  214. package/substrate/standards/artifact-composition.md +0 -209
  215. package/substrate/standards/bash-hook-safety.md +0 -246
  216. package/substrate/standards/bassclef-configs-schema.md +0 -232
  217. package/substrate/standards/bassclef-evolution.md +0 -143
  218. package/substrate/standards/bassclef-internal-jargon.md +0 -244
  219. package/substrate/standards/bassclef-managed-sentinel.md +0 -96
  220. package/substrate/standards/bassclef-source-config.md +0 -228
  221. package/substrate/standards/branch-stacking.md +0 -408
  222. package/substrate/standards/code-safety-principles.md +0 -176
  223. package/substrate/standards/composer-prerequisites.md +0 -155
  224. package/substrate/standards/deferred-actions-schema.md +0 -204
  225. package/substrate/standards/dependency-discipline/cargo.md +0 -39
  226. package/substrate/standards/dependency-discipline/gem.md +0 -43
  227. package/substrate/standards/dependency-discipline/go-mod.md +0 -41
  228. package/substrate/standards/dependency-discipline/npm.md +0 -42
  229. package/substrate/standards/dependency-discipline/pip.md +0 -42
  230. package/substrate/standards/deployment-topology/ec2-tailscale.md +0 -225
  231. package/substrate/standards/deployment-topology.md +0 -69
  232. package/substrate/standards/docs-sync-allowlist.md +0 -76
  233. package/substrate/standards/domain-and-dns.md +0 -145
  234. package/substrate/standards/frontend-stack.md +0 -67
  235. package/substrate/standards/frontmatter-schema.md +0 -154
  236. package/substrate/standards/graceful-exit.md +0 -227
  237. package/substrate/standards/hook-idempotency.md +0 -102
  238. package/substrate/standards/hook-injection-discipline.md +0 -202
  239. package/substrate/standards/hook-install-class.md +0 -215
  240. package/substrate/standards/input-handler-interface.md +0 -152
  241. package/substrate/standards/lite-manifest-schema-changes.md +0 -135
  242. package/substrate/standards/luminary-matching.md +0 -105
  243. package/substrate/standards/luminary-problem-patterns.md +0 -481
  244. package/substrate/standards/migration-discipline/active-record.md +0 -50
  245. package/substrate/standards/migration-discipline/alembic.md +0 -43
  246. package/substrate/standards/migration-discipline/gorm.md +0 -50
  247. package/substrate/standards/migration-discipline/prisma.md +0 -53
  248. package/substrate/standards/migration-discipline/sqlalchemy.md +0 -51
  249. package/substrate/standards/mobile-ephemeral-session.md +0 -167
  250. package/substrate/standards/model-routing-discipline.md +0 -160
  251. package/substrate/standards/ogilvy-writing-rules.md +0 -225
  252. package/substrate/standards/opener-discipline.md +0 -96
  253. package/substrate/standards/operator-facing-prose-discipline.md +0 -201
  254. package/substrate/standards/persona-schema.md +0 -229
  255. package/substrate/standards/pluggable-luminaries.md +0 -323
  256. package/substrate/standards/pr-body-discipline.md +0 -115
  257. package/substrate/standards/preview-state-schema.md +0 -189
  258. package/substrate/standards/project-directory-layout.md +0 -276
  259. package/substrate/standards/release-tagging.md +0 -137
  260. package/substrate/standards/reserved-skill-names.md +0 -120
  261. package/substrate/standards/scannable-multi-option-output.md +0 -261
  262. package/substrate/standards/sdlc-compliance.md +0 -286
  263. package/substrate/standards/sdlc-gates/typescript.md +0 -57
  264. package/substrate/standards/secrets-lifecycle.md +0 -210
  265. package/substrate/standards/security-scanner-adapter.md +0 -145
  266. package/substrate/standards/session-board.md +0 -256
  267. package/substrate/standards/skill-output-discipline.md +0 -90
  268. package/substrate/standards/state-spine-contract.md +0 -255
  269. package/substrate/standards/state-spine.md +0 -511
  270. package/substrate/standards/steering-hints/kiss-words.md +0 -11
  271. package/substrate/standards/substrate-config-schema.md +0 -267
  272. package/substrate/standards/tech-stack-config.md +0 -109
  273. package/substrate/standards/tier-dependency-analysis.md +0 -167
  274. package/substrate/standards/tier-runtime-deps/lite.md +0 -57
  275. package/substrate/standards/tier-tag-schema.md +0 -155
  276. package/substrate/standards/two-layer-config.md +0 -99
  277. package/substrate/standards/use-case-format.md +0 -292
  278. package/substrate/standards/user-story-invest.md +0 -268
  279. package/substrate/standards/velocity-and-appetite.md +0 -229
  280. package/substrate/standards/voice-input-pattern.md +0 -119
  281. package/substrate/standards/whereami-schema.md +0 -301
  282. package/substrate/standards/worktree-management.md +0 -211
  283. package/substrate/standards/writing-guide.md +0 -213
  284. package/substrate/templates/chronicle-template.md +0 -75
  285. package/substrate/templates/deferred-action-template.md +0 -45
  286. package/substrate/templates/memory-proposal-template.md +0 -77
  287. package/substrate/templates/persona-template.md +0 -200
  288. package/substrate/templates/pr-faq.md +0 -45
  289. package/substrate/templates/secret-rotation-template.md +0 -162
  290. package/substrate/templates/spec-template.md +0 -131
  291. package/substrate/templates/use-case-template.md +0 -194
  292. package/substrate/templates/user-story-template.md +0 -107
  293. package/substrate/templates/whereami-template.md +0 -101
@@ -1,1110 +0,0 @@
1
- #!/bin/bash
2
- # tier: lite
3
- # Substrate Clarity Gate — fires on Write/Edit to substrate paths
4
- # (.claude/skills/, .claude/rules/, .claude/luminaries/). Enforces the
5
- # clarity + context-engineering discipline from bassclef#371 + #377.
6
- #
7
- # v1 (2026-05-07 Phase I — Plain English Sweep) — ALL THREE SURFACES BLOCK:
8
- # - Skill descriptions ≤280 chars, verb-first opener, no jargon ladder
9
- # - Rule INSTEAD-block discipline (every don't/never/avoid paired
10
- # with INSTEAD within 5 lines) — flipped from ADVISORY to BLOCK
11
- # - Luminary INSTEAD-block discipline — flipped from ADVISORY to BLOCK
12
- # - Luminary required-sections check stays ADVISORY (documentation
13
- # discipline, not blocking concern)
14
- #
15
- # Allowlist grandfathers ~94 existing violators per bassclef#373/#374/#375
16
- # audits. Each audit shrinks its section as files get fixed individually.
17
- #
18
- # Exit codes:
19
- # 0 — allow (path doesn't match, or all checks pass, or override,
20
- # or path on allowlist)
21
- # 2 — block (matcher hit, violation found, no override)
22
- #
23
- # Override: SKIP_SUBSTRATE_CLARITY=1 to bypass (logged via trace-helper)
24
- #
25
- # Allowlist: .claude/hooks/substrate-clarity-allowlist.txt
26
- #
27
- # Rule: .claude/rules/skill-description-clarity.md
28
- # Issue: bassclef#382 (v0); Phase I bet (v1 flip)
29
-
30
- set +e # never crash the tool call; fall through to allow on any unexpected error
31
-
32
- # === Hook liveness heartbeat (WU-3 of bet 2026-07-31d; closes #1002) ===
33
- # Silent-fail — a missing lib never crashes the hook.
34
- {
35
- _hb_sd="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
36
- for _hb_c in "${_hb_sd}/../../lib/hook-heartbeat.sh" "${HOME:-/}/lib/hook-heartbeat.sh"; do
37
- [ -f "$_hb_c" ] && source "$_hb_c" && heartbeat_mark "substrate-clarity-gate" && break
38
- done
39
- unset _hb_sd _hb_c
40
- } 2>/dev/null || true
41
-
42
- INPUT=$(cat)
43
- TOOL_NAME=$(jq <<< "$INPUT" -r '.tool_name // ""' 2>/dev/null)
44
- FILE_PATH=$(jq <<< "$INPUT" -r '.tool_input.file_path // ""' 2>/dev/null)
45
-
46
- # Only fire on Write / Edit
47
- if [ "$TOOL_NAME" != "Write" ] && [ "$TOOL_NAME" != "Edit" ]; then
48
- exit 0
49
- fi
50
-
51
- # No file path → allow (defensive)
52
- if [ -z "$FILE_PATH" ]; then
53
- exit 0
54
- fi
55
-
56
- # Determine surface type from path.
57
- # v0 scope: skill SKILL.md (length + verb-first + jargon — strict)
58
- # v1 scope: rule + luminary (INSTEAD-block — strict; flipped from advisory in Phase I)
59
- # v2 scope (Phase II WU-7b): chronicle + iteration_bet + journal_entry + decomposition
60
- # Advisory only — sentence-length, passive-voice, expanded jargon list.
61
- # These surfaces are operator-narrative; voice trumps strict rule.
62
- SURFACE=""
63
- case "$FILE_PATH" in
64
- */.claude/skills/*/SKILL.md) SURFACE="skill" ;;
65
- */.claude/rules/*.md) SURFACE="rule" ;;
66
- */.claude/luminaries/*.md) SURFACE="luminary" ;;
67
- */.claude/agents/*.md) SURFACE="agent" ;;
68
- */.claude/hooks/*.sh) SURFACE="hook" ;;
69
- */architecture/decisions/ADR-*.md) SURFACE="adr" ;;
70
- */chronicle/*.md|*/docs/chronicle/*.md) SURFACE="chronicle" ;;
71
- */docs/iteration-bets/*.md) SURFACE="iteration_bet" ;;
72
- */docs/operator-private/journals/*.md) SURFACE="journal_entry" ;;
73
- */docs/decompositions/*.md) SURFACE="decomposition" ;;
74
- */docs/whereami.md) SURFACE="whereami" ;;
75
- # Surfaces added per goal 25e Path A — 15 SDLC artifact classes newly covered
76
- */docs/canvases/*.md) SURFACE="canvas" ;;
77
- */docs/use-cases/*.md) SURFACE="use_case" ;;
78
- */docs/personas/*.md) SURFACE="persona" ;;
79
- */docs/roadmaps/*.md) SURFACE="roadmap" ;;
80
- */docs/risk-ledgers/*.md) SURFACE="risk_ledger" ;;
81
- */docs/empathy-maps/*.md) SURFACE="empathy_map" ;;
82
- */docs/interaction-design/*.md) SURFACE="interaction_design" ;;
83
- */docs/handoffs/*.md) SURFACE="handoff" ;;
84
- */docs/audits/*.md|*/architecture/audits/*.md) SURFACE="audit" ;;
85
- */docs/reflections/*.md) SURFACE="reflection" ;;
86
- */docs/regressions/*.md) SURFACE="regression" ;;
87
- */docs/release-notes/*.md) SURFACE="release_notes" ;;
88
- */docs/analysis/*.md) SURFACE="analysis" ;;
89
- */docs/curation/*.md) SURFACE="curation" ;;
90
- */standards/*.md) SURFACE="standard" ;;
91
- */templates/*.md) SURFACE="template" ;;
92
- esac
93
-
94
- if [ -z "$SURFACE" ]; then
95
- exit 0
96
- fi
97
-
98
- # Surface tier — strict surfaces BLOCK; advisory surfaces only emit notices.
99
- # chronicle + iteration_bet + decomposition FLIPPED from advisory to strict
100
- # after the bassclef-upstream#303 audit emptied the grandfathered allowlist
101
- # (38 files scrubbed via bet 2026-06-25j). journal_entry remains advisory
102
- # per memory feedback_writing_voice — personal-narrative voice trumps strict
103
- # rule.
104
- case "$SURFACE" in
105
- skill|rule|luminary|adr|chronicle|iteration_bet|decomposition)
106
- SURFACE_TIER="strict"
107
- ;;
108
- journal_entry|whereami)
109
- # journal_entry — personal-narrative voice trumps strict rule per memory
110
- # feedback_writing_voice.
111
- # whereami — V1 advisory per bassclef-upstream bet 2026-07-04a WU-1.
112
- # Advisory tier lets us ship coverage without retroactively scrubbing
113
- # today's whereami frontmatter (which carries historical terms).
114
- SURFACE_TIER="advisory"
115
- ;;
116
- canvas|use_case|persona|roadmap|risk_ledger|empathy_map|interaction_design|handoff|audit|reflection|regression|release_notes|analysis|curation|standard|template)
117
- # New surfaces per goal 25e Path A — V1 ADVISORY only.
118
- # These surfaces get the rewrite-check heuristics layer below.
119
- # They do NOT run strict INSTEAD-block checks (those apply only to
120
- # rules + luminaries + skills). V2 may tier strict/medium after
121
- # calibration cycle.
122
- SURFACE_TIER="advisory"
123
- ;;
124
- *)
125
- SURFACE_TIER="strict"
126
- ;;
127
- esac
128
-
129
- # Surface-level allowlist for flip-from-advisory grandfathered content.
130
- # Bypasses strict checks until the file is scrubbed (per bassclef-upstream#303 audit).
131
- # After scrub, remove the file's path from the allowlist file.
132
- ALLOWLIST_FILE="$(dirname "$0")/substrate-clarity-gate-allowlist.txt"
133
- if [ -f "$ALLOWLIST_FILE" ]; then
134
- # Match by basename relative to repo root (path may be absolute or relative)
135
- REL_PATH="${FILE_PATH#*$PWD/}"
136
- REL_PATH="${REL_PATH#$PWD/}"
137
- if grep -Fxq "$REL_PATH" "$ALLOWLIST_FILE" 2>/dev/null \
138
- || grep -Fxq "$FILE_PATH" "$ALLOWLIST_FILE" 2>/dev/null; then
139
- exit 0
140
- fi
141
- fi
142
-
143
- # Override hatch — log and allow
144
- if [ "${SKIP_SUBSTRATE_CLARITY:-0}" = "1" ]; then
145
- SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
146
- if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/trace-helper.sh" ]; then
147
- source "$SCRIPT_DIR/trace-helper.sh" 2>/dev/null || true
148
- trace_log "substrate-clarity-gate" "OVERRIDE $TOOL_NAME $FILE_PATH ($SURFACE)" 2>/dev/null || true
149
- fi
150
- exit 0
151
- fi
152
-
153
- # Allowlist check (WU-4 ships the file; pre-WU-4 the file may not exist)
154
- SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
155
- ALLOWLIST_FILE="${SCRIPT_DIR}/substrate-clarity-allowlist.txt"
156
- # Match against repo-relative path (strip leading /Users/.../bassclef/ etc.)
157
- REPO_REL=$(echo "$FILE_PATH" | sed -E 's|^.*/(\.claude/.*)$|\1|')
158
- if [ -f "$ALLOWLIST_FILE" ]; then
159
- if grep -Fxq "$REPO_REL" "$ALLOWLIST_FILE" 2>/dev/null; then
160
- exit 0 # allowlisted (SCRIPT_DIR match); check skipped
161
- fi
162
- fi
163
-
164
- # Repo-allowlist fallback (bassclef-upstream#358).
165
- # When hook is installed at user level (~/.claude/hooks/) and SCRIPT_DIR
166
- # allowlist doesn't match, also check the consumer repo's allowlist via
167
- # CLAUDE_PROJECT_DIR. Closes the propagation gap surfaced during bet 25h WU-3.
168
- if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
169
- REPO_ALLOWLIST_FILE="${CLAUDE_PROJECT_DIR}/.claude/hooks/substrate-clarity-allowlist.txt"
170
- if [ -f "$REPO_ALLOWLIST_FILE" ] && [ "$REPO_ALLOWLIST_FILE" != "$ALLOWLIST_FILE" ]; then
171
- if grep -Fxq "$REPO_REL" "$REPO_ALLOWLIST_FILE" 2>/dev/null; then
172
- exit 0 # allowlisted (repo-fallback match); check skipped
173
- fi
174
- fi
175
- fi
176
-
177
- # Assemble the content that WILL exist after this operation.
178
- FUTURE_CONTENT=""
179
- if [ "$TOOL_NAME" = "Write" ]; then
180
- FUTURE_CONTENT=$(jq <<< "$INPUT" -r '.tool_input.content // ""' 2>/dev/null)
181
- elif [ "$TOOL_NAME" = "Edit" ]; then
182
- NEW_STRING=$(jq <<< "$INPUT" -r '.tool_input.new_string // ""' 2>/dev/null)
183
- EXISTING=""
184
- if [ -f "$FILE_PATH" ]; then
185
- EXISTING=$(cat "$FILE_PATH" 2>/dev/null)
186
- fi
187
- # Apply the edit: replace OLD with NEW within EXISTING. For check purposes
188
- # we use full FUTURE_CONTENT — concat is rough but covers both surface text.
189
- OLD_STRING=$(jq <<< "$INPUT" -r '.tool_input.old_string // ""' 2>/dev/null)
190
- if [ -n "$EXISTING" ] && [ -n "$OLD_STRING" ]; then
191
- # Use python3 for safe string substitution (env vars avoid bash 4+ ${var@Q})
192
- FUTURE_CONTENT=$(OLD="$OLD_STRING" NEW="$NEW_STRING" python3 -c "
193
- import os, sys
194
- existing = sys.stdin.read()
195
- print(existing.replace(os.environ['OLD'], os.environ['NEW'], 1), end='')
196
- " <<< "$EXISTING" 2>/dev/null || echo "${EXISTING}
197
- ${NEW_STRING}")
198
- else
199
- FUTURE_CONTENT="${EXISTING}
200
- ${NEW_STRING}"
201
- fi
202
- fi
203
-
204
- # === Check dispatch ===
205
- VIOLATION=""
206
- LINE_REF=""
207
- SUGGESTED_FIX=""
208
-
209
- # ---------------------------------------------------------------------------
210
- # Documentation-surface tier check (bassclef-upstream#1012)
211
- #
212
- # The tier: field applies to substrate building blocks (skills, rules,
213
- # luminaries, agents, hooks, ADRs, standards) whose maturity governs
214
- # release-script filtering. Documentation surfaces — release-notes,
215
- # roadmaps, canvases — ship unconditionally via the release script's
216
- # fallback branch (scripts/release-to-bassclef.sh:367-372). The tier:
217
- # field there is cosmetic and misleads adopters.
218
- #
219
- # Per /luminary consult 2026-07-31: Norman (signifier discipline),
220
- # Brooks (conceptual integrity), Linus (adopter contract) — all
221
- # endorsed removing the field from documentation surfaces.
222
- # ---------------------------------------------------------------------------
223
- case "$SURFACE" in
224
- release_notes|roadmap|canvas)
225
- # Extract frontmatter block from FUTURE_CONTENT and grep for tier: line.
226
- # Frontmatter opens with --- on line 1 and closes with --- on a later line.
227
- TIER_LINE=$(echo "$FUTURE_CONTENT" | awk '
228
- BEGIN { in_fm = 0 }
229
- NR == 1 && /^---[[:space:]]*$/ { in_fm = 1; next }
230
- in_fm && /^---[[:space:]]*$/ { exit }
231
- in_fm && /^tier:[[:space:]]/ { print; exit }
232
- ')
233
- if [ -n "$TIER_LINE" ]; then
234
- VIOLATION="tier: field on ${SURFACE} surface is cosmetic (release script does not read it here) — remove per bassclef-upstream#1012."
235
- LINE_REF="frontmatter tier: line"
236
- SUGGESTED_FIX="Delete the tier: line from frontmatter. The field applies to substrate building-block surfaces (skills, rules, luminaries, agents, hooks, ADRs, standards) — not documentation surfaces (release-notes, roadmaps, canvases). See standards/tier-tag-schema.md § Scope."
237
- fi
238
- ;;
239
- esac
240
-
241
- # ---------------------------------------------------------------------------
242
- # Frontmatter schema validation (bassclef#862 Gap 1, WU-3 of bet 2026-06-18a)
243
- # Fires for skill + rule surfaces only. Validates:
244
- # 1. Required fields present and non-empty
245
- # 2. YAML parses cleanly (malformed → BLOCK)
246
- # 3. Field names are recognized (unknown → WARN, no block)
247
- # Schema source: standards/frontmatter-schema.md
248
- # Allowlist: .claude/hooks/substrate-frontmatter-allowlist.txt (60-day grace)
249
- # ---------------------------------------------------------------------------
250
- # Hook (.sh) tier validation — separate path from YAML frontmatter.
251
- # Hooks carry `# tier: <value>` as a header comment on line 2 (after shebang).
252
- # Per goal 2026-08-13d Piece 2 (bassclef-upstream#1200):
253
- # missing `# tier:` header → BLOCK; invalid value → BLOCK.
254
- if [ "$SURFACE" = "hook" ]; then
255
- HOOK_TIER=$(echo "$FUTURE_CONTENT" | head -5 | awk '/^# tier:[[:space:]]/ {sub(/^# tier:[[:space:]]*/,""); gsub(/[[:space:]]/,""); print; exit}')
256
- if [ -z "$HOOK_TIER" ]; then
257
- echo "" >&2
258
- echo "============================================" >&2
259
- echo "🛑 SUBSTRATE-CLARITY GATE — BLOCKED 🛑" >&2
260
- echo "============================================" >&2
261
- echo "" >&2
262
- echo "File: $FILE_PATH" >&2
263
- echo "Tool: $TOOL_NAME" >&2
264
- echo "" >&2
265
- echo "Violation: missing required '# tier:' header for hook (.sh)" >&2
266
- echo "Line: header comment on line 2 (after shebang)" >&2
267
- echo "" >&2
268
- echo "Suggested fix:" >&2
269
- echo " Add '# tier: <value>' as the second line, e.g.:" >&2
270
- echo " #!/usr/bin/env bash" >&2
271
- echo " # tier: standard" >&2
272
- echo "" >&2
273
- echo "Valid values: upstream | lite | standard | standard-pro | ultra" >&2
274
- echo " (private accepted as deprecated alias for upstream)" >&2
275
- echo "" >&2
276
- echo "Per rule: standards/tier-tag-schema.md" >&2
277
- echo "Per goal: bassclef-upstream#1200 Piece 2" >&2
278
- echo "" >&2
279
- echo "Override (logged): SKIP_SUBSTRATE_CLARITY=1 <command>" >&2
280
- echo "============================================" >&2
281
- exit 2
282
- fi
283
- case "$HOOK_TIER" in
284
- upstream|private|lite|standard|standard-pro|ultra) : ;;
285
- *)
286
- echo "" >&2
287
- echo "============================================" >&2
288
- echo "🛑 SUBSTRATE-CLARITY GATE — BLOCKED 🛑" >&2
289
- echo "============================================" >&2
290
- echo "" >&2
291
- echo "File: $FILE_PATH" >&2
292
- echo "Tool: $TOOL_NAME" >&2
293
- echo "" >&2
294
- echo "Violation: invalid '# tier:' value '$HOOK_TIER' on hook" >&2
295
- echo "Valid values: upstream | lite | standard | standard-pro | ultra" >&2
296
- echo " (private accepted as deprecated alias for upstream)" >&2
297
- echo "" >&2
298
- echo "Per rule: standards/tier-tag-schema.md" >&2
299
- echo "Override (logged): SKIP_SUBSTRATE_CLARITY=1 <command>" >&2
300
- echo "============================================" >&2
301
- exit 2
302
- ;;
303
- esac
304
- fi
305
-
306
- if [ "$SURFACE" = "skill" ] || [ "$SURFACE" = "rule" ] || \
307
- [ "$SURFACE" = "luminary" ] || [ "$SURFACE" = "agent" ] || \
308
- [ "$SURFACE" = "adr" ] || [ "$SURFACE" = "standard" ]; then
309
- FRONTMATTER_ALLOWLIST="${SCRIPT_DIR}/substrate-frontmatter-allowlist.txt"
310
- FM_SKIP=0
311
- if [ -f "$FRONTMATTER_ALLOWLIST" ]; then
312
- REPO_REL=$(echo "$FILE_PATH" | sed -E 's|^.*/(\.claude/.*)$|\1|')
313
- # Allowlist line shape: <repo-rel-path>|<grace-until ISO 8601>. Allow either
314
- # plain path (legacy) or path|date (matches testing-tier-enforce-allowlist).
315
- while IFS= read -r LINE; do
316
- # Strip comments and blank lines
317
- LINE_TRIM=$(echo "$LINE" | sed -E 's/[[:space:]]*#.*$//' | xargs)
318
- [ -z "$LINE_TRIM" ] && continue
319
- LINE_PATH=$(echo "$LINE_TRIM" | cut -d'|' -f1 | xargs)
320
- LINE_DATE=$(echo "$LINE_TRIM" | cut -d'|' -f2 -s | xargs)
321
- if [ "$LINE_PATH" = "$REPO_REL" ]; then
322
- # Grace window check: if a date is supplied, only honor while current
323
- if [ -n "$LINE_DATE" ]; then
324
- NOW_ISO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
325
- if [ "$NOW_ISO" \< "$LINE_DATE" ] || [ "$NOW_ISO" = "$LINE_DATE" ]; then
326
- FM_SKIP=1
327
- fi
328
- else
329
- FM_SKIP=1
330
- fi
331
- break
332
- fi
333
- done < "$FRONTMATTER_ALLOWLIST"
334
- fi
335
-
336
- if [ "$FM_SKIP" = "0" ]; then
337
- # Run frontmatter validation via python3 yaml
338
- FM_RESULT=$(echo "$FUTURE_CONTENT" | SURFACE_NAME="$SURFACE" FILE_PATH_ENV="$FILE_PATH" python3 -c "
339
- import os, sys, re
340
- try:
341
- import yaml
342
- except ImportError:
343
- # python yaml not available; pass without validation
344
- print('OK')
345
- sys.exit(0)
346
-
347
- content = sys.stdin.read()
348
- surface = os.environ.get('SURFACE_NAME', '')
349
-
350
- # Find frontmatter — opens with --- on line 1, closes with --- on a later line.
351
- m = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
352
- if not m:
353
- # No frontmatter.
354
- # Per goal 2026-08-13d Piece 2 (bassclef-upstream#1200):
355
- # substrate primitive classes require tier tag; missing frontmatter
356
- # means the tier tag cannot be present, so BLOCK. Backfill via prior
357
- # commits brought all 488 primitives to 100% tier coverage.
358
- if surface in ('skill', 'rule', 'luminary', 'agent', 'adr', 'standard'):
359
- print('MISSING_FRONTMATTER')
360
- else:
361
- # Non-primitive surfaces with no frontmatter — pass through.
362
- print('OK')
363
- sys.exit(0)
364
-
365
- fm_text = m.group(1)
366
- try:
367
- fm = yaml.safe_load(fm_text)
368
- except yaml.YAMLError as e:
369
- print(f'MALFORMED_YAML:{str(e).splitlines()[0]}')
370
- sys.exit(0)
371
-
372
- if not isinstance(fm, dict):
373
- print('MALFORMED_YAML:frontmatter is not a mapping')
374
- sys.exit(0)
375
-
376
- # Required fields per surface
377
- # Per goal 2026-08-13d Piece 2 — tier extended to all 6 .md primitive classes.
378
- # hook (.sh) is handled in a separate bash pre-check above (different extraction path).
379
- required = {
380
- 'skill': ['name', 'description', 'tier'],
381
- 'rule': ['tier'],
382
- 'luminary': ['tier'],
383
- 'agent': ['tier'],
384
- 'adr': ['tier'],
385
- 'standard': ['tier'],
386
- }.get(surface, [])
387
-
388
- for field in required:
389
- if field not in fm:
390
- print(f'MISSING_REQUIRED:{field}')
391
- sys.exit(0)
392
- val = fm[field]
393
- if val is None or (isinstance(val, str) and not val.strip()):
394
- print(f'MISSING_REQUIRED:{field}')
395
- sys.exit(0)
396
-
397
- # tier value validation (WU-1: bassclef-upstream→bassclef export gate)
398
- # Values per standards/tier-tag-schema.md.
399
- # 'private' kept accepted as deprecated alias for 'upstream' per ADR-031
400
- # we-dont-break-adopters compat-shim discipline.
401
- if 'tier' in fm:
402
- tier_val = fm['tier']
403
- if tier_val not in ('upstream', 'private', 'lite', 'standard', 'standard-pro', 'ultra'):
404
- print(f'INVALID_TIER:{tier_val} (expected upstream|lite|standard|standard-pro|ultra; private accepted as deprecated alias)')
405
- sys.exit(0)
406
-
407
- # Recognized optional fields per surface
408
- recognized = {
409
- 'skill': {
410
- 'name', 'description', 'args', 'composes_baseline', 'composes_with',
411
- 'depends_on', 'deprecated', 'deprecated_decision', 'deprecated_in_favor_of',
412
- 'disable_friction_check', 'disable_model_invocation', 'model_tier',
413
- 'modes', 'no_user_modes', 'requires', 'reserved_name_override',
414
- 'sizes', 'tech_stack_match', 'tier', 'tiers', 'user_invocable',
415
- },
416
- 'rule': {
417
- 'description', 'composes_with', 'depends_on', 'globs', 'model_tier',
418
- 'tech_stack_match', 'tier',
419
- },
420
- }.get(surface, set())
421
-
422
- unknown = [k for k in fm.keys() if k not in recognized]
423
- if unknown:
424
- print(f'UNKNOWN_FIELDS:{\",\".join(unknown)}')
425
- sys.exit(0)
426
-
427
- # bassclef#862 Gap 3 (WU-15): composes_with + depends_on targets-exist validation
428
- # composes_with → STRICT (BLOCK if target skill missing)
429
- # depends_on → ADVISORY (warn if target missing; pass for V1)
430
- file_path = os.environ.get('FILE_PATH_ENV', '')
431
- substrate_root = ''
432
- if '/.claude/' in file_path:
433
- substrate_root = file_path.split('/.claude/')[0]
434
-
435
- def target_resolved(name, substrate_root):
436
- skill_path = f'{substrate_root}/.claude/skills/{name}/SKILL.md'
437
- rule_path = f'{substrate_root}/.claude/rules/{name}.md'
438
- known_config_blocks = {
439
- 'tech_stack', 'adopter_defaults', 'plan_tier',
440
- 'prose_discipline', 'longrun', 'testing',
441
- }
442
- return (os.path.isfile(skill_path)
443
- or os.path.isfile(rule_path)
444
- or name in known_config_blocks)
445
-
446
- if substrate_root and 'composes_with' in fm:
447
- cw = fm['composes_with']
448
- if not isinstance(cw, list):
449
- print(f'COMPOSES_WITH_TYPE_ERROR:got {type(cw).__name__}, expected list')
450
- sys.exit(0)
451
- missing = []
452
- for name in cw:
453
- if not isinstance(name, str):
454
- print(f'COMPOSES_WITH_TYPE_ERROR:list item is {type(name).__name__}, expected string')
455
- sys.exit(0)
456
- skill_path = f'{substrate_root}/.claude/skills/{name}/SKILL.md'
457
- if not os.path.isfile(skill_path):
458
- missing.append(name)
459
- if missing:
460
- print(f'COMPOSES_WITH_MISSING:{\",\".join(missing)}')
461
- sys.exit(0)
462
-
463
- if substrate_root and 'depends_on' in fm:
464
- do = fm['depends_on']
465
- if isinstance(do, list):
466
- missing = [n for n in do
467
- if isinstance(n, str) and not target_resolved(n, substrate_root)]
468
- if missing:
469
- print(f'DEPENDS_ON_MISSING:{\",\".join(missing)}')
470
- sys.exit(0)
471
-
472
- print('OK')
473
- " 2>/dev/null)
474
-
475
- case "$FM_RESULT" in
476
- OK|"")
477
- # Pass — no violation
478
- ;;
479
- MISSING_FRONTMATTER)
480
- VIOLATION="Frontmatter required for $SURFACE files but none found."
481
- LINE_REF="top of file"
482
- SUGGESTED_FIX="Add a YAML frontmatter block opening with --- on line 1. See standards/frontmatter-schema.md for required + recognized fields."
483
- ;;
484
- MALFORMED_YAML:*)
485
- REASON=$(echo "$FM_RESULT" | sed 's/^MALFORMED_YAML://')
486
- VIOLATION="Frontmatter YAML is malformed: $REASON"
487
- LINE_REF="frontmatter block"
488
- SUGGESTED_FIX="Fix YAML syntax. Common causes: unquoted colons in values, mismatched indentation, trailing tabs. See standards/frontmatter-schema.md."
489
- ;;
490
- MISSING_REQUIRED:*)
491
- FIELD=$(echo "$FM_RESULT" | sed 's/^MISSING_REQUIRED://')
492
- VIOLATION="Frontmatter missing required field for $SURFACE: \`$FIELD\`"
493
- LINE_REF="frontmatter $FIELD: field"
494
- if [ "$FIELD" = "tier" ]; then
495
- SUGGESTED_FIX="Add \`tier: standard\` to the frontmatter. Values: upstream | lite | standard | standard-pro | ultra. See standards/tier-tag-schema.md."
496
- else
497
- SUGGESTED_FIX="Add \`$FIELD:\` with a non-empty value to the frontmatter. See standards/frontmatter-schema.md for the full schema."
498
- fi
499
- ;;
500
- INVALID_TIER:*)
501
- REASON=$(echo "$FM_RESULT" | sed 's/^INVALID_TIER://')
502
- VIOLATION="Frontmatter \`tier:\` has invalid value — $REASON"
503
- LINE_REF="frontmatter tier: field"
504
- SUGGESTED_FIX="Use one of: private (never ships) | basic (ships first release) | premium (ships after promotion) | public (legacy/already-shipped). See standards/tier-tag-schema.md (WU-1)."
505
- ;;
506
- UNKNOWN_FIELDS:*)
507
- # WARN only — not a block. Surface to stderr but exit 0.
508
- FIELDS=$(echo "$FM_RESULT" | sed 's/^UNKNOWN_FIELDS://')
509
- echo "substrate-clarity-gate: WARN — unknown frontmatter field(s) in $SURFACE [$REPO_REL]: $FIELDS" >&2
510
- echo " Recognized fields documented in standards/frontmatter-schema.md." >&2
511
- echo " If this is a new substrate field, propose it via /promote substrate-evolution." >&2
512
- ;;
513
- COMPOSES_WITH_TYPE_ERROR:*)
514
- REASON=$(echo "$FM_RESULT" | sed 's/^COMPOSES_WITH_TYPE_ERROR://')
515
- VIOLATION="composes_with must be a YAML list of skill names ($REASON)."
516
- LINE_REF="frontmatter composes_with: field"
517
- SUGGESTED_FIX="Format: composes_with: [skill-name-1, skill-name-2]. See .claude/rules/skill-composition-declarations.md for semantics."
518
- ;;
519
- COMPOSES_WITH_MISSING:*)
520
- NAMES=$(echo "$FM_RESULT" | sed 's/^COMPOSES_WITH_MISSING://')
521
- VIOLATION="composes_with names skills that don't exist: $NAMES"
522
- LINE_REF="frontmatter composes_with: field"
523
- SUGGESTED_FIX="Each name must match a .claude/skills/<name>/SKILL.md file. Likely a typo. List available skills: ls .claude/skills/. See .claude/rules/skill-composition-declarations.md."
524
- ;;
525
- DEPENDS_ON_MISSING:*)
526
- # ADVISORY — pass with warning to stderr.
527
- NAMES=$(echo "$FM_RESULT" | sed 's/^DEPENDS_ON_MISSING://')
528
- echo "substrate-clarity-gate: ADVISORY — depends_on names targets without a matching skill, rule, or known config-block: $NAMES" >&2
529
- echo " Recognized config blocks: tech_stack, adopter_defaults, plan_tier, prose_discipline, longrun, testing." >&2
530
- echo " Per .claude/rules/skill-composition-declarations.md — V1 advisory tier; strict enforcement deferred." >&2
531
- ;;
532
- esac
533
- fi
534
- fi
535
-
536
- # If frontmatter check fired a violation, skip the per-surface dispatch — fix
537
- # the frontmatter first.
538
- if [ -z "$VIOLATION" ]; then
539
-
540
- case "$SURFACE" in
541
- skill)
542
- # Extract description from frontmatter
543
- DESCRIPTION=$(echo "$FUTURE_CONTENT" | awk '
544
- /^---$/{c++; next}
545
- c==1 && /^description:/ {
546
- sub(/^description:[[:space:]]*/, "")
547
- # Handle quoted descriptions (single or double quotes)
548
- if (/^".*"$/) { gsub(/^"|"$/, "") }
549
- else if (/^'\''.*'\''$/) { gsub(/^'\''|'\''$/, "") }
550
- print
551
- exit
552
- }
553
- ')
554
-
555
- if [ -z "$DESCRIPTION" ]; then
556
- # No description field at all — not a violation per this rule (other
557
- # rules cover required-field). Pass.
558
- exit 0
559
- fi
560
-
561
- DESC_LEN=${#DESCRIPTION}
562
-
563
- # Check 1: length
564
- if [ "$DESC_LEN" -gt 280 ]; then
565
- VIOLATION="Description exceeds 280 chars (got $DESC_LEN)."
566
- LINE_REF="frontmatter description: field"
567
- SUGGESTED_FIX="Use verb + outcome + how + why-distinct ≤280 chars total. See worked examples in .claude/rules/skill-description-clarity.md"
568
- fi
569
-
570
- # Check 2: verb-first opener (skip if check 1 already fired)
571
- if [ -z "$VIOLATION" ]; then
572
- FIRST_WORD=$(echo "$DESCRIPTION" | awk '{print $1}' | tr -d '[:punct:]' | tr '[:upper:]' '[:lower:]')
573
- # Whitelist of imperative starters (extensible)
574
- VERB_FIRST=0
575
- for verb in show run turn create build take write reads generate compose audit launch render fire propose make produce dispatch read scan check verify validate enforce bootstrap install update extend extract ship pause start stop print emit publish capture flag detect orchestrate manage track surface route plan diagnose decompose review compose-with frame mock preview retrieve sync configure deploy invoke kill enumerate filter inspect sweep explain browse scaffold load draft convert declare get set pick choose name handle manage rename pull push kick fetch list count parse format save store post send drop add remove submit accept reject pass fail open close reopen claim release commit log copy move; do
576
- if [ "$FIRST_WORD" = "$verb" ]; then
577
- VERB_FIRST=1
578
- break
579
- fi
580
- done
581
- if [ "$VERB_FIRST" -eq 0 ]; then
582
- VIOLATION="Description doesn't open with a verb. First word: '$FIRST_WORD'."
583
- LINE_REF="frontmatter description: field"
584
- SUGGESTED_FIX="Open with imperative: Show, Run, Turn, Create, Build, Compose, etc. Avoid noun-first openers like 'Composer for...' or 'Tier-preset...'."
585
- fi
586
- fi
587
-
588
- # Check 3: jargon detection (BLOCK — flipped from ADVISORY in bassclef#496
589
- # LR6.5). Description must use plain language; bassclef jargon names
590
- # internal substrate concepts that engineers + boss audiences don't have
591
- # context for. Per `.claude/rules/skill-description-clarity.md`
592
- # substitution table.
593
- #
594
- # Single-word terms checked with word-boundary regex. The "compose"
595
- # family also catches the verb-form "Composes into" / "composes with"
596
- # that triggered bassclef#496 (/roadmap-reconcile shipped under 280c
597
- # but with "Composes into /longrun prep Step 0.5").
598
- if [ -z "$VIOLATION" ]; then
599
- JARGON=""
600
- for term in 'composer' 'primitive' 'tier-preset' 'load-bearing' 'blast radius' 'compose-with' 'scope-bounded' 'operationalize' 'substrate'; do
601
- if echo "$DESCRIPTION" | grep -qiE "\\b$term\\b"; then
602
- JARGON="${JARGON}${term} "
603
- fi
604
- done
605
- # Compose-verb forms used as methodology references ("Composes into",
606
- # "composes with", "Composing the X chain"). The plain "compose" verb
607
- # by itself (e.g., "compose a chronicle") isn't flagged.
608
- if echo "$DESCRIPTION" | grep -qiE '\bcomposes? (into|with|across|over)\b'; then
609
- JARGON="${JARGON}composes-into-or-with "
610
- fi
611
- if [ -n "$JARGON" ]; then
612
- VIOLATION="Description contains bassclef jargon: $JARGON"
613
- LINE_REF="frontmatter description: field"
614
- SUGGESTED_FIX="Substitute plain English per .claude/rules/skill-description-clarity.md table. Examples: 'composer' → 'runs' / 'chains' / 'combines'; 'primitive' → 'building block'; 'load-bearing' → 'required'; 'substrate' → 'system' / 'framework'; 'Composes into X' → name what the skill produces and where it slots in (e.g., 'used by /longrun prep' or just drop the methodology reference)."
615
- fi
616
- fi
617
-
618
- # Check 4 (bassclef#1249): Skill-body INSTEAD-block discipline.
619
- # Same shape as rule + luminary blocks below: every \b(don't|never|avoid)\b
620
- # (case-insensitive, word-boundary) requires INSTEAD/Instead within 5 lines
621
- # AFTER the trigger. Code blocks are exempt. Inline pairing passes.
622
- # Allowlist (.claude/hooks/substrate-clarity-allowlist.txt) grandfathers
623
- # existing skill bodies with a 60-day grace window.
624
- if [ -z "$VIOLATION" ]; then
625
- UNPAIRED=$(echo "$FUTURE_CONTENT" | awk '
626
- BEGIN { trigger_line = 0; trigger_text = ""; }
627
- {
628
- line_num++
629
- line = $0
630
- if (line ~ /^```/) { in_code = !in_code; next }
631
- if (in_code) next
632
-
633
- if (trigger_line > 0) {
634
- if (line_num - trigger_line <= 5) {
635
- if (line ~ /INSTEAD/ || line ~ /^Instead[ ,:]/) {
636
- trigger_line = 0
637
- trigger_text = ""
638
- }
639
- } else {
640
- print "L" trigger_line ": " trigger_text
641
- trigger_line = 0
642
- trigger_text = ""
643
- }
644
- }
645
-
646
- if (trigger_line == 0) {
647
- if (line ~ /INSTEAD/ || line ~ /^Instead[ ,:]/) next
648
- lower = tolower(line)
649
- if (lower ~ /(^|[^a-z])(don.t|never|avoid)([^a-z]|$)/) {
650
- trigger_line = line_num
651
- trigger_text = substr(line, 1, 80)
652
- }
653
- }
654
- }
655
- END {
656
- if (trigger_line > 0) {
657
- print "L" trigger_line ": " trigger_text
658
- }
659
- }
660
- ')
661
-
662
- if [ -n "$UNPAIRED" ]; then
663
- COUNT=$(echo "$UNPAIRED" | wc -l | tr -d ' ')
664
- VIOLATION="Skill body has $COUNT unpaired don't/never/avoid (no literal INSTEAD within 5 lines)."
665
- LINE_REF=$(echo "$UNPAIRED" | head -1 | cut -d: -f1)
666
- SUGGESTED_FIX="Pair each negation with a literal INSTEAD block within 5 lines. Example: 'Don't X.' followed by 'INSTEAD: do Y.' Per rule .claude/rules/context-engineering.md §INSTEAD-block pattern. To grandfather a legacy file: add path to .claude/hooks/substrate-clarity-allowlist.txt with bassclef#1249 reference."
667
- fi
668
- fi
669
-
670
- # Check 5 (bassclef-upstream#799): ADR-044 section-presence — V1 ADVISORY.
671
- # Scans SKILL body for the 13 required ## section headers named at
672
- # ADR-044 L55-L82. V1 emits ADVISORY to stderr; exit 0 (does not
673
- # block). V2 BLOCK flip deferred past 2026-10-31 per
674
- # we-dont-break-adopters grace window (separate follow-on ticket).
675
- if [ -z "$VIOLATION" ]; then
676
- MISSING_SECTIONS=""
677
- for SECTION in \
678
- "What you get" \
679
- "When to use it" \
680
- "When NOT to use" \
681
- "FAQ" \
682
- "Details below the fold" \
683
- "Examples" \
684
- "Anti-patterns and INSTEAD" \
685
- "Composes with" \
686
- "Distinct from" \
687
- "Rules" \
688
- "Test cases" \
689
- "Sources read" \
690
- "Why this skill exists"
691
- do
692
- if ! echo "$FUTURE_CONTENT" | grep -qE "^## ${SECTION}"; then
693
- MISSING_SECTIONS="${MISSING_SECTIONS}${SECTION}|"
694
- fi
695
- done
696
- if [ -n "$MISSING_SECTIONS" ]; then
697
- echo "[substrate-clarity-gate: ADVISORY (ADR-044 section-presence) — $FILE_PATH]" >&2
698
- echo " Missing required section(s) per ADR-044 L53-L82:" >&2
699
- echo "$MISSING_SECTIONS" | tr '|' '\n' | sed '/^$/d' | sed 's/^/ - ## /' >&2
700
- echo " V1 advisory; V2 BLOCK flip deferred past 2026-10-31 per .claude/rules/we-dont-break-adopters.md grace window." >&2
701
- fi
702
- fi
703
- ;;
704
-
705
- rule)
706
- # Rule INSTEAD-block discipline: every \b(don't|never|avoid)\b
707
- # (case-insensitive, word-boundary) requires INSTEAD/Instead within
708
- # 5 lines AFTER the trigger. Multi-line scan via awk.
709
- UNPAIRED=$(echo "$FUTURE_CONTENT" | awk '
710
- BEGIN { trigger_line = 0; trigger_text = ""; }
711
- {
712
- line_num++
713
- line = $0
714
- # Skip code blocks (between ``` markers) — quoted prose is exempt
715
- if (line ~ /^```/) {
716
- in_code = !in_code
717
- next
718
- }
719
- if (in_code) next
720
-
721
- # If we are tracking a trigger and find INSTEAD within window: clear
722
- # Require INSTEAD (all caps) OR line-start Instead — strict bassclef
723
- # pattern. Mid-prose "instead" doesnt qualify (the rule discipline
724
- # is to put a deliberate INSTEAD: substitute, not natural language).
725
- if (trigger_line > 0) {
726
- if (line_num - trigger_line <= 5) {
727
- if (line ~ /INSTEAD/ || line ~ /^Instead[ ,:]/) {
728
- trigger_line = 0
729
- trigger_text = ""
730
- }
731
- } else {
732
- # Window expired without INSTEAD — emit unpaired
733
- print "L" trigger_line ": " trigger_text
734
- trigger_line = 0
735
- trigger_text = ""
736
- }
737
- }
738
-
739
- # Detect new trigger (only if not already tracking)
740
- if (trigger_line == 0) {
741
- # Skip lines that already contain INSTEAD/line-start Instead on the
742
- # same line (paired inline)
743
- if (line ~ /INSTEAD/ || line ~ /^Instead[ ,:]/) next
744
-
745
- # Check for trigger words. Use lowercased copy for matching.
746
- lower = tolower(line)
747
- if (lower ~ /(^|[^a-z])(don.t|never|avoid)([^a-z]|$)/) {
748
- trigger_line = line_num
749
- trigger_text = substr(line, 1, 80)
750
- }
751
- }
752
- }
753
- END {
754
- # Final pending trigger at EOF
755
- if (trigger_line > 0) {
756
- print "L" trigger_line ": " trigger_text
757
- }
758
- }
759
- ')
760
-
761
- if [ -n "$UNPAIRED" ]; then
762
- # v1 BLOCK (flipped 2026-05-07 in Phase I — Plain English Sweep).
763
- # Allowlist grandfathers existing violators per bassclef#374
764
- # audit; net-new edits to non-allowlisted rules MUST pair every
765
- # don't/never/avoid with a literal INSTEAD within 5 lines.
766
- COUNT=$(echo "$UNPAIRED" | wc -l | tr -d ' ')
767
- VIOLATION="Rule has $COUNT unpaired don't/never/avoid (no literal INSTEAD within 5 lines)."
768
- LINE_REF=$(echo "$UNPAIRED" | head -1 | cut -d: -f1)
769
- SUGGESTED_FIX="Pair each negation with a literal INSTEAD block within 5 lines. Example: 'Don't X.' followed by 'INSTEAD: do Y.' Per rule .claude/rules/context-engineering.md §INSTEAD-block pattern. To grandfather a legacy file: add path to .claude/hooks/substrate-clarity-allowlist.txt with bassclef#374 reference."
770
- fi
771
- ;;
772
-
773
- luminary)
774
- # Luminary INSTEAD-block discipline: same as rule check (v0 ADVISORY).
775
- # Plus required-sections check: every luminary should declare when to
776
- # channel it + what anti-patterns mark drift.
777
- UNPAIRED=$(echo "$FUTURE_CONTENT" | awk '
778
- BEGIN { trigger_line = 0; trigger_text = ""; }
779
- {
780
- line_num++
781
- line = $0
782
- if (line ~ /^```/) { in_code = !in_code; next }
783
- if (in_code) next
784
-
785
- if (trigger_line > 0) {
786
- if (line_num - trigger_line <= 5) {
787
- if (line ~ /INSTEAD/ || line ~ /^Instead[ ,:]/) {
788
- trigger_line = 0
789
- trigger_text = ""
790
- }
791
- } else {
792
- print "L" trigger_line ": " trigger_text
793
- trigger_line = 0
794
- trigger_text = ""
795
- }
796
- }
797
-
798
- if (trigger_line == 0) {
799
- if (line ~ /INSTEAD/ || line ~ /^Instead[ ,:]/) next
800
- lower = tolower(line)
801
- if (lower ~ /(^|[^a-z])(don.t|never|avoid)([^a-z]|$)/) {
802
- trigger_line = line_num
803
- trigger_text = substr(line, 1, 80)
804
- }
805
- }
806
- }
807
- END {
808
- if (trigger_line > 0) {
809
- print "L" trigger_line ": " trigger_text
810
- }
811
- }
812
- ')
813
-
814
- if [ -n "$UNPAIRED" ]; then
815
- # v1 BLOCK (flipped 2026-05-07 in Phase I — Plain English Sweep).
816
- # Allowlist grandfathers existing violators per bassclef#373
817
- # audit; net-new edits to non-allowlisted luminaries MUST pair
818
- # every don't/never/avoid with a literal INSTEAD within 5 lines.
819
- COUNT=$(echo "$UNPAIRED" | wc -l | tr -d ' ')
820
- VIOLATION="Luminary has $COUNT unpaired don't/never/avoid (no literal INSTEAD within 5 lines)."
821
- LINE_REF=$(echo "$UNPAIRED" | head -1 | cut -d: -f1)
822
- SUGGESTED_FIX="Pair each negation with a literal INSTEAD block within 5 lines. Example: 'Don't X.' followed by 'INSTEAD: do Y.' Per rule .claude/rules/context-engineering.md §INSTEAD-block pattern. To grandfather a legacy file: add path to .claude/hooks/substrate-clarity-allowlist.txt with bassclef#373 reference. Brand-negation luminaries (whose signature IS negation) get per-luminary treatment in the audit."
823
- fi
824
-
825
- # Required sections check (kept as ADVISORY — section presence is
826
- # documentation discipline, not a blocking concern. bassclef#373
827
- # audit decides whether to promote this to BLOCK separately).
828
- if [ -z "$VIOLATION" ]; then
829
- MISSING_SECTIONS=""
830
- if ! echo "$FUTURE_CONTENT" | grep -qiE "^#{1,6}[[:space:]]+when[[:space:]]+to[[:space:]]+channel"; then
831
- MISSING_SECTIONS="${MISSING_SECTIONS}'When to channel this luminary' section, "
832
- fi
833
- if ! echo "$FUTURE_CONTENT" | grep -qiE "^#{1,6}[[:space:]]+anti-patterns?\\b"; then
834
- MISSING_SECTIONS="${MISSING_SECTIONS}'Anti-patterns' section, "
835
- fi
836
- if [ -n "$MISSING_SECTIONS" ]; then
837
- MISSING_SECTIONS="${MISSING_SECTIONS%, }"
838
- echo "[substrate-clarity-gate: ADVISORY — $FILE_PATH missing $MISSING_SECTIONS]" >&2
839
- echo "[advisory only; documentation discipline, not blocking]" >&2
840
- fi
841
- fi
842
- ;;
843
-
844
- chronicle|iteration_bet|journal_entry|decomposition|whereami)
845
- # Phase II WU-7b — broader plain-English discipline. Jargon check
846
- # honors SURFACE_TIER:
847
- # - chronicle / iteration_bet / decomposition → strict (BLOCK on jargon)
848
- # after the #303 audit emptied the grandfathered allowlist.
849
- # - journal_entry → advisory (voice trumps rule on personal narrative).
850
- # - whereami → advisory V1 (bassclef-upstream bet 2026-07-04a WU-1).
851
- # Long-sentence + passive-voice stay advisory on all surfaces; those
852
- # are soft signals, not BLOCK-worthy.
853
- NOTICES=""
854
-
855
- # Check 1 — bassclef-internal jargon
856
- # 'substrate' is on the allowlist per bassclef#763 — keep it OUT of
857
- # the BLOCK list here so historical narrative referencing the framework
858
- # by name does not trip the gate.
859
- JARGON=""
860
- for term in 'composer' 'primitive' 'tier-preset' 'load-bearing' 'blast radius' 'compose-with' 'scope-bounded' 'operationalize' 'stuck-signal' 'substrate-defect'; do
861
- if echo "$FUTURE_CONTENT" | grep -qiE "\\b$term\\b"; then
862
- JARGON="${JARGON}${term} "
863
- fi
864
- done
865
- if [ -n "$JARGON" ]; then
866
- if [ "$SURFACE_TIER" = "strict" ]; then
867
- VIOLATION="bassclef-internal jargon found in ${SURFACE}: ${JARGON}— rewrite in plain English per standards/bassclef-internal-jargon.md"
868
- else
869
- NOTICES="${NOTICES}bassclef-internal jargon found: ${JARGON}\\n"
870
- fi
871
- fi
872
-
873
- # Check 2 — long sentences (>50 words) — advisory only on these surfaces
874
- # Strip frontmatter, strip code blocks, then count words per sentence.
875
- LONG_SENTENCES=$(echo "$FUTURE_CONTENT" | awk '
876
- BEGIN { in_frontmatter = 0; in_code = 0; long_count = 0 }
877
- /^---$/ {
878
- if (NR == 1) { in_frontmatter = 1; next }
879
- if (in_frontmatter) { in_frontmatter = 0; next }
880
- }
881
- in_frontmatter { next }
882
- /^```/ { in_code = !in_code; next }
883
- in_code { next }
884
- {
885
- # Skip table rows + headers (markdown noise that fragments sentences)
886
- if ($0 ~ /^[[:space:]]*\|/) next
887
- if ($0 ~ /^[[:space:]]*#/) next
888
- # Split on sentence terminators
889
- n = split($0, parts, /[.!?]+[[:space:]]+/)
890
- for (i = 1; i <= n; i++) {
891
- part = parts[i]
892
- gsub(/`[^`]*`/, "", part) # strip inline code
893
- gsub(/^[[:space:]]+|[[:space:]]+$/, "", part)
894
- if (length(part) == 0) continue
895
- word_count = split(part, words, /[[:space:]]+/)
896
- if (word_count > 50) long_count++
897
- }
898
- }
899
- END { print long_count }
900
- ')
901
- if [ "$LONG_SENTENCES" -gt 0 ]; then
902
- NOTICES="${NOTICES}${LONG_SENTENCES} sentence(s) over 50 words — break into shorter sentences\\n"
903
- fi
904
-
905
- # Check 3 — passive voice (advisory) — simple regex; conservative
906
- PASSIVE=$(echo "$FUTURE_CONTENT" | grep -ciE '\b(was|were|been|being)[[:space:]]+[a-z]+ed[[:space:]]+by\b' || true)
907
- if [ "$PASSIVE" -gt 0 ]; then
908
- NOTICES="${NOTICES}${PASSIVE} passive-voice construction(s) detected (e.g., 'X was Y-ed by Z') — prefer active voice\\n"
909
- fi
910
-
911
- if [ -n "$NOTICES" ]; then
912
- echo "[substrate-clarity-gate: ADVISORY ($SURFACE) — $FILE_PATH]" >&2
913
- echo -e "$NOTICES" >&2
914
- echo "[advisory only; per .claude/rules/plain-english-discipline.md — voice trumps strict rule on narrative surfaces]" >&2
915
- fi
916
- # Do not set VIOLATION
917
- ;;
918
- esac
919
-
920
- # === Soft-INSTEAD audit (V1 ADVISORY — bassclef-upstream#375) ===
921
- # Per @luminary alistair-cockburn (MUST/SHALL precision) + @luminary tony-hoare
922
- # (preconditions absolute). Scans rule/skill/luminary surfaces for soft escape
923
- # clauses that hand the agent an out from a hard MUST contract.
924
- # V1: ADVISORY stderr notice. V2 (deferred, after calibration cycle): BLOCK on
925
- # net-new violations; allowlist for existing.
926
- # Surfaces scanned: skill, rule, luminary, adr (strict surfaces).
927
- case "$SURFACE" in
928
- skill|rule|luminary|adr)
929
- # Strip code blocks + inline backticks from FUTURE_CONTENT before scan
930
- # (matches scripts/soft-instead-audit.sh behavior — false-positive guard
931
- # on quoted examples + pattern mentions).
932
- SOFT_INSTEAD_STRIPPED=$(echo "$FUTURE_CONTENT" | awk '
933
- BEGIN { in_code = 0 }
934
- /^```/ { in_code = !in_code; next }
935
- !in_code {
936
- gsub(/`[^`]*`/, "")
937
- print
938
- }
939
- ')
940
- SOFT_HITS=""
941
- for pattern in 'OR hand-roll' 'OR write inline' 'OR compose by hand' 'if available' 'if at hand' 'when at hand' 'when available' 'if the skill is not' 'when the skill is not'; do
942
- if echo "$SOFT_INSTEAD_STRIPPED" | grep -qiF "$pattern"; then
943
- SOFT_HITS="${SOFT_HITS}${pattern} "
944
- fi
945
- done
946
- if [ -n "$SOFT_HITS" ]; then
947
- echo "[substrate-clarity-gate: ADVISORY (soft-INSTEAD) — $FILE_PATH]" >&2
948
- echo " Soft-INSTEAD patterns found: $SOFT_HITS" >&2
949
- echo " Per @luminary alistair-cockburn: INSTEAD names a MUST contract, not a soft alternative." >&2
950
- echo " Cure: rewrite as MUST + explicit alternative-path (BLOCK when prereq missing)." >&2
951
- echo " See: scripts/soft-instead-audit.sh + architecture/audits/2026-06-26-soft-instead-clauses.md" >&2
952
- echo " V1 advisory; V2 (deferred) BLOCKs net-new violations." >&2
953
- fi
954
- ;;
955
- esac
956
-
957
- # === Claim-shape scan (V1 ADVISORY — bet 2026-07-01e WU-4) ===
958
- # Per bassclef-upstream#582 (R8 class extension to file-write surface) +
959
- # .claude/rules/assert-only-after-verify.md. Fires on rule / chronicle /
960
- # iteration_bet / adr surfaces. Detects paragraphs that read as claims
961
- # (multiple modal words: must, should, always, never, wrong, broken, will)
962
- # without a nearby citation pattern (file path, ticket #, git SHA,
963
- # @luminary slug). Skips journal_entry (personal voice) + skill (WU-4 out
964
- # of scope) + luminary + decomposition.
965
- #
966
- # V1 ADVISORY: emits stderr notice; does not block. V2 (deferred, after
967
- # calibration cycle): flip specific paragraph patterns to BLOCK.
968
- case "$SURFACE" in
969
- rule|chronicle|iteration_bet|adr)
970
- CLAIM_HITS=$(echo "$FUTURE_CONTENT" | python3 -c "
971
- import sys, re
972
-
973
- content = sys.stdin.read()
974
-
975
- # Strip frontmatter
976
- m = re.match(r'^---\n.*?\n---\n', content, re.DOTALL)
977
- if m:
978
- content = content[m.end():]
979
-
980
- # Strip fenced code blocks
981
- content = re.sub(r'\`\`\`.*?\`\`\`', '', content, flags=re.DOTALL)
982
-
983
- # Split into paragraphs (blank-line separated)
984
- paragraphs = re.split(r'\n\s*\n', content)
985
-
986
- modal_re = re.compile(r'\b(must|should|always|never|wrong|broken|will)\b', re.IGNORECASE)
987
- # Citation patterns:
988
- # file paths: .md .sh .json .yml .yaml .py .ts .tsx
989
- # ticket refs: #NNN, bassclef#NNN, sunj-labs/, poa#NNN, GH#NNN
990
- # luminary slugs: @luminary <slug>
991
- # inline backtick paths or accessors: '\`.*\`'
992
- # 'per ' or 'see ' followed by any word
993
- # git SHA (7+ hex)
994
- citation_re = re.compile(
995
- r'\b(?:'
996
- r'[\w./-]+\.(?:md|sh|json|yml|yaml|py|ts|tsx)'
997
- r'|[a-zA-Z-]*#\d{2,}'
998
- r'|@luminary\s+[a-z-]+'
999
- r'|per\s+[\`\w./-]+'
1000
- r'|see\s+[\`\w./-]+'
1001
- r'|sunj-labs/'
1002
- r'|[0-9a-f]{7,40}'
1003
- r')',
1004
- re.IGNORECASE
1005
- )
1006
- # Inline backtick content also counts as citation
1007
- inline_backtick_re = re.compile(r'\`[^\`]+\`')
1008
-
1009
- count = 0
1010
- for p in paragraphs:
1011
- p_stripped = p.strip()
1012
- if not p_stripped:
1013
- continue
1014
- # Skip headings and lists and blockquotes and tables
1015
- first_non_ws = p_stripped[0]
1016
- if first_non_ws in ('#', '-', '*', '|', '>'):
1017
- continue
1018
- # Numbered list
1019
- if re.match(r'^\d+\.\s', p_stripped):
1020
- continue
1021
- modal_count = len(modal_re.findall(p_stripped))
1022
- if modal_count < 2:
1023
- continue
1024
- has_citation = bool(citation_re.search(p_stripped)) or bool(inline_backtick_re.search(p_stripped))
1025
- if not has_citation:
1026
- count += 1
1027
-
1028
- print(count)
1029
- " 2>/dev/null)
1030
- if [ -n "$CLAIM_HITS" ] && [ "$CLAIM_HITS" -gt 0 ]; then
1031
- echo "[substrate-clarity-gate: ADVISORY (claim-shape) — $FILE_PATH]" >&2
1032
- echo " $CLAIM_HITS paragraph(s) read as claims without cited source (2+ modal words, zero citation patterns nearby)." >&2
1033
- echo " Per .claude/rules/assert-only-after-verify.md + bassclef-upstream#582 (R8 at file-write surface)." >&2
1034
- echo " V1 advisory; V2 deferred after calibration cycle." >&2
1035
- fi
1036
- ;;
1037
- esac
1038
-
1039
- fi # close the "skip per-surface dispatch if frontmatter check fired"
1040
-
1041
- # === Rewrite-check heuristics (V1 ADVISORY — goal 25e Path A) ===
1042
- # Sources lib/rewrite-check.sh and runs 6 heuristics on the future content:
1043
- # passive voice, hyphen compounds, sentence length, non-SVO openers,
1044
- # noun stacks, adverb density. Grounded in UK ONS + NN/g + US Federal
1045
- # Plain Language + Hemingway.
1046
- #
1047
- # V1 ADVISORY: emits stderr notice; does not block. Fires on 17 surfaces
1048
- # (rule + luminary + adr + skill + iteration_bet + decomposition + chronicle
1049
- # from existing scope + 15 new SDLC artifact surfaces per Path A). Skips
1050
- # journal_entry (personal voice) + whereami (frontmatter-heavy).
1051
- # V2 flip after calibration cycle.
1052
- case "$SURFACE" in
1053
- journal_entry|whereami)
1054
- # Skip — narrative voice / frontmatter-heavy content
1055
- ;;
1056
- *)
1057
- REWRITE_LIB="$(dirname "$0")/../../lib/rewrite-check.sh"
1058
- if [ -f "$REWRITE_LIB" ]; then
1059
- # Write FUTURE_CONTENT to temp file for heuristic
1060
- REWRITE_TMP=$(mktemp 2>/dev/null)
1061
- if [ -n "$REWRITE_TMP" ]; then
1062
- echo "$FUTURE_CONTENT" > "$REWRITE_TMP"
1063
- # Source in subshell so functions don't pollute parent scope
1064
- HITS=$(
1065
- source "$REWRITE_LIB" 2>/dev/null
1066
- run_rewrite_checks "$REWRITE_TMP" 2>/dev/null | grep -cE "^-- "
1067
- )
1068
- if [ -n "$HITS" ] && [ "$HITS" -gt 0 ]; then
1069
- echo "[substrate-clarity-gate: ADVISORY (rewrite-check) — $FILE_PATH]" >&2
1070
- echo " $HITS finding category/ies from lib/rewrite-check.sh — passive voice / hyphen compounds / sentence length / non-SVO openers / noun stacks / adverb density." >&2
1071
- echo " For details: bash scripts/sweep-kiss-dispatch.sh --dry-run --input <(echo $FILE_PATH)" >&2
1072
- echo " Per goal 25e Path A. V1 advisory; V2 BLOCK deferred until calibration." >&2
1073
- fi
1074
- rm -f "$REWRITE_TMP"
1075
- fi
1076
- fi
1077
- ;;
1078
- esac
1079
-
1080
- # === Emit BLOCK if violation found ===
1081
- if [ -n "$VIOLATION" ]; then
1082
- cat >&2 <<EOF
1083
-
1084
- ============================================
1085
- 🛑 SUBSTRATE-CLARITY GATE — BLOCKED 🛑
1086
- ============================================
1087
-
1088
- File: $FILE_PATH
1089
- Tool: $TOOL_NAME
1090
-
1091
- Violation: $VIOLATION
1092
- Line: $LINE_REF
1093
-
1094
- Suggested fix:
1095
- $SUGGESTED_FIX
1096
-
1097
- Per rule: .claude/rules/skill-description-clarity.md (bassclef#377)
1098
- Per meta: .claude/rules/context-engineering.md (bassclef#371)
1099
-
1100
- Override (logged): SKIP_SUBSTRATE_CLARITY=1 <command>
1101
-
1102
- Allowlist (existing-violations grace):
1103
- .claude/hooks/substrate-clarity-allowlist.txt
1104
- ============================================
1105
-
1106
- EOF
1107
- exit 2
1108
- fi
1109
-
1110
- exit 0