@rigour-labs/cli 6.13.0 → 6.14.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 +6 -3
- package/dist/commands/brief.d.ts +1 -1
- package/dist/commands/brief.js +7 -2
- package/dist/commands/check.js +3 -1
- package/dist/commands/git-identity.d.ts +2 -1
- package/dist/commands/git-identity.js +9 -9
- package/dist/commands/hooks-push.d.ts +12 -0
- package/dist/commands/hooks-push.js +17 -1
- package/dist/commands/hooks-stop.js +1 -1
- package/dist/commands/hooks.js +18 -0
- package/dist/commands/learn-reviews.js +27 -10
- package/dist/commands/review-human.js +2 -0
- package/dist/commands/studio-activity.d.ts +2 -0
- package/dist/commands/studio-activity.js +31 -7
- package/dist/commands/studio-context.js +6 -2
- package/dist/commands/studio-learning.js +5 -10
- package/dist/commands/studio-week.d.ts +5 -0
- package/dist/commands/studio-week.js +5 -1
- package/dist/commands/studio.js +1 -1
- package/dist/commands/team.js +2 -2
- package/package.json +3 -3
- package/studio-dist/assets/{index-sDp-i8Et.js → index-pu9kGWhF.js} +12 -12
- package/studio-dist/index.html +1 -1
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ import { backtestCommand, backtestInitCommand } from './commands/backtest.js';
|
|
|
17
17
|
import { threadCommand } from './commands/thread.js';
|
|
18
18
|
import { outcomesCommand } from './commands/outcomes.js';
|
|
19
19
|
import { briefCommand, hooksBriefCommand, hooksBriefFileCommand } from './commands/brief.js';
|
|
20
|
-
import { hooksPushCommand } from './commands/hooks-push.js';
|
|
20
|
+
import { hooksPushCommand, pushHookOutput } from './commands/hooks-push.js';
|
|
21
21
|
import { hooksReviewBackgroundCommand } from './commands/hooks-review-background.js';
|
|
22
22
|
import { gitPushGateCommand, selfTestCommand, selfTestGitPushHook } from './commands/hooks-git.js';
|
|
23
23
|
import { profileAddCommand, profileListCommand, profileWhichCommand } from './commands/profile.js';
|
|
@@ -617,8 +617,11 @@ hooksCmd
|
|
|
617
617
|
.action(async (options) => {
|
|
618
618
|
const stdin = await hookStdin();
|
|
619
619
|
const result = options.git ? await gitPushGateCommand(stdin, process.cwd()) : await hooksPushCommand(stdin, process.cwd());
|
|
620
|
-
|
|
621
|
-
|
|
620
|
+
const out = pushHookOutput(result, !!options.git);
|
|
621
|
+
if (out.stdout)
|
|
622
|
+
process.stdout.write(out.stdout + '\n');
|
|
623
|
+
if (out.stderr)
|
|
624
|
+
process.stderr.write(out.stderr + '\n');
|
|
622
625
|
process.exit(result.exitCode);
|
|
623
626
|
});
|
|
624
627
|
hooksCmd
|
package/dist/commands/brief.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** `rigour brief [goal]`: the briefing for the task in this checkout, as text or JSON. The goal, else the pull request's title and description, else the branch. */
|
|
2
|
-
export declare function briefCommand(
|
|
2
|
+
export declare function briefCommand(start: string, goal: string | undefined, options: {
|
|
3
3
|
files?: string;
|
|
4
4
|
json?: boolean;
|
|
5
5
|
}): Promise<number>;
|
package/dist/commands/brief.js
CHANGED
|
@@ -9,6 +9,10 @@ import path from 'path';
|
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { briefFile, briefingText, briefTask, fileBriefingText, threadsDir } from '@rigour-labs/core';
|
|
11
11
|
import { loadHookConfig } from './hooks-stop.js';
|
|
12
|
+
/** The repository's top, where its rigour.yml, rules and briefing state live; the directory itself outside a repository. */
|
|
13
|
+
function repositoryRoot(dir) {
|
|
14
|
+
return gitOut(dir, ['rev-parse', '--show-toplevel']) || dir;
|
|
15
|
+
}
|
|
12
16
|
/** Whether briefings are switched off here: the team's rigour.yml or the person's environment. */
|
|
13
17
|
async function briefingOff(cwd) {
|
|
14
18
|
if (/^(0|false|off|no)$/i.test(process.env.RIGOUR_BRIEF?.trim() ?? ''))
|
|
@@ -23,7 +27,8 @@ async function briefingOff(cwd) {
|
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
/** `rigour brief [goal]`: the briefing for the task in this checkout, as text or JSON. The goal, else the pull request's title and description, else the branch. */
|
|
26
|
-
export async function briefCommand(
|
|
30
|
+
export async function briefCommand(start, goal, options) {
|
|
31
|
+
const cwd = repositoryRoot(start);
|
|
27
32
|
const { off, max, lessons } = await briefingOff(cwd);
|
|
28
33
|
if (off) {
|
|
29
34
|
if (options.json)
|
|
@@ -52,7 +57,7 @@ export async function hooksBriefCommand(stdin, fallbackCwd) {
|
|
|
52
57
|
catch {
|
|
53
58
|
return '';
|
|
54
59
|
}
|
|
55
|
-
const cwd = payload.cwd || fallbackCwd;
|
|
60
|
+
const cwd = repositoryRoot(payload.cwd || fallbackCwd);
|
|
56
61
|
const session = payload.session_id;
|
|
57
62
|
const { off, max, lessons } = await briefingOff(cwd);
|
|
58
63
|
if (off || !session || briefedAlready(cwd, session))
|
package/dist/commands/check.js
CHANGED
|
@@ -312,7 +312,9 @@ function renderDeepOutput(report, config, options, resolvedDeepMode) {
|
|
|
312
312
|
console.log(chalk.yellow(` ☁️ Cloud provider execution. Code context may be sent to ${provider} API.`));
|
|
313
313
|
}
|
|
314
314
|
// Deep stats
|
|
315
|
-
if (stats.deep)
|
|
315
|
+
if (stats.deep?.skipped)
|
|
316
|
+
console.log(chalk.yellow(` Deep review skipped: ${stats.deep.skipped}`));
|
|
317
|
+
else if (stats.deep) {
|
|
316
318
|
const tier = stats.deep.tier === 'cloud' ? provider : stats.deep.tier;
|
|
317
319
|
const model = stats.deep.model || 'unknown';
|
|
318
320
|
const inferenceSec = stats.deep.total_ms ? (stats.deep.total_ms / 1000).toFixed(1) + 's' : '';
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
/** Who a person's decision is recorded as: their git email in the checkout, the one the team sync matches. Throws without one. */
|
|
2
|
+
export declare function decider(cwd: string): string;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { personOf } from '@rigour-labs/core';
|
|
2
|
+
/** Every decision path refuses with this: a decision recorded as nobody would never reach the team. */
|
|
3
|
+
const NO_GIT_EMAIL = 'no git email is set in this checkout (git config user.email)';
|
|
4
|
+
/** Who a person's decision is recorded as: their git email in the checkout, the one the team sync matches. Throws without one. */
|
|
5
|
+
export function decider(cwd) {
|
|
6
|
+
const by = personOf(cwd);
|
|
7
|
+
if (by === 'unknown')
|
|
8
|
+
throw new Error(NO_GIT_EMAIL);
|
|
9
|
+
return by;
|
|
10
10
|
}
|
|
@@ -5,6 +5,18 @@ export interface PushGateResult {
|
|
|
5
5
|
message: string;
|
|
6
6
|
}
|
|
7
7
|
export declare function hooksPushCommand(stdin: string, fallbackCwd: string): Promise<PushGateResult>;
|
|
8
|
+
/**
|
|
9
|
+
* Where a push gate's message goes. Claude Code feeds a block's stderr (exit 2) back to the agent but shows no one the stderr
|
|
10
|
+
* of a hook that exits 0, so a note on a push that goes ahead (a review starting, or skipped by a cap) is PreToolUse context
|
|
11
|
+
* on stdout. git's pre-push hook speaks to the person at the terminal: stderr.
|
|
12
|
+
*/
|
|
13
|
+
export declare function pushHookOutput(result: {
|
|
14
|
+
exitCode: number;
|
|
15
|
+
message: string;
|
|
16
|
+
}, git: boolean): {
|
|
17
|
+
stdout?: string;
|
|
18
|
+
stderr?: string;
|
|
19
|
+
};
|
|
8
20
|
/** The gate itself, on the repository's branch against main: shared by the agent hook and git's pre-push (hooks-git.ts). */
|
|
9
21
|
export declare function pushGate(dir: string): Promise<PushGateResult>;
|
|
10
22
|
/**
|
|
@@ -16,7 +16,7 @@ import { spawnSync } from 'child_process';
|
|
|
16
16
|
import fs from 'fs';
|
|
17
17
|
import os from 'os';
|
|
18
18
|
import path from 'path';
|
|
19
|
-
import { appendTaskEvent, branchBase, branchFailures, captureHumanEdits, diffFromGit, hookGoalDescription, itemLine, recordGoal, mergeBaseOf, resolveReviewer, reviewChange, reviewerInputs, reviewerBlocks, runReviewer, runToolchain, startBackgroundReview, } from '@rigour-labs/core';
|
|
19
|
+
import { appendTaskEvent, branchBase, branchFailures, captureHumanEdits, diffFromGit, hookGoalDescription, itemLine, recordGoal, mergeBaseOf, resolveReviewer, reviewChange, reviewerCapReached, reviewerInputs, reviewerBlocks, runReviewer, runToolchain, startBackgroundReview, } from '@rigour-labs/core';
|
|
20
20
|
import { loadHookConfig } from './hooks-stop.js';
|
|
21
21
|
export async function hooksPushCommand(stdin, fallbackCwd) {
|
|
22
22
|
const payload = parse(stdin);
|
|
@@ -28,6 +28,18 @@ export async function hooksPushCommand(stdin, fallbackCwd) {
|
|
|
28
28
|
return { exitCode: 0, message: '' };
|
|
29
29
|
return pushGate(repo);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Where a push gate's message goes. Claude Code feeds a block's stderr (exit 2) back to the agent but shows no one the stderr
|
|
33
|
+
* of a hook that exits 0, so a note on a push that goes ahead (a review starting, or skipped by a cap) is PreToolUse context
|
|
34
|
+
* on stdout. git's pre-push hook speaks to the person at the terminal: stderr.
|
|
35
|
+
*/
|
|
36
|
+
export function pushHookOutput(result, git) {
|
|
37
|
+
if (!result.message)
|
|
38
|
+
return {};
|
|
39
|
+
if (git || result.exitCode !== 0)
|
|
40
|
+
return { stderr: result.message };
|
|
41
|
+
return { stdout: JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: result.message } }) };
|
|
42
|
+
}
|
|
31
43
|
/** The gate itself, on the repository's branch against main: shared by the agent hook and git's pre-push (hooks-git.ts). */
|
|
32
44
|
export async function pushGate(dir) {
|
|
33
45
|
const repo = repositoryOf(dir);
|
|
@@ -97,6 +109,10 @@ async function backgroundReviewNote(repo, mainRef, config) {
|
|
|
97
109
|
if (!head || !branch || branch === 'HEAD')
|
|
98
110
|
return '';
|
|
99
111
|
const base = mainRef.replace(/^refs\/(remotes\/|heads\/)/, '');
|
|
112
|
+
// A reached cap is said, never hidden behind "it runs in the background": that run would only skip.
|
|
113
|
+
const capped = await reviewerCapReached(repo, config).catch(() => undefined);
|
|
114
|
+
if (capped)
|
|
115
|
+
return `Rigour: the model review of ${head.slice(0, 9)} was skipped: ${capped}.`;
|
|
100
116
|
const log = await startBackgroundReview(repo, { head, branch, base }, [process.execPath, process.argv[1], 'hooks', 'review-background', '--commit', head, '--branch', branch, '--base', base]);
|
|
101
117
|
return log ? `Rigour: the model review of ${head.slice(0, 9)} runs in the background (rigour review --status; log: ${log}). Run \`rigour review --reviewer --full\` before asking for a human review.` : '';
|
|
102
118
|
}
|
|
@@ -44,7 +44,7 @@ export async function hooksStopCommand(tool, stdin, fallbackCwd) {
|
|
|
44
44
|
try {
|
|
45
45
|
const decision = await stopReview(cwd, config ?? await loadHookConfig(cwd), attempt, sessionBaseline(cwd, session));
|
|
46
46
|
const nothing = decision.reviewedFiles.length === 0;
|
|
47
|
-
appendAgentEvent(cwd, { type: 'stop_review', tool, session, blocked: decision.block, blocking: decision.blocking, against: decision.against, ...(nothing ? { nothing_to_review: true } : {}) });
|
|
47
|
+
appendAgentEvent(cwd, { type: 'stop_review', tool, session, blocked: decision.block, blocking: decision.blocking, against: decision.against, ...(decision.held.length ? { findings: decision.held.slice(0, 20) } : {}), ...(nothing ? { nothing_to_review: true } : {}) });
|
|
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`);
|
package/dist/commands/hooks.js
CHANGED
|
@@ -20,6 +20,7 @@ import { installGitPushHook } from './hooks-git.js';
|
|
|
20
20
|
import { isRigourScript, mergeHooksInto, recordCreated } from './install-record.js';
|
|
21
21
|
import { agentHome, asUserLevel, installedAgents } from './personal.js';
|
|
22
22
|
import { hookStdin } from './hook-input.js';
|
|
23
|
+
import { loadHookConfig } from './hooks-stop.js';
|
|
23
24
|
function getHookCliVersion() {
|
|
24
25
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
const packagePath = path.resolve(thisDir, '../../package.json');
|
|
@@ -612,6 +613,16 @@ function claudeToolDecision(detections) {
|
|
|
612
613
|
const seen = detections.filter(d => d.decision !== 'allow');
|
|
613
614
|
return seen.length ? { warning: `Rigour: possible credential${seen.length === 1 ? '' : 's'} in this tool call, not blocked:\n${seen.map(line).join('\n')}` } : {};
|
|
614
615
|
}
|
|
616
|
+
/** Every string a tool call carries (a Write's content, an Edit's new_string, a command), decoded, one per line. */
|
|
617
|
+
function toolInputText(input) {
|
|
618
|
+
if (typeof input === 'string')
|
|
619
|
+
return input;
|
|
620
|
+
if (Array.isArray(input))
|
|
621
|
+
return input.map(toolInputText).filter(Boolean).join('\n');
|
|
622
|
+
if (input && typeof input === 'object')
|
|
623
|
+
return Object.values(input).map(toolInputText).filter(Boolean).join('\n');
|
|
624
|
+
return '';
|
|
625
|
+
}
|
|
615
626
|
/** Cursor's own hook event names. Claude Code sends hook_event_name too (PreToolUse, PostToolUse, Stop, ...). */
|
|
616
627
|
const CURSOR_EVENTS = new Set([
|
|
617
628
|
'beforeSubmitPrompt', 'beforeShellExecution', 'beforeMCPExecution', 'beforeReadFile', 'beforeTabFileRead',
|
|
@@ -668,6 +679,10 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
668
679
|
cursorMode = true;
|
|
669
680
|
textToScan = extractCursorPromptText(payload);
|
|
670
681
|
}
|
|
682
|
+
else if (payload && typeof payload.tool_input === 'object') {
|
|
683
|
+
// What the tool will receive, decoded: in the JSON envelope a quote is `\"`, so `password = "…"` never matched.
|
|
684
|
+
textToScan = toolInputText(payload.tool_input) || rawInput;
|
|
685
|
+
}
|
|
671
686
|
}
|
|
672
687
|
catch {
|
|
673
688
|
// Not JSON — scan raw text as-is
|
|
@@ -676,11 +691,14 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
676
691
|
process.stdout.write(JSON.stringify(cursorMode ? { continue: true } : { status: 'clean', detections: [], duration_ms: 0, scanned_length: 0 }));
|
|
677
692
|
return;
|
|
678
693
|
}
|
|
694
|
+
// The team's switch for secrets in plain assignments (gates.security.secret_assignments), as the review reads it.
|
|
695
|
+
const secretAssignments = await loadHookConfig(cwd).then(c => c.gates.security?.secret_assignments, () => undefined);
|
|
679
696
|
const result = scanInputForCredentials(textToScan, {
|
|
680
697
|
enabled: true,
|
|
681
698
|
block_on_detection: options.block ?? false,
|
|
682
699
|
cwd,
|
|
683
700
|
use_learned_feedback: true,
|
|
701
|
+
...(secretAssignments === false ? { secret_assignments: false } : {}),
|
|
684
702
|
});
|
|
685
703
|
const messages = result.detections
|
|
686
704
|
.map((d) => `[${d.type}] ${d.description} → ${d.recommendation}`)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { execFileSync } from 'child_process';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import path from 'path';
|
|
13
|
-
import {
|
|
13
|
+
import { decider } from './git-identity.js';
|
|
14
14
|
import { acceptSuggestedText, scopeLesson, branchBase, decideCompiledCheck, decideLesson, defaultExec, proposeCompiledChecks, readCompiledChecks, suspension, githubReader, learnFromReviews, lessonsPath, pendingDecision, quietBotCandidate, readLessons, ruleWriterFor, ConfigSchema } from '@rigour-labs/core';
|
|
15
15
|
import { loadConfig } from './review-config.js';
|
|
16
16
|
export async function learnReviewsCommand(cwd, options) {
|
|
@@ -84,7 +84,7 @@ function printLesson(l) {
|
|
|
84
84
|
const label = { verified: chalk.green('lesson '), candidate: chalk.yellow('candidate'), rejected: chalk.red('rejected ') };
|
|
85
85
|
const prs = [...new Set(l.evidence.map(e => `#${e.pr}`))].join(', ');
|
|
86
86
|
const by = chalk.dim(`${l.promotedBy ? ` [${l.promotedBy}]` : ''}${l.scope ? ` [${l.scope === 'repo' ? 'every change' : 'its folder'}]` : ''}`);
|
|
87
|
-
console.log(`${label[l.state]} ${chalk.dim(l.id)} ${l.file || '(
|
|
87
|
+
console.log(`${label[l.state]} ${chalk.dim(l.id)} ${l.file || '(no file)'}: ${l.text}${by} ${chalk.dim(`(${prs})`)}`);
|
|
88
88
|
const pending = pendingDecision(l);
|
|
89
89
|
if (pending)
|
|
90
90
|
console.log(chalk.yellow(` ${pendingReason(pending)}`));
|
|
@@ -134,14 +134,22 @@ function decideCheck(cwd, id, state) {
|
|
|
134
134
|
process.exitCode = 1;
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
/** Who decides, or undefined after saying why not (exit code 1): a decision always carries who made it. */
|
|
138
|
+
function deciderOrExit(cwd) {
|
|
139
|
+
try {
|
|
140
|
+
return decider(cwd);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
console.error(chalk.red(e.message));
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
137
148
|
function decideCheckOrThrow(cwd, id, state) {
|
|
138
149
|
// Who decided is committed with the check: a decision with no one to name is refused, never recorded empty.
|
|
139
|
-
const by =
|
|
140
|
-
if (by
|
|
141
|
-
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.'));
|
|
142
|
-
process.exitCode = 1;
|
|
150
|
+
const by = deciderOrExit(cwd);
|
|
151
|
+
if (!by)
|
|
143
152
|
return;
|
|
144
|
-
}
|
|
145
153
|
const check = decideCompiledCheck(cwd, id, state, by);
|
|
146
154
|
if (!check) {
|
|
147
155
|
console.error(chalk.red(`No compiled check ${id}.`));
|
|
@@ -157,9 +165,12 @@ function scope(cwd, id, to, why) {
|
|
|
157
165
|
process.exitCode = 1;
|
|
158
166
|
return;
|
|
159
167
|
}
|
|
168
|
+
const by = deciderOrExit(cwd);
|
|
169
|
+
if (!by)
|
|
170
|
+
return;
|
|
160
171
|
let lesson;
|
|
161
172
|
try {
|
|
162
|
-
lesson = scopeLesson(cwd, id, to,
|
|
173
|
+
lesson = scopeLesson(cwd, id, to, by, why);
|
|
163
174
|
}
|
|
164
175
|
catch (e) {
|
|
165
176
|
console.error(chalk.red(e.message));
|
|
@@ -176,7 +187,10 @@ function scope(cwd, id, to, why) {
|
|
|
176
187
|
}
|
|
177
188
|
/** A person takes the suggested wording of a lesson they decided; the old wording is kept as evidence. */
|
|
178
189
|
function useWording(cwd, id) {
|
|
179
|
-
const
|
|
190
|
+
const by = deciderOrExit(cwd);
|
|
191
|
+
if (!by)
|
|
192
|
+
return;
|
|
193
|
+
const lesson = acceptSuggestedText(cwd, id, by);
|
|
180
194
|
if (!lesson) {
|
|
181
195
|
console.error(chalk.red(`No lesson ${id} with a suggested wording.`));
|
|
182
196
|
process.exitCode = 1;
|
|
@@ -186,7 +200,10 @@ function useWording(cwd, id) {
|
|
|
186
200
|
}
|
|
187
201
|
/** A person's decision, kept as evidence with who made it (their git email) and why. */
|
|
188
202
|
function decide(cwd, id, decision, why) {
|
|
189
|
-
const
|
|
203
|
+
const by = deciderOrExit(cwd);
|
|
204
|
+
if (!by)
|
|
205
|
+
return;
|
|
206
|
+
const lesson = decideLesson(cwd, id, decision, by, why);
|
|
190
207
|
if (!lesson) {
|
|
191
208
|
console.error(chalk.red(`No lesson ${id}.`));
|
|
192
209
|
process.exitCode = 1;
|
|
@@ -92,6 +92,8 @@ function printQuietLines(result, notes) {
|
|
|
92
92
|
const outside = result.excludedOutsideChangedLines;
|
|
93
93
|
if (outside)
|
|
94
94
|
groups.push({ line: `Not shown: ${outside} issue${outside === 1 ? '' : 's'} on lines this change did not touch${leftOutByCheck(result.outsideChangeByCheck)}.` });
|
|
95
|
+
if (result.report?.stats?.deep?.skipped)
|
|
96
|
+
groups.push({ line: `Deep review skipped: ${result.report.stats.deep.skipped}.` });
|
|
95
97
|
if (result.baseUnknown)
|
|
96
98
|
groups.push({ line: 'Compared with no base: HEAD already holds this diff, so findings on its lines were not checked against the code before it. Pass --base to compare.' });
|
|
97
99
|
if (result.dismissed)
|
|
@@ -5,6 +5,8 @@ export interface ActivityItem {
|
|
|
5
5
|
kind: ActivityKind;
|
|
6
6
|
text: string;
|
|
7
7
|
detail?: string;
|
|
8
|
+
/** The problems a stopped item is about (check and file, as the open-findings ledger keys them): one problem stopped at an edit and again at the stop counts once. */
|
|
9
|
+
problems?: string[];
|
|
8
10
|
}
|
|
9
11
|
export declare function buildActivity(input: {
|
|
10
12
|
events: AgentEvent[];
|
|
@@ -7,6 +7,8 @@ import path from 'path';
|
|
|
7
7
|
import { readLedger } from '@rigour-labs/core';
|
|
8
8
|
import { checkoutRoots, eventsAcross, storiesAcross } from './studio-checkouts.js';
|
|
9
9
|
const LIMIT = 200;
|
|
10
|
+
/** A problem as the open-findings ledger keys it: its check and its file. */
|
|
11
|
+
const problemKey = (check, file) => `${check ?? '?'}:${file ?? '?'}`;
|
|
10
12
|
export function buildActivity(input) {
|
|
11
13
|
const items = [
|
|
12
14
|
...input.events.flatMap(e => fromEvent(e)),
|
|
@@ -44,11 +46,18 @@ export function groupSessions(items) {
|
|
|
44
46
|
}
|
|
45
47
|
return sessions.map(list => {
|
|
46
48
|
const counts = { stopped: 0, reported: 0, fixed: 0, checked: 0, reviewed: 0, taught: 0, pr: 0, reviewedFixed: 0 };
|
|
49
|
+
const stopped = new Set();
|
|
47
50
|
for (const item of list) {
|
|
51
|
+
if (item.kind === 'stopped') {
|
|
52
|
+
for (const [i, problem] of (item.problems ?? [`${item.at}#0`]).entries())
|
|
53
|
+
stopped.add(problem || `${item.at}#${i}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
48
56
|
counts[item.kind]++;
|
|
49
57
|
if (item.kind === 'reviewed' && item.text.startsWith('Fixed '))
|
|
50
58
|
counts.reviewedFixed++;
|
|
51
59
|
}
|
|
60
|
+
counts.stopped = stopped.size;
|
|
52
61
|
const highlight = (i) => IMPORTANT.includes(i.kind) || (i.kind === 'reviewed' && i.text.startsWith('Fixed '));
|
|
53
62
|
return { start: list[list.length - 1].at, end: list[0].at, counts, highlights: list.filter(highlight), rest: list.filter(i => !highlight(i)) };
|
|
54
63
|
});
|
|
@@ -65,15 +74,30 @@ function fromEvent(event) {
|
|
|
65
74
|
const found = findings.map(f => f.message).filter(Boolean).join('; ');
|
|
66
75
|
// Stopped only when the hook ran with --block and said so; an event without the flag never claims a block.
|
|
67
76
|
return [event.blocked
|
|
68
|
-
? { at, kind: 'stopped', text: `Stopped an edit to ${files}: ${found}
|
|
77
|
+
? { at, kind: 'stopped', text: `Stopped an edit to ${files}: ${found}`, problems: findings.map(f => problemKey(f.gate, f.file)) }
|
|
69
78
|
: { at, kind: 'reported', text: `Reported on an edit to ${files}: ${found}` }];
|
|
70
79
|
}
|
|
71
|
-
case 'stop_review':
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
case 'stop_review': {
|
|
81
|
+
if (!event.blocked)
|
|
82
|
+
return [{ at, kind: 'checked', text: 'The agent finished with nothing blocking' }];
|
|
83
|
+
// What held it, as the edit line names what it stopped; an older record has only the count.
|
|
84
|
+
const held = event.findings ?? [];
|
|
85
|
+
const named = (f) => `${f.file ? `${f.file}: ` : ''}${f.title}${f.detail ? ` (${f.detail})` : ''}`;
|
|
86
|
+
if (held.length === 1 && (event.blocking ?? 1) === 1)
|
|
87
|
+
return [{ at, kind: 'stopped', text: `Kept the agent working: ${named(held[0])}`, problems: [problemKey(held[0].rule, held[0].file)] }]; // written at the block: the "Fixed" story says when it was fixed
|
|
88
|
+
// An older record names no problems: each counts as its own.
|
|
89
|
+
const problems = held.length ? held.map(f => problemKey(f.rule, f.file)) : Array.from({ length: event.blocking ?? 1 }, (_, i) => `${at}#${i}`);
|
|
90
|
+
return [{ at, kind: 'stopped', text: `Kept the agent working: ${count(event.blocking ?? held.length, 'problem')} left when it tried to finish`, problems, ...(held.length ? { detail: held.map(named).join(' · ') } : {}) }];
|
|
91
|
+
}
|
|
92
|
+
case 'lessons_served': {
|
|
93
|
+
const items = event.lessons?.length ?? 0;
|
|
94
|
+
if (event.via !== 'brief')
|
|
95
|
+
return [{ at, kind: 'taught', text: `Told the agent ${count(items, 'lesson')} before it wrote`, detail: event.lessons?.join(' · ') }];
|
|
96
|
+
// A briefing: the repository's rules and the team's lessons, for the files the task touches.
|
|
97
|
+
const rules = event.rules ?? 0;
|
|
98
|
+
const told = [rules ? count(rules, 'rule') : '', items - rules ? count(items - rules, 'lesson') : ''].filter(Boolean).join(' and ');
|
|
99
|
+
return [{ at, kind: 'taught', text: `Briefed the agent before it wrote: ${told}${event.files?.length ? ` for ${event.files.join(', ')}` : ''}`, detail: event.lessons?.join(' · ') }];
|
|
100
|
+
}
|
|
77
101
|
case 'pr_catches':
|
|
78
102
|
return [{ at, kind: 'pr', text: `A branch review found ${count(event.findings?.length ?? 0, 'problem')}`, detail: event.findings?.map(f => `${f.title} (${f.file})`).join(' · ') }];
|
|
79
103
|
default:
|
|
@@ -9,6 +9,10 @@ import { checkoutRoots, eventsAcross } from './studio-checkouts.js';
|
|
|
9
9
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
10
10
|
const WEEKS = 8;
|
|
11
11
|
const SCOPE_TOOL = 'rigour_context_scope';
|
|
12
|
+
/** The lessons these events gave agents: a briefing's items count only when they are lessons, not the repository's rules. */
|
|
13
|
+
function lessonsIn(served) {
|
|
14
|
+
return served.reduce((n, e) => n + (e.lessons?.length ?? 0) - (e.rules ?? 0), 0);
|
|
15
|
+
}
|
|
12
16
|
export function buildAgentContext(input) {
|
|
13
17
|
const at = (e) => e.createdAt ?? 0;
|
|
14
18
|
const since = input.now.getTime() - WEEK_MS;
|
|
@@ -26,7 +30,7 @@ export function buildAgentContext(input) {
|
|
|
26
30
|
tokensConsidered: sum(scopes, e => e.candidateTokens),
|
|
27
31
|
tokensReturned: sum(scopes, e => e.returnedTokens),
|
|
28
32
|
recalls: recentContext.filter(e => e.toolName === 'rigour_recall').length,
|
|
29
|
-
lessonsTold: served.filter(inWeek)
|
|
33
|
+
lessonsTold: lessonsIn(served.filter(inWeek)),
|
|
30
34
|
reuse: reuse.filter(inWeek).reverse().map(e => ({ at: e.timestamp ?? '', planned: e.planned ?? '', existing: e.existing ?? '', action: e.action ?? '' })),
|
|
31
35
|
},
|
|
32
36
|
weeks: Array.from({ length: WEEKS }, (_, i) => {
|
|
@@ -40,7 +44,7 @@ export function buildAgentContext(input) {
|
|
|
40
44
|
filesReturned: sum(weekScopes, e => e.returnedFiles),
|
|
41
45
|
filesConsidered: sum(weekScopes, e => e.candidateFiles),
|
|
42
46
|
recalls: input.context.filter(e => e.toolName === 'rigour_recall' && within(at(e))).length,
|
|
43
|
-
lessonsTold: served.filter(e => within(Date.parse(e.timestamp ?? '')))
|
|
47
|
+
lessonsTold: lessonsIn(served.filter(e => within(Date.parse(e.timestamp ?? '')))),
|
|
44
48
|
reuse: reuse.filter(e => within(Date.parse(e.timestamp ?? ''))).length,
|
|
45
49
|
};
|
|
46
50
|
}),
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* recorded on another machine (CI) never reach this one.
|
|
9
9
|
*/
|
|
10
10
|
import { acceptSuggestedText, scopeLesson, decideCompiledCheck, decideLesson, fixLessonPrefix, proposeCompiledChecks, readCompiledChecks, suspension, localOutcomeMetrics, pendingDecision, quietBotCandidate, listKnowledgeLessons, readLessons } from '@rigour-labs/core';
|
|
11
|
-
import {
|
|
11
|
+
import { decider } from './git-identity.js';
|
|
12
12
|
import { checkoutRoots, eventsAcross, storiesAcross } from './studio-checkouts.js';
|
|
13
13
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
14
14
|
const WEEKS = 4;
|
|
@@ -105,7 +105,7 @@ export function decideReviewLesson(cwd, body) {
|
|
|
105
105
|
return scopeFromStudio(cwd, id, body.to, why);
|
|
106
106
|
if (decision !== 'accepted' && decision !== 'rejected' && decision !== 'dismissed')
|
|
107
107
|
throw new Error('decision is accepted, rejected, dismissed, reworded or scope');
|
|
108
|
-
const lesson = decideLesson(cwd, id, decision,
|
|
108
|
+
const lesson = decideLesson(cwd, id, decision, decider(cwd), studioWhy(why));
|
|
109
109
|
if (!lesson)
|
|
110
110
|
throw new Error(`no review lesson ${id}`);
|
|
111
111
|
return { id: lesson.id, state: lesson.state };
|
|
@@ -117,17 +117,14 @@ export function decideReviewLesson(cwd, body) {
|
|
|
117
117
|
function scopeFromStudio(cwd, id, to, why) {
|
|
118
118
|
if (to !== 'file' && to !== 'folder' && to !== 'repo')
|
|
119
119
|
throw new Error('to is file, folder or repo');
|
|
120
|
-
const
|
|
121
|
-
if (by === 'unknown')
|
|
122
|
-
throw new Error('no git email is set in this checkout (git config user.email): who decides how far a lesson reaches is recorded with it');
|
|
123
|
-
const lesson = scopeLesson(cwd, id, to, by, studioWhy(why));
|
|
120
|
+
const lesson = scopeLesson(cwd, id, to, decider(cwd), studioWhy(why));
|
|
124
121
|
if (!lesson)
|
|
125
122
|
throw new Error(`no review lesson ${id}`);
|
|
126
123
|
return { id: lesson.id, state: lesson.state };
|
|
127
124
|
}
|
|
128
125
|
/** A person takes a decided lesson's suggested wording from Studio, recorded as `--use-wording` records it. */
|
|
129
126
|
function rewordFromStudio(cwd, id) {
|
|
130
|
-
const lesson = acceptSuggestedText(cwd, id,
|
|
127
|
+
const lesson = acceptSuggestedText(cwd, id, decider(cwd));
|
|
131
128
|
if (!lesson)
|
|
132
129
|
throw new Error(`no review lesson ${id} with a suggested wording`);
|
|
133
130
|
return { id: lesson.id, state: lesson.state };
|
|
@@ -142,9 +139,7 @@ export function decideCompiledCheckFromStudio(cwd, body) {
|
|
|
142
139
|
throw new Error('a compiled check id is required');
|
|
143
140
|
if (state !== 'active' && state !== 'withdrawn')
|
|
144
141
|
throw new Error('state is active or withdrawn');
|
|
145
|
-
const by =
|
|
146
|
-
if (by === 'unknown')
|
|
147
|
-
throw new Error('no git email is set in this checkout (git config user.email): who decides a compiled check is committed with it');
|
|
142
|
+
const by = decider(cwd);
|
|
148
143
|
const check = decideCompiledCheck(cwd, id, state, by);
|
|
149
144
|
if (!check)
|
|
150
145
|
throw new Error(`no compiled check ${id}`);
|
|
@@ -30,8 +30,13 @@ export interface StudioWeek {
|
|
|
30
30
|
stage?: CatchStage;
|
|
31
31
|
}>;
|
|
32
32
|
stories: Story[];
|
|
33
|
+
/**
|
|
34
|
+
* Problems Rigour held an agent on this week during development, each counted once, at the stage that caught it first
|
|
35
|
+
* (an edit, then the stop on the same problem, is one problem caught at the edit); `fixed` of them were fixed since.
|
|
36
|
+
*/
|
|
33
37
|
stopped: {
|
|
34
38
|
total: number;
|
|
39
|
+
fixed: number;
|
|
35
40
|
byStage: Record<CatchStage, number>;
|
|
36
41
|
};
|
|
37
42
|
/** Times an agent tried to finish with a problem Rigour proved, and then fixed it. */
|
|
@@ -13,9 +13,13 @@ export function buildWeek(input) {
|
|
|
13
13
|
const from = new Date(input.now.getTime() - WEEK_MS);
|
|
14
14
|
const inWeek = (at) => Date.parse(at) >= from.getTime() && Date.parse(at) <= input.now.getTime();
|
|
15
15
|
const stories = input.stories.filter(s => DEVELOPMENT.includes(s.stage) && inWeek(s.at));
|
|
16
|
+
// Still open: caught this week and not fixed yet. A fixed one is a story; the ledger keys both by check and file.
|
|
17
|
+
const caught = input.open.filter(o => inWeek(o.openedAt) && DEVELOPMENT.includes(o.stage ?? 'edit'));
|
|
16
18
|
const byStage = { edit: 0, review: 0, stop: 0, pr: 0 };
|
|
17
19
|
for (const story of stories)
|
|
18
20
|
byStage[story.stage]++;
|
|
21
|
+
for (const open of caught)
|
|
22
|
+
byStage[open.stage ?? 'edit']++;
|
|
19
23
|
const overruled = input.dismissals.filter(d => inWeek(d.at)).length;
|
|
20
24
|
const openedThisWeek = input.open.filter(o => inWeek(o.openedAt)).length;
|
|
21
25
|
const prEvents = input.events.filter(e => e.type === 'pr_catches');
|
|
@@ -25,7 +29,7 @@ export function buildWeek(input) {
|
|
|
25
29
|
to: input.now.toISOString(),
|
|
26
30
|
needs: input.open.map(o => ({ ...o, title: o.title ?? o.rule })),
|
|
27
31
|
stories: [...stories].reverse().slice(0, MAX_STORIES),
|
|
28
|
-
stopped: { total: stories.length, byStage },
|
|
32
|
+
stopped: { total: stories.length + caught.length, fixed: stories.length, byStage },
|
|
29
33
|
agentSaidDone: byStage.stop,
|
|
30
34
|
prCatches: prEvents.length === 0 ? null : prEvents.filter(e => e.timestamp && inWeek(e.timestamp)).reduce((n, e) => n + (e.findings?.length ?? 0), 0),
|
|
31
35
|
raised: stories.length + overruled + openedThisWeek,
|
package/dist/commands/studio.js
CHANGED
|
@@ -507,7 +507,7 @@ export const studioCommand = new Command('studio')
|
|
|
507
507
|
const syncTimer = setInterval(() => {
|
|
508
508
|
void loadTeamConfiguration().then((config) => {
|
|
509
509
|
if (config)
|
|
510
|
-
return syncTeamOutbox().catch(() => undefined);
|
|
510
|
+
return syncTeamOutbox({ cwd }).catch(() => undefined);
|
|
511
511
|
return undefined;
|
|
512
512
|
});
|
|
513
513
|
}, 30_000);
|
package/dist/commands/team.js
CHANGED
|
@@ -95,10 +95,10 @@ teamCommand
|
|
|
95
95
|
});
|
|
96
96
|
teamCommand
|
|
97
97
|
.command('sync')
|
|
98
|
-
.description("Send queued lessons from the team's repositories, and receive the team's")
|
|
98
|
+
.description("Send queued lessons from the team's repositories, and your decisions on this repository's review lessons; receive the team's")
|
|
99
99
|
.option('--dry-run', 'Report pending records without sending them')
|
|
100
100
|
.action(async (options) => {
|
|
101
|
-
const result = await syncTeamOutbox({ dryRun: Boolean(options.dryRun) });
|
|
101
|
+
const result = await syncTeamOutbox({ dryRun: Boolean(options.dryRun), cwd: process.cwd() });
|
|
102
102
|
console.log(JSON.stringify(result, null, 2));
|
|
103
103
|
});
|
|
104
104
|
/** Initialize the schema, or print why it failed (with the fix) and set exit code 1. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/cli",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.14.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.14.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.14.0-rc.1"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsc && pnpm bundle-studio",
|