@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.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +13 -2
- package/bin/install.mjs +42 -8
- package/hooks/codex-agent-completion.mjs +82 -0
- package/hooks/hooks.json +5 -2
- package/hooks/risk-scorer-dispatch.sh +3 -0
- package/hooks/risk-scorer-scaffold-nudge.sh +15 -4
- package/package.json +5 -2
- package/scripts/codex-agents.mjs +208 -0
- package/scripts/sync-codex-skills.mjs +8 -3
- package/skills/assess-external-comms/SKILL.md +5 -4
- package/skills/assess-inbound-report/SKILL.md +5 -4
- package/skills/assess-release/SKILL.md +5 -4
- package/skills/assess-wip/SKILL.md +5 -4
- package/skills/bootstrap-catalog/SKILL.md +5 -4
- package/skills/create-risk/SKILL.md +5 -4
- package/skills/external-comms/SKILL.md +5 -4
- package/skills/pipeline/SKILL.md +5 -4
- package/skills/update-policy/SKILL.md +6 -5
- package/skills/wip/SKILL.md +5 -4
- package/agents/test/inbound-report-contract.bats +0 -225
- package/agents/test/risk-scorer-above-appetite-stop.bats +0 -80
- package/agents/test/risk-scorer-catalog-consumption.bats +0 -138
- package/agents/test/risk-scorer-commit-verdict.bats +0 -66
- package/agents/test/risk-scorer-monitoring-not-a-control.bats +0 -76
- package/agents/test/risk-scorer-reducing-bypass-criteria.bats +0 -62
- package/agents/test/risk-scorer-register-hint.bats +0 -111
- package/agents/test/risk-scorer-structured-remediations.bats +0 -123
- package/agents/test/risk-scorer-user-stated-preconditions.bats +0 -89
- package/hooks/test/ci-status-gate.bats +0 -234
- package/hooks/test/external-comms-gate.bats +0 -528
- package/hooks/test/external-comms-key-substance.bats +0 -74
- package/hooks/test/git-push-gate.bats +0 -82
- package/hooks/test/pipeline-state-changeset-partition.bats +0 -181
- package/hooks/test/pipeline-state-hash.bats +0 -195
- package/hooks/test/plan-risk-guidance-once-per-session.bats +0 -95
- package/hooks/test/reducing-marker-persistence.bats +0 -236
- package/hooks/test/risk-gate.bats +0 -330
- package/hooks/test/risk-policy-project-root.bats +0 -20
- package/hooks/test/risk-score-commit-gate-adr-031-bypass.bats +0 -98
- package/hooks/test/risk-score-commit-gate-cadence-staleness.bats +0 -105
- package/hooks/test/risk-score-mark-external-comms-prompt-parse.bats +0 -94
- package/hooks/test/risk-score-mark-register-queue.bats +0 -253
- package/hooks/test/risk-score-mark.bats +0 -112
- package/hooks/test/risk-scorer-hook-dispatch.bats +0 -77
- package/hooks/test/risk-scorer-no-stop-hook.bats +0 -19
- package/hooks/test/risk-scorer-scaffold-nudge.bats +0 -132
- package/hooks/test/slide-marker-on-subprocess-return.bats +0 -118
- package/hooks/test/wip-risk-mark.bats +0 -64
- package/scripts/test/drain-register-queue.bats +0 -359
- package/scripts/test/evaluate-graduation.bats +0 -533
- package/scripts/test/extract-risks-from-reports.bats +0 -366
- package/scripts/test/restage-commit.bats +0 -137
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
|
-
|
|
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 {
|
|
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
|
-
|
|
41
|
-
|
|
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": [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|