coderifts 3.4.0 → 4.0.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ ## 4.0.0
4
+
5
+ **Breaking** — `coderifts claude-hook` is fail-closed by default. The CLI now
6
+ depends on `@coderifts/agent-guard` ^8.1.1.
7
+
8
+ ### Breaking
9
+ - **`claude-hook` default is fail-closed (P0-3/P0-4).** No API key, API
10
+ unreachable, disk unreadable, or edit-apply failure on the spec path →
11
+ **exit 2** (`enforce_indeterminate`), not exit 0. `REQUEST_APPROVAL` →
12
+ exit 2 (`approval_required`). `CONTINUE_WITH_MONITORING` without a
13
+ host-asserted monitoring sink → exit 2. Empty verdict is no longer
14
+ mapped to ALLOW (P0 #5).
15
+ - **Migration:** hosts that ran the hook without a key / without
16
+ `CODERIFTS_STRICT` will now **block** tool calls. Explicit opt-out:
17
+ `CODERIFTS_ADVISORY=1` restores the old soft-allow on "governance could
18
+ not run" sites. `CODERIFTS_STRICT=1` remains fail-closed and cannot be
19
+ weakened by ADVISORY.
20
+
21
+ ### Changed
22
+ - **`@coderifts/agent-guard` `^6.4.0` → `^8.1.1`.** CLI still calls only
23
+ `deployGate` / `matchGlob` (not the wrap/TOCTOU path). `inescapable_deploy`
24
+ on 8.x is `ENFORCING ∧ ¬bypass ∧ expected_fingerprint` rebound.
25
+ - **`deployReportResiduals`:** rebound-missing is `change_set_not_rebound`,
26
+ never `bypass_open`. Enforcement-axis residuals still named first (same
27
+ order as guard `residuals[]`). CLI AND on the inescapable flag is kept
28
+ (never widen).
29
+ - **`MIN_GUARD_MAJOR`:** 6 → 8 (`scripts/assert-guard-major.js`).
30
+
31
+ ### Fixed
32
+ - **Installer copy (P0 #9 / e76e846):** `agent-setup` / `init` teach
33
+ guard@8 execution-state **DEFAULT ON**, not the old OFF.
34
+
35
+ ## 3.4.0
36
+
37
+ ### Agent-setup
38
+
39
+ - Rule files now state that receipts are operation-scoped (a merge receipt never authorizes deploy/publish; re-preflight with the new `context.operation` — do not decline) and that a stale receipt on a changed head needs a new preflight (`verify_receipt` cannot re-diff).
40
+ - Hosts with existing agent-setup files: `coderifts agent-setup --force` to pick up the new paragraphs; `--check` will report drift until then.
package/README.md CHANGED
@@ -236,17 +236,22 @@ git config coderifts.specPath api/openapi.yaml # default if omitted
236
236
  - **User** `~/.claude/settings.json` — personal; applies to all projects on this machine.
237
237
  - Matcher is case-sensitive tool names/regex. Exit-2 blocking is **Claude Code** semantics; the same decision discipline at push-time is the git pre-push hook above.
238
238
 
239
- **`CODERIFTS_STRICT` (opt-in, not a package guarantee):** env `1` or `true`
240
- (case-insensitive) hardens **enforcement-relevant** soft sites into **exit 2
241
- (BLOCK)** “governance did not run” must not read as ALLOW. Hardened: missing
242
- API key; API unreachable (network/timeout/error); disk unreadable on the
243
- contract path; Edit/MultiEdit apply failure on the contract path. Still soft
244
- even in strict: non-spec path, identical content, unparseable stdin, missing
245
- `file_path`, WARN/REQUIRE_APPROVAL (governance ran), real ALLOW. Unset/empty →
246
- identical to the default soft UX. Hosts choose strict for CI/prod.
239
+ **Fail-closed default:** missing API key, API unreachable, disk unreadable, or
240
+ edit-apply failure on the spec path is **exit 2** (`enforce_indeterminate`) —
241
+ absence of a key is not ALLOW. `REQUEST_APPROVAL` is **exit 2**
242
+ (`approval_required`). `CONTINUE_WITH_MONITORING` is **exit 2**
243
+ (`monitoring_unwired`) unless the host asserts a sink
244
+ (`CODERIFTS_MONITORING_SINK_WIRED=1` or `git config coderifts.monitoringSinkWired true`)
245
+ a host claim, not delivery proof.
246
+
247
+ **`CODERIFTS_ADVISORY=1|true`:** explicit opt-out that restores soft-allow on
248
+ “governance could not run” sites. Not implied by a missing key.
249
+
250
+ **`CODERIFTS_STRICT=1|true`:** same fail-closed as the default; cannot be
251
+ weakened by `CODERIFTS_ADVISORY`.
247
252
 
248
253
  ```bash
249
- CODERIFTS_STRICT=1 coderifts claude-hook
254
+ CODERIFTS_ADVISORY=1 coderifts claude-hook
250
255
  ```
251
256
 
252
257
  ### `coderifts status [repo]`
@@ -485,7 +490,9 @@ Two honest modes:
485
490
  |----------|-------------|
486
491
  | `CODERIFTS_API_KEY` | API key (alternative to `coderifts login`) |
487
492
  | `CODERIFTS_OASDIFF_BIN` | Explicit path to the `oasdiff` binary (overrides auto-detect) |
488
- | `CODERIFTS_STRICT` | `claude-hook` only: `1` or `true` (case-insensitive) turns enforcement-relevant soft sites into exit **2** (BLOCK). Unset/empty default soft allow. Not a package-level guarantee. |
493
+ | `CODERIFTS_STRICT` | `claude-hook`: `1` or `true` pins fail-closed (cannot be weakened by `CODERIFTS_ADVISORY`). Default is already fail-closed. |
494
+ | `CODERIFTS_ADVISORY` | `claude-hook`: `1` or `true` is the **explicit** soft-allow opt-out on “governance could not run” sites. Absence of a key is not this flag. |
495
+ | `CODERIFTS_MONITORING_SINK_WIRED` | `claude-hook`: `1` or `true` host-asserts a monitoring sink so `CONTINUE_WITH_MONITORING` may exit 0. Not delivery proof. |
489
496
  | `CODERIFTS_DEPLOY_ENFORCE` | `deploy-gate`: `true` marks the step enforcing (same as `--enforce`); `unknown` is explicitly unobservable (never assume ENFORCING) |
490
497
  | `NO_COLOR` | Disable colored output |
491
498
 
package/bin/coderifts.js CHANGED
@@ -259,8 +259,9 @@ program
259
259
  program
260
260
  .command('claude-hook')
261
261
  .description(
262
- 'Claude Code PreToolUse gate: block contract-touching Write|Edit|MultiEdit on BLOCK/STOP '
263
- + '(exit 2). Soft-allow when unconfigured/unparseable/API down.',
262
+ 'Claude Code PreToolUse gate: block contract-touching Write|Edit|MultiEdit on BLOCK/STOP/'
263
+ + 'REQUEST_APPROVAL (exit 2). Fail-closed when governance cannot run; '
264
+ + 'CODERIFTS_ADVISORY=1 is the explicit soft opt-out.',
264
265
  )
265
266
  .addHelpText('after', () => {
266
267
  const { USAGE } = require('../src/commands/claude-hook');
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: "3.4.0",
3010
+ version: "4.0.0",
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",
@@ -3020,6 +3020,7 @@ var require_package = __commonJS({
3020
3020
  "bin/",
3021
3021
  "scripts/",
3022
3022
  "README.md",
3023
+ "CHANGELOG.md",
3023
3024
  "corpus/"
3024
3025
  ],
3025
3026
  keywords: [
@@ -3051,7 +3052,7 @@ var require_package = __commonJS({
3051
3052
  test: "node --test test/*.test.js"
3052
3053
  },
3053
3054
  dependencies: {
3054
- "@coderifts/agent-guard": "^6.4.0",
3055
+ "@coderifts/agent-guard": "^8.1.1",
3055
3056
  chalk: "^4.1.2",
3056
3057
  "cli-table3": "^0.6.4",
3057
3058
  commander: "^12.0.0",
@@ -67465,43 +67466,53 @@ var require_guard = __commonJS({
67465
67466
  }
67466
67467
  const enforceable = receiptVerified && (kind === "ALLOW" || sinkWired);
67467
67468
  if (enforceable) {
67468
- const execStateMode = config.requireExecutionStateMatch;
67469
- if (execStateMode === true || execStateMode === "warn") {
67470
- const et = (0, execution_time_fingerprint_js_1.checkExecutionTimeFingerprint)({
67471
- artifacts: detection.artifacts,
67472
- // Same context slots the guard sent on preflight (server folds these into crbundle.v1).
67473
- context: {
67474
- operation: config.operation ?? "tool_call",
67475
- environment: config.environment
67476
- },
67477
- envelope
67469
+ const execStateMode = config.requireExecutionStateMatch === void 0 ? true : config.requireExecutionStateMatch;
67470
+ if (execStateMode === false) {
67471
+ emit(config, {
67472
+ type: "execution_state_check_disabled",
67473
+ at: iso(),
67474
+ decisionId: envelope.decision_id,
67475
+ cause: "requireExecutionStateMatch_false"
67478
67476
  });
67479
- if (!et.match) {
67480
- if (execStateMode === true) {
67481
- breakerRecord(config);
67482
- return closedIntegrity(config, "EXECUTION_STATE_DRIFT", failPolicy, fctx, cwctx, redacted, detection.artifacts);
67483
- }
67484
- if ((0, execution_time_fingerprint_js_1.isUnmeasurableExecutionStateReason)(et.reason)) {
67485
- emit(config, {
67486
- type: "execution_state_unmeasurable",
67487
- at: iso(),
67488
- decisionId: envelope.decision_id,
67489
- current_fingerprint: et.current_fingerprint,
67490
- authorized_fingerprint: et.authorized_fingerprint,
67491
- reason: et.reason,
67492
- note: execution_time_fingerprint_js_1.EXECUTION_STATE_UNMEASURABLE_NOTE
67493
- });
67494
- } else {
67495
- emit(config, {
67496
- type: "execution_state_drift_observed",
67497
- at: iso(),
67498
- decisionId: envelope.decision_id,
67499
- current_fingerprint: et.current_fingerprint,
67500
- authorized_fingerprint: et.authorized_fingerprint,
67501
- reason: et.reason
67502
- });
67503
- }
67477
+ const offVerdict = kind === "ALLOW" ? { kind: "ALLOW", action: "CONTINUE", envelope, receiptVerified } : { kind: "MONITOR", action: "CONTINUE_WITH_MONITORING", envelope, receiptVerified };
67478
+ return runUnenforced(config, executeFactory, envelope, offVerdict, true, redacted, freshBasis, cwBasis);
67479
+ }
67480
+ const et = (0, execution_time_fingerprint_js_1.checkExecutionTimeFingerprint)({
67481
+ artifacts: detection.artifacts,
67482
+ context: {
67483
+ operation: config.operation ?? "tool_call",
67484
+ environment: config.environment
67485
+ },
67486
+ envelope
67487
+ });
67488
+ if (!et.match) {
67489
+ const unmeasurable = (0, execution_time_fingerprint_js_1.isUnmeasurableExecutionStateReason)(et.reason);
67490
+ if (execStateMode === true) {
67491
+ breakerRecord(config);
67492
+ return closedIntegrity(config, unmeasurable ? "EXECUTION_STATE_UNMEASURABLE" : "EXECUTION_STATE_DRIFT", failPolicy, fctx, cwctx, redacted, detection.artifacts);
67493
+ }
67494
+ if (unmeasurable) {
67495
+ emit(config, {
67496
+ type: "execution_state_unmeasurable",
67497
+ at: iso(),
67498
+ decisionId: envelope.decision_id,
67499
+ current_fingerprint: et.current_fingerprint,
67500
+ authorized_fingerprint: et.authorized_fingerprint,
67501
+ reason: et.reason,
67502
+ note: execution_time_fingerprint_js_1.EXECUTION_STATE_UNMEASURABLE_NOTE
67503
+ });
67504
+ } else {
67505
+ emit(config, {
67506
+ type: "execution_state_drift_observed",
67507
+ at: iso(),
67508
+ decisionId: envelope.decision_id,
67509
+ current_fingerprint: et.current_fingerprint,
67510
+ authorized_fingerprint: et.authorized_fingerprint,
67511
+ reason: et.reason
67512
+ });
67504
67513
  }
67514
+ const warnVerdict = kind === "ALLOW" ? { kind: "ALLOW", action: "CONTINUE", envelope, receiptVerified } : { kind: "MONITOR", action: "CONTINUE_WITH_MONITORING", envelope, receiptVerified };
67515
+ return runUnenforced(config, executeFactory, envelope, warnVerdict, true, redacted, freshBasis, cwBasis);
67505
67516
  }
67506
67517
  const approved = kind === "ALLOW" ? { kind: "ALLOW", action: "CONTINUE", envelope, receiptVerified: true } : { kind: "MONITOR", action: "CONTINUE_WITH_MONITORING", envelope, receiptVerified: true };
67507
67518
  return runEnforced(config, executeFactory, approved, redacted, freshBasis, cwBasis);
@@ -69861,24 +69872,25 @@ var require_merge_gate = __commonJS({
69861
69872
  if (!isAllowClass(receipt, allowWarnMerge)) {
69862
69873
  return fail("failure", "decision_not_allow");
69863
69874
  }
69864
- const inescapable_merge = enforcement_state === "ENFORCING" && protection.admin_bypass_possible === false;
69875
+ const protectionOk = enforcement_state === "ENFORCING" && protection.admin_bypass_possible === false;
69876
+ const appBound = protection.required_check_app_bound === true;
69877
+ const rebound = rc.expected_fingerprint != null && String(rc.expected_fingerprint).length > 0;
69878
+ const inescapable_merge = protectionOk && appBound && rebound;
69865
69879
  const residuals = [];
69866
- if (!inescapable_merge) {
69880
+ if (!protectionOk) {
69867
69881
  if (enforcement_state === "ENFORCING")
69868
69882
  residuals.push("admin_bypass_open");
69869
69883
  else if (enforcement_state === "ADVISORY")
69870
69884
  residuals.push("protection_advisory_only");
69871
69885
  else
69872
69886
  residuals.push("protection_not_configured");
69887
+ } else if (protection.required_check_app_bound === true) {
69888
+ } else if (protection.required_check_app_bound === false) {
69889
+ residuals.push("required_check_app_not_bound");
69873
69890
  } else {
69874
- if (protection.required_check_app_bound === true) {
69875
- } else if (protection.required_check_app_bound === false) {
69876
- residuals.push("required_check_app_not_bound");
69877
- } else {
69878
- residuals.push("required_check_app_binding_unknown");
69879
- }
69891
+ residuals.push("required_check_app_binding_unknown");
69880
69892
  }
69881
- if (rc.expected_fingerprint == null) {
69893
+ if (!rebound) {
69882
69894
  residuals.push("change_set_not_rebound");
69883
69895
  }
69884
69896
  const residual = residuals.length > 0 ? residuals[0] : void 0;
@@ -70001,15 +70013,17 @@ var require_deploy_gate = __commonJS({
70001
70013
  if (!isAllowClass(receipt, allowWarnDeploy)) {
70002
70014
  return deny("failure", "decision_not_allow");
70003
70015
  }
70004
- const inescapable_deploy = enforcement_state === "ENFORCING" && enf.bypass_possible === false;
70016
+ const enforcementOk = enforcement_state === "ENFORCING" && enf.bypass_possible === false;
70017
+ const rebound = rc.expected_fingerprint != null && String(rc.expected_fingerprint).length > 0;
70018
+ const inescapable_deploy = enforcementOk && rebound;
70005
70019
  const residuals = [];
70006
- if (!inescapable_deploy) {
70020
+ if (!enforcementOk) {
70007
70021
  if (enforcement_state === "ENFORCING")
70008
70022
  residuals.push("bypass_open");
70009
70023
  else
70010
70024
  residuals.push("enforcement_not_configured");
70011
70025
  }
70012
- if (rc.expected_fingerprint == null) {
70026
+ if (!rebound) {
70013
70027
  residuals.push("change_set_not_rebound");
70014
70028
  }
70015
70029
  const residual = residuals.length > 0 ? residuals[0] : void 0;
@@ -70298,6 +70312,7 @@ var require_with_coderifts = __commonJS({
70298
70312
  var COMPOSITION_CALL_POLICY_COMPLETE = false;
70299
70313
  var RESIDUAL_CALL_POLICY_INCOMPLETE = "composition_call_policy_incomplete";
70300
70314
  var RESIDUAL_FRESHNESS_NOT_CONFIGURED = "composition_freshness_not_configured";
70315
+ var RESIDUAL_CALLS_OUTSIDE_GUARDED_PATH = "calls_outside_guarded_path_invisible";
70301
70316
  var RESIDUAL_FORCED_READONLY = "composition_forced_readonly_on_heuristic_mutator";
70302
70317
  var RESIDUAL_UNKNOWN_READONLY = "composition_unknown_treated_as_readonly";
70303
70318
  var COVERAGE_STRENGTH = {
@@ -70309,6 +70324,30 @@ var require_with_coderifts = __commonJS({
70309
70324
  function coverageRank(coverage) {
70310
70325
  return Object.prototype.hasOwnProperty.call(COVERAGE_STRENGTH, coverage) ? COVERAGE_STRENGTH[coverage] : void 0;
70311
70326
  }
70327
+ function isEnforcingStrict(input) {
70328
+ return input.profile === "ENFORCING_STRICT";
70329
+ }
70330
+ function enforcingStrictWeakenFlags(input) {
70331
+ const flags = [];
70332
+ if (input.requireCoverage !== void 0) {
70333
+ const rank = coverageRank(input.requireCoverage);
70334
+ if (rank !== void 0 && rank < COVERAGE_STRENGTH.COMPLETE)
70335
+ flags.push("requireCoverage");
70336
+ }
70337
+ if (input.requireFreshness === false)
70338
+ flags.push("requireFreshness");
70339
+ if (input.requireExecutionStateMatch === false || input.requireExecutionStateMatch === "warn") {
70340
+ flags.push("requireExecutionStateMatch");
70341
+ }
70342
+ if (input.requireConditionalWrite === false)
70343
+ flags.push("requireConditionalWrite");
70344
+ const reg = input.registry ?? {};
70345
+ if (reg.failOnUnguardedMutator === false)
70346
+ flags.push("failOnUnguardedMutator");
70347
+ if (reg.unknownToolPolicy === "readonly")
70348
+ flags.push("unknownToolPolicy");
70349
+ return flags;
70350
+ }
70312
70351
  async function safeOnSettledCall(onSettledCall, payload) {
70313
70352
  try {
70314
70353
  await Promise.resolve(onSettledCall(payload));
@@ -70495,6 +70534,18 @@ var require_with_coderifts = __commonJS({
70495
70534
  if (input.requireCoverage !== void 0 && coverageRank(input.requireCoverage) === void 0) {
70496
70535
  problems.push(`\`requireCoverage\` must be one of COMPLETE | PARTIAL | BYPASSED | UNKNOWN (got ${JSON.stringify(input.requireCoverage)})`);
70497
70536
  }
70537
+ if (input.profile !== void 0 && input.profile !== "ENFORCING_STRICT") {
70538
+ problems.push(`\`profile\` must be 'ENFORCING_STRICT' when set (got ${JSON.stringify(input.profile)})`);
70539
+ }
70540
+ if (isEnforcingStrict(input)) {
70541
+ const weaken = enforcingStrictWeakenFlags(input);
70542
+ if (weaken.length > 0) {
70543
+ problems.push(`ENFORCING_STRICT cannot be weakened: ${weaken.join(", ")} conflicts`);
70544
+ }
70545
+ if (typeof input.resolvePriorContent !== "function") {
70546
+ problems.push("ENFORCING_STRICT cannot be weakened: resolvePriorContent conflicts");
70547
+ }
70548
+ }
70498
70549
  if (problems.length > 0) {
70499
70550
  throw new Error(`withCodeRifts: construction aborted \u2014 ${problems.length} condition(s):
70500
70551
  ` + problems.map((p) => ` - ${p}`).join("\n"));
@@ -70533,20 +70584,27 @@ var require_with_coderifts = __commonJS({
70533
70584
  if (input.requireExecutionStateMatch !== void 0) {
70534
70585
  guard.requireExecutionStateMatch = input.requireExecutionStateMatch;
70535
70586
  }
70587
+ const strict = isEnforcingStrict(input);
70588
+ if (strict) {
70589
+ guard.requireFreshness = true;
70590
+ guard.requireConditionalWrite = true;
70591
+ guard.requireExecutionStateMatch = true;
70592
+ }
70536
70593
  const config = {
70537
70594
  guard,
70538
- unknownToolPolicy: reg.unknownToolPolicy ?? "mutating",
70595
+ unknownToolPolicy: strict ? reg.unknownToolPolicy === "reject" ? "reject" : "mutating" : reg.unknownToolPolicy ?? "mutating",
70539
70596
  classify: reg.classify,
70540
70597
  binders: reg.binders,
70541
70598
  forceReadonly: reg.forceReadonly,
70542
- failOnUnguardedMutator: reg.failOnUnguardedMutator
70599
+ failOnUnguardedMutator: strict ? true : reg.failOnUnguardedMutator
70543
70600
  };
70601
+ const requireCoverage = strict ? "COMPLETE" : input.requireCoverage;
70544
70602
  const { tools, report } = (0, tool_registry_js_1.guardToolRegistry)(input.tools, config);
70545
- if (input.requireCoverage !== void 0) {
70546
- const requiredRank = coverageRank(input.requireCoverage);
70603
+ if (requireCoverage !== void 0) {
70604
+ const requiredRank = coverageRank(requireCoverage);
70547
70605
  const actualRank = coverageRank(report.coverage) ?? -1;
70548
70606
  if (requiredRank !== void 0 && actualRank < requiredRank) {
70549
- throw new Error(`withCodeRifts: requireCoverage not met \u2014 registry coverage '${report.coverage}' is weaker than required '${input.requireCoverage}' (strength ordering COMPLETE > PARTIAL > BYPASSED > UNKNOWN). requireCoverage constrains the REGISTRY tool-boundary surface ONLY; a green construction here is NOT a product-level runtime-inescapability guarantee \u2014 composition_assurance.inescapable_runtime stays false until receipt carry-forward and a freshness-safe prior for write-style calls land.`);
70607
+ throw new Error(`withCodeRifts: requireCoverage not met \u2014 registry coverage '${report.coverage}' is weaker than required '${requireCoverage}' (strength ordering COMPLETE > PARTIAL > BYPASSED > UNKNOWN). requireCoverage constrains the REGISTRY tool-boundary surface ONLY; a green construction here is NOT a product-level runtime-inescapability guarantee \u2014 composition_assurance.inescapable_runtime stays false until receipt carry-forward and a freshness-safe prior for write-style calls land.`);
70550
70608
  }
70551
70609
  }
70552
70610
  const compositionInescapableRuntime = report.claim.inescapable_runtime && COMPOSITION_CALL_POLICY_COMPLETE;
@@ -70561,9 +70619,12 @@ var require_with_coderifts = __commonJS({
70561
70619
  if (!freshness_resolver_wired) {
70562
70620
  residuals.push(RESIDUAL_FRESHNESS_NOT_CONFIGURED);
70563
70621
  }
70564
- if (input.requireConditionalWrite === true) {
70622
+ if (input.requireConditionalWrite === true || strict) {
70565
70623
  residuals.push("composition_unconditional_write_under_policy");
70566
70624
  }
70625
+ if (strict) {
70626
+ residuals.push(RESIDUAL_CALLS_OUTSIDE_GUARDED_PATH);
70627
+ }
70567
70628
  const composition_assurance = {
70568
70629
  coverage: "PARTIAL",
70569
70630
  inescapable_runtime: compositionInescapableRuntime,
@@ -71755,7 +71816,8 @@ var require_deploy_gate2 = __commonJS({
71755
71816
  if (enforcement === "ENFORCING") out.push("bypass_open");
71756
71817
  else if (enforcement === "ADVISORY") out.push("deploy_gate_advisory");
71757
71818
  else if (enforcement === "ABSENT") out.push("deploy_path_ungated");
71758
- } else if (change_set_rebound !== true) {
71819
+ }
71820
+ if (change_set_rebound !== true) {
71759
71821
  out.push("change_set_not_rebound");
71760
71822
  }
71761
71823
  return out;
@@ -71793,8 +71855,8 @@ var require_deploy_gate2 = __commonJS({
71793
71855
  if (expected_body_hash != null) requiredContext.expected_body_hash = expected_body_hash;
71794
71856
  }
71795
71857
  const gate = deployGate({ deployTarget: { environment, artifact_id }, receipt, requiredContext });
71796
- const enforcement_inescapable = gate.inescapable_deploy === true;
71797
- const inescapable_deploy = enforcement_inescapable && change_set_rebound === true;
71858
+ const enforcement_inescapable = attested_enforcement === "ENFORCING" && !!(observed_cd_enforcement && observed_cd_enforcement.bypass_possible === false);
71859
+ const inescapable_deploy = gate.inescapable_deploy === true && change_set_rebound === true;
71798
71860
  return {
71799
71861
  deploy_check_status: gate.state,
71800
71862
  reason: gate.reason,
@@ -73285,9 +73347,9 @@ generator_detection: true
73285
73347
  chalk.dim(" (uses YOUR gh credentials; CodeRifts never takes admin rights)."),
73286
73348
  chalk.dim(" To attest cross-layer ENFORCING after wiring: coderifts enforce --apply"),
73287
73349
  chalk.dim(" (dry-run without --apply; does not auto-flip defaults)."),
73288
- chalk.dim(" For agent tool-call guarding with execution-state telemetry: set"),
73289
- chalk.dim(" requireExecutionStateMatch: 'warn' in your guard config"),
73290
- chalk.dim(" (observes drift, does not block) \u2014 see: coderifts agent-setup"),
73350
+ chalk.dim(" For agent tool-call guarding: @coderifts/agent-guard requireExecutionStateMatch"),
73351
+ chalk.dim(" defaults ON (absent\u2192true) \u2014 observed drift STOPS. Opt-down: 'warn' (emit, proceed)"),
73352
+ chalk.dim(" or false (proceed-on-drift) \u2014 see: coderifts agent-setup"),
73291
73353
  chalk.dim(" Agent-using repo? Run: coderifts agent-setup")
73292
73354
  ]);
73293
73355
  module2.exports = {
@@ -100150,13 +100212,14 @@ ${USAGE}` };
100150
100212
  return { exitCode: 0, code: "OK", outDir, ...summary };
100151
100213
  }
100152
100214
  var AGENT_SETUP_WARN_TELEMETRY_LINES = Object.freeze([
100153
- chalk.bold(" Opt-in: execution-state warn telemetry (does not block)"),
100154
- chalk.dim(" Set requireExecutionStateMatch: 'warn' in @coderifts/agent-guard"),
100155
- chalk.dim(" (or withCodeRifts) config. Default remains OFF \u2014 this is opt-in only."),
100156
- chalk.dim(" What it emits (then PROCEEDS \u2014 never blocks in warn mode):"),
100215
+ chalk.bold(" Execution-state recheck (guard@8): DEFAULT ON"),
100216
+ chalk.dim(" requireExecutionStateMatch absent/true \u2192 enforce: mismatch \u2192 EXECUTION_STATE_DRIFT"),
100217
+ chalk.dim(" (factory does not run); missing fingerprint/artifacts \u2192 EXECUTION_STATE_UNMEASURABLE (STOP)."),
100218
+ chalk.dim(" Opt-down: requireExecutionStateMatch: 'warn' (emit, then run unenforced on mismatch)"),
100219
+ chalk.dim(" or false (no recheck \u2014 v7 proceed-on-drift). Must be set explicitly."),
100220
+ chalk.dim(" Warn emits (then PROCEEDS \u2014 never blocks in warn mode):"),
100157
100221
  chalk.dim(" \xB7 real drift \u2192 execution_state_drift_observed (loud)"),
100158
100222
  chalk.dim(" \xB7 missing artifacts/fingerprint \u2192 execution_state_unmeasurable (quiet)"),
100159
- chalk.dim(" Enforce later with requireExecutionStateMatch: true only when ready."),
100160
100223
  chalk.dim(" Merge gate block is separate: coderifts setup-required-check --apply")
100161
100224
  ]);
100162
100225
  module2.exports = {
@@ -101133,21 +101196,23 @@ var require_claude_hook = __commonJS({
101133
101196
  "configured contract spec path. Reads JSON context from STDIN.",
101134
101197
  "",
101135
101198
  "Exit map (Claude Code semantics \u2014 get this exactly right):",
101136
- " 2 BLOCK \u2014 tool call cancelled (decision BLOCK/STOP or unrecognised execution_action)",
101137
- " 0 allow \u2014 or soft-skip (see soft contracts)",
101199
+ " 2 BLOCK \u2014 tool call cancelled (BLOCK/STOP, REQUEST_APPROVAL, unrecognised action,",
101200
+ " governance could not run, CONTINUE_WITH_MONITORING without a wired sink)",
101201
+ " 0 allow \u2014 CONTINUE, or CWM with a host-asserted monitoring sink, or non-contract skip",
101138
101202
  " 1 NEVER used for security deny (Claude treats exit 1 as non-blocking; action proceeds)",
101139
101203
  "",
101140
- "Soft contracts DEFAULT (exit 0 + one-line stderr \u2014 do not brick the session):",
101141
- " no API key (git config coderifts.apiKey / coderifts login / CODERIFTS_API_KEY)",
101142
- " unparseable/unknown stdin shape",
101143
- " file_path is not the configured spec path",
101144
- " Edit/MultiEdit cannot be applied cleanly to disk content",
101145
- " API unreachable / network error / disk unreadable",
101204
+ "Fail-closed DEFAULT (exit 2, reason enforce_indeterminate) when governance could not run:",
101205
+ " no API key / API unreachable / disk unreadable / edit-apply failure on the spec path",
101206
+ " Absence of a key is not permission. Explicit opt-out: CODERIFTS_ADVISORY=1|true.",
101207
+ "",
101208
+ "Still exit 0 (nothing to govern): unparseable stdin, missing file_path, non-spec path,",
101209
+ "identical content.",
101146
101210
  "",
101147
- "CODERIFTS_STRICT=1|true (opt-in): enforcement-relevant soft sites \u2192 EXIT 2 (BLOCK).",
101148
- " Hardens: no API key, API unreachable, disk unreadable, edit-apply failure on the spec.",
101149
- " Still soft: non-spec path, identical content, bad stdin, WARN (governance already ran).",
101150
- " Host-side choice for CI/prod \u2014 not a package-level guarantee.",
101211
+ "CONTINUE_WITH_MONITORING: allow only if the host asserts a sink",
101212
+ " (CODERIFTS_MONITORING_SINK_WIRED=1|true or git config coderifts.monitoringSinkWired).",
101213
+ " Host claim \u2014 not delivery proof (same class as GuardConfig.monitoringSinkWired).",
101214
+ "",
101215
+ "CODERIFTS_STRICT=1|true: same fail-closed as default; cannot be weakened by ADVISORY.",
101151
101216
  "",
101152
101217
  "Spec path (same source as git pre-push hook):",
101153
101218
  " git config coderifts.specPath (default: api/openapi.yaml)",
@@ -101158,21 +101223,56 @@ var require_claude_hook = __commonJS({
101158
101223
  "",
101159
101224
  "Push-time equivalent: coderifts hook install (git exit 1 on BLOCK)."
101160
101225
  ].join("\n");
101161
- function isStrictMode(env = process.env) {
101162
- const v = env && env.CODERIFTS_STRICT;
101226
+ function isEnvFlag(env, name) {
101227
+ const v = env && env[name];
101163
101228
  if (v == null || v === "") return false;
101164
101229
  const s = String(v).trim().toLowerCase();
101165
101230
  return s === "1" || s === "true";
101166
101231
  }
101167
- function softOrStrictBlock({ strict, reason, softMsg, why, errLog }) {
101232
+ function isStrictMode(env = process.env) {
101233
+ return isEnvFlag(env, "CODERIFTS_STRICT");
101234
+ }
101235
+ function isAdvisoryMode(env = process.env) {
101236
+ return isEnvFlag(env, "CODERIFTS_ADVISORY");
101237
+ }
101238
+ function isMonitoringSinkWired({ env = process.env, deps = {}, result = null } = {}) {
101239
+ if (deps && deps.monitoringSinkWired === true) return true;
101240
+ if (isEnvFlag(env, "CODERIFTS_MONITORING_SINK_WIRED")) return true;
101241
+ const git = readGitConfig("coderifts.monitoringSinkWired", deps);
101242
+ if (git) {
101243
+ const s = String(git).trim().toLowerCase();
101244
+ if (s === "1" || s === "true") return true;
101245
+ }
101246
+ if (result && result.monitoringSinkWired === true) return true;
101247
+ const dr = result && result.decision_result;
101248
+ if (dr && typeof dr === "object" && dr.monitoringSinkWired === true) return true;
101249
+ return false;
101250
+ }
101251
+ function failClosedOrAdvisory({ advisory, strict, site, softMsg, why, errLog }) {
101252
+ if (advisory && !strict) {
101253
+ errLog(`${softMsg} (CODERIFTS_ADVISORY)`);
101254
+ return { exitCode: 0, reason: site, site };
101255
+ }
101168
101256
  if (strict) {
101169
101257
  errLog(
101170
101258
  `CodeRifts claude-hook: CODERIFTS_STRICT: governance could not run (${why}) \u2014 blocking. Set a key / restore network, or unset CODERIFTS_STRICT.`
101171
101259
  );
101172
- return { exitCode: 2, reason, strictBlocked: true };
101260
+ return { exitCode: 2, reason: "enforce_indeterminate", site, strictBlocked: true };
101173
101261
  }
101174
- errLog(softMsg);
101175
- return { exitCode: 0, reason };
101262
+ errLog(
101263
+ `CodeRifts claude-hook: governance could not run (${why}) \u2014 blocking. Set CODERIFTS_ADVISORY=1 to allow without governance (explicit opt-out).`
101264
+ );
101265
+ return { exitCode: 2, reason: "enforce_indeterminate", site };
101266
+ }
101267
+ function softOrStrictBlock({ strict, reason, softMsg, why, errLog }) {
101268
+ return failClosedOrAdvisory({
101269
+ advisory: false,
101270
+ strict: !!strict,
101271
+ site: reason,
101272
+ softMsg,
101273
+ why,
101274
+ errLog
101275
+ });
101176
101276
  }
101177
101277
  function readGitConfig(key, deps = {}) {
101178
101278
  const run = deps.execSync || execSync;
@@ -101279,7 +101379,7 @@ var require_claude_hook = __commonJS({
101279
101379
  return { ok: false, reason: `unsupported tool_name for content derive: ${name}` };
101280
101380
  }
101281
101381
  function mapDecisionSeverity(result) {
101282
- const d = result && typeof result === "object" ? result : {};
101382
+ const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101283
101383
  let ea = null;
101284
101384
  const dr = d.decision_result;
101285
101385
  if (dr && typeof dr === "object" && typeof dr.execution_action === "string") {
@@ -101287,20 +101387,33 @@ var require_claude_hook = __commonJS({
101287
101387
  } else if (typeof d.execution_action === "string") {
101288
101388
  ea = d.execution_action;
101289
101389
  }
101390
+ const hasDecision = d.omega_decision != null && d.omega_decision !== "" || d.decision != null && d.decision !== "" || dr && typeof dr === "object" && dr.execution_action;
101391
+ if (d.error && !hasDecision && (ea == null || ea === "")) {
101392
+ return {
101393
+ severity: "INDETERMINATE",
101394
+ decision: null,
101395
+ decisionId: null,
101396
+ executionAction: null
101397
+ };
101398
+ }
101290
101399
  let severity;
101291
101400
  if (ea != null && ea !== "") {
101292
101401
  if (!CLOSED_ACTIONS.has(ea)) {
101293
101402
  severity = "UNKNOWN";
101294
- } else if (ea === "CONTINUE" || ea === "CONTINUE_WITH_MONITORING") {
101403
+ } else if (ea === "CONTINUE") {
101295
101404
  severity = "ALLOW";
101405
+ } else if (ea === "CONTINUE_WITH_MONITORING") {
101406
+ severity = "MONITOR";
101296
101407
  } else if (ea === "STOP") {
101297
101408
  severity = "BLOCK";
101298
101409
  } else {
101299
101410
  severity = "REQUIRE_APPROVAL";
101300
101411
  }
101301
101412
  } else {
101302
- const od = d.omega_decision || d.decision || "ALLOW";
101303
- if (od === "BLOCK") severity = "BLOCK";
101413
+ const od = d.omega_decision || d.decision;
101414
+ if (od == null || od === "") {
101415
+ severity = "INDETERMINATE";
101416
+ } else if (od === "BLOCK") severity = "BLOCK";
101304
101417
  else if (od === "REQUIRE_APPROVAL") severity = "REQUIRE_APPROVAL";
101305
101418
  else if (od === "WARN") severity = "WARN";
101306
101419
  else severity = "ALLOW";
@@ -101309,7 +101422,7 @@ var require_claude_hook = __commonJS({
101309
101422
  const decisionId = dr && dr.decision_id || d.decision_id || null;
101310
101423
  return {
101311
101424
  severity,
101312
- decision: decision != null ? String(decision) : null,
101425
+ decision: decision != null && decision !== "" ? String(decision) : null,
101313
101426
  decisionId: decisionId != null ? String(decisionId) : null,
101314
101427
  executionAction: ea
101315
101428
  };
@@ -101329,11 +101442,13 @@ var require_claude_hook = __commonJS({
101329
101442
  const cwd = deps.cwd || process.cwd();
101330
101443
  const env = deps.env || process.env;
101331
101444
  const strict = isStrictMode(env);
101445
+ const advisory = isAdvisoryMode(env);
101332
101446
  const apiKey = resolveApiKey({ ...deps, cwd, env });
101333
101447
  if (!apiKey) {
101334
- return softOrStrictBlock({
101448
+ return failClosedOrAdvisory({
101449
+ advisory,
101335
101450
  strict,
101336
- reason: "missing_api_key",
101451
+ site: "missing_api_key",
101337
101452
  why: "no API key",
101338
101453
  softMsg: "CodeRifts claude-hook: no API key (coderifts login / CODERIFTS_API_KEY / git config coderifts.apiKey) \u2014 allowing",
101339
101454
  errLog
@@ -101361,9 +101476,10 @@ var require_claude_hook = __commonJS({
101361
101476
  try {
101362
101477
  diskBefore = readFile(absPath, "utf8");
101363
101478
  } catch (e) {
101364
- return softOrStrictBlock({
101479
+ return failClosedOrAdvisory({
101480
+ advisory,
101365
101481
  strict,
101366
- reason: "disk_unreadable",
101482
+ site: "disk_unreadable",
101367
101483
  why: `cannot read contract file ${absPath}`,
101368
101484
  softMsg: `CodeRifts claude-hook: cannot read ${absPath} \u2014 allowing (soft)`,
101369
101485
  errLog
@@ -101372,9 +101488,10 @@ var require_claude_hook = __commonJS({
101372
101488
  }
101373
101489
  const derived = deriveAfterContent(toolName, toolInput, diskBefore);
101374
101490
  if (!derived.ok) {
101375
- return softOrStrictBlock({
101491
+ return failClosedOrAdvisory({
101492
+ advisory,
101376
101493
  strict,
101377
- reason: "edit_apply_failed",
101494
+ site: "edit_apply_failed",
101378
101495
  why: derived.reason,
101379
101496
  softMsg: `CodeRifts claude-hook: ${derived.reason} \u2014 allowing (soft; never guess edit apply)`,
101380
101497
  errLog
@@ -101391,9 +101508,10 @@ var require_claude_hook = __commonJS({
101391
101508
  });
101392
101509
  } catch (e) {
101393
101510
  const msg = e && e.message ? String(e.message) : "request failed";
101394
- return softOrStrictBlock({
101511
+ return failClosedOrAdvisory({
101512
+ advisory,
101395
101513
  strict,
101396
- reason: "api_unreachable",
101514
+ site: "api_unreachable",
101397
101515
  why: `API unreachable: ${msg}`,
101398
101516
  softMsg: `CodeRifts claude-hook: API unreachable (${msg}) \u2014 allowing (soft; availability must not brick the editor)`,
101399
101517
  errLog
@@ -101403,6 +101521,18 @@ var require_claude_hook = __commonJS({
101403
101521
  const idPart = mapped.decisionId ? ` decision_id=${mapped.decisionId}` : "";
101404
101522
  const decPart = mapped.decision ? ` decision=${mapped.decision}` : "";
101405
101523
  const eaPart = mapped.executionAction ? ` execution_action=${mapped.executionAction}` : "";
101524
+ if (mapped.severity === "INDETERMINATE") {
101525
+ errLog(
101526
+ "CodeRifts claude-hook: BLOCKED (indeterminate response \u2014 no execution_action and no decision; not permission)"
101527
+ );
101528
+ return {
101529
+ exitCode: 2,
101530
+ reason: "indeterminate",
101531
+ severity: "INDETERMINATE",
101532
+ decision: mapped.decision,
101533
+ decisionId: mapped.decisionId
101534
+ };
101535
+ }
101406
101536
  if (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN") {
101407
101537
  errLog(
101408
101538
  `CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : "")
@@ -101415,16 +101545,40 @@ var require_claude_hook = __commonJS({
101415
101545
  decisionId: mapped.decisionId
101416
101546
  };
101417
101547
  }
101418
- if (mapped.severity === "REQUIRE_APPROVAL" || mapped.severity === "WARN") {
101419
- errLog(`CodeRifts claude-hook: WARNING ${mapped.severity}${decPart}${eaPart}${idPart}`);
101548
+ if (mapped.severity === "REQUIRE_APPROVAL") {
101549
+ errLog(
101550
+ `CodeRifts claude-hook: BLOCKED approval_required${decPart}${eaPart}${idPart}`
101551
+ );
101420
101552
  return {
101421
- exitCode: 0,
101422
- reason: "warn",
101553
+ exitCode: 2,
101554
+ reason: "approval_required",
101423
101555
  severity: mapped.severity,
101424
101556
  decision: mapped.decision,
101425
101557
  decisionId: mapped.decisionId
101426
101558
  };
101427
101559
  }
101560
+ if (mapped.severity === "MONITOR" || mapped.severity === "WARN") {
101561
+ const sinkWired = isMonitoringSinkWired({ env, deps, result });
101562
+ if (!sinkWired) {
101563
+ errLog(
101564
+ `CodeRifts claude-hook: BLOCKED monitoring_unwired${decPart}${eaPart}${idPart} \u2014 CONTINUE_WITH_MONITORING requires CODERIFTS_MONITORING_SINK_WIRED=1 (host assertion; not delivery proof)`
101565
+ );
101566
+ return {
101567
+ exitCode: 2,
101568
+ reason: "monitoring_unwired",
101569
+ severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
101570
+ decision: mapped.decision,
101571
+ decisionId: mapped.decisionId
101572
+ };
101573
+ }
101574
+ return {
101575
+ exitCode: 0,
101576
+ reason: "allow",
101577
+ severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
101578
+ decision: mapped.decision,
101579
+ decisionId: mapped.decisionId
101580
+ };
101581
+ }
101428
101582
  return {
101429
101583
  exitCode: 0,
101430
101584
  reason: "allow",
@@ -101442,7 +101596,11 @@ var require_claude_hook = __commonJS({
101442
101596
  resolveApiKey,
101443
101597
  resolveSpecPath,
101444
101598
  readGitConfig,
101599
+ isEnvFlag,
101445
101600
  isStrictMode,
101601
+ isAdvisoryMode,
101602
+ isMonitoringSinkWired,
101603
+ failClosedOrAdvisory,
101446
101604
  softOrStrictBlock,
101447
101605
  DEFAULT_SPEC_PATH,
101448
101606
  CLOSED_ACTIONS,
@@ -103446,7 +103604,7 @@ ${USAGE}
103446
103604
  }
103447
103605
  });
103448
103606
  program.command("claude-hook").description(
103449
- "Claude Code PreToolUse gate: block contract-touching Write|Edit|MultiEdit on BLOCK/STOP (exit 2). Soft-allow when unconfigured/unparseable/API down."
103607
+ "Claude Code PreToolUse gate: block contract-touching Write|Edit|MultiEdit on BLOCK/STOP/REQUEST_APPROVAL (exit 2). Fail-closed when governance cannot run; CODERIFTS_ADVISORY=1 is the explicit soft opt-out."
103450
103608
  ).addHelpText("after", () => {
103451
103609
  const { USAGE } = require_claude_hook();
103452
103610
  return `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "3.4.0",
3
+ "version": "4.0.0",
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",
@@ -13,6 +13,7 @@
13
13
  "bin/",
14
14
  "scripts/",
15
15
  "README.md",
16
+ "CHANGELOG.md",
16
17
  "corpus/"
17
18
  ],
18
19
  "keywords": [
@@ -44,7 +45,7 @@
44
45
  "test": "node --test test/*.test.js"
45
46
  },
46
47
  "dependencies": {
47
- "@coderifts/agent-guard": "^6.4.0",
48
+ "@coderifts/agent-guard": "^8.1.1",
48
49
  "chalk": "^4.1.2",
49
50
  "cli-table3": "^0.6.4",
50
51
  "commander": "^12.0.0",
@@ -5,18 +5,19 @@
5
5
  * CLI release gate (prepublishOnly): refuse to publish if the declared
6
6
  * @coderifts/agent-guard range floors below the minimum acceptable major.
7
7
  *
8
- * Honest scope: compares the dependency *range floor* (e.g. ^6.4.06) to a
8
+ * Honest scope: compares the dependency *range floor* (e.g. ^8.1.18) to a
9
9
  * pinned constant. Does not call npm registry. Optional: if node_modules has a
10
10
  * resolved package, also fail when its major is below the floor (stale install).
11
11
  *
12
12
  * Why: evening audit 2.4 — CLI sat on ^1.6.0 while published guard is 6.x.
13
+ * 4.0.0: floor raised to 8 (deployGate inescapable_deploy includes fingerprint rebound).
13
14
  */
14
15
 
15
16
  const fs = require('fs');
16
17
  const path = require('path');
17
18
 
18
19
  /** Minimum acceptable major for @coderifts/agent-guard (published line). */
19
- const MIN_GUARD_MAJOR = 6;
20
+ const MIN_GUARD_MAJOR = 8;
20
21
  const DEP = '@coderifts/agent-guard';
21
22
 
22
23
  function fail(msg) {