@codyswann/lisa 2.124.4 → 2.124.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.
Files changed (81) hide show
  1. package/package.json +1 -1
  2. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  3. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  4. package/plugins/lisa-agy/plugin.json +1 -1
  5. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  8. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  9. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -34
  12. package/plugins/lisa-cursor/hooks/hooks.json +20 -0
  13. package/plugins/lisa-cursor/rules/{reference/base-rules.md → base-rules-reference.mdc} +5 -0
  14. package/plugins/lisa-cursor/rules/{eager/base-rules.md → base-rules.mdc} +6 -1
  15. package/plugins/lisa-cursor/rules/{reference/coding-philosophy.md → coding-philosophy-reference.mdc} +5 -0
  16. package/plugins/lisa-cursor/rules/{eager/coding-philosophy.md → coding-philosophy.mdc} +6 -1
  17. package/plugins/lisa-cursor/rules/{reference/config-resolution.md → config-resolution-reference.mdc} +5 -0
  18. package/plugins/lisa-cursor/rules/{eager/config-resolution.md → config-resolution.mdc} +6 -1
  19. package/plugins/lisa-cursor/rules/{reference/documentation-source-paths.md → documentation-source-paths-reference.mdc} +5 -0
  20. package/plugins/lisa-cursor/rules/{eager/documentation-source-paths.md → documentation-source-paths.mdc} +6 -1
  21. package/plugins/lisa-cursor/rules/{reference/empirical-inquiry.md → empirical-inquiry-reference.mdc} +5 -0
  22. package/plugins/lisa-cursor/rules/{eager/empirical-inquiry.md → empirical-inquiry.mdc} +6 -1
  23. package/plugins/lisa-cursor/rules/{reference/intent-routing.md → intent-routing-reference.mdc} +5 -0
  24. package/plugins/lisa-cursor/rules/{eager/intent-routing.md → intent-routing.mdc} +6 -1
  25. package/plugins/lisa-cursor/rules/{reference/leaf-only-lifecycle.md → leaf-only-lifecycle-reference.mdc} +5 -0
  26. package/plugins/lisa-cursor/rules/{eager/leaf-only-lifecycle.md → leaf-only-lifecycle.mdc} +6 -1
  27. package/plugins/lisa-cursor/rules/{reference/prd-lifecycle-rollup.md → prd-lifecycle-rollup-reference.mdc} +5 -0
  28. package/plugins/lisa-cursor/rules/{eager/prd-lifecycle-rollup.md → prd-lifecycle-rollup.mdc} +6 -1
  29. package/plugins/lisa-cursor/rules/{reference/repo-scope-split.md → repo-scope-split-reference.mdc} +5 -0
  30. package/plugins/lisa-cursor/rules/{eager/repo-scope-split.md → repo-scope-split.mdc} +6 -1
  31. package/plugins/lisa-cursor/rules/{reference/security-audit-handling.md → security-audit-handling-reference.mdc} +5 -0
  32. package/plugins/lisa-cursor/rules/{eager/security-audit-handling.md → security-audit-handling.mdc} +6 -1
  33. package/plugins/lisa-cursor/rules/{reference/usage-accounting.md → usage-accounting-reference.mdc} +5 -0
  34. package/plugins/lisa-cursor/rules/{eager/usage-accounting.md → usage-accounting.mdc} +6 -1
  35. package/plugins/lisa-cursor/rules/{reference/verification.md → verification-reference.mdc} +5 -0
  36. package/plugins/lisa-cursor/rules/{eager/verification.md → verification.mdc} +6 -1
  37. package/plugins/lisa-cursor/rules/{reference/wiki-knowledge-source.md → wiki-knowledge-source-reference.mdc} +5 -0
  38. package/plugins/lisa-cursor/rules/{eager/wiki-knowledge-source.md → wiki-knowledge-source.mdc} +6 -1
  39. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-expo-agy/plugin.json +1 -1
  42. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric-cursor/rules/{harper-fabric.md → harper-fabric.mdc} +5 -0
  50. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +2 -15
  55. package/plugins/lisa-nestjs-cursor/hooks/hooks.json +11 -0
  56. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-rails-agy/plugin.json +1 -1
  64. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +2 -19
  66. package/plugins/lisa-rails-cursor/hooks/hooks.json +15 -0
  67. package/plugins/lisa-rails-cursor/rules/{rails-conventions.md → rails-conventions.mdc} +5 -0
  68. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  71. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +2 -34
  73. package/plugins/lisa-typescript-cursor/hooks/hooks.json +25 -0
  74. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  77. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  79. package/scripts/generate-cursor-plugin-artifacts.mjs +212 -20
  80. package/scripts/lib/per-agent-hook-filter.mjs +133 -23
  81. /package/plugins/lisa-expo-cursor/{.mcp.json → mcp.json} +0 -0
