@thebassclef/lite 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +65 -0
- package/dist/cli.js +65 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/agents/architect.md +70 -0
- package/dist/lite/.claude/agents/builder.md +114 -0
- package/dist/lite/.claude/agents/designer.md +156 -0
- package/dist/lite/.claude/agents/reviewer.md +88 -0
- package/dist/lite/.claude/hooks/bassclef-sync.sh +85 -27
- package/dist/lite/.claude/hooks/longrun-prep-compounding-axis-check.sh +492 -0
- package/dist/lite/.claude/hooks/longrun-prep-render-check.sh +236 -0
- package/dist/lite/.claude/hooks/persona-name-scrub-check.sh +105 -0
- package/dist/lite/.claude/hooks/skill-step-list-check.sh +171 -0
- package/dist/lite/.claude/luminaries/alan-cooper.md +170 -0
- package/dist/lite/.claude/luminaries/alistair-cockburn.md +140 -0
- package/dist/lite/.claude/luminaries/amazon-pr-faq.md +34 -0
- package/dist/lite/.claude/luminaries/andreas-zeller.md +74 -0
- package/dist/lite/.claude/luminaries/ash-maurya.md +121 -0
- package/dist/lite/.claude/luminaries/bill-buxton.md +210 -0
- package/dist/lite/.claude/luminaries/charles-sanders-peirce.md +150 -0
- package/dist/lite/.claude/luminaries/david-ogilvy.md +192 -0
- package/dist/lite/.claude/luminaries/don-norman.md +173 -0
- package/dist/lite/.claude/luminaries/edward-tufte.md +179 -0
- package/dist/lite/.claude/luminaries/eric-evans.md +160 -0
- package/dist/lite/.claude/luminaries/frederick-brooks.md +50 -0
- package/dist/lite/.claude/luminaries/gang-of-four.md +157 -0
- package/dist/lite/.claude/luminaries/glenford-myers.md +230 -0
- package/dist/lite/.claude/luminaries/hunt-thomas.md +115 -0
- package/dist/lite/.claude/luminaries/hyrum-wright.md +94 -0
- package/dist/lite/.claude/luminaries/jason-fried-dhh.md +46 -0
- package/dist/lite/.claude/luminaries/jesse-james-garrett.md +154 -0
- package/dist/lite/.claude/luminaries/john-ousterhout.md +94 -0
- package/dist/lite/.claude/luminaries/karl-popper.md +132 -0
- package/dist/lite/.claude/luminaries/kent-beck.md +168 -0
- package/dist/lite/.claude/luminaries/linus-torvalds.md +218 -0
- package/dist/lite/.claude/luminaries/martin-fowler.md +164 -0
- package/dist/lite/.claude/luminaries/michael-feathers.md +125 -0
- package/dist/lite/.claude/luminaries/michael-nygard.md +118 -0
- package/dist/lite/.claude/luminaries/robert-c-martin.md +164 -0
- package/dist/lite/.claude/luminaries/saltzer-schroeder.md +194 -0
- package/dist/lite/.claude/luminaries/sophia-prater.md +193 -0
- package/dist/lite/.claude/luminaries/stephen-toulmin.md +131 -0
- package/dist/lite/.claude/luminaries/tony-hoare.md +170 -0
- package/dist/lite/.claude/luminaries/vaughn-vernon.md +50 -0
- package/dist/lite/.claude/luminaries/w-edwards-deming.md +158 -0
- package/dist/lite/.claude/rules/accessor-library-discipline.md +110 -0
- package/dist/lite/.claude/rules/adr-discipline.md +96 -0
- package/dist/lite/.claude/rules/api-conventions.md +115 -0
- package/dist/lite/.claude/rules/artifact-ingestion.md +137 -0
- package/dist/lite/.claude/rules/assert-only-after-verify.md +111 -0
- package/dist/lite/.claude/rules/blocked-items.md +139 -0
- package/dist/lite/.claude/rules/bootstrap-pair-discipline.md +106 -0
- package/dist/lite/.claude/rules/branching.md +28 -0
- package/dist/lite/.claude/rules/cold-adopter-harness-discipline.md +93 -0
- package/dist/lite/.claude/rules/commit-conventions.md +22 -0
- package/dist/lite/.claude/rules/compounding-axis-fresh-analysis.md +173 -0
- package/dist/lite/.claude/rules/compounding-sequence-fresh-analysis.md +173 -0
- package/dist/lite/.claude/rules/context-engineering.md +155 -0
- package/dist/lite/.claude/rules/context-management.md +85 -0
- package/dist/lite/.claude/rules/defensive-bash.md +52 -0
- package/dist/lite/.claude/rules/deferred-actions.md +233 -0
- package/dist/lite/.claude/rules/destructive-operations.md +69 -0
- package/dist/lite/.claude/rules/diagnosis.md +38 -0
- package/dist/lite/.claude/rules/github-issue-flash-tweet.md +114 -0
- package/dist/lite/.claude/rules/guardrails.md +73 -0
- package/dist/lite/.claude/rules/hook-wire-on-author.md +77 -0
- package/dist/lite/.claude/rules/identifier-leak-prevention.md +73 -0
- package/dist/lite/.claude/rules/iteration-bet-brief-completeness.md +43 -0
- package/dist/lite/.claude/rules/lite-manifest-schema-change-discipline.md +79 -0
- package/dist/lite/.claude/rules/longrun-prep-plan-doc-compression.md +77 -0
- package/dist/lite/.claude/rules/loop-discipline.md +71 -0
- package/dist/lite/.claude/rules/manual-prod-approval.md +100 -0
- package/dist/lite/.claude/rules/marker-enrichment-discipline.md +72 -0
- package/dist/lite/.claude/rules/mobile-ephemeral-session.md +109 -0
- package/dist/lite/.claude/rules/new-dependency-check.md +44 -0
- package/dist/lite/.claude/rules/oo-ad-entry-point.md +95 -0
- package/dist/lite/.claude/rules/operator-facing-prose.md +177 -0
- package/dist/lite/.claude/rules/option-label-discipline.md +82 -0
- package/dist/lite/.claude/rules/pattern-annotation.md +83 -0
- package/dist/lite/.claude/rules/persona-name-leak-scrub.md +90 -0
- package/dist/lite/.claude/rules/plain-english-discipline.md +132 -0
- package/dist/lite/.claude/rules/plan-enumeration-needs-value-props.md +177 -0
- package/dist/lite/.claude/rules/pr-body-shape.md +195 -0
- package/dist/lite/.claude/rules/pr-strategy.md +142 -0
- package/dist/lite/.claude/rules/pr-title-shape.md +90 -0
- package/dist/lite/.claude/rules/prototype-workflow.md +53 -0
- package/dist/lite/.claude/rules/reserved-skill-names.md +103 -0
- package/dist/lite/.claude/rules/schema-management.md +42 -0
- package/dist/lite/.claude/rules/sdlc-gates.md +149 -0
- package/dist/lite/.claude/rules/security.md +37 -0
- package/dist/lite/.claude/rules/session-artifacts.md +236 -0
- package/dist/lite/.claude/rules/skill-composition-declarations.md +105 -0
- package/dist/lite/.claude/rules/skill-description-clarity.md +196 -0
- package/dist/lite/.claude/rules/skill-procedure-step-list.md +119 -0
- package/dist/lite/.claude/rules/sprint-prep-preset-picker.md +76 -0
- package/dist/lite/.claude/rules/state-schema-validation.md +156 -0
- package/dist/lite/.claude/rules/stuck-signal-diagnostic.md +111 -0
- package/dist/lite/.claude/rules/substrate-config-schema.md +85 -0
- package/dist/lite/.claude/rules/test-list-discipline.md +129 -0
- package/dist/lite/.claude/rules/test-sufficiency.md +186 -0
- package/dist/lite/.claude/rules/testing-tier-config.md +125 -0
- package/dist/lite/.claude/rules/testing.md +38 -0
- package/dist/lite/.claude/rules/turn-estimate-grounding.md +82 -0
- package/dist/lite/.claude/rules/visual-hierarchy.md +427 -0
- package/dist/lite/.claude/rules/we-dont-break-adopters.md +103 -0
- package/dist/lite/.claude/rules/whereami-load-bearing.md +184 -0
- package/dist/lite/.claude/rules/writing-craft-discipline.md +72 -0
- package/dist/lite/.claude/rules/wu-sequencing-compounds.md +119 -0
- package/dist/lite/.claude/skills/build/SKILL.md +640 -0
- package/dist/lite/.claude/skills/chronicle/SKILL.md +55 -0
- package/dist/lite/.claude/skills/clean-artifacts/SKILL.md +249 -0
- package/dist/lite/.claude/skills/decompose/SKILL.md +280 -0
- package/dist/lite/.claude/skills/diagnose/SKILL.md +297 -0
- package/dist/lite/.claude/skills/feynman/SKILL.md +90 -0
- package/dist/lite/.claude/skills/howdoi/SKILL.md +105 -0
- package/dist/lite/.claude/skills/ia-model/SKILL.md +108 -0
- package/dist/lite/.claude/skills/interaction-design/SKILL.md +112 -0
- package/dist/lite/.claude/skills/interpret-input/SKILL.md +180 -0
- package/dist/lite/.claude/skills/journal/SKILL.md +209 -0
- package/dist/lite/.claude/skills/kiss/SKILL.md +449 -0
- package/dist/lite/.claude/skills/launch/SKILL.md +915 -0
- package/dist/lite/.claude/skills/lean-canvas/SKILL.md +332 -0
- package/dist/lite/.claude/skills/longrun/SKILL.md +512 -0
- package/dist/lite/.claude/skills/luminary/SKILL.md +481 -0
- package/dist/lite/.claude/skills/ogilvy-writing-audit/SKILL.md +177 -0
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +1640 -0
- package/dist/lite/.claude/skills/pattern-review/SKILL.md +99 -0
- package/dist/lite/.claude/skills/personas/SKILL.md +207 -0
- package/dist/lite/.claude/skills/promote/SKILL.md +283 -0
- package/dist/lite/.claude/skills/requirement/SKILL.md +98 -0
- package/dist/lite/.claude/skills/retro/SKILL.md +117 -0
- package/dist/lite/.claude/skills/riff/SKILL.md +114 -0
- package/dist/lite/.claude/skills/roadmap-reconcile/SKILL.md +163 -0
- package/dist/lite/.claude/skills/session-end/SKILL.md +309 -0
- package/dist/lite/.claude/skills/session-log/SKILL.md +299 -0
- package/dist/lite/.claude/skills/skills/SKILL.md +228 -0
- package/dist/lite/.claude/skills/spec/SKILL.md +105 -0
- package/dist/lite/.claude/skills/sprint/SKILL.md +418 -0
- package/dist/lite/.claude/skills/stage/SKILL.md +384 -0
- package/dist/lite/.claude/skills/state-a-problem/SKILL.md +185 -0
- package/dist/lite/.claude/skills/temperance/SKILL.md +108 -0
- package/dist/lite/.claude/skills/use-case/SKILL.md +417 -0
- package/dist/lite/.claude/skills/user-stories/SKILL.md +268 -0
- package/dist/lite/.claude/skills/value-prop/SKILL.md +251 -0
- package/dist/lite/.claude/skills/verify/SKILL.md +160 -0
- package/dist/lite/.claude/skills/visual-review/SKILL.md +503 -0
- package/dist/lite/.claude/skills/whats-the-plan/SKILL.md +202 -0
- package/dist/lite/.claude/skills/whereami/SKILL.md +307 -0
- package/dist/lite/AGENTS.md +79 -0
- package/dist/lite/CLAUDE-lite.md +85 -0
- package/dist/lite/CODE_OF_CONDUCT.md +28 -0
- package/dist/lite/CONTRIBUTING.md +177 -0
- package/dist/lite/README.md +173 -0
- package/dist/lite/SECURITY.md +19 -0
- package/dist/lite/architecture/decisions/ADR-029-release-pipeline.md +79 -0
- package/dist/lite/architecture/decisions/ADR-031-non-breaking-changes-adopter-discipline.md +139 -0
- package/dist/lite/architecture/decisions/ADR-032-adopter-sync-dispatcher-architecture.md +192 -0
- package/dist/lite/architecture/decisions/ADR-039-release-tagging-scheme.md +145 -0
- package/dist/lite/architecture/decisions/ADR-040-planning-skill-vocabulary-and-lite-profile.md +155 -0
- package/dist/lite/architecture/decisions/ADR-044-unified-skill-body-template.md +162 -0
- package/dist/lite/lib/clean-artifacts-sweep.sh +112 -0
- package/dist/lite/lib/code-comment-discipline.sh +144 -0
- package/dist/lite/lib/composer-preflight.sh +459 -0
- package/dist/lite/lib/deferred-actions.sh +146 -0
- package/dist/lite/lib/hook-header-parser.sh +61 -0
- package/dist/lite/lib/hook-heartbeat.sh +152 -0
- package/dist/lite/lib/hook-inject.sh +255 -0
- package/dist/lite/lib/hook-installer.sh +195 -0
- package/dist/lite/lib/luminary-pick.sh +96 -0
- package/dist/lite/lib/output-discipline.sh +143 -0
- package/dist/lite/lib/prose-scan-boundary.sh +171 -0
- package/dist/lite/lib/rewrite-check.sh +214 -0
- package/dist/lite/lib/session-rescue.sh +151 -0
- package/dist/lite/lib/source-graph-walker.sh +277 -0
- package/dist/lite/lib/state.sh +1372 -0
- package/dist/lite/lib/telemetry.sh +205 -0
- package/dist/lite/lib/tier-check.sh +187 -0
- package/dist/lite/lib/tier-dependency-audit.sh +1088 -0
- package/dist/lite/presence/install/bassclef-hook-connect.sh +178 -0
- package/dist/lite/presence/install/bassclef-sync.dispatcher.template.sh +841 -0
- package/dist/lite/presence/install/bassclef-sync.template.sh +2278 -0
- package/dist/lite/presence/install/schedule-auto-save.cron.sh +88 -0
- package/dist/lite/presence/install/schedule-auto-save.taskscheduler.md +122 -0
- package/dist/lite/scripts/aggregate-telemetry.sh +217 -0
- package/dist/lite/scripts/analyze-tier-dependencies.sh +239 -0
- package/dist/lite/scripts/generate-lite-manifest.sh +505 -0
- package/dist/lite/scripts/generate-tier-manifest.sh +28 -0
- package/dist/lite/scripts/intent-drift-check.sh +456 -0
- package/dist/lite/scripts/lite-manifest-drift-check.sh +146 -0
- package/dist/lite/scripts/render-lite-manifest-doc.sh +150 -0
- package/dist/lite/standards/adr-template.md +86 -0
- package/dist/lite/standards/api-conventions/nextjs.md +84 -0
- package/dist/lite/standards/artifact-composition.md +209 -0
- package/dist/lite/standards/bash-hook-safety.md +246 -0
- package/dist/lite/standards/bassclef-configs-schema.md +232 -0
- package/dist/lite/standards/bassclef-evolution.md +143 -0
- package/dist/lite/standards/bassclef-internal-jargon.md +244 -0
- package/dist/lite/standards/bassclef-managed-sentinel.md +96 -0
- package/dist/lite/standards/bassclef-source-config.md +228 -0
- package/dist/lite/standards/branch-stacking.md +408 -0
- package/dist/lite/standards/code-safety-principles.md +176 -0
- package/dist/lite/standards/composer-prerequisites.md +155 -0
- package/dist/lite/standards/deferred-actions-schema.md +204 -0
- package/dist/lite/standards/dependency-discipline/cargo.md +39 -0
- package/dist/lite/standards/dependency-discipline/gem.md +43 -0
- package/dist/lite/standards/dependency-discipline/go-mod.md +41 -0
- package/dist/lite/standards/dependency-discipline/npm.md +42 -0
- package/dist/lite/standards/dependency-discipline/pip.md +42 -0
- package/dist/lite/standards/deployment-topology/ec2-tailscale.md +225 -0
- package/dist/lite/standards/deployment-topology.md +69 -0
- package/dist/lite/standards/docs-sync-allowlist.md +76 -0
- package/dist/lite/standards/domain-and-dns.md +145 -0
- package/dist/lite/standards/frontend-stack.md +67 -0
- package/dist/lite/standards/frontmatter-schema.md +154 -0
- package/dist/lite/standards/graceful-exit.md +227 -0
- package/dist/lite/standards/hook-idempotency.md +102 -0
- package/dist/lite/standards/hook-injection-discipline.md +202 -0
- package/dist/lite/standards/hook-install-class.md +215 -0
- package/dist/lite/standards/input-handler-interface.md +152 -0
- package/dist/lite/standards/lite-manifest-schema-changes.md +180 -0
- package/dist/lite/standards/lite-manifest.json +2797 -0
- package/dist/lite/standards/luminary-matching.md +105 -0
- package/dist/lite/standards/luminary-problem-patterns.md +481 -0
- package/dist/lite/standards/migration-discipline/active-record.md +50 -0
- package/dist/lite/standards/migration-discipline/alembic.md +43 -0
- package/dist/lite/standards/migration-discipline/gorm.md +50 -0
- package/dist/lite/standards/migration-discipline/prisma.md +53 -0
- package/dist/lite/standards/migration-discipline/sqlalchemy.md +51 -0
- package/dist/lite/standards/mobile-ephemeral-session.md +167 -0
- package/dist/lite/standards/model-routing-discipline.md +160 -0
- package/dist/lite/standards/ogilvy-writing-rules.md +225 -0
- package/dist/lite/standards/opener-discipline.md +96 -0
- package/dist/lite/standards/operator-facing-prose-discipline.md +201 -0
- package/dist/lite/standards/persona-schema.md +229 -0
- package/dist/lite/standards/pluggable-luminaries.md +323 -0
- package/dist/lite/standards/pr-body-discipline.md +115 -0
- package/dist/lite/standards/preview-state-schema.md +189 -0
- package/dist/lite/standards/project-directory-layout.md +276 -0
- package/dist/lite/standards/release-tagging.md +137 -0
- package/dist/lite/standards/reserved-skill-names.md +120 -0
- package/dist/lite/standards/scannable-multi-option-output.md +265 -0
- package/dist/lite/standards/sdlc-compliance.md +286 -0
- package/dist/lite/standards/sdlc-gates/typescript.md +57 -0
- package/dist/lite/standards/secrets-lifecycle.md +210 -0
- package/dist/lite/standards/security-scanner-adapter.md +145 -0
- package/dist/lite/standards/session-board.md +256 -0
- package/dist/lite/standards/skill-output-discipline.md +121 -0
- package/dist/lite/standards/state-spine-contract.md +255 -0
- package/dist/lite/standards/state-spine.md +511 -0
- package/dist/lite/standards/steering-hints/kiss-words.md +11 -0
- package/dist/lite/standards/substrate-config-schema.md +267 -0
- package/dist/lite/standards/tech-stack-config.md +109 -0
- package/dist/lite/standards/tier-dependency-analysis.md +167 -0
- package/dist/lite/standards/tier-runtime-deps/lite.md +57 -0
- package/dist/lite/standards/tier-tag-schema.md +155 -0
- package/dist/lite/standards/two-layer-config.md +99 -0
- package/dist/lite/standards/use-case-format.md +292 -0
- package/dist/lite/standards/user-story-invest.md +268 -0
- package/dist/lite/standards/velocity-and-appetite.md +229 -0
- package/dist/lite/standards/voice-input-pattern.md +119 -0
- package/dist/lite/standards/whereami-schema.md +301 -0
- package/dist/lite/standards/worktree-management.md +211 -0
- package/dist/lite/standards/writing-guide.md +213 -0
- package/dist/lite/templates/chronicle-template.md +75 -0
- package/dist/lite/templates/deferred-action-template.md +45 -0
- package/dist/lite/templates/memory-proposal-template.md +77 -0
- package/dist/lite/templates/persona-template.md +200 -0
- package/dist/lite/templates/pr-faq.md +45 -0
- package/dist/lite/templates/secret-rotation-template.md +162 -0
- package/dist/lite/templates/spec-template.md +131 -0
- package/dist/lite/templates/use-case-template.md +194 -0
- package/dist/lite/templates/user-story-template.md +107 -0
- package/dist/lite/templates/whereami-template.md +101 -0
- package/package.json +1 -1
|
@@ -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
|