@thebassclef/lite 1.9.16 → 1.9.19

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 (45) 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/.claude/agents/builder.md +7 -4
  6. package/dist/lite/.claude/bassclef-orientation.md +22 -0
  7. package/dist/lite/.claude/hooks/artifact-ingestion-gate.sh +11 -3
  8. package/dist/lite/.claude/hooks/assert-verify-steering.sh +1 -19
  9. package/dist/lite/.claude/hooks/plain-english-steering.sh +86 -41
  10. package/dist/lite/.claude/hooks/pre-commit-gate.sh +10 -2
  11. package/dist/lite/.claude/hooks/pre-push-shellcheck.sh +196 -0
  12. package/dist/lite/.claude/hooks/reviewer-dispatch-check.sh +153 -0
  13. package/dist/lite/.claude/hooks/session-reflection.d/83-install-written-paths-rebuild.sh +15 -32
  14. package/dist/lite/.claude/hooks/turn-prose-density-measure.sh +109 -0
  15. package/dist/lite/.claude/hooks/turn-prose-kiss-check.sh +28 -0
  16. package/dist/lite/.claude/rules/branching.md +2 -0
  17. package/dist/lite/.claude/rules/identifier-leak-prevention.md +2 -1
  18. package/dist/lite/.claude/rules/install-written-paths-mediation.md +7 -0
  19. package/dist/lite/.claude/rules/lite-manifest-schema-change-discipline.md +23 -0
  20. package/dist/lite/.claude/rules/reviewer-dispatch.md +95 -0
  21. package/dist/lite/.claude/settings.json +10 -0
  22. package/dist/lite/.claude/skills/build/SKILL.md +81 -6
  23. package/dist/lite/.claude/skills/eisenhower/SKILL.md +26 -0
  24. package/dist/lite/.claude/skills/launch/SKILL.md +45 -2
  25. package/dist/lite/.claude/skills/onboard-repo/SKILL.md +208 -40
  26. package/dist/lite/.claude/skills/onboard-repo/claude-oauth-probe.sh +60 -0
  27. package/dist/lite/.claude/skills/onboard-repo/concise-prompt.sh +73 -0
  28. package/dist/lite/.claude/skills/onboard-repo/write-state-marker.sh +81 -0
  29. package/dist/lite/.claude/skills/pre-mortem/SKILL.md +56 -0
  30. package/dist/lite/.claude/skills/sprint/SKILL.md +24 -3
  31. package/dist/lite/.claude/skills/temperance/SKILL.md +15 -4
  32. package/dist/lite/README.md +2 -0
  33. package/dist/lite/bassclef-version.json +5 -5
  34. package/dist/lite/lib/install-written-paths.sh +250 -3
  35. package/dist/lite/lib/prose-marker.sh +203 -0
  36. package/dist/lite/lib/tier-dependency-audit.sh +52 -25
  37. package/dist/lite/presence/install/bassclef-sync.template.sh +86 -8
  38. package/dist/lite/presence/install/schedule-auto-save.cron.sh +30 -2
  39. package/dist/lite/scripts/generate-lite-manifest.sh +15 -0
  40. package/dist/lite/scripts/launch/local-serve.sh +149 -8
  41. package/dist/lite/standards/bassclef-wiring-manifest.json +12 -0
  42. package/dist/lite/standards/lite-manifest-schema-changes.md +84 -0
  43. package/dist/lite/standards/lite-manifest.json +100 -29
  44. package/dist/lite/standards/prose-marker-format.md +105 -0
  45. package/package.json +1 -1
package/README.md CHANGED
@@ -67,7 +67,7 @@ your browser.
67
67
 
68
68
  ## Current release
69
69
 
70
- <!-- version-start -->1.9.16<!-- version-end -->
70
+ <!-- version-start -->1.9.19<!-- version-end -->
71
71
 
72
72
  See [CHANGELOG.md](CHANGELOG.md) for the full changelog. Adopter-facing
73
73
  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.16";
3
+ const version = "1.9.19";
4
4
  exports.version = version;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const version: "1.9.16";
1
+ export declare const version: "1.9.19";
2
2
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const version = "1.9.16";
1
+ const version = "1.9.19";
2
2
  export {
3
3
  version
4
4
  };
@@ -3,7 +3,7 @@ tier: lite
3
3
  name: Builder
