coderifts 3.0.0 → 3.2.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/README.md CHANGED
@@ -138,6 +138,53 @@ After upgrading the `coderifts` CLI, re-run `coderifts hook install` so the
138
138
  installed hook matches the package (already-installed hooks are not
139
139
  auto-updated).
140
140
 
141
+ ### Claude Code PreToolUse (`coderifts claude-hook`) — ID824
142
+
143
+ Tool-call-time gate for **Claude Code**: blocks contract-touching `Write` /
144
+ `Edit` / `MultiEdit` when authorize preflight returns **BLOCK/STOP**.
145
+
146
+ **Exit map (Claude Code semantics — fixed):**
147
+
148
+ | Exit | Meaning |
149
+ |------|---------|
150
+ | **2** | **BLOCK** — tool call cancelled; stderr is shown to the model |
151
+ | **0** | Allow, or soft-skip (no key / wrong file / parse gap / API down) |
152
+ | **1** | **Never used for deny** — Claude treats exit 1 as non-blocking (action proceeds) |
153
+
154
+ Push-time equivalent (git exit **1** on BLOCK): `coderifts hook install`.
155
+
156
+ **Install path:** `npm i -g coderifts` then set key + spec (same as the git hook):
157
+
158
+ ```bash
159
+ git config coderifts.apiKey 'cr_live_…' # or: coderifts login / CODERIFTS_API_KEY
160
+ git config coderifts.specPath api/openapi.yaml # default if omitted
161
+ ```
162
+
163
+ **Recipe — project** (`.claude/settings.json`) **or user** (`~/.claude/settings.json`):
164
+
165
+ ```json
166
+ {
167
+ "hooks": {
168
+ "PreToolUse": [
169
+ {
170
+ "matcher": "Write|Edit|MultiEdit",
171
+ "hooks": [
172
+ {
173
+ "type": "command",
174
+ "command": "coderifts claude-hook",
175
+ "timeout": 60
176
+ }
177
+ ]
178
+ }
179
+ ]
180
+ }
181
+ }
182
+ ```
183
+
184
+ - **Project** `.claude/settings.json` — shared with the repo (commit if the team wants the gate).
185
+ - **User** `~/.claude/settings.json` — personal; applies to all projects on this machine.
186
+ - 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.
187
+
141
188
  ### Agent host files (`coderifts agent-setup`)
142
189
 
143
190
  Writes the six CodeRifts agent-host rule files into the current repo (or `--out <dir>`):
package/bin/coderifts.js CHANGED
@@ -109,6 +109,7 @@ program
109
109
  .option('--branch <name>', 'Branch to protect (default: repo default branch)')
110
110
  .option('--repo <owner/repo>', 'Override owner/repo (default: git remote origin)')
111
111
  .option('--apply', 'Apply the protection change (default: print the exact gh command only)')
112
+ .option('--enforce-admins', 'Set enforce_admins:true on create (default: false — admins can bypass; gate reports admin_bypass_open)')
112
113
  .option('--json', 'Machine-readable JSON result')
113
114
  .action(async (options) => {
114
115
  const { runSetupRequiredCheck } = require('../src/commands/setup-required-check');
@@ -169,6 +170,80 @@ program
169
170
  runAgentSetup(options, { exit: true });
170
171
  });
171
172
 
173
+ // ── copilot-setup — emit GitHub Copilot MCP configs (VS Code + cloud agent + custom agent) ──
174
+ // Single-source from CANONICAL_TOOL_NAMES; root keys: servers (VS Code) vs mcpServers (cloud).
175
+ program
176
+ .command('copilot-setup')
177
+ .description('Write GitHub Copilot MCP configs (.vscode/mcp.json + cloud-agent paste JSON + docs)')
178
+ .option('--out <dir>', 'Target directory (default: current working directory)')
179
+ .option('--check', 'Exit 0 if on-disk files match embedded content; exit 1 on drift')
180
+ .option('--force', 'Overwrite existing files (default: skip collisions)')
181
+ .action((options) => {
182
+ const { runCopilotSetup } = require('../src/commands/copilot-setup');
183
+ runCopilotSetup(options, { exit: true });
184
+ });
185
+
186
+ // ── lock — agent contract lockfile v1 (observed usage; ID847) ──
187
+ // GET /api/v1/lock?repo= → write coderifts.lock. Observed-only; empty agents when none recorded.
188
+ program
189
+ .command('lock [repo]')
190
+ .description('Fetch the observed agent-contract lockfile (coderifts.lock v1) for a repo')
191
+ .option('--repo <owner/repo>', 'Repository (owner/repo); also accepted as a positional argument')
192
+ .option('--out <path>', 'Output path (default: coderifts.lock in cwd)')
193
+ .option('--json', 'Print the lock document JSON to stdout (still writes --out)')
194
+ .action(async (repoPositional, options) => {
195
+ const { runLock } = require('../src/commands/lock');
196
+ const result = await runLock({
197
+ ...options,
198
+ repo: options.repo || repoPositional || null,
199
+ });
200
+ if (result && typeof result.exitCode === 'number') {
201
+ process.exitCode = result.exitCode;
202
+ }
203
+ });
204
+
205
+ // ── outcome — POST /api/v1/outcomes (caller assertion; does not verify the deploy) ──
206
+ program
207
+ .command('outcome <kind>')
208
+ .description(
209
+ 'Report a post-hoc outcome for a decision_id (deploy_succeeded/deploy_failed/…). '
210
+ + 'Records the caller\'s assertion — does not verify the deploy itself.',
211
+ )
212
+ .option('--decision <decision_id>', 'Decision id from decision_result.decision_id (required)')
213
+ .option('--observed-at <iso>', 'When the outcome was observed (ISO-8601; default: now)')
214
+ .option('--details <json>', 'Optional JSON object/array bound to the outcome row')
215
+ .option('--json', 'Print the API response JSON to stdout')
216
+ .addHelpText('after', () => {
217
+ const { USAGE } = require('../src/commands/outcome');
218
+ return `\n${USAGE}\n`;
219
+ })
220
+ .action(async (kind, options) => {
221
+ const { runOutcome } = require('../src/commands/outcome');
222
+ const result = await runOutcome(kind, options);
223
+ if (result && typeof result.exitCode === 'number') {
224
+ process.exitCode = result.exitCode;
225
+ }
226
+ });
227
+
228
+ // ── claude-hook — Claude Code PreToolUse (exit 2 = BLOCK; never exit 1 for deny) ──
229
+ program
230
+ .command('claude-hook')
231
+ .description(
232
+ 'Claude Code PreToolUse gate: block contract-touching Write|Edit|MultiEdit on BLOCK/STOP '
233
+ + '(exit 2). Soft-allow when unconfigured/unparseable/API down.',
234
+ )
235
+ .addHelpText('after', () => {
236
+ const { USAGE } = require('../src/commands/claude-hook');
237
+ return `\n${USAGE}\n`;
238
+ })
239
+ .action(async () => {
240
+ const { runClaudeHook } = require('../src/commands/claude-hook');
241
+ const result = await runClaudeHook();
242
+ if (result && typeof result.exitCode === 'number') {
243
+ process.exitCode = result.exitCode;
244
+ }
245
+ });
246
+
172
247
  // ── hook command group ──
173
248
  const hookCmd = program
