ajhahn.de
← eeco
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())
}