@rigour-labs/cli 6.10.1 → 6.11.0-rc.1
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/dist/cli.js +3 -0
- package/dist/commands/hooks-stop.js +2 -2
- package/dist/commands/hooks.js +3 -2
- package/dist/commands/learn-reviews.d.ts +5 -0
- package/dist/commands/learn-reviews.js +58 -1
- package/dist/commands/review.js +5 -4
- package/dist/commands/studio-learning.d.ts +14 -1
- package/dist/commands/studio-learning.js +26 -2
- package/dist/commands/studio.js +11 -0
- package/package.json +3 -3
- package/studio-dist/assets/{index-D0ngbwSa.js → index-dx-aFlSN.js} +30 -30
- package/studio-dist/index.html +1 -1
package/dist/cli.js
CHANGED
|
@@ -403,6 +403,9 @@ program
|
|
|
403
403
|
.option('--promote <id>', 'Accept a candidate as a lesson (recorded as your decision)')
|
|
404
404
|
.option('--reject <id>', 'Reject a candidate or lesson: the judges are told this team decided against it')
|
|
405
405
|
.option('--why <text>', 'Why, recorded with --promote or --reject')
|
|
406
|
+
.option('--compile', 'Propose a deterministic check for each verified lesson a template fits, and list the compiled checks')
|
|
407
|
+
.option('--approve-check <id>', 'Approve a compiled check: it runs on every review (a note unless gates.compiled_lessons.block)')
|
|
408
|
+
.option('--withdraw-check <id>', 'Take a compiled check back: it stops running')
|
|
406
409
|
.option('--json', 'Output as JSON')
|
|
407
410
|
.action(async (options) => {
|
|
408
411
|
await learnReviewsCommand(process.cwd(), options);
|
|
@@ -48,9 +48,9 @@ export async function hooksStopCommand(tool, stdin, fallbackCwd) {
|
|
|
48
48
|
appendTaskEvent(cwd, { kind: 'stop-review', agent: tool, session, blocked: decision.block, blocking: decision.blocking, files: decision.reviewedFiles });
|
|
49
49
|
if (nothing)
|
|
50
50
|
process.stderr.write(`Rigour stop review: nothing to review against ${decision.against}.\n`);
|
|
51
|
-
countUsage('stop_review');
|
|
51
|
+
countUsage('stop_review', 1, { agent: tool });
|
|
52
52
|
if (decision.block)
|
|
53
|
-
countUsage(attempt > 1 ? 'stop_block_repeat' : 'stop_block');
|
|
53
|
+
countUsage(attempt > 1 ? 'stop_block_repeat' : 'stop_block', 1, { agent: tool });
|
|
54
54
|
recordReviewed(cwd, session, fingerprint);
|
|
55
55
|
const capture = recordReviewOutcome(cwd, decision.findings, decision.reviewedFiles, 'stop');
|
|
56
56
|
await recordFixLessons(cwd, capture.fixes).catch(() => undefined); // learning never blocks the agent
|
package/dist/commands/hooks.js
CHANGED
|
@@ -760,9 +760,10 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
760
760
|
result: await runHookChecker({ cwd: group.root, files: group.files, timeout_ms: Number.isFinite(timeout) ? timeout : 5000, agentId }),
|
|
761
761
|
})));
|
|
762
762
|
const result = mergeHookResults(runs.map(r => r.result));
|
|
763
|
-
|
|
763
|
+
const counted = { agent: cursorMode ? 'cursor' : agentId };
|
|
764
|
+
countUsage('hook_check', 1, counted);
|
|
764
765
|
for (const failure of result.failures)
|
|
765
|
-
countUsage(`hook_finding:${failure.gate}
|
|
766
|
+
countUsage(`hook_finding:${failure.gate}`, 1, counted);
|
|
766
767
|
await Promise.allSettled(runs.flatMap(({ root, files: repoFiles, result: repoResult }) => {
|
|
767
768
|
const requestId = randomUUID();
|
|
768
769
|
const outcome = repoResult.status === 'pass' ? 'success' : repoResult.status === 'fail' ? 'rejected' : 'error';
|
|
@@ -10,6 +10,11 @@ export interface LearnReviewsOptions {
|
|
|
10
10
|
/** Rewrite each new point as the rule behind it with the team's reviewer CLI (memory, never training). */
|
|
11
11
|
rules?: boolean;
|
|
12
12
|
pr?: string;
|
|
13
|
+
/** Propose a deterministic check for every verified lesson a template fits, and list the compiled checks. */
|
|
14
|
+
compile?: boolean;
|
|
15
|
+
/** A person's decision on a compiled check: it runs (approve) or stops (withdraw). */
|
|
16
|
+
approveCheck?: string;
|
|
17
|
+
withdrawCheck?: string;
|
|
13
18
|
}
|
|
14
19
|
export declare function learnReviewsCommand(cwd: string, options: LearnReviewsOptions): Promise<void>;
|
|
15
20
|
/** owner/name from the origin remote (https or ssh, any host alias). */
|
|
@@ -11,7 +11,7 @@ import { execFileSync } from 'child_process';
|
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import { personOf } from './git-identity.js';
|
|
14
|
-
import { branchBase, decideLesson, defaultExec, githubToken, learnFromReviews, lessonsPath, readLessons, ruleWriterFor, ConfigSchema } from '@rigour-labs/core';
|
|
14
|
+
import { branchBase, decideCompiledCheck, decideLesson, defaultExec, proposeCompiledChecks, readCompiledChecks, suspension, githubToken, learnFromReviews, lessonsPath, readLessons, ruleWriterFor, ConfigSchema } from '@rigour-labs/core';
|
|
15
15
|
import { loadConfig } from './review-config.js';
|
|
16
16
|
export async function learnReviewsCommand(cwd, options) {
|
|
17
17
|
if (options.promote)
|
|
@@ -20,6 +20,12 @@ export async function learnReviewsCommand(cwd, options) {
|
|
|
20
20
|
return decide(cwd, options.reject, 'rejected', options.why);
|
|
21
21
|
if (options.list)
|
|
22
22
|
return list(cwd, options.json);
|
|
23
|
+
if (options.compile)
|
|
24
|
+
return compile(cwd, options.json);
|
|
25
|
+
if (options.approveCheck)
|
|
26
|
+
return decideCheck(cwd, options.approveCheck, 'active');
|
|
27
|
+
if (options.withdrawCheck)
|
|
28
|
+
return decideCheck(cwd, options.withdrawCheck, 'withdrawn');
|
|
23
29
|
try {
|
|
24
30
|
const config = await teamConfig(cwd);
|
|
25
31
|
const writeRules = options.rules ? await ruleWriterFor(cwd, config, defaultExec, line => console.error(chalk.yellow(line))) : undefined;
|
|
@@ -56,6 +62,57 @@ function list(cwd, json) {
|
|
|
56
62
|
if (lessons.length === 0)
|
|
57
63
|
console.log('No review lessons yet. Run `rigour learn-reviews`.');
|
|
58
64
|
}
|
|
65
|
+
/** Proposes checks for the verified lessons a template fits, then lists every compiled check and its state. */
|
|
66
|
+
function compile(cwd, json) {
|
|
67
|
+
const proposed = proposeCompiledChecks(cwd);
|
|
68
|
+
const checks = readCompiledChecks(cwd);
|
|
69
|
+
if (json)
|
|
70
|
+
return void console.log(JSON.stringify({ proposed: proposed.map(c => c.id), checks }, null, 2));
|
|
71
|
+
const label = { proposed: chalk.yellow('proposed '), active: chalk.green('active '), withdrawn: chalk.dim('withdrawn') };
|
|
72
|
+
const lessons = new Map(readLessons(cwd).map(l => [l.id, l]));
|
|
73
|
+
for (const c of checks) {
|
|
74
|
+
const suspended = suspension(c, lessons);
|
|
75
|
+
console.log(`${suspended ? chalk.red('suspended') : label[c.state]} ${chalk.dim(c.id)} ${c.files}: ${c.kind === 'forbid' ? `no \`${c.symbol}\`` : `\`${c.symbol}\` needs \`${c.with}\``} (${c.message})`);
|
|
76
|
+
if (suspended)
|
|
77
|
+
console.log(chalk.dim(` ${suspended}`));
|
|
78
|
+
if (c.state === 'proposed' && c.backtest)
|
|
79
|
+
console.log(chalk.dim(` ${backtestLine(c.backtest)}`));
|
|
80
|
+
}
|
|
81
|
+
if (checks.length === 0)
|
|
82
|
+
console.log('No verified lesson fits a check yet: a lesson compiles when it names its file and its symbols in backticks and says never, avoid, instead of, always or must.');
|
|
83
|
+
else if (proposed.length)
|
|
84
|
+
console.log(`\n${proposed.length} new proposal(s). A proposed check runs only once a person approves it: rigour learn-reviews --approve-check <id>`);
|
|
85
|
+
}
|
|
86
|
+
/** What the history says about a proposed check: counts, and a rate only from RATE_MIN. */
|
|
87
|
+
function backtestLine(b) {
|
|
88
|
+
const share = (s) => `${s.fired} of ${s.n}${s.rate === null ? '' : ` (${Math.round(s.rate * 100)}%)`}`;
|
|
89
|
+
return `fires on ${share(b.repeating)} merged pull request(s) a review found the lesson repeating in; on ${share(b.other)} other merged change(s) to its files`;
|
|
90
|
+
}
|
|
91
|
+
function decideCheck(cwd, id, state) {
|
|
92
|
+
try {
|
|
93
|
+
decideCheckOrThrow(cwd, id, state);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error(chalk.red(error.message));
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function decideCheckOrThrow(cwd, id, state) {
|
|
101
|
+
// Who decided is committed with the check: a decision with no one to name is refused, never recorded empty.
|
|
102
|
+
const by = personOf(cwd);
|
|
103
|
+
if (by === 'unknown') {
|
|
104
|
+
console.error(chalk.red('No git email is set in this checkout (git config user.email): who approves or takes back a compiled check is committed with it.'));
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const check = decideCompiledCheck(cwd, id, state, by);
|
|
109
|
+
if (!check) {
|
|
110
|
+
console.error(chalk.red(`No compiled check ${id}.`));
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
console.log(state === 'active' ? chalk.green(`✔ Runs from now on (a note unless gates.compiled_lessons.block): ${check.message}`) : chalk.yellow(`✔ Taken back: ${check.message}`));
|
|
115
|
+
}
|
|
59
116
|
/** A person's decision, kept as evidence with who made it (their git email) and why. */
|
|
60
117
|
function decide(cwd, id, decision, why) {
|
|
61
118
|
const lesson = decideLesson(cwd, id, decision, personOf(cwd), why);
|
package/dist/commands/review.js
CHANGED
|
@@ -16,7 +16,7 @@ import { execFileSync } from 'child_process';
|
|
|
16
16
|
import fs from 'fs-extra';
|
|
17
17
|
import path from 'path';
|
|
18
18
|
import chalk from 'chalk';
|
|
19
|
-
import { buildReviewTask, costBucket, diffFromGit, durationBucket, flushDailyUsage, Logger, LogLevel, resolveDeepOptions, trackUsage, resolveSwitch, reviewChange, toReviewFinding, GitDiffError, mergeBaseOf, receiptReport, recordPrCatches, reviewerBlocks } from '@rigour-labs/core';
|
|
19
|
+
import { learningUsage, buildReviewTask, costBucket, diffFromGit, durationBucket, flushDailyUsage, Logger, LogLevel, resolveDeepOptions, trackUsage, resolveSwitch, reviewChange, toReviewFinding, GitDiffError, mergeBaseOf, receiptReport, recordPrCatches, reviewerBlocks } from '@rigour-labs/core';
|
|
20
20
|
import { goalReport } from './review-goal.js';
|
|
21
21
|
import { receiptFor } from './review-receipt.js';
|
|
22
22
|
import { whatWasChecked } from './review-checked.js';
|
|
@@ -68,7 +68,7 @@ export async function reviewCommand(cwd, options = {}) {
|
|
|
68
68
|
if (!isDeep && !receipt && !options.ci && !options.json && !options.githubSummary) {
|
|
69
69
|
hintReviewTask(cwd, diff ?? diffFromGit(cwd, source), config.gates.deep?.router);
|
|
70
70
|
}
|
|
71
|
-
await reportUsage(result, isDeep, options, Date.now() - started);
|
|
71
|
+
await reportUsage(result, isDeep, options, Date.now() - started, cwd, config);
|
|
72
72
|
process.exit(reviewer && reviewerBlocks(reviewer) ? Math.max(exitCodeFor(result), EXIT_FAIL) : exitCodeFor(result));
|
|
73
73
|
}
|
|
74
74
|
catch (error) {
|
|
@@ -250,7 +250,7 @@ function hintReviewTask(cwd, diff, router) {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
/** Anonymous usage (opt-in; TELEMETRY.md): counts and gate names only, never files or messages. */
|
|
253
|
-
async function reportUsage(result, isDeep, options, ms) {
|
|
253
|
+
async function reportUsage(result, isDeep, options, ms, cwd, config) {
|
|
254
254
|
const byGate = (findings) => findings.reduce((acc, f) => ({ ...acc, [f.id]: (acc[f.id] ?? 0) + 1 }), {});
|
|
255
255
|
const deep = result.report?.stats.deep;
|
|
256
256
|
await trackUsage('review_completed', {
|
|
@@ -267,7 +267,8 @@ async function reportUsage(result, isDeep, options, ms) {
|
|
|
267
267
|
deep_cost_bucket: costBucket(deep?.cost_usd),
|
|
268
268
|
duration: durationBucket(ms),
|
|
269
269
|
}, { version: process.env.RIGOUR_CLI_VERSION });
|
|
270
|
-
|
|
270
|
+
// Once a day, what this repository's learning loop did goes with the day's counts (TELEMETRY.md).
|
|
271
|
+
await flushDailyUsage({ daily: () => learningUsage(cwd, config) });
|
|
271
272
|
}
|
|
272
273
|
/** A deep run that did not happen overrides the changed-line verdict. */
|
|
273
274
|
function exitCodeFor(result) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* lesson's subject prefix). Counts that Rigour cannot know here are null, never 0: PR catches
|
|
8
8
|
* recorded on another machine (CI) never reach this one.
|
|
9
9
|
*/
|
|
10
|
-
import { type OutcomeMetrics, type AgentEvent, type LessonRecord, type ReviewLesson, type Story } from '@rigour-labs/core';
|
|
10
|
+
import { type CompiledCheck, type OutcomeMetrics, type AgentEvent, type LessonRecord, type ReviewLesson, type Story } from '@rigour-labs/core';
|
|
11
11
|
export interface LessonJourney {
|
|
12
12
|
id: string;
|
|
13
13
|
text: string;
|
|
@@ -49,6 +49,10 @@ export interface StudioLearning {
|
|
|
49
49
|
prRecorded: boolean;
|
|
50
50
|
/** What happened after merges this checkout read (core outcomes/metrics.ts); absent before `rigour outcomes` has run. */
|
|
51
51
|
outcomes?: OutcomeMetrics;
|
|
52
|
+
/** Lessons compiled into checks (core review-learning/compiled-lessons.ts), for a person to approve or take back; `suspended` says why one approved no longer runs. */
|
|
53
|
+
compiled?: Array<CompiledCheck & {
|
|
54
|
+
suspended?: string;
|
|
55
|
+
}>;
|
|
52
56
|
}
|
|
53
57
|
export declare function buildLearning(input: {
|
|
54
58
|
now: Date;
|
|
@@ -63,6 +67,15 @@ export declare function decideReviewLesson(cwd: string, body: unknown): {
|
|
|
63
67
|
id: string;
|
|
64
68
|
state: string;
|
|
65
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* A person's decision on a compiled check from Studio, as `rigour learn-reviews --approve-check / --withdraw-check`
|
|
72
|
+
* records it: with their git email, committed with the check. With no git email set, it is refused.
|
|
73
|
+
*/
|
|
74
|
+
export declare function decideCompiledCheckFromStudio(cwd: string, body: unknown): CompiledCheck;
|
|
75
|
+
/** Proposes checks for the verified lessons a template fits, each backtested on the main branch's history. */
|
|
76
|
+
export declare function proposeChecksFromStudio(cwd: string): {
|
|
77
|
+
proposed: number;
|
|
78
|
+
};
|
|
66
79
|
export declare function loadLearning(cwd: string, now?: Date, weeks?: number): Promise<StudioLearning>;
|
|
67
80
|
/**
|
|
68
81
|
* "Fixed before: Credential header follows redirects (semantic-bugs). The header…" → the defect,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* lesson's subject prefix). Counts that Rigour cannot know here are null, never 0: PR catches
|
|
8
8
|
* recorded on another machine (CI) never reach this one.
|
|
9
9
|
*/
|
|
10
|
-
import { decideLesson, fixLessonPrefix, localOutcomeMetrics, pendingDecision, listKnowledgeLessons, readLessons } from '@rigour-labs/core';
|
|
10
|
+
import { decideCompiledCheck, decideLesson, fixLessonPrefix, proposeCompiledChecks, readCompiledChecks, suspension, localOutcomeMetrics, pendingDecision, listKnowledgeLessons, readLessons } from '@rigour-labs/core';
|
|
11
11
|
import { personOf } from './git-identity.js';
|
|
12
12
|
import { checkoutRoots, eventsAcross, storiesAcross } from './studio-checkouts.js';
|
|
13
13
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -99,11 +99,35 @@ export function decideReviewLesson(cwd, body) {
|
|
|
99
99
|
throw new Error(`no review lesson ${id}`);
|
|
100
100
|
return { id: lesson.id, state: lesson.state };
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* A person's decision on a compiled check from Studio, as `rigour learn-reviews --approve-check / --withdraw-check`
|
|
104
|
+
* records it: with their git email, committed with the check. With no git email set, it is refused.
|
|
105
|
+
*/
|
|
106
|
+
export function decideCompiledCheckFromStudio(cwd, body) {
|
|
107
|
+
const { id, state } = (body ?? {});
|
|
108
|
+
if (typeof id !== 'string' || !/^c-[\w-]+$/.test(id))
|
|
109
|
+
throw new Error('a compiled check id is required');
|
|
110
|
+
if (state !== 'active' && state !== 'withdrawn')
|
|
111
|
+
throw new Error('state is active or withdrawn');
|
|
112
|
+
const by = personOf(cwd);
|
|
113
|
+
if (by === 'unknown')
|
|
114
|
+
throw new Error('no git email is set in this checkout (git config user.email): who decides a compiled check is committed with it');
|
|
115
|
+
const check = decideCompiledCheck(cwd, id, state, by);
|
|
116
|
+
if (!check)
|
|
117
|
+
throw new Error(`no compiled check ${id}`);
|
|
118
|
+
return check;
|
|
119
|
+
}
|
|
120
|
+
/** Proposes checks for the verified lessons a template fits, each backtested on the main branch's history. */
|
|
121
|
+
export function proposeChecksFromStudio(cwd) {
|
|
122
|
+
return { proposed: proposeCompiledChecks(cwd).length };
|
|
123
|
+
}
|
|
102
124
|
export async function loadLearning(cwd, now = new Date(), weeks = WEEKS) {
|
|
103
125
|
const roots = checkoutRoots(cwd);
|
|
104
126
|
const learning = buildLearning({ now, lessons: await listKnowledgeLessons(cwd), reviewLessons: readLessons(cwd), stories: storiesAcross(roots), events: eventsAcross(roots), weeks });
|
|
105
127
|
const outcomes = localOutcomeMetrics(cwd);
|
|
106
|
-
|
|
128
|
+
const lessons = new Map(readLessons(cwd).map(l => [l.id, l]));
|
|
129
|
+
const withChecks = { ...learning, compiled: readCompiledChecks(cwd).map(c => ({ ...c, ...(suspension(c, lessons) ? { suspended: suspension(c, lessons) } : {}) })) };
|
|
130
|
+
return outcomes ? { ...withChecks, outcomes } : withChecks;
|
|
107
131
|
}
|
|
108
132
|
/**
|
|
109
133
|
* "Fixed before: Credential header follows redirects (semantic-bugs). The header…" → the defect,
|
package/dist/commands/studio.js
CHANGED
|
@@ -323,6 +323,17 @@ async function handleApiRequest(req, res, url, ctx) {
|
|
|
323
323
|
}
|
|
324
324
|
return true;
|
|
325
325
|
}
|
|
326
|
+
if (url.pathname === '/api/compiled-checks' && req.method === 'POST') {
|
|
327
|
+
try {
|
|
328
|
+
const { decideCompiledCheckFromStudio, proposeChecksFromStudio } = await import('./studio-learning.js');
|
|
329
|
+
const body = JSON.parse((await readBody(req)) || '{}');
|
|
330
|
+
sendJson(res, 200, body?.propose === true ? proposeChecksFromStudio(cwd) : decideCompiledCheckFromStudio(cwd, body));
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
sendJson(res, 400, { error: e.message });
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
326
337
|
if (url.pathname === '/api/learning') {
|
|
327
338
|
try {
|
|
328
339
|
const { loadLearning } = await import('./studio-learning.js');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/cli",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.11.0-rc.1",
|
|
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.
|
|
62
|
+
"@rigour-labs/core": "6.11.0-rc.1"
|
|
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.
|
|
68
|
+
"@rigour-labs/studio": "6.11.0-rc.1"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsc && pnpm bundle-studio",
|