@thebassclef/lite 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/lite/.bassclef-source.json +2 -2
  6. package/dist/lite/.claude/hooks/_lib/wirings.sh +237 -0
  7. package/dist/lite/.claude/hooks/pre-commit-manifest-autoregen.sh +212 -0
  8. package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +66 -6
  9. package/dist/lite/.claude/hooks/session-reflection.d/20-artifact-staleness.sh +25 -5
  10. package/dist/lite/.claude/hooks/session-reflection.d/30-metrics-staleness.sh +11 -0
  11. package/dist/lite/.claude/hooks/session-reflection.d/60-deferred-actions.sh +2 -1
  12. package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +20 -0
  13. package/dist/lite/.claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh +89 -0
  14. package/dist/lite/.claude/hooks/session-reflection.sh +12 -1
  15. package/dist/lite/.claude/luminaries/david-farley.md +115 -0
  16. package/dist/lite/.claude/luminaries/jez-humble.md +124 -0
  17. package/dist/lite/.claude/luminaries/martin-fowler.md +18 -0
  18. package/dist/lite/.claude/skills/longrun/SKILL.md +51 -0
  19. package/dist/lite/.claude/skills/onboard-repo/SKILL.md +39 -11
  20. package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +2 -2
  21. package/dist/lite/lib/ancestor-claude-check.sh +101 -0
  22. package/dist/lite/lib/fixture-builder.sh +324 -0
  23. package/dist/lite/lib/mechanism-fidelity.sh +14 -0
  24. package/dist/lite/lib/workflow-metrics.sh +166 -0
  25. package/dist/lite/presence/install/bassclef-configs.template.jsonc +76 -0
  26. package/dist/lite/presence/install/bassclef-sync.template.sh +7 -4
  27. package/dist/lite/scripts/generate-lite-manifest.sh +151 -1
  28. package/dist/lite/scripts/workflow-metrics-query.sh +57 -0
  29. package/dist/lite/standards/lite-manifest-schema-changes.md +24 -0
  30. package/dist/lite/standards/lite-manifest.json +82 -71
  31. package/dist/lite/standards/mechanism-fidelity.md +1 -0
  32. package/dist/lite/standards/substrate-config-schema.md +2 -2
  33. package/package.json +1 -1
  34. package/dist/lite/.claude/skills/journal-export/SKILL.md +0 -293
  35. 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.0<!-- version-end -->
38
+ <!-- version-start -->1.5.1<!-- version-end -->
39
39
 
40
40
  See [CHANGELOG.md](CHANGELOG.md) for release notes.
41
41
 
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const version = "1.4.0";
3
+ const version = "1.5.1";
4
4
  exports.version = version;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const version: "1.4.0";
1
+ export declare const version: "1.5.1";
2
2
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const version = "1.4.0";
1
+ const version = "1.5.1";
2
2
  export {
3
3
  version
4
4
  };
@@ -3,8 +3,8 @@
3
3
  "source_repo": "sunj-labs/bassclef",
4
4
  "source_ref": "main",
5
5
  "tier": "[TIER]",
