@windyroad/risk-scorer 0.19.16 → 0.20.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/hooks/external-comms-gate.sh +6 -6
- package/hooks/git-push-gate.sh +5 -7
- package/hooks/lib/gate-helpers.sh +8 -0
- package/hooks/lib/risk-gate.sh +18 -5
- package/hooks/risk-policy-enforce-edit.sh +1 -9
- package/hooks/risk-score-commit-gate.sh +6 -6
- package/hooks/risk-score-plan-enforce.sh +1 -9
- package/hooks/risk-scorer-dispatch.sh +27 -4
- package/hooks/wip-risk-gate.sh +1 -9
- package/package.json +1 -1
|
@@ -439,11 +439,6 @@ print(json.dumps({
|
|
|
439
439
|
" "$reason"
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
-
if [ "$BODY_FILE_ERROR" -eq 1 ]; then
|
|
443
|
-
deny_with_reason "BLOCKED (external-comms gate): body-file could not be read unambiguously from the command checkout. Use one readable UTF-8 --body-file path and review its exact contents before retrying."
|
|
444
|
-
exit 0
|
|
445
|
-
fi
|
|
446
|
-
|
|
447
442
|
permit_with_advisory() {
|
|
448
443
|
local msg="$1"
|
|
449
444
|
python3 -c "
|
|
@@ -452,6 +447,11 @@ print(json.dumps({'systemMessage': sys.argv[1]}))
|
|
|
452
447
|
" "$msg"
|
|
453
448
|
}
|
|
454
449
|
|
|
450
|
+
if [ "$BODY_FILE_ERROR" -eq 1 ]; then
|
|
451
|
+
permit_with_advisory "Governance evidence unavailable (external-comms gate): body-file could not be read unambiguously from the command checkout. Use one readable UTF-8 --body-file path and review its exact contents before retrying."
|
|
452
|
+
exit 0
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
455
|
# ---------- Advisory-only fallback when policy file is absent ----------
|
|
456
456
|
if [ ! -f "$EXTERNAL_COMMS_POLICY_FILE" ]; then
|
|
457
457
|
permit_with_advisory "$EXTERNAL_COMMS_POLICY_FILE not found — $EXTERNAL_COMMS_SUBAGENT_TYPE gate is advisory-only on $SURFACE."
|
|
@@ -537,5 +537,5 @@ VERDICT_PREFIX="${EXTERNAL_COMMS_VERDICT_PREFIX:-EXTERNAL_COMMS_${EXTERNAL_COMMS
|
|
|
537
537
|
COMPLETION_GUIDANCE='On Codex, the calling agent waits for the reviewer to finish, then invokes `interrupt_agent` exactly once on that completed target before retrying; the PostToolUse compatibility hook consumes the completed response and persists its structured verdict, with no transcript parsing or nested codex exec. On Claude Code, the calling agent dispatches the reviewer synchronously (`run_in_background: false`) before retrying; the completion hook does not persist the marker for a background reviewer.'
|
|
538
538
|
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 completion hook marks the draft reviewed when the subagent emits %s_VERDICT: PASS — single fire suffices. %s Use %s for an interactive walkthrough. There is no environment-variable override; `BYPASS_RISK_GATE` is unavailable.' \
|
|
539
539
|
"$EXTERNAL_COMMS_EVALUATOR_ID" "$SURFACE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$EXTERNAL_COMMS_SUBAGENT_TYPE" "$SURFACE" "$VERDICT_PREFIX" "$COMPLETION_GUIDANCE" "$EXTERNAL_COMMS_ASSESS_SKILL")
|
|
540
|
-
|
|
540
|
+
permit_with_advisory "Governance evidence unavailable; action permitted. $REASON"
|
|
541
541
|
exit 0
|
package/hooks/git-push-gate.sh
CHANGED
|
@@ -30,7 +30,7 @@ _watchers_own_ci_wait() {
|
|
|
30
30
|
|
|
31
31
|
if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*(git push|npm run (push:watch|merge:watch|release:watch)|npx changeset|npm run changeset|gh pr merge)(\s|$)'; then
|
|
32
32
|
if ! _enter_hook_cwd; then
|
|
33
|
-
|
|
33
|
+
_governance_advisory "Pipeline checkout could not be validated. Run the command from an absolute Git working directory and rescore that checkout."
|
|
34
34
|
exit 0
|
|
35
35
|
fi
|
|
36
36
|
fi
|
|
@@ -63,10 +63,9 @@ if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*npm run (push:watch|merge:watch)(
|
|
|
63
63
|
AGE=$(( NOW - MARK_TIME ))
|
|
64
64
|
TTL_SECONDS="${RISK_TTL:-3600}"
|
|
65
65
|
if [ "$AGE" -lt "$TTL_SECONDS" ] && ! _checkout_matches "${RDIR}/checkout-id"; then
|
|
66
|
-
|
|
67
|
-
exit 0
|
|
66
|
+
_governance_advisory "Push receipt unavailable: this event resolved to a different Git checkout than the valid risk assessment. Retry the same command with an explicit leading \`cd /absolute/path/to/the/assessed-checkout && npm run push:watch\`; the marker was preserved, so do not rescore unless that checkout changed."
|
|
68
67
|
fi
|
|
69
|
-
if [ "$AGE" -lt "$TTL_SECONDS" ] && [ -f "${RDIR}/state-hash" ]; then
|
|
68
|
+
if [ "$AGE" -lt "$TTL_SECONDS" ] && _checkout_matches "${RDIR}/checkout-id" && [ -f "${RDIR}/state-hash" ]; then
|
|
70
69
|
STORED_HASH=$(cat "${RDIR}/state-hash")
|
|
71
70
|
CURRENT_HASH=$("$SCRIPT_DIR/lib/pipeline-state.sh" --hash-inputs 2>/dev/null | _hashcmd | cut -d' ' -f1)
|
|
72
71
|
if [ "$STORED_HASH" = "$CURRENT_HASH" ]; then
|
|
@@ -151,10 +150,9 @@ if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*npm run release:watch(\s|$)'; the
|
|
|
151
150
|
AGE=$(( NOW - MARK_TIME ))
|
|
152
151
|
TTL_SECONDS="${RISK_TTL:-3600}"
|
|
153
152
|
if [ "$AGE" -lt "$TTL_SECONDS" ] && ! _checkout_matches "${RDIR}/checkout-id"; then
|
|
154
|
-
|
|
155
|
-
exit 0
|
|
153
|
+
_governance_advisory "Release receipt unavailable: this event resolved to a different Git checkout than the valid risk assessment. Retry the same command with an explicit leading \`cd /absolute/path/to/the/assessed-checkout && npm run release:watch\`; the marker was preserved, so do not rescore unless that checkout changed."
|
|
156
154
|
fi
|
|
157
|
-
if [ "$AGE" -lt "$TTL_SECONDS" ] && [ -f "${RDIR}/state-hash" ]; then
|
|
155
|
+
if [ "$AGE" -lt "$TTL_SECONDS" ] && _checkout_matches "${RDIR}/checkout-id" && [ -f "${RDIR}/state-hash" ]; then
|
|
158
156
|
STORED_HASH=$(cat "${RDIR}/state-hash")
|
|
159
157
|
CURRENT_HASH=$("$SCRIPT_DIR/lib/pipeline-state.sh" --hash-inputs 2>/dev/null | _hashcmd | cut -d' ' -f1)
|
|
160
158
|
if [ "$STORED_HASH" = "$CURRENT_HASH" ]; then
|
|
@@ -169,6 +169,9 @@ _HOOK_INPUT=""
|
|
|
169
169
|
|
|
170
170
|
_parse_input() {
|
|
171
171
|
_HOOK_INPUT=$(cat)
|
|
172
|
+
if ! printf '%s' "$_HOOK_INPUT" | python3 -c 'import json,sys; assert isinstance(json.load(sys.stdin),dict)' 2>/dev/null; then
|
|
173
|
+
_governance_advisory "Hook input could not be parsed."
|
|
174
|
+
fi
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
_get_tool_name() {
|
|
@@ -381,3 +384,8 @@ _is_doc_file() {
|
|
|
381
384
|
esac
|
|
382
385
|
return 1
|
|
383
386
|
}
|
|
387
|
+
|
|
388
|
+
# Mechanical evaluation failures are advisory; never mint approval receipts.
|
|
389
|
+
_governance_advisory() {
|
|
390
|
+
python3 -c 'import json,sys; print(json.dumps({"systemMessage": "Governance evidence unavailable; action permitted. " + sys.argv[1]}))' "$1" || echo 'Governance evidence unavailable; action permitted.' >&2
|
|
391
|
+
}
|
package/hooks/lib/risk-gate.sh
CHANGED
|
@@ -22,7 +22,7 @@ source "$_RISK_GATE_DIR/gate-helpers.sh"
|
|
|
22
22
|
# If the hash drifted, halt as before.
|
|
23
23
|
# Band C: age ≥ TTL → halt with the existing expired message.
|
|
24
24
|
# Usage: check_risk_gate "$SESSION_ID" "commit"
|
|
25
|
-
|
|
25
|
+
_check_risk_receipt() {
|
|
26
26
|
local SESSION_ID="$1"
|
|
27
27
|
local ACTION="$2"
|
|
28
28
|
local RDIR
|
|
@@ -193,7 +193,7 @@ print(('yes' if score > N else 'no') + ' ' + str(N))
|
|
|
193
193
|
# - conclusion ∈ {success, skipped, neutral} or unknown → allow
|
|
194
194
|
#
|
|
195
195
|
# Usage: check_ci_status "$SESSION_ID" "push" # or "release"
|
|
196
|
-
|
|
196
|
+
_check_ci_evidence() {
|
|
197
197
|
local SESSION_ID="$1"
|
|
198
198
|
local ACTION="$2"
|
|
199
199
|
local RDIR
|
|
@@ -233,7 +233,7 @@ check_ci_status() {
|
|
|
233
233
|
|
|
234
234
|
if [ -n "${GH_EXIT:-}" ] && [ "$GH_EXIT" != "0" ]; then
|
|
235
235
|
CI_GATE_CATEGORY="gh-error"
|
|
236
|
-
CI_GATE_REASON="CI status
|
|
236
|
+
CI_GATE_REASON="CI status unavailable (gh exit ${GH_EXIT}: lookup, auth, timeout or API error). Verify CI through the standard watcher."
|
|
237
237
|
return 1
|
|
238
238
|
fi
|
|
239
239
|
|
|
@@ -269,7 +269,7 @@ except Exception:
|
|
|
269
269
|
|
|
270
270
|
if [ "$STATUS" = "PARSE_ERROR" ]; then
|
|
271
271
|
CI_GATE_CATEGORY="gh-error"
|
|
272
|
-
CI_GATE_REASON="CI status
|
|
272
|
+
CI_GATE_REASON="CI status unavailable: lookup returned an unparseable response. Verify CI through the standard watcher."
|
|
273
273
|
return 1
|
|
274
274
|
fi
|
|
275
275
|
|
|
@@ -312,7 +312,7 @@ except Exception:
|
|
|
312
312
|
esac
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
# Emit
|
|
315
|
+
# Emit denial for successfully evaluated substantive failures.
|
|
316
316
|
risk_gate_deny() {
|
|
317
317
|
local REASON="$1"
|
|
318
318
|
cat <<EOF
|
|
@@ -325,3 +325,16 @@ risk_gate_deny() {
|
|
|
325
325
|
}
|
|
326
326
|
EOF
|
|
327
327
|
}
|
|
328
|
+
|
|
329
|
+
check_risk_gate() {
|
|
330
|
+
if _check_risk_receipt "$@"; then return 0; fi
|
|
331
|
+
if [ "$RISK_GATE_CATEGORY" = threshold ]; then return 1; fi
|
|
332
|
+
_governance_advisory "$RISK_GATE_REASON"
|
|
333
|
+
return 0
|
|
334
|
+
}
|
|
335
|
+
check_ci_status() {
|
|
336
|
+
if _check_ci_evidence "$@"; then return 0; fi
|
|
337
|
+
case "$CI_GATE_CATEGORY" in red|pending) return 1 ;; esac
|
|
338
|
+
_governance_advisory "CI status unavailable. $CI_GATE_REASON"
|
|
339
|
+
return 0
|
|
340
|
+
}
|
|
@@ -40,13 +40,5 @@ if [ -f "$MARKER" ]; then
|
|
|
40
40
|
exit 0
|
|
41
41
|
fi
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
{
|
|
45
|
-
"hookSpecificOutput": {
|
|
46
|
-
"hookEventName": "PreToolUse",
|
|
47
|
-
"permissionDecision": "deny",
|
|
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\"."
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
EOF
|
|
43
|
+
_governance_advisory "Review receipt unavailable; required substantive review remains the agent's responsibility."
|
|
52
44
|
exit 0
|
|
@@ -16,7 +16,7 @@ TOOL_NAME=$(_get_tool_name)
|
|
|
16
16
|
COMMAND=$(_get_command)
|
|
17
17
|
echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*git commit' || exit 0
|
|
18
18
|
if ! _enter_hook_cwd; then
|
|
19
|
-
|
|
19
|
+
_governance_advisory "Commit checkout could not be validated. Run the command from an absolute Git working directory and rescore that checkout."
|
|
20
20
|
exit 0
|
|
21
21
|
fi
|
|
22
22
|
|
|
@@ -43,8 +43,8 @@ SESSION_ID=$(_get_session_id)
|
|
|
43
43
|
# contract). Fallback threshold 14 days when the cadence line is absent
|
|
44
44
|
# or the word unrecognised. P408.
|
|
45
45
|
if [ ! -f "RISK-POLICY.md" ] || [ ! -s "RISK-POLICY.md" ]; then
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
_governance_advisory "Commit blocked: RISK-POLICY.md is missing. Run /risk-policy to create it before committing."
|
|
47
|
+
# Continue evaluating any applicable known score.
|
|
48
48
|
fi
|
|
49
49
|
POLICY_STALE=$(python3 -c "
|
|
50
50
|
from datetime import date
|
|
@@ -74,8 +74,8 @@ except:
|
|
|
74
74
|
print('no')
|
|
75
75
|
" 2>/dev/null || echo "no")
|
|
76
76
|
if [ "$POLICY_STALE" != "no" ]; then
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
_governance_advisory "Commit blocked: RISK-POLICY.md is stale (last reviewed ${POLICY_STALE}). Run /risk-policy to update it before committing."
|
|
78
|
+
# Continue evaluating any applicable known score.
|
|
79
79
|
fi
|
|
80
80
|
|
|
81
81
|
# Clean tree bypass
|
|
@@ -97,7 +97,7 @@ if [ -f "${RDIR}/reducing-commit" ]; then
|
|
|
97
97
|
AGE=$(( NOW - MARK_TIME ))
|
|
98
98
|
TTL_SECONDS="${RISK_TTL:-3600}"
|
|
99
99
|
if [ "$AGE" -lt "$TTL_SECONDS" ] && ! _checkout_matches "${RDIR}/checkout-id"; then
|
|
100
|
-
|
|
100
|
+
_governance_advisory "Commit blocked: this event resolved to a different Git checkout than the valid risk assessment. Retry the same command with an explicit leading \`cd /absolute/path/to/the/assessed-checkout && git commit ...\`; the marker was preserved, so do not rescore unless that checkout changed."
|
|
101
101
|
exit 0
|
|
102
102
|
fi
|
|
103
103
|
if [ "$AGE" -lt "$TTL_SECONDS" ] && [ -f "${RDIR}/state-hash" ]; then
|
|
@@ -19,13 +19,5 @@ if [ -f "$MARKER" ]; then
|
|
|
19
19
|
exit 0
|
|
20
20
|
fi
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
"hookSpecificOutput": {
|
|
25
|
-
"hookEventName": "PreToolUse",
|
|
26
|
-
"permissionDecision": "deny",
|
|
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
|
-
}
|
|
29
|
-
}
|
|
30
|
-
EOF
|
|
22
|
+
_governance_advisory "Review receipt unavailable; required substantive review remains the agent's responsibility."
|
|
31
23
|
exit 0
|
|
@@ -7,13 +7,34 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
7
7
|
EVENT="${1:-}"
|
|
8
8
|
INPUT="$(cat)"
|
|
9
9
|
|
|
10
|
+
ADVISORIES=""
|
|
11
|
+
collect_gate_output() {
|
|
12
|
+
local output="$1" parsed
|
|
13
|
+
[ -n "$output" ] || return 0
|
|
14
|
+
if ! parsed="$(printf '%s' "$output" | jq -cs '
|
|
15
|
+
if any(.[]; .hookSpecificOutput.permissionDecision == "deny")
|
|
16
|
+
then map(select(.hookSpecificOutput.permissionDecision == "deny"))[0]
|
|
17
|
+
else {systemMessage: map(.systemMessage // .hookSpecificOutput.permissionDecisionReason // "Governance evidence unavailable; action permitted.") | join("\n")} end' 2>/dev/null)"; then
|
|
18
|
+
parsed='{"systemMessage":"Governance evidence unavailable: malformed evaluator output; action permitted."}'
|
|
19
|
+
fi
|
|
20
|
+
if printf '%s' "$parsed" | jq -e '.hookSpecificOutput.permissionDecision == "deny"' >/dev/null 2>&1; then
|
|
21
|
+
printf '%s\n' "$parsed"
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
ADVISORIES="$ADVISORIES
|
|
25
|
+
$parsed"
|
|
26
|
+
}
|
|
27
|
+
flush_advisories() {
|
|
28
|
+
[ -n "$ADVISORIES" ] || return 0
|
|
29
|
+
printf '%s' "$ADVISORIES" | jq -s '{systemMessage: map(.systemMessage // .hookSpecificOutput.permissionDecisionReason // "Governance evidence unavailable; action permitted.") | join("\n")}'
|
|
30
|
+
}
|
|
10
31
|
run_hook() {
|
|
11
32
|
local output
|
|
12
|
-
output="$(printf '%s' "$INPUT" | "$SCRIPT_DIR/$1")"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
exit 0
|
|
33
|
+
if ! output="$(printf '%s' "$INPUT" | "$SCRIPT_DIR/$1" 2>/dev/null)"; then
|
|
34
|
+
collect_gate_output '{"systemMessage":"Governance evaluator unavailable; action permitted. A crashed hook output was discarded."}'
|
|
35
|
+
return 0
|
|
16
36
|
fi
|
|
37
|
+
collect_gate_output "$output"
|
|
17
38
|
}
|
|
18
39
|
|
|
19
40
|
tool_name() {
|
|
@@ -108,3 +129,5 @@ if messages:
|
|
|
108
129
|
exit 0
|
|
109
130
|
;;
|
|
110
131
|
esac
|
|
132
|
+
|
|
133
|
+
flush_advisories
|
package/hooks/wip-risk-gate.sh
CHANGED
|
@@ -32,13 +32,5 @@ if [ -f "$MARKER" ]; then
|
|
|
32
32
|
exit 0
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
"hookSpecificOutput": {
|
|
38
|
-
"hookEventName": "PreToolUse",
|
|
39
|
-
"permissionDecision": "deny",
|
|
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
|
-
}
|
|
42
|
-
}
|
|
43
|
-
EOF
|
|
35
|
+
_governance_advisory "Review receipt unavailable; required substantive review remains the agent's responsibility."
|
|
44
36
|
exit 0
|
package/package.json
CHANGED