@windyroad/voice-tone 0.1.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/.claude-plugin/plugin.json +5 -0
- package/agents/agent.md +74 -0
- package/bin/install.mjs +42 -0
- package/hooks/hooks.json +16 -0
- package/hooks/lib/review-gate.sh +102 -0
- package/hooks/voice-tone-enforce-edit.sh +58 -0
- package/hooks/voice-tone-eval.sh +34 -0
- package/hooks/voice-tone-mark-reviewed.sh +49 -0
- package/hooks/voice-tone-reset-marker.sh +22 -0
- package/lib/install-utils.mjs +163 -0
- package/package.json +29 -0
- package/skills/wr:voice-tone/SKILL.md +97 -0
package/agents/agent.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent
|
|
3
|
+
description: Voice and tone reviewer for copy changes. Use before editing any
|
|
4
|
+
user-facing copy in source files. Reads docs/VOICE-AND-TONE.md and reviews
|
|
5
|
+
proposed changes against the guide's voice principles, tone guidance, banned
|
|
6
|
+
patterns, and word list. Reports violations with suggested fixes.
|
|
7
|
+
tools:
|
|
8
|
+
- Read
|
|
9
|
+
- Glob
|
|
10
|
+
- Grep
|
|
11
|
+
model: inherit
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
You are the Voice and Tone Lead. You review proposed copy changes against the project's docs/VOICE-AND-TONE.md guide before any user-facing text is edited. You are a reviewer, not an editor.
|
|
15
|
+
|
|
16
|
+
## Your Role
|
|
17
|
+
|
|
18
|
+
1. Read `docs/VOICE-AND-TONE.md` in the project to load the current guide
|
|
19
|
+
2. Read the file(s) being edited to understand the existing copy and context
|
|
20
|
+
3. Review proposed changes against every section of the guide
|
|
21
|
+
4. Report: OK if compliant, or list specific violations with suggested fixes
|
|
22
|
+
|
|
23
|
+
## What You Check
|
|
24
|
+
|
|
25
|
+
All review criteria come from `docs/VOICE-AND-TONE.md`. Read the guide first and apply its sections. Typical sections include:
|
|
26
|
+
|
|
27
|
+
- **Voice principles** — the personality and values behind the copy
|
|
28
|
+
- **Tone by context** — how tone varies by situation (errors, onboarding, success, etc.)
|
|
29
|
+
- **Banned patterns** — specific phrases or anti-patterns to reject
|
|
30
|
+
- **Word list / terminology** — preferred terms and domain vocabulary
|
|
31
|
+
- **Language & locale** — spelling conventions, casing rules, app name formatting
|
|
32
|
+
|
|
33
|
+
If the guide defines additional sections, check those too. Do not invent rules that are not in the guide.
|
|
34
|
+
|
|
35
|
+
## How to Report
|
|
36
|
+
|
|
37
|
+
If the copy is compliant:
|
|
38
|
+
> **Voice & Tone Review: PASS**
|
|
39
|
+
> No violations found. Copy aligns with the voice guide.
|
|
40
|
+
|
|
41
|
+
If there are violations, list each one:
|
|
42
|
+
|
|
43
|
+
> **Voice & Tone Review: VIOLATIONS FOUND**
|
|
44
|
+
>
|
|
45
|
+
> 1. **[Principle/Rule]** - File: `path`, Line ~N
|
|
46
|
+
> - **Issue**: What is wrong
|
|
47
|
+
> - **Copy**: The offending text
|
|
48
|
+
> - **Fix**: Suggested replacement
|
|
49
|
+
>
|
|
50
|
+
> 2. ...
|
|
51
|
+
|
|
52
|
+
## Guide Gap Detection
|
|
53
|
+
|
|
54
|
+
If the code introduces a UI context, audience, or copy pattern not covered by `docs/VOICE-AND-TONE.md`, flag this as a guide gap:
|
|
55
|
+
|
|
56
|
+
> **Voice & Tone Review: GUIDE UPDATE NEEDED**
|
|
57
|
+
>
|
|
58
|
+
> The code introduces [context/audience/pattern] which is not covered by the current voice and tone guide.
|
|
59
|
+
> Recommended addition to `docs/VOICE-AND-TONE.md`: [specific section/content to add]
|
|
60
|
+
|
|
61
|
+
This is a FAIL verdict — the guide must be updated before the code can proceed. Write `printf 'FAIL' > /tmp/voice-tone-verdict` for guide gaps.
|
|
62
|
+
|
|
63
|
+
## Verdict
|
|
64
|
+
|
|
65
|
+
After completing your review, write your verdict to `/tmp/voice-tone-verdict`:
|
|
66
|
+
- `printf 'PASS' > /tmp/voice-tone-verdict` — copy is compliant and guide covers the context
|
|
67
|
+
- `printf 'FAIL' > /tmp/voice-tone-verdict` — violations found or guide gap detected
|
|
68
|
+
|
|
69
|
+
## Constraints
|
|
70
|
+
|
|
71
|
+
- You are read-only. You do not edit files (except writing the verdict file).
|
|
72
|
+
- You review copy in user-facing source files.
|
|
73
|
+
- If the change is purely structural (no user-visible text changes), report PASS.
|
|
74
|
+
- Do not block styling-only changes (CSS classes, layout, imports with no copy).
|
package/bin/install.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { resolve, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const utils = await import(resolve(__dirname, "../lib/install-utils.mjs"));
|
|
8
|
+
|
|
9
|
+
const PLUGIN = "wr-voice-tone";
|
|
10
|
+
const DEPS = [];
|
|
11
|
+
|
|
12
|
+
const flags = utils.parseStandardArgs(process.argv);
|
|
13
|
+
|
|
14
|
+
if (flags.help) {
|
|
15
|
+
console.log(`
|
|
16
|
+
Usage: npx @windyroad/voice-tone [options]
|
|
17
|
+
|
|
18
|
+
Voice and tone enforcement for user-facing copy
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--update Update this plugin and its skills
|
|
22
|
+
--uninstall Remove this plugin
|
|
23
|
+
--dry-run Show what would be done without executing
|
|
24
|
+
--help, -h Show this help
|
|
25
|
+
`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (flags.dryRun) {
|
|
30
|
+
utils.setDryRun(true);
|
|
31
|
+
console.log("[dry-run mode — no commands will be executed]\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
utils.checkPrerequisites();
|
|
35
|
+
|
|
36
|
+
if (flags.uninstall) {
|
|
37
|
+
utils.uninstallPackage(PLUGIN);
|
|
38
|
+
} else if (flags.update) {
|
|
39
|
+
utils.updatePackage(PLUGIN);
|
|
40
|
+
} else {
|
|
41
|
+
utils.installPackage(PLUGIN, { deps: DEPS });
|
|
42
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/voice-tone-eval.sh" }] }
|
|
5
|
+
],
|
|
6
|
+
"PreToolUse": [
|
|
7
|
+
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/voice-tone-enforce-edit.sh" }] }
|
|
8
|
+
],
|
|
9
|
+
"PostToolUse": [
|
|
10
|
+
{ "matcher": "Agent", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/voice-tone-mark-reviewed.sh" }] }
|
|
11
|
+
],
|
|
12
|
+
"Stop": [
|
|
13
|
+
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/voice-tone-reset-marker.sh" }] }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared gate logic for review enforcement hooks (a11y, voice-tone, style-guide).
|
|
3
|
+
# Sourced by *-enforce-edit.sh hooks and review-plan-enforce.sh.
|
|
4
|
+
# Provides: check_review_gate, review_gate_deny, review_gate_parse_error
|
|
5
|
+
|
|
6
|
+
# Source shared portable helpers (_mtime, _hashcmd)
|
|
7
|
+
_REVIEW_GATE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
source "$_REVIEW_GATE_DIR/gate-helpers.sh"
|
|
9
|
+
|
|
10
|
+
# Check review gate marker. Returns 0 if marker is valid (allow), 1 if invalid (deny).
|
|
11
|
+
# Sets REVIEW_GATE_REASON on failure.
|
|
12
|
+
# Usage: check_review_gate "$SESSION_ID" "style-guide" "docs/STYLE-GUIDE.md"
|
|
13
|
+
check_review_gate() {
|
|
14
|
+
local SESSION_ID="$1"
|
|
15
|
+
local SYSTEM="$2" # e.g., "a11y", "voice-tone", "style-guide"
|
|
16
|
+
local POLICY_FILE="$3" # e.g., "docs/STYLE-GUIDE.md"
|
|
17
|
+
local MARKER="/tmp/${SYSTEM}-reviewed-${SESSION_ID}"
|
|
18
|
+
local HASH_FILE="/tmp/${SYSTEM}-reviewed-${SESSION_ID}.hash"
|
|
19
|
+
local TTL_SECONDS="${REVIEW_TTL:-600}"
|
|
20
|
+
|
|
21
|
+
# 1. Marker must exist
|
|
22
|
+
if [ ! -f "$MARKER" ]; then
|
|
23
|
+
REVIEW_GATE_REASON="No ${SYSTEM} review marker found. The ${SYSTEM} agent must review first."
|
|
24
|
+
return 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# 2. TTL check — marker mtime must be within TTL
|
|
28
|
+
local NOW=$(date +%s)
|
|
29
|
+
local MARKER_TIME=$(_mtime "$MARKER")
|
|
30
|
+
local AGE=$(( NOW - MARKER_TIME ))
|
|
31
|
+
if [ "$AGE" -ge "$TTL_SECONDS" ]; then
|
|
32
|
+
rm -f "$MARKER" "$HASH_FILE"
|
|
33
|
+
REVIEW_GATE_REASON="${SYSTEM} review expired (${AGE}s old, TTL ${TTL_SECONDS}s). Re-run the ${SYSTEM} agent."
|
|
34
|
+
return 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# 3. Drift detection — policy file hash must match
|
|
38
|
+
if [ -f "$HASH_FILE" ] && [ -n "$POLICY_FILE" ]; then
|
|
39
|
+
local STORED_HASH=$(cat "$HASH_FILE")
|
|
40
|
+
local CURRENT_HASH=""
|
|
41
|
+
if [ -f "$POLICY_FILE" ]; then
|
|
42
|
+
CURRENT_HASH=$(cat "$POLICY_FILE" | _hashcmd | cut -d' ' -f1)
|
|
43
|
+
elif [ -d "$POLICY_FILE" ]; then
|
|
44
|
+
# Directory (e.g., docs/decisions/) — hash all .md files
|
|
45
|
+
CURRENT_HASH=$(find "$POLICY_FILE" -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
|
|
46
|
+
else
|
|
47
|
+
CURRENT_HASH="missing"
|
|
48
|
+
fi
|
|
49
|
+
if [ "$STORED_HASH" != "$CURRENT_HASH" ]; then
|
|
50
|
+
rm -f "$MARKER" "$HASH_FILE"
|
|
51
|
+
REVIEW_GATE_REASON="${SYSTEM} policy file changed since last review. Re-run the ${SYSTEM} agent."
|
|
52
|
+
return 1
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Slide TTL window forward
|
|
57
|
+
touch "$MARKER"
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Store policy file hash after a successful review.
|
|
62
|
+
# Usage: store_review_hash "$SESSION_ID" "style-guide" "docs/STYLE-GUIDE.md"
|
|
63
|
+
store_review_hash() {
|
|
64
|
+
local SESSION_ID="$1"
|
|
65
|
+
local SYSTEM="$2"
|
|
66
|
+
local POLICY_FILE="$3"
|
|
67
|
+
local HASH_FILE="/tmp/${SYSTEM}-reviewed-${SESSION_ID}.hash"
|
|
68
|
+
|
|
69
|
+
if [ -n "$POLICY_FILE" ]; then
|
|
70
|
+
local HASH=""
|
|
71
|
+
if [ -f "$POLICY_FILE" ]; then
|
|
72
|
+
HASH=$(cat "$POLICY_FILE" | _hashcmd | cut -d' ' -f1)
|
|
73
|
+
elif [ -d "$POLICY_FILE" ]; then
|
|
74
|
+
HASH=$(find "$POLICY_FILE" -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
|
|
75
|
+
else
|
|
76
|
+
HASH="missing"
|
|
77
|
+
fi
|
|
78
|
+
echo "$HASH" > "$HASH_FILE"
|
|
79
|
+
fi
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Emit fail-closed deny JSON for PreToolUse hooks.
|
|
83
|
+
review_gate_deny() {
|
|
84
|
+
local REASON="$1"
|
|
85
|
+
cat <<EOF
|
|
86
|
+
{
|
|
87
|
+
"hookSpecificOutput": {
|
|
88
|
+
"hookEventName": "PreToolUse",
|
|
89
|
+
"permissionDecision": "deny",
|
|
90
|
+
"permissionDecisionReason": "$REASON"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
EOF
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Emit fail-closed deny JSON for parse failures.
|
|
97
|
+
review_gate_parse_error() {
|
|
98
|
+
cat <<'EOF'
|
|
99
|
+
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny",
|
|
100
|
+
"permissionDecisionReason": "BLOCKED: Could not parse hook input. Gate is fail-closed." } }
|
|
101
|
+
EOF
|
|
102
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Voice & Tone - PreToolUse enforcement hook
|
|
3
|
+
# BLOCKS Edit/Write to copy-bearing files until voice-and-tone-lead is consulted.
|
|
4
|
+
# Uses shared review-gate.sh for TTL, drift detection, and fail-closed.
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "$SCRIPT_DIR/lib/review-gate.sh"
|
|
8
|
+
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
FILE_PATH=$(echo "$INPUT" | python3 -c "
|
|
12
|
+
import sys, json
|
|
13
|
+
try:
|
|
14
|
+
data = json.load(sys.stdin)
|
|
15
|
+
print(data.get('tool_input', {}).get('file_path', ''))
|
|
16
|
+
except:
|
|
17
|
+
print('')
|
|
18
|
+
" 2>/dev/null || echo "")
|
|
19
|
+
|
|
20
|
+
SESSION_ID=$(echo "$INPUT" | python3 -c "
|
|
21
|
+
import sys, json
|
|
22
|
+
try:
|
|
23
|
+
data = json.load(sys.stdin)
|
|
24
|
+
print(data.get('session_id', ''))
|
|
25
|
+
except:
|
|
26
|
+
print('')
|
|
27
|
+
" 2>/dev/null || echo "")
|
|
28
|
+
|
|
29
|
+
if [ -z "$SESSION_ID" ]; then
|
|
30
|
+
review_gate_parse_error
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if [ -z "$FILE_PATH" ]; then
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Gate copy-bearing files
|
|
39
|
+
case "$FILE_PATH" in
|
|
40
|
+
*.html|*.jsx|*.tsx|*.vue|*.svelte|*.ejs|*.hbs) ;;
|
|
41
|
+
*) exit 0 ;;
|
|
42
|
+
esac
|
|
43
|
+
|
|
44
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
45
|
+
|
|
46
|
+
# If no policy file exists, block and direct to create skill
|
|
47
|
+
if [ ! -f "docs/VOICE-AND-TONE.md" ]; then
|
|
48
|
+
review_gate_deny "BLOCKED: Cannot edit '${BASENAME}' because docs/VOICE-AND-TONE.md does not exist. Run /wr-voice-tone:create to generate a voice and tone guide for this project, then delegate to wr-voice-tone:agent for review."
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Check gate with TTL + drift detection
|
|
53
|
+
if check_review_gate "$SESSION_ID" "voice-tone" "docs/VOICE-AND-TONE.md"; then
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
review_gate_deny "BLOCKED: Cannot edit '${BASENAME}' without voice & tone review. You MUST first delegate to wr-voice-tone:agent using the Agent tool (subagent_type: 'wr-voice-tone:agent'). ${REVIEW_GATE_REASON}"
|
|
58
|
+
exit 0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Voice & Tone - UserPromptSubmit hook
|
|
3
|
+
# Detects VOICE-AND-TONE.md in the project and injects delegation instruction.
|
|
4
|
+
# If the file doesn't exist, instructs Claude to create it via the agent.
|
|
5
|
+
|
|
6
|
+
if [ -f "docs/VOICE-AND-TONE.md" ]; then
|
|
7
|
+
cat <<'HOOK_OUTPUT'
|
|
8
|
+
INSTRUCTION: MANDATORY VOICE & TONE CHECK. YOU MUST FOLLOW THIS.
|
|
9
|
+
DETECTED: docs/VOICE-AND-TONE.md exists in this project.
|
|
10
|
+
|
|
11
|
+
This is a NON-OPTIONAL instruction. You MUST use the voice-and-tone-lead agent
|
|
12
|
+
before editing any user-facing copy in HTML, JSX, template, or component files.
|
|
13
|
+
This is proactive. Do not wait for the user to ask.
|
|
14
|
+
|
|
15
|
+
REQUIRED ACTIONS:
|
|
16
|
+
1. Use the Agent tool to delegate to wr-voice-tone:agent
|
|
17
|
+
(subagent_type: "wr-voice-tone:agent")
|
|
18
|
+
2. The voice-and-tone-lead will review proposed copy against docs/VOICE-AND-TONE.md
|
|
19
|
+
3. Do NOT write or edit copy without voice-and-tone-lead review FIRST
|
|
20
|
+
4. Do NOT skip this step even if you think you can handle it yourself
|
|
21
|
+
|
|
22
|
+
SCOPE: User-facing files (.html, .jsx, .tsx, .vue, .svelte, .ejs, .hbs).
|
|
23
|
+
Does NOT apply to: .css files, .ts/.js backend files, config files.
|
|
24
|
+
HOOK_OUTPUT
|
|
25
|
+
else
|
|
26
|
+
# Check if this is a web project (has UI files)
|
|
27
|
+
if ls src/**/*.tsx src/**/*.jsx src/**/*.html 2>/dev/null | head -1 | grep -q .; then
|
|
28
|
+
cat <<'HOOK_OUTPUT'
|
|
29
|
+
NOTE: This project has UI files but no docs/VOICE-AND-TONE.md.
|
|
30
|
+
If the user's task involves editing user-facing copy, the edit will be blocked
|
|
31
|
+
until a voice and tone guide exists. Run /wr-voice-tone:create to generate one.
|
|
32
|
+
HOOK_OUTPUT
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Voice & Tone - PostToolUse hook for Agent tool
|
|
3
|
+
# Creates a session marker when voice-and-tone-lead has been consulted with PASS verdict.
|
|
4
|
+
# This marker unlocks the voice-tone-enforce-edit.sh PreToolUse block.
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "$SCRIPT_DIR/lib/review-gate.sh"
|
|
8
|
+
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
SUBAGENT=$(echo "$INPUT" | jq -r '.tool_input.subagent_type // empty') || true
|
|
12
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
|
|
13
|
+
|
|
14
|
+
if [ -z "$SESSION_ID" ]; then
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
case "$SUBAGENT" in
|
|
19
|
+
*voice-and-tone-lead*|*wr-voice-tone*)
|
|
20
|
+
VERDICT_FILE="/tmp/voice-tone-verdict"
|
|
21
|
+
VERDICT=""
|
|
22
|
+
if [ -f "$VERDICT_FILE" ]; then
|
|
23
|
+
VERDICT=$(cat "$VERDICT_FILE")
|
|
24
|
+
rm -f "$VERDICT_FILE"
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
case "$VERDICT" in
|
|
28
|
+
PASS)
|
|
29
|
+
touch "/tmp/voice-tone-reviewed-${SESSION_ID}"
|
|
30
|
+
store_review_hash "$SESSION_ID" "voice-tone" "docs/VOICE-AND-TONE.md"
|
|
31
|
+
;;
|
|
32
|
+
FAIL)
|
|
33
|
+
# Do NOT create marker — review found issues
|
|
34
|
+
;;
|
|
35
|
+
*)
|
|
36
|
+
# No verdict file — backward compat, allow with marker
|
|
37
|
+
touch "/tmp/voice-tone-reviewed-${SESSION_ID}"
|
|
38
|
+
store_review_hash "$SESSION_ID" "voice-tone" "docs/VOICE-AND-TONE.md"
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
|
|
42
|
+
# Plan review: agent completion = reviewed.
|
|
43
|
+
# The main agent must actually run the review agent to reach this hook.
|
|
44
|
+
# No verdict file needed — PostToolUse:Agent is the unforgeable signal.
|
|
45
|
+
touch "/tmp/voice-tone-plan-reviewed-${SESSION_ID}"
|
|
46
|
+
;;
|
|
47
|
+
esac
|
|
48
|
+
|
|
49
|
+
exit 0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Voice & Tone - Stop hook
|
|
3
|
+
# Removes the session marker so the next prompt requires a fresh voice review.
|
|
4
|
+
# This tightens the gate from per-session to per-turn.
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
|
|
8
|
+
SESSION_ID=$(echo "$INPUT" | python3 -c "
|
|
9
|
+
import sys, json
|
|
10
|
+
data = json.load(sys.stdin)
|
|
11
|
+
print(data.get('session_id', ''))
|
|
12
|
+
" 2>/dev/null)
|
|
13
|
+
|
|
14
|
+
if [ -n "$SESSION_ID" ]; then
|
|
15
|
+
rm -f "/tmp/voice-tone-reviewed-${SESSION_ID}" \
|
|
16
|
+
"/tmp/voice-tone-reviewed-${SESSION_ID}.hash" \
|
|
17
|
+
"/tmp/voice-tone-verdict" \
|
|
18
|
+
"/tmp/voice-tone-plan-reviewed-${SESSION_ID}" \
|
|
19
|
+
"/tmp/voice-tone-plan-verdict"
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
exit 0
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared install utilities for @windyroad/* packages.
|
|
3
|
+
* Used by both per-plugin installers and the meta-installer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const MARKETPLACE_REPO = "windyroad/agent-plugins";
|
|
9
|
+
const MARKETPLACE_NAME = "windyroad";
|
|
10
|
+
|
|
11
|
+
let _dryRun = false;
|
|
12
|
+
|
|
13
|
+
export { MARKETPLACE_REPO, MARKETPLACE_NAME };
|
|
14
|
+
|
|
15
|
+
export function setDryRun(value) {
|
|
16
|
+
_dryRun = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isDryRun() {
|
|
20
|
+
return _dryRun;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function run(cmd, label) {
|
|
24
|
+
console.log(` ${label}...`);
|
|
25
|
+
if (_dryRun) {
|
|
26
|
+
console.log(` [dry-run] ${cmd}`);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
execSync(cmd, { stdio: "inherit" });
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
console.error(` FAILED: ${label}`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function checkPrerequisites() {
|
|
39
|
+
if (_dryRun) return;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
43
|
+
} catch {
|
|
44
|
+
console.error(
|
|
45
|
+
"Error: 'claude' CLI not found. Install Claude Code first:\n https://docs.anthropic.com/en/docs/claude-code\n"
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
execSync("npx --version", { stdio: "pipe" });
|
|
52
|
+
} catch {
|
|
53
|
+
console.error(
|
|
54
|
+
"Error: 'npx' not found. Install Node.js first:\n https://nodejs.org\n"
|
|
55
|
+
);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function addMarketplace() {
|
|
61
|
+
return run(
|
|
62
|
+
`claude plugin marketplace add ${MARKETPLACE_REPO}`,
|
|
63
|
+
`Marketplace: ${MARKETPLACE_NAME}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function installPlugin(pluginName) {
|
|
68
|
+
return run(
|
|
69
|
+
`claude plugin install ${pluginName}@${MARKETPLACE_NAME}`,
|
|
70
|
+
pluginName
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function updatePlugin(pluginName) {
|
|
75
|
+
return run(`claude plugin update ${pluginName}`, pluginName);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function uninstallPlugin(pluginName) {
|
|
79
|
+
return run(`claude plugin uninstall ${pluginName}`, `Removing ${pluginName}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function installSkills() {
|
|
83
|
+
return run(
|
|
84
|
+
`npx -y skills add --yes --all ${MARKETPLACE_REPO}`,
|
|
85
|
+
"Skills (via skills package)"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function updateSkills() {
|
|
90
|
+
return run("npx -y skills update", "Skills update");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function removeSkills() {
|
|
94
|
+
return run(
|
|
95
|
+
`npx -y skills remove --yes --all ${MARKETPLACE_REPO}`,
|
|
96
|
+
"Removing skills"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Install a single package: marketplace add + plugin install + skills.
|
|
102
|
+
*/
|
|
103
|
+
export function installPackage(pluginName, { deps = [] } = {}) {
|
|
104
|
+
console.log(`\nInstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
105
|
+
|
|
106
|
+
addMarketplace();
|
|
107
|
+
installPlugin(pluginName);
|
|
108
|
+
installSkills();
|
|
109
|
+
|
|
110
|
+
if (deps.length > 0) {
|
|
111
|
+
console.log(`\nNote: This plugin works best with:`);
|
|
112
|
+
for (const dep of deps) {
|
|
113
|
+
console.log(` - @windyroad/${dep.replace("wr-", "")} (npx @windyroad/${dep.replace("wr-", "")})`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(
|
|
118
|
+
`\nDone! Restart Claude Code to activate.\n`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Update a single package.
|
|
124
|
+
*/
|
|
125
|
+
export function updatePackage(pluginName) {
|
|
126
|
+
console.log(`\nUpdating @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
127
|
+
|
|
128
|
+
run(
|
|
129
|
+
`claude plugin marketplace update ${MARKETPLACE_NAME}`,
|
|
130
|
+
"Updating marketplace"
|
|
131
|
+
);
|
|
132
|
+
updatePlugin(pluginName);
|
|
133
|
+
updateSkills();
|
|
134
|
+
|
|
135
|
+
console.log("\nDone! Restart Claude Code to apply updates.\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Uninstall a single package.
|
|
140
|
+
*/
|
|
141
|
+
export function uninstallPackage(pluginName) {
|
|
142
|
+
console.log(`\nUninstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
143
|
+
|
|
144
|
+
uninstallPlugin(pluginName);
|
|
145
|
+
|
|
146
|
+
console.log("\nDone. Restart Claude Code to apply changes.\n");
|
|
147
|
+
console.log("Note: Skills are shared across packages. Run");
|
|
148
|
+
console.log(" npx @windyroad/agent-plugins --uninstall");
|
|
149
|
+
console.log("to remove all skills.\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse standard flags used by all per-plugin installers.
|
|
154
|
+
*/
|
|
155
|
+
export function parseStandardArgs(argv) {
|
|
156
|
+
const args = argv.slice(2);
|
|
157
|
+
return {
|
|
158
|
+
help: args.includes("--help") || args.includes("-h"),
|
|
159
|
+
uninstall: args.includes("--uninstall"),
|
|
160
|
+
update: args.includes("--update"),
|
|
161
|
+
dryRun: args.includes("--dry-run"),
|
|
162
|
+
};
|
|
163
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@windyroad/voice-tone",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Voice and tone enforcement for user-facing copy",
|
|
5
|
+
"bin": {
|
|
6
|
+
"windyroad-voice-tone": "./bin/install.mjs"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/windyroad/agent-plugins.git",
|
|
13
|
+
"directory": "packages/voice-tone"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude-code",
|
|
17
|
+
"claude-code-plugin",
|
|
18
|
+
"ai-agent",
|
|
19
|
+
"ai-coding"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"bin/",
|
|
23
|
+
"agents/",
|
|
24
|
+
"hooks/",
|
|
25
|
+
"skills/",
|
|
26
|
+
".claude-plugin/",
|
|
27
|
+
"lib/"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wr:voice-tone
|
|
3
|
+
description: Create or update the project's docs/VOICE-AND-TONE.md by examining existing content and asking the user about brand voice, audience, and tone preferences.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Voice and Tone Guide Generator
|
|
8
|
+
|
|
9
|
+
Create or update `docs/VOICE-AND-TONE.md` tailored to this project's brand, audience, and content. The voice-and-tone-lead agent reads this file to review user-facing copy.
|
|
10
|
+
|
|
11
|
+
## What belongs in docs/VOICE-AND-TONE.md
|
|
12
|
+
|
|
13
|
+
- **Brand voice**: The personality and character of the product's communication
|
|
14
|
+
- **Audience**: Who the product speaks to and their expectations
|
|
15
|
+
- **Tone spectrum**: How tone shifts across contexts (error messages vs success vs onboarding)
|
|
16
|
+
- **Do/Don't examples**: Concrete before/after examples drawn from the actual project
|
|
17
|
+
- **Terminology**: Preferred terms and terms to avoid
|
|
18
|
+
- **Last reviewed date**: When the guide was last reviewed or updated
|
|
19
|
+
|
|
20
|
+
## Steps
|
|
21
|
+
|
|
22
|
+
### 1. Discover project context
|
|
23
|
+
|
|
24
|
+
Examine the project to understand what it does, who uses it, and what voice it currently has.
|
|
25
|
+
|
|
26
|
+
**Find existing content** by scanning for:
|
|
27
|
+
- User-facing copy in UI files (.tsx, .jsx, .html, .vue, .svelte, .ejs, .hbs)
|
|
28
|
+
- README.md and documentation
|
|
29
|
+
- Error messages and validation text
|
|
30
|
+
- Marketing or landing page content
|
|
31
|
+
- Existing brand guidelines or tone documentation
|
|
32
|
+
|
|
33
|
+
**Identify the audience**:
|
|
34
|
+
- Is this a developer tool, consumer product, enterprise SaaS, documentation site?
|
|
35
|
+
- Who are the primary users? Technical or non-technical?
|
|
36
|
+
- What is the relationship between the product and its users (formal, casual, peer)?
|
|
37
|
+
|
|
38
|
+
**Sample existing voice**:
|
|
39
|
+
- Read 5-10 representative UI strings or content blocks
|
|
40
|
+
- Note patterns: formal vs casual, technical vs plain, terse vs verbose
|
|
41
|
+
- Identify inconsistencies that the guide should resolve
|
|
42
|
+
|
|
43
|
+
### 2. Check for existing guide
|
|
44
|
+
|
|
45
|
+
If `docs/VOICE-AND-TONE.md` already exists, read it. Identify:
|
|
46
|
+
- Whether the voice description still matches the current product direction
|
|
47
|
+
- Whether examples reference features or copy that no longer exists
|
|
48
|
+
- Whether the last reviewed date is stale (> 2 weeks)
|
|
49
|
+
|
|
50
|
+
### 3. Draft the voice and tone guide
|
|
51
|
+
|
|
52
|
+
Based on project discovery, draft sections covering:
|
|
53
|
+
|
|
54
|
+
**Brand Voice** (3-5 adjectives with explanations):
|
|
55
|
+
Example: "Confident but not arrogant. We know our product well but respect the user's expertise."
|
|
56
|
+
|
|
57
|
+
**Audience**:
|
|
58
|
+
Who we're speaking to and what they care about.
|
|
59
|
+
|
|
60
|
+
**Tone Spectrum**:
|
|
61
|
+
How tone adapts to context. Include at least:
|
|
62
|
+
- Success/confirmation messages
|
|
63
|
+
- Error/warning messages
|
|
64
|
+
- Onboarding/help text
|
|
65
|
+
- Empty states
|
|
66
|
+
- Technical documentation
|
|
67
|
+
|
|
68
|
+
**Do/Don't Examples**:
|
|
69
|
+
At least 5 concrete pairs drawn from the actual project's content patterns. Show the wrong way and the right way, with brief explanations.
|
|
70
|
+
|
|
71
|
+
**Terminology**:
|
|
72
|
+
- Preferred terms (and what they replace)
|
|
73
|
+
- Terms to avoid (and why)
|
|
74
|
+
|
|
75
|
+
### 4. Confirm with the user
|
|
76
|
+
|
|
77
|
+
You MUST use the AskUserQuestion tool to collect user confirmation. Do not proceed to step 5 until you have answers.
|
|
78
|
+
|
|
79
|
+
Present:
|
|
80
|
+
1. The drafted brand voice adjectives and ask if they match the intended personality
|
|
81
|
+
2. The audience description and ask if it's accurate
|
|
82
|
+
3. The do/don't examples and ask if the direction feels right
|
|
83
|
+
4. Whether any specific tone requirements are missing (e.g., accessibility language, inclusive language, regulatory constraints)
|
|
84
|
+
|
|
85
|
+
### 5. Write docs/VOICE-AND-TONE.md
|
|
86
|
+
|
|
87
|
+
Write the guide including:
|
|
88
|
+
- A header with "Last reviewed" date (today's date)
|
|
89
|
+
- All sections from step 3, refined based on user feedback from step 4
|
|
90
|
+
- A note that the wr-voice-tone:agent reads this file to review user-facing copy
|
|
91
|
+
|
|
92
|
+
If updating rather than creating:
|
|
93
|
+
- Preserve existing guidance the user hasn't asked to change
|
|
94
|
+
- Show the user a diff of what changed
|
|
95
|
+
- Update the "Last reviewed" date
|
|
96
|
+
|
|
97
|
+
$ARGUMENTS
|