@windyroad/risk-scorer 0.2.0 → 0.2.1-preview.70
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/README.md +74 -0
- package/agents/pipeline.md +24 -7
- package/agents/plan.md +10 -2
- package/agents/wip.md +17 -7
- package/bin/install.mjs +3 -2
- package/hooks/git-push-gate.sh +21 -5
- package/hooks/hooks.json +0 -4
- package/hooks/lib/risk-gate.sh +4 -4
- package/hooks/risk-policy-enforce-edit.sh +11 -1
- package/hooks/risk-score-commit-gate.sh +1 -1
- package/hooks/risk-score-mark.sh +5 -5
- package/hooks/risk-score-plan-enforce.sh +1 -1
- package/hooks/test/git-push-gate.bats +82 -0
- package/hooks/test/risk-policy-project-root.bats +20 -0
- package/hooks/test/risk-score-mark.bats +112 -0
- package/hooks/test/risk-scorer-no-stop-hook.bats +19 -0
- package/hooks/wip-risk-gate.sh +1 -1
- package/hooks/wip-risk-mark.sh +3 -11
- package/lib/install-utils.mjs +143 -0
- package/package.json +3 -2
- package/skills/{wr:risk-policy → update-policy}/SKILL.md +1 -1
- package/hooks/risk-policy-reset-marker.sh +0 -17
- package/hooks/risk-score-reset.sh +0 -17
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @windyroad/risk-scorer
|
|
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.
|
|
4
|
+
|
|
5
|
+
Part of [Windy Road Agent Plugins](../../README.md).
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
The risk-scorer plugin brings ISO 31000-aligned risk management to your AI coding workflow. It:
|
|
10
|
+
|
|
11
|
+
1. **Scores risk** on every edit, assessing cumulative pipeline risk as changes build up
|
|
12
|
+
2. **Gates commits** -- blocks `git commit` when cumulative risk exceeds your policy threshold
|
|
13
|
+
3. **Gates pushes** -- blocks `git push` for high-risk changesets (use `npm run push:watch` instead)
|
|
14
|
+
4. **Detects secrets** -- scans edits for API keys, tokens, passwords, and other credentials before they're written
|
|
15
|
+
5. **Reviews plans** -- scores implementation plans for risk before you start building
|
|
16
|
+
|
|
17
|
+
All thresholds are configurable through your project's `RISK-POLICY.md`.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @windyroad/risk-scorer
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Restart Claude Code after installing.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
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:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
/wr-risk-scorer:update-policy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This creates a `RISK-POLICY.md` tailored to your project, defining impact levels, likelihood scales, risk appetite, and the risk matrix -- all aligned to ISO 31000.
|
|
36
|
+
|
|
37
|
+
## How It Works
|
|
38
|
+
|
|
39
|
+
| Hook | Trigger | What it does |
|
|
40
|
+
|------|---------|-------------|
|
|
41
|
+
| `risk-score.sh` | Every prompt | Injects risk scoring context |
|
|
42
|
+
| `secret-leak-gate.sh` | Edit or Write | Blocks writes containing secrets |
|
|
43
|
+
| `wip-risk-gate.sh` | Edit or Write | Blocks edits if WIP risk hasn't been assessed |
|
|
44
|
+
| `risk-policy-enforce-edit.sh` | Edit or Write | Blocks edits if no `RISK-POLICY.md` exists |
|
|
45
|
+
| `git-push-gate.sh` | Bash (git push) | Blocks direct `git push`; requires `npm run push:watch` |
|
|
46
|
+
| `risk-score-commit-gate.sh` | Bash (git commit) | Blocks commits when risk exceeds threshold |
|
|
47
|
+
| `risk-score-plan-enforce.sh` | ExitPlanMode | Ensures plans are risk-scored before execution |
|
|
48
|
+
| `plan-risk-guidance.sh` | EnterPlanMode | Injects risk guidance into plan mode |
|
|
49
|
+
| `wip-risk-mark.sh` | After edit | Records WIP risk assessment |
|
|
50
|
+
| `risk-score-mark.sh` | Agent completes | Marks risk review as done |
|
|
51
|
+
| `risk-hash-refresh.sh` | After Bash | Refreshes content hashes |
|
|
52
|
+
|
|
53
|
+
## Agents
|
|
54
|
+
|
|
55
|
+
The plugin includes five specialised agents:
|
|
56
|
+
|
|
57
|
+
| Agent | Purpose |
|
|
58
|
+
|-------|---------|
|
|
59
|
+
| `wr-risk-scorer:agent` | Routes to the appropriate mode-specific agent |
|
|
60
|
+
| `wr-risk-scorer:wip` | Assesses cumulative risk after each edit |
|
|
61
|
+
| `wr-risk-scorer:pipeline` | Scores pipeline actions (commit, push, release) |
|
|
62
|
+
| `wr-risk-scorer:plan` | Reviews implementation plans for risk |
|
|
63
|
+
| `wr-risk-scorer:policy` | Validates `RISK-POLICY.md` for ISO 31000 compliance |
|
|
64
|
+
|
|
65
|
+
## Updating and Uninstalling
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx @windyroad/risk-scorer --update
|
|
69
|
+
npx @windyroad/risk-scorer --uninstall
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Licence
|
|
73
|
+
|
|
74
|
+
[MIT](../../LICENSE)
|
package/agents/pipeline.md
CHANGED
|
@@ -98,18 +98,35 @@ Assess whether each action is risk-reducing, risk-neutral, or risk-increasing. I
|
|
|
98
98
|
|
|
99
99
|
For live incidents (outage, security, information disclosure), include `RISK_BYPASS: incident`.
|
|
100
100
|
|
|
101
|
-
##
|
|
101
|
+
## Below-Appetite Output Rule (ADR-013 Rule 5)
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
When ALL cumulative scores are within appetite (≤ 4 per RISK-POLICY.md), your output MUST contain ONLY:
|
|
104
|
+
1. The Pipeline Risk Report structure (layers, risk items, summary table)
|
|
105
|
+
2. `RISK_SCORES: commit=N push=N release=N`
|
|
106
|
+
3. `RISK_BYPASS: reducing` (if applicable)
|
|
105
107
|
|
|
106
|
-
|
|
108
|
+
Do NOT emit: "Suggested Actions", "Your call:", advisory warnings, back-pressure notes, or any prose that implies the user needs to make a decision. Policy-authorised releases proceed silently.
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
## Above-Appetite Remediations
|
|
111
|
+
|
|
112
|
+
When ANY cumulative score exceeds appetite (> 4), emit a structured `RISK_REMEDIATIONS:` block after the `RISK_SCORES:` line. This gives the calling skill machine-readable input for structured decision prompts.
|
|
113
|
+
|
|
114
|
+
Format:
|
|
115
|
+
```
|
|
116
|
+
RISK_REMEDIATIONS:
|
|
117
|
+
- R1 | <description of remediation> | <files affected>
|
|
118
|
+
- R2 | <description of remediation> | <files affected>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Include downstream back-pressure in the remediation list:
|
|
122
|
+
- **Commit**: If adding this commit would push the push queue risk >= 5, include a remediation to split the commit.
|
|
123
|
+
- **Push**: If pushing would push the release queue risk >= 5, include a remediation to release first.
|
|
109
124
|
|
|
110
|
-
|
|
125
|
+
Do NOT emit free-text "Your call:" or "consider splitting" prose. The structured `RISK_REMEDIATIONS:` block is the only output for above-appetite guidance.
|
|
111
126
|
|
|
112
|
-
|
|
127
|
+
## Confidential Information Disclosure
|
|
128
|
+
|
|
129
|
+
Check diffs for business metrics (revenue, user counts, pricing, traffic volumes). Flag as a standalone risk if found.
|
|
113
130
|
|
|
114
131
|
## Report History
|
|
115
132
|
|
package/agents/plan.md
CHANGED
|
@@ -22,8 +22,8 @@ You are the Risk Scorer in plan review mode. Assess both the plan's own risk AND
|
|
|
22
22
|
|
|
23
23
|
## Verdict Logic
|
|
24
24
|
|
|
25
|
-
- **PASS** if both the plan's own residual risk AND projected release risk are within appetite
|
|
26
|
-
- **FAIL** if either exceeds appetite —
|
|
25
|
+
- **PASS** if both the plan's own residual risk AND projected release risk are within appetite. Do NOT emit advisory prose, suggestions, or "consider" recommendations on PASS — the plan is policy-authorised (ADR-013 Rule 5).
|
|
26
|
+
- **FAIL** if either exceeds appetite — emit a structured `RISK_REMEDIATIONS:` block (see below) explaining which dimension failed and what the plan should include.
|
|
27
27
|
|
|
28
28
|
## Output Format
|
|
29
29
|
|
|
@@ -49,6 +49,14 @@ You are the Risk Scorer in plan review mode. Assess both the plan's own risk AND
|
|
|
49
49
|
|
|
50
50
|
End your report with `RISK_VERDICT: PASS` or `RISK_VERDICT: FAIL` on its own line. A PostToolUse hook reads this and writes the marker files — do NOT write files yourself.
|
|
51
51
|
|
|
52
|
+
On FAIL, emit a structured `RISK_REMEDIATIONS:` block after the verdict:
|
|
53
|
+
```
|
|
54
|
+
RISK_REMEDIATIONS:
|
|
55
|
+
- R1 | <description of what the plan must add/change> | <affected area>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Do NOT emit free-text "consider" or "you should" prose. The structured block is the only output for above-appetite guidance.
|
|
59
|
+
|
|
52
60
|
## Control Discovery
|
|
53
61
|
|
|
54
62
|
For each control claimed to reduce risk:
|
package/agents/wip.md
CHANGED
|
@@ -45,15 +45,25 @@ Always provide the cumulative risk picture:
|
|
|
45
45
|
- [specific guidance based on current pipeline state]
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
### Below-Appetite Rule (ADR-013 Rule 5)
|
|
49
49
|
|
|
50
|
-
If cumulative risk **
|
|
51
|
-
- "Commit your current changes to move WIP forward"
|
|
52
|
-
- "Write tests for [risk item from report]" — name the specific risk and test file
|
|
53
|
-
- "The release report flags [X] — address it before adding more changes"
|
|
54
|
-
- "Push your commits to get CI feedback"
|
|
50
|
+
If cumulative risk is **within appetite** (< 5): provide the assessment table and verdict only. Do NOT emit advisory prose, recommendations, or suggestions. The verdict is `RISK_VERDICT: CONTINUE`.
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
### Above-Appetite Remediations
|
|
53
|
+
|
|
54
|
+
If cumulative risk **exceeds appetite** (>= 5): provide the assessment table, then emit a structured `RISK_REMEDIATIONS:` block with specific risk-reducing actions:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
RISK_REMEDIATIONS:
|
|
58
|
+
- R1 | Commit current changes to move WIP forward | <uncommitted files>
|
|
59
|
+
- R2 | Write tests for <risk item from report> | <test file to create/extend>
|
|
60
|
+
- R3 | Address release report risk <X> before adding more changes | <affected files>
|
|
61
|
+
- R4 | Push commits to get CI feedback | N/A
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Do NOT emit free-text suggestions as prose. The structured block is the only output for above-appetite guidance.
|
|
65
|
+
|
|
66
|
+
The verdict is `RISK_VERDICT: PAUSE`. This blocks the next edit until the risk is addressed.
|
|
57
67
|
|
|
58
68
|
## Control Discovery
|
|
59
69
|
|
package/bin/install.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { resolve, dirname } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const utils = await import(resolve(__dirname, "
|
|
7
|
+
const utils = await import(resolve(__dirname, "../lib/install-utils.mjs"));
|
|
8
8
|
|
|
9
9
|
const PLUGIN = "wr-risk-scorer";
|
|
10
10
|
const DEPS = [];
|
|
@@ -20,6 +20,7 @@ Pipeline risk scoring, commit/push gates, and secret leak detection
|
|
|
20
20
|
Options:
|
|
21
21
|
--update Update this plugin and its skills
|
|
22
22
|
--uninstall Remove this plugin
|
|
23
|
+
--scope Installation scope: project (default) or user
|
|
23
24
|
--dry-run Show what would be done without executing
|
|
24
25
|
--help, -h Show this help
|
|
25
26
|
`);
|
|
@@ -38,5 +39,5 @@ if (flags.uninstall) {
|
|
|
38
39
|
} else if (flags.update) {
|
|
39
40
|
utils.updatePackage(PLUGIN);
|
|
40
41
|
} else {
|
|
41
|
-
utils.installPackage(PLUGIN, { deps: DEPS });
|
|
42
|
+
utils.installPackage(PLUGIN, { deps: DEPS, scope: flags.scope });
|
|
42
43
|
}
|
package/hooks/git-push-gate.sh
CHANGED
|
@@ -47,7 +47,7 @@ if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*npm run push:watch(\s|$)'; then
|
|
|
47
47
|
fi
|
|
48
48
|
PUSH_SCORE_FILE="${RDIR}/push"
|
|
49
49
|
if [ ! -f "$PUSH_SCORE_FILE" ]; then
|
|
50
|
-
risk_gate_deny "Push blocked: No push risk score found. Delegate to risk-scorer
|
|
50
|
+
risk_gate_deny "Push blocked: No push risk score found. Delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') to assess cumulative pipeline risk."
|
|
51
51
|
exit 0
|
|
52
52
|
fi
|
|
53
53
|
PUSH_NOW=$(date +%s)
|
|
@@ -65,19 +65,26 @@ if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*npm run push:watch(\s|$)'; then
|
|
|
65
65
|
fi
|
|
66
66
|
PUSH_DENIED=$(python3 -c "print('yes' if float('${PUSH_SCORE}') >= 5 else 'no')" 2>/dev/null || echo "no")
|
|
67
67
|
if [ "$PUSH_DENIED" = "yes" ]; then
|
|
68
|
-
risk_gate_deny "Push blocked: Push risk score ${PUSH_SCORE}/25 (Medium or above). To proceed: (1) release first via \`npm run release:watch\`, (2) split the push, or (3) add risk-reducing measures. If risk-neutral or risk-reducing, delegate to risk-scorer
|
|
68
|
+
risk_gate_deny "Push blocked: Push risk score ${PUSH_SCORE}/25 (Medium or above). To proceed: (1) release first via \`npm run release:watch\`, (2) split the push, or (3) add risk-reducing measures. If risk-neutral or risk-reducing, delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') — it will create a bypass marker."
|
|
69
69
|
exit 0
|
|
70
70
|
fi
|
|
71
71
|
fi
|
|
72
72
|
exit 0
|
|
73
73
|
fi
|
|
74
74
|
|
|
75
|
+
# Block `changeset version` — versioning is done by the release pipeline,
|
|
76
|
+
# not locally. Creating changesets (`npx changeset`) is fine.
|
|
77
|
+
if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*(npx changeset|npm run changeset)\s+version(\s|$)'; then
|
|
78
|
+
risk_gate_deny "Do not run \`changeset version\` locally. The release pipeline handles versioning automatically. To release: (1) push your changes with \`npm run push:watch\`, (2) the pipeline creates a release PR via changesets, (3) merge the release PR to publish. If you need to create a changeset, use \`npx changeset\` (without \`version\`)."
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
75
82
|
# Gate changeset creation on release risk score (fail-closed).
|
|
76
83
|
# Changesets feed directly into releases, so gate on the release score.
|
|
77
84
|
if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*(npx changeset|npm run changeset)(\s|$)'; then
|
|
78
85
|
if [ -n "$SESSION_ID" ]; then
|
|
79
86
|
if ! check_risk_gate "$SESSION_ID" "release"; then
|
|
80
|
-
risk_gate_deny "Changeset blocked: ${RISK_GATE_REASON}"
|
|
87
|
+
risk_gate_deny "Changeset blocked: ${RISK_GATE_REASON}. To create a changeset, the release risk score must be within appetite. Delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') to assess."
|
|
81
88
|
exit 0
|
|
82
89
|
fi
|
|
83
90
|
fi
|
|
@@ -101,7 +108,7 @@ if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*npm run release:watch(\s|$)'; the
|
|
|
101
108
|
exit 0
|
|
102
109
|
fi
|
|
103
110
|
if ! check_risk_gate "$SESSION_ID" "release"; then
|
|
104
|
-
risk_gate_deny "Release blocked: ${RISK_GATE_REASON}
|
|
111
|
+
risk_gate_deny "Release blocked: ${RISK_GATE_REASON}"
|
|
105
112
|
exit 0
|
|
106
113
|
fi
|
|
107
114
|
fi
|
|
@@ -110,7 +117,16 @@ fi
|
|
|
110
117
|
|
|
111
118
|
# Match gh pr merge. Should go via npm run release:watch instead.
|
|
112
119
|
if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*gh pr merge(\s|$)'; then
|
|
113
|
-
|
|
120
|
+
# Check if the project has a release:watch script
|
|
121
|
+
if [ -f "package.json" ] && python3 -c "
|
|
122
|
+
import json, sys
|
|
123
|
+
pkg = json.load(open('package.json'))
|
|
124
|
+
sys.exit(0 if 'release:watch' in pkg.get('scripts', {}) else 1)
|
|
125
|
+
" 2>/dev/null; then
|
|
126
|
+
risk_gate_deny "Use \`npm run release:watch\` instead of \`gh pr merge\`. It merges the release PR, watches the publish pipeline, and surfaces the production URL when live -- or tells you what failed and how to fix it."
|
|
127
|
+
else
|
|
128
|
+
risk_gate_deny "Direct \`gh pr merge\` is blocked (no release:watch script found). Create a release:watch npm script that: (1) finds and merges the release PR with \`gh pr merge\`, (2) waits for the CI workflow with \`gh run list\`, and (3) watches it with \`gh run watch --exit-status\`. Then run \`npm run release:watch\` to release."
|
|
129
|
+
fi
|
|
114
130
|
exit 0
|
|
115
131
|
fi
|
|
116
132
|
|
package/hooks/hooks.json
CHANGED
|
@@ -15,10 +15,6 @@
|
|
|
15
15
|
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/wip-risk-mark.sh" }] },
|
|
16
16
|
{ "matcher": "Agent", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-score-mark.sh" }] },
|
|
17
17
|
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-hash-refresh.sh" }] }
|
|
18
|
-
],
|
|
19
|
-
"Stop": [
|
|
20
|
-
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-score-reset.sh" }] },
|
|
21
|
-
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-policy-reset-marker.sh" }] }
|
|
22
18
|
]
|
|
23
19
|
}
|
|
24
20
|
}
|
package/hooks/lib/risk-gate.sh
CHANGED
|
@@ -21,7 +21,7 @@ check_risk_gate() {
|
|
|
21
21
|
|
|
22
22
|
# 1. Score file must exist (fail-closed)
|
|
23
23
|
if [ ! -f "$SCORE_FILE" ]; then
|
|
24
|
-
RISK_GATE_REASON="No ${ACTION} risk score found.
|
|
24
|
+
RISK_GATE_REASON="No ${ACTION} risk score found. Delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') to assess cumulative pipeline risk."
|
|
25
25
|
return 1
|
|
26
26
|
fi
|
|
27
27
|
|
|
@@ -30,7 +30,7 @@ check_risk_gate() {
|
|
|
30
30
|
local SCORE_TIME=$(_mtime "$SCORE_FILE")
|
|
31
31
|
local AGE=$(( NOW - SCORE_TIME ))
|
|
32
32
|
if [ "$AGE" -ge "$TTL_SECONDS" ]; then
|
|
33
|
-
RISK_GATE_REASON="Risk score expired (${AGE}s old, TTL ${TTL_SECONDS}s).
|
|
33
|
+
RISK_GATE_REASON="Risk score expired (${AGE}s old, TTL ${TTL_SECONDS}s). Delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') to rescore."
|
|
34
34
|
return 1
|
|
35
35
|
fi
|
|
36
36
|
|
|
@@ -43,7 +43,7 @@ check_risk_gate() {
|
|
|
43
43
|
local CURRENT_HASH
|
|
44
44
|
CURRENT_HASH=$("$_RISK_GATE_DIR/pipeline-state.sh" --hash-inputs 2>/dev/null | _hashcmd | cut -d' ' -f1)
|
|
45
45
|
if [ "$STORED_HASH" != "$CURRENT_HASH" ]; then
|
|
46
|
-
RISK_GATE_REASON="Pipeline state drift:
|
|
46
|
+
RISK_GATE_REASON="Pipeline state drift: working tree changed since the last ${ACTION} risk assessment. Delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') to rescore against the current state."
|
|
47
47
|
return 1
|
|
48
48
|
fi
|
|
49
49
|
fi
|
|
@@ -63,7 +63,7 @@ print('yes' if score >= 5 else 'no')
|
|
|
63
63
|
" 2>/dev/null || echo "no")
|
|
64
64
|
|
|
65
65
|
if [ "$DENIED" = "yes" ]; then
|
|
66
|
-
RISK_GATE_REASON="${ACTION} risk score ${SCORE}/25 (Medium or above).
|
|
66
|
+
RISK_GATE_REASON="${ACTION} risk score ${SCORE}/25 (Medium or above). To proceed: (1) split the ${ACTION}, (2) add risk-reducing measures, or (3) for a LIVE INCIDENT, delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') with incident context for an incident bypass."
|
|
67
67
|
return 1
|
|
68
68
|
fi
|
|
69
69
|
|
|
@@ -18,6 +18,16 @@ if [ -z "$SESSION_ID" ] || [ -z "$FILE_PATH" ]; then
|
|
|
18
18
|
exit 0
|
|
19
19
|
fi
|
|
20
20
|
|
|
21
|
+
# P004: Only gate files inside the project root.
|
|
22
|
+
case "$FILE_PATH" in
|
|
23
|
+
/*)
|
|
24
|
+
case "$FILE_PATH" in
|
|
25
|
+
"$PWD"/*) ;;
|
|
26
|
+
*) exit 0 ;;
|
|
27
|
+
esac
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
21
31
|
# Only gate RISK-POLICY.md
|
|
22
32
|
BASENAME=$(basename "$FILE_PATH")
|
|
23
33
|
if [ "$BASENAME" != "RISK-POLICY.md" ]; then
|
|
@@ -35,7 +45,7 @@ cat <<'EOF'
|
|
|
35
45
|
"hookSpecificOutput": {
|
|
36
46
|
"hookEventName": "PreToolUse",
|
|
37
47
|
"permissionDecision": "deny",
|
|
38
|
-
"permissionDecisionReason": "BLOCKED: Cannot edit RISK-POLICY.md directly. Run
|
|
48
|
+
"permissionDecisionReason": "BLOCKED: Cannot edit RISK-POLICY.md directly. Run /wr-risk-scorer:update-policy first -- it enforces ISO 31000 compliance (reads the risk-scorer contract, discovers project context, checks for incidents, validates with you, and smoke-tests the result). Use the Skill tool with skill: \"wr-risk-scorer:update-policy\"."
|
|
39
49
|
}
|
|
40
50
|
}
|
|
41
51
|
EOF
|
|
@@ -57,7 +57,7 @@ fi
|
|
|
57
57
|
|
|
58
58
|
# Gate check: existence, TTL, drift, threshold
|
|
59
59
|
if ! check_risk_gate "$SESSION_ID" "commit"; then
|
|
60
|
-
risk_gate_deny "Commit blocked: ${RISK_GATE_REASON} To proceed: (1) stage files with git add, (2) delegate to risk-scorer
|
|
60
|
+
risk_gate_deny "Commit blocked: ${RISK_GATE_REASON} To proceed: (1) stage files with git add, (2) delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') to assess cumulative pipeline risk. If the commit is risk-neutral or risk-reducing, the scorer will create a bypass marker."
|
|
61
61
|
exit 0
|
|
62
62
|
fi
|
|
63
63
|
|
package/hooks/risk-score-mark.sh
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# risk-scorer agents. This is the ONLY place score files are written —
|
|
5
5
|
# agents output structured markers, this hook writes the files.
|
|
6
6
|
#
|
|
7
|
-
# Handles: risk-scorer
|
|
7
|
+
# Handles: wr-risk-scorer:pipeline, wr-risk-scorer:plan, wr-risk-scorer:wip, wr-risk-scorer:policy
|
|
8
8
|
# Replaces: risk-policy-mark-reviewed.sh (which had fragile P001 backup parsing)
|
|
9
9
|
|
|
10
10
|
set -euo pipefail
|
|
@@ -34,7 +34,7 @@ RDIR=$(_risk_dir "$SESSION_ID")
|
|
|
34
34
|
# ---------------------------------------------------------------------------
|
|
35
35
|
# Pipeline scorer: write commit/push/release scores + bypass markers
|
|
36
36
|
# ---------------------------------------------------------------------------
|
|
37
|
-
if echo "$SUBAGENT" | grep -qE 'risk-scorer
|
|
37
|
+
if echo "$SUBAGENT" | grep -qE 'risk-scorer.pipeline'; then
|
|
38
38
|
# Parse RISK_SCORES: commit=N push=N release=N
|
|
39
39
|
SCORES_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^RISK_SCORES:' | tail -1) || true
|
|
40
40
|
if [ -n "$SCORES_LINE" ]; then
|
|
@@ -79,7 +79,7 @@ fi
|
|
|
79
79
|
# ---------------------------------------------------------------------------
|
|
80
80
|
# Plan scorer: write plan-reviewed marker on PASS
|
|
81
81
|
# ---------------------------------------------------------------------------
|
|
82
|
-
if echo "$SUBAGENT" | grep -qE 'risk-scorer
|
|
82
|
+
if echo "$SUBAGENT" | grep -qE 'risk-scorer.plan'; then
|
|
83
83
|
VERDICT_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^RISK_VERDICT:' | tail -1) || true
|
|
84
84
|
VERDICT=$(echo "$VERDICT_LINE" | sed 's/^RISK_VERDICT:[[:space:]]*//' | tr -d '[:space:]')
|
|
85
85
|
case "$VERDICT" in
|
|
@@ -98,7 +98,7 @@ fi
|
|
|
98
98
|
# ---------------------------------------------------------------------------
|
|
99
99
|
# WIP scorer: write wip-reviewed marker (unblocks next edit)
|
|
100
100
|
# ---------------------------------------------------------------------------
|
|
101
|
-
if echo "$SUBAGENT" | grep -qE 'risk-scorer
|
|
101
|
+
if echo "$SUBAGENT" | grep -qE 'risk-scorer.wip'; then
|
|
102
102
|
# WIP assessment was done — unblock next edit regardless of CONTINUE/PAUSE
|
|
103
103
|
# (PAUSE is advisory guidance to the user, not a hard gate)
|
|
104
104
|
touch "${RDIR}/wip-reviewed"
|
|
@@ -107,7 +107,7 @@ fi
|
|
|
107
107
|
# ---------------------------------------------------------------------------
|
|
108
108
|
# Policy scorer: write policy-reviewed marker on PASS
|
|
109
109
|
# ---------------------------------------------------------------------------
|
|
110
|
-
if echo "$SUBAGENT" | grep -qE 'risk-scorer
|
|
110
|
+
if echo "$SUBAGENT" | grep -qE 'risk-scorer.policy'; then
|
|
111
111
|
VERDICT_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^RISK_VERDICT:' | tail -1) || true
|
|
112
112
|
VERDICT=$(echo "$VERDICT_LINE" | sed 's/^RISK_VERDICT:[[:space:]]*//' | tr -d '[:space:]')
|
|
113
113
|
case "$VERDICT" in
|
|
@@ -24,7 +24,7 @@ cat <<'EOF'
|
|
|
24
24
|
"hookSpecificOutput": {
|
|
25
25
|
"hookEventName": "PreToolUse",
|
|
26
26
|
"permissionDecision": "deny",
|
|
27
|
-
"permissionDecisionReason": "BLOCKED: Risk-scorer must review the plan before exiting plan mode. Delegate to risk-scorer
|
|
27
|
+
"permissionDecisionReason": "BLOCKED: Risk-scorer must review the plan before exiting plan mode. Delegate to wr-risk-scorer:plan (subagent_type: 'wr-risk-scorer:plan') to review the plan file for risk, including projected release risk."
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
EOF
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
# Tests for git-push-gate.sh — gh pr merge block and release:watch guidance
|
|
3
|
+
|
|
4
|
+
setup() {
|
|
5
|
+
HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
6
|
+
HOOK="$HOOKS_DIR/git-push-gate.sh"
|
|
7
|
+
|
|
8
|
+
TEST_SESSION="bats-push-gate-$$-${BATS_TEST_NUMBER}"
|
|
9
|
+
# Ensure a clean risk dir
|
|
10
|
+
RDIR="${TMPDIR:-/tmp}/claude-risk-${TEST_SESSION}"
|
|
11
|
+
rm -rf "$RDIR"
|
|
12
|
+
mkdir -p "$RDIR"
|
|
13
|
+
|
|
14
|
+
# Create a temp project dir for package.json detection
|
|
15
|
+
TEST_PROJECT_DIR="$(mktemp -d)"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
teardown() {
|
|
19
|
+
rm -rf "$RDIR"
|
|
20
|
+
rm -rf "$TEST_PROJECT_DIR"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Helper: build a PreToolUse Bash input with a given command
|
|
24
|
+
build_input() {
|
|
25
|
+
local cmd="$1"
|
|
26
|
+
cat <<ENDJSON
|
|
27
|
+
{
|
|
28
|
+
"session_id": "$TEST_SESSION",
|
|
29
|
+
"tool_name": "Bash",
|
|
30
|
+
"tool_input": {
|
|
31
|
+
"command": "$cmd"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
ENDJSON
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@test "gh pr merge is blocked with release:watch guidance when script exists" {
|
|
38
|
+
# Create a package.json with release:watch
|
|
39
|
+
cat > "$TEST_PROJECT_DIR/package.json" <<'PKG'
|
|
40
|
+
{ "scripts": { "release:watch": "bash scripts/release-watch.sh" } }
|
|
41
|
+
PKG
|
|
42
|
+
|
|
43
|
+
INPUT=$(build_input "gh pr merge 4 --merge")
|
|
44
|
+
run bash -c "cd '$TEST_PROJECT_DIR' && echo '$INPUT' | '$HOOK'"
|
|
45
|
+
[ "$status" -eq 0 ]
|
|
46
|
+
[[ "$output" == *"permissionDecision"* ]]
|
|
47
|
+
[[ "$output" == *"deny"* ]]
|
|
48
|
+
[[ "$output" == *"release:watch"* ]]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@test "gh pr merge tells agent to create release:watch when script missing" {
|
|
52
|
+
# Create a package.json WITHOUT release:watch
|
|
53
|
+
cat > "$TEST_PROJECT_DIR/package.json" <<'PKG'
|
|
54
|
+
{ "scripts": { "test": "echo test" } }
|
|
55
|
+
PKG
|
|
56
|
+
|
|
57
|
+
INPUT=$(build_input "gh pr merge 4 --merge")
|
|
58
|
+
run bash -c "cd '$TEST_PROJECT_DIR' && echo '$INPUT' | '$HOOK'"
|
|
59
|
+
[ "$status" -eq 0 ]
|
|
60
|
+
[[ "$output" == *"permissionDecision"* ]]
|
|
61
|
+
[[ "$output" == *"deny"* ]]
|
|
62
|
+
# Should tell agent to create the script
|
|
63
|
+
[[ "$output" == *"no release:watch script"* ]]
|
|
64
|
+
[[ "$output" == *"gh pr merge"* ]]
|
|
65
|
+
[[ "$output" == *"gh run watch"* ]]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@test "gh pr merge tells agent to create release:watch when no package.json" {
|
|
69
|
+
local empty_dir="$(mktemp -d)"
|
|
70
|
+
|
|
71
|
+
INPUT=$(build_input "gh pr merge 4 --merge")
|
|
72
|
+
run bash -c "cd '$empty_dir' && echo '$INPUT' | '$HOOK'"
|
|
73
|
+
[ "$status" -eq 0 ]
|
|
74
|
+
[[ "$output" == *"permissionDecision"* ]]
|
|
75
|
+
[[ "$output" == *"deny"* ]]
|
|
76
|
+
# Should tell agent to create the script
|
|
77
|
+
[[ "$output" == *"no release:watch script"* ]]
|
|
78
|
+
[[ "$output" == *"gh pr merge"* ]]
|
|
79
|
+
[[ "$output" == *"gh run watch"* ]]
|
|
80
|
+
|
|
81
|
+
rm -rf "$empty_dir"
|
|
82
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P004: risk-policy-enforce-edit.sh project-root check.
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
7
|
+
HOOK="$SCRIPT_DIR/risk-policy-enforce-edit.sh"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
run_hook_with_file() {
|
|
11
|
+
local file_path="$1"
|
|
12
|
+
local json="{\"tool_input\":{\"file_path\":\"${file_path}\"},\"session_id\":\"test-$$\"}"
|
|
13
|
+
echo "$json" | bash "$HOOK"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@test "risk-policy project-root: RISK-POLICY.md outside project exits 0" {
|
|
17
|
+
run run_hook_with_file "/Users/other/project/RISK-POLICY.md"
|
|
18
|
+
[ "$status" -eq 0 ]
|
|
19
|
+
[[ "$output" != *"BLOCKED"* ]]
|
|
20
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Tests for risk-score-mark.sh — verifies the PostToolUse:Agent hook
|
|
4
|
+
# parses risk-scorer agent output and writes the right files into
|
|
5
|
+
# the session-scoped risk dir.
|
|
6
|
+
#
|
|
7
|
+
# Per ADR-005 (P011): behavioural assertions are functional — they
|
|
8
|
+
# pipe mock hook input to the script and assert on side-effects, not
|
|
9
|
+
# on what the source happens to contain. The four "echo X | grep X"
|
|
10
|
+
# tautologies that previously lived here have been removed (they
|
|
11
|
+
# always passed regardless of hook behaviour).
|
|
12
|
+
|
|
13
|
+
setup() {
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
15
|
+
HOOK="$SCRIPT_DIR/risk-score-mark.sh"
|
|
16
|
+
ORIG_DIR="$PWD"
|
|
17
|
+
TEST_DIR=$(mktemp -d)
|
|
18
|
+
cd "$TEST_DIR"
|
|
19
|
+
TMPDIR="$TEST_DIR/tmp"
|
|
20
|
+
export TMPDIR
|
|
21
|
+
mkdir -p "$TMPDIR"
|
|
22
|
+
SESSION_ID="test-session-$$"
|
|
23
|
+
RDIR="$TMPDIR/claude-risk-${SESSION_ID}"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
teardown() {
|
|
27
|
+
cd "$ORIG_DIR"
|
|
28
|
+
rm -rf "$TEST_DIR"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Helper: build the PostToolUse:Agent JSON envelope and pipe it to the hook.
|
|
32
|
+
# AGENT_OUTPUT is wrapped in tool_response.content[0].text to match the
|
|
33
|
+
# real Claude Code PostToolUse hook payload shape.
|
|
34
|
+
run_hook() {
|
|
35
|
+
local subagent="$1"
|
|
36
|
+
local agent_output="$2"
|
|
37
|
+
python3 -c "
|
|
38
|
+
import json, sys
|
|
39
|
+
print(json.dumps({
|
|
40
|
+
'tool_name': 'Agent',
|
|
41
|
+
'session_id': '${SESSION_ID}',
|
|
42
|
+
'tool_input': {'subagent_type': '${subagent}'},
|
|
43
|
+
'tool_response': {'content': [{'type': 'text', 'text': sys.stdin.read()}]}
|
|
44
|
+
}))" <<<"$agent_output" | bash "$HOOK"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# --- Pipeline scorer: writes commit/push/release score files ---
|
|
48
|
+
|
|
49
|
+
@test "pipeline: writes commit/push/release scores from RISK_SCORES line" {
|
|
50
|
+
run_hook "wr-risk-scorer:pipeline" "Header text
|
|
51
|
+
RISK_SCORES: commit=2 push=3 release=1
|
|
52
|
+
Trailing text"
|
|
53
|
+
[ "$(cat "$RDIR/commit")" = "2" ]
|
|
54
|
+
[ "$(cat "$RDIR/push")" = "3" ]
|
|
55
|
+
[ "$(cat "$RDIR/release")" = "1" ]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@test "pipeline: writes reducing bypass markers when RISK_BYPASS: reducing" {
|
|
59
|
+
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=2 push=2 release=0
|
|
60
|
+
RISK_BYPASS: reducing"
|
|
61
|
+
[ -f "$RDIR/reducing-commit" ]
|
|
62
|
+
[ -f "$RDIR/reducing-push" ]
|
|
63
|
+
[ -f "$RDIR/reducing-release" ]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@test "pipeline: writes incident bypass marker when RISK_BYPASS: incident" {
|
|
67
|
+
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=10 push=10 release=10
|
|
68
|
+
RISK_BYPASS: incident"
|
|
69
|
+
[ -f "$RDIR/incident-release" ]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@test "pipeline: writes nothing when output has no RISK_SCORES line" {
|
|
73
|
+
run_hook "wr-risk-scorer:pipeline" "No score line in this output"
|
|
74
|
+
[ ! -f "$RDIR/commit" ]
|
|
75
|
+
[ ! -f "$RDIR/push" ]
|
|
76
|
+
[ ! -f "$RDIR/release" ]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# --- Plan scorer: writes plan-reviewed marker on PASS only ---
|
|
80
|
+
|
|
81
|
+
@test "plan: writes plan-reviewed marker on RISK_VERDICT: PASS" {
|
|
82
|
+
run_hook "wr-risk-scorer:plan" "RISK_VERDICT: PASS"
|
|
83
|
+
[ -f "$RDIR/plan-reviewed" ]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@test "plan: does NOT write plan-reviewed marker on RISK_VERDICT: FAIL" {
|
|
87
|
+
run_hook "wr-risk-scorer:plan" "RISK_VERDICT: FAIL"
|
|
88
|
+
[ ! -f "$RDIR/plan-reviewed" ]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# --- Subagent routing: case guard ignores non-risk-scorer agents ---
|
|
92
|
+
|
|
93
|
+
@test "case guard: skips unrelated agent without writing files" {
|
|
94
|
+
run_hook "wr-architect:agent" "RISK_SCORES: commit=99 push=99 release=99"
|
|
95
|
+
[ ! -f "$RDIR/commit" ]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@test "case guard: matches wr-risk-scorer:pipeline subagent" {
|
|
99
|
+
SUBAGENT="wr-risk-scorer:pipeline"
|
|
100
|
+
case "$SUBAGENT" in
|
|
101
|
+
*risk-scorer*) true ;;
|
|
102
|
+
*) false ;;
|
|
103
|
+
esac
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@test "case guard: does NOT match wr-architect:agent" {
|
|
107
|
+
SUBAGENT="wr-architect:agent"
|
|
108
|
+
case "$SUBAGENT" in
|
|
109
|
+
*risk-scorer*) false ;;
|
|
110
|
+
*) true ;;
|
|
111
|
+
esac
|
|
112
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P001 / ADR-009: Stop-hook marker reset removed.
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
PLUGIN_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@test "risk-scorer: hooks.json has no Stop hook entry (ADR-009)" {
|
|
10
|
+
! grep -q '"Stop"' "$PLUGIN_DIR/hooks/hooks.json"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@test "risk-scorer: risk-score-reset.sh has been removed" {
|
|
14
|
+
[ ! -f "$PLUGIN_DIR/hooks/risk-score-reset.sh" ]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@test "risk-scorer: risk-policy-reset-marker.sh has been removed" {
|
|
18
|
+
[ ! -f "$PLUGIN_DIR/hooks/risk-policy-reset-marker.sh" ]
|
|
19
|
+
}
|
package/hooks/wip-risk-gate.sh
CHANGED
|
@@ -37,7 +37,7 @@ cat <<'EOF'
|
|
|
37
37
|
"hookSpecificOutput": {
|
|
38
38
|
"hookEventName": "PreToolUse",
|
|
39
39
|
"permissionDecision": "deny",
|
|
40
|
-
"permissionDecisionReason": "WIP risk assessment required. Delegate to risk-scorer
|
|
40
|
+
"permissionDecisionReason": "WIP risk assessment required. Delegate to wr-risk-scorer:wip (subagent_type: 'wr-risk-scorer:wip') to assess cumulative pipeline risk for changes so far."
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
EOF
|
package/hooks/wip-risk-mark.sh
CHANGED
|
@@ -17,16 +17,8 @@ SESSION_ID=$(_get_session_id)
|
|
|
17
17
|
|
|
18
18
|
MARKER="$(_risk_dir "$SESSION_ID")/wip-reviewed"
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
[ -n "$FILE_PATH" ] || exit 0
|
|
24
|
-
|
|
25
|
-
if ! _is_doc_file "$FILE_PATH"; then
|
|
26
|
-
rm -f "$MARKER"
|
|
27
|
-
fi
|
|
28
|
-
;;
|
|
29
|
-
# Agent case handled by risk-score-mark.sh
|
|
30
|
-
esac
|
|
20
|
+
# WIP marker persists after assessment — allows multiple edits.
|
|
21
|
+
# Marker lifecycle: TTL + drift detection (ADR-009).
|
|
22
|
+
# Agent case (marker creation) handled by risk-score-mark.sh.
|
|
31
23
|
|
|
32
24
|
exit 0
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared install utilities for @windyroad/* packages.
|
|
3
|
+
* Used by both per-plugin installers and the meta-installer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const MARKETPLACE_REPO = "windyroad/agent-plugins";
|
|
9
|
+
const MARKETPLACE_NAME = "windyroad";
|
|
10
|
+
|
|
11
|
+
let _dryRun = false;
|
|
12
|
+
|
|
13
|
+
export { MARKETPLACE_REPO, MARKETPLACE_NAME };
|
|
14
|
+
|
|
15
|
+
export function setDryRun(value) {
|
|
16
|
+
_dryRun = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isDryRun() {
|
|
20
|
+
return _dryRun;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function run(cmd, label) {
|
|
24
|
+
console.log(` ${label}...`);
|
|
25
|
+
if (_dryRun) {
|
|
26
|
+
console.log(` [dry-run] ${cmd}`);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
execSync(cmd, { stdio: "inherit" });
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
console.error(` FAILED: ${label}`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function checkPrerequisites() {
|
|
39
|
+
if (_dryRun) return;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
43
|
+
} catch {
|
|
44
|
+
console.error(
|
|
45
|
+
"Error: 'claude' CLI not found. Install Claude Code first:\n https://docs.anthropic.com/en/docs/claude-code\n"
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function addMarketplace() {
|
|
52
|
+
return run(
|
|
53
|
+
`claude plugin marketplace add ${MARKETPLACE_REPO}`,
|
|
54
|
+
`Marketplace: ${MARKETPLACE_NAME}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function installPlugin(pluginName, { scope = "project" } = {}) {
|
|
59
|
+
return run(
|
|
60
|
+
`claude plugin install ${pluginName}@${MARKETPLACE_NAME} --scope ${scope}`,
|
|
61
|
+
pluginName
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function updatePlugin(pluginName) {
|
|
66
|
+
return run(`claude plugin update ${pluginName}`, pluginName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function uninstallPlugin(pluginName) {
|
|
70
|
+
return run(`claude plugin uninstall ${pluginName}`, `Removing ${pluginName}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Install a single package: marketplace add + plugin install.
|
|
75
|
+
*/
|
|
76
|
+
export function installPackage(pluginName, { deps = [], scope = "project" } = {}) {
|
|
77
|
+
console.log(`\nInstalling @windyroad/${pluginName.replace("wr-", "")} (${scope} scope)...\n`);
|
|
78
|
+
|
|
79
|
+
addMarketplace();
|
|
80
|
+
installPlugin(pluginName, { scope });
|
|
81
|
+
|
|
82
|
+
if (deps.length > 0) {
|
|
83
|
+
console.log(`\nNote: This plugin works best with:`);
|
|
84
|
+
for (const dep of deps) {
|
|
85
|
+
console.log(` - @windyroad/${dep.replace("wr-", "")} (npx @windyroad/${dep.replace("wr-", "")})`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(
|
|
90
|
+
`\nDone! Restart Claude Code to activate.\n`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Update a single package.
|
|
96
|
+
*/
|
|
97
|
+
export function updatePackage(pluginName) {
|
|
98
|
+
console.log(`\nUpdating @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
99
|
+
|
|
100
|
+
run(
|
|
101
|
+
`claude plugin marketplace update ${MARKETPLACE_NAME}`,
|
|
102
|
+
"Updating marketplace"
|
|
103
|
+
);
|
|
104
|
+
updatePlugin(pluginName);
|
|
105
|
+
|
|
106
|
+
console.log("\nDone! Restart Claude Code to apply updates.\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Uninstall a single package.
|
|
111
|
+
*/
|
|
112
|
+
export function uninstallPackage(pluginName) {
|
|
113
|
+
console.log(`\nUninstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
114
|
+
|
|
115
|
+
uninstallPlugin(pluginName);
|
|
116
|
+
|
|
117
|
+
console.log("\nDone. Restart Claude Code to apply changes.\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse standard flags used by all per-plugin installers.
|
|
122
|
+
*/
|
|
123
|
+
export function parseStandardArgs(argv) {
|
|
124
|
+
const args = argv.slice(2);
|
|
125
|
+
const flags = {
|
|
126
|
+
help: args.includes("--help") || args.includes("-h"),
|
|
127
|
+
uninstall: args.includes("--uninstall"),
|
|
128
|
+
update: args.includes("--update"),
|
|
129
|
+
dryRun: args.includes("--dry-run"),
|
|
130
|
+
scope: "project",
|
|
131
|
+
};
|
|
132
|
+
const scopeIdx = args.indexOf("--scope");
|
|
133
|
+
if (scopeIdx !== -1 && args[scopeIdx + 1]) {
|
|
134
|
+
const val = args[scopeIdx + 1];
|
|
135
|
+
if (["project", "user", "local"].includes(val)) {
|
|
136
|
+
flags.scope = val;
|
|
137
|
+
} else {
|
|
138
|
+
console.error("--scope requires: project, user, or local");
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return flags;
|
|
143
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windyroad/risk-scorer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-preview.70",
|
|
4
4
|
"description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
|
|
5
5
|
"bin": {
|
|
6
6
|
"windyroad-risk-scorer": "./bin/install.mjs"
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"agents/",
|
|
24
24
|
"hooks/",
|
|
25
25
|
"skills/",
|
|
26
|
-
".claude-plugin/"
|
|
26
|
+
".claude-plugin/",
|
|
27
|
+
"lib/"
|
|
27
28
|
]
|
|
28
29
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: wr
|
|
2
|
+
name: wr-risk-scorer: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, AskUserQuestion, Agent
|
|
5
5
|
---
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Stop hook: Clears risk-policy session marker.
|
|
3
|
-
# Mirrors: architect-reset-marker.sh
|
|
4
|
-
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
-
source "$SCRIPT_DIR/lib/gate-helpers.sh"
|
|
7
|
-
|
|
8
|
-
_parse_input
|
|
9
|
-
|
|
10
|
-
SESSION_ID=$(_get_session_id)
|
|
11
|
-
|
|
12
|
-
if [ -n "$SESSION_ID" ]; then
|
|
13
|
-
RDIR=$(_risk_dir "$SESSION_ID")
|
|
14
|
-
rm -f "${RDIR}/policy-reviewed" "${RDIR}/plan-reviewed"
|
|
15
|
-
fi
|
|
16
|
-
|
|
17
|
-
exit 0
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Stop hook: Clears risk score temp files on session end.
|
|
3
|
-
|
|
4
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
source "$SCRIPT_DIR/lib/gate-helpers.sh"
|
|
6
|
-
|
|
7
|
-
_parse_input
|
|
8
|
-
|
|
9
|
-
SESSION_ID=$(_get_session_id)
|
|
10
|
-
|
|
11
|
-
if [ -n "$SESSION_ID" ]; then
|
|
12
|
-
# Remove the entire session-scoped directory
|
|
13
|
-
RDIR="${TMPDIR:-/tmp}/claude-risk-${SESSION_ID}"
|
|
14
|
-
rm -rf "$RDIR"
|
|
15
|
-
fi
|
|
16
|
-
|
|
17
|
-
exit 0
|