@rigour-labs/cli 7.0.0-rc.1 → 7.0.0-rc.11
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 +11 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +23 -2
- package/dist/commands/hooks-git.js +4 -2
- package/dist/commands/hooks-push.d.ts +2 -0
- package/dist/commands/hooks-push.js +14 -2
- package/dist/commands/hooks-stop.js +9 -2
- package/dist/commands/hooks.d.ts +2 -0
- package/dist/commands/hooks.js +32 -8
- package/dist/commands/learn-reviews.d.ts +2 -0
- package/dist/commands/learn-reviews.js +22 -3
- package/dist/commands/profile.js +15 -3
- package/dist/commands/repo-setup.js +12 -10
- package/dist/commands/report-only.d.ts +13 -0
- package/dist/commands/report-only.js +45 -0
- package/dist/commands/semantic.js +13 -1
- package/dist/commands/setup.d.ts +4 -0
- package/dist/commands/setup.js +100 -14
- package/dist/commands/shell-writes.d.ts +13 -0
- package/dist/commands/shell-writes.js +65 -0
- package/dist/commands/studio-learning.d.ts +2 -0
- package/dist/commands/studio-learning.js +5 -3
- package/dist/commands/team.js +8 -2
- package/dist/commands/uninstall.js +2 -1
- package/package.json +3 -3
- package/studio-dist/assets/{index-D5EEZJUE.js → index-CmWtilgA.js} +17 -17
- package/studio-dist/index.html +1 -1
package/dist/cli.js
CHANGED
|
@@ -237,13 +237,21 @@ program
|
|
|
237
237
|
.option('--instructions', 'With --team: also write AGENTS.md, and a one-line CLAUDE.md that imports it, where the project has none')
|
|
238
238
|
.option('--no-semantic', 'Skip installing semantic search (recall and pattern matching then use keywords)')
|
|
239
239
|
.option('--no-brief', 'Do not brief the agent with the team\'s rules and lessons (on by default: from each session\'s first prompt, and on the first edit of each file)')
|
|
240
|
+
.option('--report-only', 'A team\'s first week: the edit check, the stop review and the push gate (git\'s pre-push hook too) say what they would block and block nothing. A real credential is still refused. With --team, for everyone (rigour.yml)')
|
|
241
|
+
.option('--block', 'Make the gates block again after --report-only')
|
|
240
242
|
.action(async (options) => {
|
|
243
|
+
if (options.reportOnly && options.block) {
|
|
244
|
+
console.error('Choose one: --report-only or --block.');
|
|
245
|
+
process.exit(2);
|
|
246
|
+
}
|
|
241
247
|
await setupCommand(process.cwd(), options);
|
|
242
248
|
});
|
|
243
249
|
program
|
|
244
250
|
.command('doctor')
|
|
245
251
|
.description('Check that Rigour is wired up and firing here, and that the install is healthy')
|
|
246
252
|
.option('--clean-cache', 'Remove context-cache rows Rigour no longer reads, and shrink the database when the disk has room')
|
|
253
|
+
.option('--merge-state', 'Merge what a home ending in .rigour from before 6.11.1 left over into the live state directory (a dry run; add --yes)')
|
|
254
|
+
.option('--yes', 'With --merge-state: do it')
|
|
247
255
|
.action(async (options) => {
|
|
248
256
|
await doctorCommand(options);
|
|
249
257
|
});
|
|
@@ -406,7 +414,9 @@ program
|
|
|
406
414
|
.option('--include-bots', 'With --list: also list candidates only review bots raised (hidden by default)')
|
|
407
415
|
.option('--promote <id>', 'Accept a candidate as a lesson (recorded as your decision)')
|
|
408
416
|
.option('--reject <id>', 'Reject a candidate or lesson: the judges are told this team decided against it')
|
|
409
|
-
.option('--
|
|
417
|
+
.option('--not-a-rule <id>', 'Drop a candidate that is a one-off for its pull request, not a practice either way: never served, the judges are told nothing')
|
|
418
|
+
.option('--take-back <id>', 'Take back the last decision on a lesson (promote, reject or not a rule): a candidate again; the decision stays in its history')
|
|
419
|
+
.option('--why <text>', 'Why, recorded with --promote, --reject, --not-a-rule or --take-back')
|
|
410
420
|
.option('--scope <id>', 'Set how far a lesson reaches, with --to: its file, its folder, or every change (a team standard); recorded as your decision')
|
|
411
421
|
.option('--to <reach>', 'With --scope: file, folder or repo')
|
|
412
422
|
.option('--use-wording <id>', 'Take the corrected wording a newer version suggests for a lesson you decided (the old wording is kept as evidence)')
|
|
@@ -2,6 +2,8 @@ export declare function detectInstallKind(binaryPath: string): string;
|
|
|
2
2
|
export declare function hasVersionShadowing(versions: string[]): boolean;
|
|
3
3
|
export declare function doctorCommand(options?: {
|
|
4
4
|
cleanCache?: boolean;
|
|
5
|
+
mergeState?: boolean;
|
|
6
|
+
yes?: boolean;
|
|
5
7
|
}, cwd?: string): Promise<void>;
|
|
6
8
|
/** This repository first: is each part of Rigour wired up, and did it fire this week. */
|
|
7
9
|
export declare function printRepoSetup(cwd: string): Promise<void>;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
|
-
import { cleanContextCache, deadCacheRows, loadSettings, resolveDeepOptions, getCachedModel, legacyStateNote, rigourUserDir, SidecarProvider } from '@rigour-labs/core';
|
|
5
|
+
import { applyStateMerge, cleanContextCache, deadCacheRows, loadSettings, resolveDeepOptions, getCachedModel, legacyStateNote, planStateMerge, rigourUserDir, SidecarProvider } from '@rigour-labs/core';
|
|
6
6
|
import { checkRepoSetup } from './repo-setup.js';
|
|
7
7
|
import { configMigrations } from './setup-migrations.js';
|
|
8
8
|
import { selfTestCommand, selfTestGitPushHook } from './hooks-git.js';
|
|
@@ -65,6 +65,8 @@ export async function doctorCommand(options = {}, cwd = process.cwd()) {
|
|
|
65
65
|
console.log(chalk.bold.cyan('\nRigour Doctor\n'));
|
|
66
66
|
if (options.cleanCache)
|
|
67
67
|
return cleanCache();
|
|
68
|
+
if (options.mergeState)
|
|
69
|
+
return mergeState(!!options.yes);
|
|
68
70
|
await printRepoSetup(cwd);
|
|
69
71
|
console.log(semanticStatusLine(cwd) + '\n');
|
|
70
72
|
await printPushGate(cwd);
|
|
@@ -151,7 +153,7 @@ export async function printRepoSetup(cwd) {
|
|
|
151
153
|
// Settings an older Rigour wrote that no longer mean what they say: loud, since nothing else reports them.
|
|
152
154
|
for (const migration of configMigrations(cwd)) {
|
|
153
155
|
console.log(` ${chalk.red('✘')} ${chalk.red(migration.problem)}`);
|
|
154
|
-
console.log(chalk.dim(` fix: ${migration.automatic ? 'rigour setup' : 'edit rigour.yml'}`));
|
|
156
|
+
console.log(chalk.dim(` fix: ${migration.automatic ? 'rigour setup --team' : 'edit rigour.yml'}`));
|
|
155
157
|
}
|
|
156
158
|
console.log('');
|
|
157
159
|
}
|
|
@@ -174,6 +176,25 @@ async function printPushGate(cwd) {
|
|
|
174
176
|
console.log(chalk.dim(` - ${step}`));
|
|
175
177
|
console.log(test.ok ? chalk.green(' ✓ The push gate holds under a real git push.\n') : chalk.red(' ✘ The push gate does not hold.\n'));
|
|
176
178
|
}
|
|
179
|
+
/** `--merge-state`: what an old home ending in .rigour left outside the live state directory, merged in; a dry run unless --yes. */
|
|
180
|
+
function mergeState(yes) {
|
|
181
|
+
const plan = planStateMerge();
|
|
182
|
+
if (!plan)
|
|
183
|
+
return void console.log(chalk.green('Nothing to merge: Rigour keeps its state in one directory here.\n'));
|
|
184
|
+
console.log(`Live state: ${plan.live}\nLeft over in: ${plan.outer}\n`);
|
|
185
|
+
for (const step of plan.steps) {
|
|
186
|
+
console.log(step.action === 'move'
|
|
187
|
+
? ` move ${step.rel} into the live directory`
|
|
188
|
+
: ` keep the live ${step.rel}; rename the left-over one to ${path.basename(step.to)}`);
|
|
189
|
+
}
|
|
190
|
+
if (plan.steps.some(step => path.basename(step.from) === 'rigour.db' && step.action === 'aside')) {
|
|
191
|
+
console.log(chalk.yellow('\n rigour.db is in both: the live one stays in use and the left-over one is kept, renamed, not merged row by row. Its old memory and lessons are not in the live database.'));
|
|
192
|
+
}
|
|
193
|
+
if (!yes)
|
|
194
|
+
return void console.log(chalk.dim('\nNothing changed (a dry run). To do it: rigour doctor --merge-state --yes. Nothing is deleted or overwritten.\n'));
|
|
195
|
+
applyStateMerge(plan.steps);
|
|
196
|
+
console.log(chalk.green(`\nMerged: ${plan.steps.length} entr${plan.steps.length === 1 ? 'y' : 'ies'}. Nothing was deleted.\n`));
|
|
197
|
+
}
|
|
177
198
|
async function printDatabaseHealth() {
|
|
178
199
|
const dead = await deadCacheRows().catch(() => null);
|
|
179
200
|
if (!dead)
|
|
@@ -13,6 +13,7 @@ import { execFileSync, spawnSync } from 'child_process';
|
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import os from 'os';
|
|
15
15
|
import path from 'path';
|
|
16
|
+
import { pinnedHomeEnv } from '@rigour-labs/core/profile';
|
|
16
17
|
import { pushGate } from './hooks-push.js';
|
|
17
18
|
const MARK = 'rigour hooks push --git';
|
|
18
19
|
/**
|
|
@@ -101,18 +102,19 @@ export async function selfTestGitPushHook(rigourCommand) {
|
|
|
101
102
|
git(clone, ['commit', '-qm', 'base']);
|
|
102
103
|
git(clone, ['push', '-q', '-u', 'origin', 'HEAD:main']);
|
|
103
104
|
git(clone, ['checkout', '-qb', 'feature']);
|
|
105
|
+
// The clone is in the temp folder, where another profile may match: the pushes keep this repository's home.
|
|
104
106
|
const installed = installGitPushHook(clone, rigourCommand);
|
|
105
107
|
steps.push(`hook ${installed.action} at ${installed.path}`);
|
|
106
108
|
fs.writeFileSync(path.join(clone, 'src/util.ts'), 'export const used = 1;\nexport const forgotten = 2;\n');
|
|
107
109
|
git(clone, ['commit', '-qam', 'adds an export nothing uses']);
|
|
108
|
-
const refused = spawnSync('git', ['push', 'origin', 'feature'], { cwd: clone, encoding: 'utf8', env: { ...
|
|
110
|
+
const refused = spawnSync('git', ['push', 'origin', 'feature'], { cwd: clone, encoding: 'utf8', env: { ...pinnedHomeEnv(), RIGOUR_TELEMETRY: '0' } });
|
|
109
111
|
const refusedRef = gitOutput(remote, ['rev-parse', '--verify', '-q', 'refs/heads/feature']);
|
|
110
112
|
if (refused.status === 0 || refusedRef)
|
|
111
113
|
return { ok: false, steps: [...steps, `FAIL: a push with an unused export went through (git exit ${refused.status}; remote has feature: ${!!refusedRef})\n${(refused.stderr || '').trim().slice(-600)}`] };
|
|
112
114
|
steps.push(`a push with an unused export was refused (git exit ${refused.status}); the remote has no feature branch`);
|
|
113
115
|
fs.writeFileSync(path.join(clone, 'src/util.ts'), 'export const used = 1;\n');
|
|
114
116
|
git(clone, ['commit', '-qam', 'fix']);
|
|
115
|
-
const accepted = spawnSync('git', ['push', 'origin', 'feature'], { cwd: clone, encoding: 'utf8', env: { ...
|
|
117
|
+
const accepted = spawnSync('git', ['push', 'origin', 'feature'], { cwd: clone, encoding: 'utf8', env: { ...pinnedHomeEnv(), RIGOUR_TELEMETRY: '0' } });
|
|
116
118
|
const acceptedRef = gitOutput(remote, ['rev-parse', '--verify', '-q', 'refs/heads/feature']);
|
|
117
119
|
if (accepted.status !== 0 || acceptedRef !== gitOutput(clone, ['rev-parse', 'HEAD']))
|
|
118
120
|
return { ok: false, steps: [...steps, `FAIL: the fixed push did not land (git exit ${accepted.status})\n${(accepted.stderr || '').trim().slice(-600)}`] };
|
|
@@ -27,3 +27,5 @@ export declare function pushGate(dir: string): Promise<PushGateResult>;
|
|
|
27
27
|
export declare function isPush(command: string): boolean;
|
|
28
28
|
/** The directory the push runs in: `cd <dir> && git push` or `git -C <dir> push`. */
|
|
29
29
|
export declare function pushTarget(command: string): string | undefined;
|
|
30
|
+
/** The repository a directory is in, or undefined outside one. */
|
|
31
|
+
export declare function repositoryOf(dir: string): string | undefined;
|
|
@@ -18,8 +18,15 @@ import os from 'os';
|
|
|
18
18
|
import path from 'path';
|
|
19
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
|
+
import { recordToolStart, toolCallId } from './shell-writes.js';
|
|
22
|
+
import { REPORT_ONLY_PREFIX, reportOnly } from './report-only.js';
|
|
21
23
|
export async function hooksPushCommand(stdin, fallbackCwd) {
|
|
22
24
|
const payload = parse(stdin);
|
|
25
|
+
// Every shell call: when it started, so the edit check after it finds the files it wrote (shell-writes.ts).
|
|
26
|
+
const started = repositoryOf(payload.cwd ?? fallbackCwd);
|
|
27
|
+
const call = toolCallId(payload);
|
|
28
|
+
if (started && call)
|
|
29
|
+
recordToolStart(started, call);
|
|
23
30
|
const command = String(payload.tool_input?.command ?? '');
|
|
24
31
|
if (!isPush(command))
|
|
25
32
|
return { exitCode: 0, message: '' };
|
|
@@ -62,9 +69,13 @@ export async function pushGate(dir) {
|
|
|
62
69
|
return { exitCode: 0, message: await backgroundReviewNote(repo, branch.mainRef, config) };
|
|
63
70
|
const log = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'rigour-push-')), 'gates.log');
|
|
64
71
|
fs.writeFileSync(log, failures.log.join('\n\n'));
|
|
72
|
+
const against = `${path.basename(repo)} against ${branch.mainRef.replace(/^refs\/(remotes\/|heads\/)/, '')} (full output: ${log})`;
|
|
73
|
+
// A team's first week: the push goes ahead, and the agent and the terminal are told what would have stopped it.
|
|
74
|
+
if (reportOnly(repo, config))
|
|
75
|
+
return { exitCode: 0, message: [`${REPORT_ONLY_PREFIX} the push would have been blocked: ${failures.lines.length} check(s) failed for ${against}:`, ...failures.lines].join('\n') };
|
|
65
76
|
return {
|
|
66
77
|
exitCode: 2,
|
|
67
|
-
message: [`Push blocked: ${failures.lines.length} check(s) failed for ${
|
|
78
|
+
message: [`Push blocked: ${failures.lines.length} check(s) failed for ${against}. Fix each, then push again:`, ...failures.lines].join('\n'),
|
|
68
79
|
};
|
|
69
80
|
}
|
|
70
81
|
async function gates(repo, base, mainRef, config) {
|
|
@@ -142,7 +153,8 @@ export function pushTarget(command) {
|
|
|
142
153
|
const dir = match?.slice(1).find(Boolean);
|
|
143
154
|
return dir?.replace(/^~(?=\/|$)/, os.homedir());
|
|
144
155
|
}
|
|
145
|
-
|
|
156
|
+
/** The repository a directory is in, or undefined outside one. */
|
|
157
|
+
export function repositoryOf(dir) {
|
|
146
158
|
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: dir, encoding: 'utf8' });
|
|
147
159
|
return result.status === 0 ? result.stdout.trim() : undefined;
|
|
148
160
|
}
|
|
@@ -20,6 +20,7 @@ import { execFileSync } from 'child_process';
|
|
|
20
20
|
import fs from 'fs-extra';
|
|
21
21
|
import path from 'path';
|
|
22
22
|
import yaml from 'yaml';
|
|
23
|
+
import { REPORT_ONLY_PREFIX, reportOnly } from './report-only.js';
|
|
23
24
|
import { alreadyReviewed, appendAgentEvent, appendTaskEvent, clearStopAttempts, ConfigSchema, countUsage, nextStopAttempt, recordFixLessons, recordReviewed, recordReviewOutcome, workFingerprint, sessionBaseline, STOP_MAX_ATTEMPTS, stopReview, teamMessage, untaught, recordTaught, captureHumanEdits, } from '@rigour-labs/core';
|
|
24
25
|
export async function hooksStopCommand(tool, stdin, fallbackCwd) {
|
|
25
26
|
const payload = parsePayload(stdin);
|
|
@@ -42,7 +43,13 @@ export async function hooksStopCommand(tool, stdin, fallbackCwd) {
|
|
|
42
43
|
if (attempt > STOP_MAX_ATTEMPTS)
|
|
43
44
|
return '';
|
|
44
45
|
try {
|
|
45
|
-
const
|
|
46
|
+
const loaded = config ?? await loadHookConfig(cwd);
|
|
47
|
+
const review = await stopReview(cwd, loaded, attempt, sessionBaseline(cwd, session));
|
|
48
|
+
// A team's first week: what would have held the agent is said, and the agent finishes (report-only.ts).
|
|
49
|
+
const reporting = review.block && reportOnly(cwd, loaded);
|
|
50
|
+
if (reporting)
|
|
51
|
+
process.stderr.write(`${REPORT_ONLY_PREFIX} the agent would have been held to fix:\n${review.message}\n`);
|
|
52
|
+
const decision = reporting ? { ...review, block: false } : review;
|
|
46
53
|
const nothing = decision.reviewedFiles.length === 0;
|
|
47
54
|
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
55
|
appendTaskEvent(cwd, { kind: 'stop-review', agent: tool, session, blocked: decision.block, blocking: decision.blocking, files: decision.reviewedFiles });
|
|
@@ -66,7 +73,7 @@ export async function hooksStopCommand(tool, stdin, fallbackCwd) {
|
|
|
66
73
|
recordTaught(cwd, session, fresh);
|
|
67
74
|
if (!decision.block) {
|
|
68
75
|
clearStopAttempts(cwd, session);
|
|
69
|
-
return guidance.length ? blockWith(tool, teamMessage(guidance)) : '';
|
|
76
|
+
return guidance.length && !reportOnly(cwd, loaded) ? blockWith(tool, teamMessage(guidance)) : '';
|
|
70
77
|
}
|
|
71
78
|
return blockWith(tool, guidance.length ? `${decision.message}\n\n${teamMessage(guidance)}` : decision.message);
|
|
72
79
|
}
|
package/dist/commands/hooks.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface HooksCheckOptions {
|
|
|
34
34
|
}
|
|
35
35
|
/** This CLI, pinned to its version, as a hook runs it (`npx --yes @rigour-labs/cli@6.6.6`). */
|
|
36
36
|
export declare function pinnedCliCommand(): string;
|
|
37
|
+
/** Every project file Rigour's hooks are written to, for every agent: what a team commits when it installs Rigour. */
|
|
38
|
+
export declare function projectHookPaths(): string[];
|
|
37
39
|
/**
|
|
38
40
|
* The personal install's agent hooks, once per machine (personal.ts): every agent's user-level
|
|
39
41
|
* config, each command guarded so it runs only in a repository switched on with `rigour setup`.
|
package/dist/commands/hooks.js
CHANGED
|
@@ -21,6 +21,9 @@ import { isRigourScript, mergeHooksInto, recordCreated } from './install-record.
|
|
|
21
21
|
import { agentHome, asUserLevel, installedAgents } from './personal.js';
|
|
22
22
|
import { hookStdin } from './hook-input.js';
|
|
23
23
|
import { loadHookConfig } from './hooks-stop.js';
|
|
24
|
+
import { REPORT_ONLY_PREFIX, reportOnly } from './report-only.js';
|
|
25
|
+
import { filesWrittenSince, toolCallId } from './shell-writes.js';
|
|
26
|
+
import { repositoryOf } from './hooks-push.js';
|
|
24
27
|
function getHookCliVersion() {
|
|
25
28
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
26
29
|
const packagePath = path.resolve(thisDir, '../../package.json');
|
|
@@ -159,8 +162,9 @@ function generateClaudeHooks(checker, block, dlp = true, brief = false) {
|
|
|
159
162
|
const blockFlag = block ? ' --block' : '';
|
|
160
163
|
const checkerCommand = checkerToShellCommand(checker);
|
|
161
164
|
const hooks = {
|
|
165
|
+
// Every write is checked, whichever tool made it: a shell command's writes too (shell-writes.ts).
|
|
162
166
|
PostToolUse: [{
|
|
163
|
-
matcher: "Write|Edit|MultiEdit",
|
|
167
|
+
matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash",
|
|
164
168
|
hooks: [{
|
|
165
169
|
type: "command",
|
|
166
170
|
command: `${checkerCommand} --stdin${blockFlag}`,
|
|
@@ -382,6 +386,11 @@ const GENERATORS = {
|
|
|
382
386
|
cline: generateClineHooks,
|
|
383
387
|
windsurf: generateWindsurfHooks,
|
|
384
388
|
};
|
|
389
|
+
/** Every project file Rigour's hooks are written to, for every agent: what a team commits when it installs Rigour. */
|
|
390
|
+
export function projectHookPaths() {
|
|
391
|
+
const checker = resolveCheckerCommand();
|
|
392
|
+
return [...new Set(Object.keys(GENERATORS).flatMap(tool => GENERATORS[tool](checker, true).map(file => file.path)))];
|
|
393
|
+
}
|
|
385
394
|
// ── File writing ─────────────────────────────────────────────────────
|
|
386
395
|
function printDryRun(files) {
|
|
387
396
|
console.log(chalk.cyan('\nDry run — files that would be created:\n'));
|
|
@@ -576,6 +585,8 @@ export function parseStdinFiles(input) {
|
|
|
576
585
|
if (typeof ti === 'object' && ti !== null) {
|
|
577
586
|
if (ti.file_path)
|
|
578
587
|
return [ti.file_path];
|
|
588
|
+
if (ti.notebook_path)
|
|
589
|
+
return [ti.notebook_path]; // Claude Code NotebookEdit
|
|
579
590
|
if (ti.path)
|
|
580
591
|
return [ti.path];
|
|
581
592
|
}
|
|
@@ -761,6 +772,7 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
761
772
|
let rawStdin = '';
|
|
762
773
|
let cursorMode = false;
|
|
763
774
|
let hookSession;
|
|
775
|
+
let shellWrites;
|
|
764
776
|
if (options.stdin) {
|
|
765
777
|
rawStdin = await hookStdin();
|
|
766
778
|
// Detect Cursor/IDE hook payload format
|
|
@@ -775,15 +787,24 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
775
787
|
recordSessionBaseline(cwd, session);
|
|
776
788
|
hookSession = session;
|
|
777
789
|
}
|
|
790
|
+
// A shell call: the files it wrote, found from the working tree, not the command (shell-writes.ts).
|
|
791
|
+
if (payload.tool_name === 'Bash') {
|
|
792
|
+
const repo = repositoryOf(typeof payload.cwd === 'string' ? payload.cwd : cwd);
|
|
793
|
+
const call = toolCallId(payload);
|
|
794
|
+
shellWrites = repo && call ? filesWrittenSince(repo, call) ?? [] : [];
|
|
795
|
+
}
|
|
778
796
|
}
|
|
779
797
|
catch (parseErr) {
|
|
780
798
|
// Not valid JSON — log for debugging (stderr only, stdout must stay clean)
|
|
781
799
|
process.stderr.write(`[rigour-hook-debug] stdin JSON parse failed: ${parseErr?.message?.slice(0, 100)}\n`);
|
|
782
800
|
}
|
|
783
801
|
}
|
|
784
|
-
|
|
802
|
+
// A shell call that wrote nothing: nothing to check, and nothing to say.
|
|
803
|
+
if (shellWrites && shellWrites.length === 0)
|
|
804
|
+
return;
|
|
805
|
+
const files = shellWrites ?? (options.stdin
|
|
785
806
|
? parseStdinFiles(rawStdin)
|
|
786
|
-
: (options.files ?? '').split(',').map(f => f.trim()).filter(Boolean);
|
|
807
|
+
: (options.files ?? '').split(',').map(f => f.trim()).filter(Boolean));
|
|
787
808
|
if (files.length === 0) {
|
|
788
809
|
// The old edit hook passes "$TOOL_INPUT_file_path", which the agent never sets: an empty --files. Say so on every
|
|
789
810
|
// edit, with a non-zero exit the agent shows the person, until it is migrated; it never blocks the edit.
|
|
@@ -803,6 +824,8 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
803
824
|
result: await runHookChecker({ cwd: group.root, files: group.files, timeout_ms: Number.isFinite(timeout) ? timeout : 5000, agentId }),
|
|
804
825
|
})));
|
|
805
826
|
const result = mergeHookResults(runs.map(r => r.result));
|
|
827
|
+
// A team's first week: the findings are said, and the edit goes through (report-only.ts).
|
|
828
|
+
const blocking = !!options.block && !(await Promise.all(runs.map(async (run) => reportOnly(run.root, await loadHookConfig(run.root).catch(() => undefined))))).some(Boolean);
|
|
806
829
|
const counted = { agent: cursorMode ? 'cursor' : agentId };
|
|
807
830
|
countUsage('hook_check', 1, counted);
|
|
808
831
|
for (const failure of result.failures)
|
|
@@ -822,8 +845,8 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
822
845
|
}),
|
|
823
846
|
logStudioEvent(root, {
|
|
824
847
|
type: 'hook_check', requestId, outcome, status: repoResult.status, agentId,
|
|
825
|
-
// Only a --block hook stops the edit; without it the findings are reported and the agent carries on.
|
|
826
|
-
blocked:
|
|
848
|
+
// Only a --block hook stops the edit; without it (or in report-only) the findings are reported and the agent carries on.
|
|
849
|
+
blocked: blocking && repoResult.status === 'fail',
|
|
827
850
|
files: repoFiles, summary: `${repoFiles.length} file(s), ${repoResult.failures.length} finding(s)`,
|
|
828
851
|
findings: repoResult.failures.slice(0, 10).map(f => ({ gate: f.gate, file: f.file, line: f.line, message: f.message, severity: f.severity })),
|
|
829
852
|
}),
|
|
@@ -839,7 +862,7 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
839
862
|
})
|
|
840
863
|
.join('\n');
|
|
841
864
|
process.stdout.write(JSON.stringify({
|
|
842
|
-
continue: !
|
|
865
|
+
continue: !blocking, // block mode = stop, otherwise warn
|
|
843
866
|
user_message: `⚠️ Rigour: ${result.failures.length} issue(s) found:\n${messages}`,
|
|
844
867
|
}));
|
|
845
868
|
}
|
|
@@ -858,9 +881,10 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
858
881
|
const loc = failure.line ? `:${failure.line}` : '';
|
|
859
882
|
process.stderr.write(`[rigour/${failure.gate}] ${failure.file}${loc}: ${failure.message}\n`);
|
|
860
883
|
}
|
|
861
|
-
if (
|
|
884
|
+
if (blocking)
|
|
862
885
|
process.exitCode = 2;
|
|
863
|
-
|
|
886
|
+
else if (options.block)
|
|
887
|
+
process.stderr.write(`${REPORT_ONLY_PREFIX} the edit would have been blocked for the above.\n`);
|
|
864
888
|
}
|
|
865
889
|
}
|
|
866
890
|
/** One verdict for a hook call that checked files in several repositories. */
|
|
@@ -4,6 +4,8 @@ export interface LearnReviewsOptions {
|
|
|
4
4
|
limit?: string;
|
|
5
5
|
promote?: string;
|
|
6
6
|
reject?: string;
|
|
7
|
+
notARule?: string;
|
|
8
|
+
takeBack?: string;
|
|
7
9
|
why?: string;
|
|
8
10
|
list?: boolean;
|
|
9
11
|
/** With --list: also list candidates only review bots raised (hidden by default, and counted). */
|
|
@@ -18,6 +18,10 @@ export async function learnReviewsCommand(cwd, options) {
|
|
|
18
18
|
return decide(cwd, options.promote, 'accepted', options.why);
|
|
19
19
|
if (options.reject)
|
|
20
20
|
return decide(cwd, options.reject, 'rejected', options.why);
|
|
21
|
+
if (options.notARule)
|
|
22
|
+
return decide(cwd, options.notARule, 'not-a-rule', options.why);
|
|
23
|
+
if (options.takeBack)
|
|
24
|
+
return decide(cwd, options.takeBack, 'took-back', options.why);
|
|
21
25
|
if (options.list)
|
|
22
26
|
return list(cwd, options.json, options.includeBots);
|
|
23
27
|
if (options.useWording)
|
|
@@ -81,7 +85,7 @@ function list(cwd, json, includeBots) {
|
|
|
81
85
|
console.log('No review lessons yet. Run `rigour learn-reviews`.');
|
|
82
86
|
}
|
|
83
87
|
function printLesson(l) {
|
|
84
|
-
const label = { verified: chalk.green('lesson '), candidate: chalk.yellow('candidate'), rejected: chalk.red('rejected ') };
|
|
88
|
+
const label = { verified: chalk.green('lesson '), candidate: chalk.yellow('candidate'), rejected: chalk.red('rejected '), dropped: chalk.dim('not a rule') };
|
|
85
89
|
const prs = [...new Set(l.evidence.map(e => `#${e.pr}`))].join(', ');
|
|
86
90
|
const by = chalk.dim(`${l.promotedBy ? ` [${l.promotedBy}]` : ''}${l.scope ? ` [${l.scope === 'repo' ? 'every change' : 'its folder'}]` : ''}`);
|
|
87
91
|
console.log(`${label[l.state]} ${chalk.dim(l.id)} ${l.file || '(no file)'}: ${l.text}${by} ${chalk.dim(`(${prs})`)}`);
|
|
@@ -199,17 +203,32 @@ function useWording(cwd, id) {
|
|
|
199
203
|
console.log(chalk.green(`✔ Now reads: ${lesson.text}`));
|
|
200
204
|
}
|
|
201
205
|
/** A person's decision, kept as evidence with who made it (their git email) and why. */
|
|
206
|
+
/** What each decision tells the person it did. */
|
|
207
|
+
const DECIDED = {
|
|
208
|
+
accepted: text => chalk.green(`✔ A lesson now: ${text}`),
|
|
209
|
+
rejected: text => chalk.yellow(`✔ Rejected: ${text}. The judges are told this team decided against it.`),
|
|
210
|
+
'not-a-rule': text => chalk.dim(`✔ Not a rule (a one-off for that pull request): ${text}. Never served, and the judges are told nothing.`),
|
|
211
|
+
'took-back': text => chalk.yellow(`✔ Taken back, a candidate again: ${text}. The earlier decision is kept in its history.`),
|
|
212
|
+
};
|
|
202
213
|
function decide(cwd, id, decision, why) {
|
|
203
214
|
const by = deciderOrExit(cwd);
|
|
204
215
|
if (!by)
|
|
205
216
|
return;
|
|
206
|
-
|
|
217
|
+
let lesson;
|
|
218
|
+
try {
|
|
219
|
+
lesson = decideLesson(cwd, id, decision, by, why);
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
console.error(chalk.red(e.message));
|
|
223
|
+
process.exitCode = 1;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
207
226
|
if (!lesson) {
|
|
208
227
|
console.error(chalk.red(`No lesson ${id}.`));
|
|
209
228
|
process.exitCode = 1;
|
|
210
229
|
return;
|
|
211
230
|
}
|
|
212
|
-
console.log(decision
|
|
231
|
+
console.log(DECIDED[decision](lesson.text));
|
|
213
232
|
}
|
|
214
233
|
/** The repository's rigour.yml, or the defaults when it cannot be read (no rigour.yml is the defaults already). */
|
|
215
234
|
async function teamConfig(cwd) {
|
package/dist/commands/profile.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* `rigour profile list|which|add`: one machine, many organizations (core utils/profile.ts).
|
|
3
3
|
*/
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { expandHome, profileFor, profilesPath, readProfiles, writeProfiles } from '@rigour-labs/core/profile';
|
|
7
|
+
import { homeFor, rigourUserDir } from '@rigour-labs/core';
|
|
7
8
|
export function profileListCommand() {
|
|
8
9
|
const profiles = readProfiles();
|
|
9
10
|
if (profiles.length === 0)
|
|
@@ -19,6 +20,15 @@ export function profileWhichCommand(cwd) {
|
|
|
19
20
|
console.log(profile ? `${chalk.bold(profile.name)} (matched ${cwd})` : chalk.dim('No profile matches here: Rigour uses its default home.'));
|
|
20
21
|
console.log(chalk.dim(` home: ${rigourUserDir()}`));
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* A new profile's home, as one directory: a home ending in .rigour with no state nested in it is that folder's parent,
|
|
25
|
+
* so the state is that folder itself (user-state.ts homeFor). One with state nested in it (a home from before 6.11.1)
|
|
26
|
+
* is kept as given, where the live state is.
|
|
27
|
+
*/
|
|
28
|
+
function profileHomeFor(home) {
|
|
29
|
+
const full = path.resolve(expandHome(home));
|
|
30
|
+
return path.basename(full) === '.rigour' ? homeFor(full) : home;
|
|
31
|
+
}
|
|
22
32
|
export function profileAddCommand(name, options) {
|
|
23
33
|
const list = (value) => (value ?? '').split(',').map(v => v.trim()).filter(Boolean);
|
|
24
34
|
const teamParts = [options.organization, options.team, options.actor];
|
|
@@ -27,7 +37,7 @@ export function profileAddCommand(name, options) {
|
|
|
27
37
|
const profile = {
|
|
28
38
|
name,
|
|
29
39
|
match: list(options.match),
|
|
30
|
-
...(options.home ? { home: options.home } : {}),
|
|
40
|
+
...(options.home ? { home: profileHomeFor(options.home) } : {}),
|
|
31
41
|
...(options.organization ? {
|
|
32
42
|
team: {
|
|
33
43
|
organization: options.organization, team: options.team, actor: options.actor,
|
|
@@ -41,4 +51,6 @@ export function profileAddCommand(name, options) {
|
|
|
41
51
|
throw new Error('--match needs at least one path or remote (github.com/acme/*).');
|
|
42
52
|
writeProfiles([...readProfiles().filter(p => p.name !== name), profile]);
|
|
43
53
|
console.log(chalk.green(`Profile ${name} saved to ${profilesPath()}.`));
|
|
54
|
+
if (profile.home)
|
|
55
|
+
console.log(chalk.dim(` Its state: ${path.join(homeFor(path.resolve(expandHome(profile.home))), '.rigour')}`));
|
|
44
56
|
}
|
|
@@ -32,11 +32,13 @@ export async function checkRepoSetup(cwd, now = new Date(), toolCalls, installed
|
|
|
32
32
|
{ name: 'Windsurf', config: read('.windsurf/hooks.json') + home('.codeium/windsurf/hooks.json') },
|
|
33
33
|
].filter(a => a.config);
|
|
34
34
|
const mcpConfig = read('.mcp.json') + read('.cursor/mcp.json') + home('.claude.json') + home('.cursor/mcp.json');
|
|
35
|
-
|
|
35
|
+
// The repository's own committed install: only `rigour setup --team` changes it (a plain setup leaves it as committed).
|
|
36
|
+
const setup = !personal && /rigour/i.test(agents.map(a => a.config).join('\n')) ? 'rigour setup --team' : 'rigour setup';
|
|
37
|
+
const version = versionCheck(agents.map(a => a.config).join('\n'), mcpConfig, installed, setup);
|
|
36
38
|
return [
|
|
37
39
|
configCheck(cwd, personal),
|
|
38
|
-
editCheck(agents, now, events),
|
|
39
|
-
stopCheck(agents.map(a => a.config).join('\n'), now, events),
|
|
40
|
+
editCheck(agents, now, events, setup),
|
|
41
|
+
stopCheck(agents.map(a => a.config).join('\n'), now, events, setup),
|
|
40
42
|
mcpCheck(mcpConfig, now, calls),
|
|
41
43
|
...(version ? [version] : []),
|
|
42
44
|
prCheck(cwd),
|
|
@@ -47,7 +49,7 @@ export async function checkRepoSetup(cwd, now = new Date(), toolCalls, installed
|
|
|
47
49
|
* (`@rigour-labs/cli@6.12.4`), so an upgrade reaches them only when `rigour setup` rewrites the pin.
|
|
48
50
|
* Nothing to say when nothing pins a version (a source checkout runs its own build).
|
|
49
51
|
*/
|
|
50
|
-
function versionCheck(hooks, mcp, installed) {
|
|
52
|
+
function versionCheck(hooks, mcp, installed, setup) {
|
|
51
53
|
const pins = (text, pkg) => [...new Set([...text.matchAll(new RegExp(`@rigour-labs/${pkg}@([0-9A-Za-z.-]+)`, 'g'))].map(m => m[1]))];
|
|
52
54
|
const hookPins = pins(hooks, 'cli');
|
|
53
55
|
const mcpPins = pins(mcp, 'mcp');
|
|
@@ -62,7 +64,7 @@ function versionCheck(hooks, mcp, installed) {
|
|
|
62
64
|
...mcpPins.filter(v => v !== installed).map(v => (/^\d+$/.test(v) || v === 'latest' ? `the MCP server floats on @${v}` : `the MCP server runs ${v}`)),
|
|
63
65
|
];
|
|
64
66
|
return stale.length
|
|
65
|
-
? { id: 'version', name, state: 'broken', detail: `${stale.join(', ')}; installed is ${installed}`, fix:
|
|
67
|
+
? { id: 'version', name, state: 'broken', detail: `${stale.join(', ')}; installed is ${installed}`, fix: setup }
|
|
66
68
|
: { id: 'version', name, state: 'working', detail: installed };
|
|
67
69
|
}
|
|
68
70
|
function configCheck(cwd, personal) {
|
|
@@ -73,22 +75,22 @@ function configCheck(cwd, personal) {
|
|
|
73
75
|
: { id: 'config', name: 'Project settings', state: 'missing', detail: 'No rigour.yml: Rigour uses its defaults', fix: 'rigour setup' };
|
|
74
76
|
}
|
|
75
77
|
/** One check for every agent's edit hook: events do not say which agent fired them, so counts are not split. */
|
|
76
|
-
function editCheck(agents, now, events) {
|
|
78
|
+
function editCheck(agents, now, events, setup) {
|
|
77
79
|
const wired = agents.filter(a => a.config.includes('hooks check'));
|
|
78
80
|
const name = wired.length ? `Checks ${listOf(wired.map(a => a.name))} as it writes` : 'Checks your agent as it writes';
|
|
79
81
|
if (wired.length === 0)
|
|
80
|
-
return { id: 'edit', name, state: 'missing', detail: 'No edit hook configured', fix:
|
|
82
|
+
return { id: 'edit', name, state: 'missing', detail: 'No edit hook configured', fix: setup };
|
|
81
83
|
const broken = wired.find(a => isOldEditHook(a.config));
|
|
82
84
|
if (broken) {
|
|
83
85
|
// setup rewrites Rigour's own entries where they live (the project, or the machine for a personal install).
|
|
84
|
-
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:
|
|
86
|
+
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: setup };
|
|
85
87
|
}
|
|
86
88
|
return fired('edit', name, events.filter(e => e.type === 'hook_check'), now, 'edit checks');
|
|
87
89
|
}
|
|
88
|
-
function stopCheck(config, now, events) {
|
|
90
|
+
function stopCheck(config, now, events, setup) {
|
|
89
91
|
const name = 'Stops an agent finishing with a known bug';
|
|
90
92
|
if (!config.includes('hooks stop'))
|
|
91
|
-
return { id: 'stop', name, state: 'missing', detail: 'No stop hook configured', fix:
|
|
93
|
+
return { id: 'stop', name, state: 'missing', detail: 'No stop hook configured', fix: setup };
|
|
92
94
|
return fired('stop', name, events.filter(e => e.type === 'stop_review'), now, 'finish checks');
|
|
93
95
|
}
|
|
94
96
|
function mcpCheck(mcpJson, now, calls) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Config } from '@rigour-labs/core';
|
|
2
|
+
/** What a gate says instead of blocking. */
|
|
3
|
+
export declare const REPORT_ONLY_PREFIX = "Rigour (report only; nothing was blocked, `rigour setup --block` makes it block):";
|
|
4
|
+
/**
|
|
5
|
+
* Whether this repository's gates only report. The team's rigour.yml decides when it says either way: an explicit
|
|
6
|
+
* `hooks.report_only: false` is a floor a person's switch never lowers, as with the reviewer's caps. Unset, the
|
|
7
|
+
* person's switch applies.
|
|
8
|
+
*/
|
|
9
|
+
export declare function reportOnly(cwd: string, config?: Config): boolean;
|
|
10
|
+
/** What a person's switch says where the team's rigour.yml requires blocking. */
|
|
11
|
+
export declare const TEAM_REQUIRES_BLOCKING = "Rigour: report-only is on for this clone, but the team's rigour.yml requires blocking here (hooks.report_only: false), so the gates block.";
|
|
12
|
+
/** Sets or clears the person's switch for this repository; false outside a git repository. */
|
|
13
|
+
export declare function setReportOnlyHere(cwd: string, on: boolean): boolean;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report-only: a team's first week. Every gate (the edit check, the stop review, the push gate, git's pre-push hook
|
|
3
|
+
* included) says what it would have blocked and lets the work through; `rigour setup --block` makes them block.
|
|
4
|
+
* The scan for credentials before a tool call is not one of them and keeps denying a real secret.
|
|
5
|
+
*
|
|
6
|
+
* Read at run time, so the hook commands never change between the first week and after: from rigour.yml
|
|
7
|
+
* (`hooks.report_only`, a team's choice, written by `rigour setup --team --report-only`) or from a marker in the
|
|
8
|
+
* repository's git directory (a person's, written by `rigour setup --report-only`, never in the working tree).
|
|
9
|
+
*/
|
|
10
|
+
import { spawnSync } from 'child_process';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
const MARKER = 'rigour-report-only';
|
|
14
|
+
/** What a gate says instead of blocking. */
|
|
15
|
+
export const REPORT_ONLY_PREFIX = 'Rigour (report only; nothing was blocked, `rigour setup --block` makes it block):';
|
|
16
|
+
function markerPath(cwd) {
|
|
17
|
+
const common = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { cwd, encoding: 'utf8' });
|
|
18
|
+
return common.status === 0 ? path.join(common.stdout.trim(), MARKER) : undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether this repository's gates only report. The team's rigour.yml decides when it says either way: an explicit
|
|
22
|
+
* `hooks.report_only: false` is a floor a person's switch never lowers, as with the reviewer's caps. Unset, the
|
|
23
|
+
* person's switch applies.
|
|
24
|
+
*/
|
|
25
|
+
export function reportOnly(cwd, config) {
|
|
26
|
+
const team = config?.hooks?.report_only;
|
|
27
|
+
const marker = markerPath(cwd);
|
|
28
|
+
const personal = !!marker && fs.existsSync(marker);
|
|
29
|
+
if (team === false && personal)
|
|
30
|
+
process.stderr.write(`${TEAM_REQUIRES_BLOCKING}\n`);
|
|
31
|
+
return team ?? personal;
|
|
32
|
+
}
|
|
33
|
+
/** What a person's switch says where the team's rigour.yml requires blocking. */
|
|
34
|
+
export const TEAM_REQUIRES_BLOCKING = "Rigour: report-only is on for this clone, but the team's rigour.yml requires blocking here (hooks.report_only: false), so the gates block.";
|
|
35
|
+
/** Sets or clears the person's switch for this repository; false outside a git repository. */
|
|
36
|
+
export function setReportOnlyHere(cwd, on) {
|
|
37
|
+
const marker = markerPath(cwd);
|
|
38
|
+
if (!marker)
|
|
39
|
+
return false;
|
|
40
|
+
if (on)
|
|
41
|
+
fs.writeFileSync(marker, 'Rigour reports and never blocks here (rigour setup --report-only). `rigour setup --block` makes it block.\n');
|
|
42
|
+
else
|
|
43
|
+
fs.rmSync(marker, { force: true });
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* meaning. `rigour setup` installs it unless asked not to, or the disk is too full; nothing else
|
|
5
5
|
* ever does, so a hook or an MCP call never waits on a download.
|
|
6
6
|
*/
|
|
7
|
+
import { confirm } from '@inquirer/prompts';
|
|
7
8
|
import chalk from 'chalk';
|
|
8
9
|
import { execa } from 'execa';
|
|
9
10
|
import fs from 'fs';
|
|
@@ -51,13 +52,24 @@ function freeBytes(dir) {
|
|
|
51
52
|
return undefined;
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
/** What semantic search takes on disk, as measured per platform after setup's pruning (semantic-runtime.ts). */
|
|
56
|
+
const SIZE = 'about 250 MB on Linux, 280 MB on macOS';
|
|
57
|
+
/** A person at a terminal, not a script, a pipe or CI. */
|
|
58
|
+
function interactive() {
|
|
59
|
+
return !!process.stdin.isTTY && !!process.stdout.isTTY && !process.env.CI;
|
|
60
|
+
}
|
|
54
61
|
/** The setup step, as a person reads it. */
|
|
55
62
|
export async function setupSemantic(cwd) {
|
|
56
63
|
if (locateTransformers(cwd)) {
|
|
57
64
|
console.log(chalk.green('✔ Semantic search is on (local embeddings)'));
|
|
58
65
|
return;
|
|
59
66
|
}
|
|
60
|
-
|
|
67
|
+
// A person at a terminal says yes before a download this size; a script or CI installs as before (--no-semantic skips it).
|
|
68
|
+
if (interactive() && !await confirm({ message: `Install semantic search? ${SIZE}, once for every Rigour version on this machine. It lets recall and "is there already a helper for this?" work by meaning.`, default: true })) {
|
|
69
|
+
console.log(chalk.yellow('Semantic search is off. Recall and pattern matching use keywords until `rigour setup` installs it.'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log(chalk.dim(`Installing semantic search: ${SIZE}, once for every Rigour version on this machine (skip with --no-semantic)...`));
|
|
61
73
|
const result = await ensureSemanticRuntime(cwd);
|
|
62
74
|
if (result.state === 'installed' || result.state === 'present')
|
|
63
75
|
console.log(chalk.green('✔ Semantic search is on (local embeddings)'));
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -4,5 +4,9 @@ export interface SetupOptions {
|
|
|
4
4
|
brief?: boolean;
|
|
5
5
|
team?: boolean;
|
|
6
6
|
instructions?: boolean;
|
|
7
|
+
/** A team's first week: every gate says what it would have blocked and blocks nothing (report-only.ts). */
|
|
8
|
+
reportOnly?: boolean;
|
|
9
|
+
/** Every gate blocks again (clears report-only). */
|
|
10
|
+
block?: boolean;
|
|
7
11
|
}
|
|
8
12
|
export declare function setupCommand(cwd?: string, options?: SetupOptions): Promise<void>;
|