brief

package
v0.1.104 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package brief is an app's brief: what it is for, who owns the data, how it looks, which services it uses, where it runs and how agents work on it, answered by the people who own the app. It lives in docs/brief.md, in git, so every developer and every agent reads the same thing; the kickoff interview (`lidza brief`, or the MCP tools lidza_brief and lidza_brief_answer) fills it with suggestions or free answers. An answer also lands where it acts: a decision in docs/decisions.md, the working agreements in the agent files, the palette in the design tokens, a seeded app recipe.

Index

Constants

View Source
const (
	RecipeScope = "Scope a query in this app"
	RecipeStyle = "Style a page to match the app"
	RecipeData  = "Import or seed data"
)

Recipes the brief seeds in the guide's App recipes section. Each is written once, from the first answer that calls for it; after that it is the app's, edited like any other recipe.

View Source
const AgentFile = "AGENTS.md"

AgentFile holds the instructions every coding agent reads: Codex and the tools that follow the AGENTS.md convention read it as it is, and Claude Code and Gemini CLI through the one-line stubs AgentStubs.

View Source
const AgentStub = "@" + AgentFile + "\n"

AgentStub is the content of each stub.

View Source
const File = "docs/brief.md"

File is the brief, relative to the project root.

View Source
const OwnBrand = "Our own brand colours"

OwnBrand is the suggestion for an app that has its colours already.

Variables

View Source
var AgentStubs = []string{"CLAUDE.md", "GEMINI.md"}

AgentStubs import AgentFile with "@AGENTS.md", which Claude Code and Gemini CLI expand; Codex has no imports, so the content stays in AGENTS.md.

View Source
var Palettes = []Palette{
	{Name: "Mulberry and paper", Detail: "Līdza's own: a berry accent on warm neutrals",
		Light:  map[string]string{"accent": "oklch(47% 0.13 345)", "accent-fg": "#ffffff", "bg": "#f6f2ed", "surface": "#fffdfa", "surface-2": "#f1ebe4", "line": "#e4dbd1", "fg": "#2a2224", "muted": "#7b6f69", "sidebar": "#241821"},
		Dark:   map[string]string{"accent": "oklch(76% 0.1 345)", "accent-fg": "#221519", "bg": "#171214", "surface": "#201a1d", "surface-2": "#2a2226", "line": "#372d32", "fg": "#eee7e3", "muted": "#a89a93"},
		Tokens: map[string]string{"brand": "oklch(47% 0.13 345)", "brand-strong": "oklch(40% 0.13 345)", "ink": "#2a2224", "muted": "#7b6f69", "surface": "#f8f4ef", "line": "#e4dbd1"}},
	{Name: "Forest and linen", Detail: "a deep green on off-white linen",
		Light:  map[string]string{"accent": "oklch(45% 0.09 160)", "accent-fg": "#ffffff", "bg": "#f4f3ee", "surface": "#fdfcf8", "surface-2": "#ecebe3", "line": "#deddd2", "fg": "#1f2622", "muted": "#6b726c", "sidebar": "#17241e"},
		Dark:   map[string]string{"accent": "oklch(75% 0.09 160)", "accent-fg": "#10201a", "bg": "#121714", "surface": "#1a201c", "surface-2": "#222a25", "line": "#2e3832", "fg": "#e6ebe7", "muted": "#99a39c"},
		Tokens: map[string]string{"brand": "oklch(45% 0.09 160)", "brand-strong": "oklch(38% 0.09 160)", "ink": "#1f2622", "muted": "#6b726c", "surface": "#f6f5f0", "line": "#deddd2"}},
	{Name: "Ink and saffron", Detail: "near-black ink with a saffron accent",
		Light:  map[string]string{"accent": "oklch(72% 0.16 70)", "accent-fg": "#1d1a16", "bg": "#f7f5f0", "surface": "#ffffff", "surface-2": "#efece5", "line": "#e2ddd3", "fg": "#1d1d22", "muted": "#6d6b70", "sidebar": "#16161b"},
		Dark:   map[string]string{"accent": "oklch(78% 0.15 75)", "accent-fg": "#1d1a16", "bg": "#141417", "surface": "#1c1c21", "surface-2": "#25252b", "line": "#33333a", "fg": "#ececf0", "muted": "#9d9ca3"},
		Tokens: map[string]string{"brand": "oklch(62% 0.16 65)", "brand-strong": "oklch(54% 0.15 60)", "ink": "#1d1d22", "muted": "#6d6b70", "surface": "#f7f5f0", "line": "#e2ddd3"}},
	{Name: "Deep teal and sand", Detail: "a sea teal on sand",
		Light:  map[string]string{"accent": "oklch(48% 0.08 195)", "accent-fg": "#ffffff", "bg": "#f5f2ec", "surface": "#fffdf9", "surface-2": "#ede8df", "line": "#e0d9cc", "fg": "#1f2426", "muted": "#6c7275", "sidebar": "#142326"},
		Dark:   map[string]string{"accent": "oklch(76% 0.08 195)", "accent-fg": "#0f1c1e", "bg": "#111719", "surface": "#192023", "surface-2": "#212a2d", "line": "#2c373a", "fg": "#e5ecee", "muted": "#97a3a6"},
		Tokens: map[string]string{"brand": "oklch(48% 0.08 195)", "brand-strong": "oklch(41% 0.08 195)", "ink": "#1f2426", "muted": "#6c7275", "surface": "#f7f4ee", "line": "#e0d9cc"}},
}

