@mutmutco/cursor-plugin 4.1.1 → 4.1.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mmi",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and organisation gates for Cursor.",
6
6
  "author": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cursor-plugin",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "description": "MMI workflow skills and organisation gates for Cursor.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -11,6 +11,7 @@ import { decide as decideCommandLadder, matchedVerb } from './command-ladder-gat
11
11
  import { handleGateCrash, handleMissingHookInput, recordGateSuccess } from './deny-gate-crash.mjs';
12
12
  import { readHookInput } from './hook-io.mjs';
13
13
  import { appendHookActivity } from './hook-trace.mjs';
14
+ import { evaluateTestCommandPolicy } from './test-command-policy-core.mjs';
14
15
 
15
16
  // Secret echoes are blocked before execution on every active host.
16
17
  const SECRET_ECHO_MODE = process.env.MMI_SECRET_ECHO_LINT || 'block';
@@ -358,34 +359,14 @@ function repositoryRoot(input) {
358
359
  return null;
359
360
  }
360
361
 
361
- function policyMandatoryGlobs(root) {
362
+ function policyMandatoryEntries(root) {
362
363
  const path = resolve(root, 'test-policy.json');
363
364
  if (!existsSync(path)) return null;
364
365
  const parsed = JSON.parse(readFileSync(path, 'utf8'));
365
366
  if (!Array.isArray(parsed.mandatory) || !parsed.mandatory.every((entry) => entry && typeof entry.glob === 'string')) {
366
367
  throw new Error('test-policy.json mandatory entries are invalid');
367
368
  }
368
- return parsed.mandatory.map((entry) => entry.glob);
369
- }
370
-
371
- function policyGlobToRegExp(glob) {
372
- let out = '';
373
- for (let i = 0; i < glob.length; i += 1) {
374
- const char = glob[i];
375
- if (char === '*') {
376
- if (glob[i + 1] === '*') {
377
- if (glob[i + 2] === '/') { out += '(?:.*/)?'; i += 2; } else { out += '.*'; i += 1; }
378
- } else out += '[^/]*';
379
- } else if (char === '{') {
380
- const close = glob.indexOf('}', i);
381
- if (close === -1) out += '\\{';
382
- else {
383
- out += `(?:${glob.slice(i + 1, close).split(',').map(policyGlobToRegExp).join('|')})`;
384
- i = close;
385
- }
386
- } else out += /[.+?^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
387
- }
388
- return out;
369
+ return parsed.mandatory;
389
370
  }
390
371
 
391
372
  function taskDiffPaths(root) {
@@ -410,12 +391,18 @@ function taskDiffPaths(root) {
410
391
  function runTestCommandPolicy(input, { stdout = process.stdout } = {}) {
411
392
  if (!requestedTestCommand(input?.tool_input?.command)) return { denied: false };
412
393
  const root = repositoryRoot(input);
413
- let globs;
394
+ let mandatory;
414
395
  try {
415
396
  // No declaration is the estate default: this hook does not regulate test execution there.
416
- if (!root || (globs = policyMandatoryGlobs(root)) === null) return { denied: false };
417
- const paths = taskDiffPaths(root);
418
- if (paths.some((path) => globs.some((glob) => new RegExp(`^${policyGlobToRegExp(glob)}$`).test(path)))) return { denied: false };
397
+ if (!root || (mandatory = policyMandatoryEntries(root)) === null) return { denied: false };
398
+ // #5519: same evaluator `mmi-cli tests policy` attaches to its OK summary — matched globs and
399
+ // command classes cannot disagree with the CLI on the same path set.
400
+ const decision = evaluateTestCommandPolicy({
401
+ paths: taskDiffPaths(root),
402
+ mandatory,
403
+ regulated: true,
404
+ });
405
+ if (decision.testCommandsAllowed) return { denied: false, decision };
419
406
  } catch (error) {
420
407
  const reason = 'TEST-POLICY TEST COMMAND REFUSED [test-command-policy-unresolvable]: '
421
408
  + `a test-policy.json applies but its repository, policy, or task diff could not be established (${error.message}). `
@@ -0,0 +1,100 @@
1
+ // test-command-policy-core.mjs — shared verdict for "may this diff run tests?" (#5519).
2
+ //
3
+ // `mmi-cli tests policy` and the PreToolUse test-command gate both answer that question. Before
4
+ // #5519 they answered it separately: the CLI summary printed the repository's CONFIGURED mandatory
5
+ // glob count on an OK line, while the gate independently matched the task diff against those globs
6
+ // and refused a focused test when none hit. Agents read "8 mandatory glob(s)" as permission, then
7
+ // hit TEST-POLICY TEST COMMAND REFUSED on the next line. Delegated workers and the parent hook also
8
+ // diverged when they did not share an evaluator.
9
+ //
10
+ // This module is the one evaluator. It reports matched globs separately from configured totals and
11
+ // the exact allowed/refused command classes. Pure: no IO, no git.
12
+
13
+ /** Command class the PreToolUse gate regulates. Non-test verification is never refused here. */
14
+ export const TEST_COMMAND_CLASS = 'test';
15
+
16
+ /**
17
+ * Glob body → regex body. Supports `**`, `*`, and `{a,b}` (including wildcards inside braces).
18
+ * Kept byte-compatible with {@link globToRegExp} in cli/src/test-policy-core.ts so rule matching
19
+ * and the command guard cannot drift on the same policy file.
20
+ */
21
+ export function translateGlob(glob) {
22
+ let out = '';
23
+ for (let i = 0; i < glob.length; i += 1) {
24
+ const char = glob[i];
25
+ if (char === '*') {
26
+ if (glob[i + 1] === '*') {
27
+ if (glob[i + 2] === '/') {
28
+ out += '(?:.*/)?';
29
+ i += 2;
30
+ } else {
31
+ out += '.*';
32
+ i += 1;
33
+ }
34
+ } else out += '[^/]*';
35
+ } else if (char === '{') {
36
+ const close = glob.indexOf('}', i);
37
+ if (close === -1) out += '\\{';
38
+ else {
39
+ out += `(?:${glob.slice(i + 1, close).split(',').map(translateGlob).join('|')})`;
40
+ i = close;
41
+ }
42
+ } else {
43
+ out += /[.+?^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+
49
+ export function globToRegExp(glob) {
50
+ return new RegExp(`^${translateGlob(glob)}$`);
51
+ }
52
+
53
+ function mandatoryGlobList(mandatory) {
54
+ if (!Array.isArray(mandatory)) return [];
55
+ return mandatory.map((entry) => (typeof entry === 'string' ? entry : entry?.glob)).filter((glob) => typeof glob === 'string');
56
+ }
57
+
58
+ /** Mandatory globs that match at least one path in `paths`. Order follows the policy declaration. */
59
+ export function matchedMandatoryGlobs(paths, mandatory) {
60
+ const globs = mandatoryGlobList(mandatory);
61
+ const list = Array.isArray(paths) ? paths : [];
62
+ return globs.filter((glob) => {
63
+ const re = globToRegExp(glob);
64
+ return list.some((path) => re.test(path));
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Decide whether test commands are allowed against a path set and a mandatory zone.
70
+ *
71
+ * @param {{ paths: string[], mandatory?: unknown, regulated?: boolean }} input
72
+ * `regulated: false` — no test-policy.json (estate default); tests are not gated.
73
+ * `regulated: true` (default) — a declared policy applies; zero matched globs refuses `test`.
74
+ */
75
+ export function evaluateTestCommandPolicy({ paths, mandatory, regulated = true } = {}) {
76
+ const configuredMandatoryCount = mandatoryGlobList(mandatory).length;
77
+ if (!regulated) {
78
+ return {
79
+ configuredMandatoryCount: 0,
80
+ matchedMandatoryGlobs: [],
81
+ matchedMandatoryCount: 0,
82
+ testCommandsAllowed: true,
83
+ reasonId: null,
84
+ commandClasses: { allowed: [TEST_COMMAND_CLASS], refused: [] },
85
+ };
86
+ }
87
+ const matched = matchedMandatoryGlobs(paths, mandatory);
88
+ const testCommandsAllowed = matched.length > 0;
89
+ return {
90
+ configuredMandatoryCount,
91
+ matchedMandatoryGlobs: matched,
92
+ matchedMandatoryCount: matched.length,
93
+ testCommandsAllowed,
94
+ reasonId: testCommandsAllowed ? null : 'test-command-outside-mandatory-zone',
95
+ commandClasses: {
96
+ allowed: testCommandsAllowed ? [TEST_COMMAND_CLASS] : [],
97
+ refused: testCommandsAllowed ? [] : [TEST_COMMAND_CLASS],
98
+ },
99
+ };
100
+ }