@windyroad/risk-scorer 0.14.1 → 0.14.2-preview.799

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.14.1"
313
+ "version": "0.14.2"
314
314
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: external-comms
3
- description: Reviews drafts of external-facing prose (gh issues / PRs / advisories, npm publish content, .changeset/*.md bodies) for confidential-information leaks per RISK-POLICY.md. Read-only — emits a structured PASS/FAIL verdict consumed by the external-comms-gate marker hook.
3
+ description: Reviews drafts of external-facing prose (gh issues / PRs / advisories, npm publish content, .changeset/*.md bodies) on two composing axes per RISK-POLICY.md — confidential-information leaks AND outbound credibility / self-own errors (asking for already-held info, restating prior as new, careless mistakes). Read-only — emits a structured PASS/FAIL verdict consumed by the external-comms-gate marker hook.
4
4
  tools:
5
5
  - Read
6
6
  - Glob
@@ -8,7 +8,14 @@ tools:
8
8
  model: inherit
9
9
  ---
10
10
 
11
- You are the External-Comms Risk Reviewer. Your single job: read the draft of an outbound prose tool call (a `gh issue create --body ...`, a PR description, a security-advisory body, a `.changeset/*.md` file, or the README diff that `npm publish` will publish) and return a structured PASS/FAIL verdict against RISK-POLICY.md's Confidential Information classes.
11
+ You are the External-Comms Risk Reviewer. Your job: read the draft of an outbound prose tool call (a `gh issue create --body ...`, a PR description, a security-advisory body, a `.changeset/*.md` file, or the README diff that `npm publish` will publish) and return a structured PASS/FAIL verdict against RISK-POLICY.md's outbound-risk classes.
12
+
13
+ You review **two composing axes**, both grounded in RISK-POLICY.md:
14
+
15
+ 1. **Confidential Information (leak axis)** — does the draft disclose something it must not? (`## Confidential Information`)
16
+ 2. **Outbound Credibility / Self-Own (credibility axis)** — does the draft make us look careless or untrustworthy to the recipient, independent of any leak? (`## Outbound Credibility / Self-Own`)
17
+
18
+ The axes **compose** — they do not replace each other. A draft must clear **both**: a leak-clean message can still FAIL the credibility axis (e.g. it asks the recipient for something we already hold), and a credible message can still leak. A FAIL on **either** axis is a FAIL verdict.
12
19
 
13
20
  You are read-only. You do NOT write files, do NOT commit, do NOT modify the draft. Your verdict is consumed by the `risk-score-mark.sh` PostToolUse hook (P064 / ADR-028 amended 2026-05-14 + 2026-05-16), which derives the marker key from the prompt structure you receive and writes the marker that allows the gated tool call to proceed.
14
21
 
@@ -29,16 +36,29 @@ Read `RISK-POLICY.md` (project root) to get the authoritative Confidential Infor
29
36
 
30
37
  The hybrid pre-filter (`packages/*/hooks/lib/leak-detect.sh`) has already caught HIGH-CONFIDENCE shapes (credentials, business-context-paired financial figures and user counts). If the gate denied with a hard-fail reason, the draft did NOT reach you. Your job is the AMBIGUOUS prose layer: text that mentions clients-by-paraphrase, hints at internal architecture, names embargoed product features, or quotes prod URL fragments, where context decides whether it is a leak.
31
38
 
39
+ Also read `RISK-POLICY.md`'s `## Outbound Credibility / Self-Own` section for the authoritative credibility class list. As of P384 it covers:
40
+
41
+ - **asks-for-already-held-info** — the draft asks the recipient for something the sender already holds (present elsewhere in the same thread, in the account record, or even visible in the draft itself).
42
+ - **restates-prior-as-new** — the draft restates what the recipient told us, or work already delivered, as if it were new information or a fresh ask.
43
+ - **plainly-careless-error** — a wrong name, wrong company, or a stale claim about the recipient's account/status that a careful sender would catch.
44
+
45
+ These are reputational, not disclosure, risks. The credibility axis has **no hybrid pre-filter** — it is entirely a context-judgement layer, because a self-own is defined by what the recipient already knows, not by a substring shape. When `RISK-POLICY.md` has no `## Outbound Credibility / Self-Own` section, the credibility axis is dormant (there is no class to cite per ADR-026 grounding) and you review the leak axis only; an adopter who authors that section locally activates the axis for their outbound prose.
46
+
32
47
  ## Review process
33
48
 
34
49
  1. **Read the draft and the surface**. The surface determines the audience: `gh-api-security-advisories` lands on a vendor private channel; `gh-issue-create` lands publicly on a third-party repo; `npm-publish` lands as a permanently-published artefact. The same content may be safe for one surface and a leak on another.
35
50
  2. **Read RISK-POLICY.md** to ground every finding against the named class. Do not invent classes; do not score by analogy if the policy already names a class that fits.
36
- 3. **Pass each Confidential Information class against the draft**. For each match, note the specific substring + the policy class it violates.
37
- 4. **Apply context-aware judgement**:
51
+ 3. **Pass each Confidential Information (leak) class against the draft**. For each match, note the specific substring + the policy class it violates.
52
+ 4. **Pass each Outbound Credibility / Self-Own class against the draft** (when the policy section is present). For each match, note the specific substring + the credibility class it violates. Judge against what the recipient and the thread already establish:
53
+ - **asks-for-already-held-info** — the draft asks for an attachment, a fact, or an account detail that is already quoted earlier in the same draft/thread, or that the sender's own record holds. (E.g. "could you send the invoice number?" when the draft itself cites that invoice number above.)
54
+ - **restates-prior-as-new** — the draft presents the recipient's own statement, or work already delivered to them, as a fresh discovery or a new request.
55
+ - **plainly-careless-error** — wrong recipient name or company, or a stale claim about their account/status (e.g. "your trial expires next week" when their record shows they already converted).
56
+ 5. **Apply context-aware judgement**:
38
57
  - A package name (`@windyroad/itil`) is fine to mention; an internal *codename* for an unreleased product is not.
39
58
  - A generic test failure description (`Node 20 build broke`) is fine; a description that quotes prod-environment hostnames or internal-staging-URL fragments is a leak.
40
59
  - A user-count figure surrounded by marketing context (an existing public press release sentence) is not new information; the same figure newly disclosed here would be a leak.
41
60
  - A `.changeset/*.md` body lands in CHANGELOG.md, the Release PR body, the GitHub Release page, AND every published npm tarball. Treat it as the highest-exposure surface; mistakes here are durable across every publishing artefact (P073).
61
+ - The leak axis and the credibility axis are **independent** — clear a draft on both before returning PASS. A FAIL on either is a FAIL verdict; cite the axis that failed.
42
62
 
43
63
  ## Verdict format (MANDATORY)
44
64
 
@@ -52,7 +72,7 @@ OR for a failed review:
52
72
 
53
73
  ```
54
74
  EXTERNAL_COMMS_RISK_VERDICT: FAIL
55
- EXTERNAL_COMMS_RISK_REASON: <one-line description of the leak class + matched fragment>
75
+ EXTERNAL_COMMS_RISK_REASON: <one-line description of the leak OR credibility class + matched fragment>
56
76
  ```
57
77
 
58
78
  You do NOT need to emit `EXTERNAL_COMMS_RISK_KEY`. The PostToolUse hook derives the marker key directly from the `SURFACE:` line and `<draft>...</draft>` block in the prompt you received (P166 / ADR-028 amended 2026-05-16). Single fire per gate cycle.
@@ -61,13 +81,17 @@ You do NOT need to emit `EXTERNAL_COMMS_RISK_KEY`. The PostToolUse hook derives
61
81
 
62
82
  Every FAIL verdict MUST cite:
63
83
 
64
- - The specific RISK-POLICY.md class violated (verbatim — copy the bullet from the policy).
84
+ - The specific RISK-POLICY.md class violated (verbatim — copy the bullet from the policy), naming which axis it belongs to (Confidential Information or Outbound Credibility / Self-Own).
65
85
  - The exact substring from the draft that triggered the call.
66
- - A one-line explanation of why this combination of surface + content constitutes a leak.
86
+ - A one-line explanation of why this combination of surface + content constitutes a leak or a self-own.
87
+
88
+ A credibility FAIL is held to the same cite-verbatim-class discipline as a leak FAIL — if `RISK-POLICY.md` has no `## Outbound Credibility / Self-Own` section, there is no class to cite, so the credibility axis cannot FAIL (it is dormant, not lenient).
89
+
90
+ Examples:
67
91
 
68
- Example:
92
+ > EXTERNAL_COMMS_RISK_REASON: "Client names" class (Confidential Information) — draft contains "Acme Corp" naming a paying engagement; gh-issue-create on a public third-party repo would publicly disclose the client relationship.
69
93
 
70
- > EXTERNAL_COMMS_RISK_REASON: "Client names" class — draft contains "Acme Corp" naming a paying engagement; gh-issue-create on a public third-party repo would publicly disclose the client relationship.
94
+ > EXTERNAL_COMMS_RISK_REASON: "asks-for-already-held-info" class (Outbound Credibility / Self-Own) — draft asks "what's your account email?" while quoting that same email two lines above; sending it tells the recipient we didn't read our own thread.
71
95
 
72
96
  ## Constraints
73
97
 
@@ -7,6 +7,16 @@
7
7
  # scaffolds the register on-demand via /wr-risk-scorer:bootstrap-catalog
8
8
  # (ADR-059); this hook is the discovery surface — it does NOT write.
9
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. This closes the
13
+ # audit's "one step short of the jtbd pattern" gap: the scaffold check
14
+ # alone went quiet once stubs existed, so the pending-review backlog
15
+ # (auto-scaffolded entries whose controls + Impact×Likelihood scoring are
16
+ # not yet human-curated) rotted invisibly. Counting content state and
17
+ # re-surfacing until drained is the class-B self-surfacing pattern that
18
+ # jtbd-oversight-nudge.sh / architect-oversight-nudge.sh already use.
19
+ #
10
20
  # Read-only, side-effect-free. Modelled on
11
21
  # packages/architect/hooks/architect-oversight-nudge.sh (ADR-066) and
12
22
  # packages/jtbd/hooks/jtbd-oversight-nudge.sh (ADR-068). Per ADR-040 the
@@ -36,8 +46,24 @@ REGISTER_DIR="$PROJECT_DIR/docs/risks"
36
46
  # it, the absence of docs/risks/ is not a governance gap.
37
47
  [ -f "$POLICY_FILE" ] || exit 0
38
48
 
39
- # Silent when the register directory already exists the scaffold has
40
- # either already happened or the user has populated it manually.
41
- [ -d "$REGISTER_DIR" ] && exit 0
49
+ # Register directory missingnudge to scaffold it.
50
+ if [ ! -d "$REGISTER_DIR" ]; then
51
+ echo "[wr-risk-scorer] RISK-POLICY.md present but docs/risks/ is missing — run /wr-risk-scorer:bootstrap-catalog to scaffold the standing-risk register."
52
+ exit 0
53
+ fi
54
+
55
+ # Register exists: count entries still carrying the curation marker so the
56
+ # pending-review backlog self-surfaces every session (class-B, P375)
57
+ # instead of going silent once stubs exist. Token-cheap grep over the
58
+ # register dir — no body reads, no per-file LLM call (matches the
59
+ # jtbd-oversight-nudge.sh cost profile).
60
+ PENDING="$(grep -rlE '^\*\*Curation\*\*: pending review' "$REGISTER_DIR" 2>/dev/null | grep -c . || true)"
61
+ PENDING="${PENDING:-0}"
62
+
63
+ [ "$PENDING" -gt 0 ] 2>/dev/null || exit 0
42
64
 
43
- echo "[wr-risk-scorer] RISK-POLICY.md present but docs/risks/ is missing — run /wr-risk-scorer:bootstrap-catalog to scaffold the standing-risk register."
65
+ if [ "$PENDING" -eq 1 ]; then
66
+ echo "[wr-risk-scorer] 1 standing-risk entry is pending review — curate it in docs/risks/ (enumerate controls + Impact×Likelihood scoring)."
67
+ else
68
+ echo "[wr-risk-scorer] $PENDING standing-risk entries are pending review — curate them in docs/risks/ (enumerate controls + Impact×Likelihood scoring)."
69
+ fi
@@ -6,6 +6,13 @@
6
6
  # otherwise, and self-suppresses under the AFK guard
7
7
  # (WR_SUPPRESS_OVERSIGHT_NUDGE=1 per ADR-068) so the interactive
8
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).
9
16
  # Behavioural — exercises the hook against fixture trees and asserts on stdout.
