@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,528 +0,0 @@
1
- #!/usr/bin/env bats
2
- # Tests for external-comms-gate.sh (P064 / ADR-028 amended).
3
- # Behavioural: the gate denies outbound prose tool calls until the
4
- # wr-risk-scorer:external-comms subagent has reviewed the draft and a
5
- # per-evaluator marker has been written. Hard-fail leak patterns deny
6
- # immediately without delegation.
7
- #
8
- # Note: secret-shaped strings are constructed at runtime via concatenation
9
- # so this test file itself does not trip the secret-leak-gate hook.
10
-
11
- setup() {
12
- HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
13
- HOOK="$HOOKS_DIR/external-comms-gate.sh"
14
-
15
- TEST_SESSION="bats-extcomms-gate-$$-${BATS_TEST_NUMBER}"
16
- RDIR="${TMPDIR:-/tmp}/claude-risk-${TEST_SESSION}"
17
- rm -rf "$RDIR"
18
- mkdir -p "$RDIR"
19
-
20
- # Default: assume RISK-POLICY.md is present in repo root (it is).
21
- TEST_PROJECT_DIR="$(mktemp -d)"
22
- printf "## Confidential Information\n- Client names\n- Revenue figures\n" \
23
- > "$TEST_PROJECT_DIR/RISK-POLICY.md"
24
-
25
- unset BYPASS_RISK_GATE
26
-
27
- # Construct secret-shaped strings at runtime to avoid tripping the
28
- # repo's own secret-leak-gate when this file is committed/edited.
29
- GH_TOKEN_LIKE="g""hp""_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
30
- AWS_KEY_LIKE="A""KIA""IOSFODNN7EXAMPLE"
31
- }
32
-
33
- teardown() {
34
- rm -rf "$RDIR"
35
- rm -rf "$TEST_PROJECT_DIR"
36
- unset BYPASS_RISK_GATE
37
- }
38
-
39
- # ---------- Helpers ----------
40
-
41
- # Build a PreToolUse:Bash input for a given command via python so quoting is safe.
42
- build_bash_input() {
43
- local cmd="$1"
44
- python3 -c "
45
- import json, sys
46
- print(json.dumps({
47
- 'session_id': '$TEST_SESSION',
48
- 'tool_name': 'Bash',
49
- 'tool_input': {'command': sys.argv[1]},
50
- }))
51
- " "$cmd"
52
- }
53
-
54
- # Build a PreToolUse:Write input for a changeset path with body content.
55
- build_write_input() {
56
- local file_path="$1"
57
- local content="$2"
58
- python3 -c "
59
- import json, sys
60
- print(json.dumps({
61
- 'session_id': '$TEST_SESSION',
62
- 'tool_name': 'Write',
63
- 'tool_input': {'file_path': sys.argv[1], 'content': sys.argv[2]},
64
- }))
65
- " "$file_path" "$content"
66
- }
67
-
68
- # Mock `gh repo view --json visibility` for the git-commit-message surface
69
- # repo-visibility precondition (P365). vis ∈ {PUBLIC,PRIVATE,INTERNAL}; pass the
70
- # literal "FAIL" to simulate gh absent / unauthenticated (non-zero exit). The
71
- # mock ignores args and prints the chosen visibility so the hook's
72
- # `gh repo view --json visibility -q .visibility` resolves deterministically.
73
- mock_gh_visibility() {
74
- local vis="$1"
75
- mkdir -p "$TEST_PROJECT_DIR/mockbin"
76
- if [ "$vis" = "FAIL" ]; then
77
- printf '#!/usr/bin/env bash\nexit 1\n' > "$TEST_PROJECT_DIR/mockbin/gh"
78
- else
79
- printf '#!/usr/bin/env bash\necho %s\n' "$vis" > "$TEST_PROJECT_DIR/mockbin/gh"
80
- fi
81
- chmod +x "$TEST_PROJECT_DIR/mockbin/gh"
82
- }
83
-
84
- # Run the hook in a project dir with RISK-POLICY.md present, piping JSON via stdin.
85
- # mockbin (if populated by mock_gh_visibility) is prepended to PATH so the
86
- # git-commit-message visibility precondition resolves against the mock.
87
- run_hook() {
88
- local input="$1"
89
- run bash -c "cd '$TEST_PROJECT_DIR' && export PATH='$TEST_PROJECT_DIR/mockbin':\$PATH && printf '%s' \"\$1\" | '$HOOK'" _ "$input"
90
- }
91
-
92
- # ---------- Tests ----------
93
-
94
- @test "non-matching Bash command (ls) is allowed silently" {
95
- INPUT=$(build_bash_input "ls -la")
96
- run_hook "$INPUT"
97
- [ "$status" -eq 0 ]
98
- [ -z "$output" ]
99
- }
100
-
101
- @test "gh issue create with clean draft denies and prompts external-comms delegation (no marker yet)" {
102
- INPUT=$(build_bash_input "gh issue create --title T --body 'we observed a build failure on Node 20'")
103
- run_hook "$INPUT"
104
- [ "$status" -eq 0 ]
105
- [[ "$output" == *"permissionDecision"* ]]
106
- [[ "$output" == *"deny"* ]]
107
- [[ "$output" == *"wr-risk-scorer:external-comms"* ]]
108
- }
109
-
110
- # P377/RFC-029: the BYPASS_RISK_GATE env override was removed. The only
111
- # clearance path named in the deny is delegation to the external-comms subagent.
112
- @test "marker-absent deny names the reviewer and offers no env override (P377/RFC-029)" {
113
- INPUT=$(build_bash_input "gh issue create --title T --body 'we observed a build failure on Node 20'")
114
- run_hook "$INPUT"
115
- [ "$status" -eq 0 ]
116
- [[ "$output" == *"deny"* ]]
117
- [[ "$output" == *"wr-risk-scorer:external-comms"* ]]
118
- [[ "$output" != *"BYPASS_RISK_GATE=1"* ]]
119
- }
120
-
121
- @test "hard-fail credential pattern (GitHub token) denies immediately with leak reason" {
122
- INPUT=$(build_bash_input "gh issue comment 42 --body 'token=${GH_TOKEN_LIKE}'")
123
- run_hook "$INPUT"
124
- [ "$status" -eq 0 ]
125
- [[ "$output" == *"permissionDecision"* ]]
126
- [[ "$output" == *"deny"* ]]
127
- [[ "$output" == *"GitHub token"* ]] || [[ "$output" == *"credential"* ]]
128
- }
129
-
130
- @test "hard-fail credential pattern (AWS key) denies immediately" {
131
- INPUT=$(build_bash_input "gh pr create --title T --body 'AWS=${AWS_KEY_LIKE}'")
132
- run_hook "$INPUT"
133
- [ "$status" -eq 0 ]
134
- [[ "$output" == *"deny"* ]]
135
- [[ "$output" == *"AWS"* ]] || [[ "$output" == *"credential"* ]]
136
- }
137
-
138
- @test "BYPASS_RISK_GATE=1 does NOT bypass the gate (removed P377/RFC-029)" {
139
- INPUT=$(build_bash_input "gh issue create --title T --body 'we observed a build failure'")
140
- run bash -c "cd '$TEST_PROJECT_DIR' && BYPASS_RISK_GATE=1 printf '%s' \"\$1\" | BYPASS_RISK_GATE=1 '$HOOK'" _ "$INPUT"
141
- [ "$status" -eq 0 ]
142
- # The env override no longer short-circuits — the gate still denies the
143
- # unreviewed external-comms draft.
144
- [[ "$output" == *"deny"* ]]
145
- [[ "$output" == *"wr-risk-scorer:external-comms"* ]]
146
- }
147
-
148
- @test "per-evaluator marker (external-comms-risk-reviewed-<KEY>) allows the call (ADR-028 amended 2026-05-14)" {
149
- DRAFT="we observed a build failure on Node 20"
150
- SURFACE="gh-issue-create"
151
- KEY=$(printf '%s\n%s' "$DRAFT" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
152
- touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
153
-
154
- INPUT=$(build_bash_input "gh issue create --title T --body '$DRAFT'")
155
- run_hook "$INPUT"
156
- [ "$status" -eq 0 ]
157
- [ -z "$output" ]
158
- }
159
-
160
- @test "legacy combined marker (external-comms-reviewed-<KEY>) does NOT satisfy the risk gate (P038 per-evaluator scheme)" {
161
- DRAFT="we observed a build failure on Node 20"
162
- SURFACE="gh-issue-create"
163
- KEY=$(printf '%s\n%s' "$DRAFT" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
164
- # Pre-amendment combined marker — should NOT satisfy the new per-evaluator gate.
165
- touch "${RDIR}/external-comms-reviewed-${KEY}"
166
-
167
- INPUT=$(build_bash_input "gh issue create --title T --body '$DRAFT'")
168
- run_hook "$INPUT"
169
- [ "$status" -eq 0 ]
170
- [[ "$output" == *"deny"* ]]
171
- [[ "$output" == *"wr-risk-scorer:external-comms"* ]]
172
- }
173
-
174
- @test "RISK-POLICY.md absent yields advisory-only mode (permits)" {
175
- rm -f "$TEST_PROJECT_DIR/RISK-POLICY.md"
176
- INPUT=$(build_bash_input "gh issue create --title T --body 'we observed a failure'")
177
- run_hook "$INPUT"
178
- [ "$status" -eq 0 ]
179
- # Must NOT deny when policy file is absent.
180
- [[ "$output" != *"\"permissionDecision\": \"deny\""* ]]
181
- [[ "$output" != *"\"permissionDecision\":\"deny\""* ]]
182
- }
183
-
184
- @test "PreToolUse:Write on .changeset/*.md with revenue leak content denies" {
185
- INPUT=$(build_write_input ".changeset/wr-risk-scorer-extcomms.md" "Add gate; covers Acme Corp \$2.4M ARR client.")
186
- run_hook "$INPUT"
187
- [ "$status" -eq 0 ]
188
- [[ "$output" == *"deny"* ]]
189
- }
190
-
191
- @test "PreToolUse:Write on .changeset/*.md with clean content denies-then-delegates (no marker yet)" {
192
- INPUT=$(build_write_input ".changeset/wr-risk-scorer-extcomms.md" "Add external-comms gate covering gh issue and pr surfaces.")
193
- run_hook "$INPUT"
194
- [ "$status" -eq 0 ]
195
- [[ "$output" == *"deny"* ]]
196
- [[ "$output" == *"wr-risk-scorer:external-comms"* ]]
197
- }
198
-
199
- @test "PreToolUse:Write on a non-changeset path is ignored" {
200
- INPUT=$(build_write_input "src/foo.ts" "Acme Corp client revenue \$2.4M")
201
- run_hook "$INPUT"
202
- [ "$status" -eq 0 ]
203
- [ -z "$output" ]
204
- }
205
-
206
- @test "gh api security-advisories triggers the gate" {
207
- INPUT=$(build_bash_input "gh api repos/foo/bar/security-advisories --method POST --field summary='leak via X'")
208
- run_hook "$INPUT"
209
- [ "$status" -eq 0 ]
210
- [[ "$output" == *"deny"* ]]
211
- [[ "$output" == *"wr-risk-scorer:external-comms"* ]]
212
- }
213
-
214
- # ---------------------------------------------------------------------------
215
- # P405 — read-only `gh api` polls must NOT trip the gate. `gh api` defaults to
216
- # GET; it only carries an outbound prose body (the thing this gate reviews)
217
- # when a request-body flag (-f/--field, -F/--raw-field, --input) is present.
218
- # The `/wr-itil:review-problems` Step 4.5c security-advisories discovery poll
219
- # is read-only (`--jq` filter, no body flag) and was being denied as if it
220
- # were an advisory draft, silently blacking out one discovery channel.
221
- # ---------------------------------------------------------------------------
222
-
223
- @test "P405: read-only gh api security-advisories poll (--jq, no body flag) is not gated" {
224
- INPUT=$(build_bash_input "gh api repos/foo/bar/security-advisories --jq '.[].ghsa_id'")
225
- run_hook "$INPUT"
226
- [ "$status" -eq 0 ]
227
- [ -z "$output" ]
228
- }
229
-
230
- @test "P405: bare gh api security-advisories read (no flags) is not gated" {
231
- INPUT=$(build_bash_input "gh api repos/foo/bar/security-advisories")
232
- run_hook "$INPUT"
233
- [ "$status" -eq 0 ]
234
- [ -z "$output" ]
235
- }
236
-
237
- @test "P405: read-only gh api comments poll (--jq, no body flag) is not gated" {
238
- INPUT=$(build_bash_input "gh api repos/foo/bar/issues/1/comments --jq '.[].body'")
239
- run_hook "$INPUT"
240
- [ "$status" -eq 0 ]
241
- [ -z "$output" ]
242
- }
243
-
244
- @test "P405: gh api security-advisories write with -f body flag still triggers the gate" {
245
- INPUT=$(build_bash_input "gh api repos/foo/bar/security-advisories -f summary='new advisory'")
246
- run_hook "$INPUT"
247
- [ "$status" -eq 0 ]
248
- [[ "$output" == *"deny"* ]]
249
- [[ "$output" == *"wr-risk-scorer:external-comms"* ]]
250
- }
251
-
252
- @test "npm publish triggers the gate" {
253
- INPUT=$(build_bash_input "npm publish")
254
- run_hook "$INPUT"
255
- [ "$status" -eq 0 ]
256
- [[ "$output" == *"deny"* ]]
257
- [[ "$output" == *"wr-risk-scorer:external-comms"* ]]
258
- }
259
-
260
- # ---------------------------------------------------------------------------
261
- # P010 / ADR-028 amended 2026-05-25 — deny-after-PASS regression.
262
- # The gate sees the FULL Write content (YAML frontmatter + body) on the
263
- # changeset-author surface, but the mark hook keys the marker on the body
264
- # the agent wrapped in <draft>. Before the fix the gate hashed the full
265
- # content (incl. frontmatter), so the body-keyed PASS marker landed at a
266
- # key the gate never re-read → permanent deny-after-PASS. After the fix the
267
- # gate strips frontmatter before hashing, so a body-keyed marker permits.
268
- # ---------------------------------------------------------------------------
269
-
270
- @test "P010: changeset Write permits when the PASS marker is keyed on the <draft> body (frontmatter stripped before hash)" {
271
- BODY="external-comms gate strips changeset frontmatter before key hash"
272
- SURFACE="changeset-author"
273
- # Marker keyed on the body the mark-hook helper derives from <draft> — for a
274
- # frontmatter-free body the canonical key equals the raw printf-of-body key.
275
- KEY=$(printf '%s\n%s' "$BODY" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
276
- touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
277
-
278
- # The gate sees the full changeset file: frontmatter + blank line + body.
279
- CONTENT=$'---\n"@windyroad/risk-scorer": patch\n---\n\n'"$BODY"
280
- INPUT=$(build_write_input ".changeset/p010-fix.md" "$CONTENT")
281
- run_hook "$INPUT"
282
- [ "$status" -eq 0 ]
283
- [ -z "$output" ]
284
- }
285
-
286
- # ---------------------------------------------------------------------------
287
- # P082 Phase 1 — git commit message surface (risk evaluator).
288
- # Commit messages reach git log / PR commits tab / release notes /
289
- # CHANGELOG. The risk evaluator gates the body for leak patterns
290
- # (credentials, prod URLs, business-context-paired financials/user counts)
291
- # AND defers structured leak-free drafts to the wr-risk-scorer:external-comms
292
- # subagent. Editor flow (bare `git commit`) is out of scope per P082 SC1.
293
- # ---------------------------------------------------------------------------
294
-
295
- @test "P082: git commit -m with leak-shaped credential body denies via leak pre-filter" {
296
- INPUT=$(build_bash_input "git commit -m \"docs: token=${GH_TOKEN_LIKE}\"")
297
- run_hook "$INPUT"
298
- [ "$status" -eq 0 ]
299
- [[ "$output" == *"deny"* ]]
300
- [[ "$output" == *"git-commit-message"* ]]
301
- [[ "$output" == *"GitHub token"* ]] || [[ "$output" == *"credential"* ]]
302
- }
303
-
304
- @test "P082: git commit -m with leak-free body denies and delegates to risk evaluator" {
305
- mock_gh_visibility PUBLIC
306
- INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
307
- run_hook "$INPUT"
308
- [ "$status" -eq 0 ]
309
- [[ "$output" == *"deny"* ]]
310
- [[ "$output" == *"git-commit-message"* ]]
311
- [[ "$output" == *"wr-risk-scorer:external-comms"* ]]
312
- }
313
-
314
- @test "P082: git commit --amend -m is intercepted (P082 SC2)" {
315
- mock_gh_visibility PUBLIC
316
- INPUT=$(build_bash_input "git commit --amend -m \"rewritten subject\"")
317
- run_hook "$INPUT"
318
- [ "$status" -eq 0 ]
319
- [[ "$output" == *"deny"* ]]
320
- [[ "$output" == *"git-commit-message"* ]]
321
- }
322
-
323
- @test "P082: git commit HEREDOC body is intercepted and the body becomes the marker key" {
324
- # Build a HEREDOC-shaped command. The hook regex pulls the body BETWEEN
325
- # the <<'EOF' opener and the closing EOF marker — the extracted DRAFT is
326
- # the inner text, NOT the literal `$(cat <<'EOF' ... EOF)` wrapper.
327
- mock_gh_visibility PUBLIC
328
- BODY=$'feat(foo): add bar\n\nWe observed a build failure on Node 20.'
329
- CMD=$'git commit -m "$(cat <<\'EOF\'\n'"$BODY"$'\nEOF\n)"'
330
- INPUT=$(build_bash_input "$CMD")
331
- run_hook "$INPUT"
332
- [ "$status" -eq 0 ]
333
- [[ "$output" == *"deny"* ]]
334
- [[ "$output" == *"git-commit-message"* ]]
335
-
336
- # Pre-place the per-evaluator marker keyed on the extracted HEREDOC body
337
- # + the git-commit-message surface; the second run must permit silently.
338
- SURFACE="git-commit-message"
339
- KEY=$(printf '%s\n%s' "$BODY" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
340
- touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
341
- run_hook "$INPUT"
342
- [ "$status" -eq 0 ]
343
- [ -z "$output" ]
344
- }
345
-
346
- @test "P082: bare git commit (editor flow) is silently allowed per SC1" {
347
- # No -m / --message → .git/COMMIT_EDITMSG doesn't exist at PreToolUse
348
- # time. Phase 1 skip is pragmatic; the editor flow has user-eyeballs.
349
- INPUT=$(build_bash_input "git commit")
350
- run_hook "$INPUT"
351
- [ "$status" -eq 0 ]
352
- [ -z "$output" ]
353
- }
354
-
355
- @test "P082: git merge is silently allowed (not a git commit verb, SC3)" {
356
- INPUT=$(build_bash_input "git merge --no-ff feature-branch")
357
- run_hook "$INPUT"
358
- [ "$status" -eq 0 ]
359
- [ -z "$output" ]
360
- }
361
-
362
- @test "P082→P377/RFC-029: BYPASS_RISK_GATE=1 is inert — does not short-circuit the gate" {
363
- # gh-issue is an unconditionally-gated external surface (no visibility
364
- # precondition). Setting BYPASS_RISK_GATE=1 has no effect: the unreviewed
365
- # draft still denies, identically to the no-env-var case.
366
- INPUT=$(build_bash_input "gh issue create --title T --body 'we observed a build failure on Node 20'")
367
- run bash -c "cd '$TEST_PROJECT_DIR' && BYPASS_RISK_GATE=1 printf '%s' \"\$1\" | BYPASS_RISK_GATE=1 '$HOOK'" _ "$INPUT"
368
- [ "$status" -eq 0 ]
369
- [[ "$output" == *"deny"* ]]
370
- [[ "$output" == *"wr-risk-scorer:external-comms"* ]]
371
- }
372
-
373
- @test "P082: per-evaluator marker keyed on (body, git-commit-message) permits the call" {
374
- mock_gh_visibility PUBLIC
375
- BODY="docs(retro): close iter 3 ask-hygiene trail"
376
- SURFACE="git-commit-message"
377
- KEY=$(printf '%s\n%s' "$BODY" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
378
- touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
379
-
380
- INPUT=$(build_bash_input "git commit -m \"$BODY\"")
381
- run_hook "$INPUT"
382
- [ "$status" -eq 0 ]
383
- [ -z "$output" ]
384
- }
385
-
386
- # ---------------------------------------------------------------------------
387
- # P365 — repo-visibility precondition on the git-commit-message surface.
388
- # A commit message is external-facing prose ONLY when it lands in a PUBLIC
389
- # GitHub repo (git log / PR commits tab / release notes / CHANGELOG). In
390
- # private/internal repos — or any repo whose visibility cannot be confirmed
391
- # PUBLIC — the marker-review delegation deny is a pure false-positive
392
- # (user direction 2026-06-11: "this MUST NOT fire for private repos"). The
393
- # precondition silent-passes the marker gate for the git-commit-message
394
- # surface on any non-PUBLIC / indeterminate gh result. It is scoped to that
395
- # surface only and runs AFTER the leak pre-filter, so the credential/prod-URL
396
- # security net survives the short-circuit.
397
- # ---------------------------------------------------------------------------
398
-
399
- @test "P365: git commit -m in a PRIVATE repo silent-passes (no external-comms deny)" {
400
- mock_gh_visibility PRIVATE
401
- INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
402
- run_hook "$INPUT"
403
- [ "$status" -eq 0 ]
404
- [ -z "$output" ]
405
- }
406
-
407
- @test "P365: git commit -m in an INTERNAL repo silent-passes" {
408
- mock_gh_visibility INTERNAL
409
- INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
410
- run_hook "$INPUT"
411
- [ "$status" -eq 0 ]
412
- [ -z "$output" ]
413
- }
414
-
415
- @test "P365: git commit -m when gh is unavailable/indeterminate silent-passes (fail-non-public)" {
416
- mock_gh_visibility FAIL
417
- INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
418
- run_hook "$INPUT"
419
- [ "$status" -eq 0 ]
420
- [ -z "$output" ]
421
- }
422
-
423
- @test "P365: git commit -m in a PUBLIC repo still denies+delegates (gate intact, precondition surface-scoped)" {
424
- mock_gh_visibility PUBLIC
425
- INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
426
- run_hook "$INPUT"
427
- [ "$status" -eq 0 ]
428
- [[ "$output" == *"deny"* ]]
429
- [[ "$output" == *"git-commit-message"* ]]
430
- }
431
-
432
- @test "P360: empty EXTERNAL_COMMS_SKIP_SURFACES leaves the risk evaluator gating commit messages (divergence guard)" {
433
- # The voice-tone evaluator skips git-commit-message (its policy disclaims it);
434
- # the risk evaluator's .conf leaves EXTERNAL_COMMS_SKIP_SURFACES empty, so its
435
- # leak check on commit bodies stays meaningful. Guards against a regression
436
- # that accidentally skips this surface for risk-scorer (e.g. defaulting the
437
- # knob non-empty, or syncing voice-tone's value into the shared gate body).
438
- grep -qE '^EXTERNAL_COMMS_SKIP_SURFACES=$' "$HOOKS_DIR/external-comms-evaluator.conf"
439
- mock_gh_visibility PUBLIC
440
- INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
441
- run_hook "$INPUT"
442
- [ "$status" -eq 0 ]
443
- [[ "$output" == *"deny"* ]]
444
- [[ "$output" == *"git-commit-message"* ]]
445
- }
446
-
447
- @test "P365: leak-shaped credential in a PRIVATE-repo commit body still hard-fails (security net survives)" {
448
- mock_gh_visibility PRIVATE
449
- INPUT=$(build_bash_input "git commit -m \"docs: token=${GH_TOKEN_LIKE}\"")
450
- run_hook "$INPUT"
451
- [ "$status" -eq 0 ]
452
- [[ "$output" == *"deny"* ]]
453
- [[ "$output" == *"git-commit-message"* ]]
454
- }
455
-
456
- @test "P365: PRIVATE visibility does NOT short-circuit the gh-issue surface (still denies+delegates)" {
457
- mock_gh_visibility PRIVATE
458
- INPUT=$(build_bash_input "gh issue create --title x --body 'a clean issue body'")
459
- run_hook "$INPUT"
460
- [ "$status" -eq 0 ]
461
- [[ "$output" == *"deny"* ]]
462
- [[ "$output" == *"gh-issue-create"* ]]
463
- }
464
-
465
- # ---------------------------------------------------------------------------
466
- # P364 — backtick-bearing double-quoted --body marker-key mismatch.
467
- # When an outbound body contains markdown backticks (a code span), the
468
- # orchestrator must backslash-escape them to survive bash double quotes:
469
- # `gh issue comment 42 --body "Fixed in \`foo\` ..."`. Before the fix the
470
- # gate extracted the RAW shell-escaped body (literal `\`` with backslash)
471
- # from the command text, while the PostToolUse mark hook hashed the LOGICAL
472
- # <draft> body (plain backticks). The two sha256 keys diverged → the PASS
473
- # marker landed at a key the gate never re-read → permanent deny-after-PASS.
474
- # After the fix the gate unescapes bash double-quote backslash-escapes on
475
- # the captured double-quoted body so its DRAFT is byte-equal to the logical
476
- # reviewed draft and the body-keyed marker permits. DISTINCT from P276 /
477
- # P010 (whitespace / frontmatter) — this is a shell-escaping-layer fix.
478
- # ---------------------------------------------------------------------------
479
-
480
- @test "P364: backtick-bearing double-quoted --body permits when marker keyed on the unescaped logical body" {
481
- # Logical draft body (plain backticks) — what the agent wraps in <draft>
482
- # and what the mark hook hashes via compute_external_comms_key.
483
- LOGICAL='Fixed in `compute_external_comms_key` per the patch on Node 20.'
484
- SURFACE="gh-issue-comment"
485
- KEY=$(printf '%s\n%s' "$LOGICAL" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
486
- touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
487
-
488
- # The orchestrator backslash-escapes the backticks to survive bash double
489
- # quotes — this is the byte-different command text the gate actually sees.
490
- CMD='gh issue comment 42 --body "Fixed in \`compute_external_comms_key\` per the patch on Node 20."'
491
- INPUT=$(build_bash_input "$CMD")
492
- run_hook "$INPUT"
493
- [ "$status" -eq 0 ]
494
- [ -z "$output" ]
495
- }
496
-
497
- @test "P364: escaped backslash adjacent to escaped backtick (\\\\\\\`) unescapes to literal backslash + backtick (single left-to-right pass)" {
498
- # Adjacency edge case (architect note): \\\` is escaped-backslash THEN
499
- # escaped-backtick and must unescape to a literal backslash followed by a
500
- # literal backtick — NOT mis-collapsed. The logical body therefore carries
501
- # one backslash then one backtick.
502
- LOGICAL=$'edge: \\`token here'
503
- SURFACE="gh-issue-comment"
504
- KEY=$(printf '%s\n%s' "$LOGICAL" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
505
- touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
506
-
507
- # Command text carries \\\` (escaped backslash + escaped backtick).
508
- CMD='gh issue comment 42 --body "edge: \\\`token here"'
509
- INPUT=$(build_bash_input "$CMD")
510
- run_hook "$INPUT"
511
- [ "$status" -eq 0 ]
512
- [ -z "$output" ]
513
- }
514
-
515
- @test "P364: single-quoted --body with literal backticks stays literal (no unescaping applied)" {
516
- # Inside single quotes bash does NOT process backslashes or backticks, so
517
- # the orchestrator writes plain backticks and the captured body already
518
- # equals the logical draft. The fix must NOT alter the single-quote path.
519
- LOGICAL='Fixed in `plain_span` here.'
520
- SURFACE="gh-issue-comment"
521
- KEY=$(printf '%s\n%s' "$LOGICAL" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
522
- touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
523
-
524
- INPUT=$(build_bash_input "gh issue comment 42 --body 'Fixed in \`plain_span\` here.'")
525
- run_hook "$INPUT"
526
- [ "$status" -eq 0 ]
527
- [ -z "$output" ]
528
- }
@@ -1,74 +0,0 @@
1
- #!/usr/bin/env bats
2
- # Behavioural tests for compute_external_comms_key substance-aware draft
3
- # normalization (P276 — external-comms gate marker over-fires on PASS-class
4
- # content edits).
5
- #
6
- # Contract (ADR-009 + ADR-028 amended 2026-06-06, ratified): the marker key
7
- # must survive TRIVIAL whitespace-class edits to the draft body — interior
8
- # CRLF/CR line endings and per-line trailing whitespace — so a PASS review
9
- # does not have to re-fire on a no-op reformat. The conservative boundary is
10
- # preserved: single-numeral edits and frontmatter-key changes remain
11
- # SUBSTANTIVE (the key changes → review re-fires) so the leak-detection
12
- # guarantee is never weakened.
13
- #
14
- # These exercise compute_external_comms_key directly (its stdout = the 64-char
15
- # sha256 key) — behavioural assertions on the function's output, not a
16
- # structural grep of the source.
17
-
18
- setup() {
19
- HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
20
- # shellcheck source=../lib/external-comms-key.sh
21
- source "$HOOKS_DIR/lib/external-comms-key.sh"
22
- }
23
-
24
- # ---------- Trivial edits must NOT change the key (P276) ----------
25
-
26
- @test "P276: interior CRLF line endings produce the same key as LF" {
27
- lf=$(compute_external_comms_key $'Line one\nLine two\nLine three' 'gh-issue-comment')
28
- crlf=$(compute_external_comms_key $'Line one\r\nLine two\r\nLine three' 'gh-issue-comment')
29
- [ -n "$lf" ]
30
- [ "$lf" = "$crlf" ]
31
- }
32
-
33
- @test "P276: per-line trailing whitespace produces the same key as clean lines" {
34
- clean=$(compute_external_comms_key $'Line one\nLine two' 'gh-issue-comment')
35
- trailing=$(compute_external_comms_key $'Line one \nLine two\t' 'gh-issue-comment')
36
- [ -n "$clean" ]
37
- [ "$clean" = "$trailing" ]
38
- }
39
-
40
- @test "P276: trailing whitespace at end of draft produces the same key (regression guard for prior rstrip)" {
41
- clean=$(compute_external_comms_key 'A short draft body' 'gh-issue-comment')
42
- trailed=$(compute_external_comms_key $'A short draft body\n\n ' 'gh-issue-comment')
43
- [ "$clean" = "$trailed" ]
44
- }
45
-
46
- # ---------- Conservative boundary: substantive edits MUST change the key ----------
47
-
48
- @test "P276 boundary: a single-numeral edit changes the key (review re-fires)" {
49
- before=$(compute_external_comms_key 'We support 12 plugins today' 'gh-issue-comment')
50
- after=$(compute_external_comms_key 'We support 11 plugins today' 'gh-issue-comment')
51
- [ "$before" != "$after" ]
52
- }
53
-
54
- @test "P276 boundary: a word/content edit changes the key (review re-fires)" {
55
- before=$(compute_external_comms_key 'The release is ready' 'gh-issue-comment')
56
- after=$(compute_external_comms_key 'The release is delayed' 'gh-issue-comment')
57
- [ "$before" != "$after" ]
58
- }
59
-
60
- # ---------- Surface binding + changeset frontmatter strip preserved ----------
61
-
62
- @test "P276: the surface remains part of the key (same body, different surface → different key)" {
63
- a=$(compute_external_comms_key 'Same body text' 'gh-issue-comment')
64
- b=$(compute_external_comms_key 'Same body text' 'gh-pr-comment')
65
- [ "$a" != "$b" ]
66
- }
67
-
68
- @test "P276: changeset frontmatter strip still holds — full content keys equal the body-only key" {
69
- body=$'Add the substance-aware normalization to the external-comms key.'
70
- full=$'---\n"@windyroad/risk-scorer": patch\n---\n\n'"$body"
71
- body_key=$(compute_external_comms_key "$body" 'changeset-author')
72
- full_key=$(compute_external_comms_key "$full" 'changeset-author')
73
- [ "$body_key" = "$full_key" ]
74
- }
@@ -1,82 +0,0 @@
1
- #!/usr/bin/env bats
2
- # Tests for git-push-gate.sh — gh pr merge block and release:watch guidance
3
-
4
- setup() {
5
- HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
6
- HOOK="$HOOKS_DIR/git-push-gate.sh"
7
-
8
- TEST_SESSION="bats-push-gate-$$-${BATS_TEST_NUMBER}"
9
- # Ensure a clean risk dir
10
- RDIR="${TMPDIR:-/tmp}/claude-risk-${TEST_SESSION}"
11
- rm -rf "$RDIR"
12
- mkdir -p "$RDIR"
13
-
14
- # Create a temp project dir for package.json detection
15
- TEST_PROJECT_DIR="$(mktemp -d)"
16
- }
17
-
18
- teardown() {
19
- rm -rf "$RDIR"
20
- rm -rf "$TEST_PROJECT_DIR"
21
- }
22
-
23
- # Helper: build a PreToolUse Bash input with a given command
24
- build_input() {
25
- local cmd="$1"
26
- cat <<ENDJSON
27
- {
28
- "session_id": "$TEST_SESSION",
29
- "tool_name": "Bash",
30
- "tool_input": {
31
- "command": "$cmd"
32
- }
33
- }
34
- ENDJSON
35
- }
36
-
37
- @test "gh pr merge is blocked with release:watch guidance when script exists" {
38
- # Create a package.json with release:watch
39
- cat > "$TEST_PROJECT_DIR/package.json" <<'PKG'
40
- { "scripts": { "release:watch": "bash scripts/release-watch.sh" } }
41
- PKG
42
-
43
- INPUT=$(build_input "gh pr merge 4 --merge")
44
- run bash -c "cd '$TEST_PROJECT_DIR' && echo '$INPUT' | '$HOOK'"
45
- [ "$status" -eq 0 ]
46
- [[ "$output" == *"permissionDecision"* ]]
47
- [[ "$output" == *"deny"* ]]
48
- [[ "$output" == *"release:watch"* ]]
49
- }
50
-
51
- @test "gh pr merge tells agent to create release:watch when script missing" {
52
- # Create a package.json WITHOUT release:watch
53
- cat > "$TEST_PROJECT_DIR/package.json" <<'PKG'
54
- { "scripts": { "test": "echo test" } }
55
- PKG
56
-
57
- INPUT=$(build_input "gh pr merge 4 --merge")
58
- run bash -c "cd '$TEST_PROJECT_DIR' && echo '$INPUT' | '$HOOK'"
59
- [ "$status" -eq 0 ]
60
- [[ "$output" == *"permissionDecision"* ]]
61
- [[ "$output" == *"deny"* ]]
62
- # Should tell agent to create the script
63
- [[ "$output" == *"no release:watch script"* ]]
64
- [[ "$output" == *"gh pr merge"* ]]
65
- [[ "$output" == *"gh run watch"* ]]
66
- }
67
-
68
- @test "gh pr merge tells agent to create release:watch when no package.json" {
69
- local empty_dir="$(mktemp -d)"
70
-
71
- INPUT=$(build_input "gh pr merge 4 --merge")
72
- run bash -c "cd '$empty_dir' && echo '$INPUT' | '$HOOK'"
73
- [ "$status" -eq 0 ]
74
- [[ "$output" == *"permissionDecision"* ]]
75
- [[ "$output" == *"deny"* ]]
76
- # Should tell agent to create the script
77
- [[ "$output" == *"no release:watch script"* ]]
78
- [[ "$output" == *"gh pr merge"* ]]
79
- [[ "$output" == *"gh run watch"* ]]
80
-
81
- rm -rf "$empty_dir"
82
- }