coderifts 4.8.0 → 4.10.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 CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.10.0
4
+
5
+ ### Added
6
+ - **`coderifts init --agents --strict`** writes the STRICT contract-gate
7
+ workflow: `require-verified-monitoring: 'true'` (plus attestation /
8
+ keyring inputs). The default template is unchanged (monitoring stays
9
+ commented / false).
10
+
11
+ ## 4.9.0
12
+
13
+ ### Fixed
14
+ - **P0 / ID621 — `enforce --check` no longer reports `inescapable_deploy: true`
15
+ for an unenforced configuration.** External audit 2026-08-24 reproduced two
16
+ holes on the published CLI: (a) the required check matched by **name only**
17
+ (`required_status_checks.checks[]` with no `app_id`), so any repository
18
+ writer could satisfy it with a same-named success status; (b) the two local
19
+ workflow checks were **text searches**, so `# uses: coderifts/contract-gate@v0`
20
+ in a comment counted as VERIFIED. Both now fail closed.
21
+
22
+ ### Changed
23
+ - **`--json` spec `provider-enforcement-evidence.v2`.** The meaning of
24
+ `VERIFIED` changed. A layer whose evidence cannot be bound to an issuer
25
+ (CodeRifts GitHub App id, public `GET /apps/coderifts`) or to an **active**
26
+ workflow job is `NOT_VERIFIED` or `UNVERIFIABLE` — never `VERIFIED`.
27
+ `inescapable_deploy` is true only when every layer is VERIFIED under these
28
+ rules.
29
+ - **Issuer binding:** `required_check` is VERIFIED only when `checks[]`
30
+ carries our App's numeric `app_id`. Legacy `contexts: string[]` is
31
+ `legacy_contexts_no_issuer_binding`. Name-only `checks[]` is
32
+ `no_issuer_binding`. A different `app_id` is `issuer_app_id_mismatch`.
33
+ - **YAML AST, not grep:** workflow files are parsed with `js-yaml` (already a
34
+ CLI dependency — no new package). VERIFIED requires a job that is not
35
+ `if: false` and a step whose `uses:` is `coderifts/contract-gate` at a
36
+ pinned major, or a `run:` that actually invokes `coderifts deploy-gate`.
37
+ Comments, string literals in other keys, and commented-out steps fail.
38
+ - **Environment correlation:** `environment_protection` is VERIFIED only when
39
+ `--env` names a protected GitHub Environment **and** an active deploy-gate
40
+ job sets that same `environment:` key (`environment_mismatch` otherwise).
41
+
3
42
  ## 4.8.0
4
43
 
5
44
  ### Fixed
package/README.md CHANGED
@@ -75,6 +75,7 @@ coderifts init --agents --hosts=cursor # Cursor only
75
75
  coderifts init --agents --hosts=copilot # GitHub Copilot / VS Code only
76
76
  coderifts init --agents --no-hook # skip host hook install
77
77
  coderifts init --agents --no-workflow # skip .github/workflows/coderifts.yml
78
+ coderifts init --agents --strict # STRICT: require-verified-monitoring: true
78
79
  coderifts init --agents --check # which of the four pieces are present
79
80
  ```
80
81
 
@@ -83,7 +84,7 @@ coderifts init --agents --check # which of the four pieces are present
83
84
  | **MCP config** | `.mcp.json` (Claude Code), `.cursor/mcp.json` (Cursor), `.vscode/mcp.json` (Copilot / VS Code) | Three shapes, never normalised into one: Claude `mcpServers` + `type: "http"` + `url`; Cursor `mcpServers` + `url` only (no `type`); Copilot **`servers`** (not `mcpServers`) + `type: "http"` + `url`. Never `httpUrl` (that is Gemini, extension-level — this command does not write it). |
84
85
  | **Agent rules** | generated host files via `agent-setup`'s writer | `AGENTS.md` is always written. Claude also gets `CLAUDE.md`; Cursor also gets `.cursor/rules/coderifts.mdc`; Copilot also gets `.github/copilot-instructions.md`. `--hosts=all` adds LangGraph and OpenAI instructions. (`coderifts init agent` is a different command — it writes a `.coderifts.yml` template.) |
85
86
  | **Host hook** | `.claude/settings.json`, `.cursor/hooks.json` | Reuses `hook install --claude` / `--cursor` (JSON-merge, backup, idempotent). Copilot shares Claude's `.claude/settings.json` — VS Code reads it by default. |
86
- | **CI workflow** | `.github/workflows/coderifts.yml` | `coderifts/contract-gate@v0`. `require-verified-monitoring` is commented (honest default: **false**) |
87
+ | **CI workflow** | `.github/workflows/coderifts.yml` | `coderifts/contract-gate@v0`. Default: `require-verified-monitoring` commented (honest default: **false**). `--strict`: the same file with `require-verified-monitoring: 'true'` |
87
88
 
88
89
  **Next manual step:** add `CODERIFTS_API_KEY` to the repository secrets. The
89
90
  CLI never writes a key anywhere.
@@ -437,12 +438,14 @@ Provider-native pipeline-protection evidence. This is **not** a host claim:
437
438
 
438
439
  | Layer | VERIFIED when |
439
440
  |-------|----------------|
440
- | `required_check` | Default-branch protection requires `CodeRifts / contract-gate` |
441
+ | `required_check` | Default-branch protection `required_status_checks.checks[]` lists `CodeRifts / contract-gate` **bound to the CodeRifts GitHub App `app_id`** (public metadata: `GET https://api.github.com/apps/coderifts`). Name-only `checks[]` (`app_id` null) is `no_issuer_binding`. The legacy `contexts` array has no issuer field → `legacy_contexts_no_issuer_binding`. Both are **NOT_VERIFIED** (ID621). |
441
442
  | `enforce_admins` | `enforce_admins.enabled` is **true** (admins cannot bypass). `false` = NOT_VERIFIED (admin bypass open) |
442
443
  | `strict_status_checks` | `required_status_checks.strict` is true |
443
- | `environment_protection` | `--env` names a GitHub Environment with protection rules or a deployment branch policy |
444
- | `workflow_contract_gate` | a local `.github/workflows` file references `coderifts/contract-gate@v0` |
445
- | `workflow_deploy_gate` | a local workflow runs `coderifts deploy-gate` |
444
+ | `environment_protection` | `--env` names a GitHub Environment with protection rules **and** an active deploy-gate job sets that same `environment:` (mismatch → `environment_mismatch`) |
445
+ | `workflow_contract_gate` | an **active** job (not `if: false`) has a step whose `uses:` is `coderifts/contract-gate` at a pinned major. Comments, string literals, and commented-out steps are **NOT_VERIFIED** |
446
+ | `workflow_deploy_gate` | an **active** job step **invokes** `coderifts deploy-gate` (not `echo` of the string; not a comment) |
447
+
448
+ **Invariant:** any layer whose evidence cannot be bound to an issuer (our GitHub App id) or to an active job is `NOT_VERIFIED` or `UNVERIFIABLE` — never `VERIFIED`. `inescapable_deploy` remains true only when every layer is `VERIFIED` under that rule.
446
449
 
