@windyroad/risk-scorer 0.11.3 → 0.12.0-preview.512

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.
@@ -310,5 +310,5 @@
310
310
  }
311
311
  },
312
312
  "name": "wr-risk-scorer",
313
- "version": "0.11.3"
313
+ "version": "0.12.0"
314
314
  }
@@ -1,2 +1,51 @@
1
1
  #!/usr/bin/env bash
2
- exec "$(dirname "$0")/../scripts/drain-register-queue.sh" "$@"
2
+ # Generated by scripts/sync-shim-wrappers.sh from
3
+ # packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
4
+ # shim files in packages/*/bin/wr-* directly; edit the template + run
5
+ # `npm run sync:shim-wrappers` to regenerate.
6
+ #
7
+ # Resolution (ADR-080):
8
+ # 1. If the wrapper's parent dir is semver-shaped, treat as installed-
9
+ # cache execution and resolve to the highest-version sibling's
10
+ # scripts/ entry below.
11
+ # 2. Otherwise (parent dir is e.g. `architect`), treat as source-
12
+ # monorepo execution and dispatch to own scripts/. The source-repo-
13
+ # guard `exec` is the anchor parsed by
14
+ # packages/retrospective/scripts/check-tarball-shipped-shims.sh.
15
+ # 3. If the cache parent contains zero semver-shaped siblings, exit
16
+ # 127 with a stderr message naming the cache parent (per SQ-080-2).
17
+ #
18
+ # @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
19
+ # @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
20
+ # @problem P343 (mid-session staleness window)
21
+
22
+ set -euo pipefail
23
+
24
+ SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
26
+ OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
27
+ CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
28
+
29
+ SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
30
+
31
+ # Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
32
+ if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
33
+ exec "$SHIM_DIR/../scripts/drain-register-queue.sh" "$@"
34
+ fi
35
+
36
+ # Cache execution: pick the highest-semver sibling under CACHE_PARENT.
37
+ HIGHEST=""
38
+ while IFS= read -r dir; do
39
+ name="$(basename "$dir")"
40
+ [[ "$name" =~ $SEMVER_RE ]] || continue
41
+ if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
42
+ HIGHEST="$name"
43
+ fi
44
+ done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
45
+
46
+ if [[ -z "$HIGHEST" ]]; then
47
+ printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
48
+ exit 127
49
+ fi
50
+
51
+ exec "$CACHE_PARENT/$HIGHEST/scripts/drain-register-queue.sh" "$@"
@@ -1,2 +1,51 @@
1
1
  #!/usr/bin/env bash
2
- exec "$(dirname "$0")/../scripts/evaluate-graduation.sh" "$@"
2
+ # Generated by scripts/sync-shim-wrappers.sh from
3
+ # packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
4
+ # shim files in packages/*/bin/wr-* directly; edit the template + run
5
+ # `npm run sync:shim-wrappers` to regenerate.
6
+ #
7
+ # Resolution (ADR-080):
8
+ # 1. If the wrapper's parent dir is semver-shaped, treat as installed-
9
+ # cache execution and resolve to the highest-version sibling's
10
+ # scripts/ entry below.
11
+ # 2. Otherwise (parent dir is e.g. `architect`), treat as source-
12
+ # monorepo execution and dispatch to own scripts/. The source-repo-
13
+ # guard `exec` is the anchor parsed by
14
+ # packages/retrospective/scripts/check-tarball-shipped-shims.sh.
15
+ # 3. If the cache parent contains zero semver-shaped siblings, exit
16
+ # 127 with a stderr message naming the cache parent (per SQ-080-2).
17
+ #
18
+ # @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
19
+ # @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
20
+ # @problem P343 (mid-session staleness window)
21
+
22
+ set -euo pipefail
23
+
24
+ SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
26
+ OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
27
+ CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
28
+
29
+ SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
30
+
31
+ # Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
32
+ if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
33
+ exec "$SHIM_DIR/../scripts/evaluate-graduation.sh" "$@"
34
+ fi
35
+
36
+ # Cache execution: pick the highest-semver sibling under CACHE_PARENT.
37
+ HIGHEST=""
38
+ while IFS= read -r dir; do
39
+ name="$(basename "$dir")"
40
+ [[ "$name" =~ $SEMVER_RE ]] || continue
41
+ if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
42
+ HIGHEST="$name"
43
+ fi
44
+ done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
45
+
46
+ if [[ -z "$HIGHEST" ]]; then
47
+ printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
48
+ exit 127
49
+ fi
50
+
51
+ exec "$CACHE_PARENT/$HIGHEST/scripts/evaluate-graduation.sh" "$@"
@@ -1,3 +1,51 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: dispatches to the canonical script body per ADR-049 ($PATH-resolved bin/).
3
- exec "$(dirname "$(readlink -f "${BASH_SOURCE[0]:-$0}")")/../scripts/extract-risks-from-reports.sh" "$@"
2
+ # Generated by scripts/sync-shim-wrappers.sh from
3
+ # packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
4
+ # shim files in packages/*/bin/wr-* directly; edit the template + run
5
+ # `npm run sync:shim-wrappers` to regenerate.
6
+ #
7
+ # Resolution (ADR-080):
8
+ # 1. If the wrapper's parent dir is semver-shaped, treat as installed-
9
+ # cache execution and resolve to the highest-version sibling's
10
+ # scripts/ entry below.
11
+ # 2. Otherwise (parent dir is e.g. `architect`), treat as source-
12
+ # monorepo execution and dispatch to own scripts/. The source-repo-
13
+ # guard `exec` is the anchor parsed by
14
+ # packages/retrospective/scripts/check-tarball-shipped-shims.sh.
15
+ # 3. If the cache parent contains zero semver-shaped siblings, exit
16
+ # 127 with a stderr message naming the cache parent (per SQ-080-2).
17
+ #
18
+ # @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
19
+ # @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
20
+ # @problem P343 (mid-session staleness window)
21
+
22
+ set -euo pipefail
23
+
24
+ SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
26
+ OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
27
+ CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
28
+
29
+ SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
30
+
31
+ # Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
32
+ if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
33
+ exec "$SHIM_DIR/../scripts/extract-risks-from-reports.sh" "$@"
34
+ fi
35
+
36
+ # Cache execution: pick the highest-semver sibling under CACHE_PARENT.
37
+ HIGHEST=""
38
+ while IFS= read -r dir; do
39
+ name="$(basename "$dir")"
40
+ [[ "$name" =~ $SEMVER_RE ]] || continue
41
+ if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
42
+ HIGHEST="$name"
43
+ fi
44
+ done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
45
+
46
+ if [[ -z "$HIGHEST" ]]; then
47
+ printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
48
+ exit 127
49
+ fi
50
+
51
+ exec "$CACHE_PARENT/$HIGHEST/scripts/extract-risks-from-reports.sh" "$@"
@@ -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: --body 'TEXT' or --body "TEXT" or --field summary='TEXT'.
152
- # When absent (npm publish, --body-file), DRAFT="" is acceptable: the agent will
153
- # be invoked with command context and read whatever body source the call uses.
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
- # Match --body '...' or --body \"...\" or --field summary='...'
158
- for pat in [r\"--body[= ]'([^']*)'\", r'--body[= ]\"([^\"]*)\"',
159
- r\"--field [a-zA-Z_]+='([^']*)'\", r'--field [a-zA-Z_]+=\"([^\"]*)\"']:
160
- m = re.search(pat, cmd)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.11.3",
3
+ "version": "0.12.0-preview.512",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "bin": {
6
6
  "windyroad-risk-scorer": "./bin/install.mjs"