Imported from SALeeWenLing/hackathon (
AGENTS.md). Install upstream withnpx skills add SALeeWenLing/hackathon. Copyright stays with the author.
AGENTS.md
Instructions for AI coding agents (Claude Code, Copilot, etc.) working in this repo. Read product-proposal.md first — it has the full rationale behind the choices below. This file is the enforceable subset. See BUILD_PLAN.md for the three-team build sequence, file ownership map, and checkpoints if you're building this from scratch.
Project in one line
English input + situation → structured intent → grounded retrieval → one 3-tier generation call (Textbook/Paisa/Nea) → lightweight validation/repair → cards + on-demand TTS.
Non-negotiable design constraints
These prevent specific, well-understood failure modes — see product-proposal.md §2 for the reasoning behind each. Do not silently "simplify" past them — they are deliberate design decisions, not decoration.
-
Never let one raw LLM sample become user-facing output. Every generation pass goes through the deterministic checks and the validator step before rendering. If a tier fails validation twice, fall back to plain natural Spanish — never serve a forced or caricatured line just because it's the only candidate.
-
The model must be allowed to decline slang. Prompts must explicitly instruct: if no verified regional expression naturally fits, return natural conversational Spanish rather than forcing one in. This is the single biggest lever against forced, unnatural regionalism insertion — the most common failure mode of naive slang-injection prompting. Do not remove this instruction when iterating on prompts.
-
Register generation is grounded, not freeform. Paisa and Nea generation must be conditioned on retrieved phrase-bank examples (
used_lexiconin the output should trace back to retrieved entries). Do not let the model invent regionalisms with no corpus backing — that's how caricature and outright wrong slang get produced. -
Nea ≠ Paisa + more slang. It's a distinct, identity-marked, socially riskier register. Any Nea output must:
- carry a usage note (
appropriate_when/avoid_when) - pass a register-separation check against the Paisa output for the same input (see
backend/src/pipeline/validate.ts— lexical overlap threshold) - for the
Tirar los perrossituation specifically, pass the flirty-tone guardrail (charming, not crude) — this is a separate check from the general aggression check used forProblemas en casa.
Authentic crude/vulgar language is expected in Nea, not a defect. Sanitizing it away for politeness misrepresents the register. Do not flag or block a Nea output for being rude, aggressive, or profane on its own — the bar is naturalness (would a real person actually say this here), not politeness. Reclaimed-slur-origin terms that are common as in-group address/insult slang (currently
marica,perra) are allowed, not blocked — but every occurrence requires a specific, substantiveavoid_whennaming the term's origin and its actual narrow safe-usage window, not generic boilerplate; seeSENSITIVE_TERMSinbackend/src/pipeline/validate.ts. Any other identity-based slur (racial, or homophobic/misogynistic terms outside that short list) stays a hard block — seeBLOCKED_TERMSin the same file. - carry a usage note (
-
Context drives generation, not just vocabulary choice. The situation object (
relationship,stakes,tone,context_confidence) produced by intent extraction is a hard input to generation and validation — not a hint. A request phrased identically in two situations (e.g.Con un parcerovsProblemas en casa) should produce meaningfully different Paisa/Nea output, not the same sentence with a word swapped. -
Situation chips map to underlying tags — don't collapse this. The user sees relatable labels (
Problemas en casa,Con un parcero,Resolviendo vueltas,Tirar los perros,Otros); the pipeline works off{relationship, stakes, tone}. If you add a new chip, define its tag mapping inbackend/src/corpus/situations.tsbefore wiring it into the UI.
Pipeline call budget
Target: 2–3 LLM calls per request in the common case (intent extraction → generation → optional single-tier repair). Do not add multi-candidate generation (N candidates per register) or a full scoring/selection layer without first running it against /eval/cases.json and confirming the simpler pipeline is failing often enough to justify the cost/latency increase. This is deferred scope, not forgotten scope — see product-proposal.md §14.
Explicitly out of scope — do not add without discussion
- Vector DB / pgvector (corpus is tag-filtered JSON at this size; revisit past ~150–200 entries)
- Redis or any distributed cache (single table / in-memory map is sufficient)
- User accounts, auth, contributor review queues
- Formal observability/analytics infra — a plain log of
{intent, retrieved_examples, validator_verdict}per request is enough for now
If a task seems to require one of these, flag it rather than implementing it — it likely means scope has drifted from the hackathon build toward the post-hackathon roadmap.
Corpus entries (backend/src/corpus/phrase-bank.json)
Every entry must include relationship and stakes tags, not just topic keywords — retrieval filters on these first, then ranks semantically. When adding entries, include a register_warning note for any Nea entry that could read as more aggressive/familiar than the situation warrants.
Evaluation
backend/eval/cases.json is the source of truth for whether a pipeline change is an improvement. Any change to intent extraction, retrieval, generation prompts, or validation logic should be run against the eval set before merging. Growing this set is higher priority than most new features — see product-proposal.md §12.
Repo layout
Two apps, one repo:
/frontend— React + TypeScript + Vite. Design was produced in Claude Design; when implementing components, match that design's visual language rather than defaulting to generic UI patterns. This app only talks to the backend over HTTP — it has no server routes and should never call Whisper/ElevenLabs/the LLM directly (keys must not reach the client)./backend— Node/TypeScript API service (Express) that owns the full pipeline: intent extraction, retrieval, generation, validation/repair, STT/TTS calls, caching, and the corpus. This is where nearly all agent work on pipeline logic happens.
Keep the pipeline logic entirely in /backend. Do not move any part of intent extraction, retrieval, generation, or validation into the frontend, even for "quick" prototyping — API keys and prompt logic stay server-side.
Style
- TypeScript throughout, in both apps — the request/response types for the API contract are deliberately duplicated (not re-exported from a shared package) in
backend/src/types.tsandfrontend/src/lib/types.ts. This is intentional: a shared package would be one more file both sides need to touch, reintroducing the conflict surface the frontend/backend split is meant to avoid. Keep the two files in sync by hand whenever the contract changes — seeBUILD_PLAN.mdGround rule 1. - Keep intent/generation/validation prompt templates in dedicated files under
backend/src/pipeline/, not inlined in route handlers — they change often during eval-driven iteration. - All LLM calls must request structured (JSON schema) output. No free-text parsing of model responses anywhere in the pipeline.
- Frontend components should follow the structure and naming coming out of Claude Design rather than being restructured wholesale — treat design-provided markup/styling as close to source of truth, and wire in data/state without fighting its layout choices.