@windyroad/risk-scorer 0.17.4 → 0.18.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/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +13 -2
- package/bin/install.mjs +42 -8
- package/hooks/codex-agent-completion.mjs +82 -0
- package/hooks/hooks.json +5 -2
- package/hooks/risk-scorer-dispatch.sh +3 -0
- package/hooks/risk-scorer-scaffold-nudge.sh +15 -4
- package/package.json +5 -2
- package/scripts/codex-agents.mjs +208 -0
- package/scripts/sync-codex-skills.mjs +8 -3
- package/skills/assess-external-comms/SKILL.md +5 -4
- package/skills/assess-inbound-report/SKILL.md +5 -4
- package/skills/assess-release/SKILL.md +5 -4
- package/skills/assess-wip/SKILL.md +5 -4
- package/skills/bootstrap-catalog/SKILL.md +5 -4
- package/skills/create-risk/SKILL.md +5 -4
- package/skills/external-comms/SKILL.md +5 -4
- package/skills/pipeline/SKILL.md +5 -4
- package/skills/update-policy/SKILL.md +6 -5
- package/skills/wip/SKILL.md +5 -4
- package/agents/test/inbound-report-contract.bats +0 -225
- package/agents/test/risk-scorer-above-appetite-stop.bats +0 -80
- package/agents/test/risk-scorer-catalog-consumption.bats +0 -138
- package/agents/test/risk-scorer-commit-verdict.bats +0 -66
- package/agents/test/risk-scorer-monitoring-not-a-control.bats +0 -76
- package/agents/test/risk-scorer-reducing-bypass-criteria.bats +0 -62
- package/agents/test/risk-scorer-register-hint.bats +0 -111
- package/agents/test/risk-scorer-structured-remediations.bats +0 -123
- package/agents/test/risk-scorer-user-stated-preconditions.bats +0 -89
- package/hooks/test/ci-status-gate.bats +0 -234
- package/hooks/test/external-comms-gate.bats +0 -528
- package/hooks/test/external-comms-key-substance.bats +0 -74
- package/hooks/test/git-push-gate.bats +0 -82
- package/hooks/test/pipeline-state-changeset-partition.bats +0 -181
- package/hooks/test/pipeline-state-hash.bats +0 -195
- package/hooks/test/plan-risk-guidance-once-per-session.bats +0 -95
- package/hooks/test/reducing-marker-persistence.bats +0 -236
- package/hooks/test/risk-gate.bats +0 -330
- package/hooks/test/risk-policy-project-root.bats +0 -20
- package/hooks/test/risk-score-commit-gate-adr-031-bypass.bats +0 -98
- package/hooks/test/risk-score-commit-gate-cadence-staleness.bats +0 -105
- package/hooks/test/risk-score-mark-external-comms-prompt-parse.bats +0 -94
- package/hooks/test/risk-score-mark-register-queue.bats +0 -253
- package/hooks/test/risk-score-mark.bats +0 -112
- package/hooks/test/risk-scorer-hook-dispatch.bats +0 -77
- package/hooks/test/risk-scorer-no-stop-hook.bats +0 -19
- package/hooks/test/risk-scorer-scaffold-nudge.bats +0 -132
- package/hooks/test/slide-marker-on-subprocess-return.bats +0 -118
- package/hooks/test/wip-risk-mark.bats +0 -64
- package/scripts/test/drain-register-queue.bats +0 -359
- package/scripts/test/evaluate-graduation.bats +0 -533
- package/scripts/test/extract-risks-from-reports.bats +0 -366
- package/scripts/test/restage-commit.bats +0 -137
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# Tests for the RISK_REGISTER_HINT queue-write extension to risk-score-mark.sh
|
|
4
|
-
# (ADR-056 Phase 2a). Verifies the PostToolUse:Agent hook parses the
|
|
5
|
-
# RISK_REGISTER_HINT block from pipeline-agent output and appends one JSONL
|
|
6
|
-
# line per valid bullet to .afk-run-state/risk-register-queue.jsonl.
|
|
7
|
-
#
|
|
8
|
-
# Behavioural fixtures per ADR-052: each test pipes a mock agent output to
|
|
9
|
-
# the hook and asserts on side-effects (queue file content / shape / silence).
|
|
10
|
-
# No structural grep against source.
|
|
11
|
-
#
|
|
12
|
-
# Cross-references:
|
|
13
|
-
# ADR-056: docs/decisions/056-risk-register-back-channel-write-contract.proposed.md
|
|
14
|
-
# ADR-045: hook injection budget Pattern 2 (silent on stdout)
|
|
15
|
-
# P033: docs/problems/033-no-persistent-risk-register.known-error.md (driver)
|
|
16
|
-
# P110: pipeline back-channel hint (consumer of this contract)
|
|
17
|
-
|
|
18
|
-
setup() {
|
|
19
|
-
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
20
|
-
HOOK="$SCRIPT_DIR/risk-score-mark.sh"
|
|
21
|
-
ORIG_DIR="$PWD"
|
|
22
|
-
TEST_DIR=$(mktemp -d)
|
|
23
|
-
cd "$TEST_DIR"
|
|
24
|
-
TMPDIR="$TEST_DIR/tmp"
|
|
25
|
-
export TMPDIR
|
|
26
|
-
mkdir -p "$TMPDIR"
|
|
27
|
-
SESSION_ID="test-session-$$"
|
|
28
|
-
RDIR="$TMPDIR/claude-risk-${SESSION_ID}"
|
|
29
|
-
QUEUE_FILE="$TEST_DIR/.afk-run-state/risk-register-queue.jsonl"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
teardown() {
|
|
33
|
-
cd "$ORIG_DIR"
|
|
34
|
-
rm -rf "$TEST_DIR"
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
# Build mock PostToolUse:Agent JSON envelope and pipe to the hook.
|
|
38
|
-
run_hook() {
|
|
39
|
-
local subagent="$1"
|
|
40
|
-
local agent_output="$2"
|
|
41
|
-
python3 -c "
|
|
42
|
-
import json, sys
|
|
43
|
-
print(json.dumps({
|
|
44
|
-
'tool_name': 'Agent',
|
|
45
|
-
'session_id': '${SESSION_ID}',
|
|
46
|
-
'tool_input': {'subagent_type': '${subagent}'},
|
|
47
|
-
'tool_response': {'content': [{'type': 'text', 'text': sys.stdin.read()}]}
|
|
48
|
-
}))" <<<"$agent_output" | bash "$HOOK"
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
# Capture hook stdout (separate from filesystem side-effects).
|
|
52
|
-
run_hook_capture_stdout() {
|
|
53
|
-
local subagent="$1"
|
|
54
|
-
local agent_output="$2"
|
|
55
|
-
python3 -c "
|
|
56
|
-
import json, sys
|
|
57
|
-
print(json.dumps({
|
|
58
|
-
'tool_name': 'Agent',
|
|
59
|
-
'session_id': '${SESSION_ID}',
|
|
60
|
-
'tool_input': {'subagent_type': '${subagent}'},
|
|
61
|
-
'tool_response': {'content': [{'type': 'text', 'text': sys.stdin.read()}]}
|
|
62
|
-
}))" <<<"$agent_output" | bash "$HOOK"
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
# ---------------------------------------------------------------------------
|
|
66
|
-
# 3-column (preferred) parse path
|
|
67
|
-
# ---------------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
@test "3-col hint with one above-appetite bullet → one JSONL line, slug_source=agent" {
|
|
70
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=12 push=8 release=4
|
|
71
|
-
|
|
72
|
-
RISK_REGISTER_HINT:
|
|
73
|
-
- above-appetite-residual | cumulative-residual-commit-layer-above-appetite | Cumulative residual reached 12/25 due to mass-edit across 17 files."
|
|
74
|
-
[ -f "$QUEUE_FILE" ]
|
|
75
|
-
LINE_COUNT=$(wc -l < "$QUEUE_FILE")
|
|
76
|
-
[ "$LINE_COUNT" -eq 1 ]
|
|
77
|
-
REASON=$(python3 -c "import json,sys; print(json.loads(sys.stdin.readline())['reason_tag'])" < "$QUEUE_FILE")
|
|
78
|
-
SLUG=$(python3 -c "import json,sys; print(json.loads(sys.stdin.readline())['risk_slug'])" < "$QUEUE_FILE")
|
|
79
|
-
SOURCE=$(python3 -c "import json,sys; print(json.loads(sys.stdin.readline())['slug_source'])" < "$QUEUE_FILE")
|
|
80
|
-
PREFILL=$(python3 -c "import json,sys; print(json.loads(sys.stdin.readline())['prefill'])" < "$QUEUE_FILE")
|
|
81
|
-
[ "$REASON" = "above-appetite-residual" ]
|
|
82
|
-
[ "$SLUG" = "cumulative-residual-commit-layer-above-appetite" ]
|
|
83
|
-
[ "$SOURCE" = "agent" ]
|
|
84
|
-
[[ "$PREFILL" == *"mass-edit across 17 files"* ]]
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
@test "3-col hint with three bullets → three JSONL lines in order, all slug_source=agent" {
|
|
88
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=10 push=8 release=5
|
|
89
|
-
|
|
90
|
-
RISK_REGISTER_HINT:
|
|
91
|
-
- above-appetite-residual | cumulative-residual-above-appetite | Above-appetite residual.
|
|
92
|
-
- confidentiality-disclosure | revenue-figures-leaked | Revenue figures in changeset.
|
|
93
|
-
- user-stated-precondition | paired-capability-unmet | Pair B not yet shipped."
|
|
94
|
-
LINE_COUNT=$(wc -l < "$QUEUE_FILE")
|
|
95
|
-
[ "$LINE_COUNT" -eq 3 ]
|
|
96
|
-
TAGS=$(python3 -c "
|
|
97
|
-
import json
|
|
98
|
-
with open('$QUEUE_FILE') as f:
|
|
99
|
-
for line in f:
|
|
100
|
-
print(json.loads(line)['reason_tag'])
|
|
101
|
-
")
|
|
102
|
-
EXPECTED="above-appetite-residual
|
|
103
|
-
confidentiality-disclosure
|
|
104
|
-
user-stated-precondition"
|
|
105
|
-
[ "$TAGS" = "$EXPECTED" ]
|
|
106
|
-
ALL_AGENT=$(python3 -c "
|
|
107
|
-
import json
|
|
108
|
-
with open('$QUEUE_FILE') as f:
|
|
109
|
-
src = [json.loads(line)['slug_source'] for line in f]
|
|
110
|
-
print(all(s == 'agent' for s in src))
|
|
111
|
-
")
|
|
112
|
-
[ "$ALL_AGENT" = "True" ]
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
@test "3-col hint includes report_path matching the just-written .risk-reports file" {
|
|
116
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=10 push=5 release=2
|
|
117
|
-
|
|
118
|
-
RISK_REGISTER_HINT:
|
|
119
|
-
- above-appetite-residual | example-slug | Example."
|
|
120
|
-
REPORT_PATH=$(python3 -c "import json,sys; print(json.loads(sys.stdin.readline())['report_path'])" < "$QUEUE_FILE")
|
|
121
|
-
[[ "$REPORT_PATH" == .risk-reports/*-commit.md ]]
|
|
122
|
-
[ -f "$REPORT_PATH" ]
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
# ---------------------------------------------------------------------------
|
|
126
|
-
# 2-column legacy parse path (backward compatibility)
|
|
127
|
-
# ---------------------------------------------------------------------------
|
|
128
|
-
|
|
129
|
-
@test "2-col legacy hint → JSONL line with derived slug, slug_source=derived" {
|
|
130
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=12 push=8 release=4
|
|
131
|
-
|
|
132
|
-
RISK_REGISTER_HINT:
|
|
133
|
-
- above-appetite-residual | Cumulative residual risk for commit layer."
|
|
134
|
-
[ -f "$QUEUE_FILE" ]
|
|
135
|
-
LINE_COUNT=$(wc -l < "$QUEUE_FILE")
|
|
136
|
-
[ "$LINE_COUNT" -eq 1 ]
|
|
137
|
-
SOURCE=$(python3 -c "import json,sys; print(json.loads(sys.stdin.readline())['slug_source'])" < "$QUEUE_FILE")
|
|
138
|
-
SLUG=$(python3 -c "import json,sys; print(json.loads(sys.stdin.readline())['risk_slug'])" < "$QUEUE_FILE")
|
|
139
|
-
[ "$SOURCE" = "derived" ]
|
|
140
|
-
# Derived slug starts with reason-tag prefix
|
|
141
|
-
[[ "$SLUG" == above-appetite-residual-* ]]
|
|
142
|
-
# Derived slug is filename-safe (lowercase, kebab, no spaces)
|
|
143
|
-
[[ "$SLUG" =~ ^[a-z0-9-]+$ ]]
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
@test "2-col legacy hint: same prefill produces same derived slug across runs" {
|
|
147
|
-
PREFILL_TEXT="Cumulative residual risk for commit layer."
|
|
148
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=12 push=8 release=4
|
|
149
|
-
|
|
150
|
-
RISK_REGISTER_HINT:
|
|
151
|
-
- above-appetite-residual | $PREFILL_TEXT"
|
|
152
|
-
SLUG_1=$(python3 -c "import json,sys; print(json.loads(sys.stdin.readline())['risk_slug'])" < "$QUEUE_FILE")
|
|
153
|
-
rm -f "$QUEUE_FILE"
|
|
154
|
-
sleep 1 # ensure different timestamp on second .risk-reports write
|
|
155
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=12 push=8 release=4
|
|
156
|
-
|
|
157
|
-
RISK_REGISTER_HINT:
|
|
158
|
-
- above-appetite-residual | $PREFILL_TEXT"
|
|
159
|
-
SLUG_2=$(python3 -c "import json,sys; print(json.loads(sys.stdin.readline())['risk_slug'])" < "$QUEUE_FILE")
|
|
160
|
-
[ "$SLUG_1" = "$SLUG_2" ]
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
@test "Mixed 3-col and 2-col bullets in same block → both shapes appended with correct slug_source" {
|
|
164
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=10 push=5 release=2
|
|
165
|
-
|
|
166
|
-
RISK_REGISTER_HINT:
|
|
167
|
-
- above-appetite-residual | preferred-slug | First risk.
|
|
168
|
-
- confidentiality-disclosure | Second risk in legacy shape."
|
|
169
|
-
LINE_COUNT=$(wc -l < "$QUEUE_FILE")
|
|
170
|
-
[ "$LINE_COUNT" -eq 2 ]
|
|
171
|
-
SOURCES=$(python3 -c "
|
|
172
|
-
import json
|
|
173
|
-
with open('$QUEUE_FILE') as f:
|
|
174
|
-
for line in f:
|
|
175
|
-
print(json.loads(line)['slug_source'])
|
|
176
|
-
")
|
|
177
|
-
EXPECTED="agent
|
|
178
|
-
derived"
|
|
179
|
-
[ "$SOURCES" = "$EXPECTED" ]
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
# ---------------------------------------------------------------------------
|
|
183
|
-
# Silence + no-op paths
|
|
184
|
-
# ---------------------------------------------------------------------------
|
|
185
|
-
|
|
186
|
-
@test "no hint emitted (silent-pass) → queue file is not created" {
|
|
187
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=2 push=2 release=1
|
|
188
|
-
RISK_BYPASS: reducing"
|
|
189
|
-
[ ! -f "$QUEUE_FILE" ]
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
@test "empty agent output → no queue file, no crash" {
|
|
193
|
-
run_hook "wr-risk-scorer:pipeline" ""
|
|
194
|
-
[ ! -f "$QUEUE_FILE" ]
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
@test "malformed hint bullet (invalid reason-tag) is skipped; valid bullet appended" {
|
|
198
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=10 push=5 release=2
|
|
199
|
-
|
|
200
|
-
RISK_REGISTER_HINT:
|
|
201
|
-
- not-a-real-tag | bogus-slug | Should be skipped.
|
|
202
|
-
- above-appetite-residual | valid-slug | Should be kept."
|
|
203
|
-
LINE_COUNT=$(wc -l < "$QUEUE_FILE")
|
|
204
|
-
[ "$LINE_COUNT" -eq 1 ]
|
|
205
|
-
SLUG=$(python3 -c "import json,sys; print(json.loads(sys.stdin.readline())['risk_slug'])" < "$QUEUE_FILE")
|
|
206
|
-
[ "$SLUG" = "valid-slug" ]
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
# ---------------------------------------------------------------------------
|
|
210
|
-
# Append semantics (queue is append-only; dedupe is drain-step concern)
|
|
211
|
-
# ---------------------------------------------------------------------------
|
|
212
|
-
|
|
213
|
-
@test "two consecutive hook runs with same hint → six JSONL lines (queue is append-only)" {
|
|
214
|
-
HINT="RISK_SCORES: commit=10 push=5 release=2
|
|
215
|
-
|
|
216
|
-
RISK_REGISTER_HINT:
|
|
217
|
-
- above-appetite-residual | slug-a | First.
|
|
218
|
-
- confidentiality-disclosure | slug-b | Second.
|
|
219
|
-
- user-stated-precondition | slug-c | Third."
|
|
220
|
-
run_hook "wr-risk-scorer:pipeline" "$HINT"
|
|
221
|
-
sleep 1
|
|
222
|
-
run_hook "wr-risk-scorer:pipeline" "$HINT"
|
|
223
|
-
LINE_COUNT=$(wc -l < "$QUEUE_FILE")
|
|
224
|
-
[ "$LINE_COUNT" -eq 6 ]
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
# ---------------------------------------------------------------------------
|
|
228
|
-
# Directory creation
|
|
229
|
-
# ---------------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
@test ".afk-run-state/ absent → hook creates it; queue file written" {
|
|
232
|
-
[ ! -d ".afk-run-state" ]
|
|
233
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=10 push=5 release=2
|
|
234
|
-
|
|
235
|
-
RISK_REGISTER_HINT:
|
|
236
|
-
- above-appetite-residual | example | Example."
|
|
237
|
-
[ -d ".afk-run-state" ]
|
|
238
|
-
[ -f "$QUEUE_FILE" ]
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
# ---------------------------------------------------------------------------
|
|
242
|
-
# ADR-045 Pattern 2: silent on stdout
|
|
243
|
-
# ---------------------------------------------------------------------------
|
|
244
|
-
|
|
245
|
-
@test "hook stdout is empty on queue-write success (ADR-045 Pattern 2)" {
|
|
246
|
-
STDOUT=$(run_hook_capture_stdout "wr-risk-scorer:pipeline" "RISK_SCORES: commit=10 push=5 release=2
|
|
247
|
-
|
|
248
|
-
RISK_REGISTER_HINT:
|
|
249
|
-
- above-appetite-residual | quiet-slug | Quiet please.")
|
|
250
|
-
[ -z "$STDOUT" ]
|
|
251
|
-
# Verify the side-effect did happen
|
|
252
|
-
[ -f "$QUEUE_FILE" ]
|
|
253
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# Tests for risk-score-mark.sh — verifies the PostToolUse:Agent hook
|
|
4
|
-
# parses risk-scorer agent output and writes the right files into
|
|
5
|
-
# the session-scoped risk dir.
|
|
6
|
-
#
|
|
7
|
-
# Per ADR-005 (P011): behavioural assertions are functional — they
|
|
8
|
-
# pipe mock hook input to the script and assert on side-effects, not
|
|
9
|
-
# on what the source happens to contain. The four "echo X | grep X"
|
|
10
|
-
# tautologies that previously lived here have been removed (they
|
|
11
|
-
# always passed regardless of hook behaviour).
|
|
12
|
-
|
|
13
|
-
setup() {
|
|
14
|
-
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
15
|
-
HOOK="$SCRIPT_DIR/risk-score-mark.sh"
|
|
16
|
-
ORIG_DIR="$PWD"
|
|
17
|
-
TEST_DIR=$(mktemp -d)
|
|
18
|
-
cd "$TEST_DIR"
|
|
19
|
-
TMPDIR="$TEST_DIR/tmp"
|
|
20
|
-
export TMPDIR
|
|
21
|
-
mkdir -p "$TMPDIR"
|
|
22
|
-
SESSION_ID="test-session-$$"
|
|
23
|
-
RDIR="$TMPDIR/claude-risk-${SESSION_ID}"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
teardown() {
|
|
27
|
-
cd "$ORIG_DIR"
|
|
28
|
-
rm -rf "$TEST_DIR"
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
# Helper: build the PostToolUse:Agent JSON envelope and pipe it to the hook.
|
|
32
|
-
# AGENT_OUTPUT is wrapped in tool_response.content[0].text to match the
|
|
33
|
-
# real Claude Code PostToolUse hook payload shape.
|
|
34
|
-
run_hook() {
|
|
35
|
-
local subagent="$1"
|
|
36
|
-
local agent_output="$2"
|
|
37
|
-
python3 -c "
|
|
38
|
-
import json, sys
|
|
39
|
-
print(json.dumps({
|
|
40
|
-
'tool_name': 'Agent',
|
|
41
|
-
'session_id': '${SESSION_ID}',
|
|
42
|
-
'tool_input': {'subagent_type': '${subagent}'},
|
|
43
|
-
'tool_response': {'content': [{'type': 'text', 'text': sys.stdin.read()}]}
|
|
44
|
-
}))" <<<"$agent_output" | bash "$HOOK"
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
# --- Pipeline scorer: writes commit/push/release score files ---
|
|
48
|
-
|
|
49
|
-
@test "pipeline: writes commit/push/release scores from RISK_SCORES line" {
|
|
50
|
-
run_hook "wr-risk-scorer:pipeline" "Header text
|
|
51
|
-
RISK_SCORES: commit=2 push=3 release=1
|
|
52
|
-
Trailing text"
|
|
53
|
-
[ "$(cat "$RDIR/commit")" = "2" ]
|
|
54
|
-
[ "$(cat "$RDIR/push")" = "3" ]
|
|
55
|
-
[ "$(cat "$RDIR/release")" = "1" ]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@test "pipeline: writes reducing bypass markers when RISK_BYPASS: reducing" {
|
|
59
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=2 push=2 release=0
|
|
60
|
-
RISK_BYPASS: reducing"
|
|
61
|
-
[ -f "$RDIR/reducing-commit" ]
|
|
62
|
-
[ -f "$RDIR/reducing-push" ]
|
|
63
|
-
[ -f "$RDIR/reducing-release" ]
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
@test "pipeline: writes incident bypass marker when RISK_BYPASS: incident" {
|
|
67
|
-
run_hook "wr-risk-scorer:pipeline" "RISK_SCORES: commit=10 push=10 release=10
|
|
68
|
-
RISK_BYPASS: incident"
|
|
69
|
-
[ -f "$RDIR/incident-release" ]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
@test "pipeline: writes nothing when output has no RISK_SCORES line" {
|
|
73
|
-
run_hook "wr-risk-scorer:pipeline" "No score line in this output"
|
|
74
|
-
[ ! -f "$RDIR/commit" ]
|
|
75
|
-
[ ! -f "$RDIR/push" ]
|
|
76
|
-
[ ! -f "$RDIR/release" ]
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
# --- Plan scorer: writes plan-reviewed marker on PASS only ---
|
|
80
|
-
|
|
81
|
-
@test "plan: writes plan-reviewed marker on RISK_VERDICT: PASS" {
|
|
82
|
-
run_hook "wr-risk-scorer:plan" "RISK_VERDICT: PASS"
|
|
83
|
-
[ -f "$RDIR/plan-reviewed" ]
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
@test "plan: does NOT write plan-reviewed marker on RISK_VERDICT: FAIL" {
|
|
87
|
-
run_hook "wr-risk-scorer:plan" "RISK_VERDICT: FAIL"
|
|
88
|
-
[ ! -f "$RDIR/plan-reviewed" ]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
# --- Subagent routing: case guard ignores non-risk-scorer agents ---
|
|
92
|
-
|
|
93
|
-
@test "case guard: skips unrelated agent without writing files" {
|
|
94
|
-
run_hook "wr-architect:agent" "RISK_SCORES: commit=99 push=99 release=99"
|
|
95
|
-
[ ! -f "$RDIR/commit" ]
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@test "case guard: matches wr-risk-scorer:pipeline subagent" {
|
|
99
|
-
SUBAGENT="wr-risk-scorer:pipeline"
|
|
100
|
-
case "$SUBAGENT" in
|
|
101
|
-
*risk-scorer*) true ;;
|
|
102
|
-
*) false ;;
|
|
103
|
-
esac
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
@test "case guard: does NOT match wr-architect:agent" {
|
|
107
|
-
SUBAGENT="wr-architect:agent"
|
|
108
|
-
case "$SUBAGENT" in
|
|
109
|
-
*risk-scorer*) false ;;
|
|
110
|
-
*) true ;;
|
|
111
|
-
esac
|
|
112
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
setup() {
|
|
4
|
-
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
5
|
-
HOOKS="$REPO_ROOT/packages/risk-scorer/hooks"
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
@test "hooks.json registers four command hooks" {
|
|
9
|
-
run python3 - "$HOOKS/hooks.json" <<'PY'
|
|
10
|
-
import json, sys
|
|
11
|
-
data = json.load(open(sys.argv[1]))
|
|
12
|
-
print(sum(len(entry["hooks"]) for entries in data["hooks"].values() for entry in entries))
|
|
13
|
-
PY
|
|
14
|
-
[ "$status" -eq 0 ]
|
|
15
|
-
[ "$output" = "4" ]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
@test "dispatcher is registered for prompt, pre-tool, and post-tool events" {
|
|
19
|
-
run grep -n "risk-scorer-dispatch.sh user-prompt" "$HOOKS/hooks.json"
|
|
20
|
-
[ "$status" -eq 0 ]
|
|
21
|
-
run grep -n "risk-scorer-dispatch.sh pre-tool" "$HOOKS/hooks.json"
|
|
22
|
-
[ "$status" -eq 0 ]
|
|
23
|
-
run grep -n "risk-scorer-dispatch.sh post-tool" "$HOOKS/hooks.json"
|
|
24
|
-
[ "$status" -eq 0 ]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
@test "dispatcher routes Write through the secret leak gate" {
|
|
28
|
-
local fake_key input
|
|
29
|
-
fake_key="AKIA""ABCDEFGHIJKLMNOP"
|
|
30
|
-
input="$(python3 - "$fake_key" <<'PY'
|
|
31
|
-
import json, sys
|
|
32
|
-
print(json.dumps({
|
|
33
|
-
"tool_name": "Write",
|
|
34
|
-
"session_id": "dispatch-test",
|
|
35
|
-
"tool_input": {"file_path": "src/example.txt", "content": sys.argv[1]},
|
|
36
|
-
}))
|
|
37
|
-
PY
|
|
38
|
-
)"
|
|
39
|
-
|
|
40
|
-
run bash "$HOOKS/risk-scorer-dispatch.sh" pre-tool <<<"$input"
|
|
41
|
-
[ "$status" -eq 0 ]
|
|
42
|
-
[[ "$output" == *"permissionDecision"* ]]
|
|
43
|
-
[[ "$output" == *"deny"* ]]
|
|
44
|
-
[[ "$output" == *"AWS access key"* ]]
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
@test "dispatcher routes PostToolUse Write through the WIP nudge" {
|
|
48
|
-
local dir input orig
|
|
49
|
-
dir="$(mktemp -d)"
|
|
50
|
-
orig="$PWD"
|
|
51
|
-
mkdir -p "$dir/repo" "$dir/tmp"
|
|
52
|
-
cd "$dir/repo"
|
|
53
|
-
git init -q
|
|
54
|
-
git config user.email test@example.com
|
|
55
|
-
git config user.name "Test User"
|
|
56
|
-
printf 'base\n' > base.txt
|
|
57
|
-
git add base.txt
|
|
58
|
-
git commit -q -m initial
|
|
59
|
-
mkdir -p src
|
|
60
|
-
printf 'one\n' > src/one.txt
|
|
61
|
-
printf 'two\n' > src/two.txt
|
|
62
|
-
printf 'three\n' > src/three.txt
|
|
63
|
-
input="$(python3 - <<'PY'
|
|
64
|
-
import json
|
|
65
|
-
print(json.dumps({
|
|
66
|
-
"tool_name": "Write",
|
|
67
|
-
"session_id": "dispatch-wip",
|
|
68
|
-
"tool_input": {"file_path": "src/three.txt"},
|
|
69
|
-
}))
|
|
70
|
-
PY
|
|
71
|
-
)"
|
|
72
|
-
run env TMPDIR="$dir/tmp" bash "$HOOKS/risk-scorer-dispatch.sh" post-tool <<<"$input"
|
|
73
|
-
cd "$orig"
|
|
74
|
-
rm -rf "$dir"
|
|
75
|
-
[ "$status" -eq 0 ]
|
|
76
|
-
[[ "$output" == *"Batching risk rising"* ]]
|
|
77
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# P001 / ADR-009: Stop-hook marker reset removed.
|
|
4
|
-
|
|
5
|
-
setup() {
|
|
6
|
-
PLUGIN_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
@test "risk-scorer: hooks.json has no Stop hook entry (ADR-009)" {
|
|
10
|
-
! grep -q '"Stop"' "$PLUGIN_DIR/hooks/hooks.json"
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
@test "risk-scorer: risk-score-reset.sh has been removed" {
|
|
14
|
-
[ ! -f "$PLUGIN_DIR/hooks/risk-score-reset.sh" ]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
@test "risk-scorer: risk-policy-reset-marker.sh has been removed" {
|
|
18
|
-
[ ! -f "$PLUGIN_DIR/hooks/risk-policy-reset-marker.sh" ]
|
|
19
|
-
}
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# ADR-047 Amendment 2026-06-08 (P297): risk-scorer-scaffold-nudge.sh
|
|
4
|
-
# (SessionStart) emits a one-line nudge when RISK-POLICY.md exists but the
|
|
5
|
-
# docs/risks/ standing-risk register directory is missing, is silent
|
|
6
|
-
# otherwise, and self-suppresses under the AFK guard
|
|
7
|
-
# (WR_SUPPRESS_OVERSIGHT_NUDGE=1 per ADR-068) so the interactive
|
|
8
|
-
# scaffold-confirm never fires into an absent-user iteration (JTBD-006).
|
|
9
|
-
#
|
|
10
|
-
# P375 (2026-06-27): once docs/risks/ exists, the hook no longer goes
|
|
11
|
-
# silent — it counts entries still carrying the `**Curation**: pending
|
|
12
|
-
# review` marker and re-surfaces the count every session so the
|
|
13
|
-
# pending-review backlog self-surfaces (class-B) instead of rotting
|
|
14
|
-
# silently once stubs exist (the audit's "one step short of the jtbd
|
|
15
|
-
# pattern" gap).
|
|
16
|
-
#
|
|
17
|
-
# P379 (2026-06-28): inverse predicate of the register-missing arm — when
|
|
18
|
-
# RISK-POLICY.md is ABSENT ENTIRELY the hook nudges the adopter to author
|
|
19
|
-
# one via /wr-risk-scorer:update-policy (the gates otherwise run at the
|
|
20
|
-
# default appetite silently). Same AFK-suppress envelope (ADR-068).
|
|
21
|
-
# Behavioural — exercises the hook against fixture trees and asserts on stdout.
|
|
22
|
-
|
|
23
|
-
setup() {
|
|
24
|
-
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
|
|
25
|
-
HOOK="$REPO_ROOT/packages/risk-scorer/hooks/risk-scorer-scaffold-nudge.sh"
|
|
26
|
-
PLUGIN_ROOT="$REPO_ROOT/packages/risk-scorer"
|
|
27
|
-
DIR="$(mktemp -d)"
|
|
28
|
-
# Hermeticity (P391): the AFK work-problems orchestrator exports
|
|
29
|
-
# WR_SUPPRESS_OVERSIGHT_NUDGE=1, which the hook self-suppresses on. Unset it so
|
|
30
|
-
# nudge-emitting tests assert real behaviour; guard-specific tests set it
|
|
31
|
-
# per-invocation via `run env WR_SUPPRESS_OVERSIGHT_NUDGE=...`.
|
|
32
|
-
unset WR_SUPPRESS_OVERSIGHT_NUDGE
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
teardown() {
|
|
36
|
-
rm -rf "$DIR"
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@test "emits a scaffold nudge when RISK-POLICY.md exists and docs/risks/ is missing" {
|
|
40
|
-
printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
|
|
41
|
-
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
42
|
-
[ "$status" -eq 0 ]
|
|
43
|
-
[[ "$output" == *"RISK-POLICY.md present but docs/risks/ is missing"* ]]
|
|
44
|
-
[[ "$output" == *"/wr-risk-scorer:bootstrap-catalog"* ]]
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
@test "silent when RISK-POLICY.md exists and docs/risks/ exists but is empty" {
|
|
48
|
-
printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
|
|
49
|
-
mkdir -p "$DIR/docs/risks"
|
|
50
|
-
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
51
|
-
[ "$status" -eq 0 ]
|
|
52
|
-
[ -z "$output" ]
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
@test "silent when docs/risks/ exists with entries but none are pending review" {
|
|
56
|
-
printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
|
|
57
|
-
mkdir -p "$DIR/docs/risks"
|
|
58
|
-
printf '# R001\n**Curation**: curated\n' > "$DIR/docs/risks/R001-foo.active.md"
|
|
59
|
-
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
60
|
-
[ "$status" -eq 0 ]
|
|
61
|
-
[ -z "$output" ]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
@test "surfaces a pending-review count when entries carry the curation marker (P375 self-surfacing)" {
|
|
65
|
-
printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
|
|
66
|
-
mkdir -p "$DIR/docs/risks"
|
|
67
|
-
printf '# R001\n**Curation**: pending review (auto-scaffolded 2026-06-17)\n' > "$DIR/docs/risks/R001-foo.active.md"
|
|
68
|
-
printf '# R002\n**Curation**: pending review\n' > "$DIR/docs/risks/R002-bar.active.md"
|
|
69
|
-
printf '# R003\n**Curation**: curated\n' > "$DIR/docs/risks/R003-baz.active.md"
|
|
70
|
-
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
71
|
-
[ "$status" -eq 0 ]
|
|
72
|
-
[[ "$output" == *"2 standing-risk entries are pending review"* ]]
|
|
73
|
-
[[ "$output" == *"docs/risks/"* ]]
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
@test "singular phrasing when exactly one entry is pending review" {
|
|
77
|
-
printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
|
|
78
|
-
mkdir -p "$DIR/docs/risks"
|
|
79
|
-
printf '# R001\n**Curation**: pending review\n' > "$DIR/docs/risks/R001-foo.active.md"
|
|
80
|
-
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
81
|
-
[ "$status" -eq 0 ]
|
|
82
|
-
[[ "$output" == *"1 standing-risk entry is pending review"* ]]
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@test "AFK guard suppresses the pending-review nudge too" {
|
|
86
|
-
printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
|
|
87
|
-
mkdir -p "$DIR/docs/risks"
|
|
88
|
-
printf '# R001\n**Curation**: pending review\n' > "$DIR/docs/risks/R001-foo.active.md"
|
|
89
|
-
run env WR_SUPPRESS_OVERSIGHT_NUDGE=1 CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
90
|
-
[ "$status" -eq 0 ]
|
|
91
|
-
[ -z "$output" ]
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
@test "emits a policy-authoring nudge when RISK-POLICY.md is absent (P379 inverse predicate)" {
|
|
95
|
-
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
96
|
-
[ "$status" -eq 0 ]
|
|
97
|
-
[[ "$output" == *"No RISK-POLICY.md"* ]]
|
|
98
|
-
[[ "$output" == *"/wr-risk-scorer:update-policy"* ]]
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
@test "policy-absent nudge fires even when docs/risks/ exists (policy-absence wins, P379)" {
|
|
102
|
-
mkdir -p "$DIR/docs/risks"
|
|
103
|
-
run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
104
|
-
[ "$status" -eq 0 ]
|
|
105
|
-
[[ "$output" == *"No RISK-POLICY.md"* ]]
|
|
106
|
-
[[ "$output" == *"/wr-risk-scorer:update-policy"* ]]
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
@test "AFK guard suppresses the policy-absent nudge too (P379)" {
|
|
110
|
-
run env WR_SUPPRESS_OVERSIGHT_NUDGE=1 CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
111
|
-
[ "$status" -eq 0 ]
|
|
112
|
-
[ -z "$output" ]
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
@test "AFK guard WR_SUPPRESS_OVERSIGHT_NUDGE=1 suppresses the nudge entirely" {
|
|
116
|
-
printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
|
|
117
|
-
run env WR_SUPPRESS_OVERSIGHT_NUDGE=1 CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
118
|
-
[ "$status" -eq 0 ]
|
|
119
|
-
[ -z "$output" ]
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
@test "guard value other than 1 does not suppress" {
|
|
123
|
-
printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
|
|
124
|
-
run env WR_SUPPRESS_OVERSIGHT_NUDGE=0 CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
125
|
-
[[ "$output" == *"docs/risks/ is missing"* ]]
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
@test "silent when CLAUDE_PROJECT_DIR points at a non-existent path" {
|
|
129
|
-
run env CLAUDE_PROJECT_DIR="$DIR/nonexistent" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
130
|
-
[ "$status" -eq 0 ]
|
|
131
|
-
[ -z "$output" ]
|
|
132
|
-
}
|