Palettes are the suggestions; none is a stock blue.

View Source
var Questions = []Question{

	{ID: "purpose", Section: "Product", Kind: Text, Required: true,
		Ask: "What does the app do, in one sentence?",
		Why: "Every agent reads it first; it decides what a feature is for."},
	{ID: "users", Section: "Product", Kind: Many, Required: true,
		Ask: "Who uses it?",
		Why: "Sets the accounts, the tone and what a page shows first.",
		Suggestions: []Suggestion{
			{"Individuals, for themselves", ""}, {"Small teams working together", ""},
			{"Businesses and their customers", "two sides with different pages"},
			{"Visitors who only read", "public pages without an account"}, {"Our own staff", "an internal tool"},
		}},
	{ID: "journeys", Section: "Product", Kind: Text, Required: true,
		Ask: "What are the three to five things people come to do?",
		Why: "They become the first features, the browser tests and the landing page."},
	{ID: "out_of_scope", Section: "Product", Kind: Many,
		Ask:         "What is out of scope for now?",
		Why:         "Agents do not build it, and say so when a request needs it.",
		Suggestions: []Suggestion{{"Native mobile apps", ""}, {"Payments", ""}, {"Offline use", ""}, {"Public API for other developers", ""}, {"More than one language", ""}}},

	{ID: "ownership", Section: "Data", Kind: One, Required: true, Decision: "Brief: who owns the data",
		Ask: "Who owns the data, and who may see it?",
		Why: "Every query is scoped by it; the most expensive answer to change later.",
		Suggestions: []Suggestion{
			{"Each user owns their rows", "private; another user's row is a 404"},
			{"Shared with invited members", "projects, boards or lists with members and roles"},
			{"Organizations own everything", "teams; members by role; one account in several teams"},
			{"Public content with authors", "anyone reads, authors edit their own"},
			{"One organization, internal", "every signed-in user sees everything"},
		}},
	{ID: "roles", Section: "Data", Kind: One,
		Ask:         "Which roles are there?",
		Why:         "Decides the membership table and every permission check.",
		Suggestions: []Suggestion{{"Owner and member", ""}, {"Admin, editor and viewer", ""}, {"No roles: everyone who has access may do everything", ""}}},
	{ID: "personal_data", Section: "Data", Kind: Many, Decision: "Brief: personal data kept",
		Ask:         "Which personal data does it keep?",
		Why:         "Decides what is encrypted, exported, deleted, and what the privacy page says.",
		Suggestions: []Suggestion{{"Email addresses only", ""}, {"Names and profiles", ""}, {"Files people upload", ""}, {"Payment details, through a provider", "the app never stores card numbers"}, {"Regulated data", "health, finance, children: say which"}}},
	{ID: "retention", Section: "Data", Kind: One,
		Ask:         "What happens to an account's data when it is deleted?",
		Why:         "Becomes the delete-account feature and the jobs that purge.",
		Suggestions: []Suggestion{{"Deleted at once", ""}, {"Kept 30 days, then purged", "undo is possible"}, {"Kept for legal reasons", "say how long"}}},

	{ID: "signin", Section: "Accounts", Kind: Many, Required: true, Decision: "Brief: sign-in",
		Ask:         "How do people sign in?",
		Why:         "Configures auth.Mount and the sign-in page.",
		Suggestions: []Suggestion{{"Email and password", ""}, {"Google", ""}, {"GitHub", ""}, {"Microsoft", "work and school accounts"}, {"A company identity provider", "Okta, Keycloak, Auth0: OpenID Connect"}}},
	{ID: "registration", Section: "Accounts", Kind: One,
		Ask:         "Who may create an account?",
		Why:         "auth.Options NoRegister, invitations, or open sign-up.",
		Suggestions: []Suggestion{{"Anyone", "open registration"}, {"Only invited people", ""}, {"Only admins create accounts", ""}}},

	{ID: "palette", Section: "Design", Kind: One, Required: true, Decision: "Brief: palette",
		Ask:         "Which palette?",
		Why:         "Written into the design tokens (src/index.css) and the admin theme; agents never fall back to a stock blue.",
		Suggestions: paletteSuggestions()},
	{ID: "typography", Section: "Design", Kind: One,
		Ask:         "Which typography?",
		Why:         "Sets the fonts in the tokens and the page recipe.",
		Suggestions: []Suggestion{{"An editorial serif for headings, a clean sans for text", ""}, {"One clean sans throughout", ""}, {"A rounded, friendly sans", ""}, {"A sans with monospace accents", "technical tools"}}},
	{ID: "mood", Section: "Design", Kind: Many,
		Ask:         "What should it feel like?",
		Why:         "Spacing, density, motion and the tone of the copy.",
		Suggestions: []Suggestion{{"Calm and spacious", ""}, {"Dense and efficient", "many rows per screen"}, {"Playful", ""}, {"Premium", ""}, {"Plain and technical", ""}}},
	{ID: "themes", Section: "Design", Kind: One,
		Ask:         "Light and dark?",
		Why:         "Whether every page ships both themes.",
		Suggestions: []Suggestion{{"Light and dark, following the system, with a toggle", ""}, {"Light only", ""}, {"Dark only", ""}}},
	{ID: "languages", Section: "Design", Kind: Many,
		Ask:         "Which languages?",
		Why:         "The i18n pack's catalogs from the first page, or English strings only.",
		Suggestions: []Suggestion{{"English", ""}, {"Albanian", ""}, {"German", ""}, {"Italian", ""}, {"French", ""}, {"Spanish", ""}}},
	{ID: "locale", Section: "Design", Kind: Text,
		Ask:         "Which currency and time zone?",
		Why:         "Formatting of money and dates.",
		Suggestions: []Suggestion{{"EUR, Europe/Tirane", ""}, {"USD, America/New_York", ""}, {"The viewer's own", "from the browser"}}},

	{ID: "content", Section: "Content", Kind: One, Decision: "Brief: where the content comes from",
		Ask:         "Where does the content come from?",
		Why:         "Decides the import job, the seed data and the fixtures tests use.",
		Suggestions: []Suggestion{{"People create it in the app", ""}, {"Seed data we provide", ""}, {"A public dataset", "name it and its licence"}, {"An existing database to import", ""}}},

	{ID: "mail", Section: "Services", Kind: One, Decision: "Brief: mail provider",
		Ask:         "Which mail provider in production?",
		Why:         "The mail pack's settings; tests use the outbox either way.",
		Suggestions: []Suggestion{{"Resend", ""}, {"Postmark", ""}, {"Mailgun", ""}, {"An SMTP server", ""}, {"No mail", ""}}},
	{ID: "model", Section: "Services", Kind: One, Decision: "Brief: language model",
		Ask:         "Which language model provider?",
		Why:         "The llm pack's settings; embeddings need OpenAI, Gemini, Ollama or a compatible server.",
		Suggestions: []Suggestion{{"Anthropic", ""}, {"OpenAI", ""}, {"Google Gemini", ""}, {"A local or self-hosted server", "llama.cpp, vLLM, Ollama"}, {"No model", ""}}},
	{ID: "model_budget", Section: "Services", Kind: One,
		Ask:         "How much may the model cost a month?",
		Why:         "Agents cache, pick smaller models and label usage to stay under it.",
		Suggestions: []Suggestion{{"Under $20", ""}, {"Under $100", ""}, {"Under $500", ""}, {"No limit yet", ""}}},
	{ID: "storage", Section: "Services", Kind: One, Decision: "Brief: file storage",
		Ask:         "Where do files go in production?",
		Why:         "The storage pack's settings.",
		Suggestions: []Suggestion{{"Amazon S3", ""}, {"Cloudflare R2", ""}, {"Another S3-compatible service", ""}, {"No files", ""}}},
	{ID: "payments", Section: "Services", Kind: One, Decision: "Brief: payments",
		Ask:         "Payments?",
		Why:         "A provider's checkout and webhooks, keys in the credentials.",
		Suggestions: []Suggestion{{"Stripe", ""}, {"Paddle", "merchant of record"}, {"Not needed", ""}}},

	{ID: "hosting", Section: "Deployment", Kind: One, Decision: "Brief: hosting",
		Ask:         "Where will it run?",
		Why:         "lidza ship's output: the systemd unit or the image.",
		Suggestions: []Suggestion{{"One server with systemd", ""}, {"Docker on a server", ""}, {"A container platform", "Fly, Render, Kubernetes"}, {"Not decided", ""}}},
	{ID: "domain", Section: "Deployment", Kind: Text,
		Ask: "Which domain?",
		Why: "LIDZA_TLS_DOMAINS, the sign-in callbacks and the links in mail."},
	{ID: "scale", Section: "Deployment", Kind: One,
		Ask:         "How many people in the first year?",
		Why:         "How far the agents go with caching, paging and background work.",
		Suggestions: []Suggestion{{"Up to a hundred", ""}, {"Thousands", ""}, {"Tens of thousands or more", ""}}},
	{ID: "region", Section: "Deployment", Kind: One, Decision: "Brief: where data lives",
		Ask:         "Where must the data stay?",
		Why:         "The region of the database, storage and providers.",
		Suggestions: []Suggestion{{"In the EU", ""}, {"In the US", ""}, {"Anywhere", ""}}},

	{ID: "push", Section: "Working agreements", Kind: One, Required: true, Agreement: "Pushing",
		Ask:         "When should an agent push?",
		Why:         "Unpushed work runs no CI and nobody sees it.",
		Suggestions: []Suggestion{{"After every verified commit", ""}, {"When a feature is done", ""}, {"Never: I push myself", ""}}},
	{ID: "approval", Section: "Working agreements", Kind: Many, Agreement: "Ask first",
		Ask:         "What must an agent ask before doing?",
		Why:         "Everything else it does and reports.",
		Suggestions: []Suggestion{{"Adding a dependency", ""}, {"Changing the schema", ""}, {"Deleting data or files", ""}, {"Anything outside the request", ""}, {"Nothing: go ahead and report", ""}}},
	{ID: "tests", Section: "Working agreements", Kind: One, Agreement: "Tests",
		Ask:         "What is the test bar?",
		Why:         "What a feature needs before it counts as done.",
		Suggestions: []Suggestion{{"A Go test for every handler and a browser test for every page", ""}, {"A Go test for every handler", ""}, {"The critical paths only", ""}}},
	{ID: "done", Section: "Working agreements", Kind: Text, Agreement: "Done means",
		Ask:         "What else does done mean?",
		Why:         "Beyond green tests: docs, screenshots, a demo, a changelog line.",
		Suggestions: []Suggestion{{"The README says how to use it", ""}, {"A line in the changelog", ""}, {"Screenshots in the report", ""}}},
	{ID: "reports", Section: "Working agreements", Kind: One, Agreement: "Reports",
		Ask:         "How should an agent report?",
		Why:         "The shape of the message at the end of a task.",
		Suggestions: []Suggestion{{"Short: the outcome and what is next", ""}, {"Per feature: what was built and how it is tested", ""}, {"Only when blocked or done", ""}}},
}