4
4
  description: "Executes against spec. Respects ADR constraints. Commits with conventional commits. Doesn't redesign mid-build. Raises blockers immediately."
5
5
  tools: Read, Write, Edit, Glob, Grep, Bash
6
- model: claude-sonnet-4-6
6
+ model: claude-sonnet-5
7
7
  model_tier: balanced
8
8
  ---
9
9
 
@@ -16,15 +16,18 @@ You execute. You don't redesign mid-build. You raise blockers immediately rather
16
16
  - Construction (primary)
17
17
 
18
18
  ## Responsibilities
19
- - Run /pattern-review BEFORE implementing — check GoF patterns, Fowler
20
- refactoring smells, SOLID principles, DDD alignment. This shapes the
21
- implementation approach, not just reviews it after the fact.
19
+ - Run /pattern-review BEFORE implementing **when a pattern catalog exists** —
20
+ check `state/pattern-implementations/` or `patterns/` for a catalog. If
21
+ present, review GoF patterns, Fowler refactoring smells, SOLID principles,
22
+ DDD alignment. If absent, skip the ask — do not block on a missing catalog.
23
+ Per bassclef-upstream#2175 Batch C (F8).
22
24
  - Implement against spec acceptance criteria + pattern guidance
23
25
  - Respect ADR constraints — flag violations rather than working around them
24
26
  - Commit with conventional commits
25
27
  - Open PRs with context, not just diffs
26
28
  - Run /temperance before non-trivial tasks
27
29
  - Run /verify after each task before committing
