@thebassclef/lite 1.0.4 → 1.1.1

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