Questions are the brief, in interview order.

View Source
var Sections = []string{"Product", "Data", "Accounts", "Design", "Content", "Services", "Deployment", "Working agreements"}

Sections, in interview order.

Functions

func AddNote

func AddNote(dir, text string) ([]string, error)

AddNote appends a dated note to the Team notes of every agent file; it returns the files it changed.

func AgentFiles

func AgentFiles(dir string) []string

AgentFiles lists the agent files in dir that hold instructions: AGENTS.md, and a CLAUDE.md or GEMINI.md that is still a full copy (an app from before the stubs that ConvertAgentStubs left alone because it differs).

func Clean

func Clean(q Question, answer string) string

Clean removes what a copy and paste of the interview can carry into an answer: the question header, the question and its explanation. What is left is the answer; "" when nothing is.

func ConvertAgentStubs

func ConvertAgentStubs(dir string) (changed, kept []string, err error)

ConvertAgentStubs makes CLAUDE.md and GEMINI.md the stubs: a missing one is written, and one identical to AGENTS.md (the three copies apps had before) is replaced. One that differs keeps its content, and is returned in kept so the developer merges it into AGENTS.md first.

func Ensure

func Ensure(dir, app string) (bool, error)

Ensure writes an empty brief when the app has none; it reports whether it did.

