@thebassclef/lite 1.3.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.
Files changed (32) 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 +187 -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/workflow-metrics.sh +166 -0
  24. package/dist/lite/presence/install/bassclef-configs.template.jsonc +76 -0
  25. package/dist/lite/presence/install/bassclef-sync.template.sh +7 -4
  26. package/dist/lite/scripts/generate-lite-manifest.sh +151 -1
  27. package/dist/lite/scripts/workflow-metrics-query.sh +57 -0
  28. package/dist/lite/standards/lite-manifest-schema-changes.md +24 -0
  29. package/dist/lite/standards/lite-manifest.json +79 -68
  30. package/package.json +1 -1
  31. package/dist/lite/.claude/skills/journal-export/SKILL.md +0 -293
  32. 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.3.0<!-- version-end -->
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
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const version = "1.3.0";
3
+ const version = "1.4.1";
4
4
  exports.version = version;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const version: "1.3.0";
1
+ export declare const version: "1.4.1";
2
2
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const version = "1.3.0";
1
+ const version = "1.4.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,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
- ${__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
 
@@ -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