@rigour-labs/cli 6.13.0-rc.2 → 6.13.0-rc.4

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.
@@ -4,6 +4,7 @@ import fs from 'fs';
4
4
  import { execFileSync } from 'child_process';
5
5
  import { cleanContextCache, deadCacheRows, loadSettings, resolveDeepOptions, getCachedModel, legacyStateNote, rigourUserDir, SidecarProvider } from '@rigour-labs/core';
6
6
  import { checkRepoSetup } from './repo-setup.js';
7
+ import { configMigrations } from './setup-migrations.js';
7
8
  import { selfTestCommand, selfTestGitPushHook } from './hooks-git.js';
8
9
  import { semanticStatusLine } from './semantic.js';
9
10
  function runText(command, args) {
@@ -147,6 +148,11 @@ export async function printRepoSetup(cwd) {
147
148
  if (check.fix && check.state !== 'working')
148
149
  console.log(chalk.dim(` fix: ${check.fix}`));
149
150
  }
151
+ // Settings an older Rigour wrote that no longer mean what they say: loud, since nothing else reports them.
152
+ for (const migration of configMigrations(cwd)) {
153
+ console.log(` ${chalk.red('✘')} ${chalk.red(migration.problem)}`);
154
+ console.log(chalk.dim(` fix: ${migration.automatic ? 'rigour setup' : 'edit rigour.yml'}`));
155
+ }
150
156
  console.log('');
151
157
  }
152
158
  const GB = 1024 ** 3;
@@ -748,9 +748,15 @@ export async function hooksCheckCommand(cwd, options = {}) {
748
748
  ? parseStdinFiles(rawStdin)
749
749
  : (options.files ?? '').split(',').map(f => f.trim()).filter(Boolean);
750
750
  if (files.length === 0) {
751
- // Nothing was checked: never report pass, or a hook wired to the wrong input looks healthy.
752
- if (!cursorMode)
751
+ // The old edit hook passes "$TOOL_INPUT_file_path", which the agent never sets: an empty --files. Say so on every
752
+ // edit, with a non-zero exit the agent shows the person, until it is migrated; it never blocks the edit.
753
+ if (!options.stdin && options.files !== undefined) {
754
+ process.stderr.write('[rigour] This edit hook is the old form: it passes a variable the agent never sets, so no edit is checked. Run: rigour setup\n');
755
+ process.exitCode = 1;
756
+ }
757
+ else if (!cursorMode)
753
758
  process.stderr.write('[rigour] hooks check: no file in the hook input, nothing checked\n');
759
+ // Nothing was checked: never report pass, or a hook wired to the wrong input looks healthy.
754
760
  process.stdout.write(JSON.stringify(cursorMode ? { continue: true } : { status: 'skipped', reason: 'no files', failures: [], duration_ms: 0 }));
755
761
  return;
756
762
  }
@@ -41,7 +41,7 @@ export async function learnReviewsCommand(cwd, options) {
41
41
  });
42
42
  if (options.json)
43
43
  return void console.log(JSON.stringify(result, null, 2));
