@mutmutco/cursor-plugin 4.2.7 → 4.3.1

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.
Files changed (35) hide show
  1. package/.cursor-plugin/plugin.json +2 -3
  2. package/package.json +1 -1
  3. package/scripts/edit-tool-paths.mjs +4 -4
  4. package/skills/bootstrap/SKILL.md +2 -2
  5. package/skills/bootstrap/seeds/README.template.md +2 -2
  6. package/skills/bootstrap/seeds/gate.template.yml +5 -5
  7. package/skills/bootstrap/seeds/manifest.json +1 -0
  8. package/skills/bootstrap/seeds/test-policy.template.json +4 -0
  9. package/skills/hotfix/SKILL.md +1 -1
  10. package/skills/rcand/SKILL.md +1 -1
  11. package/skills/release/SKILL.md +38 -9
  12. package/skills/secrets/SKILL.md +1 -1
  13. package/skills/stage/SKILL.md +1 -1
  14. package/bin/mmi-hook +0 -2
  15. package/bin/mmi-hook-console.cmd +0 -16
  16. package/bin/mmi-hook.exe +0 -0
  17. package/hooks/cursor-hooks.json +0 -26
  18. package/scripts/command-ladder-core.mjs +0 -339
  19. package/scripts/command-ladder-gate.mjs +0 -126
  20. package/scripts/deny-gate-crash.mjs +0 -179
  21. package/scripts/env-write-lint.mjs +0 -146
  22. package/scripts/hook-io.mjs +0 -22
  23. package/scripts/hook-policy.mjs +0 -78
  24. package/scripts/hook-run.mjs +0 -434
  25. package/scripts/hook-trace.mjs +0 -151
  26. package/scripts/pretooluse-shell-gates.mjs +0 -720
  27. package/scripts/secret-echo-lint.mjs +0 -177
  28. package/scripts/test-command-policy-core.mjs +0 -294
  29. package/scripts/throttle-core.mjs +0 -332
  30. package/scripts/vault-edit-gate.mjs +0 -94
  31. package/skills/browser-automation/SKILL.md +0 -122
  32. package/skills/mmi/SKILL.md +0 -544
  33. package/skills/mmi-doctor/SKILL.md +0 -66
  34. package/skills/mmi-resume/SKILL.md +0 -123
  35. package/skills/onboard/SKILL.md +0 -72