6
- "auto_sync": true,
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 `false` to pause session-start substrate refresh. Change `tier` to opt into standard or ultra (requires re-run of `bassclef init --tier <T>`). Rename docs at https://github.com/sunj-labs/bassclef."
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,212 @@
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
+ # Covers all 11 manifest types per bassclef-upstream#1872 widen (2026-09-21):
85
+ # skills, rules, hooks, luminaries, agents, standards, ADRs,
86
+ # lib, scripts, templates, presence-templates, root-docs.
87
+ # Note: bash case-pattern `*` matches slashes, so `.claude/hooks/*.sh` already
88
+ # covers subdirs like `.claude/hooks/session-reflection.d/foo.sh`. Same for
89
+ # `standards/*.md`. No separate subdir patterns needed.
90
+ # Sister refactor to path-agnostic marker check tracked at #1873.
91
+ case "$file" in
92
+ .claude/skills/*/SKILL.md \
93
+ | .claude/rules/*.md \
94
+ | .claude/hooks/*.sh \
95
+ | .claude/luminaries/*.md \
96
+ | .claude/agents/*.md \
97
+ | standards/*.md \
98
+ | architecture/decisions/ADR-*.md \
99
+ | lib/*.sh \
100
+ | scripts/*.sh \
101
+ | templates/*.md \
102
+ | presence/install/*.sh \
103
+ | presence/install/*.md \
104
+ | presence/install/*.jsonc \
105
+ | AGENTS.md \
106
+ | README.md \
107
+ | CLAUDE-lite.md \
108
+ | CODE_OF_CONDUCT.md \
109
+ | CONTRIBUTING.md \
110
+ | SECURITY.md \
111
+ | .claude/bassclef-orientation.md)
112
+ # Path class matches; check frontmatter for tier: lite (markdown YAML)
113
+ if [ -f "$REPO_ROOT/$file" ] && head -20 "$REPO_ROOT/$file" 2>/dev/null | grep -q '^tier: lite'; then
114
+ LITE_STAGED=1
115
+ break
116
+ fi
117
+ # Header comment shape for .sh files: `# tier: lite`
118
+ if [ -f "$REPO_ROOT/$file" ] && head -5 "$REPO_ROOT/$file" 2>/dev/null | grep -q '^# tier: lite'; then
119
+ LITE_STAGED=1
120
+ break
121
+ fi
122
+ # JSON root-key shape for .jsonc / .json files: `"tier": "lite"` — #1872 addition
123
+ if [ -f "$REPO_ROOT/$file" ] && head -3 "$REPO_ROOT/$file" 2>/dev/null | grep -q '"tier": "lite"'; then
124
+ LITE_STAGED=1
125
+ break
126
+ fi
127
+ ;;
128
+ esac
129
+ done <<< "$STAGED"
130
+
131
+ # No lite-tier files staged → fast exit
132
+ if [ "$LITE_STAGED" -eq 0 ]; then
133
+ exit 0
134
+ fi
135
+
136
+ # Compare current manifest against fresh generation
137
+ TMPFILE=$(mktemp)
138
+ # shellcheck disable=SC2064 # TMPFILE expansion NOW is intended for cleanup
139
+ trap "rm -f $TMPFILE" EXIT INT TERM
140
+
141
+ if ! bash "$GENERATE_SCRIPT" --dry-run > "$TMPFILE" 2>/dev/null; then
142
+ # Generate failed — fail-soft; author sees CI red if drift real
143
+ exit 0
144
+ fi
145
+
146
+ # Diff manifests. Fail-soft on any error.
147
+ if [ ! -f "$MANIFEST" ]; then
148
+ # No manifest on disk yet — regen writes it
149
+ DRIFT=1
150
+ elif ! diff -q "$MANIFEST" "$TMPFILE" >/dev/null 2>&1; then
151
+ DRIFT=1
152
+ else
153
+ DRIFT=0
154
+ fi
155
+
156
+ if [ "$DRIFT" -eq 0 ]; then
157
+ # No drift — clean exit
158
+ exit 0
159
+ fi
160
+
161
+ # Drift detected — count added/removed/modified entries for version bump
162
+ CURRENT_VERSION=$(jq -r '.manifest_version // "0.0.0"' "$MANIFEST" 2>/dev/null || echo "0.0.0")
163
+
164
+ # Extract entry keys per manifest (type + slug pair)
165
+ OLD_KEYS=$(jq -r '.entries[] | .type + "/" + .slug' "$MANIFEST" 2>/dev/null | sort || echo "")
166
+ NEW_KEYS=$(jq -r '.entries[] | .type + "/" + .slug' "$TMPFILE" 2>/dev/null | sort || echo "")
167
+
168
+ # Bug 1 fix (#1829): counting lines via wc always exits 0 and emits a
169
+ # single integer. Prior shape used a fallback that appended a second
170
+ # zero on empty input, tripping the integer comparison at L153.
171
+ ADDED=$(comm -13 <(echo "$OLD_KEYS") <(echo "$NEW_KEYS") | wc -l | tr -d ' ')
172
+ REMOVED=$(comm -23 <(echo "$OLD_KEYS") <(echo "$NEW_KEYS") | wc -l | tr -d ' ')
173
+
174
+ # Compute new version per bump policy
175
+ MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1)
176
+ MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2)
177
+ PATCH=$(echo "$CURRENT_VERSION" | cut -d. -f3)
178
+
179
+ if [ "$REMOVED" -gt 0 ]; then
180
+ NEW_VERSION="$((MAJOR + 1)).0.0"
181
+ elif [ "$ADDED" -gt 0 ]; then
182
+ NEW_VERSION="${MAJOR}.$((MINOR + 1)).0"
183
+ else
184
+ NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
185
+ fi
186
+
187
+ # Regenerate manifest for real (writes to $MANIFEST)
188
+ if ! bash "$GENERATE_SCRIPT" >/dev/null 2>&1; then
189
+ # Regen failed after dry-run passed — fail-soft; author cures manually
190
+ echo "pre-commit-manifest-autoregen: regen failed; commit proceeds; CI will catch drift" >&2
191
+ exit 0
192
+ fi
193
+
194
+ # Set new version
195
+ if command -v jq >/dev/null 2>&1; then
196
+ jq --arg v "$NEW_VERSION" '.manifest_version = $v' "$MANIFEST" > "$MANIFEST.tmp" && \
197
+ mv "$MANIFEST.tmp" "$MANIFEST"
198
+ fi
199
+
200
+ # Render the doc — fail-soft if script missing
201
+ if [ -f "$RENDER_SCRIPT" ]; then
202
+ bash "$RENDER_SCRIPT" >/dev/null 2>&1 || true
203
+ fi
204
+
205
+ # Stage the regenerated files
206
+ git add "$MANIFEST" 2>/dev/null || true
207
+ git add "$REPO_ROOT/docs/lite-manifest.md" 2>/dev/null || true
208
+
209
+ # Emit a friendly note to stderr — author sees the auto-cure fire
210
+ echo "pre-commit-manifest-autoregen: manifest regen ${CURRENT_VERSION} → ${NEW_VERSION} (staged)" >&2
211
+
212
+ 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
- ${__sd_missing_list}
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
- Resolve: bassclef-sync.sh self-heal will additively merge on next session-start once it ships (#313 WU-3). For now, run manually: bash ~/.claude/hooks/bassclef-sync.sh
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 __sd_missing_list
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 -1 "$NOTES_DIR"/*.md 2>/dev/null | grep -v .gitkeep | sort | tail -1)
98
+ LAST_NOTES=$(ls -1t "$NOTES_DIR"/*.md 2>/dev/null | grep -v .gitkeep | head -1)
86
99
  if [ -n "$LAST_NOTES" ]; then
87
- LAST_NOTES_DATE=$(basename "$LAST_NOTES" .md | cut -c1-10)
88
- DAYS_SINCE=$(( ( $(date +%s) - $(date -j -f "%Y-%m-%d" "$LAST_NOTES_DATE" +%s 2>/dev/null || echo "0") ) / 86400 ))
89
- if [ "$DAYS_SINCE" -gt 7 ]; then
90
- blocked_banner "release-notes last entry $(basename "$LAST_NOTES"), ${DAYS_SINCE} days ago. Run /release-notes."
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
- if [ -f "$HOME/.config/gcloud/sunjay-google-ops.json" ]; then
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