@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.
@@ -310,5 +310,5 @@
310
310
  }
311
311
  },
312
312
  "name": "wr-risk-scorer",
313
- "version": "0.18.17"
313
+ "version": "0.18.18"
314
314
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wr-risk-scorer",
3
- "version": "0.18.17",
3
+ "version": "0.18.18",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "author": {
6
6
  "name": "Windy Road Technology",
@@ -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
- addCodexMarketplace();
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
- updateCodexMarketplace();
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
- uninstallCodexPlugin(pluginName);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.18.17",
3
+ "version": "0.18.18",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "scripts": {
6
6
  "prepack": "node scripts/sync-codex-skills.mjs --pack",
@@ -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
- function transform(text) {
40
- const transformed = text
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: wr-risk-scorer:assess-external-comms
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: wr-risk-scorer:assess-inbound-report
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: wr-risk-scorer:assess-release
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: wr-risk-scorer:assess-wip
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: wr-risk-scorer:bootstrap-catalog
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: wr-risk-scorer:create-risk
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: wr-risk-scorer:external-comms
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
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: wr-risk-scorer:pipeline
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: wr-risk-scorer:update-policy
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
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: wr-risk-scorer:wip
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
  ---