@windyroad/risk-scorer 0.19.16 → 0.20.0-preview.1304

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.
@@ -310,5 +310,5 @@
310
310
  }
311
311
  },
312
312
  "name": "wr-risk-scorer",
313
- "version": "0.19.16"
313
+ "version": "0.20.0"
314
314
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wr-risk-scorer",
3
- "version": "0.19.16",
3
+ "version": "0.20.0",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "author": {
6
6
  "name": "Windy Road Technology",
@@ -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
- deny_with_reason "$REASON"
540
+ permit_with_advisory "Governance evidence unavailable; action permitted. $REASON"
541
541
  exit 0
@@ -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
- risk_gate_deny "Pipeline action blocked: the command checkout could not be validated. Run the command from an absolute Git working directory and rescore that checkout."
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
- risk_gate_deny "Push 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 && npm run push:watch\`; the marker was preserved, so do not rescore unless that checkout changed."
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
- risk_gate_deny "Release 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 && npm run release:watch\`; the marker was preserved, so do not rescore unless that checkout changed."
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
+ }
@@ -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
- check_risk_gate() {
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
- check_ci_status() {
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 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)."
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 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)."
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 fail-closed deny JSON for PreToolUse hooks.
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
- cat <<'EOF'
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
- risk_gate_deny "Commit blocked: the command checkout could not be validated. Run the command from an absolute Git working directory and rescore that checkout."
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
- risk_gate_deny "Commit blocked: RISK-POLICY.md is missing. Run /risk-policy to create it before committing."
47
- exit 0
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
- risk_gate_deny "Commit blocked: RISK-POLICY.md is stale (last reviewed ${POLICY_STALE}). Run /risk-policy to update it before committing."
78
- exit 0
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
- risk_gate_deny "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."
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
- cat <<'EOF'
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
- if [ -n "$output" ]; then
14
- printf '%s\n' "$output"
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
@@ -32,13 +32,5 @@ if [ -f "$MARKER" ]; then
32
32
  exit 0
33
33
  fi
34
34
 
35
- cat <<'EOF'
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.19.16",
3
+ "version": "0.20.0-preview.1304",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "scripts": {
6
6
  "prepack": "node scripts/sync-codex-skills.mjs --pack",