@webpieces/ai-hook-rules 0.4.458 → 0.4.460
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/ai-hook-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.460",
|
|
4
4
|
"description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"directory": "packages/tooling/ai-hook-rules"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@webpieces/rules-config": "0.4.
|
|
35
|
+
"@webpieces/rules-config": "0.4.460"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -48,4 +48,20 @@ export declare class ShimTestkit {
|
|
|
48
48
|
bashPayload(command: string): string;
|
|
49
49
|
/** True when `cmd` matches a POSIX ERE, judged by the SAME `grep -E` the shim itself runs. */
|
|
50
50
|
ereMatches(ere: string, cmd: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Which of `cmds` that same `grep -E` matches — answered in ONE grep process for the whole batch.
|
|
53
|
+
*
|
|
54
|
+
* grep is a line matcher, so feeding N commands as N lines asks exactly the question `-Eq` answers
|
|
55
|
+
* per command; the engine, the ERE and the anchors are unchanged. What changes is cost: a process
|
|
56
|
+
* spawn is ~5ms on an idle machine but ~100ms when the suite runs projects in parallel, so a
|
|
57
|
+
* 16-command twin check used to be 16 spawns (~2s of pure spawn latency) for one grep pass. That
|
|
58
|
+
* is what made these files miss the per-test timeout under load.
|
|
59
|
+
*/
|
|
60
|
+
ereMatchSet(ere: string, cmds: readonly string[]): EreMatchSet;
|
|
61
|
+
}
|
|
62
|
+
/** The lines `grep -E` matched in one batched run — ask it per command with {@link matched}. */
|
|
63
|
+
export declare class EreMatchSet {
|
|
64
|
+
private readonly hits;
|
|
65
|
+
constructor(hits: ReadonlySet<string>);
|
|
66
|
+
matched(cmd: string): boolean;
|
|
51
67
|
}
|
package/src/bin/shim-testkit.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ShimTestkit = exports.ShimRun = exports.PreToolUseDecision = exports.HookSpecificOutput = void 0;
|
|
3
|
+
exports.EreMatchSet = exports.ShimTestkit = exports.ShimRun = exports.PreToolUseDecision = exports.HookSpecificOutput = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
6
|
const os = tslib_1.__importStar(require("os"));
|
|
@@ -89,8 +89,39 @@ class ShimTestkit {
|
|
|
89
89
|
}
|
|
90
90
|
/** True when `cmd` matches a POSIX ERE, judged by the SAME `grep -E` the shim itself runs. */
|
|
91
91
|
ereMatches(ere, cmd) {
|
|
92
|
-
return
|
|
92
|
+
return this.ereMatchSet(ere, [cmd]).matched(cmd);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Which of `cmds` that same `grep -E` matches — answered in ONE grep process for the whole batch.
|
|
96
|
+
*
|
|
97
|
+
* grep is a line matcher, so feeding N commands as N lines asks exactly the question `-Eq` answers
|
|
98
|
+
* per command; the engine, the ERE and the anchors are unchanged. What changes is cost: a process
|
|
99
|
+
* spawn is ~5ms on an idle machine but ~100ms when the suite runs projects in parallel, so a
|
|
100
|
+
* 16-command twin check used to be 16 spawns (~2s of pure spawn latency) for one grep pass. That
|
|
101
|
+
* is what made these files miss the per-test timeout under load.
|
|
102
|
+
*/
|
|
103
|
+
ereMatchSet(ere, cmds) {
|
|
104
|
+
for (const cmd of cmds) {
|
|
105
|
+
// A command carrying a newline would arrive at grep as TWO lines and be judged as two
|
|
106
|
+
// different commands — a silently wrong answer. Nothing in the allowlists does this.
|
|
107
|
+
if (cmd.includes('\n'))
|
|
108
|
+
throw new Error(`ereMatchSet cannot batch a multi-line command: ${JSON.stringify(cmd)}`);
|
|
109
|
+
}
|
|
110
|
+
const result = (0, child_process_1.spawnSync)('grep', ['-E', ere], { input: cmds.join('\n'), encoding: 'utf8' });
|
|
111
|
+
const hits = new Set((result.stdout ?? '').split('\n').filter((line) => line !== ''));
|
|
112
|
+
return new EreMatchSet(hits);
|
|
93
113
|
}
|
|
94
114
|
}
|
|
95
115
|
exports.ShimTestkit = ShimTestkit;
|
|
116
|
+
/** The lines `grep -E` matched in one batched run — ask it per command with {@link matched}. */
|
|
117
|
+
class EreMatchSet {
|
|
118
|
+
hits;
|
|
119
|
+
constructor(hits) {
|
|
120
|
+
this.hits = hits;
|
|
121
|
+
}
|
|
122
|
+
matched(cmd) {
|
|
123
|
+
return this.hits.has(cmd);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.EreMatchSet = EreMatchSet;
|
|
96
127
|
//# sourceMappingURL=shim-testkit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shim-testkit.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-testkit.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,+CAAyB;AACzB,mDAA6B;AAC7B,iDAA0C;AAC1C,iCAAoC;AAEpC;;;GAGG;AACH,MAAa,kBAAkB;IACC;IAA5B,YAA4B,wBAAgC;QAAhC,6BAAwB,GAAxB,wBAAwB,CAAQ;IAAG,CAAC;CACnE;AAFD,gDAEC;AAED,iEAAiE;AACjE,MAAa,kBAAkB;IACC;IAA5B,YAA4B,kBAAsC;QAAtC,uBAAkB,GAAlB,kBAAkB,CAAoB;IAAG,CAAC;CACzE;AAFD,gDAEC;AAED,8EAA8E;AAC9E,MAAa,OAAO;IAEI;IACA;IACA;IAHpB,YACoB,MAAqB,EACrB,MAAc,EACd,MAAc;QAFd,WAAM,GAAN,MAAM,CAAe;QACrB,WAAM,GAAN,MAAM,CAAQ;QACd,WAAM,GAAN,MAAM,CAAQ;IAC/B,CAAC;IAEJ,oDAAoD;IACpD,QAAQ;QACJ,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,6BAA6B,CAAC,CAAC;IAC/D,CAAC;IAED;;;OAGG;IACH,UAAU;QACN,8DAA8D;QAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAuB,CAAC;QAC/D,OAAO,QAAQ,CAAC,kBAAkB,CAAC,wBAAwB,CAAC;IAChE,CAAC;CACJ;AArBD,0BAqBC;AAED;;;;;;;;;;GAUG;AACH,MAAa,WAAW;IACpB,mDAAmD;IACnD,KAAK;QACD,OAAO,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,IAAY,EAAE,GAAW,EAAE,KAAa;QAC5C,4FAA4F;QAC5F,+FAA+F;QAC/F,mDAAmD;QACnD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;QACtE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,IAAA,iBAAU,GAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;QAC1D,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,MAAM,CAAC,GAAG,IAAA,yBAAS,EAAC,SAAS,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAChG,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IACrD,CAAC;IAED,6DAA6D;IAC7D,WAAW,CAAC,OAAe;QACvB,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;IAC1E,CAAC;IAED,8FAA8F;IAC9F,UAAU,CAAC,GAAW,EAAE,GAAW;QAC/B,OAAO,IAAA,yBAAS,EAAC,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"shim-testkit.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-testkit.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { spawnSync } from 'child_process';\nimport { renderShim } from './shim';\n\n/**\n * The shim's PreToolUse deny payload, as it prints it on stdout. Named (not an inline literal on the\n * JSON.parse cast) so the wire shape this testkit depends on is stated once, in one place.\n */\nexport class HookSpecificOutput {\n constructor(public readonly permissionDecisionReason: string) {}\n}\n\n/** The decision envelope wrapping {@link HookSpecificOutput}. */\nexport class PreToolUseDecision {\n constructor(public readonly hookSpecificOutput: HookSpecificOutput) {}\n}\n\n/** The outcome of one shim invocation. Data-only → a class, per CLAUDE.md. */\nexport class ShimRun {\n constructor(\n public readonly status: number | null,\n public readonly stdout: string,\n public readonly stderr: string,\n ) {}\n\n /** True when the shim emitted a PreToolUse deny. */\n isDenied(): boolean {\n return this.stdout.includes('\"permissionDecision\":\"deny\"');\n }\n\n /**\n * The deny REASON, parsed out of the PreToolUse JSON.\n * @throws if this run was not a deny (there is no reason to read).\n */\n denyReason(): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n const decision = JSON.parse(this.stdout) as PreToolUseDecision;\n return decision.hookSpecificOutput.permissionDecisionReason;\n }\n}\n\n/**\n * ShimTestkit — the shared harness for driving the rendered shim through a REAL /bin/sh.\n *\n * Extracted so setup.spec.ts and shim-drift.spec.ts drive the shim the SAME way instead of each\n * keeping its own copy: the shim's entire contract is \"what /bin/sh actually does with it\", so two\n * drifting harnesses would silently become two different contracts.\n *\n * An instance class (not module-scope functions) because this is normal source to the linter — only\n * *.spec.ts is exempt from no-function-outside-class, and a testkit should not need a disable comment\n * to exist.\n */\nexport class ShimTestkit {\n /** A throwaway repo root under the OS temp dir. */\n mktmp(): string {\n return fs.mkdtempSync(path.join(os.tmpdir(), 'wp-setup-'));\n }\n\n /**\n * Run the rendered shim exactly as Claude Code would: `sh <shim> <bin> ...`, from a repo cwd,\n * piping tool-payload JSON on stdin. spawnSync never throws on non-zero exit.\n */\n runShim(root: string, bin: string, stdin: string): ShimRun {\n // Place the shim at its REAL relative location (<root>/.claude/webpieces/ai-hook.sh) so its\n // self-location (`dirname $0/../..` → <root>) resolves the bin correctly. Run it from a SUBDIR\n // to prove it does not depend on the caller's cwd.\n const shimAbs = path.join(root, '.claude', 'webpieces', 'ai-hook.sh');\n fs.mkdirSync(path.dirname(shimAbs), { recursive: true });\n fs.writeFileSync(shimAbs, renderShim(), { mode: 0o755 });\n const subdir = path.join(root, 'packages', 'deep', 'sub');\n fs.mkdirSync(subdir, { recursive: true });\n const r = spawnSync('/bin/sh', [shimAbs, bin], { cwd: subdir, input: stdin, encoding: 'utf8' });\n return new ShimRun(r.status, r.stdout, r.stderr);\n }\n\n /** A Bash tool payload, as Claude Code sends it on stdin. */\n bashPayload(command: string): string {\n return JSON.stringify({ tool_name: 'Bash', tool_input: { command } });\n }\n\n /** True when `cmd` matches a POSIX ERE, judged by the SAME `grep -E` the shim itself runs. */\n ereMatches(ere: string, cmd: string): boolean {\n return this.ereMatchSet(ere, [cmd]).matched(cmd);\n }\n\n /**\n * Which of `cmds` that same `grep -E` matches — answered in ONE grep process for the whole batch.\n *\n * grep is a line matcher, so feeding N commands as N lines asks exactly the question `-Eq` answers\n * per command; the engine, the ERE and the anchors are unchanged. What changes is cost: a process\n * spawn is ~5ms on an idle machine but ~100ms when the suite runs projects in parallel, so a\n * 16-command twin check used to be 16 spawns (~2s of pure spawn latency) for one grep pass. That\n * is what made these files miss the per-test timeout under load.\n */\n ereMatchSet(ere: string, cmds: readonly string[]): EreMatchSet {\n for (const cmd of cmds) {\n // A command carrying a newline would arrive at grep as TWO lines and be judged as two\n // different commands — a silently wrong answer. Nothing in the allowlists does this.\n if (cmd.includes('\\n')) throw new Error(`ereMatchSet cannot batch a multi-line command: ${JSON.stringify(cmd)}`);\n }\n const result = spawnSync('grep', ['-E', ere], { input: cmds.join('\\n'), encoding: 'utf8' });\n const hits = new Set((result.stdout ?? '').split('\\n').filter((line: string): boolean => line !== ''));\n return new EreMatchSet(hits);\n }\n}\n\n/** The lines `grep -E` matched in one batched run — ask it per command with {@link matched}. */\nexport class EreMatchSet {\n private readonly hits: ReadonlySet<string>;\n\n constructor(hits: ReadonlySet<string>) {\n this.hits = hits;\n }\n\n matched(cmd: string): boolean {\n return this.hits.has(cmd);\n }\n}\n"]}
|