feat(cursor): discover FS skills and apply $mentions on send#4297
feat(cursor): discover FS skills and apply $mentions on send#4297blaqat wants to merge 5 commits into
Conversation
Populate Cursor provider snapshot.skills from project/user .cursor/skills SKILL.md files, and inject matched skill bodies in CursorAdapter.sendTurn so Codex-style $name tokens are useful under ACP (which has no $ interpreter). Co-authored-by: Cursor <cursoragent@cursor.com>
Align snapshot `$` skill listing with send-path apply by scanning project skills under the active workspace cwd instead of process.cwd(). Co-authored-by: Cursor <cursoragent@cursor.com>
Provider `$` listing is process-wide from ServerConfig.cwd, while send used only session cwd. Merge both project roots on send-apply so menu-listed skills still inject when a thread/worktree cwd differs. Co-authored-by: Cursor <cursoragent@cursor.com>
Reject symlink escapes via realpath/lstat containment, require a leading boundary on $skill mentions so ENV=$name is not expanded, and cap injected SKILL.md bodies (strip frontmatter, 64KiB) at discover and apply. Co-authored-by: Cursor <cursoragent@cursor.com>
Record beach-mode gate status, shipped SHAs, and residuals (per-thread menu, OpenCode, ACP slash) before push. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7678e7b. Configure here.
| Effect.provideService(Path.Path, path), | ||
| Effect.orElseSucceed(() => [] as const), | ||
| ); | ||
| const promptText = applyCursorSkillMentions(input.input.trim(), discoveredSkills); |
There was a problem hiding this comment.
Scans skills on every send
Medium Severity
Every sendTurn with non-empty text unconditionally calls discoverCursorSkills, scanning project and user skill directories. This occurs even when no $ skill mention is present in the input, adding unnecessary filesystem I/O and latency to most turns.
Reviewed by Cursor Bugbot for commit 7678e7b. Configure here.
| discoveredModels = discoveryExit.value; | ||
| } | ||
| } | ||
| // Soft-fail FS skill discovery for the `$` menu snapshot. |
There was a problem hiding this comment.
Probe early exit drops skills
Medium Severity
checkCursorProviderStatus only runs filesystem skill discovery after the parameterized model-picker gate. When that check fails (for example non-lab CLI channel or an older Agent version), the function returns early with default empty skills, so the composer $ menu stays empty even when .cursor/skills files exist on disk.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7678e7b. Configure here.
| if (!realSkillPath || !isPathInsideRoot(realSkillsRoot, realSkillPath)) { | ||
| continue; | ||
| } | ||
| const raw = yield* readFile(realSkillPath); |
There was a problem hiding this comment.
🟡 Medium provider/cursorSkillDiscovery.ts:390
discoverCursorSkills calls fileSystem.readFileString to read each SKILL.md entirely into memory, and only afterward does parseCursorSkillMarkdown enforce the 64 KiB cap. A multi-megabyte SKILL.md under a scanned skills root is therefore fully materialized on every invocation before being rejected, so the size cap does not bound discovery memory usage and a large file can exhaust the server heap. Consider checking the file size via stat (or doing a bounded read) before reading the full contents.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/cursorSkillDiscovery.ts around line 390:
`discoverCursorSkills` calls `fileSystem.readFileString` to read each `SKILL.md` entirely into memory, and only afterward does `parseCursorSkillMarkdown` enforce the 64 KiB cap. A multi-megabyte `SKILL.md` under a scanned skills root is therefore fully materialized on every invocation before being rejected, so the size cap does not bound discovery memory usage and a large file can exhaust the server heap. Consider checking the file size via `stat` (or doing a bounded read) before reading the full contents.
| /** Strip leading YAML frontmatter; returns body only (trimmed). */ | ||
| export function stripYamlFrontmatter(content: string): string { | ||
| const normalized = content.replace(/^\uFEFF/, ""); | ||
| if (!normalized.startsWith("---")) { |
There was a problem hiding this comment.
🟡 Medium provider/cursorSkillDiscovery.ts:79
stripYamlFrontmatter treats any content starting with --- and any later line starting with --- as YAML frontmatter delimiters, without requiring delimiter-only lines. A skill file that begins with a Markdown horizontal rule (---) and has another horizontal rule later silently loses everything through the second rule; strings like ---not-frontmatter are also misparsed as frontmatter. Match complete delimiter lines only (i.e., --- followed immediately by a newline or end-of-string).
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/cursorSkillDiscovery.ts around line 79:
`stripYamlFrontmatter` treats any content starting with `---` and any later line starting with `---` as YAML frontmatter delimiters, without requiring delimiter-only lines. A skill file that begins with a Markdown horizontal rule (`---`) and has another horizontal rule later silently loses everything through the second rule; strings like `---not-frontmatter` are also misparsed as frontmatter. Match complete delimiter lines only (i.e., `---` followed immediately by a newline or end-of-string).
| return prompt; | ||
| } | ||
|
|
||
| const appliedNames = new Set(applied.map((skill) => skill.name.toLowerCase())); |
There was a problem hiding this comment.
🟡 Medium provider/cursorSkillDiscovery.ts:467
applyCursorSkillMentions collapses every run of two or more spaces or tabs across the entire prompt when it removes a $skill mention. Any unrelated whitespace-sensitive content — indented code blocks, tables, aligned text — is destroyed. For example, a prompt containing Python code with 4-space indentation gets those indent levels collapsed to a single space whenever any skill mention is expanded. The .replace(/[ \t]{2,}/g, " ") cleanup on line 472 applies globally instead of only around the removed token, so it rewrites content far from the deleted mention. Consider limiting whitespace normalization to the immediate vicinity of each removed $skill token rather than the whole prompt.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/cursorSkillDiscovery.ts around line 467:
`applyCursorSkillMentions` collapses every run of two or more spaces or tabs across the entire prompt when it removes a `$skill` mention. Any unrelated whitespace-sensitive content — indented code blocks, tables, aligned text — is destroyed. For example, a prompt containing Python code with 4-space indentation gets those indent levels collapsed to a single space whenever any skill mention is expanded. The `.replace(/[ \t]{2,}/g, " ")` cleanup on line 472 applies globally instead of only around the removed token, so it rewrites content far from the deleted mention. Consider limiting whitespace normalization to the immediate vicinity of each removed `$skill` token rather than the whole prompt.
| function formatInjectedSkillBlock(skill: Pick<DiscoveredCursorSkill, "name" | "content">): string { | ||
| const body = skillBodyForInjection(skill.content) ?? ""; | ||
| return [`Skill \`${skill.name}\` (applied by T3 Code for Cursor ACP):`, "", body].join("\n"); | ||
| } |
There was a problem hiding this comment.
🟠 High provider/cursorSkillDiscovery.ts:424
formatInjectedSkillBlock re-runs skillBodyForInjection on skill.content that was already stripped during discovery and again at line 457, so a legitimate skill body starting with --- is parsed as YAML frontmatter and everything up to the next --- line is silently removed before injection. The injected text drops instructions the author wrote. Avoid re-stripping already-normalized content — pass the body through unchanged.
| function formatInjectedSkillBlock(skill: Pick<DiscoveredCursorSkill, "name" | "content">): string { | |
| const body = skillBodyForInjection(skill.content) ?? ""; | |
| return [`Skill \`${skill.name}\` (applied by T3 Code for Cursor ACP):`, "", body].join("\n"); | |
| } | |
| function formatInjectedSkillBlock(skill: Pick<DiscoveredCursorSkill, "name" | "content">): string { | |
| const body = skill.content; | |
| return [`Skill \`${skill.name}\` (applied by T3 Code for Cursor ACP):`, "", body].join("\n"); | |
| } |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/cursorSkillDiscovery.ts around lines 424-427:
`formatInjectedSkillBlock` re-runs `skillBodyForInjection` on `skill.content` that was already stripped during discovery and again at line 457, so a legitimate skill body starting with `---` is parsed as YAML frontmatter and everything up to the next `---` line is silently removed before injection. The injected text drops instructions the author wrote. Avoid re-stripping already-normalized content — pass the body through unchanged.
ApprovabilityVerdict: Needs human review 4 blocking correctness issues found. This PR introduces significant new capability (Cursor FS skill discovery and $mention injection) with ~2000 lines of new code. Multiple unresolved review comments exist, including a high-severity bug where skill content can be silently dropped during injection. New features of this scope with identified bugs warrant human review. You can customize Macroscope's approvability policy. Learn more. |
|
Closing — this PR was opened by mistake; not intended for review. |


Summary
<cwd>/.cursor/skills/**/SKILL.mdand~/.cursor/skills/**/SKILL.mdinto the provider$menu$skillmentions by injecting SKILL.md bodies into the ACP prompt (Cursor has no native$interpreter)$token boundary, 64KiB body cap + frontmatter stripCommits (5)
7173f10a3feat(cursor): discover FS skills and apply $mentions on send95e17c1c7fix(cursor): discover FS skills using ServerConfig.cwd44c3e9ad4fix(cursor): apply listed skills when session cwd diverges82c445edefix(cursor): harden FS skill discovery before ship7678e7bbfdocs(plans): mark Cursor FS skill Phase 1 doneTest plan
$— project/user skills appear$name)ENV=$foodoes not trigger skill expandpnpmfocused tests: cursorSkillDiscovery, CursorProvider, CursorAdapterNotes
/slash commands deferred$menu still process-wide (ServerConfig.cwd)Made with Cursor
Note
Discover filesystem Cursor skills and inject
$mentioncontent into prompts on sendcursorSkillDiscoverymodule that reads.cursor/skills/SKILL.mdfiles from the session cwd andServerConfig.cwd, enforcing size caps, symlink containment, and name validation.makeCursorAdapternow runs skill discovery and callsapplyCursorSkillMentionsinsendTurn, replacing$skilltokens with the corresponding SKILL.md body before sending to Cursor ACP.checkCursorProviderStatusaccepts a newcwdargument and includes discovered skills in the returnedServerProviderDraft; callers must now supply this argument.CursorDriver.createpassesserverConfig.cwdtocheckCursorProviderStatus, changing where skill discovery is rooted from the implicit process cwd to the configured workspace directory.📊 Macroscope summarized 7678e7b. 5 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.