@mutmutco/claude-plugin 4.0.3 → 4.0.5

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,8 +1,8 @@
1
1
  {
2
2
  "name": "mmi",
3
3
  "displayName": "MMI",
4
- "version": "4.0.3",
5
- "mmiCompat": "3.x",
4
+ "version": "4.0.5",
5
+ "mmiCompat": "4.x",
6
6
  "description": "MMI workflow skills and org gates delivery.",
7
7
  "author": {
8
8
  "name": "MMI Future",
package/bin/mmi-cli CHANGED
File without changes
package/bin/mmi-hook CHANGED
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/claude-plugin",
3
- "version": "4.0.3",
3
+ "version": "4.0.5",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -4,7 +4,7 @@
4
4
  // then command-ladder. The broad shell-dialect advisory remains retired.
5
5
  import { execFileSync } from 'node:child_process';
6
6
  import { existsSync, readFileSync } from 'node:fs';
7
- import { resolve } from 'node:path';
7
+ import { isAbsolute, resolve } from 'node:path';
8
8
  import { analyze as analyzeSecretEcho } from './secret-echo-lint.mjs';
9
9
  import { analyze as analyzeEnvWrite } from './env-write-lint.mjs';
10
10
  import { decide as decideCommandLadder, matchedVerb } from './command-ladder-gate.mjs';
@@ -43,9 +43,45 @@ function preToolUseDeny(reason) {
43
43
  });
44
44
  }
45
45
 
46
+ /** Commands whose heredoc body IS executed, so its text must stay under analysis. Deliberately a
47
+ * small allowlist matched by executable NAME: anything unrecognised counts as an interpreter and
48
+ * keeps its body scanned, so an unknown consumer can never become a bypass (#5266). */
49
+ const HEREDOC_DATA_CONSUMERS = new Set(['git', 'cat', 'tee', 'mmi-cli', 'jerv-cli', 'gh', 'jq', 'grep', 'sed', 'diff', 'sort', 'wc', 'head', 'tail']);
50
+
51
+ /** Blank out heredoc BODIES that are data rather than script (#5266).
52
+ *
53
+ * Segmentation splits on `;|&` and newlines, so a heredoc body — a commit message, a PR body, an
54
+ * issue report — was chopped into pseudo-segments and analysed as if it were a command. Prose that
55
+ * merely QUOTED a test runner was therefore refused as an attempt to run tests, which penalised
56
+ * writing accurate evidence about test behaviour: the exact opposite of what #5264 was fixing.
57
+ *
58
+ * Bodies are replaced with equal-length blanks, never deleted, so every offset the analysers and
59
+ * the segment-ordinal reporting depend on is preserved. A body is only blanked when the command
60
+ * introducing it is a known data consumer; `sh <<EOF … EOF` genuinely executes its body and stays
61
+ * fully analysed. */
62
+ function maskHeredocData(source) {
63
+ const lines = source.split('\n');
64
+ const out = [...lines];
65
+ for (let i = 0; i < lines.length; i += 1) {
66
+ const intro = /<<-?\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))/.exec(lines[i]);
67
+ if (!intro) continue;
68
+ const delimiter = intro[1] ?? intro[2] ?? intro[3];
69
+ // The consumer is the command the redirection attaches to — the LAST one before `<<`, not the
70
+ // first on the line. `cd <dir> && git commit -F - <<EOF` is consumed by git, not by cd.
71
+ const consumer = /(?:^|[;|&])\s*([^\s;|&]+)[^;|&]*$/.exec(lines[i].slice(0, intro.index));
72
+ if (!HEREDOC_DATA_CONSUMERS.has(executableName(consumer?.[1]))) continue;
73
+ for (let j = i + 1; j < lines.length; j += 1) {
74
+ if (lines[j].trim() === delimiter) { i = j; break; }
75
+ out[j] = ' '.repeat(lines[j].length);
76
+ if (j === lines.length - 1) i = j;
77
+ }
78
+ }
79
+ return out.join('\n');
80
+ }
81
+
46
82
  /** Split only on unquoted compound-command operators. Text is retained solely for analyzers and is never surfaced. */
47
83
  function boundedShellSegments(command) {
48
- const source = String(command ?? '');
84
+ const source = maskHeredocData(String(command ?? ''));
49
85
  const segments = [];
50
86
  let start = 0;
51
87
  let quote = null;
@@ -293,8 +329,24 @@ function git(root, args) {
293
329
  });
294
330
  }
295
331
 
332
+ /** The directory the COMMAND will run in, when it names one itself (#5264).
333
+ *
334
+ * A compound command routinely opens with `cd <path> && …`, and the host's `cwd` is the session's,
335
+ * not the command's. Judging `cd <other-repo-worktree> && npm test` by the session cwd evaluates a
336
+ * DIFFERENT repository — its test-policy.json and its (typically clean) diff — and then states a
337
+ * conclusion about the repo it never looked at. Reuses the same bounded segmentation the gate
338
+ * already trusts, and only ever feeds a read-only `git -C` probe. */
339
+ function commandWorkingDirectory(command, base) {
340
+ const [first] = boundedShellSegments(command);
341
+ const match = /^cd\s+(?!-)(?:"([^"]+)"|'([^']+)'|([^\s;&|]+))\s*$/.exec(first?.text ?? '');
342
+ const target = match?.[1] ?? match?.[2] ?? match?.[3];
343
+ if (!target || target.startsWith('~')) return null;
344
+ const resolved = isAbsolute(target) ? target : (typeof base === 'string' && base ? resolve(base, target) : null);
345
+ return resolved && existsSync(resolved) ? resolved : null;
346
+ }
347
+
296
348
  function repositoryRoot(input) {
297
- const candidates = [input?.cwd, process.cwd()]
349
+ const candidates = [commandWorkingDirectory(input?.tool_input?.command, input?.cwd), input?.cwd, process.cwd()]
298
350
  .filter((cwd, index, values) => typeof cwd === 'string' && cwd && values.indexOf(cwd) === index);
299
351
  for (const cwd of candidates) {
300
352
  try {
@@ -372,8 +424,14 @@ function runTestCommandPolicy(input, { stdout = process.stdout } = {}) {
372
424
  stdout.write(preToolUseDeny(reason) + '\n');
373
425
  return { denied: true };
374
426
  }
427
+ // #5264: name the repository this verdict was computed from. The refusal text is authoritative and
428
+ // gets pasted into PR bodies as verification, so a bare "the current diff" — with no statement of
429
+ // WHICH diff — reads as a claim about the repo the author is working in even when the gate resolved
430
+ // a different one. Naming the root makes a wrong resolution self-evident instead of quotable.
375
431
  const reason = 'TEST-POLICY TEST COMMAND REFUSED [test-command-outside-mandatory-zone]: '
376
- + 'the current diff does not touch any test-policy.json mandatory glob. Do not run tests; use policy-approved non-test verification, '
432
+ + `no path in ${root}'s task diff matches a mandatory glob in its test-policy.json. `
433
+ + 'Verify that is the repository you meant before quoting this: it is resolved from the command\'s own `cd`, then the host cwd. '
434
+ + 'Do not run tests; use policy-approved non-test verification, '
377
435
  + 'or touch and run mandatory-zone coverage only when the diff actually requires it.';
378
436
  appendHookActivity({ event: 'PreToolUse', script: TEST_COMMAND_GATE_NAME, outcome: 'deny', action: reason, reasonId: 'test-command-outside-mandatory-zone', tool: input?.tool_name });
379
437
  stdout.write(preToolUseDeny(reason) + '\n');