@windyroad/risk-scorer 0.4.0 → 0.4.1

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "wr-risk-scorer",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Pipeline risk scoring, commit/push/release gates for Claude Code"
5
5
  }
package/hooks/hooks.json CHANGED
@@ -14,7 +14,8 @@
14
14
  "PostToolUse": [
15
15
  { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/wip-risk-mark.sh" }] },
16
16
  { "matcher": "Agent", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-score-mark.sh" }] },
17
- { "matcher": "Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-hash-refresh.sh" }] }
17
+ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-hash-refresh.sh" }] },
18
+ { "matcher": "Agent|Bash", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-slide-marker.sh" }] }
18
19
  ]
19
20
  }
20
21
  }
@@ -153,6 +153,56 @@ _risk_dir() {
153
153
  echo "$dir"
154
154
  }
155
155
 
156
+ # ---------------------------------------------------------------------------
157
+ # Subprocess-completion marker slide (P111, ADR-009 amendment)
158
+ # ---------------------------------------------------------------------------
159
+
160
+ # Slides an existing session-review marker forward on subprocess return,
161
+ # treating subprocess wall-clock as continuous parent-session work for TTL
162
+ # purposes. Intended for PostToolUse hooks on Agent / Bash that may have
163
+ # been long-running subprocesses (Agent-tool delegations, `claude -p`
164
+ # iteration subprocesses, run_in_background completions).
165
+ #
166
+ # Contract:
167
+ # - Touches the marker ONLY if it already exists. NEVER creates a marker
168
+ # (creating requires a real gate review with verdict parsing).
169
+ # - Skips the touch if tool_response.is_error == true. A failed
170
+ # subprocess MUST NOT extend the parent's trust window.
171
+ # - Fail-safe on parse error: if _HOOK_INPUT cannot be parsed, treat as
172
+ # error and skip the touch.
173
+ # - No-op when marker path is empty or marker file does not exist.
174
+ #
175
+ # Why this is NOT cross-process marker sharing (ADR-032 line 123 invariant):
176
+ # the parent's PostToolUse hook touches the parent's OWN marker. The
177
+ # subprocess's session id, marker, and gate state are never read or shared.
178
+ # This is identical in shape to the existing PreToolUse:Edit slide; only
179
+ # the trigger expands to subprocess return.
180
+ #
181
+ # Usage: slide_marker_on_subprocess_return "/tmp/architect-reviewed-${SESSION_ID}"
182
+ slide_marker_on_subprocess_return() {
183
+ local MARKER="$1"
184
+ [ -n "$MARKER" ] || return 0
185
+ [ -f "$MARKER" ] || return 0
186
+
187
+ local IS_ERROR
188
+ IS_ERROR=$(echo "$_HOOK_INPUT" | python3 -c "
189
+ import sys, json
190
+ try:
191
+ data = json.load(sys.stdin)
192
+ tr = data.get('tool_response', {})
193
+ if isinstance(tr, dict):
194
+ print('true' if tr.get('is_error') is True else 'false')
195
+ else:
196
+ print('false')
197
+ except Exception:
198
+ print('true')
199
+ " 2>/dev/null || echo "true")
200
+
201
+ if [ "$IS_ERROR" = "false" ]; then
202
+ touch "$MARKER"
203
+ fi
204
+ }
205
+
156
206
  # ---------------------------------------------------------------------------
157
207
  # Non-doc file detection for WIP gating
158
208
  # ---------------------------------------------------------------------------
@@ -0,0 +1,40 @@
1
+ #!/bin/bash
2
+ # Risk Scorer - PostToolUse:Agent|Bash slide-marker hook (P111).
3
+ # Slides the parent session's existing risk score files forward on
4
+ # subprocess return, treating subprocess wall-clock as continuous parent-
5
+ # session work for TTL purposes. Only TOUCHES existing score files — never
6
+ # creates one (creation requires a real risk-scorer:pipeline run that emits
7
+ # RISK_SCORES, parsed in risk-score-mark.sh).
8
+ #
9
+ # Score files that carry TTL semantics (Band A/B/C policy in risk-gate.sh):
10
+ # ${RDIR}/commit, ${RDIR}/push, ${RDIR}/release.
11
+ #
12
+ # Files DELIBERATELY NOT slid:
13
+ # - ${RDIR}/*-born — birth timestamps for the 2×TTL hard-cap (P090). The
14
+ # hard-cap is meant to be invariant under sliding so an unchanged-but-
15
+ # idle tree cannot ride a single score indefinitely (ADR-009 footnote
16
+ # "Three-band TTL refinement"). Sliding the born marker would defeat
17
+ # that protection.
18
+ # - ${RDIR}/state-hash — drift-detection hash, not TTL-governed.
19
+ # - ${RDIR}/{reducing,incident}-* — bypass markers, presence-only.
20
+ # - ${RDIR}/{plan,wip,policy}-reviewed — presence-only review markers.
21
+ #
22
+ # See ADR-009 "Subprocess-boundary refresh" and P111 for context. Failed
23
+ # subprocesses (tool_response.is_error=true) do NOT extend the trust window
24
+ # — see slide_marker_on_subprocess_return in lib/gate-helpers.sh.
25
+
26
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
27
+ source "$SCRIPT_DIR/lib/gate-helpers.sh"
28
+
29
+ _parse_input
30
+
31
+ SESSION_ID=$(_get_session_id)
32
+ [ -n "$SESSION_ID" ] || exit 0
33
+
34
+ RDIR=$(_risk_dir "$SESSION_ID")
35
+
36
+ slide_marker_on_subprocess_return "${RDIR}/commit"
37
+ slide_marker_on_subprocess_return "${RDIR}/push"
38
+ slide_marker_on_subprocess_return "${RDIR}/release"
39
+
40
+ exit 0
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Tests for slide_marker_on_subprocess_return helper (P111).
4
+ #
5
+ # Behavioural contract:
6
+ # - Slides an existing marker forward (touch) on PostToolUse:Agent|Bash
7
+ # completion, treating subprocess wall-clock as continuous parent-session
8
+ # work for TTL purposes.
9
+ # - Never CREATES a marker (creating requires a real gate review).
10
+ # - Skips slide on subprocess error (tool_response.is_error=true) so a failed
11
+ # subprocess does NOT extend the parent's trust window (ADR-009 amendment).
12
+ # - No-op when no marker exists or session_id is empty (fail-safe).
13
+
14
+ setup() {
15
+ HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
16
+ source "$HOOKS_DIR/lib/gate-helpers.sh"
17
+
18
+ TEST_SESSION="bats-slide-$$-${BATS_TEST_NUMBER}"
19
+ MARKER="/tmp/architect-reviewed-${TEST_SESSION}"
20
+ rm -f "$MARKER"
21
+ }
22
+
23
+ teardown() {
24
+ rm -f "$MARKER"
25
+ }
26
+
27
+ # Helper: backdate file mtime by N seconds (portable between macOS and Linux)
28
+ _backdate() {
29
+ local file="$1" seconds="$2"
30
+ local stamp
31
+ stamp=$(date -v-${seconds}S +%Y%m%d%H%M.%S 2>/dev/null \
32
+ || date -d "${seconds} seconds ago" +%Y%m%d%H%M.%S 2>/dev/null)
33
+ touch -t "$stamp" "$file"
34
+ }
35
+
36
+ @test "slide: existing marker is touched on success response" {
37
+ touch "$MARKER"
38
+ _backdate "$MARKER" 60
39
+ BEFORE=$(_mtime "$MARKER")
40
+ _HOOK_INPUT='{"tool_response":{"content":[]}}'
41
+ slide_marker_on_subprocess_return "$MARKER"
42
+ AFTER=$(_mtime "$MARKER")
43
+ [ "$AFTER" -gt "$BEFORE" ]
44
+ }
45
+
46
+ @test "slide: long-running subprocess does NOT cause parent marker expiry on return (P111 reproduction)" {
47
+ # Simulate the P111 failure mode: parent's marker is set, then a long
48
+ # subprocess runs (we backdate the marker to simulate elapsed wall-clock),
49
+ # then PostToolUse fires with a successful tool_response. The marker mtime
50
+ # must be refreshed so the parent's NEXT PreToolUse gate check (which
51
+ # compares NOW - mtime against TTL) sees a fresh marker.
52
+ touch "$MARKER"
53
+ # Marker is 50 minutes old — under default 60-min TTL but close to expiry.
54
+ # Without the slide on subprocess return, a subsequent 15-min subprocess
55
+ # would push the mtime past TTL and the next PreToolUse would deny.
56
+ _backdate "$MARKER" 3000
57
+ BEFORE=$(_mtime "$MARKER")
58
+ _HOOK_INPUT='{"tool_response":{"content":[{"type":"text","text":"OK"}]}}'
59
+ slide_marker_on_subprocess_return "$MARKER"
60
+ AFTER=$(_mtime "$MARKER")
61
+ NOW=$(date +%s)
62
+ [ "$AFTER" -gt "$BEFORE" ]
63
+ # And the new mtime is approximately NOW (within 5 seconds of slide call)
64
+ AGE=$((NOW - AFTER))
65
+ [ "$AGE" -lt 5 ]
66
+ }
67
+
68
+ @test "slide: does NOT touch marker when tool_response.is_error=true" {
69
+ touch "$MARKER"
70
+ _backdate "$MARKER" 60
71
+ BEFORE=$(_mtime "$MARKER")
72
+ _HOOK_INPUT='{"tool_response":{"is_error":true,"content":[]}}'
73
+ slide_marker_on_subprocess_return "$MARKER"
74
+ AFTER=$(_mtime "$MARKER")
75
+ [ "$BEFORE" = "$AFTER" ]
76
+ }
77
+
78
+ @test "slide: no-op when marker does not exist (never creates)" {
79
+ [ ! -f "$MARKER" ]
80
+ _HOOK_INPUT='{"tool_response":{"content":[]}}'
81
+ slide_marker_on_subprocess_return "$MARKER"
82
+ [ ! -f "$MARKER" ]
83
+ }
84
+
85
+ @test "slide: no-op when marker path argument is empty" {
86
+ _HOOK_INPUT='{"tool_response":{"content":[]}}'
87
+ run slide_marker_on_subprocess_return ""
88
+ [ "$status" -eq 0 ]
89
+ }
90
+
91
+ @test "slide: malformed hook input is fail-safe (no slide)" {
92
+ touch "$MARKER"
93
+ _backdate "$MARKER" 60
94
+ BEFORE=$(_mtime "$MARKER")
95
+ _HOOK_INPUT='not valid json'
96
+ slide_marker_on_subprocess_return "$MARKER"
97
+ AFTER=$(_mtime "$MARKER")
98
+ # Fail-safe: when the hook input cannot be parsed, treat as error and skip
99
+ [ "$BEFORE" = "$AFTER" ]
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "bin": {
6
6
  "windyroad-risk-scorer": "./bin/install.mjs"