coderifts 4.1.0 → 4.1.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli.js +94 -17
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.1.1
4
+
5
+ ### Fixed
6
+
7
+ - **P0-2: `mapDecisionSeverity` never maps missing `execution_action` to ALLOW.**
8
+ Missing EA in any body shape → `INDETERMINATE` (exit 2). Legacy decision→severity
9
+ map is allowed only when `decision_spec_version === "1.0"` on a non-v2 body
10
+ (no `decision_result`, no `preflight_mode`). Top-level-only `{decision:"ALLOW"}`
11
+ without that pin is not permission.
12
+ - **P0-3: parse-gap is fail-closed by default.** Unparseable stdin and missing
13
+ `tool_input.file_path` → **exit 2**, stderr
14
+ `unparseable input — refusing (fail-closed); set CODERIFTS_ADVISORY=1 to soften`,
15
+ JSONL `hook_blocked` with `cause: stdin_unparseable|missing_file_path`.
16
+ `CODERIFTS_ADVISORY=1` restores exit 0. API-key-before-parse order is unchanged
17
+ (garbage stdin + no key → `missing_api_key`). Non-spec Write stays exit 0.
18
+ Unknown-tool-on-spec was already exit 2.
19
+
3
20
  ## 4.1.0
4
21
 
5
22
  ### Added
