Go 50 lines
package cockpit
import (
"errors"
"fmt"
"strings"
)
// cursorRenderer emits a Playbook as a modern Cursor rule file
// (.cursor/rules/<name>.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
}