Go 116 lines
package cockpit
import (
"fmt"
"sort"
"strings"
)
// advisoryBanner is the loud header every advisory artifact (Cursor,
// AGENTS.md, GEMINI.md) carries so the operator can never mistake it for
// harness-enforced config. It is a single package constant shared by the
// renderers and the self-consistency parser, so the honesty notice can never
// silently drift; fidelityBannerPresent asserts it in tests.
const advisoryBanner = "**ADVISORY ONLY — NOT HARNESS-ENFORCED.** This file documents the tool policy " +
"the AI should honor; the harness does not enforce it at runtime. eeco still refuses to emit any " +
"playbook that declares a forbidden write-git verb — the advisory note describes only the harness side."
// Heading texts the self-consistency parser keys on. The renderers and the
// parser reference the same constants so a section can never be renamed in one
// place and silently missed in the other.
const (
headingForbidden = "Forbidden" // section listing the safety denylist
headingAllowed = "Allowed (advisory)" // section listing the composed allowlist
headingStep = "Step " // prefix of a per-step heading's text
headingOutput = "Output" // closing output-format section
)
// renderPlaybookBody appends the shared advisory body for one playbook under
// the given heading depth marker (e.g. "##" for a per-file Cursor doc, "###"
// for a section inside an aggregate AGENTS.md / GEMINI.md). The structure —
// derived safety warning, a Forbidden block enumerating both the git-verb
// denylist (as `git <verb>`) and the human Intent.Forbidden phrases, an
// Allowed (advisory) list from composeAllowedTools, the numbered Steps, and
// the Output section — is identical across advisory targets so one
// self-consistency parser serves them all.
func renderPlaybookBody(b *strings.Builder, p Playbook, depth string) {
b.WriteString(deriveSafetyWarning(p.Intent))
b.WriteString("\n\n")
fmt.Fprintf(b, "%s %s\n", depth, headingForbidden)
for _, v := range p.Intent.forbiddenVerbs() {
fmt.Fprintf(b, "- `git %s`\n", v)
}
for _, ph := range p.Intent.Forbidden {
fmt.Fprintf(b, "- %s\n", ph)
}
b.WriteString("\n")
fmt.Fprintf(b, "%s %s\n", depth, headingAllowed)
for _, a := range composeAllowedTools(p) {
fmt.Fprintf(b, "- %s\n", a)
}
b.WriteString("\n")
for _, s := range p.Steps {
fmt.Fprintf(b, "%s %s%d — %s\n", depth, headingStep, s.Index, s.Title)
if body := strings.TrimRight(s.Body, "\n"); body != "" {
b.WriteString(body)
b.WriteString("\n")
}
if len(s.Runs) > 0 {
b.WriteString("\n```\n")
for _, run := range s.Runs {
b.WriteString(run)
b.WriteString("\n")
}
b.WriteString("```\n")
}
b.WriteString("\n")
}
if out := strings.TrimRight(p.OutputFormat, "\n"); out != "" {
fmt.Fprintf(b, "%s %s\n", depth, headingOutput)
b.WriteString(out)
b.WriteString("\n")
}
}
// renderAggregateMarkdown renders a whole playbook set as one advisory
// Markdown document: an H1 title, the ADVISORY banner, an optional
// target-specific header note, a fidelity report naming the set, then one
// section per playbook. The set is sorted by Name so the output is
// byte-deterministic regardless of input order (re-emit stays sha-idempotent).
// Shared by the AGENTS.md and GEMINI.md renderers.
func renderAggregateMarkdown(ps []Playbook, title, header string) []byte {
set := append([]Playbook(nil), ps...)
sort.Slice(set, func(i, j int) bool { return set[i].Name < set[j].Name })
var b strings.Builder
fmt.Fprintf(&b, "# %s\n\n", title)
b.WriteString(advisoryBanner)
b.WriteString("\n\n")
if h := strings.TrimSpace(header); h != "" {
b.WriteString(h)
b.WriteString("\n\n")
}
b.WriteString("## Fidelity report\n\n")
b.WriteString("Enforcement: advisory (not harness-enforced). Playbooks in this file:\n\n")
for _, p := range set {
fmt.Fprintf(&b, "- %s\n", p.Name)
}
b.WriteString("\n")
for _, p := range set {
fmt.Fprintf(&b, "## %s\n\n", deriveTitle(p.Name))
if d := strings.TrimSpace(p.Description); d != "" {
b.WriteString(d)
b.WriteString("\n\n")
}
renderPlaybookBody(&b, p, "###")
b.WriteString("\n")
}
return []byte(b.String())
}