@@ -1,332 +0,0 @@
1
- // Shell-dialect detection (#979/#2089) — pure, no IO; exported for the shell-redirect lint + tests.
2
- // Catches PowerShell syntax emitted into Bash and bash/zsh syntax emitted into PowerShell, so the
3
- // model re-issues the command in the correct dialect instead of burning a turn on a failed call.
4
-
5
- /** Remove quoted spans so redirect/log patterns in string literals do not trip the lint. */
6
- export function stripQuoted(cmd) {
7
- return cmd.replace(/'[^']*'/g, ' ').replace(/"[^"]*"/g, ' ');
8
- }
9
-
10
- export function normalizeToolName(name) {
11
- return String(name ?? '').trim();
12
- }
13
-
14
- // #3563: Codex canonicalises its shell call to `Bash` on 0.145.0, but the Codex hook manifest also
15
- // matches `shell`/`local_shell` for forward safety — and a matcher that fires a gate the gate itself
16
- // does not recognise is a decorative claim, not protection (cross-vendor check, openai-sol). One list,
17
- // used by every shell gate, so the matcher and the implementation cannot drift apart.
18
- const SHELL_TOOL_FAMILY = new Map([
19
- ['Bash', 'bash'],
20
- ['PowerShell', 'powershell'],
21
- ['shell', 'bash'],
22
- ['local_shell', 'bash'],
23
- ]);
24
-
25
- export function isShellTool(toolName) {
26
- return SHELL_TOOL_FAMILY.has(normalizeToolName(toolName));
27
- }
28
-
29
- export function shellFamilyFromToolName(toolName) {
30
- return SHELL_TOOL_FAMILY.get(normalizeToolName(toolName)) ?? null;
31
- }
32
-
33
- export function normalizeShellFamily(shell, platform = process.platform) {
34
- const s = String(shell ?? '').trim().toLowerCase();
35
- if (/\b(pwsh|powershell|power-shell)\b/.test(s)) return 'powershell';
36
- if (/\b(bash|zsh|sh|fish|git-bash|wsl)\b/.test(s)) return 'bash';
37
- if (platform === 'win32') return 'powershell';
38
- if (platform === 'darwin' || platform === 'linux' || platform === 'freebsd') return 'bash';
39
- return null;
40
- }
41
-
42
- function shellUnquoted(command) {
43
- return stripQuoted(String(command ?? ''));
44
- }
45
-
46
- function dialectReason(reasonId, reason) {
47
- return { reasonId, reason };
48
- }
49
-
50
- function isExplicitCrossShellInvocation(cmd, family) {
51
- const hasOuterSeparator = /(?:^|[^|])\|(?!\|)|;|&&|\|\|/.test(cmd);
52
- if (hasOuterSeparator) return false;
53
- if (family === 'bash') {
54
- return /^\s*(?:pwsh|powershell)(?:\.exe)?\s+(?:(?:-NoProfile|-NonInteractive)\s+)*(?:-Command|-c)\b/i.test(cmd);
55
- }
56
- if (family === 'powershell') {
57
- return /^\s*(?:bash|zsh|sh)(?:\.exe)?\s+-l?c\b/i.test(cmd) || /^\s*wsl(?:\.exe)?\b/i.test(cmd);
58
- }
59
- return false;
60
- }
61
-
62
- /**
63
- * True when the command carries a real heredoc operator (`<<` / `<<-`) at a shell position — i.e. a `<<`
64
- * that sits OUTSIDE any quoted span, followed by an (optionally quoted) delimiter word. The delimiter
65
- * itself may be quoted (`<<'EOF'`), so we scan the RAW command for a `<<` outside quotes rather than the
66
- * quote-stripped form. This is what distinguishes a genuine heredoc from a `<<` inside a string literal
67
- * (e.g. `echo 'a << b'`), which must not trip the #2268 native-CLI heredoc lint.
68
- * Returns the raw-command index of the `<<` (so callers can scope checks to the text BEFORE the
69
- * heredoc, #2648), or -1 when no heredoc exists.
70
- */
71
- function heredocIndexOutsideQuotes(command) {
72
- let inSingle = false;
73
- let inDouble = false;
74
- for (let i = 0; i < command.length - 1; i += 1) {
75
- const c = command[i];
76
- if (inSingle) {
77
- if (c === "'") inSingle = false;
78
- continue;
79
- }
80
- if (inDouble) {
81
- if (c === '\\') { i += 1; continue; }
82
- if (c === '"') inDouble = false;
83
- continue;
84
- }
85
- if (c === "'") { inSingle = true; continue; }
86
- if (c === '"') { inDouble = true; continue; }
87
- if (c === '<' && command[i + 1] === '<') {
88
- if (/^-?\s*['"]?[A-Za-z_]/.test(command.slice(i + 2))) return i;
89
- i += 1;
90
- }
91
- }
92
- return -1;
93
- }
94
-
95
- /**
96
- * True when a backtick sits at end-of-line OUTSIDE any quoted span — the real PowerShell
97
- * line-continuation smell. A backtick+newline inside a single/double-quoted body (e.g. a Markdown
98
- * code fence in an inert `printf '%s' '…'` payload) is data, not shell syntax, and must not trip
99
- * the lint (#2648).
100
- */
101
- function backtickAtEolOutsideQuotes(command) {
102
- let inSingle = false;
103
- let inDouble = false;
104
- for (let i = 0; i < command.length; i += 1) {
105
- const c = command[i];
106
- if (inSingle) {
107
- if (c === "'") inSingle = false;
108
- continue;
109
- }
110
- if (inDouble) {
111
- if (c === '\\') { i += 1; continue; }
112
- if (c === '"') inDouble = false;
113
- continue;
114
- }
115
- if (c === "'") { inSingle = true; continue; }
116
- if (c === '"') { inDouble = true; continue; }
117
- if (c === '`' && (command[i + 1] === '\n' || (command[i + 1] === '\r' && command[i + 2] === '\n'))) return true;
118
- }
119
- return false;
120
- }
121
-
122
- /**
123
- * Neutralize heredoc bodies so shell-dialect matching never trips on literal file content being written
124
- * verbatim (#2746). A heredoc body is not shell to execute, so a `<<` header opens a BODY that must be kept
125
- * out of the char-scanner:
126
- * - QUOTED heredoc (`<<'EOF' … EOF`): the body is 100% inert data, so it is BLANKED — PowerShell-cmdlet
127
- * tokens (`Select-Object`, `2>$null`), backticks, etc. inside authored Markdown become empty lines.
128
- * - UNQUOTED heredoc (`<<EOF … EOF`): the body may expand, so it is PASSED THROUGH unchanged for the other
129
- * lints — but it is STILL not scanned for quote state, because scanning it lets an odd quote in the body
130
- * (an apostrophe in prose, `don't`) poison the cross-line `inSingle`/`inDouble` state and suppress
131
- * detection of a LATER quoted heredoc (the recurrence #2750 fixes).
132
- * The HEADER and TERMINATOR lines are kept (so the native-CLI/heredoc lints still see the `<<`, and no
133
- * unterminated-heredoc artifact is created), text before the header and after the terminator is untouched, and
134
- * every character index up to the header is preserved. Quote state is tracked ACROSS non-body lines so a
135
- * `<<'…'` inside a multi-line quoted string is NOT mistaken for a real header. Plain `<<` needs an EXACT
136
- * terminator; bash strips indentation only for `<<-`.
137
- */
138
- export function stripQuotedHeredocBodies(command) {
139
- const text = String(command ?? '');
140
- if (!text.includes('<<')) return text;
141
- const lines = text.split('\n');
142
- const out = [];
143
- let closer = null; // marker word when inside a heredoc body (quoted OR unquoted)
144
- let closerDash = false; // `<<-` allows an indented terminator; plain `<<` needs an exact match
145
- let closerBlank = false; // true = blank the body (quoted heredoc); false = pass it through (unquoted)
146
- let inSingle = false; // quote state carried ACROSS non-body lines
147
- let inDouble = false;
148
- for (const line of lines) {
149
- if (closer !== null) {
150
- const bare = line.replace(/\r$/, '');
151
- if (closerDash ? bare.trim() === closer : bare === closer) {
152
- out.push(line); // keep the terminator line intact
153
- closer = null; closerDash = false; closerBlank = false;
154
- } else {
155
- out.push(closerBlank ? '' : line); // blank a quoted body; pass an unquoted body through untouched
156
- }
157
- continue; // body lines never touch quote state
158
- }
159
- let marker = null;
160
- let dash = false;
161
- let quoted = false;
162
- for (let i = 0; i < line.length; i += 1) {
163
- const c = line[i];
164
- if (inSingle) { if (c === "'") inSingle = false; continue; }
165
- if (inDouble) { if (c === '\\') { i += 1; continue; } if (c === '"') inDouble = false; continue; }
166
- if (c === "'") { inSingle = true; continue; }
167
- if (c === '"') { inDouble = true; continue; }
168
- if (marker === null && c === '<' && line[i + 1] === '<') {
169
- const m = /^(-?)\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\2/.exec(line.slice(i + 2));
170
- if (m) { dash = m[1] === '-'; quoted = m[2] !== ''; marker = m[3]; i += m[0].length + 1; continue; }
171
- i += 1;
172
- }
173
- }
174
- out.push(line);
175
- if (marker !== null) { closer = marker; closerDash = dash; closerBlank = quoted; }
176
- }
177
- return out.join('\n');
178
- }
179
-
180
- /**
181
- * ADVISORY (#2650): a Windows backslash-separated path (two or more `\`-joined segments, e.g.
182
- * `docs\Architecture\x.md`) inside a Bash command body. The Bash tool's own pipeline can silently
183
- * rewrite specific backslash+letter byte pairs (`\A` became a JSON-escaped bell, `\r` lost its
184
- * backslash — harness-level, see #2650) before the interpreter runs, corrupting authored file
185
- * content with no error. This never blocks — it warns and points at the safe patterns (forward
186
- * slashes; the Write tool for file bodies). Single escapes like `printf '%s\n'` never match:
187
- * the pattern requires two path separators.
188
- */
189
- const WINDOWS_BACKSLASH_PATH_RE = /[A-Za-z0-9_.):-]+\\[A-Za-z][A-Za-z0-9_.-]*\\[A-Za-z0-9_.-]/;
190
- export function adviseBackslashPath(command) {
191
- if (!command || typeof command !== 'string') return null;
192
- if (!WINDOWS_BACKSLASH_PATH_RE.test(command)) return null;
193
- return {
194
- reasonId: 'shell_backslash_path_advisory',
195
- reason:
196
- 'Windows backslash path in a Bash command body. The Bash tool pipeline can silently mangle '
197
- + 'backslash+letter byte pairs (e.g. `\\A`, `\\r`) before the interpreter sees them (#2650) — '
198
- + 'use forward slashes for paths, or author file bodies with the Write tool.',
199
- };
200
- }
201
-
202
- export function analyzeShellDialect(command, shellFamily, platform = process.platform) {
203
- if (!command || typeof command !== 'string') return null;
204
- const family = shellFamily === 'bash' || shellFamily === 'powershell' ? shellFamily : null;
205
- if (!family) return null;
206
-
207
- const cmd = shellUnquoted(command).trim();
208
- if (!cmd) return null;
209
- if (isExplicitCrossShellInvocation(cmd, family)) return null;
210
-
211
- if (family === 'bash') {
212
- // #2746: a quoted heredoc body (`<<'EOF' … EOF`) is literal file content, not shell — strip it before
213
- // dialect matching so PowerShell-cmdlet-shaped tokens, backticks, and `2>$null` inside authored Markdown
214
- // never trip these Bash lints. The header, everything outside the body, and unquoted heredocs are
215
- // matched exactly as before (indices up to the header are preserved by the blank-line substitution).
216
- const bashScan = stripQuotedHeredocBodies(command);
217
- const bashCmd = shellUnquoted(bashScan).trim();
218
- // PowerShell stderr/stdout suppression smuggled into Bash: `2>$null`, `>$null`, `*>&$null`,
219
- // or a piped `... | Out-Null`. In Bash `$null` is the empty string → ambiguous redirect.
220
- if (/\$null\b/.test(bashCmd) || /\bOut-Null\b/.test(bashCmd)) {
221
- return dialectReason(
222
- 'shell_dialect_powershell_redirect_in_bash',
223
- 'PowerShell `$null` in a Bash command. `$null` is empty in Bash, and redirects like `2>$null` leave '
224
- + '`2>` with no target → "ambiguous redirect". Use `2>/dev/null` for stderr suppression, '
225
- + '`>/dev/null` for stdout suppression, or Bash variables instead.',
226
- );
227
- }
228
- // #5561: Windows device name NUL as a relative redirect creates ./NUL under Git Bash / MSYS.
229
- if (/(?:^|[\s;&|])(?:\d*>+|>>)\s*NUL\b/i.test(bashCmd)) {
230
- return dialectReason(
231
- 'shell_dialect_nul_redirect_in_bash',
232
- 'Redirecting to relative `NUL` creates a `./NUL` file under Git Bash / MSYS. '
233
- + 'Use `>/dev/null` (or `2>/dev/null`) instead.',
234
- );
235
- }
236
- if (/\bSelect-Object\b/i.test(bashCmd)) {
237
- return dialectReason(
238
- 'shell_dialect_select_object_in_bash',
239
- 'PowerShell `Select-Object` in a Bash command. Use `head -n <n>` or `tail -n <n>` in Bash.',
240
- );
241
- }
242
- if (/\bGet-Content\b/i.test(bashCmd)) {
243
- return dialectReason(
244
- 'shell_dialect_get_content_in_bash',
245
- 'PowerShell `Get-Content` in a Bash command. Use `cat`, `head`, `tail`, or `sed -n` in Bash.',
246
- );
247
- }
248
- if (/(^|[;&|]\s*)`[^\r\n]+/.test(bashCmd) || backtickAtEolOutsideQuotes(bashScan)) {
249
- return dialectReason(
250
- 'shell_dialect_powershell_backtick_in_bash',
251
- 'Backtick reached the Bash tool. On a PowerShell-backed shell this is usually a here-string or '
252
- + 'Markdown body, which the Bash tool mangles — write the body to a temp file with the Write tool '
253
- + 'and pass its path (e.g. `--body-file <path>`). For a real Bash line continuation use `\\`.',
254
- );
255
- }
256
- // #2268: on Windows the Bash tool is a `.cmd` shim whose stdin is not forwarded (#1511), so a
257
- // heredoc feeding a native CLI silently sends an empty body. Redirect to the file-path pattern.
258
- // #2648: test the native-CLI names only against the command SEGMENT that consumes the heredoc
259
- // (the text between the last unquoted separator and the `<<`), never the heredoc BODY — markdown
260
- // prose mentioning "mmi-cli" fed to `cat > file` is inert.
261
- const heredocIdx = platform === 'win32' ? heredocIndexOutsideQuotes(bashScan) : -1;
262
- if (
263
- heredocIdx >= 0
264
- && /\b(?:mmi-cli|jerv-cli|gh|git|npm|aws|node|claude)\b/i.test(
265
- shellUnquoted(bashScan.slice(0, heredocIdx)).split(/[;&|]/).pop() ?? '',
266
- )
267
- ) {
268
- return dialectReason(
269
- 'shell_dialect_heredoc_native_cli_windows',
270
- 'Bash heredoc feeding a native CLI fails on Windows: the Bash tool is a `.cmd` shim and stdin is '
271
- + 'not forwarded (#1511), so the body arrives empty. Write the body to a temp file with the Write '
272
- + 'tool and pass its path (e.g. `mmi-cli ... --body-file <path>`).',
273
- );
274
- }
275
- }
276
-
277
- if (family === 'powershell') {
278
- // Bash device-null redirect smuggled into PowerShell: `2>/dev/null`, `>/dev/null`, `&>/dev/null`.
279
- // PowerShell has no `/dev/null`; the correct sink is `$null`.
280
- if (/[0-9&]?>\s*\/dev\/null\b/.test(cmd)) {
281
- return dialectReason(
282
- 'shell_dialect_bash_redirect_in_powershell',
283
- 'Bash redirect in a PowerShell command. PowerShell has no `/dev/null`. Use `2>$null` '
284
- + '(or `| Out-Null`) to suppress output.',
285
- );
286
- }
287
- if (/\|\s*head(?:\s|$|-)/i.test(cmd)) {
288
- return dialectReason(
289
- 'shell_dialect_head_in_powershell',
290
- 'Bash `head` in a PowerShell command. Use `Select-Object -First <n>`.',
291
- );
292
- }
293
- if (/\|\s*tail(?:\s|$|-)/i.test(cmd)) {
294
- return dialectReason(
295
- 'shell_dialect_tail_in_powershell',
296
- 'Bash `tail` in a PowerShell command. Use `Select-Object -Last <n>` or `Get-Content -Tail <n>`.',
297
- );
298
- }
299
- if (/(^|[;&|\r\n]\s*)sed\s+/i.test(cmd)) {
300
- return dialectReason(
301
- 'shell_dialect_sed_in_powershell',
302
- 'Bash `sed` in a PowerShell command. Use `Select-String`, `ForEach-Object`, or run it explicitly through `bash -lc`.',
303
- );
304
- }
305
- if (/(^|[;&|\r\n]\s*)awk\s+/i.test(cmd)) {
306
- return dialectReason(
307
- 'shell_dialect_awk_in_powershell',
308
- 'Bash `awk` in a PowerShell command. Use PowerShell object processing, or run it explicitly through `bash -lc`.',
309
- );
310
- }
311
- if (/(^|[;&|\r\n]\s*)xargs(?:\s|$)/i.test(cmd)) {
312
- return dialectReason(
313
- 'shell_dialect_xargs_in_powershell',
314
- 'Bash `xargs` in a PowerShell command. Use the PowerShell pipeline with `ForEach-Object`, or run it explicitly through `bash -lc`.',
315
- );
316
- }
317
- if (/(^|[;&|\r\n]\s*)rm\s+-[^\s]*r[^\s]*f|(^|[;&|\r\n]\s*)rm\s+-[^\s]*f[^\s]*r/i.test(cmd)) {
318
- return dialectReason(
319
- 'shell_dialect_rm_rf_in_powershell',
320
- 'Bash `rm -rf` in a PowerShell command. Use `Remove-Item -Recurse -Force -LiteralPath <path>` after verifying the target path.',
321
- );
322
- }
323
- if (/<<-?\s*['"]?[A-Za-z_][A-Za-z0-9_]*['"]?/.test(cmd)) {
324
- return dialectReason(
325
- 'shell_dialect_heredoc_in_powershell',
326
- 'Bash heredoc syntax in a PowerShell command. Use a PowerShell here-string or write a temp file with `Set-Content`.',
327
- );
328
- }
329
- }
330
-
331
- return null;
332
- }
@@ -1,94 +0,0 @@
1
- // No-env floor: deny Edit/Write on real .env files (#1706, hardened #2663).
2
- // PreToolUse on Claude Code for Edit|Write, and on Codex for Edit|Write|apply_patch (#3563) — Codex's
3
- // apply_patch carries the target paths inside the patch body, not in `file_path`, so path extraction is
4
- // shared with plan-edit-sync via edit-tool-paths.mjs. Allows .env.example and template paths.
5
- // The org is env-free — secrets/config are vault-native (mmi-cli stage injects them into the process env;
6
- // /secrets manages names). NO .env is ever created, even locally. Fail-closed on gate crashes (#2598).
7
- import { handleGateCrash, handleMissingHookInput, recordGateSuccess } from './deny-gate-crash.mjs';
8
- import { editedPaths, isEditTool } from './edit-tool-paths.mjs';
9
- import { isRealEnvToken } from './env-write-lint.mjs';
10
- import { readHookInput } from './hook-io.mjs';
11
- import { appendHookActivity } from './hook-trace.mjs';
12
-
13
- const MODE = process.env.MMI_VAULT_GATE_MODE ?? 'block';
14
- const GATE_NAME = 'vault-edit';
15
-
16
- /**
17
- * @param {{ toolName?: string, filePath?: string }} input
18
- * @returns {{ block: boolean, reason: string }}
19
- */
20
- export function analyze(input) {
21
- const toolName = input?.toolName;
22
- if (!isEditTool(toolName)) return { block: false, reason: '' };
23
- // #3563: `filePaths` is the list, because Codex's `apply_patch` carries no `file_path` and can touch
24
- // several files in one call. A single-path caller still passes `filePath`.
25
- const paths = (input?.filePaths ?? []).concat(typeof input?.filePath === 'string' ? [input.filePath] : []);
26
- const hit = paths.find((p) => typeof p === 'string' && isRealEnvToken(p.replace(/\\/g, '/').split('/').pop() ?? ''));
27
- if (!hit) return { block: false, reason: '' };
28
-
29
- return {
30
- block: true,
31
- reason:
32
- 'No .env — the org is env-free. Never create, edit, or write a .env file (not even a local or '
33
- + 'gitignored one). Deliver config vault-native: `mmi-cli stage` injects the declared secrets into the '
34
- + 'compose process env (no file), and `mmi-cli vault secrets` manages the names. If something genuinely cannot '
35
- + 'proceed without a .env, STOP and consult the board (file an issue) — do not create the file.',
36
- };
37
- }
38
-
39
- /** Shared entry (#4118): awaitable and exit-free so hook-run.mjs can import it in-process instead of
40
- * booting a second node. `input` is the buffered payload when the runner already drained stdin. */
41
- export async function runHookGate({ input } = {}) {
42
- let parsed;
43
- try {
44
- parsed = await readHookInput(input);
45
- } catch {
46
- // Unreadable/absent payload = out-of-contract host (#2992): fail open without counting a crash.
47
- const res = handleMissingHookInput(GATE_NAME);
48
- if (res.stdout) process.stdout.write(res.stdout);
49
- if (res.stderr) process.stderr.write(res.stderr);
50
- return;
51
- }
52
- recordGateSuccess(GATE_NAME);
53
-
54
- const { block, reason } = analyze({
55
- toolName: parsed?.tool_name,
56
- filePaths: editedPaths(parsed),
57
- });
58
-
59
- appendHookActivity({
60
- event: 'PreToolUse',
61
- script: GATE_NAME,
62
- outcome: block ? (MODE === 'observe' ? 'observe' : 'deny') : 'ran',
63
- action: block ? reason : 'clean',
64
- tool: parsed?.tool_name,
65
- });
66
-
67
- if (block) {
68
- if (MODE === 'observe') {
69
- process.stderr.write(`[mmi-vault-gate] would-block: ${reason}\n`);
70
- } else {
71
- const decision = JSON.stringify({
72
- hookSpecificOutput: {
73
- hookEventName: 'PreToolUse',
74
- permissionDecision: 'deny',
75
- permissionDecisionReason: reason,
76
- },
77
- });
78
- process.stdout.write(decision + '\n');
79
- }
80
- }
81
- }
82
-
83
- if (
84
- process.argv[1] &&
85
- (process.argv[1].endsWith('vault-edit-gate.mjs') ||
86
- process.argv[1].replace(/\\/g, '/').endsWith('scripts/vault-edit-gate.mjs'))
87
- ) {
88
- runHookGate().then(() => process.exit(0)).catch(() => {
89
- const res = handleGateCrash(GATE_NAME);
90
- if (res.stdout) process.stdout.write(res.stdout);
91
- if (res.stderr) process.stderr.write(res.stderr);
92
- process.exit(0);
93
- });
94
- }
@@ -1,122 +0,0 @@
1
- ---
2
- name: browser-automation
3
- description: Route browser work by host — JervCode lanes or Claude Code Playwright MCP.
4
- ---
5
-
6
- **Host-native invocation:** Claude `/mmi:browser-automation` · Codex `$mmi:browser-automation` · jervcode/Kimi `/skill:browser-automation` · Kilo `skill` tool. A backticked `/name` in this doc names the matching workflow (this skill or a sibling), not a literal command.
7
-
8
- # Browser automation — host lanes
9
-
10
- Org browser skill. **Choose the host lane first.** JervCode and Kimi use the native three-lane browser contract; Claude Code uses DOM-first Playwright MCP. Do not install or select Playwright MCP on JervCode.
11
-
12
- ## Host routing (non-negotiable)
13
-
14
- | Host | Path |
15
- |------|------|
16
- | **JervCode / Kimi** | Three lanes below — never Playwright MCP |
17
- | **Claude Code** | DOM-first Playwright MCP (Claude-only section) |
18
- | Local app stack | **`/stage`** — see `skills/stage/SKILL.md` |
19
-
20
- ### JervCode / Kimi — three lanes
21
-
22
- Authoritative for native JervCode browser work (Jerv-JervCode `product/runbooks/browser.md`, #1694). Pick by need:
23
-
24
- | Need | Tool |
25
- |------|------|
26
- | Plain read (no interaction) | `fetch_content` |
27
- | Public interaction (clicks/types on public pages) | `agent_browser` |
28
- | Authenticated interaction | `trusted_browser` |
29
-
30
- **Do not** install or select Playwright MCP for JervCode. Public interaction goes to `agent_browser`, not Playwright.
31
-
32
- ### Claude Code — DOM-first Playwright MCP
33
-
34
- Org standard for agent browser work on Claude Code. **Playwright** is the engine; agents interact through **structure-first** MCP tools, not pixels-first defaults.
35
-
36
- #### Doctrine (non-negotiable on Claude Code)
37
-
38
- 1. **Accessibility tree first** — semantic structure the agent can reason about (`browser_snapshot`, a11y refs).
39
- 2. **DOM second** — selectors, snapshots, network when the tree is not enough.
40
- 3. **Vision last** — screenshots only when tree + DOM cannot answer the question.
41
- 4. **Prefer HTTP/OpenAPI** — call APIs directly when discovery or the task allows; do not drive UI for data you can fetch.
42
-
43
- **Never** pass `--caps=vision` (or equivalent vision-first defaults) on Playwright MCP. Vision caps burn tokens, hide structure, and break on theme/layout drift.
44
-
45
- #### MCP configuration (Claude Code, no vision)
46
-
47
- Enable the official Playwright plugin (`playwright@claude-plugins-official`) via `/plugin`. Follow its DOM-first tools; do not enable vision-first modes for routine agent work. Point MCP output at `tmp/playwright-mcp` when the server accepts `--output-dir`.
48
-
49
- Editor-host MCP configs (Cursor `.cursor/mcp.json`, Codex `config.toml`) are retired org surfaces (#2741/#2808) — bootstrap no longer seeds them; do not reintroduce them.
50
-
51
- #### Playwright availability
52
-
53
- Check the configured/global CLI before adding a temporary local dependency. On Windows PowerShell:
54
-
55
- ```powershell
56
- Get-Command playwright
57
- playwright --version
58
- ```
59
-
60
- `node -e "require.resolve('playwright')"` only proves a Node module is installed in the current package; it
61
- can fail while the global or editor-configured Playwright CLI works. Use the available CLI for smoke checks
62
- and MCP setup. Temporary per-worktree installs are fallback-only, and should stay untracked.
63
-
64
- #### Agent workflow (MCP) (Claude Code)
65
-
66
- 1. **Goal** — what observable outcome proves success?
67
- 2. **Navigate** — open the target URL (often from `/stage` JSON: `mmi-cli stage --json`).
68
- 3. **Snapshot** — a11y tree / DOM snapshot before interacting.
69
- 4. **Act** — click, type, select using refs from the latest snapshot.
70
- 5. **Re-snapshot** after navigation or major DOM changes (refs go stale).
71
- 6. **Vision only if stuck** — one screenshot to disambiguate; then return to tree/DOM.
72
-
73
- Core MCP loop:
74
-
75
- ```
76
- browser_navigate → browser_snapshot → browser_click / browser_type → browser_snapshot
77
- ```
78
-
79
- #### Artifacts and hygiene (Claude Code)
80
-
81
- - **All Playwright MCP output → `tmp/playwright-mcp/`** (pass `--output-dir tmp/playwright-mcp` when the MCP server supports it).
82
- - **Never** leave traces, screenshots, or reports at the repo root.
83
- - `.playwright-mcp/` at repo root is gitignored as a **safety net only** — not the canonical path.
84
- - The housekeeping gate refuses tracked browser artifacts (`.playwright-mcp/`, `playwright-report/`, `test-results/`).
85
-
86
- ## When to use what
87
-
88
- | Need | Use |
89
- |------|-----|
90
- | Local dev server + smoke on current branch | **`/stage`** — gitignored stack under `tmp/stage/`; see `skills/stage/SKILL.md` |
91
- | Personal cloud dev preview of your branch | **`/stage --live`** — IP-gated dev stage; not rc/prod |
92
- | Interactive UI on **Claude Code** | **Playwright MCP** (Claude-only section) — DOM-first, artifacts under `tmp/` |
93
- | Interactive UI on **JervCode / Kimi** (public) | **`agent_browser`** |
94
- | Interactive UI on **JervCode / Kimi** (authenticated) | **`trusted_browser`** |
95
- | Plain page/content read on **JervCode / Kimi** | **`fetch_content`** |
96
- | Durable hosted automation outside dev machines | **Stagehand + Browserbase** (production path) — explicit choice, not the default for every local task |
97
-
98
- `/stage` and the host browser path **complement** each other. `/stage` spins the app; the host lane drives the browser against a URL (often the stage URL).
99
-
100
- ## Anti-patterns (org-wide avoid)
101
-
102
- - Installing or selecting Playwright MCP on JervCode (use `fetch_content` / `agent_browser` / `trusted_browser`)
103
- - `--caps=vision` or screenshot-first wrappers for routine Claude Code tasks
104
- - Skyvern, Magnitude, LaVague, or other vision-first agent browsers as org defaults
105
- - Committing `.playwright-mcp/`, `playwright-report/`, or `test-results/` from agent runs
106
- - Replacing `/stage` with ad-hoc MCP servers for branch smoke (use `/stage` for the stack, the host lane for the browser)
107
-
108
- ## Related
109
-
110
- - **`/stage`** — `skills/stage/SKILL.md`
111
- - **`/grind`** (optional external tool) — use DOM-first browser checks in verification when criteria need UI proof
112
- - JervCode browser contract — Jerv-JervCode `product/runbooks/browser.md` (#1694)
113
-
114
- ## Retro — one check before you finish
115
-
116
- Before your final report, answer one question honestly: did **this skill's own instructions** misfire
117
- this run — ambiguous wording, a misleading MCP snippet, or an artifact path it should have warned
118
- about? (Process only — never the user's code or task.) If yes, file **one** lesson and move on; a clean run is
119
- silent (hard cap: one per run). It lands on the Hub board (deduped) and is fixed only via a reviewed PR —
120
- never edit the skill live; the retro is advisory, so if the call fails, note it and continue:
121
-
122
- `mmi-cli learning skill-lesson --skill browser-automation --title "<what misfired>" --body "<what; evidence; proposed amendment>"`