coderifts 3.1.0 → 3.3.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
@@ -94,6 +94,57 @@ is **never** treated as a silent NEW_ARTIFACT pass.
94
94
 
95
95
  On success the command prints a **receipt reference** when the preflight path issues one.
96
96
 
97
+ ### `coderifts deploy-gate`
98
+
99
+ Gate a **deploy** on the current `{ environment, artifact }` using a preflight
100
+ receipt (phase-1 advisory). The CD sibling of `publish-gate` / the merge-gate
101
+ App check. Composes the validated `deployGate` from `@coderifts/agent-guard` —
102
+ the CLI only binds env + artifact + receipt + observed CD enforcement. No
103
+ scoring is reimplemented here.
104
+
105
+ Phase 1 **reports** and exits **0** even on a gate deny, unless the step is
106
+ marked enforcing (`--enforce` or `CODERIFTS_DEPLOY_ENFORCE=true`), in which
107
+ case a failure exits **1**. Missing `--env` / `--artifact` is a usage error
108
+ (exit **1**). No receipt → pending (never a fabricated allow).
109
+
110
+ | Flag | Description | Default |
111
+ |------|-------------|---------|
112
+ | `--env <environment>` | Target environment (e.g. `production`, `staging`) | required |
113
+ | `--artifact <artifact_id>` | Immutable artifact identity (content digest or commit SHA) | required |
114
+ | `--receipt <file>` | Path to the deploy-scoped receipt JSON from preflight | none |
115
+ | `--json` | Binding result as JSON | `false` |
116
+ | `--enforce` | Attest ENFORCING and exit non-zero on a gate failure | `false` |
117
+
118
+ ```bash
119
+ coderifts deploy-gate --env production --artifact "$SHA" --receipt receipt.json
120
+ coderifts deploy-gate --env production --artifact "$SHA" --receipt receipt.json --enforce
121
+ ```
122
+
123
+ ### `coderifts outcome <kind>`
124
+
125
+ Report a **post-hoc** observed outcome for a past `decision_id` to
126
+ `POST /api/v1/outcomes`. Records the **caller's assertion** (your deploy job
127
+ knows success/failure) — the command does **not** itself verify the deploy.
128
+
129
+ `kind` is a closed set: `deploy_succeeded`, `deploy_failed`, `rolled_back`,
130
+ `consumer_break_reported`, `remediation_verified_working`,
131
+ `false_positive_reported`, `other_reported`.
132
+
133
+ Requires a cloud API key (`coderifts login` or `CODERIFTS_API_KEY`). Reporter
134
+ is derived from the key. Exit **0** on success, **1** on usage / API error.
135
+
136
+ | Flag | Description | Default |
137
+ |------|-------------|---------|
138
+ | `--decision <decision_id>` | `decision_result.decision_id` (required) | none |
139
+ | `--observed-at <iso>` | When the outcome was observed (ISO-8601) | now |
140
+ | `--details <json>` | Optional JSON object/array bound to the row | none |
141
+ | `--json` | Print the API response JSON | `false` |
142
+
143
+ ```bash
144
+ coderifts outcome deploy_succeeded --decision "$DECISION_ID"
145
+ coderifts outcome deploy_failed --decision "$DECISION_ID" --json
146
+ ```
147
+
97
148
  ### `coderifts registry-gate [dir]`
98
149
 
99
150
  Local **registry admission gate** for a directory of OpenAPI/Swagger specs
@@ -138,6 +189,111 @@ After upgrading the `coderifts` CLI, re-run `coderifts hook install` so the
138
189
  installed hook matches the package (already-installed hooks are not
139
190
  auto-updated).
140
191
 