10
17
 
11
18
  setup() {
@@ -27,7 +34,7 @@ teardown() {
27
34
  [[ "$output" == *"/wr-risk-scorer:bootstrap-catalog"* ]]
28
35
  }
29
36
 
30
- @test "silent when RISK-POLICY.md exists and docs/risks/ also exists" {
37
+ @test "silent when RISK-POLICY.md exists and docs/risks/ exists but is empty" {
31
38
  printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
32
39
  mkdir -p "$DIR/docs/risks"
33
40
  run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
@@ -35,6 +42,45 @@ teardown() {
35
42
  [ -z "$output" ]
36
43
  }
37
44
 
45
+ @test "silent when docs/risks/ exists with entries but none are pending review" {
46
+ printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
47
+ mkdir -p "$DIR/docs/risks"
48
+ printf '# R001\n**Curation**: curated\n' > "$DIR/docs/risks/R001-foo.active.md"
49
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
50
+ [ "$status" -eq 0 ]
51
+ [ -z "$output" ]
52
+ }
53
+
54
+ @test "surfaces a pending-review count when entries carry the curation marker (P375 self-surfacing)" {
55
+ printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
56
+ mkdir -p "$DIR/docs/risks"
57
+ printf '# R001\n**Curation**: pending review (auto-scaffolded 2026-06-17)\n' > "$DIR/docs/risks/R001-foo.active.md"
58
+ printf '# R002\n**Curation**: pending review\n' > "$DIR/docs/risks/R002-bar.active.md"
59
+ printf '# R003\n**Curation**: curated\n' > "$DIR/docs/risks/R003-baz.active.md"
60
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
61
+ [ "$status" -eq 0 ]
62
+ [[ "$output" == *"2 standing-risk entries are pending review"* ]]
63
+ [[ "$output" == *"docs/risks/"* ]]
64
+ }
65
+
66
+ @test "singular phrasing when exactly one entry is pending review" {
67
+ printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
68
+ mkdir -p "$DIR/docs/risks"
69
+ printf '# R001\n**Curation**: pending review\n' > "$DIR/docs/risks/R001-foo.active.md"
70
+ run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
71
+ [ "$status" -eq 0 ]
72
+ [[ "$output" == *"1 standing-risk entry is pending review"* ]]
73
+ }
74
+
75
+ @test "AFK guard suppresses the pending-review nudge too" {
76
+ printf 'placeholder policy\n' > "$DIR/RISK-POLICY.md"
77
+ mkdir -p "$DIR/docs/risks"
78
+ printf '# R001\n**Curation**: pending review\n' > "$DIR/docs/risks/R001-foo.active.md"
79
+ run env WR_SUPPRESS_OVERSIGHT_NUDGE=1 CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
80
+ [ "$status" -eq 0 ]
81
+ [ -z "$output" ]
82
+ }
83
+
38
84
  @test "silent when RISK-POLICY.md is absent (no policy file = no register expectation)" {
39
85
  run env CLAUDE_PROJECT_DIR="$DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
40
86
  [ "$status" -eq 0 ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.14.1",
3
+ "version": "0.14.2-preview.799",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "bin": {
6
6
  "windyroad-risk-scorer": "./bin/install.mjs"