447
450
  Statuses: `VERIFIED` / `NOT_VERIFIED` / `UNVERIFIABLE(reason)`. Auth:
448
451
  `GITHUB_TOKEN` (never printed). **No token** → API layers `UNVERIFIABLE` with
@@ -451,9 +454,11 @@ CodeRifts GitHub App **cannot** query branch protection (permissions are
451
454
  contents / pull requests / checks — not `administration:read`).
452
455
 
453
456
  `--json` is the quotable evidence document (`spec`:
454
- `provider-enforcement-evidence.v1`): each layer's status, the API endpoint
455
- consulted, `queried_at`. Exit **0** when a report was produced (evidence, not
456
- a gate). GitLab/Bitbucket: no adapter `UNVERIFIABLE`, never stubbed.
457
+ `provider-enforcement-evidence.v2` VERIFIED now requires issuer binding and
458
+ an active YAML-AST job; v1 treated name-only checks and text search as
459
+ VERIFIED): each layer's status, `reason_code`, the API endpoint consulted,
460
+ `queried_at`. Exit **0** when a report was produced (evidence, not a gate).
461
+ GitLab/Bitbucket: no adapter — `UNVERIFIABLE`, never stubbed.
457
462
 
458
463
  ### Agent host files (`coderifts agent-setup`)
459
464
 
package/bin/coderifts.js CHANGED
@@ -95,6 +95,7 @@ program
95
95
  .option('--hosts <list>', 'With --agents: claude, cursor, copilot, or all (default: all)', 'all')
96
96
  .option('--no-hook', 'With --agents: skip host hook install')
97
97
  .option('--no-workflow', 'With --agents: skip writing .github/workflows/coderifts.yml')
98
+ .option('--strict', 'With --agents: STRICT workflow (require-verified-monitoring: true)')
98
99
  .option('--dry-run', 'With --agents: print the plan without writing')
99
100
  .option('--check', 'With --agents: report which of the four pieces are present (no write)')
100
101
  .option('--out <dir>', 'With --agents: target directory (default: cwd)')
@@ -118,6 +119,7 @@ program
118
119
  dryRun: !!options.dryRun,
119
120
  check: !!options.check,
120
121
  out: options.out,
122
+ strict: !!options.strict,
121
123
  });