@@ -1,24 +1,11 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM) and hooks (migration write-protection)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
7
7
  },
8
8
  "dependencies": [
9
9
  "lisa-typescript"
10
- ],
11
- "hooks": {
12
- "PreToolUse": [
13
- {
14
- "matcher": "Write|Edit",
15
- "hooks": [
16
- {
17
- "type": "command",
18
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-migration-edits.sh"
19
- }
20
- ]
21
- }
22
- ]
23
- }
10
+ ]
24
11
  }
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "preToolUse": [
5
+ {
6
+ "command": "./hooks/block-migration-edits.sh",
7
+ "matcher": "Write|Edit"
8
+ }
9
+ ]
10
+ }
11
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-openclaw",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "Ruby on Rails-specific skills and hooks for RuboCop and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,28 +1,11 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
7
7
  },
8
8
  "dependencies": [
9
9
  "lisa"
10
- ],
11
- "hooks": {
12
- "PostToolUse": [
13
- {
14
- "matcher": "Write|Edit",
15
- "hooks": [
16
- {
17
- "type": "command",
18
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/rubocop-on-edit.sh"
19
- },
20
- {
21
- "type": "command",
22
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/sg-scan-on-edit.sh"
23
- }
24
- ]
25
- }
26
- ]
27
- }
10
+ ]
28
11
  }
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "postToolUse": [
5
+ {
6
+ "command": "./hooks/rubocop-on-edit.sh",
7
+ "matcher": "Write|Edit"
8
+ },
9
+ {
10
+ "command": "./hooks/sg-scan-on-edit.sh",
11
+ "matcher": "Write|Edit"
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -1,3 +1,8 @@
1
+ ---
2
+ description: "Rails Coding Conventions"
3
+ alwaysApply: true
4
+ ---
5
+
1
6
  # Rails Coding Conventions
2
7
 
3
8
  This rule enforces Rails-specific coding standards for consistency, maintainability, and performance.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "TypeScript-specific hooks for formatting, linting, and ast-grep scanning on edit.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,43 +1,11 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, ast-grep scanning, and error-suppression blocking on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
7
7
  },
8
8
  "dependencies": [
9
9
  "lisa"
10
- ],
11
- "hooks": {
12
- "PreToolUse": [
13
- {
14
- "matcher": "Write|Edit",
15
- "hooks": [
16
- {
17
- "type": "command",
18
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-suppress-directives.sh"
19
- }
20
- ]
21
- }
22
- ],
23
- "PostToolUse": [
24
- {
25
- "matcher": "Write|Edit",
26
- "hooks": [
27
- {
28
- "type": "command",
29
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/format-on-edit.sh"
30
- },
31
- {
32
- "type": "command",
33
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/sg-scan-on-edit.sh"
34
- },
35
- {
36
- "type": "command",
37
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/lint-on-edit.sh"
38
- }
39
- ]
40
- }
41
- ]
42
- }
10
+ ]
43
11
  }
