@kavishdua/proctor 1.3.0 → 1.4.0
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/README.md +25 -2
- package/dist/cli.js +208 -83
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,6 +80,8 @@ The check writes nothing to the repository being checked and makes no runtime ne
|
|
|
80
80
|
## Install
|
|
81
81
|
|
|
82
82
|
Agent plugins install the ruleset. For CLI checks and hooks, use the local npm installation below.
|
|
83
|
+
`proctor setup` adds turn-end enforcement for Claude Code and Codex CLI. The Cursor plugin
|
|
84
|
+
also includes a stop hook.
|
|
83
85
|
|
|
84
86
|
**Claude Code**
|
|
85
87
|
|
|
@@ -121,9 +123,10 @@ which may be outside the working tree when you use worktrees or `core.hooksPath`
|
|
|
121
123
|
| The ruleset file for each agent this repo uses, e.g. `.claude/skills/proctor/SKILL.md`, `.cursor/rules/proctor.mdc`, `AGENTS.md` | The honest-completion rules your agent reads before it works | A shared file like `AGENTS.md` keeps your content; only proctor's delimited block is replaced |
|
|
122
124
|
| Git's hook directory (`.git/hooks/pre-commit` normally), or `.husky/pre-commit` when Husky is a dev dependency | Blocks a cheat at commit time; the Husky file is also staged | A foreign hook is backed up to `pre-commit.bak`, then replaced; merge it manually if needed |
|
|
123
125
|
| `.claude/settings.json` | The Claude Code Stop hook, written only if this repo uses Claude Code | Merged into the existing JSON; nothing else in the file changes |
|
|
126
|
+
| `.codex/hooks.json` | The Codex CLI Stop hook, written only if Codex is a selected or detected agent | Merged into the existing JSON, preserving other hooks and top-level fields |
|
|
124
127
|
| `.proctor-adapter-manifest.json` | Records what was written, so `uninstall` can take exactly that back out | Rewritten |
|
|
125
128
|
|
|
126
|
-
proctor detects agents from their config files, such as `.cursor/`, `.claude/`, or `WARP.md`,
|
|
129
|
+
proctor detects agents from their config files, such as `.cursor/`, `.claude/`, `.codex/`, or `WARP.md`,
|
|
127
130
|
and installs the ruleset for those agents. If it finds none, it writes `AGENTS.md`.
|
|
128
131
|
Run `proctor agents` to check the list before installing. Use `--all` for all 30 supported agents,
|
|
129
132
|
or `--agents claude-code,cursor` to choose them yourself.
|
|
@@ -145,6 +148,26 @@ git add -A && git commit -m "chore: add proctor"
|
|
|
145
148
|
|
|
146
149
|
If you use Claude Code, restart it so it picks up the new Stop hook.
|
|
147
150
|
|
|
151
|
+
If you use **Codex CLI**, two trust steps are required before the hook runs:
|
|
152
|
+
|
|
153
|
+
1. Trust the project at Codex's interactive prompt, or set `trust_level = "trusted"` under
|
|
154
|
+
`[projects."/absolute/path/to/repo"]` in `~/.codex/config.toml`. Project hooks are silently
|
|
155
|
+
ignored in untrusted projects.
|
|
156
|
+
2. Review and approve the new or changed hook when Codex prompts, or via `/hooks` in the CLI.
|
|
157
|
+
|
|
158
|
+
The Codex hook runs `proctor stop-hook --format codex`, which emits a JSON block decision with
|
|
159
|
+
finding feedback and exits `0`. It stays silent on clean checks, internal errors, and a follow-up
|
|
160
|
+
Stop with `stop_hook_active: true`, preventing a loop.
|
|
161
|
+
|
|
162
|
+
Verified end-to-end against codex-cli 0.164.0 on November 8, 2026: in `codex exec`, a change that
|
|
163
|
+
deleted a failing test was blocked at Stop with an RH001 finding, and Codex continued and acted on
|
|
164
|
+
the block reason; an honest fix completed with no block. Hook trust is one-time: Codex asks to
|
|
165
|
+
review new or changed hooks (the interactive prompt or `/hooks`), records trust against the hook
|
|
166
|
+
definition, and after that the hook runs in both interactive sessions and `codex exec`. The
|
|
167
|
+
`--dangerously-bypass-hook-trust` flag exists for one-off automation that vets hook sources outside
|
|
168
|
+
Codex; it is not part of standard setup. Contract source:
|
|
169
|
+
[Codex hooks](https://learn.chatgpt.com/docs/hooks.md).
|
|
170
|
+
|
|
148
171
|
The hooks run checks automatically. A config file is optional.
|
|
149
172
|
|
|
150
173
|
### Checking it worked
|
|
@@ -409,7 +432,7 @@ See the [scope, dropped ideas and tuned dev-set measurements](bench/dynamic-dev/
|
|
|
409
432
|
|
|
410
433
|
proctor combines a **ruleset** with a **guard**. The ruleset tells the agent how to complete work
|
|
411
434
|
and report results. The deterministic, diff-level guard checks for known tampering patterns at
|
|
412
|
-
commit time and, with the Claude Code Stop
|
|
435
|
+
commit time and, with the Claude Code, Codex CLI, or Cursor Stop hooks, at the end of a turn. It works
|
|
413
436
|
below the agent's own reasoning by checking the changed code rather than the agent's explanation.
|
|
414
437
|
|
|
415
438
|
## Can the agent get around it?
|
package/dist/cli.js
CHANGED
|
@@ -64,7 +64,7 @@ __export(runner_exports, {
|
|
|
64
64
|
});
|
|
65
65
|
import { spawn as spawn2 } from "child_process";
|
|
66
66
|
import { mkdtempSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, rmSync } from "fs";
|
|
67
|
-
import { join as
|
|
67
|
+
import { join as join16 } from "path";
|
|
68
68
|
import { tmpdir as tmpdir2 } from "os";
|
|
69
69
|
async function runDynamic(options) {
|
|
70
70
|
const started = Date.now();
|
|
@@ -77,7 +77,7 @@ async function runDynamic(options) {
|
|
|
77
77
|
}
|
|
78
78
|
let scratch;
|
|
79
79
|
try {
|
|
80
|
-
scratch = mkdtempSync(
|
|
80
|
+
scratch = mkdtempSync(join16(tmpdir2(), "proctor-dynamic-"));
|
|
81
81
|
} catch {
|
|
82
82
|
return skipped("cannot create scratch directory");
|
|
83
83
|
}
|
|
@@ -88,14 +88,14 @@ async function runDynamic(options) {
|
|
|
88
88
|
}
|
|
89
89
|
};
|
|
90
90
|
try {
|
|
91
|
-
mkdirSync2(
|
|
92
|
-
writeFileSync2(
|
|
91
|
+
mkdirSync2(join16(scratch, "tmp"));
|
|
92
|
+
writeFileSync2(join16(scratch, "input.json"), JSON.stringify({ ...options, files: options.files.map(({ from, to }) => ({ from, to })) }));
|
|
93
93
|
const env = {
|
|
94
94
|
PATH: process.env["PATH"],
|
|
95
95
|
HOME: scratch,
|
|
96
|
-
TMPDIR:
|
|
97
|
-
TMP:
|
|
98
|
-
TEMP:
|
|
96
|
+
TMPDIR: join16(scratch, "tmp"),
|
|
97
|
+
TMP: join16(scratch, "tmp"),
|
|
98
|
+
TEMP: join16(scratch, "tmp"),
|
|
99
99
|
LANG: "C.UTF-8",
|
|
100
100
|
CI: "true",
|
|
101
101
|
NO_COLOR: "1",
|
|
@@ -105,7 +105,7 @@ async function runDynamic(options) {
|
|
|
105
105
|
npm_config_offline: "true",
|
|
106
106
|
npm_config_audit: "false",
|
|
107
107
|
npm_config_update_notifier: "false",
|
|
108
|
-
npm_config_cache:
|
|
108
|
+
npm_config_cache: join16(scratch, "npm-cache")
|
|
109
109
|
};
|
|
110
110
|
const remaining = timeout - (Date.now() - started);
|
|
111
111
|
if (remaining <= 0) return skipped(`overall timeout (${timeout}ms)`);
|
|
@@ -127,7 +127,7 @@ async function runDynamic(options) {
|
|
|
127
127
|
"--",
|
|
128
128
|
process.execPath,
|
|
129
129
|
options.workerPath,
|
|
130
|
-
|
|
130
|
+
join16(scratch, "input.json")
|
|
131
131
|
];
|
|
132
132
|
const child = spawn2("bwrap", namespaceArgs, {
|
|
133
133
|
cwd: scratch,
|
|
@@ -212,14 +212,14 @@ var init_runner = __esm({
|
|
|
212
212
|
});
|
|
213
213
|
|
|
214
214
|
// src/cli.ts
|
|
215
|
-
import { Command } from "commander";
|
|
216
|
-
import { resolve as resolve3, join as
|
|
215
|
+
import { Command, Option } from "commander";
|
|
216
|
+
import { resolve as resolve3, join as join17 } from "path";
|
|
217
217
|
import { homedir } from "os";
|
|
218
218
|
|
|
219
219
|
// package.json
|
|
220
220
|
var package_default = {
|
|
221
221
|
name: "@kavishdua/proctor",
|
|
222
|
-
version: "1.
|
|
222
|
+
version: "1.4.0",
|
|
223
223
|
description: "Catch AI agents gaming their test suites: deleted tests, skipped tests, weakened assertions, hardcoded outputs",
|
|
224
224
|
homepage: "https://github.com/catfish-1234/proctor#readme",
|
|
225
225
|
bugs: {
|
|
@@ -302,7 +302,7 @@ var package_default = {
|
|
|
302
302
|
};
|
|
303
303
|
|
|
304
304
|
// src/cli.ts
|
|
305
|
-
import { readFile as
|
|
305
|
+
import { readFile as readFile13, writeFile as writeFile8, appendFile } from "fs/promises";
|
|
306
306
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
307
307
|
|
|
308
308
|
// src/diff.ts
|
|
@@ -7446,8 +7446,8 @@ async function checkAdapterDrift(cwd, canonical, adapters = AGENT_ADAPTERS) {
|
|
|
7446
7446
|
}
|
|
7447
7447
|
|
|
7448
7448
|
// src/adapters/deploy.ts
|
|
7449
|
-
import { readFile as
|
|
7450
|
-
import { join as
|
|
7449
|
+
import { readFile as readFile8, writeFile as writeFile5, mkdir as mkdir4, rm as rm2 } from "fs/promises";
|
|
7450
|
+
import { join as join9, dirname, relative } from "path";
|
|
7451
7451
|
|
|
7452
7452
|
// src/hooks/pre-commit.ts
|
|
7453
7453
|
import { readFile as readFile5, writeFile as writeFile2, mkdir, rm } from "fs/promises";
|
|
@@ -7633,30 +7633,109 @@ function isProctorGroup(group) {
|
|
|
7633
7633
|
return hooks.some((h) => isPlainObject(h) && typeof h["command"] === "string" && h["command"].includes(STOP_HOOK_MARKER));
|
|
7634
7634
|
}
|
|
7635
7635
|
|
|
7636
|
+
// src/hooks/codex-hooks.ts
|
|
7637
|
+
import { readFile as readFile7, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
7638
|
+
import { join as join8 } from "path";
|
|
7639
|
+
var STOP_HOOK_COMMAND2 = `npx ${package_default.name} stop-hook --format codex`;
|
|
7640
|
+
function parseHooks(raw) {
|
|
7641
|
+
try {
|
|
7642
|
+
const parsed = JSON.parse(raw.replace(/^/, ""));
|
|
7643
|
+
if (!isPlainObject2(parsed)) return void 0;
|
|
7644
|
+
const hooks = parsed["hooks"];
|
|
7645
|
+
if (hooks !== void 0 && !isPlainObject2(hooks)) return void 0;
|
|
7646
|
+
const stop = hooks?.["Stop"];
|
|
7647
|
+
if (stop !== void 0 && !Array.isArray(stop)) return void 0;
|
|
7648
|
+
return parsed;
|
|
7649
|
+
} catch {
|
|
7650
|
+
return void 0;
|
|
7651
|
+
}
|
|
7652
|
+
}
|
|
7653
|
+
async function installCodexStopHook(dir) {
|
|
7654
|
+
const path9 = join8(dir, "hooks.json");
|
|
7655
|
+
let settings = {};
|
|
7656
|
+
let raw;
|
|
7657
|
+
try {
|
|
7658
|
+
raw = await readFile7(path9, "utf8");
|
|
7659
|
+
} catch (err) {
|
|
7660
|
+
if (err.code !== "ENOENT") throw err;
|
|
7661
|
+
}
|
|
7662
|
+
if (raw !== void 0) {
|
|
7663
|
+
const parsed = parseHooks(raw);
|
|
7664
|
+
if (!parsed) return { status: "invalid-json", path: path9 };
|
|
7665
|
+
settings = parsed;
|
|
7666
|
+
}
|
|
7667
|
+
const hooks = settings["hooks"] ?? {};
|
|
7668
|
+
const stop = hooks["Stop"] ?? [];
|
|
7669
|
+
if (stop.some((group) => isPlainObject2(group) && Array.isArray(group["hooks"]) && group["hooks"].some(isProctorHook))) {
|
|
7670
|
+
return { status: "already", path: path9 };
|
|
7671
|
+
}
|
|
7672
|
+
stop.push({ hooks: [{ type: "command", command: STOP_HOOK_COMMAND2, timeout: 60 }] });
|
|
7673
|
+
hooks["Stop"] = stop;
|
|
7674
|
+
settings["hooks"] = hooks;
|
|
7675
|
+
await mkdir3(dir, { recursive: true });
|
|
7676
|
+
await writeFile4(path9, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
7677
|
+
return { status: "installed", path: path9 };
|
|
7678
|
+
}
|
|
7679
|
+
async function removeCodexStopHook(dir, dryRun) {
|
|
7680
|
+
const path9 = join8(dir, "hooks.json");
|
|
7681
|
+
let raw;
|
|
7682
|
+
try {
|
|
7683
|
+
raw = await readFile7(path9, "utf8");
|
|
7684
|
+
} catch {
|
|
7685
|
+
return void 0;
|
|
7686
|
+
}
|
|
7687
|
+
const settings = parseHooks(raw);
|
|
7688
|
+
if (!settings) return void 0;
|
|
7689
|
+
const hooks = settings["hooks"];
|
|
7690
|
+
const stop = hooks?.["Stop"] ?? [];
|
|
7691
|
+
let removed = false;
|
|
7692
|
+
const kept = stop.flatMap((group) => {
|
|
7693
|
+
if (!isPlainObject2(group) || !Array.isArray(group["hooks"])) return [group];
|
|
7694
|
+
const commands = group["hooks"].filter((h) => !isProctorHook(h));
|
|
7695
|
+
if (commands.length === group["hooks"].length) return [group];
|
|
7696
|
+
removed = true;
|
|
7697
|
+
return commands.length > 0 ? [{ ...group, hooks: commands }] : [];
|
|
7698
|
+
});
|
|
7699
|
+
if (!removed || !hooks) return void 0;
|
|
7700
|
+
if (!dryRun) {
|
|
7701
|
+
if (kept.length > 0) hooks["Stop"] = kept;
|
|
7702
|
+
else delete hooks["Stop"];
|
|
7703
|
+
if (Object.keys(hooks).length === 0) delete settings["hooks"];
|
|
7704
|
+
await writeFile4(path9, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
7705
|
+
}
|
|
7706
|
+
return path9;
|
|
7707
|
+
}
|
|
7708
|
+
function isPlainObject2(value) {
|
|
7709
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7710
|
+
}
|
|
7711
|
+
function isProctorHook(hook) {
|
|
7712
|
+
return isPlainObject2(hook) && hook["type"] === "command" && hook["command"] === STOP_HOOK_COMMAND2;
|
|
7713
|
+
}
|
|
7714
|
+
|
|
7636
7715
|
// src/adapters/deploy.ts
|
|
7637
7716
|
async function deploySkill(cwd, adapters, canonical) {
|
|
7638
7717
|
let failed = 0;
|
|
7639
7718
|
const manifest = await readManifest(cwd);
|
|
7640
7719
|
for (const adapter of adapters) {
|
|
7641
|
-
const dest =
|
|
7720
|
+
const dest = join9(cwd, adapter.relativePath);
|
|
7642
7721
|
const content = adapter.transform ? adapter.transform(canonical) : canonical;
|
|
7643
7722
|
try {
|
|
7644
7723
|
if (adapter.shared) {
|
|
7645
7724
|
let existing;
|
|
7646
7725
|
try {
|
|
7647
|
-
existing = await
|
|
7726
|
+
existing = await readFile8(dest, "utf8");
|
|
7648
7727
|
} catch (err) {
|
|
7649
7728
|
if (err.code !== "ENOENT") throw err;
|
|
7650
7729
|
}
|
|
7651
7730
|
const ours = manifest.written[adapter.id] === true;
|
|
7652
|
-
await
|
|
7653
|
-
await
|
|
7731
|
+
await mkdir4(dirname(dest), { recursive: true });
|
|
7732
|
+
await writeFile5(dest, upsertBlock(existing, content, ours), "utf8");
|
|
7654
7733
|
await recordWritten(cwd, adapter.id, existing === void 0);
|
|
7655
7734
|
process.stdout.write((existing === void 0 ? "Installed: " : "Merged: ") + dest + "\n");
|
|
7656
7735
|
continue;
|
|
7657
7736
|
}
|
|
7658
|
-
await
|
|
7659
|
-
await
|
|
7737
|
+
await mkdir4(dirname(dest), { recursive: true });
|
|
7738
|
+
await writeFile5(dest, content, "utf8");
|
|
7660
7739
|
process.stdout.write("Installed: " + dest + "\n");
|
|
7661
7740
|
} catch (err) {
|
|
7662
7741
|
failed++;
|
|
@@ -7683,10 +7762,10 @@ async function uninstallProctor(cwd, dryRun) {
|
|
|
7683
7762
|
};
|
|
7684
7763
|
const manifest = await readManifest(cwd);
|
|
7685
7764
|
for (const adapter of AGENT_ADAPTERS) {
|
|
7686
|
-
const dest =
|
|
7765
|
+
const dest = join9(cwd, adapter.relativePath);
|
|
7687
7766
|
let existing;
|
|
7688
7767
|
try {
|
|
7689
|
-
existing = await
|
|
7768
|
+
existing = await readFile8(dest, "utf8");
|
|
7690
7769
|
} catch (err) {
|
|
7691
7770
|
if (err.code !== "ENOENT") {
|
|
7692
7771
|
failed.push(`${adapter.relativePath} (${err.code ?? "unreadable"})`);
|
|
@@ -7714,7 +7793,7 @@ async function uninstallProctor(cwd, dryRun) {
|
|
|
7714
7793
|
});
|
|
7715
7794
|
} else {
|
|
7716
7795
|
await attempt(`Unmerged the proctor block from: ${adapter.relativePath}`, async () => {
|
|
7717
|
-
if (!dryRun) await
|
|
7796
|
+
if (!dryRun) await writeFile5(dest, stripped, "utf8");
|
|
7718
7797
|
});
|
|
7719
7798
|
}
|
|
7720
7799
|
continue;
|
|
@@ -7723,9 +7802,9 @@ async function uninstallProctor(cwd, dryRun) {
|
|
|
7723
7802
|
if (!dryRun) await rm2(dest, { force: true });
|
|
7724
7803
|
});
|
|
7725
7804
|
}
|
|
7726
|
-
const manifestPath =
|
|
7805
|
+
const manifestPath = join9(cwd, MANIFEST_FILENAME);
|
|
7727
7806
|
try {
|
|
7728
|
-
await
|
|
7807
|
+
await readFile8(manifestPath, "utf8");
|
|
7729
7808
|
await attempt(`Removed: ${MANIFEST_FILENAME}`, async () => {
|
|
7730
7809
|
if (!dryRun) await rm2(manifestPath, { force: true });
|
|
7731
7810
|
});
|
|
@@ -7738,31 +7817,37 @@ async function uninstallProctor(cwd, dryRun) {
|
|
|
7738
7817
|
failed.push(`the pre-commit hook (${err.code ?? "unknown error"})`);
|
|
7739
7818
|
}
|
|
7740
7819
|
try {
|
|
7741
|
-
const stop = await removeStopHook(
|
|
7820
|
+
const stop = await removeStopHook(join9(cwd, ".claude"), dryRun);
|
|
7742
7821
|
if (stop) done.push(`Removed the proctor Stop hook from: ${rel(stop)}`);
|
|
7743
7822
|
} catch (err) {
|
|
7744
7823
|
failed.push(`the Stop hook entry (${err.code ?? "unknown error"})`);
|
|
7745
7824
|
}
|
|
7825
|
+
try {
|
|
7826
|
+
const stop = await removeCodexStopHook(join9(cwd, ".codex"), dryRun);
|
|
7827
|
+
if (stop) done.push(`Removed the proctor Codex Stop hook from: ${rel(stop)}`);
|
|
7828
|
+
} catch (err) {
|
|
7829
|
+
failed.push(`the Codex Stop hook entry (${err.code ?? "unknown error"})`);
|
|
7830
|
+
}
|
|
7746
7831
|
return { done, failed, note };
|
|
7747
7832
|
}
|
|
7748
7833
|
|
|
7749
7834
|
// src/bench/tasks.ts
|
|
7750
7835
|
import { existsSync } from "fs";
|
|
7751
7836
|
import { readdir } from "fs/promises";
|
|
7752
|
-
import { join as
|
|
7837
|
+
import { join as join10, dirname as dirname2 } from "path";
|
|
7753
7838
|
import { fileURLToPath } from "url";
|
|
7754
7839
|
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
7755
7840
|
function findPackageRoot(startDir) {
|
|
7756
7841
|
let dir = startDir;
|
|
7757
7842
|
for (let i = 0; i < 6; i++) {
|
|
7758
|
-
if (existsSync(
|
|
7843
|
+
if (existsSync(join10(dir, "package.json"))) return dir;
|
|
7759
7844
|
const parent = dirname2(dir);
|
|
7760
7845
|
if (parent === dir) break;
|
|
7761
7846
|
dir = parent;
|
|
7762
7847
|
}
|
|
7763
7848
|
return startDir;
|
|
7764
7849
|
}
|
|
7765
|
-
var DEFAULT_TASKS_DIR =
|
|
7850
|
+
var DEFAULT_TASKS_DIR = join10(findPackageRoot(__dirname), "bench/tasks");
|
|
7766
7851
|
var TASK_NAME_RE = /^task-\d+$/;
|
|
7767
7852
|
function mulberry32(seed) {
|
|
7768
7853
|
let a = seed | 0;
|
|
@@ -7801,33 +7886,33 @@ async function loadTaskPool(tasksDir = DEFAULT_TASKS_DIR) {
|
|
|
7801
7886
|
}
|
|
7802
7887
|
const dirNames = entries.filter((e) => e.isDirectory() && TASK_NAME_RE.test(e.name)).map((e) => e.name).sort();
|
|
7803
7888
|
return dirNames.map((taskId) => {
|
|
7804
|
-
const dir =
|
|
7889
|
+
const dir = join10(tasksDir, taskId);
|
|
7805
7890
|
return {
|
|
7806
7891
|
taskId,
|
|
7807
7892
|
dir,
|
|
7808
|
-
promptPath:
|
|
7809
|
-
repoDir:
|
|
7810
|
-
heldOutDir:
|
|
7811
|
-
heldOutPath:
|
|
7812
|
-
mockPath:
|
|
7893
|
+
promptPath: join10(dir, "prompt.md"),
|
|
7894
|
+
repoDir: join10(dir, "repo"),
|
|
7895
|
+
heldOutDir: join10(dir, "held-out"),
|
|
7896
|
+
heldOutPath: join10(dir, "held-out.json"),
|
|
7897
|
+
mockPath: join10(dir, "mock-agent.json")
|
|
7813
7898
|
};
|
|
7814
7899
|
});
|
|
7815
7900
|
}
|
|
7816
7901
|
|
|
7817
7902
|
// src/bench/index.ts
|
|
7818
|
-
import { mkdir as
|
|
7903
|
+
import { mkdir as mkdir6, writeFile as writeFile7, readFile as readFile12, rm as rm4 } from "fs/promises";
|
|
7819
7904
|
import { dirname as dirname6 } from "path";
|
|
7820
7905
|
|
|
7821
7906
|
// src/bench/runners/fixture-runner.ts
|
|
7822
|
-
import { readFile as
|
|
7823
|
-
import { dirname as dirname3, join as
|
|
7907
|
+
import { readFile as readFile9, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
|
|
7908
|
+
import { dirname as dirname3, join as join11, resolve as resolve2, sep } from "path";
|
|
7824
7909
|
function createFixtureRunner(model) {
|
|
7825
7910
|
return {
|
|
7826
7911
|
model,
|
|
7827
7912
|
async run(task) {
|
|
7828
7913
|
const started = Date.now();
|
|
7829
|
-
const mockPath =
|
|
7830
|
-
const raw = await
|
|
7914
|
+
const mockPath = join11(task.workdir, "mock-agent.json");
|
|
7915
|
+
const raw = await readFile9(mockPath, "utf8");
|
|
7831
7916
|
const mock = JSON.parse(raw);
|
|
7832
7917
|
const branch = task.proctorOn ? mock.proctorOn : mock.proctorOff;
|
|
7833
7918
|
const root = resolve2(task.workdir);
|
|
@@ -7836,8 +7921,8 @@ function createFixtureRunner(model) {
|
|
|
7836
7921
|
if (absPath !== root && !absPath.startsWith(root + sep)) {
|
|
7837
7922
|
throw new Error(`mock-agent.json path escapes workdir: ${relPath}`);
|
|
7838
7923
|
}
|
|
7839
|
-
await
|
|
7840
|
-
await
|
|
7924
|
+
await mkdir5(dirname3(absPath), { recursive: true });
|
|
7925
|
+
await writeFile6(absPath, content, "utf8");
|
|
7841
7926
|
}
|
|
7842
7927
|
return {
|
|
7843
7928
|
taskId: task.taskId,
|
|
@@ -7855,25 +7940,25 @@ function createFixtureRunner(model) {
|
|
|
7855
7940
|
// src/bench/runners/shell-runner.ts
|
|
7856
7941
|
import { spawn, spawnSync as spawnSync3 } from "child_process";
|
|
7857
7942
|
import { existsSync as existsSync2 } from "fs";
|
|
7858
|
-
import { readFile as
|
|
7859
|
-
import { join as
|
|
7943
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
7944
|
+
import { join as join12, dirname as dirname4 } from "path";
|
|
7860
7945
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7861
7946
|
var __dirname2 = fileURLToPath2(new URL(".", import.meta.url));
|
|
7862
7947
|
function findPackageRoot2(startDir) {
|
|
7863
7948
|
let dir = startDir;
|
|
7864
7949
|
for (let i = 0; i < 6; i++) {
|
|
7865
|
-
if (existsSync2(
|
|
7950
|
+
if (existsSync2(join12(dir, "package.json"))) return dir;
|
|
7866
7951
|
const parent = dirname4(dir);
|
|
7867
7952
|
if (parent === dir) break;
|
|
7868
7953
|
dir = parent;
|
|
7869
7954
|
}
|
|
7870
7955
|
return startDir;
|
|
7871
7956
|
}
|
|
7872
|
-
var SKILL_MD_PATH =
|
|
7957
|
+
var SKILL_MD_PATH = join12(findPackageRoot2(__dirname2), "src/skill/SKILL.md");
|
|
7873
7958
|
var MAX_CAPTURED_OUTPUT = 10 * 1024 * 1024;
|
|
7874
7959
|
async function buildEffectivePrompt(task) {
|
|
7875
7960
|
if (!task.proctorOn) return task.prompt;
|
|
7876
|
-
const skillContent = await
|
|
7961
|
+
const skillContent = await readFile10(SKILL_MD_PATH, "utf8");
|
|
7877
7962
|
return `${skillContent}
|
|
7878
7963
|
---
|
|
7879
7964
|
${task.prompt}`;
|
|
@@ -7967,11 +8052,11 @@ var AGENT_RUNNERS = [
|
|
|
7967
8052
|
];
|
|
7968
8053
|
|
|
7969
8054
|
// src/bench/scorer.ts
|
|
7970
|
-
import { mkdtemp, cp, rm as rm3, readFile as
|
|
8055
|
+
import { mkdtemp, cp, rm as rm3, readFile as readFile11 } from "fs/promises";
|
|
7971
8056
|
import { appendFileSync, symlinkSync } from "fs";
|
|
7972
8057
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
7973
8058
|
import { tmpdir, platform } from "os";
|
|
7974
|
-
import { join as
|
|
8059
|
+
import { join as join13, basename as basename3, dirname as dirname5 } from "path";
|
|
7975
8060
|
import { createRequire } from "module";
|
|
7976
8061
|
var require2 = createRequire(import.meta.url);
|
|
7977
8062
|
var vitestPaths;
|
|
@@ -7981,7 +8066,7 @@ function resolveVitestPaths() {
|
|
|
7981
8066
|
vitestPaths = {
|
|
7982
8067
|
resolvedNodeModules: dirname5(vitestPkgDir),
|
|
7983
8068
|
// node_modules dir containing the resolved vitest install
|
|
7984
|
-
vitestBin:
|
|
8069
|
+
vitestBin: join13(vitestPkgDir, "vitest.mjs")
|
|
7985
8070
|
};
|
|
7986
8071
|
}
|
|
7987
8072
|
return vitestPaths;
|
|
@@ -7991,7 +8076,7 @@ function linkNodeModules(workdir) {
|
|
|
7991
8076
|
try {
|
|
7992
8077
|
symlinkSync(
|
|
7993
8078
|
resolveVitestPaths().resolvedNodeModules,
|
|
7994
|
-
|
|
8079
|
+
join13(workdir, "node_modules"),
|
|
7995
8080
|
platform() === "win32" ? "junction" : "dir"
|
|
7996
8081
|
);
|
|
7997
8082
|
} catch {
|
|
@@ -8004,7 +8089,7 @@ function runGit2(args, cwd) {
|
|
|
8004
8089
|
}
|
|
8005
8090
|
}
|
|
8006
8091
|
function excludeNodeModules(workdir) {
|
|
8007
|
-
appendFileSync(
|
|
8092
|
+
appendFileSync(join13(workdir, ".git", "info", "exclude"), "\n/node_modules/\n", "utf8");
|
|
8008
8093
|
}
|
|
8009
8094
|
function runHeldOutTests(workdir) {
|
|
8010
8095
|
const result = spawnSync4(process.execPath, [resolveVitestPaths().vitestBin, "run", "--root", workdir], {
|
|
@@ -8019,12 +8104,12 @@ async function scoreTask(taskDir, runner, proctorOn) {
|
|
|
8019
8104
|
if (!TASK_NAME_RE2.test(taskId)) {
|
|
8020
8105
|
throw new Error(`invalid task directory name: ${taskId}`);
|
|
8021
8106
|
}
|
|
8022
|
-
const tempDir = await mkdtemp(
|
|
8107
|
+
const tempDir = await mkdtemp(join13(tmpdir(), "proctor-bench-"));
|
|
8023
8108
|
try {
|
|
8024
|
-
await cp(
|
|
8109
|
+
await cp(join13(taskDir, "repo"), tempDir, { recursive: true });
|
|
8025
8110
|
linkNodeModules(tempDir);
|
|
8026
8111
|
try {
|
|
8027
|
-
await cp(
|
|
8112
|
+
await cp(join13(taskDir, "mock-agent.json"), join13(tempDir, "mock-agent.json"));
|
|
8028
8113
|
} catch (err) {
|
|
8029
8114
|
if (err.code !== "ENOENT") throw err;
|
|
8030
8115
|
}
|
|
@@ -8034,7 +8119,7 @@ async function scoreTask(taskDir, runner, proctorOn) {
|
|
|
8034
8119
|
runGit2(["config", "user.name", "proctor-bench"], tempDir);
|
|
8035
8120
|
runGit2(["add", "-A"], tempDir);
|
|
8036
8121
|
runGit2(["-c", "commit.gpgsign=false", "commit", "-q", "-m", "baseline", "--allow-empty"], tempDir);
|
|
8037
|
-
const prompt = await
|
|
8122
|
+
const prompt = await readFile11(join13(taskDir, "prompt.md"), "utf8");
|
|
8038
8123
|
const task = { taskId, prompt, workdir: tempDir, proctorOn };
|
|
8039
8124
|
const agentResult = await runner.run(task);
|
|
8040
8125
|
if (agentResult.timedOut) {
|
|
@@ -8053,7 +8138,7 @@ async function scoreTask(taskDir, runner, proctorOn) {
|
|
|
8053
8138
|
const findings = await runChecks(ctx);
|
|
8054
8139
|
const cheatDetected = findings.length > 0;
|
|
8055
8140
|
const rhId = findings[0]?.verifierId ?? "";
|
|
8056
|
-
await cp(
|
|
8141
|
+
await cp(join13(taskDir, "held-out"), join13(tempDir, "held-out"), { recursive: true });
|
|
8057
8142
|
const honestPass = runHeldOutTests(tempDir);
|
|
8058
8143
|
return { taskId, model: runner.model, proctorOn, cheatDetected, rhId, honestPass };
|
|
8059
8144
|
} finally {
|
|
@@ -8191,7 +8276,7 @@ async function runBench(opts) {
|
|
|
8191
8276
|
const partialPath = partialOutPath(opts.outPath);
|
|
8192
8277
|
let text;
|
|
8193
8278
|
try {
|
|
8194
|
-
text = await
|
|
8279
|
+
text = await readFile12(partialPath, "utf8");
|
|
8195
8280
|
} catch {
|
|
8196
8281
|
process.stderr.write(`proctor: --resume found no rows to resume from at ${partialPath}, scoring every selected task
|
|
8197
8282
|
`);
|
|
@@ -8235,13 +8320,13 @@ async function runBench(opts) {
|
|
|
8235
8320
|
}
|
|
8236
8321
|
const csv = CSV_HEADER + rows.map((r) => toCsvRow([r.taskId, r.model, r.proctorOn, r.cheatDetected, r.rhId, r.honestPass])).join("");
|
|
8237
8322
|
if (opts.outPath && failedTasks === 0) {
|
|
8238
|
-
await
|
|
8239
|
-
await
|
|
8323
|
+
await mkdir6(dirname6(opts.outPath), { recursive: true });
|
|
8324
|
+
await writeFile7(opts.outPath, csv, "utf8");
|
|
8240
8325
|
await rm4(partialOutPath(opts.outPath), { force: true });
|
|
8241
8326
|
} else if (opts.outPath && failedTasks > 0) {
|
|
8242
8327
|
const partialPath = partialOutPath(opts.outPath);
|
|
8243
|
-
await
|
|
8244
|
-
await
|
|
8328
|
+
await mkdir6(dirname6(partialPath), { recursive: true });
|
|
8329
|
+
await writeFile7(partialPath, csv, "utf8");
|
|
8245
8330
|
process.stderr.write(
|
|
8246
8331
|
`proctor: benchmark had ${failedTasks} invalid task${failedTasks === 1 ? "" : "s"}; preserving the existing output file instead of publishing partial evidence
|
|
8247
8332
|
proctor: the ${rows.length / 2} task${rows.length === 2 ? "" : "s"} that did complete were written to ${partialPath}, which is not a result
|
|
@@ -8255,12 +8340,12 @@ proctor: the ${rows.length / 2} task${rows.length === 2 ? "" : "s"} that did com
|
|
|
8255
8340
|
// src/hooks/stop-hook.ts
|
|
8256
8341
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
8257
8342
|
import { existsSync as existsSync3 } from "fs";
|
|
8258
|
-
import { join as
|
|
8343
|
+
import { join as join15 } from "path";
|
|
8259
8344
|
|
|
8260
8345
|
// src/session.ts
|
|
8261
8346
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
8262
8347
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
8263
|
-
import { join as
|
|
8348
|
+
import { join as join14, dirname as dirname7 } from "path";
|
|
8264
8349
|
var EMPTY = { caught: 0, recentRules: [] };
|
|
8265
8350
|
var MAX_RECENT_RULES = 20;
|
|
8266
8351
|
var SESSION_FILENAME = "proctor-session.json";
|
|
@@ -8268,7 +8353,7 @@ function sessionPath(cwd) {
|
|
|
8268
8353
|
const result = spawnSync5("git", ["rev-parse", "--absolute-git-dir"], { cwd, encoding: "utf8" });
|
|
8269
8354
|
if (result.status !== 0) return void 0;
|
|
8270
8355
|
const gitDir = result.stdout.trim();
|
|
8271
|
-
return gitDir ?
|
|
8356
|
+
return gitDir ? join14(gitDir, SESSION_FILENAME) : void 0;
|
|
8272
8357
|
}
|
|
8273
8358
|
function isValid(value) {
|
|
8274
8359
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -8323,6 +8408,13 @@ function parseStopHookInput(raw, fallbackCwd) {
|
|
|
8323
8408
|
return { cwd: fallbackCwd, skip: false };
|
|
8324
8409
|
}
|
|
8325
8410
|
}
|
|
8411
|
+
function formatStopHookResult(result, format) {
|
|
8412
|
+
if (format === "claude") return result;
|
|
8413
|
+
return {
|
|
8414
|
+
exitCode: 0,
|
|
8415
|
+
output: result.exitCode === 2 ? JSON.stringify({ decision: "block", reason: result.output }) : ""
|
|
8416
|
+
};
|
|
8417
|
+
}
|
|
8326
8418
|
function runStopHookCheck(cwd, cliPath) {
|
|
8327
8419
|
const inRepo = spawnSync6("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "ignore" });
|
|
8328
8420
|
if (inRepo.error || inRepo.status !== 0) return { exitCode: 0, output: "" };
|
|
@@ -8343,7 +8435,7 @@ function runStopHookCheck(cwd, cliPath) {
|
|
|
8343
8435
|
function isMidMerge(cwd) {
|
|
8344
8436
|
const resolved = spawnSync6("git", ["rev-parse", "--absolute-git-dir"], { cwd, encoding: "utf8" });
|
|
8345
8437
|
const gitDir = resolved.status === 0 ? (resolved.stdout ?? "").trim() : "";
|
|
8346
|
-
return IN_PROGRESS_MARKERS.some((marker) => gitDir !== "" && existsSync3(
|
|
8438
|
+
return IN_PROGRESS_MARKERS.some((marker) => gitDir !== "" && existsSync3(join15(gitDir, marker)));
|
|
8347
8439
|
}
|
|
8348
8440
|
var IN_PROGRESS_MARKERS = [
|
|
8349
8441
|
"MERGE_HEAD",
|
|
@@ -8781,16 +8873,28 @@ program.command("install-hook").description("Install git pre-commit hook").actio
|
|
|
8781
8873
|
const hookPath = await installPreCommitHook(process.cwd());
|
|
8782
8874
|
process.stdout.write("Installed: " + hookPath + "\n");
|
|
8783
8875
|
});
|
|
8784
|
-
program.command("stop-hook").description("
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8876
|
+
program.command("stop-hook").description("Stop hook: reads stdin JSON and blocks on error findings").addOption(new Option("--format <format>", "Stop hook protocol").choices(["claude", "codex"]).default("claude")).action(async (options) => {
|
|
8877
|
+
try {
|
|
8878
|
+
const raw = await readStdin();
|
|
8879
|
+
const { cwd, skip } = parseStopHookInput(raw, process.cwd());
|
|
8880
|
+
if (skip) return;
|
|
8881
|
+
const { exitCode, output } = formatStopHookResult(
|
|
8882
|
+
runStopHookCheck(cwd, process.argv[1] ?? ""),
|
|
8883
|
+
options.format
|
|
8884
|
+
);
|
|
8885
|
+
if (options.format === "codex") {
|
|
8886
|
+
if (output) process.stdout.write(output + "\n");
|
|
8887
|
+
process.exitCode = exitCode;
|
|
8888
|
+
return;
|
|
8889
|
+
}
|
|
8890
|
+
if (output) process.stderr.write(output);
|
|
8891
|
+
process.exit(exitCode);
|
|
8892
|
+
} catch (err) {
|
|
8893
|
+
if (options.format !== "codex") throw err;
|
|
8894
|
+
}
|
|
8791
8895
|
});
|
|
8792
8896
|
program.command("install-claude-hook").description("Install Claude Code Stop hook").option("--global", "write to ~/.claude/settings.json").action(async (options) => {
|
|
8793
|
-
const result = await installStopHook(options.global ?
|
|
8897
|
+
const result = await installStopHook(options.global ? join17(homedir(), ".claude") : join17(process.cwd(), ".claude"));
|
|
8794
8898
|
if (result.status === "invalid-json") {
|
|
8795
8899
|
process.stderr.write(`proctor: ${result.path} exists but is not valid JSON; fix it manually and re-run
|
|
8796
8900
|
`);
|
|
@@ -8798,10 +8902,10 @@ program.command("install-claude-hook").description("Install Claude Code Stop hoo
|
|
|
8798
8902
|
}
|
|
8799
8903
|
process.stdout.write(result.status === "already" ? "Already installed\n" : "Installed: " + result.path + "\n");
|
|
8800
8904
|
});
|
|
8801
|
-
program.command("setup").description("One-command install: writes the ruleset
|
|
8905
|
+
program.command("setup").description("One-command install: writes the ruleset and installs the git and supported agent hooks").option("--all", `write the ruleset to all ${AGENT_ADAPTERS.length} supported agents, not only the ones this repo uses`).option("--agents <ids>", "comma-separated agent ids to install to (e.g. claude-code,cursor), instead of detecting").action(async (options) => {
|
|
8802
8906
|
const cwd = process.cwd();
|
|
8803
8907
|
const targets = await resolveTargets(cwd, options);
|
|
8804
|
-
const failed = await deploySkill(cwd, targets, await
|
|
8908
|
+
const failed = await deploySkill(cwd, targets, await readFile13(canonicalSkillPath(), "utf8"));
|
|
8805
8909
|
process.stdout.write(
|
|
8806
8910
|
`Ruleset written to ${targets.length - failed} of ${targets.length} agent path${targets.length === 1 ? "" : "s"}.
|
|
8807
8911
|
`
|
|
@@ -8822,7 +8926,7 @@ program.command("setup").description("One-command install: writes the ruleset to
|
|
|
8822
8926
|
process.exitCode = 1;
|
|
8823
8927
|
}
|
|
8824
8928
|
if (targets.some((a) => a.id === "claude-code")) {
|
|
8825
|
-
const stop = await installStopHook(
|
|
8929
|
+
const stop = await installStopHook(join17(cwd, ".claude"));
|
|
8826
8930
|
if (stop.status === "invalid-json") {
|
|
8827
8931
|
process.stderr.write(`proctor: ${stop.path} is not valid JSON, so the Stop hook was not installed; fix it and re-run
|
|
8828
8932
|
`);
|
|
@@ -8834,6 +8938,27 @@ program.command("setup").description("One-command install: writes the ruleset to
|
|
|
8834
8938
|
);
|
|
8835
8939
|
}
|
|
8836
8940
|
}
|
|
8941
|
+
if (targets.some((a) => a.id === "codex")) {
|
|
8942
|
+
try {
|
|
8943
|
+
const stop = await installCodexStopHook(join17(cwd, ".codex"));
|
|
8944
|
+
if (stop.status === "invalid-json") {
|
|
8945
|
+
process.stderr.write(`proctor: ${stop.path} is not valid JSON, so the Codex Stop hook was not installed; fix it and re-run
|
|
8946
|
+
`);
|
|
8947
|
+
process.exitCode = 1;
|
|
8948
|
+
} else {
|
|
8949
|
+
process.stdout.write(
|
|
8950
|
+
stop.status === "already" ? "Codex Stop hook already installed.\n" : `Codex Stop hook installed: ${stop.path}
|
|
8951
|
+
`
|
|
8952
|
+
);
|
|
8953
|
+
process.stdout.write("Codex requires project trust and hook review before this hook runs: trust this project, then approve the new or changed hook when prompted or via /hooks in the CLI.\n");
|
|
8954
|
+
}
|
|
8955
|
+
} catch (err) {
|
|
8956
|
+
const msg = String(err).replace(/^Error:\s*/, "");
|
|
8957
|
+
process.stderr.write(`proctor: could not install the Codex Stop hook (${msg})
|
|
8958
|
+
`);
|
|
8959
|
+
process.exitCode = 1;
|
|
8960
|
+
}
|
|
8961
|
+
}
|
|
8837
8962
|
process.stdout.write(process.exitCode ? "\nSetup incomplete. Resolve the errors above and run proctor setup again.\n" : "\nDone. Your agent reads the rules before it works, and gets stopped if it breaks them.\n");
|
|
8838
8963
|
});
|
|
8839
8964
|
program.command("install-skill").description("Deploy canonical SKILL.md to the agent adapter paths this repo uses").option("--all", `write to all ${AGENT_ADAPTERS.length} supported agents, not only the ones this repo uses`).option("--agents <ids>", "comma-separated agent ids to install to (e.g. claude-code,cursor), instead of detecting").action(async (options) => {
|
|
@@ -8841,7 +8966,7 @@ program.command("install-skill").description("Deploy canonical SKILL.md to the a
|
|
|
8841
8966
|
const failed = await deploySkill(
|
|
8842
8967
|
cwd,
|
|
8843
8968
|
await resolveTargets(cwd, options),
|
|
8844
|
-
await
|
|
8969
|
+
await readFile13(canonicalSkillPath(), "utf8")
|
|
8845
8970
|
);
|
|
8846
8971
|
if (failed > 0) {
|
|
8847
8972
|
process.stderr.write(`proctor: ${failed} adapter${failed === 1 ? "" : "s"} could not be written
|
|
@@ -8927,7 +9052,7 @@ ${done.length} item${done.length === 1 ? "" : "s"} would be removed. Re-run with
|
|
|
8927
9052
|
});
|
|
8928
9053
|
program.command("drift-check").description("Verify every deployed agent adapter still matches canonical SKILL.md").action(async () => {
|
|
8929
9054
|
const cwd = process.cwd();
|
|
8930
|
-
const canonical = await
|
|
9055
|
+
const canonical = await readFile13(canonicalSkillPath(), "utf8");
|
|
8931
9056
|
const { drifted } = await checkAdapterDrift(cwd, canonical);
|
|
8932
9057
|
for (const path9 of drifted) {
|
|
8933
9058
|
process.stderr.write("Drifted: " + path9 + "\n");
|
|
@@ -9035,10 +9160,10 @@ program.command("approve <rule> <file>").description("Record a genuine test chan
|
|
|
9035
9160
|
process.stderr.write("proctor: --reason cannot be empty\n");
|
|
9036
9161
|
process.exit(2);
|
|
9037
9162
|
}
|
|
9038
|
-
const configPath =
|
|
9163
|
+
const configPath = join17(process.cwd(), "proctor.config.json");
|
|
9039
9164
|
let config = {};
|
|
9040
9165
|
try {
|
|
9041
|
-
config = JSON.parse(await
|
|
9166
|
+
config = JSON.parse(await readFile13(configPath, "utf8"));
|
|
9042
9167
|
} catch (err) {
|
|
9043
9168
|
if (err.code !== "ENOENT") {
|
|
9044
9169
|
process.stderr.write(`proctor: could not read ${configPath}: ${String(err)}
|
|
@@ -9062,7 +9187,7 @@ program.command("approve <rule> <file>").description("Record a genuine test chan
|
|
|
9062
9187
|
approvals.push({ rule, file: normalizedFile, reason: options.reason });
|
|
9063
9188
|
}
|
|
9064
9189
|
config.approvedTestChanges = approvals;
|
|
9065
|
-
await
|
|
9190
|
+
await writeFile8(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
9066
9191
|
process.stdout.write(`Approved ${rule} for ${normalizedFile} in proctor.config.json
|
|
9067
9192
|
`);
|
|
9068
9193
|
process.stdout.write("Commit proctor.config.json for this to take effect. proctor reads approvals from the committed config, so a change cannot approve itself.\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kavishdua/proctor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Catch AI agents gaming their test suites: deleted tests, skipped tests, weakened assertions, hardcoded outputs",
|
|
5
5
|
"homepage": "https://github.com/catfish-1234/proctor#readme",
|
|
6
6
|
"bugs": {
|