@windyroad/risk-scorer 0.18.8 → 0.18.9-preview.1048
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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/hooks/codex-agent-completion.mjs +45 -4
- package/hooks/git-push-gate.sh +13 -3
- package/hooks/lib/gate-helpers.sh +54 -0
- package/hooks/lib/risk-gate.sh +9 -0
- package/hooks/risk-hash-refresh.sh +2 -0
- package/hooks/risk-score-commit-gate.sh +5 -1
- package/hooks/risk-score-mark.sh +20 -12
- package/package.json +1 -1
- package/scripts/codex-agents.mjs +13 -1
- package/skills/assess-release/SKILL.md +4 -0
- package/skills/pipeline/SKILL.md +12 -2
|
@@ -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
|
|
128
|
+
cwd,
|
|
88
129
|
env: process.env,
|
|
89
130
|
input: JSON.stringify(synthetic),
|
|
90
131
|
encoding: "utf8",
|
package/hooks/git-push-gate.sh
CHANGED
|
@@ -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
|
package/hooks/lib/risk-gate.sh
CHANGED
|
@@ -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
|
package/hooks/risk-score-mark.sh
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
package/scripts/codex-agents.mjs
CHANGED
|
@@ -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
|
-
:
|
|
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)
|
package/skills/pipeline/SKILL.md
CHANGED
|
@@ -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
|