@windyroad/risk-scorer 0.11.2 → 0.11.3-preview.479
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.
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
# - gh api .../comments (any REST surface accepting prose)
|
|
15
15
|
# - npm publish (README / package metadata to npm)
|
|
16
16
|
# - PreToolUse:Write|Edit on .changeset/*.md (P073 — gates author-time)
|
|
17
|
+
# - git commit -m / --message (incl. HEREDOC) (P082 Phase 1 — commit message
|
|
18
|
+
# body reaches every reader of git
|
|
19
|
+
# log, PR commits tab, release-page
|
|
20
|
+
# auto-notes, CHANGELOG. Editor
|
|
21
|
+
# flow is out of scope per P082 SC1
|
|
22
|
+
# — message is written to
|
|
23
|
+
# .git/COMMIT_EDITMSG AFTER
|
|
24
|
+
# PreToolUse, nothing to read.)
|
|
17
25
|
#
|
|
18
26
|
# Gate behaviour:
|
|
19
27
|
# 1. BYPASS_RISK_GATE=1 short-circuits the gate (consistent with git-push-gate.sh).
|
|
@@ -144,20 +152,59 @@ except Exception:
|
|
|
144
152
|
SURFACE="gh-api-comments"
|
|
145
153
|
elif echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*npm publish(\s|$)'; then
|
|
146
154
|
SURFACE="npm-publish"
|
|
155
|
+
elif echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*git commit(\s|$)'; then
|
|
156
|
+
# P082 Phase 1: gate `git commit -m / --message / HEREDOC` so commit
|
|
157
|
+
# message bodies are reviewed by the voice-tone + risk evaluators
|
|
158
|
+
# before they land in git log / PR commits tab / release notes /
|
|
159
|
+
# CHANGELOG. Editor flow (bare `git commit`) is out of scope per
|
|
160
|
+
# P082 SC1 — git writes .git/COMMIT_EDITMSG AFTER PreToolUse fires,
|
|
161
|
+
# so there's no body to extract at gate time. Skip silently when
|
|
162
|
+
# neither -m nor --message is present.
|
|
163
|
+
if echo "$COMMAND" | grep -qE '(\s|^)(-m|--message)(\s|=)'; then
|
|
164
|
+
SURFACE="git-commit-message"
|
|
165
|
+
else
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
147
168
|
else
|
|
148
169
|
exit 0
|
|
149
170
|
fi
|
|
150
171
|
|
|
151
|
-
# Best-effort body extraction
|
|
152
|
-
#
|
|
153
|
-
#
|
|
172
|
+
# Best-effort body extraction. Order matters — most-specific first.
|
|
173
|
+
#
|
|
174
|
+
# HEREDOC first: `git commit -m "$(cat <<'EOF'\n...\nEOF\n)"` is the
|
|
175
|
+
# AI-dominant form. Must precede --body "..." / -m "..." because
|
|
176
|
+
# those would otherwise match the literal `$(cat <<'EOF'...EOF)`
|
|
177
|
+
# text as the body, defeating the marker key match against the
|
|
178
|
+
# subagent's <draft> body.
|
|
179
|
+
# Then --body / --field for the gh + npm + security-advisories surfaces.
|
|
180
|
+
# Then -m / --message for git commit (single-line literal forms).
|
|
181
|
+
#
|
|
182
|
+
# When absent (npm publish, --body-file, editor flow already filtered),
|
|
183
|
+
# DRAFT="" is acceptable: the agent will be invoked with command
|
|
184
|
+
# context and read whatever body source the call uses.
|
|
154
185
|
DRAFT=$(printf '%s' "$COMMAND" | python3 -c "
|
|
155
186
|
import sys, re
|
|
156
187
|
cmd = sys.stdin.read()
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
188
|
+
# (pattern, flags) — first match wins.
|
|
189
|
+
patterns = [
|
|
190
|
+
# HEREDOC body — matches a here-doc with EOF delimiter (quoted or
|
|
191
|
+
# unquoted). The literal '<<' is written as the char-class pair
|
|
192
|
+
# [<][<] so bash's command-substitution parser does NOT mis-parse
|
|
193
|
+
# this regex as a real here-doc operator (P082 implementation note).
|
|
194
|
+
# DOTALL so the body can span newlines.
|
|
195
|
+
(r\"[<][<]\s*['\\\"]?EOF['\\\"]?\s*\n(.*?)\nEOF\", re.DOTALL),
|
|
196
|
+
# gh issue/pr + npm publish --body 'TEXT' / --body \"TEXT\" (existing).
|
|
197
|
+
(r\"--body[= ]'([^']*)'\", 0),
|
|
198
|
+
(r'--body[= ]\"([^\"]*)\"', 0),
|
|
199
|
+
# gh api --field summary='TEXT' / --field summary=\"TEXT\" (existing).
|
|
200
|
+
(r\"--field [a-zA-Z_]+='([^']*)'\", 0),
|
|
201
|
+
(r'--field [a-zA-Z_]+=\"([^\"]*)\"', 0),
|
|
202
|
+
# git commit -m / --message single-line literal forms (P082 Phase 1).
|
|
203
|
+
(r\"(?:-m|--message)[= ]'([^']*)'\", 0),
|
|
204
|
+
(r'(?:-m|--message)[= ]\"([^\"]*)\"', 0),
|
|
205
|
+
]
|
|
206
|
+
for pat, flags in patterns:
|
|
207
|
+
m = re.search(pat, cmd, flags)
|
|
161
208
|
if m:
|
|
162
209
|
print(m.group(1))
|
|
163
210
|
break
|
|
@@ -224,3 +224,95 @@ run_hook() {
|
|
|
224
224
|
[ "$status" -eq 0 ]
|
|
225
225
|
[ -z "$output" ]
|
|
226
226
|
}
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# P082 Phase 1 — git commit message surface (risk evaluator).
|
|
230
|
+
# Commit messages reach git log / PR commits tab / release notes /
|
|
231
|
+
# CHANGELOG. The risk evaluator gates the body for leak patterns
|
|
232
|
+
# (credentials, prod URLs, business-context-paired financials/user counts)
|
|
233
|
+
# AND defers structured leak-free drafts to the wr-risk-scorer:external-comms
|
|
234
|
+
# subagent. Editor flow (bare `git commit`) is out of scope per P082 SC1.
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
@test "P082: git commit -m with leak-shaped credential body denies via leak pre-filter" {
|
|
238
|
+
INPUT=$(build_bash_input "git commit -m \"docs: token=${GH_TOKEN_LIKE}\"")
|
|
239
|
+
run_hook "$INPUT"
|
|
240
|
+
[ "$status" -eq 0 ]
|
|
241
|
+
[[ "$output" == *"deny"* ]]
|
|
242
|
+
[[ "$output" == *"git-commit-message"* ]]
|
|
243
|
+
[[ "$output" == *"GitHub token"* ]] || [[ "$output" == *"credential"* ]]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@test "P082: git commit -m with leak-free body denies and delegates to risk evaluator" {
|
|
247
|
+
INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
|
|
248
|
+
run_hook "$INPUT"
|
|
249
|
+
[ "$status" -eq 0 ]
|
|
250
|
+
[[ "$output" == *"deny"* ]]
|
|
251
|
+
[[ "$output" == *"git-commit-message"* ]]
|
|
252
|
+
[[ "$output" == *"wr-risk-scorer:external-comms"* ]]
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@test "P082: git commit --amend -m is intercepted (P082 SC2)" {
|
|
256
|
+
INPUT=$(build_bash_input "git commit --amend -m \"rewritten subject\"")
|
|
257
|
+
run_hook "$INPUT"
|
|
258
|
+
[ "$status" -eq 0 ]
|
|
259
|
+
[[ "$output" == *"deny"* ]]
|
|
260
|
+
[[ "$output" == *"git-commit-message"* ]]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@test "P082: git commit HEREDOC body is intercepted and the body becomes the marker key" {
|
|
264
|
+
# Build a HEREDOC-shaped command. The hook regex pulls the body BETWEEN
|
|
265
|
+
# the <<'EOF' opener and the closing EOF marker — the extracted DRAFT is
|
|
266
|
+
# the inner text, NOT the literal `$(cat <<'EOF' ... EOF)` wrapper.
|
|
267
|
+
BODY=$'feat(foo): add bar\n\nWe observed a build failure on Node 20.'
|
|
268
|
+
CMD=$'git commit -m "$(cat <<\'EOF\'\n'"$BODY"$'\nEOF\n)"'
|
|
269
|
+
INPUT=$(build_bash_input "$CMD")
|
|
270
|
+
run_hook "$INPUT"
|
|
271
|
+
[ "$status" -eq 0 ]
|
|
272
|
+
[[ "$output" == *"deny"* ]]
|
|
273
|
+
[[ "$output" == *"git-commit-message"* ]]
|
|
274
|
+
|
|
275
|
+
# Pre-place the per-evaluator marker keyed on the extracted HEREDOC body
|
|
276
|
+
# + the git-commit-message surface; the second run must permit silently.
|
|
277
|
+
SURFACE="git-commit-message"
|
|
278
|
+
KEY=$(printf '%s\n%s' "$BODY" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
|
|
279
|
+
touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
|
|
280
|
+
run_hook "$INPUT"
|
|
281
|
+
[ "$status" -eq 0 ]
|
|
282
|
+
[ -z "$output" ]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@test "P082: bare git commit (editor flow) is silently allowed per SC1" {
|
|
286
|
+
# No -m / --message → .git/COMMIT_EDITMSG doesn't exist at PreToolUse
|
|
287
|
+
# time. Phase 1 skip is pragmatic; the editor flow has user-eyeballs.
|
|
288
|
+
INPUT=$(build_bash_input "git commit")
|
|
289
|
+
run_hook "$INPUT"
|
|
290
|
+
[ "$status" -eq 0 ]
|
|
291
|
+
[ -z "$output" ]
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@test "P082: git merge is silently allowed (not a git commit verb, SC3)" {
|
|
295
|
+
INPUT=$(build_bash_input "git merge --no-ff feature-branch")
|
|
296
|
+
run_hook "$INPUT"
|
|
297
|
+
[ "$status" -eq 0 ]
|
|
298
|
+
[ -z "$output" ]
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@test "P082: BYPASS_RISK_GATE=1 short-circuits the git commit gate" {
|
|
302
|
+
INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
|
|
303
|
+
run bash -c "cd '$TEST_PROJECT_DIR' && BYPASS_RISK_GATE=1 printf '%s' \"\$1\" | BYPASS_RISK_GATE=1 '$HOOK'" _ "$INPUT"
|
|
304
|
+
[ "$status" -eq 0 ]
|
|
305
|
+
[ -z "$output" ]
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@test "P082: per-evaluator marker keyed on (body, git-commit-message) permits the call" {
|
|
309
|
+
BODY="docs(retro): close iter 3 ask-hygiene trail"
|
|
310
|
+
SURFACE="git-commit-message"
|
|
311
|
+
KEY=$(printf '%s\n%s' "$BODY" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
|
|
312
|
+
touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
|
|
313
|
+
|
|
314
|
+
INPUT=$(build_bash_input "git commit -m \"$BODY\"")
|
|
315
|
+
run_hook "$INPUT"
|
|
316
|
+
[ "$status" -eq 0 ]
|
|
317
|
+
[ -z "$output" ]
|
|
318
|
+
}
|
package/package.json
CHANGED
|
@@ -48,7 +48,6 @@ set -uo pipefail
|
|
|
48
48
|
PROJECT_ROOT="${1:-$(pwd)}"
|
|
49
49
|
QUEUE_FILE="${PROJECT_ROOT}/.afk-run-state/risk-register-queue.jsonl"
|
|
50
50
|
RISKS_DIR="${PROJECT_ROOT}/docs/risks"
|
|
51
|
-
TEMPLATE_FILE="${RISKS_DIR}/TEMPLATE.md"
|
|
52
51
|
README_FILE="${RISKS_DIR}/README.md"
|
|
53
52
|
|
|
54
53
|
emit_no_op() {
|
|
@@ -63,7 +62,7 @@ if [ ! -f "$QUEUE_FILE" ] || [ ! -s "$QUEUE_FILE" ]; then
|
|
|
63
62
|
exit 0
|
|
64
63
|
fi
|
|
65
64
|
|
|
66
|
-
if [ ! -d "$RISKS_DIR" ] || [ ! -f "$
|
|
65
|
+
if [ ! -d "$RISKS_DIR" ] || [ ! -f "$README_FILE" ]; then
|
|
67
66
|
emit_no_op
|
|
68
67
|
exit 0
|
|
69
68
|
fi
|
|
@@ -80,7 +79,7 @@ ORIGIN_MAX="${ORIGIN_MAX:-0}"
|
|
|
80
79
|
# so R099 → 099 → "value too great for base" without the 10# prefix.
|
|
81
80
|
NEXT_ID=$(( (10#$LOCAL_MAX > 10#$ORIGIN_MAX ? 10#$LOCAL_MAX : 10#$ORIGIN_MAX) + 1 ))
|
|
82
81
|
|
|
83
|
-
DRAIN_RESULT=$(python3 - "$QUEUE_FILE" "$RISKS_DIR" "$
|
|
82
|
+
DRAIN_RESULT=$(python3 - "$QUEUE_FILE" "$RISKS_DIR" "$README_FILE" "$NEXT_ID" "$PROJECT_ROOT" <<'PYEOF'
|
|
84
83
|
import json
|
|
85
84
|
import os
|
|
86
85
|
import re
|
|
@@ -88,7 +87,7 @@ import sys
|
|
|
88
87
|
from collections import OrderedDict
|
|
89
88
|
from datetime import datetime
|
|
90
89
|
|
|
91
|
-
queue_file, risks_dir,
|
|
90
|
+
queue_file, risks_dir, readme_file, next_id_str, project_root = sys.argv[1:6]
|
|
92
91
|
next_id = int(next_id_str)
|
|
93
92
|
|
|
94
93
|
hints = []
|
|
@@ -121,7 +120,7 @@ for h in hints:
|
|
|
121
120
|
|
|
122
121
|
existing = {}
|
|
123
122
|
for fn in os.listdir(risks_dir):
|
|
124
|
-
if fn
|
|
123
|
+
if fn == 'README.md':
|
|
125
124
|
continue
|
|
126
125
|
m = re.match(r'^R(\d+)-(.+)\.active\.md$', fn)
|
|
127
126
|
if m:
|
|
@@ -18,33 +18,13 @@ setup() {
|
|
|
18
18
|
git config user.email "drain-test@example.com"
|
|
19
19
|
git config user.name "Drain Test"
|
|
20
20
|
git commit --quiet --allow-empty -m "init"
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# an old-shape R001-...active.md inline so the drain contract is exercised
|
|
28
|
-
# end-to-end without depending on the canonical (post-wipe) state. The
|
|
29
|
-
# divergence between the drain script's expected R-file shape (.active.md
|
|
30
|
-
# with structured frontmatter) and the canonical post-wipe R-file shape
|
|
31
|
-
# (bare .md without status frontmatter, slug-only body) is captured as P171
|
|
32
|
-
# (docs/problems/171-drain-register-queue-script-and-tests-reference-
|
|
33
|
-
# obsolete-pre-wipe-r-file-shape.open.md). This synthetic-fixture pattern
|
|
34
|
-
# is the workaround until P171's fix lands.
|
|
21
|
+
# Seed README + a single R-file fixture matching the canonical .active.md
|
|
22
|
+
# shape. P171 resolved 2026-05-31: drain script's vestigial TEMPLATE.md gate
|
|
23
|
+
# was removed (it was a pre-wipe-direction residual). Canonical docs/risks/
|
|
24
|
+
# has NO TEMPLATE.md per the 2026-05-04 user direction (commit 8edaf7b) +
|
|
25
|
+
# the canonical .active.md suffix per commit 9b52610. Seeded R-file uses the
|
|
26
|
+
# canonical shape; tests no longer synthesize a fixture-local TEMPLATE.md.
|
|
35
27
|
mkdir -p docs/risks .afk-run-state
|
|
36
|
-
cat > docs/risks/TEMPLATE.md <<'TEMPLATE_EOF'
|
|
37
|
-
# Risk RNNN: <title>
|
|
38
|
-
|
|
39
|
-
**Status**: Active
|
|
40
|
-
**Category**: <category>
|
|
41
|
-
**Identified**: <YYYY-MM-DD>
|
|
42
|
-
**Owner**: <owner>
|
|
43
|
-
|
|
44
|
-
## Description
|
|
45
|
-
|
|
46
|
-
<description>
|
|
47
|
-
TEMPLATE_EOF
|
|
48
28
|
cp "$REPO_ROOT/docs/risks/README.md" docs/risks/README.md
|
|
49
29
|
cat > docs/risks/R001-confidential-info-leak-via-public-repo-push.active.md <<'R001_EOF'
|
|
50
30
|
# Risk R001: Confidential info leak via public repo push
|
|
@@ -325,3 +305,22 @@ EOF
|
|
|
325
305
|
echo "$output" | grep -q '^new_risks_created=1$'
|
|
326
306
|
[ -f docs/risks/R002-good-line.active.md ]
|
|
327
307
|
}
|
|
308
|
+
|
|
309
|
+
@test "drain succeeds against canonical (post-wipe) docs/risks/ with NO TEMPLATE.md (P171)" {
|
|
310
|
+
# P171 regression coverage. The 2026-05-04 wipe direction (commit 8edaf7b)
|
|
311
|
+
# removed TEMPLATE.md from canonical docs/risks/; commit 9b52610 then re-
|
|
312
|
+
# canonicalized the R-file suffix to .active.md. The drain script previously
|
|
313
|
+
# gated on TEMPLATE.md existence and would silent-no-op against the canonical
|
|
314
|
+
# (TEMPLATE.md-absent) state. This test asserts the gate is gone: a queue with
|
|
315
|
+
# one hint MUST materialize a register entry even without TEMPLATE.md.
|
|
316
|
+
rm -f docs/risks/TEMPLATE.md
|
|
317
|
+
cat > .afk-run-state/risk-register-queue.jsonl <<EOF
|
|
318
|
+
{"ts":"2026-05-03T14:00:00Z","session_id":"s1","report_path":".risk-reports/p171.md","reason_tag":"above-appetite-residual","risk_slug":"p171-canonical-fire","slug_source":"agent","prefill":"Canonical post-wipe drain works without TEMPLATE.md."}
|
|
319
|
+
EOF
|
|
320
|
+
run bash "$SCRIPT" "$WORK_DIR"
|
|
321
|
+
[ "$status" -eq 0 ]
|
|
322
|
+
echo "$output" | grep -q '^entries_drained=1$'
|
|
323
|
+
echo "$output" | grep -q '^new_risks_created=1$'
|
|
324
|
+
echo "$output" | grep -q '^next_action=commit-staged$'
|
|
325
|
+
[ -f docs/risks/R002-p171-canonical-fire.active.md ]
|
|
326
|
+
}
|