192
+ ### Claude Code PreToolUse (`coderifts claude-hook`) — ID824
193
+
194
+ Tool-call-time gate for **Claude Code**: blocks contract-touching `Write` /
195
+ `Edit` / `MultiEdit` when authorize preflight returns **BLOCK/STOP**.
196
+
197
+ **Exit map (Claude Code semantics — fixed):**
198
+
199
+ | Exit | Meaning |
200
+ |------|---------|
201
+ | **2** | **BLOCK** — tool call cancelled; stderr is shown to the model |
202
+ | **0** | Allow, or soft-skip (no key / wrong file / parse gap / API down) |
203
+ | **1** | **Never used for deny** — Claude treats exit 1 as non-blocking (action proceeds) |
204
+
205
+ Push-time equivalent (git exit **1** on BLOCK): `coderifts hook install`.
206
+
207
+ **Install path:** `npm i -g coderifts` then set key + spec (same as the git hook):
208
+
209
+ ```bash
210
+ git config coderifts.apiKey 'cr_live_…' # or: coderifts login / CODERIFTS_API_KEY
211
+ git config coderifts.specPath api/openapi.yaml # default if omitted
212
+ ```
213
+
214
+ **Recipe — project** (`.claude/settings.json`) **or user** (`~/.claude/settings.json`):
215
+
216
+ ```json
217
+ {
218
+ "hooks": {
219
+ "PreToolUse": [
220
+ {
221
+ "matcher": "Write|Edit|MultiEdit",
222
+ "hooks": [
223
+ {
224
+ "type": "command",
225
+ "command": "coderifts claude-hook",
226
+ "timeout": 60
227
+ }
228
+ ]
229
+ }
230
+ ]
231
+ }
232
+ }
233
+ ```
234
+
235
+ - **Project** `.claude/settings.json` — shared with the repo (commit if the team wants the gate).
236
+ - **User** `~/.claude/settings.json` — personal; applies to all projects on this machine.
237
+ - Matcher is case-sensitive tool names/regex. Exit-2 blocking is **Claude Code** semantics; the same decision discipline at push-time is the git pre-push hook above.
238
+
239
+ **`CODERIFTS_STRICT` (opt-in, not a package guarantee):** env `1` or `true`
240
+ (case-insensitive) hardens **enforcement-relevant** soft sites into **exit 2
241
+ (BLOCK)** — “governance did not run” must not read as ALLOW. Hardened: missing
242
+ API key; API unreachable (network/timeout/error); disk unreadable on the
243
+ contract path; Edit/MultiEdit apply failure on the contract path. Still soft
244
+ even in strict: non-spec path, identical content, unparseable stdin, missing
245
+ `file_path`, WARN/REQUIRE_APPROVAL (governance ran), real ALLOW. Unset/empty →
246
+ identical to the default soft UX. Hosts choose strict for CI/prod.
247
+
248
+ ```bash
249
+ CODERIFTS_STRICT=1 coderifts claude-hook
250
+ ```
251
+
252
+ ### `coderifts status [repo]`
253
+
254
+ **Read-only** cross-layer enforcement report (Runtime / Merge / Deploy, plus
255
+ Content). Calls `GET /api/v1/enforcement-status` and prints the statuses the
256
+ endpoint measured — no GitHub writes, no branch-protection changes. Pair with
257
+ `coderifts enforce` (that command **acts**; this one only **shows**).
258
+
259
+ Requires a cloud API key. Distinct from `coderifts hook status` (local
260
+ pre-push hook install state). Exit **0** on a successful read, **1** on
261
+ missing/invalid repo, missing key, or API error.
262
+
263
+ | Flag | Description | Default |
264
+ |------|-------------|---------|
265
+ | `[repo]` / `--repo <owner/repo>` | Repository | none (required) |
266
+ | `--json` | Machine-readable JSON (raw API body) | `false` |
267
+
268
+ ```bash
269
+ coderifts status owner/repo
270
+ coderifts status --repo owner/repo --json
271
+ ```
272
+
273
+ ### `coderifts enforce [repo]`
274
+
275
+ Close enforcement gaps by chaining existing setup commands: Merge →
276
+ `setup-required-check`; Runtime → `hook install` (apply only) + agent-guard
277
+ guidance; Deploy → `deploy-gate` CD-step guidance; Content → guidance only.
278
+ **Dry-run by default** (`--apply` to mutate). Does not bypass underlying
279
+ command safety. Does not label Runtime/Content as server ENFORCING. Does not
280
+ upgrade UNKNOWN.
281
+
282
+ Requires a cloud API key for the status read. Merge `--apply` uses your local
283
+ `gh` credentials, not the CodeRifts API key. Exit **0** when no chained step
284
+ failed, **1** on missing repo/key, API error, or any apply failure.
285
+
286
+ | Flag | Description | Default |
287
+ |------|-------------|---------|
288
+ | `[repo]` / `--repo <owner/repo>` | Repository | none (required) |
289
+ | `--apply` | Run underlying setup commands | dry-run |
290
+ | `--json` | Machine-readable JSON result | `false` |
291
+
292
+ ```bash
293
+ coderifts enforce owner/repo
294
+ coderifts enforce --repo owner/repo --apply
295
+ ```
296
+
141
297
  ### Agent host files (`coderifts agent-setup`)
