@windyroad/voice-tone 0.5.11 → 0.5.13
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.
|
@@ -185,28 +185,59 @@ except Exception:
|
|
|
185
185
|
DRAFT=$(printf '%s' "$COMMAND" | python3 -c "
|
|
186
186
|
import sys, re
|
|
187
187
|
cmd = sys.stdin.read()
|
|
188
|
-
#
|
|
188
|
+
# P364: bash double-quote unescape. The double-quoted body capture groups
|
|
189
|
+
# carry RAW shell-escaped command text — an orchestrator must backslash-escape
|
|
190
|
+
# backticks (and \$, \", \\) inside \"...\" to survive bash parsing, e.g.
|
|
191
|
+
# --body \"Fixed in \\\`code\\\` ...\". The PostToolUse mark hook hashes the
|
|
192
|
+
# LOGICAL <draft> body (plain backticks), so the gate must undo those escapes
|
|
193
|
+
# or the two marker keys diverge → permanent deny-after-PASS. Inside double
|
|
194
|
+
# quotes a backslash is special ONLY before \$ \` \" \\ or a newline (line
|
|
195
|
+
# continuation); single-quoted and <<'EOF' forms are literal and need none.
|
|
196
|
+
# Single left-to-right pass so an escaped backslash adjacent to another escape
|
|
197
|
+
# (\\\\\` -> backslash + backtick) is NOT mis-collapsed. chr() literals keep
|
|
198
|
+
# this source free of the very metacharacters the surrounding shell double
|
|
199
|
+
# quotes would otherwise eat.
|
|
200
|
+
def unescape_dq(s):
|
|
201
|
+
out = []
|
|
202
|
+
i = 0
|
|
203
|
+
n = len(s)
|
|
204
|
+
special = set([chr(36), chr(96), chr(34), chr(92), chr(10)])
|
|
205
|
+
while i < n:
|
|
206
|
+
if s[i] == chr(92) and i + 1 < n and s[i + 1] in special:
|
|
207
|
+
if s[i + 1] != chr(10):
|
|
208
|
+
out.append(s[i + 1])
|
|
209
|
+
i += 2
|
|
210
|
+
else:
|
|
211
|
+
out.append(s[i])
|
|
212
|
+
i += 1
|
|
213
|
+
return ''.join(out)
|
|
214
|
+
# (pattern, flags, unescape) — first match wins. unescape=True for the
|
|
215
|
+
# double-quoted forms only (P364).
|
|
189
216
|
patterns = [
|
|
190
217
|
# HEREDOC body — matches a here-doc with EOF delimiter (quoted or
|
|
191
218
|
# unquoted). The literal '<<' is written as the char-class pair
|
|
192
219
|
# [<][<] so bash's command-substitution parser does NOT mis-parse
|
|
193
220
|
# this regex as a real here-doc operator (P082 implementation note).
|
|
194
|
-
# DOTALL so the body can span newlines.
|
|
195
|
-
|
|
221
|
+
# DOTALL so the body can span newlines. Left literal: the AI-canonical
|
|
222
|
+
# form is the quoted <<'EOF' heredoc, whose body bash does not unescape.
|
|
223
|
+
(r\"[<][<]\s*['\\\"]?EOF['\\\"]?\s*\n(.*?)\nEOF\", re.DOTALL, False),
|
|
196
224
|
# gh issue/pr + npm publish --body 'TEXT' / --body \"TEXT\" (existing).
|
|
197
|
-
(r\"--body[= ]'([^']*)'\", 0),
|
|
198
|
-
(r'--body[= ]\"([^\"]*)\"', 0),
|
|
225
|
+
(r\"--body[= ]'([^']*)'\", 0, False),
|
|
226
|
+
(r'--body[= ]\"([^\"]*)\"', 0, True),
|
|
199
227
|
# gh api --field summary='TEXT' / --field summary=\"TEXT\" (existing).
|
|
200
|
-
(r\"--field [a-zA-Z_]+='([^']*)'\", 0),
|
|
201
|
-
(r'--field [a-zA-Z_]+=\"([^\"]*)\"', 0),
|
|
228
|
+
(r\"--field [a-zA-Z_]+='([^']*)'\", 0, False),
|
|
229
|
+
(r'--field [a-zA-Z_]+=\"([^\"]*)\"', 0, True),
|
|
202
230
|
# git commit -m / --message single-line literal forms (P082 Phase 1).
|
|
203
|
-
(r\"(?:-m|--message)[= ]'([^']*)'\", 0),
|
|
204
|
-
(r'(?:-m|--message)[= ]\"([^\"]*)\"', 0),
|
|
231
|
+
(r\"(?:-m|--message)[= ]'([^']*)'\", 0, False),
|
|
232
|
+
(r'(?:-m|--message)[= ]\"([^\"]*)\"', 0, True),
|
|
205
233
|
]
|
|
206
|
-
for pat, flags in patterns:
|
|
234
|
+
for pat, flags, unescape in patterns:
|
|
207
235
|
m = re.search(pat, cmd, flags)
|
|
208
236
|
if m:
|
|
209
|
-
|
|
237
|
+
body = m.group(1)
|
|
238
|
+
if unescape:
|
|
239
|
+
body = unescape_dq(body)
|
|
240
|
+
print(body)
|
|
210
241
|
break
|
|
211
242
|
" 2>/dev/null || echo "")
|
|
212
243
|
;;
|
|
@@ -287,6 +318,28 @@ if [ "$EXTERNAL_COMMS_LEAK_PREFILTER" = "yes" ]; then
|
|
|
287
318
|
fi
|
|
288
319
|
fi
|
|
289
320
|
|
|
321
|
+
# ---------- Repo-visibility precondition: git-commit-message surface (P365) ----------
|
|
322
|
+
# A commit message only becomes external-facing prose when it lands in a PUBLIC
|
|
323
|
+
# GitHub repo (git log / PR commits tab / release-page auto-notes / CHANGELOG).
|
|
324
|
+
# In private or internal repos the marker-review delegation deny below is a pure
|
|
325
|
+
# false-positive (P365 — user direction 2026-06-11: "this MUST NOT fire for
|
|
326
|
+
# private repos"). Confirm visibility authoritatively via gh and silent-pass the
|
|
327
|
+
# marker gate on any non-PUBLIC result. Any INDETERMINATE result (gh absent,
|
|
328
|
+
# unauthenticated, no remote, API error → empty $REPO_VISIBILITY) is treated as
|
|
329
|
+
# non-public: a commit message is only demonstrably external when the repo is
|
|
330
|
+
# confirmably PUBLIC, so the conservative direction for THIS surface is to not
|
|
331
|
+
# fire. This is a fail-open on the voice/tone-and-prose review ONLY — the
|
|
332
|
+
# leak-pattern pre-filter above (credentials / prod-URLs) has already run for
|
|
333
|
+
# every surface in every repo, so the high-stakes secrecy net is unaffected.
|
|
334
|
+
# Scoped to git-commit-message only; the gh-issue/pr/api, npm-publish, and
|
|
335
|
+
# changeset-author surfaces are inherently external and stay gated regardless.
|
|
336
|
+
if [ "$SURFACE" = "git-commit-message" ]; then
|
|
337
|
+
REPO_VISIBILITY=$(gh repo view --json visibility -q .visibility 2>/dev/null || echo "")
|
|
338
|
+
if [ "$REPO_VISIBILITY" != "PUBLIC" ]; then
|
|
339
|
+
exit 0
|
|
340
|
+
fi
|
|
341
|
+
fi
|
|
342
|
+
|
|
290
343
|
# ---------- Marker-based gate (per-evaluator marker per ADR-028 amended 2026-05-14) ----------
|
|
291
344
|
SESSION_DIR="${TMPDIR:-/tmp}/claude-risk-${SESSION_ID}"
|
|
292
345
|
mkdir -p "$SESSION_DIR"
|
|
@@ -61,9 +61,23 @@ print(json.dumps({
|
|
|
61
61
|
" "$file_path" "$content"
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
# Mock `gh repo view --json visibility` for the git-commit-message surface
|
|
65
|
+
# repo-visibility precondition (P365). vis ∈ {PUBLIC,PRIVATE,INTERNAL}; pass the
|
|
66
|
+
# literal "FAIL" to simulate gh absent / unauthenticated (non-zero exit).
|
|
67
|
+
mock_gh_visibility() {
|
|
68
|
+
local vis="$1"
|
|
69
|
+
mkdir -p "$TEST_PROJECT_DIR/mockbin"
|
|
70
|
+
if [ "$vis" = "FAIL" ]; then
|
|
71
|
+
printf '#!/usr/bin/env bash\nexit 1\n' > "$TEST_PROJECT_DIR/mockbin/gh"
|
|
72
|
+
else
|
|
73
|
+
printf '#!/usr/bin/env bash\necho %s\n' "$vis" > "$TEST_PROJECT_DIR/mockbin/gh"
|
|
74
|
+
fi
|
|
75
|
+
chmod +x "$TEST_PROJECT_DIR/mockbin/gh"
|
|
76
|
+
}
|
|
77
|
+
|
|
64
78
|
run_hook() {
|
|
65
79
|
local input="$1"
|
|
66
|
-
run bash -c "cd '$TEST_PROJECT_DIR' && printf '%s' \"\$1\" | '$HOOK'" _ "$input"
|
|
80
|
+
run bash -c "cd '$TEST_PROJECT_DIR' && export PATH='$TEST_PROJECT_DIR/mockbin':\$PATH && printf '%s' \"\$1\" | '$HOOK'" _ "$input"
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
# ---------- Tests ----------
|
|
@@ -242,6 +256,7 @@ run_hook() {
|
|
|
242
256
|
# ---------------------------------------------------------------------------
|
|
243
257
|
|
|
244
258
|
@test "P082: git commit -m with literal -m body denies and delegates to voice-tone evaluator" {
|
|
259
|
+
mock_gh_visibility PUBLIC
|
|
245
260
|
INPUT=$(build_bash_input "git commit -m \"I've implemented the feature\"")
|
|
246
261
|
run_hook "$INPUT"
|
|
247
262
|
[ "$status" -eq 0 ]
|
|
@@ -251,6 +266,7 @@ run_hook() {
|
|
|
251
266
|
}
|
|
252
267
|
|
|
253
268
|
@test "P082: git commit --message with literal body denies and delegates" {
|
|
269
|
+
mock_gh_visibility PUBLIC
|
|
254
270
|
INPUT=$(build_bash_input "git commit --message \"happy to help further with this fix\"")
|
|
255
271
|
run_hook "$INPUT"
|
|
256
272
|
[ "$status" -eq 0 ]
|
|
@@ -259,6 +275,7 @@ run_hook() {
|
|
|
259
275
|
}
|
|
260
276
|
|
|
261
277
|
@test "P082: git commit --amend -m is intercepted (P082 SC2)" {
|
|
278
|
+
mock_gh_visibility PUBLIC
|
|
262
279
|
INPUT=$(build_bash_input "git commit --amend -m \"rewritten subject\"")
|
|
263
280
|
run_hook "$INPUT"
|
|
264
281
|
[ "$status" -eq 0 ]
|
|
@@ -270,6 +287,7 @@ run_hook() {
|
|
|
270
287
|
# Build a HEREDOC-shaped command. The hook regex pulls the body BETWEEN
|
|
271
288
|
# the <<'EOF' opener and the closing EOF marker — the extracted DRAFT is
|
|
272
289
|
# the inner text, NOT the literal `$(cat <<'EOF' ... EOF)` wrapper.
|
|
290
|
+
mock_gh_visibility PUBLIC
|
|
273
291
|
BODY=$'feat(foo): add bar\n\nWe observed a build failure on Node 20.'
|
|
274
292
|
CMD=$'git commit -m "$(cat <<\'EOF\'\n'"$BODY"$'\nEOF\n)"'
|
|
275
293
|
INPUT=$(build_bash_input "$CMD")
|
|
@@ -312,6 +330,7 @@ run_hook() {
|
|
|
312
330
|
}
|
|
313
331
|
|
|
314
332
|
@test "P082: per-evaluator marker keyed on (body, git-commit-message) permits the call" {
|
|
333
|
+
mock_gh_visibility PUBLIC
|
|
315
334
|
BODY="docs(retro): close iter 3 ask-hygiene trail"
|
|
316
335
|
SURFACE="git-commit-message"
|
|
317
336
|
KEY=$(printf '%s\n%s' "$BODY" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
|
|
@@ -322,3 +341,90 @@ run_hook() {
|
|
|
322
341
|
[ "$status" -eq 0 ]
|
|
323
342
|
[ -z "$output" ]
|
|
324
343
|
}
|
|
344
|
+
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
# P365 — repo-visibility precondition on the git-commit-message surface.
|
|
347
|
+
# Shared canonical hook (ADR-017 sync), so the precondition applies to the
|
|
348
|
+
# voice-tone evaluator too: a commit message is external-facing prose ONLY in
|
|
349
|
+
# a PUBLIC repo. In private/internal repos — or any indeterminate gh result —
|
|
350
|
+
# the marker-review deny is a pure false-positive (user direction 2026-06-11:
|
|
351
|
+
# "this MUST NOT fire for private repos"). Scoped to the git-commit-message
|
|
352
|
+
# surface only; the gh-issue/pr/npm/changeset surfaces are inherently external
|
|
353
|
+
# and stay gated regardless of repo visibility.
|
|
354
|
+
# ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
@test "P365: git commit -m in a PRIVATE repo silent-passes (no external-comms deny)" {
|
|
357
|
+
mock_gh_visibility PRIVATE
|
|
358
|
+
INPUT=$(build_bash_input "git commit -m \"I've implemented the feature\"")
|
|
359
|
+
run_hook "$INPUT"
|
|
360
|
+
[ "$status" -eq 0 ]
|
|
361
|
+
[ -z "$output" ]
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
@test "P365: git commit -m in an INTERNAL repo silent-passes" {
|
|
365
|
+
mock_gh_visibility INTERNAL
|
|
366
|
+
INPUT=$(build_bash_input "git commit -m \"I've implemented the feature\"")
|
|
367
|
+
run_hook "$INPUT"
|
|
368
|
+
[ "$status" -eq 0 ]
|
|
369
|
+
[ -z "$output" ]
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
@test "P365: git commit -m when gh is unavailable/indeterminate silent-passes (fail-non-public)" {
|
|
373
|
+
mock_gh_visibility FAIL
|
|
374
|
+
INPUT=$(build_bash_input "git commit -m \"I've implemented the feature\"")
|
|
375
|
+
run_hook "$INPUT"
|
|
376
|
+
[ "$status" -eq 0 ]
|
|
377
|
+
[ -z "$output" ]
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
@test "P365: git commit -m in a PUBLIC repo still denies+delegates (gate intact, precondition surface-scoped)" {
|
|
381
|
+
mock_gh_visibility PUBLIC
|
|
382
|
+
INPUT=$(build_bash_input "git commit -m \"I've implemented the feature\"")
|
|
383
|
+
run_hook "$INPUT"
|
|
384
|
+
[ "$status" -eq 0 ]
|
|
385
|
+
[[ "$output" == *"deny"* ]]
|
|
386
|
+
[[ "$output" == *"git-commit-message"* ]]
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@test "P365: PRIVATE visibility does NOT short-circuit the gh-issue surface (still denies+delegates)" {
|
|
390
|
+
mock_gh_visibility PRIVATE
|
|
391
|
+
INPUT=$(build_bash_input "gh issue create --title x --body 'a clean issue body'")
|
|
392
|
+
run_hook "$INPUT"
|
|
393
|
+
[ "$status" -eq 0 ]
|
|
394
|
+
[[ "$output" == *"deny"* ]]
|
|
395
|
+
[[ "$output" == *"gh-issue-create"* ]]
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
# ---------------------------------------------------------------------------
|
|
399
|
+
# P364 — backtick-bearing double-quoted --body marker-key mismatch.
|
|
400
|
+
# The voice-tone gate shares the byte-identical canonical external-comms-gate.sh
|
|
401
|
+
# (ADR-017 sync), so the P364 shell-unescape fix applies here too: a body with
|
|
402
|
+
# backslash-escaped backticks in --body "..." must unescape to the logical
|
|
403
|
+
# <draft> body the PostToolUse mark hook hashes, or the PASS marker never
|
|
404
|
+
# permits. DISTINCT from P276 / P010 (whitespace / frontmatter).
|
|
405
|
+
# ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
@test "P364: backtick-bearing double-quoted --body permits when marker keyed on the unescaped logical body" {
|
|
408
|
+
LOGICAL='Tidied the wording in `external-comms-gate` for the patch.'
|
|
409
|
+
SURFACE="gh-issue-comment"
|
|
410
|
+
KEY=$(printf '%s\n%s' "$LOGICAL" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
|
|
411
|
+
touch "${RDIR}/external-comms-voice-tone-reviewed-${KEY}"
|
|
412
|
+
|
|
413
|
+
CMD='gh issue comment 42 --body "Tidied the wording in \`external-comms-gate\` for the patch."'
|
|
414
|
+
INPUT=$(build_bash_input "$CMD")
|
|
415
|
+
run_hook "$INPUT"
|
|
416
|
+
[ "$status" -eq 0 ]
|
|
417
|
+
[ -z "$output" ]
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@test "P364: single-quoted --body with literal backticks stays literal (no unescaping applied)" {
|
|
421
|
+
LOGICAL='Tidied the wording in `plain_span` here.'
|
|
422
|
+
SURFACE="gh-issue-comment"
|
|
423
|
+
KEY=$(printf '%s\n%s' "$LOGICAL" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
|
|
424
|
+
touch "${RDIR}/external-comms-voice-tone-reviewed-${KEY}"
|
|
425
|
+
|
|
426
|
+
INPUT=$(build_bash_input "gh issue comment 42 --body 'Tidied the wording in \`plain_span\` here.'")
|
|
427
|
+
run_hook "$INPUT"
|
|
428
|
+
[ "$status" -eq 0 ]
|
|
429
|
+
[ -z "$output" ]
|
|
430
|
+
}
|