44
- console.log(chalk.green(`✔ ${options.pr ? `PR #${options.pr}` : `${result.prs} merged PR(s)`}: ${result.candidates.person + result.candidates.bot} new candidate point(s) (${result.candidates.person} under people's logins, ${result.candidates.bot} from review bots); ${result.actedOn} of ${result.comments} review comment(s) acted on before the merge (recorded, not evidence).`));
44
+ console.log(chalk.green(`✔ ${options.pr ? `PR #${options.pr}` : `${result.prs} merged PR(s)`}: ${result.candidates.person + result.candidates.bot} new candidate point(s) (${result.candidates.person} under people's logins, ${result.candidates.bot} from review bots), ${skippedCount(result.skipped)} skipped: not a request${skippedWhy(result.skipped)}; ${result.actedOn} of ${result.comments} review comment(s) acted on before the merge (recorded, not evidence).`));
45
45
  const p = result.promoted;
46
46
  console.log(` Lessons on evidence: ${result.verified} (outcome ${p.outcome}, a person's edit of agent work ${p.correction}, a person's decision ${p.person}, recurrence across authors ${p.recurrence}${p.legacy ? `, from before evidence ${p.legacy}` : ''}); ${result.heldBack} held back by counter-evidence; ${result.rejected} rejected.`);
47
47
  if (result.rules !== undefined)
@@ -58,6 +58,15 @@ async function reader(cwd, account) {
58
58
  const { token, as } = await githubReader(cwd, account, defaultExec);
59
59
  return { token, readAs: as };
60
60
  }
61
+ /** Points that asked for nothing, in all. */
62
+ function skippedCount(skipped) {
63
+ return Object.values(skipped).reduce((n, k) => n + (k ?? 0), 0);
64
+ }
65
+ /** Why they were skipped, when any were: ` (describes the change 4, review tool status 3)`. */
66
+ function skippedWhy(skipped) {
67
+ const parts = Object.entries(skipped).filter(([, n]) => n).sort((a, b) => b[1] - a[1]).map(([why, n]) => `${why} ${n}`);
68
+ return parts.length ? ` (${parts.join(', ')})` : '';
69
+ }
61
70
  function list(cwd, json, includeBots) {
62
71
  const all = readLessons(cwd);
63
72
  if (json)
@@ -8,6 +8,7 @@ import path from 'path';
8
8
  import { getContextEvents } from '@rigour-labs/core';
9
9
  import { resolveMCPServerConfig } from './init.js';
10
10
  import { agentHome, enabledHere } from './personal.js';
11
+ import { isOldEditHook } from './setup-migrations.js';
11
12
  import { checkoutRoots, eventsAcross } from './studio-checkouts.js';
12
13
  const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
13
14
  export async function checkRepoSetup(cwd, now = new Date(), toolCalls) {
@@ -50,9 +51,10 @@ function editCheck(agents, now, events) {
50
51
  const name = wired.length ? `Checks ${listOf(wired.map(a => a.name))} as it writes` : 'Checks your agent as it writes';
51
52
  if (wired.length === 0)
52
53
  return { id: 'edit', name, state: 'missing', detail: 'No edit hook configured', fix: 'rigour setup' };
53
- const broken = wired.find(a => a.config.includes('TOOL_INPUT_file_path'));
54
+ const broken = wired.find(a => isOldEditHook(a.config));
54
55
  if (broken) {
55
- return { id: 'edit', name, state: 'broken', detail: `The ${broken.name} hook reads a variable the agent never sets, so it checks nothing`, fix: 'rigour hooks init --force' };
56
+ // setup rewrites Rigour's own entries where they live (the project, or the machine for a personal install).
57
+ return { id: 'edit', name, state: 'broken', detail: `The ${broken.name} hook is the old form: it reads a variable the agent never sets, so no edit is checked`, fix: 'rigour setup' };
56
58
  }
57
59
  return fired('edit', name, events.filter(e => e.type === 'hook_check'), now, 'edit checks');
58
60
  }
@@ -1,4 +1,4 @@
1
- import type { Failure } from '@rigour-labs/core';
1
+ import { type Failure } from '@rigour-labs/core';
2
2
  type Severity = 'critical' | 'high' | 'medium' | 'low';
3
3
  export interface CiFinding {
4
4
  rule: string;
@@ -1,10 +1,12 @@
1
+ import { shownSeverity } from '@rigour-labs/core';
1
2
  const SEVERITIES = ['critical', 'high', 'medium', 'low'];
2
3
  const MAX_FINDINGS = 5;
3
4
  function safeLabel(value, max = 120) {
4
5
  return value.replace(/[^a-zA-Z0-9_./: -]/g, '_').slice(0, max);
5
6
  }
6
7
  function severityOf(failure) {
7
- return SEVERITIES.includes(failure.severity) ? failure.severity : 'medium';
8
+ const shown = shownSeverity(failure);
9
+ return SEVERITIES.includes(shown) ? shown : 'medium';
8
10
  }
9
11
  function guidance(rule) {
10
12
  if (rule === 'logic-drift')
@@ -0,0 +1,12 @@
1
+ /** The edit hook older `rigour hooks init` wrote: it passes a variable the agent never sets, so no edit is checked. */
2
+ export declare function isOldEditHook(config: string): boolean;
3
+ export interface ConfigMigration {
4
+ id: 'preset-security-block' | 'security-deprecated-default';
5
+ /** What is wrong, in a sentence a person acts on. */
6
+ problem: string;
7
+ /** True when `rigour setup` changes the file; false when only a person can decide. */
8
+ automatic: boolean;
9
+ }
10
+ export declare function configMigrations(cwd: string): ConfigMigration[];
11
+ /** Applies the automatic migrations to rigour.yml, keeping its comments and order; returns what it changed. */
12
+ export declare function applyConfigMigrations(cwd: string): string[];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * What an older Rigour left in this repository that no longer does what it says: an edit hook that checks nothing,
3
+ * and a rigour.yml written before a default changed. `rigour doctor` and `rigour setup` report them; `rigour setup`
4
+ * fixes the ones that only restore what the team had, and leaves a team's possible choice for a person to decide.
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import yaml from 'yaml';
9
+ import { TEMPLATES } from '@rigour-labs/core';
10
+ /** The edit hook older `rigour hooks init` wrote: it passes a variable the agent never sets, so no edit is checked. */
11
+ export function isOldEditHook(config) {
12
+ return config.includes('TOOL_INPUT_file_path');
13
+ }
14
+ /** The presets that block on every security finding (`gates.security.block`), read from the presets themselves. */
15
+ function blockingPresets() {
16
+ return new Set(TEMPLATES.filter(t => t.config.gates?.security?.block === true).map(t => t.name));
17
+ }
18
+ function readConfig(cwd) {
19
+ const file = path.join(cwd, 'rigour.yml');
20
+ if (!fs.existsSync(file))
21
+ return undefined;
22
+ const doc = yaml.parseDocument(fs.readFileSync(file, 'utf8'));
23
+ return doc.errors.length ? undefined : doc;
24
+ }
25
+ export function configMigrations(cwd) {
26
+ const doc = readConfig(cwd);
27
+ if (!doc)
28
+ return [];
29
+ const found = [];
30
+ const preset = doc.get('preset');
31
+ if (typeof preset === 'string' && blockingPresets().has(preset) && doc.getIn(['gates', 'security', 'block']) === undefined) {
32
+ found.push({
33
+ id: 'preset-security-block',
34
+ problem: `rigour.yml was made from the ${preset} preset, which blocks on every security finding, but it predates gates.security.block: security findings are shown as notes here, not blocks`,
35
+ automatic: true,
36
+ });
37
+ }
38
+ if (doc.getIn(['gates', 'deprecated_apis', 'block_security_deprecated']) === true) {
39
+ found.push({
40
+ id: 'security-deprecated-default',
41
+ problem: 'rigour.yml sets gates.deprecated_apis.block_security_deprecated: true, which older rigour init wrote for everyone: an API deprecated for security reasons (md5 for a cache key, shell=True with a constant) blocks here. Keep it if your team chose it; otherwise delete the line',
42
+ automatic: false,
43
+ });
44
+ }
45
+ return found;
46
+ }
47
+ /** Applies the automatic migrations to rigour.yml, keeping its comments and order; returns what it changed. */
48
+ export function applyConfigMigrations(cwd) {
49
+ const due = configMigrations(cwd).filter(m => m.automatic);
50
+ const doc = due.length ? readConfig(cwd) : undefined;
51
+ if (!doc)
52
+ return [];
53
+ const done = [];
54
+ for (const migration of due) {
55
+ if (migration.id === 'preset-security-block') {
56
+ doc.setIn(['gates', 'security', 'block'], true);
57
+ done.push(`rigour.yml: gates.security.block: true, as the ${String(doc.get('preset'))} preset sets it (security findings block again)`);
58
+ }
59
+ }
60
+ fs.writeFileSync(path.join(cwd, 'rigour.yml'), doc.toString());
61
+ return done;
62
+ }
@@ -22,6 +22,7 @@ import { hooksInitCommand, installMachineHooks, pinnedCliCommand } from './hooks
22
22
  import { initCommand, resolveMCPServerConfig } from './init.js';
23
23
  import { writeAgentInstructions } from './init-handshake.js';
24
24
  import { disableHere, enableHere, registerUserMcp } from './personal.js';
25
+ import { applyConfigMigrations } from './setup-migrations.js';
25
26
  import { setupSemantic } from './semantic.js';
26
27
  const AGENT_NAME = { claude: 'Claude Code', cursor: 'Cursor', cline: 'Cline', windsurf: 'Windsurf' };
27
28
  export async function setupCommand(cwd = process.cwd(), options = {}) {
@@ -31,6 +32,8 @@ export async function setupCommand(cwd = process.cwd(), options = {}) {
31
32
  await teamSetup(cwd, options);
32
33
  else
33
34
  await personalSetup(cwd);
35
+ for (const change of applyConfigMigrations(cwd))
36
+ console.log(chalk.green(`✔ Migrated ${change}`));
34
37
  if (options.semantic !== false)
35
38
  await setupSemantic(cwd);
36
39
  await printRepoSetup(cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "6.13.0-rc.2",
3
+ "version": "6.13.0-rc.4",
4
4
  "description": "Code review that happens while your agent writes: deterministic checks on the lines it changed, quiet unless it can prove a defect, and learning from your team's fixes and dismissals. Works with Claude Code, Cursor, Codex, Cline and Windsurf.",
5
5
  "engines": {
6
6
  "node": ">=22.13"
@@ -59,13 +59,13 @@
59
59
  "inquirer": "9.2.16",
60
60
  "ora": "^8.0.1",
61
61
  "yaml": "^2.8.2",
62
- "@rigour-labs/core": "6.13.0-rc.2"
62
+ "@rigour-labs/core": "6.13.0-rc.4"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/fs-extra": "^11.0.4",
66
66
  "@types/inquirer": "9.0.7",
67
67
  "@types/node": "^25.0.3",
68
- "@rigour-labs/studio": "6.13.0-rc.2"
68
+ "@rigour-labs/studio": "6.13.0-rc.4"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsc && pnpm bundle-studio",