@thebassclef/lite 1.1.1 → 1.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +23 -6
- package/dist/cli.js +23 -6
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/bassclef-configs.jsonc +56 -0
- package/dist/lite/.claude/bassclef-orientation.md +120 -0
- package/dist/lite/.claude/hooks/adr-discipline-check.sh +132 -0
- package/dist/lite/.claude/hooks/atomic-pr-check.sh +169 -0
- package/dist/lite/.claude/hooks/auto-save-idle.sh +51 -0
- package/dist/lite/.claude/hooks/bassclef-sync.sh +51 -13
- package/dist/lite/.claude/hooks/bet-doc-gate.sh +283 -0
- package/dist/lite/.claude/hooks/bug-diagnosis.sh +64 -0
- package/dist/lite/.claude/hooks/destructive-command-guard.sh +136 -0
- package/dist/lite/.claude/hooks/file-rename-discipline-check.sh +147 -0
- package/dist/lite/.claude/hooks/hook-classifier-canary.sh +158 -0
- package/dist/lite/.claude/hooks/kiss-default-check.sh +129 -0
- package/dist/lite/.claude/hooks/multi-tenancy-check.sh +83 -0
- package/dist/lite/.claude/hooks/oo-ad-entry-point-check.sh +216 -0
- package/dist/lite/.claude/hooks/operator-private-path-check.sh +203 -0
- package/dist/lite/.claude/hooks/pattern-annotation-validate.sh +142 -0
- package/dist/lite/.claude/hooks/plain-english-steering.sh +15 -1
- package/dist/lite/.claude/hooks/post-write-trace.sh +66 -0
- package/dist/lite/.claude/hooks/pr-body-scrub-check.sh +446 -0
- package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +137 -0
- package/dist/lite/.claude/hooks/pre-gh-pr-body-scrub.sh +192 -0
- package/dist/lite/.claude/hooks/pre-gh-pr-title-scrub.sh +172 -0
- package/dist/lite/.claude/hooks/role-entry-gate.sh +115 -0
- package/dist/lite/.claude/hooks/save-state.sh +268 -0
- package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +8 -1
- package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +52 -0
- package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +40 -4
- package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +12 -5
- package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +18 -2
- package/dist/lite/.claude/hooks/session-reflection.d/91-npm-version-drift.sh +54 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/10-abrupt-stop-recovery.test.sh +95 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +62 -15
- package/dist/lite/.claude/hooks/tool-failure-diagnosis.sh +50 -0
- package/dist/lite/.claude/luminaries/andrej-karpathy.md +165 -0
- package/dist/lite/.claude/luminaries/dave-gray.md +155 -0
- package/dist/lite/.claude/luminaries/gary-klein.md +166 -0
- package/dist/lite/.claude/luminaries/kaoru-ishikawa.md +162 -0
- package/dist/lite/.claude/luminaries/lean-product-canvas.md +33 -0
- package/dist/lite/.claude/luminaries/luke-wroblewski.md +224 -0
- package/dist/lite/.claude/luminaries/ryan-singer.md +113 -0
- package/dist/lite/.claude/luminaries/steve-krug.md +168 -0
- package/dist/lite/.claude/luminaries/tony-ulwick.md +134 -0
- package/dist/lite/.claude/rules/arc-level-orientation-before-leaf.md +60 -0
- package/dist/lite/.claude/rules/architect-review-discipline.md +71 -0
- package/dist/lite/.claude/rules/auto-fix-criteria.md +126 -0
- package/dist/lite/.claude/rules/bassclef-hook-drift-blocker.md +144 -0
- package/dist/lite/.claude/rules/bassclef-models-discovery.md +107 -0
- package/dist/lite/.claude/rules/bassclef-workflow-parity.md +104 -0
- package/dist/lite/.claude/rules/cost-governance.md +154 -0
- package/dist/lite/.claude/rules/file-rename-discipline.md +95 -0
- package/dist/lite/.claude/rules/mechanism-fidelity.md +88 -0
- package/dist/lite/.claude/rules/prototype-production-coupling.md +125 -0
- package/dist/lite/.claude/rules/quick-fix-criteria.md +90 -0
- package/dist/lite/.claude/rules/session-artifacts.md +30 -0
- package/dist/lite/.claude/rules/sibling-smoke-after-substrate-change.md +107 -0
- package/dist/lite/.claude/rules/substrate-as-system.md +67 -0
- package/dist/lite/.claude/rules/usability.md +22 -0
- package/dist/lite/.claude/settings.json +20 -0
- package/dist/lite/.claude/skills/agent-research-spawn/SKILL.md +100 -0
- package/dist/lite/.claude/skills/architect-review/SKILL.md +201 -0
- package/dist/lite/.claude/skills/auto-fix/SKILL.md +297 -0
- package/dist/lite/.claude/skills/autonomous/SKILL.md +300 -0
- package/dist/lite/.claude/skills/bind-subdomain/SKILL.md +232 -0
- package/dist/lite/.claude/skills/canvas/SKILL.md +41 -0
- package/dist/lite/.claude/skills/deploy-prod/SKILL.md +106 -0
- package/dist/lite/.claude/skills/docs-sync/SKILL.md +188 -0
- package/dist/lite/.claude/skills/eisenhower/SKILL.md +273 -0
- package/dist/lite/.claude/skills/empathy-map/SKILL.md +168 -0
- package/dist/lite/.claude/skills/frontend-design/SKILL.md +53 -0
- package/dist/lite/.claude/skills/interview-me/SKILL.md +171 -0
- package/dist/lite/.claude/skills/journal-export/SKILL.md +293 -0
- package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +203 -0
- package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +256 -0
- package/dist/lite/.claude/skills/pick-luminaries/SKILL.md +100 -0
- package/dist/lite/.claude/skills/pre-mortem/SKILL.md +180 -0
- package/dist/lite/.claude/skills/prototype-gallery/SKILL.md +264 -0
- package/dist/lite/.claude/skills/provision-deploy-host/SKILL.md +257 -0
- package/dist/lite/.claude/skills/quick-fix/SKILL.md +273 -0
- package/dist/lite/.claude/skills/release/SKILL.md +311 -0
- package/dist/lite/.claude/skills/release-notes/SKILL.md +202 -0
- package/dist/lite/.claude/skills/rfc/SKILL.md +222 -0
- package/dist/lite/.claude/skills/riff-prototypes/SKILL.md +637 -0
- package/dist/lite/.claude/skills/secrets-bootstrap/SKILL.md +209 -0
- package/dist/lite/.claude/skills/skill/SKILL.md +304 -0
- package/dist/lite/.claude/skills/smoke-test/SKILL.md +100 -0
- package/dist/lite/.claude/skills/state/SKILL.md +204 -0
- package/dist/lite/.claude/skills/substrate-check/SKILL.md +218 -0
- package/dist/lite/.claude/skills/synthetic-user/SKILL.md +381 -0
- package/dist/lite/.claude/skills/task-scenarios/SKILL.md +105 -0
- package/dist/lite/.claude/skills/tier-dependency-audit/SKILL.md +139 -0
- package/dist/lite/.claude/skills/ux-migration/SKILL.md +424 -0
- package/dist/lite/.claude/skills/value-prop-canvas/SKILL.md +297 -0
- package/dist/lite/README.md +5 -1
- package/dist/lite/gitignore +6 -0
- package/dist/lite/lib/bassclef-dir-resolver.sh +85 -0
- package/dist/lite/lib/clone-failure-classifier.sh +59 -0
- package/dist/lite/lib/fixture-builder.sh +190 -0
- package/dist/lite/lib/fresh-install-check.sh +49 -0
- package/dist/lite/lib/hook-heartbeat.sh +0 -1
- package/dist/lite/lib/mechanism-fidelity.sh +569 -0
- package/dist/lite/lib/npm-version-check.sh +112 -0
- package/dist/lite/lib/preview-state.sh +229 -0
- package/dist/lite/lib/session-rescue.sh +0 -1
- package/dist/lite/scripts/generate-lite-manifest.sh +145 -4
- package/dist/lite/scripts/lite-manifest-drift-check.sh +5 -1
- package/dist/lite/standards/architect-review-discipline.md +222 -0
- package/dist/lite/standards/bassclef-platform.md +249 -0
- package/dist/lite/standards/bassclef-test-repos.md +97 -0
- package/dist/lite/standards/bassclef-wiring-manifest.json +25 -1
- package/dist/lite/standards/cold-adopter-harness.md +205 -0
- package/dist/lite/standards/data-management.md +135 -0
- package/dist/lite/standards/frontend-stack/nextjs-prisma.md +124 -0
- package/dist/lite/standards/lite-manifest-schema-changes.md +27 -0
- package/dist/lite/standards/lite-manifest.json +1456 -14
- package/dist/lite/standards/mechanism-fidelity.md +205 -0
- package/dist/lite/standards/operator-private-discipline.md +200 -0
- package/dist/lite/standards/operator-to-public-flow.md +195 -0
- package/dist/lite/standards/prototype-gallery.md +251 -0
- package/dist/lite/standards/pseudonym-key-schema.md +109 -0
- package/dist/lite/standards/sibling-validation-checklist.md +181 -0
- package/dist/lite/standards/slack-notifications.md +161 -0
- package/dist/lite/standards/synthetic-user-testing.md +173 -0
- package/dist/lite/standards/vendor-bindings.md +165 -0
- package/dist/lite/standards/vendor-plugins/frontend-design.md +66 -0
- package/dist/lite/standards/vpc-fit-validation.md +167 -0
- package/dist/lite/templates/gallery-yml.md +113 -0
- package/dist/lite/templates/product-canvas.md +169 -0
- package/dist/lite/templates/value-prop-canvas.md +180 -0
- package/package.json +3 -3
- /package/dist/lite/{whereami.md → docs/whereami.md} +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# pre-gh-pr-body-scrub — PreToolUse hook that runs the body scrub logic
|
|
4
|
+
# LOCALLY before a `gh pr create`, `gh pr edit --body`, `gh issue create`,
|
|
5
|
+
# or `gh issue edit --body` Bash invocation reaches GitHub. Sister to
|
|
6
|
+
# atomic-pr-check.sh (Bash-matcher pattern); reuses pr-body-scrub-check.sh
|
|
7
|
+
# (the CI scan logic) for shared rule semantics.
|
|
8
|
+
#
|
|
9
|
+
# bassclef#1176 (V1) — closed the gap PR #1172 demonstrated for PR bodies.
|
|
10
|
+
# bassclef#1177 (V2) — extends the same hook to gh issue bodies. The
|
|
11
|
+
# filename keeps the V1 name for git-history continuity; the matcher covers
|
|
12
|
+
# both surfaces. Commit-message bodies are covered by the sibling hook
|
|
13
|
+
# pre-git-commit-msg-scrub.sh.
|
|
14
|
+
#
|
|
15
|
+
# Exit codes:
|
|
16
|
+
# 0 — allow (non-matching Bash, no body extractable, clean body, or override)
|
|
17
|
+
# 2 — block (Rule 1 findings present in the extracted body)
|
|
18
|
+
#
|
|
19
|
+
# Override: SKIP_PRE_GH_PR_BODY_SCRUB=1 (logged via trace-helper)
|
|
20
|
+
|
|
21
|
+
set +e # never crash the tool call; fall through to allow on any unexpected error
|
|
22
|
+
|
|
23
|
+
INPUT=$(cat 2>/dev/null)
|
|
24
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
25
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
|
|
26
|
+
|
|
27
|
+
# Only fire on Bash
|
|
28
|
+
if [ "$TOOL_NAME" != "Bash" ]; then
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Match the surfaces this hook covers: gh pr create + gh pr edit (V1) and
|
|
33
|
+
# gh issue create + gh issue edit (V2 per bassclef#1177) and curl REST
|
|
34
|
+
# equivalents (V3 per bassclef#1239 — closes the curl REST bypass gap).
|
|
35
|
+
IS_CURL=0
|
|
36
|
+
case "$COMMAND" in
|
|
37
|
+
*"gh pr create"*|*"gh pr edit"*|*"gh issue create"*|*"gh issue edit"*)
|
|
38
|
+
: # match — continue to body extraction
|
|
39
|
+
;;
|
|
40
|
+
*curl*api.github.com*/pulls*|*curl*api.github.com*/issues*)
|
|
41
|
+
case "$COMMAND" in
|
|
42
|
+
*"-X POST"*|*"-X PATCH"*|*"--request POST"*|*"--request PATCH"*)
|
|
43
|
+
IS_CURL=1
|
|
44
|
+
;;
|
|
45
|
+
*)
|
|
46
|
+
exit 0 # read-only curl
|
|
47
|
+
;;
|
|
48
|
+
esac
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
exit 0
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
# Override hatch
|
|
56
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
|
|
57
|
+
if [ "${SKIP_PRE_GH_PR_BODY_SCRUB:-0}" = "1" ]; then
|
|
58
|
+
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/trace-helper.sh" ]; then
|
|
59
|
+
source "$SCRIPT_DIR/trace-helper.sh" 2>/dev/null || true
|
|
60
|
+
trace_log "pre-gh-pr-body-scrub" "OVERRIDE gh pr create|edit" 2>/dev/null || true
|
|
61
|
+
fi
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Locate the shared scrub script
|
|
66
|
+
SCRUB_SCRIPT="${SCRIPT_DIR}/pr-body-scrub-check.sh"
|
|
67
|
+
if [ ! -f "$SCRUB_SCRIPT" ]; then
|
|
68
|
+
# Shared scrub missing — degrade to advisory (allow)
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Extract body content from the command. Three forms in order of precedence:
|
|
73
|
+
# 1. Heredoc: --body "$(cat <<EOF ... EOF)" or <<'EOF' ... EOF
|
|
74
|
+
# 2. --body-file <path>
|
|
75
|
+
# 3. Inline: --body "..." or --body '...'
|
|
76
|
+
TMP_BODY=$(mktemp 2>/dev/null) || exit 0
|
|
77
|
+
trap 'rm -f "$TMP_BODY"' EXIT
|
|
78
|
+
|
|
79
|
+
EXTRACTED=$(COMMAND="$COMMAND" IS_CURL="$IS_CURL" python3 -c '
|
|
80
|
+
import os, re, sys, json
|
|
81
|
+
cmd = os.environ.get("COMMAND", "")
|
|
82
|
+
is_curl = os.environ.get("IS_CURL", "0") == "1"
|
|
83
|
+
|
|
84
|
+
if is_curl:
|
|
85
|
+
# curl REST shape — extract body from --data @<file> or -d "<inline>"
|
|
86
|
+
# bassclef#1239
|
|
87
|
+
m = re.search(r"(?:--data|-d)\s+@(\S+)", cmd)
|
|
88
|
+
if m:
|
|
89
|
+
path = m.group(1)
|
|
90
|
+
try:
|
|
91
|
+
with open(path) as f:
|
|
92
|
+
payload = json.load(f)
|
|
93
|
+
body = payload.get("body", "")
|
|
94
|
+
if body:
|
|
95
|
+
sys.stdout.write(body)
|
|
96
|
+
sys.exit(0)
|
|
97
|
+
except (OSError, json.JSONDecodeError):
|
|
98
|
+
pass
|
|
99
|
+
m = re.search(r"(?:--data|-d)\s+\x27([^\x27]*)\x27", cmd, re.DOTALL)
|
|
100
|
+
if m:
|
|
101
|
+
try:
|
|
102
|
+
payload = json.loads(m.group(1))
|
|
103
|
+
body = payload.get("body", "")
|
|
104
|
+
if body:
|
|
105
|
+
sys.stdout.write(body)
|
|
106
|
+
sys.exit(0)
|
|
107
|
+
except json.JSONDecodeError:
|
|
108
|
+
pass
|
|
109
|
+
m = re.search(r"(?:--data|-d)\s+\"((?:[^\"\\\\]|\\\\.)*)\"", cmd, re.DOTALL)
|
|
110
|
+
if m:
|
|
111
|
+
try:
|
|
112
|
+
payload = json.loads(m.group(1).replace("\\\"", "\""))
|
|
113
|
+
body = payload.get("body", "")
|
|
114
|
+
if body:
|
|
115
|
+
sys.stdout.write(body)
|
|
116
|
+
sys.exit(0)
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
pass
|
|
119
|
+
# No body extractable from curl payload — nothing to scan
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
|
|
122
|
+
# gh shape: existing extraction paths
|
|
123
|
+
# 1. Heredoc body (handles quoted or unquoted marker)
|
|
124
|
+
m = re.search(r"cat\s*<<\s*[\x27\"]?(\w+)[\x27\"]?\s*\n(.*?)\n\1", cmd, re.DOTALL)
|
|
125
|
+
if m:
|
|
126
|
+
sys.stdout.write(m.group(2))
|
|
127
|
+
sys.exit(0)
|
|
128
|
+
# 2. --body-file <path>
|
|
129
|
+
m = re.search(r"--body-file\s+(\S+)", cmd)
|
|
130
|
+
if m:
|
|
131
|
+
path = m.group(1).strip("\x27\"")
|
|
132
|
+
try:
|
|
133
|
+
with open(path) as f:
|
|
134
|
+
sys.stdout.write(f.read())
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
sys.exit(0)
|
|
138
|
+
# 3. Inline --body "..." (handle escaped quotes)
|
|
139
|
+
m = re.search(r"--body\s+\"((?:[^\"\\\\]|\\\\.)*)\"", cmd, re.DOTALL)
|
|
140
|
+
if m:
|
|
141
|
+
sys.stdout.write(m.group(1))
|
|
142
|
+
sys.exit(0)
|
|
143
|
+
m = re.search(r"--body\s+\x27((?:[^\x27\\\\]|\\\\.)*)\x27", cmd, re.DOTALL)
|
|
144
|
+
if m:
|
|
145
|
+
sys.stdout.write(m.group(1))
|
|
146
|
+
sys.exit(0)
|
|
147
|
+
# Parse failure — emit nothing; caller treats as advisory pass
|
|
148
|
+
' 2>/dev/null)
|
|
149
|
+
|
|
150
|
+
if [ -z "$EXTRACTED" ]; then
|
|
151
|
+
# Parse failure — fall back to advisory (allow). The CI hook is the backstop.
|
|
152
|
+
exit 0
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
printf '%s' "$EXTRACTED" > "$TMP_BODY"
|
|
156
|
+
|
|
157
|
+
# Extract PR title for Rule 7 (bassclef#974) — check for Closes-keyword when
|
|
158
|
+
# title carries feat(#NNN) or fix(#NNN). Advisory only in V1.
|
|
159
|
+
PR_TITLE=$(COMMAND="$COMMAND" python3 -c '
|
|
160
|
+
import os, re, sys
|
|
161
|
+
cmd = os.environ.get("COMMAND", "")
|
|
162
|
+
m = re.search(r"--title\s+\"([^\"]+)\"", cmd) or re.search(r"--title\s+\x27([^\x27]+)\x27", cmd)
|
|
163
|
+
if m:
|
|
164
|
+
sys.stdout.write(m.group(1))
|
|
165
|
+
' 2>/dev/null)
|
|
166
|
+
|
|
167
|
+
# Run the shared scrub — exit 1 means Rule 1 findings (CI's blocking semantic)
|
|
168
|
+
PR_TITLE="$PR_TITLE" bash "$SCRUB_SCRIPT" "$TMP_BODY" >&2 2>&1
|
|
169
|
+
SCRUB_RC=$?
|
|
170
|
+
|
|
171
|
+
if [ "$SCRUB_RC" = "1" ]; then
|
|
172
|
+
cat >&2 <<MSG
|
|
173
|
+
|
|
174
|
+
============================================
|
|
175
|
+
🛑 pre-gh-pr-body-scrub — BLOCKED LOCALLY
|
|
176
|
+
============================================
|
|
177
|
+
|
|
178
|
+
The PR body extracted from the gh command contains bassclef-internal
|
|
179
|
+
jargon (Rule 1). The CI hook would have caught this after push; this
|
|
180
|
+
local hook catches it before the call reaches GitHub.
|
|
181
|
+
|
|
182
|
+
Rewrite the body using plain-English alternatives from
|
|
183
|
+
standards/bassclef-internal-jargon.md.
|
|
184
|
+
|
|
185
|
+
Override (logged): SKIP_PRE_GH_PR_BODY_SCRUB=1 <command>
|
|
186
|
+
Per bassclef#1176.
|
|
187
|
+
|
|
188
|
+
MSG
|
|
189
|
+
exit 2
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
exit 0
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# pre-gh-pr-title-scrub — PreToolUse hook that runs the body scrub logic
|
|
4
|
+
# LOCALLY against the --title argument of a `gh pr create` or
|
|
5
|
+
# `gh pr edit --title` Bash invocation. Sister to pre-gh-pr-body-scrub.sh
|
|
6
|
+
# (bassclef#1176) and pre-git-commit-msg-scrub.sh (bassclef#1177);
|
|
7
|
+
# extends the same Bash-matcher + shared-scrub pattern to the title surface.
|
|
8
|
+
#
|
|
9
|
+
# bassclef#1201 — closes the gap that PR body + commit message had covered
|
|
10
|
+
# but PR title had not. Titles are the most-scanned text in any PR list;
|
|
11
|
+
# jargon there compounds across every operator who scrolls.
|
|
12
|
+
#
|
|
13
|
+
# Exit codes:
|
|
14
|
+
# 0 — allow (non-matching Bash, no --title in command, clean title, or override)
|
|
15
|
+
# 2 — block (Rule 1 findings present in the extracted title)
|
|
16
|
+
#
|
|
17
|
+
# Override: SKIP_PRE_GH_PR_TITLE_SCRUB=1 (logged via trace-helper)
|
|
18
|
+
|
|
19
|
+
set +e # never crash the tool call; fall through to allow on any unexpected error
|
|
20
|
+
|
|
21
|
+
INPUT=$(cat 2>/dev/null)
|
|
22
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
23
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
|
|
24
|
+
|
|
25
|
+
# Only fire on Bash
|
|
26
|
+
if [ "$TOOL_NAME" != "Bash" ]; then
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Match the surfaces this hook covers: gh pr create, gh pr edit, and
|
|
31
|
+
# curl REST equivalents (POST/PATCH against /pulls or /issues).
|
|
32
|
+
# bassclef#1239 — curl REST fallback used to bypass every scrub hook when
|
|
33
|
+
# GraphQL rate-limit exhausted gh CLI.
|
|
34
|
+
IS_CURL=0
|
|
35
|
+
case "$COMMAND" in
|
|
36
|
+
*"gh pr create"*|*"gh pr edit"*)
|
|
37
|
+
: # match — continue to title extraction
|
|
38
|
+
;;
|
|
39
|
+
*curl*api.github.com*/pulls*|*curl*api.github.com*/issues*)
|
|
40
|
+
# curl REST shape — verify it's a write (POST or PATCH), not GET
|
|
41
|
+
case "$COMMAND" in
|
|
42
|
+
*"-X POST"*|*"-X PATCH"*|*"--request POST"*|*"--request PATCH"*)
|
|
43
|
+
IS_CURL=1
|
|
44
|
+
;;
|
|
45
|
+
*)
|
|
46
|
+
exit 0 # read-only curl (GET / HEAD) — nothing to scan
|
|
47
|
+
;;
|
|
48
|
+
esac
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
exit 0
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
# Override hatch
|
|
56
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
|
|
57
|
+
if [ "${SKIP_PRE_GH_PR_TITLE_SCRUB:-0}" = "1" ]; then
|
|
58
|
+
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/trace-helper.sh" ]; then
|
|
59
|
+
source "$SCRIPT_DIR/trace-helper.sh" 2>/dev/null || true
|
|
60
|
+
trace_log "pre-gh-pr-title-scrub" "OVERRIDE gh pr create|edit" 2>/dev/null || true
|
|
61
|
+
fi
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Locate the shared scrub script
|
|
66
|
+
SCRUB_SCRIPT="${SCRIPT_DIR}/pr-body-scrub-check.sh"
|
|
67
|
+
if [ ! -f "$SCRUB_SCRIPT" ]; then
|
|
68
|
+
# Shared scrub missing — degrade to advisory (allow)
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Extract title content from the command. Two quote forms:
|
|
73
|
+
# 1. Double-quoted: --title "..."
|
|
74
|
+
# 2. Single-quoted: --title '...'
|
|
75
|
+
# If no --title flag present, exit 0 (interactive title flow; nothing to scan).
|
|
76
|
+
TMP_TITLE=$(mktemp -t title-scrub.XXXXXX 2>/dev/null) || exit 0
|
|
77
|
+
trap 'rm -f "$TMP_TITLE"' EXIT
|
|
78
|
+
|
|
79
|
+
EXTRACTED=$(COMMAND="$COMMAND" IS_CURL="$IS_CURL" python3 -c '
|
|
80
|
+
import os, re, sys, json
|
|
81
|
+
cmd = os.environ.get("COMMAND", "")
|
|
82
|
+
is_curl = os.environ.get("IS_CURL", "0") == "1"
|
|
83
|
+
|
|
84
|
+
if is_curl:
|
|
85
|
+
# curl REST shape — extract title from --data @<file> or -d "<inline>"
|
|
86
|
+
# bassclef#1239
|
|
87
|
+
# First try: --data @<path>
|
|
88
|
+
m = re.search(r"(?:--data|-d)\s+@(\S+)", cmd)
|
|
89
|
+
if m:
|
|
90
|
+
path = m.group(1)
|
|
91
|
+
try:
|
|
92
|
+
with open(path) as f:
|
|
93
|
+
payload = json.load(f)
|
|
94
|
+
title = payload.get("title", "")
|
|
95
|
+
if title:
|
|
96
|
+
sys.stdout.write(title)
|
|
97
|
+
sys.exit(0)
|
|
98
|
+
except (OSError, json.JSONDecodeError):
|
|
99
|
+
pass
|
|
100
|
+
# Try inline -d single-quoted JSON
|
|
101
|
+
m = re.search(r"(?:--data|-d)\s+\x27([^\x27]*)\x27", cmd, re.DOTALL)
|
|
102
|
+
if m:
|
|
103
|
+
try:
|
|
104
|
+
payload = json.loads(m.group(1))
|
|
105
|
+
title = payload.get("title", "")
|
|
106
|
+
if title:
|
|
107
|
+
sys.stdout.write(title)
|
|
108
|
+
sys.exit(0)
|
|
109
|
+
except json.JSONDecodeError:
|
|
110
|
+
pass
|
|
111
|
+
# Try inline -d double-quoted JSON
|
|
112
|
+
m = re.search(r"(?:--data|-d)\s+\"((?:[^\"\\\\]|\\\\.)*)\"", cmd, re.DOTALL)
|
|
113
|
+
if m:
|
|
114
|
+
try:
|
|
115
|
+
payload = json.loads(m.group(1).replace("\\\"", "\""))
|
|
116
|
+
title = payload.get("title", "")
|
|
117
|
+
if title:
|
|
118
|
+
sys.stdout.write(title)
|
|
119
|
+
sys.exit(0)
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
pass
|
|
122
|
+
# No title extractable from curl payload — nothing to scan
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
# gh shape: extract --title argument
|
|
126
|
+
# Double-quoted --title "..."
|
|
127
|
+
m = re.search(r"--title\s+\"((?:[^\"\\\\]|\\\\.)*)\"", cmd, re.DOTALL)
|
|
128
|
+
if m:
|
|
129
|
+
sys.stdout.write(m.group(1))
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
# Single-quoted --title '"'"'...'"'"'
|
|
132
|
+
m = re.search(r"--title\s+\x27((?:[^\x27\\\\]|\\\\.)*)\x27", cmd, re.DOTALL)
|
|
133
|
+
if m:
|
|
134
|
+
sys.stdout.write(m.group(1))
|
|
135
|
+
sys.exit(0)
|
|
136
|
+
# No --title flag — nothing to scan
|
|
137
|
+
' 2>/dev/null)
|
|
138
|
+
|
|
139
|
+
if [ -z "$EXTRACTED" ]; then
|
|
140
|
+
# No title to scan (interactive flow, or --title absent) — allow
|
|
141
|
+
exit 0
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
printf '%s' "$EXTRACTED" > "$TMP_TITLE"
|
|
145
|
+
|
|
146
|
+
# Run the shared scrub with SCRUB_SURFACE=title so BLOCK-title terms fire
|
|
147
|
+
# (bassclef#1241 WU-2 — substrate-defect: blocks title surface, not body).
|
|
148
|
+
SCRUB_SURFACE=title bash "$SCRUB_SCRIPT" "$TMP_TITLE" >&2 2>&1
|
|
149
|
+
SCRUB_RC=$?
|
|
150
|
+
|
|
151
|
+
if [ "$SCRUB_RC" = "1" ]; then
|
|
152
|
+
cat >&2 <<MSG
|
|
153
|
+
|
|
154
|
+
============================================
|
|
155
|
+
🛑 pre-gh-pr-title-scrub — BLOCKED LOCALLY
|
|
156
|
+
============================================
|
|
157
|
+
|
|
158
|
+
The PR title extracted from the gh command contains bassclef-internal
|
|
159
|
+
jargon (Rule 1). Titles are the most-scanned text in any PR list;
|
|
160
|
+
jargon there compounds across every operator who scrolls.
|
|
161
|
+
|
|
162
|
+
Rewrite the title using plain-English alternatives from
|
|
163
|
+
standards/bassclef-internal-jargon.md.
|
|
164
|
+
|
|
165
|
+
Override (logged): SKIP_PRE_GH_PR_TITLE_SCRUB=1 <command>
|
|
166
|
+
Per bassclef#1201.
|
|
167
|
+
|
|
168
|
+
MSG
|
|
169
|
+
exit 2
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
exit 0
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# role-entry-gate.sh — mode-agnostic role-entry discipline (bassclef#1145 T13)
|
|
4
|
+
#
|
|
5
|
+
# Fires on PreToolUse Edit|Write|MultiEdit. Detects work-shape from file_path
|
|
6
|
+
# and BLOCKs when the matching agent role marker is absent for the current
|
|
7
|
+
# session.
|
|
8
|
+
#
|
|
9
|
+
# MVP scope: Builder role only (source code matching). Architect / Reviewer /
|
|
10
|
+
# Designer role detection deferred to a follow-on ticket — the design choices
|
|
11
|
+
# on multi-role matching deserve their own pass.
|
|
12
|
+
#
|
|
13
|
+
# Marker convention:
|
|
14
|
+
# $CWD/state/markers/role/<role>/<session_id>.marker
|
|
15
|
+
#
|
|
16
|
+
# The marker is written by the agent (or the operator-invoked /role-entry
|
|
17
|
+
# skill, deferred to follow-on) once the role's persona has been loaded.
|
|
18
|
+
# This hook reads the marker; it does not write.
|
|
19
|
+
#
|
|
20
|
+
# Mode-agnostic behavior:
|
|
21
|
+
# - sequential mode: BLOCK message tells Claude to read the agent persona
|
|
22
|
+
# itself, write the marker, then proceed
|
|
23
|
+
# - orchestrator-gated mode: BLOCK message tells Orchestrator to route the
|
|
24
|
+
# next-step dispatch to the matching role agent
|
|
25
|
+
#
|
|
26
|
+
# Exit codes:
|
|
27
|
+
# 0 — pass (override, skip, marker present)
|
|
28
|
+
# 3 — BLOCK (source-code edit without Builder marker)
|
|
29
|
+
#
|
|
30
|
+
# Override: SKIP_ROLE_ENTRY=1 — logged via trace-helper (bassclef idiom).
|
|
31
|
+
|
|
32
|
+
set -u
|
|
33
|
+
|
|
34
|
+
# --- Override path ---
|
|
35
|
+
if [ "${SKIP_ROLE_ENTRY:-0}" = "1" ]; then
|
|
36
|
+
echo "SKIP_ROLE_ENTRY=1 — role-entry gate bypassed" >&2
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# --- Read stdin (fail-soft on malformed JSON per bassclef#1101 pattern) ---
|
|
41
|
+
INPUT=$(cat)
|
|
42
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
|
|
43
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
|
|
44
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null || echo "unknown")
|
|
45
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
|
|
46
|
+
|
|
47
|
+
# Malformed JSON or missing tool_name → pass silently (no work-shape signal)
|
|
48
|
+
if [ -z "$TOOL_NAME" ]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# --- Tool-name matcher ---
|
|
53
|
+
case "$TOOL_NAME" in
|
|
54
|
+
Edit|Write|MultiEdit) ;;
|
|
55
|
+
*) exit 0 ;;
|
|
56
|
+
esac
|
|
57
|
+
|
|
58
|
+
# --- Path matcher: source code per common languages ---
|
|
59
|
+
# MVP language list per bassclef tech_stack vocabulary. Stack-bound matchers
|
|
60
|
+
# can be tightened later by reading tech_stack.lang from bassclef-configs.jsonc.
|
|
61
|
+
case "$FILE_PATH" in
|
|
62
|
+
*.ts|*.tsx|*.js|*.jsx|*.py|*.rb|*.go|*.rs|*.java|*.kt|*.swift|*.cs|*.cpp|*.c|*.h)
|
|
63
|
+
EXPECTED_ROLE="builder"
|
|
64
|
+
;;
|
|
65
|
+
*)
|
|
66
|
+
# Not a source file → no role discipline applies in MVP
|
|
67
|
+
exit 0
|
|
68
|
+
;;
|
|
69
|
+
esac
|
|
70
|
+
|
|
71
|
+
# --- Marker check ---
|
|
72
|
+
MARKER_PATH="${CWD}/state/markers/role/${EXPECTED_ROLE}/${SESSION_ID}.marker"
|
|
73
|
+
|
|
74
|
+
if [ -f "$MARKER_PATH" ]; then
|
|
75
|
+
# Marker present → pass. Freshness window check deferred to follow-on.
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# --- BLOCK ---
|
|
80
|
+
cat >&2 <<BLOCK
|
|
81
|
+
============================================
|
|
82
|
+
🛑 role-entry-gate — BLOCKED
|
|
83
|
+
============================================
|
|
84
|
+
|
|
85
|
+
About to do ${EXPECTED_ROLE}-shaped work (editing ${FILE_PATH}) but the
|
|
86
|
+
${EXPECTED_ROLE} agent role has not been entered for this session.
|
|
87
|
+
|
|
88
|
+
Expected marker (absent):
|
|
89
|
+
${MARKER_PATH}
|
|
90
|
+
|
|
91
|
+
Two paths forward depending on session mode:
|
|
92
|
+
|
|
93
|
+
Sequential mode (default):
|
|
94
|
+
1. Read ~/.claude/agents/${EXPECTED_ROLE}.md to load luminaries + responsibilities
|
|
95
|
+
2. Write the marker:
|
|
96
|
+
mkdir -p $(dirname "$MARKER_PATH")
|
|
97
|
+
cat > "$MARKER_PATH" <<EOF
|
|
98
|
+
{"role":"${EXPECTED_ROLE}","session_id":"${SESSION_ID}","entered_at":"\$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
|
|
99
|
+
EOF
|
|
100
|
+
3. Re-run the edit — the marker now satisfies the gate.
|
|
101
|
+
|
|
102
|
+
Orchestrator-gated mode:
|
|
103
|
+
Orchestrator routes this edit to the ${EXPECTED_ROLE} agent for
|
|
104
|
+
sandbox execution. The role agent writes its own marker before
|
|
105
|
+
proceeding.
|
|
106
|
+
|
|
107
|
+
Override (logged):
|
|
108
|
+
SKIP_ROLE_ENTRY=1 <command>
|
|
109
|
+
|
|
110
|
+
Per bassclef#1145 T13 (this hook) and .claude/agents/${EXPECTED_ROLE}.md (the
|
|
111
|
+
role persona this gate routes to).
|
|
112
|
+
============================================
|
|
113
|
+
BLOCK
|
|
114
|
+
|
|
115
|
+
exit 3
|