@windyroad/risk-scorer 0.18.17 → 0.18.18
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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/lib/install-utils.mjs +138 -8
- package/package.json +1 -1
- package/scripts/sync-codex-skills.mjs +14 -3
- package/skills/assess-external-comms/SKILL.md +1 -1
- package/skills/assess-inbound-report/SKILL.md +1 -1
- package/skills/assess-release/SKILL.md +1 -1
- package/skills/assess-wip/SKILL.md +1 -1
- package/skills/bootstrap-catalog/SKILL.md +1 -1
- package/skills/create-risk/SKILL.md +1 -1
- package/skills/external-comms/SKILL.md +1 -1
- package/skills/pipeline/SKILL.md +1 -1
- package/skills/update-policy/SKILL.md +1 -1
- package/skills/wip/SKILL.md +1 -1
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
|
@@ -36,8 +36,19 @@ const updatePolicyCodexAgentStep = `Run the Codex policy reviewer with this prom
|
|
|
36
36
|
|
|
37
37
|
Use a native Codex subagent workflow with the installed custom agent named \`wr-risk-scorer:policy\`. If it is not visible in the current session, restart Codex; do not substitute a different agent identity because the policy marker hook consumes it.`;
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// Codex namespaces every skill by the plugin manifest `name`, so a skill's own
|
|
40
|
+
// frontmatter `name:` must be BARE or the prefix lands twice and the advertised
|
|
41
|
+
// invocation cannot be typed (P527). Scoped to the frontmatter block so a body
|
|
42
|
+
// line starting `name:` is never rewritten.
|
|
43
|
+
function bareName(skill, text) {
|
|
44
|
+
if (!text.startsWith("---\n")) return text;
|
|
45
|
+
const end = text.indexOf("\n---\n", 4);
|
|
46
|
+
if (end === -1) return text;
|
|
47
|
+
return text.slice(0, end).replace(/^name:.*$/m, `name: ${skill}`) + text.slice(end);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function transform(skill, text) {
|
|
51
|
+
const transformed = bareName(skill, text)
|
|
41
52
|
.replaceAll("AskUserQuestion", "request_user_input")
|
|
42
53
|
.replaceAll("`.claude/agents/risk-scorer-pipeline.md`", "`agents/pipeline.md`")
|
|
43
54
|
.replace(
|
|
@@ -69,7 +80,7 @@ function generatedFiles(sourceRoot, targetRoot) {
|
|
|
69
80
|
}
|
|
70
81
|
generated.push({
|
|
71
82
|
path: join(targetRoot, entry, "SKILL.md"),
|
|
72
|
-
text: transform(readFileSync(source, "utf8")),
|
|
83
|
+
text: transform(entry, readFileSync(source, "utf8")),
|
|
73
84
|
});
|
|
74
85
|
}
|
|
75
86
|
return generated;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: assess-external-comms
|
|
3
3
|
description: On-demand external-comms risk review. Reviews a draft of an outbound prose tool call (gh issue/pr body, security advisory, npm publish content, or .changeset/*.md body) for confidential-information leaks per RISK-POLICY.md. Delegates to wr-risk-scorer:external-comms and pre-satisfies the external-comms-gate marker for the current session.
|
|
4
4
|
allowed-tools: Read, Glob, Grep, Bash, request_user_input, Skill
|
|
5
5
|
---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: assess-inbound-report
|
|
3
3
|
description: On-demand inbound-report risk review. Reviews a third-party submission against this repo's intake (problem-report issue body, Q&A discussion, security-advisory body) for Request-risk (info-extraction / backdoor request / malicious-code injection) and Fix-risk (privilege escalation / removal of load-bearing safety check / adopter-attack-surface expansion) per RISK-POLICY.md. Delegates to wr-risk-scorer:inbound-report and emits the structured verdict consumed by ADR-062's assessment-pipeline branch routing.
|
|
4
4
|
allowed-tools: Read, Glob, Grep, Bash, request_user_input, Skill
|
|
5
5
|
---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: assess-release
|
|
3
3
|
description: On-demand release risk assessment. Scores commit, push, and release risk for the current unpushed changes. Delegates to wr-risk-scorer:pipeline and satisfies the commit gate for the current session.
|
|
4
4
|
allowed-tools: Read, Glob, Grep, Bash, request_user_input, Skill
|
|
5
5
|
---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: assess-wip
|
|
3
3
|
description: On-demand WIP risk nudge. Scores the current uncommitted diff for pipeline risk. Use during development to catch high-risk changes before committing.
|
|
4
4
|
allowed-tools: Read, Glob, Grep, Bash, request_user_input, Skill
|
|
5
5
|
---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: bootstrap-catalog
|
|
3
3
|
description: Bootstrap docs/risks/ standing-risk catalog from existing .risk-reports/ corpus. Walks reports, dedupes by ADR-056 slug, emits one R<NNN>-<slug>.active.md per unique slug with ## Source Evidence block citing originating reports. Idempotent — re-runs are no-ops by file-existence per slug. One-shot per project lifetime; install-updates Step 6.5 auto-triggers when catalog is empty AND .risk-reports/ is non-empty AND RISK-POLICY.md is present.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
5
|
maturity: proposed
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: create-risk
|
|
3
3
|
description: Create a new standing-risk entry in docs/risks/. Examines existing risks, gathers impact/likelihood/controls from the user, writes a file using the entry shape inlined in this skill (no TEMPLATE.md dependency — the entry shape is owned by this skill per user direction 2026-05-04), and updates the register index.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, request_user_input
|
|
5
5
|
---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: external-comms
|
|
3
3
|
description: Invokable SKILL wrapper around the wr-risk-scorer:external-comms leak-review agent. Delegates to the agent via the Agent tool and returns the agent's structured EXTERNAL_COMMS_RISK_VERDICT. Internal-use plumbing used by `/wr-risk-scorer:assess-external-comms` per ADR-015's Confirmation literal phrasing. End users should invoke `/wr-risk-scorer:assess-external-comms` instead.
|
|
4
4
|
allowed-tools: Read, Glob, Grep, Bash, Agent
|
|
5
5
|
---
|
package/skills/pipeline/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: pipeline
|
|
3
3
|
description: Invokable SKILL wrapper around the wr-risk-scorer:pipeline scoring agent. Delegates to the agent via the Agent tool and returns the agent's structured RISK_SCORES output. Internal-use plumbing used by `/wr-risk-scorer:assess-release` and any other consumer SKILL that needs Skill-tool-shaped invocation of the pipeline scorer per ADR-015's Confirmation literal phrasing. End users should invoke `/wr-risk-scorer:assess-release` instead.
|
|
4
4
|
allowed-tools: Read, Glob, Bash, Agent
|
|
5
5
|
---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: update-policy
|
|
3
3
|
description: Create or update the project's RISK-POLICY.md per ISO 31000 and the risk-scorer agent. Examines the project to derive business-specific impact levels.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, request_user_input, Agent
|
|
5
5
|
---
|
package/skills/wip/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: wip
|
|
3
3
|
description: Invokable SKILL wrapper around the wr-risk-scorer:wip nudge agent. Delegates to the agent via the Agent tool and returns the agent's structured WIP risk verdict. Internal-use plumbing used by `/wr-risk-scorer:assess-wip` per ADR-015's Confirmation literal phrasing. End users should invoke `/wr-risk-scorer:assess-wip` instead.
|
|
4
4
|
allowed-tools: Read, Glob, Bash, Agent
|
|
5
5
|
---
|