@rigour-labs/cli 7.0.0-rc.10 → 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.
|
@@ -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,9 +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';
|
|
21
22
|
import { REPORT_ONLY_PREFIX, reportOnly } from './report-only.js';
|
|
22
23
|
export async function hooksPushCommand(stdin, fallbackCwd) {
|
|
23
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);
|
|
24
30
|
const command = String(payload.tool_input?.command ?? '');
|
|
25
31
|
if (!isPush(command))
|
|
26
32
|
return { exitCode: 0, message: '' };
|
|
@@ -147,7 +153,8 @@ export function pushTarget(command) {
|
|
|
147
153
|
const dir = match?.slice(1).find(Boolean);
|
|
148
154
|
return dir?.replace(/^~(?=\/|$)/, os.homedir());
|
|
149
155
|
}
|
|
150
|
-
|
|
156
|
+
/** The repository a directory is in, or undefined outside one. */
|
|
157
|
+
export function repositoryOf(dir) {
|
|
151
158
|
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: dir, encoding: 'utf8' });
|
|
152
159
|
return result.status === 0 ? result.stdout.trim() : undefined;
|
|
153
160
|
}
|
package/dist/commands/hooks.js
CHANGED
|
@@ -22,6 +22,8 @@ import { agentHome, asUserLevel, installedAgents } from './personal.js';
|
|
|
22
22
|
import { hookStdin } from './hook-input.js';
|
|
23
23
|
import { loadHookConfig } from './hooks-stop.js';
|
|
24
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';
|
|
25
27
|
function getHookCliVersion() {
|
|
26
28
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
27
29
|
const packagePath = path.resolve(thisDir, '../../package.json');
|
|
@@ -160,8 +162,9 @@ function generateClaudeHooks(checker, block, dlp = true, brief = false) {
|
|
|
160
162
|
const blockFlag = block ? ' --block' : '';
|
|
161
163
|
const checkerCommand = checkerToShellCommand(checker);
|
|
162
164
|
const hooks = {
|
|
165
|
+
// Every write is checked, whichever tool made it: a shell command's writes too (shell-writes.ts).
|
|
163
166
|
PostToolUse: [{
|
|
164
|
-
matcher: "Write|Edit|MultiEdit",
|
|
167
|
+
matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash",
|
|
165
168
|
hooks: [{
|
|
166
169
|
type: "command",
|
|
167
170
|
command: `${checkerCommand} --stdin${blockFlag}`,
|
|
@@ -582,6 +585,8 @@ export function parseStdinFiles(input) {
|
|
|
582
585
|
if (typeof ti === 'object' && ti !== null) {
|
|
583
586
|
if (ti.file_path)
|
|
584
587
|
return [ti.file_path];
|
|
588
|
+
if (ti.notebook_path)
|
|
589
|
+
return [ti.notebook_path]; // Claude Code NotebookEdit
|
|
585
590
|
if (ti.path)
|
|
586
591
|
return [ti.path];
|
|
587
592
|
}
|
|
@@ -767,6 +772,7 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
767
772
|
let rawStdin = '';
|
|
768
773
|
let cursorMode = false;
|
|
769
774
|
let hookSession;
|
|
775
|
+
let shellWrites;
|
|
770
776
|
if (options.stdin) {
|
|
771
777
|
rawStdin = await hookStdin();
|
|
772
778
|
// Detect Cursor/IDE hook payload format
|
|
@@ -781,15 +787,24 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
781
787
|
recordSessionBaseline(cwd, session);
|
|
782
788
|
hookSession = session;
|
|
783
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
|
+
}
|
|
784
796
|
}
|
|
785
797
|
catch (parseErr) {
|
|
786
798
|
// Not valid JSON — log for debugging (stderr only, stdout must stay clean)
|
|
787
799
|
process.stderr.write(`[rigour-hook-debug] stdin JSON parse failed: ${parseErr?.message?.slice(0, 100)}\n`);
|
|
788
800
|
}
|
|
789
801
|
}
|
|
790
|
-
|
|
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
|
|
791
806
|
? parseStdinFiles(rawStdin)
|
|
792
|
-
: (options.files ?? '').split(',').map(f => f.trim()).filter(Boolean);
|
|
807
|
+
: (options.files ?? '').split(',').map(f => f.trim()).filter(Boolean));
|
|
793
808
|
if (files.length === 0) {
|
|
794
809
|
// The old edit hook passes "$TOOL_INPUT_file_path", which the agent never sets: an empty --files. Say so on every
|
|
795
810
|
// edit, with a non-zero exit the agent shows the person, until it is migrated; it never blocks the edit.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The call's id in the hook payload: Claude Code's tool_use_id, else the session's. */
|
|
2
|
+
export declare function toolCallId(payload: {
|
|
3
|
+
tool_use_id?: unknown;
|
|
4
|
+
session_id?: unknown;
|
|
5
|
+
conversation_id?: unknown;
|
|
6
|
+
}): string | undefined;
|
|
7
|
+
/** Before the call: when it started. */
|
|
8
|
+
export declare function recordToolStart(repo: string, id: string, now?: number): void;
|
|
9
|
+
/**
|
|
10
|
+
* After the call: the files it wrote (absolute paths), changed or new in the working tree and written since it started.
|
|
11
|
+
* Undefined when no start was recorded for it (the hook before the call did not run). The record is used once.
|
|
12
|
+
*/
|
|
13
|
+
export declare function filesWrittenSince(repo: string, id: string): string[] | undefined;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files a shell command wrote, so the edit check sees writes an agent makes through its shell (a heredoc, a
|
|
3
|
+
* redirect, `sed -i`, `gofmt -w`) as it sees an Edit. Found without reading the command: the hook before the call
|
|
4
|
+
* records when it started, and the hook after it asks git which files are changed or new and were written since then.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from 'child_process';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { repoStateDir } from '@rigour-labs/core';
|
|
10
|
+
/** A file's mtime and the call's start come from two clocks a few milliseconds apart: this much earlier still counts. */
|
|
11
|
+
const CLOCK_SLACK_MS = 50;
|
|
12
|
+
/** The call's id in the hook payload: Claude Code's tool_use_id, else the session's. */
|
|
13
|
+
export function toolCallId(payload) {
|
|
14
|
+
const id = payload.tool_use_id ?? payload.session_id ?? payload.conversation_id;
|
|
15
|
+
return typeof id === 'string' && /^[\w.:-]{1,200}$/.test(id) ? id : undefined;
|
|
16
|
+
}
|
|
17
|
+
function startFile(repo, id) {
|
|
18
|
+
return path.join(repoStateDir(repo), 'tool-calls', id);
|
|
19
|
+
}
|
|
20
|
+
/** Before the call: when it started. */
|
|
21
|
+
export function recordToolStart(repo, id, now = Date.now()) {
|
|
22
|
+
const file = startFile(repo, id);
|
|
23
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
24
|
+
fs.writeFileSync(file, String(now));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* After the call: the files it wrote (absolute paths), changed or new in the working tree and written since it started.
|
|
28
|
+
* Undefined when no start was recorded for it (the hook before the call did not run). The record is used once.
|
|
29
|
+
*/
|
|
30
|
+
export function filesWrittenSince(repo, id) {
|
|
31
|
+
const file = startFile(repo, id);
|
|
32
|
+
let start;
|
|
33
|
+
try {
|
|
34
|
+
start = Number(fs.readFileSync(file, 'utf8'));
|
|
35
|
+
fs.rmSync(file, { force: true });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const status = spawnSync('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all'], { cwd: repo, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
41
|
+
if (status.status !== 0)
|
|
42
|
+
return [];
|
|
43
|
+
const written = [];
|
|
44
|
+
const entries = status.stdout.split('\0');
|
|
45
|
+
for (let i = 0; i < entries.length; i++) {
|
|
46
|
+
const entry = entries[i];
|
|
47
|
+
if (entry.length < 4)
|
|
48
|
+
continue;
|
|
49
|
+
const code = entry.slice(0, 2);
|
|
50
|
+
if (code[0] === 'R' || code[0] === 'C')
|
|
51
|
+
i++; // a rename's old path follows; the entry holds the new one
|
|
52
|
+
if (code.includes('D'))
|
|
53
|
+
continue;
|
|
54
|
+
const full = path.join(repo, entry.slice(3));
|
|
55
|
+
try {
|
|
56
|
+
const stat = fs.statSync(full);
|
|
57
|
+
if (stat.isFile() && stat.mtimeMs >= start - CLOCK_SLACK_MS)
|
|
58
|
+
written.push(full);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// gone since git listed it
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return written;
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/cli",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.11",
|
|
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,12 +59,12 @@
|
|
|
59
59
|
"globby": "^14.0.1",
|
|
60
60
|
"ora": "^8.0.1",
|
|
61
61
|
"yaml": "^2.8.2",
|
|
62
|
-
"@rigour-labs/core": "7.0.0-rc.
|
|
62
|
+
"@rigour-labs/core": "7.0.0-rc.11"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@types/fs-extra": "^11.0.4",
|
|
66
66
|
"@types/node": "^25.0.3",
|
|
67
|
-
"@rigour-labs/studio": "7.0.0-rc.
|
|
67
|
+
"@rigour-labs/studio": "7.0.0-rc.11"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"build": "tsc && pnpm bundle-studio",
|