@thebassclef/lite 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.bassclef-source.json +2 -2
- package/dist/lite/.claude/hooks/_lib/wirings.sh +237 -0
- package/dist/lite/.claude/hooks/pre-commit-manifest-autoregen.sh +187 -0
- package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +66 -6
- package/dist/lite/.claude/hooks/session-reflection.d/20-artifact-staleness.sh +25 -5
- package/dist/lite/.claude/hooks/session-reflection.d/30-metrics-staleness.sh +11 -0
- package/dist/lite/.claude/hooks/session-reflection.d/60-deferred-actions.sh +2 -1
- package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +20 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh +89 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +12 -1
- package/dist/lite/.claude/luminaries/david-farley.md +115 -0
- package/dist/lite/.claude/luminaries/jez-humble.md +124 -0
- package/dist/lite/.claude/luminaries/martin-fowler.md +18 -0
- package/dist/lite/.claude/skills/longrun/SKILL.md +51 -0
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +39 -11
- package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +2 -2
- package/dist/lite/lib/ancestor-claude-check.sh +101 -0
- package/dist/lite/lib/fixture-builder.sh +324 -0
- package/dist/lite/lib/workflow-metrics.sh +166 -0
- package/dist/lite/presence/install/bassclef-configs.template.jsonc +76 -0
- package/dist/lite/presence/install/bassclef-sync.template.sh +7 -4
- package/dist/lite/scripts/generate-lite-manifest.sh +151 -1
- package/dist/lite/scripts/workflow-metrics-query.sh +57 -0
- package/dist/lite/standards/lite-manifest-schema-changes.md +24 -0
- package/dist/lite/standards/lite-manifest.json +79 -68
- package/package.json +1 -1
- package/dist/lite/.claude/skills/journal-export/SKILL.md +0 -293
- package/dist/lite/.claude/skills/release/SKILL.md +0 -311
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ See [`standards/os-support.md`](standards/os-support.md) for the full policy.
|
|
|
35
35
|
|
|
36
36
|
## Current release
|
|
37
37
|
|
|
38
|
-
<!-- version-start -->1.4.
|
|
38
|
+
<!-- version-start -->1.4.1<!-- version-end -->
|
|
39
39
|
|
|
40
40
|
See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
41
41
|
|
package/dist/index.cjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const version: "1.4.
|
|
1
|
+
export declare const version: "1.4.1";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"source_repo": "sunj-labs/bassclef",
|
|
4
4
|
"source_ref": "main",
|
|
5
5
|
"tier": "[TIER]",
|
|
6
|
-
"auto_sync":
|
|
6
|
+
"auto_sync": false,
|
|
7
7
|
"installed_at": "[ISO_TIMESTAMP]",
|
|
8
8
|
"installed_via": "bassclef-cli",
|
|
9
|
-
"notes": "Adopter-owned config. Change `auto_sync` to `
|
|
9
|
+
"notes": "Adopter-owned config. Default `auto_sync: false` per #1820 — cli ships self-contained; live substrate fetch is opt-in. Change `auto_sync` to `true` to enable session-start substrate refresh (or say 'enable auto-sync' per .claude/rules/auto-sync-conversational-listener.md). Change `tier` to opt into standard or ultra (requires re-run of `bassclef init --tier <T>`). Docs at https://github.com/sunj-labs/bassclef."
|
|
10
10
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# Shared accessor library for hook-wiring inspection.
|
|
4
|
+
# Retagged standard → lite 2026-09-21 per bassclef-upstream#1861 F1b.
|
|
5
|
+
# `session-reflection.d/08-settings-drift.sh` (tier: lite) sources this
|
|
6
|
+
# file. A lite-tier caller must not depend on a standard-tier lib. Cure
|
|
7
|
+
# aligns tiers so `_lib/wirings.sh` ships in the lite bundle to adopters.
|
|
8
|
+
#
|
|
9
|
+
# Used by:
|
|
10
|
+
# - .claude/hooks/session-reflection.d/08-settings-drift.sh (WU-2 of #313)
|
|
11
|
+
# - .claude/hooks/bassclef-sync.sh self-heal block (WU-3 of #313)
|
|
12
|
+
#
|
|
13
|
+
# Lives in _lib/ (underscore-prefixed) so the harness does not interpret
|
|
14
|
+
# this file as a hook. Per docs/decompositions/2026-06-24-adopter-update-distribution.md.
|
|
15
|
+
#
|
|
16
|
+
# All functions are pure: take args, emit output. No env reads beyond
|
|
17
|
+
# the optional HOME / CWD_OVERRIDE knobs in read_loaded_wirings. Tier 0
|
|
18
|
+
# strict TDD per .claude/rules/testing-tier-config.md.
|
|
19
|
+
|
|
20
|
+
# read_required_wirings <settings_path>
|
|
21
|
+
# Reads the canonical wirings list from a project-level settings.json.
|
|
22
|
+
# Output: JSON array of {event, command} pairs.
|
|
23
|
+
# Defaults to "[]" on missing file, invalid JSON, or empty .hooks.
|
|
24
|
+
# WHY: hides the jq filter shape from callers; single place to update
|
|
25
|
+
# when settings.json schema evolves.
|
|
26
|
+
read_required_wirings() {
|
|
27
|
+
local settings_path="${1:-}"
|
|
28
|
+
if [ -z "$settings_path" ] || [ ! -f "$settings_path" ]; then
|
|
29
|
+
echo "[]"
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
32
|
+
local result
|
|
33
|
+
result=$(jq -c '[
|
|
34
|
+
(.hooks // {}) | to_entries[] as $event |
|
|
35
|
+
($event.value // []) | .[] | (.hooks // [])[]? |
|
|
36
|
+
{event: $event.key, command: (.command // "")}
|
|
37
|
+
] | map(select(.command != ""))' "$settings_path" 2>/dev/null) || result=""
|
|
38
|
+
if [ -z "$result" ]; then
|
|
39
|
+
echo "[]"
|
|
40
|
+
else
|
|
41
|
+
echo "$result"
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# read_loaded_wirings
|
|
46
|
+
# Reads ~/.claude/settings.json (user-scope) + <repo>/.claude/settings.json
|
|
47
|
+
# (project-scope) and merges per Claude Code scope precedence: project
|
|
48
|
+
# overrides user per event. Output: JSON array of {event, command} pairs
|
|
49
|
+
# the harness would see.
|
|
50
|
+
#
|
|
51
|
+
# Env knobs (for testability):
|
|
52
|
+
# HOME (standard) — base for user settings
|
|
53
|
+
# CWD_OVERRIDE (test-only) — base for project settings (defaults to $PWD)
|
|
54
|
+
#
|
|
55
|
+
# Contract (RFC-0007 F2 + F6 — write it down so the next reader does not
|
|
56
|
+
# re-derive it wrong):
|
|
57
|
+
#
|
|
58
|
+
# Precondition — none. Missing or unreadable files are handled.
|
|
59
|
+
# Postcondition — stdout is a JSON array of {event, command} pairs, one
|
|
60
|
+
# per hook the harness will run, from BOTH scopes.
|
|
61
|
+
# The array is a SET: a command wired at both scopes
|
|
62
|
+
# appears once. Order follows user scope then project
|
|
63
|
+
# scope, first occurrence wins.
|
|
64
|
+
# Return — 0 on success. Non-zero when a settings file could not
|
|
65
|
+
# be parsed. Callers must tell "could not read" apart
|
|
66
|
+
# from "nothing is wired"; both print [].
|
|
67
|
+
#
|
|
68
|
+
# WHY additive, not replace (RFC-0007 F1): the harness loads hooks from
|
|
69
|
+
# user scope and project scope together. Measured 2026-09-17 — this repo
|
|
70
|
+
# declares 1 SessionStart hook in project scope and 4 in user scope, and
|
|
71
|
+
# session-reflection.sh, which is user-scope only, fires every session.
|
|
72
|
+
# The old code used jq's `*`, which replaces arrays, so any event named
|
|
73
|
+
# in both scopes lost every user-scope entry. bassclef-sync then reported
|
|
74
|
+
# those entries missing and the postcondition could never pass in a normal
|
|
75
|
+
# adopter repo. See bassclef-cli#97.
|
|
76
|
+
#
|
|
77
|
+
# WHY: hides scope-stack resolution from the comparator. Testable via
|
|
78
|
+
# env overrides.
|
|
79
|
+
read_loaded_wirings() {
|
|
80
|
+
local user_settings="${HOME:-/nonexistent}/.claude/settings.json"
|
|
81
|
+
local project_base="${CWD_OVERRIDE:-${PWD}}"
|
|
82
|
+
local project_settings="${project_base}/.claude/settings.json"
|
|
83
|
+
|
|
84
|
+
local user_json="{}"
|
|
85
|
+
local project_json="{}"
|
|
86
|
+
[ -f "$user_settings" ] && user_json=$(cat "$user_settings" 2>/dev/null || echo "{}")
|
|
87
|
+
[ -f "$project_settings" ] && project_json=$(cat "$project_settings" 2>/dev/null || echo "{}")
|
|
88
|
+
|
|
89
|
+
# Per Claude Code docs (https://code.claude.com/docs/en/settings):
|
|
90
|
+
# scope precedence — project overrides user. For event entries:
|
|
91
|
+
# * Events in BOTH: project wins (replace).
|
|
92
|
+
# * Events only in user: keep user's.
|
|
93
|
+
# * Events only in project: project's.
|
|
94
|
+
local result jq_rc
|
|
95
|
+
result=$(echo "$user_json $project_json" | jq -sc '
|
|
96
|
+
(.[0].hooks // {}) as $user_hooks |
|
|
97
|
+
(.[1].hooks // {}) as $project_hooks |
|
|
98
|
+
(($user_hooks | keys) + ($project_hooks | keys) | unique) as $events |
|
|
99
|
+
[
|
|
100
|
+
$events[] as $event |
|
|
101
|
+
(($user_hooks[$event] // []) + ($project_hooks[$event] // []))[] |
|
|
102
|
+
(.hooks // [])[]? |
|
|
103
|
+
{event: $event, command: (.command // "")}
|
|
104
|
+
]
|
|
105
|
+
| map(select(.command != ""))
|
|
106
|
+
| reduce .[] as $x ([];
|
|
107
|
+
if any(.[]; .event == $x.event and .command == $x.command)
|
|
108
|
+
then . else . + [$x] end)
|
|
109
|
+
' 2>/dev/null)
|
|
110
|
+
jq_rc=$?
|
|
111
|
+
if [ "$jq_rc" -ne 0 ] || [ -z "$result" ]; then
|
|
112
|
+
echo "[]"
|
|
113
|
+
return 1
|
|
114
|
+
fi
|
|
115
|
+
echo "$result"
|
|
116
|
+
return 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# compare_wirings <required_json> <loaded_json>
|
|
120
|
+
# Pure function. Diffs two JSON arrays of {event, command} pairs.
|
|
121
|
+
# Output: JSON {missing: [...], extra: [...], ok: bool}
|
|
122
|
+
# missing — items in required but not in loaded (drift)
|
|
123
|
+
# extra — items in loaded but not in required (informational)
|
|
124
|
+
# ok — true iff missing is empty
|
|
125
|
+
#
|
|
126
|
+
# Command normalization — sunj-labs/bassclef-upstream#1742 cure.
|
|
127
|
+
# Adopter settings.json wraps hook commands with a defensive guard shape
|
|
128
|
+
# so partial-substrate consumers do not error on missing files. bassclef
|
|
129
|
+
# ships plain form. The two forms are semantically equivalent — same
|
|
130
|
+
# underlying command, one wrapped in an existence check. Prior version
|
|
131
|
+
# did exact-string diff and reported the guarded form as missing. Now we
|
|
132
|
+
# normalize before diff by stripping the guard shape plus quotes around
|
|
133
|
+
# the path.
|
|
134
|
+
compare_wirings() {
|
|
135
|
+
local required_json="${1:-[]}"
|
|
136
|
+
local loaded_json="${2:-[]}"
|
|
137
|
+
echo "$required_json" "$loaded_json" | jq -sc '
|
|
138
|
+
# normalize_cmd strips the guard wrapper — supports quoted and
|
|
139
|
+
# unquoted paths — so "[ -f X ] && bash X || true" becomes "bash X".
|
|
140
|
+
# Also strips the double quotes that the guard shape wraps the path in,
|
|
141
|
+
# since the plain-form required side does not quote.
|
|
142
|
+
def normalize_cmd:
|
|
143
|
+
sub("^\\[ -f (\"[^\"]+\"|[^ ]+) \\] && "; "")
|
|
144
|
+
| sub(" \\|\\| true$"; "")
|
|
145
|
+
| gsub("\""; "")
|
|
146
|
+
| sub("^bash "; "");
|
|
147
|
+
|
|
148
|
+
def normalize_entry:
|
|
149
|
+
.command |= normalize_cmd;
|
|
150
|
+
|
|
151
|
+
(.[0] | map(normalize_entry)) as $required |
|
|
152
|
+
(.[1] | map(normalize_entry)) as $loaded |
|
|
153
|
+
([$required[] | select(. as $r | ($loaded | any(. == $r)) | not)]) as $missing |
|
|
154
|
+
([$loaded[] | select(. as $l | ($required | any(. == $l)) | not)]) as $extra |
|
|
155
|
+
{
|
|
156
|
+
missing: $missing,
|
|
157
|
+
extra: $extra,
|
|
158
|
+
ok: (($missing | length) == 0)
|
|
159
|
+
}
|
|
160
|
+
'
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# read_settings_version <settings_path>
|
|
164
|
+
# Output: integer (defaults to 0 when field missing, file missing, or
|
|
165
|
+
# non-integer value).
|
|
166
|
+
# WHY: hides the "default-to-0" + "validate-as-integer" logic; single
|
|
167
|
+
# place to update when version field name evolves.
|
|
168
|
+
read_settings_version() {
|
|
169
|
+
local settings_path="${1:-}"
|
|
170
|
+
if [ -z "$settings_path" ] || [ ! -f "$settings_path" ]; then
|
|
171
|
+
echo "0"
|
|
172
|
+
return 0
|
|
173
|
+
fi
|
|
174
|
+
local version
|
|
175
|
+
version=$(jq -r '.bassclef_settings_version // 0' "$settings_path" 2>/dev/null || echo "0")
|
|
176
|
+
if [[ "$version" =~ ^[0-9]+$ ]]; then
|
|
177
|
+
echo "$version"
|
|
178
|
+
else
|
|
179
|
+
echo "0"
|
|
180
|
+
fi
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# additive_merge_settings <user_path> <project_path>
|
|
184
|
+
# Merges bassclef-shipped project settings into operator's user settings.
|
|
185
|
+
# Contract:
|
|
186
|
+
# - Top-level operator keys (theme, statusLine, permissions,
|
|
187
|
+
# alwaysThinkingEnabled, skipDangerousModePermissionPrompt, etc.) — PRESERVED.
|
|
188
|
+
# - .hooks — bassclef wins PER EVENT. Events present in user but not
|
|
189
|
+
# in project survive untouched. Events present in project replace
|
|
190
|
+
# the user's entries for those events.
|
|
191
|
+
# - .bassclef_settings_version — set to project's value.
|
|
192
|
+
# Idempotent: running twice produces the same result.
|
|
193
|
+
# Bootstrap: when user file missing, copies project file as starting point.
|
|
194
|
+
# WHY: encapsulates the entire merge contract in one function callable
|
|
195
|
+
# from bassclef-sync.sh AND testable in isolation. The event-level
|
|
196
|
+
# merge granularity is Linus + ADR-031: bassclef governs the events it
|
|
197
|
+
# ships; operator owns everything else.
|
|
198
|
+
additive_merge_settings() {
|
|
199
|
+
local user_path="$1"
|
|
200
|
+
local project_path="$2"
|
|
201
|
+
if [ ! -f "$project_path" ]; then
|
|
202
|
+
return 1
|
|
203
|
+
fi
|
|
204
|
+
mkdir -p "$(dirname "$user_path")" 2>/dev/null || true
|
|
205
|
+
if [ ! -f "$user_path" ]; then
|
|
206
|
+
cp "$project_path" "$user_path"
|
|
207
|
+
# Per bassclef-upstream#340: project settings.json deliberately omits
|
|
208
|
+
# statusLine (it's user-scope). After cp, re-inject the bassclef
|
|
209
|
+
# statusLine reference when the operator's statusline script exists
|
|
210
|
+
# on disk + is executable. Without this re-injection, the cp clones
|
|
211
|
+
# the empty-statusLine state and silently drops Surface 4 aging
|
|
212
|
+
# signal. Defensive: skip injection when script missing or not
|
|
213
|
+
# executable (avoids broken reference).
|
|
214
|
+
# Env override BASSCLEF_STATUSLINE_PATH supports test fixtures.
|
|
215
|
+
local statusline_script="${BASSCLEF_STATUSLINE_PATH:-$HOME/.claude/bassclef-statusline.sh}"
|
|
216
|
+
if [ -x "$statusline_script" ] && command -v jq >/dev/null 2>&1; then
|
|
217
|
+
local tmp_status
|
|
218
|
+
tmp_status=$(mktemp)
|
|
219
|
+
if jq '.statusLine = {"type":"command", "command":"bash ~/.claude/bassclef-statusline.sh"}' \
|
|
220
|
+
"$user_path" > "$tmp_status" 2>/dev/null; then
|
|
221
|
+
mv "$tmp_status" "$user_path"
|
|
222
|
+
else
|
|
223
|
+
rm -f "$tmp_status"
|
|
224
|
+
fi
|
|
225
|
+
fi
|
|
226
|
+
return 0
|
|
227
|
+
fi
|
|
228
|
+
local tmp
|
|
229
|
+
tmp=$(mktemp)
|
|
230
|
+
jq -s '
|
|
231
|
+
(.[0]) as $user |
|
|
232
|
+
(.[1]) as $project |
|
|
233
|
+
$user
|
|
234
|
+
| .hooks = (($user.hooks // {}) + ($project.hooks // {}))
|
|
235
|
+
| .bassclef_settings_version = ($project.bassclef_settings_version // 0)
|
|
236
|
+
' "$user_path" "$project_path" > "$tmp" && mv "$tmp" "$user_path"
|
|
237
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# install-class: project
|
|
4
|
+
# PreToolUse: Bash
|
|
5
|
+
#
|
|
6
|
+
# Cure 1 — pre-commit auto-regen manifest hook.
|
|
7
|
+
#
|
|
8
|
+
# Fires on `git commit`. Detects lite-tier file edits in the staged diff.
|
|
9
|
+
# When drift is present, runs scripts/generate-lite-manifest.sh + bumps
|
|
10
|
+
# the manifest_version + regenerates docs/lite-manifest.md + stages the
|
|
11
|
+
# resulting diff. The commit then proceeds with a fresh manifest.
|
|
12
|
+
#
|
|
13
|
+
# Closes sunj-labs/bassclef-upstream#1752 (parent epic #1750).
|
|
14
|
+
# Module A1 per docs/decompositions/2026-09-18-ci-cd-subsystem.md.
|
|
15
|
+
# Anchor luminaries:
|
|
16
|
+
# @luminary michael-nygard — stability pattern (auto-cure at write pipeline)
|
|
17
|
+
# @luminary john-ousterhout — deep module (hides drift detection + bump)
|
|
18
|
+
# @luminary linus-torvalds — adopter contract (fail-soft when script absent)
|
|
19
|
+
#
|
|
20
|
+
# Adopter safety: when scripts/generate-lite-manifest.sh is missing (an
|
|
21
|
+
# adopter repo without the substrate generator), the hook falls through
|
|
22
|
+
# with exit 0 rather than blocking the commit. Adopters keep working.
|
|
23
|
+
#
|
|
24
|
+
# Override: SKIP_MANIFEST_AUTOREGEN=1 (logged via trace-helper)
|
|
25
|
+
|
|
26
|
+
set -e
|
|
27
|
+
|
|
28
|
+
# Observable failure trap
|
|
29
|
+
# shellcheck disable=SC2154 # _ec assigned in trap context via $?
|
|
30
|
+
trap '_ec=$?; if [ "$_ec" -ne 0 ]; then echo "pre-commit-manifest-autoregen.sh: FAIL at line $LINENO: exit $_ec (command: $BASH_COMMAND)" >&2; fi' ERR
|
|
31
|
+
|
|
32
|
+
# Read stdin JSON, extract command; fail-soft on malformed JSON
|
|
33
|
+
INPUT=$(cat)
|
|
34
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
|
|
35
|
+
|
|
36
|
+
# Only fire on git commit
|
|
37
|
+
if ! echo "$COMMAND" | grep -q 'git commit'; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Override path
|
|
42
|
+
if [ "${SKIP_MANIFEST_AUTOREGEN:-0}" = "1" ]; then
|
|
43
|
+
# Best-effort trace log; ignore failure so hook stays exit 0
|
|
44
|
+
if command -v bash >/dev/null 2>&1 && [ -f ".claude/hooks/trace-helper.sh" ]; then
|
|
45
|
+
bash .claude/hooks/trace-helper.sh "skip" "manifest-autoregen" "SKIP_MANIFEST_AUTOREGEN=1" 2>/dev/null || true
|
|
46
|
+
fi
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Find repo root; fail-soft if not in a git repo
|
|
51
|
+
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
52
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
GENERATE_SCRIPT="$REPO_ROOT/scripts/generate-lite-manifest.sh"
|
|
57
|
+
RENDER_SCRIPT="$REPO_ROOT/scripts/render-lite-manifest-doc.sh"
|
|
58
|
+
MANIFEST="$REPO_ROOT/lite-manifest.json"
|
|
59
|
+
|
|
60
|
+
# Adopter safety — fail-soft when script absent
|
|
61
|
+
if [ ! -f "$GENERATE_SCRIPT" ]; then
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Get staged files; fail-soft on any git error
|
|
66
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null || echo "")
|
|
67
|
+
if [ -z "$STAGED" ]; then
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Lite-tier substrate surfaces per generate-lite-manifest.sh docs:
|
|
72
|
+
# skills, rules, hooks, luminaries, agents, standards, ADRs
|
|
73
|
+
# Check if any staged file lives in one of these path classes AND has
|
|
74
|
+
# `tier: lite` frontmatter. Fast fail-soft — if grep unavailable, skip.
|
|
75
|
+
if ! command -v grep >/dev/null 2>&1; then
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
LITE_STAGED=0
|
|
80
|
+
while IFS= read -r file; do
|
|
81
|
+
[ -z "$file" ] && continue
|
|
82
|
+
|
|
83
|
+
# Quick path-class filter (avoids frontmatter reads for obviously non-lite files)
|
|
84
|
+
case "$file" in
|
|
85
|
+
.claude/skills/*/SKILL.md \
|
|
86
|
+
| .claude/rules/*.md \
|
|
87
|
+
| .claude/hooks/*.sh \
|
|
88
|
+
| .claude/luminaries/*.md \
|
|
89
|
+
| .claude/agents/*.md \
|
|
90
|
+
| standards/*.md \
|
|
91
|
+
| architecture/decisions/ADR-*.md)
|
|
92
|
+
# Path class matches; check frontmatter for tier: lite
|
|
93
|
+
if [ -f "$REPO_ROOT/$file" ] && head -20 "$REPO_ROOT/$file" 2>/dev/null | grep -q '^tier: lite'; then
|
|
94
|
+
LITE_STAGED=1
|
|
95
|
+
break
|
|
96
|
+
fi
|
|
97
|
+
# Header comment shape for .sh files: `# tier: lite`
|
|
98
|
+
if [ -f "$REPO_ROOT/$file" ] && head -5 "$REPO_ROOT/$file" 2>/dev/null | grep -q '^# tier: lite'; then
|
|
99
|
+
LITE_STAGED=1
|
|
100
|
+
break
|
|
101
|
+
fi
|
|
102
|
+
;;
|
|
103
|
+
esac
|
|
104
|
+
done <<< "$STAGED"
|
|
105
|
+
|
|
106
|
+
# No lite-tier files staged → fast exit
|
|
107
|
+
if [ "$LITE_STAGED" -eq 0 ]; then
|
|
108
|
+
exit 0
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# Compare current manifest against fresh generation
|
|
112
|
+
TMPFILE=$(mktemp)
|
|
113
|
+
# shellcheck disable=SC2064 # TMPFILE expansion NOW is intended for cleanup
|
|
114
|
+
trap "rm -f $TMPFILE" EXIT INT TERM
|
|
115
|
+
|
|
116
|
+
if ! bash "$GENERATE_SCRIPT" --dry-run > "$TMPFILE" 2>/dev/null; then
|
|
117
|
+
# Generate failed — fail-soft; author sees CI red if drift real
|
|
118
|
+
exit 0
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Diff manifests. Fail-soft on any error.
|
|
122
|
+
if [ ! -f "$MANIFEST" ]; then
|
|
123
|
+
# No manifest on disk yet — regen writes it
|
|
124
|
+
DRIFT=1
|
|
125
|
+
elif ! diff -q "$MANIFEST" "$TMPFILE" >/dev/null 2>&1; then
|
|
126
|
+
DRIFT=1
|
|
127
|
+
else
|
|
128
|
+
DRIFT=0
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
if [ "$DRIFT" -eq 0 ]; then
|
|
132
|
+
# No drift — clean exit
|
|
133
|
+
exit 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Drift detected — count added/removed/modified entries for version bump
|
|
137
|
+
CURRENT_VERSION=$(jq -r '.manifest_version // "0.0.0"' "$MANIFEST" 2>/dev/null || echo "0.0.0")
|
|
138
|
+
|
|
139
|
+
# Extract entry keys per manifest (type + slug pair)
|
|
140
|
+
OLD_KEYS=$(jq -r '.entries[] | .type + "/" + .slug' "$MANIFEST" 2>/dev/null | sort || echo "")
|
|
141
|
+
NEW_KEYS=$(jq -r '.entries[] | .type + "/" + .slug' "$TMPFILE" 2>/dev/null | sort || echo "")
|
|
142
|
+
|
|
143
|
+
# Bug 1 fix (#1829): counting lines via wc always exits 0 and emits a
|
|
144
|
+
# single integer. Prior shape used a fallback that appended a second
|
|
145
|
+
# zero on empty input, tripping the integer comparison at L153.
|
|
146
|
+
ADDED=$(comm -13 <(echo "$OLD_KEYS") <(echo "$NEW_KEYS") | wc -l | tr -d ' ')
|
|
147
|
+
REMOVED=$(comm -23 <(echo "$OLD_KEYS") <(echo "$NEW_KEYS") | wc -l | tr -d ' ')
|
|
148
|
+
|
|
149
|
+
# Compute new version per bump policy
|
|
150
|
+
MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1)
|
|
151
|
+
MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2)
|
|
152
|
+
PATCH=$(echo "$CURRENT_VERSION" | cut -d. -f3)
|
|
153
|
+
|
|
154
|
+
if [ "$REMOVED" -gt 0 ]; then
|
|
155
|
+
NEW_VERSION="$((MAJOR + 1)).0.0"
|
|
156
|
+
elif [ "$ADDED" -gt 0 ]; then
|
|
157
|
+
NEW_VERSION="${MAJOR}.$((MINOR + 1)).0"
|
|
158
|
+
else
|
|
159
|
+
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Regenerate manifest for real (writes to $MANIFEST)
|
|
163
|
+
if ! bash "$GENERATE_SCRIPT" >/dev/null 2>&1; then
|
|
164
|
+
# Regen failed after dry-run passed — fail-soft; author cures manually
|
|
165
|
+
echo "pre-commit-manifest-autoregen: regen failed; commit proceeds; CI will catch drift" >&2
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# Set new version
|
|
170
|
+
if command -v jq >/dev/null 2>&1; then
|
|
171
|
+
jq --arg v "$NEW_VERSION" '.manifest_version = $v' "$MANIFEST" > "$MANIFEST.tmp" && \
|
|
172
|
+
mv "$MANIFEST.tmp" "$MANIFEST"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Render the doc — fail-soft if script missing
|
|
176
|
+
if [ -f "$RENDER_SCRIPT" ]; then
|
|
177
|
+
bash "$RENDER_SCRIPT" >/dev/null 2>&1 || true
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# Stage the regenerated files
|
|
181
|
+
git add "$MANIFEST" 2>/dev/null || true
|
|
182
|
+
git add "$REPO_ROOT/docs/lite-manifest.md" 2>/dev/null || true
|
|
183
|
+
|
|
184
|
+
# Emit a friendly note to stderr — author sees the auto-cure fire
|
|
185
|
+
echo "pre-commit-manifest-autoregen: manifest regen ${CURRENT_VERSION} → ${NEW_VERSION} (staged)" >&2
|
|
186
|
+
|
|
187
|
+
exit 0
|
|
@@ -24,6 +24,16 @@ if [ "${SKIP_SETTINGS_DRIFT:-0}" = "1" ]; then
|
|
|
24
24
|
return 0 2>/dev/null || true
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
|
+
# SELF_MODE guard per bassclef-upstream#1804 — settings-drift only makes sense
|
|
28
|
+
# in adopter (downstream) repos where user/project settings.json can diverge
|
|
29
|
+
# from the shipped set. bassclef-upstream IS the source; self-drift is not a
|
|
30
|
+
# real signal. Detect via env var (bassclef-sync exports) OR file existence
|
|
31
|
+
# (presence/install/bassclef-sync.template.sh only exists in the source repo).
|
|
32
|
+
if [ "${SELF_MODE:-0}" = "1" ]; then
|
|
33
|
+
trace_log "skip" "settings-drift" "SELF_MODE — bassclef-upstream is the source" 2>/dev/null || true
|
|
34
|
+
return 0 2>/dev/null || true
|
|
35
|
+
fi
|
|
36
|
+
|
|
27
37
|
# Source the shared wirings accessor (per decomposition § Interface Registry).
|
|
28
38
|
__sd_wirings_lib="${BASSCLEF_DIR:-${PWD}}/.claude/hooks/_lib/wirings.sh"
|
|
29
39
|
if [ ! -f "$__sd_wirings_lib" ]; then
|
|
@@ -53,16 +63,66 @@ if [ "$__sd_ok" = "true" ]; then
|
|
|
53
63
|
trace_log "ok" "settings-drift" "no drift" 2>/dev/null || true
|
|
54
64
|
else
|
|
55
65
|
__sd_missing_count=$(echo "$__sd_diff" | jq '.missing | length')
|
|
56
|
-
__sd_missing_list=$(echo "$__sd_diff" | jq -r '.missing[] | " - " + .event + " → " + .command')
|
|
57
|
-
blocked_banner "settings-drift — ${__sd_missing_count} required hook wiring(s) missing from harness-loaded settings:
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
# Helper — extract first descriptive comment from a hook file. Skips
|
|
68
|
+
# shebang, metadata lines (# tier:, # install-class:, # PreToolUse:,
|
|
69
|
+
# # Stop, # SessionStart, # UserPromptSubmit, # PostToolUse:), and
|
|
70
|
+
# empty lines. Returns the first substantive one-line description or
|
|
71
|
+
# empty string. Caps scan at 15 lines for speed (per bassclef-upstream#1794).
|
|
72
|
+
__sd_hook_purpose() {
|
|
73
|
+
local cmd="$1"
|
|
74
|
+
# Extract path from command string: strip "bash " prefix + resolve $CLAUDE_PROJECT_DIR
|
|
75
|
+
local path="${cmd#bash }"
|
|
76
|
+
path="${path%% *}"
|
|
77
|
+
path="${path//\$CLAUDE_PROJECT_DIR/${BASSCLEF_DIR:-${PWD}}}"
|
|
78
|
+
path="${path//\$HOME/${HOME}}"
|
|
79
|
+
[ -f "$path" ] || { echo ""; return 0; }
|
|
80
|
+
awk '
|
|
81
|
+
NR > 15 { exit }
|
|
82
|
+
/^#!/ { next }
|
|
83
|
+
/^# tier:/ || /^# install-class:/ { next }
|
|
84
|
+
/^# PreToolUse:/ || /^# PostToolUse:/ || /^# Stop/ || /^# SessionStart/ || /^# UserPromptSubmit/ { next }
|
|
85
|
+
/^# *$/ { next }
|
|
86
|
+
/^#/ { sub(/^# ?/, ""); print; exit }
|
|
87
|
+
' "$path" 2>/dev/null | cut -c1-80
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Detect SELF_MODE — bassclef-upstream repo carries presence/install/bassclef-sync.template.sh.
|
|
91
|
+
# Adopter repos never carry this specific file.
|
|
92
|
+
__sd_self_mode=0
|
|
93
|
+
if [ -f "${BASSCLEF_DIR:-${PWD}}/presence/install/bassclef-sync.template.sh" ]; then
|
|
94
|
+
__sd_self_mode=1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Build per-hook explain block. jq emits event + command per line;
|
|
98
|
+
# bash loop looks up purpose per hook.
|
|
99
|
+
__sd_missing_block=""
|
|
100
|
+
while IFS=$'\t' read -r __sd_event __sd_cmd; do
|
|
101
|
+
__sd_purpose=$(__sd_hook_purpose "$__sd_cmd")
|
|
102
|
+
__sd_missing_block+=" - ${__sd_event} → ${__sd_cmd}"$'\n'
|
|
103
|
+
if [ -n "$__sd_purpose" ]; then
|
|
104
|
+
__sd_missing_block+=" purpose: ${__sd_purpose}"$'\n'
|
|
105
|
+
fi
|
|
106
|
+
done < <(echo "$__sd_diff" | jq -r '.missing[] | .event + "\t" + .command')
|
|
107
|
+
|
|
108
|
+
# Cure text branches on SELF_MODE. Both paths land runnable commands.
|
|
109
|
+
if [ "$__sd_self_mode" = "1" ]; then
|
|
110
|
+
__sd_cure="Cure (SELF_MODE — this repo IS the source):
|
|
111
|
+
Edit .claude/settings.json directly, or refresh from the hand-maintained template at
|
|
112
|
+
presence/install/public-bassclef-settings.template.json (source-of-truth for the shipped set)."
|
|
113
|
+
else
|
|
114
|
+
__sd_cure="Cure (adopter mode): bash ~/.claude/hooks/bassclef-sync.sh (additive merge on next session-start)"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
blocked_banner "settings-drift — ${__sd_missing_count} required hook wiring(s) missing from harness-loaded settings:
|
|
60
118
|
|
|
119
|
+
${__sd_missing_block}
|
|
61
120
|
Root cause class: substrate ships new wiring; operator's settings.json never picked it up.
|
|
62
|
-
|
|
121
|
+
${__sd_cure}
|
|
63
122
|
Override: SKIP_SETTINGS_DRIFT=1 (one-off bypass; logged to trace)"
|
|
64
|
-
trace_log "blocked" "settings-drift" "missing=${__sd_missing_count}" 2>/dev/null || true
|
|
65
|
-
unset __sd_missing_count
|
|
123
|
+
trace_log "blocked" "settings-drift" "missing=${__sd_missing_count} self_mode=${__sd_self_mode}" 2>/dev/null || true
|
|
124
|
+
unset __sd_missing_count __sd_missing_block __sd_event __sd_cmd __sd_purpose __sd_self_mode __sd_cure
|
|
125
|
+
unset -f __sd_hook_purpose 2>/dev/null || true
|
|
66
126
|
fi
|
|
67
127
|
|
|
68
128
|
unset __sd_wirings_lib __sd_project_settings __sd_required __sd_loaded __sd_diff __sd_ok
|
|
@@ -80,14 +80,34 @@ if [ -n "$LAST_REVIEW" ]; then
|
|
|
80
80
|
fi
|
|
81
81
|
|
|
82
82
|
# === Release notes ===
|
|
83
|
+
# Freshness signal reads file mtime, not filename per bassclef-upstream#1802.
|
|
84
|
+
# Prior shape parsed `basename | cut -c1-10` as YYYY-MM-DD; release notes
|
|
85
|
+
# ship as v<SEMVER>.md, so the parse failed silently, fell back to epoch 0,
|
|
86
|
+
# and produced "20715 days ago" banner every session. Cure: pick newest file
|
|
87
|
+
# by mtime (`ls -1t | head -1`) and compare mtime to now.
|
|
88
|
+
#
|
|
89
|
+
# SELF_MODE guard per bassclef-upstream#1804 — release-notes freshness only
|
|
90
|
+
# meaningful for adopters watching for shipped releases; bassclef-upstream IS
|
|
91
|
+
# the release source. Detect via env var (bassclef-sync sets); file-based
|
|
92
|
+
# fallback got dropped in a follow-up commit after CI-fixture false positives.
|
|
93
|
+
if [ "${SELF_MODE:-0}" = "1" ]; then
|
|
94
|
+
return 0 2>/dev/null || true
|
|
95
|
+
fi
|
|
83
96
|
NOTES_DIR="docs/release-notes"
|
|
84
97
|
if [ -d "$NOTES_DIR" ]; then
|
|
85
|
-
LAST_NOTES=$(ls -
|
|
98
|
+
LAST_NOTES=$(ls -1t "$NOTES_DIR"/*.md 2>/dev/null | grep -v .gitkeep | head -1)
|
|
86
99
|
if [ -n "$LAST_NOTES" ]; then
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
# Portable stat mtime: GNU `stat -c %Y` (Linux/CI) then BSD `stat -f %m` (macOS).
|
|
101
|
+
# GNU-first order is load-bearing: on Linux, BSD's `-f` triggers filesystem-status
|
|
102
|
+
# mode with a different specifier grammar, returning multi-line filesystem info
|
|
103
|
+
# that breaks the [ -gt 0 ] check. GNU `-c` is unknown on BSD and fails cleanly
|
|
104
|
+
# to the fallback. Per bassclef-upstream#1819 CI shard 2 diagnosis.
|
|
105
|
+
LAST_NOTES_MTIME=$(stat -c %Y "$LAST_NOTES" 2>/dev/null || stat -f %m "$LAST_NOTES" 2>/dev/null || echo 0)
|
|
106
|
+
if [ "$LAST_NOTES_MTIME" -gt 0 ]; then
|
|
107
|
+
DAYS_SINCE=$(( ( $(date +%s) - LAST_NOTES_MTIME ) / 86400 ))
|
|
108
|
+
if [ "$DAYS_SINCE" -gt 7 ]; then
|
|
109
|
+
blocked_banner "release-notes — last entry $(basename "$LAST_NOTES"), ${DAYS_SINCE} days ago. Run /release-notes."
|
|
110
|
+
fi
|
|
91
111
|
fi
|
|
92
112
|
else
|
|
93
113
|
TOTAL_COMMITS=$(git log --oneline 2>/dev/null | wc -l | tr -d ' ')
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
# Metrics-staleness check: version tags, iteration closeouts, metrics
|
|
3
3
|
# dashboard, DORA freshness, changelog_doc_id. Surfaces as one BLOCKED
|
|
4
4
|
# with all gaps enumerated.
|
|
5
|
+
#
|
|
6
|
+
# Fresh-install gate (#1818 Ship 3): if docs/metrics/ does not exist,
|
|
7
|
+
# this repo does not own a metrics dashboard yet. Skip silently. Cold
|
|
8
|
+
# adopters see no false BLOCKED banner on their first skill run. The
|
|
9
|
+
# gate activates the moment docs/metrics/ appears (after the operator
|
|
10
|
+
# runs scripts/metrics.sh --all for real).
|
|
11
|
+
# Anchor: @luminary donald-norman — a banner fires only when a real
|
|
12
|
+
# problem exists.
|
|
13
|
+
if [ ! -d "docs/metrics" ]; then
|
|
14
|
+
return 0 2>/dev/null || exit 0
|
|
15
|
+
fi
|
|
5
16
|
|
|
6
17
|
METRICS_SCRIPT="scripts/metrics.sh"
|
|
7
18
|
if [ -f "$METRICS_SCRIPT" ] || [ -f "$BASSCLEF_DIR/$METRICS_SCRIPT" ]; then
|
|
@@ -66,7 +66,8 @@ CAP_NPM_INSTALL=0
|
|
|
66
66
|
CAP_PLAYWRIGHT_MCP=0
|
|
67
67
|
|
|
68
68
|
# gcloud-auth + google-docs-write: presence of SA key
|
|
69
|
-
|
|
69
|
+
# Ship 1 (#1816): env var lookup; unset = capability disabled.
|
|
70
|
+
if [ -n "${BASSCLEF_GCLOUD_KEY:-}" ] && [ -f "$BASSCLEF_GCLOUD_KEY" ]; then
|
|
70
71
|
CAP_GCLOUD=1
|
|
71
72
|
fi
|
|
72
73
|
|
|
@@ -20,6 +20,26 @@
|
|
|
20
20
|
# Anchors: @luminary michael-nygard (steady-state), @luminary
|
|
21
21
|
# jerome-saltzer-and-michael-schroeder (complete mediation).
|
|
22
22
|
|
|
23
|
+
# SELF_MODE guard per bassclef-upstream#1804 — hook liveness check fires the
|
|
24
|
+
# banner when critical hooks have not run recently. In bassclef-upstream the
|
|
25
|
+
# operator IS actively editing hooks; heartbeat lag against source-repo work
|
|
26
|
+
# is not a real drift signal. Adopters (SELF_MODE=0) still see the check.
|
|
27
|
+
if [ "${SELF_MODE:-0}" = "1" ]; then
|
|
28
|
+
return 0 2>/dev/null || exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Fresh-install gate (#1818 Ship 3): if state/markers/hook-heartbeat/ does
|
|
32
|
+
# not exist, no producer has ever called heartbeat_mark on this machine, so
|
|
33
|
+
# no hook can be "stale" — a fresh install has no heartbeat history to check
|
|
34
|
+
# against. Skip silently. Cold adopters see no false "never fired" banner
|
|
35
|
+
# on their first skill run. The gate activates the moment a producer hook
|
|
36
|
+
# fires and writes its first marker.
|
|
37
|
+
# Anchor: @luminary donald-norman — a banner fires only when a real problem
|
|
38
|
+
# exists. @luminary michael-nygard — fresh state is not broken state.
|
|
39
|
+
if [ ! -d "state/markers/hook-heartbeat" ]; then
|
|
40
|
+
return 0 2>/dev/null || exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
23
43
|
# Probe bundled path first (dist/lite/ ship path per ADR-056 D3), then
|
|
24
44
|
# $BASSCLEF_DIR fallback for synced adopters. Pattern matches
|
|
25
45
|
# session-reflection.sh:11-17. Cures #1732 Phase A silent-fail at operator
|