@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.
Files changed (55) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +13 -2
  5. package/bin/install.mjs +42 -8
  6. package/hooks/codex-agent-completion.mjs +82 -0
  7. package/hooks/hooks.json +5 -2
  8. package/hooks/risk-scorer-dispatch.sh +3 -0
  9. package/hooks/risk-scorer-scaffold-nudge.sh +15 -4
  10. package/package.json +5 -2
  11. package/scripts/codex-agents.mjs +208 -0
  12. package/scripts/sync-codex-skills.mjs +8 -3
  13. package/skills/assess-external-comms/SKILL.md +5 -4
  14. package/skills/assess-inbound-report/SKILL.md +5 -4
  15. package/skills/assess-release/SKILL.md +5 -4
  16. package/skills/assess-wip/SKILL.md +5 -4
  17. package/skills/bootstrap-catalog/SKILL.md +5 -4
  18. package/skills/create-risk/SKILL.md +5 -4
  19. package/skills/external-comms/SKILL.md +5 -4
  20. package/skills/pipeline/SKILL.md +5 -4
  21. package/skills/update-policy/SKILL.md +6 -5
  22. package/skills/wip/SKILL.md +5 -4
  23. package/agents/test/inbound-report-contract.bats +0 -225
  24. package/agents/test/risk-scorer-above-appetite-stop.bats +0 -80
  25. package/agents/test/risk-scorer-catalog-consumption.bats +0 -138
  26. package/agents/test/risk-scorer-commit-verdict.bats +0 -66
  27. package/agents/test/risk-scorer-monitoring-not-a-control.bats +0 -76
  28. package/agents/test/risk-scorer-reducing-bypass-criteria.bats +0 -62
  29. package/agents/test/risk-scorer-register-hint.bats +0 -111
  30. package/agents/test/risk-scorer-structured-remediations.bats +0 -123
  31. package/agents/test/risk-scorer-user-stated-preconditions.bats +0 -89
  32. package/hooks/test/ci-status-gate.bats +0 -234
  33. package/hooks/test/external-comms-gate.bats +0 -528
  34. package/hooks/test/external-comms-key-substance.bats +0 -74
  35. package/hooks/test/git-push-gate.bats +0 -82
  36. package/hooks/test/pipeline-state-changeset-partition.bats +0 -181
  37. package/hooks/test/pipeline-state-hash.bats +0 -195
  38. package/hooks/test/plan-risk-guidance-once-per-session.bats +0 -95
  39. package/hooks/test/reducing-marker-persistence.bats +0 -236
  40. package/hooks/test/risk-gate.bats +0 -330
  41. package/hooks/test/risk-policy-project-root.bats +0 -20
  42. package/hooks/test/risk-score-commit-gate-adr-031-bypass.bats +0 -98
  43. package/hooks/test/risk-score-commit-gate-cadence-staleness.bats +0 -105
  44. package/hooks/test/risk-score-mark-external-comms-prompt-parse.bats +0 -94
  45. package/hooks/test/risk-score-mark-register-queue.bats +0 -253
  46. package/hooks/test/risk-score-mark.bats +0 -112
  47. package/hooks/test/risk-scorer-hook-dispatch.bats +0 -77
  48. package/hooks/test/risk-scorer-no-stop-hook.bats +0 -19
  49. package/hooks/test/risk-scorer-scaffold-nudge.bats +0 -132
  50. package/hooks/test/slide-marker-on-subprocess-return.bats +0 -118
  51. package/hooks/test/wip-risk-mark.bats +0 -64
  52. package/scripts/test/drain-register-queue.bats +0 -359
  53. package/scripts/test/evaluate-graduation.bats +0 -533
  54. package/scripts/test/extract-risks-from-reports.bats +0 -366
  55. 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
- }