@windyroad/voice-tone 0.5.0-preview.313 → 0.5.0-preview.317
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/agents/external-comms.md +7 -15
- package/hooks/external-comms-gate.sh +12 -3
- package/hooks/external-comms-mark-reviewed.sh +45 -4
- package/hooks/lib/external-comms-key.sh +44 -0
- package/hooks/test/external-comms-mark-prompt-parse.bats +117 -0
- package/package.json +1 -1
- package/skills/assess-external-comms/SKILL.md +18 -6
package/agents/external-comms.md
CHANGED
|
@@ -10,16 +10,16 @@ model: inherit
|
|
|
10
10
|
|
|
11
11
|
You are the External-Comms Voice & Tone Reviewer. Your single job: read the draft of an outbound prose tool call (a `gh issue create --body ...`, a PR description, a security-advisory body, a `.changeset/*.md` file, or the README diff that `npm publish` will publish) and return a structured PASS/FAIL verdict against `docs/VOICE-AND-TONE.md`.
|
|
12
12
|
|
|
13
|
-
You are read-only. You do NOT write files, do NOT commit, do NOT modify the draft. Your verdict is consumed by the `external-comms-mark-reviewed.sh` PostToolUse hook (P038 / ADR-028 amended 2026-05-14), which writes the per-evaluator marker that allows the gated tool call to proceed.
|
|
13
|
+
You are read-only. You do NOT write files, do NOT commit, do NOT modify the draft. Your verdict is consumed by the `external-comms-mark-reviewed.sh` PostToolUse hook (P038 / ADR-028 amended 2026-05-14 + 2026-05-16), which derives the marker key from the prompt structure you receive and writes the per-evaluator marker that allows the gated tool call to proceed.
|
|
14
14
|
|
|
15
15
|
You are the voice-tone half of the external-comms gate. The risk/leak half is handled by `wr-risk-scorer:external-comms`. When both plugins are installed, both evaluators must PASS independently before the gate permits the tool call. The two gates compose at the firing level (per-evaluator markers, no shared composite marker).
|
|
16
16
|
|
|
17
17
|
## What you receive
|
|
18
18
|
|
|
19
|
-
The invoking skill (`/wr-voice-tone:assess-external-comms`) or the agent that hit the gate provides:
|
|
19
|
+
The invoking skill (`/wr-voice-tone:assess-external-comms`) or the agent that hit the gate provides a structured prompt (P166 / ADR-028 amended 2026-05-16):
|
|
20
20
|
|
|
21
|
-
-
|
|
22
|
-
- The **
|
|
21
|
+
- A leading `SURFACE: <name>` line — one of: `gh-issue-create`, `gh-issue-comment`, `gh-issue-edit`, `gh-pr-create`, `gh-pr-comment`, `gh-pr-edit`, `gh-api-security-advisories`, `gh-api-comments`, `npm-publish`, `changeset-author`.
|
|
22
|
+
- The **draft body** verbatim, wrapped in `<draft>...</draft>` markers so the PostToolUse hook can extract it for marker-key derivation.
|
|
23
23
|
- The **destination** when known (e.g. `anthropics/claude-code#52831`).
|
|
24
24
|
|
|
25
25
|
Read `docs/VOICE-AND-TONE.md` (project root) to get the authoritative voice profile. Typical sections include voice principles, tone by context, banned patterns, word list / terminology, and language/locale conventions.
|
|
@@ -38,28 +38,20 @@ If `docs/VOICE-AND-TONE.md` is absent, the gate will run in advisory-only mode (
|
|
|
38
38
|
|
|
39
39
|
## Verdict format (MANDATORY)
|
|
40
40
|
|
|
41
|
-
End your report with a structured block consumed by `external-comms-mark-reviewed.sh
|
|
41
|
+
End your report with a structured block consumed by `external-comms-mark-reviewed.sh`:
|
|
42
42
|
|
|
43
43
|
```
|
|
44
44
|
EXTERNAL_COMMS_VOICE_TONE_VERDICT: PASS
|
|
45
|
-
EXTERNAL_COMMS_VOICE_TONE_KEY: <sha256 hex string>
|
|
46
45
|
```
|
|
47
46
|
|
|
48
47
|
OR for a failed review:
|
|
49
48
|
|
|
50
49
|
```
|
|
51
50
|
EXTERNAL_COMMS_VOICE_TONE_VERDICT: FAIL
|
|
52
|
-
EXTERNAL_COMMS_VOICE_TONE_KEY: <sha256 hex string>
|
|
53
51
|
EXTERNAL_COMMS_VOICE_TONE_REASON: <one-line description of the voice/tone violation + matched pattern>
|
|
54
52
|
```
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
printf '%s\n%s' "<draft body verbatim>" "<surface name>" | shasum -a 256 | cut -d' ' -f1
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
The key MUST match the gate's computation exactly — a key mismatch means the marker is written for a different draft and the original gated call will continue to deny.
|
|
54
|
+
You do NOT need to emit `EXTERNAL_COMMS_VOICE_TONE_KEY`. The PostToolUse hook derives the marker key directly from the `SURFACE:` line and `<draft>...</draft>` block in the prompt you received (P166 / ADR-028 amended 2026-05-16). Single fire per gate cycle.
|
|
63
55
|
|
|
64
56
|
## Grounding (ADR-026)
|
|
65
57
|
|
|
@@ -78,7 +70,7 @@ Example:
|
|
|
78
70
|
- You are a reviewer, not an editor — do NOT propose rewrites in the verdict block. (Free prose suggestions outside the verdict block are fine and helpful.)
|
|
79
71
|
- Do NOT score by analogy when the guide names the principle.
|
|
80
72
|
- Do NOT write to `/tmp/` or any marker location yourself — the PostToolUse hook owns that.
|
|
81
|
-
-
|
|
73
|
+
- You do NOT need to emit `EXTERNAL_COMMS_VOICE_TONE_KEY` — the hook derives the key from the prompt's `SURFACE:` + `<draft>` structure (P166 / ADR-028 amended 2026-05-16). If your prompt lacks that structure (legacy caller), the hook falls back to an emitted KEY line for backward compatibility, but the canonical path is hook-side derivation.
|
|
82
74
|
- When the draft is empty (e.g. `npm publish` with no extractable body fragment), review the staged content the publish would push (README diff, package.json description) instead. If neither is available, FAIL with reason "draft body unresolvable; cannot voice-tone-review without text" so the user can pre-review manually.
|
|
83
75
|
|
|
84
76
|
## Below-Appetite Output Rule (ADR-013 Rule 5)
|
|
@@ -31,7 +31,12 @@
|
|
|
31
31
|
# Marker location: ${TMPDIR:-/tmp}/claude-risk-${SESSION_ID}/external-comms-<EVALUATOR_ID>-reviewed-<sha256>
|
|
32
32
|
# Marker writer: PostToolUse:Agent hook in each consumer plugin
|
|
33
33
|
# (risk-score-mark.sh or external-comms-mark-reviewed.sh) on
|
|
34
|
-
# subagent type wr-<plugin>:external-comms.
|
|
34
|
+
# subagent type wr-<plugin>:external-comms. The mark hook
|
|
35
|
+
# derives the marker key from the agent's tool_input.prompt
|
|
36
|
+
# by parsing the same `SURFACE:` + `<draft>` structure the
|
|
37
|
+
# orchestrator was instructed to include (P166 / ADR-028
|
|
38
|
+
# amended 2026-05-16). Single fire per gate cycle suffices;
|
|
39
|
+
# the agent no longer needs to compute the key itself.
|
|
35
40
|
#
|
|
36
41
|
# Per-evaluator marker scheme (ADR-028 amended 2026-05-14): when both
|
|
37
42
|
# voice-tone and risk-scorer are installed, both gates fire on the same
|
|
@@ -234,8 +239,12 @@ if [ -f "$MARKER" ]; then
|
|
|
234
239
|
fi
|
|
235
240
|
|
|
236
241
|
# Marker absent — deny + delegate.
|
|
242
|
+
# P166: instruct the orchestrator to structure the agent prompt with a
|
|
243
|
+
# leading `SURFACE: <name>` line and a `<draft>...</draft>` block so the
|
|
244
|
+
# PostToolUse mark hook can derive the canonical marker key locally
|
|
245
|
+
# (sha256(DRAFT + '\n' + SURFACE)). Single fire per gate cycle.
|
|
237
246
|
VERDICT_PREFIX="${EXTERNAL_COMMS_VERDICT_PREFIX:-EXTERNAL_COMMS_${EXTERNAL_COMMS_EVALUATOR_ID^^}}"
|
|
238
|
-
REASON=$(printf 'BLOCKED (external-comms gate / %s evaluator): %s draft has not been reviewed by %s. Delegate to %s (subagent_type: '"'"'%s'"'"') with the draft body
|
|
239
|
-
"$EXTERNAL_COMMS_EVALUATOR_ID" "$SURFACE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$VERDICT_PREFIX" "$EXTERNAL_COMMS_ASSESS_SKILL")
|
|
247
|
+
REASON=$(printf 'BLOCKED (external-comms gate / %s evaluator): %s draft has not been reviewed by %s. Delegate to %s (subagent_type: '"'"'%s'"'"') with a prompt that starts with the line `SURFACE: %s` and wraps the draft body verbatim inside `<draft>...</draft>` markers. The PostToolUse hook derives the marker key from that structure and marks the draft reviewed when the subagent emits %s_VERDICT: PASS — single fire suffices. Use %s for an interactive walkthrough. Override only when intentional: BYPASS_RISK_GATE=1.' \
|
|
248
|
+
"$EXTERNAL_COMMS_EVALUATOR_ID" "$SURFACE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$SURFACE" "$VERDICT_PREFIX" "$EXTERNAL_COMMS_ASSESS_SKILL")
|
|
240
249
|
deny_with_reason "$REASON"
|
|
241
250
|
exit 0
|
|
@@ -2,11 +2,23 @@
|
|
|
2
2
|
# PostToolUse:Agent hook for the wr-voice-tone:external-comms subagent.
|
|
3
3
|
# Parses structured verdict from agent output and writes the per-evaluator
|
|
4
4
|
# marker that the canonical external-comms-gate.sh checks (P038 / ADR-028
|
|
5
|
-
# amended 2026-05-14).
|
|
5
|
+
# amended 2026-05-14, further amended 2026-05-16 P166).
|
|
6
6
|
#
|
|
7
7
|
# Marker filename: external-comms-voice-tone-reviewed-<KEY>
|
|
8
8
|
# Marker location: ${TMPDIR:-/tmp}/claude-risk-${SESSION_ID}/
|
|
9
9
|
#
|
|
10
|
+
# Marker key derivation (P166 / ADR-028 amended 2026-05-16):
|
|
11
|
+
# 1. PRIMARY — derive from agent's tool_input.prompt via
|
|
12
|
+
# derive_external_comms_key_from_prompt (parses `SURFACE: <name>` +
|
|
13
|
+
# `<draft>...</draft>` block, computes sha256(DRAFT + '\n' + SURFACE)).
|
|
14
|
+
# Matches the gate's canonical key shape (external-comms-gate.sh
|
|
15
|
+
# line 229). Single fire per gate cycle — agent does not compute the
|
|
16
|
+
# key itself.
|
|
17
|
+
# 2. FALLBACK — when the prompt lacks structure (cached old SKILL.md /
|
|
18
|
+
# agent prompts during the deprecation window), fall back to the
|
|
19
|
+
# agent-emitted EXTERNAL_COMMS_VOICE_TONE_KEY line. Removed after
|
|
20
|
+
# one release cycle per architect direction.
|
|
21
|
+
#
|
|
10
22
|
# The risk-scorer evaluator writes its own per-evaluator marker
|
|
11
23
|
# (external-comms-risk-reviewed-<KEY>) from packages/risk-scorer/hooks/
|
|
12
24
|
# risk-score-mark.sh. When both plugins installed, both gates fire on the
|
|
@@ -16,6 +28,8 @@
|
|
|
16
28
|
set -euo pipefail
|
|
17
29
|
|
|
18
30
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
31
|
+
# shellcheck source=lib/external-comms-key.sh
|
|
32
|
+
source "$SCRIPT_DIR/lib/external-comms-key.sh"
|
|
19
33
|
|
|
20
34
|
INPUT=$(cat)
|
|
21
35
|
|
|
@@ -57,19 +71,46 @@ try:
|
|
|
57
71
|
data = json.load(sys.stdin)
|
|
58
72
|
out = data.get('tool_response', {}).get('output', '')
|
|
59
73
|
if not out:
|
|
60
|
-
|
|
74
|
+
# PostToolUse may deliver content as a list of {type,text} parts
|
|
75
|
+
# (matches the Claude Code PostToolUse hook payload shape).
|
|
76
|
+
tr = data.get('tool_response', {})
|
|
77
|
+
if isinstance(tr, dict):
|
|
78
|
+
content = tr.get('content', [])
|
|
79
|
+
if isinstance(content, list):
|
|
80
|
+
texts = [c.get('text', '') for c in content if isinstance(c, dict) and c.get('type') == 'text']
|
|
81
|
+
if texts:
|
|
82
|
+
out = '\n'.join(texts)
|
|
83
|
+
if not out:
|
|
84
|
+
out = data.get('tool_response', '')
|
|
61
85
|
print(out if isinstance(out, str) else json.dumps(out))
|
|
62
86
|
except Exception:
|
|
63
87
|
print('')
|
|
64
88
|
" 2>/dev/null || echo "")
|
|
65
89
|
|
|
90
|
+
# Extract the prompt the orchestrator sent to the agent — P166 uses this
|
|
91
|
+
# as the canonical source for the marker key (sha256(DRAFT+'\n'+SURFACE)).
|
|
92
|
+
PROMPT=$(printf '%s' "$INPUT" | python3 -c "
|
|
93
|
+
import sys, json
|
|
94
|
+
try:
|
|
95
|
+
print(json.load(sys.stdin).get('tool_input', {}).get('prompt', ''))
|
|
96
|
+
except Exception:
|
|
97
|
+
print('')
|
|
98
|
+
" 2>/dev/null || echo "")
|
|
99
|
+
|
|
66
100
|
RDIR="${TMPDIR:-/tmp}/claude-risk-${SESSION_ID}"
|
|
67
101
|
mkdir -p "$RDIR"
|
|
68
102
|
|
|
69
103
|
VERDICT_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^EXTERNAL_COMMS_VOICE_TONE_VERDICT:' | tail -1) || true
|
|
70
|
-
KEY_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^EXTERNAL_COMMS_VOICE_TONE_KEY:' | tail -1) || true
|
|
71
104
|
VERDICT=$(echo "$VERDICT_LINE" | sed 's/^EXTERNAL_COMMS_VOICE_TONE_VERDICT:[[:space:]]*//' | tr -d '[:space:]')
|
|
72
|
-
|
|
105
|
+
|
|
106
|
+
# ---------- P166 key derivation (primary: from prompt; fallback: from agent emit) ----------
|
|
107
|
+
KEY=$(derive_external_comms_key_from_prompt "$PROMPT")
|
|
108
|
+
if [ -z "$KEY" ]; then
|
|
109
|
+
# Backward-compat: cached old SKILL.md still instructs the agent to emit
|
|
110
|
+
# EXTERNAL_COMMS_VOICE_TONE_KEY. Honour it during the deprecation window.
|
|
111
|
+
KEY_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^EXTERNAL_COMMS_VOICE_TONE_KEY:' | tail -1) || true
|
|
112
|
+
KEY=$(echo "$KEY_LINE" | sed 's/^EXTERNAL_COMMS_VOICE_TONE_KEY:[[:space:]]*//' | tr -d '[:space:]')
|
|
113
|
+
fi
|
|
73
114
|
|
|
74
115
|
# Validate key: 64 hex chars (sha256 output). Reject anything else.
|
|
75
116
|
if echo "$KEY" | grep -qE '^[0-9a-f]{64}$'; then
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared helper: derive the external-comms marker key from an agent's
|
|
3
|
+
# tool_input.prompt by extracting the structured `SURFACE: <name>` line
|
|
4
|
+
# and `<draft>...</draft>` block, then computing
|
|
5
|
+
# sha256(DRAFT + '\n' + SURFACE) — the same key shape the gate computes
|
|
6
|
+
# at PreToolUse time (external-comms-gate.sh line 229).
|
|
7
|
+
#
|
|
8
|
+
# P166 + ADR-028 amended 2026-05-16: the PostToolUse:Agent mark hook
|
|
9
|
+
# derives the marker key from observed runtime state instead of trusting
|
|
10
|
+
# an agent-emitted EXTERNAL_COMMS_<EVAL>_KEY line. Removes the
|
|
11
|
+
# double-invocation cost class — single fire per gate cycle suffices.
|
|
12
|
+
#
|
|
13
|
+
# Canonical source: packages/shared/hooks/lib/external-comms-key.sh
|
|
14
|
+
# Synced byte-identically into each consumer plugin's hooks/lib/ via
|
|
15
|
+
# scripts/sync-external-comms-gate.sh (ADR-017 duplicate-script pattern).
|
|
16
|
+
#
|
|
17
|
+
# Returns the 64-char hex sha256 on stdout when both markers are present
|
|
18
|
+
# in the prompt. Returns empty string when either marker is absent — the
|
|
19
|
+
# caller falls back to the agent-emitted KEY for backward compatibility
|
|
20
|
+
# with cached old SKILL.md / agent prompts.
|
|
21
|
+
|
|
22
|
+
derive_external_comms_key_from_prompt() {
|
|
23
|
+
local prompt="$1"
|
|
24
|
+
[ -n "$prompt" ] || { echo ""; return 0; }
|
|
25
|
+
printf '%s' "$prompt" | python3 -c "
|
|
26
|
+
import sys, re, hashlib
|
|
27
|
+
text = sys.stdin.read()
|
|
28
|
+
# DRAFT extraction: non-greedy match between <draft>...</draft>.
|
|
29
|
+
# Tolerates an optional newline immediately after <draft> and before </draft>
|
|
30
|
+
# so the body content does not capture wrapping newlines.
|
|
31
|
+
draft_match = re.search(r'<draft>\n?(.*?)\n?</draft>', text, re.DOTALL)
|
|
32
|
+
# SURFACE extraction: must be anchored to line start (MULTILINE) to avoid
|
|
33
|
+
# matching prose like 'context says SURFACE: x'. Surface name is a single
|
|
34
|
+
# token: letter + word/hyphen chars.
|
|
35
|
+
surface_match = re.search(r'^SURFACE:\s*([A-Za-z][\w-]*)', text, re.MULTILINE)
|
|
36
|
+
if not draft_match or not surface_match:
|
|
37
|
+
print('')
|
|
38
|
+
sys.exit(0)
|
|
39
|
+
draft = draft_match.group(1)
|
|
40
|
+
surface = surface_match.group(1)
|
|
41
|
+
payload = (draft + '\n' + surface).encode('utf-8')
|
|
42
|
+
print(hashlib.sha256(payload).hexdigest())
|
|
43
|
+
" 2>/dev/null
|
|
44
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
# Behavioural tests for packages/voice-tone/hooks/external-comms-mark-reviewed.sh
|
|
3
|
+
# under P166 hook-side key derivation (ADR-028 amended 2026-05-16).
|
|
4
|
+
#
|
|
5
|
+
# Contract: the PostToolUse:Agent hook derives the marker key from
|
|
6
|
+
# tool_input.prompt's `SURFACE: <name>` + `<draft>...</draft>` structure
|
|
7
|
+
# instead of trusting an agent-emitted EXTERNAL_COMMS_VOICE_TONE_KEY line.
|
|
8
|
+
# On PASS, writes external-comms-voice-tone-reviewed-<KEY> at the derived
|
|
9
|
+
# key. Backward-compat: if the prompt lacks structure, falls back to the
|
|
10
|
+
# agent-emitted KEY line (one release-cycle window per architect direction).
|
|
11
|
+
|
|
12
|
+
setup() {
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
14
|
+
HOOK="$SCRIPT_DIR/external-comms-mark-reviewed.sh"
|
|
15
|
+
ORIG_DIR="$PWD"
|
|
16
|
+
TEST_DIR=$(mktemp -d)
|
|
17
|
+
cd "$TEST_DIR"
|
|
18
|
+
TMPDIR="$TEST_DIR/tmp"
|
|
19
|
+
export TMPDIR
|
|
20
|
+
mkdir -p "$TMPDIR"
|
|
21
|
+
SESSION_ID="test-vt-mark-prompt-$$-${BATS_TEST_NUMBER}"
|
|
22
|
+
RDIR="$TMPDIR/claude-risk-${SESSION_ID}"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
teardown() {
|
|
26
|
+
cd "$ORIG_DIR"
|
|
27
|
+
rm -rf "$TEST_DIR"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Gate-side reference key — same computation as external-comms-gate.sh line 229.
|
|
31
|
+
gate_key() {
|
|
32
|
+
local draft="$1" surface="$2"
|
|
33
|
+
printf '%s\n%s' "$draft" "$surface" | shasum -a 256 | cut -d' ' -f1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Build the PostToolUse:Agent payload and pipe it to the hook.
|
|
37
|
+
# - tool_input.prompt carries the structured prompt the orchestrator sent to the agent.
|
|
38
|
+
# - tool_response.content[0].text carries the agent's stdout (verdict block).
|
|
39
|
+
run_hook() {
|
|
40
|
+
local prompt="$1"
|
|
41
|
+
local agent_output="$2"
|
|
42
|
+
python3 -c "
|
|
43
|
+
import json, sys
|
|
44
|
+
print(json.dumps({
|
|
45
|
+
'tool_name': 'Agent',
|
|
46
|
+
'session_id': '${SESSION_ID}',
|
|
47
|
+
'tool_input': {'subagent_type': 'wr-voice-tone:external-comms', 'prompt': sys.argv[1]},
|
|
48
|
+
'tool_response': {'content': [{'type': 'text', 'text': sys.argv[2]}]}
|
|
49
|
+
}))" "$prompt" "$agent_output" | bash "$HOOK"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@test "PASS with structured prompt: marker lands at hook-derived key" {
|
|
53
|
+
DRAFT="we noticed a build failure on Node 20"
|
|
54
|
+
SURFACE="changeset-author"
|
|
55
|
+
PROMPT=$'SURFACE: '"$SURFACE"$'\n<draft>\n'"$DRAFT"$'\n</draft>\nReview against docs/VOICE-AND-TONE.md.'
|
|
56
|
+
AGENT_OUTPUT=$'no voice/tone violation matched\nEXTERNAL_COMMS_VOICE_TONE_VERDICT: PASS'
|
|
57
|
+
run_hook "$PROMPT" "$AGENT_OUTPUT"
|
|
58
|
+
KEY=$(gate_key "$DRAFT" "$SURFACE")
|
|
59
|
+
[ -f "$RDIR/external-comms-voice-tone-reviewed-${KEY}" ]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@test "FAIL with structured prompt: no marker written" {
|
|
63
|
+
DRAFT="happy to help further on this 2-year-old issue"
|
|
64
|
+
SURFACE="gh-issue-comment"
|
|
65
|
+
PROMPT=$'SURFACE: '"$SURFACE"$'\n<draft>\n'"$DRAFT"$'\n</draft>'
|
|
66
|
+
AGENT_OUTPUT=$'EXTERNAL_COMMS_VOICE_TONE_VERDICT: FAIL\nEXTERNAL_COMMS_VOICE_TONE_REASON: banned closer'
|
|
67
|
+
run_hook "$PROMPT" "$AGENT_OUTPUT"
|
|
68
|
+
KEY=$(gate_key "$DRAFT" "$SURFACE")
|
|
69
|
+
[ ! -f "$RDIR/external-comms-voice-tone-reviewed-${KEY}" ]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@test "PASS with structured prompt AND agent-emitted KEY: hook-derived key wins" {
|
|
73
|
+
DRAFT="hook-derived path text"
|
|
74
|
+
SURFACE="gh-pr-create"
|
|
75
|
+
PROMPT=$'SURFACE: '"$SURFACE"$'\n<draft>\n'"$DRAFT"$'\n</draft>'
|
|
76
|
+
# Agent emits a different (wrong) key — hook must ignore it in favour of derived key.
|
|
77
|
+
BOGUS_KEY="0000000000000000000000000000000000000000000000000000000000000000"
|
|
78
|
+
AGENT_OUTPUT=$'EXTERNAL_COMMS_VOICE_TONE_VERDICT: PASS\nEXTERNAL_COMMS_VOICE_TONE_KEY: '"$BOGUS_KEY"
|
|
79
|
+
run_hook "$PROMPT" "$AGENT_OUTPUT"
|
|
80
|
+
DERIVED_KEY=$(gate_key "$DRAFT" "$SURFACE")
|
|
81
|
+
[ -f "$RDIR/external-comms-voice-tone-reviewed-${DERIVED_KEY}" ]
|
|
82
|
+
[ ! -f "$RDIR/external-comms-voice-tone-reviewed-${BOGUS_KEY}" ]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@test "backward-compat: PASS with no structured prompt but agent-emitted KEY lands marker" {
|
|
86
|
+
# Cached old SKILL.md still tells the agent to emit the KEY; hook honours it.
|
|
87
|
+
LEGACY_KEY="abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
|
|
88
|
+
PROMPT="please review this draft (legacy unstructured prompt)"
|
|
89
|
+
AGENT_OUTPUT=$'EXTERNAL_COMMS_VOICE_TONE_VERDICT: PASS\nEXTERNAL_COMMS_VOICE_TONE_KEY: '"$LEGACY_KEY"
|
|
90
|
+
run_hook "$PROMPT" "$AGENT_OUTPUT"
|
|
91
|
+
[ -f "$RDIR/external-comms-voice-tone-reviewed-${LEGACY_KEY}" ]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@test "no structured prompt and no agent KEY: no marker written" {
|
|
95
|
+
PROMPT="legacy prompt"
|
|
96
|
+
AGENT_OUTPUT=$'EXTERNAL_COMMS_VOICE_TONE_VERDICT: PASS'
|
|
97
|
+
run_hook "$PROMPT" "$AGENT_OUTPUT"
|
|
98
|
+
# Nothing to key on — no marker at any path under RDIR.
|
|
99
|
+
found=$(ls "$RDIR" 2>/dev/null | wc -l | tr -d ' ')
|
|
100
|
+
[ "$found" -eq 0 ]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@test "ignores unrelated subagent types" {
|
|
104
|
+
PROMPT=$'SURFACE: gh-issue-create\n<draft>\nbody\n</draft>'
|
|
105
|
+
AGENT_OUTPUT=$'EXTERNAL_COMMS_VOICE_TONE_VERDICT: PASS'
|
|
106
|
+
# Swap the subagent type in the input to an unrelated one.
|
|
107
|
+
python3 -c "
|
|
108
|
+
import json, sys
|
|
109
|
+
print(json.dumps({
|
|
110
|
+
'tool_name': 'Agent',
|
|
111
|
+
'session_id': '${SESSION_ID}',
|
|
112
|
+
'tool_input': {'subagent_type': 'wr-architect:agent', 'prompt': sys.argv[1]},
|
|
113
|
+
'tool_response': {'content': [{'type': 'text', 'text': sys.argv[2]}]}
|
|
114
|
+
}))" "$PROMPT" "$AGENT_OUTPUT" | bash "$HOOK"
|
|
115
|
+
found=$(ls "$RDIR" 2>/dev/null | wc -l | tr -d ' ')
|
|
116
|
+
[ "$found" -eq 0 ]
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -53,12 +53,24 @@ Do not ask if the surface is obvious from the conversation context.
|
|
|
53
53
|
|
|
54
54
|
### 3. Construct the review prompt
|
|
55
55
|
|
|
56
|
-
Build a self-contained prompt for the `wr-voice-tone:external-comms` subagent
|
|
56
|
+
Build a self-contained prompt for the `wr-voice-tone:external-comms` subagent. The prompt MUST be structured so the PostToolUse hook can derive the marker key locally (P166 / ADR-028 amended 2026-05-16) — single fire per gate cycle suffices:
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
```
|
|
59
|
+
SURFACE: <surface-name>
|
|
60
|
+
<draft>
|
|
61
|
+
<draft body verbatim>
|
|
62
|
+
</draft>
|
|
63
|
+
|
|
64
|
+
Destination: <destination if known>
|
|
65
|
+
Review against docs/VOICE-AND-TONE.md.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Two requirements:
|
|
69
|
+
|
|
70
|
+
- A leading line `SURFACE: <surface-name>` where `<surface-name>` is one of the canonical strings (`gh-issue-create`, `gh-pr-comment`, etc.) — anchored to line start, single token.
|
|
71
|
+
- The **draft body** wrapped verbatim inside `<draft>...</draft>` markers — the hook extracts everything between these markers and uses it for `sha256(DRAFT + '\n' + SURFACE)`.
|
|
72
|
+
|
|
73
|
+
The orchestrator does NOT pre-compute the key — the hook derives it from the prompt structure. Skip the agent-emitted key entirely.
|
|
62
74
|
|
|
63
75
|
### 4. Delegate to wr-voice-tone:external-comms
|
|
64
76
|
|
|
@@ -69,7 +81,7 @@ subagent_type: wr-voice-tone:external-comms
|
|
|
69
81
|
prompt: <constructed review prompt from step 3>
|
|
70
82
|
```
|
|
71
83
|
|
|
72
|
-
Wait for the subagent to complete. The subagent
|
|
84
|
+
Wait for the subagent to complete. The subagent outputs a structured verdict block (`EXTERNAL_COMMS_VOICE_TONE_VERDICT: PASS|FAIL` + optional `EXTERNAL_COMMS_VOICE_TONE_REASON: ...` on FAIL). The `PostToolUse:Agent` hook (`external-comms-mark-reviewed.sh`) parses the verdict, derives the marker key from the prompt's `SURFACE:` + `<draft>` structure, and writes the per-evaluator marker automatically on PASS.
|
|
73
85
|
|
|
74
86
|
**Do not write to `${TMPDIR:-/tmp}/claude-risk-*` yourself.** The hook is the only correct mechanism.
|
|
75
87
|
|