30
+ - **Friction report before marking a step done (per bassclef-upstream#2175 Batch C — F9).** Before stating a step is complete, read `docs/session-friction-log.md` for new entries from this step. Report friction count in the PR body under a Friction section. Escalate `BLOCK`-tagged entries to Architect.
28
31
 
29
32
  ## Decision authority
30
33
  Implementation approach within ADR constraints. Escalate if a constraint must be broken.
@@ -146,6 +146,28 @@ Pick the tier that matches your trust + budget. Upgrade path is additive — no
146
146
  - `/luminary` — design-lens catalog
147
147
  - `/hooks` — mechanical-enforcement layer
148
148
 
149
+ ## Concise output style (opt-in)
150
+
151
+ Anthropic ships four output styles besides Default — Proactive, Concise, Explanatory, Learning. See [Output styles docs](https://code.claude.com/docs/en/output-styles). The Concise style (v2.1.237+) leads replies with the result, skips step-by-step narration, and skips closing recaps. Simple questions answer in one to three sentences. Full-length output stays for error reports, failing tests, security warnings, and destructive-action confirmations.
152
+
153
+ Concise covers prose dimensions bassclef already steers via `operator-facing-prose.md` + `writing-craft-discipline.md` + visual hierarchy. Enabling it at operator-user scope reduces per-turn hook-injected reminders for those dimensions. The bassclef-specific layers (grade target, jargon wordlist, appositive discipline, claim + evidence + warrant shape, per-turn metric feedback) stay as the written contract. Your project CLAUDE.md and rules stay loaded regardless of the style — Concise shapes response form, not what the model reads about your project. The before-and-after measure for Concise plus bassclef combined lands in bassclef-upstream#2197 Step 1 baseline.
154
+
155
+ Bassclef does not set the output style at project scope. Project settings would override adopter choice. To enable, add this field to your user-scope `~/.claude/settings.json`:
156
+
157
+ ```json
158
+ {
159
+ "outputStyle": "Concise"
160
+ }
161
+ ```
162
+
163
+ The value is case-sensitive — `"Concise"` with a capital C. A value that does not match an output-style name exactly falls back to Default silently. The `/output-style concise` command (v2.1.269+) is case-insensitive and offers the same result.
164
+
165
+ Switching styles mid-session takes effect at the next message and invalidates one prompt-cache slot per the [prompt-caching docs](https://code.claude.com/docs/en/prompt-caching#changing-output-style). After the first message post-switch, cache resumes.
166
+
167
+ See `docs/audits/2026-10-12-anthropic-builtins-vs-bassclef-steering.md` for the overlap analysis — which bassclef dimensions Concise covers, which bassclef dimensions stay bassclef's own.
168
+
169
+ Ships bassclef-upstream#2205.
170
+
149
171
  ## Statusline (opt-in)
150
172
 
151
173
  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.
@@ -84,16 +84,24 @@ elif [ "$TOOL_NAME" = "Edit" ]; then
84
84
  ${NEW_STRING}"
85
85
  fi
86
86
 
87
- # Check for Sources-read block (case-insensitive, tolerant of heading depth).
87
+ # Check for Sources-read block (case-insensitive, tolerant of heading depth,
88
+ # tolerant of leading whitespace per bassclef-upstream#2142).
88
89
  # Accepted forms:
89
- # ## Sources read
90
+ # ## Sources read (markdown artifact at col 0)
90
91
  # ### Sources read
91
92
  # ## Sources Read
92
93
  # ## SOURCES READ
94
+ # ## Sources read (indented inside HTML comment per #2142)
93
95
  # Also accepts an explicit from-scratch rationale:
94
96
  # "No prior artifacts" (per rule)
97
+ #
98
+ # Prior anchor `^#{1,6}` required the hash to sit at column 0. HTML prototype
99
+ # files produced by /launch --local carry sources inside a top-of-file
100
+ # `<!-- ... -->` comment where the hash is naturally indented. The anchor
101
+ # removal keeps match specificity (hash prefix + exact phrase) without
102
+ # false-positive risk on arbitrary prose.
95
103
  HAS_SOURCES_READ=0
96
- if echo "$FUTURE_CONTENT" | grep -qiE '^#{1,6}[[:space:]]+sources[[:space:]]+read'; then
104
+ if echo "$FUTURE_CONTENT" | grep -qiE '#{1,6}[[:space:]]+sources[[:space:]]+read'; then
97
105
  HAS_SOURCES_READ=1
98
106
  fi
99
107
  if echo "$FUTURE_CONTENT" | grep -qi 'no prior artifacts'; then
@@ -52,25 +52,7 @@ if [ "${SKIP_ASSERT_VERIFY_STEERING:-0}" = "1" ]; then
52
52
  fi
53
53
 
54
54
  # === Steering content ===
55
- STEERING_CONTENT="**Plain check:** Before I make a claim in this turn, I read the source first and cite it. If I cannot cite the source, I do not ship the claim.
56
-
57
- **Check my claims before I write them:**
58
-
59
- For any claim to the operator — a finding, a rating, a fix I recommend, why I think X caused Y, how many turns something will take, or a strong-word claim (wrong, broken, should, must, always) — I fill three parts:
60
-
61
- 1. **My claim** — the assertion in one sentence.
62
- 2. **Evidence** — the source I read, cited inline (file path, ticket number, git SHA, chronicle filename, standard section).
63
- 3. **Why the evidence supports the claim** — one sentence tying the source to the claim.
64
-
65
- Add more when it helps:
66
-
67
- 4. **Confidence plus how many observations back it** — from Toulmin, *The Uses of Argument* (1958).
68
- 5. **What would falsify the claim** — from Popper, *The Logic of Scientific Discovery* (1959). A claim that cannot be falsified is a guess.
69
- 6. **Two or three alternative explanations that fit the same data** — from Peirce, *Deduction, Induction, and Hypothesis* (1878). Name each option before you pick one.
70
-
71
- If I cannot fill all three, I do not ship the claim. I read the source first.
72
-
73
- Per bassclef#559 and .claude/rules/assert-only-after-verify.md."
55
+ STEERING_CONTENT="**Plain check:** Before I make a claim, I read the source first and cite it. Every operator-facing claim carries three parts — **My claim** (one sentence) + **Evidence** (source cited inline — file path, ticket, SHA) + **Why the evidence supports the claim** (one sentence). Add Toulmin confidence, Popper falsifiability, or Peirce alternatives when they help. If any part is missing, I do not ship — read the source first. Full shape at .claude/rules/assert-only-after-verify.md (bassclef#559)."
74
56
 
75
57
  # === Emit envelope ===
76
58
  emit_additional_context "UserPromptSubmit" "$STEERING_CONTENT"
@@ -33,6 +33,21 @@ for _candidate in \
33
33
  fi
34
34
  done
35
35
 
36
+ # === Source lib/prose-marker.sh (Longrun 2 Step 2d — scorer stack reader) ===
37
+ # Provides read_prose_markers for density + appositive scorer markers.
38
+ # Silent-fail — missing lib = no scorer line injection; existing grade +
39
+ # flags paths unchanged.
40
+ for _candidate in \
41
+ "${SCRIPT_DIR}/../../lib/prose-marker.sh" \
42
+ "${HOME_OVERRIDE:-${HOME:-/}}/lib/prose-marker.sh"; do
43
+ if [ -f "$_candidate" ]; then
44
+ # shellcheck disable=SC1090
45
+ source "$_candidate"
46
+ break
47
+ fi
48
+ done
49
+ unset _candidate
50
+
36
51
  # Defensive: if lib not loaded, exit silent
37
52
  if ! declare -f emit_additional_context >/dev/null 2>&1; then
38
53
  exit 0
@@ -119,51 +134,81 @@ if [ -f "$FLAGS_MARKER" ]; then
119
134
  fi
120
135
  fi
121
136
 
122
- # === Emit steering content ===
123
- # Extension per ticket #935 — first-person Ogilvy compose tests
124
- # grounded in standards/ogilvy-writing-rules.md (from #934 PR #938)
125
- STEERING_CONTENT="**Plain English for anything the operator or an adopter reads:**
126
-
127
- For turn output, PR body, issue body, session log, journal entry, and skill output:
128
-
129
- - Bassclef aims for grade 8 in most prose. Grade 10 is the top limit for simple technical sentences.
130
- - Above grade 10, give a clear reason grades 8-10 could not say it. A technical topic on its own is not a reason.
131
- - Short sentences. Subject-verb-object.
132
- - If I want to use a hyphen word (X-side, X-as-Y, N-layer X, X gate), I write what it does instead.
133
- - No more than two nouns in a row without a verb between them.
134
- - Drop words that only bassclef readers know — see standards/bassclef-internal-jargon.md.
135
- - Keep standard tech words: API, hook, PR, schema, ORM.
136
-
137
- **Vocabulary rename active (per ADR-040 D1; grace window through 2026-10-31):**
138
-
139
- When I author NEW prose for the operator or an adopter, I use the new terms:
140
-
141
- - Say **goal** not **bet**. (Historical goal IDs like \`bet 08-11a\` stay as-is.)
142
- - Say **step** not **WU** or **workunit**. (Historical \`WU-2\` refs stay.)
143
- - Say **in_flight_goal** not **in_flight_bet** for new whereami updates.
144
- - Say **time budget** not **appetite** for new prose.
145
- - Say **parent goal** and **thread** not **parent bet** and **arc** for new prose.
146
-
147
- **Discrimination cue.** Two situations, two different rules:
148
-
149
- 1. When I AUTHOR — writing my own sentence about work in flight or scope decisions — I use the new terms (Step-N, goal, time budget). Never introduce \`WU-N\` or \`bet\` or \`appetite\` in new prose.
150
- 2. When I QUOTE a past goal ID or reference (\`bet 27c WU-5\`, \`bet 2026-06-20a WU-1\`) — I preserve the exact historical string verbatim inside backticks. The backticks mark the historical boundary. The reader sees \"quote\" not \"active vocabulary.\"
151
-
152
- Rule of thumb: if I am about to type \`WU-\` in a sentence, ask — am I quoting a historical goal ID? If yes, wrap in backticks. If no, use Step-N instead.
137
+ # === Resolve scorer_track opt-in flag (Longrun 2 Step 2f — ADR-049) ===
138
+ # Default false per ADR-049 opt-in. Config at .claude/bassclef-configs.jsonc
139
+ # field prose_discipline.scorer_track. Env PROSE_DISCIPLINE_SCORER_TRACK
140
+ # overrides the config.
141
+ SCORER_TRACK="${PROSE_DISCIPLINE_SCORER_TRACK:-}"
142
+ if [ -z "$SCORER_TRACK" ]; then
143
+ if declare -f resolve_config_path >/dev/null 2>&1; then
144
+ CONFIG_FILE_FOR_SCORER=$(resolve_config_path 2>/dev/null) || CONFIG_FILE_FOR_SCORER=""
145
+ else
146
+ CONFIG_FILE_FOR_SCORER="${SCRIPT_DIR}/../bassclef-configs.jsonc"
147
+ fi
148
+ if [ -n "$CONFIG_FILE_FOR_SCORER" ] && [ -f "$CONFIG_FILE_FOR_SCORER" ]; then
149
+ SCORER_TRACK=$(sed 's|//.*||g' "$CONFIG_FILE_FOR_SCORER" 2>/dev/null \
150
+ | jq -r '.prose_discipline.scorer_track // false' 2>/dev/null)
151
+ fi
152
+ SCORER_TRACK="${SCORER_TRACK:-false}"
153
+ fi
153
154
 
154
- Historical mentions (past goal IDs, closed PR bodies, archived session logs) keep the old words — read-through via \`standards/vocabulary-migration.json\`.
155
+ # === Read scorer-stack markers (Longrun 2 Step 2d — density + appositive) ===
156
+ # Reads turn-prose-density + turn-prose-appositive markers via lib. Builds
157
+ # a one-line scorer digest with glyph legend per standards/prose-marker-format.md v1.0.
158
+ # Silent-fail — missing lib or markers = empty SCORER_LINE; existing paths unchanged.
159
+ # Gated by SCORER_TRACK opt-in flag (Step 2f).
160
+ SCORER_LINE=""
161
+ if [ "$SCORER_TRACK" = "true" ] && declare -f read_prose_markers >/dev/null 2>&1; then
162
+ SCORER_JSON=$(read_prose_markers "turn-prose-density" "turn-prose-appositive" 2>/dev/null)
163
+ if [ -n "$SCORER_JSON" ]; then
164
+ SCORER_LINE=$(echo "$SCORER_JSON" | python3 -c '
165
+ import json, sys
166
+ try:
167
+ data = json.loads(sys.stdin.read())
168
+ except Exception:
169
+ sys.exit(0)
170
+ parts = []
171
+ # density scorer — green=0 amber=3 per standards/prose-marker-format.md
172
+ d = data.get("turn-prose-density")
173
+ if d and isinstance(d, dict) and "value" in d:
174
+ v = d["value"]
175
+ glyph = "🟢" if v <= 0 else ("🟡" if v <= 3 else "🔴")
176
+ parts.append(f"density {v} {glyph}")
177
+ # appositive scorer — green=0 amber=2
178
+ a = data.get("turn-prose-appositive")
179
+ if a and isinstance(a, dict) and "value" in a:
180
+ v = a["value"]
181
+ glyph = "🟢" if v <= 0 else ("🟡" if v <= 2 else "🔴")
182
+ parts.append(f"appositive misses {v} {glyph}")
183
+ if parts:
184
+ print(" · ".join(parts))
185
+ ' 2>/dev/null)
186
+ fi
187
+ fi
155
188
 
156
- **Compose check this turn (per Ogilvy + Roman-Raphaelson via standards/ogilvy-writing-rules.md).**
157
- Before I write each sentence, I ask:
189
+ # Format the scorer line as a steering block addition when non-empty
190
+ SCORER_BLOCK=""
191
+ if [ -n "$SCORER_LINE" ]; then
192
+ SCORER_BLOCK="
158
193
 
159
- - Natural voice — would a real person say this to a peer? If it sounds like corporate throat-clearing, rewrite.
160
- - Short units — is this sentence past 25 words? If yes, split.
161
- - Read-aloud — when I read this in my head, does it stumble? If yes, rewrite until it flows.
162
- - Completeness — is the ONE thing the reader needs to take from this actually written down in plain language?
163
- - Honesty — am I saying what I mean or hiding behind hedge words and euphemisms? Say it straight.
164
- - Facts with context — for each number or fact, did I say why it matters?
194
+ **Scorer stack — $SCORER_LINE.**
195
+ - Density counts odd compound words — jargon that stacks.
196
+ - Appositive misses count shortcuts I used without saying what they mean.
197
+ - Thresholds at standards/prose-marker-format.md v1.0."
198
+ fi
165
199
 
166
- Per bassclef#790 + .claude/rules/operator-facing-prose.md + .claude/rules/writing-craft-discipline.md.${GRADE_LINE}${FLAGS_LINE}${FALLBACK_WARNING}"
200
+ # === Emit steering content ===
201
+ # Extension per ticket #935 — first-person Ogilvy compose tests
202
+ # grounded in standards/ogilvy-writing-rules.md (from #934 PR #938)
203
+ # Scorer-stack SCORER_BLOCK added at Longrun 2 Step 2d (#2197).
204
+ STEERING_CONTENT="**Plain English for operator-facing prose.**
205
+ - Target grade 8. Ceiling grade 10 for simple tech sentences.
206
+ - Short sentences. Subject-verb-object.
207
+ - Drop bassclef-internal jargon. Full list at standards/bassclef-internal-jargon.md.
208
+ - Keep standard tech words — API, hook, PR, schema.
209
+ - Vocabulary rename per ADR-040 D1 (grace through 2026-10-31): say goal not bet, step not WU, time budget not appetite, parent goal + thread not parent bet + arc.
210
+ - Historical goal IDs stay verbatim in backticks.
211
+ - Compose check (Ogilvy, six tests): natural voice · short units · read-aloud · completeness · honesty · facts with context. Full guidance at .claude/rules/writing-craft-discipline.md and .claude/rules/operator-facing-prose.md.${GRADE_LINE}${FLAGS_LINE}${SCORER_BLOCK}${FALLBACK_WARNING}"
167
212
 
168
213
  # Use emit_additional_context to wrap as JSON envelope
169
214
  emit_additional_context "UserPromptSubmit" "$STEERING_CONTENT"
@@ -23,8 +23,16 @@ INPUT=$(cat)
23
23
  # stdin (exit 5 from jq).
24
24
  COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
25
25
 
26
- # Only fire on git commit commands
27
- if ! echo "$COMMAND" | grep -q 'git commit'; then
26
+ # Only fire on git commit commands.
27
+ # Per bassclef-upstream#2139 — the prior matcher `grep -q 'git commit'` had no
28
+ # command-position anchor, so a heredoc body containing the words "git commit"
29
+ # fired the hook on cat/echo writes. The anchored regex below matches `git commit`
30
+ # only when it sits at a shell command position: start-of-line, after a chain
31
+ # operator (`&&`, `||`, `|`, `;`), or after an env-assignment prefix
32
+ # (`VAR=val git commit`). Grep is line-oriented, so each line of a multi-line
33
+ # heredoc is checked independently; body text with "git commit" as prose does
34
+ # not match any anchor and the hook skips.
35
+ if ! echo "$COMMAND" | grep -qE '(^|&&|\|\||[|;])[[:space:]]*([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*git[[:space:]]+commit([[:space:]]|$)'; then
28
36
  exit 0
29
37
  fi
30
38
 
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env bash
2
+ # tier: lite
3
+ # install-class: dual
4
+ # pre-push-shellcheck — PreToolUse Bash hook that scans .sh files going
5
+ # out at `git push` and BLOCKs on un-allowlisted shellcheck findings.
6
+ #
7
+ # Catches shellcheck SC2164 and sister findings at author time so the
8
+ # CI shellcheck gate (.github/workflows/shellcheck.yml) does not fire
9
+ # red on a fresh PR. Closes bassclef-upstream#2108 + #2253 + #2254.
10
+ #
11
+ # Reuses the existing allowlist at .claude/hooks/shellcheck-allowlist.txt
12
+ # (format: SC<code>:<path>). Scans files changed between upstream HEAD
13
+ # and local HEAD; falls back to origin/main when no upstream is set.
14
+ #
15
+ # Install-written-paths consult (#2253) — bassclef-shipped .sh files
16
+ # (recorded in state/install-written-paths.json with matching
17
+ # content_hash) skip the scan. Adopter edits to those files mismatch
18
+ # the hash and still get scanned. Lib fails soft when manifest absent.
19
+ #
20
+ # Exit codes:
21
+ # 0 — pass OR fail-soft (shellcheck missing / jq missing / no .sh changed)
22
+ # 2 — BLOCK (un-allowlisted shellcheck finding above severity=warning)
23
+ #
24
+ # Override — env (shell): SKIP_PRE_PUSH_SHELLCHECK=1 git push ...
25
+ # Override — marker (agent, #2254): touch state/markers/skip/pre-push-shellcheck.marker
26
+ # (both logged via trace-helper when available)
27
+ #
28
+ # Per @luminary jerome-saltzer-and-michael-schroeder — complete mediation.
29
+ # Per @luminary michael-nygard — fail-soft on missing binaries.
30
+ # Per @luminary michael-feathers — reuses CI's shellcheck JSON + allowlist shape.
31
+
32
+ set +e # never crash the tool call
33
+
34
+ INPUT=$(cat 2>/dev/null)
35
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
36
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
37
+
38
+ # Filter: Bash only
39
+ [ "$TOOL_NAME" = "Bash" ] || exit 0
40
+
41
+ # Filter: git push only
42
+ case "$COMMAND" in
43
+ *"git push"*) : ;;
44
+ *) exit 0 ;;
45
+ esac
46
+
47
+ # Override — env var (compat; may not reach from Claude Code harness per #2254)
48
+ if [ "${SKIP_PRE_PUSH_SHELLCHECK:-}" = "1" ]; then
49
+ if [ -f "${CLAUDE_PROJECT_DIR:-.}/lib/trace-helper.sh" ]; then
50
+ # shellcheck disable=SC1091
51
+ source "${CLAUDE_PROJECT_DIR:-.}/lib/trace-helper.sh" 2>/dev/null || true
52
+ command -v trace_record >/dev/null 2>&1 \
53
+ && trace_record "pre-push-shellcheck" "override" "SKIP_PRE_PUSH_SHELLCHECK=1" 2>/dev/null || true
54
+ fi
55
+ exit 0
56
+ fi
57
+
58
+ # Override — state-marker bypass (#2254; agent-reachable from Claude Code
59
+ # harness via Write tool when env prefix does not propagate). File check
60
+ # fires on filesystem each invocation; no env dependency.
61
+ _PROJECT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
62
+ if [ -f "$_PROJECT/state/markers/skip/pre-push-shellcheck.marker" ]; then
63
+ if [ -f "$_PROJECT/lib/trace-helper.sh" ]; then
64
+ # shellcheck disable=SC1091
65
+ source "$_PROJECT/lib/trace-helper.sh" 2>/dev/null || true
66
+ command -v trace_record >/dev/null 2>&1 \
67
+ && trace_record "pre-push-shellcheck" "override" "state-marker" 2>/dev/null || true
68
+ fi
69
+ exit 0
70
+ fi
71
+
72
+ # Fail-soft on missing binaries
73
+ if ! command -v shellcheck >/dev/null 2>&1; then
74
+ echo "pre-push-shellcheck: shellcheck not installed; skipping scan" >&2
75
+ exit 0
76
+ fi
77
+ if ! command -v jq >/dev/null 2>&1; then
78
+ echo "pre-push-shellcheck: jq not installed; skipping scan" >&2
79
+ exit 0
80
+ fi
81
+
82
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
83
+ cd "$PROJECT_DIR" || exit 0
84
+
85
+ # Figure out which .sh files are going out
86
+ UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null)
87
+ if [ -z "$UPSTREAM" ]; then
88
+ UPSTREAM="origin/main"
89
+ fi
90
+
91
+ CHANGED_FILES=$(git diff --name-only "$UPSTREAM"..HEAD 2>/dev/null | grep -E '\.sh$' || true)
92
+
93
+ if [ -z "$CHANGED_FILES" ]; then
94
+ exit 0
95
+ fi
96
+
97
+ # Only scan files that still exist (skip renamed / deleted)
98
+ EXISTING_FILES=()
99
+ while IFS= read -r f; do
100
+ [ -z "$f" ] && continue
101
+ [ -f "$f" ] && EXISTING_FILES+=("$f")
102
+ done <<< "$CHANGED_FILES"
103
+
104
+ if [ "${#EXISTING_FILES[@]}" -eq 0 ]; then
105
+ exit 0
106
+ fi
107
+
108
+ # #2253 — consult install-written-paths lib. Files bassclef ships to
109
+ # the adopter via `bassclef init` or `bassclef-sync` are not the
110
+ # adopter's work; shellcheck findings in them should not BLOCK the
111
+ # adopter's first push. Lib's `is_install_written_path` returns 0 only
112
+ # when the path is in the manifest AND the current content_hash still
113
+ # matches — adopter edits to install-written files mismatch the hash
114
+ # and still get scanned.
115
+ #
116
+ # Fail-soft per @luminary michael-nygard — missing lib OR missing
117
+ # accessor falls back to pre-cure scan behavior (BLOCK on findings).
118
+ # The pre-cure BLOCK is better than a crash.
119
+ # shellcheck disable=SC1091
120
+ # Lib enables `set -u` on source. Guard the array ops below against
121
+ # unbound-array semantics by scoping the lib under +u then restoring.
122
+ set +u
123
+ source "$PROJECT_DIR/lib/install-written-paths.sh" 2>/dev/null || true
124
+ set +u # lib may re-enable; keep +u for our own array ops
125
+ if declare -f is_install_written_path >/dev/null 2>&1; then
126
+ FILTERED_FILES=()
127
+ for f in "${EXISTING_FILES[@]}"; do
128
+ if is_install_written_path "$f" 2>/dev/null; then
129
+ continue
130
+ fi
131
+ FILTERED_FILES+=("$f")
132
+ done
133
+ if [ "${#FILTERED_FILES[@]}" -eq 0 ]; then
134
+ exit 0
135
+ fi
136
+ EXISTING_FILES=("${FILTERED_FILES[@]}")
137
+ fi
138
+
139
+ # Run shellcheck on changed .sh files — exit non-zero means findings exist
140
+ # (expected); do NOT fall back to "[]" on non-zero or the hook silently passes.
141
+ FINDINGS_JSON=$(shellcheck --format=json --severity=warning "${EXISTING_FILES[@]}" 2>/dev/null)
142
+
143
+ # Guard against malformed JSON (empty output or non-array)
144
+ if [ -z "$FINDINGS_JSON" ] || ! echo "$FINDINGS_JSON" | jq -e 'type == "array"' >/dev/null 2>&1; then
145
+ FINDINGS_JSON="[]"
146
+ fi
147
+
148
+ COUNT=$(echo "$FINDINGS_JSON" | jq 'length' 2>/dev/null || echo "0")
149
+ [ "$COUNT" = "0" ] && exit 0
150
+
151
+ # Apply allowlist filter — reuse existing allowlist file
152
+ ALLOWLIST="$PROJECT_DIR/.claude/hooks/shellcheck-allowlist.txt"
153
+ ALLOW_FILTER="[]"
154
+ if [ -f "$ALLOWLIST" ]; then
155
+ CANDIDATE=$(grep -v '^#' "$ALLOWLIST" | grep -v '^[[:space:]]*$' \
156
+ | jq -R -s -c 'split("\n") | map(select(length > 0))' 2>/dev/null || echo "[]")
157
+ if echo "$CANDIDATE" | jq -e 'type == "array"' >/dev/null 2>&1; then
158
+ ALLOW_FILTER="$CANDIDATE"
159
+ fi
160
+ fi
161
+
162
+ UNALLOWLISTED=$(echo "$FINDINGS_JSON" | jq --argjson allow "$ALLOW_FILTER" '
163
+ map(select(
164
+ ("SC" + (.code | tostring) + ":" + ((.file // "") | ltrimstr("./"))) as $key |
165
+ ([$key] | inside($allow)) | not
166
+ ))
167
+ ' 2>/dev/null || echo "[]")
168
+
169
+ if ! echo "$UNALLOWLISTED" | jq -e 'type == "array"' >/dev/null 2>&1; then
170
+ UNALLOWLISTED="[]"
171
+ fi
172
+
173
+ UCOUNT=$(echo "$UNALLOWLISTED" | jq 'length' 2>/dev/null || echo "0")
174
+ [ "$UCOUNT" = "0" ] && exit 0
175
+
176
+ # BLOCK
177
+ cat >&2 <<EOF
178
+
179
+ 🛑 pre-push-shellcheck — BLOCKED
180
+
181
+ $UCOUNT un-allowlisted shellcheck finding(s) in files going out:
182
+
183
+ EOF
184
+ echo "$UNALLOWLISTED" | jq -r '.[] | " \(.file):\(.line):\(.column) SC\(.code): \(.message)"' >&2
185
+ cat >&2 <<EOF
186
+
187
+ Resolution paths:
188
+ 1. Fix the finding in each listed file (preferred — e.g. 'cd \"\$dir\" || exit')
189
+ 2. Allowlist the SC<code>:<path> pair in .claude/hooks/shellcheck-allowlist.txt
190
+ 3. Agent bypass — mkdir -p state/markers/skip && touch state/markers/skip/pre-push-shellcheck.marker
191
+ 4. Shell bypass — SKIP_PRE_PUSH_SHELLCHECK=1 git push ... (logged; may not propagate from agent harness per #2254)
192
+
193
+ Per .claude/rules/defensive-bash.md + bassclef-upstream#2108.
194
+
195
+ EOF
196
+ exit 2