@mmnto/cli 1.34.3 → 1.36.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/dist/commands/eject.d.ts.map +1 -1
- package/dist/commands/eject.js +64 -2
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/eject.test.js +51 -0
- package/dist/commands/eject.test.js.map +1 -1
- package/dist/commands/hook-run.d.ts +91 -0
- package/dist/commands/hook-run.d.ts.map +1 -0
- package/dist/commands/hook-run.js +149 -0
- package/dist/commands/hook-run.js.map +1 -0
- package/dist/commands/hook-run.test.d.ts +2 -0
- package/dist/commands/hook-run.test.d.ts.map +1 -0
- package/dist/commands/hook-run.test.js +264 -0
- package/dist/commands/hook-run.test.js.map +1 -0
- package/dist/commands/hook-test.d.ts +29 -0
- package/dist/commands/hook-test.d.ts.map +1 -0
- package/dist/commands/hook-test.js +132 -0
- package/dist/commands/hook-test.js.map +1 -0
- package/dist/commands/init-templates.d.ts +11 -0
- package/dist/commands/init-templates.d.ts.map +1 -1
- package/dist/commands/init-templates.js +119 -0
- package/dist/commands/init-templates.js.map +1 -1
- package/dist/commands/init.d.ts +21 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +90 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +91 -2
- package/dist/commands/init.test.js.map +1 -1
- package/dist/hook/classification.d.ts +45 -0
- package/dist/hook/classification.d.ts.map +1 -0
- package/dist/hook/classification.js +24 -0
- package/dist/hook/classification.js.map +1 -0
- package/dist/hook/classification.test.d.ts +2 -0
- package/dist/hook/classification.test.d.ts.map +1 -0
- package/dist/hook/classification.test.js +40 -0
- package/dist/hook/classification.test.js.map +1 -0
- package/dist/hook/loader.d.ts +47 -0
- package/dist/hook/loader.d.ts.map +1 -0
- package/dist/hook/loader.js +66 -0
- package/dist/hook/loader.js.map +1 -0
- package/dist/hook/loader.test.d.ts +2 -0
- package/dist/hook/loader.test.d.ts.map +1 -0
- package/dist/hook/loader.test.js +205 -0
- package/dist/hook/loader.test.js.map +1 -0
- package/dist/hook/runtime.d.ts +47 -0
- package/dist/hook/runtime.d.ts.map +1 -0
- package/dist/hook/runtime.js +85 -0
- package/dist/hook/runtime.js.map +1 -0
- package/dist/hook/runtime.test.d.ts +2 -0
- package/dist/hook/runtime.test.d.ts.map +1 -0
- package/dist/hook/runtime.test.js +135 -0
- package/dist/hook/runtime.test.js.map +1 -0
- package/dist/hook/schema.d.ts +385 -0
- package/dist/hook/schema.d.ts.map +1 -0
- package/dist/hook/schema.js +164 -0
- package/dist/hook/schema.js.map +1 -0
- package/dist/hook/schema.test.d.ts +2 -0
- package/dist/hook/schema.test.d.ts.map +1 -0
- package/dist/hook/schema.test.js +233 -0
- package/dist/hook/schema.test.js.map +1 -0
- package/dist/hook/test-runner.d.ts +64 -0
- package/dist/hook/test-runner.d.ts.map +1 -0
- package/dist/hook/test-runner.js +57 -0
- package/dist/hook/test-runner.js.map +1 -0
- package/dist/hook/test-runner.test.d.ts +2 -0
- package/dist/hook/test-runner.test.d.ts.map +1 -0
- package/dist/hook/test-runner.test.js +237 -0
- package/dist/hook/test-runner.test.js.map +1 -0
- package/dist/index.js +57 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { loadCompiledHooks } from './loader.js';
|
|
6
|
+
let workDir;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-hook-loader-'));
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
function writeManifest(content) {
|
|
14
|
+
const manifestPath = path.join(workDir, 'compiled-hooks.json');
|
|
15
|
+
fs.writeFileSync(manifestPath, JSON.stringify(content), 'utf8');
|
|
16
|
+
return manifestPath;
|
|
17
|
+
}
|
|
18
|
+
const validRule = {
|
|
19
|
+
id: 'r1',
|
|
20
|
+
packId: '@mmnto/pack-bot-coderabbit',
|
|
21
|
+
trigger: { tool: 'bash', pattern: '.*' },
|
|
22
|
+
check: { pattern: 'x', type: 'reject-if-match' },
|
|
23
|
+
message: 'm',
|
|
24
|
+
};
|
|
25
|
+
describe('loadCompiledHooks', () => {
|
|
26
|
+
it('returns an empty result when the manifest file does not exist (fresh repo, ENOENT)', () => {
|
|
27
|
+
const result = loadCompiledHooks({
|
|
28
|
+
manifestPath: path.join(workDir, 'missing.json'),
|
|
29
|
+
installedPackVersions: {},
|
|
30
|
+
});
|
|
31
|
+
expect(result.hooks).toEqual([]);
|
|
32
|
+
expect(result.warnings).toEqual([]);
|
|
33
|
+
expect(result.errors).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
it('surfaces non-ENOENT read errors as HOOKS_LOAD_FAILED (does not pretend the file is missing)', () => {
|
|
36
|
+
// Reading a directory as a file produces EISDIR on POSIX / EBADF or
|
|
37
|
+
// ENOTSUP-flavoured failures on Windows. Whatever the platform-specific
|
|
38
|
+
// errno, it is NOT ENOENT — so the loader must surface it, not silently
|
|
39
|
+
// return the "manifest absent" result that the prior existsSync pre-check
|
|
40
|
+
// would have masked.
|
|
41
|
+
const dirAsManifest = path.join(workDir, 'a-directory');
|
|
42
|
+
fs.mkdirSync(dirAsManifest);
|
|
43
|
+
const result = loadCompiledHooks({
|
|
44
|
+
manifestPath: dirAsManifest,
|
|
45
|
+
installedPackVersions: {},
|
|
46
|
+
});
|
|
47
|
+
expect(result.hooks).toEqual([]);
|
|
48
|
+
expect(result.errors.length).toBe(1);
|
|
49
|
+
const err = result.errors[0];
|
|
50
|
+
expect(err.code).toBe('HOOKS_LOAD_FAILED');
|
|
51
|
+
expect(err.message).toContain('failed to read compiled-hooks manifest');
|
|
52
|
+
expect(err.cause).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
it('records a structural error on invalid JSON and preserves the original SyntaxError via cause', () => {
|
|
55
|
+
const manifestPath = path.join(workDir, 'compiled-hooks.json');
|
|
56
|
+
fs.writeFileSync(manifestPath, '{ not valid json', 'utf8');
|
|
57
|
+
const result = loadCompiledHooks({
|
|
58
|
+
manifestPath,
|
|
59
|
+
installedPackVersions: {},
|
|
60
|
+
});
|
|
61
|
+
expect(result.hooks).toEqual([]);
|
|
62
|
+
expect(result.errors.length).toBe(1);
|
|
63
|
+
const err = result.errors[0];
|
|
64
|
+
expect(err.message).toContain('not valid JSON');
|
|
65
|
+
expect(err.code).toBe('HOOKS_LOAD_FAILED');
|
|
66
|
+
// Original parse error preserved on `.cause` so debug consumers can
|
|
67
|
+
// walk the chain without the stack being collapsed into a string.
|
|
68
|
+
expect(err.cause).toBeInstanceOf(SyntaxError);
|
|
69
|
+
});
|
|
70
|
+
it('warns and skips when schemaVersion is higher than the runner supports', () => {
|
|
71
|
+
const manifestPath = writeManifest({
|
|
72
|
+
schemaVersion: 2,
|
|
73
|
+
compiledAt: '2026-05-11T18:43:00Z',
|
|
74
|
+
sourcePackVersions: {},
|
|
75
|
+
hooks: [],
|
|
76
|
+
});
|
|
77
|
+
const result = loadCompiledHooks({
|
|
78
|
+
manifestPath,
|
|
79
|
+
installedPackVersions: {},
|
|
80
|
+
});
|
|
81
|
+
expect(result.hooks).toEqual([]);
|
|
82
|
+
expect(result.errors).toEqual([]);
|
|
83
|
+
expect(result.warnings.length).toBe(1);
|
|
84
|
+
expect(result.warnings[0]).toContain('[totem:hook-schema]');
|
|
85
|
+
expect(result.warnings[0]).toContain('schemaVersion 2');
|
|
86
|
+
});
|
|
87
|
+
it('warns and skips when schemaVersion is missing', () => {
|
|
88
|
+
const manifestPath = writeManifest({
|
|
89
|
+
compiledAt: '2026-05-11T18:43:00Z',
|
|
90
|
+
sourcePackVersions: {},
|
|
91
|
+
hooks: [],
|
|
92
|
+
});
|
|
93
|
+
const result = loadCompiledHooks({
|
|
94
|
+
manifestPath,
|
|
95
|
+
installedPackVersions: {},
|
|
96
|
+
});
|
|
97
|
+
expect(result.hooks).toEqual([]);
|
|
98
|
+
expect(result.warnings.length).toBe(1);
|
|
99
|
+
expect(result.warnings[0]).toContain('[totem:hook-schema]');
|
|
100
|
+
});
|
|
101
|
+
it('records a structural error when the supported-version manifest fails schema validation', () => {
|
|
102
|
+
const manifestPath = writeManifest({
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
compiledAt: 'not-a-real-date',
|
|
105
|
+
sourcePackVersions: {},
|
|
106
|
+
hooks: [],
|
|
107
|
+
});
|
|
108
|
+
const result = loadCompiledHooks({
|
|
109
|
+
manifestPath,
|
|
110
|
+
installedPackVersions: {},
|
|
111
|
+
});
|
|
112
|
+
expect(result.hooks).toEqual([]);
|
|
113
|
+
expect(result.errors.length).toBe(1);
|
|
114
|
+
const err = result.errors[0];
|
|
115
|
+
expect(err.message).toContain('schema validation');
|
|
116
|
+
expect(err.code).toBe('HOOKS_LOAD_FAILED');
|
|
117
|
+
// Zod's ZodError preserved as the cause for debug-mode chain traversal.
|
|
118
|
+
expect(err.cause).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
it('returns hooks with no warnings when installed pack versions match compiled versions', () => {
|
|
121
|
+
const manifestPath = writeManifest({
|
|
122
|
+
schemaVersion: 1,
|
|
123
|
+
compiledAt: '2026-05-11T18:43:00Z',
|
|
124
|
+
sourcePackVersions: { '@mmnto/pack-bot-coderabbit': '1.0.0' },
|
|
125
|
+
hooks: [validRule],
|
|
126
|
+
});
|
|
127
|
+
const result = loadCompiledHooks({
|
|
128
|
+
manifestPath,
|
|
129
|
+
installedPackVersions: { '@mmnto/pack-bot-coderabbit': '1.0.0' },
|
|
130
|
+
});
|
|
131
|
+
expect(result.hooks).toHaveLength(1);
|
|
132
|
+
expect(result.warnings).toEqual([]);
|
|
133
|
+
expect(result.errors).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
it('emits a staleness warning when the installed pack version differs from compiled', () => {
|
|
136
|
+
const manifestPath = writeManifest({
|
|
137
|
+
schemaVersion: 1,
|
|
138
|
+
compiledAt: '2026-05-11T18:43:00Z',
|
|
139
|
+
sourcePackVersions: { '@mmnto/pack-bot-coderabbit': '1.0.0' },
|
|
140
|
+
hooks: [validRule],
|
|
141
|
+
});
|
|
142
|
+
const result = loadCompiledHooks({
|
|
143
|
+
manifestPath,
|
|
144
|
+
installedPackVersions: { '@mmnto/pack-bot-coderabbit': '1.1.0' },
|
|
145
|
+
});
|
|
146
|
+
expect(result.hooks).toHaveLength(1);
|
|
147
|
+
expect(result.warnings.length).toBe(1);
|
|
148
|
+
expect(result.warnings[0]).toContain('[totem:hook-stale]');
|
|
149
|
+
expect(result.warnings[0]).toContain('@mmnto/pack-bot-coderabbit');
|
|
150
|
+
expect(result.warnings[0]).toContain('compiled against 1.0.0, installed 1.1.0');
|
|
151
|
+
});
|
|
152
|
+
it('emits a staleness warning when a compiled-against pack is not installed at all', () => {
|
|
153
|
+
const manifestPath = writeManifest({
|
|
154
|
+
schemaVersion: 1,
|
|
155
|
+
compiledAt: '2026-05-11T18:43:00Z',
|
|
156
|
+
sourcePackVersions: { '@mmnto/pack-bot-coderabbit': '1.0.0' },
|
|
157
|
+
hooks: [validRule],
|
|
158
|
+
});
|
|
159
|
+
const result = loadCompiledHooks({
|
|
160
|
+
manifestPath,
|
|
161
|
+
installedPackVersions: {},
|
|
162
|
+
});
|
|
163
|
+
expect(result.hooks).toHaveLength(1);
|
|
164
|
+
expect(result.warnings.length).toBe(1);
|
|
165
|
+
expect(result.warnings[0]).toContain('not currently installed');
|
|
166
|
+
});
|
|
167
|
+
it('ignores extra installed packs not in sourcePackVersions (no warning)', () => {
|
|
168
|
+
// A pack installed after the last `totem sync` is benign — its hooks
|
|
169
|
+
// are not yet active, but that is not staleness in the compiled set.
|
|
170
|
+
const manifestPath = writeManifest({
|
|
171
|
+
schemaVersion: 1,
|
|
172
|
+
compiledAt: '2026-05-11T18:43:00Z',
|
|
173
|
+
sourcePackVersions: { '@mmnto/pack-bot-coderabbit': '1.0.0' },
|
|
174
|
+
hooks: [validRule],
|
|
175
|
+
});
|
|
176
|
+
const result = loadCompiledHooks({
|
|
177
|
+
manifestPath,
|
|
178
|
+
installedPackVersions: {
|
|
179
|
+
'@mmnto/pack-bot-coderabbit': '1.0.0',
|
|
180
|
+
'@mmnto/pack-bot-gemini-code-assist': '1.0.0',
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
expect(result.warnings).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
it('emits one staleness warning per drifting pack', () => {
|
|
186
|
+
const manifestPath = writeManifest({
|
|
187
|
+
schemaVersion: 1,
|
|
188
|
+
compiledAt: '2026-05-11T18:43:00Z',
|
|
189
|
+
sourcePackVersions: {
|
|
190
|
+
'@mmnto/pack-bot-coderabbit': '1.0.0',
|
|
191
|
+
'@mmnto/pack-bot-gemini-code-assist': '2.0.0',
|
|
192
|
+
},
|
|
193
|
+
hooks: [validRule],
|
|
194
|
+
});
|
|
195
|
+
const result = loadCompiledHooks({
|
|
196
|
+
manifestPath,
|
|
197
|
+
installedPackVersions: {
|
|
198
|
+
'@mmnto/pack-bot-coderabbit': '1.1.0',
|
|
199
|
+
'@mmnto/pack-bot-gemini-code-assist': '2.0.1',
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
expect(result.warnings.length).toBe(2);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
//# sourceMappingURL=loader.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.test.js","sourceRoot":"","sources":["../../src/hook/loader.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CompiledHookRule } from './schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Hook runtime evaluator (ADR-104 § Decisions 1, 2 + § Convergence).
|
|
4
|
+
*
|
|
5
|
+
* Takes a single hook rule plus a tool-call payload (tool name + tool args
|
|
6
|
+
* as a single string) and returns a structured allow/reject decision. The
|
|
7
|
+
* runtime is deterministic Node.js — no LLM calls in this path (Tenet 15
|
|
8
|
+
* corollary, ADR-103 § 8).
|
|
9
|
+
*
|
|
10
|
+
* V1 matcher class is regex-only per execution plan § 4. ast-grep and
|
|
11
|
+
* other matcher classes are deferred to V2; the schema permits future
|
|
12
|
+
* `verification_shadow` blocks but the V1 runtime ignores them.
|
|
13
|
+
*/
|
|
14
|
+
export interface ToolCallPayload {
|
|
15
|
+
/** The tool the agent is attempting to invoke (e.g. "bash"). */
|
|
16
|
+
tool: string;
|
|
17
|
+
/** Serialized tool arguments. For bash this is the command string;
|
|
18
|
+
* for structured tools, callers serialize to a stable string form. */
|
|
19
|
+
args: string;
|
|
20
|
+
}
|
|
21
|
+
export type AllowDecision = {
|
|
22
|
+
decision: 'allow';
|
|
23
|
+
};
|
|
24
|
+
export type RejectDecision = {
|
|
25
|
+
decision: 'reject';
|
|
26
|
+
message: string;
|
|
27
|
+
packId: string;
|
|
28
|
+
ruleId: string;
|
|
29
|
+
recoveryHint?: string;
|
|
30
|
+
};
|
|
31
|
+
export type HookDecision = AllowDecision | RejectDecision;
|
|
32
|
+
/**
|
|
33
|
+
* Build the structured rejection message per ADR-104 § Decision 1:
|
|
34
|
+
*
|
|
35
|
+
* [totem:hook-block] <packId>/<ruleId>: <message>
|
|
36
|
+
* → <recoveryHint>
|
|
37
|
+
*
|
|
38
|
+
* The `→ <recoveryHint>` line is omitted when no recoveryHint is provided.
|
|
39
|
+
* Agents and operators grep for the `[totem:hook-block]` prefix; the
|
|
40
|
+
* `<packId>/<ruleId>` carries provenance.
|
|
41
|
+
*
|
|
42
|
+
* Parameter is narrowed to `RejectDecision` so the type system prevents
|
|
43
|
+
* passing an allow decision — no runtime guard needed.
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatRejection(decision: RejectDecision): string;
|
|
46
|
+
export declare function evaluateHook(rule: CompiledHookRule, payload: ToolCallPayload): HookDecision;
|
|
47
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/hook/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb;2EACuE;IACvE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,cAAc,CAAC;AAE1D;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAMhE;AA4CD,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,eAAe,GAAG,YAAY,CA4B3F"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the structured rejection message per ADR-104 § Decision 1:
|
|
3
|
+
*
|
|
4
|
+
* [totem:hook-block] <packId>/<ruleId>: <message>
|
|
5
|
+
* → <recoveryHint>
|
|
6
|
+
*
|
|
7
|
+
* The `→ <recoveryHint>` line is omitted when no recoveryHint is provided.
|
|
8
|
+
* Agents and operators grep for the `[totem:hook-block]` prefix; the
|
|
9
|
+
* `<packId>/<ruleId>` carries provenance.
|
|
10
|
+
*
|
|
11
|
+
* Parameter is narrowed to `RejectDecision` so the type system prevents
|
|
12
|
+
* passing an allow decision — no runtime guard needed.
|
|
13
|
+
*/
|
|
14
|
+
export function formatRejection(decision) {
|
|
15
|
+
const header = `[totem:hook-block] ${decision.packId}/${decision.ruleId}: ${decision.message}`;
|
|
16
|
+
if (decision.recoveryHint) {
|
|
17
|
+
return `${header}\n → ${decision.recoveryHint}`;
|
|
18
|
+
}
|
|
19
|
+
return header;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Evaluate a single compiled hook rule against a tool-call payload.
|
|
23
|
+
*
|
|
24
|
+
* Two-stage gate:
|
|
25
|
+
* 1. Trigger gate: does this rule apply to this tool + args?
|
|
26
|
+
* Rule applies when `rule.trigger.tool` equals `payload.tool` AND
|
|
27
|
+
* `rule.trigger.pattern` matches `payload.args`.
|
|
28
|
+
* 2. Check gate: when the trigger matches, apply `rule.check.pattern` to
|
|
29
|
+
* args. `reject-if-match` rejects on match; `reject-if-no-match`
|
|
30
|
+
* rejects on non-match.
|
|
31
|
+
*
|
|
32
|
+
* Returns `{ decision: 'allow' }` when either gate passes the call through.
|
|
33
|
+
*
|
|
34
|
+
* V1 invariant: regex matching only. Future matcher classes (ast-grep,
|
|
35
|
+
* Rego-shadow) ship in V2 follow-on ADRs.
|
|
36
|
+
*
|
|
37
|
+
* Per ADR-104 § Convergence, any `verification_shadow` block on the rule
|
|
38
|
+
* is silently ignored at the runtime layer (V1 hooks are Interpretive
|
|
39
|
+
* Rule class — no formal-verification obligation). Warn-and-ignore of
|
|
40
|
+
* verification_shadow happens at the load layer when compiling pack
|
|
41
|
+
* hooks.yaml, not on every hook-run invocation.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* Memoizes compiled RegExp instances keyed by pattern string. Pack rule
|
|
45
|
+
* patterns are stable for the lifetime of a CLI process (the manifest is
|
|
46
|
+
* loaded once at startup), so a string-keyed cache is sufficient — there
|
|
47
|
+
* is no unbounded-growth concern in the single-shot CLI use case.
|
|
48
|
+
*
|
|
49
|
+
* Safe to memoize because the patterns are compiled with no flags, so the
|
|
50
|
+
* returned RegExp has no stateful `lastIndex` carry-over between `.test()`
|
|
51
|
+
* calls.
|
|
52
|
+
*/
|
|
53
|
+
const regexCache = new Map();
|
|
54
|
+
function getCompiledRegex(pattern) {
|
|
55
|
+
const cached = regexCache.get(pattern);
|
|
56
|
+
if (cached)
|
|
57
|
+
return cached;
|
|
58
|
+
const compiled = new RegExp(pattern);
|
|
59
|
+
regexCache.set(pattern, compiled);
|
|
60
|
+
return compiled;
|
|
61
|
+
}
|
|
62
|
+
export function evaluateHook(rule, payload) {
|
|
63
|
+
if (rule.trigger.tool !== payload.tool) {
|
|
64
|
+
return { decision: 'allow' };
|
|
65
|
+
}
|
|
66
|
+
const triggerRegex = getCompiledRegex(rule.trigger.pattern);
|
|
67
|
+
if (!triggerRegex.test(payload.args)) {
|
|
68
|
+
return { decision: 'allow' };
|
|
69
|
+
}
|
|
70
|
+
const checkRegex = getCompiledRegex(rule.check.pattern);
|
|
71
|
+
const matched = checkRegex.test(payload.args);
|
|
72
|
+
const shouldReject = (rule.check.type === 'reject-if-match' && matched) ||
|
|
73
|
+
(rule.check.type === 'reject-if-no-match' && !matched);
|
|
74
|
+
if (!shouldReject) {
|
|
75
|
+
return { decision: 'allow' };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
decision: 'reject',
|
|
79
|
+
message: rule.message,
|
|
80
|
+
packId: rule.packId,
|
|
81
|
+
ruleId: rule.id,
|
|
82
|
+
recoveryHint: rule.recoveryHint,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/hook/runtime.ts"],"names":[],"mappings":"AAmCA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,QAAwB;IACtD,MAAM,MAAM,GAAG,sBAAsB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC/F,IAAI,QAAQ,CAAC,YAAY,EAAE,CAAC;QAC1B,OAAO,GAAG,MAAM,SAAS,QAAQ,CAAC,YAAY,EAAE,CAAC;IACnD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;;;;;;;;GASG;AACH,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;AAE7C,SAAS,gBAAgB,CAAC,OAAe;IACvC,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;IACrC,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAClC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAsB,EAAE,OAAwB;IAC3E,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC;QACvC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC/B,CAAC;IAED,MAAM,YAAY,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC/B,CAAC;IAED,MAAM,UAAU,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9C,MAAM,YAAY,GAChB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,iBAAiB,IAAI,OAAO,CAAC;QAClD,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,oBAAoB,IAAI,CAAC,OAAO,CAAC,CAAC;IAEzD,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC/B,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,QAAQ;QAClB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,IAAI,CAAC,EAAE;QACf,YAAY,EAAE,IAAI,CAAC,YAAY;KAChC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.test.d.ts","sourceRoot":"","sources":["../../src/hook/runtime.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { evaluateHook, formatRejection } from './runtime.js';
|
|
3
|
+
function rule(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
id: 'gca-tag-xor-command',
|
|
6
|
+
packId: '@mmnto/pack-bot-gemini-code-assist',
|
|
7
|
+
trigger: { tool: 'bash', pattern: 'gh\\s+(pr|issue)\\s+comment' },
|
|
8
|
+
check: {
|
|
9
|
+
pattern: '(?=.*@gemini-code-assist)(?=.*\\/gemini review)',
|
|
10
|
+
type: 'reject-if-match',
|
|
11
|
+
},
|
|
12
|
+
message: 'GCA tag XOR command — never both; doubling wastes GCA quota.',
|
|
13
|
+
recoveryHint: 'Choose one: @-mention to comment, /gemini review for fresh review.',
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
describe('evaluateHook', () => {
|
|
18
|
+
describe('trigger gate', () => {
|
|
19
|
+
it('allows when payload.tool does not match rule.trigger.tool', () => {
|
|
20
|
+
const result = evaluateHook(rule(), { tool: 'write', args: 'whatever' });
|
|
21
|
+
expect(result.decision).toBe('allow');
|
|
22
|
+
});
|
|
23
|
+
it('allows when payload.tool matches but trigger.pattern does not', () => {
|
|
24
|
+
// Trigger requires `gh pr comment` or `gh issue comment`; this is `gh pr list`.
|
|
25
|
+
const result = evaluateHook(rule(), { tool: 'bash', args: 'gh pr list' });
|
|
26
|
+
expect(result.decision).toBe('allow');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('check gate — reject-if-match', () => {
|
|
30
|
+
it('rejects when both trigger and check patterns match', () => {
|
|
31
|
+
const args = 'gh pr comment 123 --body "@gemini-code-assist /gemini review please"';
|
|
32
|
+
const result = evaluateHook(rule(), { tool: 'bash', args });
|
|
33
|
+
expect(result.decision).toBe('reject');
|
|
34
|
+
if (result.decision === 'reject') {
|
|
35
|
+
expect(result.message).toMatch(/GCA tag XOR/);
|
|
36
|
+
expect(result.packId).toBe('@mmnto/pack-bot-gemini-code-assist');
|
|
37
|
+
expect(result.ruleId).toBe('gca-tag-xor-command');
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
it('allows when trigger matches but check does not (only one of the two tags present)', () => {
|
|
41
|
+
// Trigger matches `gh pr comment` but the check requires BOTH the
|
|
42
|
+
// @mention AND the slash command — this comment has only the mention.
|
|
43
|
+
const args = 'gh pr comment 123 --body "@gemini-code-assist take a look"';
|
|
44
|
+
const result = evaluateHook(rule(), { tool: 'bash', args });
|
|
45
|
+
expect(result.decision).toBe('allow');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('check gate — reject-if-no-match', () => {
|
|
49
|
+
it('rejects when trigger matches but the required check pattern is absent', () => {
|
|
50
|
+
const r = rule({
|
|
51
|
+
id: 'requires-fixes-block',
|
|
52
|
+
check: { pattern: '## Fixes', type: 'reject-if-no-match' },
|
|
53
|
+
trigger: { tool: 'bash', pattern: 'git\\s+commit' },
|
|
54
|
+
message: 'Commit message must include a `## Fixes` section.',
|
|
55
|
+
});
|
|
56
|
+
const result = evaluateHook(r, { tool: 'bash', args: 'git commit -m "wip"' });
|
|
57
|
+
expect(result.decision).toBe('reject');
|
|
58
|
+
});
|
|
59
|
+
it('allows when the required check pattern is present', () => {
|
|
60
|
+
const r = rule({
|
|
61
|
+
check: { pattern: '## Fixes', type: 'reject-if-no-match' },
|
|
62
|
+
trigger: { tool: 'bash', pattern: 'git\\s+commit' },
|
|
63
|
+
});
|
|
64
|
+
const result = evaluateHook(r, {
|
|
65
|
+
tool: 'bash',
|
|
66
|
+
args: 'git commit -m "feat: x\\n\\n## Fixes\\n- y"',
|
|
67
|
+
});
|
|
68
|
+
expect(result.decision).toBe('allow');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('reject decision payload', () => {
|
|
72
|
+
it('carries packId and ruleId for provenance', () => {
|
|
73
|
+
const args = 'gh pr comment 1 --body "@gemini-code-assist /gemini review"';
|
|
74
|
+
const result = evaluateHook(rule(), { tool: 'bash', args });
|
|
75
|
+
expect(result).toMatchObject({
|
|
76
|
+
decision: 'reject',
|
|
77
|
+
packId: '@mmnto/pack-bot-gemini-code-assist',
|
|
78
|
+
ruleId: 'gca-tag-xor-command',
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
it('preserves recoveryHint when present', () => {
|
|
82
|
+
const args = 'gh pr comment 1 --body "@gemini-code-assist /gemini review"';
|
|
83
|
+
const result = evaluateHook(rule(), { tool: 'bash', args });
|
|
84
|
+
if (result.decision === 'reject') {
|
|
85
|
+
expect(result.recoveryHint).toMatch(/Choose one/);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
it('returns undefined recoveryHint when the rule omits it', () => {
|
|
89
|
+
const r = rule({ recoveryHint: undefined });
|
|
90
|
+
const args = 'gh pr comment 1 --body "@gemini-code-assist /gemini review"';
|
|
91
|
+
const result = evaluateHook(r, { tool: 'bash', args });
|
|
92
|
+
if (result.decision === 'reject') {
|
|
93
|
+
expect(result.recoveryHint).toBeUndefined();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('verification_shadow ignored at runtime', () => {
|
|
98
|
+
it('evaluates as Interpretive Rule even when verification_shadow is present', () => {
|
|
99
|
+
// Per ADR-104 § Convergence: hooks fall into Interpretive Rule class
|
|
100
|
+
// in V1. verification_shadow on a hook rule is reserved schema field
|
|
101
|
+
// for future Spine-Rule promotion; the V1 runtime must not change
|
|
102
|
+
// its decision based on its presence or absence.
|
|
103
|
+
const args = 'gh pr comment 1 --body "@gemini-code-assist /gemini review"';
|
|
104
|
+
const withShadow = rule({ verification_shadow: { rego: 'package x' } });
|
|
105
|
+
const withoutShadow = rule();
|
|
106
|
+
expect(evaluateHook(withShadow, { tool: 'bash', args })).toEqual(evaluateHook(withoutShadow, { tool: 'bash', args }));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('formatRejection', () => {
|
|
111
|
+
it('formats the structured prefix with packId/ruleId when recoveryHint is absent', () => {
|
|
112
|
+
const decision = {
|
|
113
|
+
decision: 'reject',
|
|
114
|
+
packId: '@mmnto/pack-bot-coderabbit',
|
|
115
|
+
ruleId: 'r1',
|
|
116
|
+
message: 'do not commit secrets',
|
|
117
|
+
};
|
|
118
|
+
expect(formatRejection(decision)).toBe('[totem:hook-block] @mmnto/pack-bot-coderabbit/r1: do not commit secrets');
|
|
119
|
+
});
|
|
120
|
+
it('includes the recovery-hint line when present', () => {
|
|
121
|
+
const decision = {
|
|
122
|
+
decision: 'reject',
|
|
123
|
+
packId: '@mmnto/pack-bot-coderabbit',
|
|
124
|
+
ruleId: 'r1',
|
|
125
|
+
message: 'do not commit secrets',
|
|
126
|
+
recoveryHint: 'use git-crypt or vault',
|
|
127
|
+
};
|
|
128
|
+
expect(formatRejection(decision)).toBe('[totem:hook-block] @mmnto/pack-bot-coderabbit/r1: do not commit secrets\n' +
|
|
129
|
+
' → use git-crypt or vault');
|
|
130
|
+
});
|
|
131
|
+
// The previous "throws when given an allow decision" runtime guard is gone —
|
|
132
|
+
// formatRejection's parameter is now narrowed to RejectDecision so the type
|
|
133
|
+
// system prevents the caller bug at compile time. TS-only enforcement.
|
|
134
|
+
});
|
|
135
|
+
//# sourceMappingURL=runtime.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.test.js","sourceRoot":"","sources":["../../src/hook/runtime.test.ts"],"names":[],"mappings":"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"}
|