@thebassclef/lite 1.9.3 → 1.9.5
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/.claude/bassclef-orientation.md +20 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +23 -0
- package/dist/lite/.claude/rules/blocked-items.md +18 -0
- package/dist/lite/.claude/skills/longrun/SKILL.md +66 -0
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +33 -0
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/lib/ancestor-claude-check.sh +26 -4
- package/dist/lite/lib/bassclef-dir-resolver.sh +20 -14
- package/dist/lite/lib/capability-probe.sh +11 -17
- package/dist/lite/lib/statusline-version.sh +103 -0
- package/dist/lite/presence/cli/bassclef-statusline.dispatcher.sh +26 -7
- package/dist/lite/presence/install/bassclef-hook-connect.sh +56 -2
- package/dist/lite/presence/install/bassclef-sync.template.sh +43 -0
- package/dist/lite/scripts/generate-lite-manifest.sh +12 -6
- package/dist/lite/scripts/interpret-input/extract-repo.sh +239 -0
- package/dist/lite/scripts/interpret-input.sh +212 -0
- package/dist/lite/scripts/launch/local-serve-stop.sh +41 -0
- package/dist/lite/scripts/launch/local-serve.sh +122 -0
- package/dist/lite/scripts/launch/parse-input-args.sh +177 -0
- package/dist/lite/scripts/longrun-preset-detect.sh +87 -0
- package/dist/lite/standards/lite-manifest-schema-changes.md +36 -0
- package/dist/lite/standards/lite-manifest.json +71 -15
- package/dist/lite/standards/session-start-banner-discipline.md +18 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ your browser.
|
|
|
63
63
|
|
|
64
64
|
## Current release
|
|
65
65
|
|
|
66
|
-
<!-- version-start -->1.9.
|
|
66
|
+
<!-- version-start -->1.9.5<!-- version-end -->
|
|
67
67
|
|
|
68
68
|
See [CHANGELOG.md](CHANGELOG.md) for the full changelog. Adopter-facing
|
|
69
69
|
release notes live at [docs.bassclef.dev/docs/whats-new/releases](https://docs.bassclef.dev/docs/whats-new/releases).
|
package/dist/index.cjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const version: "1.9.
|
|
1
|
+
export declare const version: "1.9.5";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -145,3 +145,23 @@ Pick the tier that matches your trust + budget. Upgrade path is additive — no
|
|
|
145
145
|
- `/skills` — phase-grouped skill catalog
|
|
146
146
|
- `/luminary` — design-lens catalog
|
|
147
147
|
- `/hooks` — mechanical-enforcement layer
|
|
148
|
+
|
|
149
|
+
## Statusline (opt-in)
|
|
150
|
+
|
|
151
|
+
Bassclef ships a statusline reader at `lib/statusline-version.sh`. When enabled, terminal Claude Code sessions render `bassclef v1.6.2 · up-to-date` at the bottom of every prompt. The reader consults `state/bassclef-sync-status.json` (written by `bassclef-sync.sh` at end-of-run) and `bassclef-version.json` at the repo root.
|
|
152
|
+
|
|
153
|
+
Bassclef does not set the statusline at project scope. Adopter statusline choices survive untouched. To enable, add this block to your user-scope `~/.claude/settings.json`:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"statusLine": {
|
|
158
|
+
"command": "bash /path/to/your/repo/lib/statusline-version.sh"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Replace `/path/to/your/repo` with the actual repo root. Multiple repos work — the reader resolves `CLAUDE_PROJECT_DIR` at each invocation.
|
|
164
|
+
|
|
165
|
+
Fallback behavior: if the state file is absent, the reader emits `bassclef · initializing`. If the version file is absent OR either file is malformed, the reader emits `bassclef · not synced`. Every path exits 0 with no stderr on the happy path.
|
|
166
|
+
|
|
167
|
+
Ships bassclef-upstream#1961.
|
|
@@ -111,6 +111,29 @@ blocked_banner() {
|
|
|
111
111
|
echo ""
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
# Vendor-swap constant — the AI in the harness running bassclef. Adopter
|
|
115
|
+
# swaps via BASSCLEF_AI_NAME env var; default reads as "Claude". Per
|
|
116
|
+
# #1930 Category A design lock (2026-09-27T19:20:00Z ticket comment).
|
|
117
|
+
AI_NAME="${BASSCLEF_AI_NAME:-Claude}"
|
|
118
|
+
|
|
119
|
+
# guardrail_banner — sibling of blocked_banner() with softer tone for
|
|
120
|
+
# discipline gates (chronicle stale, architect-review overdue, etc.).
|
|
121
|
+
# Label ⚙ BASSCLEF DISCIPLINE GATE signals "pause + apply discipline",
|
|
122
|
+
# not "something broke". Per #1930 Category A. Callers stay on
|
|
123
|
+
# blocked_banner() until each is migrated one at a time; this helper
|
|
124
|
+
# ships additive.
|
|
125
|
+
guardrail_banner() {
|
|
126
|
+
local hook_name="$1"
|
|
127
|
+
local step="$2"
|
|
128
|
+
local retry="$3"
|
|
129
|
+
echo ""
|
|
130
|
+
echo "⚙ BASSCLEF DISCIPLINE GATE — $hook_name"
|
|
131
|
+
echo "────────────────────────────────────────────"
|
|
132
|
+
echo "bassclef is steering $AI_NAME to $step — $retry"
|
|
133
|
+
echo "────────────────────────────────────────────"
|
|
134
|
+
echo ""
|
|
135
|
+
}
|
|
136
|
+
|
|
114
137
|
# RFC-0008 F6 — this block is steering aimed at the agent, but the adopter
|
|
115
138
|
# reads it too. Three imperatives and two shouted negations, before a single
|
|
116
139
|
# check has run, read as "you already did something wrong". Say who it is
|
|
@@ -8,6 +8,24 @@ description: "Session-start hook output may contain lines prefixed with BLOCKED:
|
|
|
8
8
|
Session-start hook output may contain lines prefixed with `BLOCKED:`.
|
|
9
9
|
These are not suggestions. They are mandatory scope.
|
|
10
10
|
|
|
11
|
+
## Banner header shapes — two siblings
|
|
12
|
+
|
|
13
|
+
Two banner shapes signal a blocked item. Both are valid discipline-gate
|
|
14
|
+
signals. Machine readers grep the `BLOCKED:` prefix on body lines; both
|
|
15
|
+
shapes preserve that prefix.
|
|
16
|
+
|
|
17
|
+
- `🛑🛑🛑 BLOCKED 🛑🛑🛑` — historical shape from `blocked_banner()`.
|
|
18
|
+
Continues to fire for real defects (schema violation, dangling ref,
|
|
19
|
+
state corruption) and for gate hooks not yet migrated.
|
|
20
|
+
- `⚙ BASSCLEF DISCIPLINE GATE — <hook>` — softer shape from
|
|
21
|
+
`guardrail_banner()`. Fires for discipline gates that ask the agent
|
|
22
|
+
to pause and apply discipline (chronicle stale, architect-review
|
|
23
|
+
overdue, temperance missing). Per sunj-labs/bassclef-upstream#1930
|
|
24
|
+
Category A.
|
|
25
|
+
|
|
26
|
+
The agent treats both header shapes as siblings and follows the same
|
|
27
|
+
resolve-or-explicitly-defer protocol regardless of which shape fires.
|
|
28
|
+
|
|
11
29
|
## Protocol
|
|
12
30
|
|
|
13
31
|
1. **Before proposing any new work**, scan session-start output for
|
|
@@ -251,6 +251,72 @@ Rule: `.claude/rules/longrun-prep-plan-doc-compression.md`. Anchor luminaries: @
|
|
|
251
251
|
Rationale: `/longrun prep` output ran too long. One shape did not fit every situation. Operator asked for a shape that reads clean in under 15 seconds. #1598 filed 2026-09-10; design session at 2026-09-12 picked the three presets under Path 2.
|
|
252
252
|
<!-- /FKGL-EXEMPT -->
|
|
253
253
|
|
|
254
|
+
<!-- FKGL-EXEMPT: Step 0.86 taxonomy — technical scope-class reference; short SVO loses precision on the class boundaries per bassclef-upstream#1978 -->
|
|
255
|
+
**Step 0.86 — Scope class taxonomy (per bassclef-upstream#1978).** After the preset picks discipline shape, `/longrun` picks discipline depth per ticket. Preset (converged / exploratory / reversible-small) and class (a / b / c / d) are **orthogonal** — preset shapes how prep renders; class shapes how each ticket runs. A single `/longrun` may ship class-a and class-b tickets in the same converged prep.
|
|
256
|
+
|
|
257
|
+
**Class label is optional.** When the ticket body or plan doc names a class, `/longrun` reads it. When neither names one, `/longrun` infers from the ticket body plus the preset (converged → class b or c; exploratory → class a; reversible-small → class a). Prior invocations without labels keep working through the grace window per `.claude/rules/we-dont-break-adopters.md`.
|
|
258
|
+
|
|
259
|
+
## Scope class taxonomy
|
|
260
|
+
|
|
261
|
+
Three base classes plus one modifier. Every ticket in scope carries one base class label. Class d layers on top when the ticket crosses a repo boundary.
|
|
262
|
+
|
|
263
|
+
| Class | Fires when | Steps | Base ceremony |
|
|
264
|
+
|---|---|---|---|
|
|
265
|
+
| **class a — small fix** | ≤20 turns; single file; no adopter surface change | 5 | OOAD casual (commit body sentence) + `/luminary` lead + Beck RED-first + impl + `/verify` + PR |
|
|
266
|
+
| **class b — script or hook extension** | adopter-facing script body change OR hook body change | 6 | brief use case + `/luminary consult` (lead + 1 supporting) + `/pre-mortem light` (2 lenses × 3-5 risks) + Beck RED-first + impl + `/verify` + PR |
|
|
267
|
+
| **class c — new building block** | new hook / lib / skill / rule / standard / ADR | 8 | fully-dressed use case + `/decompose` + `/luminary consult` (lead + 2 supporting) + `/pre-mortem light` (3 lenses × 5-8 risks) + `/rfc adversarial` (3 outside lenses) + bootstrap pair + Beck RED-first + impl + `/verify` + PR + CI + admin merge |
|
|
268
|
+
| **class d — cross-repo cure** | additive on top of a/b/c; ticket touches sibling repo | +3 | names sibling repos + release cascade + peer coordination via SendMessage + cold-adopter smoke post-merge |
|
|
269
|
+
|
|
270
|
+
Ceremony rows cross-reference `.claude/rules/oo-ad-entry-point.md` matrix (existing ceremony-per-code-class rows). Class labels here map 1:1 to matrix rows there.
|
|
271
|
+
|
|
272
|
+
## Discipline overrides
|
|
273
|
+
|
|
274
|
+
Three named override patterns fire regardless of base class:
|
|
275
|
+
|
|
276
|
+
- **/rfc adversarial when 2+ sister rules touched.** When the ticket changes behavior at 2+ rules (e.g., `.claude/rules/oo-ad-entry-point.md` + `.claude/rules/bootstrap-pair-discipline.md` + `.claude/rules/loop-discipline.md` together), dispatch `/rfc adversarial` with 3 outside lenses. Fold or reject per RFC council synthesis.
|
|
277
|
+
- **Elevated consult when new decision surface.** When the ticket introduces a new operator-facing decision menu, picker, gate, OR class boundary, elevate `/luminary consult` from base (1 supporting) to elevated (2 supporting). The new decision surface deserves a wider lens set.
|
|
278
|
+
- **Sibling smoke when adopter-observable Tier 0.** When the ticket touches adopter-observable Tier 0 substrate (e.g., `.claude/hooks/`, `.claude/skills/`, sync template), fire sibling smoke on a downstream adopter after PR merges but before release cascade. Runs the changed hook or skill on a real cold adopter clone; smokes the acceptance criteria.
|
|
279
|
+
|
|
280
|
+
Every override composes with the base class — never replaces it. A class-b ticket with 2+ sister rules touched runs class-b discipline plus `/rfc adversarial`.
|
|
281
|
+
|
|
282
|
+
## Luminary tier ladder
|
|
283
|
+
|
|
284
|
+
`/longrun` runs on whatever tier the adopter's credentials support. Three-row fallback:
|
|
285
|
+
|
|
286
|
+
| Tier | Prereq keys | Behavior | Notify shape |
|
|
287
|
+
|---|---|---|---|
|
|
288
|
+
| **ultra** | ANTHROPIC + VOYAGE | LLM picker + Voyage embeddings for luminary re-rank | stderr: `[longrun] tier=ultra (Voyage shortlist + Anthropic judge)`; inline: none |
|
|
289
|
+
| **standard** | ANTHROPIC only | LLM picker against full catalog | stderr: `[longrun] tier=standard (Anthropic full catalog)`; inline: none |
|
|
290
|
+
| **lite** | none | deterministic stub; skill dispatches without LLM signal | stderr: `[longrun] tier=lite (deterministic fallback; add ANTHROPIC_API_KEY for LLM picking)`; inline: one-line reminder that tier=lite fired |
|
|
291
|
+
|
|
292
|
+
Tier detection lives in `lib/tier-check.sh`. This ladder is guidance for readers; the actual detection stays in the lib per @luminary david-parnas (information hiding — the ladder is what changes; mechanism stays hidden).
|
|
293
|
+
|
|
294
|
+
## Class d cross-repo cure
|
|
295
|
+
|
|
296
|
+
Class d is a **modifier**, not a base class. It layers on top of a/b/c whenever the ticket crosses a repo boundary. Three obligations fire in addition to the base class discipline:
|
|
297
|
+
|
|
298
|
+
- **Name sibling repos.** In the PR body's Refs section, name every sibling repo the cure touches (e.g., `bassclef-cli#274`, `bassclef-web#256`). Cite the sister ticket URL when the sibling holds its own.
|
|
299
|
+
- **Release cascade.** After the upstream PR merges, run `/release` dry-run + andon + live to ship the fix downstream. Per `.claude/skills/release/SKILL.md`.
|
|
300
|
+
- **Peer coordination via SendMessage.** After the release page goes live, send a `SendMessage` to each affected sibling repo agent with the release URL + rebase-pin ask + smoke test ask. Address by `uds:/tmp/cc-socks/*.sock` per bassclef agent-to-agent protocol.
|
|
301
|
+
- **Cold-adopter smoke post-merge.** For every substrate change that reaches an adopter, verify on a fresh cold-adopter install (peer runs docker smoke against the new package pinning the fresh release tag).
|
|
302
|
+
|
|
303
|
+
## Refs
|
|
304
|
+
|
|
305
|
+
- `.claude/rules/oo-ad-entry-point.md` — ceremony-per-code-class matrix; class labels here map 1:1
|
|
306
|
+
- `.claude/rules/bootstrap-pair-discipline.md` — every new primitive ships with its paired bootstrap
|
|
307
|
+
- `.claude/rules/loop-discipline.md` — 6-step + 3-substep cycle per PR
|
|
308
|
+
- `.claude/rules/we-dont-break-adopters.md` — grace window keeps unlabeled prior invocations working
|
|
309
|
+
- `docs/rfcs/2026-09-27-longrun-4class-taxonomy.md` — 3-outside-lens RFC (Torvalds + Feathers + Parnas)
|
|
310
|
+
- `docs/risk-ledgers/2026-09-27-amend-1978-longrun-4class-taxonomy.md` — 3-lens pre-mortem (Ousterhout + Cooper + Prater)
|
|
311
|
+
- `docs/use-cases/UC-skill-longrun-4class-taxonomy.md` — brief use case
|
|
312
|
+
- @luminary john-ousterhout — lead (deep module — one lookup for discipline depth)
|
|
313
|
+
- @luminary alan-cooper — supporting (satisficing default — 3 base classes fit the scan ceiling)
|
|
314
|
+
- @luminary sophia-prater — supporting (progressive disclosure — orientation → labels → mechanisms)
|
|
315
|
+
- @luminary linus-torvalds — outside lens (adopter contract preserved via grace window)
|
|
316
|
+
- @luminary michael-feathers — outside lens (characterization test on the amended shape)
|
|
317
|
+
- @luminary david-parnas — outside lens (SKILL body hides mechanism paths behind rule cross-references)
|
|
318
|
+
<!-- /FKGL-EXEMPT -->
|
|
319
|
+
|
|
254
320
|
**Step 0.87 — Amendment A1: OOAD chain read at pre-flight (per #1150 + #1167 + #1414).** When the goal doc references touch adopter app paths (`src/`, `apps/`, `packages/`, `app/`) OR substrate paths (`lib/`, `scripts/`, `.claude/hooks/`, `.claude/skills/`, `.claude/rules/`, `.claude/luminaries/`, `standards/state-spine/schemas/`, `presence/install/`) OR scope prose names UI or code work, read the design chain per `.claude/bassclef-orientation.md` L100-108. Ceremony depth scales per `.claude/rules/oo-ad-entry-point.md` matrix — script bug fix = skip tier; script extension = brief use case; hook + lib = fully-dressed:
|
|
255
321
|
|
|
256
322
|
```
|
|
@@ -34,6 +34,39 @@ Run once per new consumer repo.
|
|
|
34
34
|
- Re-onboarding a repo that pre-dates the bassclef substrate
|
|
35
35
|
- Refreshing substrate wiring in an existing consumer (idempotent)
|
|
36
36
|
|
|
37
|
+
## First-run greeting (state-detected)
|
|
38
|
+
|
|
39
|
+
When the adopter just ran `bassclef init` and no other setup has advanced, the two most common paths are (a) share on GitHub and (b) top up local files. The full 4-option picker below covers every case; on a fresh install it reads longer than it needs to.
|
|
40
|
+
|
|
41
|
+
**State detection.** The greeting fires when ALL three hold:
|
|
42
|
+
|
|
43
|
+
- `.bassclef/init.manifest.json` exists at the repo root AND its `mtime` is under 24 hours old
|
|
44
|
+
- `git remote get-url origin` fails OR no upstream is configured on the current branch
|
|
45
|
+
- Current branch is `main` (or the repo's default)
|
|
46
|
+
|
|
47
|
+
When any condition fails, the greeting is silent and the full picker below fires unchanged. This preserves the current path for older repos, existing remotes, and feature branches.
|
|
48
|
+
|
|
49
|
+
**The greeting.** When state matches, emit:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Just ran bassclef init — welcome. Two most common paths from here:
|
|
53
|
+
|
|
54
|
+
1. I'll set up the GitHub remote and standard wiring.
|
|
55
|
+
(Same as Path A in the full picker.)
|
|
56
|
+
2. I'll top up the missing files on a `chore/bassclef-bootstrap`
|
|
57
|
+
branch. No remote work.
|
|
58
|
+
(Same as Path C in the full picker.)
|
|
59
|
+
|
|
60
|
+
Neither of these? Type `skip` for the full picker (adds `browse` and
|
|
61
|
+
`just me on this Mac` greenfield-from-intent options).
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Routing.** Answer `1` runs Path A. Answer `2` runs Path C. Answer `skip` — or any other input — falls through to the current 4-option picker below.
|
|
65
|
+
|
|
66
|
+
Anchor luminaries — @luminary don-norman (signifier + affordance) + @luminary alan-cooper (state detection matches the fresh-init user model).
|
|
67
|
+
|
|
68
|
+
Per `docs/use-cases/UC-skill-onboard-repo-friendlier-lead.md`.
|
|
69
|
+
|
|
37
70
|
## Which preset fits your setup?
|
|
38
71
|
|
|
39
72
|
Pick the shape that matches how you plan to work. The picker is the first fork. The procedure below runs the phases that match your pick.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "v1.
|
|
3
|
-
"marker_tag": "release-2026-09-
|
|
4
|
-
"release_date": "2026-09-
|
|
5
|
-
"release_sha": "
|
|
6
|
-
"release_tag": "release-2026-09-
|
|
2
|
+
"version": "v1.6.3",
|
|
3
|
+
"marker_tag": "release-2026-09-27-39c21000",
|
|
4
|
+
"release_date": "2026-09-27T23:49:28Z",
|
|
5
|
+
"release_sha": "39c21000",
|
|
6
|
+
"release_tag": "release-2026-09-27-39c21000"
|
|
7
7
|
}
|
|
@@ -22,8 +22,13 @@
|
|
|
22
22
|
# Adopter cure hint printed to stderr per match — non-blocking:
|
|
23
23
|
# "cure: mv <path> <path>.stale-<date>"
|
|
24
24
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
25
|
+
# Guard (bassclef-upstream#1958): the ceiling match on $HOME/.claude is
|
|
26
|
+
# suppressed. $HOME/.claude is Claude Code's per-user config home; suggesting
|
|
27
|
+
# `mv` on it would wipe adopter settings. Mid-walk cures for stale ancestor
|
|
28
|
+
# .claude/ dirs still fire. Env override BASSCLEF_ANCESTOR_CHECK_SKIP_HOME_CLAUDE=0
|
|
29
|
+
# disables the guard (used only by regression tests).
|
|
30
|
+
#
|
|
31
|
+
# Ships bassclef-upstream#573. Guard shipped bassclef-upstream#1958.
|
|
27
32
|
#
|
|
28
33
|
# Anchors:
|
|
29
34
|
# @luminary saltzer-schroeder — complete mediation (every ancestor level checked)
|
|
@@ -76,6 +81,19 @@ ancestor_claude_check() {
|
|
|
76
81
|
return 1
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
# Guard against destructive suggestions on $HOME/.claude (bassclef-upstream#1958).
|
|
85
|
+
# Claude Code stores per-user config under $HOME/.claude for every install.
|
|
86
|
+
# Suggesting `mv` on that path would wipe adopter settings. When the walker
|
|
87
|
+
# hits the canonicalized $HOME, skip both stdout and stderr for the .claude/
|
|
88
|
+
# match; mid-walk cures still fire. Env override
|
|
89
|
+
# BASSCLEF_ANCESTOR_CHECK_SKIP_HOME_CLAUDE=0 disables the guard (regression
|
|
90
|
+
# path for tests). Default = guard on (Nygard fail-safe default).
|
|
91
|
+
local guard_home_claude="${BASSCLEF_ANCESTOR_CHECK_SKIP_HOME_CLAUDE:-1}"
|
|
92
|
+
local protect_abs=""
|
|
93
|
+
if [ "$guard_home_claude" = "1" ] && [ -n "${HOME:-}" ] && [ -d "${HOME:-}" ]; then
|
|
94
|
+
protect_abs=$(cd "$HOME" && pwd -P)
|
|
95
|
+
fi
|
|
96
|
+
|
|
79
97
|
# Walk from parent of start up to (and including) ceiling
|
|
80
98
|
local cur="$start_abs"
|
|
81
99
|
local today
|
|
@@ -83,8 +101,12 @@ ancestor_claude_check() {
|
|
|
83
101
|
while [ "$cur" != "/" ]; do
|
|
84
102
|
cur=$(dirname "$cur")
|
|
85
103
|
if [ -d "$cur/.claude" ]; then
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
if [ -n "$protect_abs" ] && [ "$cur" = "$protect_abs" ]; then
|
|
105
|
+
: # guarded — $cur is canonicalized $HOME; skip emit per bassclef-upstream#1958
|
|
106
|
+
else
|
|
107
|
+
echo "$cur/.claude"
|
|
108
|
+
echo "cure: mv \"$cur/.claude\" \"$cur/.claude.stale-$today\"" >&2
|
|
109
|
+
fi
|
|
88
110
|
fi
|
|
89
111
|
# Stop after processing ceiling — do not walk above it
|
|
90
112
|
if [ "$cur" = "$ceiling_abs" ]; then
|
|
@@ -8,14 +8,17 @@
|
|
|
8
8
|
# file at $HOME/.claude/hooks/, not a symlink to a checkout).
|
|
9
9
|
#
|
|
10
10
|
# The resolver probes four paths in order per Chain-of-Responsibility:
|
|
11
|
-
# 1.
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# 1. Bundled path — walk up from $HOOK_REAL_PATH with sentinel present
|
|
12
|
+
# (was Check 3 pre-#1954; moved to Check 1 for #1953 shadow class cure —
|
|
13
|
+
# adopters with adjacent bassclef checkouts now get the npm install path)
|
|
14
|
+
# 2. Peer-checkout — $CWD/../bassclef with sentinel file present
|
|
15
|
+
# 3. $HOME/bassclef — tarball adopter path with sentinel file present
|
|
14
16
|
# 4. Fallback — $CWD (adopter repo); safer than $HOME because the files
|
|
15
17
|
# fragments want ship into the repo via bassclef init
|
|
16
18
|
#
|
|
17
19
|
# The sentinel file is presence/install/bassclef-hook-connect.sh. Every
|
|
18
20
|
# probe requires it — an empty peer dir or empty $HOME/bassclef does NOT win.
|
|
21
|
+
# Nygard R1 fold — bundled probe fail-through preserves peer-checkout adopters.
|
|
19
22
|
#
|
|
20
23
|
# Silent contract: never crashes, never writes to stderr on the resolve path.
|
|
21
24
|
# Consumers (13 fragments plus session-reflection.sh coordinator) treat the
|
|
@@ -42,17 +45,9 @@ resolve_bassclef_dir() {
|
|
|
42
45
|
local home_dir="${HOME_OVERRIDE:-${HOME:-/nonexistent}}"
|
|
43
46
|
local sentinel="presence/install/bassclef-hook-connect.sh"
|
|
44
47
|
|
|
45
|
-
# 1.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
# 2. $HOME/bassclef — tarball adopter path
|
|
51
|
-
if [ -d "$home_dir/bassclef" ] && [ -f "$home_dir/bassclef/$sentinel" ]; then
|
|
52
|
-
printf '%s\n' "$home_dir/bassclef" && return 0
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
# 3. Bundled path — walk up two levels from dispatcher
|
|
48
|
+
# 1. Bundled path — walk up two levels from dispatcher (SWAPPED to Check 1
|
|
49
|
+
# per #1954 Layer 1a — closes #1953 shadow class where an adjacent
|
|
50
|
+
# bassclef checkout at $CWD/../bassclef was winning over the npm install).
|
|
56
51
|
if [ -n "$hook_path" ]; then
|
|
57
52
|
local candidate
|
|
58
53
|
candidate="$( cd "$(dirname "$hook_path")/../.." 2>/dev/null && pwd )" || candidate=""
|
|
@@ -61,6 +56,17 @@ resolve_bassclef_dir() {
|
|
|
61
56
|
fi
|
|
62
57
|
fi
|
|
63
58
|
|
|
59
|
+
# 2. Peer-checkout — sibling bassclef dir next to $CWD (Nygard R1 fold —
|
|
60
|
+
# preserves peer-checkout adopters when bundled probe fails).
|
|
61
|
+
if [ -d "$cwd/../bassclef" ] && [ -f "$cwd/../bassclef/$sentinel" ]; then
|
|
62
|
+
( cd "$cwd/../bassclef" 2>/dev/null && pwd ) && return 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# 3. $HOME/bassclef — tarball adopter path
|
|
66
|
+
if [ -d "$home_dir/bassclef" ] && [ -f "$home_dir/bassclef/$sentinel" ]; then
|
|
67
|
+
printf '%s\n' "$home_dir/bassclef" && return 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
64
70
|
# 4. Fallback — $CWD (adopter repo). Safer than $HOME because the files
|
|
65
71
|
# fragments want (init manifest, presence/install/*) ship into the repo.
|
|
66
72
|
( cd "$cwd" 2>/dev/null && pwd ) || printf '%s\n' "$cwd"
|
|
@@ -121,20 +121,14 @@ announce_mode() {
|
|
|
121
121
|
echo "${skill}: running in \`${mode}\` mode — ${reason}. Say \`--mode=<other>\` to override." >&2
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
# --- Lib self-check
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return 1
|
|
136
|
-
fi
|
|
137
|
-
return 0
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
__bassclef_capability_probe_self_check || true
|
|
124
|
+
# --- Lib self-check removed (bassclef-upstream#1962) -------------------------
|
|
125
|
+
# The runtime self-check at this location fired a false WARN against cold
|
|
126
|
+
# `@thebassclef/lite@1.6.2` on bassclef-cli-aa smoke — every named function
|
|
127
|
+
# was defined earlier in the same file yet `type -t` returned non-zero. Root
|
|
128
|
+
# cause not reproduced in bassclef-upstream root. The Tier 0 test at
|
|
129
|
+
# lib/tests/capability-probe.test.sh T02 already asserts every function
|
|
130
|
+
# resolves after source. That test is a stricter signal (blocks merge on
|
|
131
|
+
# regression) and lands more precisely (names missing functions) than the
|
|
132
|
+
# runtime WARN. Per @luminary michael-nygard — a warning that fires on healthy
|
|
133
|
+
# state erodes trust. Per @luminary michael-feathers — the characterization
|
|
134
|
+
# test is the single source of truth for the contract.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# lib/statusline-version.sh — bassclef statusline reader.
|
|
4
|
+
#
|
|
5
|
+
# Emits one plain-text line under 80 characters describing bassclef substrate
|
|
6
|
+
# version + sync status. Called by Claude Code's statusLine.command hook every
|
|
7
|
+
# ~300 ms; must be fast (< 20 ms typical) and fail-soft (exit 0 on every path).
|
|
8
|
+
#
|
|
9
|
+
# Reads:
|
|
10
|
+
# $REPO_ROOT/bassclef-version.json — repo-root version file
|
|
11
|
+
# $REPO_ROOT/state/bassclef-sync-status.json — sync state (written by
|
|
12
|
+
# bassclef-sync.sh at end-of-run)
|
|
13
|
+
#
|
|
14
|
+
# Output shapes:
|
|
15
|
+
# bassclef v<version> · <sync_status> — happy path (both files valid)
|
|
16
|
+
# bassclef · initializing — state file absent
|
|
17
|
+
# bassclef · not synced — version file absent OR either
|
|
18
|
+
# file malformed JSON
|
|
19
|
+
#
|
|
20
|
+
# Adopter opt-in — user-scope ~/.claude/settings.json:
|
|
21
|
+
# {
|
|
22
|
+
# "statusLine": {
|
|
23
|
+
# "command": "bash /path/to/repo/lib/statusline-version.sh"
|
|
24
|
+
# }
|
|
25
|
+
# }
|
|
26
|
+
#
|
|
27
|
+
# Ships bassclef-upstream#1961.
|
|
28
|
+
# Anchors:
|
|
29
|
+
# @luminary don-norman — signifier (always-visible substrate state)
|
|
30
|
+
# @luminary alan-cooper — persistent status over modal announcement
|
|
31
|
+
# @luminary michael-nygard — fail-soft; exit 0 on every path
|
|
32
|
+
|
|
33
|
+
set +e # fail-soft on every read; never crash the statusline
|
|
34
|
+
|
|
35
|
+
# --- Resolve repo root ---
|
|
36
|
+
_resolve_repo_root() {
|
|
37
|
+
if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "${CLAUDE_PROJECT_DIR:-}" ]; then
|
|
38
|
+
echo "$CLAUDE_PROJECT_DIR"
|
|
39
|
+
return
|
|
40
|
+
fi
|
|
41
|
+
local root
|
|
42
|
+
root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
43
|
+
if [ -n "$root" ] && [ -d "$root" ]; then
|
|
44
|
+
echo "$root"
|
|
45
|
+
return
|
|
46
|
+
fi
|
|
47
|
+
pwd
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# --- Emit under 80-char cap ---
|
|
51
|
+
_emit() {
|
|
52
|
+
local line="$1"
|
|
53
|
+
# Cap at 80 chars per pre-mortem R1 fold.
|
|
54
|
+
if [ "${#line}" -gt 80 ]; then
|
|
55
|
+
line="${line:0:77}..."
|
|
56
|
+
fi
|
|
57
|
+
echo "$line"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# --- Main ---
|
|
61
|
+
statusline_version() {
|
|
62
|
+
local root
|
|
63
|
+
root=$(_resolve_repo_root)
|
|
64
|
+
|
|
65
|
+
local version_file="$root/bassclef-version.json"
|
|
66
|
+
local status_file="$root/state/bassclef-sync-status.json"
|
|
67
|
+
|
|
68
|
+
# Version file — required for happy path
|
|
69
|
+
if [ ! -f "$version_file" ]; then
|
|
70
|
+
_emit "bassclef · not synced"
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
local version
|
|
75
|
+
version=$(jq -r '.version // ""' "$version_file" 2>/dev/null)
|
|
76
|
+
if [ -z "$version" ]; then
|
|
77
|
+
# jq returned empty (malformed JSON or missing .version)
|
|
78
|
+
_emit "bassclef · not synced"
|
|
79
|
+
return 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# State file — absent means "sync has not run this session"
|
|
83
|
+
if [ ! -f "$status_file" ]; then
|
|
84
|
+
_emit "bassclef · initializing"
|
|
85
|
+
return 0
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
local sync_status
|
|
89
|
+
sync_status=$(jq -r '.sync_status // ""' "$status_file" 2>/dev/null)
|
|
90
|
+
if [ -z "$sync_status" ]; then
|
|
91
|
+
# Malformed state file OR missing .sync_status field
|
|
92
|
+
_emit "bassclef · not synced"
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
_emit "bassclef ${version} · ${sync_status}"
|
|
97
|
+
return 0
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# When invoked directly (not sourced), emit the line.
|
|
101
|
+
if [ "${BASH_SOURCE[0]:-}" = "${0}" ]; then
|
|
102
|
+
statusline_version
|
|
103
|
+
fi
|
|
@@ -48,21 +48,40 @@ __bassclef_statusline_version() {
|
|
|
48
48
|
echo "thin-pointer-statusline-2026-06-22"
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
# Read
|
|
52
|
-
|
|
51
|
+
# Read stdin only when it is a pipe. When stdin is a TTY (operator direct-
|
|
52
|
+
# invokes for debugging), `cat` would block forever. Per bassclef-upstream#1981
|
|
53
|
+
# defect A2: treat terminal stdin as empty input.
|
|
54
|
+
if [ -t 0 ]; then
|
|
55
|
+
INPUT="{}"
|
|
56
|
+
else
|
|
57
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
58
|
+
fi
|
|
53
59
|
|
|
54
|
-
# Resolve this dispatcher's directory for the script-relative
|
|
55
|
-
# `cd $(dirname); pwd` is portable across macOS
|
|
56
|
-
#
|
|
57
|
-
|
|
60
|
+
# Resolve this dispatcher's directory + canonical path for the script-relative
|
|
61
|
+
# fallback + self-loop guard. `cd $(dirname); pwd -P` is portable across macOS
|
|
62
|
+
# (BSD readlink lacks -f) and Linux, and resolves symlinks so a symlinked
|
|
63
|
+
# install still matches the underlying rich-impl file.
|
|
64
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
65
|
+
SELF_PATH="$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")"
|
|
58
66
|
|
|
59
|
-
# Find rich impl via sibling fast-path, then script-relative fallback
|
|
67
|
+
# Find rich impl via sibling fast-path, then script-relative fallback. Self-
|
|
68
|
+
# loop guard per bassclef-upstream#1981 defect A1: skip any candidate that
|
|
69
|
+
# canonicalizes to this dispatcher's own path. Real adopter case — `bassclef
|
|
70
|
+
# init` installs this dispatcher AS `~/.claude/bassclef-statusline.sh` (same
|
|
71
|
+
# basename as the rich impl in the tarball). Without the guard, the script-
|
|
72
|
+
# relative candidate resolves to the dispatcher itself; `bash $CANDIDATE`
|
|
73
|
+
# then exec's the dispatcher forever.
|
|
60
74
|
RICH_IMPL=""
|
|
61
75
|
for CANDIDATE in \
|
|
62
76
|
"$HOME/src/sunj-labs/bassclef/presence/cli/bassclef-statusline.sh" \
|
|
63
77
|
"$HOME/src/sunj-labs/bassclef-upstream/presence/cli/bassclef-statusline.sh" \
|
|
64
78
|
"$SCRIPT_DIR/bassclef-statusline.sh"; do
|
|
65
79
|
if [ -x "$CANDIDATE" ]; then
|
|
80
|
+
# Canonicalize the candidate; skip when it matches SELF_PATH.
|
|
81
|
+
CAND_ABS="$(cd "$(dirname "$CANDIDATE")" 2>/dev/null && pwd -P)/$(basename "$CANDIDATE")"
|
|
82
|
+
if [ "$CAND_ABS" = "$SELF_PATH" ]; then
|
|
83
|
+
continue
|
|
84
|
+
fi
|
|
66
85
|
RICH_IMPL="$CANDIDATE"
|
|
67
86
|
break
|
|
68
87
|
fi
|
|
@@ -241,13 +241,58 @@ bassclef_hook_connect_apply() {
|
|
|
241
241
|
echo "[bassclef-hook-connect] no tier declared in .bassclef-source.json — merging all wires (grace window). Set tier: lite to filter." >&2
|
|
242
242
|
fi
|
|
243
243
|
|
|
244
|
+
# === Filter-on-read (bassclef-upstream#1954) ===
|
|
245
|
+
# For each bassclef hook command, verify the target file exists at
|
|
246
|
+
# $bassclef_root/.claude/hooks/<basename>. Skip any entry whose target is
|
|
247
|
+
# missing. Log every skip to stderr. Emit warn banner over threshold.
|
|
248
|
+
# Saltzer-Schroeder R4 fold — matches both $CLAUDE_PROJECT_DIR- and
|
|
249
|
+
# $HOME-prefixed commands via basename extraction from .claude/hooks/ path.
|
|
250
|
+
# Nygard R2 mitigation — [ -f ] with symlink follow.
|
|
251
|
+
local present_re=""
|
|
252
|
+
local skip_count=0
|
|
253
|
+
local skipped_targets_file="$adopter_cwd/.bassclef-hook-connect-skipped.$$.tmp"
|
|
254
|
+
: > "$skipped_targets_file"
|
|
255
|
+
local present_basenames_file="$adopter_cwd/.bassclef-hook-connect-present.$$.tmp"
|
|
256
|
+
: > "$present_basenames_file"
|
|
257
|
+
local bhc_cmd bhc_base bhc_target
|
|
258
|
+
while IFS= read -r bhc_cmd; do
|
|
259
|
+
[ -z "$bhc_cmd" ] && continue
|
|
260
|
+
bhc_base=$(echo "$bhc_cmd" | grep -oE '\.claude/hooks/[^"'"'"'[:space:]\\]+\.sh' | head -1 | sed 's#\.claude/hooks/##')
|
|
261
|
+
if [ -z "$bhc_base" ]; then
|
|
262
|
+
continue
|
|
263
|
+
fi
|
|
264
|
+
bhc_target="$bassclef_root/.claude/hooks/$bhc_base"
|
|
265
|
+
if [ -f "$bhc_target" ] || [ -L "$bhc_target" ]; then
|
|
266
|
+
echo "$bhc_base" >> "$present_basenames_file"
|
|
267
|
+
else
|
|
268
|
+
echo "$bhc_target" >> "$skipped_targets_file"
|
|
269
|
+
skip_count=$((skip_count + 1))
|
|
270
|
+
echo "[bassclef-hook-connect] skip — target missing at $bhc_target" >&2
|
|
271
|
+
fi
|
|
272
|
+
done < <(jq -r '.hooks // {} | to_entries[] | .value[]? | .hooks[]? | .command // empty' "$bassclef_settings" 2>/dev/null)
|
|
273
|
+
|
|
274
|
+
if [ -s "$present_basenames_file" ]; then
|
|
275
|
+
present_re=$(sort -u "$present_basenames_file" | sed 's/\.sh$//' | paste -sd'|' -)
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
# Warn banner over threshold. Threshold=0 disables banner but per-skip
|
|
279
|
+
# stderr above still fires.
|
|
280
|
+
local warn_threshold="${BASSCLEF_HOOK_CONNECT_SKIP_WARN:-5}"
|
|
281
|
+
if [ "$skip_count" -gt 0 ] && [ "$warn_threshold" -gt 0 ] && [ "$skip_count" -gt "$warn_threshold" ]; then
|
|
282
|
+
echo "[bassclef-hook-connect] WARN — $skip_count entries skipped (target files missing). Top 3:" >&2
|
|
283
|
+
sort -u "$skipped_targets_file" | head -3 | sed 's/^/ - /' >&2
|
|
284
|
+
echo " Set BASSCLEF_HOOK_CONNECT_SKIP_WARN=N to change threshold, or 0 to disable banner." >&2
|
|
285
|
+
fi
|
|
286
|
+
rm -f "$present_basenames_file" "$skipped_targets_file"
|
|
287
|
+
|
|
244
288
|
# Merge every hook entry from every event type. Preserves operator-owned
|
|
245
289
|
# entries. Adds bassclef entries under matching (event, matcher) pairs
|
|
246
290
|
# without duplicating commands. Skips Lane 3 hooks at $CLAUDE_PROJECT_DIR
|
|
247
291
|
# per <consumer-repo>#1262. Filters off-tier entries when tier_re non-empty
|
|
248
|
-
# per bassclef-upstream#1946.
|
|
292
|
+
# per bassclef-upstream#1946. Filters missing-target entries when
|
|
293
|
+
# present_re non-empty per bassclef-upstream#1954.
|
|
249
294
|
local merged
|
|
250
|
-
merged=$(jq --slurpfile bassclef "$bassclef_settings" --arg lane3re "$lane3_re" --arg tierre "$tier_re" '
|
|
295
|
+
merged=$(jq --slurpfile bassclef "$bassclef_settings" --arg lane3re "$lane3_re" --arg tierre "$tier_re" --arg presentre "$present_re" '
|
|
251
296
|
. as $adopter
|
|
252
297
|
| ($bassclef[0].hooks // {}) as $bhooks
|
|
253
298
|
| .hooks //= {}
|
|
@@ -264,6 +309,15 @@ bassclef_hook_connect_apply() {
|
|
|
264
309
|
# tier manifest set. Empty tier_re means no filter (fallback).
|
|
265
310
|
($tierre == "") or
|
|
266
311
|
((.command // "") | test("\\.claude/hooks/(" + $tierre + ")\\.sh"))
|
|
312
|
+
)) | map(select(
|
|
313
|
+
# Filter-on-read (bassclef-upstream#1954): keep only entries whose
|
|
314
|
+
# target hook file exists at $bassclef_root/.claude/hooks/<basename>.
|
|
315
|
+
# Empty present_re means no filter (fallback merge-all — matches
|
|
316
|
+
# the tier filter fallback shape). Non-.claude/hooks commands pass
|
|
317
|
+
# through (not our target class).
|
|
318
|
+
($presentre == "") or
|
|
319
|
+
((.command // "") | test("\\.claude/hooks/[a-zA-Z0-9._-]+\\.sh") | not) or
|
|
320
|
+
((.command // "") | test("\\.claude/hooks/(" + $presentre + ")\\.sh"))
|
|
267
321
|
))) as $bhookslist |
|
|
268
322
|
if ($bhookslist | length) == 0 then .
|
|
269
323
|
elif any(.hooks[$event][]; (.matcher // "") == $bmatcher) then
|