142
298
 
143
299
  Writes the six CodeRifts agent-host rule files into the current repo (or `--out <dir>`):
@@ -163,6 +319,28 @@ re-sync with `node scripts/sync-cli-agent-host-embed.js` when the rule source ch
163
319
 
164
320
  Policy template for agent/MCP APIs: `coderifts init ai-agent` (aliases: `agent`, `mcp`).
165
321
 
322
+ ### Copilot MCP configs (`coderifts copilot-setup`)
323
+
324
+ Writes GitHub Copilot MCP configs from the canonical tool names. Root keys
325
+ differ: VS Code uses `servers`; Copilot cloud Settings paste uses `mcpServers`.
326
+
327
+ ```bash
328
+ coderifts copilot-setup # write (skip existing files)
329
+ coderifts copilot-setup --force # overwrite
330
+ coderifts copilot-setup --check # drift vs embedded content (exit 1 on drift)
331
+ coderifts copilot-setup --out ./docs # custom root
332
+ ```
333
+
334
+ | Flag | Description | Default |
335
+ |------|-------------|---------|
336
+ | `--out <dir>` | Target directory | cwd |
337
+ | `--check` | Exit 0 if on-disk files match embedded content; exit 1 on drift | `false` |
338
+ | `--force` | Overwrite existing files | skip collisions |
339
+
340
+ Writes: `.vscode/mcp.json`, `copilot-cloud-agent-mcp.json`,
341
+ `copilot-custom-agent-mcp.frontmatter.md`, `docs/copilot-mcp.md`. Unknown flags
342
+ exit 1 (never silently ignored).
343
+
166
344
  ### Required check wizard (`coderifts setup-required-check`)
167
345
 
168
346
  Guided setup so **CodeRifts / contract-gate** is a **required status check** on
@@ -198,6 +376,57 @@ coderifts setup-required-check --apply
198
376
  coderifts setup-required-check --branch main
