@windyroad/risk-scorer 0.4.1-preview.215 → 0.4.2-preview.217

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "wr-risk-scorer",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Pipeline risk scoring, commit/push/release gates for Claude Code"
5
5
  }
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # Shared session-announcement marker helpers (P095 / ADR-038).
3
+ #
4
+ # Used by UserPromptSubmit hooks to gate verbose MANDATORY instruction
5
+ # prose behind a once-per-session check. First prompt of a session emits
6
+ # the full block AND calls mark_announced; subsequent prompts see the
7
+ # marker via has_announced and emit only a terse reminder.
8
+ #
9
+ # Why no TTL or drift check (unlike review-gate.sh): announcement is
10
+ # bookkeeping for prose verbosity, not enforcement. PreToolUse gates
11
+ # still block unauthorised edits regardless of announcement state; the
12
+ # delegated agent re-reads policy when it runs. Extending the marker's
13
+ # lifetime across policy changes mid-session is safe — the gate, not
14
+ # the announcement, is load-bearing.
15
+ #
16
+ # Marker path convention: /tmp/${SYSTEM}-announced-${SESSION_ID}
17
+ # (mirrors the /tmp/${SYSTEM}-reviewed-${SESSION_ID} convention from
18
+ # style-guide/voice-tone/risk-scorer review-gate.sh; the -announced-
19
+ # suffix distinguishes announcement markers from clearance markers).
20
+ #
21
+ # Empty SESSION_ID fallback: has_announced returns 1 (not announced,
22
+ # full block emits) and mark_announced is a no-op (no file written).
23
+ # This covers manual hook invocation, test harnesses, and any rare
24
+ # case where Claude Code does not pass a session_id on stdin.
25
+
26
+ # Returns 0 if the hook for SYSTEM has already announced in SESSION_ID,
27
+ # 1 otherwise. Empty SESSION_ID => returns 1 (never announced).
28
+ #
29
+ # Usage: has_announced "architect" "$SESSION_ID"
30
+ has_announced() {
31
+ local SYSTEM="$1"
32
+ local SESSION_ID="$2"
33
+ [ -n "$SESSION_ID" ] || return 1
34
+ [ -f "/tmp/${SYSTEM}-announced-${SESSION_ID}" ]
35
+ }
36
+
37
+ # Writes the announcement marker for SYSTEM in SESSION_ID. Empty
38
+ # SESSION_ID => no-op. Safe to call more than once per session.
39
+ #
40
+ # Usage: mark_announced "architect" "$SESSION_ID"
41
+ mark_announced() {
42
+ local SYSTEM="$1"
43
+ local SESSION_ID="$2"
44
+ [ -n "$SESSION_ID" ] || return 0
45
+ : > "/tmp/${SYSTEM}-announced-${SESSION_ID}"
46
+ }
@@ -7,6 +7,7 @@ set -euo pipefail
7
7
 
8
8
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
9
  source "$SCRIPT_DIR/lib/gate-helpers.sh"
10
+ source "$SCRIPT_DIR/lib/session-marker.sh"
10
11
  _enable_err_trap
11
12
 
12
13
  _parse_input
@@ -39,13 +40,32 @@ if [ -f "RISK-POLICY.md" ]; then
39
40
  fi
40
41
  fi
41
42
 
42
- # --- Emit guidance (allowadvisory only, not a gate) ---
43
+ # --- P096 Phase 2once-per-session gating (ADR-038 progressive disclosure) ---
44
+ # First EnterPlanMode of a session emits the full advisory body; subsequent
45
+ # entries within the same session emit a terse reminder (≤150 bytes per the
46
+ # ADR-038 budget). Pipeline state and appetite are unchanged across plan-mode
47
+ # entries within one session, so re-emitting full prose is repetition.
48
+ if has_announced "risk-scorer-plan-guidance" "$SESSION_ID"; then
49
+ cat <<EOF
50
+ {
51
+ "hookSpecificOutput": {
52
+ "hookEventName": "PreToolUse",
53
+ "permissionDecision": "allow",
54
+ "systemMessage": "MANDATORY release-risk gate active (RISK-POLICY.md present). Release risk: ${RELEASE_SCORE}; appetite: ${APPETITE}. ExitPlanMode will FAIL plans projected above appetite. See first-EnterPlanMode emission for full guidance."
55
+ }
56
+ }
57
+ EOF
58
+ exit 0
59
+ fi
60
+
61
+ # --- First emission: full advisory (compressed per audit recommendation) ---
62
+ mark_announced "risk-scorer-plan-guidance" "$SESSION_ID"
43
63
  cat <<EOF
44
64
  {
45
65
  "hookSpecificOutput": {
46
66
  "hookEventName": "PreToolUse",
47
67
  "permissionDecision": "allow",
48
- "systemMessage": "RELEASE RISK GUIDANCE FOR PLANNING:\nThe unreleased queue currently contains:\n${UNRELEASED_SUMMARY}\n\nCurrent release risk score: ${RELEASE_SCORE}.\nRisk appetite threshold: ${APPETITE} (Medium).\n\nYour plan MUST account for projected release risk. If the plan's proposed changes would push projected release risk above appetite when combined with the existing unreleased queue, the plan MUST include one or more of:\n- Release the current unreleased queue first (before implementing the plan)\n- Split the plan into smaller batches that keep projected release risk within appetite\n- Include specific risk-reducing steps (additional tests, rollback procedures)\n\nThe risk-scorer will assess projected release risk at ExitPlanMode and FAIL plans that exceed appetite without a release strategy."
68
+ "systemMessage": "RELEASE RISK GUIDANCE FOR PLANNING:\nUnreleased queue:\n${UNRELEASED_SUMMARY}\n\nRelease risk: ${RELEASE_SCORE}. Appetite threshold: ${APPETITE} (Medium).\n\nIf projected release risk would exceed appetite, the plan MUST include a release strategy (release queue first, split into smaller batches, or risk-reducing steps). See RISK-POLICY.md for option details. ExitPlanMode runs the risk-scorer and FAILS plans above appetite without a strategy."
49
69
  }
50
70
  }
