@windyroad/risk-scorer 0.13.5 → 0.14.0-preview.762
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -2
- package/agents/pipeline.md +5 -5
- package/agents/wip.md +2 -2
- package/hooks/external-comms-gate.sh +8 -7
- package/hooks/lib/risk-gate.sh +12 -16
- package/hooks/plan-risk-guidance.sh +10 -2
- package/hooks/risk-score-commit-gate.sh +2 -1
- package/hooks/test/ci-status-gate.bats +7 -13
- package/hooks/test/external-comms-gate.bats +17 -11
- package/lib/install-utils.mjs +99 -23
- package/package.json +1 -1
- package/skills/assess-external-comms/SKILL.md +1 -1
- package/skills/assess-release/SKILL.md +1 -6
- package/skills/update-policy/SKILL.md +5 -0
package/README.md
CHANGED
|
@@ -113,8 +113,10 @@ Behaviour:
|
|
|
113
113
|
3. If `RISK-POLICY.md` is absent, the gate runs in advisory-only mode and
|
|
114
114
|
permits the call (graceful adoption).
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
There is no env override of the gate (the `BYPASS_RISK_GATE` knob was removed —
|
|
117
|
+
P377/RFC-029). Clear the gate by reducing risk to within appetite, or — for a
|
|
118
|
+
false-positive leak flag — by letting the reviewer re-judge. A genuine gate
|
|
119
|
+
misfire is recovered per ADR-048 (documented recovery), not an env bypass.
|
|
118
120
|
|
|
119
121
|
The canonical hook lives at `packages/shared/hooks/external-comms-gate.sh` and
|
|
120
122
|
is synced into each consumer plugin via `scripts/sync-external-comms-gate.sh`
|
package/agents/pipeline.md
CHANGED
|
@@ -26,7 +26,7 @@ If the action is risk-reducing or risk-neutral, add on a separate line:
|
|
|
26
26
|
RISK_BYPASS: reducing
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
For live
|
|
29
|
+
For a live-outage restore-service release that must proceed despite red/unreadable CI (the narrow CI-gate short-circuit case — see Incident-context scoring below; the change must still be net-risk-reducing per ADR-042 Rule 1b, no appetite carve-out):
|
|
30
30
|
```
|
|
31
31
|
RISK_BYPASS: incident
|
|
32
32
|
```
|
|
@@ -164,11 +164,11 @@ Acceptable `RISK_BYPASS_REASON:` values cite the ticket ID closed, the prior
|
|
|
164
164
|
risk report remediated, or the removed risk — matching one of the three
|
|
165
165
|
criteria above.
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
**Incident-context scoring (ADR-042 Rule 1b; P377/RFC-029 — no incident carve-out).** An active incident is a risk being realised (Likelihood already 5). Score the incident-response change against that live realised-risk baseline: weigh P(it increases impact) vs P(it reduces impact & likelihood / restores service) + P(it introduces a new incident). When net risk-reducing, emit `RISK_BYPASS: reducing` (the incident path collapses into the risk-reducing path — no separate appetite carve-out). `RISK_BYPASS: incident` is retained ONLY for the narrow operational case where a restore-service release must proceed despite red/unreadable CI during a live outage (it short-circuits the CI-status gate, which `reducing` does not); emit it only for a genuine live outage requiring immediate deployment AND only when the change is net-risk-reducing per the baseline scoring above.
|
|
168
168
|
|
|
169
169
|
## Below-Appetite Output Rule (ADR-013 Rule 5)
|
|
170
170
|
|
|
171
|
-
When ALL cumulative scores are within appetite (≤
|
|
171
|
+
When ALL cumulative scores are within appetite (≤ the appetite threshold in `RISK-POLICY.md` § Risk Appetite — read it; `Threshold: N`, default 4/Low when absent; do NOT hardcode 4), your output MUST contain ONLY:
|
|
172
172
|
1. The Pipeline Risk Report structure (layers, risk items, summary table)
|
|
173
173
|
2. `RISK_SCORES: commit=N push=N release=N`
|
|
174
174
|
3. `RISK_BYPASS: reducing` (if applicable)
|
|
@@ -177,7 +177,7 @@ Do NOT emit: "Suggested Actions", "Your call:", advisory warnings, back-pressure
|
|
|
177
177
|
|
|
178
178
|
## Above-Appetite Remediations
|
|
179
179
|
|
|
180
|
-
When ANY cumulative score exceeds appetite (> 4), the verbal verdict is **STOP**.
|
|
180
|
+
When ANY cumulative score exceeds appetite (> the RISK-POLICY.md appetite threshold, default 4 — read it, do NOT hardcode), the verbal verdict is **STOP**.
|
|
181
181
|
The scorer is not the primary decision-maker — the hook gate will block the
|
|
182
182
|
action — but the scorer's verdict must match the structured score so the agent
|
|
183
183
|
does not waste tool calls acting on an ambiguous nudge.
|
|
@@ -214,7 +214,7 @@ When a pipeline run identifies a **register-worthy risk shape**, emit a structur
|
|
|
214
214
|
|
|
215
215
|
### Trigger conditions (emit a hint when ANY fire)
|
|
216
216
|
|
|
217
|
-
1. **Above-appetite residual** — any cumulative residual score exceeds appetite
|
|
217
|
+
1. **Above-appetite residual** — any cumulative residual score exceeds the appetite threshold declared in `RISK-POLICY.md` § Risk Appetite (default 4; read it, do NOT hardcode). A risk that breaches appetite on this change is a standing-risk candidate, not just a one-off remediation target.
|
|
218
218
|
2. **Confidentiality disclosure** — the Confidential Information Disclosure check (below) flagged business metrics (revenue, user counts, pricing, client names, traffic volumes) in the diff. Confidentiality leaks are standing-risk-shaped even after the immediate remediation.
|
|
219
219
|
3. **User-stated precondition** — the User-Stated Preconditions Check (below) flagged an unmet paired capability as a standalone Risk item. Unmet preconditions are standing-risk-shaped because the dependency gap persists until the paired capability ships.
|
|
220
220
|
|
package/agents/wip.md
CHANGED
|
@@ -47,7 +47,7 @@ Always provide the cumulative risk picture:
|
|
|
47
47
|
|
|
48
48
|
### Below-Appetite Rule (ADR-013 Rule 5)
|
|
49
49
|
|
|
50
|
-
If cumulative risk is **within appetite** (
|
|
50
|
+
If cumulative risk is **within appetite** (≤ the appetite threshold in `RISK-POLICY.md` § Risk Appetite — read it; `Threshold: N`, default 4/Low when absent; do NOT hardcode): provide the assessment table and verdict only. Do NOT emit advisory prose, recommendations, or suggestions. The verdict is `RISK_VERDICT: CONTINUE`.
|
|
51
51
|
|
|
52
52
|
### Above-Appetite Remediations
|
|
53
53
|
|
|
@@ -89,7 +89,7 @@ After assessing the risk profile, check whether uncommitted changes represent **
|
|
|
89
89
|
- `packages/*/skills/**/*.bats`
|
|
90
90
|
- `docs/decisions/*.md`
|
|
91
91
|
|
|
92
|
-
AND cumulative risk is **within appetite** (≤ 4), AND at least one completion signal is present:
|
|
92
|
+
AND cumulative risk is **within appetite** (≤ the RISK-POLICY.md appetite threshold, default 4; read it, do NOT hardcode), AND at least one completion signal is present:
|
|
93
93
|
- A problem file diff contains "Fix Released" or a status transition keyword. Under the flat layout this surfaces as a `.known-error.md` / `.closed.md` filename suffix; under the per-state subdir layout it surfaces as a path move into `docs/problems/known-error/` or `docs/problems/closed/`.
|
|
94
94
|
- A SKILL.md was modified alongside a problem file update
|
|
95
95
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
# PreToolUse, nothing to read.)
|
|
25
25
|
#
|
|
26
26
|
# Gate behaviour:
|
|
27
|
-
# 1.
|
|
27
|
+
# 1. (removed P377/RFC-029) — BYPASS_RISK_GATE env override no longer exists; the gate is cleared only by delegating to the reviewer.
|
|
28
28
|
# 2. POLICY_FILE absent → advisory-only mode (permits with systemMessage).
|
|
29
29
|
# 3. Hybrid leak-pattern pre-filter (lib/leak-detect.sh) hard-fails on
|
|
30
30
|
# credentials, prod-URL prefixes, business-context-paired financial figures,
|
|
@@ -97,10 +97,11 @@ EXTERNAL_COMMS_POLICY_FILE="${EXTERNAL_COMMS_POLICY_FILE:-RISK-POLICY.md}"
|
|
|
97
97
|
EXTERNAL_COMMS_LEAK_PREFILTER="${EXTERNAL_COMMS_LEAK_PREFILTER:-yes}"
|
|
98
98
|
EXTERNAL_COMMS_SKIP_SURFACES="${EXTERNAL_COMMS_SKIP_SURFACES:-}"
|
|
99
99
|
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
# P377/RFC-029: the BYPASS_RISK_GATE=1 env override is REMOVED (never
|
|
101
|
+
# authorised). There is no env escape from the external-comms gate — clear it
|
|
102
|
+
# by delegating to the wr-risk-scorer:external-comms reviewer (the gate marks
|
|
103
|
+
# the draft reviewed on a PASS verdict). A genuine gate misfire is recovered
|
|
104
|
+
# per ADR-048 (documented recovery), not an env bypass.
|
|
104
105
|
|
|
105
106
|
INPUT=$(cat)
|
|
106
107
|
|
|
@@ -316,7 +317,7 @@ fi
|
|
|
316
317
|
# EXTERNAL_COMMS_LEAK_PREFILTER=yes (risk) or =no (voice-tone).
|
|
317
318
|
if [ "$EXTERNAL_COMMS_LEAK_PREFILTER" = "yes" ]; then
|
|
318
319
|
if ! leak_detect_scan "$DRAFT"; then
|
|
319
|
-
REASON=$(printf 'BLOCKED (external-comms gate / %s evaluator): %s on %s. Remove the leak before retrying.
|
|
320
|
+
REASON=$(printf 'BLOCKED (external-comms gate / %s evaluator): %s on %s. Remove the leak before retrying. There is no env override (P377/RFC-029 — BYPASS_RISK_GATE removed).' \
|
|
320
321
|
"$EXTERNAL_COMMS_EVALUATOR_ID" "$LEAK_DETECT_REASON" "$SURFACE")
|
|
321
322
|
deny_with_reason "$REASON"
|
|
322
323
|
exit 0
|
|
@@ -386,7 +387,7 @@ fi
|
|
|
386
387
|
# PostToolUse mark hook can derive the canonical marker key locally
|
|
387
388
|
# (sha256(DRAFT + '\n' + SURFACE)). Single fire per gate cycle.
|
|
388
389
|
VERDICT_PREFIX="${EXTERNAL_COMMS_VERDICT_PREFIX:-EXTERNAL_COMMS_${EXTERNAL_COMMS_EVALUATOR_ID^^}}"
|
|
389
|
-
REASON=$(printf 'BLOCKED (external-comms gate / %s evaluator): %s draft has not been reviewed by %s. Delegate to %s (subagent_type: '"'"'%s'"'"') with a prompt that starts with the line `SURFACE: %s` and wraps the draft body verbatim inside `<draft>...</draft>` markers (for the changeset-author surface the body is the changeset summary WITHOUT the leading `---` frontmatter block — the gate strips frontmatter before hashing the marker key). The PostToolUse hook derives the marker key from that structure and marks the draft reviewed when the subagent emits %s_VERDICT: PASS — single fire suffices. Use %s for an interactive walkthrough.
|
|
390
|
+
REASON=$(printf 'BLOCKED (external-comms gate / %s evaluator): %s draft has not been reviewed by %s. Delegate to %s (subagent_type: '"'"'%s'"'"') with a prompt that starts with the line `SURFACE: %s` and wraps the draft body verbatim inside `<draft>...</draft>` markers (for the changeset-author surface the body is the changeset summary WITHOUT the leading `---` frontmatter block — the gate strips frontmatter before hashing the marker key). The PostToolUse hook derives the marker key from that structure and marks the draft reviewed when the subagent emits %s_VERDICT: PASS — single fire suffices. Use %s for an interactive walkthrough. There is no env override (P377/RFC-029 — BYPASS_RISK_GATE removed).' \
|
|
390
391
|
"$EXTERNAL_COMMS_EVALUATOR_ID" "$SURFACE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$SURFACE" "$VERDICT_PREFIX" "$EXTERNAL_COMMS_ASSESS_SKILL")
|
|
391
392
|
deny_with_reason "$REASON"
|
|
392
393
|
exit 0
|
package/hooks/lib/risk-gate.sh
CHANGED
|
@@ -146,7 +146,7 @@ print(('yes' if score > N else 'no') + ' ' + str(N))
|
|
|
146
146
|
if [ "$DENIED" = "yes" ]; then
|
|
147
147
|
RISK_GATE_CATEGORY="threshold"
|
|
148
148
|
RISK_GATE_SCORE="$SCORE"
|
|
149
|
-
RISK_GATE_REASON="${ACTION} risk score ${SCORE}/25 exceeds the project appetite of ${APPETITE}/25 (RISK-POLICY.md). To proceed: (1) split the ${ACTION}, (2) add risk-reducing measures, or (3)
|
|
149
|
+
RISK_GATE_REASON="${ACTION} risk score ${SCORE}/25 exceeds the project appetite of ${APPETITE}/25 (RISK-POLICY.md). Reduce risk or halt — there is no proceed-anyway path (P377/RFC-029). To proceed within appetite: (1) split the ${ACTION}, (2) add risk-reducing measures and re-score, or (3) if this is incident response, delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') with incident context — it scores the change against the live realised-risk baseline (ADR-042 Rule 1b) and clears it via the risk-reducing path if it is net-risk-reducing."
|
|
150
150
|
return 1
|
|
151
151
|
fi
|
|
152
152
|
|
|
@@ -164,8 +164,7 @@ print(('yes' if score > N else 'no') + ' ' + str(N))
|
|
|
164
164
|
# status,conclusion,databaseId,url` for the working branch's most recent
|
|
165
165
|
# CI run.
|
|
166
166
|
#
|
|
167
|
-
# Decision table:
|
|
168
|
-
# - bypass marker present (${RDIR}/ci-bypass-${ACTION}) → allow, consume
|
|
167
|
+
# Decision table (no override — ci-bypass removed P377/RFC-029):
|
|
169
168
|
# - gh failure (auth / timeout / API error) → DENY (fail-CLOSED, per
|
|
170
169
|
# P208 safe-high-fix-risk classifier — a buggy harden must NOT
|
|
171
170
|
# degrade to allow)
|
|
@@ -182,19 +181,16 @@ check_ci_status() {
|
|
|
182
181
|
local ACTION="$2"
|
|
183
182
|
local RDIR
|
|
184
183
|
RDIR=$(_risk_dir "$SESSION_ID")
|
|
185
|
-
local BYPASS_MARKER="${RDIR}/ci-bypass-${ACTION}"
|
|
186
184
|
|
|
187
185
|
CI_GATE_REASON=""
|
|
188
186
|
CI_GATE_CATEGORY=""
|
|
189
187
|
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return 0
|
|
197
|
-
fi
|
|
188
|
+
# P377/RFC-029: the `ci-bypass-${ACTION}` one-shot override is REMOVED
|
|
189
|
+
# (never authorised). The CI-status gate is fail-closed per P208 with NO
|
|
190
|
+
# escape — red/unreadable CI hard-fails; fix the underlying CI / gh failure.
|
|
191
|
+
# The "first push triggers CI" case is handled by the empty-history natural
|
|
192
|
+
# allow below (no marker needed). Live-outage releases use the separate
|
|
193
|
+
# `incident-release` marker (git-push-gate.sh), justified by ADR-042 Rule 1b.
|
|
198
194
|
|
|
199
195
|
# Resolve current branch. If we're not in a git repo or HEAD is
|
|
200
196
|
# detached, skip the CI check (the surrounding push/release gate
|
|
@@ -220,7 +216,7 @@ check_ci_status() {
|
|
|
220
216
|
|
|
221
217
|
if [ -n "${GH_EXIT:-}" ] && [ "$GH_EXIT" != "0" ]; then
|
|
222
218
|
CI_GATE_CATEGORY="gh-error"
|
|
223
|
-
CI_GATE_REASON="CI status check failed (gh exit ${GH_EXIT}: auth / timeout / API error). Fail-closed per P208 safe-high-fix-risk. Fix the underlying gh failure
|
|
219
|
+
CI_GATE_REASON="CI status check failed (gh exit ${GH_EXIT}: auth / timeout / API error). Fail-closed per P208 safe-high-fix-risk. Fix the underlying gh / CI failure before pushing/releasing — there is no override (P377/RFC-029; the ci-bypass marker was removed)."
|
|
224
220
|
return 1
|
|
225
221
|
fi
|
|
226
222
|
|
|
@@ -256,14 +252,14 @@ except Exception:
|
|
|
256
252
|
|
|
257
253
|
if [ "$STATUS" = "PARSE_ERROR" ]; then
|
|
258
254
|
CI_GATE_CATEGORY="gh-error"
|
|
259
|
-
CI_GATE_REASON="CI status check returned unparseable response. Fail-closed per P208 safe-high-fix-risk.
|
|
255
|
+
CI_GATE_REASON="CI status check returned unparseable response. Fail-closed per P208 safe-high-fix-risk. Fix the gh / CI failure before pushing/releasing — there is no override (P377/RFC-029)."
|
|
260
256
|
return 1
|
|
261
257
|
fi
|
|
262
258
|
|
|
263
259
|
case "$STATUS" in
|
|
264
260
|
queued|in_progress|pending|requested|waiting)
|
|
265
261
|
CI_GATE_CATEGORY="pending"
|
|
266
|
-
CI_GATE_REASON="Latest CI run on branch '${BRANCH}' is still in flight (status: ${STATUS}). Wait for it to settle: ${URL}
|
|
262
|
+
CI_GATE_REASON="Latest CI run on branch '${BRANCH}' is still in flight (status: ${STATUS}). Wait for it to settle before pushing/releasing: ${URL} (no override — P377/RFC-029)."
|
|
267
263
|
return 1
|
|
268
264
|
;;
|
|
269
265
|
completed)
|
|
@@ -274,7 +270,7 @@ except Exception:
|
|
|
274
270
|
;;
|
|
275
271
|
failure|cancelled|timed_out|action_required|startup_failure)
|
|
276
272
|
CI_GATE_CATEGORY="red"
|
|
277
|
-
CI_GATE_REASON="Latest CI run on branch '${BRANCH}' concluded ${CONCLUSION}: ${URL}. Fix CI before pushing/releasing
|
|
273
|
+
CI_GATE_REASON="Latest CI run on branch '${BRANCH}' concluded ${CONCLUSION}: ${URL}. Fix CI before pushing/releasing — there is no override (P377/RFC-029; ci-bypass removed). A live-outage restore-service release uses the separate incident-release path."
|
|
278
274
|
return 1
|
|
279
275
|
;;
|
|
280
276
|
*)
|
|
@@ -32,9 +32,17 @@ if [ -n "$SESSION_ID" ] && [ -f "$RELEASE_SCORE_FILE" ]; then
|
|
|
32
32
|
fi
|
|
33
33
|
|
|
34
34
|
# --- Read appetite from RISK-POLICY.md ---
|
|
35
|
-
|
|
35
|
+
# Default 4 (Low) — MUST match the canonical gate parser default in
|
|
36
|
+
# packages/risk-scorer/hooks/lib/risk-gate.sh (P377/RFC-029: a default of 5
|
|
37
|
+
# here was an ADR-065 confirmation violation — the canonical default is 4).
|
|
38
|
+
# The authoritative parser lives in risk-gate.sh; this guidance script mirrors
|
|
39
|
+
# its 3 tolerated phrasings (Threshold: N | exceeds N | N / Low appetite).
|
|
40
|
+
APPETITE="4"
|
|
36
41
|
if [ -f "RISK-POLICY.md" ]; then
|
|
37
|
-
EXTRACTED=$(grep -oP 'Threshold:\s
|
|
42
|
+
EXTRACTED=$(grep -oP '(?:Threshold:\s*|exceeds\s+)\K[0-9]+' RISK-POLICY.md 2>/dev/null | head -1 || echo "")
|
|
43
|
+
if [ -z "$EXTRACTED" ]; then
|
|
44
|
+
EXTRACTED=$(grep -oP '[0-9]+(?=\s*/\s*Low appetite)' RISK-POLICY.md 2>/dev/null | head -1 || echo "")
|
|
45
|
+
fi
|
|
38
46
|
if [ -n "$EXTRACTED" ]; then
|
|
39
47
|
APPETITE="$EXTRACTED"
|
|
40
48
|
fi
|
|
@@ -69,7 +69,8 @@ fi
|
|
|
69
69
|
# TTL is unexpired; consumed on drift or TTL expiry so a genuine risk-
|
|
70
70
|
# profile change forces a fresh wr-risk-scorer:pipeline rescore. Mirrors
|
|
71
71
|
# the clean-marker persist-until-drift precedent (above) — distinct from
|
|
72
|
-
# incident-release
|
|
72
|
+
# incident-release, which remains the one-shot live-outage override
|
|
73
|
+
# (ci-bypass was removed per P377/RFC-029).
|
|
73
74
|
if [ -f "${RDIR}/reducing-commit" ]; then
|
|
74
75
|
NOW=$(date +%s)
|
|
75
76
|
MARK_TIME=$(_mtime "${RDIR}/reducing-commit")
|
|
@@ -129,25 +129,19 @@ _run_check() {
|
|
|
129
129
|
export FAKE_GH_EXIT=1
|
|
130
130
|
result=$(_run_check "push")
|
|
131
131
|
[[ "$result" == DENY:* ]]
|
|
132
|
-
#
|
|
133
|
-
|
|
132
|
+
# P377/RFC-029: ci-bypass removed — the deny states there is no override
|
|
133
|
+
# and does not instruct the user to create/touch a bypass marker.
|
|
134
|
+
[[ "$result" == *"no override"* ]]
|
|
135
|
+
[[ "$result" != *"touch "* ]]
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
@test "
|
|
138
|
+
@test "P377/RFC-029: a ci-bypass marker is IGNORED — red CI still denies (override removed)" {
|
|
137
139
|
: > "$RDIR/ci-bypass-push"
|
|
138
140
|
export FAKE_GH_OUTPUT='[{"status":"completed","conclusion":"failure","databaseId":7,"url":"https://github.com/x/y/actions/runs/7"}]'
|
|
139
141
|
result=$(_run_check "push")
|
|
140
|
-
|
|
141
|
-
# Bypass markers are one-shot — same family as reducing-push / incident-release.
|
|
142
|
-
[ ! -f "$RDIR/ci-bypass-push" ]
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
@test "check_ci_status bypass marker is action-scoped (push marker does not bypass release)" {
|
|
146
|
-
: > "$RDIR/ci-bypass-push"
|
|
147
|
-
export FAKE_GH_OUTPUT='[{"status":"completed","conclusion":"failure","databaseId":8,"url":"https://github.com/x/y/actions/runs/8"}]'
|
|
148
|
-
result=$(_run_check "release")
|
|
142
|
+
# The marker no longer short-circuits; red CI is fail-closed regardless.
|
|
149
143
|
[[ "$result" == DENY:* ]]
|
|
150
|
-
#
|
|
144
|
+
# And the gate does not consume / act on the marker.
|
|
151
145
|
[ -f "$RDIR/ci-bypass-push" ]
|
|
152
146
|
}
|
|
153
147
|
|
|
@@ -107,16 +107,15 @@ run_hook() {
|
|
|
107
107
|
[[ "$output" == *"wr-risk-scorer:external-comms"* ]]
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
# delegation to the external-comms subagent (already named in the deny).
|
|
114
|
-
@test "P173 marker-absent deny clarifies the env override is pre-session" {
|
|
110
|
+
# P377/RFC-029: the BYPASS_RISK_GATE env override was removed. The only
|
|
111
|
+
# clearance path named in the deny is delegation to the external-comms subagent.
|
|
112
|
+
@test "marker-absent deny names the reviewer and offers no env override (P377/RFC-029)" {
|
|
115
113
|
INPUT=$(build_bash_input "gh issue create --title T --body 'we observed a build failure on Node 20'")
|
|
116
114
|
run_hook "$INPUT"
|
|
117
115
|
[ "$status" -eq 0 ]
|
|
118
116
|
[[ "$output" == *"deny"* ]]
|
|
119
|
-
[[ "$output" == *"
|
|
117
|
+
[[ "$output" == *"wr-risk-scorer:external-comms"* ]]
|
|
118
|
+
[[ "$output" != *"BYPASS_RISK_GATE=1"* ]]
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
@test "hard-fail credential pattern (GitHub token) denies immediately with leak reason" {
|
|
@@ -136,11 +135,14 @@ run_hook() {
|
|
|
136
135
|
[[ "$output" == *"AWS"* ]] || [[ "$output" == *"credential"* ]]
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
@test "BYPASS_RISK_GATE=1
|
|
138
|
+
@test "BYPASS_RISK_GATE=1 does NOT bypass the gate (removed P377/RFC-029)" {
|
|
140
139
|
INPUT=$(build_bash_input "gh issue create --title T --body 'we observed a build failure'")
|
|
141
140
|
run bash -c "cd '$TEST_PROJECT_DIR' && BYPASS_RISK_GATE=1 printf '%s' \"\$1\" | BYPASS_RISK_GATE=1 '$HOOK'" _ "$INPUT"
|
|
142
141
|
[ "$status" -eq 0 ]
|
|
143
|
-
|
|
142
|
+
# The env override no longer short-circuits — the gate still denies the
|
|
143
|
+
# unreviewed external-comms draft.
|
|
144
|
+
[[ "$output" == *"deny"* ]]
|
|
145
|
+
[[ "$output" == *"wr-risk-scorer:external-comms"* ]]
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
@test "per-evaluator marker (external-comms-risk-reviewed-<KEY>) allows the call (ADR-028 amended 2026-05-14)" {
|
|
@@ -319,11 +321,15 @@ run_hook() {
|
|
|
319
321
|
[ -z "$output" ]
|
|
320
322
|
}
|
|
321
323
|
|
|
322
|
-
@test "P082: BYPASS_RISK_GATE=1 short-
|
|
323
|
-
|
|
324
|
+
@test "P082→P377/RFC-029: BYPASS_RISK_GATE=1 is inert — does not short-circuit the gate" {
|
|
325
|
+
# gh-issue is an unconditionally-gated external surface (no visibility
|
|
326
|
+
# precondition). Setting BYPASS_RISK_GATE=1 has no effect: the unreviewed
|
|
327
|
+
# draft still denies, identically to the no-env-var case.
|
|
328
|
+
INPUT=$(build_bash_input "gh issue create --title T --body 'we observed a build failure on Node 20'")
|
|
324
329
|
run bash -c "cd '$TEST_PROJECT_DIR' && BYPASS_RISK_GATE=1 printf '%s' \"\$1\" | BYPASS_RISK_GATE=1 '$HOOK'" _ "$INPUT"
|
|
325
330
|
[ "$status" -eq 0 ]
|
|
326
|
-
[
|
|
331
|
+
[[ "$output" == *"deny"* ]]
|
|
332
|
+
[[ "$output" == *"wr-risk-scorer:external-comms"* ]]
|
|
327
333
|
}
|
|
328
334
|
|
|
329
335
|
@test "P082: per-evaluator marker keyed on (body, git-commit-message) permits the call" {
|
package/lib/install-utils.mjs
CHANGED
|
@@ -7,10 +7,12 @@ import { execSync } from "node:child_process";
|
|
|
7
7
|
|
|
8
8
|
const MARKETPLACE_REPO = "windyroad/agent-plugins";
|
|
9
9
|
const MARKETPLACE_NAME = "windyroad";
|
|
10
|
+
const CODEX_MARKETPLACE_PATH = ".";
|
|
11
|
+
const CODEX_MARKETPLACE_NAME = "windyroad-local";
|
|
10
12
|
|
|
11
13
|
let _dryRun = false;
|
|
12
14
|
|
|
13
|
-
export { MARKETPLACE_REPO, MARKETPLACE_NAME };
|
|
15
|
+
export { MARKETPLACE_REPO, MARKETPLACE_NAME, CODEX_MARKETPLACE_PATH, CODEX_MARKETPLACE_NAME };
|
|
14
16
|
|
|
15
17
|
export function setDryRun(value) {
|
|
16
18
|
_dryRun = value;
|
|
@@ -35,16 +37,34 @@ export function run(cmd, label) {
|
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
function runtimesFor(runtime = "claude") {
|
|
41
|
+
if (runtime === "both") return ["claude", "codex"];
|
|
42
|
+
return [runtime];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function checkPrerequisites({ runtime = "claude" } = {}) {
|
|
39
46
|
if (_dryRun) return;
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
for (const currentRuntime of runtimesFor(runtime)) {
|
|
49
|
+
if (currentRuntime === "claude") {
|
|
50
|
+
try {
|
|
51
|
+
execSync("claude --version", { stdio: "pipe" });
|
|
52
|
+
} catch {
|
|
53
|
+
console.error(
|
|
54
|
+
"Error: 'claude' CLI not found. Install Claude Code first:\n https://docs.anthropic.com/en/docs/claude-code\n"
|
|
55
|
+
);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
} else if (currentRuntime === "codex") {
|
|
59
|
+
try {
|
|
60
|
+
execSync("codex --version", { stdio: "pipe" });
|
|
61
|
+
} catch {
|
|
62
|
+
console.error(
|
|
63
|
+
"Error: 'codex' CLI not found. Install Codex CLI first:\n https://developers.openai.com/codex\n"
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
48
68
|
}
|
|
49
69
|
}
|
|
50
70
|
|
|
@@ -55,6 +75,13 @@ export function addMarketplace() {
|
|
|
55
75
|
);
|
|
56
76
|
}
|
|
57
77
|
|
|
78
|
+
export function addCodexMarketplace() {
|
|
79
|
+
return run(
|
|
80
|
+
`codex plugin marketplace add ${CODEX_MARKETPLACE_PATH}`,
|
|
81
|
+
`Codex marketplace: ${CODEX_MARKETPLACE_NAME}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
58
85
|
export function installPlugin(pluginName, { scope = "project" } = {}) {
|
|
59
86
|
return run(
|
|
60
87
|
`claude plugin install ${pluginName}@${MARKETPLACE_NAME} --scope ${scope}`,
|
|
@@ -62,6 +89,13 @@ export function installPlugin(pluginName, { scope = "project" } = {}) {
|
|
|
62
89
|
);
|
|
63
90
|
}
|
|
64
91
|
|
|
92
|
+
export function installCodexPlugin(pluginName) {
|
|
93
|
+
return run(
|
|
94
|
+
`codex plugin add ${pluginName}@${CODEX_MARKETPLACE_NAME}`,
|
|
95
|
+
pluginName
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
65
99
|
export function updatePlugin(pluginName, { scope = "project" } = {}) {
|
|
66
100
|
return run(
|
|
67
101
|
`claude plugin update "${pluginName}@${MARKETPLACE_NAME}" --scope ${scope}`,
|
|
@@ -69,18 +103,36 @@ export function updatePlugin(pluginName, { scope = "project" } = {}) {
|
|
|
69
103
|
);
|
|
70
104
|
}
|
|
71
105
|
|
|
106
|
+
export function updateCodexMarketplace() {
|
|
107
|
+
return run(
|
|
108
|
+
`codex plugin marketplace add ${CODEX_MARKETPLACE_PATH}`,
|
|
109
|
+
`Codex marketplace: ${CODEX_MARKETPLACE_NAME}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
72
113
|
export function uninstallPlugin(pluginName) {
|
|
73
114
|
return run(`claude plugin uninstall ${pluginName}`, `Removing ${pluginName}`);
|
|
74
115
|
}
|
|
75
116
|
|
|
117
|
+
export function uninstallCodexPlugin(pluginName) {
|
|
118
|
+
return run(`codex plugin remove ${pluginName}`, `Removing ${pluginName}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
76
121
|
/**
|
|
77
122
|
* Install a single package: marketplace add + plugin install.
|
|
78
123
|
*/
|
|
79
|
-
export function installPackage(pluginName, { deps = [], scope = "project" } = {}) {
|
|
124
|
+
export function installPackage(pluginName, { deps = [], scope = "project", runtime = "claude" } = {}) {
|
|
80
125
|
console.log(`\nInstalling @windyroad/${pluginName.replace("wr-", "")} (${scope} scope)...\n`);
|
|
81
126
|
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
if (runtime === "claude" || runtime === "both") {
|
|
128
|
+
addMarketplace();
|
|
129
|
+
installPlugin(pluginName, { scope });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (runtime === "codex" || runtime === "both") {
|
|
133
|
+
addCodexMarketplace();
|
|
134
|
+
installCodexPlugin(pluginName);
|
|
135
|
+
}
|
|
84
136
|
|
|
85
137
|
if (deps.length > 0) {
|
|
86
138
|
console.log(`\nNote: This plugin works best with:`);
|
|
@@ -90,34 +142,47 @@ export function installPackage(pluginName, { deps = [], scope = "project" } = {}
|
|
|
90
142
|
}
|
|
91
143
|
|
|
92
144
|
console.log(
|
|
93
|
-
`\nDone! Restart Claude Code to activate.\n`
|
|
145
|
+
`\nDone! Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to activate.\n`
|
|
94
146
|
);
|
|
95
147
|
}
|
|
96
148
|
|
|
97
149
|
/**
|
|
98
150
|
* Update a single package.
|
|
99
151
|
*/
|
|
100
|
-
export function updatePackage(pluginName, { scope = "project" } = {}) {
|
|
152
|
+
export function updatePackage(pluginName, { scope = "project", runtime = "claude" } = {}) {
|
|
101
153
|
console.log(`\nUpdating @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
102
154
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
155
|
+
if (runtime === "claude" || runtime === "both") {
|
|
156
|
+
run(
|
|
157
|
+
`claude plugin marketplace update ${MARKETPLACE_NAME}`,
|
|
158
|
+
"Updating marketplace"
|
|
159
|
+
);
|
|
160
|
+
updatePlugin(pluginName, { scope });
|
|
161
|
+
}
|
|
108
162
|
|
|
109
|
-
|
|
163
|
+
if (runtime === "codex" || runtime === "both") {
|
|
164
|
+
updateCodexMarketplace();
|
|
165
|
+
installCodexPlugin(pluginName);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(`\nDone! Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to apply updates.\n`);
|
|
110
169
|
}
|
|
111
170
|
|
|
112
171
|
/**
|
|
113
172
|
* Uninstall a single package.
|
|
114
173
|
*/
|
|
115
|
-
export function uninstallPackage(pluginName) {
|
|
174
|
+
export function uninstallPackage(pluginName, { runtime = "claude" } = {}) {
|
|
116
175
|
console.log(`\nUninstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
117
176
|
|
|
118
|
-
|
|
177
|
+
if (runtime === "claude" || runtime === "both") {
|
|
178
|
+
uninstallPlugin(pluginName);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (runtime === "codex" || runtime === "both") {
|
|
182
|
+
uninstallCodexPlugin(pluginName);
|
|
183
|
+
}
|
|
119
184
|
|
|
120
|
-
console.log(
|
|
185
|
+
console.log(`\nDone. Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to apply changes.\n`);
|
|
121
186
|
}
|
|
122
187
|
|
|
123
188
|
/**
|
|
@@ -131,6 +196,7 @@ export function parseStandardArgs(argv) {
|
|
|
131
196
|
update: args.includes("--update"),
|
|
132
197
|
dryRun: args.includes("--dry-run"),
|
|
133
198
|
scope: "project",
|
|
199
|
+
runtime: "claude",
|
|
134
200
|
};
|
|
135
201
|
const scopeIdx = args.indexOf("--scope");
|
|
136
202
|
if (scopeIdx !== -1 && args[scopeIdx + 1]) {
|
|
@@ -142,5 +208,15 @@ export function parseStandardArgs(argv) {
|
|
|
142
208
|
process.exit(1);
|
|
143
209
|
}
|
|
144
210
|
}
|
|
211
|
+
const runtimeIdx = args.indexOf("--runtime");
|
|
212
|
+
if (runtimeIdx !== -1 && args[runtimeIdx + 1]) {
|
|
213
|
+
const val = args[runtimeIdx + 1];
|
|
214
|
+
if (["claude", "codex", "both"].includes(val)) {
|
|
215
|
+
flags.runtime = val;
|
|
216
|
+
} else {
|
|
217
|
+
console.error("--runtime requires: claude, codex, or both");
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
145
221
|
return flags;
|
|
146
222
|
}
|
package/package.json
CHANGED
|
@@ -100,7 +100,7 @@ If the verdict is FAIL, do NOT auto-rewrite the draft. Use `AskUserQuestion`:
|
|
|
100
100
|
- options:
|
|
101
101
|
1. `Rewrite the draft and re-review` — return to step 1 with the rewritten body.
|
|
102
102
|
2. `Move to a private channel` — direct the user to a non-public surface (vendor private email, internal Slack, etc.) where the leak does not apply.
|
|
103
|
-
3. `
|
|
103
|
+
3. `Re-review with context` — if the flag is a false positive (e.g. the "client name" is actually the user's own org), give that context to the `wr-risk-scorer:external-comms` reviewer and let it re-judge; a PASS verdict clears the gate via the review marker. There is no `BYPASS_RISK_GATE` env override (removed P377/RFC-029) — the reviewer's judgement, not an env knob, is the clearance path.
|
|
104
104
|
4. `Cancel` — abandon the post.
|
|
105
105
|
|
|
106
106
|
Do not make the decision unilaterally — per ADR-013 Rule 1, all leak/no-leak judgement calls outside the regex pre-filter belong to the user.
|
|
@@ -82,11 +82,6 @@ Present the full risk report to the user. Highlight:
|
|
|
82
82
|
- Any risks above appetite (score ≥ 5 per RISK-POLICY.md)
|
|
83
83
|
- Whether the gate is now pre-satisfied for the current session (i.e., a subsequent `git commit` will reuse the bypass marker without re-triggering the scorer)
|
|
84
84
|
|
|
85
|
-
If any score is above appetite, use `AskUserQuestion` to ask whether the
|
|
86
|
-
- (a) Proceed anyway (bypass)
|
|
87
|
-
- (b) Remediate the identified risks first
|
|
88
|
-
- (c) Cancel
|
|
89
|
-
|
|
90
|
-
Do not make the decision unilaterally — per ADR-013 Rule 1, all above-appetite risk decisions are the user's.
|
|
85
|
+
If any score is above appetite, the action is **framework-mediated, not a user decision** (P377/RFC-029; ADR-044 category-3 explicitly excludes above-appetite). Per **ADR-042 Rule 1**: auto-apply scorer remediations incrementally until residual risk is within appetite, OR halt per ADR-042 Rule 5 if the scorer cannot converge. **MUST NOT commit/push/release above appetite, and MUST NOT use `AskUserQuestion` to ask whether to proceed anyway** — there is no "proceed anyway (bypass)" option; the answer is always reduce-risk-or-halt. The only sanctioned non-blocking path is the risk-reducing path (a genuinely risk-reducing change clears via the `reducing` bypass). Under non-interactive contexts the ADR-013 Rule 6 fail-safe is the terminal fallback: skip the action and report the uncommitted/unreleased state.
|
|
91
86
|
|
|
92
87
|
$ARGUMENTS
|
|
@@ -162,6 +162,11 @@ Write the policy using the structure derived from the agent contract (step 1). T
|
|
|
162
162
|
- The impact levels with project-specific descriptions from step 5
|
|
163
163
|
- The likelihood levels (universal 1-5 scale)
|
|
164
164
|
- The risk matrix (Impact × Likelihood table) and label bands
|
|
165
|
+
- **An `## Authorized Bypass Scenarios` section** (P377/RFC-029) stating exactly which gate bypasses are sanctioned — and that nothing else is. The canonical text to write:
|
|
166
|
+
- **Risk-reducing / risk-neutral changes** proceed via the risk-reducing path (a change that lowers or holds residual risk is not blocked by the gate). The scorer emits `RISK_BYPASS: reducing`; the gate honours a drift-revalidated, TTL-bounded `reducing-*` marker.
|
|
167
|
+
- **Incident response** is NOT a separate carve-out — an active incident is a risk already being realised (Likelihood 5), so an incident-response change is scored against that live baseline and proceeds only if net-risk-reducing (per ADR-042 Rule 1b). The `incident-release` marker exists only to let a net-reducing restore-service release proceed despite red/unreadable CI during a live outage.
|
|
168
|
+
- **Above appetite is never bypassable by a prompt or an env var.** There is no "commit/release anyway" question and no `BYPASS_RISK_GATE` / `ci-bypass` override (removed P377/RFC-029) — above appetite, the action auto-remediates to within appetite or halts (ADR-042 Rule 1).
|
|
169
|
+
- **Default-permitted-when-silent**: a project whose RISK-POLICY.md predates this section still permits the risk-reducing and incident paths above; this section makes the policy the explicit single source of truth and SHOULD be added at the next review.
|
|
165
170
|
- A note that both the risk-scorer agent and problem management process reference this policy
|
|
166
171
|
|
|
167
172
|
## Updating an existing policy
|