@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.
|
@@ -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
|
-
# ---
|
|
43
|
+
# --- P096 Phase 2 — once-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:\
|
|
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