174
249
  .command('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.0.0",
3010
+ version: "3.2.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",
@@ -3045,7 +3045,7 @@ var require_package = __commonJS({
3045
3045
  },
3046
3046
  scripts: {
3047
3047
  build: "node scripts/copy-corpus.js && esbuild bin/coderifts.js --bundle --platform=node --target=node18 --outfile=dist/cli.js",
3048
- prepublishOnly: "npm run build",
3048
+ prepublishOnly: "bash ../../scripts/freeze-gate.sh && npm run build",
3049
3049
  postinstall: "node scripts/postinstall.js",
3050
3050
  test: "node --test test/*.test.js"
3051
3051
  },
@@ -13549,10 +13549,37 @@ var require_cloud = __commonJS({
13549
13549
  const q = encodeURIComponent(String(repo || ""));
13550
13550
  return cloudRequest("GET", `/api/v1/enforcement-status?repo=${q}`, apiKey);
13551
13551
  }
13552
+ function cloudGetLock(repo, apiKey) {
13553
+ const q = encodeURIComponent(String(repo || ""));
13554
+ return cloudRequest("GET", `/api/v1/lock?repo=${q}`, apiKey);
13555
+ }
13556
+ function cloudPostOutcome(apiKey, body) {
13557
+ return cloudRequest("POST", "/api/v1/outcomes", apiKey, body);
13558
+ }
13559
+ function cloudAuthorizePreflight(before, after, apiKey, ctx = {}) {
13560
+ return cloudRequest("POST", "/api/v1/preflight", apiKey, {
13561
+ preflight_mode: "authorize",
13562
+ artifacts: [
13563
+ {
13564
+ id: "api",
13565
+ type: "openapi",
13566
+ before: String(before == null ? "" : before),
13567
+ after: String(after == null ? "" : after)
13568
+ }
13569
+ ],
13570
+ context: {
13571
+ operation: ctx.operation || "merge",
13572
+ environment: ctx.environment || "staging"
13573
+ }
13574
+ });
13575
+ }
13552
13576
  module2.exports = {
13553
13577
  cloudDiff,
13554
13578
  cloudRequest,
13555
13579
  cloudGetEnforcementStatus,
13580
+ cloudGetLock,
13581
+ cloudPostOutcome,
13582
+ cloudAuthorizePreflight,
13556
13583
  API_BASE
13557
13584
  };
13558
13585
  }
@@ -69051,6 +69078,24 @@ var require_deploy_gate2 = __commonJS({
69051
69078
  return null;
69052
69079
  }
69053
69080
  }
69081
+ function extractDecisionIdFromReceipt(receipt) {
69082
+ if (!receipt || typeof receipt !== "object") return null;
69083
+ if (typeof receipt.decision_id === "string" && receipt.decision_id.trim()) {
69084
+ return receipt.decision_id.trim();
69085
+ }
69086
+ const nested = receipt.decision_result;
69087
+ if (nested && typeof nested === "object" && typeof nested.decision_id === "string" && nested.decision_id.trim()) {
69088
+ return nested.decision_id.trim();
69089
+ }
69090
+ return null;
69091
+ }
69092
+ function formatOutcomeReportHint(decisionId) {
69093
+ const id = String(decisionId);
69094
+ return [
69095
+ "# report the deploy outcome after your deploy step:",
69096
+ `coderifts outcome <deploy_succeeded|deploy_failed|rolled_back> --decision ${id}`
69097
+ ].join("\n");
69098
+ }
69054
69099
  function renderDeployGateTerminal(bind, enforce) {
69055
69100
  const g = bind.gate;
69056
69101
  const color = bind.deploy_check_status === "success" ? chalk.green : bind.deploy_check_status === "failure" ? chalk.red : chalk.yellow;
@@ -69080,10 +69125,23 @@ var require_deploy_gate2 = __commonJS({
69080
69125
  const observed = observeCDEnforcement({ enforce });
69081
69126
  const bind = deployBind({ environment, artifact_id: artifactId, receipt, observed_cd_enforcement: observed });
69082
69127
  const code = clampExit(bind.deploy_check_status, enforce);
69128
+ const decisionId = extractDecisionIdFromReceipt(receipt);
69083
69129
  if (options.json) {
69084
- console.log(renderJson({ command: "deploy-gate", environment, artifact_id: artifactId, phase: enforce ? "enforcing" : "advisory", exit_code: code, ...bind }));
69130
+ console.log(renderJson({
69131
+ command: "deploy-gate",
69132
+ environment,
69133
+ artifact_id: artifactId,
69134
+ phase: enforce ? "enforcing" : "advisory",
69135
+ exit_code: code,
69136
+ decision_id: decisionId,
69137
+ ...bind
69138
+ }));
69085
69139
  } else {
69086
69140
  console.log(renderDeployGateTerminal(bind, enforce));
69141
+ if (decisionId) {
69142
+ console.log(formatOutcomeReportHint(decisionId));
69143
+ console.log("");
69144
+ }
69087
69145
  }
69088
69146
  process.exit(code);
69089
69147
  }
@@ -69093,6 +69151,8 @@ var require_deploy_gate2 = __commonJS({
69093
69151
  observeCDEnforcement,
69094
69152
  clampExit,
69095
69153
  readReceiptFile,
69154
+ extractDecisionIdFromReceipt,
69155
+ formatOutcomeReportHint,
69096
69156
  renderDeployGateTerminal
69097
69157
  };
69098
69158
  }
@@ -95850,13 +95910,30 @@ var require_setup_required_check = __commonJS({
95850
95910
  if (!protection || typeof protection !== "object") return [];
95851
95911
  const rsc = protection.required_status_checks;
95852
95912
  if (!rsc || typeof rsc !== "object") return [];
95853
- if (Array.isArray(rsc.contexts) && rsc.contexts.length) {
95854
- return rsc.contexts.map((c) => String(c));
95913
+ const raw = rsc.contexts || (Array.isArray(rsc.checks) ? rsc.checks.map((c) => c && c.context) : []);
95914
+ if (!Array.isArray(raw)) return [];
95915
+ return raw.map((c) => c == null ? "" : String(c)).filter(Boolean);
95916
+ }
95917
+ var HINT_403_PLAN_LIMIT = "private repo on a free plan: branch protection requires GitHub Pro or a public repo";
95918
+ var HINT_403_PERMISSION = "administration:read (or repo admin) required to read branch protection \u2014 try: gh auth refresh -s repo";
95919
+ var HINT_403_BOTH = "cannot distinguish cause from the response body \u2014 either (1) private repo on a free plan (branch protection needs GitHub Pro or a public repo) or (2) missing administration:read / repo admin (try: gh auth refresh -s repo)";
95920
+ function github403EvidenceText(read) {
95921
+ const parts = [];
95922
+ const body = read && read.body;
95923
+ if (body && typeof body === "object" && body.message != null) parts.push(String(body.message));
95924
+ if (typeof body === "string" && body.trim()) parts.push(body);
95925
+ if (read && read.errorMessage) parts.push(String(read.errorMessage));
95926
+ return parts.join("\n");
95927
+ }
95928
+ function classify403Cause(read) {
95929
+ const text = github403EvidenceText(read || {});
95930
+ if (/Upgrade to GitHub Pro|make this repository public/i.test(text)) {
95931
+ return { cause: "plan_limit", permission_hint: HINT_403_PLAN_LIMIT };
95855
95932
  }
95856
- if (Array.isArray(rsc.checks)) {
95857
- return rsc.checks.map((c) => c && c.context != null ? String(c.context) : "").filter(Boolean);
95933
+ if (/Resource not accessible|insufficient.*scope|not have permission|Must have admin|Requires authentication|Bad credentials|admin(istration)?:?(read|write)?/i.test(text) && !/Upgrade to GitHub Pro/i.test(text)) {
95934
+ return { cause: "permission", permission_hint: HINT_403_PERMISSION };
95858
95935
  }
95859
- return [];
95936
+ return { cause: "unknown", permission_hint: HINT_403_BOTH };
95860
95937
  }
95861
95938
  function classifyObservation(read, contextName = CHECK_NAME) {
95862
95939
  const status = read && read.status != null ? Number(read.status) : null;
@@ -95870,13 +95947,15 @@ var require_setup_required_check = __commonJS({
95870
95947
  };
95871
95948
  }
95872
95949
  if (status === 403) {
95950
+ const disc = classify403Cause(read);
95873
95951
  return {
95874
95952
  state: "UNKNOWN",
95875
95953
  context_is_required: false,
95876
95954
  required_contexts: [],
95877
95955
  protection: null,
95878
95956
  observation_error: "403",
95879
- permission_hint: "administration:read (or repo admin) required to read branch protection"
95957
+ observation_cause: disc.cause,
95958
+ permission_hint: disc.permission_hint
95880
95959
  };
95881
95960
  }
95882
95961
  if (status != null && status >= 400) {
@@ -96220,11 +96299,25 @@ var require_setup_required_check = __commonJS({
96220
96299
  if (!options.json) {
96221
96300
  logErr(chalk.red("Cannot observe branch protection (UNKNOWN)."));
96222
96301
  logErr(` HTTP: ${obs.observation_error || "unknown"}`);
96223
- logErr(` ${obs.permission_hint || "An admin with administration:read must run this command."}`);
96224
- logErr(" The CodeRifts App never writes protection; an admin must grant or run this.");
96302
+ logErr(` ${obs.permission_hint || HINT_403_BOTH}`);
96303
+ if (obs.observation_cause === "plan_limit") {
96304
+ logErr(" Not a missing-permission issue \u2014 the free-plan private-repo limit blocks classic protection reads.");
96305
+ } else if (obs.observation_cause === "permission") {
96306
+ logErr(" The CodeRifts App never writes protection; an admin must grant or run this.");
96307
+ } else {
96308
+ logErr(" The CodeRifts App never writes protection; fix plan limit or admin access, then re-run.");
96309
+ }
96225
96310
  }
96226
- return finish(EXIT.PERMISSION, { ...basePayload, ok: false, code: "PERMISSION", observation_error: obs.observation_error });
96311
+ return finish(EXIT.PERMISSION, {
96312
+ ...basePayload,
96313
+ ok: false,
96314
+ code: "PERMISSION",
96315
+ observation_error: obs.observation_error,
96316
+ observation_cause: obs.observation_cause || null,
96317
+ permission_hint: obs.permission_hint || null
96318
+ });
96227
96319
  }
96320
+ const enforceAdmins = options.enforceAdmins === true;
96228
96321
  let payload;
96229
96322
  if (obs.state === "ABSENT" || !obs.protection) {
96230
96323
  payload = {
@@ -96232,12 +96325,13 @@ var require_setup_required_check = __commonJS({
96232
96325
  strict: false,
96233
96326
  contexts: [CHECK_NAME]
96234
96327
  },
96235
- enforce_admins: false,
96328
+ enforce_admins: enforceAdmins,
96236
96329
  required_pull_request_reviews: null,
96237
96330
  restrictions: null
96238
96331
  };
96239
96332
  } else {
96240
96333
  payload = buildProtectionUpdatePayload(obs.protection, CHECK_NAME);
96334
+ if (enforceAdmins) payload.enforce_admins = true;
96241
96335
  }
96242
96336
  const cmdText = printApplyCommand(owner, repo, branch, payload);
96243
96337
  if (!options.apply) {
@@ -96246,6 +96340,11 @@ var require_setup_required_check = __commonJS({
96246
96340
  log(` repo: ${owner}/${repo}`);
96247
96341
  log(` branch: ${branch}`);
96248
96342
  log("");
96343
+ log("enforce_admins (admin bypass of the required check):");
96344
+ log(" true: admins cannot bypass the contract gate (closes the admin_bypass_open residual)");
96345
+ log(" false (default): admins can bypass; the gate reports admin_bypass_open");
96346
+ log(` this run: ${enforceAdmins ? "true (--enforce-admins)" : "false (default; pass --enforce-admins to set true)"}`);
96347
+ log("");
96249
96348
  log("Dry-run (default). Exact command to add the required check with your credentials:");
96250
96349
  log("");
96251
96350
  log(cmdText);
@@ -96256,6 +96355,7 @@ var require_setup_required_check = __commonJS({
96256
96355
  return finish(EXIT.NEEDS_APPLY, {
96257
96356
  ...basePayload,
96258
96357
  code: "NEEDS_APPLY",
96358
+ enforce_admins: enforceAdmins,
96259
96359
  apply_command: cmdText,
96260
96360
  apply_payload: payload
96261
96361
  });
@@ -96331,6 +96431,11 @@ var require_setup_required_check = __commonJS({
96331
96431
  EXIT,
96332
96432
  extractRequiredContexts,
96333
96433
  classifyObservation,
96434
+ classify403Cause,
96435
+ github403EvidenceText,
96436
+ HINT_403_PLAN_LIMIT,
96437
+ HINT_403_PERMISSION,
96438
+ HINT_403_BOTH,
96334
96439
  buildProtectionUpdatePayload,
96335
96440
  detectRulesetRequiredCheck,
96336
96441
  parseGitHubRemote,
@@ -97229,6 +97334,744 @@ ${USAGE}` };
97229
97334
  }
97230
97335
  });
97231
97336
 
97337
+ // src/copilot-mcp.embedded.js
97338
+ var require_copilot_mcp_embedded = __commonJS({
97339
+ "src/copilot-mcp.embedded.js"(exports2, module2) {
97340
+ "use strict";
97341
+ var COPILOT_MCP_PATHS = Object.freeze([
97342
+ ".vscode/mcp.json",
97343
+ "copilot-cloud-agent-mcp.json",
97344
+ "copilot-custom-agent-mcp.frontmatter.md",
97345
+ "docs/copilot-mcp.md"
97346
+ ]);
97347
+ var COPILOT_MCP_FILES = Object.freeze({
97348
+ ".vscode/mcp.json": '{\n "$comment": "GENERATED by scripts/generate-copilot-mcp.js from MCP_MANIFEST / CANONICAL_TOOL_NAMES (src/routes/mcp-streamable.js). Do not hand-edit; re-run the generator.",\n "servers": {\n "coderifts": {\n "type": "http",\n "url": "https://app.coderifts.com/mcp",\n "headers": {\n "Authorization": "Bearer ${input:coderifts_api_key}"\n }\n }\n },\n "inputs": [\n {\n "type": "promptString",\n "id": "coderifts_api_key",\n "description": "CodeRifts API key (https://coderifts.com) \u2014 Authorization Bearer",\n "password": true\n }\n ]\n}\n',
97349
+ "copilot-cloud-agent-mcp.json": '{\n "mcpServers": {\n "coderifts": {\n "type": "http",\n "url": "https://app.coderifts.com/mcp",\n "tools": [\n "preflight_change_set",\n "verify_receipt",\n "get_decision_details"\n ],\n "headers": {\n "Authorization": "Bearer ${COPILOT_MCP_CODERIFTS_API_KEY}"\n }\n }\n }\n}\n',
97350
+ "copilot-custom-agent-mcp.frontmatter.md": "---\n# GENERATED by scripts/generate-copilot-mcp.js from MCP_MANIFEST / CANONICAL_TOOL_NAMES (src/routes/mcp-streamable.js). Do not hand-edit; re-run the generator.\nname: coderifts-governance\ndescription: >\n CodeRifts API governance \u2014 preflight contract change sets before merge/deploy/publish,\n verify receipts, look up prior decisions. Branch on execution_action only.\ntools: ['coderifts/preflight_change_set', 'coderifts/verify_receipt', 'coderifts/get_decision_details']\nmcp-servers:\n coderifts:\n type: http\n url: https://app.coderifts.com/mcp\n tools:\n - preflight_change_set\n - verify_receipt\n - get_decision_details\n headers:\n Authorization: Bearer ${{ secrets.COPILOT_MCP_CODERIFTS_API_KEY }}\n---\n\nYou are a CodeRifts-aware agent. Before merge, deploy, publish, or tool registration when\ncontract artifacts changed, call `coderifts/preflight_change_set` with the complete base\u2192head\nchange set. Branch on `execution_action` only (CONTINUE, CONTINUE_WITH_MONITORING,\nREQUEST_APPROVAL, STOP). Do not treat `decision` or `safe_for_agent` as control flow.\nUse `coderifts/verify_receipt` to check an existing receipt; `coderifts/get_decision_details`\nfor a prior decision_id.\n",
97351
+ "docs/copilot-mcp.md": '# CodeRifts + GitHub Copilot MCP\n\n<!-- GENERATED by scripts/generate-copilot-mcp.js from MCP_MANIFEST / CANONICAL_TOOL_NAMES (src/routes/mcp-streamable.js). Do not hand-edit; re-run the generator. -->\n\nWire the **hosted** CodeRifts MCP server (`https://app.coderifts.com/mcp`) into every Copilot surface from **one**\nsource of truth (`CANONICAL_TOOL_NAMES` in `src/routes/mcp-streamable.js`).\n\n## Canonical tools (live tools/list)\n\n- `preflight_change_set`\n- `verify_receipt`\n- `get_decision_details`\n\nDo **not** list hidden/advanced aliases here. Only these 3 tools are the default\nagent-facing surface.\n\n## The servers-vs-mcpServers trap\n\n| Surface | Config location | Root key | Auth |\n|---------|-----------------|----------|------|\n| **VS Code / Copilot Chat** | `.vscode/mcp.json` | **`servers`** | `${input:coderifts_api_key}` + `inputs[]` |\n| **Copilot cloud agent + code review** | Repo **Settings \u2192 Copilot \u2192 MCP servers** (paste JSON) | **`mcpServers`** | Agents secret `COPILOT_MCP_CODERIFTS_API_KEY` in `headers` |\n| **Custom agent** (org/enterprise) | Agent profile `.md` YAML frontmatter | **`mcp-servers`** | `${{ secrets.COPILOT_MCP_CODERIFTS_API_KEY }}` |\n\nCursor / Claude Desktop / Grok use `mcpServers` in their own files \u2014 that is a **different**\necosystem. Do not copy a Cursor config into `.vscode/mcp.json`, and do not paste a VS Code\n`servers` document into GitHub repo Settings.\n\n## 1. VS Code / Copilot Chat (developer surface)\n\n1. Get an API key at https://coderifts.com\n2. Write the generated file to `.vscode/mcp.json` (or run `coderifts copilot-setup`)\n3. Reload VS Code; when prompted, paste the API key for `coderifts_api_key`\n4. In Copilot Chat Agent mode, confirm tools: preflight_change_set, verify_receipt, get_decision_details\n\n```json\n{\n "$comment": "GENERATED by scripts/generate-copilot-mcp.js from MCP_MANIFEST / CANONICAL_TOOL_NAMES (src/routes/mcp-streamable.js). Do not hand-edit; re-run the generator.",\n "servers": {\n "coderifts": {\n "type": "http",\n "url": "https://app.coderifts.com/mcp",\n "headers": {\n "Authorization": "Bearer ${input:coderifts_api_key}"\n }\n }\n },\n "inputs": [\n {\n "type": "promptString",\n "id": "coderifts_api_key",\n "description": "CodeRifts API key (https://coderifts.com) \u2014 Authorization Bearer",\n "password": true\n }\n ]\n}\n```\n\n## 2. Copilot cloud agent (PR / issue governance surface)\n\n1. Repo **Settings \u2192 Copilot \u2192 MCP servers** (or **Cloud agent \u2192 MCP configuration**)\n2. Paste the JSON below (root key **`mcpServers`**, includes required `tools` allowlist)\n3. Add an Agents secret: name `COPILOT_MCP_CODERIFTS_API_KEY`, value = CodeRifts API key\n4. Save. Validate: assign an issue to Copilot \u2192 session logs \u2192 **Start MCP Servers**\n\n```json\n{\n "mcpServers": {\n "coderifts": {\n "type": "http",\n "url": "https://app.coderifts.com/mcp",\n "tools": [\n "preflight_change_set",\n "verify_receipt",\n "get_decision_details"\n ],\n "headers": {\n "Authorization": "Bearer ${COPILOT_MCP_CODERIFTS_API_KEY}"\n }\n }\n }\n}\n```\n\nNotes:\n\n- Cloud agent does **not** support OAuth remote MCP; Bearer is correct for CodeRifts.\n- Cloud agent does **not** support interactive `inputs` \u2014 use Agents secrets only.\n- `tools` is required; list the 3 canonical tools (or `["*"]` only if you accept every tool the server exposes).\n\n## 3. Custom agent (optional org/enterprise)\n\nAdd `mcp-servers` to the agent profile frontmatter (see generated\n`copilot-custom-agent-mcp.frontmatter.md`). Tool names may be namespaced as\n`coderifts/<tool>` in the profile `tools` list.\n\n## Regenerate / drift-check\n\n```bash\nnode scripts/generate-copilot-mcp.js\nnode scripts/generate-copilot-mcp.js --check\ncoderifts copilot-setup --out . # write into a repo\ncoderifts copilot-setup --check # drift vs embedded\n```\n\nServer URL and tool names always come from the live manifest \u2014 never hand-edit tool lists.\n'
97352
+ });
97353
+ module2.exports = { COPILOT_MCP_FILES, COPILOT_MCP_PATHS };
97354
+ }
97355
+ });
97356
+
97357
+ // src/commands/copilot-setup.js
97358
+ var require_copilot_setup = __commonJS({
97359
+ "src/commands/copilot-setup.js"(exports2, module2) {
97360
+ "use strict";
97361
+ var fs = require("fs");
97362
+ var path = require("path");
97363
+ var chalk = require_source();
97364
+ var { COPILOT_MCP_FILES, COPILOT_MCP_PATHS } = require_copilot_mcp_embedded();
97365
+ if (process.env.NO_COLOR) chalk.level = 0;
97366
+ var USAGE = `Usage: coderifts copilot-setup [--out <dir>] [--check] [--force]
97367
+
97368
+ --out <dir> Target directory (default: current working directory)
97369
+ --check Exit 0 if on-disk files match embedded content; exit 1 on drift
97370
+ --force Overwrite existing files (default: skip collisions)
97371
+ Unknown flags exit 1 (never silently ignored).
97372
+
97373
+ Writes:
97374
+ .vscode/mcp.json VS Code / Copilot Chat (root key: servers)
97375
+ copilot-cloud-agent-mcp.json Paste into Settings \u2192 Copilot \u2192 MCP (mcpServers)
97376
+ copilot-custom-agent-mcp.frontmatter.md Custom agent YAML frontmatter
97377
+ docs/copilot-mcp.md Install guide
97378
+ `;
97379
+ function parseCopilotSetupArgs(argv) {
97380
+ const args = argv.slice(2);
97381
+ let out = null;
97382
+ let check = false;
97383
+ let force = false;
97384
+ let i = 0;
97385
+ while (i < args.length && !String(args[i]).startsWith("-")) i += 1;
97386
+ while (i < args.length) {
97387
+ const a = args[i];
97388
+ if (a === "--out") {
97389
+ const v = args[i + 1];
97390
+ if (v == null || v.startsWith("-")) {
97391
+ return { out, check, force, error: `copilot-setup: --out requires a path
97392
+ ${USAGE}` };
97393
+ }
97394
+ out = path.resolve(v);
97395
+ i += 2;
97396
+ continue;
97397
+ }
97398
+ if (a === "--check") {
97399
+ check = true;
97400
+ i += 1;
97401
+ continue;
97402
+ }
97403
+ if (a === "--force") {
97404
+ force = true;
97405
+ i += 1;
97406
+ continue;
97407
+ }
97408
+ if (a.startsWith("-")) {
97409
+ return { out, check, force, error: `copilot-setup: unrecognized argument: ${a}
97410
+ ${USAGE}` };
97411
+ }
97412
+ return { out, check, force, error: `copilot-setup: unrecognized argument: ${a}
97413
+ ${USAGE}` };
97414
+ }
97415
+ return { out, check, force };
97416
+ }
97417
+ function runCopilotSetup(options = {}, deps = {}) {
97418
+ const log = deps.log || console.log.bind(console);
97419
+ const logErr = deps.logErr || console.error.bind(console);
97420
+ const doExit = deps.exit !== false;
97421
+ const cwd = deps.cwd || process.cwd();
97422
+ const exists = deps.exists || fs.existsSync.bind(fs);
97423
+ const readFile = deps.readFile || ((p) => fs.readFileSync(p, "utf8"));
97424
+ const writeFile = deps.writeFile || ((p, c) => {
97425
+ fs.mkdirSync(path.dirname(p), { recursive: true });
97426
+ fs.writeFileSync(p, c, "utf8");
97427
+ });
97428
+ let outDir = options.out ? path.resolve(String(options.out)) : cwd;
97429
+ let check = !!options.check;
97430
+ let force = !!options.force;
97431
+ if (deps.argv) {
97432
+ const parsed = parseCopilotSetupArgs(deps.argv);
97433
+ if (parsed.error) {
97434
+ logErr(parsed.error.trimEnd());
97435
+ if (doExit) process.exit(1);
97436
+ return { exitCode: 1, code: "USAGE", message: parsed.error };
97437
+ }
97438
+ if (parsed.out) outDir = parsed.out;
97439
+ check = parsed.check;
97440
+ force = parsed.force;
97441
+ }
97442
+ const files = deps.files || COPILOT_MCP_FILES;
97443
+ const relPaths = deps.paths || COPILOT_MCP_PATHS;
97444
+ if (check) {
97445
+ let stale = false;
97446
+ for (const rel of relPaths) {
97447
+ const fp = path.join(outDir, rel);
97448
+ const expected = files[rel];
97449
+ if (!exists(fp)) {
97450
+ logErr(chalk.red(`copilot-setup --check: missing ${rel}`));
97451
+ stale = true;
97452
+ continue;
97453
+ }
97454
+ const onDisk = readFile(fp);
97455
+ if (onDisk !== expected) {
97456
+ logErr(chalk.red(`copilot-setup --check: drift ${rel}`));
97457
+ stale = true;
97458
+ }
97459
+ }
97460
+ if (stale) {
97461
+ logErr("Run: coderifts copilot-setup --out " + outDir + " --force");
97462
+ if (doExit) process.exit(1);
97463
+ return { exitCode: 1, code: "DRIFT", outDir };
97464
+ }
97465
+ log(chalk.green(`copilot-setup: up to date (${outDir}, ${relPaths.length} files)`));
97466
+ if (doExit) process.exit(0);
97467
+ return { exitCode: 0, code: "UP_TO_DATE", outDir };
97468
+ }
97469
+ const summary = { written: [], skipped: [], forced: [] };
97470
+ for (const rel of relPaths) {
97471
+ const fp = path.join(outDir, rel);
97472
+ const content = files[rel];
97473
+ if (exists(fp) && !force) {
97474
+ summary.skipped.push(rel);
97475
+ continue;
97476
+ }
97477
+ if (exists(fp) && force) summary.forced.push(rel);
97478
+ writeFile(fp, content);
97479
+ summary.written.push(rel);
97480
+ }
97481
+ if (!options.json) {
97482
+ log(chalk.bold("CodeRifts copilot-setup"));
97483
+ log(` target: ${outDir}`);
97484
+ for (const rel of summary.written) {
97485
+ const tag = summary.forced.includes(rel) ? "overwrote" : "wrote";
97486
+ log(chalk.green(` ${tag}: ${rel}`));
97487
+ }
97488
+ for (const rel of summary.skipped) {
97489
+ log(chalk.yellow(` skipped: ${rel} (exists; use --force to overwrite)`));
97490
+ }
97491
+ log("");
97492
+ log(chalk.dim(' VS Code: open .vscode/mcp.json \u2014 root key is "servers"'));
97493
+ log(chalk.dim(" Cloud: paste copilot-cloud-agent-mcp.json into Settings \u2192 Copilot \u2192 MCP"));
97494
+ log(chalk.dim(" Secret: COPILOT_MCP_CODERIFTS_API_KEY (Agents secret) = CodeRifts API key"));
97495
+ log(chalk.dim(` ${summary.written.length} written, ${summary.skipped.length} skipped`));
97496
+ }
97497
+ if (doExit) process.exit(0);
97498
+ return { exitCode: 0, code: "OK", outDir, ...summary };
97499
+ }
97500
+ module2.exports = {
97501
+ runCopilotSetup,
97502
+ parseCopilotSetupArgs,
97503
+ COPILOT_MCP_FILES,
97504
+ COPILOT_MCP_PATHS,
97505
+ USAGE
97506
+ };
97507
+ }
97508
+ });
97509
+
97510
+ // src/commands/lock.js
97511
+ var require_lock = __commonJS({
97512
+ "src/commands/lock.js"(exports2, module2) {
97513
+ "use strict";
97514
+ var fs = require("fs");
97515
+ var path = require("path");
97516
+ var chalk = require_source();
97517
+ var { getApiKey } = require_config();
97518
+ var { cloudGetLock } = require_cloud();
97519
+ if (process.env.NO_COLOR) chalk.level = 0;
97520
+ var DEFAULT_OUT = "coderifts.lock";
97521
+ var EPHEMERAL_LOCK_KEYS = Object.freeze([
97522
+ "correlation_id",
97523
+ "generated_at",
97524
+ "request_correlation_id",
97525
+ "meta"
97526
+ ]);
97527
+ var COMMITTED_KEY_ORDER = Object.freeze([
97528
+ "lockfile_version",
97529
+ "repo",
97530
+ "provenance",
97531
+ "agents",
97532
+ "last_accepted_fingerprint",
97533
+ "decision_spec_version",
97534
+ "receipt_kind"
97535
+ ]);
97536
+ var USAGE = [
97537
+ "Usage: coderifts lock --repo owner/repo [--out coderifts.lock]",
97538
+ " or: coderifts lock owner/repo [--out path]",
97539
+ "",
97540
+ "Fetches the observed agent-contract lockfile (coderifts.lock v1) and writes it to disk.",
97541
+ "Requires a cloud API key (coderifts login or CODERIFTS_API_KEY).",
97542
+ "Observed-only: agents[] come from real usage observations; empty when none recorded.",
97543
+ "Written file is byte-stable for the same state (no correlation_id / generated_at)."
97544
+ ].join("\n");
97545
+ function isValidRepo(full) {
97546
+ const parts = String(full || "").split("/");
97547
+ if (parts.length !== 2) return false;
97548
+ const [owner, repo] = parts.map((p) => p.trim());
97549
+ return !!(owner && repo);
97550
+ }
97551
+ function toCommittedLockfile(doc) {
97552
+ const src = doc && typeof doc === "object" && !Array.isArray(doc) ? doc : {};
97553
+ const out = {};
97554
+ for (const key of COMMITTED_KEY_ORDER) {
97555
+ if (!Object.prototype.hasOwnProperty.call(src, key)) continue;
97556
+ if (EPHEMERAL_LOCK_KEYS.includes(key)) continue;
97557
+ out[key] = src[key];
97558
+ }
97559
+ for (const k of EPHEMERAL_LOCK_KEYS) {
97560
+ if (Object.prototype.hasOwnProperty.call(out, k)) delete out[k];
97561
+ }
97562
+ return out;
97563
+ }
97564
+ function renderLockfileJson(doc) {
97565
+ return `${JSON.stringify(doc, null, 2)}
97566
+ `;
97567
+ }
97568
+ async function runLock(options = {}, deps = {}) {
97569
+ const getKey = deps.getApiKey || getApiKey;
97570
+ const fetchLock = deps.cloudGetLock || cloudGetLock;
97571
+ const log = deps.log || console.log;
97572
+ const errLog = deps.errLog || console.error;
97573
+ const writeFile = deps.writeFile || ((p, c) => {
97574
+ fs.mkdirSync(path.dirname(path.resolve(p)), { recursive: true });
97575
+ fs.writeFileSync(p, c, "utf8");
97576
+ });
97577
+ const cwd = deps.cwd || process.cwd();
97578
+ const repo = options.repo != null && String(options.repo).trim() ? String(options.repo).trim() : null;
97579
+ if (!repo) {
97580
+ errLog(chalk.red("Error: missing repo"));
97581
+ errLog(USAGE);
97582
+ return { exitCode: 1, error: "missing_repo" };
97583
+ }
97584
+ if (!isValidRepo(repo)) {
97585
+ errLog(chalk.red("Error: repo must be in owner/repo form (e.g. coderifts/app)"));
97586
+ return { exitCode: 1, error: "invalid_repo" };
97587
+ }
97588
+ const apiKey = getKey();
97589
+ if (!apiKey) {
97590
+ errLog(chalk.red("Error: no API key. Run `coderifts login` or set CODERIFTS_API_KEY."));
97591
+ return { exitCode: 1, error: "missing_api_key" };
97592
+ }
97593
+ let doc;
97594
+ try {
97595
+ doc = await fetchLock(repo, apiKey);
97596
+ } catch (e) {
97597
+ const msg = e && e.message ? String(e.message) : "request failed";
97598
+ errLog(chalk.red(`Error: ${msg}`));
97599
+ if (e && e.code) errLog(chalk.dim(` (${e.code})`));
97600
+ return { exitCode: 1, error: msg };
97601
+ }
97602
+ if (!doc || typeof doc !== "object" || doc.lockfile_version == null) {
97603
+ errLog(chalk.red("Error: invalid lockfile response from API"));
97604
+ return { exitCode: 1, error: "invalid_response" };
97605
+ }
97606
+ const committed = toCommittedLockfile(doc);
97607
+ const body = renderLockfileJson(committed);
97608
+ const outRel = options.out != null && String(options.out).trim() ? String(options.out).trim() : DEFAULT_OUT;
97609
+ const outPath = path.isAbsolute(outRel) ? outRel : path.join(cwd, outRel);
97610
+ try {
97611
+ writeFile(outPath, body);
97612
+ } catch (e) {
97613
+ const msg = e && e.message ? String(e.message) : "write failed";
97614
+ errLog(chalk.red(`Error: failed to write ${outPath}: ${msg}`));
97615
+ return { exitCode: 1, error: msg };
97616
+ }
97617
+ if (options.json) {
97618
+ log(body.trimEnd());
97619
+ } else {
97620
+ const nAgents = Array.isArray(committed.agents) ? committed.agents.length : 0;
97621
+ const nOps = Array.isArray(committed.agents) ? committed.agents.reduce((n, a) => n + (a.operations && a.operations.length || 0), 0) : 0;
97622
+ log(chalk.bold("CodeRifts lock (observed)"));
97623
+ log(` repo: ${committed.repo || repo}`);
97624
+ log(` wrote: ${outPath}`);
97625
+ log(` agents: ${nAgents} operations: ${nOps}`);
97626
+ log(` fingerprint: ${committed.last_accepted_fingerprint || chalk.dim("(none)")}`);
97627
+ log(chalk.dim(" Observed-only; file is byte-stable (no correlation_id / generated_at)."));
97628
+ }
97629
+ return { exitCode: 0, doc, committed, outPath };
97630
+ }
97631
+ module2.exports = {
97632
+ runLock,
97633
+ renderLockfileJson,
97634
+ toCommittedLockfile,
97635
+ isValidRepo,
97636
+ DEFAULT_OUT,
97637
+ EPHEMERAL_LOCK_KEYS,
97638
+ COMMITTED_KEY_ORDER,
97639
+ USAGE
97640
+ };
97641
+ }
97642
+ });
97643
+
97644
+ // src/commands/outcome.js
97645
+ var require_outcome = __commonJS({
97646
+ "src/commands/outcome.js"(exports2, module2) {
97647
+ "use strict";
97648
+ var chalk = require_source();
97649
+ var { getApiKey } = require_config();
97650
+ var { cloudPostOutcome } = require_cloud();
97651
+ if (process.env.NO_COLOR) chalk.level = 0;
97652
+ var OUTCOME_KINDS = Object.freeze([
97653
+ "deploy_succeeded",
97654
+ "deploy_failed",
97655
+ "rolled_back",
97656
+ "consumer_break_reported",
97657
+ "remediation_verified_working",
97658
+ "false_positive_reported",
97659
+ "other_reported"
97660
+ ]);
97661
+ var OUTCOME_KIND_SET = new Set(OUTCOME_KINDS);
97662
+ var USAGE = [
97663
+ "Usage: coderifts outcome <kind> --decision <decision_id> [--observed-at <ISO>] [--details <json>] [--json]",
97664
+ "",
97665
+ "Report a post-hoc observed outcome for a past decision_id to the CodeRifts cloud API.",
97666
+ "This records the CALLER'S assertion (e.g. your deploy job knows success/failure) \u2014",
97667
+ "the command does NOT itself verify that a deploy succeeded or failed.",
97668
+ "",
97669
+ "kind (required, closed set):",
97670
+ ` ${OUTCOME_KINDS.join(", ")}`,
97671
+ "",
97672
+ "Requires a cloud API key (coderifts login or CODERIFTS_API_KEY).",
97673
+ "POST /api/v1/outcomes \u2014 source is always reported (server-side); reporter is derived from the key."
97674
+ ].join("\n");
97675
+ function isValidOutcomeKind(kind) {
97676
+ return typeof kind === "string" && OUTCOME_KIND_SET.has(kind);
97677
+ }
97678
+ function isValidObservedAt(iso) {
97679
+ if (typeof iso !== "string" || !iso.trim()) return false;
97680
+ const t = Date.parse(iso);
97681
+ return Number.isFinite(t);
97682
+ }
97683
+ function parseDetails(raw) {
97684
+ if (raw == null || raw === "") return { ok: true, details: null };
97685
+ if (typeof raw !== "string") return { ok: false, error: "--details must be a JSON string" };
97686
+ try {
97687
+ const v = JSON.parse(raw);
97688
+ if (v !== null && typeof v !== "object") {
97689
+ return { ok: false, error: "--details must be a JSON object or array" };
97690
+ }
97691
+ return { ok: true, details: v };
97692
+ } catch (e) {
97693
+ return { ok: false, error: `--details is not valid JSON: ${e && e.message || "parse error"}` };
97694
+ }
97695
+ }
97696
+ async function runOutcome(kind, options = {}, deps = {}) {
97697
+ const getKey = deps.getApiKey || getApiKey;
97698
+ const postOutcome = deps.cloudPostOutcome || cloudPostOutcome;
97699
+ const log = deps.log || console.log;
97700
+ const errLog = deps.errLog || console.error;
97701
+ const nowIso = deps.nowIso || (() => (/* @__PURE__ */ new Date()).toISOString());
97702
+ if (kind == null || String(kind).trim() === "") {
97703
+ errLog(chalk.red("Error: missing outcome kind"));
97704
+ errLog(USAGE);
97705
+ return { exitCode: 1, error: "missing_kind" };
97706
+ }
97707
+ const outcomeKind = String(kind).trim();
97708
+ if (!isValidOutcomeKind(outcomeKind)) {
97709
+ errLog(chalk.red(`Error: invalid outcome kind '${outcomeKind}'`));
97710
+ errLog(chalk.dim(` Valid kinds: ${OUTCOME_KINDS.join(", ")}`));
97711
+ return { exitCode: 1, error: "invalid_kind" };
97712
+ }
97713
+ const decisionId = options.decision != null ? String(options.decision).trim() : "";
97714
+ if (!decisionId) {
97715
+ errLog(chalk.red("Error: --decision <decision_id> is required"));
97716
+ errLog(USAGE);
97717
+ return { exitCode: 1, error: "missing_decision" };
97718
+ }
97719
+ let observedAt;
97720
+ if (options.observedAt != null && String(options.observedAt).trim() !== "") {
97721
+ observedAt = String(options.observedAt).trim();
97722
+ if (!isValidObservedAt(observedAt)) {
97723
+ errLog(chalk.red("Error: --observed-at must be a valid ISO-8601 timestamp"));
97724
+ return { exitCode: 1, error: "invalid_observed_at" };
97725
+ }
97726
+ } else {
97727
+ observedAt = nowIso();
97728
+ }
97729
+ const det = parseDetails(options.details);
97730
+ if (!det.ok) {
97731
+ errLog(chalk.red(`Error: ${det.error}`));
97732
+ return { exitCode: 1, error: "invalid_details" };
97733
+ }
97734
+ const apiKey = getKey();
97735
+ if (!apiKey) {
97736
+ errLog(chalk.red("Error: no API key. Run `coderifts login` or set CODERIFTS_API_KEY."));
97737
+ return { exitCode: 1, error: "missing_api_key" };
97738
+ }
97739
+ const body = {
97740
+ decision_id: decisionId,
97741
+ outcome_kind: outcomeKind,
97742
+ observed_at: observedAt
97743
+ };
97744
+ if (det.details != null) body.details = det.details;
97745
+ let response;
97746
+ try {
97747
+ response = await postOutcome(apiKey, body);
97748
+ } catch (e) {
97749
+ const msg = e && e.message ? String(e.message) : "request failed";
97750
+ errLog(chalk.red(`Error: ${msg}`));
97751
+ if (e && e.statusCode) errLog(chalk.dim(` (HTTP ${e.statusCode})`));
97752
+ if (e && e.code) errLog(chalk.dim(` (${e.code})`));
97753
+ return { exitCode: 1, error: msg, body };
97754
+ }
97755
+ if (options.json) {
97756
+ log(JSON.stringify(response, null, 2));
97757
+ } else {
97758
+ const o = response && response.outcome ? response.outcome : response;
97759
+ log(chalk.bold("CodeRifts outcome recorded"));
97760
+ log(` kind: ${outcomeKind}`);
97761
+ log(` decision_id: ${decisionId}`);
97762
+ log(` observed_at: ${observedAt}`);
97763
+ if (o && o.id) log(` id: ${o.id}`);
97764
+ if (response && response.source) log(` source: ${response.source}`);
97765
+ log(chalk.dim(" Caller assertion only \u2014 this command did not verify the deploy."));
97766
+ }
97767
+ return { exitCode: 0, response, body };
97768
+ }
97769
+ module2.exports = {
97770
+ runOutcome,
97771
+ isValidOutcomeKind,
97772
+ isValidObservedAt,
97773
+ parseDetails,
97774
+ OUTCOME_KINDS,
97775
+ USAGE
97776
+ };
97777
+ }
97778
+ });
97779
+
97780
+ // src/commands/claude-hook.js
97781
+ var require_claude_hook = __commonJS({
97782
+ "src/commands/claude-hook.js"(exports2, module2) {
97783
+ "use strict";
97784
+ var fs = require("fs");
97785
+ var path = require("path");
97786
+ var { execSync } = require("child_process");
97787
+ var { getApiKey } = require_config();
97788
+ var { cloudAuthorizePreflight } = require_cloud();
97789
+ var DEFAULT_SPEC_PATH = "api/openapi.yaml";
97790
+ var CLOSED_ACTIONS = /* @__PURE__ */ new Set([
97791
+ "CONTINUE",
97792
+ "CONTINUE_WITH_MONITORING",
97793
+ "REQUEST_APPROVAL",
97794
+ "STOP"
97795
+ ]);
97796
+ var USAGE = [
97797
+ "Usage: coderifts claude-hook",
97798
+ "",
97799
+ "Claude Code PreToolUse hook: gates Write|Edit|MultiEdit when they touch the",
97800
+ "configured contract spec path. Reads JSON context from STDIN.",
97801
+ "",
97802
+ "Exit map (Claude Code semantics \u2014 get this exactly right):",
97803
+ " 2 BLOCK \u2014 tool call cancelled (decision BLOCK/STOP or unrecognised execution_action)",
97804
+ " 0 allow \u2014 or soft-skip (see soft contracts)",
97805
+ " 1 NEVER used for security deny (Claude treats exit 1 as non-blocking; action proceeds)",
97806
+ "",
97807
+ "Soft contracts (exit 0 + one-line stderr note \u2014 do not brick the session):",
97808
+ " no API key (git config coderifts.apiKey / coderifts login / CODERIFTS_API_KEY)",
97809
+ " unparseable/unknown stdin shape",
97810
+ " file_path is not the configured spec path",
97811
+ " Edit/MultiEdit cannot be applied cleanly to disk content",
97812
+ " API unreachable / network error",
97813
+ "",
97814
+ "Spec path (same source as git pre-push hook):",
97815
+ " git config coderifts.specPath (default: api/openapi.yaml)",
97816
+ "",
97817
+ "Baseline at PreToolUse: before = current file on disk (pre-edit state \u2014 the tool has",
97818
+ "not written yet). after = proposed content from tool_input (Write: content; Edit/MultiEdit:",
97819
+ "old\u2192new applied to disk content).",
97820
+ "",
97821
+ "Push-time equivalent: coderifts hook install (git exit 1 on BLOCK)."
97822
+ ].join("\n");
97823
+ function readGitConfig(key, deps = {}) {
97824
+ const run = deps.execSync || execSync;
97825
+ const cwd = deps.cwd || process.cwd();
97826
+ try {
97827
+ const v = run(`git config ${key}`, { encoding: "utf8", cwd, stdio: ["ignore", "pipe", "pipe"] });
97828
+ const t = String(v || "").trim();
97829
+ return t || null;
97830
+ } catch {
97831
+ return null;
97832
+ }
97833
+ }
97834
+ function resolveApiKey(deps = {}) {
97835
+ const getKey = deps.getApiKey || getApiKey;
97836
+ const fromLogin = getKey();
97837
+ if (fromLogin) return fromLogin;
97838
+ const fromEnv = process.env.CODERIFTS_API_KEY && String(process.env.CODERIFTS_API_KEY).trim();
97839
+ if (fromEnv) return fromEnv;
97840
+ return readGitConfig("coderifts.apiKey", deps);
97841
+ }
97842
+ function resolveSpecPath(deps = {}) {
97843
+ const fromGit = readGitConfig("coderifts.specPath", deps);
97844
+ if (fromGit) return fromGit;
97845
+ return DEFAULT_SPEC_PATH;
97846
+ }
97847
+ function parseStdinJson(raw) {
97848
+ if (raw == null || String(raw).trim() === "") {
97849
+ return { ok: false, reason: "empty stdin" };
97850
+ }
97851
+ let obj;
97852
+ try {
97853
+ obj = JSON.parse(String(raw));
97854
+ } catch {
97855
+ return { ok: false, reason: "stdin is not JSON" };
97856
+ }
97857
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
97858
+ return { ok: false, reason: "stdin JSON is not an object" };
97859
+ }
97860
+ const toolName = obj.tool_name || obj.toolName || obj.name || null;
97861
+ let toolInput = obj.tool_input || obj.toolInput || obj.input || null;
97862
+ if (toolInput == null && obj.file_path) toolInput = obj;
97863
+ if (!toolName || typeof toolName !== "string") {
97864
+ return { ok: false, reason: "missing tool_name" };
97865
+ }
97866
+ if (!toolInput || typeof toolInput !== "object") {
97867
+ return { ok: false, reason: "missing tool_input" };
97868
+ }
97869
+ return { ok: true, toolName: String(toolName), toolInput };
97870
+ }
97871
+ function isSpecPath(filePath, specPath) {
97872
+ if (!filePath || !specPath) return false;
97873
+ const fp = path.normalize(String(filePath).replace(/\\/g, "/"));
97874
+ const sp = path.normalize(String(specPath).replace(/\\/g, "/"));
97875
+ if (fp === sp) return true;
97876
+ if (fp.endsWith("/" + sp) || fp.endsWith(sp)) return true;
97877
+ const baseFp = path.basename(fp);
97878
+ const baseSp = path.basename(sp);
97879
+ if (baseFp === baseSp && (fp.endsWith(sp) || sp.endsWith(baseSp))) {
97880
+ const tail = sp.split("/").filter(Boolean).join("/");
97881
+ return fp.replace(/\\/g, "/").endsWith(tail);
97882
+ }
97883
+ return false;
97884
+ }
97885
+ function deriveAfterContent(toolName, toolInput, diskBefore) {
97886
+ const name = String(toolName || "");
97887
+ if (name === "Write" || name === "write") {
97888
+ if (typeof toolInput.content !== "string") {
97889
+ return { ok: false, reason: "Write tool_input.content missing or not a string" };
97890
+ }
97891
+ return { ok: true, after: toolInput.content };
97892
+ }
97893
+ if (name === "Edit" || name === "edit") {
97894
+ const oldS = toolInput.old_string != null ? toolInput.old_string : toolInput.oldString;
97895
+ const newS = toolInput.new_string != null ? toolInput.new_string : toolInput.newString;
97896
+ if (typeof oldS !== "string" || typeof newS !== "string") {
97897
+ return { ok: false, reason: "Edit tool_input.old_string/new_string missing" };
97898
+ }
97899
+ if (!diskBefore.includes(oldS)) {
97900
+ return { ok: false, reason: "Edit old_string not found in disk content" };
97901
+ }
97902
+ return { ok: true, after: diskBefore.replace(oldS, newS) };
97903
+ }
97904
+ if (name === "MultiEdit" || name === "multi_edit" || name === "multiEdit") {
97905
+ const edits = toolInput.edits || toolInput.Edits;
97906
+ if (!Array.isArray(edits) || edits.length === 0) {
97907
+ return { ok: false, reason: "MultiEdit tool_input.edits missing or empty" };
97908
+ }
97909
+ let cur = diskBefore;
97910
+ for (let i = 0; i < edits.length; i++) {
97911
+ const e = edits[i] || {};
97912
+ const oldS = e.old_string != null ? e.old_string : e.oldString;
97913
+ const newS = e.new_string != null ? e.new_string : e.newString;
97914
+ if (typeof oldS !== "string" || typeof newS !== "string") {
97915
+ return { ok: false, reason: `MultiEdit edits[${i}] missing old_string/new_string` };
97916
+ }
97917
+ if (!cur.includes(oldS)) {
97918
+ return { ok: false, reason: `MultiEdit edits[${i}] old_string not found in content` };
97919
+ }
97920
+ cur = cur.replace(oldS, newS);
97921
+ }
97922
+ return { ok: true, after: cur };
97923
+ }
97924
+ return { ok: false, reason: `unsupported tool_name for content derive: ${name}` };
97925
+ }
97926
+ function mapDecisionSeverity(result) {
97927
+ const d = result && typeof result === "object" ? result : {};
97928
+ let ea = null;
97929
+ const dr = d.decision_result;
97930
+ if (dr && typeof dr === "object" && typeof dr.execution_action === "string") {
97931
+ ea = dr.execution_action;
97932
+ } else if (typeof d.execution_action === "string") {
97933
+ ea = d.execution_action;
97934
+ }
97935
+ let severity;
97936
+ if (ea != null && ea !== "") {
97937
+ if (!CLOSED_ACTIONS.has(ea)) {
97938
+ severity = "UNKNOWN";
97939
+ } else if (ea === "CONTINUE" || ea === "CONTINUE_WITH_MONITORING") {
97940
+ severity = "ALLOW";
97941
+ } else if (ea === "STOP") {
97942
+ severity = "BLOCK";
97943
+ } else {
97944
+ severity = "REQUIRE_APPROVAL";
97945
+ }
97946
+ } else {
97947
+ const od = d.omega_decision || d.decision || "ALLOW";
97948
+ if (od === "BLOCK") severity = "BLOCK";
97949
+ else if (od === "REQUIRE_APPROVAL") severity = "REQUIRE_APPROVAL";
97950
+ else if (od === "WARN") severity = "WARN";
97951
+ else severity = "ALLOW";
97952
+ }
97953
+ const decision = dr && dr.decision || d.decision || d.omega_decision || null;
97954
+ const decisionId = dr && dr.decision_id || d.decision_id || null;
97955
+ return {
97956
+ severity,
97957
+ decision: decision != null ? String(decision) : null,
97958
+ decisionId: decisionId != null ? String(decisionId) : null,
97959
+ executionAction: ea
97960
+ };
97961
+ }
97962
+ async function runClaudeHook(options = {}, deps = {}) {
97963
+ const errLog = deps.errLog || ((m) => console.error(String(m)));
97964
+ const readStdin = deps.readStdin || (() => {
97965
+ try {
97966
+ return fs.readFileSync(0, "utf8");
97967
+ } catch {
97968
+ return "";
97969
+ }
97970
+ });
97971
+ const readFile = deps.readFile || ((p) => fs.readFileSync(p, "utf8"));
97972
+ const exists = deps.exists || ((p) => fs.existsSync(p));
97973
+ const authorize = deps.cloudAuthorizePreflight || cloudAuthorizePreflight;
97974
+ const cwd = deps.cwd || process.cwd();
97975
+ const apiKey = resolveApiKey({ ...deps, cwd });
97976
+ if (!apiKey) {
97977
+ errLog("CodeRifts claude-hook: no API key (coderifts login / CODERIFTS_API_KEY / git config coderifts.apiKey) \u2014 allowing");
97978
+ return { exitCode: 0, reason: "missing_api_key" };
97979
+ }
97980
+ const raw = typeof options.stdin === "string" ? options.stdin : readStdin();
97981
+ const parsed = parseStdinJson(raw);
97982
+ if (!parsed.ok) {
97983
+ errLog(`CodeRifts claude-hook: ${parsed.reason} \u2014 allowing (soft; never block on parse gap)`);
97984
+ return { exitCode: 0, reason: "stdin_unparseable" };
97985
+ }
97986
+ const { toolName, toolInput } = parsed;
97987
+ const filePath = toolInput.file_path || toolInput.filePath || toolInput.path;
97988
+ if (!filePath || typeof filePath !== "string") {
97989
+ errLog("CodeRifts claude-hook: tool_input.file_path missing \u2014 allowing (soft)");
97990
+ return { exitCode: 0, reason: "missing_file_path" };
97991
+ }
97992
+ const specPath = resolveSpecPath({ ...deps, cwd });
97993
+ if (!isSpecPath(filePath, specPath)) {
97994
+ return { exitCode: 0, reason: "not_spec_path" };
97995
+ }
97996
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
97997
+ let diskBefore = "";
97998
+ if (exists(absPath)) {
97999
+ try {
98000
+ diskBefore = readFile(absPath, "utf8");
98001
+ } catch (e) {
98002
+ errLog(`CodeRifts claude-hook: cannot read ${absPath} \u2014 allowing (soft)`);
98003
+ return { exitCode: 0, reason: "disk_unreadable" };
98004
+ }
98005
+ }
98006
+ const derived = deriveAfterContent(toolName, toolInput, diskBefore);
98007
+ if (!derived.ok) {
98008
+ errLog(`CodeRifts claude-hook: ${derived.reason} \u2014 allowing (soft; never guess edit apply)`);
98009
+ return { exitCode: 0, reason: "edit_apply_failed" };
98010
+ }
98011
+ if (diskBefore === derived.after) {
98012
+ return { exitCode: 0, reason: "identical" };
98013
+ }
98014
+ let result;
98015
+ try {
98016
+ result = await authorize(diskBefore, derived.after, apiKey, {
98017
+ operation: "merge",
98018
+ environment: "staging"
98019
+ });
98020
+ } catch (e) {
98021
+ const msg = e && e.message ? String(e.message) : "request failed";
98022
+ errLog(`CodeRifts claude-hook: API unreachable (${msg}) \u2014 allowing (soft; availability must not brick the editor)`);
98023
+ return { exitCode: 0, reason: "api_unreachable" };
98024
+ }
98025
+ const mapped = mapDecisionSeverity(result);
98026
+ const idPart = mapped.decisionId ? ` decision_id=${mapped.decisionId}` : "";
98027
+ const decPart = mapped.decision ? ` decision=${mapped.decision}` : "";
98028
+ const eaPart = mapped.executionAction ? ` execution_action=${mapped.executionAction}` : "";
98029
+ if (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN") {
98030
+ errLog(
98031
+ `CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : "")
98032
+ );
98033
+ return {
98034
+ exitCode: 2,
98035
+ reason: mapped.severity === "UNKNOWN" ? "unknown_action" : "block",
98036
+ severity: mapped.severity,
98037
+ decision: mapped.decision,
98038
+ decisionId: mapped.decisionId
98039
+ };
98040
+ }
98041
+ if (mapped.severity === "REQUIRE_APPROVAL" || mapped.severity === "WARN") {
98042
+ errLog(`CodeRifts claude-hook: WARNING ${mapped.severity}${decPart}${eaPart}${idPart}`);
98043
+ return {
98044
+ exitCode: 0,
98045
+ reason: "warn",
98046
+ severity: mapped.severity,
98047
+ decision: mapped.decision,
98048
+ decisionId: mapped.decisionId
98049
+ };
98050
+ }
98051
+ return {
98052
+ exitCode: 0,
98053
+ reason: "allow",
98054
+ severity: "ALLOW",
98055
+ decision: mapped.decision,
98056
+ decisionId: mapped.decisionId
98057
+ };
98058
+ }
98059
+ module2.exports = {
98060
+ runClaudeHook,
98061
+ parseStdinJson,
98062
+ isSpecPath,
98063
+ deriveAfterContent,
98064
+ mapDecisionSeverity,
98065
+ resolveApiKey,
98066
+ resolveSpecPath,
98067
+ readGitConfig,
98068
+ DEFAULT_SPEC_PATH,
98069
+ CLOSED_ACTIONS,
98070
+ USAGE
98071
+ };
98072
+ }
98073
+ });
98074
+
97232
98075
  // corpus/vectors-mcp-fpfn.json
97233
98076
  var require_vectors_mcp_fpfn = __commonJS({
97234
98077
  "corpus/vectors-mcp-fpfn.json"(exports2, module2) {
@@ -99148,7 +99991,7 @@ program.command("login").description("Save your API key for cloud features").act
99148
99991
  const { login } = require_login();
99149
99992
  await login();
99150
99993
  });
99151
- program.command("setup-required-check").description('Guide setup of required status check "CodeRifts / contract-gate" (uses gh, your credentials)').option("--branch <name>", "Branch to protect (default: repo default branch)").option("--repo <owner/repo>", "Override owner/repo (default: git remote origin)").option("--apply", "Apply the protection change (default: print the exact gh command only)").option("--json", "Machine-readable JSON result").action(async (options) => {
99994
+ program.command("setup-required-check").description('Guide setup of required status check "CodeRifts / contract-gate" (uses gh, your credentials)').option("--branch <name>", "Branch to protect (default: repo default branch)").option("--repo <owner/repo>", "Override owner/repo (default: git remote origin)").option("--apply", "Apply the protection change (default: print the exact gh command only)").option("--enforce-admins", "Set enforce_admins:true on create (default: false \u2014 admins can bypass; gate reports admin_bypass_open)").option("--json", "Machine-readable JSON result").action(async (options) => {
99152
99995
  const { runSetupRequiredCheck } = require_setup_required_check();
99153
99996
  const result = await runSetupRequiredCheck(options);
99154
99997
  if (result && typeof result.exitCode === "number") {
@@ -99180,6 +100023,48 @@ program.command("agent-setup").description("Write AGENTS.md / CLAUDE.md / Cursor
99180
100023
  const { runAgentSetup } = require_agent_setup();
99181
100024
  runAgentSetup(options, { exit: true });
99182
100025
  });
100026
+ program.command("copilot-setup").description("Write GitHub Copilot MCP configs (.vscode/mcp.json + cloud-agent paste JSON + docs)").option("--out <dir>", "Target directory (default: current working directory)").option("--check", "Exit 0 if on-disk files match embedded content; exit 1 on drift").option("--force", "Overwrite existing files (default: skip collisions)").action((options) => {
100027
+ const { runCopilotSetup } = require_copilot_setup();
100028
+ runCopilotSetup(options, { exit: true });
100029
+ });
100030
+ program.command("lock [repo]").description("Fetch the observed agent-contract lockfile (coderifts.lock v1) for a repo").option("--repo <owner/repo>", "Repository (owner/repo); also accepted as a positional argument").option("--out <path>", "Output path (default: coderifts.lock in cwd)").option("--json", "Print the lock document JSON to stdout (still writes --out)").action(async (repoPositional, options) => {
100031
+ const { runLock } = require_lock();
100032
+ const result = await runLock({
100033
+ ...options,
100034
+ repo: options.repo || repoPositional || null
100035
+ });
100036
+ if (result && typeof result.exitCode === "number") {
100037
+ process.exitCode = result.exitCode;
100038
+ }
100039
+ });
100040
+ program.command("outcome <kind>").description(
100041
+ "Report a post-hoc outcome for a decision_id (deploy_succeeded/deploy_failed/\u2026). Records the caller's assertion \u2014 does not verify the deploy itself."
100042
+ ).option("--decision <decision_id>", "Decision id from decision_result.decision_id (required)").option("--observed-at <iso>", "When the outcome was observed (ISO-8601; default: now)").option("--details <json>", "Optional JSON object/array bound to the outcome row").option("--json", "Print the API response JSON to stdout").addHelpText("after", () => {
100043
+ const { USAGE } = require_outcome();
100044
+ return `
100045
+ ${USAGE}
100046
+ `;
100047
+ }).action(async (kind, options) => {
100048
+ const { runOutcome } = require_outcome();
100049
+ const result = await runOutcome(kind, options);
100050
+ if (result && typeof result.exitCode === "number") {
100051
+ process.exitCode = result.exitCode;
100052
+ }
100053
+ });
100054
+ program.command("claude-hook").description(
100055
+ "Claude Code PreToolUse gate: block contract-touching Write|Edit|MultiEdit on BLOCK/STOP (exit 2). Soft-allow when unconfigured/unparseable/API down."
100056
+ ).addHelpText("after", () => {
100057
+ const { USAGE } = require_claude_hook();
100058
+ return `
100059
+ ${USAGE}
100060
+ `;
100061
+ }).action(async () => {
100062
+ const { runClaudeHook } = require_claude_hook();
100063
+ const result = await runClaudeHook();
100064
+ if (result && typeof result.exitCode === "number") {
100065
+ process.exitCode = result.exitCode;
100066
+ }
100067
+ });
99183
100068
  var hookCmd = program.command("hook").description("Manage the CodeRifts pre-push Git hook");
99184
100069
  hookCmd.command("install").description("Install the CodeRifts pre-push hook in the current Git repo").action(() => {
99185
100070
  const { install } = require_hook();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "3.0.0",
3
+ "version": "3.2.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",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node scripts/copy-corpus.js && esbuild bin/coderifts.js --bundle --platform=node --target=node18 --outfile=dist/cli.js",
41
- "prepublishOnly": "npm run build",
41
+ "prepublishOnly": "bash ../../scripts/freeze-gate.sh && npm run build",
42
42
  "postinstall": "node scripts/postinstall.js",
43
43
  "test": "node --test test/*.test.js"
44
44
  },