199
377
  ```
200
378
 
379
+ ### `coderifts lock [repo]`
380
+
381
+ Fetch the observed agent-contract lockfile (`coderifts.lock` v1) from
382
+ `GET /api/v1/lock`, **or** `--check` MCP drift of a locked manifest against a
383
+ live one (`scoreMcpRisk`). `--check` is **monitor only** — it does not block.
384
+ Unreachable live → status `unreachable` (not drift). Lock v1 records
385
+ agents/ops, not MCP tool bodies; pass `--locked-manifest` (or additive
386
+ `mcp_manifests` on the lockfile) for `--check`.
387
+
388
+ Requires a cloud API key for write mode.
389
+
390
+ | Flag | Description | Default |
391
+ |------|-------------|---------|
392
+ | `[repo]` / `--repo <owner/repo>` | Repository | none (write mode) |
393
+ | `--out <path>` | Output path | `coderifts.lock` |
394
+ | `--json` | stdout-only JSON (write: lock doc; check: drift report) | `false` |
395
+ | `--check` | Compare locked vs live MCP manifest | `false` |
396
+ | `--locked-manifest <path>` | Baseline MCP manifest JSON (`tools[]`) for `--check` | none |
397
+ | `--live-manifest <path>` | Live MCP manifest JSON for `--check` | none |
398
+ | `--live-url <url>` | Fetch live MCP manifest JSON from URL for `--check` | none |
399
+ | `--lockfile <path>` | Read `coderifts.lock` for additive `mcp_manifests` | none |
400
+
401
+ ```bash
402
+ coderifts lock owner/repo
403
+ coderifts lock --check --locked-manifest mcp.json --live-manifest live.json
404
+ ```
405
+
406
+ **`--check` exit codes** (from `lock.js`): `0` unchanged; `3` drift; `2`
407
+ unreachable / missing locked or live (not clean); `1` usage/error.
408
+
409
+ ### `coderifts adopt`
410
+
411
+ Counterfactual report: what CodeRifts **would have done** on contract-artifact
412
+ changes in **local git** history. Extracts spec-only paths (not source files
413
+ whose names happen to contain “openapi”), then runs
414
+ `buildCounterfactualReport`. Step B (GitHub API + outcome correlation) is not
415
+ this command. Not a git repository → fail-closed (`GIT_ERROR`, exit **1**).
416
+ Success → exit **0**.
417
+
418
+ | Flag | Description | Default |
419
+ |------|-------------|---------|
420
+ | `--days <n>` | Look back N days (`git log --since`) | last 50 commits if neither flag set |
421
+ | `--commits <n>` | Last N commits | last 50 commits if neither flag set |
422
+ | `--json` | Machine-readable JSON report (analyzer logs silenced) | `false` |
423
+
424
+ ```bash
425
+ coderifts adopt
426
+ coderifts adopt --days 30
427
+ coderifts adopt --commits 20 --json
428
+ ```
429
+
201
430
  ## CI/CD Integration
202
431
 
203
432
  ### GitHub Actions
@@ -256,14 +485,18 @@ Two honest modes:
256
485
  |----------|-------------|
257
486
  | `CODERIFTS_API_KEY` | API key (alternative to `coderifts login`) |
258
487
  | `CODERIFTS_OASDIFF_BIN` | Explicit path to the `oasdiff` binary (overrides auto-detect) |
488
+ | `CODERIFTS_STRICT` | `claude-hook` only: `1` or `true` (case-insensitive) turns enforcement-relevant soft sites into exit **2** (BLOCK). Unset/empty → default soft allow. Not a package-level guarantee. |
489
+ | `CODERIFTS_DEPLOY_ENFORCE` | `deploy-gate`: `true` marks the step enforcing (same as `--enforce`); `unknown` is explicitly unobservable (never assume ENFORCING) |
259
490
  | `NO_COLOR` | Disable colored output |
260
491
 
261
492
  ## Exit Codes
262
493
 
263
494
  | Code | Meaning |
264
495
  |------|---------|
265
- | `0` | No breaking changes (or below threshold) |
266
- | `1` | Breaking changes found (in CI mode) or analysis failed |
496
+ | `0` | Success: no breaking changes (or below threshold); advisory `deploy-gate`; `lock --check` unchanged; `adopt` / `status` / `outcome` success |
497
+ | `1` | Breaking changes (CI mode), analysis/usage/API failure, or `deploy-gate` deny **when enforcing** |
498
+ | `2` | `claude-hook` BLOCK (Claude Code cancel); `lock --check` unreachable / missing locked or live |
499
+ | `3` | `lock --check` drift detected (monitor only — does not block the package) |
267
500
 
268
501
  ## Links
269
502
 
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');
@@ -182,14 +183,21 @@ program
182
183
  runCopilotSetup(options, { exit: true });
183
184
  });
184
185
 
185
- // ── lock — agent contract lockfile v1 (observed usage; ID847) ──
186
- // GET /api/v1/lock?repo= → write coderifts.lock. Observed-only; empty agents when none recorded.
186
+ // ── lock — agent contract lockfile v1 (observed usage; ID847) + MCP drift --check (896) ──
187
+ // Write: GET /api/v1/lock?repo= → coderifts.lock. Check: locked MCP vs live via scoreMcpRisk.
187
188
  program
188
189
  .command('lock [repo]')
189
- .description('Fetch the observed agent-contract lockfile (coderifts.lock v1) for a repo')
190
+ .description(
191
+ 'Fetch observed agent-contract lockfile (coderifts.lock v1), or --check MCP drift vs live manifest',
192
+ )
190
193
  .option('--repo <owner/repo>', 'Repository (owner/repo); also accepted as a positional argument')
191
194
  .option('--out <path>', 'Output path (default: coderifts.lock in cwd)')
192
- .option('--json', 'Print the lock document JSON to stdout (still writes --out)')
195
+ .option('--json', 'Print JSON to stdout only (write: lock doc; check: drift report)')
196
+ .option('--check', 'MCP supply-chain monitor: compare locked vs live manifest (scoreMcpRisk)')
197
+ .option('--locked-manifest <path>', 'Baseline MCP manifest JSON (tools[]) for --check')
198
+ .option('--live-manifest <path>', 'Live MCP manifest JSON for --check')
199
+ .option('--live-url <url>', 'Fetch live MCP manifest JSON from URL for --check')
200
+ .option('--lockfile <path>', 'Read coderifts.lock for additive mcp_manifests (optional)')
193
201
  .action(async (repoPositional, options) => {
194
202
  const { runLock } = require('../src/commands/lock');
195
203
  const result = await runLock({
@@ -201,6 +209,71 @@ program
201
209
  }
202
210
  });
203
211
 
212
+ // ── adopt — counterfactual "what would CodeRifts have done" from local git (roadmap 893 Step A) ──
213
+ // Stateless core in app src/core/counterfactual-adopt.js; CLI extracts change sets via git
214
+ // (mirrors publish-gate gitImpl). Not named "shadow" (shadowApis is taken).
215
+ program
216
+ .command('adopt')
217
+ .description(
218
+ 'Counterfactual report: what CodeRifts would have done on contract-artifact changes in local git history',
219
+ )
220
+ .option('--days <n>', 'Look back N days of git history', (v) => parseInt(v, 10))
221
+ .option('--commits <n>', 'Look at the last N commits', (v) => parseInt(v, 10))
222
+ .option('--json', 'Machine-readable JSON report')
223
+ .action(async (options) => {
224
+ const { runAdopt } = require('../src/commands/adopt');
225
+ const result = await runAdopt({
226
+ days: options.days,
227
+ commits: options.commits,
228
+ json: !!options.json,
229
+ });
230
+ if (result && typeof result.exitCode === 'number') {
231
+ process.exitCode = result.exitCode;
232
+ }
233
+ });
234
+
235
+ // ── outcome — POST /api/v1/outcomes (caller assertion; does not verify the deploy) ──
236
+ program
237
+ .command('outcome <kind>')
238
+ .description(
239
+ 'Report a post-hoc outcome for a decision_id (deploy_succeeded/deploy_failed/…). '
240
+ + 'Records the caller\'s assertion — does not verify the deploy itself.',
241
+ )
242
+ .option('--decision <decision_id>', 'Decision id from decision_result.decision_id (required)')
243
+ .option('--observed-at <iso>', 'When the outcome was observed (ISO-8601; default: now)')
244
+ .option('--details <json>', 'Optional JSON object/array bound to the outcome row')
245
+ .option('--json', 'Print the API response JSON to stdout')
246
+ .addHelpText('after', () => {
247
+ const { USAGE } = require('../src/commands/outcome');
248
+ return `\n${USAGE}\n`;
249
+ })
250
+ .action(async (kind, options) => {
251
+ const { runOutcome } = require('../src/commands/outcome');
252
+ const result = await runOutcome(kind, options);
253
+ if (result && typeof result.exitCode === 'number') {
254
+ process.exitCode = result.exitCode;
255
+ }
256
+ });
257
+
258
+ // ── claude-hook — Claude Code PreToolUse (exit 2 = BLOCK; never exit 1 for deny) ──
259
+ program
260
+ .command('claude-hook')
261
+ .description(
262
+ 'Claude Code PreToolUse gate: block contract-touching Write|Edit|MultiEdit on BLOCK/STOP '
263
+ + '(exit 2). Soft-allow when unconfigured/unparseable/API down.',
264
+ )
265
+ .addHelpText('after', () => {
266
+ const { USAGE } = require('../src/commands/claude-hook');
267
+ return `\n${USAGE}\n`;
268
+ })
269
+ .action(async () => {
270
+ const { runClaudeHook } = require('../src/commands/claude-hook');
271
+ const result = await runClaudeHook();
272
+ if (result && typeof result.exitCode === 'number') {
273
+ process.exitCode = result.exitCode;
274
+ }
275
+ });
276
+
204
277
  // ── hook command group ──
205
278
  const hookCmd = program
206
279
  .command('hook')