@thebassclef/lite 0.0.1 → 0.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/LICENSE +202 -0
- package/README.md +85 -4
- package/dist/cli.cjs +1610 -0
- package/dist/cli.js +1587 -0
- package/dist/index.cjs +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/package.json +51 -3
- package/substrate/.bassclef/lite-manifest.json +1794 -0
- package/substrate/.claude/agents/architect.md +70 -0
- package/substrate/.claude/agents/builder.md +114 -0
- package/substrate/.claude/agents/designer.md +156 -0
- package/substrate/.claude/agents/reviewer.md +88 -0
- package/substrate/.claude/hooks/artifact-ingestion-gate.sh +357 -0
- package/substrate/.claude/hooks/assert-verify-steering.sh +77 -0
- package/substrate/.claude/hooks/bassclef-source-config-validate.sh +215 -0
- package/substrate/.claude/hooks/bassclef-sync.sh +634 -0
- package/substrate/.claude/hooks/compound-noun-scrub.sh +292 -0
- package/substrate/.claude/hooks/kiss-expansion-inject.sh +69 -0
- package/substrate/.claude/hooks/plain-english-steering.sh +156 -0
- package/substrate/.claude/hooks/post-skill-friction-check.sh +177 -0
- package/substrate/.claude/hooks/post-skill-telemetry.sh +62 -0
- package/substrate/.claude/hooks/pre-build-gate.sh +511 -0
- package/substrate/.claude/hooks/pre-commit-gate.sh +452 -0
- package/substrate/.claude/hooks/session-end.sh +433 -0
- package/substrate/.claude/hooks/session-reflection.sh +303 -0
- package/substrate/.claude/hooks/skill-body-grade-gate.sh +219 -0
- package/substrate/.claude/hooks/skill-body-intent-drift.sh +107 -0
- package/substrate/.claude/hooks/skill-step-list-check.sh +171 -0
- package/substrate/.claude/hooks/state-validate.sh +271 -0
- package/substrate/.claude/hooks/substrate-clarity-gate.sh +1110 -0
- package/substrate/.claude/hooks/temperance-gate.sh +147 -0
- package/substrate/.claude/hooks/testing-tier-enforce.sh +233 -0
- package/substrate/.claude/hooks/turn-prose-grade-measure.sh +219 -0
- package/substrate/.claude/hooks/turn-prose-kiss-check.sh +434 -0
- package/substrate/.claude/hooks/vocabulary-migration-check.sh +171 -0
- package/substrate/.claude/hooks/whereami-utc-gate.sh +142 -0
- package/substrate/.claude/luminaries/alan-cooper.md +170 -0
- package/substrate/.claude/luminaries/alistair-cockburn.md +140 -0
- package/substrate/.claude/luminaries/amazon-pr-faq.md +34 -0
- package/substrate/.claude/luminaries/ash-maurya.md +121 -0
- package/substrate/.claude/luminaries/bill-buxton.md +210 -0
- package/substrate/.claude/luminaries/charles-sanders-peirce.md +150 -0
- package/substrate/.claude/luminaries/david-ogilvy.md +192 -0
- package/substrate/.claude/luminaries/don-norman.md +173 -0
- package/substrate/.claude/luminaries/edward-tufte.md +179 -0
- package/substrate/.claude/luminaries/eric-evans.md +160 -0
- package/substrate/.claude/luminaries/frederick-brooks.md +50 -0
- package/substrate/.claude/luminaries/gang-of-four.md +157 -0
- package/substrate/.claude/luminaries/jason-fried-dhh.md +46 -0
- package/substrate/.claude/luminaries/jesse-james-garrett.md +154 -0
- package/substrate/.claude/luminaries/john-ousterhout.md +94 -0
- package/substrate/.claude/luminaries/karl-popper.md +132 -0
- package/substrate/.claude/luminaries/kent-beck.md +168 -0
- package/substrate/.claude/luminaries/linus-torvalds.md +218 -0
- package/substrate/.claude/luminaries/martin-fowler.md +164 -0
- package/substrate/.claude/luminaries/michael-feathers.md +125 -0
- package/substrate/.claude/luminaries/michael-nygard.md +118 -0
- package/substrate/.claude/luminaries/robert-c-martin.md +164 -0
- package/substrate/.claude/luminaries/saltzer-schroeder.md +194 -0
- package/substrate/.claude/luminaries/sophia-prater.md +193 -0
- package/substrate/.claude/luminaries/stephen-toulmin.md +131 -0
- package/substrate/.claude/rules/artifact-ingestion.md +179 -0
- package/substrate/.claude/rules/assert-only-after-verify.md +137 -0
- package/substrate/.claude/rules/blocked-items.md +146 -0
- package/substrate/.claude/rules/branching.md +28 -0
- package/substrate/.claude/rules/commit-conventions.md +22 -0
- package/substrate/.claude/rules/context-engineering.md +202 -0
- package/substrate/.claude/rules/context-management.md +85 -0
- package/substrate/.claude/rules/destructive-operations.md +69 -0
- package/substrate/.claude/rules/diagnosis.md +38 -0
- package/substrate/.claude/rules/guardrails.md +73 -0
- package/substrate/.claude/rules/identifier-leak-prevention.md +104 -0
- package/substrate/.claude/rules/lite-manifest-schema-change-discipline.md +98 -0
- package/substrate/.claude/rules/oo-ad-entry-point.md +117 -0
- package/substrate/.claude/rules/operator-facing-prose.md +196 -0
- package/substrate/.claude/rules/plain-english-discipline.md +154 -0
- package/substrate/.claude/rules/pr-body-shape.md +317 -0
- package/substrate/.claude/rules/pr-strategy.md +167 -0
- package/substrate/.claude/rules/sdlc-gates.md +149 -0
- package/substrate/.claude/rules/session-artifacts.md +236 -0
- package/substrate/.claude/rules/state-schema-validation.md +162 -0
- package/substrate/.claude/rules/whereami-load-bearing.md +202 -0
- package/substrate/.claude/rules/writing-craft-discipline.md +92 -0
- package/substrate/.claude/skills/build/SKILL.md +640 -0
- package/substrate/.claude/skills/decompose/SKILL.md +280 -0
- package/substrate/.claude/skills/diagnose/SKILL.md +297 -0
- package/substrate/.claude/skills/howdoi/SKILL.md +105 -0
- package/substrate/.claude/skills/ia-model/SKILL.md +108 -0
- package/substrate/.claude/skills/interaction-design/SKILL.md +112 -0
- package/substrate/.claude/skills/interpret-input/SKILL.md +180 -0
- package/substrate/.claude/skills/kiss/SKILL.md +449 -0
- package/substrate/.claude/skills/launch/SKILL.md +924 -0
- package/substrate/.claude/skills/lean-canvas/SKILL.md +332 -0
- package/substrate/.claude/skills/longrun/SKILL.md +426 -0
- package/substrate/.claude/skills/luminary/SKILL.md +481 -0
- package/substrate/.claude/skills/ogilvy-writing-audit/SKILL.md +177 -0
- package/substrate/.claude/skills/onboard-repo/SKILL.md +2190 -0
- package/substrate/.claude/skills/pattern-review/SKILL.md +99 -0
- package/substrate/.claude/skills/personas/SKILL.md +207 -0
- package/substrate/.claude/skills/promote/SKILL.md +283 -0
- package/substrate/.claude/skills/requirement/SKILL.md +98 -0
- package/substrate/.claude/skills/retro/SKILL.md +117 -0
- package/substrate/.claude/skills/riff/SKILL.md +114 -0
- package/substrate/.claude/skills/roadmap-reconcile/SKILL.md +163 -0
- package/substrate/.claude/skills/session-end/SKILL.md +309 -0
- package/substrate/.claude/skills/session-log/SKILL.md +299 -0
- package/substrate/.claude/skills/skills/SKILL.md +228 -0
- package/substrate/.claude/skills/spec/SKILL.md +105 -0
- package/substrate/.claude/skills/sprint/SKILL.md +392 -0
- package/substrate/.claude/skills/stage/SKILL.md +384 -0
- package/substrate/.claude/skills/state-a-problem/SKILL.md +185 -0
- package/substrate/.claude/skills/temperance/SKILL.md +108 -0
- package/substrate/.claude/skills/use-case/SKILL.md +417 -0
- package/substrate/.claude/skills/user-stories/SKILL.md +268 -0
- package/substrate/.claude/skills/value-prop/SKILL.md +251 -0
- package/substrate/.claude/skills/verify/SKILL.md +160 -0
- package/substrate/.claude/skills/whereami/SKILL.md +307 -0
- package/substrate/AGENTS.md +79 -0
- package/substrate/CLAUDE-lite.md +85 -0
- package/substrate/CODE_OF_CONDUCT.md +28 -0
- package/substrate/CONTRIBUTING.md +177 -0
- package/substrate/README.md +173 -0
- package/substrate/SECURITY.md +19 -0
- package/substrate/architecture/decisions/ADR-029-release-pipeline.md +79 -0
- package/substrate/architecture/decisions/ADR-031-non-breaking-changes-adopter-discipline.md +139 -0
- package/substrate/architecture/decisions/ADR-032-adopter-sync-dispatcher-architecture.md +192 -0
- package/substrate/architecture/decisions/ADR-039-release-tagging-scheme.md +145 -0
- package/substrate/architecture/decisions/ADR-040-planning-skill-vocabulary-and-lite-profile.md +155 -0
- package/substrate/architecture/decisions/ADR-044-unified-skill-body-template.md +162 -0
- package/substrate/lib/clean-artifacts-sweep.sh +112 -0
- package/substrate/lib/code-comment-discipline.sh +144 -0
- package/substrate/lib/composer-preflight.sh +459 -0
- package/substrate/lib/hook-inject.sh +255 -0
- package/substrate/lib/luminary-pick.sh +96 -0
- package/substrate/lib/output-discipline.sh +143 -0
- package/substrate/lib/rewrite-check.sh +214 -0
- package/substrate/lib/state.sh +1372 -0
- package/substrate/lib/telemetry.sh +205 -0
- package/substrate/lib/tier-check.sh +138 -0
- package/substrate/lib/tier-dependency-audit.sh +933 -0
- package/substrate/presence/install/bassclef-hook-connect.sh +178 -0
- package/substrate/presence/install/bassclef-sync.dispatcher.template.sh +841 -0
- package/substrate/presence/install/bassclef-sync.template.sh +2076 -0
- package/substrate/presence/install/schedule-auto-save.cron.sh +88 -0
- package/substrate/presence/install/schedule-auto-save.taskscheduler.md +122 -0
- package/substrate/scripts/aggregate-telemetry.sh +217 -0
- package/substrate/scripts/analyze-tier-dependencies.sh +239 -0
- package/substrate/scripts/generate-lite-manifest.sh +489 -0
- package/substrate/scripts/generate-tier-manifest.sh +28 -0
- package/substrate/scripts/intent-drift-check.sh +456 -0
- package/substrate/scripts/lite-manifest-drift-check.sh +146 -0
- package/substrate/scripts/render-lite-manifest-doc.sh +150 -0
- package/substrate/standards/bassclef-configs-schema.md +232 -0
- package/substrate/standards/bassclef-evolution.md +143 -0
- package/substrate/standards/bassclef-internal-jargon.md +244 -0
- package/substrate/standards/bassclef-managed-sentinel.md +96 -0
- package/substrate/standards/bassclef-source-config.md +228 -0
- package/substrate/standards/deferred-actions-schema.md +204 -0
- package/substrate/standards/docs-sync-allowlist.md +76 -0
- package/substrate/standards/graceful-exit.md +227 -0
- package/substrate/standards/hook-idempotency.md +102 -0
- package/substrate/standards/lite-manifest-schema-changes.md +75 -0
- package/substrate/standards/luminary-problem-patterns.md +481 -0
- package/substrate/standards/ogilvy-writing-rules.md +225 -0
- package/substrate/standards/opener-discipline.md +96 -0
- package/substrate/standards/operator-facing-prose-discipline.md +201 -0
- package/substrate/standards/project-directory-layout.md +276 -0
- package/substrate/standards/release-tagging.md +137 -0
- package/substrate/standards/sdlc-compliance.md +286 -0
- package/substrate/standards/secrets-lifecycle.md +210 -0
- package/substrate/standards/security-scanner-adapter.md +145 -0
- package/substrate/standards/skill-output-discipline.md +90 -0
- package/substrate/standards/state-spine.md +511 -0
- package/substrate/standards/tech-stack-config.md +109 -0
- package/substrate/standards/tier-dependency-analysis.md +167 -0
- package/substrate/standards/tier-runtime-deps/lite.md +57 -0
- package/substrate/standards/tier-tag-schema.md +155 -0
- package/substrate/standards/whereami-schema.md +301 -0
- package/substrate/standards/writing-guide.md +213 -0
- package/substrate/templates/deferred-action-template.md +45 -0
- package/substrate/templates/whereami-template.md +101 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
# Session Start hook coordinator. Injects reflection prompt and iterates
|
|
6
|
+
# session-reflection.d/*.sh modules in numbered order. Modules inherit
|
|
7
|
+
# the blocked_banner helper and $BASSCLEF_DIR defined here.
|
|
8
|
+
|
|
9
|
+
# === Hook liveness heartbeat (WU-3 of bet 2026-07-31d; closes #1002) ===
|
|
10
|
+
# Silent-fail — a missing lib never crashes the hook.
|
|
11
|
+
{
|
|
12
|
+
_hb_sd="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
for _hb_c in "${_hb_sd}/../../lib/hook-heartbeat.sh" "${HOME:-/}/lib/hook-heartbeat.sh"; do
|
|
14
|
+
[ -f "$_hb_c" ] && source "$_hb_c" && heartbeat_mark "session-reflection-coordinator" && break
|
|
15
|
+
done
|
|
16
|
+
unset _hb_sd _hb_c
|
|
17
|
+
} 2>/dev/null || true
|
|
18
|
+
|
|
19
|
+
INPUT=$(cat)
|
|
20
|
+
# Fail-soft on malformed JSON — same pattern as bassclef#1101 + #1119 + #1123.
|
|
21
|
+
# Prevents crash when harness sends malformed stdin at SessionStart.
|
|
22
|
+
# Audit ticket: bassclef#1125 (jq+set -e pattern across all hooks).
|
|
23
|
+
SOURCE=$(echo "$INPUT" | jq -r '.source // "unknown"' 2>/dev/null || echo "unknown")
|
|
24
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
|
|
25
|
+
|
|
26
|
+
cd "$CWD"
|
|
27
|
+
|
|
28
|
+
CWD_HASH=$(echo -n "$CWD" | md5 -q 2>/dev/null || echo -n "$CWD" | md5sum 2>/dev/null | cut -c1-8 || echo "default")
|
|
29
|
+
SESSION_TIMING_FILE="/tmp/claude-session-timing-${CWD_HASH}"
|
|
30
|
+
TIMING_NOW=$(date +"%Y-%m-%dT%H:%M:%S%z")
|
|
31
|
+
echo "$TIMING_NOW" > "$SESSION_TIMING_FILE"
|
|
32
|
+
|
|
33
|
+
# Durable dual-write (mobile-ephemeral-session.md §2).
|
|
34
|
+
DURABLE_TIMING_DIR="$CWD/state/markers/session-timing"
|
|
35
|
+
mkdir -p "$DURABLE_TIMING_DIR" 2>/dev/null
|
|
36
|
+
echo "$TIMING_NOW" > "$DURABLE_TIMING_DIR/${CWD_HASH}.timing" 2>/dev/null
|
|
37
|
+
|
|
38
|
+
source "$(dirname "$0")/trace-helper.sh"
|
|
39
|
+
trace_log "session-start" "$SOURCE"
|
|
40
|
+
|
|
41
|
+
# Resolve bassclef dir through symlinks — used by modules to find scripts.
|
|
42
|
+
HOOK_REAL_PATH="$(readlink -f "$0" 2>/dev/null || readlink "$0" 2>/dev/null || echo "$0")"
|
|
43
|
+
BASSCLEF_DIR="$(cd "$(dirname "$HOOK_REAL_PATH")/../.." && pwd)"
|
|
44
|
+
|
|
45
|
+
# High-contrast BLOCKED banner; used by every module that raises a gap.
|
|
46
|
+
blocked_banner() {
|
|
47
|
+
local msg="$1"
|
|
48
|
+
echo ""
|
|
49
|
+
echo "🛑🛑🛑 BLOCKED 🛑🛑🛑"
|
|
50
|
+
echo "────────────────────────────────────────────"
|
|
51
|
+
echo "$msg"
|
|
52
|
+
echo "────────────────────────────────────────────"
|
|
53
|
+
echo "ACTION: resolve OR explicitly defer (per .claude/rules/blocked-items.md)."
|
|
54
|
+
echo "Silence is not deferral. Propose this as item #1 in your session plan."
|
|
55
|
+
echo ""
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
echo "============================================"
|
|
59
|
+
echo "SESSION START — MANDATORY REFLECT → PLAN"
|
|
60
|
+
echo "============================================"
|
|
61
|
+
echo ""
|
|
62
|
+
echo "You MUST run the Session Start protocol before doing ANY other work."
|
|
63
|
+
echo "Do NOT skip this. Do NOT jump to answering the user's question first."
|
|
64
|
+
echo ""
|
|
65
|
+
|
|
66
|
+
# === Last save state ===
|
|
67
|
+
LAST_SAVE_FILE=".claude/LAST_SAVE"
|
|
68
|
+
if [ -f "$LAST_SAVE_FILE" ]; then
|
|
69
|
+
SAVE_TIMESTAMP=$(grep '^timestamp:' "$LAST_SAVE_FILE" | cut -d' ' -f2-)
|
|
70
|
+
SAVE_MACHINE=$(grep '^machine:' "$LAST_SAVE_FILE" | cut -d' ' -f2-)
|
|
71
|
+
SAVE_BRANCH=$(grep '^branch:' "$LAST_SAVE_FILE" | cut -d' ' -f2-)
|
|
72
|
+
SAVE_COMMIT=$(grep '^last_commit:' "$LAST_SAVE_FILE" | cut -d' ' -f2-)
|
|
73
|
+
echo "### LAST SAVE STATE"
|
|
74
|
+
echo ""
|
|
75
|
+
echo "Saved: $SAVE_TIMESTAMP"
|
|
76
|
+
echo "Machine: $SAVE_MACHINE"
|
|
77
|
+
echo "Branch: $SAVE_BRANCH"
|
|
78
|
+
echo "Last commit: $SAVE_COMMIT"
|
|
79
|
+
echo ""
|
|
80
|
+
else
|
|
81
|
+
echo "### NO PREVIOUS SAVE STATE FOUND"
|
|
82
|
+
echo ""
|
|
83
|
+
echo "This may be the first session, or save-state has never run."
|
|
84
|
+
echo "Proceed with caution — check git log for context."
|
|
85
|
+
echo ""
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# === Session lock check (BEFORE writing — idempotency rule) ===
|
|
89
|
+
LOCK_FILE=".claude/SESSION_LOCK"
|
|
90
|
+
if [ -f "$LOCK_FILE" ]; then
|
|
91
|
+
# Platform-portable mtime: GNU `stat -c %Y` first, BSD `stat -f %m` fallback.
|
|
92
|
+
# Order matters — GNU `stat -f` exits 0 with WRONG output (mount-point info
|
|
93
|
+
# not mtime), so the `||` chain only triggers on BSD when -c fails. Caught
|
|
94
|
+
# by WU-8 test of /longrun-c on Linux CI (7th TDD-find of the day; first
|
|
95
|
+
# attempt at the fix had the order reversed and still failed).
|
|
96
|
+
LOCK_MTIME=$(stat -c %Y "$LOCK_FILE" 2>/dev/null || stat -f %m "$LOCK_FILE" 2>/dev/null || echo "0")
|
|
97
|
+
LOCK_AGE=$(( $(date +%s) - LOCK_MTIME ))
|
|
98
|
+
if [ "$LOCK_AGE" -lt 7200 ]; then
|
|
99
|
+
echo "### ANOTHER SESSION MAY BE ACTIVE"
|
|
100
|
+
echo ""
|
|
101
|
+
echo "SESSION_LOCK exists and is $(( LOCK_AGE / 60 )) minutes old."
|
|
102
|
+
echo "Another agent or session may be working in this repo."
|
|
103
|
+
echo ""
|
|
104
|
+
echo "→ Ask the user before proceeding. Do NOT overwrite another session's work."
|
|
105
|
+
echo ""
|
|
106
|
+
else
|
|
107
|
+
echo "### STALE SESSION LOCK ($(( LOCK_AGE / 3600 )) hours old)"
|
|
108
|
+
echo ""
|
|
109
|
+
echo "Previous session likely crashed. Proceeding with caution."
|
|
110
|
+
echo ""
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# === Write session lock (AFTER checking — per standards/hook-idempotency.md) ===
|
|
115
|
+
MACHINE=$(hostname -s 2>/dev/null || echo "unknown")
|
|
116
|
+
echo "machine: $MACHINE" > "$LOCK_FILE"
|
|
117
|
+
echo "timestamp: $(date +%Y-%m-%dT%H:%M:%S%z)" >> "$LOCK_FILE"
|
|
118
|
+
echo "pid: $$" >> "$LOCK_FILE"
|
|
119
|
+
|
|
120
|
+
# === Fetch origin before staleness checks (bassclef-upstream#403) ===
|
|
121
|
+
# Several session-reflection.d/ fragments read git log / file listings
|
|
122
|
+
# to compute staleness banners (chronicle, danger-mode summary,
|
|
123
|
+
# architect-review, release-notes, recent-strategic-artifacts,
|
|
124
|
+
# workflow-staleness, metrics-staleness). Without fetching origin
|
|
125
|
+
# first, those reads see this machine's last-pull state — not the
|
|
126
|
+
# shared truth — and may show wrong-now banners when another session
|
|
127
|
+
# has pushed since (e.g., 2026-06-26c saw stale local while origin
|
|
128
|
+
# had today's session-end commit from #401).
|
|
129
|
+
#
|
|
130
|
+
# Best-effort: || true keeps the dispatcher working when offline / no
|
|
131
|
+
# remote / detached HEAD. Each fragment continues to use local git
|
|
132
|
+
# state; we just ensure local has had a chance to sync first.
|
|
133
|
+
CURRENT_BRANCH_FOR_FETCH=$(git symbolic-ref --short HEAD 2>/dev/null)
|
|
134
|
+
if [ -n "$CURRENT_BRANCH_FOR_FETCH" ]; then
|
|
135
|
+
git fetch origin "$CURRENT_BRANCH_FOR_FETCH" --quiet 2>/dev/null || true
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# === Iterate modules in numbered order ===
|
|
139
|
+
# Use HOOK_REAL_PATH (line 15, symlinks resolved) so fragments load when
|
|
140
|
+
# the dispatcher is invoked via a symlink from an adopter checkout.
|
|
141
|
+
# Per bassclef-upstream#591 — link-relative path silently skipped all
|
|
142
|
+
# fragments on adopter machines, breaking every session-start gate.
|
|
143
|
+
MODULES_DIR="$(dirname "$HOOK_REAL_PATH")/session-reflection.d"
|
|
144
|
+
if [ -d "$MODULES_DIR" ]; then
|
|
145
|
+
# Modules use the pattern `return 0 2>/dev/null || exit 0` (safe when
|
|
146
|
+
# sourced OR run standalone). A module returning 2 signals a fatal
|
|
147
|
+
# condition the coordinator MUST honor (e.g., invalid substrate config
|
|
148
|
+
# per 15-substrate-config-validate.sh L86). Per Feathers R3.2 semantic
|
|
149
|
+
# preservation — cure b (2026-07-30) restored return semantics while
|
|
150
|
+
# keeping the abort contract via return code 2 + this catch.
|
|
151
|
+
#
|
|
152
|
+
# Disable `set -e` briefly so the coordinator sees each module's
|
|
153
|
+
# return code cleanly. Without this, `return 1` from a sourced module
|
|
154
|
+
# would abort the coordinator via set -e regardless of code.
|
|
155
|
+
set +e
|
|
156
|
+
for module in "$MODULES_DIR"/*.sh; do
|
|
157
|
+
[ -f "$module" ] || continue
|
|
158
|
+
source "$module"
|
|
159
|
+
MODULE_RC=$?
|
|
160
|
+
# Modules may set their own shell options (e.g., 90-release-backlog
|
|
161
|
+
# uses `set -euo pipefail`). Those persist to the coordinator via
|
|
162
|
+
# sourcing. Reset to a permissive default after each module so one
|
|
163
|
+
# module's discipline does not abort the coordinator's remaining
|
|
164
|
+
# body. Cure b (2026-07-30) — Nygard bulkhead + Feathers R3.5.
|
|
165
|
+
set +eu
|
|
166
|
+
if [ "$MODULE_RC" -eq 2 ]; then
|
|
167
|
+
# Module signaled fatal abort. Preserve pre-cure behavior — exit
|
|
168
|
+
# with non-zero code the harness treats as coordinator failure.
|
|
169
|
+
echo "" >&2
|
|
170
|
+
echo "!! session-reflection: aborting after $(basename "$module") returned 2" >&2
|
|
171
|
+
exit 1
|
|
172
|
+
fi
|
|
173
|
+
done
|
|
174
|
+
# Deliberately DO NOT re-enable set -e. Cure b (2026-07-30) discovered
|
|
175
|
+
# the coordinator's ACTIVE CONTEXT block at L162+ uses grep patterns
|
|
176
|
+
# that return 1 when whereami has no in_flight_bet field. Under set -e
|
|
177
|
+
# those aborted the coordinator BEFORE reaching Step 1: Read memory
|
|
178
|
+
# (L200+). Pre-cure the abort was masked because 05-active-bet.sh
|
|
179
|
+
# exited earlier. Post-cure the abort surfaces. Fix: coordinator runs
|
|
180
|
+
# without set -e for the rest of its body — matches modules' tolerance
|
|
181
|
+
# for non-zero returns from optional grep/find calls.
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# === Context loading — proactively surface active work ===
|
|
185
|
+
echo "### ACTIVE CONTEXT (read these before planning)"
|
|
186
|
+
echo ""
|
|
187
|
+
|
|
188
|
+
if [ -f "docs/whereami.md" ]; then
|
|
189
|
+
ACTIVE_BET=$(grep 'iteration_bet:' docs/whereami.md 2>/dev/null | head -1 | sed 's/iteration_bet: *//')
|
|
190
|
+
if [ -n "$ACTIVE_BET" ] && [ -f "$ACTIVE_BET" ]; then
|
|
191
|
+
echo "**Active iteration goal**: $ACTIVE_BET"
|
|
192
|
+
head -20 "$ACTIVE_BET" 2>/dev/null
|
|
193
|
+
echo ""
|
|
194
|
+
fi
|
|
195
|
+
ACTIVE_PHASE=$(grep 'iteration_phase:' docs/whereami.md 2>/dev/null | head -1 | sed 's/iteration_phase: *//')
|
|
196
|
+
echo "**Phase**: ${ACTIVE_PHASE:-unknown}"
|
|
197
|
+
echo ""
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
for CDIR in "chronicle" "docs/chronicle"; do
|
|
201
|
+
if [ -d "$CDIR" ]; then
|
|
202
|
+
LATEST_CHRONICLE=$(ls -1 "$CDIR"/*.md 2>/dev/null | sort | tail -1)
|
|
203
|
+
if [ -n "$LATEST_CHRONICLE" ]; then
|
|
204
|
+
echo "**Last chronicle**: $(basename "$LATEST_CHRONICLE")"
|
|
205
|
+
OPEN_THREADS=$(sed -n '/## Open threads/,/## /p' "$LATEST_CHRONICLE" 2>/dev/null | head -15)
|
|
206
|
+
if [ -n "$OPEN_THREADS" ]; then
|
|
207
|
+
echo "$OPEN_THREADS"
|
|
208
|
+
fi
|
|
209
|
+
echo ""
|
|
210
|
+
fi
|
|
211
|
+
break
|
|
212
|
+
fi
|
|
213
|
+
done
|
|
214
|
+
|
|
215
|
+
if [ -f "substrate.config.md" ]; then
|
|
216
|
+
LEVEL=$(grep '^level:' substrate.config.md 2>/dev/null | head -1 | sed 's/level: *//')
|
|
217
|
+
echo "**Process level**: ${LEVEL:-core}"
|
|
218
|
+
echo ""
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
echo ""
|
|
222
|
+
|
|
223
|
+
# === Standard reflection steps ===
|
|
224
|
+
echo "### Step 1: Read memory"
|
|
225
|
+
echo "Read project memory from .claude/ directory"
|
|
226
|
+
echo ""
|
|
227
|
+
echo "### Step 2: Recent commits"
|
|
228
|
+
git log --oneline -5 2>/dev/null || echo "(no git history)"
|
|
229
|
+
echo ""
|
|
230
|
+
echo "### Step 3: Open issues"
|
|
231
|
+
REPO_NAME=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null || echo "")
|
|
232
|
+
if [ -n "$REPO_NAME" ]; then
|
|
233
|
+
gh issue list -R "$REPO_NAME" --limit 10 2>/dev/null || echo "(issues unavailable)"
|
|
234
|
+
else
|
|
235
|
+
echo "(gh repo not detected — check open issues manually)"
|
|
236
|
+
fi
|
|
237
|
+
echo ""
|
|
238
|
+
echo "### Step 4: Summarize + propose 1-3 items, then WAIT for human confirmation."
|
|
239
|
+
echo ""
|
|
240
|
+
echo "### MANDATORY RESPONSE FORMAT (user-visible)"
|
|
241
|
+
echo ""
|
|
242
|
+
echo "Claude Code hook stdout lands in MODEL context only — the operator"
|
|
243
|
+
echo "never sees the 🛑 banners above. The only user-visible surface is"
|
|
244
|
+
echo "what YOU write in your first message. So:"
|
|
245
|
+
echo ""
|
|
246
|
+
echo "Your FIRST user-facing message this session MUST begin with a"
|
|
247
|
+
echo "verbatim echo of every 🛑 BLOCKED block surfaced above — same"
|
|
248
|
+
echo "emoji, same horizontal rule, same message text — inside a fenced"
|
|
249
|
+
echo "code block so the operator sees exactly what the hook emitted,"
|
|
250
|
+
echo "not your paraphrase."
|
|
251
|
+
echo ""
|
|
252
|
+
echo "THEN, immediately below the echoed blocks, add your disposition:"
|
|
253
|
+
echo ""
|
|
254
|
+
echo " BLOCKED ITEMS — my disposition:"
|
|
255
|
+
echo " - [name]: RESOLVE NOW / RESOLVE THIS SESSION / DEFER (reason)"
|
|
256
|
+
echo ""
|
|
257
|
+
echo "If there are zero blocked items, state: 'No blocked items surfaced"
|
|
258
|
+
echo "by session-start hook.'"
|
|
259
|
+
echo ""
|
|
260
|
+
echo "Only AFTER the verbatim echo + disposition may you address the"
|
|
261
|
+
echo "user's question. This overrides the pull of any specific user"
|
|
262
|
+
echo "question — the user's prior instructions live in these rules."
|
|
263
|
+
echo ""
|
|
264
|
+
|
|
265
|
+
# === Manifest-aware session planning ===
|
|
266
|
+
if [ -f "substrate.config.md" ]; then
|
|
267
|
+
LEVEL=$(grep '^level:' substrate.config.md 2>/dev/null | head -1 | sed 's/level: *//')
|
|
268
|
+
CEILING=$(grep 'iteration_ceiling:' substrate.config.md 2>/dev/null | head -1 | sed 's/.*iteration_ceiling: *//')
|
|
269
|
+
|
|
270
|
+
ACTIVE_BET=""
|
|
271
|
+
if [ -f "docs/whereami.md" ]; then
|
|
272
|
+
ACTIVE_BET=$(grep 'iteration_bet:' docs/whereami.md 2>/dev/null | head -1 | sed 's/iteration_bet: *//')
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
if [ -n "$ACTIVE_BET" ] && [ -f "$ACTIVE_BET" ]; then
|
|
276
|
+
echo "### Active iteration — continue or start new?"
|
|
277
|
+
echo ""
|
|
278
|
+
echo "An iteration goal exists: $ACTIVE_BET"
|
|
279
|
+
echo "Check if it's complete or still has open scope."
|
|
280
|
+
echo "If complete → propose a new iteration from open issues."
|
|
281
|
+
echo "If in progress → resume where it left off."
|
|
282
|
+
else
|
|
283
|
+
echo "### Propose iteration (manifest-aware)"
|
|
284
|
+
echo ""
|
|
285
|
+
echo "No active iteration goal. Based on open issues above,"
|
|
286
|
+
echo "propose a light iteration:"
|
|
287
|
+
echo ""
|
|
288
|
+
echo '```'
|
|
289
|
+
echo "## This session (light iteration)"
|
|
290
|
+
echo "Retiring: [highest-risk open issue]"
|
|
291
|
+
echo "Proving: [most valuable open issue]"
|
|
292
|
+
echo "Scope: [2-4 issues that fit together]"
|
|
293
|
+
echo "Cost ceiling: ${CEILING:-\$0}"
|
|
294
|
+
echo '```'
|
|
295
|
+
echo ""
|
|
296
|
+
echo "Read the open issues and propose this. User confirms or adjusts."
|
|
297
|
+
fi
|
|
298
|
+
echo ""
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
echo "============================================"
|
|
302
|
+
|
|
303
|
+
exit 0
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# install-class: dual
|
|
4
|
+
# skill-body-grade-gate.sh — PreToolUse Edit|Write gate on
|
|
5
|
+
# .claude/skills/*/SKILL.md. Measures per-sentence Flesch-Kincaid grade.
|
|
6
|
+
# Honors FKGL-EXEMPT annotations. BLOCKs when the 80%-at-grade-8-9 rule
|
|
7
|
+
# fails per ADR-040 Decision 4.
|
|
8
|
+
#
|
|
9
|
+
# Sister to turn-prose-grade-measure.sh (Stop event; turn prose;
|
|
10
|
+
# measurement only, no gate). This hook fires on file writes and blocks.
|
|
11
|
+
#
|
|
12
|
+
# Input (stdin): JSON with tool_name + tool_input.file_path + content/new_string
|
|
13
|
+
# Exit: 0 pass, 2 block
|
|
14
|
+
#
|
|
15
|
+
# Override per-call: SKIP_SKILL_BODY_GRADE=1
|
|
16
|
+
#
|
|
17
|
+
# Per @luminary kent-beck (Tier 0 strict TDD)
|
|
18
|
+
# + @luminary donald-norman (plain-English target per ADR-040 D4)
|
|
19
|
+
# + @luminary michael-feathers (characterization tests pin real inputs)
|
|
20
|
+
|
|
21
|
+
set +e
|
|
22
|
+
|
|
23
|
+
# === Override path ===
|
|
24
|
+
if [ "${SKIP_SKILL_BODY_GRADE:-0}" = "1" ]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# === Parse stdin JSON ===
|
|
29
|
+
STDIN_JSON=$(cat 2>/dev/null || echo '{}')
|
|
30
|
+
|
|
31
|
+
# Bail on unparseable JSON
|
|
32
|
+
if ! echo "$STDIN_JSON" | jq -e . >/dev/null 2>&1; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
TOOL_NAME=$(echo "$STDIN_JSON" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
37
|
+
FILE_PATH=$(echo "$STDIN_JSON" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
38
|
+
|
|
39
|
+
# Only fire on Write or Edit
|
|
40
|
+
if [ "$TOOL_NAME" != "Write" ] && [ "$TOOL_NAME" != "Edit" ]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Only fire on .claude/skills/*/SKILL.md paths
|
|
45
|
+
case "$FILE_PATH" in
|
|
46
|
+
*".claude/skills/"*"/SKILL.md")
|
|
47
|
+
;;
|
|
48
|
+
*)
|
|
49
|
+
exit 0
|
|
50
|
+
;;
|
|
51
|
+
esac
|
|
52
|
+
|
|
53
|
+
# === Extract text to grade ===
|
|
54
|
+
if [ "$TOOL_NAME" = "Write" ]; then
|
|
55
|
+
CONTENT=$(echo "$STDIN_JSON" | jq -r '.tool_input.content // ""' 2>/dev/null)
|
|
56
|
+
else
|
|
57
|
+
# Edit: grade the new_string (text being added)
|
|
58
|
+
CONTENT=$(echo "$STDIN_JSON" | jq -r '.tool_input.new_string // ""' 2>/dev/null)
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
[ -z "$CONTENT" ] && exit 0
|
|
62
|
+
|
|
63
|
+
# === Run the gate via python ===
|
|
64
|
+
# Python does the scrubbing + per-sentence FKGL + threshold check.
|
|
65
|
+
# Same textstat-preferred / fallback pattern as turn-prose-grade-measure.sh.
|
|
66
|
+
GATE_OUTPUT=$(printf '%s' "$CONTENT" | python3 -c '
|
|
67
|
+
import sys, re
|
|
68
|
+
|
|
69
|
+
text = sys.stdin.read()
|
|
70
|
+
|
|
71
|
+
# === Strip YAML frontmatter ===
|
|
72
|
+
# Matches leading "---\n...\n---\n" block
|
|
73
|
+
text = re.sub(r"^---\n.*?\n---\n", "", text, count=1, flags=re.DOTALL)
|
|
74
|
+
|
|
75
|
+
# === Strip FKGL-EXEMPT blocks ===
|
|
76
|
+
# Matches <!-- FKGL-EXEMPT: any-text --> ... <!-- /FKGL-EXEMPT -->
|
|
77
|
+
text = re.sub(r"<!--\s*FKGL-EXEMPT:.*?-->.*?<!--\s*/FKGL-EXEMPT\s*-->",
|
|
78
|
+
"", text, flags=re.DOTALL)
|
|
79
|
+
|
|
80
|
+
# === Strip code blocks ===
|
|
81
|
+
text = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
|
|
82
|
+
|
|
83
|
+
# === Strip inline backticks ===
|
|
84
|
+
text = re.sub(r"`[^`]*`", "", text)
|
|
85
|
+
|
|
86
|
+
# === Strip URLs ===
|
|
87
|
+
text = re.sub(r"https?://[^\s)]+", "", text)
|
|
88
|
+
|
|
89
|
+
# === Strip markdown table pipes and headers ===
|
|
90
|
+
text = re.sub(r"^\s*\|.*\|\s*$", "", text, flags=re.MULTILINE)
|
|
91
|
+
text = re.sub(r"^\s*#+\s+.*$", "", text, flags=re.MULTILINE)
|
|
92
|
+
|
|
93
|
+
# === Strip HTML comments ===
|
|
94
|
+
text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
|
|
95
|
+
|
|
96
|
+
# === Split into sentences ===
|
|
97
|
+
# Also split on bullet markers + numbered lists so a bullet block is not
|
|
98
|
+
# treated as one long "sentence" (which inflates FKGL unfairly).
|
|
99
|
+
sentences = [s.strip() for s in re.split(r"[.!?\n]+\s*(?:[-*]\s|\d+\.\s)?", text) if s.strip()]
|
|
100
|
+
# Second pass — plain sentence-terminator split (some markdown lines lack terminators)
|
|
101
|
+
sentences = [ss.strip() for s in sentences for ss in re.split(r"[.!?]+", s) if ss.strip()]
|
|
102
|
+
|
|
103
|
+
# === Total word count check ===
|
|
104
|
+
all_words = re.findall(r"\b[a-zA-Z][a-zA-Z\x27]*\b", text)
|
|
105
|
+
if len(all_words) < 20:
|
|
106
|
+
print("SKIP short")
|
|
107
|
+
sys.exit(0)
|
|
108
|
+
|
|
109
|
+
# === Preferred: textstat per-sentence FKGL ===
|
|
110
|
+
try:
|
|
111
|
+
from textstat import flesch_kincaid_grade
|
|
112
|
+
grade_fn = flesch_kincaid_grade
|
|
113
|
+
except ImportError:
|
|
114
|
+
# Fallback formula — same as turn-prose-grade-measure.sh
|
|
115
|
+
def count_syllables(word):
|
|
116
|
+
word = re.sub(r"[^a-z]", "", word.lower())
|
|
117
|
+
if not word:
|
|
118
|
+
return 0
|
|
119
|
+
vowels = "aeiouy"
|
|
120
|
+
count = 0
|
|
121
|
+
prev_was_vowel = False
|
|
122
|
+
for c in word:
|
|
123
|
+
is_vowel = c in vowels
|
|
124
|
+
if is_vowel and not prev_was_vowel:
|
|
125
|
+
count += 1
|
|
126
|
+
prev_was_vowel = is_vowel
|
|
127
|
+
if word.endswith("e") and count > 1:
|
|
128
|
+
count -= 1
|
|
129
|
+
return max(1, count)
|
|
130
|
+
|
|
131
|
+
def grade_fn(s):
|
|
132
|
+
words = re.findall(r"\b[a-zA-Z][a-zA-Z\x27]*\b", s)
|
|
133
|
+
if not words:
|
|
134
|
+
return 0
|
|
135
|
+
syllables = sum(count_syllables(w) for w in words)
|
|
136
|
+
return 0.39 * len(words) + 11.8 * (syllables / len(words)) - 15.59
|
|
137
|
+
|
|
138
|
+
# === Grade each sentence ===
|
|
139
|
+
above_ceiling = 0
|
|
140
|
+
graded_count = 0
|
|
141
|
+
failing_examples = []
|
|
142
|
+
|
|
143
|
+
for sent in sentences:
|
|
144
|
+
# Skip short sentences (< 5 words — likely list items, fragments)
|
|
145
|
+
words = re.findall(r"\b[a-zA-Z][a-zA-Z\x27]*\b", sent)
|
|
146
|
+
if len(words) < 5:
|
|
147
|
+
continue
|
|
148
|
+
graded_count += 1
|
|
149
|
+
grade = grade_fn(sent)
|
|
150
|
+
if grade > 10:
|
|
151
|
+
above_ceiling += 1
|
|
152
|
+
if len(failing_examples) < 3:
|
|
153
|
+
failing_examples.append((round(grade, 1), sent[:80]))
|
|
154
|
+
|
|
155
|
+
if graded_count == 0:
|
|
156
|
+
print("SKIP no-graded")
|
|
157
|
+
sys.exit(0)
|
|
158
|
+
|
|
159
|
+
fail_pct = (above_ceiling / graded_count) * 100
|
|
160
|
+
|
|
161
|
+
# === 80% at grade 8-9 rule: at most 20% above grade 10 ===
|
|
162
|
+
if fail_pct > 20:
|
|
163
|
+
print(f"BLOCK {above_ceiling} {graded_count} {fail_pct:.0f}")
|
|
164
|
+
for grade, snippet in failing_examples:
|
|
165
|
+
print(f" EXAMPLE {grade} {snippet}")
|
|
166
|
+
sys.exit(0)
|
|
167
|
+
|
|
168
|
+
print(f"PASS {above_ceiling} {graded_count} {fail_pct:.0f}")
|
|
169
|
+
' 2>/dev/null)
|
|
170
|
+
|
|
171
|
+
# === Parse gate result ===
|
|
172
|
+
FIRST_LINE=$(echo "$GATE_OUTPUT" | head -1)
|
|
173
|
+
VERDICT=$(echo "$FIRST_LINE" | awk '{print $1}')
|
|
174
|
+
|
|
175
|
+
case "$VERDICT" in
|
|
176
|
+
PASS|SKIP)
|
|
177
|
+
exit 0
|
|
178
|
+
;;
|
|
179
|
+
BLOCK)
|
|
180
|
+
ABOVE=$(echo "$FIRST_LINE" | awk '{print $2}')
|
|
181
|
+
TOTAL=$(echo "$FIRST_LINE" | awk '{print $3}')
|
|
182
|
+
PCT=$(echo "$FIRST_LINE" | awk '{print $4}')
|
|
183
|
+
;;
|
|
184
|
+
*)
|
|
185
|
+
# Empty or unexpected — pass through safely
|
|
186
|
+
exit 0
|
|
187
|
+
;;
|
|
188
|
+
esac
|
|
189
|
+
|
|
190
|
+
# === Emit BLOCK message ===
|
|
191
|
+
echo "" >&2
|
|
192
|
+
echo "🛑 SKILL-BODY-GRADE-GATE — BLOCKED" >&2
|
|
193
|
+
echo "" >&2
|
|
194
|
+
echo " File: $FILE_PATH" >&2
|
|
195
|
+
echo " Result: $ABOVE of $TOTAL graded sentences above grade 10 (${PCT}%)" >&2
|
|
196
|
+
echo " Target: 80% at grade 8-9. At most 20% above grade 10." >&2
|
|
197
|
+
echo " Per: ADR-040 Decision 4 (grade 8-9 for 80% of prose)" >&2
|
|
198
|
+
echo "" >&2
|
|
199
|
+
echo " Failing sentence samples (grade | first 80 chars):" >&2
|
|
200
|
+
echo "$GATE_OUTPUT" | grep '^ EXAMPLE' | while read -r _ grade snippet; do
|
|
201
|
+
echo " [$grade] $snippet" >&2
|
|
202
|
+
done
|
|
203
|
+
echo "" >&2
|
|
204
|
+
echo " Cure paths:" >&2
|
|
205
|
+
echo " 1. Rewrite failing sentences in short SVO English" >&2
|
|
206
|
+
echo " 2. Mark sentence exempt: <!-- FKGL-EXEMPT: reason --> ... <!-- /FKGL-EXEMPT -->" >&2
|
|
207
|
+
echo "" >&2
|
|
208
|
+
echo " Override (logged via trace-helper): SKIP_SKILL_BODY_GRADE=1" >&2
|
|
209
|
+
echo "" >&2
|
|
210
|
+
|
|
211
|
+
# === Trace log ===
|
|
212
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
213
|
+
if [ -f "$SCRIPT_DIR/trace-helper.sh" ]; then
|
|
214
|
+
# shellcheck disable=SC1091
|
|
215
|
+
source "$SCRIPT_DIR/trace-helper.sh"
|
|
216
|
+
trace_log "skill-body-grade-gate" "BLOCKED above=$ABOVE total=$TOTAL pct=$PCT file=$FILE_PATH"
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
exit 2
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# install-class: dual
|
|
4
|
+
# skill-body-intent-drift.sh — PostToolUse Edit|Write hook on
|
|
5
|
+
# .claude/skills/*/SKILL.md. Runs scripts/intent-drift-check.sh in
|
|
6
|
+
# advisory mode. Reports cosine + verdict to stderr. Never blocks.
|
|
7
|
+
#
|
|
8
|
+
# Closes bassclef-upstream#788 (narrow slice of #785). Makes /loop
|
|
9
|
+
# step 5 automatic — no more honor-system per cure.
|
|
10
|
+
#
|
|
11
|
+
# Input (stdin): JSON — tool_name, tool_input.file_path
|
|
12
|
+
# Exit: always 0 (advisory only at V1)
|
|
13
|
+
#
|
|
14
|
+
# Env:
|
|
15
|
+
# VOYAGE_API_KEY — required to run the check; missing = skip silently
|
|
16
|
+
# SKIP_SKILL_BODY_INTENT_DRIFT=1 — per-call override (logged)
|
|
17
|
+
#
|
|
18
|
+
# Per @luminary michael-feathers (characterization measurable not subjective)
|
|
19
|
+
# + @luminary kent-beck (Tier 0 strict TDD)
|
|
20
|
+
|
|
21
|
+
set +e
|
|
22
|
+
|
|
23
|
+
# === Override path ===
|
|
24
|
+
if [ "${SKIP_SKILL_BODY_INTENT_DRIFT:-0}" = "1" ]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# === Parse stdin JSON ===
|
|
29
|
+
STDIN_JSON=$(cat 2>/dev/null || echo '{}')
|
|
30
|
+
|
|
31
|
+
if ! echo "$STDIN_JSON" | jq -e . >/dev/null 2>&1; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
TOOL_NAME=$(echo "$STDIN_JSON" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
36
|
+
FILE_PATH=$(echo "$STDIN_JSON" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
37
|
+
|
|
38
|
+
# Only fire on Write or Edit
|
|
39
|
+
if [ "$TOOL_NAME" != "Write" ] && [ "$TOOL_NAME" != "Edit" ]; then
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Only fire on .claude/skills/*/SKILL.md
|
|
44
|
+
case "$FILE_PATH" in
|
|
45
|
+
*".claude/skills/"*"/SKILL.md")
|
|
46
|
+
;;
|
|
47
|
+
*)
|
|
48
|
+
exit 0
|
|
49
|
+
;;
|
|
50
|
+
esac
|
|
51
|
+
|
|
52
|
+
# Extract slug from path
|
|
53
|
+
SLUG=$(echo "$FILE_PATH" | sed -E 's|.*\.claude/skills/([^/]+)/SKILL\.md|\1|')
|
|
54
|
+
if [ -z "$SLUG" ] || [ "$SLUG" = "$FILE_PATH" ]; then
|
|
55
|
+
exit 0
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Locate repo root
|
|
59
|
+
if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "$CLAUDE_PROJECT_DIR/.claude" ]; then
|
|
60
|
+
REPO_ROOT="$CLAUDE_PROJECT_DIR"
|
|
61
|
+
else
|
|
62
|
+
REPO_ROOT="$(pwd)"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
VECTOR_PATH="$REPO_ROOT/.claude/embeddings/skills/${SLUG}.json"
|
|
66
|
+
DRIFT_SCRIPT="$REPO_ROOT/scripts/intent-drift-check.sh"
|
|
67
|
+
|
|
68
|
+
# Skip when baseline vector absent
|
|
69
|
+
if [ ! -f "$VECTOR_PATH" ]; then
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Skip when drift script missing
|
|
74
|
+
if [ ! -x "$DRIFT_SCRIPT" ]; then
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Skip when VOYAGE_API_KEY missing
|
|
79
|
+
if [ -z "${VOYAGE_API_KEY:-}" ]; then
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Run the check + surface result to stderr
|
|
84
|
+
RESULT=$(bash "$DRIFT_SCRIPT" --skill "$SLUG" --json 2>&1)
|
|
85
|
+
CHECK_EXIT=$?
|
|
86
|
+
|
|
87
|
+
# Parse cosine + verdict from JSON result
|
|
88
|
+
COSINE=$(echo "$RESULT" | jq -r '.cosine // "n/a"' 2>/dev/null)
|
|
89
|
+
VERDICT=$(echo "$RESULT" | jq -r '.verdict // "n/a"' 2>/dev/null)
|
|
90
|
+
THRESHOLD=$(echo "$RESULT" | jq -r '.threshold // "n/a"' 2>/dev/null)
|
|
91
|
+
|
|
92
|
+
# Advisory surface — never blocks
|
|
93
|
+
if [ "$VERDICT" = "PASS" ]; then
|
|
94
|
+
echo "" >&2
|
|
95
|
+
echo "[intent-drift-check] /$SLUG cosine $COSINE (threshold $THRESHOLD) — PASS. Intent preserved by measurable cosine." >&2
|
|
96
|
+
elif [ "$VERDICT" = "FAIL" ]; then
|
|
97
|
+
echo "" >&2
|
|
98
|
+
echo "⚠ [intent-drift-check] /$SLUG cosine $COSINE (threshold $THRESHOLD) — FAIL." >&2
|
|
99
|
+
echo " Advisory only at V1 — commit not blocked. Consider re-checking intent preservation." >&2
|
|
100
|
+
echo " Run \`bash scripts/intent-drift-check.sh --skill $SLUG\` for detail." >&2
|
|
101
|
+
else
|
|
102
|
+
# Drift script failed or returned non-JSON — silent skip
|
|
103
|
+
:
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Always exit 0 — advisory only at V1
|
|
107
|
+
exit 0
|