@windyroad/risk-scorer 0.18.12 → 0.18.13-preview.1069

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.12"
313
+ "version": "0.18.13"
314
314
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wr-risk-scorer",
3
- "version": "0.18.12",
3
+ "version": "0.18.13",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "author": {
6
6
  "name": "Windy Road Technology",
@@ -293,6 +293,18 @@ readiness may be listed separately as "post-release follow-ups" outside the
293
293
  residual risk computation, but MUST NOT appear in a Controls list and MUST NOT
294
294
  reduce any inherent risk score.
295
295
 
296
+ **Deferral is not a control.** Moving release metadata, leaving work on a
297
+ branch, or promising to ship it later does not remove or bound a hazard, so it
298
+ MUST NOT reduce the current action's residual risk. Score only the action in
299
+ scope; record deferred work separately rather than charging an unintegrated
300
+ future action to the current score.
301
+
302
+ Do not confuse deferral with genuine scope reduction. A small, complete,
303
+ independently releasable change can reduce accumulated risk and blast radius.
304
+ Prefer frequent small commits, pushes, and releases over batching. The test is
305
+ whether the current action is complete without the omitted work: if yes, scope
306
+ was reduced; if no, the hazard was merely deferred and earns no control credit.
307
+
296
308
  ### R009 control vocabulary — SKILL/agent-prose surfaces (P355 / RFC-012 / ADR-075)
297
309
 
298
310
  For diffs touching `packages/*/skills/*/SKILL.md`, `packages/*/skills/*/REFERENCE.md`, or `packages/*/agents/*.md`, the **previously-irreducible R009 "no behavioural harness for the LLM-prose surface" floor is discharged** when a paired promptfoo Tier-A/B eval exists for the changed prose AND `npx promptfoo eval` passes on this commit. Credit it as a named likelihood-reducing control with the same evidence shape as behavioural bats:
@@ -33,6 +33,37 @@ function pendingDir() {
33
33
  return join(process.env.TMPDIR || "/tmp", "claude-risk-pending");
34
34
  }
35
35
 
36
+ function fieldType(input, field) {
37
+ if (!input || !Object.prototype.hasOwnProperty.call(input, field)) return "absent";
38
+ if (input[field] === null) return "null";
39
+ if (Array.isArray(input[field])) return "array";
40
+ return typeof input[field];
41
+ }
42
+
43
+ function diagnoseSubagentStop(input, outcome, reason) {
44
+ const dir = pendingDir();
45
+ const path = join(dir, "subagent-stop-diagnostic.json");
46
+ const temporary = join(dir, `.subagent-stop-diagnostic-${process.pid}.tmp`);
47
+ try {
48
+ mkdirSync(dir, { recursive: true });
49
+ writeFileSync(temporary, JSON.stringify({
50
+ timestamp: new Date().toISOString(),
51
+ outcome,
52
+ reason,
53
+ event: input?.hook_event_name === "SubagentStop" ? "SubagentStop" : "other",
54
+ fields: Object.fromEntries([
55
+ "session_id",
56
+ "agent_id",
57
+ "agent_type",
58
+ "last_assistant_message",
59
+ ].map((field) => [field, fieldType(input, field)])),
60
+ }), { mode: 0o600 });
61
+ renameSync(temporary, path);
62
+ } catch {
63
+ rmSync(temporary, { force: true });
64
+ }
65
+ }
66
+
36
67
  function statePath(input, target, suffix = "") {
37
68
  return join(riskDir(input.session_id), `codex-agent-${Buffer.from(target).toString("base64url")}${suffix}`);
38
69
  }
@@ -72,29 +103,47 @@ function claimTarget(input, target) {
72
103
  return { claim, done };
73
104
  }
74
105
 
75
- function pipelineAssessment(output) {
106
+ function pipelineAssessment(output, reject = () => {}) {
107
+ if (typeof output !== "string" || !output) {
108
+ reject("missing-output");
109
+ return null;
110
+ }
76
111
  const roots = [...output.matchAll(/^RISK_CWD:[ \t]*(.+)$/gm)];
77
- if (roots.length !== 1) return null;
112
+ if (roots.length !== 1) {
113
+ reject(roots.length ? "multiple-risk-cwd" : "missing-risk-cwd");
114
+ return null;
115
+ }
78
116
 
79
117
  const declaredRoot = roots[0][1].trim();
80
- if (!isAbsolute(declaredRoot)) return null;
118
+ if (!isAbsolute(declaredRoot)) {
119
+ reject("relative-risk-cwd");
120
+ return null;
121
+ }
81
122
 
82
123
  let root;
83
124
  try {
84
125
  root = realpathSync(declaredRoot);
85
126
  } catch {
127
+ reject("unreadable-risk-cwd");
86
128
  return null;
87
129
  }
88
130
  const git = spawnSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
89
- if (git.status !== 0) return null;
131
+ if (git.status !== 0) {
132
+ reject("not-git-worktree");
133
+ return null;
134
+ }
90
135
 
91
136
  let gitRoot;
92
137
  try {
93
138
  gitRoot = realpathSync(git.stdout.trim());
94
139
  } catch {
140
+ reject("unreadable-git-root");
141
+ return null;
142
+ }
143
+ if (gitRoot !== root) {
144
+ reject("risk-cwd-not-git-root");
95
145
  return null;
96
146
  }
97
- if (gitRoot !== root) return null;
98
147
 
99
148
  let sanitized = output.split(/\r?\n/).filter((line) => !line.startsWith("RISK_CWD:")).join("\n");
100
149
  for (const privatePath of new Set([declaredRoot, root])) {
@@ -133,17 +182,39 @@ function freshReceipt(path) {
133
182
  }
134
183
 
135
184
  function persistPendingPipeline(input) {
136
- if (input.agent_type !== "wr-risk-scorer:pipeline") return;
137
- const assessment = pipelineAssessment(input.last_assistant_message);
138
- if (!assessment) return;
185
+ diagnoseSubagentStop(input, "received", "pipeline-receipt-attempt");
186
+ if (input.agent_type !== "wr-risk-scorer:pipeline") {
187
+ diagnoseSubagentStop(input, "rejected", "unexpected-agent-type");
188
+ return;
189
+ }
190
+ let rejection;
191
+ const assessment = pipelineAssessment(input.last_assistant_message, (reason) => { rejection = reason; });
192
+ if (!assessment) {
193
+ diagnoseSubagentStop(input, "rejected", rejection || "invalid-assessment");
194
+ return;
195
+ }
139
196
  const id = checkoutId(assessment.root);
140
197
  const hash = stateHash(assessment.root);
141
198
  const completion = completionId(input, assessment.output);
142
- if (!hash || !completion || !/^RISK_SCORES: commit=\d+ push=\d+ release=\d+$/m.test(assessment.output)) return;
199
+ if (!hash) {
200
+ diagnoseSubagentStop(input, "rejected", "state-hash-failed");
201
+ return;
202
+ }
203
+ if (!completion) {
204
+ diagnoseSubagentStop(input, "rejected", "missing-completion-identity");
205
+ return;
206
+ }
207
+ if (!/^RISK_SCORES: commit=\d+ push=\d+ release=\d+$/m.test(assessment.output)) {
208
+ diagnoseSubagentStop(input, "rejected", "missing-risk-scores");
209
+ return;
210
+ }
143
211
  mkdirSync(pendingDir(), { recursive: true });
144
212
  const path = pendingPath(id, hash, completion);
145
213
  for (const candidate of [path, `${path}.done`]) {
146
- if (freshReceipt(candidate)) return;
214
+ if (freshReceipt(candidate)) {
215
+ diagnoseSubagentStop(input, "duplicate", "fresh-receipt-exists");
216
+ return;
217
+ }
147
218
  rmSync(candidate, { force: true });
148
219
  }
149
220
  try {
@@ -155,8 +226,14 @@ function persistPendingPipeline(input) {
155
226
  completionId: completion,
156
227
  createdAt: Date.now(),
157
228
  }), { flag: "wx", mode: 0o600 });
229
+ diagnoseSubagentStop(input, "receipt-written", "checkout-bound-receipt");
158
230
  } catch (error) {
159
- if (error?.code !== "EEXIST") throw error;
231
+ if (error?.code === "EEXIST") {
232
+ diagnoseSubagentStop(input, "duplicate", "receipt-race");
233
+ return;
234
+ }
235
+ diagnoseSubagentStop(input, "rejected", "receipt-write-failed");
236
+ throw error;
160
237
  }
161
238
  }
162
239
 
@@ -306,6 +383,9 @@ let input;
306
383
  try {
307
384
  input = JSON.parse(body);
308
385
  } catch {
386
+ if (process.argv.includes("--subagent-stop")) {
387
+ diagnoseSubagentStop({}, "rejected", "malformed-json");
388
+ }
309
389
  process.exit(0);
310
390
  }
311
391
 
@@ -314,7 +394,12 @@ if (process.argv.includes("--consume-pending")) {
314
394
  process.exit(process.exitCode || 0);
315
395
  }
316
396
 
317
- if (!/^[A-Za-z0-9-]+$/.test(input.session_id || "")) process.exit(0);
397
+ if (!/^[A-Za-z0-9-]+$/.test(input.session_id || "")) {
398
+ if (process.argv.includes("--subagent-stop")) {
399
+ diagnoseSubagentStop(input, "rejected", "invalid-session-id");
400
+ }
401
+ process.exit(0);
402
+ }
318
403
 
319
404
  if (["collaborationspawn_agent", "spawn_agent", "multi_agent_v1__spawn_agent"].includes(input.tool_name)) {
320
405
  rememberSpawn(input);
@@ -100,7 +100,7 @@ if messages:
100
100
  esac
101
101
  ;;
102
102
  subagent-stop)
103
- printf '%s' "$INPUT" | node "$SCRIPT_DIR/codex-agent-completion.mjs"
103
+ printf '%s' "$INPUT" | node "$SCRIPT_DIR/codex-agent-completion.mjs" --subagent-stop
104
104
  ;;
105
105
  *)
106
106
  exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.18.12",
3
+ "version": "0.18.13-preview.1069",
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",