@thebassclef/lite 1.9.16 → 1.9.18

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 (42) 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 +144 -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/lite-manifest-schema-change-discipline.md +23 -0
  19. package/dist/lite/.claude/rules/reviewer-dispatch.md +95 -0
  20. package/dist/lite/.claude/settings.json +10 -0
  21. package/dist/lite/.claude/skills/build/SKILL.md +81 -6
  22. package/dist/lite/.claude/skills/eisenhower/SKILL.md +26 -0
  23. package/dist/lite/.claude/skills/launch/SKILL.md +45 -2
  24. package/dist/lite/.claude/skills/onboard-repo/SKILL.md +204 -40
  25. package/dist/lite/.claude/skills/onboard-repo/concise-prompt.sh +73 -0
  26. package/dist/lite/.claude/skills/onboard-repo/write-state-marker.sh +81 -0
  27. package/dist/lite/.claude/skills/pre-mortem/SKILL.md +56 -0
  28. package/dist/lite/.claude/skills/sprint/SKILL.md +24 -3
  29. package/dist/lite/.claude/skills/temperance/SKILL.md +15 -4
  30. package/dist/lite/README.md +2 -0
  31. package/dist/lite/bassclef-version.json +5 -5
  32. package/dist/lite/lib/install-written-paths.sh +250 -3
  33. package/dist/lite/lib/prose-marker.sh +203 -0
  34. package/dist/lite/lib/tier-dependency-audit.sh +52 -25
  35. package/dist/lite/presence/install/bassclef-sync.template.sh +86 -8
  36. package/dist/lite/scripts/generate-lite-manifest.sh +15 -0
  37. package/dist/lite/scripts/launch/local-serve.sh +149 -8
  38. package/dist/lite/standards/bassclef-wiring-manifest.json +12 -0
  39. package/dist/lite/standards/lite-manifest-schema-changes.md +49 -0
  40. package/dist/lite/standards/lite-manifest.json +90 -27
  41. package/dist/lite/standards/prose-marker-format.md +105 -0
  42. 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.18<!-- 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.18";
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.18";
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.18";
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,144 @@
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.
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
+ # Exit codes:
16
+ # 0 — pass OR fail-soft (shellcheck missing / jq missing / no .sh changed)
17
+ # 2 — BLOCK (un-allowlisted shellcheck finding above severity=warning)
18
+ #
19
+ # Override: SKIP_PRE_PUSH_SHELLCHECK=1 git push ...
20
+ # (logged via trace-helper when available)
21
+ #
22
+ # Per @luminary jerome-saltzer-and-michael-schroeder — complete mediation.
23
+ # Per @luminary michael-nygard — fail-soft on missing binaries.
24
+ # Per @luminary michael-feathers — reuses CI's shellcheck JSON + allowlist shape.
25
+
26
+ set +e # never crash the tool call
27
+
28
+ INPUT=$(cat 2>/dev/null)
29
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
30
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
31
+
32
+ # Filter: Bash only
33
+ [ "$TOOL_NAME" = "Bash" ] || exit 0
34
+
35
+ # Filter: git push only
36
+ case "$COMMAND" in
37
+ *"git push"*) : ;;
38
+ *) exit 0 ;;
39
+ esac
40
+
41
+ # Override
42
+ if [ "${SKIP_PRE_PUSH_SHELLCHECK:-}" = "1" ]; then
43
+ if [ -f "${CLAUDE_PROJECT_DIR:-.}/lib/trace-helper.sh" ]; then
44
+ # shellcheck disable=SC1091
45
+ source "${CLAUDE_PROJECT_DIR:-.}/lib/trace-helper.sh" 2>/dev/null || true
46
+ command -v trace_record >/dev/null 2>&1 \
47
+ && trace_record "pre-push-shellcheck" "override" "SKIP_PRE_PUSH_SHELLCHECK=1" 2>/dev/null || true
48
+ fi
49
+ exit 0
50
+ fi
51
+
52
+ # Fail-soft on missing binaries
53
+ if ! command -v shellcheck >/dev/null 2>&1; then
54
+ echo "pre-push-shellcheck: shellcheck not installed; skipping scan" >&2
55
+ exit 0
56
+ fi
57
+ if ! command -v jq >/dev/null 2>&1; then
58
+ echo "pre-push-shellcheck: jq not installed; skipping scan" >&2
59
+ exit 0
60
+ fi
61
+
62
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
63
+ cd "$PROJECT_DIR" || exit 0
64
+
65
+ # Figure out which .sh files are going out
66
+ UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null)
67
+ if [ -z "$UPSTREAM" ]; then
68
+ UPSTREAM="origin/main"
69
+ fi
70
+
71
+ CHANGED_FILES=$(git diff --name-only "$UPSTREAM"..HEAD 2>/dev/null | grep -E '\.sh$' || true)
72
+
73
+ if [ -z "$CHANGED_FILES" ]; then
74
+ exit 0
75
+ fi
76
+
77
+ # Only scan files that still exist (skip renamed / deleted)
78
+ EXISTING_FILES=()
79
+ while IFS= read -r f; do
80
+ [ -z "$f" ] && continue
81
+ [ -f "$f" ] && EXISTING_FILES+=("$f")
82
+ done <<< "$CHANGED_FILES"
83
+
84
+ if [ "${#EXISTING_FILES[@]}" -eq 0 ]; then
85
+ exit 0
86
+ fi
87
+
88
+ # Run shellcheck on changed .sh files — exit non-zero means findings exist
89
+ # (expected); do NOT fall back to "[]" on non-zero or the hook silently passes.
90
+ FINDINGS_JSON=$(shellcheck --format=json --severity=warning "${EXISTING_FILES[@]}" 2>/dev/null)
91
+
92
+ # Guard against malformed JSON (empty output or non-array)
93
+ if [ -z "$FINDINGS_JSON" ] || ! echo "$FINDINGS_JSON" | jq -e 'type == "array"' >/dev/null 2>&1; then
94
+ FINDINGS_JSON="[]"
95
+ fi
96
+
97
+ COUNT=$(echo "$FINDINGS_JSON" | jq 'length' 2>/dev/null || echo "0")
98
+ [ "$COUNT" = "0" ] && exit 0
99
+
100
+ # Apply allowlist filter — reuse existing allowlist file
101
+ ALLOWLIST="$PROJECT_DIR/.claude/hooks/shellcheck-allowlist.txt"
102
+ ALLOW_FILTER="[]"
103
+ if [ -f "$ALLOWLIST" ]; then
104
+ CANDIDATE=$(grep -v '^#' "$ALLOWLIST" | grep -v '^[[:space:]]*$' \
105
+ | jq -R -s -c 'split("\n") | map(select(length > 0))' 2>/dev/null || echo "[]")
106
+ if echo "$CANDIDATE" | jq -e 'type == "array"' >/dev/null 2>&1; then
107
+ ALLOW_FILTER="$CANDIDATE"
108
+ fi
109
+ fi
110
+
111
+ UNALLOWLISTED=$(echo "$FINDINGS_JSON" | jq --argjson allow "$ALLOW_FILTER" '
112
+ map(select(
113
+ ("SC" + (.code | tostring) + ":" + ((.file // "") | ltrimstr("./"))) as $key |
114
+ ([$key] | inside($allow)) | not
115
+ ))
116
+ ' 2>/dev/null || echo "[]")
117
+
118
+ if ! echo "$UNALLOWLISTED" | jq -e 'type == "array"' >/dev/null 2>&1; then
119
+ UNALLOWLISTED="[]"
120
+ fi
121
+
122
+ UCOUNT=$(echo "$UNALLOWLISTED" | jq 'length' 2>/dev/null || echo "0")
123
+ [ "$UCOUNT" = "0" ] && exit 0
124
+
125
+ # BLOCK
126
+ cat >&2 <<EOF
127
+
128
+ 🛑 pre-push-shellcheck — BLOCKED
129
+
130
+ $UCOUNT un-allowlisted shellcheck finding(s) in files going out:
131
+
132
+ EOF
133
+ echo "$UNALLOWLISTED" | jq -r '.[] | " \(.file):\(.line):\(.column) SC\(.code): \(.message)"' >&2
134
+ cat >&2 <<EOF
135
+
136
+ Resolution paths:
137
+ 1. Fix the finding in each listed file (preferred — e.g. 'cd \"\$dir\" || exit')
138
+ 2. Allowlist the SC<code>:<path> pair in .claude/hooks/shellcheck-allowlist.txt
139
+ 3. SKIP_PRE_PUSH_SHELLCHECK=1 git push ... (logged; rare)
140
+
141
+ Per .claude/rules/defensive-bash.md + bassclef-upstream#2108.
142
+
143
+ EOF
144
+ exit 2
@@ -0,0 +1,153 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # reviewer-dispatch-check.sh — Stop hook on code-shipping sessions
4
+ #
5
+ # Refuses to let a session end if it shipped source code (>0 src/ file
6
+ # changes per git diff) without a reviewer marker at
7
+ # state/markers/reviewer/<session-id>.md or *.md naming the session.
8
+ #
9
+ # Mode-agnostic: orchestrator-gated dispatches Reviewer agent; sequential
10
+ # mode self-invokes Reviewer responsibilities and writes the marker.
11
+ #
12
+ # Per bassclef#1146 + WU-12 of bet 2026-06-20e.
13
+ # Override: SKIP_REVIEWER_CHECK=1 (logged via trace-helper).
14
+
15
+ set -euo pipefail
16
+
17
+ # ============================================================================
18
+ # Read stdin JSON from Claude Code harness (Stop event payload)
19
+ # ============================================================================
20
+ PAYLOAD=$(cat 2>/dev/null || echo '{}')
21
+
22
+ # ============================================================================
23
+ # Override check (FIRST — before any work)
24
+ # ============================================================================
25
+ if [ "${SKIP_REVIEWER_CHECK:-0}" = "1" ]; then
26
+ TRACE_HELPER="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/trace-helper.sh"
27
+ if [ -f "$TRACE_HELPER" ]; then
28
+ bash "$TRACE_HELPER" override "reviewer-dispatch-check" "SKIP_REVIEWER_CHECK=1" 2>/dev/null || true
29
+ fi
30
+ exit 0
31
+ fi
32
+
33
+ # ============================================================================
34
+ # Repo root + check if we're in a git repo
35
+ # ============================================================================
36
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
37
+
38
+ # If REVIEWER_CHECK_REPO is set, override (for tests)
39
+ if [ -n "${REVIEWER_CHECK_REPO:-}" ]; then
40
+ REPO_ROOT="$REVIEWER_CHECK_REPO"
41
+ fi
42
+
43
+ if ! (cd "$REPO_ROOT" && git rev-parse --git-dir >/dev/null 2>&1); then
44
+ # Not a git repo — skip
45
+ exit 0
46
+ fi
47
+
48
+ # ============================================================================
49
+ # Detect source-file changes in this session
50
+ # Uses git diff against origin/main as proxy for session changes; this is a
51
+ # heuristic — a more precise version would track session-start ref.
52
+ # ============================================================================
53
+ cd "$REPO_ROOT"
54
+
55
+ # Define source-file path matchers — files that count as "shipped code"
56
+ SOURCE_MATCHERS=(
57
+ 'src/'
58
+ 'apps/'
59
+ 'packages/'
60
+ 'lib/'
61
+ 'app/'
62
+ )
63
+
64
+ # Count source files changed (uncommitted + committed-since-origin)
65
+ SHIPPED_SOURCE_COUNT=0
66
+
67
+ # Uncommitted changes
68
+ for matcher in "${SOURCE_MATCHERS[@]}"; do
69
+ count=$(git diff --name-only -- "$matcher" 2>/dev/null | wc -l | tr -d ' ')
70
+ SHIPPED_SOURCE_COUNT=$((SHIPPED_SOURCE_COUNT + count))
71
+ done
72
+
73
+ # Committed-since-origin/main
74
+ if git rev-parse origin/main >/dev/null 2>&1; then
75
+ for matcher in "${SOURCE_MATCHERS[@]}"; do
76
+ count=$(git diff --name-only origin/main..HEAD -- "$matcher" 2>/dev/null | wc -l | tr -d ' ')
77
+ SHIPPED_SOURCE_COUNT=$((SHIPPED_SOURCE_COUNT + count))
78
+ done
79
+ fi
80
+
81
+ # No source files changed — session is non-code (docs, config, etc.); pass
82
+ if [ "$SHIPPED_SOURCE_COUNT" -eq 0 ]; then
83
+ exit 0
84
+ fi
85
+
86
+ # ============================================================================
87
+ # Marker check: state/markers/reviewer/*.md
88
+ # ============================================================================
89
+ MARKER_DIR="$REPO_ROOT/state/markers/reviewer"
90
+
91
+ if [ -d "$MARKER_DIR" ] && find "$MARKER_DIR" -maxdepth 1 -name "*.md" 2>/dev/null | grep -q .; then
92
+ # At least one reviewer marker exists — pass
93
+ exit 0
94
+ fi
95
+
96
+ # ============================================================================
97
+ # Block: source shipped + no reviewer marker
98
+ # ============================================================================
99
+ cat >&2 <<EOF
100
+ 🛑 reviewer-dispatch-check — BLOCK (bassclef#1146)
101
+
102
+ This session shipped source code (${SHIPPED_SOURCE_COUNT} source files
103
+ changed across src/ apps/ packages/ lib/ app/) but no reviewer marker
104
+ exists at state/markers/reviewer/.
105
+
106
+ Reviewer responsibilities (per ~/.claude/agents/reviewer.md):
107
+ - Verify spec acceptance criteria met
108
+ - Verify tests present + green
109
+ - Check for TDD test-after pattern (Kent Beck)
110
+ - Verify ADR compliance for architectural decisions
111
+ - Verify Designer sign-off for UX changes
112
+ - Verify /architect-review invoked for Construction work
113
+
114
+ Resolve via one of:
115
+
116
+ 1. Orchestrator-gated mode — dispatch Reviewer agent + capture marker:
117
+ /reviewer-pass
118
+ (or invoke via your orchestrator's handoff to Reviewer)
119
+
120
+ 2. Sequential mode — self-invoke Reviewer responsibilities + write marker:
121
+ mkdir -p $MARKER_DIR
122
+ cat > $MARKER_DIR/<session-id>.md <<MARKER
123
+ # Reviewer pass — <session-id>
124
+
125
+ mode: sequential
126
+ reviewed_at: $(date -u +%Y-%m-%dT%H:%M:%SZ)
127
+ source_files_changed: $SHIPPED_SOURCE_COUNT
128
+
129
+ ## Checks
130
+ - spec acceptance criteria: PASS | FAIL | N/A
131
+ - tests present + green: PASS | FAIL
132
+ - test-after pattern check: NONE FOUND | FOUND (list)
133
+ - ADR compliance: PASS | N/A
134
+ - Designer sign-off: PRESENT | N/A
135
+ - architect-review: INVOKED | N/A
136
+
137
+ ## Findings
138
+ - <severity>: <description> # BLOCK / WARN / NOTE
139
+
140
+ acknowledged: true
141
+ MARKER
142
+
143
+ 3. Defer to next session — write marker NOW with "deferred to next session"
144
+ note + commit; resolve at next session start per deferred-actions protocol.
145
+
146
+ Override (logged): SKIP_REVIEWER_CHECK=1 <command>
147
+
148
+ When to use override:
149
+ - Documentation-only session that accidentally touched a src/ comment
150
+ - Emergency rescue when hook itself misclassifies
151
+ EOF
152
+
153
+ exit 2