@windyroad/voice-tone 0.7.3 → 0.8.0
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/plugins/marketplace.json +1 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +42 -0
- package/bin/install.mjs +9 -4
- package/hooks/hooks.json +61 -7
- package/hooks-codex/codex-adapter.sh +27 -0
- package/hooks-codex/hooks.json +71 -0
- package/lib/install-utils.mjs +138 -8
- package/package.json +9 -1
- package/skills/assess-external-comms/SKILL.md +15 -7
- package/skills/assess-external-comms/agents/openai.yaml +3 -0
- package/skills/update-guide/SKILL.md +10 -2
- package/skills/update-guide/agents/openai.yaml +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"windyroad-voice-tone-local","interface":{"displayName":"Windy Road Voice and Tone"},"plugins":[{"name":"wr-voice-tone","source":{"source":"local","path":"."},"policy":{"installation":"AVAILABLE","authentication":"ON_INSTALL"},"category":"Developer Tools"}]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wr-voice-tone",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Voice, tone, and external-communications governance",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Windy Road Technology",
|
|
7
|
+
"url": "https://windyroad.com.au"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/windyroad/agent-plugins/tree/main/packages/voice-tone",
|
|
10
|
+
"repository": "https://github.com/windyroad/agent-plugins",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"voice",
|
|
14
|
+
"tone",
|
|
15
|
+
"copy",
|
|
16
|
+
"communications",
|
|
17
|
+
"codex",
|
|
18
|
+
"claude-code"
|
|
19
|
+
],
|
|
20
|
+
"skills": "./skills/",
|
|
21
|
+
"interface": {
|
|
22
|
+
"displayName": "Windy Road Voice and Tone",
|
|
23
|
+
"shortDescription": "Govern user-facing and outbound copy",
|
|
24
|
+
"longDescription": "Review user-facing copy against a project voice-and-tone guide and assess outbound communications before sending.",
|
|
25
|
+
"developerName": "Windy Road Technology",
|
|
26
|
+
"category": "Developer Tools",
|
|
27
|
+
"capabilities": [
|
|
28
|
+
"Interactive",
|
|
29
|
+
"Read",
|
|
30
|
+
"Write"
|
|
31
|
+
],
|
|
32
|
+
"websiteURL": "https://windyroad.com.au",
|
|
33
|
+
"privacyPolicyURL": "https://windyroad.com.au/privacy",
|
|
34
|
+
"termsOfServiceURL": "https://windyroad.com.au/terms",
|
|
35
|
+
"defaultPrompt": [
|
|
36
|
+
"Review this copy against our voice and tone",
|
|
37
|
+
"Assess this outbound communication"
|
|
38
|
+
],
|
|
39
|
+
"brandColor": "#B45309",
|
|
40
|
+
"screenshots": []
|
|
41
|
+
}
|
|
42
|
+
}
|
package/bin/install.mjs
CHANGED
|
@@ -8,6 +8,10 @@ const utils = await import(resolve(__dirname, "../lib/install-utils.mjs"));
|
|
|
8
8
|
|
|
9
9
|
const PLUGIN = "wr-voice-tone";
|
|
10
10
|
const DEPS = [];
|
|
11
|
+
const AGENTS = [
|
|
12
|
+
{ source: "agents/agent.md", name: "wr-voice-tone:agent", filename: "wr-voice-tone-agent.toml" },
|
|
13
|
+
{ source: "agents/external-comms.md", name: "wr-voice-tone:external-comms", filename: "wr-voice-tone-external-comms.toml" },
|
|
14
|
+
];
|
|
11
15
|
|
|
12
16
|
const flags = utils.parseStandardArgs(process.argv);
|
|
13
17
|
|
|
@@ -21,6 +25,7 @@ Options:
|
|
|
21
25
|
--update Update this plugin and its skills
|
|
22
26
|
--uninstall Remove this plugin
|
|
23
27
|
--scope Installation scope: project (default) or user
|
|
28
|
+
--runtime Runtime to install for: claude (default), codex, or both
|
|
24
29
|
--dry-run Show what would be done without executing
|
|
25
30
|
--help, -h Show this help
|
|
26
31
|
`);
|
|
@@ -32,12 +37,12 @@ if (flags.dryRun) {
|
|
|
32
37
|
console.log("[dry-run mode — no commands will be executed]\n");
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
utils.checkPrerequisites();
|
|
40
|
+
utils.checkPrerequisites({ runtime: flags.runtime });
|
|
36
41
|
|
|
37
42
|
if (flags.uninstall) {
|
|
38
|
-
utils.uninstallPackage(PLUGIN);
|
|
43
|
+
utils.uninstallPackage(PLUGIN, { agents: AGENTS, scope: flags.scope, runtime: flags.runtime });
|
|
39
44
|
} else if (flags.update) {
|
|
40
|
-
utils.updatePackage(PLUGIN, { scope: flags.scope });
|
|
45
|
+
utils.updatePackage(PLUGIN, { agents: AGENTS, scope: flags.scope, runtime: flags.runtime });
|
|
41
46
|
} else {
|
|
42
|
-
utils.installPackage(PLUGIN, { deps: DEPS, scope: flags.scope });
|
|
47
|
+
utils.installPackage(PLUGIN, { agents: AGENTS, deps: DEPS, scope: flags.scope, runtime: flags.runtime });
|
|
43
48
|
}
|
package/hooks/hooks.json
CHANGED
|
@@ -1,17 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
3
|
"UserPromptSubmit": [
|
|
4
|
-
{
|
|
5
|
-
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/voice-tone-eval.sh"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"hooks": [
|
|
14
|
+
{
|
|
15
|
+
"type": "command",
|
|
16
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/staleness-check.sh"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
6
20
|
],
|
|
7
21
|
"PreToolUse": [
|
|
8
|
-
{
|
|
9
|
-
|
|
22
|
+
{
|
|
23
|
+
"matcher": "Edit|Write",
|
|
24
|
+
"hooks": [
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/voice-tone-enforce-edit.sh\""
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"matcher": "Bash|Edit|Write",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/external-comms-gate.sh\""
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
10
40
|
],
|
|
11
41
|
"PostToolUse": [
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
42
|
+
{
|
|
43
|
+
"matcher": "Agent",
|
|
44
|
+
"hooks": [
|
|
45
|
+
{
|
|
46
|
+
"type": "command",
|
|
47
|
+
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/voice-tone-mark-reviewed.sh\""
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"matcher": "Agent",
|
|
53
|
+
"hooks": [
|
|
54
|
+
{
|
|
55
|
+
"type": "command",
|
|
56
|
+
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/external-comms-mark-reviewed.sh\""
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"matcher": "Agent|Bash|Skill",
|
|
62
|
+
"hooks": [
|
|
63
|
+
{
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/voice-tone-slide-marker.sh\""
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
15
69
|
]
|
|
16
70
|
}
|
|
17
71
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
target="$1"
|
|
5
|
+
input="$(cat)"
|
|
6
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
|
|
7
|
+
|
|
8
|
+
run_hook() {
|
|
9
|
+
printf '%s' "$1" | "$target"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if [ "$tool" = "apply_patch" ]; then
|
|
13
|
+
paths="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' | sed -nE 's/^\*\*\* (Add|Update|Delete) File: (.*)$/\2/p')"
|
|
14
|
+
if [ -n "$paths" ]; then
|
|
15
|
+
while IFS= read -r path; do
|
|
16
|
+
payload="$(printf '%s' "$input" | jq --arg path "$path" '.tool_name = "Edit" | .tool_input.file_path = $path')"
|
|
17
|
+
run_hook "$payload" || exit $?
|
|
18
|
+
done <<< "$paths"
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if [ "$tool" = "spawn_agent" ] || [ "$tool" = "Agent" ]; then
|
|
24
|
+
input="$(printf '%s' "$input" | jq '.tool_name = "Agent" | .tool_input.subagent_type = (.tool_input.agent_type // .tool_input.subagent_type // "") | .tool_input.prompt = (.tool_input.message // .tool_input.prompt // "")')"
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
run_hook "$input"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/voice-tone-eval.sh"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"hooks": [
|
|
14
|
+
{
|
|
15
|
+
"type": "command",
|
|
16
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/staleness-check.sh"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"PreToolUse": [
|
|
22
|
+
{
|
|
23
|
+
"matcher": "Edit|Write",
|
|
24
|
+
"hooks": [
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/voice-tone-enforce-edit.sh\""
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"matcher": "Bash|Edit|Write",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/external-comms-gate.sh\""
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"PostToolUse": [
|
|
42
|
+
{
|
|
43
|
+
"matcher": "Agent",
|
|
44
|
+
"hooks": [
|
|
45
|
+
{
|
|
46
|
+
"type": "command",
|
|
47
|
+
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/voice-tone-mark-reviewed.sh\""
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"matcher": "Agent",
|
|
53
|
+
"hooks": [
|
|
54
|
+
{
|
|
55
|
+
"type": "command",
|
|
56
|
+
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/external-comms-mark-reviewed.sh\""
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"matcher": "Agent|Bash|Skill",
|
|
62
|
+
"hooks": [
|
|
63
|
+
{
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/voice-tone-slide-marker.sh\""
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}
|
package/lib/install-utils.mjs
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { dirname, join, resolve } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
7
12
|
|
|
8
13
|
const MARKETPLACE_REPO = "windyroad/agent-plugins";
|
|
9
14
|
const MARKETPLACE_NAME = "windyroad";
|
|
10
15
|
const CODEX_MARKETPLACE_PATH = ".";
|
|
11
16
|
const CODEX_MARKETPLACE_NAME = "windyroad-local";
|
|
17
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
18
|
|
|
13
19
|
let _dryRun = false;
|
|
14
20
|
|
|
@@ -82,6 +88,132 @@ export function addCodexMarketplace() {
|
|
|
82
88
|
);
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
function codexMarketplace(pluginName) {
|
|
92
|
+
return `windyroad-${pluginName.replace(/^wr-/, "")}-local`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function codexMarketplaceRoot(pluginName) {
|
|
96
|
+
const version = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")).version;
|
|
97
|
+
return join(process.env.CODEX_HOME || join(homedir(), ".codex"), ".tmp", "marketplaces", `${pluginName}-${version}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function codexAgentDir(scope) {
|
|
101
|
+
return scope === "user"
|
|
102
|
+
? join(process.env.CODEX_HOME || join(homedir(), ".codex"), "agents")
|
|
103
|
+
: join(process.cwd(), ".codex", "agents");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function codexTerms(text) {
|
|
107
|
+
return text
|
|
108
|
+
.replaceAll("AskUserQuestion", "request_user_input")
|
|
109
|
+
.replaceAll("Agent tool", "native Codex subagent tool")
|
|
110
|
+
.replaceAll("Skill tool", "installed skill invocation")
|
|
111
|
+
.replaceAll("Claude Code", "Codex")
|
|
112
|
+
.replace(/\bClaude\b/g, "Codex")
|
|
113
|
+
.replaceAll(".claude", ".codex");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function splitFrontmatter(markdown) {
|
|
117
|
+
const end = markdown.indexOf("\n---\n", 4);
|
|
118
|
+
return markdown.startsWith("---\n") && end !== -1
|
|
119
|
+
? { frontmatter: markdown.slice(4, end), body: markdown.slice(end + 5) }
|
|
120
|
+
: { frontmatter: "", body: markdown };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function frontmatterDescription(frontmatter) {
|
|
124
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
125
|
+
const start = lines.findIndex((line) => line.startsWith("description:"));
|
|
126
|
+
if (start === -1) return "Windy Road reviewer.";
|
|
127
|
+
const value = [lines[start].slice("description:".length).trim()];
|
|
128
|
+
for (let index = start + 1; index < lines.length && /^\s+/.test(lines[index]); index += 1) {
|
|
129
|
+
value.push(lines[index].trim());
|
|
130
|
+
}
|
|
131
|
+
return value.filter(Boolean).join(" ");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderCodexAgent(pluginName, agent) {
|
|
135
|
+
const source = join(PACKAGE_ROOT, agent.source);
|
|
136
|
+
const { frontmatter, body } = splitFrontmatter(readFileSync(source, "utf8"));
|
|
137
|
+
const codexInstructions = agent.name === "wr-voice-tone:external-comms"
|
|
138
|
+
? `\n\n## Codex completion marker compatibility\n\nOn PASS, compute the lowercase SHA-256 marker key using the normalization specified above and append \`EXTERNAL_COMMS_VOICE_TONE_KEY: <64 lowercase hex characters>\`. Codex may hide the spawn prompt from PostToolUse hooks, so this emitted key is required. On FAIL, do not emit a key.`
|
|
139
|
+
: "";
|
|
140
|
+
const payload = [
|
|
141
|
+
`# Do not edit by hand; update ${agent.source} and reinstall.`,
|
|
142
|
+
`name = ${JSON.stringify(agent.name)}`,
|
|
143
|
+
`description = ${JSON.stringify(frontmatterDescription(frontmatter))}`,
|
|
144
|
+
'sandbox_mode = "read-only"',
|
|
145
|
+
'developer_instructions = """',
|
|
146
|
+
codexTerms(`${body.trimEnd()}${codexInstructions}`).replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"').trimEnd(),
|
|
147
|
+
'"""',
|
|
148
|
+
"",
|
|
149
|
+
].join("\n");
|
|
150
|
+
const owner = `# Generated by @windyroad/${pluginName.replace(/^wr-/, "")} from ${agent.source}.`;
|
|
151
|
+
const hash = createHash("sha256").update(payload).digest("hex");
|
|
152
|
+
return `${owner}\n# Generated content SHA-256: ${hash}\n${payload}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isOwnedCodexAgent(content, pluginName, agent) {
|
|
156
|
+
const owner = `# Generated by @windyroad/${pluginName.replace(/^wr-/, "")} from ${agent.source}.`;
|
|
157
|
+
const lines = content.split("\n");
|
|
158
|
+
const hash = lines[1]?.match(/^# Generated content SHA-256: ([0-9a-f]{64})$/)?.[1];
|
|
159
|
+
return content.startsWith(`${owner}\n`) && Boolean(hash)
|
|
160
|
+
&& createHash("sha256").update(lines.slice(2).join("\n")).digest("hex") === hash;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function installCodexAgents(pluginName, agents, scope) {
|
|
164
|
+
if (agents.length === 0 || _dryRun) return;
|
|
165
|
+
const targetDir = codexAgentDir(scope);
|
|
166
|
+
mkdirSync(targetDir, { recursive: true });
|
|
167
|
+
for (const agent of agents) {
|
|
168
|
+
const target = join(targetDir, agent.filename);
|
|
169
|
+
const expected = renderCodexAgent(pluginName, agent);
|
|
170
|
+
if (existsSync(target)) {
|
|
171
|
+
const current = readFileSync(target, "utf8");
|
|
172
|
+
if (current === expected) continue;
|
|
173
|
+
if (!isOwnedCodexAgent(current, pluginName, agent)) {
|
|
174
|
+
console.log(`Preserved user-managed Codex agent at ${target}.`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
writeFileSync(target, expected, "utf8");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function uninstallCodexAgents(pluginName, agents, scope) {
|
|
183
|
+
if (_dryRun) return;
|
|
184
|
+
for (const targetDir of new Set([codexAgentDir(scope), codexAgentDir("user")])) {
|
|
185
|
+
for (const agent of agents) {
|
|
186
|
+
const target = join(targetDir, agent.filename);
|
|
187
|
+
if (existsSync(target) && isOwnedCodexAgent(readFileSync(target, "utf8"), pluginName, agent)) rmSync(target);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function installPackedCodexPlugin(pluginName, { agents = [], scope = "project" } = {}) {
|
|
193
|
+
const marketplace = codexMarketplace(pluginName);
|
|
194
|
+
const root = codexMarketplaceRoot(pluginName);
|
|
195
|
+
if (!_dryRun) {
|
|
196
|
+
rmSync(root, { recursive: true, force: true });
|
|
197
|
+
mkdirSync(dirname(root), { recursive: true });
|
|
198
|
+
cpSync(PACKAGE_ROOT, root, { recursive: true });
|
|
199
|
+
const hooks = join(root, "hooks-codex", "hooks.json");
|
|
200
|
+
if (existsSync(hooks)) cpSync(hooks, join(root, "hooks", "hooks.json"));
|
|
201
|
+
}
|
|
202
|
+
if (!run(`codex plugin marketplace add ${JSON.stringify(root)}`, `Codex marketplace: ${marketplace}`)) return false;
|
|
203
|
+
if (!run(`codex plugin add ${pluginName}@${marketplace}`, pluginName)) return false;
|
|
204
|
+
installCodexAgents(pluginName, agents, scope);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function uninstallPackedCodexPlugin(pluginName, { agents = [], scope = "project" } = {}) {
|
|
209
|
+
const marketplace = codexMarketplace(pluginName);
|
|
210
|
+
const removed = run(`codex plugin remove ${pluginName}@${marketplace}`, `Removing ${pluginName}`);
|
|
211
|
+
run(`codex plugin marketplace remove ${marketplace}`, `Removing ${marketplace}`);
|
|
212
|
+
uninstallCodexAgents(pluginName, agents, scope);
|
|
213
|
+
if (!_dryRun) rmSync(codexMarketplaceRoot(pluginName), { recursive: true, force: true });
|
|
214
|
+
return removed;
|
|
215
|
+
}
|
|
216
|
+
|
|
85
217
|
export function installPlugin(pluginName, { scope = "project" } = {}) {
|
|
86
218
|
return run(
|
|
87
219
|
`claude plugin install ${pluginName}@${MARKETPLACE_NAME} --scope ${scope}`,
|
|
@@ -121,7 +253,7 @@ export function uninstallCodexPlugin(pluginName) {
|
|
|
121
253
|
/**
|
|
122
254
|
* Install a single package: marketplace add + plugin install.
|
|
123
255
|
*/
|
|
124
|
-
export function installPackage(pluginName, { deps = [], scope = "project", runtime = "claude" } = {}) {
|
|
256
|
+
export function installPackage(pluginName, { agents = [], deps = [], scope = "project", runtime = "claude" } = {}) {
|
|
125
257
|
console.log(`\nInstalling @windyroad/${pluginName.replace("wr-", "")} (${scope} scope)...\n`);
|
|
126
258
|
|
|
127
259
|
if (runtime === "claude" || runtime === "both") {
|
|
@@ -130,8 +262,7 @@ export function installPackage(pluginName, { deps = [], scope = "project", runti
|
|
|
130
262
|
}
|
|
131
263
|
|
|
132
264
|
if (runtime === "codex" || runtime === "both") {
|
|
133
|
-
|
|
134
|
-
installCodexPlugin(pluginName);
|
|
265
|
+
if (!installPackedCodexPlugin(pluginName, { agents, scope })) process.exitCode = 1;
|
|
135
266
|
}
|
|
136
267
|
|
|
137
268
|
if (deps.length > 0) {
|
|
@@ -149,7 +280,7 @@ export function installPackage(pluginName, { deps = [], scope = "project", runti
|
|
|
149
280
|
/**
|
|
150
281
|
* Update a single package.
|
|
151
282
|
*/
|
|
152
|
-
export function updatePackage(pluginName, { scope = "project", runtime = "claude" } = {}) {
|
|
283
|
+
export function updatePackage(pluginName, { agents = [], scope = "project", runtime = "claude" } = {}) {
|
|
153
284
|
console.log(`\nUpdating @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
154
285
|
|
|
155
286
|
if (runtime === "claude" || runtime === "both") {
|
|
@@ -161,8 +292,7 @@ export function updatePackage(pluginName, { scope = "project", runtime = "claude
|
|
|
161
292
|
}
|
|
162
293
|
|
|
163
294
|
if (runtime === "codex" || runtime === "both") {
|
|
164
|
-
|
|
165
|
-
installCodexPlugin(pluginName);
|
|
295
|
+
if (!installPackedCodexPlugin(pluginName, { agents, scope })) process.exitCode = 1;
|
|
166
296
|
}
|
|
167
297
|
|
|
168
298
|
console.log(`\nDone! Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to apply updates.\n`);
|
|
@@ -171,7 +301,7 @@ export function updatePackage(pluginName, { scope = "project", runtime = "claude
|
|
|
171
301
|
/**
|
|
172
302
|
* Uninstall a single package.
|
|
173
303
|
*/
|
|
174
|
-
export function uninstallPackage(pluginName, { runtime = "claude" } = {}) {
|
|
304
|
+
export function uninstallPackage(pluginName, { agents = [], scope = "project", runtime = "claude" } = {}) {
|
|
175
305
|
console.log(`\nUninstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
176
306
|
|
|
177
307
|
if (runtime === "claude" || runtime === "both") {
|
|
@@ -179,7 +309,7 @@ export function uninstallPackage(pluginName, { runtime = "claude" } = {}) {
|
|
|
179
309
|
}
|
|
180
310
|
|
|
181
311
|
if (runtime === "codex" || runtime === "both") {
|
|
182
|
-
|
|
312
|
+
if (!uninstallPackedCodexPlugin(pluginName, { agents, scope })) process.exitCode = 1;
|
|
183
313
|
}
|
|
184
314
|
|
|
185
315
|
console.log(`\nDone. Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to apply changes.\n`);
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windyroad/voice-tone",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Voice and tone enforcement for user-facing copy",
|
|
5
5
|
"bin": {
|
|
6
6
|
"windyroad-voice-tone": "./bin/install.mjs"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"prepack": "node ../../scripts/sync-codex-plugin-surfaces.mjs voice-tone --pack",
|
|
11
|
+
"postpack": "node ../../scripts/sync-codex-plugin-surfaces.mjs voice-tone --restore-pack"
|
|
12
|
+
},
|
|
9
13
|
"license": "MIT",
|
|
10
14
|
"repository": {
|
|
11
15
|
"type": "git",
|
|
@@ -15,6 +19,7 @@
|
|
|
15
19
|
"keywords": [
|
|
16
20
|
"claude-code",
|
|
17
21
|
"claude-code-plugin",
|
|
22
|
+
"codex",
|
|
18
23
|
"ai-agent",
|
|
19
24
|
"ai-coding"
|
|
20
25
|
],
|
|
@@ -23,7 +28,10 @@
|
|
|
23
28
|
"agents/",
|
|
24
29
|
"hooks/",
|
|
25
30
|
"skills/",
|
|
31
|
+
"hooks-codex/",
|
|
32
|
+
".agents/",
|
|
26
33
|
".claude-plugin/",
|
|
34
|
+
".codex-plugin/",
|
|
27
35
|
"lib/",
|
|
28
36
|
"!hooks/test/"
|
|
29
37
|
]
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wr-voice-tone:assess-external-comms
|
|
3
3
|
description: On-demand external-comms voice & tone review. Reviews a draft of an outbound prose tool call (gh issue/pr body, security advisory, npm publish content, or .changeset/*.md body) against docs/VOICE-AND-TONE.md. Delegates to wr-voice-tone:external-comms and pre-satisfies the external-comms-gate marker for the current session.
|
|
4
|
-
allowed-tools: Read, Glob, Grep, Bash,
|
|
4
|
+
allowed-tools: Read, Glob, Grep, Bash, request_user_input, Skill
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
<!-- Generated from the runtime-neutral skill source. Do not edit. -->
|
|
8
|
+
|
|
9
|
+
> Codex runtime note: invoke installed skills directly, use
|
|
10
|
+
> `request_user_input` only when the contract requires a human decision, and
|
|
11
|
+
> use native Codex subagents for agent delegation. Resolve bundled files from
|
|
12
|
+
> this installed plugin instead of the adopter repository.
|
|
13
|
+
|
|
14
|
+
|
|
7
15
|
# External-Comms Voice & Tone Assessment Skill
|
|
8
16
|
|
|
9
17
|
Run a voice & tone review on demand against any drafted outbound prose — outside a hook gate trigger. Pre-satisfies the `external-comms-gate.sh` marker for the current session so the gated tool call (gh issue/pr/api/npm publish/changeset write) proceeds without re-prompting.
|
|
@@ -34,11 +42,11 @@ If both draft and surface are present, proceed to step 3. If either is missing,
|
|
|
34
42
|
|
|
35
43
|
### 2. Resolve missing context
|
|
36
44
|
|
|
37
|
-
If the draft is missing, use `
|
|
45
|
+
If the draft is missing, use `request_user_input`:
|
|
38
46
|
|
|
39
47
|
> "What draft do you want me to review? Paste the body verbatim — I will pass it to the external-comms voice-tone reviewer."
|
|
40
48
|
|
|
41
|
-
If the surface is missing AND cannot be inferred from context (e.g. user just said "before I post this comment"), use `
|
|
49
|
+
If the surface is missing AND cannot be inferred from context (e.g. user just said "before I post this comment"), use `request_user_input`:
|
|
42
50
|
|
|
43
51
|
- header: "Target surface"
|
|
44
52
|
- options:
|
|
@@ -74,11 +82,11 @@ The orchestrator does NOT pre-compute the key — the hook derives it from the p
|
|
|
74
82
|
|
|
75
83
|
### 4. Delegate to wr-voice-tone:external-comms
|
|
76
84
|
|
|
77
|
-
Invoke the subagent via the
|
|
85
|
+
Invoke the subagent via the native Codex subagent tool. Dispatch it **synchronously** (`wait_for_completion: true`) — the `PostToolUse:Agent` mark hook (`external-comms-mark-reviewed.sh`) fires reliably only for a synchronous agent; a background-launched reviewer's mark hook does not fire in time, so no marker persists and the voice-tone gate re-blocks despite a PASS verdict (P402):
|
|
78
86
|
|
|
79
87
|
```
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
agent_type: wr-voice-tone:external-comms
|
|
89
|
+
wait_for_completion: true
|
|
82
90
|
prompt: <constructed review prompt from step 3>
|
|
83
91
|
```
|
|
84
92
|
|
|
@@ -99,7 +107,7 @@ When both evaluators are required (voice-tone + risk-scorer both installed), rem
|
|
|
99
107
|
|
|
100
108
|
### 6. Above-appetite handling (ADR-013 Rule 6)
|
|
101
109
|
|
|
102
|
-
If the verdict is FAIL, do NOT auto-rewrite the draft. Use `
|
|
110
|
+
If the verdict is FAIL, do NOT auto-rewrite the draft. Use `request_user_input`:
|
|
103
111
|
|
|
104
112
|
- header: "Voice/tone violation — next step"
|
|
105
113
|
- options:
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wr-voice-tone:update-guide
|
|
3
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,
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, request_user_input
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
<!-- Generated from the runtime-neutral skill source. Do not edit. -->
|
|
8
|
+
|
|
9
|
+
> Codex runtime note: invoke installed skills directly, use
|
|
10
|
+
> `request_user_input` only when the contract requires a human decision, and
|
|
11
|
+
> use native Codex subagents for agent delegation. Resolve bundled files from
|
|
12
|
+
> this installed plugin instead of the adopter repository.
|
|
13
|
+
|
|
14
|
+
|
|
7
15
|
# Voice and Tone Guide Generator
|
|
8
16
|
|
|
9
17
|
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.
|
|
@@ -74,7 +82,7 @@ At least 5 concrete pairs drawn from the actual project's content patterns. Show
|
|
|
74
82
|
|
|
75
83
|
### 4. Confirm with the user
|
|
76
84
|
|
|
77
|
-
You MUST use the
|
|
85
|
+
You MUST use the request_user_input tool to collect user confirmation. Do not proceed to step 5 until you have answers.
|
|
78
86
|
|
|
79
87
|
Present:
|
|
80
88
|
1. The drafted brand voice adjectives and ask if they match the intended personality
|