@thebassclef/lite 1.9.4 → 1.9.6

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 CHANGED
@@ -63,7 +63,7 @@ your browser.
63
63
 
64
64
  ## Current release
65
65
 
66
- <!-- version-start -->1.9.4<!-- version-end -->
66
+ <!-- version-start -->1.9.6<!-- 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
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const version = "1.9.4";
3
+ const version = "1.9.6";
4
4
  exports.version = version;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const version: "1.9.4";
1
+ export declare const version: "1.9.6";
2
2
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const version = "1.9.4";
1
+ const version = "1.9.6";
2
2
  export {
3
3
  version
4
4
  };
@@ -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
  ```
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  tier: lite
3
3
  name: onboard-repo
4
- description: "Bootstrap the bassclef framework in a new repo. Installs the sync hook. Scaffolds config, whereami, and CLAUDE.md. Modes: default, --with-deploy-host, --with-secrets, --full, --greenfield-from-intent (cold-adopter magic demo)."
4
+ description: "Bootstrap the bassclef framework in a new repo. Installs the sync hook. Scaffolds config, whereami, and CLAUDE.md. Modes: default, --with-deploy-host, --with-secrets, --full, --greenfield-from-intent (starts a new repo from a short intent paragraph)."
5
5
  problem: "Setting up bassclef in a new repo takes an hour of trial. Missing directories and wrong hook wiring."
6
6
  value: "One command bootstraps everything \u2014 sync hook and config file and whereami scaffold."
7
7
  inputs: [Optional flags — --with-deploy-host or --with-secrets or --full]
@@ -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.
@@ -56,7 +89,7 @@ Path A and Path B stay as internal names for the procedure sections below. Adopt
56
89
  ## Prerequisites (fail fast if missing)
57
90
 
58
91
  Different prereqs apply to Path A (standard onboarding) vs Path B
59
- (`--greenfield-from-intent` cold-adopter magic demo). Check the path
92
+ (`--greenfield-from-intent` starts a new repo from intent). Check the path
60
93
  the operator picked before validating.
61
94
 
62
95
  ### Path A — standard onboarding
@@ -74,7 +107,7 @@ the operator picked before validating.
74
107
  - Claude Code session has access to the bassclef checkout at `${BASSCLEF_DIR:-$CLAUDE_PROJECT_DIR}` (via `--add-dir` or bassclef init default)
75
108
  so this skill file is accessible
76
109
 
77
- ### Path B — `--greenfield-from-intent` (cold-adopter magic demo)
110
+ ### Path B — `--greenfield-from-intent` (starts a new repo from intent)
78
111
 
79
112
  - `claude` resolvable in PATH
80
113
  - `git` available
@@ -171,7 +204,7 @@ When in doubt, ask the operator which path; don't infer.
171
204
 
172
205
  ---
173
206
 
174
- ### Path B — `--greenfield-from-intent` (cold-adopter magic demo)
207
+ ### Path B — `--greenfield-from-intent` (starts a new repo from intent)
175
208
 
176
209
  **Working directory: empty.** Never leaves it. No GitHub repo. No vendor accounts touched. The path runs in ≤5 steps using the LLM your Claude Code session provides.
177
210
 
@@ -103,6 +103,4 @@ The durable marker commits with the branch's normal flow. `state/markers/` is gi
103
103
 
104
104
  Then proceed with the work.
105
105
 
106
- ## Output discipline
107
-
108
- Dispatch `/kiss words --rewrite` on your skill output before you return it. See `standards/skill-output-discipline.md` for the contract.
106
+ <!-- Output discipline runs at the Stop hook layer via turn-prose-kiss-check.sh (wired in .claude/settings.json). SKILL-body dispatch of /kiss words --rewrite was removed 2026-09-28 per bassclef-upstream#1987 — cold adopter first-fire exceeded 120s harness timeout. See docs/use-cases/UC-skill-temperance-cold-adopter-timeout.md. -->
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "v1.6.0",
3
- "marker_tag": "release-2026-09-26-db89d305",
4
- "release_date": "2026-09-26T12:19:56Z",
5
- "release_sha": "db89d305",
6
- "release_tag": "release-2026-09-26-db89d305"
2
+ "version": "v1.6.4",
3
+ "marker_tag": "release-2026-09-28-21212e17",
4
+ "release_date": "2026-09-28T01:33:41Z",
5
+ "release_sha": "21212e17",
6
+ "release_tag": "release-2026-09-28-21212e17"
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
- # Ships bassclef-upstream#573. Sister to sibling ticket that adds the manual
26
- # audit step to the onboarding checklist.
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
- echo "$cur/.claude"
87
- echo "cure: mv \"$cur/.claude\" \"$cur/.claude.stale-$today\"" >&2
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
@@ -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 on source ------------------------------------------------
125
- # Per R13 folded (Nygard stability): a broken lib should not silently return
126
- # FALSE for everything. Emit a WARN if any required function is missing.
127
- __bassclef_capability_probe_self_check() {
128
- local missing=""
129
- local f
130
- for f in probe_git probe_gh_auth probe_mcp_playwright probe_network resolve_mode validate_mode announce_mode; do
131
- type -t "$f" >/dev/null 2>&1 || missing="$missing $f"
132
- done
133
- if [ -n "$missing" ]; then
134
- echo "WARN: lib/capability-probe.sh is missing functions:$missing" >&2
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 all of stdin so we can pipe it through to the rich impl
52
- INPUT=$(cat 2>/dev/null || echo "{}")
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 fallback.
55
- # `cd $(dirname); pwd` is portable across macOS (BSD readlink lacks -f) and
56
- # Linux (GNU readlink -f works but we don't need it).
57
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
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
@@ -292,6 +292,42 @@ _template_emit_install_postcondition() {
292
292
  fi
293
293
  }
294
294
 
295
+ # === #1981 — sync state file writer (parity with operator hook L688-707) ===
296
+ # Persist sync state so lib/statusline-version.sh reader can render
297
+ # `bassclef v<X> · <sync_status>` on the adopter terminal statusline every
298
+ # ~300ms tick. Mirrors the operator-scope write block at
299
+ # .claude/hooks/bassclef-sync.sh L688-707. Both hooks must produce the same
300
+ # postcondition per ADR-032 two-stage rocket.
301
+ #
302
+ # Fail-soft: jq missing OR write path unwritable → file simply doesn't land;
303
+ # reader falls back to `bassclef · initializing` per lib/statusline-version.sh L83-85.
304
+ # Never break the sync summary that follows.
305
+ #
306
+ # Defined ABOVE the BASSCLEF_SYNC_TEST_MODE guard so tests can source + invoke.
307
+ #
308
+ # Anchor luminaries — @luminary michael-nygard (bounded-time observable state),
309
+ # @luminary michael-feathers (mirrors the operator hook exactly — characterization
310
+ # parity), @luminary jerome-saltzer-and-michael-schroeder (complete mediation
311
+ # across both operator + adopter paths).
312
+ _template_write_sync_status_file() {
313
+ local cwd="$1"
314
+ local sync_status="$2"
315
+ local status_file="${cwd}/state/bassclef-sync-status.json"
316
+ local version release_tag
317
+ version=$(jq -r '.version // ""' "${cwd}/bassclef-version.json" 2>/dev/null || echo "")
318
+ release_tag=$(jq -r '.release_tag // ""' "${cwd}/bassclef-version.json" 2>/dev/null || echo "")
319
+ mkdir -p "$(dirname "$status_file")" 2>/dev/null || return 0
320
+ cat > "$status_file" 2>/dev/null <<EOF
321
+ {
322
+ "version": "${version}",
323
+ "release_tag": "${release_tag}",
324
+ "sync_status": "${sync_status}",
325
+ "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
326
+ }
327
+ EOF
328
+ return 0
329
+ }
330
+
295
331
  # === Test-mode early return (#897 Cure A) ===
296
332
  # Tests source this template to access helper functions (see
297
333
  # .claude/hooks/tests/bassclef-sync-*.test.sh). Early return under
@@ -1936,6 +1972,13 @@ if [ -f "$HOOK_SELF" ] && [ -f "$HOOK_BASSCLEF" ]; then
1936
1972
  esac
1937
1973
  fi
1938
1974
 
1975
+ # --- Persist sync state for statusline reader (bassclef-upstream#1981) ---
1976
+ # Mirrors operator hook L688-707. Adopters who opt in to
1977
+ # lib/statusline-version.sh at ~/.claude/settings.json read this file at every
1978
+ # prompt to render `bassclef v<X> · <sync_status>`. Written here at end-of-run;
1979
+ # sole writer for the adopter path. Fail-soft — never break sync summary.
1980
+ { _template_write_sync_status_file "$CWD" "$SYNC_STATUS"; } 2>/dev/null || true
1981
+
1939
1982
  echo ""
1940
1983
  echo "### BASSCLEF SUBSTRATE — SYNCED"
1941
1984
  echo ""
@@ -479,13 +479,15 @@ done < <(find "$REPO_ROOT/.claude/embeddings" -type f -name '*.json' 2>/dev/null
479
479
  LIB_FILES=()
480
480
  while IFS= read -r f; do
481
481
  LIB_FILES+=("$f")
482
- done < <(find "$REPO_ROOT/lib" -maxdepth 1 -type f -name '*.sh' 2>/dev/null | sort)
482
+ done < <(find "$REPO_ROOT/lib" -type f -name '*.sh' \
483
+ -not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
483
484
 
484
485
  # Script files (bash) — per #1392 Shape 3; adopter runtime needs scripts/bassclef-*.sh helpers
485
486
  SCRIPT_FILES=()
486
487
  while IFS= read -r f; do
487
488
  SCRIPT_FILES+=("$f")
488
- done < <(find "$REPO_ROOT/scripts" -maxdepth 1 -type f -name '*.sh' 2>/dev/null | sort)
489
+ done < <(find "$REPO_ROOT/scripts" -type f -name '*.sh' \
490
+ -not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
489
491
 
490
492
  # Hook fragments (bash) — per goal 2026-09-16a (bassclef-upstream#1690): session-reflection.sh
491
493
  # iterates session-reflection.d/*.sh at SessionStart; fragments ship as type=hook.
@@ -506,25 +508,29 @@ done < <(find "$REPO_ROOT/.claude" -maxdepth 1 -type f -name '*.md' 2>/dev/null
506
508
  PRESENCE_TEMPLATE_SH_FILES=()
507
509
  while IFS= read -r f; do
508
510
  PRESENCE_TEMPLATE_SH_FILES+=("$f")
509
- done < <(find "$REPO_ROOT/presence/install" -maxdepth 1 -type f -name '*.sh' 2>/dev/null | sort)
511
+ done < <(find "$REPO_ROOT/presence/install" -type f -name '*.sh' \
512
+ -not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
510
513
 
511
514
  PRESENCE_TEMPLATE_MD_FILES=()
512
515
  while IFS= read -r f; do
513
516
  PRESENCE_TEMPLATE_MD_FILES+=("$f")
514
- done < <(find "$REPO_ROOT/presence/install" -maxdepth 1 -type f -name '*.md' 2>/dev/null | sort)
517
+ done < <(find "$REPO_ROOT/presence/install" -type f -name '*.md' \
518
+ -not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
515
519
 
516
520
  # Presence templates (jsonc) — Ship 1 (bassclef-cli#104) — configs template
517
521
  # ships so `bassclef init` can copy it to adopter's `.claude/bassclef-configs.jsonc`.
518
522
  PRESENCE_TEMPLATE_JSONC_FILES=()
519
523
  while IFS= read -r f; do
520
524
  PRESENCE_TEMPLATE_JSONC_FILES+=("$f")
521
- done < <(find "$REPO_ROOT/presence/install" -maxdepth 1 -type f -name '*.jsonc' 2>/dev/null | sort)
525
+ done < <(find "$REPO_ROOT/presence/install" -type f -name '*.jsonc' \
526
+ -not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
522
527
 
523
528
  # Templates (md) — per #1392 Shape 3; chronicle-template + iteration-goal etc adopters copy
524
529
  TEMPLATE_FILES=()
525
530
  while IFS= read -r f; do
526
531
  TEMPLATE_FILES+=("$f")
527
- done < <(find "$REPO_ROOT/templates" -maxdepth 1 -type f -name '*.md' 2>/dev/null | sort)
532
+ done < <(find "$REPO_ROOT/templates" -type f -name '*.md' \
533
+ -not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
528
534
 
529
535
  # Root docs (md) — per #1392 Shape 3; README + CLAUDE + AGENTS adopters read on install
530
536
  ROOT_DOC_FILES=()