@thebassclef/lite 0.1.3 → 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 +216 -137
  2. package/dist/cli.js +218 -139
  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,2076 +0,0 @@
1
- #!/bin/bash
2
- # tier: lite
3
- # description: BASSCLEF_SYNC_VERSION=2026-09-06-1496-sessionstart-heal-v1
4
- # BASSCLEF_SYNC_VERSION=2026-09-06-1496-sessionstart-heal-v1
5
- # ^^ DO NOT REMOVE — used by drift check to detect out-of-date consumer copies.
6
- # Bump when changing this template's semantics. Format: YYYY-MM-DD-vN.
7
- #
8
- # 2026-08-29-v1: #1417 Branch B (Option F full build) — swap ln -s → cp for
9
- # Sections 2 (skills project-level), 3a (hooks), 3a-postcheck
10
- # (self-heal), 3a-fragments, 3b-skills (user-level), 3d (rules).
11
- # Adopter owns copies; symlinks migrate on next sync per
12
- # ADR-031 grace window. Skills use .bassclef-managed sentinel
13
- # to distinguish bassclef-cp'd dirs from app-specific overrides.
14
- # Rules use per-file cp inside a real dir (adopter-added files
15
- # preserved). Hooks use per-file cp + chmod +x per spike
16
- # prototype pattern (PR #1293). Luminaries + agents + orientation
17
- # + lib (Sections 3c + 3e + 3d-bis + 3f) deferred to follow-on.
18
- #
19
- # 2026-07-26-v1: #897 Cure A — whereami parser accepts +HHMM/+HH:MM offsets
20
- # AND YAML frontmatter last_updated.at fallback. Extracted to
21
- # bassclef_sync_parse_whereami_ts() helper. Test-mode source
22
- # guard added via BASSCLEF_SYNC_TEST_MODE=1. Fixes "unparseable
23
- # — schema non-conformance" warning that fired every session
24
- # start since 2026-07-19 f2929de6 when whereami switched from
25
- # Z to local BST timestamps.
26
- #
27
- # 2026-07-04-v1: bet 2026-07-03b WU-0 (bassclef-upstream#625) — §3c-hooks
28
- # dynamic-derive. Reads bassclef's own .claude/settings.json
29
- # via presence/install/bassclef-hook-connect.sh (sourceable lib)
30
- # and merges every hook entry across all 6 event types into the
31
- # adopter settings.json. Prior 5-entry hardcoded list retired.
32
- # Dispatcher exports BASSCLEF_SOURCE_ROOT before exec.
33
- # Closes the 10 %-coverage class POA hit 2026-07-02.
34
- #
35
- # 2026-05-07-v1: Phase II Trim the Fat WU-10 — auto-wire bassclef-managed
36
- # PreToolUse hooks into consumer settings.json (bassclef#569).
37
- # Surfaced at sibling validation 2026-05-07: POA + TwoDo + Ops
38
- # all confirmed Phase II hooks ship dead-letter — scripts on
39
- # disk, no settings.json registration, real tool calls bypass
40
- # them. Section 3c-hooks added: jq-based merge of bassclef-
41
- # managed PreToolUse entries into <consumer>/.claude/settings.json.
42
- # Idempotent. Skips bassclef itself (detected via remote URL).
43
- # Operator override: SKIP_BASSCLEF_HOOK_WIRE=1.
44
- #
45
- # 2026-05-05-v3: drift-block remediation message updated to match thin-pointer
46
- # reality (bassclef#418 LR8.5 WU-3). Pre-2026-05-03 vendored-
47
- # consumer drift was fixed by `gh workflow run bassclef-template-
48
- # sync.yml`; that workflow retired in PR #445. New remediation:
49
- # migrate to thin-pointer via /onboard-repo (one-shot — eliminates
50
- # the drift class entirely since thin-pointer fetches bassclef's
51
- # current template every session-start).
52
- # 2026-05-05-v2: whereami staleness uses commit-delta (bassclef#514). Idle
53
- # consumers (zero commits since whereami was last committed)
54
- # downgrade from BLOCKED to INFO at the >=7d tier — calendar
55
- # staleness alone stops being a gate when nothing changed in
56
- # the repo. Commits-since computed via git log -1 -- docs/
57
- # whereami.md + git rev-list --count. SKIP_WHEREAMI_COMMIT_DELTA=1
58
- # forces idle (commits=0) for migration. This is sequenced
59
- # with v1 (Strategy D HTTP API fetch) — both ship in LR8.5.
60
- # 2026-05-05-v1: Strategy D — HTTP API fetch via .bassclef-source.json.
61
- # Reads consumer's .bassclef-source.json (per
62
- # standards/bassclef-source-config.md), dispatches authenticated
63
- # tarball curl to the configured source_url, extracts via
64
- # --strip-components=1 (GitHub repo-API tarball pattern). Soft-
65
- # falls to Strategy B (S3) if config missing, malformed, token
66
- # unset, fetch fails, or extraction fails. Banner emits
67
- # "Status: fetched from bassclef (HTTP API)" so the new path
68
- # is observable. Closes bassclef#419. Sets up bassclef#446
69
- # (retire S3 publish) and bassclef#418 (default thin-pointer
70
- # onboarding writes .bassclef-source.json instead of
71
- # .bassclef-url).
72
- # 2026-05-02-v1: drift check is thin-pointer-aware. Consumer hooks with
73
- # version string matching `thin-pointer-*` opt out of the
74
- # drift comparison — they are intentionally a different file
75
- # from this template (130-line dispatcher vs 800+ line
76
- # template). Different role, different version namespace.
77
- # The dispatcher's job is to delegate to bassclef's current
78
- # template on every fire, not mirror its version string.
79
- # Per bassclef#418. Closes the false-positive drift-block
80
- # that fired every session on thin-pointer consumers between
81
- # 2026-05-02 (TwoDo migration) and this fix.
82
- # 2026-04-26-v1: #267/#268/#269 fire-and-forget hardening.
83
- # #267 — auto-heal drift when working tree is clean AND on main
84
- # AND no unpushed commits AND remote-main matches bassclef.
85
- # Self-pulls + re-execs hook silently (no operator BLOCKED on
86
- # the common case). Preconditions guard against destructive
87
- # pulls into uncommitted/diverged state.
88
- # #268 — when both local AND remote main lag template AND no
89
- # open sync PR exists, surface "orphaned closure" diagnosis
90
- # instead of "try git pull" (which would be a no-op).
91
- # #269 — paired with #268, offer one-line workflow_dispatch
92
- # remediation: `gh workflow run bassclef-template-sync.yml`
93
- # fires a fresh sync. Operator decides merge vs override.
94
- # 2026-04-20-v2: #225 follow-up — surface URL-age tier in the mandatory
95
- # "YOU MUST confirm" line so agents reinforce warn/expired
96
- # state to operators, not just emit it in verbose hook output.
97
- # OK tier stays silent on the confirm line (no clutter); warn
98
- # and expired tiers are guaranteed-surfaced.
99
- # 2026-04-20-v1: #224 tiered URL-age banner at session-start — observability
100
- # into presigned URL freshness so operators can distinguish
101
- # benign rotation-race from stale-URL/reclone-needed without
102
- # diving into Actions logs. Thresholds: <18h OK, 18-24h WARN,
103
- # fetched-when-expired is unreachable here (curl 403s earlier).
104
- # 2026-04-19-v3: #173 drift block first checks remote main hook version via
105
- # gh API; if remote matches template, emit STALE message
106
- # (recommend git pull) instead of BLOCKED. Generalizes #189's
107
- # merged-PR detection to any cause of local-behind-main drift.
108
- # Falls through to existing BLOCKED on gh unavailable / network
109
- # down / remote-main also lags template (genuine drift).
110
- # 2026-04-19-v2: #192 session-start health check for missing onboarding
111
- # artifacts (CLAUDE.md + substrate.config.md — whereami
112
- # already covered by its own block)
113
- # 2026-04-19-v1: #189 drift block detects merged-but-unpulled PR (branches
114
- # remediation text: open PR → merge; merged → git pull)
115
- # 2026-04-17-v1: #162 tarball integrity check + ephemeral-gated settings.json
116
- # #161 fail-loud on empty bassclef tree
117
- # #160 bootstrap bassclef-evolution labels
118
- #
119
- # Bassclef Substrate Sync — fires on SessionStart.
120
- # Ensures bassclef standards are available regardless of device.
121
- #
122
- # Fetch strategies (first match wins; D and B both ephemeral, D preferred):
123
- # A. Peer directory — $CWD/../bassclef already present (desktop / web
124
- # with multi-repo / CI with pre-clone)
125
- # D. HTTP API — .bassclef-source.json in CWD + ephemeral env
126
- # detected; reads source_url + auth_method +
127
- # token_env, curls authenticated tarball, extracts
128
- # with --strip-components=1 (GitHub repo-API pattern).
129
- # Soft-falls to B on any failure (malformed, token
130
- # unset, HTTP non-200, extract fails).
131
- # B. S3 tarball — .bassclef-url in CWD + ephemeral env detected.
132
- # Fires when D unavailable or fails. Retires once
133
- # every consumer is migrated to .bassclef-source.json
134
- # (bassclef#446).
135
- # C. Bash clone — original path; desktop without peer and with
136
- # GitHub credentials
137
- #
138
- # Post-fetch (same for all strategies):
139
- # 1. Symlinks bassclef skills into project .claude/skills/
140
- # 2. Installs general hooks + agents to ~/.claude/
141
- # 3. Writes ~/.claude/settings.json for user-level hook wiring
142
- # 4. Maintains .gitignore managed block for symlinks
143
- # 5. Sets env var for rules/CLAUDE.md inheritance
144
- #
145
- # See ADR-014 in bassclef for the distribution model rationale.
146
-
147
- INPUT=$(cat)
148
- CWD=$(echo "$INPUT" | jq -r '.cwd // "."')
149
- cd "$CWD"
150
-
151
- # Opt-out sentinel (#119): repos that must NOT inherit bassclef substrate
152
- # (public OSS repos curated from bassclef, ephemeral dirs, test fixtures)
153
- # can place a .no-bassclef-sync file in the repo root to suppress sync.
154
- if [ -f "$CWD/.no-bassclef-sync" ]; then
155
- echo "Bassclef substrate sync: skipped (.no-bassclef-sync sentinel present)."
156
- exit 0
157
- fi
158
-
159
- BASSCLEF_REPO="https://github.com/sunj-labs/bassclef.git"
160
- SYNC_STATUS="up-to-date"
161
- SYNC_SOURCE="unknown"
162
- SYNC_NEW=""
163
- FETCH_DONE=0
164
-
165
- # === V1 helper: installed-metadata writeback (bassclef#1037 Phase B WU-V1) ===
166
- # After a successful fetch, write installed_sha + installed_at into the
167
- # adopter's .bassclef-source.json so 45-auto-sync-status.sh and /whereami
168
- # can display the install version + delta-to-current. Best-effort: silent
169
- # failure when jq missing, config absent, config malformed, or SHA empty.
170
- # Adopter still gets the substrate even if the metadata write fails.
171
- bassclef_sync_write_installed_metadata() {
172
- local config="$1"
173
- local sha="$2"
174
- local at="$3"
175
- # Silent skip if any required input missing
176
- [ -z "$config" ] && return 0
177
- [ -z "$sha" ] && return 0
178
- [ -z "$at" ] && return 0
179
- [ ! -f "$config" ] && return 0
180
- command -v jq >/dev/null 2>&1 || return 0
181
- # Validate existing JSON before mutating
182
- jq empty "$config" >/dev/null 2>&1 || return 0
183
- local tmp="$config.installed-writeback.tmp"
184
- if jq --arg sha "$sha" --arg at "$at" \
185
- '.installed_sha = $sha | .installed_at = $at' \
186
- "$config" > "$tmp" 2>/dev/null; then
187
- mv "$tmp" "$config"
188
- else
189
- rm -f "$tmp" 2>/dev/null || true
190
- fi
191
- return 0
192
- }
193
-
194
- # === #897 Cure A: whereami timestamp parser ===
195
- # Extracts the whereami last_updated timestamp and normalizes it to
196
- # UTC Z form for downstream date math. Reads YAML frontmatter
197
- # `last_updated.at: <TS>` — the canonical shape since #1481 diagnose.
198
- # Legacy `## Last updated` markdown-section reader retired per goal
199
- # 2026-09-06d Cure 3. Adopters mid-migration write frontmatter via
200
- # session-end.sh; sync inherits new shape on next SessionStart.
201
- # Regex accepts Z, +HHMM, -HHMM, +HH:MM, -HH:MM offsets per RFC 3339.
202
- # Output is normalized to Z form so downstream date -j -f "%Y-%m-%dT%H:%M:%SZ"
203
- # reads one canonical shape. Returns empty on missing file, missing
204
- # timestamp, or malformed timestamp — caller treats empty as unparseable.
205
- #
206
- # Portable across Darwin (BSD date) + Linux (GNU date).
207
- #
208
- # Per #897 Cure A. Sister #897 Cure B (write-time PreToolUse gate on
209
- # docs/whereami.md edits) deferred to sister ticket.
210
- bassclef_sync_parse_whereami_ts() {
211
- local whereami_path="$1"
212
- local ts=""
213
- local ts_regex='[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(Z|[+-][0-9]{2}:?[0-9]{2})?'
214
-
215
- [ -z "$whereami_path" ] && return 0
216
- [ ! -f "$whereami_path" ] && return 0
217
-
218
- # YAML frontmatter last_updated.at (canonical shape)
219
- ts=$(awk '/^last_updated:/{flag=1;next}
220
- /^[a-zA-Z]/{flag=0}
221
- flag && /^ at:/{
222
- sub(/^ at:[ ]*/, "");
223
- sub(/[ ]*$/, "");
224
- print;
225
- exit
226
- }' "$whereami_path" 2>/dev/null | \
227
- grep -oE "$ts_regex" | head -1)
228
-
229
- [ -z "$ts" ] && return 0
230
-
231
- # Normalize to Z form. Already-Z passes through.
232
- case "$ts" in
233
- *Z)
234
- echo "$ts"
235
- return 0
236
- ;;
237
- esac
238
-
239
- # Strip colon from offset for BSD date's %z (which expects +HHMM not +HH:MM)
240
- local ts_no_colon
241
- ts_no_colon=$(echo "$ts" | sed -E 's/([+-][0-9]{2}):([0-9]{2})$/\1\2/')
242
-
243
- local epoch=""
244
- # Darwin: date -j -f "%Y-%m-%dT%H:%M:%S%z"
245
- if epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$ts_no_colon" "+%s" 2>/dev/null); then
246
- date -u -r "$epoch" "+%Y-%m-%dT%H:%M:%SZ"
247
- return 0
248
- fi
249
- # GNU: date -d handles ISO 8601 with offset natively
250
- if epoch=$(date -d "$ts" "+%s" 2>/dev/null); then
251
- date -u -d "@$epoch" "+%Y-%m-%dT%H:%M:%SZ"
252
- return 0
253
- fi
254
-
255
- # Fallback: pass through unnormalized (downstream will call it unparseable)
256
- return 0
257
- }
258
-
259
- # === Test-mode early return (#897 Cure A) ===
260
- # Tests source this template to access helper functions (see
261
- # .claude/hooks/tests/bassclef-sync-*.test.sh). Early return under
262
- # BASSCLEF_SYNC_TEST_MODE=1 skips the main body's stdin read + sync
263
- # flow. Helpers are defined ABOVE this guard; test access preserved.
264
- if [ "${BASSCLEF_SYNC_TEST_MODE:-0}" = "1" ]; then
265
- return 0 2>/dev/null || exit 0
266
- fi
267
-
268
- # === 0. Environment detection ===
269
- # Primary signal: operator-set CLAUDE_EPHEMERAL=1.
270
- # Fallback heuristic: cwd not under /Users/ (operator Mac) AND no ../bassclef peer dir.
271
- # See ADR-014 and .claude/rules/mobile-ephemeral-session.md.
272
- IS_EPHEMERAL=0
273
- if [ "${CLAUDE_EPHEMERAL:-}" = "1" ]; then
274
- IS_EPHEMERAL=1
275
- elif [[ "$CWD" != /Users/* ]] && [ ! -d "$CWD/../bassclef/.claude" ]; then
276
- # Likely ephemeral: non-operator filesystem AND no peer bassclef
277
- IS_EPHEMERAL=1
278
- fi
279
-
280
- # === 1. Fetch bassclef (strategy order: peer > HTTP API > S3 > bash clone) ===
281
- #
282
- # Strategies are now independent guarded blocks rather than if/elif/else, so
283
- # Strategy D can soft-fall to Strategy B without restructuring control flow.
284
- # Each block sets FETCH_DONE=1 on success; subsequent blocks skip when that
285
- # flag is set. Strategy B's existing failure paths still call exit 0 (its
286
- # behavior is unchanged when it runs); D's failures are non-fatal — they
287
- # log to stderr and let B/C try.
288
-
289
- # === 0a. SELF_MODE substrate-repo detection (per #901 Cure 1) ===
290
- # Extends #293 SELF_MODE detection with a second condition: CWD is a
291
- # bassclef substrate SOURCE repo (bassclef or bassclef-upstream). The
292
- # #293 check only matched bassclef-on-bassclef (CWD == BASSCLEF_DIR).
293
- # bassclef-upstream running against the public bassclef sibling
294
- # ($CWD/../bassclef) has DIFFERENT paths, so #293 missed it — Section
295
- # 3a then ran and converted 84 tracked hook files to cross-repo
296
- # symlinks (root cause of #893/#894/#896/#898/#899/#900/#902 CI failures
297
- # per session log chronicle/2026-07-26b-tree-corruption-5-lens-
298
- # diagnosis.md).
299
- #
300
- # Cure: detect substrate-repo status via git remote pattern.
301
- # If CWD's origin URL matches sunj-labs/(bassclef|bassclef-upstream),
302
- # set SELF_MODE=1 regardless of Strategy A's peer-resolve check.
303
- #
304
- # Override: SKIP_SELF_MODE_DETECTION=1 for one-shot recovery scenarios.
305
- #
306
- # Anchor: @luminary linus-torvalds (we-dont-break-adopters per
307
- # ADR-031 — a fresh checkout of bassclef-upstream must not inherit
308
- # broken tree state) + @luminary jerome-saltzer-and-michael-schroeder
309
- # (complete mediation — every install path guarded).
310
- SELF_MODE=${SELF_MODE:-0}
311
- if [ "${SKIP_SELF_MODE_DETECTION:-0}" != "1" ]; then
312
- CWD_REMOTE=$(git -C "$CWD" config --get remote.origin.url 2>/dev/null || echo "")
313
- if echo "$CWD_REMOTE" | grep -qE 'sunj-labs/(bassclef|bassclef-upstream)(\.git)?$'; then
314
- SYNC_SOURCE="self (bassclef substrate source repo)"
315
- SELF_MODE=1
316
- fi
317
- fi
318
-
319
- # Strategy A: peer directory (desktop / web with multi-repo / any env with bassclef pre-present)
320
- if [ -d "$CWD/../bassclef/.claude" ]; then
321
- BASSCLEF_DIR="$CWD/../bassclef"
322
- # Self-detection (#293): if peer resolves back to ourselves (bassclef-on-bassclef),
323
- # mark SELF_MODE=1 so Section 3a (the only installer that replaces real files
324
- # rather than leaving them alone) is skipped. Without this guard, Section 3a's
325
- # "replace stale real files with symlinks" policy turns bassclef's own real
326
- # hook files into symlinks pointing back to themselves via .. → ELOOP at hook
327
- # execution time. Sections 2, 3b-skills, and 3c are already self-safe via
328
- # their existing "leave real dirs alone" or "different filesystem location"
329
- # patterns and need no gating.
330
- #
331
- # Note: 0a above catches the bassclef-upstream case where CWD ≠ BASSCLEF_DIR
332
- # but CWD still IS a substrate source repo. Both checks compose.
333
- if [ "$(cd "$BASSCLEF_DIR" 2>/dev/null && pwd -P)" = "$(cd "$CWD" 2>/dev/null && pwd -P)" ]; then
334
- SYNC_SOURCE="self (bassclef on bassclef)"
335
- SELF_MODE=1
336
- elif [ "$SELF_MODE" != "1" ]; then
337
- SYNC_SOURCE="peer"
338
- fi
339
- # Best-effort pull (may fail in ephemeral / no creds — non-fatal)
340
- BEFORE=$(git -C "$BASSCLEF_DIR" rev-parse HEAD 2>/dev/null)
341
- git -C "$BASSCLEF_DIR" pull --quiet 2>/dev/null || true
342
- AFTER=$(git -C "$BASSCLEF_DIR" rev-parse HEAD 2>/dev/null)
343
- if [ -n "$BEFORE" ] && [ "$BEFORE" != "$AFTER" ]; then
344
- SYNC_STATUS="updated"
345
- SYNC_NEW=$(git -C "$BASSCLEF_DIR" log --oneline "$BEFORE".."$AFTER" 2>/dev/null)
346
- fi
347
- FETCH_DONE=1
348
- fi
349
-
350
- # Strategy D: HTTP API via .bassclef-source.json (ephemeral; primary)
351
- # Per standards/bassclef-source-config.md schema:
352
- # { source_url, auth_method (Bearer|Basic|none), token_env, comment? }
353
- # Soft-fall to Strategy B if config is malformed, the named env var is unset,
354
- # the curl fetch returns non-200, or extraction fails. None of those are fatal
355
- # at this layer — they let B/C try. Genuinely-fatal cases (no fallback files
356
- # present) are handled in Strategy C's failure branch below.
357
- if [ "$FETCH_DONE" = "0" ] && [ "$IS_EPHEMERAL" = "1" ] && [ -f "$CWD/.bassclef-source.json" ]; then
358
- D_CONFIG="$CWD/.bassclef-source.json"
359
- D_SOURCE_URL=$(jq -r '.source_url // empty' "$D_CONFIG" 2>/dev/null)
360
- D_AUTH_METHOD=$(jq -r '.auth_method // empty' "$D_CONFIG" 2>/dev/null)
361
- D_TOKEN_ENV=$(jq -r '.token_env // empty' "$D_CONFIG" 2>/dev/null)
362
-
363
- # Validate config minimally — schema-level validation lives in
364
- # bassclef-source-config-validate.sh PreToolUse hook; this is the runtime
365
- # defensive check for the case the file was hand-edited or the validator
366
- # was bypassed.
367
- if [ -z "$D_SOURCE_URL" ] || [ -z "$D_AUTH_METHOD" ]; then
368
- echo "[bassclef-sync] Strategy D: .bassclef-source.json malformed (source_url or auth_method missing); falling through" >&2
369
- else
370
- # Resolve token via bash indirect expansion (safer than eval).
371
- # ${!VAR:-} returns empty when VAR is unset.
372
- D_TOKEN_VALUE=""
373
- if [ "$D_AUTH_METHOD" != "none" ] && [ -n "$D_TOKEN_ENV" ]; then
374
- D_TOKEN_VALUE="${!D_TOKEN_ENV:-}"
375
- fi
376
-
377
- if [ "$D_AUTH_METHOD" != "none" ] && [ -z "$D_TOKEN_VALUE" ]; then
378
- echo "[bassclef-sync] Strategy D: env var '$D_TOKEN_ENV' is unset; falling through to S3" >&2
379
- else
380
- D_DIR="$HOME/bassclef"
381
- mkdir -p "$D_DIR" 2>/dev/null || true
382
- rm -rf "$D_DIR"/* "$D_DIR"/.[!.]* 2>/dev/null || true
383
-
384
- D_TARBALL="/tmp/bassclef-sync-d-$$.tar.gz"
385
-
386
- # GitHub repo tarball API redirects (302) to a codeload URL — -L follows.
387
- # No retry loop here (unlike B's two-shot); the soft-fall to S3 IS the
388
- # retry. Future hardening: per-strategy retry with backoff if real-world
389
- # flakiness shows up.
390
- if [ "$D_AUTH_METHOD" = "none" ]; then
391
- D_HTTP=$(curl -sSL -w "%{http_code}" -o "$D_TARBALL" "$D_SOURCE_URL" 2>/dev/null || echo "000")
392
- else
393
- D_HTTP=$(curl -sSL -w "%{http_code}" -o "$D_TARBALL" \
394
- -H "Authorization: $D_AUTH_METHOD $D_TOKEN_VALUE" \
395
- "$D_SOURCE_URL" 2>/dev/null || echo "000")
396
- fi
397
-
398
- if [ "$D_HTTP" != "200" ]; then
399
- rm -f "$D_TARBALL" 2>/dev/null
400
- echo "[bassclef-sync] Strategy D: HTTP $D_HTTP from $D_SOURCE_URL; falling through to S3" >&2
401
- elif ! tar -xzf "$D_TARBALL" --strip-components=1 -C "$D_DIR" 2>/dev/null; then
402
- # GitHub repo-API tarballs wrap content under <repo>-<sha>/; --strip-
403
- # components=1 normalizes. If a future provider returns a non-wrapped
404
- # tarball, this fails and we fall to B — the schema does not yet
405
- # parameterize strip-components. Schema extension is a future-when-
406
- # needed concern, not a today concern (sunj-labs all use the wrapped
407
- # GitHub repo-API form per the bassclef#419 acceptance).
408
- rm -f "$D_TARBALL" 2>/dev/null
409
- rm -rf "$D_DIR"/* "$D_DIR"/.[!.]* 2>/dev/null || true
410
- echo "[bassclef-sync] Strategy D: tar extraction failed; falling through to S3" >&2
411
- else
412
- # V1 (bassclef#1037 WU-V1): capture installed SHA from the tarball's
413
- # wrapping directory before normalization. GitHub repo-API tarballs
414
- # wrap content under <repo>-<sha>/ where <sha> is the full commit SHA.
415
- D_INSTALLED_SHA=$(tar -tzf "$D_TARBALL" 2>/dev/null | head -1 | sed 's|/$||' | sed 's|^.*-||')
416
- rm -f "$D_TARBALL"
417
- BASSCLEF_DIR="$D_DIR"
418
- SYNC_STATUS="fetched from bassclef (HTTP API)"
419
- SYNC_SOURCE="bassclef-http-api"
420
- FETCH_DONE=1
421
- # Best-effort writeback; silent failure does not block sync
422
- bassclef_sync_write_installed_metadata \
423
- "$CWD/.bassclef-source.json" \
424
- "$D_INSTALLED_SHA" \
425
- "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
426
- fi
427
- fi
428
- fi
429
- fi
430
-
431
- # Strategy B: S3 tarball via .bassclef-url (mobile / ephemeral)
432
- if [ "$FETCH_DONE" = "0" ] && [ "$IS_EPHEMERAL" = "1" ] && [ -f "$CWD/.bassclef-url" ]; then
433
- BASSCLEF_DIR="$HOME/bassclef"
434
- SYNC_SOURCE="s3"
435
- URL=$(jq -r '.url' "$CWD/.bassclef-url" 2>/dev/null)
436
- URL_AGE_GENERATED_AT=$(jq -r '.generated_at' "$CWD/.bassclef-url" 2>/dev/null)
437
- URL_EXPIRES_AT=$(jq -r '.expires_at' "$CWD/.bassclef-url" 2>/dev/null)
438
- if [ -z "$URL" ] || [ "$URL" = "null" ]; then
439
- echo "### BASSCLEF SYNC FAILED"
440
- echo ""
441
- echo ".bassclef-url is present but malformed (missing .url field)."
442
- echo ""
443
- exit 0
444
- fi
445
-
446
- mkdir -p "$BASSCLEF_DIR" 2>/dev/null || true
447
- rm -rf "$BASSCLEF_DIR"/* "$BASSCLEF_DIR"/.[!.]* 2>/dev/null || true
448
-
449
- # Fetch tarball; retry once on transient failure
450
- TARBALL="/tmp/bassclef-sync-$$.tar.gz"
451
- HTTP_CODE=$(curl -sS -w "%{http_code}" -o "$TARBALL" "$URL" 2>/dev/null || echo "000")
452
-
453
- if [ "$HTTP_CODE" != "200" ]; then
454
- sleep 5
455
- HTTP_CODE=$(curl -sS -w "%{http_code}" -o "$TARBALL" "$URL" 2>/dev/null || echo "000")
456
- fi
457
-
458
- if [ "$HTTP_CODE" != "200" ]; then
459
- rm -f "$TARBALL" 2>/dev/null
460
- echo "### BASSCLEF SYNC FAILED"
461
- echo ""
462
- echo "S3 fetch returned HTTP $HTTP_CODE. Presigned URL may be expired."
463
- echo "Generated: $URL_AGE_GENERATED_AT"
464
- echo "Expires: $URL_EXPIRES_AT"
465
- echo "Wait for bassclef main merge or run bassclef-s3-publish workflow_dispatch."
466
- echo ""
467
- exit 0
468
- fi
469
-
470
- # === 1a. Tarball integrity check (#162) ===
471
- # Fail closed if downloaded tarball's sha256 doesn't match what the workflow
472
- # recorded in .bassclef-url. Defends against S3 content tampering and
473
- # corrupted downloads. Emergency override: SKIP_INTEGRITY_CHECK=1.
474
- EXPECTED_SHA=$(jq -r '.tarball_sha256 // empty' "$CWD/.bassclef-url" 2>/dev/null)
475
- if [ "${SKIP_INTEGRITY_CHECK:-}" != "1" ] && [ -n "$EXPECTED_SHA" ]; then
476
- if command -v sha256sum >/dev/null 2>&1; then
477
- ACTUAL_SHA=$(sha256sum "$TARBALL" | awk '{print $1}')
478
- else
479
- ACTUAL_SHA=$(shasum -a 256 "$TARBALL" | awk '{print $1}')
480
- fi
481
- if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
482
- rm -f "$TARBALL" 2>/dev/null
483
- echo "### BASSCLEF SYNC FAILED — INTEGRITY CHECK"
484
- echo ""
485
- echo "Tarball sha256 mismatch:"
486
- echo " Expected: $EXPECTED_SHA"
487
- echo " Actual: $ACTUAL_SHA"
488
- echo ""
489
- echo "Possible causes: S3 content tampering, corrupted download, stale URL."
490
- echo "Emergency override (review first): SKIP_INTEGRITY_CHECK=1"
491
- echo ""
492
- exit 0
493
- fi
494
- fi
495
-
496
- if ! tar -xzf "$TARBALL" -C "$BASSCLEF_DIR" 2>/dev/null; then
497
- rm -f "$TARBALL" 2>/dev/null
498
- rm -rf "$BASSCLEF_DIR"/* "$BASSCLEF_DIR"/.[!.]* 2>/dev/null || true
499
- echo "### BASSCLEF SYNC FAILED"
500
- echo ""
501
- echo "Tarball extraction failed. Partial extract removed."
502
- echo ""
503
- exit 0
504
- fi
505
- rm -f "$TARBALL"
506
- SYNC_STATUS="fetched from S3"
507
- FETCH_DONE=1
508
- fi
509
-
510
- # Strategy C: bash clone (original path, desktop without peer)
511
- if [ "$FETCH_DONE" = "0" ]; then
512
- BASSCLEF_DIR="$CWD/../bassclef"
513
- SYNC_SOURCE="bash-clone"
514
- if git clone "$BASSCLEF_REPO" "$BASSCLEF_DIR" 2>/dev/null; then
515
- SYNC_STATUS="freshly cloned"
516
- FETCH_DONE=1
517
- else
518
- echo "### BASSCLEF SYNC FAILED"
519
- echo ""
520
- echo "Could not clone bassclef. Skills, rules, and agents unavailable."
521
- echo "Paths attempted:"
522
- echo " - peer (\$CWD/../bassclef): not present"
523
- if [ "$IS_EPHEMERAL" = "1" ]; then
524
- echo " - HTTP API (.bassclef-source.json): $([ -f "$CWD/.bassclef-source.json" ] && echo "file present but Strategy D fell through (see [bassclef-sync] stderr lines above for reason)" || echo "file missing; run /onboard-repo to write one, or set up .bassclef-url as fallback")"
525
- echo " - S3 (.bassclef-url): $([ -f "$CWD/.bassclef-url" ] && echo "file present but Strategy B fell through" || echo "file missing; run bassclef-s3-publish workflow or merge pending sync PR")"
526
- fi
527
- echo " - bash clone: failed (private repo, no credentials)"
528
- echo ""
529
- exit 0
530
- fi
531
- fi
532
-
533
- # === 1b. Assert bassclef tree is non-empty (#161) ===
534
- # Defense against silent failure: fetch + extract can succeed (HTTP 200, tar
535
- # exits 0) while leaving an empty or partial tree. Without this check, the
536
- # sync banner reports success and SessionStart proceeds with no substrate.
537
- for REQUIRED in ".claude/skills" ".claude/hooks" ".claude/luminaries" ".claude/rules" ".claude/agents" "lib"; do
538
- if [ ! -d "$BASSCLEF_DIR/$REQUIRED" ] || [ -z "$(ls -A "$BASSCLEF_DIR/$REQUIRED" 2>/dev/null)" ]; then
539
- echo "### BASSCLEF SYNC FAILED — EMPTY OR PARTIAL TREE"
540
- echo ""
541
- echo "Bassclef tree is missing or empty at: $BASSCLEF_DIR/$REQUIRED"
542
- echo "Source: $SYNC_SOURCE"
543
- echo ""
544
- echo "Recovery:"
545
- echo " - Peer source: re-pull \$CWD/../bassclef"
546
- echo " - S3 source: rm -rf \$HOME/bassclef and re-trigger; or re-run bassclef-s3-publish"
547
- echo " - Bash clone: check credentials; try again"
548
- echo ""
549
- exit 0
550
- fi
551
- done
552
-
553
- # === 2. Copy bassclef skills into project for auto-discovery ===
554
- # Per goal 2026-08-29c #1417 Branch B (Option F full build) — swap ln -s → cp
555
- # so adopter owns the files. Symlinks break under operator mv/rename of
556
- # bassclef source; owned copies survive. Spike-proven per PR #1293.
557
- #
558
- # Migration cutover per ADR-031 grace window: existing symlinks land as
559
- # copies on the next sync. App-specific skills marked by absence of the
560
- # .bassclef-managed sentinel stay untouched.
561
- mkdir -p "$CWD/.claude/skills" 2>/dev/null || true
562
- SKILL_INSTALL_COUNT=0
563
- for SKILL_DIR in "$BASSCLEF_DIR/.claude/skills/"*/; do
564
- SKILL_NAME=$(basename "$SKILL_DIR")
565
- TARGET="$CWD/.claude/skills/$SKILL_NAME"
566
- if [ -L "$TARGET" ]; then
567
- # Existing symlink from prior sync — migrate to owned copy
568
- rm "$TARGET"
569
- cp -R "$SKILL_DIR" "$TARGET"
570
- touch "$TARGET/.bassclef-managed" 2>/dev/null || true
571
- SKILL_INSTALL_COUNT=$((SKILL_INSTALL_COUNT + 1))
572
- elif [ ! -d "$TARGET" ]; then
573
- # Doesn't exist — cp fresh
574
- cp -R "$SKILL_DIR" "$TARGET"
575
- touch "$TARGET/.bassclef-managed" 2>/dev/null || true
576
- SKILL_INSTALL_COUNT=$((SKILL_INSTALL_COUNT + 1))
577
- elif [ -f "$TARGET/.bassclef-managed" ]; then
578
- # Prior cp'd bassclef skill — refresh in place (adopter files inside skill dir preserved)
579
- cp -Rf "$SKILL_DIR"* "$TARGET/" 2>/dev/null || true
580
- touch "$TARGET/.bassclef-managed" 2>/dev/null || true
581
- SKILL_INSTALL_COUNT=$((SKILL_INSTALL_COUNT + 1))
582
- fi
583
- # Else: real dir without .bassclef-managed → app-specific override; leave alone
584
- done
585
- [ "$SKILL_INSTALL_COUNT" -gt 0 ] && echo " bassclef-sync: installed/refreshed $SKILL_INSTALL_COUNT skill(s) via cp" >&2 || true
586
-
587
- # === 2b. Park user-level orphans (widened per ADR-033 + bassclef#278; settings-aware per #294) ===
588
- # Closes the skill / agent / hook duplication-in-available-skills bug.
589
- # Calls the shared orphan-parking helper for each surface where user-level
590
- # entries can duplicate project-level entries. Helper:
591
- # - Parks symlinks AND cp'd files whose basename matches project-level
592
- # - Leaves real directories untouched (operator-authored content)
593
- # - EXCLUDES hooks wired in ~/.claude/settings.json at $HOME paths
594
- # (per #294 — settings.json wiring is observable behavior; parking
595
- # must not break it)
596
- # - Idempotent — re-running on already-parked state is a no-op
597
- # - Backup dirs (~/.claude/<prefix>.orphans.<date>/) survive 30 days
598
- # minimum per Hyrum + Linus discipline
599
- PARKING_HELPER="$BASSCLEF_DIR/scripts/bassclef-orphan-parking.sh"
600
- USER_SETTINGS="$HOME/.claude/settings.json"
601
- ORPHAN_COUNT=0
602
- if [ -f "$PARKING_HELPER" ]; then
603
- for surface in skills agents hooks; do
604
- USER_SURFACE="$HOME/.claude/$surface"
605
- PROJECT_SURFACE="$CWD/.claude/$surface"
606
- if [ -d "$USER_SURFACE" ] && [ -d "$PROJECT_SURFACE" ]; then
607
- mkdir -p "$HOME/.claude" 2>/dev/null
608
- # Settings-aware: passing user settings.json as 5th arg so wired
609
- # hooks are excluded from parking (#294).
610
- PARKED_LINE=$(bash "$PARKING_HELPER" "$USER_SURFACE" "$PROJECT_SURFACE" "$HOME/.claude" "$surface" "$USER_SETTINGS" 2>/dev/null || echo "")
611
- if [ -n "$PARKED_LINE" ]; then
612
- COUNT_N=$(echo "$PARKED_LINE" | grep -oE 'parked [0-9]+' | grep -oE '[0-9]+' | head -1)
613
- [ -n "$COUNT_N" ] && ORPHAN_COUNT=$((ORPHAN_COUNT + COUNT_N))
614
- fi
615
- fi
616
- done
617
- fi
618
-
619
- # === 3a. Copy bassclef hooks into repo (same pattern as skills) ===
620
- # Per goal 2026-08-29c #1417 Branch B (Option F full build) — swap ln -s → cp
621
- # so adopter owns the hook files. Symlinks break under operator mv/rename;
622
- # owned copies survive. Spike-proven per PR #1293 (6/6 assertions).
623
- #
624
- # Shared hooks come from bassclef. bassclef-sync.sh itself is always
625
- # app-specific (it IS the sync mechanism) — skipped.
626
- # In SELF_MODE (bassclef-on-bassclef, #293), skip entirely.
627
- # Migration cutover per ADR-031: existing symlinks land as copies on next sync.
628
- if [ "${SELF_MODE:-}" != "1" ]; then
629
- mkdir -p "$CWD/.claude/hooks" 2>/dev/null || true
630
- HOOK_INSTALL_COUNT=0
631
- for HOOK_FILE in "$BASSCLEF_DIR/.claude/hooks/"*.sh; do
632
- [ -f "$HOOK_FILE" ] || continue
633
- HOOK_NAME=$(basename "$HOOK_FILE")
634
- TARGET="$CWD/.claude/hooks/$HOOK_NAME"
635
- # Skip self — bassclef-sync.sh is app-specific
636
- [ "$HOOK_NAME" = "bassclef-sync.sh" ] && continue
637
- if [ -L "$TARGET" ]; then
638
- # Existing symlink from prior sync — migrate to owned copy
639
- rm "$TARGET"
640
- cp "$HOOK_FILE" "$TARGET"
641
- elif [ ! -f "$TARGET" ]; then
642
- # Doesn't exist — cp fresh
643
- cp "$HOOK_FILE" "$TARGET"
644
- else
645
- # Real file exists — overwrite with fresh cp (stale copy → bassclef source)
646
- cp -f "$HOOK_FILE" "$TARGET"
647
- fi
648
- # Preserve execute bit on hook file (per prototype pattern)
649
- chmod +x "$TARGET" 2>/dev/null || true
650
- HOOK_INSTALL_COUNT=$((HOOK_INSTALL_COUNT + 1))
651
- done
652
- [ "$HOOK_INSTALL_COUNT" -gt 0 ] && echo " bassclef-sync: installed/refreshed $HOOK_INSTALL_COUNT hook(s) via cp" >&2 || true
653
- fi
654
-
655
- # === 3a-postcheck. Verify every hook wired in settings.json has a resolvable target ===
656
- # Per bassclef-web#167 Cure Path 1 — the section 3a loop above enumerates
657
- # upstream files and symlinks them. It does NOT verify that every hook
658
- # referenced in project OR operator settings.json ended up with a resolvable
659
- # target on-disk. When the initial loop fails (stale $BASSCLEF_DIR, upstream
660
- # file added after sync ran, or SELF_MODE), no postcondition check catches it.
661
- # The failure surfaces as SessionStart file-not-found errors — worse when the
662
- # missing hook IS the reflection layer that would surface the drift as BLOCKED.
663
- #
664
- # This block iterates every hook command in project + operator settings.json
665
- # that references $CLAUDE_PROJECT_DIR/.claude/hooks/*.sh. For each name:
666
- # 1. If symlink or file exists at $CWD/.claude/hooks/<name> → OK (silent)
667
- # 2. Else if upstream $BASSCLEF_DIR/.claude/hooks/<name> exists → create symlink
668
- # + log INFO (self-heal path)
669
- # 3. Else → collect for structured BLOCKED emission at end
670
- #
671
- # Per @luminary saltzer-schroeder (complete mediation — every wired hook mediated)
672
- # + @luminary michael-nygard (fail-fast on drift — no silent gaps)
673
- # + @luminary hyrum-wright (settings.json wiring is observed adopter behavior).
674
- if [ "${SELF_MODE:-}" != "1" ] && command -v jq >/dev/null 2>&1; then
675
- BASSCLEF_SYNC_POSTCHECK_MISSING=()
676
- BASSCLEF_SYNC_POSTCHECK_CURED=()
677
-
678
- extract_wired_hook_names() {
679
- local settings="$1"
680
- [ ! -f "$settings" ] && return 0
681
- jq empty "$settings" >/dev/null 2>&1 || return 0
682
- # Walk any object with a .command field; extract hook basenames from
683
- # $CLAUDE_PROJECT_DIR/.claude/hooks/<name>.sh references (bash + direct).
684
- jq -r '..
685
- | objects
686
- | select(.command? != null)
687
- | .command' "$settings" 2>/dev/null \
688
- | grep -oE '\$CLAUDE_PROJECT_DIR/\.claude/hooks/[a-zA-Z0-9_.-]+\.sh' \
689
- | sed 's|.*/hooks/||' \
690
- | sort -u \
691
- || true
692
- }
693
-
694
- # Union hook names from project + operator settings.json
695
- BASSCLEF_SYNC_WIRED_NAMES=$(
696
- {
697
- extract_wired_hook_names "$CWD/.claude/settings.json"
698
- extract_wired_hook_names "$HOME/.claude/settings.json"
699
- } | sort -u
700
- )
701
-
702
- for HOOK_NAME in $BASSCLEF_SYNC_WIRED_NAMES; do
703
- [ -z "$HOOK_NAME" ] && continue
704
- TARGET="$CWD/.claude/hooks/$HOOK_NAME"
705
- # Present already — either symlink or real file — OK
706
- if [ -L "$TARGET" ] || [ -f "$TARGET" ]; then
707
- continue
708
- fi
709
- # Missing — try to cure via upstream symlink
710
- SOURCE="$BASSCLEF_DIR/.claude/hooks/$HOOK_NAME"
711
- if [ -f "$SOURCE" ]; then
712
- mkdir -p "$CWD/.claude/hooks" 2>/dev/null || true
713
- # Option F cp swap per #1417 Branch B — self-heal path uses cp not ln -s
714
- if cp "$SOURCE" "$TARGET" 2>/dev/null; then
715
- chmod +x "$TARGET" 2>/dev/null || true
716
- BASSCLEF_SYNC_POSTCHECK_CURED+=("$HOOK_NAME")
717
- else
718
- BASSCLEF_SYNC_POSTCHECK_MISSING+=("$HOOK_NAME (upstream present at $SOURCE; cp failed)")
719
- fi
720
- else
721
- BASSCLEF_SYNC_POSTCHECK_MISSING+=("$HOOK_NAME (upstream missing at $SOURCE)")
722
- fi
723
- done
724
-
725
- # Report cured (informational — self-heal succeeded via cp per #1417 Branch B)
726
- if [ "${#BASSCLEF_SYNC_POSTCHECK_CURED[@]}" -gt 0 ]; then
727
- echo " bassclef-sync: cured ${#BASSCLEF_SYNC_POSTCHECK_CURED[@]} missing hook file(s): ${BASSCLEF_SYNC_POSTCHECK_CURED[*]}"
728
- fi
729
-
730
- # Report unresolvable — structured BLOCKED with cure command inline
731
- if [ "${#BASSCLEF_SYNC_POSTCHECK_MISSING[@]}" -gt 0 ]; then
732
- {
733
- echo ""
734
- echo "🛑 BLOCKED: settings-drift — ${#BASSCLEF_SYNC_POSTCHECK_MISSING[@]} wired hook(s) missing target on-disk"
735
- for M in "${BASSCLEF_SYNC_POSTCHECK_MISSING[@]}"; do
736
- echo " - $M"
737
- done
738
- echo ""
739
- echo "Root cause class: settings.json wires a hook that has no matching file on-disk."
740
- echo "Resolve: verify upstream bassclef has the hook file, then re-run:"
741
- echo " bash ~/.claude/hooks/bassclef-sync.sh"
742
- echo "Per bassclef-web#167 Cure Path 1."
743
- echo ""
744
- } >&2
745
- fi
746
- fi
747
-
748
- # === 3a-fragments. Symlink hook subdirectory fragments ===
749
- # Per bet 2026-07-04c WU-2 — the section 3a loop above uses a shallow glob
750
- # (.claude/hooks/*.sh). It does not recurse into subdirectories like
751
- # session-reflection.d/. For adopters that copy files rather than symlink
752
- # (R2 tarball, npm install, curl script), the fragment never lands on
753
- # adopter disk without this block.
754
- #
755
- # Allow-list — only named subdirectories propagate. Unknown subdirs
756
- # (tests/, _lib/) do NOT propagate. Extend HOOK_FRAGMENT_DIRS as bassclef
757
- # adds new event-fragment directories. Per @luminary michael-nygard
758
- # (steady state via allow-list) + @luminary john-ousterhout (per-scan log
759
- # line — silent success is as dangerous as silent failure).
760
- if [ "${SELF_MODE:-}" != "1" ]; then
761
- HOOK_FRAGMENT_DIRS=(session-reflection.d)
762
- for FRAG_DIR in "${HOOK_FRAGMENT_DIRS[@]}"; do
763
- SRC_FRAG_DIR="$BASSCLEF_DIR/.claude/hooks/$FRAG_DIR"
764
- if [ ! -d "$SRC_FRAG_DIR" ]; then
765
- continue
766
- fi
767
- # Count fragments before scanning; empty subdir stays silent.
768
- FRAG_COUNT=$(find "$SRC_FRAG_DIR" -maxdepth 1 -name "*.sh" -type f 2>/dev/null | wc -l | tr -d ' ')
769
- if [ "$FRAG_COUNT" = "0" ]; then
770
- continue
771
- fi
772
- DST_FRAG_DIR="$CWD/.claude/hooks/$FRAG_DIR"
773
- mkdir -p "$DST_FRAG_DIR" 2>/dev/null || true
774
- echo "[bassclef-sync] recursing into .claude/hooks/$FRAG_DIR/ ($FRAG_COUNT fragments)" >&2
775
- for FRAG_FILE in "$SRC_FRAG_DIR"/*.sh; do
776
- [ -f "$FRAG_FILE" ] || continue
777
- FRAG_NAME=$(basename "$FRAG_FILE")
778
- TARGET="$DST_FRAG_DIR/$FRAG_NAME"
779
- # Option F cp swap per #1417 Branch B — adopter owns fragment file
780
- if [ -L "$TARGET" ]; then
781
- # Existing symlink from prior sync — migrate to owned copy
782
- rm "$TARGET"
783
- cp "$FRAG_FILE" "$TARGET"
784
- elif [ ! -f "$TARGET" ]; then
785
- # Doesn't exist — cp fresh
786
- cp "$FRAG_FILE" "$TARGET"
787
- else
788
- # Real file exists — overwrite with fresh cp (stale copy → bassclef)
789
- cp -f "$FRAG_FILE" "$TARGET"
790
- fi
791
- chmod +x "$TARGET" 2>/dev/null || true
792
- done
793
- done
794
- fi
795
-
796
- # === 3b. Install user-level hooks to ~/.claude/ (for cloud/CI) ===
797
- # This list MUST cover every hook that any version of ~/.claude/settings.json
798
- # might wire at $HOME paths (per #294). Older bassclef installs wired
799
- # session-end.sh; including it here keeps the wiring resolvable even
800
- # after orphan-parking widen runs.
801
- #
802
- # Lane 3 / #1146 / hook-scope-move (2026-08-06): the 3 SessionStart hooks
803
- # (vendored-dispatcher-refresh, session-reflection, session-start-recap-inject)
804
- # now live at user scope. They previously fired from $CLAUDE_PROJECT_DIR on
805
- # every session, but the project-scope files did not exist yet on a fresh
806
- # adopter's first session — bassclef-sync was what installed them, and it
807
- # fired AFTER the other 3 in the chain. Chicken-and-egg race per
808
- # docs/diagnoses/2026-08-06-1145-onboard-repo-chicken-and-egg.md.
809
- mkdir -p ~/.claude/hooks 2>/dev/null || true
810
- for HOOK in bug-diagnosis.sh tool-failure-diagnosis.sh trace-helper.sh session-end.sh deploy-guard.sh memory-write-audit.sh turn-prose-kiss-check.sh vendored-dispatcher-refresh.sh session-reflection.sh session-start-recap-inject.sh; do
811
- if [ -f "$BASSCLEF_DIR/.claude/hooks/$HOOK" ]; then
812
- cp "$BASSCLEF_DIR/.claude/hooks/$HOOK" ~/.claude/hooks/ 2>/dev/null || true
813
- chmod +x ~/.claude/hooks/"$HOOK" 2>/dev/null || true
814
- fi
815
- done
816
-
817
- # === 3b-cleanup. Lane 3 / hook-scope-move: remove legacy project-scope copies ===
818
- # Existing adopters have project-scope copies of the 3 SessionStart hooks
819
- # from prior bassclef installs. Post-Lane-3 the user-scope copies (installed
820
- # above) are the source of truth. Remove the project-scope files ONLY AFTER
821
- # the user-scope install succeeded — order matters per Linus adopter-compat.
822
- # Per docs/designs/2026-08-06-lane-3-canonical-wiring-race-cure.md § "Adopter
823
- # contract" — cure for R11 (existing adopters carry legacy project-scope
824
- # hooks that fire twice per session).
825
- #
826
- # Lane 3 extension (#1282, 2026-08-18): when we remove a project-scope hook
827
- # file, ALSO strip stale entries from adopter's .claude/settings.json that
828
- # still point at the removed path. R11 mitigation shipped file-only; the
829
- # wiring layer stayed broken (POA + siblings emit "No such file or directory"
830
- # every session because settings.json still references the removed files).
831
- # Full cure — files AND wiring migrate together per ADR-031.
832
- #
833
- # Pre-mortem full 8-lens ledger at docs/risk-ledgers/2026-08-18e-1282-lane-3-settings-cure.md;
834
- # top 12 risks folded into design (backup + atomic mv + jq empty validation
835
- # + preconditions + idempotent + fast-path + log line).
836
-
837
- amend_adopter_settings_for_removed_hook() {
838
- local adopter_cwd="$1"
839
- local hook_basename="$2"
840
- local settings_file="$adopter_cwd/.claude/settings.json"
841
-
842
- # H1: precondition — settings.json exists
843
- [ -f "$settings_file" ] || return 0
844
-
845
- # H2: precondition — valid JSON (else INFORM + no-op)
846
- if ! jq empty "$settings_file" 2>/dev/null; then
847
- echo " bassclef-sync: settings.json at $settings_file is invalid JSON; skipping Lane 3 amend for $hook_basename (INFORM)" >&2
848
- return 0
849
- fi
850
-
851
- # H3: precondition — has .hooks key
852
- jq -e '.hooks' "$settings_file" >/dev/null 2>&1 || return 0
853
-
854
- # F5: fast-path — count entries matching hook basename; skip if zero (no diff, no backup)
855
- local stale_count
856
- stale_count=$(jq --arg h "$hook_basename" \
857
- '[.hooks | .. | objects | select((.command? // "") | endswith($h))] | length' \
858
- "$settings_file" 2>/dev/null || echo 0)
859
- if [ "${stale_count:-0}" = "0" ]; then
860
- return 0
861
- fi
862
-
863
- # N1: backup with timestamped name (keep every backup; rotation deferred to follow-on)
864
- local ts backup tmp
865
- ts=$(date -u +%Y-%m-%dT%H-%M-%SZ)
866
- backup="${settings_file}.bak.${ts}"
867
- cp "$settings_file" "$backup" 2>/dev/null || return 0
868
-
869
- # N2 atomic write via tempfile in same directory (same inode namespace → mv is atomic)
870
- tmp="${settings_file}.tmp.$$"
871
-
872
- # Strip inner hooks[] entries whose .command endswith hook_basename;
873
- # also cull matcher-groups whose inner .hooks array became empty.
874
- if ! jq --arg h "$hook_basename" \
875
- '.hooks |= with_entries(
876
- .value |= (
877
- map(.hooks |= map(select(((.command // "") | endswith($h)) | not)))
878
- | map(select(.hooks | length > 0))
879
- )
880
- )' \
881
- "$settings_file" > "$tmp" 2>/dev/null; then
882
- rm -f "$tmp"
883
- echo " bassclef-sync: jq amend failed for $hook_basename; original preserved; backup at $backup" >&2
884
- return 0
885
- fi
886
-
887
- # L1 + H4: validate tmp is valid JSON before mv (guard against jq writing null)
888
- if ! jq empty "$tmp" 2>/dev/null; then
889
- rm -f "$tmp"
890
- echo " bassclef-sync: amended settings.json failed jq empty; original preserved; backup at $backup" >&2
891
- return 0
892
- fi
893
-
894
- # N2 atomic mv
895
- mv "$tmp" "$settings_file"
896
-
897
- # NR1 + NR2 + NR4: log removed entry + backup path
898
- echo " bassclef-sync: removed $hook_basename entry from adopter settings.json; backup at $backup (Lane 3 / #1282)"
899
- }
900
-
901
- # --------------------------------------------------------------
902
- # verify + heal SessionStart auto-sync wiring — bassclef-upstream#1496
903
- #
904
- # Postcondition-check pattern per Nygard (stability patterns) + Saltzer-Schroeder
905
- # (complete mediation). Ships with backup + jq validate discipline mirrored from
906
- # amend_adopter_settings_for_removed_hook above (Lane 3 L846 pattern). Traces
907
- # every pre-mortem risk R3+R5+R11+R16+R19 to preventative code per plan
908
- # docs/plans/2026-09-06-auto-sync-strip-loop-plan.md.
909
- #
910
- # Exit codes for verify_sessionstart_wiring:
911
- # 0 — wiring intact OR opt-out sentinel present
912
- # 1 — wiring missing / entry absent
913
- # 2 — malformed JSON (caller should skip, not heal)
914
- # --------------------------------------------------------------
915
-
916
- verify_sessionstart_wiring() {
917
- local settings_file="$1"
918
-
919
- # Absent settings.json: nothing to verify against; return 1 (heal will create)
920
- [ -f "$settings_file" ] || return 1
921
-
922
- # R8 preventative — opt-out sentinel in _comment: skip
923
- if grep -q 'BASSCLEF_AUTOSYNC_OFF=1' "$settings_file" 2>/dev/null; then
924
- return 0
925
- fi
926
-
927
- # H2 preventative — malformed JSON: return 2 (caller skips heal to avoid corrupt)
928
- if ! jq empty "$settings_file" 2>/dev/null; then
929
- return 2
930
- fi
931
-
932
- # Check that at least one SessionStart entry points at bassclef-sync.sh
933
- local hit
934
- hit=$(jq -r '[.hooks.SessionStart // [] | .[] | .hooks // [] | .[] | .command // ""] | map(select(endswith("bassclef-sync.sh"))) | length' "$settings_file" 2>/dev/null || echo 0)
935
- if [ "${hit:-0}" -ge 1 ]; then
936
- return 0
937
- fi
938
-
939
- return 1
940
- }
941
-
942
- heal_sessionstart_wiring() {
943
- local settings_file="$1"
944
- local target_cmd="\$HOME/.claude/hooks/bassclef-sync.sh"
945
-
946
- # H2 preventative — never touch malformed JSON: backup then skip
947
- if [ -f "$settings_file" ] && ! jq empty "$settings_file" 2>/dev/null; then
948
- local ts backup
949
- ts=$(date -u +%Y-%m-%dT%H-%M-%SZ)
950
- backup="${settings_file}.bak.${ts}"
951
- cp "$settings_file" "$backup" 2>/dev/null || true
952
- echo " bassclef-sync: settings.json at $settings_file is invalid JSON; skipping heal; backup at $backup (INFORM)" >&2
953
- return 0
954
- fi
955
-
956
- # R3 preventative — verify target path exists as executable before writing
957
- # (checked at $HOME expansion; adopter machine)
958
- local expanded_target="$HOME/.claude/hooks/bassclef-sync.sh"
959
- if [ ! -x "$expanded_target" ]; then
960
- echo " bassclef-sync: target hook missing at $expanded_target; cannot heal (INFORM)" >&2
961
- return 1
962
- fi
963
-
964
- # Create settings.json if absent — minimum valid shape
965
- if [ ! -f "$settings_file" ]; then
966
- mkdir -p "$(dirname "$settings_file")" 2>/dev/null || true
967
- echo '{"hooks":{}}' > "$settings_file" 2>/dev/null || return 1
968
- fi
969
-
970
- # R5 preventative — backup with timestamped name before write
971
- local ts backup tmp
972
- ts=$(date -u +%Y-%m-%dT%H-%M-%SZ)
973
- backup="${settings_file}.bak.${ts}"
974
- cp "$settings_file" "$backup" 2>/dev/null || return 1
975
-
976
- # R19 preventative — atomic write via tempfile + mv in same directory
977
- tmp="${settings_file}.tmp.$$"
978
-
979
- # Add bassclef-sync SessionStart entry — preserves other hooks
980
- if ! jq --arg cmd "$target_cmd" '
981
- .hooks //= {}
982
- | .hooks.SessionStart //= []
983
- | .hooks.SessionStart += [
984
- { "matcher": "", "hooks": [
985
- { "type": "command", "command": $cmd, "timeout": 30 }
986
- ] }
987
- ]
988
- ' "$settings_file" > "$tmp" 2>/dev/null; then
989
- rm -f "$tmp"
990
- echo " bassclef-sync: jq heal failed; original preserved; backup at $backup" >&2
991
- return 1
992
- fi
993
-
994
- # H4+L1 preventative — validate tmp is valid JSON before mv
995
- if ! jq empty "$tmp" 2>/dev/null; then
996
- rm -f "$tmp"
997
- echo " bassclef-sync: healed settings.json failed jq empty; original preserved; backup at $backup" >&2
998
- return 1
999
- fi
1000
-
1001
- mv "$tmp" "$settings_file"
1002
- echo " bassclef-sync: healed SessionStart wiring in $settings_file; backup at $backup (bassclef-upstream#1496)"
1003
- return 0
1004
- }
1005
-
1006
- # === LANE 3 LOOP BEGIN === (sentinel — used by test extractor per PR #1288 review A2)
1007
- LEGACY_HOOKS_REMOVED=0
1008
- for HOOK in vendored-dispatcher-refresh.sh session-reflection.sh session-start-recap-inject.sh; do
1009
- # Guard: only proceed when user-scope hook exists (proves Lane 3 migration succeeded — either now or earlier).
1010
- # Without this guard, we could amend adopter settings.json for hooks that have no user-scope home.
1011
- #
1012
- # Choice of `[ -f ]` over `[ -e ]` (per PR #1288 review A3):
1013
- # Section 3b installs user-scope hooks via `cp` (not symlink) — so broken symlinks in ~/.claude/hooks/
1014
- # are not a normal state we produce. `[ -f ]` returns true for regular files + symlinks that resolve
1015
- # to regular files, false for broken symlinks + missing files. If a prior 3b `cp` failed and left a
1016
- # broken symlink, we would silently skip amend on that hook. Trade-off accepted: false-negative on
1017
- # a broken symlink beats false-positive on a partial-install state.
1018
- if [ -f "$HOME/.claude/hooks/$HOOK" ]; then
1019
- # Remove project-scope file when still present (fresh Lane 3 case).
1020
- if [ -f "$CWD/.claude/hooks/$HOOK" ]; then
1021
- if rm "$CWD/.claude/hooks/$HOOK" 2>/dev/null; then
1022
- LEGACY_HOOKS_REMOVED=$((LEGACY_HOOKS_REMOVED + 1))
1023
- fi
1024
- fi
1025
- # Amend adopter settings.json unconditionally per hook (per #1282 follow-up).
1026
- # Handles both fresh-removal (project-scope just deleted) AND already-removed cases
1027
- # (project-scope deleted in prior sync; settings.json entry left dangling — POA state per smoke 2026-08-18e).
1028
- # Function is idempotent + fast-path when no matching entries — no diff, no backup.
1029
- # Bulkhead per Nygard N7: settings.json amend failure must not halt the loop.
1030
- { amend_adopter_settings_for_removed_hook "$CWD" "$HOOK"; } || true
1031
- fi
1032
- done
1033
- if [ "$LEGACY_HOOKS_REMOVED" -gt 0 ]; then
1034
- echo " bassclef-sync: removed $LEGACY_HOOKS_REMOVED legacy project-scope SessionStart hook(s); user-scope copies now source of truth (Lane 3 / #1146)"
1035
- fi
1036
- # === LANE 3 LOOP END === (sentinel — used by test extractor per PR #1288 review A2)
1037
-
1038
- # === 3b-skills. Symlink bassclef skills into ~/.claude/skills/ (dir-level) ===
1039
- # Per goal 2026-09-01 #1444 — revert PR #1428 + PR #1446 cp swap for skills only.
1040
- # The Skill tool registry double-registered cp'd skills at project + user-global
1041
- # scope, producing every skill twice in the type-ahead menu. Dir-level symlink
1042
- # collapses the two registrations back to one entry.
1043
- #
1044
- # Hooks + lib + agents + luminaries stay on cp per #1428 + #1446 rationale —
1045
- # adopters may edit those. Skills are read-mostly; adopters do not edit skill
1046
- # bodies. Sister ADR-031; sister risk ledger docs/risk-ledgers/2026-09-01c-1444-*
1047
- #
1048
- # Three branches encode migration state (Peirce lens per RFC-0002):
1049
- # Branch A — fresh install (target absent): create symlink
1050
- # Branch B — existing symlink: leave if right target, refresh if stale
1051
- # Branch C — cp'd dir with .bassclef-managed marker: rm-and-symlink to
1052
- # migrate adopters from #1428 era on next sync
1053
- # Skip — real dir without .bassclef-managed marker: adopter override,
1054
- # leave alone (R-L1 fold per Linus)
1055
- #
1056
- # Nygard N7 bulkhead per R-N1 — a single skill's symlink failure logs to
1057
- # stderr and the loop continues. No one skill halts the sync.
1058
- mkdir -p ~/.claude/skills 2>/dev/null || true
1059
- USER_SKILL_INSTALL_COUNT=0
1060
- USER_SKILL_MIGRATE_COUNT=0
1061
- USER_SKILL_REFRESH_COUNT=0
1062
- for SKILL_DIR in "$BASSCLEF_DIR/.claude/skills/"*/; do
1063
- SKILL_NAME=$(basename "$SKILL_DIR")
1064
- TARGET="$HOME/.claude/skills/$SKILL_NAME"
1065
- SKILL_SRC="${SKILL_DIR%/}" # strip trailing slash for readlink comparison
1066
- ACTION=""
1067
- if [ -L "$TARGET" ]; then
1068
- # Branch B — existing symlink
1069
- CURRENT_TARGET=$(readlink "$TARGET" 2>/dev/null || echo "")
1070
- if [ "$CURRENT_TARGET" = "$SKILL_SRC" ]; then
1071
- continue # already correct — no-op
1072
- fi
1073
- rm "$TARGET"
1074
- ACTION="refresh"
1075
- elif [ ! -e "$TARGET" ]; then
1076
- # Branch A — fresh install
1077
- ACTION="fresh"
1078
- elif [ -d "$TARGET" ] && [ -f "$TARGET/.bassclef-managed" ]; then
1079
- # Branch C — migrate cp'd dir from #1428 era back to symlink
1080
- rm -rf "$TARGET"
1081
- ACTION="migrate"
1082
- else
1083
- continue # adopter override — leave alone (R-L1)
1084
- fi
1085
- if ln -s "$SKILL_SRC" "$TARGET" 2>/dev/null; then
1086
- USER_SKILL_INSTALL_COUNT=$((USER_SKILL_INSTALL_COUNT + 1))
1087
- case "$ACTION" in
1088
- migrate) USER_SKILL_MIGRATE_COUNT=$((USER_SKILL_MIGRATE_COUNT + 1)) ;;
1089
- refresh) USER_SKILL_REFRESH_COUNT=$((USER_SKILL_REFRESH_COUNT + 1)) ;;
1090
- esac
1091
- else
1092
- echo " bassclef-sync: warn — symlink $ACTION failed for $SKILL_NAME; skill not installed at user scope" >&2
1093
- fi
1094
- done
1095
- if [ "$USER_SKILL_INSTALL_COUNT" -gt 0 ]; then
1096
- echo " bassclef-sync: installed $USER_SKILL_INSTALL_COUNT user-level skill(s) via symlink (migrated: $USER_SKILL_MIGRATE_COUNT, refreshed: $USER_SKILL_REFRESH_COUNT)" >&2
1097
- fi
1098
- # === 3b-skills-end. ===
1099
-
1100
- # === Install agents to ~/.claude/ ===
1101
- mkdir -p ~/.claude/agents 2>/dev/null || true
1102
- cp "$BASSCLEF_DIR/.claude/agents/"*.md ~/.claude/agents/ 2>/dev/null || true
1103
-
1104
- # === 3c. Copy luminaries into project (per-file cp inside real dir) ===
1105
- # Per goal 2026-08-29c #1417 Branch B (Option F full build) + goal 2026-08-31
1106
- # #1429 (remaining paths) — swap ln -s whole-dir → cp per file. Symlinks break
1107
- # under operator mv/rename of bassclef source; owned copies survive. Adopter-
1108
- # added luminary files inside the dir preserved (cp -f overwrites matching
1109
- # names only). See standards/pluggable-luminaries.md for file architecture.
1110
- #
1111
- # Migration cutover per ADR-031: existing whole-dir symlink → real dir with
1112
- # bassclef files cp'd in.
1113
- if [ "${SELF_MODE:-}" != "1" ] && [ -d "$BASSCLEF_DIR/.claude/luminaries" ]; then
1114
- TARGET="$CWD/.claude/luminaries"
1115
- # Migrate prior whole-dir symlink OR create fresh dir
1116
- if [ -L "$TARGET" ]; then
1117
- rm "$TARGET"
1118
- mkdir -p "$TARGET"
1119
- elif [ ! -e "$TARGET" ]; then
1120
- mkdir -p "$TARGET"
1121
- fi
1122
- # Refresh bassclef luminary files inside the dir (per-file cp; adopter files preserved)
1123
- if [ -d "$TARGET" ]; then
1124
- LUMINARIES_INSTALL_COUNT=0
1125
- for LUMINARY_FILE in "$BASSCLEF_DIR/.claude/luminaries/"*.md; do
1126
- [ -f "$LUMINARY_FILE" ] || continue
1127
- LUMINARY_NAME=$(basename "$LUMINARY_FILE")
1128
- cp -f "$LUMINARY_FILE" "$TARGET/$LUMINARY_NAME"
1129
- LUMINARIES_INSTALL_COUNT=$((LUMINARIES_INSTALL_COUNT + 1))
1130
- done
1131
- [ "$LUMINARIES_INSTALL_COUNT" -gt 0 ] && echo " bassclef-sync: installed/refreshed $LUMINARIES_INSTALL_COUNT luminary/luminaries via cp" >&2 || true
1132
- fi
1133
- fi
1134
-
1135
- # === 3d. Copy rules directory (per-file cp inside a real dir) ===
1136
- # Per goal 2026-08-29c #1417 Branch B — swap ln -s whole-dir → cp per file.
1137
- # Bassclef's rules are loaded via additionalDirectories in settings.json,
1138
- # but tools that grep .claude/rules/<name>.md directly (skill SKILL.md
1139
- # bodies, hook scripts, agent definitions) need them at the standard path.
1140
- #
1141
- # Migration cutover per ADR-031: existing whole-dir symlink → real dir with
1142
- # bassclef files cp'd in. Adopter-added rule files inside the dir preserved
1143
- # (cp -f overwrites matching names only).
1144
- if [ "${SELF_MODE:-}" != "1" ] && [ -d "$BASSCLEF_DIR/.claude/rules" ]; then
1145
- TARGET="$CWD/.claude/rules"
1146
- # Migrate prior whole-dir symlink OR create fresh dir
1147
- if [ -L "$TARGET" ]; then
1148
- rm "$TARGET"
1149
- mkdir -p "$TARGET"
1150
- elif [ ! -e "$TARGET" ]; then
1151
- mkdir -p "$TARGET"
1152
- fi
1153
- # Refresh bassclef rule files inside the dir (per-file cp; adopter files preserved)
1154
- if [ -d "$TARGET" ]; then
1155
- RULES_INSTALL_COUNT=0
1156
- for RULE_FILE in "$BASSCLEF_DIR/.claude/rules/"*.md; do
1157
- [ -f "$RULE_FILE" ] || continue
1158
- RULE_NAME=$(basename "$RULE_FILE")
1159
- cp -f "$RULE_FILE" "$TARGET/$RULE_NAME"
1160
- RULES_INSTALL_COUNT=$((RULES_INSTALL_COUNT + 1))
1161
- done
1162
- [ "$RULES_INSTALL_COUNT" -gt 0 ] && echo " bassclef-sync: installed/refreshed $RULES_INSTALL_COUNT rule(s) via cp" >&2 || true
1163
- fi
1164
- fi
1165
-
1166
- # === 3d-bis. Copy .claude/bassclef-orientation.md (bassclef-upstream#925 + #1429) ===
1167
- # CLAUDE.md carries `@.claude/bassclef-orientation.md` at line 2 (per ADR-032).
1168
- # Without this symlink, the @import resolves only if bassclef sibling loads via
1169
- # additionalDirectories. When the sibling path breaks (rename, delete, corp
1170
- # laptop with no sibling), the import silently no-ops. Substrate discipline
1171
- # vanishes from every session with zero visible signal.
1172
- #
1173
- # Cure: symlink the orientation file into the adopter's .claude/ so the @import
1174
- # resolves without depending on additionalDirectories. Follows the same pattern
1175
- # as 3c (luminaries), 3d (rules), 3e (agents) — but for a single file.
1176
- #
1177
- # Per @luminary linus-torvalds — we do not break adopters. The failure surface
1178
- # for this class is silent-and-critical; a symlink closes it structurally.
1179
- if [ "${SELF_MODE:-}" != "1" ] && [ -f "$BASSCLEF_DIR/.claude/bassclef-orientation.md" ]; then
1180
- mkdir -p "$CWD/.claude" 2>/dev/null
1181
- TARGET="$CWD/.claude/bassclef-orientation.md"
1182
- # Per goal 2026-08-31 #1429 — swap ln -s → cp so adopter owns the file.
1183
- # Symlinks break under bassclef source mv/rename; owned copies survive.
1184
- if [ -L "$TARGET" ]; then
1185
- rm "$TARGET"
1186
- cp -f "$BASSCLEF_DIR/.claude/bassclef-orientation.md" "$TARGET"
1187
- elif [ ! -e "$TARGET" ]; then
1188
- cp -f "$BASSCLEF_DIR/.claude/bassclef-orientation.md" "$TARGET"
1189
- elif [ -f "$TARGET" ]; then
1190
- # Refresh bassclef copy in place (rare — file evolves with each release)
1191
- cp -f "$BASSCLEF_DIR/.claude/bassclef-orientation.md" "$TARGET"
1192
- fi
1193
- # Else: a real dir or other non-file target — leave alone
1194
- fi
1195
-
1196
- # === 3e. Copy agents into project (per-file cp inside real dir) ===
1197
- # Per goal 2026-08-31 #1429 — swap ln -s → cp so adopter owns files.
1198
- # Project-level cp complements the ~/.claude/agents/ user-global cp at
1199
- # L970-971 (which serves the Agent-tool registry, similar to ~/.claude/skills/).
1200
- #
1201
- # Migration cutover per ADR-031: existing whole-dir symlink → real dir with
1202
- # bassclef agent files cp'd in. Adopter-added agent files preserved.
1203
- if [ "${SELF_MODE:-}" != "1" ] && [ -d "$BASSCLEF_DIR/.claude/agents" ]; then
1204
- TARGET="$CWD/.claude/agents"
1205
- # Migrate prior whole-dir symlink OR create fresh dir
1206
- if [ -L "$TARGET" ]; then
1207
- rm "$TARGET"
1208
- mkdir -p "$TARGET"
1209
- elif [ ! -e "$TARGET" ]; then
1210
- mkdir -p "$TARGET"
1211
- fi
1212
- # Refresh bassclef agent files inside the dir (per-file cp; adopter files preserved)
1213
- if [ -d "$TARGET" ]; then
1214
- AGENTS_INSTALL_COUNT=0
1215
- for AGENT_FILE in "$BASSCLEF_DIR/.claude/agents/"*.md; do
1216
- [ -f "$AGENT_FILE" ] || continue
1217
- AGENT_NAME=$(basename "$AGENT_FILE")
1218
- cp -f "$AGENT_FILE" "$TARGET/$AGENT_NAME"
1219
- AGENTS_INSTALL_COUNT=$((AGENTS_INSTALL_COUNT + 1))
1220
- done
1221
- [ "$AGENTS_INSTALL_COUNT" -gt 0 ] && echo " bassclef-sync: installed/refreshed $AGENTS_INSTALL_COUNT agent(s) via cp" >&2 || true
1222
- fi
1223
- fi
1224
-
1225
- # === 3f. Copy lib into consumer root (per-file cp inside real dir) ===
1226
- # Per goal 2026-08-31 #1429 — swap ln -s → cp so adopter owns files.
1227
- # lib/state.sh, lib/composer-preflight.sh, lib/release-version.sh, etc. are
1228
- # state-spine accessors and shared helpers. They live at bassclef's repo
1229
- # root (NOT under .claude/) so consumers own them at their root too. Skills
1230
- # (e.g. /preview-build pre-flight per bassclef#408, release-to-bassclef.sh
1231
- # per #1435 helper extract) source these by relative path.
1232
- #
1233
- # Migration cutover per ADR-031: existing whole-dir symlink → real dir with
1234
- # bassclef lib files cp'd in. Adopter-added lib files preserved.
1235
- if [ "${SELF_MODE:-}" != "1" ] && [ -d "$BASSCLEF_DIR/lib" ]; then
1236
- TARGET="$CWD/lib"
1237
- # Migrate prior whole-dir symlink OR create fresh dir
1238
- if [ -L "$TARGET" ]; then
1239
- rm "$TARGET"
1240
- mkdir -p "$TARGET"
1241
- elif [ ! -e "$TARGET" ]; then
1242
- mkdir -p "$TARGET"
1243
- fi
1244
- # Refresh bassclef lib files inside the dir (per-file cp; adopter files preserved)
1245
- # Note: recursive cp -R for subdirs (e.g. lib/adapters/, lib/tests/)
1246
- if [ -d "$TARGET" ]; then
1247
- LIB_INSTALL_COUNT=0
1248
- for LIB_ENTRY in "$BASSCLEF_DIR/lib/"*; do
1249
- [ -e "$LIB_ENTRY" ] || continue
1250
- LIB_NAME=$(basename "$LIB_ENTRY")
1251
- if [ -d "$LIB_ENTRY" ]; then
1252
- cp -Rf "$LIB_ENTRY" "$TARGET/"
1253
- else
1254
- cp -f "$LIB_ENTRY" "$TARGET/$LIB_NAME"
1255
- fi
1256
- LIB_INSTALL_COUNT=$((LIB_INSTALL_COUNT + 1))
1257
- done
1258
- [ "$LIB_INSTALL_COUNT" -gt 0 ] && echo " bassclef-sync: installed/refreshed $LIB_INSTALL_COUNT lib entr(y|ies) via cp" >&2 || true
1259
- fi
1260
- fi
1261
-
1262
- # === 3c-ignore. Maintain .gitignore block for bassclef symlinks ===
1263
- # Symlinks into ../bassclef are per-machine artifacts, not content. Enumerate
1264
- # them explicitly so real app-specific dirs (e.g. deploy-prod/) stay tracked.
1265
- # Managed block — rewritten each sync between sentinels.
1266
- #
1267
- # bassclef#589 (Phase IIIb WU-3): block ownership extends to the
1268
- # missing-file case. If consumer wholesale-deletes .gitignore (or never
1269
- # had one), bassclef creates it with the managed block on next sync.
1270
- # This covers the symmetric case to consumer-wholesale-rewrite (already
1271
- # handled — the BEGIN/END awk loop survives rewrites since bassclef
1272
- # re-appends the block).
1273
- GITIGNORE="$CWD/.gitignore"
1274
- BEGIN="# >>> bassclef-sync managed symlinks (do not edit) >>>"
1275
- END="# <<< bassclef-sync managed symlinks <<<"
1276
-
1277
- # Build IGNORE_LINES — same in both file-exists and file-missing branches
1278
- # Per bassclef-cli#37: prepend "/" to anchor every pattern to repo root.
1279
- # Bare "lib" (before fix) matched every folder depth per gitignore semantics —
1280
- # adopter files under src/lib/, harness/lib/, .claude/lib/ all silently ignored.
1281
- # Leading "/" anchors to repo root only. Multi-segment paths like ".claude/skills/foo"
1282
- # already anchor via embedded slash; the leading "/" is harmless there.
1283
- IGNORE_LINES=""
1284
- for T in "$CWD/.claude/skills/"* "$CWD/.claude/luminaries" "$CWD/.claude/rules" "$CWD/.claude/agents" "$CWD/.claude/hooks/"*.sh "$CWD/.claude/bassclef-orientation.md" "$CWD/lib"; do
1285
- if [ -L "$T" ]; then
1286
- REL="${T#$CWD/}"
1287
- IGNORE_LINES="${IGNORE_LINES}/${REL}"$'\n'
1288
- fi
1289
- done
1290
-
1291
- # Also ignore CLAUDE.md.bak.<ts> backup files that the dispatcher writes when
1292
- # curing content drift (per bassclef-upstream#889). The backup itself stays
1293
- # on disk for adopter inspection; only the tracking noise goes away.
1294
- IGNORE_LINES="${IGNORE_LINES}CLAUDE.md.bak.*"$'\n'
1295
-
1296
- # Only bother writing if there are actually bassclef symlinks to ignore
1297
- if [ -n "$IGNORE_LINES" ]; then
1298
- if [ -f "$GITIGNORE" ]; then
1299
- # Existing file: rewrite the managed block (preserves consumer content)
1300
- awk -v b="$BEGIN" -v e="$END" '
1301
- $0==b {skip=1; next}
1302
- $0==e {skip=0; next}
1303
- !skip {print}
1304
- ' "$GITIGNORE" > "$GITIGNORE.tmp"
1305
- {
1306
- cat "$GITIGNORE.tmp"
1307
- echo "$BEGIN"
1308
- printf '%s' "$IGNORE_LINES"
1309
- echo "$END"
1310
- } > "$GITIGNORE"
1311
- rm -f "$GITIGNORE.tmp"
1312
- else
1313
- # Missing file: create with just the managed block (bassclef#589)
1314
- {
1315
- echo "$BEGIN"
1316
- printf '%s' "$IGNORE_LINES"
1317
- echo "$END"
1318
- } > "$GITIGNORE"
1319
- fi
1320
- fi
1321
-
1322
- # === 3c-hooks. Auto-connect bassclef-managed hooks into consumer settings.json ===
1323
- # bassclef#569 (V1) + bassclef-upstream#625 (V2 dynamic-derive).
1324
- #
1325
- # V1 shipped a hardcoded 5-entry list under PreToolUse. Every other event type
1326
- # (PostToolUse, SessionStart, Stop, UserPromptSubmit, UserPromptExpansion) went
1327
- # dead-letter — scripts on disk, never fired. Bassclef itself connects 49 hooks
1328
- # across 6 event types; V1 coverage was 5/49 = 10%.
1329
- #
1330
- # V2 reads bassclef's own .claude/settings.json (the source of truth) via a
1331
- # sourceable lib and merges every entry into the consumer file. Adopters stop
1332
- # hitting BLOCKED on every hook add.
1333
- #
1334
- # Anchor luminaries — @luminary michael-nygard (Steady State — self-healing on
1335
- # every session-start), @luminary linus-torvalds (adopter contract — ADR-031
1336
- # commits bassclef to auto-migration), @luminary john-ousterhout (deep module
1337
- # — one clean interface hides the jq merge bookkeeping).
1338
- #
1339
- # Skip paths (all return 0 so bassclef-sync's other sections still run):
1340
- # - SKIP_BASSCLEF_HOOK_WIRE=1 — operator override (logged to stderr)
1341
- # - bassclef itself — detected via remote URL; bassclef manages its own file
1342
- # - jq missing — graceful skip with stderr notice
1343
- # - BASSCLEF_SOURCE_ROOT unset or bassclef settings.json missing/malformed
1344
- #
1345
- # Source discovery — BASSCLEF_SOURCE_ROOT is exported by the dispatcher
1346
- # (bassclef-sync.dispatcher.template.sh) before it execs this template. When
1347
- # the template is run directly (developer / test harness), BASSCLEF_SOURCE_ROOT
1348
- # stays unset and the section skips cleanly.
1349
- HOOK_CONNECT_LIB="$(dirname "$0")/bassclef-hook-connect.sh"
1350
- if [ -f "$HOOK_CONNECT_LIB" ]; then
1351
- # shellcheck disable=SC1090
1352
- source "$HOOK_CONNECT_LIB"
1353
- bassclef_hook_connect_apply "$CWD" || true
1354
- fi
1355
-
1356
- # === 3g-workflows. Auto-wire bassclef-managed GitHub Actions workflows ===
1357
- # bassclef#611 — clarity-check propagation. Writes a tiny consumer-side
1358
- # workflow that calls bassclef's reusable clarity-check workflow.
1359
- #
1360
- # Vendor binding tier: GitHub-Actions-Locked (per bassclef vendor-bindings standard; see operator substrate for full spec).
1361
- # ADO/GitLab/Bitbucket migration tracked at bassclef#629.
1362
- #
1363
- # Skip paths:
1364
- # - SKIP_BASSCLEF_WORKFLOWS=1 — operator-controlled override
1365
- # - bassclef itself (IS_BASSCLEF_ITSELF=1; bassclef manages its own .github/workflows/ directly)
1366
- # - Consumer not on GitHub (detected via remote URL — no github.com → skip silently)
1367
- #
1368
- # Idempotency: file is bassclef-managed (header marker). If file exists
1369
- # without the marker, treat as operator-customized + leave alone. If file
1370
- # exists WITH the marker, refresh content (operator can detect bassclef
1371
- # updates via git diff).
1372
- if [ "${SKIP_BASSCLEF_WORKFLOWS:-0}" != "1" ] && \
1373
- [ "$IS_BASSCLEF_ITSELF" = "0" ]; then
1374
-
1375
- # Only proceed on GitHub-hosted consumers (extracted from remote URL)
1376
- case "$CONSUMER_REMOTE_URL" in
1377
- *github.com*|*github*) IS_GITHUB_CONSUMER=1 ;;
1378
- *) IS_GITHUB_CONSUMER=0 ;;
1379
- esac
1380
-
1381
- if [ "$IS_GITHUB_CONSUMER" = "1" ]; then
1382
- WORKFLOWS_DIR="$CWD/.github/workflows"
1383
- CLARITY_WORKFLOW="$WORKFLOWS_DIR/clarity-check.yml"
1384
- BASSCLEF_MANAGED_MARKER="# bassclef-managed: clarity-check (bassclef#611)"
1385
-
1386
- mkdir -p "$WORKFLOWS_DIR" 2>/dev/null || true
1387
-
1388
- # If file exists without our marker, leave it alone (operator-customized)
1389
- WRITE_WORKFLOW=1
1390
- if [ -f "$CLARITY_WORKFLOW" ] && ! grep -q "bassclef-managed: clarity-check" "$CLARITY_WORKFLOW" 2>/dev/null; then
1391
- WRITE_WORKFLOW=0
1392
- fi
1393
-
1394
- if [ "$WRITE_WORKFLOW" = "1" ]; then
1395
- cat > "$CLARITY_WORKFLOW" <<'CLARITY_EOF'
1396
- # bassclef-managed: clarity-check (bassclef#611)
1397
- # Distributed via bassclef-sync. To customize, remove the bassclef-managed
1398
- # marker above + bassclef-sync will leave this file alone on next run.
1399
-
1400
- name: clarity-check
1401
-
1402
- on:
1403
- pull_request:
1404
- paths-ignore:
1405
- - 'docs/chronicle/**'
1406
- - 'chronicle/**'
1407
- - 'docs/operator-private/journals/**'
1408
- - 'docs/release-notes/**'
1409
-
1410
- jobs:
1411
- clarity:
1412
- name: PR body clarity (advisory)
1413
- uses: sunj-labs/bassclef/.github/workflows/clarity-check.reusable.yml@main
1414
- permissions:
1415
- contents: read
1416
- pull-requests: write
1417
- CLARITY_EOF
1418
- fi
1419
- fi
1420
- fi
1421
-
1422
- # === 3d. Ensure state/markers/ directory exists (per-repo, not shared) ===
1423
- # /verify, /temperance, /diagnose, etc. write markers here. Per-repo because
1424
- # each repo has its own branches + gate state. Per bassclef#342: markers
1425
- # are runtime workflow events, live under state/ (runtime drawer), not
1426
- # .claude/ (rulebook drawer). See standards/mobile-ephemeral-session.md.
1427
- mkdir -p "$CWD/state/markers/verify" 2>/dev/null || true
1428
- mkdir -p "$CWD/state/markers/temperance" 2>/dev/null || true
1429
- mkdir -p "$CWD/state/markers/diagnose" 2>/dev/null || true
1430
- [ ! -f "$CWD/state/markers/.gitkeep" ] && \
1431
- echo "# gate markers — per-repo runtime workflow events (bassclef#342)" > \
1432
- "$CWD/state/markers/.gitkeep"
1433
-
1434
- # === 4. Write ~/.claude/settings.json for user-level hook wiring (cloud) ===
1435
- # Ephemeral-gated (#162): desktop operators own their ~/.claude/settings.json
1436
- # and a repo-local hook should not rewrite it. Skill/agent/hook symlinks
1437
- # above are tool-resolution infrastructure (needed on every platform) and
1438
- # remain unconditional. Only the personal-config settings.json write is
1439
- # gated.
1440
- if [ "$IS_EPHEMERAL" = "1" ] && \
1441
- { [ ! -f ~/.claude/settings.json ] || ! grep -q 'bug-diagnosis' ~/.claude/settings.json 2>/dev/null; }; then
1442
- cat > ~/.claude/settings.json <<'SETTINGS'
1443
- {
1444
- "hooks": {
1445
- "UserPromptSubmit": [
1446
- {
1447
- "matcher": "",
1448
- "hooks": [
1449
- {
1450
- "type": "command",
1451
- "command": "$HOME/.claude/hooks/bug-diagnosis.sh",
1452
- "timeout": 5
1453
- }
1454
- ]
1455
- }
1456
- ],
1457
- "PostToolUse": [
1458
- {
1459
- "matcher": "Bash",
1460
- "hooks": [
1461
- {
1462
- "type": "command",
1463
- "command": "$HOME/.claude/hooks/tool-failure-diagnosis.sh",
1464
- "timeout": 5
1465
- }
1466
- ]
1467
- }
1468
- ]
1469
- }
1470
- }
1471
- SETTINGS
1472
- fi
1473
-
1474
- # === 5. Set env var for rules/CLAUDE.md inheritance ===
1475
- if [ -n "$CLAUDE_ENV_FILE" ]; then
1476
- echo "CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1" >> "$CLAUDE_ENV_FILE"
1477
- fi
1478
-
1479
- # === 5b. Bootstrap bassclef-evolution labels (#160) ===
1480
- # /promote creates issues with these labels; consumer repos need them created
1481
- # once so the issues are scannable via gh issue list --label bassclef-evolution.
1482
- # Idempotent: --force updates metadata if label exists, creates if not.
1483
- # Skip silently if gh missing or not authenticated (ephemeral mobile).
1484
- if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
1485
- CONSUMER_OWNER_REPO=$(git -C "$CWD" remote get-url origin 2>/dev/null | \
1486
- sed -E 's#(git@github.com:|https://github.com/)##; s#\.git$##')
1487
- if [ -n "$CONSUMER_OWNER_REPO" ]; then
1488
- gh label create bassclef-evolution --repo "$CONSUMER_OWNER_REPO" --force \
1489
- --color "0E8A16" --description "Promote this pattern to bassclef substrate" \
1490
- >/dev/null 2>&1 || true
1491
- gh label create agent-user-proposed --repo "$CONSUMER_OWNER_REPO" --force \
1492
- --color "D876E3" --description "Pattern surfaced by agent-user collaboration" \
1493
- >/dev/null 2>&1 || true
1494
- fi
1495
- fi
1496
-
1497
- # === 6. Whereami freshness check ===
1498
- # docs/whereami.md is the authoritative project-state snapshot
1499
- # (standards/whereami-schema.md). Session-start reads it; session-end
1500
- # updates it. Staleness fires BLOCKED per .claude/rules/whereami-load-bearing.md.
1501
- #
1502
- # Two-signal staleness (v2, bassclef#514): age threshold AND commits-since-
1503
- # last-whereami-update. Idle consumers (zero commits since whereami was last
1504
- # committed) get INFO instead of BLOCKED at the >=7d tier — calendar staleness
1505
- # alone is not a gate when nothing changed. Consumer override:
1506
- # substrate.config.md field `whereami_freshness_days: N`.
1507
- #
1508
- # Thresholds:
1509
- # <3d → green (no banner)
1510
- # 3-7d (any commits) → stale-warn (visible warning)
1511
- # >=7d AND commits_since == 0 → stale-info (idle; non-blocking)
1512
- # >=7d AND commits_since > 0 → stale-block (BLOCKED)
1513
- # missing → BLOCKED
1514
- WHEREAMI="$CWD/docs/whereami.md"
1515
- WHEREAMI_STATUS=""
1516
- WHEREAMI_AGE_DAYS=""
1517
- WHEREAMI_COMMITS_SINCE=""
1518
- if [ ! -f "$WHEREAMI" ]; then
1519
- WHEREAMI_STATUS="missing"
1520
- else
1521
- # Extract ISO timestamp via helper (#897 Cure A). Helper reads either
1522
- # the "## Last updated" markdown section OR YAML frontmatter
1523
- # `last_updated.at` fallback, and normalizes offset forms (+HHMM,
1524
- # -HHMM, +HH:MM) to UTC Z form so downstream date -j -f "%Y-%m-%dT%H:%M:%SZ"
1525
- # reads one canonical shape. Returns empty on missing / malformed.
1526
- WHEREAMI_TS=$(bassclef_sync_parse_whereami_ts "$WHEREAMI")
1527
-
1528
- # Commits since whereami was last committed (idle detection).
1529
- # On non-git repos, a missing whereami commit, or git unavailable: treat as
1530
- # "active" (commits_since=1) so existing block behavior is preserved — idle
1531
- # detection requires affirmative evidence of zero commits, not absence of
1532
- # signal. Override SKIP_WHEREAMI_COMMIT_DELTA=1 forces commits_since=0
1533
- # (treat as idle) for migration / testing.
1534
- if [ "${SKIP_WHEREAMI_COMMIT_DELTA:-}" = "1" ]; then
1535
- WHEREAMI_COMMITS_SINCE=0
1536
- elif command -v git >/dev/null 2>&1 && git -C "$CWD" rev-parse --git-dir >/dev/null 2>&1; then
1537
- LAST_WHEREAMI_COMMIT=$(git -C "$CWD" log -1 --format=%H -- docs/whereami.md 2>/dev/null)
1538
- if [ -n "$LAST_WHEREAMI_COMMIT" ]; then
1539
- WHEREAMI_COMMITS_SINCE=$(git -C "$CWD" rev-list --count "$LAST_WHEREAMI_COMMIT"..HEAD 2>/dev/null || echo "1")
1540
- else
1541
- # Whereami file is uncommitted (fresh add) — treat as active so the file
1542
- # gets committed before BLOCKED downgrades to INFO. Caller can also use
1543
- # the override above for migration.
1544
- WHEREAMI_COMMITS_SINCE=1
1545
- fi
1546
- else
1547
- WHEREAMI_COMMITS_SINCE=1
1548
- fi
1549
-
1550
- if [ -n "$WHEREAMI_TS" ]; then
1551
- # Portable date math: compute age in days (Darwin + GNU)
1552
- if date -j -f "%Y-%m-%dT%H:%M:%SZ" "$WHEREAMI_TS" "+%s" >/dev/null 2>&1; then
1553
- WHEREAMI_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$WHEREAMI_TS" "+%s")
1554
- else
1555
- WHEREAMI_EPOCH=$(date -d "$WHEREAMI_TS" "+%s" 2>/dev/null || echo "0")
1556
- fi
1557
- NOW_EPOCH=$(date "+%s")
1558
- if [ "$WHEREAMI_EPOCH" != "0" ] && [ "$NOW_EPOCH" -gt "$WHEREAMI_EPOCH" ]; then
1559
- WHEREAMI_AGE_SECS=$((NOW_EPOCH - WHEREAMI_EPOCH))
1560
- WHEREAMI_AGE_DAYS=$((WHEREAMI_AGE_SECS / 86400))
1561
- if [ "$WHEREAMI_AGE_DAYS" -ge 7 ]; then
1562
- if [ "$WHEREAMI_COMMITS_SINCE" -gt 0 ]; then
1563
- WHEREAMI_STATUS="stale-block"
1564
- else
1565
- WHEREAMI_STATUS="stale-info"
1566
- fi
1567
- elif [ "$WHEREAMI_AGE_DAYS" -ge 3 ]; then
1568
- WHEREAMI_STATUS="stale-warn"
1569
- fi
1570
- else
1571
- WHEREAMI_STATUS="unparseable"
1572
- fi
1573
- else
1574
- WHEREAMI_STATUS="unparseable"
1575
- fi
1576
- fi
1577
-
1578
- # === Summary ===
1579
- SKILL_COUNT=$(ls "$BASSCLEF_DIR/.claude/skills/" 2>/dev/null | wc -l | tr -d ' ')
1580
- RULE_COUNT=$(ls "$BASSCLEF_DIR/.claude/rules/" 2>/dev/null | wc -l | tr -d ' ')
1581
- AGENT_COUNT=$(ls ~/.claude/agents/ 2>/dev/null | wc -l | tr -d ' ')
1582
- HOOK_COUNT=$(ls "$CWD/.claude/hooks/"*.sh 2>/dev/null | wc -l | tr -d ' ')
1583
-
1584
- # === Bassclef version readout (per WU-2 of bet 2026-06-22c; closes #3) ===
1585
- # Read bassclef-version.json from the synced bassclef. Surfaces "what version
1586
- # do I have + when was it released" in the SessionStart confirm banner.
1587
- # Best-effort releases-behind count if bassclef is a git checkout (Path 1).
1588
- BASSCLEF_VERSION=""
1589
- BASSCLEF_RELEASE_DATE_SHORT=""
1590
- BASSCLEF_RELEASES_BEHIND=""
1591
- VERSION_FILE="$BASSCLEF_DIR/bassclef-version.json"
1592
- if [ -f "$VERSION_FILE" ]; then
1593
- BASSCLEF_VERSION=$(jq -r '.version // empty' "$VERSION_FILE" 2>/dev/null)
1594
- RELEASE_DATE_FULL=$(jq -r '.release_date // empty' "$VERSION_FILE" 2>/dev/null)
1595
- # Trim to YYYY-MM-DD
1596
- BASSCLEF_RELEASE_DATE_SHORT="${RELEASE_DATE_FULL%%T*}"
1597
- # Best-effort: count release-* tags created after this version (git checkout only)
1598
- if [ -d "$BASSCLEF_DIR/.git" ] && [ -n "$BASSCLEF_VERSION" ]; then
1599
- BEHIND=$(git -C "$BASSCLEF_DIR" tag --list "release-*" --sort=creatordate 2>/dev/null \
1600
- | awk -v cur="$BASSCLEF_VERSION" 'found {count++} $0==cur {found=1} END {print count+0}')
1601
- if [ "${BEHIND:-0}" -gt 0 ] 2>/dev/null; then
1602
- BASSCLEF_RELEASES_BEHIND=" · $BEHIND releases behind"
1603
- fi
1604
- fi
1605
- fi
1606
-
1607
- # === URL age computation (#224) ===
1608
- # When fetched from S3, compute presigned URL age + remaining TTL so the
1609
- # session-start banner can distinguish "fresh / benign" from "stale / near
1610
- # refresh boundary". Expired tier is unreachable here (curl would have 403'd
1611
- # and the hook exited at line ~121 before we got this far), but the WARN
1612
- # tier gives the operator a heads-up on the next-refresh expectation.
1613
- # Portable date math mirrors the whereami block below.
1614
- URL_TIER=""
1615
- URL_AGE_HOURS=""
1616
- URL_REMAINING_HOURS=""
1617
- if [ "$SYNC_SOURCE" = "s3" ] && [ -n "$URL_AGE_GENERATED_AT" ] && [ "$URL_AGE_GENERATED_AT" != "null" ]; then
1618
- if date -j -f "%Y-%m-%dT%H:%M:%SZ" "$URL_AGE_GENERATED_AT" "+%s" >/dev/null 2>&1; then
1619
- URL_GEN_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$URL_AGE_GENERATED_AT" "+%s")
1620
- else
1621
- URL_GEN_EPOCH=$(date -d "$URL_AGE_GENERATED_AT" "+%s" 2>/dev/null || echo "0")
1622
- fi
1623
- NOW_EPOCH_URL=$(date "+%s")
1624
- if [ "$URL_GEN_EPOCH" != "0" ] && [ "$NOW_EPOCH_URL" -ge "$URL_GEN_EPOCH" ]; then
1625
- URL_AGE_SECS=$((NOW_EPOCH_URL - URL_GEN_EPOCH))
1626
- URL_AGE_HOURS=$((URL_AGE_SECS / 3600))
1627
- URL_REMAINING_SECS=$((86400 - URL_AGE_SECS))
1628
- if [ "$URL_REMAINING_SECS" -lt 0 ]; then
1629
- URL_REMAINING_HOURS=0
1630
- URL_TIER="expired"
1631
- elif [ "$URL_AGE_SECS" -ge 64800 ]; then # 18h threshold
1632
- URL_REMAINING_HOURS=$((URL_REMAINING_SECS / 3600))
1633
- URL_TIER="warn"
1634
- else
1635
- URL_REMAINING_HOURS=$((URL_REMAINING_SECS / 3600))
1636
- URL_TIER="ok"
1637
- fi
1638
- fi
1639
- fi
1640
-
1641
- # === Hook drift check ===
1642
- # Compare this hook's version header against the bassclef template.
1643
- # If consumer hook is out of date, surface a warning (non-blocking).
1644
- #
1645
- # Thin-pointer architecture: consumer hooks with version `thin-pointer-*`
1646
- # are intentionally a different file from this template (different role,
1647
- # different version namespace). They opt out of drift comparison entirely
1648
- # because their job is to delegate to bassclef's current template on
1649
- # every fire — not mirror its version string. Per bassclef#418.
1650
- HOOK_SELF="$CWD/.claude/hooks/bassclef-sync.sh"
1651
- HOOK_BASSCLEF="$BASSCLEF_DIR/presence/install/bassclef-sync.template.sh"
1652
- HOOK_DRIFT=""
1653
- HOOK_ARCH="vendored-snapshot" # default architecture
1654
- if [ -f "$HOOK_SELF" ] && [ -f "$HOOK_BASSCLEF" ]; then
1655
- SELF_VER=$(grep -m1 '^# BASSCLEF_SYNC_VERSION=' "$HOOK_SELF" 2>/dev/null | sed 's/^# BASSCLEF_SYNC_VERSION=//' | tr -d '[:space:]')
1656
- BASSCLEF_VER=$(grep -m1 '^# BASSCLEF_SYNC_VERSION=' "$HOOK_BASSCLEF" 2>/dev/null | sed 's/^# BASSCLEF_SYNC_VERSION=//' | tr -d '[:space:]')
1657
- case "$SELF_VER" in
1658
- thin-pointer-*)
1659
- # Thin-pointer architecture: skip drift comparison.
1660
- # Consumer hook is a dispatcher, not a snapshot.
1661
- HOOK_ARCH="thin-pointer"
1662
- ;;
1663
- *)
1664
- if [ -n "$SELF_VER" ] && [ -n "$BASSCLEF_VER" ] && [ "$SELF_VER" != "$BASSCLEF_VER" ]; then
1665
- HOOK_DRIFT="yes"
1666
- fi
1667
- ;;
1668
- esac
1669
- fi
1670
-
1671
- echo ""
1672
- echo "### BASSCLEF SUBSTRATE — SYNCED"
1673
- echo ""
1674
- echo "Status: $SYNC_STATUS"
1675
- echo "Source: $SYNC_SOURCE"
1676
- echo "Architecture: $HOOK_ARCH"
1677
- if [ "$SYNC_SOURCE" = "s3" ] && [ -n "$URL_AGE_GENERATED_AT" ] && [ "$URL_AGE_GENERATED_AT" != "null" ]; then
1678
- case "$URL_TIER" in
1679
- ok)
1680
- echo "Presigned URL: generated ${URL_AGE_HOURS}h ago, expires in ${URL_REMAINING_HOURS}h ✓"
1681
- ;;
1682
- warn)
1683
- echo "⚠️ Presigned URL: generated ${URL_AGE_HOURS}h ago, expires in ${URL_REMAINING_HOURS}h"
1684
- echo " Next bassclef merge or daily 06:00 UTC cron will refresh."
1685
- ;;
1686
- expired)
1687
- EXPIRED_HOURS=$((URL_AGE_HOURS - 24))
1688
- echo "🛑 Presigned URL expired ${EXPIRED_HOURS}h ago (generated ${URL_AGE_HOURS}h ago)."
1689
- echo " Fix: end session + restart (fresh clone), or workflow_dispatch bassclef-s3-publish."
1690
- ;;
1691
- *)
1692
- # Fallback when date math couldn't parse generated_at — preserves prior line format.
1693
- echo "Tarball URL generated: $URL_AGE_GENERATED_AT (expires: $URL_EXPIRES_AT)"
1694
- ;;
1695
- esac
1696
- fi
1697
- echo "Skills: ${SKILL_COUNT} (symlinked to ~/.claude/skills/ for Skill-tool registry + .claude/skills/ for Read/Grep)"
1698
- echo "Rules: ${RULE_COUNT} (loaded via additionalDirectories + env var)"
1699
- echo "Agents: ${AGENT_COUNT} (installed to ~/.claude/agents/)"
1700
- echo "Hooks: ${HOOK_COUNT} (symlinked from bassclef + app-specific)"
1701
- echo ""
1702
-
1703
- if [ "${ORPHAN_COUNT:-0}" -gt 0 ]; then
1704
- echo "### USER-LEVEL SUBSTRATE CLEANUP (per ADR-033 + bassclef#278)"
1705
- echo ""
1706
- echo "Parked $ORPHAN_COUNT user-level entries (skills + agents + hooks; symlinks AND cp'd files)"
1707
- echo "whose basenames duplicated project-level entries. Backup dirs:"
1708
- echo " ~/.claude/skills.orphans.$(date +%Y%m%d)/ (if skills parked)"
1709
- echo " ~/.claude/agents.orphans.$(date +%Y%m%d)/ (if agents parked)"
1710
- echo " ~/.claude/hooks.orphans.$(date +%Y%m%d)/ (if hooks parked)"
1711
- echo ""
1712
- echo "Project-level entries now load without dedupe conflict. Backups"
1713
- echo "survive 30 days minimum; restore if a script you wrote depends"
1714
- echo "on the user-level location."
1715
- echo ""
1716
- fi
1717
-
1718
- # === Whereami status block ===
1719
- if [ "$WHEREAMI_STATUS" = "missing" ]; then
1720
- echo "🛑🛑🛑 BLOCKED 🛑🛑🛑"
1721
- echo "────────────────────────────────────────────"
1722
- echo "whereami-missing — docs/whereami.md not found; project-state snapshot required."
1723
- echo ""
1724
- echo " Create from template:"
1725
- echo " cp $BASSCLEF_DIR/strategy/templates/whereami-template.md docs/whereami.md"
1726
- echo " # then fill phase, iteration, subsystems, last_updated"
1727
- echo ""
1728
- echo " Or defer explicitly:"
1729
- echo " tell the agent: 'skip whereami this session'"
1730
- echo "────────────────────────────────────────────"
1731
- echo ""
1732
- elif [ "$WHEREAMI_STATUS" = "stale-block" ]; then
1733
- echo "🛑🛑🛑 BLOCKED 🛑🛑🛑"
1734
- echo "────────────────────────────────────────────"
1735
- echo "whereami-stale — docs/whereami.md last updated ${WHEREAMI_AGE_DAYS} days ago (threshold: 7); ${WHEREAMI_COMMITS_SINCE} commits since the last whereami update."
1736
- echo ""
1737
- echo " Resolve by running session-end-style whereami update as first action,"
1738
- echo " OR defer: tell the agent 'skip whereami this session'."
1739
- echo "────────────────────────────────────────────"
1740
- echo ""
1741
- elif [ "$WHEREAMI_STATUS" = "stale-info" ]; then
1742
- echo "ℹ️ whereami ${WHEREAMI_AGE_DAYS} days old, but no commits have touched the repo since the last whereami update — repo is idle, no refresh needed."
1743
- echo ""
1744
- elif [ "$WHEREAMI_STATUS" = "stale-warn" ]; then
1745
- echo "⚠️ whereami ${WHEREAMI_AGE_DAYS} days old (warn threshold: 3) — refresh at session-end."
1746
- echo ""
1747
- elif [ "$WHEREAMI_STATUS" = "unparseable" ]; then
1748
- echo "⚠️ whereami exists but last_updated field unparseable — schema non-conformance."
1749
- echo " See standards/whereami-schema.md §Last updated."
1750
- echo ""
1751
- fi
1752
-
1753
- # === Onboarding-artifact health check ===
1754
- # If the consumer has bassclef substrate installed (.bassclef-url exists),
1755
- # verify the other required onboarding artifacts are present. Whereami is
1756
- # already handled by the block above; check the remaining two here.
1757
- # Catches consumers onboarded before /onboard-repo was codified (observed
1758
- # 2026-04-18: TwoDo, pruneguice, ops missing whereami; TwoDo also missing
1759
- # CLAUDE.md). Prevents mid-skill blocks by surfacing the gap at session-start.
1760
- if [ -f "$CWD/.bassclef-url" ]; then
1761
- ONBOARD_MISSING=""
1762
- [ -f "$CWD/CLAUDE.md" ] || ONBOARD_MISSING="${ONBOARD_MISSING}\n - CLAUDE.md"
1763
- [ -f "$CWD/substrate.config.md" ] || ONBOARD_MISSING="${ONBOARD_MISSING}\n - substrate.config.md"
1764
-
1765
- if [ -n "$ONBOARD_MISSING" ]; then
1766
- echo "🛑🛑🛑 BLOCKED 🛑🛑🛑"
1767
- echo "────────────────────────────────────────────"
1768
- echo "onboarding-artifacts-missing — bassclef substrate installed but required files absent:"
1769
- echo -e "$ONBOARD_MISSING"
1770
- echo ""
1771
- echo " → Resolve by re-running /onboard-repo (idempotent — only scaffolds missing):"
1772
- echo " In a Claude Code session in this repo, invoke /onboard-repo"
1773
- echo ""
1774
- echo " Or defer explicitly:"
1775
- echo " tell the agent: 'skip onboarding check this session'"
1776
- echo "────────────────────────────────────────────"
1777
- echo ""
1778
- fi
1779
- fi
1780
-
1781
- if [ -n "$HOOK_DRIFT" ]; then
1782
- # Resolve consumer repo owner/name from git remote (for PR link + remote check)
1783
- CONSUMER_REMOTE=$(git -C "$CWD" remote get-url origin 2>/dev/null | sed -E 's#(git@github.com:|https://github.com/)##; s#\.git$##' || echo "your-org/your-repo")
1784
-
1785
- # === #173: Remote-main version check (preempts BLOCKED on stale-local) ===
1786
- # Before declaring drift, check if remote main already has the bassclef
1787
- # template version. If yes → local is just behind main (e.g., haven't pulled
1788
- # since the sync PR merged); recommend git pull, do NOT block. This generalizes
1789
- # the merged-PR detection below to any cause of local-behind-main divergence.
1790
- # Graceful fallback: if gh unavailable / unauthenticated / network down /
1791
- # parse fails, leave REMOTE_MAIN_VER empty and fall through to existing
1792
- # BLOCKED behavior — never worse than current.
1793
- REMOTE_MAIN_VER=""
1794
- if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
1795
- REMOTE_MAIN_VER=$(gh api "repos/$CONSUMER_REMOTE/contents/.claude/hooks/bassclef-sync.sh?ref=main" \
1796
- --jq '.content' 2>/dev/null | base64 --decode 2>/dev/null | \
1797
- grep -m1 '^# BASSCLEF_SYNC_VERSION=' | sed 's/^# BASSCLEF_SYNC_VERSION=//' | tr -d '[:space:]')
1798
- fi
1799
-
1800
- if [ -n "$REMOTE_MAIN_VER" ] && [ "$REMOTE_MAIN_VER" = "$BASSCLEF_VER" ]; then
1801
- # Local is behind, but remote main matches bassclef template → stale-local,
1802
- # not bassclef drift. Surface the diagnosis + recommend pull. Do NOT block.
1803
- #
1804
- # #267 fire-and-forget self-heal: if working tree is clean AND on main AND
1805
- # no unpushed commits, do the pull silently and re-exec the hook. Operator
1806
- # in autonomous-overnight mode isn't there to manually clear the BLOCKED;
1807
- # the common case is safe to auto-resolve.
1808
- SELF_HEAL_OK=0
1809
- if [ -z "$(cd "$CWD" && git status --porcelain 2>/dev/null)" ]; then
1810
- CURRENT_BRANCH="$(cd "$CWD" && git symbolic-ref --short HEAD 2>/dev/null || echo "")"
1811
- if [ "$CURRENT_BRANCH" = "main" ]; then
1812
- UNPUSHED="$(cd "$CWD" && git rev-list --count '@{u}..HEAD' 2>/dev/null || echo "0")"
1813
- if [ "$UNPUSHED" = "0" ]; then
1814
- SELF_HEAL_OK=1
1815
- fi
1816
- fi
1817
- fi
1818
-
1819
- if [ "$SELF_HEAL_OK" = "1" ]; then
1820
- echo "ℹ️ Auto-healing bassclef drift: clean tree on main, no unpushed commits, remote-main matches template."
1821
- echo " Pulling latest and re-executing sync (#267)."
1822
- echo ""
1823
- if (cd "$CWD" && git pull origin main --quiet 2>&1); then
1824
- # Re-exec the hook against the freshly-pulled version.
1825
- # exec replaces the current process so the new hook output supersedes
1826
- # this stale one — operator sees the fresh summary, not "drift then
1827
- # something else."
1828
- exec bash "$CWD/.claude/hooks/bassclef-sync.sh" <<< "{\"cwd\":\"$CWD\"}"
1829
- else
1830
- echo "⚠️ Auto-heal pull failed — falling through to manual remediation block."
1831
- echo ""
1832
- # Fall through to the manual stale-local block below
1833
- fi
1834
- fi
1835
-
1836
- echo "### LOCAL CLONE STALE — NOT BASSCLEF DRIFT"
1837
- echo ""
1838
- echo "Remote main on $CONSUMER_REMOTE matches bassclef template ($BASSCLEF_VER)."
1839
- echo "Your local hook ($SELF_VER) is behind. Pull to bring it in:"
1840
- echo ""
1841
- echo " cd \"$CWD\" && git pull origin main && \\"
1842
- echo " echo '{\"cwd\":\"$CWD\"}' | bash .claude/hooks/bassclef-sync.sh"
1843
- if [ "$SELF_HEAL_OK" = "1" ]; then
1844
- echo ""
1845
- echo " (Auto-heal attempted but pull failed; resolve the pull error above and re-run.)"
1846
- else
1847
- echo ""
1848
- echo " (Auto-heal skipped: working tree dirty, off main, or unpushed commits exist."
1849
- echo " Resolve those first so the pull is non-destructive.)"
1850
- fi
1851
- echo ""
1852
- else
1853
- # Remote main also lags template OR remote check unavailable → genuine drift.
1854
- # Determine the remediation: open sync PR (merge it), merged sync PR (pull),
1855
- # or fallback (try both).
1856
- OPEN_SYNC_PR=""
1857
- MERGED_SYNC_PR_URL=""
1858
- if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
1859
- OPEN_SYNC_PR=$(gh pr list -R "$CONSUMER_REMOTE" --search "head:bassclef-sync/hook-template is:open" --limit 1 --json number,url --jq '.[0].url' 2>/dev/null)
1860
- if [ -z "$OPEN_SYNC_PR" ]; then
1861
- MERGED_SYNC_PR_URL=$(gh pr list -R "$CONSUMER_REMOTE" --state merged --search "head:bassclef-sync/hook-template" --limit 1 --json url --jq '.[0].url' 2>/dev/null)
1862
- fi
1863
- fi
1864
-
1865
- echo "🛑🛑🛑 BLOCKED 🛑🛑🛑"
1866
- echo "────────────────────────────────────────────"
1867
- echo "bassclef hook drift — consumer hook version lags bassclef template."
1868
- echo ""
1869
- echo " Consumer hook version: $SELF_VER"
1870
- echo " Bassclef template: $BASSCLEF_VER"
1871
- if [ -n "$REMOTE_MAIN_VER" ]; then
1872
- echo " Remote main version: $REMOTE_MAIN_VER (also lags template)"
1873
- fi
1874
- echo ""
1875
- if [ -n "$OPEN_SYNC_PR" ]; then
1876
- # Open sync PR exists — merge path
1877
- echo " → Review + merge the open template-sync PR:"
1878
- echo " $OPEN_SYNC_PR"
1879
- echo ""
1880
- echo " After merging, resume with:"
1881
- echo " cd \"$CWD\" && git pull origin main && \\"
1882
- echo " echo '{\"cwd\":\"$CWD\"}' | bash .claude/hooks/bassclef-sync.sh"
1883
- elif [ -n "$MERGED_SYNC_PR_URL" ]; then
1884
- # No open PR but a merged one exists — local is behind main
1885
- echo " → Sync PR already merged ($MERGED_SYNC_PR_URL) but your local working"
1886
- echo " tree is behind main. Pull to bring the updated hook in:"
1887
- echo ""
1888
- echo " cd \"$CWD\" && git pull origin main && \\"
1889
- echo " echo '{\"cwd\":\"$CWD\"}' | bash .claude/hooks/bassclef-sync.sh"
1890
- else
1891
- # No open sync PR AND no merged sync PR found.
1892
- #
1893
- # Pre-2026-05-03 path (bassclef#268, #269): bassclef-template-sync.yml
1894
- # auto-generated drift-fix PRs in consumer repos; remediation was
1895
- # `gh workflow run bassclef-template-sync.yml`. Workflow retired
1896
- # 2026-05-03 (PR #445); thin-pointer architecture replaces it.
1897
- #
1898
- # Current remediation: vendored consumer migrates to thin-pointer by
1899
- # running /onboard-repo (which writes .bassclef-source.json + a
1900
- # 130-line dispatcher hook that fetches bassclef's current template
1901
- # at every session-start — no vendoring, no drift class). Or, if
1902
- # local clone is stale, pull main.
1903
- echo " → No open sync PR + no recent merged sync PR. bassclef-template-sync"
1904
- echo " workflow retired 2026-05-03; thin-pointer architecture supersedes it."
1905
- echo ""
1906
- echo " Recommended path — migrate this consumer to thin-pointer (one-shot):"
1907
- echo ""
1908
- echo " In a Claude Code session in this repo, invoke /onboard-repo"
1909
- echo ""
1910
- echo " # /onboard-repo writes .bassclef-source.json + the thin-pointer"
1911
- echo " # dispatcher hook. Drift class disappears (the dispatcher fetches"
1912
- echo " # bassclef's current template every session)."
1913
- echo ""
1914
- echo " Or, if local clone is stale (rare for vendored consumers):"
1915
- echo ""
1916
- echo " cd \"$CWD\" && git pull origin main && \\"
1917
- echo " echo '{\"cwd\":\"$CWD\"}' | bash .claude/hooks/bassclef-sync.sh"
1918
- fi
1919
- echo ""
1920
- echo " Override (accept stale hook, proceed at own risk):"
1921
- echo " tell the agent: 'proceed anyway, will merge later'"
1922
- echo "────────────────────────────────────────────"
1923
- echo ""
1924
- fi
1925
- fi
1926
-
1927
- if [ -n "$SYNC_NEW" ]; then
1928
- echo "### NEW SINCE LAST SYNC — REVIEW BEFORE PROCEEDING"
1929
- echo ""
1930
- echo "$SYNC_NEW"
1931
- echo ""
1932
- echo "→ New bassclef standards detected. Ask the user to confirm"
1933
- echo " before proceeding. These may change how this session operates."
1934
- echo ""
1935
- fi
1936
-
1937
- echo "YOU MUST confirm to the user:"
1938
- echo " 'Bassclef substrate synced: ${SKILL_COUNT} skills, ${RULE_COUNT} rules,"
1939
- echo -n " ${AGENT_COUNT} agents, ${HOOK_COUNT} hooks. Status: ${SYNC_STATUS}."
1940
- # Version readout (per WU-2 of bet 2026-06-22c; closes #3)
1941
- if [ -n "$BASSCLEF_VERSION" ]; then
1942
- echo ""
1943
- echo -n " Version: ${BASSCLEF_VERSION} (released ${BASSCLEF_RELEASE_DATE_SHORT}${BASSCLEF_RELEASES_BEHIND})."
1944
- fi
1945
- # Surface non-OK URL tiers on the mandatory confirm line (#225 follow-up).
1946
- # OK tier stays silent — no clutter on the hot path. Warn/expired tiers
1947
- # are guaranteed-visible to the operator, not buried in verbose output.
1948
- if [ "$URL_TIER" = "warn" ]; then
1949
- echo ""
1950
- echo " ⚠️ Presigned URL: ${URL_AGE_HOURS}h old, expires in ${URL_REMAINING_HOURS}h (refreshes on next merge or 06:00 UTC cron).'"
1951
- elif [ "$URL_TIER" = "expired" ]; then
1952
- echo ""
1953
- echo " 🛑 Presigned URL expired — end session and restart for fresh clone.'"
1954
- else
1955
- echo "'"
1956
- fi
1957
- echo ""
1958
-
1959
- # === 5b. Substrate vs bassclef-content access-model note (bassclef#1290) ===
1960
- # Per bassclef#1290 — the "Status: up-to-date" line above refers to substrate
1961
- # primitives (skills + rules + agents + hooks) which sync into this repo. Bassclef
1962
- # also ships non-substrate content (standards/, architecture/, design/, docs/) that
1963
- # is NOT vendored into this repo — instead it's accessed via the agent's
1964
- # additionalDirectories pointing at bassclef's checkout. This note prevents the
1965
- # adopter from interpreting "Status: up-to-date" as "all bassclef content is
1966
- # mirrored here" — they hit a "file not found" if they try to dispatch a
1967
- # bassclef-only standard from an adopter-rooted relative path.
1968
- echo " Note: bassclef also ships standards/, architecture/, design/, docs/ that"
1969
- echo " are NOT vendored into this repo. The agent reads them via additionalDirectories"
1970
- echo " pointing at bassclef's checkout. To dispatch a bassclef-only standard from"
1971
- echo " your shell, use the full path (e.g. $HOME/src/sunj-labs/bassclef/standards/..."
1972
- echo " or wherever your bassclef checkout lives) — not the adopter-rooted relative path."
1973
- echo " Per bassclef#1290."
1974
- echo ""
1975
-
1976
- # === 6. Boot check — only report what's missing ===
1977
- MISSING=""
1978
-
1979
- # Google service account (needed for LinkedIn → Google Docs)
1980
- SA_KEY="$HOME/.config/gcloud/sunjay-google-ops.json"
1981
- if [ ! -f "$SA_KEY" ]; then
1982
- MISSING="${MISSING}\n- Google Docs service account key missing at $SA_KEY (needed for /journal → Google Doc push)"
1983
- fi
1984
-
1985
- # gh CLI auth (needed for /promote, issue creation)
1986
- if ! command -v gh &>/dev/null; then
1987
- MISSING="${MISSING}\n- gh CLI not installed (needed for /promote, issue creation)"
1988
- elif ! gh auth status &>/dev/null 2>&1; then
1989
- MISSING="${MISSING}\n- gh CLI not authenticated (run: gh auth login)"
1990
- fi
1991
-
1992
- if [ -n "$MISSING" ]; then
1993
- echo "### SUBSTRATE DEPENDENCIES — OPTIONAL SETUP NEEDED"
1994
- echo ""
1995
- echo "The following are not blocking but limit some skills:"
1996
- echo -e "$MISSING"
1997
- echo ""
1998
- echo "Run: bassclef/scripts/boot-bassclef.sh for full setup guide."
1999
- echo "These are informational — proceed with your work."
2000
- echo ""
2001
- fi
2002
-
2003
- # === 7. Auto self-heal — refresh stale adopter wiring (per ADR-032 Layer 3) ===
2004
- #
2005
- # Anti-brittle pattern (Nygard Steady State): every adopter session-start
2006
- # auto-runs migrate-adopter-references.sh against the adopter repo. Catches:
2007
- # - Stale `canonical/` symlinks under .claude/hooks/ (Surface 1)
2008
- # - Stale `canonical` references in .claude/settings.json (Surface 2)
2009
- # - Vendored dispatcher version drift (Surface 3)
2010
- #
2011
- # Without this auto-trigger, adopters stay on stale state forever unless
2012
- # operator manually invokes the migrate script. Operator framing (2026-06-22b
2013
- # closeout): "no other adopter is going to do (2). How do we solve for that —
2014
- # make this anti-brittle."
2015
- #
2016
- # Defense-in-depth:
2017
- # - SKIP_BASSCLEF_SELF_HEAL=1 — opt out per session
2018
- # - Substrate-self guard — silently skip when CWD is bassclef itself
2019
- # - Dry-run first; only apply when findings > 0 (no churn on clean state)
2020
- # - migrate-script-not-found → silent no-op (old bassclef snapshots without it)
2021
- # - Failures fail-soft; never exit non-zero
2022
-
2023
- if [ "${SKIP_BASSCLEF_SELF_HEAL:-0}" = "1" ]; then
2024
- echo "### bassclef self-heal: SKIPPED (SKIP_BASSCLEF_SELF_HEAL=1)"
2025
- elif [ -f "$CWD/architecture/dual-repo-flow.md" ]; then
2026
- # We're in bassclef-upstream itself; migrate-script's bulkhead would refuse
2027
- # anyway. Skip silently for cleaner session-start output.
2028
- :
2029
- else
2030
- # Override hatch for tests + non-standard layouts; defaults to sibling lookup
2031
- MIGRATE_SCRIPT="${BASSCLEF_MIGRATE_SCRIPT:-$(dirname "${BASH_SOURCE[0]}")/migrate-adopter-references.sh}"
2032
- if [ -x "$MIGRATE_SCRIPT" ]; then
2033
- # Dry-run first: count findings without mutating anything
2034
- DRY_OUTPUT=$(bash "$MIGRATE_SCRIPT" --dry-run "$CWD" 2>&1) || true
2035
- FINDINGS=$(echo "$DRY_OUTPUT" | sed -n 's/^findings: *\([0-9][0-9]*\).*/\1/p' | head -1)
2036
- FINDINGS=${FINDINGS:-0}
2037
- if [ "$FINDINGS" -gt 0 ] 2>/dev/null; then
2038
- echo ""
2039
- echo "### bassclef self-heal: $FINDINGS finding(s) — applying refresh"
2040
- bash "$MIGRATE_SCRIPT" --apply "$CWD" 2>&1 | sed 's/^/ /' || true
2041
- echo ""
2042
- fi
2043
- fi
2044
- fi
2045
-
2046
- # --------------------------------------------------------------
2047
- # Tail check — SessionStart wiring postcondition (bassclef-upstream#1496)
2048
- #
2049
- # Fires after every sync. Verifies adopter's project-scope settings.json
2050
- # has bassclef-sync wired at SessionStart. Self-heals when stale.
2051
- # Per Nygard bulkhead (R2): heal failure logs INFORM but does not kill
2052
- # session. BASSCLEF_SYNC_STRICT=1 promotes postcondition fail to exit 3
2053
- # (opt-in for CI validation per R16).
2054
- # --------------------------------------------------------------
2055
- ADOPTER_SETTINGS="$CWD/.claude/settings.json"
2056
- verify_sessionstart_wiring "$ADOPTER_SETTINGS"
2057
- VERIFY_RC=$?
2058
- if [ "$VERIFY_RC" -eq 1 ]; then
2059
- # Missing entry — attempt heal
2060
- if heal_sessionstart_wiring "$ADOPTER_SETTINGS"; then
2061
- : # heal succeeded; log line already emitted
2062
- else
2063
- echo " bassclef-sync: SessionStart wiring heal did not succeed for $ADOPTER_SETTINGS (INFORM); see stderr above" >&2
2064
- if [ "${BASSCLEF_SYNC_STRICT:-0}" = "1" ]; then
2065
- exit 3
2066
- fi
2067
- fi
2068
- elif [ "$VERIFY_RC" -eq 2 ]; then
2069
- # Malformed JSON — R5 backup already handled by heal path; do not attempt jq write
2070
- echo " bassclef-sync: adopter settings.json malformed; skipping SessionStart wiring heal (INFORM)" >&2
2071
- if [ "${BASSCLEF_SYNC_STRICT:-0}" = "1" ]; then
2072
- exit 3
2073
- fi
2074
- fi
2075
-
2076
- exit 0