@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.
@@ -123,5 +123,5 @@
123
123
  }
124
124
  },
125
125
  "name": "wr-voice-tone",
126
- "version": "0.5.11"
126
+ "version": "0.5.13"
127
127
  }
@@ -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
- # (pattern, flags) first match wins.
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
- (r\"[<][<]\s*['\\\"]?EOF['\\\"]?\s*\n(.*?)\nEOF\", re.DOTALL),
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
- print(m.group(1))
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/voice-tone",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "Voice and tone enforcement for user-facing copy",
5
5
  "bin": {
6
6
  "windyroad-voice-tone": "./bin/install.mjs"