@windyroad/risk-scorer 0.17.4 → 0.18.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.
Files changed (55) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +13 -2
  5. package/bin/install.mjs +42 -8
  6. package/hooks/codex-agent-completion.mjs +82 -0
  7. package/hooks/hooks.json +5 -2
  8. package/hooks/risk-scorer-dispatch.sh +3 -0
  9. package/hooks/risk-scorer-scaffold-nudge.sh +15 -4
  10. package/package.json +5 -2
  11. package/scripts/codex-agents.mjs +208 -0
  12. package/scripts/sync-codex-skills.mjs +8 -3
  13. package/skills/assess-external-comms/SKILL.md +5 -4
  14. package/skills/assess-inbound-report/SKILL.md +5 -4
  15. package/skills/assess-release/SKILL.md +5 -4
  16. package/skills/assess-wip/SKILL.md +5 -4
  17. package/skills/bootstrap-catalog/SKILL.md +5 -4
  18. package/skills/create-risk/SKILL.md +5 -4
  19. package/skills/external-comms/SKILL.md +5 -4
  20. package/skills/pipeline/SKILL.md +5 -4
  21. package/skills/update-policy/SKILL.md +6 -5
  22. package/skills/wip/SKILL.md +5 -4
  23. package/agents/test/inbound-report-contract.bats +0 -225
  24. package/agents/test/risk-scorer-above-appetite-stop.bats +0 -80
  25. package/agents/test/risk-scorer-catalog-consumption.bats +0 -138
  26. package/agents/test/risk-scorer-commit-verdict.bats +0 -66
  27. package/agents/test/risk-scorer-monitoring-not-a-control.bats +0 -76
  28. package/agents/test/risk-scorer-reducing-bypass-criteria.bats +0 -62
  29. package/agents/test/risk-scorer-register-hint.bats +0 -111
  30. package/agents/test/risk-scorer-structured-remediations.bats +0 -123
  31. package/agents/test/risk-scorer-user-stated-preconditions.bats +0 -89
  32. package/hooks/test/ci-status-gate.bats +0 -234
  33. package/hooks/test/external-comms-gate.bats +0 -528
  34. package/hooks/test/external-comms-key-substance.bats +0 -74
  35. package/hooks/test/git-push-gate.bats +0 -82
  36. package/hooks/test/pipeline-state-changeset-partition.bats +0 -181
  37. package/hooks/test/pipeline-state-hash.bats +0 -195
  38. package/hooks/test/plan-risk-guidance-once-per-session.bats +0 -95
  39. package/hooks/test/reducing-marker-persistence.bats +0 -236
  40. package/hooks/test/risk-gate.bats +0 -330
  41. package/hooks/test/risk-policy-project-root.bats +0 -20
  42. package/hooks/test/risk-score-commit-gate-adr-031-bypass.bats +0 -98
  43. package/hooks/test/risk-score-commit-gate-cadence-staleness.bats +0 -105
  44. package/hooks/test/risk-score-mark-external-comms-prompt-parse.bats +0 -94
  45. package/hooks/test/risk-score-mark-register-queue.bats +0 -253
  46. package/hooks/test/risk-score-mark.bats +0 -112
  47. package/hooks/test/risk-scorer-hook-dispatch.bats +0 -77
  48. package/hooks/test/risk-scorer-no-stop-hook.bats +0 -19
  49. package/hooks/test/risk-scorer-scaffold-nudge.bats +0 -132
  50. package/hooks/test/slide-marker-on-subprocess-return.bats +0 -118
  51. package/hooks/test/wip-risk-mark.bats +0 -64
  52. package/scripts/test/drain-register-queue.bats +0 -359
  53. package/scripts/test/evaluate-graduation.bats +0 -533
  54. package/scripts/test/extract-risks-from-reports.bats +0 -366
  55. package/scripts/test/restage-commit.bats +0 -137
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "windyroad-local",
2
+ "name": "windyroad-risk-scorer-local",
3
3
  "interface": {
4
4
  "displayName": "Windy Road Risk Scorer"
5
5
  },
@@ -310,5 +310,5 @@
310
310
  }
311
311
  },
312
312
  "name": "wr-risk-scorer",
