@windyroad/risk-scorer 0.18.8 → 0.18.9-preview.1049

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.18.8"
313
+ "version": "0.18.9"
314
314
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wr-risk-scorer",
3
- "version": "0.18.8",
3
+ "version": "0.18.9",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "author": {
6
6
  "name": "Windy Road Technology",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
- import { dirname, join } from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { dirname, isAbsolute, join } from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
@@ -67,6 +67,37 @@ function claimTarget(input, target) {
67
67
  return { claim, done };
68
68
  }
69
69
 
70
+ function pipelineAssessment(output) {
71
+ const roots = [...output.matchAll(/^RISK_CWD:[ \t]*(.+)$/gm)];
72
+ if (roots.length !== 1) return null;
73
+
74
+ const declaredRoot = roots[0][1].trim();
75
+ if (!isAbsolute(declaredRoot)) return null;
76
+
77
+ let root;
78
+ try {
79
+ root = realpathSync(declaredRoot);
80
+ } catch {
81
+ return null;
82
+ }
83
+ const git = spawnSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
84
+ if (git.status !== 0) return null;
85
+
86
+ let gitRoot;
87
+ try {
88
+ gitRoot = realpathSync(git.stdout.trim());
89
+ } catch {
90
+ return null;
91
+ }
92
+ if (gitRoot !== root) return null;
93
+
94
+ let sanitized = output.split(/\r?\n/).filter((line) => !line.startsWith("RISK_CWD:")).join("\n");
95
+ for (const privatePath of new Set([declaredRoot, root])) {
96
+ sanitized = sanitized.split(privatePath).join("<assessed-root>");
97
+ }
98
+ return { root, output: sanitized };
99
+ }
100
+
70
101
  function markTarget(input, target, output) {
71
102
  if (typeof target !== "string" || typeof output !== "string" || !output) return;
72
103
 
@@ -77,14 +108,24 @@ function markTarget(input, target, output) {
77
108
  const claim = claimTarget(input, target);
78
109
  if (!claim) return;
79
110
 
111
+ const assessment = role === "wr-risk-scorer:pipeline" ? pipelineAssessment(output) : null;
112
+ if (role === "wr-risk-scorer:pipeline" && !assessment) {
113
+ rmSync(claim.claim, { force: true });
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+
118
+ const cwd = assessment?.root || input.cwd || process.cwd();
119
+
80
120
  const synthetic = {
81
121
  ...input,
122
+ cwd,
82
123
  tool_name: "Agent",
83
124
  tool_input: { subagent_type: role, prompt: "" },
84
- tool_response: { content: [{ type: "text", text: output }] },
125
+ tool_response: { content: [{ type: "text", text: assessment?.output || output }] },
85
126
  };
86
127
  const result = spawnSync(join(hookDir, "risk-score-mark.sh"), {
87
- cwd: input.cwd || process.cwd(),
128
+ cwd,
88
129
  env: process.env,
89
130
  input: JSON.stringify(synthetic),
90
131
  encoding: "utf8",
@@ -20,6 +20,13 @@ TOOL_NAME=$(_get_tool_name)
20
20
  COMMAND=$(_get_command)
21
21
  SESSION_ID=$(_get_session_id)
22
22
 
23
+ if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*(git push|npm run (push:watch|release:watch)|npx changeset|npm run changeset|gh pr merge)(\s|$)'; then
24
+ if ! _enter_hook_cwd; then
25
+ risk_gate_deny "Pipeline action blocked: the command checkout could not be validated. Run the command from an absolute Git working directory and rescore that checkout."
26
+ exit 0
27
+ fi
28
+ fi
29
+
23
30
  # Block git push to master/main/publish/changeset-release/*, or bare git push.
24
31
  # Allow explicit pushes to other branches (feature branches etc).
25
32
  if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*git push(\s|$)'; then
@@ -45,7 +52,7 @@ if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*npm run push:watch(\s|$)'; then
45
52
  MARK_TIME=$(_mtime "${RDIR}/reducing-push")
46
53
  AGE=$(( NOW - MARK_TIME ))
47
54
  TTL_SECONDS="${RISK_TTL:-3600}"
48
- if [ "$AGE" -lt "$TTL_SECONDS" ] && [ -f "${RDIR}/state-hash" ]; then
55
+ if [ "$AGE" -lt "$TTL_SECONDS" ] && [ -f "${RDIR}/state-hash" ] && _checkout_matches "${RDIR}/checkout-id"; then
49
56
  STORED_HASH=$(cat "${RDIR}/state-hash")
50
57
  CURRENT_HASH=$("$SCRIPT_DIR/lib/pipeline-state.sh" --hash-inputs 2>/dev/null | _hashcmd | cut -d' ' -f1)
51
58
  if [ "$STORED_HASH" = "$CURRENT_HASH" ]; then
@@ -109,8 +116,11 @@ if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*npm run release:watch(\s|$)'; the
109
116
  # Per JTBD-201, this MUST short-circuit BEFORE the CI-status check
110
117
  # so the hotfix path is unaffected by red CI on master.
111
118
  if [ -f "${RDIR}/incident-release" ]; then
119
+ if _checkout_matches "${RDIR}/checkout-id"; then
120
+ rm -f "${RDIR}/incident-release"
121
+ exit 0
122
+ fi
112
123
  rm -f "${RDIR}/incident-release"
113
- exit 0
114
124
  fi
115
125
  # Risk-reducing bypass for release — session-scoped, drift-
116
126
  # revalidated (P192). Same lifecycle as reducing-push above.
@@ -119,7 +129,7 @@ if echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*npm run release:watch(\s|$)'; the
119
129
  MARK_TIME=$(_mtime "${RDIR}/reducing-release")
120
130
  AGE=$(( NOW - MARK_TIME ))
121
131
  TTL_SECONDS="${RISK_TTL:-3600}"
122
- if [ "$AGE" -lt "$TTL_SECONDS" ] && [ -f "${RDIR}/state-hash" ]; then
132
+ if [ "$AGE" -lt "$TTL_SECONDS" ] && [ -f "${RDIR}/state-hash" ] && _checkout_matches "${RDIR}/checkout-id"; then
123
133
  STORED_HASH=$(cat "${RDIR}/state-hash")
124
134
  CURRENT_HASH=$("$SCRIPT_DIR/lib/pipeline-state.sh" --hash-inputs 2>/dev/null | _hashcmd | cut -d' ' -f1)
125
135
  if [ "$STORED_HASH" = "$CURRENT_HASH" ]; then
@@ -13,6 +13,21 @@ _mtime() { stat -c%Y "$1" 2>/dev/null || /usr/bin/stat -f%m "$1" 2>/dev/null ||
13
13
  # Portable hash: tries md5sum, falls back to md5 -r, then shasum
14
14
  _hashcmd() { md5sum 2>/dev/null || md5 -r 2>/dev/null || shasum 2>/dev/null; }
15
15
 
16
+ # Opaque identity for the physical Git checkout. Device + inode distinguishes
17
+ # separate clones with identical trees without persisting their local paths.
18
+ _checkout_id() {
19
+ local root
20
+ root=$(git rev-parse --show-toplevel 2>/dev/null) || return 1
21
+ python3 -c 'import hashlib, os, sys; s=os.stat(os.path.realpath(sys.argv[1])); print(hashlib.sha256(f"{s.st_dev}:{s.st_ino}".encode()).hexdigest())' "$root" 2>/dev/null
22
+ }
23
+
24
+ _checkout_matches() {
25
+ local stored_file="$1" current
26
+ [ -s "$stored_file" ] || return 1
27
+ current=$(_checkout_id) || return 1
28
+ [ -n "$current" ] && [ "$(cat "$stored_file")" = "$current" ]
29
+ }
30
+
16
31
  # ---------------------------------------------------------------------------
17
32
  # Substance-aware drift hash + atomic verdict-write (ADR-009 amendment
18
33
  # 2026-06-06, P353 + P303 close).
@@ -189,6 +204,45 @@ except:
189
204
  " 2>/dev/null || echo ""
190
205
  }
191
206
 
207
+ _get_cwd() {
208
+ echo "$_HOOK_INPUT" | python3 -c "
209
+ import json, re, shlex, sys
210
+ try:
211
+ data = json.load(sys.stdin)
212
+ tool = data.get('tool_input', {})
213
+ command_cwd = ''
214
+ try:
215
+ tokens = shlex.split(tool.get('command', ''))
216
+ while tokens and re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*=.*', tokens[0]):
217
+ tokens.pop(0)
218
+ if len(tokens) >= 3 and tokens[0] == 'cd' and tokens[2] == '&&':
219
+ command_cwd = tokens[1]
220
+ except ValueError:
221
+ pass
222
+ print(tool.get('cwd') or tool.get('workdir') or command_cwd or data.get('cwd') or '')
223
+ except:
224
+ print('')
225
+ " 2>/dev/null || echo ""
226
+ }
227
+
228
+ _enter_hook_cwd() {
229
+ local declared real root
230
+ declared=$(_get_cwd)
231
+ if [ -z "$declared" ]; then
232
+ # Legacy Claude payloads omit cwd. The caller's process root remains a
233
+ # safe fallback because checkout-id matching still denies a marker
234
+ # minted by any other physical checkout.
235
+ return 0
236
+ fi
237
+ case "$declared" in /*) ;; *) return 1 ;; esac
238
+ real=$(python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$declared" 2>/dev/null) || return 1
239
+ [ -d "$real" ] || return 1
240
+ root=$(git -C "$real" rev-parse --show-toplevel 2>/dev/null) || return 1
241
+ root=$(python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$root" 2>/dev/null) || return 1
242
+ [ -d "$root" ] || return 1
243
+ cd "$root"
244
+ }
245
+
192
246
  _get_file_path() {
193
247
  echo "$_HOOK_INPUT" | python3 -c "
194
248
  import sys, json
@@ -30,6 +30,7 @@ check_risk_gate() {
30
30
  local SCORE_FILE="${RDIR}/${ACTION}"
31
31
  local BORN_FILE="${RDIR}/${ACTION}-born"
32
32
  local HASH_FILE="${RDIR}/state-hash"
33
+ local CHECKOUT_FILE="${RDIR}/checkout-id"
33
34
  local TTL_SECONDS="${RISK_TTL:-3600}"
34
35
 
35
36
  RISK_GATE_CATEGORY=""
@@ -46,6 +47,14 @@ check_risk_gate() {
46
47
  return 1
47
48
  fi
48
49
 
50
+ # Scores and bypasses belong to the physical checkout that was assessed.
51
+ # Fail closed for legacy markers that predate this binding.
52
+ if ! _checkout_matches "$CHECKOUT_FILE"; then
53
+ RISK_GATE_CATEGORY="drift"
54
+ RISK_GATE_REASON="Risk assessment checkout binding is missing or does not match the current Git checkout. Delegate to wr-risk-scorer:pipeline (subagent_type: 'wr-risk-scorer:pipeline') to rescore this checkout."
55
+ return 1
56
+ fi
57
+
49
58
  # 2. TTL — Band C hard expiry first
50
59
  local NOW=$(date +%s)
51
60
  local SCORE_TIME=$(_mtime "$SCORE_FILE")
@@ -12,6 +12,7 @@ COMMAND=$(_get_command)
12
12
 
13
13
  # Only act on commands that change git state
14
14
  echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*git (add|commit|stash|reset|checkout|restore)' || exit 0
15
+ _enter_hook_cwd || exit 0
15
16
 
16
17
  SESSION_ID=$(_get_session_id)
17
18
  [ -n "$SESSION_ID" ] || exit 0
@@ -19,6 +20,7 @@ SESSION_ID=$(_get_session_id)
19
20
  RDIR=$(_risk_dir "$SESSION_ID")
20
21
  HASH_FILE="${RDIR}/state-hash"
21
22
  [ -f "$HASH_FILE" ] || exit 0 # No hash file yet — scorer hasn't run
23
+ _checkout_matches "${RDIR}/checkout-id" || exit 0
22
24
 
23
25
  CURRENT_HASH=$("$SCRIPT_DIR/lib/pipeline-state.sh" --hash-inputs 2>/dev/null | _hashcmd | cut -d' ' -f1)
24
26
  if [ -n "$CURRENT_HASH" ]; then
@@ -15,6 +15,10 @@ TOOL_NAME=$(_get_tool_name)
15
15
 
16
16
  COMMAND=$(_get_command)
17
17
  echo "$COMMAND" | grep -qE '(^|;|&&|\|\|)\s*git commit' || exit 0
18
+ if ! _enter_hook_cwd; then
19
+ risk_gate_deny "Commit blocked: the command checkout could not be validated. Run the command from an absolute Git working directory and rescore that checkout."
20
+ exit 0
21
+ fi
18
22
 
19
23
  # P170 / RFC-002 / ADR-031 T11: commit-message-embedded RISK_BYPASS
20
24
  # marker recognition. The adopter auto-migrate routine (T7,
@@ -92,7 +96,7 @@ if [ -f "${RDIR}/reducing-commit" ]; then
92
96
  MARK_TIME=$(_mtime "${RDIR}/reducing-commit")
93
97
  AGE=$(( NOW - MARK_TIME ))
94
98
  TTL_SECONDS="${RISK_TTL:-3600}"
95
- if [ "$AGE" -lt "$TTL_SECONDS" ] && [ -f "${RDIR}/state-hash" ]; then
99
+ if [ "$AGE" -lt "$TTL_SECONDS" ] && [ -f "${RDIR}/state-hash" ] && _checkout_matches "${RDIR}/checkout-id"; then
96
100
  STORED_HASH=$(cat "${RDIR}/state-hash")
97
101
  CURRENT_HASH=$("$SCRIPT_DIR/lib/pipeline-state.sh" --hash-inputs 2>/dev/null | _hashcmd | cut -d' ' -f1)
98
102
  if [ "$STORED_HASH" = "$CURRENT_HASH" ]; then
@@ -39,19 +39,23 @@ RDIR=$(_risk_dir "$SESSION_ID")
39
39
  if echo "$SUBAGENT" | grep -qE 'risk-scorer.pipeline'; then
40
40
  # Parse RISK_SCORES: commit=N push=N release=N
41
41
  SCORES_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^RISK_SCORES:' | tail -1) || true
42
- if [ -n "$SCORES_LINE" ]; then
43
- COMMIT=$(echo "$SCORES_LINE" | grep -oE 'commit=[0-9]+' | cut -d= -f2) || true
44
- PUSH=$(echo "$SCORES_LINE" | grep -oE 'push=[0-9]+' | cut -d= -f2) || true
45
- RELEASE=$(echo "$SCORES_LINE" | grep -oE 'release=[0-9]+' | cut -d= -f2) || true
42
+ [ -n "$SCORES_LINE" ] || exit 1
43
+ COMMIT=$(echo "$SCORES_LINE" | grep -oE 'commit=[0-9]+' | cut -d= -f2) || true
44
+ PUSH=$(echo "$SCORES_LINE" | grep -oE 'push=[0-9]+' | cut -d= -f2) || true
45
+ RELEASE=$(echo "$SCORES_LINE" | grep -oE 'release=[0-9]+' | cut -d= -f2) || true
46
+ [ -n "$COMMIT" ] && [ -n "$PUSH" ] && [ -n "$RELEASE" ] || exit 1
46
47
 
47
- # Birth markers (<action>-born) capture the scorer-run timestamp. Band B
48
- # of the three-band TTL policy (P090) uses them to enforce a 2×TTL
49
- # hard-cap on sliding-window extension, so an unchanged-but-idle tree
50
- # cannot ride a single score indefinitely.
51
- [ -n "$COMMIT" ] && { printf '%s' "$COMMIT" > "${RDIR}/commit"; touch "${RDIR}/commit-born"; }
52
- [ -n "$PUSH" ] && { printf '%s' "$PUSH" > "${RDIR}/push"; touch "${RDIR}/push-born"; }
53
- [ -n "$RELEASE" ] && { printf '%s' "$RELEASE" > "${RDIR}/release"; touch "${RDIR}/release-born"; }
54
- fi
48
+ CHECKOUT_ID=$(_checkout_id) || exit 1
49
+ [ -n "$CHECKOUT_ID" ] || exit 1
50
+ rm -f "${RDIR}/checkout-id"
51
+
52
+ # Birth markers (<action>-born) capture the scorer-run timestamp. Band B
53
+ # of the three-band TTL policy (P090) uses them to enforce a 2×TTL
54
+ # hard-cap on sliding-window extension, so an unchanged-but-idle tree
55
+ # cannot ride a single score indefinitely.
56
+ printf '%s' "$COMMIT" > "${RDIR}/commit"; touch "${RDIR}/commit-born"
57
+ printf '%s' "$PUSH" > "${RDIR}/push"; touch "${RDIR}/push-born"
58
+ printf '%s' "$RELEASE" > "${RDIR}/release"; touch "${RDIR}/release-born"
55
59
 
56
60
  # Parse RISK_BYPASS: reducing|incident
57
61
  BYPASS_LINE=$(echo "$AGENT_OUTPUT" | grep -E '^RISK_BYPASS:' | tail -1) || true
@@ -162,6 +166,10 @@ print(json.dumps({
162
166
  done <<< "$HINT_BLOCK"
163
167
  fi
164
168
  } 2>/dev/null || true
169
+
170
+ # Publish the physical-checkout binding last. If any required marker write
171
+ # above fails, legacy scores remain unusable in a different checkout.
172
+ printf '%s' "$CHECKOUT_ID" > "${RDIR}/checkout-id"
165
173
  fi
166
174
 
167
175
  # ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.18.8",
3
+ "version": "0.18.9-preview.1049",
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",
@@ -33,6 +33,16 @@ On PASS, append \`EXTERNAL_COMMS_RISK_KEY: <64 lowercase hex characters>\` after
33
33
  the verdict. On FAIL, do not emit a key. This is generated Codex-only behavior;
34
34
  the completion hook independently validates the key shape before writing a marker.`;
35
35
 
36
+ const pipelineCodexInstructions = `
37
+
38
+ ## Codex assessed-checkout binding
39
+
40
+ The request must contain exactly one \`RISK_CWD: <absolute Git root>\` line.
41
+ Repeat that exact line once after the structured risk output so the completion
42
+ bridge can bind marker generation to the checkout that was assessed. Do not
43
+ repeat the path elsewhere. If the request omits the line or provides more than
44
+ one, do not emit \`RISK_SCORES\`; report that the assessed checkout is unbound.`;
45
+
36
46
  export const riskAgentSpecs = [
37
47
  ["pipeline", "Scores pipeline actions for cumulative residual risk."],
38
48
  ["plan", "Reviews implementation plans and projected release risk."],
@@ -73,7 +83,9 @@ function renderPayload(spec) {
73
83
  const description = frontmatterValue(frontmatter, "description") || spec.fallbackDescription;
74
84
  const instructions = spec.mode === "external-comms"
75
85
  ? `${body.trimEnd()}${externalCommsCodexInstructions}`
76
- : body;
86
+ : spec.mode === "pipeline"
87
+ ? `${body.trimEnd()}${pipelineCodexInstructions}`
88
+ : body;
77
89
  return [
78
90
  "# Do not edit by hand; update the Claude agent markdown and regenerate.",
79
91
  `name = ${JSON.stringify(spec.name)}`,
@@ -32,6 +32,9 @@ Read `$ARGUMENTS` for an explicit release scope (e.g., "release v1.3.0", "commit
32
32
  Run the following to establish the assessment scope:
33
33
 
34
34
  ```bash
35
+ # Exact checkout whose state the completion marker must bind to.
36
+ git rev-parse --show-toplevel
37
+
35
38
  # Unpushed commits
36
39
  git log origin/$(git rev-parse --abbrev-ref HEAD)..HEAD --oneline 2>/dev/null || git log HEAD --oneline -10
37
40
 
@@ -62,6 +65,7 @@ Do not ask if there is an obvious unpushed commit queue.
62
65
  ### 4. Construct the assessment prompt
63
66
 
64
67
  Build a self-contained prompt for the pipeline subagent that includes:
68
+ - A single `RISK_CWD: <absolute Git root>` line from Step 2
65
69
  - The git log summary (unpushed commits with subjects)
66
70
  - The staged diff summary (file names and line counts)
67
71
  - The changeset list (if any)
@@ -18,7 +18,7 @@ This SKILL is an **invokable wrapper** around the `wr-risk-scorer:pipeline` agen
18
18
  ## Contract
19
19
 
20
20
  - **Input** (`$ARGUMENTS`): a self-contained scoring prompt with pipeline state context. Caller assembles UNCOMMITTED / UNPUSHED / UNRELEASED sections per `packages/risk-scorer/agents/pipeline.md` § Pipeline State.
21
- - **Output**: the agent's verbatim report, including the structured `RISK_SCORES: commit=N push=N release=N` block, optional `RISK_BYPASS:` line, optional `RISK_REMEDIATIONS:` block, optional `RISK_REGISTER_HINT:` block, and optional `CATALOG_HIT_RATE:` line.
21
+ - **Output**: the agent's verbatim report, including the structured `RISK_SCORES: commit=N push=N release=N` block, required `RISK_CWD:` line supplied by the caller, optional `RISK_BYPASS:` line, optional `RISK_REMEDIATIONS:` block, optional `RISK_REGISTER_HINT:` block, and optional `CATALOG_HIT_RATE:` line.
22
22
  - **Side effects**: the `PostToolUse:Agent` hook (`risk-score-mark.sh`) reads the agent's output downstream of this wrapper and writes the bypass marker files to `${TMPDIR}/claude-risk-${SESSION_ID}/`. The wrapper itself writes no files.
23
23
 
24
24
  ## Steps
@@ -27,6 +27,16 @@ This SKILL is an **invokable wrapper** around the `wr-risk-scorer:pipeline` agen
27
27
 
28
28
  Invoke the pipeline subagent via the Agent tool with the caller's `$ARGUMENTS` verbatim. Dispatch it **synchronously** (`run_in_background: false`) — the `PostToolUse:Agent` mark hook that writes the risk-gate marker fires reliably only for a synchronous agent; a background-launched scorer's mark hook does not fire in time, so no marker persists and the commit/push gate re-blocks despite a within-appetite score (P402):
29
29
 
30
+ On Codex, when the Agent tool is exposed as `spawn_agent`, pass
31
+ `agent_type: wr-risk-scorer:pipeline` with `fork_turns: "none"`. Codex rejects an
32
+ explicit agent type on a full-history fork; omitting the type on retry also
33
+ prevents the completion bridge from binding the returned verdict. `$ARGUMENTS`
34
+ is already self-contained, so no forked conversation context is required.
35
+ After the Codex agent reports completion, close that completed agent exactly
36
+ once so the compatibility hook receives the structured result. When Codex
37
+ exposes completed-agent close as `interrupt_agent`, invoke it once on the
38
+ completed target; do not relaunch or inspect a transcript.
39
+
30
40
  ```
31
41
  subagent_type: wr-risk-scorer:pipeline
32
42
  run_in_background: false
@@ -35,6 +45,6 @@ prompt: $ARGUMENTS
35
45
 
36
46
  ### 2. Return the agent report verbatim
37
47
 
38
- Return the agent's response to the caller without alteration. Do NOT strip, paraphrase, or post-process the structured output blocks (`RISK_SCORES:`, `RISK_BYPASS:`, `RISK_REMEDIATIONS:`, `RISK_REGISTER_HINT:`, `CATALOG_HIT_RATE:`). The PostToolUse hook depends on the exact byte sequence to parse.
48
+ Return the agent's response to the caller without alteration. Do NOT strip, paraphrase, or post-process the structured output blocks (`RISK_SCORES:`, `RISK_CWD:`, `RISK_BYPASS:`, `RISK_REMEDIATIONS:`, `RISK_REGISTER_HINT:`, `CATALOG_HIT_RATE:`). The PostToolUse hook depends on the exact byte sequence to parse.
39
49
 
40
50
  $ARGUMENTS