122
124
  if (result && typeof result.exitCode === 'number') {
123
125
  process.exitCode = result.exitCode;
package/dist/cli.js CHANGED
@@ -3028,7 +3028,7 @@ var require_package = __commonJS({
3028
3028
  "package.json"(exports2, module2) {
3029
3029
  module2.exports = {
3030
3030
  name: "coderifts",
3031
- version: "4.8.0",
3031
+ version: "4.10.0",
3032
3032
  description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
3033
3033
  author: "CodeRifts <hello@coderifts.com>",
3034
3034
  license: "MIT",
@@ -200889,9 +200889,20 @@ var require_decision_result_v1_producer = __commonJS({
200889
200889
  source: {
200890
200890
  type: "string",
200891
200891
  enum: [
200892
- "github_compare"
200892
+ "github_compare",
200893
+ "gitlab_compare",
200894
+ "bitbucket_compare"
200893
200895
  ],
200894
- description: "How the server listed the change-set. github_compare = GitHub Compare API via the App installation."
200896
+ description: "How the server listed the change-set. github_compare = GitHub Compare API via the App installation. gitlab_compare / bitbucket_compare = platform Compare via a caller-supplied short-lived SCM token (never stored)."
200897
+ },
200898
+ platform: {
200899
+ type: "string",
200900
+ enum: [
200901
+ "github",
200902
+ "gitlab",
200903
+ "bitbucket"
200904
+ ],
200905
+ description: "Additive P1-6. SCM platform that produced this SERVER_DERIVED set. Taken from the proven binding / context.platform \u2014 never guessed from the repository string. Absent on envelopes minted before this field existed."
200895
200906
  },
200896
200907
  base_sha: {
200897
200908
  type: "string",
@@ -201799,6 +201810,9 @@ var require_change_set_completeness = __commonJS({
201799
201810
  if (v == null) return "";
201800
201811
  return typeof v === "string" ? v : JSON.stringify(v);
201801
201812
  }
201813
+ function afterContentHash(content) {
201814
+ return sha256hexLocal(specStr(content));
201815
+ }
201802
201816
  function artifactPath(artifact) {
201803
201817
  if (!artifact || typeof artifact !== "object") return "";
201804
201818
  if (typeof artifact.path === "string" && artifact.path.trim()) return artifact.path.trim();
@@ -202097,7 +202111,8 @@ var require_change_set_completeness = __commonJS({
202097
202111
  contract_selector_hash,
202098
202112
  completeness_count: normalizedLeaves.length,
202099
202113
  submitted_set_digest,
202100
- expected_channel: "webhook_full_file_list"
202114
+ expected_channel: "webhook_full_file_list",
202115
+ leaves: normalizedLeaves
202101
202116
  };
202102
202117
  }
202103
202118
  function assertCompletenessRequirement(block, requireLevel) {
@@ -202181,9 +202196,45 @@ var require_change_set_completeness = __commonJS({
202181
202196
  contract_selector_hash: computeContractSelectorHash(DEFAULT_CONTRACT_SELECTOR),
202182
202197
  completeness_count: leaves.length,
202183
202198
  submitted_set_digest,
202184
- expected_channel: null
202199
+ expected_channel: null,
202200
+ leaves
202185
202201
  };
202186
202202
  }
202203
+ function authorizedPathsFromArtifacts(artifacts) {
202204
+ const list = Array.isArray(artifacts) ? artifacts : [];
202205
+ const seen = /* @__PURE__ */ new Set();
202206
+ const rows = [];
202207
+ for (const a of list) {
202208
+ const path = artifactPath(a);
202209
+ if (!path || seen.has(path)) continue;
202210
+ seen.add(path);
202211
+ rows.push({
202212
+ path,
202213
+ after_hex: sha256hexLocal(specStr(a && a.after)),
202214
+ change_type: deriveChangeType(a && a.before, a && a.after)
202215
+ });
202216
+ }
202217
+ return rows;
202218
+ }
202219
+ function authorizedPathsForPersist(block, artifacts) {
202220
+ const submitted = authorizedPathsFromArtifacts(artifacts);
202221
+ const leaves = block && Array.isArray(block.leaves) ? block.leaves : null;
202222
+ if (!leaves || leaves.length === 0) return submitted;
202223
+ const byPath = /* @__PURE__ */ new Map();
202224
+ for (const L of leaves) {
202225
+ if (!L || typeof L !== "object") continue;
202226
+ const path = String(L.path == null ? "" : L.path).trim();
202227
+ if (!path) continue;
202228
+ const after_hex = String(L.after_hex == null ? "" : L.after_hex).replace(/^sha256:/i, "").toLowerCase();
202229
+ if (!/^[0-9a-f]{64}$/.test(after_hex)) continue;
202230
+ byPath.set(path, {
202231
+ path,
202232
+ after_hex,
202233
+ change_type: normalizeChangeType(L.change_type)
202234
+ });
202235
+ }
202236
+ return submitted.map((row) => byPath.get(row.path) || row);
202237
+ }
202187
202238
  function completenessToEnvelopeFields(block) {
202188
202239
  if (!block || block.completeness_mode === "UNBOUND") {
202189
202240
  return {
@@ -202230,7 +202281,10 @@ var require_change_set_completeness = __commonJS({
202230
202281
  assertNoBooleanCompleteness,
202231
202282
  completenessRequiredError,
202232
202283
  sha256hexLocal,
202233
- specStr
202284
+ specStr,
202285
+ afterContentHash,
202286
+ authorizedPathsFromArtifacts,
202287
+ authorizedPathsForPersist
202234
202288
  };
202235
202289
  }
202236
202290
  });
@@ -202795,7 +202849,8 @@ var require_change_set = __commonJS({
202795
202849
  assertCompletenessRequirement,
202796
202850
  completenessToEnvelopeFields,
202797
202851
  applyProvenRepoBinding,
202798
- assertNoBooleanCompleteness
202852
+ assertNoBooleanCompleteness,
202853
+ authorizedPathsForPersist
202799
202854
  } = require_change_set_completeness();
202800
202855
  var { authorFingerprintBindingEnvelopeFields } = require_fingerprint_binding_gate();
202801
202856
  var NUL = "";
@@ -203522,6 +203577,7 @@ var require_change_set = __commonJS({
203522
203577
  }
203523
203578
  }
203524
203579
  const completenessEnvelope = completenessToEnvelopeFields(completenessBlock);
203580
+ const authorizedPaths = preflightMode === "authorize" ? authorizedPathsForPersist(completenessBlock, artifacts) : [];
203525
203581
  const fingerprintBindingEnvelope = authorFingerprintBindingEnvelopeFields({
203526
203582
  preflightMode
203527
203583
  });
@@ -203583,6 +203639,7 @@ var require_change_set = __commonJS({
203583
203639
  // body_hash stays byte-identical. Covered by body_hash when set; NOT in fingerprint.
203584
203640
  ...input && input.derivation_meta && typeof input.derivation_meta === "object" ? { derivation: {
203585
203641
  source: String(input.derivation_meta.source || "github_compare"),
203642
+ platform: String(input.derivation_meta.platform || "github"),
203586
203643
  base_sha: String(input.derivation_meta.base_sha || ""),
203587
203644
  head_sha: String(input.derivation_meta.head_sha || "")
203588
203645
  } } : {},
@@ -203777,6 +203834,12 @@ var require_change_set = __commonJS({
203777
203834
  }
203778
203835
  } catch (_) {
203779
203836
  }
203837
+ if (preflightMode === "authorize" && Array.isArray(authorizedPaths) && authorizedPaths.length > 0) {
203838
+ Object.defineProperty(out, "_authorized_paths", {
203839
+ value: Object.freeze(authorizedPaths.map((r) => Object.freeze({ ...r }))),
203840
+ enumerable: false
203841
+ });
203842
+ }
203780
203843
  return out;
203781
203844
  }
203782
203845
  module2.exports = {
@@ -210159,6 +210222,46 @@ jobs:
210159
210222
  # monitoring-attestation: \${{ vars.CODERIFTS_MONITORING_ATTESTATION }}
210160
210223
  # monitoring-keyring: \${{ github.workspace }}/.coderifts/monitoring-keys.json
210161
210224
  `;
210225
+ var STRICT_WORKFLOW_BODY = `# Written by \`coderifts init --agents --strict\`. Re-run is a no-op when this file already exists.
210226
+ #
210227
+ # STRICT template: require-verified-monitoring is ON.
210228
+ # This workflow POSTS the Check Run named exactly:
210229
+ # CodeRifts / contract-gate
210230
+ # It does NOT enable branch protection and it does NOT make that check required.
210231
+ # Turn the check on in repo settings to enforce (${ENFORCEMENT_DOC}).
210232
+ #
210233
+ # Copied from the gate repo example (examples/contract-gate.yml) \u2014 action at @v0.
210234
+ name: CodeRifts Contract Gate
210235
+
210236
+ # Run on EVERY pull request. Do NOT add paths: filters \u2014 a required check that
210237
+ # never runs leaves the PR mergeable (see ENFORCEMENT.md, "fail-closed on absence").
210238
+ on:
210239
+ pull_request:
210240
+
210241
+ permissions:
210242
+ checks: write
210243
+ contents: read
210244
+
210245
+ jobs:
210246
+ contract-gate:
210247
+ runs-on: ubuntu-latest
210248
+ steps:
210249
+ - uses: actions/checkout@v4
210250
+ with:
210251
+ # REQUIRED: the gate derives the change set from git diff base...head.
210252
+ fetch-depth: 0
210253
+
210254
+ - uses: ${GATE_ACTION}
210255
+ with:
210256
+ api-key: \${{ secrets.CODERIFTS_API_KEY }}
210257
+ # api-url: https://app.coderifts.com # override only for self-hosted
210258
+ require-verified-monitoring: 'true'
210259
+ monitoring-attestation: \${{ vars.CODERIFTS_MONITORING_ATTESTATION }}
210260
+ monitoring-keyring: \${{ github.workspace }}/.coderifts/monitoring-keys.json
210261
+ `;
210262
+ function workflowBody(strict) {
210263
+ return strict ? STRICT_WORKFLOW_BODY : WORKFLOW_BODY;
210264
+ }
210162
210265
  function writeContractGateWorkflow(opts = {}) {
210163
210266
  const outDir = opts.outDir;
210164
210267
  const dest = path.join(outDir, WORKFLOW_REL);
@@ -210169,6 +210272,7 @@ jobs:
210169
210272
  fs.writeFileSync(p, c, "utf8");
210170
210273
  });
210171
210274
  const dryRun = !!opts.dryRun;
210275
+ const body = workflowBody(!!opts.strict);
210172
210276
  if (exists(dest)) {
210173
210277
  let onDisk = "";
210174
210278
  try {
@@ -210176,13 +210280,13 @@ jobs:
210176
210280
  } catch {
210177
210281
  onDisk = "";
210178
210282
  }
210179
- const reason = onDisk === WORKFLOW_BODY ? "already present, identical" : "exists; installer does not clobber workflows";
210283
+ const reason = onDisk === body ? "already present, identical" : "exists; installer does not clobber workflows";
210180
210284
  return { action: "skip", relPath: WORKFLOW_REL, reason, exitCode: 0 };
210181
210285
  }
210182
210286
  if (dryRun) {
210183
210287
  return { action: "create", relPath: WORKFLOW_REL, reason: "created", exitCode: 0, dryRun: true };
210184
210288
  }
210185
- writeFile(dest, WORKFLOW_BODY);
210289
+ writeFile(dest, body);
210186
210290
  return { action: "create", relPath: WORKFLOW_REL, reason: "created", exitCode: 0 };
210187
210291
  }
210188
210292
  function workflowPresent(outDir, deps = {}) {
@@ -210199,6 +210303,7 @@ jobs:
210199
210303
  module2.exports = {
210200
210304
  WORKFLOW_REL,
210201
210305
  WORKFLOW_BODY,
210306
+ STRICT_WORKFLOW_BODY,
210202
210307
  GATE_ACTION,
210203
210308
  ENFORCEMENT_DOC,
210204
210309
  writeContractGateWorkflow,
@@ -210230,7 +210335,7 @@ var require_init_agents = __commonJS({
210230
210335
  workflowPresent
210231
210336
  } = require_contract_gate_workflow();
210232
210337
  if (process.env.NO_COLOR) chalk.level = 0;
210233
- var USAGE = `Usage: coderifts init --agents [--hosts=claude,cursor,copilot,all] [--no-hook] [--no-workflow] [--dry-run] [--check] [--out <dir>]
210338
+ var USAGE = `Usage: coderifts init --agents [--hosts=claude,cursor,copilot,all] [--no-hook] [--no-workflow] [--strict] [--dry-run] [--check] [--out <dir>]
210234
210339
 
210235
210340
  Wire this repository for governed agent work in ONE command:
210236
210341
  1. repo-level MCP config (per host: .mcp.json / .cursor/mcp.json / .vscode/mcp.json)
@@ -210241,6 +210346,7 @@ var require_init_agents = __commonJS({
210241
210346
  --hosts <list> claude, cursor, copilot, or all (default: all)
210242
210347
  --no-hook Skip host hook install
210243
210348
  --no-workflow Skip writing the CI workflow
210349
+ --strict STRICT workflow: require-verified-monitoring: true (default template stays false)
210244
210350
  --dry-run Print the plan without writing
210245
210351
  --check Report which of the four pieces are present (no write)
210246
210352
  --out <dir> Target directory (default: cwd)
@@ -210300,9 +210406,9 @@ ${USAGE}`
210300
210406
  return { hosts, allRules: false };
210301
210407
  }
210302
210408
  function agentFlagsRequireAgents(options = {}) {
210303
- return !!(options.check || options.dryRun || options.out || options.hosts != null && String(options.hosts) !== "all" || options.hook === false || options.workflow === false || options.noHook === true || options.noWorkflow === true);
210409
+ return !!(options.check || options.dryRun || options.out || options.hosts != null && String(options.hosts) !== "all" || options.hook === false || options.workflow === false || options.noHook === true || options.noWorkflow === true || options.strict === true);
210304
210410
  }
210305
- var AGENT_FLAGS_REQUIRE_AGENTS = "Error: --check/--dry-run/--out/--hosts/--no-hook/--no-workflow require --agents";
210411
+ var AGENT_FLAGS_REQUIRE_AGENTS = "Error: --check/--dry-run/--out/--hosts/--no-hook/--no-workflow/--strict require --agents";
210306
210412
  var AGENTS_NO_TEMPLATE = "Error: init --agents does not take a template. Did you mean `coderifts init --agents` (not `coderifts init agent`)?";
210307
210413
  function rulePathsForHosts(hosts, allRules) {
210308
210414
  const out = [SHARED_RULE];
@@ -210565,12 +210671,12 @@ ${USAGE}`
210565
210671
  if (!wantWorkflow(options)) {
210566
210672
  items.push({ section: "workflow", action: "skip", relPath: WORKFLOW_REL, note: "--no-workflow" });
210567
210673
  } else {
210568
- const r = writeContractGateWorkflow({ outDir, dryRun, ...fsDeps });
210674
+ const r = writeContractGateWorkflow({ outDir, dryRun, strict: !!options.strict, ...fsDeps });
210569
210675
  items.push({
210570
210676
  section: "workflow",
210571
210677
  action: r.action,
210572
210678
  relPath: r.relPath,
210573
- note: r.action === "create" ? `${GATE_ACTION}` : r.reason
210679
+ note: r.action === "create" ? options.strict ? `${GATE_ACTION} (STRICT require-verified-monitoring)` : GATE_ACTION : r.reason
210574
210680
  });
210575
210681
  }
210576
210682
  const created = items.filter((i) => i.action === "create").length;
@@ -237167,6 +237273,162 @@ var require_status = __commonJS({
237167
237273
  }
237168
237274
  });
237169
237275
 
237276
+ // src/provider/workflow-scan.js
237277
+ var require_workflow_scan = __commonJS({
237278
+ "src/provider/workflow-scan.js"(exports2, module2) {
237279
+ "use strict";
237280
+ var yaml = require_js_yaml();
237281
+ var CONTRACT_GATE_ACTION = "coderifts/contract-gate";
237282
+ var DEPLOY_GATE_INVOCATION = /^(?:npx(?:\s+--yes)?\s+|npm\s+exec\s+|pnpm\s+(?:dlx|exec)\s+|yarn(?:\s+(?:dlx|exec))?\s+)?coderifts\s+deploy-gate(?:\s|$)/;
237283
+ var NON_SHELL = /* @__PURE__ */ new Set(["python", "python3", "node", "ruby", "perl", "php", "pwsh", "powershell"]);
237284
+ function isPinnedMajor(ref) {
237285
+ const r = String(ref || "");
237286
+ if (/^v\d+(\.\d+){0,2}$/.test(r)) return true;
237287
+ if (/^[0-9a-f]{40}$/i.test(r)) return true;
237288
+ return false;
237289
+ }
237290
+ function parseActionUses(uses) {
237291
+ const raw = String(uses || "").trim();
237292
+ const at = raw.lastIndexOf("@");
237293
+ if (at <= 0) return { name: raw, ref: "", pinnedMajor: false };
237294
+ return {
237295
+ name: raw.slice(0, at),
237296
+ ref: raw.slice(at + 1),
237297
+ pinnedMajor: isPinnedMajor(raw.slice(at + 1))
237298
+ };
237299
+ }
237300
+ function isContractGateUses(uses) {
237301
+ const parsed = parseActionUses(uses);
237302
+ return parsed.name === CONTRACT_GATE_ACTION && parsed.pinnedMajor === true;
237303
+ }
237304
+ function isExplicitlyDisabled(node) {
237305
+ if (!node || typeof node !== "object" || !Object.prototype.hasOwnProperty.call(node, "if")) {
237306
+ return false;
237307
+ }
237308
+ const ifVal = node.if;
237309
+ if (ifVal === false || ifVal === 0 || ifVal === null) return true;
237310
+ if (typeof ifVal === "string") {
237311
+ const s = ifVal.trim().toLowerCase();
237312
+ if (s === "false" || s === "0" || s === "" || s === "null") return true;
237313
+ if (/^\$\{\{\s*(?:false|0|null)\s*\}\}$/.test(s)) return true;
237314
+ }
237315
+ return false;
237316
+ }
237317
+ function jobEnvironmentName(job) {
237318
+ if (!job || typeof job !== "object") return null;
237319
+ const env = job.environment;
237320
+ if (typeof env === "string") {
237321
+ const name = env.trim();
237322
+ return name || null;
237323
+ }
237324
+ if (env && typeof env === "object" && typeof env.name === "string") {
237325
+ const name = env.name.trim();
237326
+ return name || null;
237327
+ }
237328
+ return null;
237329
+ }
237330
+ function isNonPosixShell(shell) {
237331
+ if (shell == null || shell === "") return false;
237332
+ const first = String(shell).trim().split(/\s+/)[0].replace(/^.*\//, "").toLowerCase();
237333
+ return NON_SHELL.has(first);
237334
+ }
237335
+ function isDeployGateInvocation(run, shell) {
237336
+ if (isNonPosixShell(shell)) return false;
237337
+ const text = String(run || "");
237338
+ let heredoc = null;
237339
+ let quote = null;
237340
+ for (const rawLine of text.split("\n")) {
237341
+ let line = String(rawLine || "");
237342
+ if (heredoc) {
237343
+ if (line.trim() === heredoc) heredoc = null;
237344
+ continue;
237345
+ }
237346
+ const hd = /<<[-]?\s*['"]?(\w+)['"]?/.exec(line);
237347
+ if (hd) heredoc = hd[1];
237348
+ if (quote) {
237349
+ for (let i = 0; i < line.length; i++) {
237350
+ const ch = line[i];
237351
+ if (ch === quote && line[i - 1] !== "\\") {
237352
+ quote = null;
237353
+ break;
237354
+ }
237355
+ }
237356
+ if (quote) continue;
237357
+ }
237358
+ const trimmed = line.trim();
237359
+ if (!trimmed || trimmed.startsWith("#")) continue;
237360
+ let cmd = trimmed.replace(/^(?:\w+=\S+\s+)+/, "");
237361
+ if (/^(echo|printf|cat|false|true|:)\b/.test(cmd)) {
237362
+ for (let i = 0; i < cmd.length; i++) {
237363
+ const ch = cmd[i];
237364
+ if ((ch === "'" || ch === '"') && cmd[i - 1] !== "\\") {
237365
+ quote = quote ? quote === ch ? null : quote : ch;
237366
+ }
237367
+ }
237368
+ continue;
237369
+ }
237370
+ if (DEPLOY_GATE_INVOCATION.test(cmd)) return true;
237371
+ }
237372
+ return false;
237373
+ }
237374
+ function stepsOf(job) {
237375
+ if (!job || typeof job !== "object") return [];
237376
+ return Array.isArray(job.steps) ? job.steps : [];
237377
+ }
237378
+ function inspectJob(job) {
237379
+ const out = {
237380
+ contractGate: false,
237381
+ deployGate: false,
237382
+ environment: jobEnvironmentName(job),
237383
+ disabled: isExplicitlyDisabled(job)
237384
+ };
237385
+ if (out.disabled) return out;
237386
+ for (const step of stepsOf(job)) {
237387
+ if (!step || typeof step !== "object") continue;
237388
+ if (isExplicitlyDisabled(step)) continue;
237389
+ if (step.uses != null && isContractGateUses(step.uses)) out.contractGate = true;
237390
+ if (step.run != null && isDeployGateInvocation(step.run, step.shell)) out.deployGate = true;
237391
+ }
237392
+ return out;
237393
+ }
237394
+ function inspectWorkflowYaml(body) {
237395
+ const empty = { contractGate: false, deployGate: false, deployEnvironments: [] };
237396
+ let doc;
237397
+ try {
237398
+ doc = yaml.load(String(body || ""));
237399
+ } catch {
237400
+ return empty;
237401
+ }
237402
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return empty;
237403
+ const jobs = doc.jobs && typeof doc.jobs === "object" && !Array.isArray(doc.jobs) ? doc.jobs : null;
237404
+ if (!jobs) return empty;
237405
+ let contractGate = false;
237406
+ let deployGate = false;
237407
+ const deployEnvironments = [];
237408
+ for (const name of Object.keys(jobs)) {
237409
+ const info = inspectJob(jobs[name]);
237410
+ if (info.contractGate) contractGate = true;
237411
+ if (info.deployGate) {
237412
+ deployGate = true;
237413
+ if (info.environment) deployEnvironments.push(info.environment);
237414
+ }
237415
+ }
237416
+ return { contractGate, deployGate, deployEnvironments };
237417
+ }
237418
+ module2.exports = {
237419
+ CONTRACT_GATE_ACTION,
237420
+ isPinnedMajor,
237421
+ parseActionUses,
237422
+ isContractGateUses,
237423
+ isExplicitlyDisabled,
237424
+ jobEnvironmentName,
237425
+ isDeployGateInvocation,
237426
+ inspectJob,
237427
+ inspectWorkflowYaml
237428
+ };
237429
+ }
237430
+ });
237431
+
237170
237432
  // src/provider/github-enforcement.js
237171
237433
  var require_github_enforcement = __commonJS({
237172
237434
  "src/provider/github-enforcement.js"(exports2, module2) {
@@ -237178,7 +237440,107 @@ var require_github_enforcement = __commonJS({
237178
237440
  extractRequiredContexts
237179
237441
  } = require_setup_required_check();
237180
237442
  var { GATE_ACTION } = require_contract_gate_workflow();
237181
- var EVIDENCE_SPEC = "provider-enforcement-evidence.v1";
237443
+ var { inspectWorkflowYaml } = require_workflow_scan();
237444
+ var EVIDENCE_SPEC = "provider-enforcement-evidence.v2";
237445
+ var CODERIFTS_GITHUB_APP_SLUG = "coderifts";
237446
+ var CODERIFTS_GITHUB_APP_ID = 2860592;
237447
+ var REASON = Object.freeze({
237448
+ ISSUER_BOUND: "issuer_bound",
237449
+ NO_ISSUER_BINDING: "no_issuer_binding",
237450
+ LEGACY_CONTEXTS_NO_ISSUER_BINDING: "legacy_contexts_no_issuer_binding",
237451
+ ISSUER_APP_ID_MISMATCH: "issuer_app_id_mismatch",
237452
+ ISSUER_APP_ID_UNRESOLVABLE: "issuer_app_id_unresolvable",
237453
+ REQUIRED_CHECK_ABSENT: "required_check_absent",
237454
+ ACTIVE_JOB: "active_job",
237455
+ NO_ACTIVE_JOB: "no_active_job",
237456
+ ENVIRONMENT_CORRELATED: "environment_correlated",
237457
+ ENVIRONMENT_MISMATCH: "environment_mismatch"
237458
+ });
237459
+ function verifiedRequiresBinding(layer2) {
237460
+ if (!layer2 || layer2.status !== STATUS.VERIFIED) return true;
237461
+ if (layer2.id === "required_check") return layer2.reason_code === REASON.ISSUER_BOUND;
237462
+ if (layer2.id === "workflow_contract_gate" || layer2.id === "workflow_deploy_gate") {
237463
+ return layer2.reason_code === REASON.ACTIVE_JOB;
237464
+ }
237465
+ if (layer2.id === "environment_protection") {
237466
+ return layer2.reason_code === REASON.ENVIRONMENT_CORRELATED;
237467
+ }
237468
+ return true;
237469
+ }
237470
+ function expectedIssuerAppId() {
237471
+ const id = Number(CODERIFTS_GITHUB_APP_ID);
237472
+ return Number.isInteger(id) && id > 0 ? id : null;
237473
+ }
237474
+ function ourCheckEntries(rsc) {
237475
+ if (!rsc || !Array.isArray(rsc.checks)) return [];
237476
+ return rsc.checks.filter((c) => c && typeof c.context === "string" && c.context === CHECK_NAME);
237477
+ }
237478
+ function numericAppId(value) {
237479
+ if (value == null || value === "") return null;
237480
+ const n = Number(value);
237481
+ return Number.isInteger(n) && n > 0 ? n : null;
237482
+ }
237483
+ function evaluateRequiredCheck(protection) {
237484
+ const expected = expectedIssuerAppId();
237485
+ const rsc = protection && protection.required_status_checks;
237486
+ const contexts = extractRequiredContexts(protection);
237487
+ if (expected == null) {
237488
+ return {
237489
+ status: STATUS.UNVERIFIABLE,
237490
+ reason_code: REASON.ISSUER_APP_ID_UNRESOLVABLE,
237491
+ reason: "CodeRifts GitHub App id could not be determined from public metadata; required_check is UNVERIFIABLE (never VERIFIED without an issuer id)",
237492
+ required_contexts: contexts,
237493
+ app_id: null
237494
+ };
237495
+ }
237496
+ const ours = ourCheckEntries(rsc);
237497
+ if (ours.length > 0) {
237498
+ const bound = ours.find((c) => numericAppId(c.app_id) === expected);
237499
+ if (bound) {
237500
+ return {
237501
+ status: STATUS.VERIFIED,
237502
+ reason_code: REASON.ISSUER_BOUND,
237503
+ reason: `required_status_checks.checks binds "${CHECK_NAME}" to GitHub App id ${expected} (slug ${CODERIFTS_GITHUB_APP_SLUG})`,
237504
+ required_contexts: contexts,
237505
+ app_id: expected
237506
+ };
237507
+ }
237508
+ const other = ours.find((c) => numericAppId(c.app_id) != null);
237509
+ if (other) {
237510
+ return {
237511
+ status: STATUS.NOT_VERIFIED,
237512
+ reason_code: REASON.ISSUER_APP_ID_MISMATCH,
237513
+ reason: `required_status_checks.checks binds "${CHECK_NAME}" to app_id ${numericAppId(other.app_id)}, not CodeRifts App id ${expected}`,
237514
+ required_contexts: contexts,
237515
+ app_id: numericAppId(other.app_id),
237516
+ expected_app_id: expected
237517
+ };
237518
+ }
237519
+ return {
237520
+ status: STATUS.NOT_VERIFIED,
237521
+ reason_code: REASON.NO_ISSUER_BINDING,
237522
+ reason: `required_status_checks.checks lists "${CHECK_NAME}" with no app_id (name-only; any repository writer can post a success status with that context)`,
237523
+ required_contexts: contexts,
237524
+ app_id: null
237525
+ };
237526
+ }
237527
+ if (contexts.includes(CHECK_NAME)) {
237528
+ return {
237529
+ status: STATUS.NOT_VERIFIED,
237530
+ reason_code: REASON.LEGACY_CONTEXTS_NO_ISSUER_BINDING,
237531
+ reason: `required_status_checks.contexts lists "${CHECK_NAME}" but the legacy contexts array has no app_id at all (legacy_contexts_no_issuer_binding)`,
237532
+ required_contexts: contexts,
237533
+ app_id: null
237534
+ };
237535
+ }
237536
+ return {
237537
+ status: STATUS.NOT_VERIFIED,
237538
+ reason_code: REASON.REQUIRED_CHECK_ABSENT,
237539
+ reason: `required_status_checks does not include "${CHECK_NAME}" (have: ${contexts.join(", ") || "none"})`,
237540
+ required_contexts: contexts,
237541
+ app_id: null
237542
+ };
237543
+ }
237182
237544
  var APP_INSTALLATION = Object.freeze({
237183
237545
  can_query_protection: false,
237184
237546
  permissions: Object.freeze([
@@ -237344,12 +237706,14 @@ var require_github_enforcement = __commonJS({
237344
237706
  } else {
237345
237707
  const protection = prot.body && typeof prot.body === "object" ? prot.body : {};
237346
237708
  const endpoint = prot.endpoint ? `GET ${prot.endpoint}` : null;
237347
- const contexts = extractRequiredContexts(protection);
237348
- const hasCheck = contexts.includes(CHECK_NAME);
237349
- layers.push(layer("required_check", hasCheck ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237350
- reason: hasCheck ? `required_status_checks includes "${CHECK_NAME}"` : `required_status_checks does not include "${CHECK_NAME}" (have: ${contexts.join(", ") || "none"})`,
237709
+ const required = evaluateRequiredCheck(protection);
237710
+ layers.push(layer("required_check", required.status, {
237711
+ reason: required.reason,
237712
+ reason_code: required.reason_code,
237351
237713
  endpoint,
237352
- required_contexts: contexts
237714
+ required_contexts: required.required_contexts,
237715
+ app_id: required.app_id,
237716
+ ...required.expected_app_id != null ? { expected_app_id: required.expected_app_id } : {}
237353
237717
  }));
237354
237718
  const admins = enforceAdminsEnabled(protection);
237355
237719
  layers.push(layer("enforce_admins", admins ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
@@ -237401,24 +237765,55 @@ var require_github_enforcement = __commonJS({
237401
237765
  }));
237402
237766
  } else {
237403
237767
  const ok = environmentProtected(environmentRead.body);
237404
- layers.push(layer("environment_protection", ok ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237405
- reason: ok ? `GitHub Environment "${environmentName}" has protection rules or a deployment branch policy` : `GitHub Environment "${environmentName}" exists but has no protection_rules and no deployment_branch_policy`,
237406
- endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237407
- environment: environmentName
237408
- }));
237768
+ const referenced = (localFiles.deployGateEnvironments || []).map(String);
237769
+ const correlated = ok && environmentName && referenced.includes(environmentName);
237770
+ if (correlated) {
237771
+ layers.push(layer("environment_protection", STATUS.VERIFIED, {
237772
+ reason: `GitHub Environment "${environmentName}" has protection rules and is referenced by an active deploy-gate job (environment:)`,
237773
+ reason_code: REASON.ENVIRONMENT_CORRELATED,
237774
+ endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237775
+ environment: environmentName,
237776
+ workflow_environments: referenced
237777
+ }));
237778
+ } else if (!ok) {
237779
+ layers.push(layer("environment_protection", STATUS.NOT_VERIFIED, {
237780
+ reason: `GitHub Environment "${environmentName}" exists but has no protection_rules and no deployment_branch_policy`,
237781
+ reason_code: REASON.ENVIRONMENT_MISMATCH,
237782
+ endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237783
+ environment: environmentName,
237784
+ workflow_environments: referenced
237785
+ }));
237786
+ } else {
237787
+ layers.push(layer("environment_protection", STATUS.NOT_VERIFIED, {
237788
+ reason: `GitHub Environment "${environmentName}" is protected but no active deploy-gate job sets environment: ${environmentName} (have: ${referenced.join(", ") || "none"})`,
237789
+ reason_code: REASON.ENVIRONMENT_MISMATCH,
237790
+ endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
237791
+ environment: environmentName,
237792
+ workflow_environments: referenced
237793
+ }));
237794
+ }
237409
237795
  }
237410
237796
  const cg = localFiles.contractGateFiles || [];
237411
237797
  layers.push(layer("workflow_contract_gate", cg.length ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237412
- reason: cg.length ? `workflow references ${GATE_ACTION} (${cg.join(", ")})` : `no .github/workflows file references ${GATE_ACTION}`,
237798
+ reason: cg.length ? `active job step uses ${GATE_ACTION} (${cg.join(", ")})` : `no active .github/workflows job uses ${GATE_ACTION} (comments, disabled jobs, and string literals do not count)`,
237799
+ reason_code: cg.length ? REASON.ACTIVE_JOB : REASON.NO_ACTIVE_JOB,
237413
237800
  endpoint: "local:.github/workflows",
237414
237801
  files: cg
237415
237802
  }));
237416
237803
  const dg = localFiles.deployGateFiles || [];
237417
237804
  layers.push(layer("workflow_deploy_gate", dg.length ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
237418
- reason: dg.length ? `workflow runs coderifts deploy-gate (${dg.join(", ")})` : "no .github/workflows file runs coderifts deploy-gate",
237805
+ reason: dg.length ? `active job step invokes coderifts deploy-gate (${dg.join(", ")})` : "no active .github/workflows job invokes coderifts deploy-gate (comments, disabled jobs, and string literals do not count)",
237806
+ reason_code: dg.length ? REASON.ACTIVE_JOB : REASON.NO_ACTIVE_JOB,
237419
237807
  endpoint: "local:.github/workflows",
237420
237808
  files: dg
237421
237809
  }));
237810
+ for (const l of layers) {
237811
+ if (l.status === STATUS.VERIFIED && !verifiedRequiresBinding(l)) {
237812
+ l.status = STATUS.NOT_VERIFIED;
237813
+ l.reason_code = l.reason_code || REASON.NO_ISSUER_BINDING;
237814
+ l.reason = `${l.reason || l.id} \u2014 downgraded: VERIFIED requires issuer or active-job binding`;
237815
+ }
237816
+ }
237422
237817
  const byId = Object.fromEntries(layers.map((l) => [l.id, l]));
237423
237818
  const inescapable = LAYER_IDS.every((id) => byId[id] && byId[id].status === STATUS.VERIFIED);
237424
237819
  return {
@@ -237426,7 +237821,7 @@ var require_github_enforcement = __commonJS({
237426
237821
  inescapable_deploy: inescapable,
237427
237822
  claim: {
237428
237823
  inescapable_deploy: inescapable,
237429
- basis: inescapable ? "all six layers VERIFIED from queried provider evidence + local workflow files" : "inescapable_deploy stays false until every layer is VERIFIED"
237824
+ basis: inescapable ? "all six layers VERIFIED (issuer-bound required check + active workflow jobs + correlated environment)" : "inescapable_deploy stays false until every layer is VERIFIED"
237430
237825
  }
237431
237826
  };
237432
237827
  }
@@ -237437,12 +237832,13 @@ var require_github_enforcement = __commonJS({
237437
237832
  const dir = path.join(cwd, ".github", "workflows");
237438
237833
  const contractGateFiles = [];
237439
237834
  const deployGateFiles = [];
237440
- if (!exists(dir)) return { contractGateFiles, deployGateFiles };
237835
+ const deployGateEnvironments = [];
237836
+ if (!exists(dir)) return { contractGateFiles, deployGateFiles, deployGateEnvironments };
237441
237837
  let names = [];
237442
237838
  try {
237443
237839
  names = readdir(dir);
237444
237840
  } catch {
237445
- return { contractGateFiles, deployGateFiles };
237841
+ return { contractGateFiles, deployGateFiles, deployGateEnvironments };
237446
237842
  }
237447
237843
  for (const name of names) {
237448
237844
  if (!/\.ya?ml$/i.test(name)) continue;
@@ -237453,10 +237849,18 @@ var require_github_enforcement = __commonJS({
237453
237849
  } catch {
237454
237850
  continue;
237455
237851
  }
237456
- if (body.includes(GATE_ACTION)) contractGateFiles.push(rel);
237457
- if (/\bcoderifts\s+deploy-gate\b/.test(body)) deployGateFiles.push(rel);
237852
+ const evidence = inspectWorkflowYaml(body);
237853
+ if (evidence.contractGate) contractGateFiles.push(rel);
237854
+ if (evidence.deployGate) {
237855
+ deployGateFiles.push(rel);
237856
+ for (const envName of evidence.deployEnvironments) {
237857
+ if (envName && !deployGateEnvironments.includes(envName)) {
237858
+ deployGateEnvironments.push(envName);
237859
+ }
237860
+ }
237861
+ }
237458
237862
  }
237459
- return { contractGateFiles, deployGateFiles };
237863
+ return { contractGateFiles, deployGateFiles, deployGateEnvironments };
237460
237864
  }
237461
237865
  async function queryGitHubEnforcement({
237462
237866
  owner,
@@ -237491,12 +237895,17 @@ var require_github_enforcement = __commonJS({
237491
237895
  NO_TOKEN_INSTRUCTION,
237492
237896
  LAYER_IDS,
237493
237897
  STATUS,
237898
+ REASON,
237494
237899
  CHECK_NAME,
237495
237900
  GATE_ACTION,
237901
+ CODERIFTS_GITHUB_APP_ID,
237902
+ CODERIFTS_GITHUB_APP_SLUG,
237496
237903
  resolveGitHubAuth,
237497
237904
  redactSecrets,
237498
237905
  createGitHubHttpClient,
237499
237906
  evaluateLayers,
237907
+ evaluateRequiredCheck,
237908
+ verifiedRequiresBinding,
237500
237909
  scanLocalWorkflows,
237501
237910
  queryGitHubEnforcement,
237502
237911
  enforceAdminsEnabled,
@@ -237572,7 +237981,8 @@ var require_enforce_check = __commonJS({
237572
237981
  const missing = report.layers.filter((l) => l.status !== "VERIFIED");
237573
237982
  for (const l of missing) {
237574
237983
  if (l.id === "required_check") {
237575
- lines.push(chalk.dim(' require "CodeRifts / contract-gate" on default-branch protection'));
237984
+ lines.push(chalk.dim(' require "CodeRifts / contract-gate" bound to the CodeRifts GitHub App (app_id)'));
237985
+ lines.push(chalk.dim(" on default-branch protection \u2014 name-only and legacy contexts[] are NOT_VERIFIED"));
237576
237986
  lines.push(chalk.dim(" (coderifts setup-required-check --apply)"));
237577
237987
  } else if (l.id === "enforce_admins") {
237578
237988
  lines.push(chalk.dim(" set enforce_admins: true so administrators cannot bypass"));
@@ -237580,12 +237990,13 @@ var require_enforce_check = __commonJS({
237580
237990
  } else if (l.id === "strict_status_checks") {
237581
237991
  lines.push(chalk.dim(" set required_status_checks.strict: true"));
237582
237992
  } else if (l.id === "environment_protection") {
237583
- lines.push(chalk.dim(" add a GitHub Environment with protection rules, pass --env <name>"));
237993
+ lines.push(chalk.dim(" add a GitHub Environment with protection rules, pass --env <name>,"));
237994
+ lines.push(chalk.dim(" and set that same name on the deploy-gate job's environment: key"));
237584
237995
  } else if (l.id === "workflow_contract_gate") {
237585
- lines.push(chalk.dim(" add .github/workflows/coderifts.yml using coderifts/contract-gate@v0"));
237586
- lines.push(chalk.dim(" (coderifts init --agents)"));
237996
+ lines.push(chalk.dim(" add an active job step: uses: coderifts/contract-gate@v0"));
237997
+ lines.push(chalk.dim(" (comments / if: false / wrong org do not count; coderifts init --agents)"));
237587
237998
  } else if (l.id === "workflow_deploy_gate") {
237588
- lines.push(chalk.dim(" add a CD step: coderifts deploy-gate --env <env> --artifact <id> --receipt <file>"));
237999
+ lines.push(chalk.dim(" add an active CD step that invokes: coderifts deploy-gate --env <env> --artifact <id> --receipt <file>"));
237589
238000
  }
237590
238001
  }
237591
238002
  }
@@ -240719,7 +241130,7 @@ program.command("registry-gate [dir]").description("Admit a directory of OpenAPI
240719
241130
  process.exitCode = code;
240720
241131
  process.exit(code);
240721
241132
  });
240722
- program.command("init [template]").description("Generate a .coderifts.yml from a policy template, or wire governed agent work (--agents)").option("--agents", "Wire this repo for governed agent work (MCP config, rule files, host hook, CI gate)").option("--hosts <list>", "With --agents: claude, cursor, copilot, or all (default: all)", "all").option("--no-hook", "With --agents: skip host hook install").option("--no-workflow", "With --agents: skip writing .github/workflows/coderifts.yml").option("--dry-run", "With --agents: print the plan without writing").option("--check", "With --agents: report which of the four pieces are present (no write)").option("--out <dir>", "With --agents: target directory (default: cwd)").action(async (template, options) => {
241133
+ program.command("init [template]").description("Generate a .coderifts.yml from a policy template, or wire governed agent work (--agents)").option("--agents", "Wire this repo for governed agent work (MCP config, rule files, host hook, CI gate)").option("--hosts <list>", "With --agents: claude, cursor, copilot, or all (default: all)", "all").option("--no-hook", "With --agents: skip host hook install").option("--no-workflow", "With --agents: skip writing .github/workflows/coderifts.yml").option("--strict", "With --agents: STRICT workflow (require-verified-monitoring: true)").option("--dry-run", "With --agents: print the plan without writing").option("--check", "With --agents: report which of the four pieces are present (no write)").option("--out <dir>", "With --agents: target directory (default: cwd)").action(async (template, options) => {
240723
241134
  const {
240724
241135
  runInitAgents,
240725
241136
  agentFlagsRequireAgents,
@@ -240738,7 +241149,8 @@ program.command("init [template]").description("Generate a .coderifts.yml from a
240738
241149
  workflow: options.workflow,
240739
241150
  dryRun: !!options.dryRun,
240740
241151
  check: !!options.check,
240741
- out: options.out
241152
+ out: options.out,
241153
+ strict: !!options.strict
240742
241154
  });
240743
241155
  if (result && typeof result.exitCode === "number") {
240744
241156
  process.exitCode = result.exitCode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "4.8.0",
3
+ "version": "4.10.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",