51
71
  EOF
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # P096 Phase 2: plan-risk-guidance.sh applies once-per-session gating
4
+ # (ADR-038 progressive disclosure pattern) so the advisory body emits
5
+ # in full only on the first EnterPlanMode of a session. Subsequent
6
+ # EnterPlanMode events within the same session emit a terse reminder
7
+ # (≤150 bytes payload after the systemMessage prefix).
8
+ #
9
+ # Reuses the shared session-marker.sh helper synced from
10
+ # packages/shared/hooks/lib/session-marker.sh.
11
+
12
+ setup() {
13
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
14
+ HOOK="$REPO_ROOT/packages/risk-scorer/hooks/plan-risk-guidance.sh"
15
+
16
+ WORKDIR="$(mktemp -d)"
17
+ # Minimal RISK-POLICY.md so the appetite extraction has something to read.
18
+ cat > "$WORKDIR/RISK-POLICY.md" <<'POLICY'
19
+ # Risk Policy
20
+ Threshold: 4
21
+ POLICY
22
+
23
+ SID="plan-risk-guidance-test-$$-$RANDOM"
24
+ }
25
+
26
+ teardown() {
27
+ rm -f "/tmp/risk-scorer-plan-guidance-announced-${SID}"
28
+ rm -f "/tmp/risk-scorer-plan-guidance-announced-${SID}-alt"
29
+ rm -rf "$WORKDIR"
30
+ }
31
+
32
+ run_hook() {
33
+ local sid="$1"
34
+ (cd "$WORKDIR" && \
35
+ echo "{\"session_id\":\"$sid\",\"tool_name\":\"EnterPlanMode\"}" | \
36
+ bash "$HOOK")
37
+ }
38
+
39
+ @test "plan-risk-guidance: first invocation emits the full RELEASE RISK GUIDANCE body" {
40
+ run run_hook "$SID"
41
+ [ "$status" -eq 0 ]
42
+ [[ "$output" == *"RELEASE RISK GUIDANCE FOR PLANNING"* ]]
43
+ [[ "$output" == *"Release risk:"* ]]
44
+ [[ "$output" == *"Appetite threshold"* ]]
45
+ [[ "$output" == *"release strategy"* ]] || [[ "$output" == *"release queue first"* ]]
46
+ }
47
+
48
+ @test "plan-risk-guidance: first invocation writes the announcement marker" {
49
+ run_hook "$SID" >/dev/null
50
+ [ -f "/tmp/risk-scorer-plan-guidance-announced-${SID}" ]
51
+ }
52
+
53
+ @test "plan-risk-guidance: second invocation in the same session emits a terse reminder" {
54
+ run_hook "$SID" >/dev/null
55
+ run run_hook "$SID"
56
+ [ "$status" -eq 0 ]
57
+ # Terse reminder MUST carry imperative signal word + gate name + cross-ref.
58
+ [[ "$output" == *"MANDATORY"* ]] || [[ "$output" == *"REQUIRED"* ]] || [[ "$output" == *"NON-OPTIONAL"* ]]
59
+ [[ "$output" == *"release-risk gate"* ]] || [[ "$output" == *"risk"* ]]
60
+ # Must NOT re-emit the full prose (release-strategy listing, projected-risk paragraph).
61
+ [[ "$output" != *"RELEASE RISK GUIDANCE FOR PLANNING"* ]]
62
+ }
63
+
64
+ @test "plan-risk-guidance: second invocation reminder payload is ≤300 bytes" {
65
+ run_hook "$SID" >/dev/null
66
+ run run_hook "$SID"
67
+ # Total response is JSON wrapper + systemMessage; reminder body must be
68
+ # short enough that the full response fits well under the ADR-038 budget.
69
+ [ "${#output}" -lt 600 ]
70
+ }
71
+
72
+ @test "plan-risk-guidance: different session_id re-emits the full body" {
73
+ run_hook "$SID" >/dev/null
74
+ local SID2="${SID}-alt"
75
+ run run_hook "$SID2"
76
+ [[ "$output" == *"RELEASE RISK GUIDANCE FOR PLANNING"* ]]
77
+ rm -f "/tmp/risk-scorer-plan-guidance-announced-${SID2}"
78
+ }
79
+
80
+ @test "plan-risk-guidance: empty session_id emits the full body and writes no marker" {
81
+ run run_hook ""
82
+ [[ "$output" == *"RELEASE RISK GUIDANCE FOR PLANNING"* ]]
83
+ # Empty SESSION_ID fallback per shared session-marker contract.
84
+ [ ! -f "/tmp/risk-scorer-plan-guidance-announced-" ]
85
+ }
86
+
87
+ @test "plan-risk-guidance: emits valid JSON with permissionDecision allow on both first and subsequent invocations" {
88
+ run run_hook "$SID"
89
+ [[ "$output" == *'"permissionDecision": "allow"'* ]]
90
+ [[ "$output" == *'"hookEventName": "PreToolUse"'* ]]
91
+
92
+ run run_hook "$SID"
93
+ [[ "$output" == *'"permissionDecision": "allow"'* ]]
94
+ [[ "$output" == *'"hookEventName": "PreToolUse"'* ]]
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.4.1-preview.215",
3
+ "version": "0.4.2-preview.217",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "bin": {
6
6
  "windyroad-risk-scorer": "./bin/install.mjs"