package/dist/cli.js CHANGED
@@ -3007,7 +3007,7 @@ var require_package = __commonJS({
3007
3007
  "package.json"(exports2, module2) {
3008
3008
  module2.exports = {
3009
3009
  name: "coderifts",
3010
- version: "4.1.0",
3010
+ version: "4.1.1",
3011
3011
  description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
3012
3012
  author: "CodeRifts <hello@coderifts.com>",
3013
3013
  license: "MIT",
@@ -101231,8 +101231,10 @@ var require_claude_hook = __commonJS({
101231
101231
  " no API key / API unreachable / disk unreadable / edit-apply failure on the spec path",
101232
101232
  " Absence of a key is not permission. Explicit opt-out: CODERIFTS_ADVISORY=1|true.",
101233
101233
  "",
101234
- "Still exit 0 (nothing to govern): unparseable stdin, missing file_path, non-spec path,",
101235
- "identical content.",
101234
+ "Parse-gap DEFAULT (exit 2): unparseable stdin / missing file_path \u2014 refusing (fail-closed);",
101235
+ " set CODERIFTS_ADVISORY=1 to soften. JSONL hook_blocked cause stdin_unparseable|missing_file_path.",
101236
+ "",
101237
+ "Still exit 0 (nothing to govern): non-spec path, identical content.",
101236
101238
  "",
101237
101239
  "CONTINUE_WITH_MONITORING: allow only if the host asserts a sink",
101238
101240
  " (CODERIFTS_MONITORING_SINK_WIRED=1|true or git config coderifts.monitoringSinkWired).",
@@ -101431,13 +101433,22 @@ var require_claude_hook = __commonJS({
101431
101433
  }
101432
101434
  return { ok: false, reason: `unsupported tool_name for content derive: ${name}` };
101433
101435
  }
101436
+ function isV2DecisionBody(d, dr) {
101437
+ if (dr && typeof dr === "object") return true;
101438
+ if (d.preflight_mode != null && d.preflight_mode !== "") return true;
101439
+ const ver = d.decision_spec_version;
101440
+ return typeof ver === "string" && ver.startsWith("2.");
101441
+ }
101442
+ function allowLegacyDecisionMap(d, dr) {
101443
+ return d.decision_spec_version === "1.0" && !isV2DecisionBody(d, dr);
101444
+ }
101434
101445
  function mapDecisionSeverity(result) {
101435
101446
  const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101436
101447
  let ea = null;
101437
101448
  const dr = d.decision_result;
101438
- if (dr && typeof dr === "object" && typeof dr.execution_action === "string") {
101449
+ if (dr && typeof dr === "object" && typeof dr.execution_action === "string" && dr.execution_action !== "") {
101439
101450
  ea = dr.execution_action;
101440
- } else if (typeof d.execution_action === "string") {
101451
+ } else if (typeof d.execution_action === "string" && d.execution_action !== "") {
101441
101452
  ea = d.execution_action;
101442
101453
  }
101443
101454
  const hasDecision = d.omega_decision != null && d.omega_decision !== "" || d.decision != null && d.decision !== "" || dr && typeof dr === "object" && dr.execution_action;
@@ -101462,7 +101473,7 @@ var require_claude_hook = __commonJS({
101462
101473
  } else {
101463
101474
  severity = "REQUIRE_APPROVAL";
101464
101475
  }
101465
- } else {
101476
+ } else if (allowLegacyDecisionMap(d, dr)) {
101466
101477
  const od = d.omega_decision || d.decision;
101467
101478
  if (od == null || od === "") {
101468
101479
  severity = "INDETERMINATE";
@@ -101470,6 +101481,8 @@ var require_claude_hook = __commonJS({
101470
101481
  else if (od === "REQUIRE_APPROVAL") severity = "REQUIRE_APPROVAL";
101471
101482
  else if (od === "WARN") severity = "WARN";
101472
101483
  else severity = "ALLOW";
101484
+ } else {
101485
+ severity = "INDETERMINATE";
101473
101486
  }
101474
101487
  const decision = dr && dr.decision || d.decision || d.omega_decision || null;
101475
101488
  const decisionId = dr && dr.decision_id || d.decision_id || null;
@@ -101480,6 +101493,45 @@ var require_claude_hook = __commonJS({
101480
101493
  executionAction: ea
101481
101494
  };
101482
101495
  }
101496
+ function renderDecisionWhy(result, opts = {}) {
101497
+ const maxFixes = Number.isInteger(opts.maxFixes) ? opts.maxFixes : 5;
101498
+ const maxLines = Number.isInteger(opts.maxLines) ? opts.maxLines : 13;
101499
+ const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101500
+ const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
101501
+ const pick = (k) => dr[k] !== void 0 ? dr[k] : d[k];
101502
+ const arr = (v) => Array.isArray(v) ? v : [];
101503
+ const str = (v) => typeof v === "string" ? v.trim() : "";
101504
+ const lines = [];
101505
+ const reasonCodes = [];
101506
+ for (const r of arr(pick("blocking_reasons"))) {
101507
+ if (!r || typeof r !== "object") continue;
101508
+ const code = str(r.code);
101509
+ const message = str(r.message);
101510
+ if (code) reasonCodes.push(code);
101511
+ if (!code && !message) continue;
101512
+ lines.push(`- ${code || "REASON"}${message ? `: ${message}` : ""}`);
101513
+ }
101514
+ for (const r of arr(pick("degraded_reasons"))) {
101515
+ if (!r || typeof r !== "object") continue;
101516
+ const code = str(r.code);
101517
+ const message = str(r.message);
101518
+ if (!code && !message) continue;
101519
+ lines.push(`- degraded: ${[code, message].filter(Boolean).join(": ")}`);
101520
+ }
101521
+ const action = str(pick("required_action"));
101522
+ if (action) lines.push(`- action: ${action}`);
101523
+ const rt = pick("remediation_transaction");
101524
+ const changes = arr(rt && typeof rt === "object" ? rt.required_changes : null).filter((c) => c && typeof c === "object");
101525
+ const shown = changes.slice(0, maxFixes);
101526
+ for (const c of shown) {
101527
+ const target = str(c.target);
101528
+ const instruction = str(c.instruction);
101529
+ if (!instruction && !target) continue;
101530
+ lines.push(`- fix${target ? ` (${target})` : ""}: ${instruction || str(c.precise_label)}`);
101531
+ }
101532
+ if (changes.length > shown.length) lines.push(`- fix: +${changes.length - shown.length} more`);
101533
+ return { lines: lines.slice(0, maxLines), reasonCodes };
101534
+ }
101483
101535
  async function runClaudeHook(options = {}, deps = {}) {
101484
101536
  const errLog = deps.errLog || ((m) => console.error(String(m)));
101485
101537
  const readStdin = deps.readStdin || (() => {
@@ -101515,7 +101567,14 @@ var require_claude_hook = __commonJS({
101515
101567
  decisionId: r.decisionId != null ? r.decisionId : void 0
101516
101568
  };
101517
101569
  if (r.exitCode === 2) {
101518
- logEv({ type: "hook_blocked", exit: 2, ...extra, cause: r.reason || r.site });
101570
+ const reasons = Array.isArray(r.reasons) && r.reasons.length ? r.reasons : void 0;
101571
+ logEv({
101572
+ type: "hook_blocked",
101573
+ exit: 2,
101574
+ ...extra,
101575
+ reasons,
101576
+ cause: r.reason || r.site
101577
+ });
101519
101578
  } else if (r.exitCode === 0 && r.site && advisory && !strict) {
101520
101579
  logEv({ type: "hook_advisory_passthrough", exit: 0, decision: r.decision || null, ...extra, cause: r.site });
101521
101580
  } else {
@@ -101535,20 +101594,27 @@ var require_claude_hook = __commonJS({
101535
101594
  errLog
101536
101595
  }));
101537
101596
  }
101597
+ const PARSE_GAP_STDERR = "unparseable input \u2014 refusing (fail-closed); set CODERIFTS_ADVISORY=1 to soften";
101538
101598
  const raw = typeof options.stdin === "string" ? options.stdin : readStdin();
101539
101599
  const parsed = parseStdinJson(raw);
101540
101600
  if (!parsed.ok) {
101541
- errLog(`CodeRifts claude-hook: ${parsed.reason} \u2014 allowing (soft; never block on parse gap)`);
101542
- logEv({ type: "detection_skip", signals: ["stdin_unparseable"] });
101543
- return emitTerminal({ exitCode: 0, reason: "stdin_unparseable" });
101601
+ if (advisory && !strict) {
101602
+ errLog(`CodeRifts claude-hook: ${parsed.reason} \u2014 allowing (CODERIFTS_ADVISORY)`);
101603
+ return emitTerminal({ exitCode: 0, reason: "stdin_unparseable", site: "stdin_unparseable" });
101604
+ }
101605
+ errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
101606
+ return emitTerminal({ exitCode: 2, reason: "stdin_unparseable" });
101544
101607
  }
101545
101608
  const { toolName, toolInput } = parsed;
101546
101609
  tool = toolName;
101547
101610
  const filePath = toolInput.file_path || toolInput.filePath || toolInput.path;
101548
101611
  if (!filePath || typeof filePath !== "string") {
101549
- errLog("CodeRifts claude-hook: tool_input.file_path missing \u2014 allowing (soft)");
101550
- logEv({ type: "detection_skip", signals: ["missing_file_path"], tool });
101551
- return emitTerminal({ exitCode: 0, reason: "missing_file_path" });
101612
+ if (advisory && !strict) {
101613
+ errLog("CodeRifts claude-hook: tool_input.file_path missing \u2014 allowing (CODERIFTS_ADVISORY)");
101614
+ return emitTerminal({ exitCode: 0, reason: "missing_file_path", site: "missing_file_path" });
101615
+ }
101616
+ errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
101617
+ return emitTerminal({ exitCode: 2, reason: "missing_file_path" });
101552
101618
  }
101553
101619
  filePathKnown = filePath;
101554
101620
  const specPath = resolveSpecPath({ ...deps, cwd });
@@ -101635,27 +101701,37 @@ var require_claude_hook = __commonJS({
101635
101701
  });
101636
101702
  }
101637
101703
  if (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN") {
101704
+ const why = renderDecisionWhy(result);
101638
101705
  errLog(
101639
- `CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : "")
101706
+ [
101707
+ `CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : ""),
101708
+ ...why.lines
101709
+ ].join("\n")
101640
101710
  );
101641
101711
  return emitTerminal({
101642
101712
  exitCode: 2,
101643
101713
  reason: mapped.severity === "UNKNOWN" ? "unknown_action" : "block",
101644
101714
  severity: mapped.severity,
101645
101715
  decision: mapped.decision,
101646
- decisionId: mapped.decisionId
101716
+ decisionId: mapped.decisionId,
101717
+ reasons: why.reasonCodes
101647
101718
  });
101648
101719
  }
101649
101720
  if (mapped.severity === "REQUIRE_APPROVAL") {
101721
+ const why = renderDecisionWhy(result);
101650
101722
  errLog(
101651
- `CodeRifts claude-hook: BLOCKED approval_required${decPart}${eaPart}${idPart}`
101723
+ [
101724
+ `CodeRifts claude-hook: BLOCKED approval_required${decPart}${eaPart}${idPart}`,
101725
+ ...why.lines
101726
+ ].join("\n")
101652
101727
  );
101653
101728
  return emitTerminal({
101654
101729
  exitCode: 2,
101655
101730
  reason: "approval_required",
101656
101731
  severity: mapped.severity,
101657
101732
  decision: mapped.decision,
101658
- decisionId: mapped.decisionId
101733
+ decisionId: mapped.decisionId,
101734
+ reasons: why.reasonCodes
101659
101735
  });
101660
101736
  }
101661
101737
  if (mapped.severity === "MONITOR" || mapped.severity === "WARN") {
@@ -101694,6 +101770,7 @@ var require_claude_hook = __commonJS({
101694
101770
  isSpecPath,
101695
101771
  deriveAfterContent,
101696
101772
  mapDecisionSeverity,
101773
+ renderDecisionWhy,
101697
101774
  resolveApiKey,
101698
101775
  resolveSpecPath,
101699
101776
  readGitConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
5
5
  "author": "CodeRifts <hello@coderifts.com>",
6
6
  "license": "MIT",