func EnsureAgentSections

func EnsureAgentSections(dir string) ([]string, error)

EnsureAgentSections gives agent files from before the brief their two closing sections; it returns the files it changed.

func Garbled

func Garbled(q Question, answer string) bool

Garbled reports whether a saved answer is not an answer: the pasted question or its explanation, an interview header, or menu numbers a terminal saved as text (briefs answered before those were resolved). Such a question counts as open, so the agent asks again instead of building on it.

func IsAgentStub

func IsAgentStub(data []byte) bool

IsAgentStub reports whether data is a stub importing AgentFile.

func Save

func Save(dir string, b Brief) error

Save writes the brief: every question in order, its answer or "Open".

func Skip

func Skip(dir, app string, ids ...string) ([]string, error)

Skip marks questions as skipped: not answered, not asked again, not counted by L015. No ids skips every open question (the whole brief). An answer given later replaces the skip. It returns the ids it skipped.

func SyncAgreements

func SyncAgreements(dir string, b Brief) ([]string, error)

SyncAgreements writes the working-agreement answers into every agent file; it returns the files it changed.

Types

type Brief

type Brief struct {
	App     string
	Answers map[string]string
	// Skipped are the questions the team chose not to answer.
	Skipped map[string]bool
	Notes   string
}

Brief is the file's content: the app's name, the answers by question id, and the free notes at the end.