313
- "version": "0.17.4"
313
+ "version": "0.18.0"
314
314
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wr-risk-scorer",
3
- "version": "0.17.4",
3
+ "version": "0.18.0",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "author": {
6
6
  "name": "Windy Road Technology",
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @windyroad/risk-scorer
2
2
 
3
- **Pipeline risk scoring, commit/push gates, and secret leak detection for Claude Code.** Scores every change for risk and blocks high-risk commits and pushes before they happen. *Maturity: Experimental (suite-bootstrap window; 2047 invocations / 30d).*
3
+ **Pipeline risk scoring, commit/push gates, and secret leak detection for Claude Code and Codex.** Scores every change for risk and blocks high-risk commits and pushes before they happen. *Maturity: Experimental (suite-bootstrap window; 2047 invocations / 30d).*
4
4
 
5
5
  Part of [Windy Road Agent Plugins](../../README.md).
6
6
 
@@ -25,6 +25,16 @@ npx @windyroad/risk-scorer
25
25
 
26
26
  Restart Claude Code after installing.
27
27
 
28
+ For Codex, install the plugin and its native custom agents together:
29
+
30
+ ```bash
31
+ npx @windyroad/risk-scorer --runtime codex --scope project
32
+ ```
33
+
34
+ Use `--scope user` to make the agents available across repositories. Restart
35
+ Codex after installing. Marketplace/tag-only installs self-repair the user
36
+ agent files at SessionStart and request one additional restart when needed.
37
+
28
38
  ## Usage
29
39
 
30
40
  The plugin works automatically once installed. On first run in a project without a risk policy, it blocks edits and directs you to generate one:
@@ -55,7 +65,8 @@ This creates a `RISK-POLICY.md` tailored to your project, defining impact levels
55
65
 
56
66
  ## Agents
57
67
 
58
- The plugin includes six specialised agents:
68
+ Claude Code includes the routing agent plus six specialised agents. Codex
69
+ installs the six mode-specific agents directly:
59
70
 
60
71
  | Agent | Purpose |
61
72
  |-------|---------|
package/bin/install.mjs CHANGED
@@ -1,17 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { resolve, dirname } from "node:path";
3
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { resolve, dirname, join } from "node:path";
4
6
  import { fileURLToPath } from "node:url";
5
7
 
6
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
9
  const utils = await import(resolve(__dirname, "../lib/install-utils.mjs"));
10
+ const agents = await import(resolve(__dirname, "../scripts/codex-agents.mjs"));
8
11
 
9
12
  const PLUGIN = "wr-risk-scorer";
13
+ const CODEX_MARKETPLACE = "windyroad-risk-scorer-local";
10
14
  const DEPS = [];
11
15
  const PACKAGE_ROOT = resolve(__dirname, "..");
16
+ const PACKAGE_VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")).version;
17
+
18
+ function codexMarketplaceRoot() {
19
+ const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
20
+ return join(codexHome, ".tmp", "marketplaces", `wr-risk-scorer-${PACKAGE_VERSION}`);
21
+ }
12
22
 
13
23
  const flags = utils.parseStandardArgs(process.argv);
14
24
 
25
+ if (flags.runtime === "codex" || flags.runtime === "both") {
26
+ const bundled = "/Applications/ChatGPT.app/Contents/Resources/codex";
27
+ const binary = process.env.CODEX_BINARY || (existsSync(bundled) ? bundled : null);
28
+ if (binary?.includes("/")) process.env.PATH = `${dirname(binary)}:${process.env.PATH || ""}`;
29
+ }
30
+
15
31
  if (flags.help) {
16
32
  console.log(`
17
33
  Usage: npx @windyroad/risk-scorer [options]
@@ -37,12 +53,30 @@ if (flags.dryRun) {
37
53
  utils.checkPrerequisites({ runtime: flags.runtime });
38
54
 
39
55
  function codexInstall() {
40
- utils.run(`codex plugin marketplace add ${PACKAGE_ROOT}`, "Codex marketplace: windyroad-local");
41
- utils.run(`codex plugin add ${PLUGIN}@windyroad-local`, PLUGIN);
56
+ const marketplaceRoot = codexMarketplaceRoot();
57
+ if (!flags.dryRun) {
58
+ rmSync(marketplaceRoot, { recursive: true, force: true });
59
+ mkdirSync(dirname(marketplaceRoot), { recursive: true });
60
+ cpSync(PACKAGE_ROOT, marketplaceRoot, { recursive: true });
61
+ }
62
+ if (!utils.run(`codex plugin marketplace add ${JSON.stringify(marketplaceRoot)}`, `Codex marketplace: ${CODEX_MARKETPLACE}`)) return false;
63
+ if (!utils.run(`codex plugin add ${PLUGIN}@${CODEX_MARKETPLACE}`, PLUGIN)) return false;
64
+ if (!flags.dryRun) agents.installRiskAgents(agents.agentDirForScope(flags.scope));
65
+ return true;
42
66
  }
43
67
 
44
68
  function codexUninstall() {
45
- utils.run(`codex plugin remove ${PLUGIN}`, `Removing ${PLUGIN}`);
69
+ const removed = utils.run(`codex plugin remove ${PLUGIN}@${CODEX_MARKETPLACE}`, `Removing ${PLUGIN}`);
70
+ utils.run(`codex plugin marketplace remove ${CODEX_MARKETPLACE}`, `Removing ${CODEX_MARKETPLACE}`);
71
+ if (!flags.dryRun) {
72
+ const targets = new Set([
73
+ agents.agentDirForScope(flags.scope),
74
+ agents.agentDirForScope("user"),
75
+ ]);
76
+ for (const target of targets) agents.uninstallRiskAgents(target);
77
+ rmSync(codexMarketplaceRoot(), { recursive: true, force: true });
78
+ }
79
+ return removed;
46
80
  }
47
81
 
48
82
  if (flags.uninstall) {
@@ -50,22 +84,22 @@ if (flags.uninstall) {
50
84
  utils.uninstallPackage(PLUGIN, { runtime: "claude" });
51
85
  }
52
86
  if (flags.runtime === "codex" || flags.runtime === "both") {
53
- codexUninstall();
87
+ if (!codexUninstall()) process.exit(1);
54
88
  }
55
89
  } else if (flags.update) {
56
90
  if (flags.runtime === "claude" || flags.runtime === "both") {
57
91
  utils.updatePackage(PLUGIN, { scope: flags.scope, runtime: "claude" });
58
92
  }
59
93
  if (flags.runtime === "codex" || flags.runtime === "both") {
60
- codexInstall();
94
+ if (!codexInstall()) process.exit(1);
61
95
  }
62
96
  } else if (flags.runtime === "codex") {
63
97
  console.log(`\nInstalling @windyroad/risk-scorer (${flags.scope} scope)...\n`);
64
- codexInstall();
98
+ if (!codexInstall()) process.exit(1);
65
99
  console.log("\nDone! Restart Codex to activate.\n");
66
100
  } else if (flags.runtime === "both") {
67
101
  utils.installPackage(PLUGIN, { deps: DEPS, scope: flags.scope, runtime: "claude" });
68
- codexInstall();
102
+ if (!codexInstall()) process.exit(1);
69
103
  console.log("\nDone! Restart Claude Code and Codex to activate.\n");
70
104
  } else {
71
105
  utils.installPackage(PLUGIN, { deps: DEPS, scope: flags.scope, runtime: "claude" });
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const hookDir = dirname(fileURLToPath(import.meta.url));
9
+ const riskAgentRoles = new Set([
10
+ "wr-risk-scorer:pipeline",
11
+ "wr-risk-scorer:plan",
12
+ "wr-risk-scorer:wip",
13
+ "wr-risk-scorer:policy",
14
+ "wr-risk-scorer:external-comms",
15
+ "wr-risk-scorer:inbound-report",
16
+ ]);
17
+
18
+ function response(input) {
19
+ if (typeof input.tool_response === "object" && input.tool_response) return input.tool_response;
20
+ try {
21
+ return JSON.parse(input.tool_response);
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
27
+ function riskDir(sessionId) {
28
+ return join(process.env.TMPDIR || "/tmp", `claude-risk-${sessionId}`);
29
+ }
30
+
31
+ function statePath(input, target) {
32
+ return join(riskDir(input.session_id), `codex-agent-${Buffer.from(target).toString("base64url")}`);
33
+ }
34
+
35
+ function rememberSpawn(input) {
36
+ const role = input.tool_input?.agent_type;
37
+ const target = response(input).task_name;
38
+ if (!riskAgentRoles.has(role) || typeof target !== "string" || !target.startsWith("/")) return;
39
+ mkdirSync(riskDir(input.session_id), { recursive: true });
40
+ writeFileSync(statePath(input, target), role, "utf8");
41
+ }
42
+
43
+ function markCompletion(input) {
44
+ const target = input.tool_input?.target;
45
+ const output = response(input).previous_status?.completed;
46
+ if (typeof target !== "string" || typeof output !== "string" || !output) return;
47
+
48
+ const state = statePath(input, target);
49
+ if (!existsSync(state)) return;
50
+ const role = readFileSync(state, "utf8");
51
+ if (!riskAgentRoles.has(role)) return;
52
+
53
+ const synthetic = {
54
+ ...input,
55
+ tool_name: "Agent",
56
+ tool_input: { subagent_type: role, prompt: "" },
57
+ tool_response: { content: [{ type: "text", text: output }] },
58
+ };
59
+ const result = spawnSync(join(hookDir, "risk-score-mark.sh"), {
60
+ cwd: input.cwd || process.cwd(),
61
+ env: process.env,
62
+ input: JSON.stringify(synthetic),
63
+ encoding: "utf8",
64
+ });
65
+ if (result.status === 0) rmSync(state, { force: true });
66
+ }
67
+
68
+ let body = "";
69
+ process.stdin.setEncoding("utf8");
70
+ for await (const chunk of process.stdin) body += chunk;
71
+
72
+ let input;
73
+ try {
74
+ input = JSON.parse(body);
75
+ } catch {
76
+ process.exit(0);
77
+ }
78
+
79
+ if (!/^[A-Za-z0-9-]+$/.test(input.session_id || "")) process.exit(0);
80
+
81
+ if (input.tool_name === "collaborationspawn_agent") rememberSpawn(input);
82
+ if (input.tool_name === "collaborationinterrupt_agent") markCompletion(input);
package/hooks/hooks.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "hooks": {
3
3
  "SessionStart": [
4
- { "matcher": "startup", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-scorer-scaffold-nudge.sh" }] }
4
+ { "matcher": "startup", "hooks": [
5
+ { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/codex-agents.mjs --session-start" },
6
+ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-scorer-scaffold-nudge.sh" }
7
+ ] }
5
8
  ],
6
9
  "UserPromptSubmit": [
7
10
  { "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-scorer-dispatch.sh user-prompt" }] }
@@ -10,7 +13,7 @@
10
13
  { "matcher": "Bash|Edit|Write|ExitPlanMode|EnterPlanMode", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-scorer-dispatch.sh pre-tool" }] }
11
14
  ],
12
15
  "PostToolUse": [
13
- { "matcher": "Agent|Bash|Edit|Skill|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-scorer-dispatch.sh post-tool" }] }
16
+ { "matcher": "Agent|Bash|Edit|Skill|Write|collaborationspawn_agent|collaborationinterrupt_agent", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-scorer-dispatch.sh post-tool" }] }
14
17
  ]
15
18
  }
16
19
  }
@@ -52,6 +52,9 @@ case "$EVENT" in
52
52
  run_hook risk-score-mark.sh
53
53
  run_hook risk-slide-marker.sh
54
54
  ;;
55
+ collaborationspawn_agent|collaborationinterrupt_agent)
56
+ printf '%s' "$INPUT" | node "$SCRIPT_DIR/codex-agent-completion.mjs"
57
+ ;;
55
58
  Bash)
56
59
  run_hook risk-hash-refresh.sh
57
60
  run_hook risk-slide-marker.sh
@@ -33,6 +33,17 @@
33
33
 
34
34
  set -euo pipefail
35
35
 
36
+ cat >/dev/null
37
+ IS_CODEX="${CODEX_THREAD_ID:+1}"
38
+
39
+ emit_message() {
40
+ if [ "${IS_CODEX:-0}" = "1" ]; then
41
+ MESSAGE="$1" python3 -c 'import json,os; print(json.dumps({"systemMessage": os.environ["MESSAGE"]}))'
42
+ else
43
+ printf '%s\n' "$1"
44
+ fi
45
+ }
46
+
36
47
  if [ "${WR_SUPPRESS_OVERSIGHT_NUDGE:-}" = "1" ]; then
37
48
  exit 0
38
49
  fi
@@ -55,13 +66,13 @@ REGISTER_DIR="$PROJECT_DIR/docs/risks"
55
66
  # — the hook never writes; the policy authoring is gated behind the user
56
67
  # invoking the on-demand skill.
57
68
  if [ ! -f "$POLICY_FILE" ]; then
58
- echo "[wr-risk-scorer] No RISK-POLICY.md in this project — run /wr-risk-scorer:update-policy to author one so the risk-scorer gates score against your appetite instead of the default."
69
+ emit_message "[wr-risk-scorer] No RISK-POLICY.md in this project — run /wr-risk-scorer:update-policy to author one so the risk-scorer gates score against your appetite instead of the default."
59
70
  exit 0
60
71
  fi
61
72
 
62
73
  # Register directory missing — nudge to scaffold it.
63
74
  if [ ! -d "$REGISTER_DIR" ]; then
64
- echo "[wr-risk-scorer] RISK-POLICY.md present but docs/risks/ is missing — run /wr-risk-scorer:bootstrap-catalog to scaffold the standing-risk register."
75
+ emit_message "[wr-risk-scorer] RISK-POLICY.md present but docs/risks/ is missing — run /wr-risk-scorer:bootstrap-catalog to scaffold the standing-risk register."
65
76
  exit 0
66
77
  fi
67
78
 
@@ -76,7 +87,7 @@ PENDING="${PENDING:-0}"
76
87
  [ "$PENDING" -gt 0 ] 2>/dev/null || exit 0
77
88
 
78
89
  if [ "$PENDING" -eq 1 ]; then
79
- echo "[wr-risk-scorer] 1 standing-risk entry is pending review — curate it in docs/risks/ (enumerate controls + Impact×Likelihood scoring)."
90
+ emit_message "[wr-risk-scorer] 1 standing-risk entry is pending review — curate it in docs/risks/ (enumerate controls + Impact×Likelihood scoring)."
80
91
  else
81
- echo "[wr-risk-scorer] $PENDING standing-risk entries are pending review — curate them in docs/risks/ (enumerate controls + Impact×Likelihood scoring)."
92
+ emit_message "[wr-risk-scorer] $PENDING standing-risk entries are pending review — curate them in docs/risks/ (enumerate controls + Impact×Likelihood scoring)."
82
93
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.17.4",
3
+ "version": "0.18.0",
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",
@@ -33,6 +33,9 @@
33
33
  "lib/",
34
34
  "scripts/",
35
35
  "!skills/*/eval/",
36
- "!agents/eval/"
36
+ "!agents/eval/",
37
+ "!agents/test/",
38
+ "!hooks/test/",
39
+ "!scripts/test/"
37
40
  ]
38
41
  }
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { createHash } from "node:crypto";
14
+
15
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
+ const ownerMarker = "# Generated by @windyroad/risk-scorer from ";
17
+
18
+ const externalCommsCodexInstructions = `
19
+
20
+ ## Codex completion marker compatibility
21
+
22
+ Codex encrypts custom-agent spawn prompts before PostToolUse hooks receive them.
23
+ When the request contains a \`SURFACE:\` line and a \`<draft>...</draft>\` block,
24
+ compute the canonical marker key with the same normalization documented above:
25
+
26
+ 1. For \`changeset-author\`, remove leading YAML frontmatter and its following blank line.
27
+ 2. Convert CRLF and CR line endings to LF.
28
+ 3. Remove trailing whitespace from every line, then from the complete draft.
29
+ 4. Compute lowercase SHA-256 over \`normalized draft + "\\n" + surface\`.
30
+
31
+ Use a local shell or Python command to compute the digest; do not estimate it.
32
+ On PASS, append \`EXTERNAL_COMMS_RISK_KEY: <64 lowercase hex characters>\` after
33
+ the verdict. On FAIL, do not emit a key. This is generated Codex-only behavior;
34
+ the completion hook independently validates the key shape before writing a marker.`;
35
+
36
+ export const riskAgentSpecs = [
37
+ ["pipeline", "Scores pipeline actions for cumulative residual risk."],
38
+ ["plan", "Reviews implementation plans and projected release risk."],
39
+ ["wip", "Assesses cumulative work-in-progress pipeline risk."],
40
+ ["policy", "Validates RISK-POLICY.md drafts for ISO 31000 compliance."],
41
+ ["external-comms", "Reviews outbound prose for confidentiality and credibility risk."],
42
+ ["inbound-report", "Reviews inbound reports for request and fix risk."],
43
+ ].map(([mode, fallbackDescription]) => ({
44
+ mode,
45
+ name: `wr-risk-scorer:${mode}`,
46
+ filename: `wr-risk-scorer-${mode}.toml`,
47
+ source: join(packageRoot, "agents", `${mode}.md`),
48
+ sourceLabel: `agents/${mode}.md`,
49
+ fallbackDescription,
50
+ }));
51
+
52
+ function splitFrontmatter(markdown) {
53
+ if (!markdown.startsWith("---\n")) return { frontmatter: "", body: markdown };
54
+ const end = markdown.indexOf("\n---\n", 4);
55
+ if (end === -1) return { frontmatter: "", body: markdown };
56
+ return {
57
+ frontmatter: markdown.slice(4, end),
58
+ body: markdown.slice(end + 5),
59
+ };
60
+ }
61
+
62
+ function frontmatterValue(frontmatter, key) {
63
+ const line = frontmatter.split(/\r?\n/).find((item) => item.startsWith(`${key}:`));
64
+ return line ? line.slice(key.length + 1).trim() : "";
65
+ }
66
+
67
+ function tomlMultiline(value) {
68
+ return value.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"').trimEnd();
69
+ }
70
+
71
+ function renderPayload(spec) {
72
+ const { frontmatter, body } = splitFrontmatter(readFileSync(spec.source, "utf8"));
73
+ const description = frontmatterValue(frontmatter, "description") || spec.fallbackDescription;
74
+ const instructions = spec.mode === "external-comms"
75
+ ? `${body.trimEnd()}${externalCommsCodexInstructions}`
76
+ : body;
77
+ return [
78
+ "# Do not edit by hand; update the Claude agent markdown and regenerate.",
79
+ `name = ${JSON.stringify(spec.name)}`,
80
+ `description = ${JSON.stringify(description)}`,
81
+ 'sandbox_mode = "read-only"',
82
+ 'developer_instructions = """',
83
+ tomlMultiline(instructions),
84
+ '"""',
85
+ "",
86
+ ].join("\n");
87
+ }
88
+
89
+ function payloadHash(payload) {
90
+ return createHash("sha256").update(payload).digest("hex");
91
+ }
92
+
93
+ export function renderRiskAgent(spec) {
94
+ const payload = renderPayload(spec);
95
+ return [
96
+ `${ownerMarker}${spec.sourceLabel}.`,
97
+ `# Generated content SHA-256: ${payloadHash(payload)}`,
98
+ payload,
99
+ ].join("\n");
100
+ }
101
+
102
+ function isUnmodifiedGenerated(content, spec) {
103
+ if (!content.startsWith(`${ownerMarker}${spec.sourceLabel}.\n`)) return false;
104
+ const lines = content.split("\n");
105
+ const hashMatch = lines[1]?.match(/^# Generated content SHA-256: ([0-9a-f]{64})$/);
106
+ if (hashMatch) return payloadHash(lines.slice(2).join("\n")) === hashMatch[1];
107
+
108
+ // Migrate the marker-only format generated by the unreleased pilot.
109
+ return content === [`${ownerMarker}${spec.sourceLabel}.`, renderPayload(spec)].join("\n");
110
+ }
111
+
112
+ export function installRiskAgents(targetDir, { quiet = false } = {}) {
113
+ mkdirSync(targetDir, { recursive: true });
114
+ let changed = 0;
115
+ let preserved = 0;
116
+ for (const spec of riskAgentSpecs) {
117
+ const target = join(targetDir, spec.filename);
118
+ const expected = renderRiskAgent(spec);
119
+ if (!existsSync(target)) {
120
+ writeFileSync(target, expected, "utf8");
121
+ changed += 1;
122
+ continue;
123
+ }
124
+ const current = readFileSync(target, "utf8");
125
+ if (current === expected) continue;
126
+ if (isUnmodifiedGenerated(current, spec)) {
127
+ writeFileSync(target, expected, "utf8");
128
+ changed += 1;
129
+ } else {
130
+ preserved += 1;
131
+ }
132
+ }
133
+ if (!quiet) console.log(`Codex risk agents: ${changed} updated, ${riskAgentSpecs.length - changed - preserved} current, ${preserved} preserved.`);
134
+ return changed;
135
+ }
136
+
137
+ export function checkRiskAgents(targetDir) {
138
+ return riskAgentSpecs.every((spec) => {
139
+ const target = join(targetDir, spec.filename);
140
+ return existsSync(target) && readFileSync(target, "utf8") === renderRiskAgent(spec);
141
+ });
142
+ }
143
+
144
+ export function uninstallRiskAgents(targetDir, { quiet = false } = {}) {
145
+ let removed = 0;
146
+ for (const spec of riskAgentSpecs) {
147
+ const target = join(targetDir, spec.filename);
148
+ if (!existsSync(target)) continue;
149
+ if (!isUnmodifiedGenerated(readFileSync(target, "utf8"), spec)) continue;
150
+ rmSync(target);
151
+ removed += 1;
152
+ }
153
+ if (!quiet) console.log(`Codex risk agents: ${removed} removed.`);
154
+ return removed;
155
+ }
156
+
157
+ export function agentDirForScope(scope, cwd = process.cwd(), env = process.env) {
158
+ if (scope === "user") {
159
+ return join(env.CODEX_HOME || join(homedir(), ".codex"), "agents");
160
+ }
161
+ return join(cwd, ".codex", "agents");
162
+ }
163
+
164
+ function argValue(name) {
165
+ const index = process.argv.indexOf(name);
166
+ return index >= 0 ? process.argv[index + 1] : "";
167
+ }
168
+
169
+ async function main() {
170
+ const sessionStart = process.argv.includes("--session-start");
171
+ if (sessionStart && !process.env.CODEX_THREAD_ID) return;
172
+
173
+ let sessionStartInput = {};
174
+ if (sessionStart) {
175
+ let input = "";
176
+ process.stdin.setEncoding("utf8");
177
+ for await (const chunk of process.stdin) input += chunk;
178
+ try {
179
+ sessionStartInput = JSON.parse(input);
180
+ } catch {
181
+ sessionStartInput = {};
182
+ }
183
+ }
184
+
185
+ const targetArg = argValue("--target");
186
+ const scope = argValue("--scope") || (sessionStart ? "user" : "project");
187
+ const targetDir = targetArg || agentDirForScope(scope);
188
+ const quiet = sessionStart || process.argv.includes("--quiet");
189
+
190
+ if (process.argv.includes("--check")) {
191
+ if (!checkRiskAgents(targetDir)) process.exitCode = 1;
192
+ return;
193
+ }
194
+ if (process.argv.includes("--uninstall")) {
195
+ uninstallRiskAgents(targetDir, { quiet });
196
+ return;
197
+ }
198
+
199
+ const changed = installRiskAgents(targetDir, { quiet });
200
+ if (sessionStart && changed > 0) {
201
+ const message = "wr-risk-scorer: Codex agents installed; restart Codex to activate them.";
202
+ console.log(JSON.stringify({ systemMessage: message }));
203
+ }
204
+ }
205
+
206
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
207
+ await main();
208
+ }
@@ -23,7 +23,7 @@ const restoreMode = process.argv.includes("--restore-pack");
23
23
 
24
24
  const preamble = `<!-- Generated from packages/risk-scorer/skills/*/SKILL.md by packages/risk-scorer/scripts/sync-codex-skills.mjs during npm pack. Do not edit packaged output directly. -->
25
25
 
26
- > Codex runtime note: use \`request_user_input\` only in Plan Mode where this skill needs structured user input. Outside Plan Mode, ask one concise direct question only when no safe assumption exists. If a step refers to Claude-style agent dispatch or \`subagent_type\`, use a native Codex subagent workflow: spawn the matching installed Codex custom agent when available, otherwise spawn the built-in \`default\` subagent and instruct it to read the plugin's sibling \`agents/*.md\` instructions in full before returning the same structured verdict block.
26
+ > Codex runtime note: use \`request_user_input\` only in Plan Mode where this skill needs structured user input. Outside Plan Mode, ask one concise direct question only when no safe assumption exists. If a step refers to Claude-style agent dispatch or \`subagent_type\`, use a native Codex subagent workflow and spawn the matching installed \`wr-risk-scorer:<mode>\` custom agent. Wait for it, then close that same agent so the completion hook receives its final response. Do not substitute the built-in \`default\` agent when a hook consumes the subagent identity; restart Codex if the installed agent is not yet visible.
27
27
 
28
28
  `;
29
29
 
@@ -33,10 +33,10 @@ const updatePolicyCodexAgentStep = `Run the Codex policy reviewer with this prom
33
33
  >
34
34
  > [paste the full draft policy content here]
35
35
 
36
- Use a native Codex subagent workflow. Prefer the installed plugin agent named \`wr-risk-scorer:policy\` when available. In this source repo, the generated project-local alias \`wr-risk-scorer-policy\` may also be available for dogfooding. If neither custom agent is available in the current Codex session, spawn the built-in \`default\` subagent and instruct it to read \`agents/policy.md\` from this plugin in full before performing the review.`;
36
+ 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.`;
37
37
 
38
38
  function transform(text) {
39
- return preamble + text
39
+ const transformed = text
40
40
  .replaceAll("AskUserQuestion", "request_user_input")
41
41
  .replaceAll("`.claude/agents/risk-scorer-pipeline.md`", "`agents/pipeline.md`")
42
42
  .replace(
@@ -47,6 +47,11 @@ function transform(text) {
47
47
  > [paste the full draft policy content here]`,
48
48
  updatePolicyCodexAgentStep,
49
49
  );
50
+ if (!transformed.startsWith("---\n")) return preamble + transformed;
51
+ const frontmatterEnd = transformed.indexOf("\n---\n", 4);
52
+ if (frontmatterEnd === -1) return preamble + transformed;
53
+ const bodyStart = frontmatterEnd + 5;
54
+ return transformed.slice(0, bodyStart) + "\n" + preamble + transformed.slice(bodyStart);
50
55
  }
51
56
 
52
57
  function generatedFiles(sourceRoot, targetRoot) {
@@ -1,13 +1,14 @@
1
- <!-- Generated from packages/risk-scorer/skills/*/SKILL.md by packages/risk-scorer/scripts/sync-codex-skills.mjs during npm pack. Do not edit packaged output directly. -->
2
-
3
- > Codex runtime note: use `request_user_input` only in Plan Mode where this skill needs structured user input. Outside Plan Mode, ask one concise direct question only when no safe assumption exists. If a step refers to Claude-style agent dispatch or `subagent_type`, use a native Codex subagent workflow: spawn the matching installed Codex custom agent when available, otherwise spawn the built-in `default` subagent and instruct it to read the plugin's sibling `agents/*.md` instructions in full before returning the same structured verdict block.
4
-
5
1
  ---
6
2
  name: wr-risk-scorer:assess-external-comms
7
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.
8
4
  allowed-tools: Read, Glob, Grep, Bash, request_user_input, Skill
9
5
  ---
10
6
 
7
+ <!-- Generated from packages/risk-scorer/skills/*/SKILL.md by packages/risk-scorer/scripts/sync-codex-skills.mjs during npm pack. Do not edit packaged output directly. -->
8
+
9
+ > Codex runtime note: use `request_user_input` only in Plan Mode where this skill needs structured user input. Outside Plan Mode, ask one concise direct question only when no safe assumption exists. If a step refers to Claude-style agent dispatch or `subagent_type`, use a native Codex subagent workflow and spawn the matching installed `wr-risk-scorer:<mode>` custom agent. Wait for it, then close that same agent so the completion hook receives its final response. Do not substitute the built-in `default` agent when a hook consumes the subagent identity; restart Codex if the installed agent is not yet visible.
10
+
11
+
11
12
  # External-Comms Risk Assessment Skill
12
13
 
13
14
  Run a confidential-information leak 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.
@@ -1,13 +1,14 @@
1
- <!-- Generated from packages/risk-scorer/skills/*/SKILL.md by packages/risk-scorer/scripts/sync-codex-skills.mjs during npm pack. Do not edit packaged output directly. -->
2
-
3
- > Codex runtime note: use `request_user_input` only in Plan Mode where this skill needs structured user input. Outside Plan Mode, ask one concise direct question only when no safe assumption exists. If a step refers to Claude-style agent dispatch or `subagent_type`, use a native Codex subagent workflow: spawn the matching installed Codex custom agent when available, otherwise spawn the built-in `default` subagent and instruct it to read the plugin's sibling `agents/*.md` instructions in full before returning the same structured verdict block.
4
-
5
1
  ---
6
2
  name: wr-risk-scorer:assess-inbound-report
7
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.
8
4
  allowed-tools: Read, Glob, Grep, Bash, request_user_input, Skill
9
5
  ---
10
6
 
7
+ <!-- Generated from packages/risk-scorer/skills/*/SKILL.md by packages/risk-scorer/scripts/sync-codex-skills.mjs during npm pack. Do not edit packaged output directly. -->
8
+
9
+ > Codex runtime note: use `request_user_input` only in Plan Mode where this skill needs structured user input. Outside Plan Mode, ask one concise direct question only when no safe assumption exists. If a step refers to Claude-style agent dispatch or `subagent_type`, use a native Codex subagent workflow and spawn the matching installed `wr-risk-scorer:<mode>` custom agent. Wait for it, then close that same agent so the completion hook receives its final response. Do not substitute the built-in `default` agent when a hook consumes the subagent identity; restart Codex if the installed agent is not yet visible.
10
+
11
+
11
12
  # Inbound-Report Risk Assessment Skill
12
13
 
13
14
  Run a Request-risk + Fix-risk review on demand against a single inbound report — outside the `/wr-itil:review-problems` Step 8.5 assessment-pipeline trigger. Maintainer-facing pre-flight surface per JTBD-005 + JTBD-202; the assessment-pipeline itself invokes the same `wr-risk-scorer:inbound-report` subagent in-loop per ADR-062 § Decision Outcome step 3.
@@ -1,13 +1,14 @@
1
- <!-- Generated from packages/risk-scorer/skills/*/SKILL.md by packages/risk-scorer/scripts/sync-codex-skills.mjs during npm pack. Do not edit packaged output directly. -->
2
-
3
- > Codex runtime note: use `request_user_input` only in Plan Mode where this skill needs structured user input. Outside Plan Mode, ask one concise direct question only when no safe assumption exists. If a step refers to Claude-style agent dispatch or `subagent_type`, use a native Codex subagent workflow: spawn the matching installed Codex custom agent when available, otherwise spawn the built-in `default` subagent and instruct it to read the plugin's sibling `agents/*.md` instructions in full before returning the same structured verdict block.
4
-
5
1
  ---
6
2
  name: wr-risk-scorer:assess-release
7
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.
8
4
  allowed-tools: Read, Glob, Grep, Bash, request_user_input, Skill
9
5
  ---
10
6
 
7
+ <!-- Generated from packages/risk-scorer/skills/*/SKILL.md by packages/risk-scorer/scripts/sync-codex-skills.mjs during npm pack. Do not edit packaged output directly. -->
8
+
9
+ > Codex runtime note: use `request_user_input` only in Plan Mode where this skill needs structured user input. Outside Plan Mode, ask one concise direct question only when no safe assumption exists. If a step refers to Claude-style agent dispatch or `subagent_type`, use a native Codex subagent workflow and spawn the matching installed `wr-risk-scorer:<mode>` custom agent. Wait for it, then close that same agent so the completion hook receives its final response. Do not substitute the built-in `default` agent when a hook consumes the subagent identity; restart Codex if the installed agent is not yet visible.
10
+
11
+
11
12
  # Release Risk Assessment Skill
12
13
 
13
14
  Run a pipeline risk assessment on demand — outside a hook gate trigger. Scores commit, push, and release risk layers for the current unpushed changes and satisfies the gate for the current session.