@rigour-labs/cli 6.9.0-rc.1 → 6.9.0-rc.3
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/commands/git-identity.d.ts +1 -0
- package/dist/commands/git-identity.js +10 -0
- package/dist/commands/learn-reviews.js +2 -8
- package/dist/commands/outcomes.js +2 -0
- package/dist/commands/studio-learning.d.ts +22 -0
- package/dist/commands/studio-learning.js +40 -4
- package/dist/commands/studio.js +10 -0
- package/package.json +3 -3
- package/studio-dist/assets/{index-DmqbfpVo.js → index-DvSqJYCS.js} +34 -34
- package/studio-dist/index.html +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function personOf(cwd: string): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Who a person's decision is recorded as: their git email in the checkout, or `unknown` without one. */
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
export function personOf(cwd) {
|
|
4
|
+
try {
|
|
5
|
+
return execFileSync('git', ['config', 'user.email'], { cwd, encoding: 'utf8' }).trim() || 'unknown';
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return 'unknown'; // no git identity: recorded as unknown
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { execFileSync } from 'child_process';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import path from 'path';
|
|
13
|
+
import { personOf } from './git-identity.js';
|
|
13
14
|
import { branchBase, decideLesson, defaultExec, githubToken, learnFromReviews, lessonsPath, readLessons, ruleWriterFor, ConfigSchema } from '@rigour-labs/core';
|
|
14
15
|
import { loadConfig } from './review-config.js';
|
|
15
16
|
export async function learnReviewsCommand(cwd, options) {
|
|
@@ -57,14 +58,7 @@ function list(cwd, json) {
|
|
|
57
58
|
}
|
|
58
59
|
/** A person's decision, kept as evidence with who made it (their git email) and why. */
|
|
59
60
|
function decide(cwd, id, decision, why) {
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
by = execFileSync('git', ['config', 'user.email'], { cwd, encoding: 'utf8' }).trim() || by;
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
// no git identity: recorded as unknown
|
|
66
|
-
}
|
|
67
|
-
const lesson = decideLesson(cwd, id, decision, by, why);
|
|
61
|
+
const lesson = decideLesson(cwd, id, decision, personOf(cwd), why);
|
|
68
62
|
if (!lesson) {
|
|
69
63
|
console.error(chalk.red(`No lesson ${id}.`));
|
|
70
64
|
process.exitCode = 1;
|
|
@@ -27,6 +27,8 @@ export async function outcomesCommand(cwd, options) {
|
|
|
27
27
|
for (const o of run.outcomes)
|
|
28
28
|
console.log(outcomeLine(o));
|
|
29
29
|
console.log(chalk.dim(`${run.outcomes.length} pull request(s), ${run.read} read now, the rest settled.${run.stopped ? ` Stopped early: ${run.stopped}.` : ''}`));
|
|
30
|
+
if (run.lessons?.added)
|
|
31
|
+
console.log(`Lessons: ${run.lessons.added} new piece(s) of evidence, ${run.lessons.suggested.length} to look at in Studio (a later fix changed their lines), ${run.lessons.demoted.length} taken back.`);
|
|
30
32
|
return 0;
|
|
31
33
|
}
|
|
32
34
|
function outcomeLine(o) {
|
|
@@ -21,6 +21,23 @@ export interface LessonJourney {
|
|
|
21
21
|
stoppedInDevelopment: number | null;
|
|
22
22
|
reachedPr: number | null;
|
|
23
23
|
canDecide: boolean;
|
|
24
|
+
/** A review lesson evidence took back (review-learning/outcome-evidence.ts): why, and the pull requests that did. A person may promote it again. */
|
|
25
|
+
takenBack?: {
|
|
26
|
+
detail: string;
|
|
27
|
+
prs: number[];
|
|
28
|
+
at: string;
|
|
29
|
+
};
|
|
30
|
+
/** A later fix changed the point's own lines (review-learning/outcome-evidence.ts): evidence for a person to promote or dismiss, never a promotion on its own. */
|
|
31
|
+
suggested?: {
|
|
32
|
+
detail: string;
|
|
33
|
+
pr: number;
|
|
34
|
+
at: string;
|
|
35
|
+
};
|
|
36
|
+
/** Back to a candidate when outcomes stopped promoting: why, and the evidence that had promoted it, for a person to promote again or dismiss. */
|
|
37
|
+
reclassified?: {
|
|
38
|
+
detail: string;
|
|
39
|
+
evidence: string[];
|
|
40
|
+
};
|
|
24
41
|
}
|
|
25
42
|
export interface StudioLearning {
|
|
26
43
|
lessons: LessonJourney[];
|
|
@@ -39,6 +56,11 @@ export declare function buildLearning(input: {
|
|
|
39
56
|
events: AgentEvent[];
|
|
40
57
|
weeks?: number;
|
|
41
58
|
}): StudioLearning;
|
|
59
|
+
/** A person's decision on a review lesson from Studio, recorded as `rigour learn-reviews --promote / --reject` records it: with their git email, and final. */
|
|
60
|
+
export declare function decideReviewLesson(cwd: string, body: unknown): {
|
|
61
|
+
id: string;
|
|
62
|
+
state: string;
|
|
63
|
+
};
|
|
42
64
|
export declare function loadLearning(cwd: string, now?: Date, weeks?: number): Promise<StudioLearning>;
|
|
43
65
|
/**
|
|
44
66
|
* "Fixed before: Credential header follows redirects (semantic-bugs). The header…" → the defect,
|
|
@@ -7,7 +7,8 @@
|
|
|
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 { fixLessonPrefix, listKnowledgeLessons, readLessons } from '@rigour-labs/core';
|
|
10
|
+
import { decideLesson, fixLessonPrefix, listKnowledgeLessons, readLessons } from '@rigour-labs/core';
|
|
11
|
+
import { personOf } from './git-identity.js';
|
|
11
12
|
import { checkoutRoots, eventsAcross, storiesAcross } from './studio-checkouts.js';
|
|
12
13
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
13
14
|
const WEEKS = 4;
|
|
@@ -40,16 +41,18 @@ export function buildLearning(input) {
|
|
|
40
41
|
id: l.id,
|
|
41
42
|
text: l.text,
|
|
42
43
|
origin: 'pr',
|
|
43
|
-
|
|
44
|
+
// Where it was learned: the review points, not the outcome or decision evidence added since.
|
|
45
|
+
learnedFrom: learnedFromPoints(l.evidence.filter(e => (e.kind ?? 'point') === 'point')),
|
|
44
46
|
learnedAt: l.createdAt,
|
|
45
47
|
state: l.state,
|
|
46
48
|
scope: 'this repo',
|
|
47
49
|
told: told(l.text),
|
|
48
50
|
stoppedInDevelopment: null,
|
|
49
51
|
reachedPr: null,
|
|
50
|
-
|
|
52
|
+
...decisionFor(l),
|
|
51
53
|
})),
|
|
52
|
-
|
|
54
|
+
// Lessons back to a candidate when outcomes stopped promoting come first: a person decides each once.
|
|
55
|
+
].sort((a, b) => Number(!!b.reclassified) - Number(!!a.reclassified) || b.learnedAt.localeCompare(a.learnedAt))
|
|
53
56
|
// The same lesson learned in several places (this repo and personal lessons from others) shows once.
|
|
54
57
|
.filter((lesson, i, all) => all.findIndex(other => other.text === lesson.text) === i);
|
|
55
58
|
const count = input.weeks ?? WEEKS;
|
|
@@ -65,6 +68,39 @@ export function buildLearning(input) {
|
|
|
65
68
|
});
|
|
66
69
|
return { lessons, weeks, prRecorded };
|
|
67
70
|
}
|
|
71
|
+
function learnedFromPoints(points) {
|
|
72
|
+
const authors = [...new Set(points.map(e => e.author).filter(Boolean))];
|
|
73
|
+
return `At PR ${[...new Set(points.map(e => `#${e.pr}`))].join(', ')}${authors.length ? `, from ${authors.join(', ')}` : ''}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* What a person can decide on a review lesson, from the last of its evidence and decisions: taken back by evidence
|
|
77
|
+
* (promote it again, or drop it), back to a candidate when outcomes stopped promoting (promote it again, or dismiss),
|
|
78
|
+
* or a candidate with a later fix on its lines (promote it, or dismiss the evidence).
|
|
79
|
+
*/
|
|
80
|
+
function decisionFor(lesson) {
|
|
81
|
+
if (lesson.state !== 'candidate')
|
|
82
|
+
return { canDecide: false };
|
|
83
|
+
const last = lesson.evidence.filter(e => e.kind === 'demoted' || e.kind === 'lines' || e.kind === 'reclassified' || e.kind === 'accepted' || e.kind === 'rejected' || e.kind === 'dismissed').at(-1);
|
|
84
|
+
if (last?.kind === 'reclassified')
|
|
85
|
+
return { canDecide: true, reclassified: { detail: last.detail ?? '', evidence: lesson.evidence.filter(e => e.kind === 'outcome' || e.kind === 'lines').map(e => e.detail ?? '').filter(Boolean) } };
|
|
86
|
+
if (last?.kind === 'demoted')
|
|
87
|
+
return { canDecide: true, takenBack: { detail: last.detail ?? '', prs: lesson.evidence.filter(e => e.kind === 'against').map(e => e.pr), at: last.at ?? '' } };
|
|
88
|
+
if (last?.kind === 'lines')
|
|
89
|
+
return { canDecide: true, suggested: { detail: last.detail ?? '', pr: last.pr, at: last.at ?? '' } };
|
|
90
|
+
return { canDecide: false };
|
|
91
|
+
}
|
|
92
|
+
/** A person's decision on a review lesson from Studio, recorded as `rigour learn-reviews --promote / --reject` records it: with their git email, and final. */
|
|
93
|
+
export function decideReviewLesson(cwd, body) {
|
|
94
|
+
const { id, decision, why } = (body ?? {});
|
|
95
|
+
if (typeof id !== 'string' || !/^[0-9a-f]{12}$/.test(id))
|
|
96
|
+
throw new Error('a review lesson id (12 hex characters) is required');
|
|
97
|
+
if (decision !== 'accepted' && decision !== 'rejected' && decision !== 'dismissed')
|
|
98
|
+
throw new Error('decision is accepted, rejected or dismissed');
|
|
99
|
+
const lesson = decideLesson(cwd, id, decision, personOf(cwd), typeof why === 'string' && why.trim() ? why.trim() : 'decided in Studio');
|
|
100
|
+
if (!lesson)
|
|
101
|
+
throw new Error(`no review lesson ${id}`);
|
|
102
|
+
return { id: lesson.id, state: lesson.state };
|
|
103
|
+
}
|
|
68
104
|
export async function loadLearning(cwd, now = new Date(), weeks = WEEKS) {
|
|
69
105
|
const roots = checkoutRoots(cwd);
|
|
70
106
|
return buildLearning({ now, lessons: await listKnowledgeLessons(cwd), reviewLessons: readLessons(cwd), stories: storiesAcross(roots), events: eventsAcross(roots), weeks });
|
package/dist/commands/studio.js
CHANGED
|
@@ -306,6 +306,16 @@ async function handleApiRequest(req, res, url, ctx) {
|
|
|
306
306
|
}
|
|
307
307
|
return true;
|
|
308
308
|
}
|
|
309
|
+
if (url.pathname === '/api/review-lessons' && req.method === 'POST') {
|
|
310
|
+
try {
|
|
311
|
+
const { decideReviewLesson } = await import('./studio-learning.js');
|
|
312
|
+
sendJson(res, 200, decideReviewLesson(cwd, JSON.parse((await readBody(req)) || '{}')));
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
sendJson(res, 400, { error: e.message });
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
309
319
|
if (url.pathname === '/api/learning') {
|
|
310
320
|
try {
|
|
311
321
|
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.9.0-rc.
|
|
3
|
+
"version": "6.9.0-rc.3",
|
|
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.9.0-rc.
|
|
62
|
+
"@rigour-labs/core": "6.9.0-rc.3"
|
|
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.9.0-rc.
|
|
68
|
+
"@rigour-labs/studio": "6.9.0-rc.3"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsc && pnpm bundle-studio",
|