func Load

func Load(dir string) (Brief, error)

Load reads the brief; an app without one gets an empty Brief and os.ErrNotExist.

func (Brief) Open

func (b Brief) Open() []Question

Open lists the questions without an answer that were not skipped, in interview order: the ones an interview asks.

func (Brief) OpenRequired

func (b Brief) OpenRequired() []Question

OpenRequired lists the required questions without an answer.

type Kind

type Kind string

Kind is how a question is answered.

const (
	One  Kind = "one"
	Many Kind = "many"
	Text Kind = "text"
)

Kinds: one of the suggestions, any number of them, or free text. Every kind also takes an answer of the developer's own.

type Palette

type Palette struct {
	Name, Detail string
	// Light and Dark are the admin theme's --admin-* values.
	Light, Dark map[string]string
	// Tokens are the frontend's --color-* values.
	Tokens map[string]string
}

Palette is a set of design tokens an answer to "Which palette?" writes into the app: the Tailwind tokens of src/index.css (react) and the admin pages' theme (admin/theme.css), light and dark.

type Question

type Question struct {
	ID      string `json:"id"`
	Section string `json:"section"`
	Ask     string `json:"ask"`
	// Why says what the answer changes, so the developer knows why it is
	// asked and the agent knows where it applies.
	Why         string       `json:"why"`
	Kind        Kind         `json:"kind"`
	Suggestions []Suggestion `json:"suggestions,omitempty"`
	// Required questions are the ones an agent cannot build well without;
	// lidza check warns while they are open (L015).
	Required bool `json:"required,omitempty"`
	// Decision, when set, records the answer in docs/decisions.md under
	// this title.
	Decision string `json:"-"`
	// Agreement puts the answer in the agent files' Working agreements.
	Agreement string `json:"-"`
}

Question is one question of the brief.

func Find

func Find(id string) (Question, bool)

Find returns a question by id.

type Result

type Result struct {
	ID       string   `json:"id"`
	Answer   string   `json:"answer"`
	Files    []string `json:"files"`
	Decision string   `json:"decision,omitempty"`
	Recipe   string   `json:"recipe,omitempty"`
	// Manual lists what the agent applies by hand (a stylesheet without
	// tokens, an admin theme the app wrote itself).
	Manual []string `json:"manual,omitempty"`
	// Open counts the questions still without an answer.
	Open int `json:"open"`
}

Result is what an answer changed.

func Answer

func Answer(dir, app, id, answer string) (Result, error)

Answer records the answer to a question and applies it: a decision, the agent files' working agreements, the palette, a seeded recipe. An empty answer clears the question.

type Suggestion

type Suggestion struct {
	Value  string `json:"value"`
	Detail string `json:"detail,omitempty"`
}

Suggestion is one proposed answer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL