package cockpit import ( "errors" "fmt" "strings" ) // cursorRenderer emits a Playbook as a modern Cursor rule file // (.cursor/rules/.mdc): YAML frontmatter (description / globs / // alwaysApply) followed by the shared advisory body. Cursor rules are // advisory — Cursor does not enforce a tool allowlist from an .mdc at runtime // (its enforced settings live in a separate permissions layer, out of scope // until C4) — so every emitted file carries the ADVISORY banner. Like Claude, // Cursor is per-playbook (one .mdc per playbook), so it flows through the // unchanged per-playbook emit machinery. type cursorRenderer struct{} func (cursorRenderer) Target() string { return "cursor" } // Enforcement reports that Cursor rules are advisory only. It satisfies the // Fidelity interface. func (cursorRenderer) Enforcement() Enforcement { return EnforcementAdvisory } func (cursorRenderer) RelPath(p Playbook) string { return ".cursor/rules/" + p.Name + ".mdc" } // Render produces the .mdc bytes. It is deterministic and rejects a Playbook // whose single-line frontmatter field (description) would span multiple lines // — the frontmatter is line-oriented, mirroring the Claude renderer's guard. func (cursorRenderer) Render(p Playbook) ([]byte, error) { desc := strings.TrimSpace(p.Description) if strings.ContainsAny(desc, "\r\n") { return nil, errors.New("playbook description must be a single line") } var b strings.Builder b.WriteString("---\n") fmt.Fprintf(&b, "description: %s\n", desc) b.WriteString("globs:\n") b.WriteString("alwaysApply: false\n") b.WriteString("---\n") fmt.Fprintf(&b, "# %s\n\n", deriveTitle(p.Name)) b.WriteString(advisoryBanner) b.WriteString("\n\n") renderPlaybookBody(&b, p, "##") return []byte(b.String()), nil }