@@ -0,0 +1,25 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "preToolUse": [
5
+ {
6
+ "command": "./hooks/block-suppress-directives.sh",
7
+ "matcher": "Write|Edit"
8
+ }
9
+ ],
10
+ "postToolUse": [
11
+ {
12
+ "command": "./hooks/format-on-edit.sh",
13
+ "matcher": "Write|Edit"
14
+ },
15
+ {
16
+ "command": "./hooks/sg-scan-on-edit.sh",
17
+ "matcher": "Write|Edit"
18
+ },
19
+ {
20
+ "command": "./hooks/lint-on-edit.sh",
21
+ "matcher": "Write|Edit"
22
+ }
23
+ ]
24
+ }
25
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.124.4",
3
+ "version": "2.124.5",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -2,13 +2,32 @@
2
2
  /**
3
3
  * Generate the Cursor variant of a Lisa plugin from the built Claude artifact.
4
4
  *
5
- * Cursor reads `.claude-plugin/plugin.json` natively via its dual-namespace
6
- * loader (also accepts `.cursor-plugin/plugin.json`). The Cursor variant
7
- * therefore keeps the `.claude-plugin/` manifest but filters the hooks block
8
- * to drop `inject-rules.sh` (Cursor auto-loads `rules/` from plugins natively,
9
- * so shipping the polyfill would double-inject), `enforce-team-first.sh`
10
- * (Claude-team-specific), and the `entire hooks claude-code *` analytics calls
11
- * (Claude-only).
5
+ * The variant is reshaped to match the official Cursor plugin spec (issue
6
+ * #1055). Rule application was verified empirically by loading the generated
7
+ * `.mdc` at Cursor's canonical `.cursor/rules/` location (NEW `.mdc` = applied,
8
+ * OLD nested `.md` = UNKNOWN, both with 0 tool calls). Note: the
9
+ * `cursor-agent --plugin-dir` headless flag loads a plugin's skills/agents/
10
+ * commands but does NOT inject its `rules/*.mdc` into model context, so it
11
+ * cannot be used to prove rule application — see evidence/cursor-rule-probe-1055.md:
12
+ *
13
+ * - Manifest: keep `.claude-plugin/plugin.json` — `cursor-agent --plugin-dir`
14
+ * loads it (CLI scope). The IDE marketplace's `.cursor-plugin/` + a Cursor
15
+ * `marketplace.json` is a separate, untested follow-up and is NOT emitted here.
16
+ * - Rules: Cursor discovers `rules/*.mdc` files carrying YAML frontmatter and
17
+ * ignores plain `.md`. The built Claude artifact ships a nested
18
+ * `rules/eager/*.md` + `rules/reference/*.md` tree, so the variant flattens it
19
+ * to top-level `rules/<name>.mdc` (eager → `alwaysApply: true`) and
20
+ * `rules/<name>-reference.mdc` (reference → `alwaysApply: false`). The
21
+ * `-reference` suffix avoids a same-path collision: eager and reference share
22
+ * all base names.
23
+ * - Hooks: emitted as a Cursor-native `hooks/hooks.json` (flattened schema,
24
+ * camelCase event names, relative `./hooks/` command paths) — NOT inline in
25
+ * the manifest. `inject-rules.sh` is dropped because rules now ship as native
26
+ * `.mdc` (the single delivery path; injecting would double-deliver);
27
+ * `enforce-team-first.sh` (Claude-team-specific) and the
28
+ * `entire hooks claude-code *` analytics calls (Claude-only) are dropped too.
29
+ * - MCP: renamed from `.mcp.json` to `mcp.json` — Cursor auto-discovers the
30
+ * un-dotted filename.
12
31
  *
13
32
  * The variant's `hooks/` directory mirrors the surviving script ship-list.
14
33
  *
@@ -25,7 +44,7 @@ import fs from "node:fs";
25
44
  import path from "node:path";
26
45
  import { fileURLToPath } from "node:url";
27
46
  import {
28
- filterHooksForAgent,
47
+ buildCursorHooksJson,
29
48
  filterScriptsForAgent,
30
49
  } from "./lib/per-agent-hook-filter.mjs";
31
50
 
@@ -82,6 +101,167 @@ function copyDir(src, dst, keep = () => true) {
82
101
  walk(src, "");
83
102
  }
84
103
 
104
+ /**
105
+ * Titleize a rule slug for a fallback frontmatter description.
106
+ *
107
+ * @param {string} name e.g. "base-rules"
108
+ * @returns {string} e.g. "Base Rules"
109
+ */
110
+ function titleizeRuleName(name) {
111
+ return name
112
+ .split("-")
113
+ .map(part => (part ? part[0].toUpperCase() + part.slice(1) : part))
114
+ .join(" ")
115
+ .trim();
116
+ }
117
+
118
+ /**
119
+ * Derive a single-line frontmatter description from a rule body.
120
+ *
121
+ * Prefers the first markdown H1 heading; falls back to the titleized slug. The
122
+ * result is always a non-empty single line (Cursor frontmatter `description`).
123
+ *
124
+ * @param {string} body Rule markdown content.
125
+ * @param {string} name Rule slug.
126
+ * @returns {string}
127
+ */
128
+ function deriveRuleDescription(body, name) {
129
+ // Strip fenced code blocks first so a `#`-prefixed line inside one (a shell
130
+ // comment, a Markdown example, etc.) is not mistaken for the rule's H1.
131
+ const withoutFences = body.replace(/^(```|~~~).*$[\s\S]*?^\1.*$/gm, "");
132
+ const h1 = /^#\s+(.+?)\s*$/m.exec(withoutFences);
133
+ const text = (h1 ? h1[1] : "").replace(/\s+/g, " ").trim();
134
+ return text || titleizeRuleName(name);
135
+ }
136
+
137
+ /**
138
+ * YAML-quote a description value for `.mdc` frontmatter.
139
+ *
140
+ * @param {string} value
141
+ * @returns {string}
142
+ */
143
+ function yamlQuote(value) {
144
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
145
+ }
146
+
147
+ /**
148
+ * Rewrite intra-rule cross-link URLs from the nested `.md` layout to the flat
149
+ * `.mdc` layout so links in flattened rules still resolve.
150
+ *
151
+ * Only the URL inside a Markdown `](...)` link is rewritten — the link TEXT is
152
+ * left untouched (so `[reference/base-rules.md](../reference/base-rules.md)`
153
+ * keeps its readable label while the target becomes `base-rules-reference.mdc`).
154
+ * Two URL shapes are handled:
155
+ * - tier-prefixed: `(../)?eager/<slug>.md` → `<slug>.mdc`;
156
+ * `(../)?reference/<slug>.md` → `<slug>-reference.mdc`
157
+ * - bare same-dir: `<slug>.md` (no slash) → `<slug>.mdc`
158
+ * Optional `#fragment` suffixes are preserved. Constraint: a bare same-dir link
159
+ * is assumed to target the eager / top-level rule of that slug — the real eager
160
+ * bodies always use an explicit `../reference/<name>.md` URL for the reference
161
+ * tier, so bare links never need the `-reference` suffix. URLs with any other
162
+ * shape (deeper paths, external `.md`, http links) are left as-is.
163
+ *
164
+ * @param {string} body Rule markdown content.
165
+ * @returns {string}
166
+ */
167
+ function rewriteRuleLinks(body) {
168
+ return body.replace(/\]\(([^)]+)\)/g, (match, url) => {
169
+ const tiered =
170
+ /^(?:\.\.\/)?(eager|reference)\/([A-Za-z0-9._-]+)\.md(#[^)]*)?$/.exec(
171
+ url
172
+ );
173
+ if (tiered) {
174
+ const [, tier, slug, fragment = ""] = tiered;
175
+ const base = tier === "reference" ? `${slug}-reference` : slug;
176
+ return `](${base}.mdc${fragment})`;
177
+ }
178
+ const bare = /^([A-Za-z0-9._-]+)\.md(#[^)]*)?$/.exec(url);
179
+ if (bare) {
180
+ const [, slug, fragment = ""] = bare;
181
+ return `](${slug}.mdc${fragment})`;
182
+ }
183
+ return match;
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Transform the copied nested `rules/` tree into flat Cursor-native
189
+ * `rules/<name>.mdc` files with YAML frontmatter.
190
+ *
191
+ * The base plugin splits rules into `rules/eager/*.md` (always-on) and
192
+ * `rules/reference/*.md` (on-request); stack plugins instead ship a single
193
+ * always-on `rules/<name>.md` at the top level. Both layouts are normalized:
194
+ * - eager rule → `rules/<name>.mdc` with `alwaysApply: true`
195
+ * - reference rule → `rules/<name>-reference.mdc` with `alwaysApply: false`
196
+ * (the `-reference` suffix prevents a same-path collision, since eager and
197
+ * reference share base names)
198
+ * - top-level stack rule → `rules/<name>.mdc` with `alwaysApply: true`
199
+ * Plain top-level `.md` rules are rewritten in place to `.mdc`; the nested
200
+ * `rules/eager/` and `rules/reference/` subdirs are removed afterward.
201
+ *
202
+ * @param {string} outDir Cursor variant output directory.
203
+ */
204
+ function transformRules(outDir) {
205
+ const rulesDir = path.join(outDir, "rules");
206
+ if (!fs.existsSync(rulesDir)) return;
207
+
208
+ /**
209
+ * Write one `.mdc` rule with frontmatter, rewriting intra-rule links.
210
+ *
211
+ * @param {string} srcFile Absolute path of the source `.md` rule.
212
+ * @param {string} slug Output rule slug (filename without extension).
213
+ * @param {boolean} alwaysApply Frontmatter `alwaysApply` value.
214
+ */
215
+ const writeMdc = (srcFile, slug, alwaysApply) => {
216
+ const body = fs.readFileSync(srcFile, "utf8");
217
+ const frontmatter = `---\ndescription: ${yamlQuote(
218
+ deriveRuleDescription(body, slug)
219
+ )}\nalwaysApply: ${alwaysApply}\n---\n\n`;
220
+ fs.writeFileSync(
221
+ path.join(rulesDir, `${slug}.mdc`),
222
+ frontmatter + rewriteRuleLinks(body)
223
+ );
224
+ };
225
+
226
+ // Eager / reference subdir layout (base plugin).
227
+ const tiers = [
228
+ { sub: "eager", alwaysApply: true, suffix: "" },
229
+ { sub: "reference", alwaysApply: false, suffix: "-reference" },
230
+ ];
231
+ for (const { sub, alwaysApply, suffix } of tiers) {
232
+ const subDir = path.join(rulesDir, sub);
233
+ if (!fs.existsSync(subDir)) continue;
234
+ for (const entry of fs.readdirSync(subDir)) {
235
+ if (!entry.endsWith(".md")) continue;
236
+ const slug = entry.slice(0, -".md".length);
237
+ writeMdc(path.join(subDir, entry), `${slug}${suffix}`, alwaysApply);
238
+ }
239
+ fs.rmSync(subDir, { recursive: true, force: true });
240
+ }
241
+
242
+ // Top-level stack rules (e.g. lisa-rails, lisa-harper-fabric): single
243
+ // always-on `.md` → `.mdc` with alwaysApply:true.
244
+ for (const entry of fs.readdirSync(rulesDir)) {
245
+ if (!entry.endsWith(".md")) continue;
246
+ const srcFile = path.join(rulesDir, entry);
247
+ if (!fs.statSync(srcFile).isFile()) continue;
248
+ writeMdc(srcFile, entry.slice(0, -".md".length), true);
249
+ fs.rmSync(srcFile);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Rename a copied `.mcp.json` to Cursor's auto-discovered `mcp.json`.
255
+ *
256
+ * @param {string} outDir Cursor variant output directory.
257
+ */
258
+ function renameMcpFile(outDir) {
259
+ const dotMcp = path.join(outDir, ".mcp.json");
260
+ if (fs.existsSync(dotMcp)) {
261
+ fs.renameSync(dotMcp, path.join(outDir, "mcp.json"));
262
+ }
263
+ }
264
+
85
265
  /**
86
266
  * Generate the Cursor variant.
87
267
  *
@@ -105,11 +285,10 @@ export function generateCursorVariant(srcDir, outDir, version) {
105
285
  // Drop the `.codex-plugin/` directory — Cursor does not consume it.
106
286
  if (relPath.startsWith(".codex-plugin/") || relPath === ".codex-plugin")
107
287
  return false;
108
- // Defensively drop any hooks/hooks.json. The base build now emits the Codex
109
- // hooks manifest under .codex-plugin/ (stripped above), not here (issue
110
- // #1058), but guard against a regression reintroducing it: Cursor reads its
111
- // (filtered) hooks from .claude-plugin/plugin.json, not a Codex-shaped file.
112
- // The surviving .sh scripts in hooks/ are kept below.
288
+ // Drop any source-provided hooks/hooks.json (e.g. a Codex-shaped leak from a
289
+ // regression; the base build emits Codex hooks under .codex-plugin/, stripped
290
+ // above). We emit our OWN Cursor-shaped hooks/hooks.json below from the
291
+ // manifest's hook block, so any copied one would be wrong.
113
292
  if (relPath === path.join("hooks", "hooks.json")) return false;
114
293
  // Drop Codex-specific per-skill openai.yaml artifacts — Cursor does not use them.
115
294
  // These live at skills/<n>/agents/openai.yaml and are generated by the Codex
@@ -135,18 +314,31 @@ export function generateCursorVariant(srcDir, outDir, version) {
135
314
  }
136
315
  }
137
316
 
138
- // 2. Read + filter the manifest.
317
+ // 1b. Flatten the nested rules/ tree into Cursor-native rules/*.mdc files.
318
+ transformRules(outDir);
319
+
320
+ // 1c. Rename .mcp.json → mcp.json (Cursor auto-discovers the un-dotted name).
321
+ renameMcpFile(outDir);
322
+
323
+ // 2. Read the manifest, stamp the version, and strip the inline hook block.
324
+ // Cursor reads hooks from hooks/hooks.json (emitted in 2a), never inline.
139
325
  const manifestPath = path.join(outDir, ".claude-plugin", "plugin.json");
140
326
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
141
327
  manifest.version = version;
142
- const filteredHooks = filterHooksForAgent(manifest.hooks ?? {}, "cursor");
143
- if (filteredHooks) {
144
- manifest.hooks = filteredHooks;
145
- } else {
146
- delete manifest.hooks;
147
- }
328
+ const cursorHooks = buildCursorHooksJson(manifest.hooks ?? {});
329
+ delete manifest.hooks;
148
330
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
149
331
 
332
+ // 2a. Emit hooks/hooks.json in Cursor's native shape when any hooks survive.
333
+ if (cursorHooks) {
334
+ const cursorHooksDir = path.join(outDir, "hooks");
335
+ fs.mkdirSync(cursorHooksDir, { recursive: true });
336
+ fs.writeFileSync(
337
+ path.join(cursorHooksDir, "hooks.json"),
338
+ JSON.stringify(cursorHooks, null, 2) + "\n"
339
+ );
340
+ }
341
+
150
342
  // 3. Filter the hooks/ directory to match the script ship-list.
151
343
  const hooksDir = path.join(outDir, "hooks");
152
344
  if (fs.existsSync(hooksDir)) {