@thebassclef/lite 1.0.3 → 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 +92 -6
- package/dist/cli.js +92 -6
- 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,152 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# lib/hook-heartbeat.sh — hook liveness marker helper.
|
|
4
|
+
#
|
|
5
|
+
# Deep module per @luminary john-ousterhout. Two-function interface
|
|
6
|
+
# hides the marker path convention, atomic write pattern, format
|
|
7
|
+
# schema, directory creation, and silent-fail behavior.
|
|
8
|
+
#
|
|
9
|
+
# tier: upstream
|
|
10
|
+
# install-class: dual
|
|
11
|
+
#
|
|
12
|
+
# Interface (what callers know):
|
|
13
|
+
# heartbeat_mark <hook-name>
|
|
14
|
+
# — call from any critical hook on entry; records fire timestamp
|
|
15
|
+
# — silent-fails; never crashes the caller
|
|
16
|
+
#
|
|
17
|
+
# heartbeat_check <hook-name> <max-age-seconds>
|
|
18
|
+
# — returns 0 when marker exists AND is younger than max-age
|
|
19
|
+
# — returns 1 when marker exists AND is older than max-age (stale)
|
|
20
|
+
# — returns 2 when marker file does not exist
|
|
21
|
+
#
|
|
22
|
+
# Hidden (what the module encapsulates):
|
|
23
|
+
# - Marker path: state/markers/hook-heartbeat/<hook-name>.marker
|
|
24
|
+
# (overridable via HEARTBEAT_MARKER_DIR env var for test isolation)
|
|
25
|
+
# - Format: single-line ISO 8601 UTC timestamp (YYYY-MM-DDTHH:MM:SSZ)
|
|
26
|
+
# - Atomic write: temp file + mv (survives concurrent writes)
|
|
27
|
+
# - Directory creation: mkdir -p on first mark
|
|
28
|
+
# - Silent-fail: any I/O failure returns 0 from heartbeat_mark
|
|
29
|
+
#
|
|
30
|
+
# Consumers:
|
|
31
|
+
# - Critical hooks call heartbeat_mark on entry (WU-3 of bet 2026-07-31d)
|
|
32
|
+
# - Session-start reflection module calls heartbeat_check per hook
|
|
33
|
+
# against expected cadence from standards/hook-cadence.json
|
|
34
|
+
#
|
|
35
|
+
# Rules governed by:
|
|
36
|
+
# - .claude/rules/testing-tier-config.md (Tier 0 strict TDD; lib/*.sh)
|
|
37
|
+
# - .claude/rules/mechanism-fidelity.md (interface honored end to end)
|
|
38
|
+
# - .claude/rules/substrate-as-system.md (runtime cadence axis)
|
|
39
|
+
#
|
|
40
|
+
# Anchor luminaries:
|
|
41
|
+
# - @luminary john-ousterhout (deep module)
|
|
42
|
+
# - @luminary michael-nygard (heartbeat = steady-state pattern)
|
|
43
|
+
# - @luminary jerome-saltzer-and-michael-schroeder (complete mediation)
|
|
44
|
+
# - @luminary michael-feathers (characterization pins observed shape)
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Internal helpers (leading underscore per convention)
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
# Resolve the marker directory. Test isolation via HEARTBEAT_MARKER_DIR env.
|
|
51
|
+
_heartbeat_dir() {
|
|
52
|
+
if [ -n "${HEARTBEAT_MARKER_DIR:-}" ]; then
|
|
53
|
+
printf '%s' "$HEARTBEAT_MARKER_DIR"
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
# Default: <repo-root>/state/markers/hook-heartbeat
|
|
57
|
+
local repo_root
|
|
58
|
+
if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
59
|
+
repo_root="$CLAUDE_PROJECT_DIR"
|
|
60
|
+
else
|
|
61
|
+
# Fallback: walk up from this file
|
|
62
|
+
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)"
|
|
63
|
+
fi
|
|
64
|
+
printf '%s' "$repo_root/state/markers/hook-heartbeat"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# ISO 8601 UTC timestamp. macOS + Linux compatible.
|
|
68
|
+
_heartbeat_iso_now() {
|
|
69
|
+
date -u +%Y-%m-%dT%H:%M:%SZ
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Marker mtime (epoch seconds). Portable macOS + Linux.
|
|
73
|
+
# GNU stat first (Linux default; `stat -f` on GNU means filesystem info,
|
|
74
|
+
# NOT modification time — silently returns nonsense with exit 0). BSD
|
|
75
|
+
# stat second (macOS). Fallback echoes 0 (treats as ancient).
|
|
76
|
+
_heartbeat_mtime() {
|
|
77
|
+
local file="$1"
|
|
78
|
+
stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null || echo 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Public interface
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
# heartbeat_mark <hook-name>
|
|
86
|
+
#
|
|
87
|
+
# Records that <hook-name> fired. Writes an ISO 8601 UTC timestamp to
|
|
88
|
+
# state/markers/hook-heartbeat/<hook-name>.marker via a temp-file-and-mv
|
|
89
|
+
# atomic write. Silent-fails on any error so the caller never crashes.
|
|
90
|
+
heartbeat_mark() {
|
|
91
|
+
local hook_name="${1:-}"
|
|
92
|
+
if [ -z "$hook_name" ]; then
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
local dir marker tmp
|
|
97
|
+
dir="$(_heartbeat_dir)"
|
|
98
|
+
marker="$dir/${hook_name}.marker"
|
|
99
|
+
|
|
100
|
+
# Ensure directory exists. Silent-fail on any error.
|
|
101
|
+
mkdir -p "$dir" 2>/dev/null || return 0
|
|
102
|
+
|
|
103
|
+
# Atomic write via temp + mv. mktemp under the same dir so mv is atomic
|
|
104
|
+
# on the same filesystem. Silent-fail if mktemp cannot create the temp.
|
|
105
|
+
tmp="$(mktemp "${dir}/.${hook_name}.marker.XXXXXX" 2>/dev/null)" || return 0
|
|
106
|
+
|
|
107
|
+
# Write timestamp. If any of these fail, clean up + silent-fail.
|
|
108
|
+
if ! _heartbeat_iso_now > "$tmp" 2>/dev/null; then
|
|
109
|
+
rm -f "$tmp" 2>/dev/null
|
|
110
|
+
return 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if ! mv "$tmp" "$marker" 2>/dev/null; then
|
|
114
|
+
rm -f "$tmp" 2>/dev/null
|
|
115
|
+
return 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# heartbeat_check <hook-name> <max-age-seconds>
|
|
122
|
+
#
|
|
123
|
+
# Returns:
|
|
124
|
+
# 0 — marker exists AND is younger than max-age (fresh)
|
|
125
|
+
# 1 — marker exists AND is older than max-age (stale)
|
|
126
|
+
# 2 — marker file does not exist (never fired, or purged)
|
|
127
|
+
heartbeat_check() {
|
|
128
|
+
local hook_name="${1:-}"
|
|
129
|
+
local max_age="${2:-0}"
|
|
130
|
+
if [ -z "$hook_name" ]; then
|
|
131
|
+
return 2
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
local dir marker
|
|
135
|
+
dir="$(_heartbeat_dir)"
|
|
136
|
+
marker="$dir/${hook_name}.marker"
|
|
137
|
+
|
|
138
|
+
if [ ! -f "$marker" ]; then
|
|
139
|
+
return 2
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
local mtime now age
|
|
143
|
+
mtime="$(_heartbeat_mtime "$marker")"
|
|
144
|
+
now="$(date -u +%s)"
|
|
145
|
+
age=$((now - mtime))
|
|
146
|
+
|
|
147
|
+
if [ "$age" -le "$max_age" ]; then
|
|
148
|
+
return 0
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
return 1
|
|
152
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# install-class: dual
|
|
4
|
+
# lib/hook-inject.sh — shared helper for hooks that inject additionalContext
|
|
5
|
+
# per standards/hook-injection-discipline.md and resolve project resources
|
|
6
|
+
# per standards/hook-install-class.md.
|
|
7
|
+
#
|
|
8
|
+
# This library is SOURCED by hooks, not executed. It does not set -e/-u
|
|
9
|
+
# globally (would affect callers); functions enforce their own contracts.
|
|
10
|
+
#
|
|
11
|
+
# Authority: ADR-035 (substrate-as-system + hooks-as-steering-surfaces)
|
|
12
|
+
# Tier 0 strict TDD: lib/tests/hook-inject.test.sh
|
|
13
|
+
# Anchors: @luminary john-ousterhout (deep modules) + @luminary michael-nygard
|
|
14
|
+
# (fail-soft fallback chain) + @luminary tony-hoare (pre/post contracts)
|
|
15
|
+
#
|
|
16
|
+
# Exposed functions:
|
|
17
|
+
# emit_additional_context <event> <content> # JSON envelope to stdout
|
|
18
|
+
# emit_base_pair_reminders # plain-English + assert-verify reminder text
|
|
19
|
+
# resolve_standards_path <filename> # CLAUDE_PROJECT_DIR → env → HOME → fail
|
|
20
|
+
# resolve_config_path # same chain for bassclef-configs.jsonc
|
|
21
|
+
# extract_block_terms <jargon-file> # BLOCK-tier rows from wordlist
|
|
22
|
+
#
|
|
23
|
+
# Env vars consumed:
|
|
24
|
+
# CLAUDE_PROJECT_DIR — project root (Claude Code sets this)
|
|
25
|
+
# KISS_STANDARDS_FILE — override for standards path resolution
|
|
26
|
+
# BASSCLEF_CONFIG_FILE — override for config path resolution
|
|
27
|
+
# HOME_OVERRIDE — test-only; replaces $HOME for fixture isolation
|
|
28
|
+
#
|
|
29
|
+
# Per standards/hook-injection-discipline.md:
|
|
30
|
+
# - additionalContext capped at 10,000 chars (Anthropic limit)
|
|
31
|
+
# - JSON envelope shape: {"hookSpecificOutput": {"hookEventName": "...", "additionalContext": "..."}}
|
|
32
|
+
# - Fail-soft: all resolution helpers return non-zero with structured stderr; never silently pass
|
|
33
|
+
|
|
34
|
+
# === Constants ===
|
|
35
|
+
HOOK_INJECT_MAX_CONTENT=10000
|
|
36
|
+
|
|
37
|
+
# === Resolve $HOME with test-override support ===
|
|
38
|
+
__hook_inject_home() {
|
|
39
|
+
echo "${HOME_OVERRIDE:-$HOME}"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# === emit_additional_context ===
|
|
43
|
+
# Args:
|
|
44
|
+
# $1 — event name (e.g., UserPromptSubmit, Stop)
|
|
45
|
+
# $2 — content (the additionalContext string)
|
|
46
|
+
# Stdout: JSON envelope per Anthropic's hookSpecificOutput shape
|
|
47
|
+
# Exit: 0 success; 2 missing/invalid args
|
|
48
|
+
emit_additional_context() {
|
|
49
|
+
local event="${1:-}"
|
|
50
|
+
local content="${2-}" # NOT :- so explicit empty string is allowed (test case)
|
|
51
|
+
|
|
52
|
+
# Validate event arg
|
|
53
|
+
if [ -z "$event" ]; then
|
|
54
|
+
echo "emit_additional_context: missing event arg" >&2
|
|
55
|
+
echo " Usage: emit_additional_context <event-name> <content>" >&2
|
|
56
|
+
return 2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Reject event names with spaces (Anthropic events are single tokens)
|
|
60
|
+
if echo "$event" | grep -q ' '; then
|
|
61
|
+
echo "emit_additional_context: invalid event name '$event' (spaces not allowed)" >&2
|
|
62
|
+
echo " Anthropic hook events are single tokens like UserPromptSubmit, Stop, PostToolUse" >&2
|
|
63
|
+
return 2
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Validate content arg presence (allow empty string but require the arg)
|
|
67
|
+
if [ $# -lt 2 ]; then
|
|
68
|
+
echo "emit_additional_context: missing content arg" >&2
|
|
69
|
+
echo " Usage: emit_additional_context <event-name> <content>" >&2
|
|
70
|
+
return 2
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Truncate if exceeds Anthropic 10K cap
|
|
74
|
+
local truncated=0
|
|
75
|
+
if [ ${#content} -gt $HOOK_INJECT_MAX_CONTENT ]; then
|
|
76
|
+
content="${content:0:$HOOK_INJECT_MAX_CONTENT}"
|
|
77
|
+
truncated=1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Emit JSON envelope via jq for proper escaping
|
|
81
|
+
if command -v jq >/dev/null 2>&1; then
|
|
82
|
+
jq -n \
|
|
83
|
+
--arg event "$event" \
|
|
84
|
+
--arg content "$content" \
|
|
85
|
+
'{hookSpecificOutput: {hookEventName: $event, additionalContext: $content}}'
|
|
86
|
+
else
|
|
87
|
+
# jq-missing fallback: emit minimally-escaped JSON
|
|
88
|
+
# (lib design assumes jq present per standards/bash-hook-safety.md;
|
|
89
|
+
# this fallback exists so a jq-missing system still produces parseable output)
|
|
90
|
+
local esc_event="${event//\"/\\\"}"
|
|
91
|
+
local esc_content="${content//\"/\\\"}"
|
|
92
|
+
esc_content="${esc_content//$'\n'/\\n}"
|
|
93
|
+
printf '{"hookSpecificOutput":{"hookEventName":"%s","additionalContext":"%s"}}\n' \
|
|
94
|
+
"$esc_event" "$esc_content"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
if [ $truncated -eq 1 ]; then
|
|
98
|
+
echo "emit_additional_context: content truncated to $HOOK_INJECT_MAX_CONTENT chars (Anthropic limit)" >&2
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
return 0
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# === emit_base_pair_reminders ===
|
|
105
|
+
# Emits the shared base-pair reminder text for the 5 UserPromptExpansion
|
|
106
|
+
# hooks per bet 2026-07-01e. Content covers both R8 cures:
|
|
107
|
+
# 1. Plain-English discipline (per .claude/rules/operator-facing-prose.md)
|
|
108
|
+
# 2. Assert-verify discipline (per .claude/rules/assert-only-after-verify.md)
|
|
109
|
+
#
|
|
110
|
+
# Callers prepend this to their skill-specific content, then pass the
|
|
111
|
+
# combined string to emit_additional_context.
|
|
112
|
+
#
|
|
113
|
+
# Stdout: reminder text (no envelope; caller wraps)
|
|
114
|
+
# Exit: 0 always
|
|
115
|
+
emit_base_pair_reminders() {
|
|
116
|
+
cat <<'EOF'
|
|
117
|
+
**Plain check:** Before I make a claim in this turn, I read the source first and cite it. If I cannot cite the source, I do not ship the claim.
|
|
118
|
+
|
|
119
|
+
**Base-pair reminders — plain-English + assert-verify:**
|
|
120
|
+
|
|
121
|
+
Plain-English (per .claude/rules/operator-facing-prose.md):
|
|
122
|
+
- Grade-10 reading level. Subject-verb-object. Short sentences.
|
|
123
|
+
- Drop bassclef-internal jargon (composer, primitive, load-bearing, blast radius, tier-preset, operationalize, wirings). See standards/bassclef-internal-jargon.md for the substitution table.
|
|
124
|
+
- Keep standard tech vocab: API, hook, PR, schema, ORM.
|
|
125
|
+
|
|
126
|
+
Assert-verify (per .claude/rules/assert-only-after-verify.md):
|
|
127
|
+
- Every operator-facing assertion carries three parts: My claim + Evidence + Why the evidence supports the claim.
|
|
128
|
+
- Evidence means the source read inline (file path, ticket #, git SHA).
|
|
129
|
+
- Why the evidence supports the claim means one sentence tying the source to the claim.
|
|
130
|
+
- Optional sharpeners: Qualifier (Toulmin), Rebuttal (Popper), Alternatives (Peirce).
|
|
131
|
+
- If you cannot fill all three, do not ship the assertion. Read the source first.
|
|
132
|
+
- Toulmin's original vocabulary (claim / data / warrant) stays as the research citation in .claude/luminaries/stephen-toulmin.md.
|
|
133
|
+
EOF
|
|
134
|
+
return 0
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# === resolve_standards_path ===
|
|
138
|
+
# Resolves a standards/ file via project → env → user → fail fallback.
|
|
139
|
+
# Args:
|
|
140
|
+
# $1 — relative filename under standards/ (e.g., "bassclef-internal-jargon.md")
|
|
141
|
+
# Stdout: resolved absolute path
|
|
142
|
+
# Exit: 0 success; 1 all layers fail; 2 missing arg
|
|
143
|
+
resolve_standards_path() {
|
|
144
|
+
local filename="${1:-}"
|
|
145
|
+
if [ -z "$filename" ]; then
|
|
146
|
+
echo "resolve_standards_path: missing filename arg" >&2
|
|
147
|
+
return 2
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
local project_dir="${CLAUDE_PROJECT_DIR:-}"
|
|
151
|
+
local env_override="${KISS_STANDARDS_FILE:-}"
|
|
152
|
+
local home_dir
|
|
153
|
+
home_dir="$(__hook_inject_home)"
|
|
154
|
+
|
|
155
|
+
# Layer 1: project
|
|
156
|
+
if [ -n "$project_dir" ] && [ -f "$project_dir/standards/$filename" ]; then
|
|
157
|
+
echo "$project_dir/standards/$filename"
|
|
158
|
+
return 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# Layer 2: env override (only if env file actually exists; otherwise fall through)
|
|
162
|
+
if [ -n "$env_override" ] && [ -f "$env_override" ]; then
|
|
163
|
+
echo "$env_override"
|
|
164
|
+
return 0
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# Layer 3: user-level
|
|
168
|
+
if [ -f "$home_dir/standards/$filename" ]; then
|
|
169
|
+
echo "$home_dir/standards/$filename"
|
|
170
|
+
return 0
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# All layers failed — structured stderr with the fallback chain attempted
|
|
174
|
+
cat >&2 <<EOF
|
|
175
|
+
resolve_standards_path: '$filename' not found in any fallback layer
|
|
176
|
+
Layer 1 (project): ${project_dir:-<CLAUDE_PROJECT_DIR unset>}/standards/$filename
|
|
177
|
+
Layer 2 (env KISS_STANDARDS_FILE): ${env_override:-<unset>}
|
|
178
|
+
Layer 3 (user): $home_dir/standards/$filename
|
|
179
|
+
Per standards/hook-injection-discipline.md fail-soft contract.
|
|
180
|
+
EOF
|
|
181
|
+
return 1
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# === resolve_config_path ===
|
|
185
|
+
# Resolves bassclef-configs.jsonc via project → env → user → fail.
|
|
186
|
+
# Stdout: resolved absolute path
|
|
187
|
+
# Exit: 0 success; 1 all layers fail
|
|
188
|
+
resolve_config_path() {
|
|
189
|
+
local project_dir="${CLAUDE_PROJECT_DIR:-}"
|
|
190
|
+
local env_override="${BASSCLEF_CONFIG_FILE:-}"
|
|
191
|
+
local home_dir
|
|
192
|
+
home_dir="$(__hook_inject_home)"
|
|
193
|
+
|
|
194
|
+
# Layer 1: project
|
|
195
|
+
if [ -n "$project_dir" ] && [ -f "$project_dir/.claude/bassclef-configs.jsonc" ]; then
|
|
196
|
+
echo "$project_dir/.claude/bassclef-configs.jsonc"
|
|
197
|
+
return 0
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
# Layer 2: env override
|
|
201
|
+
if [ -n "$env_override" ] && [ -f "$env_override" ]; then
|
|
202
|
+
echo "$env_override"
|
|
203
|
+
return 0
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
# Layer 3: user-level
|
|
207
|
+
if [ -f "$home_dir/.claude/bassclef-configs.jsonc" ]; then
|
|
208
|
+
echo "$home_dir/.claude/bassclef-configs.jsonc"
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
cat >&2 <<EOF
|
|
213
|
+
resolve_config_path: bassclef-configs.jsonc not found in any fallback layer
|
|
214
|
+
Layer 1 (project): ${project_dir:-<CLAUDE_PROJECT_DIR unset>}/.claude/bassclef-configs.jsonc
|
|
215
|
+
Layer 2 (env BASSCLEF_CONFIG_FILE): ${env_override:-<unset>}
|
|
216
|
+
Layer 3 (user): $home_dir/.claude/bassclef-configs.jsonc
|
|
217
|
+
Per standards/hook-injection-discipline.md fail-soft contract.
|
|
218
|
+
EOF
|
|
219
|
+
return 1
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# === extract_block_terms ===
|
|
223
|
+
# Parses a bassclef-internal-jargon.md style file + returns BLOCK-tier terms.
|
|
224
|
+
# Args:
|
|
225
|
+
# $1 — path to the jargon file
|
|
226
|
+
# Stdout: one term per line (BLOCK rows only)
|
|
227
|
+
# Exit: 0 success (even if zero terms found); 1 file missing; 2 missing arg
|
|
228
|
+
extract_block_terms() {
|
|
229
|
+
local file="${1:-}"
|
|
230
|
+
if [ -z "$file" ]; then
|
|
231
|
+
echo "extract_block_terms: missing file arg" >&2
|
|
232
|
+
return 2
|
|
233
|
+
fi
|
|
234
|
+
if [ ! -f "$file" ]; then
|
|
235
|
+
echo "extract_block_terms: file not found: $file" >&2
|
|
236
|
+
return 1
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
# Parse markdown table rows: extract column 2 (term) where column 3 (tier) == BLOCK
|
|
240
|
+
# Format: | term | TIER | replacement |
|
|
241
|
+
awk -F'|' '
|
|
242
|
+
/BLOCK *\|/ {
|
|
243
|
+
# Get tier column (3rd field after split — col 1 is leading empty)
|
|
244
|
+
tier = $3
|
|
245
|
+
gsub(/^ +| +$/, "", tier)
|
|
246
|
+
if (tier == "BLOCK") {
|
|
247
|
+
term = $2
|
|
248
|
+
gsub(/^ +| +$/, "", term)
|
|
249
|
+
# Skip header rows (term column says "term" literally)
|
|
250
|
+
if (term != "term" && term != "") print term
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
' "$file"
|
|
254
|
+
return 0
|
|
255
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# install-class: dual
|
|
4
|
+
#
|
|
5
|
+
# lib/hook-installer.sh — installs a hook file at its declared install-class scope
|
|
6
|
+
#
|
|
7
|
+
# @pattern patterns/code/gof/strategy.md
|
|
8
|
+
# Picks route (operator | project | dual) by install-class value.
|
|
9
|
+
#
|
|
10
|
+
# @pattern patterns/code/ousterhout/deep-module.md
|
|
11
|
+
# One function, one argument. Targets resolved inside the module.
|
|
12
|
+
#
|
|
13
|
+
# Contract (Hoare):
|
|
14
|
+
# - Precondition: hook_path exists AND is readable AND declares
|
|
15
|
+
# one of operator|project|dual in its # install-class: header
|
|
16
|
+
# - Postcondition (success): file present at every target the class names
|
|
17
|
+
# AND every source-graph dependent resolves AND trace logs the action
|
|
18
|
+
# - Postcondition (failure): non-zero exit AND stderr names the missing target
|
|
19
|
+
# - Silent-fail per ADR-050 R7: permission denied writes trace + exits 0
|
|
20
|
+
#
|
|
21
|
+
# Per docs/canvases/2026-09-15-install-class-dispatch.md
|
|
22
|
+
# Per docs/use-cases/UC-lib-hook-installer.md
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
# Resolve dependent lib scripts relative to this file
|
|
27
|
+
_HOOK_INSTALLER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
|
|
29
|
+
# shellcheck source=/dev/null
|
|
30
|
+
if [ -f "$_HOOK_INSTALLER_DIR/hook-header-parser.sh" ]; then
|
|
31
|
+
source "$_HOOK_INSTALLER_DIR/hook-header-parser.sh"
|
|
32
|
+
fi
|
|
33
|
+
# Source the real walker (replaces the stub defined below if present)
|
|
34
|
+
# shellcheck source=/dev/null
|
|
35
|
+
if [ -f "$_HOOK_INSTALLER_DIR/source-graph-walker.sh" ]; then
|
|
36
|
+
source "$_HOOK_INSTALLER_DIR/source-graph-walker.sh"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Trace log location — honors BASSCLEF_TRACE_LOG env override (used by tests)
|
|
40
|
+
_hook_installer_trace_log() {
|
|
41
|
+
if [ -n "${BASSCLEF_TRACE_LOG:-}" ]; then
|
|
42
|
+
echo "$BASSCLEF_TRACE_LOG"
|
|
43
|
+
elif [ -n "${REPO_ROOT:-}" ]; then
|
|
44
|
+
echo "$REPO_ROOT/state/trace.log"
|
|
45
|
+
else
|
|
46
|
+
echo "/tmp/bassclef-hook-installer-trace.log"
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Trace helper — best-effort append; falls back to stderr on file error
|
|
51
|
+
_hook_installer_trace() {
|
|
52
|
+
local line="$*"
|
|
53
|
+
local log
|
|
54
|
+
log=$(_hook_installer_trace_log)
|
|
55
|
+
{
|
|
56
|
+
mkdir -p "$(dirname "$log")" 2>/dev/null || true
|
|
57
|
+
echo "$(date -u +'%Y-%m-%dT%H:%M:%SZ') $line" >> "$log"
|
|
58
|
+
} 2>/dev/null || echo "TRACE: $line" >&2
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Home target — honors HOME_OVERRIDE env (used by tests)
|
|
62
|
+
_hook_installer_home() {
|
|
63
|
+
if [ -n "${HOME_OVERRIDE:-}" ]; then
|
|
64
|
+
echo "$HOME_OVERRIDE"
|
|
65
|
+
else
|
|
66
|
+
echo "$HOME"
|
|
67
|
+
fi
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Content-hash compare — sha256sum works on both macOS + Linux via shasum
|
|
71
|
+
_hook_installer_content_hash() {
|
|
72
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
73
|
+
sha256sum "$1" | awk '{print $1}'
|
|
74
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
75
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
76
|
+
else
|
|
77
|
+
# Fallback — content diff via cmp
|
|
78
|
+
echo "no-hash-tool"
|
|
79
|
+
fi
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Copy hook_file to target_dir; idempotent on content-hash match
|
|
83
|
+
# Silent-fail on permission denied per ADR-050 R7
|
|
84
|
+
_hook_installer_copy() {
|
|
85
|
+
local hook_file="$1"
|
|
86
|
+
local target_dir="$2"
|
|
87
|
+
local target_file
|
|
88
|
+
target_file="$target_dir/$(basename "$hook_file")"
|
|
89
|
+
|
|
90
|
+
# Create target dir — silent-fail if permission denied
|
|
91
|
+
if ! mkdir -p "$target_dir" 2>/dev/null; then
|
|
92
|
+
_hook_installer_trace "silent-fail permission-denied create $target_dir"
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Check write permission on target dir
|
|
97
|
+
if [ ! -w "$target_dir" ]; then
|
|
98
|
+
_hook_installer_trace "silent-fail permission-denied write $target_dir"
|
|
99
|
+
return 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Idempotency — skip when content-hash matches
|
|
103
|
+
if [ -f "$target_file" ]; then
|
|
104
|
+
local src_hash tgt_hash
|
|
105
|
+
src_hash=$(_hook_installer_content_hash "$hook_file")
|
|
106
|
+
tgt_hash=$(_hook_installer_content_hash "$target_file")
|
|
107
|
+
if [ "$src_hash" = "$tgt_hash" ] && [ "$src_hash" != "no-hash-tool" ]; then
|
|
108
|
+
_hook_installer_trace "no-op idempotent $hook_file -> $target_file"
|
|
109
|
+
return 0
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# Copy — silent-fail on permission denied; -p preserves mode (executable bit)
|
|
114
|
+
if cp -p "$hook_file" "$target_file" 2>/dev/null; then
|
|
115
|
+
_hook_installer_trace "install $hook_file -> $target_file"
|
|
116
|
+
return 0
|
|
117
|
+
else
|
|
118
|
+
_hook_installer_trace "silent-fail permission-denied copy $hook_file -> $target_file"
|
|
119
|
+
return 0
|
|
120
|
+
fi
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# walk_source_dependents comes from lib/source-graph-walker.sh sourced at
|
|
124
|
+
# L34-37. The naive fallback that lived here through v0.41.1 retired at
|
|
125
|
+
# goal 2026-09-14c Step 5.0 per #1663 + ADR-058 c5 — one walker, one
|
|
126
|
+
# implementation. If source-graph-walker.sh is missing (misconfigured
|
|
127
|
+
# install), install_by_class will exit 1 at the walker check line below
|
|
128
|
+
# with the resolved-to-nothing error path — better than a silent fallback
|
|
129
|
+
# that scans differently from the real walker.
|
|
130
|
+
|
|
131
|
+
# install_by_class(hook_path) → 0 on success, non-zero on failure
|
|
132
|
+
install_by_class() {
|
|
133
|
+
local hook_path="$1"
|
|
134
|
+
|
|
135
|
+
# Precondition: hook_path exists
|
|
136
|
+
if [ ! -f "$hook_path" ]; then
|
|
137
|
+
echo "install_by_class: hook_path not found: $hook_path" >&2
|
|
138
|
+
_hook_installer_trace "error hook-not-found $hook_path"
|
|
139
|
+
return 1
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# Parse install-class header
|
|
143
|
+
local install_class
|
|
144
|
+
install_class=$(parse_install_class "$hook_path" 2>/dev/null || true)
|
|
145
|
+
|
|
146
|
+
if [ -z "$install_class" ]; then
|
|
147
|
+
echo "install_by_class: missing or malformed # install-class: header in $hook_path" >&2
|
|
148
|
+
_hook_installer_trace "error missing-header $hook_path"
|
|
149
|
+
return 1
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# Walker check — Step 4 will extract real walker
|
|
153
|
+
local missing_deps
|
|
154
|
+
missing_deps=$(walk_source_dependents "$hook_path" 2>/dev/null || true)
|
|
155
|
+
if [ -n "$missing_deps" ]; then
|
|
156
|
+
echo "install_by_class: missing dependent(s) for $hook_path:" >&2
|
|
157
|
+
echo "$missing_deps" >&2
|
|
158
|
+
_hook_installer_trace "error missing-deps $hook_path"
|
|
159
|
+
return 1
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Resolve targets — install to $HOME and/or $CLAUDE_PROJECT_DIR based on class
|
|
163
|
+
local home_target project_target
|
|
164
|
+
home_target="$(_hook_installer_home)/.claude/hooks"
|
|
165
|
+
|
|
166
|
+
# Project target — CLAUDE_PROJECT_DIR must be set for project + dual
|
|
167
|
+
if [ "$install_class" = "project" ] || [ "$install_class" = "dual" ]; then
|
|
168
|
+
if [ -z "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
169
|
+
echo "install_by_class: CLAUDE_PROJECT_DIR unset — cannot install $install_class hook $hook_path" >&2
|
|
170
|
+
_hook_installer_trace "error CLAUDE_PROJECT_DIR-unset $hook_path"
|
|
171
|
+
return 1
|
|
172
|
+
fi
|
|
173
|
+
project_target="$CLAUDE_PROJECT_DIR/.claude/hooks"
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Dispatch — copy to declared target(s)
|
|
177
|
+
case "$install_class" in
|
|
178
|
+
operator)
|
|
179
|
+
_hook_installer_copy "$hook_path" "$home_target"
|
|
180
|
+
;;
|
|
181
|
+
project)
|
|
182
|
+
_hook_installer_copy "$hook_path" "$project_target"
|
|
183
|
+
;;
|
|
184
|
+
dual)
|
|
185
|
+
_hook_installer_copy "$hook_path" "$home_target"
|
|
186
|
+
_hook_installer_copy "$hook_path" "$project_target"
|
|
187
|
+
;;
|
|
188
|
+
esac
|
|
189
|
+
|
|
190
|
+
return 0
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Export for callers that source this file
|
|
194
|
+
export -f install_by_class 2>/dev/null || true
|
|
195
|
+
export -f walk_source_dependents 2>/dev/null || true
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# andon-allow: docs/operator-private/[a-z-]+/20[0-9][0-9]-[0-9]{2}
|
|
4
|
+
#
|
|
5
|
+
# lib/luminary-pick.sh — bassclef catalog helper for the in-turn luminary picker.
|
|
6
|
+
#
|
|
7
|
+
# Shape A per docs/decompositions/2026-09-03-picker-harness-llm.md.
|
|
8
|
+
# Consult at docs/operator-private/consultations/2026-09-03-picker-shape.md
|
|
9
|
+
# picked Shape A 5-0 (Cooper, Linus, Ousterhout, Nygard, Feathers).
|
|
10
|
+
#
|
|
11
|
+
# The pick itself lives in each caller SKILL body:
|
|
12
|
+
# .claude/skills/pick-luminaries/SKILL.md
|
|
13
|
+
# .claude/skills/launch/SKILL.md
|
|
14
|
+
# .claude/skills/stage/SKILL.md
|
|
15
|
+
#
|
|
16
|
+
# Claude reads the catalog list from luminary_pick_catalog() below,
|
|
17
|
+
# reasons over the intent inside the current turn, writes the JSON.
|
|
18
|
+
# No shell-out. No separate API key required.
|
|
19
|
+
#
|
|
20
|
+
# This lib provides:
|
|
21
|
+
# - luminary_pick_catalog() — read catalog dir; emit sorted slug list
|
|
22
|
+
# - luminary_pick() — grace-window stub preserving signature through
|
|
23
|
+
# 2026-10-31 per ADR-031 we-dont-break-adopters
|
|
24
|
+
# - luminary_pick_status() — mode report for adopter debugging
|
|
25
|
+
#
|
|
26
|
+
# Usage from a caller SKILL body:
|
|
27
|
+
#
|
|
28
|
+
# source "${BASSCLEF_DIR:-$HOME/src/sunj-labs/bassclef}/lib/luminary-pick.sh"
|
|
29
|
+
# CATALOG=$(luminary_pick_catalog)
|
|
30
|
+
# # then Claude reasons over $CATALOG + intent, writes JSON in-turn
|
|
31
|
+
#
|
|
32
|
+
# Output JSON shape (produced by SKILL body prose, not by this lib):
|
|
33
|
+
# {
|
|
34
|
+
# "matched_luminaries": ["slug1", "slug2", ...],
|
|
35
|
+
# "rationale": "One short sentence per pick",
|
|
36
|
+
# "picker_version": "bassclef-harness-pick-2.0",
|
|
37
|
+
# "provider": "claude-code-harness"
|
|
38
|
+
# }
|
|
39
|
+
|
|
40
|
+
set -euo pipefail
|
|
41
|
+
|
|
42
|
+
# Read catalog dir and emit one line per luminary.
|
|
43
|
+
# Format: "<slug> (<domain>) — <name>"
|
|
44
|
+
# Sort: alphabetical by slug.
|
|
45
|
+
# Return: 0 on success (including empty catalog); non-zero if dir missing.
|
|
46
|
+
luminary_pick_catalog() {
|
|
47
|
+
local basedir="${BASSCLEF_DIR:-$HOME/src/sunj-labs/bassclef}"
|
|
48
|
+
local catalog_dir="${basedir}/state/luminary-implementations"
|
|
49
|
+
|
|
50
|
+
if [ ! -d "$catalog_dir" ]; then
|
|
51
|
+
echo "[luminary-pick-catalog] catalog dir missing: $catalog_dir" >&2
|
|
52
|
+
return 2
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
(
|
|
56
|
+
shopt -s nullglob
|
|
57
|
+
set +e
|
|
58
|
+
for f in "$catalog_dir"/*.json; do
|
|
59
|
+
jq -r '"\(.luminary_slug) (\(.primary_domain // "unknown")) — \(.name // .luminary_slug)"' "$f" 2>/dev/null || true
|
|
60
|
+
done | sort -u
|
|
61
|
+
)
|
|
62
|
+
return 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Grace-window stub preserving the old function signature.
|
|
66
|
+
# Adopter scripts that source this lib and call luminary_pick continue
|
|
67
|
+
# to run. They see a stderr note pointing at the new home. Exit 0 so
|
|
68
|
+
# scripts checking exit code do not error.
|
|
69
|
+
# Retires 2026-10-31 per ADR-031.
|
|
70
|
+
luminary_pick() {
|
|
71
|
+
echo "[luminary-pick] moved to caller SKILL body prose per web#193." >&2
|
|
72
|
+
echo "[luminary-pick] adopters: see .claude/skills/pick-luminaries/SKILL.md" >&2
|
|
73
|
+
echo "[luminary-pick] grace-window stub retires 2026-10-31 per ADR-031." >&2
|
|
74
|
+
return 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Report picker mode. Kept for adopter debugging.
|
|
78
|
+
luminary_pick_status() {
|
|
79
|
+
echo "LIVE — in-turn picker per SKILL body prose (Shape A, web#193)"
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
84
|
+
case "${1:-}" in
|
|
85
|
+
catalog) luminary_pick_catalog ;;
|
|
86
|
+
status) luminary_pick_status ;;
|
|
87
|
+
*)
|
|
88
|
+
echo "Usage: $0 catalog # emit luminary catalog list"
|
|
89
|
+
echo " $0 status # report picker mode"
|
|
90
|
+
echo ""
|
|
91
|
+
echo "The pick itself moved to caller SKILL body prose per web#193."
|
|
92
|
+
echo "See .claude/skills/pick-luminaries/SKILL.md."
|
|
93
|
+
exit 0
|
|
94
|
+
;;
|
|
95
|
+
esac
|
|
96
|
+
fi
|