@windyroad/risk-scorer 0.19.12 → 0.19.13-preview.1285

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.19.12"
313
+ "version": "0.19.13"
314
314
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wr-risk-scorer",
3
- "version": "0.19.12",
3
+ "version": "0.19.13",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "author": {
6
6
  "name": "Windy Road Technology",
@@ -159,6 +159,7 @@ EOF
159
159
  # ---------- Surface detection ----------
160
160
  SURFACE=""
161
161
  DRAFT=""
162
+ BODY_FILE_ERROR=0
162
163
 
163
164
  case "$TOOL_NAME" in
164
165
  Bash)
@@ -169,6 +170,15 @@ try:
169
170
  except Exception:
170
171
  print('')
171
172
  " 2>/dev/null || echo "")
173
+ TOOL_CWD=$(printf '%s' "$INPUT" | python3 -c "
174
+ import sys, json
175
+ try:
176
+ event = json.load(sys.stdin)
177
+ tool = event.get('tool_input', {})
178
+ print(tool.get('cwd') or tool.get('workdir') or event.get('cwd') or '')
179
+ except Exception:
180
+ print('')
181
+ " 2>/dev/null || echo "")
172
182
 
173
183
  # Surface match — most-specific first.
174
184
  if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*gh issue create(\s|$)'; then
@@ -230,13 +240,78 @@ except Exception:
230
240
  # Then --body / --field for the gh + npm + security-advisories surfaces.
231
241
  # Then -m / --message for git commit (single-line literal forms).
232
242
  #
233
- # When absent (npm publish, --body-file, editor flow already filtered),
234
- # DRAFT="" is acceptable: the agent will be invoked with command
235
- # context and read whatever body source the call uses.
236
- DRAFT=$(printf '%s' "$COMMAND" | python3 -c "
237
- import sys, re
243
+ # When absent (npm publish, editor flow already filtered), DRAFT=""
244
+ # is acceptable. A named body file must be readable before review.
245
+ if ! DRAFT=$(printf '%s' "$COMMAND" | python3 -c "
246
+ import sys, re, os, shlex
238
247
  cmd = sys.stdin.read()
239
248
  surface = sys.argv[1]
249
+ tool_cwd = sys.argv[2]
250
+ if surface.startswith('gh-'):
251
+ try:
252
+ lexer = shlex.shlex(cmd, posix=True, punctuation_chars=';&|')
253
+ lexer.whitespace_split = True
254
+ lexer.commenters = ''
255
+ args = list(lexer)
256
+ except ValueError:
257
+ if '--body-file' in cmd or ' -F' in cmd or ' -b' in cmd:
258
+ raise SystemExit(2)
259
+ args = []
260
+ try:
261
+ base = os.path.abspath(tool_cwd) if os.path.isabs(tool_cwd) else ''
262
+ if len(args) >= 4 and args[0] == 'cd' and args[2] == '&&':
263
+ if os.path.isabs(args[1]):
264
+ base = os.path.abspath(args[1])
265
+ elif base:
266
+ base = os.path.abspath(os.path.join(base, args[1]))
267
+ args = args[3:]
268
+ # Skip known option values so a quoted title/body beginning with -F
269
+ # cannot be mistaken for a second body source.
270
+ valued = {'--title', '-t', '--template', '-T', '--repo', '-R',
271
+ '--assignee', '-a', '--label', '-l', '--reviewer', '-r',
272
+ '--milestone', '-m', '--project', '-p', '--base', '-B', '--head', '-H'}
273
+ sources = []
274
+ i = 3
275
+ while i < len(args):
276
+ arg = args[i]
277
+ if arg and all(ch in ';&|' for ch in arg):
278
+ raise ValueError('ambiguous command')
279
+ if arg in ('--body-file', '-F', '--body', '-b'):
280
+ sources.append(('file' if arg in ('--body-file', '-F') else 'body',
281
+ args[i + 1] if i + 1 < len(args) else '', arg))
282
+ i += 2
283
+ elif arg.startswith('--body-file=') or arg.startswith('--body='):
284
+ sources.append(('file' if arg.startswith('--body-file=') else 'body',
285
+ arg.split('=', 1)[1], arg.split('=', 1)[0]))
286
+ i += 1
287
+ elif arg.startswith('-F') or arg.startswith('-b'):
288
+ sources.append(('file' if arg.startswith('-F') else 'body',
289
+ arg[3:] if arg[2:3] == '=' else arg[2:], arg[:2]))
290
+ i += 1
291
+ elif arg in valued:
292
+ i += 2
293
+ else:
294
+ i += 1
295
+ files = [value for kind, value, _ in sources if kind == 'file']
296
+ short_bodies = [value for kind, value, flag in sources if kind == 'body' and flag == '-b']
297
+ if files or short_bodies:
298
+ if args[:2] not in (['gh', 'issue'], ['gh', 'pr']):
299
+ raise ValueError('ambiguous command')
300
+ if files:
301
+ if len(files) != 1 or len(sources) != 1 or not files[0] or files[0] == '-':
302
+ raise ValueError('ambiguous body file')
303
+ if not os.path.isabs(files[0]) and not base:
304
+ raise ValueError('unknown command checkout')
305
+ path = files[0] if os.path.isabs(files[0]) else os.path.join(base, files[0])
306
+ with open(path, encoding='utf-8') as body_file:
307
+ print(body_file.read(), end='')
308
+ else:
309
+ if len(sources) != 1 or not short_bodies[0]:
310
+ raise ValueError('ambiguous body')
311
+ print(short_bodies[0], end='')
312
+ raise SystemExit
313
+ except (OSError, UnicodeError, ValueError):
314
+ raise SystemExit(2)
240
315
  # P364: bash double-quote unescape. The double-quoted body capture groups
241
316
  # carry RAW shell-escaped command text — an orchestrator must backslash-escape
242
317
  # backticks (and \$, \", \\) inside \"...\" to survive bash parsing, e.g.
@@ -310,7 +385,9 @@ for pat, flags, unescape in patterns:
310
385
  body = unescape_dq(body)
311
386
  print(body)
312
387
  break
313
- " "$SURFACE" 2>/dev/null || echo "")
388
+ " "$SURFACE" "$TOOL_CWD" 2>/dev/null); then
389
+ BODY_FILE_ERROR=1
390
+ fi
314
391
  ;;
315
392
 
316
393
  Write|Edit)
@@ -362,6 +439,11 @@ print(json.dumps({
362
439
  " "$reason"
363
440
  }
364
441
 
442
+ if [ "$BODY_FILE_ERROR" -eq 1 ]; then
443
+ deny_with_reason "BLOCKED (external-comms gate): body-file could not be read unambiguously from the command checkout. Use one readable UTF-8 --body-file path and review its exact contents before retrying."
444
+ exit 0
445
+ fi
446
+
365
447
  permit_with_advisory() {
366
448
  local msg="$1"
367
449
  python3 -c "
@@ -39,7 +39,7 @@ check_risk_gate() {
39
39
  # 1. Score file must exist (fail-closed)
40
40
  if [ ! -f "$SCORE_FILE" ]; then
41
41
  RISK_GATE_CATEGORY="missing"
42
- RISK_GATE_REASON="No ${ACTION} risk score found. Delegate to wr-risk-scorer:pipeline to assess cumulative pipeline risk. On Codex, wait for the reviewer and confirm that exact target is completed; then invoke \`interrupt_agent\` exactly once on the completed target before retrying. If it is still running, keep waiting. The compatibility hook persists the completed structured verdict; no transcript parsing or nested codex exec is required. On Claude Code, dispatch the reviewer synchronously (\`run_in_background: false\`) before retrying."
42
+ RISK_GATE_REASON="No ${ACTION} risk score found. Delegate to wr-risk-scorer:pipeline to assess cumulative pipeline risk. On Codex, spawn a fresh typed reviewer for each new assessment; followup_task cannot refresh a completed review marker. Wait for the reviewer and confirm that exact target is completed; then invoke \`interrupt_agent\` exactly once on the completed target before retrying. If it is still running, keep waiting. The compatibility hook persists the completed structured verdict; no transcript parsing or nested codex exec is required. On Claude Code, dispatch the reviewer synchronously (\`run_in_background: false\`) before retrying."
43
43
  return 1
44
44
  fi
45
45
 
@@ -163,7 +163,7 @@ print(('yes' if score > N else 'no') + ' ' + str(N))
163
163
  if [ "$DENIED" = "yes" ]; then
164
164
  RISK_GATE_CATEGORY="threshold"
165
165
  RISK_GATE_SCORE="$SCORE"
166
- RISK_GATE_REASON="${ACTION} risk score ${SCORE}/25 exceeds the project appetite of ${APPETITE}/25 (RISK-POLICY.md). Reduce risk or halt — there is no proceed-anyway path (P377/RFC-029). To proceed within appetite: (1) split the ${ACTION}, (2) add risk-reducing measures and re-score, or (3) if this is incident response, delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') with incident context — it scores the change against the live realised-risk baseline (ADR-042 Rule 1b) and clears it via the risk-reducing path if it is net-risk-reducing."
166
+ RISK_GATE_REASON="${ACTION} risk score ${SCORE}/25 exceeds the project appetite of ${APPETITE}/25 (RISK-POLICY.md). Reduce risk or halt — there is no proceed-anyway path (P377/RFC-029). After remediation on Codex, spawn a fresh typed wr-risk-scorer:pipeline reviewer for this checkout; followup_task does not refresh the recorded score. To proceed within appetite: (1) split the ${ACTION}, (2) add risk-reducing measures and re-score, or (3) if this is incident response, delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') with incident context — it scores the change against the live realised-risk baseline (ADR-042 Rule 1b) and clears it via the risk-reducing path if it is net-risk-reducing."
167
167
  return 1
168
168
  fi
169
169
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.19.12",
3
+ "version": "0.19.13-preview.1285",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "scripts": {
6
6
  "prepack": "node scripts/sync-codex-skills.mjs --pack",
@@ -34,7 +34,10 @@ prevents the completion bridge from binding the returned verdict. `$ARGUMENTS`
34
34
  is already self-contained, so no forked conversation context is required.
35
35
  After the Codex agent reports completion, invoke `interrupt_agent` exactly once
36
36
  on that completed target so the compatibility hook receives the structured
37
- result. Do not relaunch or inspect a transcript.
37
+ result. After remediation changes the assessed risk, spawn a **new** typed
38
+ reviewer with the updated state. `followup_task` on a completed reviewer does
39
+ not create a new marker generation, so its later score cannot replace the
40
+ recorded score. Do not relaunch the same target or inspect a transcript.
38
41
 
39
42
  ```
40
43
  subagent_type: wr-risk-scorer:pipeline