@webpieces/ai-hook-rules 0.4.704 → 0.4.706

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.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * THE THREE PROCESS COUPLINGS OF THE HOOK — as injectable seams.
3
+ *
4
+ * Why they exist at all: the hook's real contract is a PIPELINE — `stdin -> parse -> adapter -> runner
5
+ * -> emit -> exit` — and until this file existed there was no way to drive that pipeline from a test.
6
+ * `runMain` read `process.stdin` itself and the emit boundary called `process.stdout.write` /
7
+ * `process.exit` directly, so the only testable units were the pure functions UNDERNEATH the pipeline.
8
+ * Tests therefore sat too low: they moved every time the composition was refactored (the AgentHookEvent
9
+ * refactor rewrote `agent-response.spec.ts`'s call sites wholesale) and they proved nothing about the
10
+ * composed bytes.
11
+ *
12
+ * The boundary is cut JUST ABOVE THE INJECTION POINT: production is
13
+ * `new Container({autobind:true}).get(HookApp).run(new HookArgs('guards'))`, and a test builds the same
14
+ * container with these three classes rebound to doubles. That is the whole difference between the two.
15
+ * NOT a spawned process (too high — slow, and nothing can be substituted), NOT the pure helpers
16
+ * underneath (too low — they move under every refactor).
17
+ *
18
+ * THREE, and no more. The rule engine, the adapters, the filesystem and git are NEVER substituted —
19
+ * substituting any of them would leave the golden tests asserting the shape of the test harness rather
20
+ * than the shape of the pipeline, which is the failure mode this whole seam exists to escape.
21
+ *
22
+ * They also stay OFF the L0 recovery path. `src/bin/install-entry.ts` and `src/bin/shim.ts` import
23
+ * nothing but `fs`/`path` on purpose, so a corrupt `node_modules` still yields a clean fault code
24
+ * instead of a crash; an inversify import reachable from either would turn a diagnosable fault into an
25
+ * opaque one. The container lives in the two hook entry points and HookApp, and nowhere else.
26
+ *
27
+ * CLASSES, not interfaces, and that is the DI convention winning over the "behavior is an interface"
28
+ * rule: this repo injects BY TYPE and forbids `Symbol()` DI tokens (`no-symbol-di-tokens`), and an
29
+ * interface is not a runtime value, so it cannot BE the token. A test double therefore `extends` the
30
+ * port and overrides the one method. See CLAUDE.md section 5.
31
+ */
32
+ export declare class HookStdinSource {
33
+ /**
34
+ * The PreToolUse payload, byte for byte as the harness wrote it.
35
+ *
36
+ * Moved here verbatim from `hook-core.readStdin()` — same encoding, same three listeners, same
37
+ * "resolve('') on error or on a TTY" behaviour, so an empty read still means "nothing to judge"
38
+ * and never a hang.
39
+ */
40
+ read(): Promise<string>;
41
+ }
42
+ export declare class HookStdoutSink {
43
+ /**
44
+ * The PreToolUse decision bytes. A deny is JSON + '\n' on STDOUT; an allow writes NOTHING — the
45
+ * protocol reads a silent exit 0 as "allow". See agent-response.ts for the wire shape itself.
46
+ */
47
+ write(bytes: string): void;
48
+ }
49
+ export declare class HookProcessExit {
50
+ /**
51
+ * The hook's exit code IS the Claude Code PreToolUse protocol: exit 0 + JSON on stdout is a deny,
52
+ * silent exit 0 is an allow, and exit 2 would make Claude ignore stdout entirely. So the exit is
53
+ * not incidental plumbing to be tidied away — it is the last byte of the contract, which is
54
+ * exactly why it is a port and not a bare call.
55
+ */
56
+ exit(code: number): void;
57
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HookProcessExit = exports.HookStdoutSink = exports.HookStdinSource = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const inversify_1 = require("inversify");
6
+ /**
7
+ * THE THREE PROCESS COUPLINGS OF THE HOOK — as injectable seams.
8
+ *
9
+ * Why they exist at all: the hook's real contract is a PIPELINE — `stdin -> parse -> adapter -> runner
10
+ * -> emit -> exit` — and until this file existed there was no way to drive that pipeline from a test.
11
+ * `runMain` read `process.stdin` itself and the emit boundary called `process.stdout.write` /
12
+ * `process.exit` directly, so the only testable units were the pure functions UNDERNEATH the pipeline.
13
+ * Tests therefore sat too low: they moved every time the composition was refactored (the AgentHookEvent
14
+ * refactor rewrote `agent-response.spec.ts`'s call sites wholesale) and they proved nothing about the
15
+ * composed bytes.
16
+ *
17
+ * The boundary is cut JUST ABOVE THE INJECTION POINT: production is
18
+ * `new Container({autobind:true}).get(HookApp).run(new HookArgs('guards'))`, and a test builds the same
19
+ * container with these three classes rebound to doubles. That is the whole difference between the two.
20
+ * NOT a spawned process (too high — slow, and nothing can be substituted), NOT the pure helpers
21
+ * underneath (too low — they move under every refactor).
22
+ *
23
+ * THREE, and no more. The rule engine, the adapters, the filesystem and git are NEVER substituted —
24
+ * substituting any of them would leave the golden tests asserting the shape of the test harness rather
25
+ * than the shape of the pipeline, which is the failure mode this whole seam exists to escape.
26
+ *
27
+ * They also stay OFF the L0 recovery path. `src/bin/install-entry.ts` and `src/bin/shim.ts` import
28
+ * nothing but `fs`/`path` on purpose, so a corrupt `node_modules` still yields a clean fault code
29
+ * instead of a crash; an inversify import reachable from either would turn a diagnosable fault into an
30
+ * opaque one. The container lives in the two hook entry points and HookApp, and nowhere else.
31
+ *
32
+ * CLASSES, not interfaces, and that is the DI convention winning over the "behavior is an interface"
33
+ * rule: this repo injects BY TYPE and forbids `Symbol()` DI tokens (`no-symbol-di-tokens`), and an
34
+ * interface is not a runtime value, so it cannot BE the token. A test double therefore `extends` the
35
+ * port and overrides the one method. See CLAUDE.md section 5.
36
+ */
37
+ let HookStdinSource = class HookStdinSource {
38
+ /**
39
+ * The PreToolUse payload, byte for byte as the harness wrote it.
40
+ *
41
+ * Moved here verbatim from `hook-core.readStdin()` — same encoding, same three listeners, same
42
+ * "resolve('') on error or on a TTY" behaviour, so an empty read still means "nothing to judge"
43
+ * and never a hang.
44
+ */
45
+ read() {
46
+ return new Promise((resolve) => {
47
+ let data = '';
48
+ process.stdin.setEncoding('utf8');
49
+ process.stdin.on('data', (chunk) => { data += chunk; });
50
+ process.stdin.on('end', () => { resolve(data); });
51
+ process.stdin.on('error', () => { resolve(''); });
52
+ if (process.stdin.isTTY)
53
+ resolve('');
54
+ });
55
+ }
56
+ };
57
+ exports.HookStdinSource = HookStdinSource;
58
+ exports.HookStdinSource = HookStdinSource = tslib_1.__decorate([
59
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
60
+ ], HookStdinSource);
61
+ let HookStdoutSink = class HookStdoutSink {
62
+ /**
63
+ * The PreToolUse decision bytes. A deny is JSON + '\n' on STDOUT; an allow writes NOTHING — the
64
+ * protocol reads a silent exit 0 as "allow". See agent-response.ts for the wire shape itself.
65
+ */
66
+ write(bytes) {
67
+ process.stdout.write(bytes);
68
+ }
69
+ };
70
+ exports.HookStdoutSink = HookStdoutSink;
71
+ exports.HookStdoutSink = HookStdoutSink = tslib_1.__decorate([
72
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
73
+ ], HookStdoutSink);
74
+ let HookProcessExit = class HookProcessExit {
75
+ /**
76
+ * The hook's exit code IS the Claude Code PreToolUse protocol: exit 0 + JSON on stdout is a deny,
77
+ * silent exit 0 is an allow, and exit 2 would make Claude ignore stdout entirely. So the exit is
78
+ * not incidental plumbing to be tidied away — it is the last byte of the contract, which is
79
+ * exactly why it is a port and not a bare call.
80
+ */
81
+ exit(code) {
82
+ // webpieces-disable no-process-exit-outside-main -- hook exit-code IS the Claude Code PreToolUse protocol (exit 0 + JSON = the contract); this class is the designated terminal boundary, injected so a test can record the code instead of killing the worker.
83
+ process.exit(code);
84
+ }
85
+ };
86
+ exports.HookProcessExit = HookProcessExit;
87
+ exports.HookProcessExit = HookProcessExit = tslib_1.__decorate([
88
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
89
+ ], HookProcessExit);
90
+ //# sourceMappingURL=hook-ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-ports.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/hook-ports.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAe;IACxB;;;;;;OAMG;IACH,IAAI;QACA,OAAO,IAAI,OAAO,CAAC,CAAC,OAAgC,EAAE,EAAE;YACpD,IAAI,IAAI,GAAG,EAAE,CAAC;YACd,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YAClC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAChE,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAClD,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAClD,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK;gBAAE,OAAO,CAAC,EAAE,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AAlBY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,eAAe,CAkB3B;AAGM,IAAM,cAAc,GAApB,MAAM,cAAc;IACvB;;;OAGG;IACH,KAAK,CAAC,KAAa;QACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;CACJ,CAAA;AARY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,cAAc,CAQ1B;AAGM,IAAM,eAAe,GAArB,MAAM,eAAe;IACxB;;;;;OAKG;IACH,IAAI,CAAC,IAAY;QACb,gQAAgQ;QAChQ,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;CACJ,CAAA;AAXY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,eAAe,CAW3B","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * THE THREE PROCESS COUPLINGS OF THE HOOK — as injectable seams.\n *\n * Why they exist at all: the hook's real contract is a PIPELINE — `stdin -> parse -> adapter -> runner\n * -> emit -> exit` — and until this file existed there was no way to drive that pipeline from a test.\n * `runMain` read `process.stdin` itself and the emit boundary called `process.stdout.write` /\n * `process.exit` directly, so the only testable units were the pure functions UNDERNEATH the pipeline.\n * Tests therefore sat too low: they moved every time the composition was refactored (the AgentHookEvent\n * refactor rewrote `agent-response.spec.ts`'s call sites wholesale) and they proved nothing about the\n * composed bytes.\n *\n * The boundary is cut JUST ABOVE THE INJECTION POINT: production is\n * `new Container({autobind:true}).get(HookApp).run(new HookArgs('guards'))`, and a test builds the same\n * container with these three classes rebound to doubles. That is the whole difference between the two.\n * NOT a spawned process (too high — slow, and nothing can be substituted), NOT the pure helpers\n * underneath (too low — they move under every refactor).\n *\n * THREE, and no more. The rule engine, the adapters, the filesystem and git are NEVER substituted —\n * substituting any of them would leave the golden tests asserting the shape of the test harness rather\n * than the shape of the pipeline, which is the failure mode this whole seam exists to escape.\n *\n * They also stay OFF the L0 recovery path. `src/bin/install-entry.ts` and `src/bin/shim.ts` import\n * nothing but `fs`/`path` on purpose, so a corrupt `node_modules` still yields a clean fault code\n * instead of a crash; an inversify import reachable from either would turn a diagnosable fault into an\n * opaque one. The container lives in the two hook entry points and HookApp, and nowhere else.\n *\n * CLASSES, not interfaces, and that is the DI convention winning over the \"behavior is an interface\"\n * rule: this repo injects BY TYPE and forbids `Symbol()` DI tokens (`no-symbol-di-tokens`), and an\n * interface is not a runtime value, so it cannot BE the token. A test double therefore `extends` the\n * port and overrides the one method. See CLAUDE.md section 5.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class HookStdinSource {\n /**\n * The PreToolUse payload, byte for byte as the harness wrote it.\n *\n * Moved here verbatim from `hook-core.readStdin()` — same encoding, same three listeners, same\n * \"resolve('') on error or on a TTY\" behaviour, so an empty read still means \"nothing to judge\"\n * and never a hang.\n */\n read(): Promise<string> {\n return new Promise((resolve: (value: string) => void) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => { data += chunk; });\n process.stdin.on('end', () => { resolve(data); });\n process.stdin.on('error', () => { resolve(''); });\n if (process.stdin.isTTY) resolve('');\n });\n }\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class HookStdoutSink {\n /**\n * The PreToolUse decision bytes. A deny is JSON + '\\n' on STDOUT; an allow writes NOTHING — the\n * protocol reads a silent exit 0 as \"allow\". See agent-response.ts for the wire shape itself.\n */\n write(bytes: string): void {\n process.stdout.write(bytes);\n }\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class HookProcessExit {\n /**\n * The hook's exit code IS the Claude Code PreToolUse protocol: exit 0 + JSON on stdout is a deny,\n * silent exit 0 is an allow, and exit 2 would make Claude ignore stdout entirely. So the exit is\n * not incidental plumbing to be tidied away — it is the last byte of the contract, which is\n * exactly why it is a port and not a bare call.\n */\n exit(code: number): void {\n // webpieces-disable no-process-exit-outside-main -- hook exit-code IS the Claude Code PreToolUse protocol (exit 0 + JSON = the contract); this class is the designated terminal boundary, injected so a test can record the code instead of killing the worker.\n process.exit(code);\n }\n}\n"]}
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
+ import 'reflect-metadata';
2
3
  export declare function main(): Promise<void>;
@@ -2,13 +2,27 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.main = main;
5
- // Claude Code PreToolUse adapter for the CODE-STYLE RULES hook (matcher Write|Edit|MultiEdit).
5
+ // Claude Code / Codex PreToolUse adapter for the CODE-STYLE RULES hook (matcher Write|Edit|MultiEdit).
6
6
  // Bash payloads pass through untouched — branch/PR/merge protection is the separate guards hook.
7
- const hook_core_1 = require("./hook-core");
8
- function main() {
9
- return (0, hook_core_1.runMain)('rules');
7
+ //
8
+ // COMPOSITION ROOT, and deliberately nothing else — see guards-hook.ts. The only difference between
9
+ // the two binaries is the HookArgs constructed here.
10
+ require("reflect-metadata");
11
+ const inversify_1 = require("inversify");
12
+ const hook_app_1 = require("./hook-app");
13
+ const hook_outcome_1 = require("./hook-outcome");
14
+ // webpieces-disable no-function-outside-class -- this IS the bin's process entry point, named in package.json `exports` as ./claude-code-guards / ./claude-code-rules; a class here would be a namespace around one call and could not be the module's callable entry.
15
+ async function main() {
16
+ const container = new inversify_1.Container({ autobind: true });
17
+ const app = container.get(hook_app_1.HookApp);
18
+ await app.run(new hook_outcome_1.HookArgs('rules'));
10
19
  }
20
+ // `.catch` and not a bare `void main()`: a container that cannot be built throws BEFORE any HookApp
21
+ // exists, and an unhandled rejection exits non-zero — which PreToolUse reads as a non-blocking error
22
+ // and lets the tool call through. See HookBootFailure. `main` is async so a synchronous throw inside it
23
+ // arrives here as a rejection too.
11
24
  if (require.main === module) {
12
- void main();
25
+ // webpieces-disable no-any-unknown -- a rejection value is `unknown` by construction; HookBootFailure narrows it through toError(), which is the one place that job belongs
26
+ void main().catch((err) => { new hook_app_1.HookBootFailure().report(err); });
13
27
  }
14
28
  //# sourceMappingURL=rules-hook.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules-hook.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/rules-hook.ts"],"names":[],"mappings":";;;AAKA,oBAEC;AAND,+FAA+F;AAC/F,iGAAiG;AACjG,2CAAsC;AAEtC,SAAgB,IAAI;IAChB,OAAO,IAAA,mBAAO,EAAC,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC1B,KAAK,IAAI,EAAE,CAAC;AAChB,CAAC","sourcesContent":["#!/usr/bin/env node\n// Claude Code PreToolUse adapter for the CODE-STYLE RULES hook (matcher Write|Edit|MultiEdit).\n// Bash payloads pass through untouched — branch/PR/merge protection is the separate guards hook.\nimport { runMain } from './hook-core';\n\nexport function main(): Promise<void> {\n return runMain('rules');\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
1
+ {"version":3,"file":"rules-hook.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/rules-hook.ts"],"names":[],"mappings":";;;AAaA,oBAIC;AAhBD,uGAAuG;AACvG,iGAAiG;AACjG,EAAE;AACF,oGAAoG;AACpG,qDAAqD;AACrD,4BAA0B;AAC1B,yCAAsC;AAEtC,yCAAsD;AACtD,iDAA0C;AAE1C,uQAAuQ;AAChQ,KAAK,UAAU,IAAI;IACtB,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,kBAAO,CAAC,CAAC;IACnC,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,uBAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,oGAAoG;AACpG,qGAAqG;AACrG,wGAAwG;AACxG,mCAAmC;AACnC,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC1B,4KAA4K;IAC5K,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAQ,EAAE,GAAG,IAAI,0BAAe,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACtF,CAAC","sourcesContent":["#!/usr/bin/env node\n// Claude Code / Codex PreToolUse adapter for the CODE-STYLE RULES hook (matcher Write|Edit|MultiEdit).\n// Bash payloads pass through untouched — branch/PR/merge protection is the separate guards hook.\n//\n// COMPOSITION ROOT, and deliberately nothing else — see guards-hook.ts. The only difference between\n// the two binaries is the HookArgs constructed here.\nimport 'reflect-metadata';\nimport { Container } from 'inversify';\n\nimport { HookApp, HookBootFailure } from './hook-app';\nimport { HookArgs } from './hook-outcome';\n\n// webpieces-disable no-function-outside-class -- this IS the bin's process entry point, named in package.json `exports` as ./claude-code-guards / ./claude-code-rules; a class here would be a namespace around one call and could not be the module's callable entry.\nexport async function main(): Promise<void> {\n const container = new Container({ autobind: true });\n const app = container.get(HookApp);\n await app.run(new HookArgs('rules'));\n}\n\n// `.catch` and not a bare `void main()`: a container that cannot be built throws BEFORE any HookApp\n// exists, and an unhandled rejection exits non-zero — which PreToolUse reads as a non-blocking error\n// and lets the tool call through. See HookBootFailure. `main` is async so a synchronous throw inside it\n// arrives here as a rejection too.\nif (require.main === module) {\n // webpieces-disable no-any-unknown -- a rejection value is `unknown` by construction; HookBootFailure narrows it through toError(), which is the one place that job belongs\n void main().catch((err: unknown): void => { new HookBootFailure().report(err); });\n}\n"]}