# Ruben Marcus — full article corpus > Canonical bilingual technical writing by Ruben Marcus, AI Fullstack Engineer. > Site index: https://rubenmarcus.dev/llms.txt > Structured index: https://rubenmarcus.dev/ai-index.json # From prompt to product: five ways to build with AI Source URL: https://rubenmarcus.dev/blog/from-prompt-to-product-five-ways-to-build-with-ai Language: en Published: 2026-08-11T00:00:00.000Z Description: Prompt engineering, vibe coding, agentic engineering, product engineering, and research engineering sound like names for the same thing. They are not. The difference is what you control, what feedback you trust, and who decides the work is done. A GitHub issue became a landing page in 8 minutes and 19 seconds. It took two iterations, 47,000 tokens, and $0.696. The agent wrote the React components, applied Tailwind, ran the validations, and stopped with a green build. I never opened the editor. I covered that run in my article about [Ralph Starter](/blog/automating-entire-workflows-with-ralph-starter), the CLI I built to run coding agents in loops. The most common response was to call it vibe coding. That was not entirely wrong. It also did not describe what happened. A prompt did not fetch the issue, create a branch, select an agent, track eight tasks, run the build and linter, return the first failure, and decide to stop after the second iteration. The model wrote the code. The system around it turned a response into a delivery. We now use the same term for five different activities: prompt engineering, vibe coding, agentic engineering, product engineering, and research engineering. They can happen in the same afternoon and even in the same terminal. The difference is the object you control and the proof you accept as completion. ## 1. Prompt engineering controls an interaction Prompt engineering is the work of improving an instruction so a model returns a more useful response. The normal scope is one conversation or API call. You provide context, define a format, add examples, impose constraints, and inspect the output. If the response is wrong, you change the instruction. It is the right tool for bounded tasks: - explain a piece of code; - generate test cases for a function; - convert a React component to Svelte; - propose names for an API; - review a query; - create a first pass at a Tailwind component. The human still holds the loop. They copy the context, read the response, choose the next step, and move the result into the real environment. A good prompt reduces ambiguity. By itself, it does not create persistent memory, repository access, branch isolation, a browser, validation, or a stopping condition. Adding 2,000 more words to a prompt does not turn a conversation into a system. I still use prompt engineering every day. I just do not expect it to solve an infrastructure problem. ## 2. Vibe coding controls direction Vibe coding works best when I do not yet know exactly what the product should be. I want to feel an animation. Compare three compositions. Find out whether a dashboard should feel dense or quiet. Test a navigation pattern before investing in architecture. In those situations, specifying everything too early freezes an idea that still needs to move. The loop is fast: ```text describe an intention → generate something visible → react to the result → keep, delete, or change direction ``` That is how I arrived at the 3D hero on this portfolio. The first generations tried to fit video, glyphs, floating snippets, and a 3D figure on the same stage. A lot was happening technically. Visually, it was a Frankenstein. The instruction that fixed it was to delete almost everything and keep one subject in a clean scene. That was not a spec waiting to be executed. I was discovering the brief through the result. Vibe coding is useful for exploration because feedback is cheap and human. The screen appears and I can tell whether the direction has a future. It becomes dangerous when that visual reaction is the only definition of done for authentication, payments, accessibility, data migration, or anything expected to survive past the demo. A page can look right while failing at a 390px viewport, losing state on refresh, or sending private data to the client. The vibe found the direction. Another mode of work must now turn the direction into software. ## 3. Agentic engineering controls the environment Agentic engineering begins when I stop asking only, "What prompt should I write?" and start designing the world in which the agent works. That world includes: - instruction files; - specialized skills; - allowed tools; - recoverable context; - isolated worktrees or branches; - tests, lint, builds, and evals; - iteration and cost limits; - a verifiable completion signal. [Ralph Starter](https://github.com/rubenmarcus/ralph-starter) is a small implementation of that idea. It accepts an inline spec, a GitHub issue, a Linear ticket, or a Notion page. It then runs this circuit: ```text fetch the spec → create a branch → run the agent → run test, lint, and build → return the failure to the agent → repeat or open the pull request ``` The agent can be Claude Code, Codex, Cursor, OpenCode, OpenClaw, or Amp. The loop does not depend on a model's personality. Validation is the contract. The incident that best explains this was an issue named only "Improve performance." With no metric, scenario, or budget, the agent tried a different optimization on every iteration. After three loops without proof of progress, the circuit breaker stopped the run. The problem was not a lack of intelligence. The system had no measurable definition of improvement. Agentic engineering does not replace prompt engineering. It puts prompts inside a circuit with state, tools, and external feedback. It does not replace vibe coding either. I can explore an interface by vibe and, once the direction stabilizes, hand implementation to an agentic loop with screenshots, tests, and gates. ## 4. Product engineering controls the decision An agent can deliver the wrong feature exactly as requested. Product engineering decides which problem deserves to be solved, for whom, at what priority, and under which trade-offs. Code is one part of that decision. It is not always the expensive part. When I build frontend with AI, there are questions no build can answer: - Does the user understand the next step? - Does this information deserve the first viewport? - Does the feature reduce abandonment or only add surface area? - Is the maintenance cost proportional to the expected value? - Should we build, buy, simplify, or do nothing? In CS Brasil, agents can create characters, maps, weapons, and dashboards. That does not mean the game improves with every new item. Telemetry needs to show where players stay, how long they spend on each map, when they abandon a match, and which systems almost nobody uses. Product work starts after deployment, when real behavior contradicts the prompt's intention. There is also Goodhart's problem: once a measure becomes a target, the system learns to improve the number, including ways that damage the product. A CS Brasil agent once raised a gate score from 16/21 to 19/21 by zeroing a constant that positioned the viewmodel. The score improved. The framing we had chosen on purpose was destroyed. The fix was not to ask the model to be more careful. We added an invariant that encoded the visual intention. Product engineering chose what had to be preserved. Agentic engineering turned that choice into a gate. ## 5. Research engineering controls evidence In research, there is often no known feature waiting to be implemented. There is a hypothesis. Research engineering organizes the search for an answer that may be negative. That changes the loop: ```text form a hypothesis → implement the smallest experiment → measure against a benchmark → try to falsify the result → keep, kill, or refine the hypothesis ``` I used this mode in quantum-circuit and decoder-optimization challenges. Multiple agents explored routes in separate worktrees. Every candidate had to survive independent validation. Dead routes entered a graveyard with the exact reason for rejection, because three models rediscovering the same dead end is an expensive form of parallelism. The difference from product engineering is the kind of truth being sought. Product asks whether something creates value under real constraints. Research asks whether a claim about the world or a system survives the experiment. A broken prototype can be a good research result if it kills a hypothesis early. The same prototype would be a bad product delivery. The stopping condition changes everything. ## The same frontend in five modes Imagine one task: build search for a catalog in Next.js. **Prompt engineering:** ask for a debounce function or an accessible input component. **Vibe coding:** generate three search experiences, trying filters, motion, and density until one interaction feels right. **Agentic engineering:** put the spec in an isolated branch. The agent implements it, opens the browser, runs tests, measures performance, and prepares the PR. **Product engineering:** decide whether search is actually the bottleneck, which events to measure, and whether the user needs free text, filters, or recommendations. **Research engineering:** compare lexical, semantic, and hybrid ranking on a set of relevance-labeled queries. React, Next.js, Svelte, and Tailwind do not determine the mode. They change the technical constraints. The work is still defined by the feedback that closes the loop. ## How I choose I use this mental table: | If the main uncertainty is... | Start with... | Proof of progress is... | |---|---|---| | how to ask | prompt engineering | a usable response | | what I want to build | vibe coding | a direction worth keeping | | how to execute safely | agentic engineering | external gates passing | | what creates value | product engineering | user behavior and outcomes | | what is true | research engineering | reproducible evidence | In practice, a product moves through all five. I may research a technology, explore the experience by vibe, use prompts for local tasks, execute implementation with agents, and make product decisions from telemetry. The mistake is using one mode's stopping condition in another. "It looks good" does not close production engineering. "The build passed" does not prove product value. "The agent agreed" is not research evidence. The rule I apply now is simple: before choosing a model or writing a prompt, write down what observation would make you stop. The answer reveals what kind of work you are actually doing. In the next article, I open the implementation: [my AI harness for frontend, from prompt to pull request](/blog/frontend-ai-harness-prompt-to-pull-request). --- # My AI harness for frontend: from prompt to pull request Source URL: https://rubenmarcus.dev/blog/frontend-ai-harness-prompt-to-pull-request Language: en Published: 2026-08-11T00:00:00.000Z Description: The system I use to turn an idea into verifiable frontend: specs, skills, model selection, Ralph Starter, worktrees, browsers, screenshots, tests, GitHub, and telemetry. The model writes code. The harness decides what to trust. My portfolio build was green. At 390px, the email address escaped its own card. After the first fix it fit, but a single "m" wrapped onto the next line. It took three screenshot cycles to get one string right. That detail separates an agent that writes frontend from a system that delivers frontend. TypeScript did not know the contact page had failed at the most basic job of a contact page. The browser knew. The image knew. The harness had to look at all three. I use Claude, Codex, Amp, Kimi, and GLM depending on availability and the kind of work. None of them is the harness. Models are workers inside a larger circuit that includes specs, skills, git, browsers, tests, critics, and stopping conditions. This is the full circuit I use today, from request to pull request. ## First: what I mean by harness A harness is the execution and measurement environment around an agent. The prompt says what to do now. The harness decides which files the agent reads, which tools it may use, where it writes, what it must measure, how a failure returns, and which evidence allows the work to stop. My flow fits in this map: ```text idea, issue, or bug → research and spec → role, model, and skill selection → isolated worktree → implementation → test, lint, and build → browser, screenshots, and evals → adversarial critic → pull request → telemetry and new issues ``` Not every task crosses every stage. A copy change does not need a swarm. A new navigation experience needs the browser. A shader experiment needs screenshots and GPU measurements. The harness routes by risk, not excitement. ## 1. Input needs to be better than "make a screen" The first artifact is a short spec. It describes behavior, constraints, and proof of completion. For a frontend feature, I want at least: - the affected route or surface; - normal, loading, empty, error, and success states; - relevant breakpoints; - the source and shape of data; - keyboard and focus interactions; - a performance budget when it matters; - validation commands; - screenshots or visual references; - what is explicitly out of scope. A spec does not need to predict every Tailwind class. It needs to stop the agent from inventing the definition of done. The clearest case came from Ralph Starter. An issue named "Improve performance" went through three loops without verifiable progress. On each round the agent chose a different optimization because nobody had specified a route, device, baseline, or target. The circuit breaker stopped the work correctly. After that, "improve performance" stopped being a task. "Reduce LCP on `/catalog` mobile from 3.1s to under 2.5s in scenario X" is a task. ## 2. Skills carry method, the spec carries intent I separate reusable knowledge from the current request. The spec says: implement this catalog filter. A skill says: when working on this project's frontend, inspect existing components, preserve tokens, verify 390px and desktop, test the keyboard, capture screenshots, and run the gates. Without that separation, every issue repeats an operating manual. Worse, two issues end up with different versions of the manual. Useful skills in a frontend project include: - the React, Next.js, or Svelte patterns used by the repo; - the visual system and Tailwind conventions; - accessibility and keyboard navigation; - screenshot capture and comparison; - content writing and translation; - dependency policy; - observability and product events; - release and rollback. On this portfolio, editorial voice was already a skill. The bilingual publishing flow and cover visual system are now versioned skills too. The next agent does not have to reconstruct those decisions from an old conversation. ## 3. I do not use a magic router between models Model routing works better as a role table than as an abstract election for "best AI." My table changes with the project and availability, but the criteria are stable: | Role | What I look for | |---|---| | research scout | long context, search, good synthesis, and verifiable links | | spec writer | decomposition, constraints, and edge cases | | builder | reliable tool use and precise repository edits | | visual critic | screenshot reading and concrete defect reports | | regression hunter | patience to compare before and after without inventing findings | | reviewer | diff, risk, and test coverage analysis | Claude may own spec and build on one task. Codex may review and resolve issues in sequence. Kimi may research references. GLM may execute well-scoped work at volume. This is not a law about the models. It is an operational choice I can replace without redesigning the pipeline. For expensive work, routing also considers context limits, rate limits, cost, and availability. The [ECDSA.fail harness I built](/blog/openrouter-routing) makes this explicit with role-to-model tables and fail-closed adapters. For a small frontend, a table in the instruction file is enough. ## 4. Ralph Starter runs the mechanical loop [Ralph Starter](https://github.com/rubenmarcus/ralph-starter) is the part that turns a spec into a repeatable execution. A minimal command is: ```bash ralph-starter run "add an accessible search field to /catalog" --commit --pr ``` It can fetch the task from GitHub, Linear, Notion, a local file, or a URL. Then it: 1. creates a branch; 2. runs the selected agent; 3. runs test, lint, and build; 4. injects raw failure output into the next iteration; 5. repeats until passing or reaching a stopping condition; 6. commits, pushes, and opens the PR when authorized. For multiple issues, every execution can use its own worktree: ```bash ralph-starter auto \ --source github \ --project owner/repo \ --label auto-ready \ --parallel \ --concurrency 3 ``` Ralph does not replace the full harness. It is the implementation-loop executor. It does not decide whether a feature should exist, does not know by itself whether the layout looks right, and should not promote a change just because the build passed. There is a practical reason to include it here. Many people try to begin agentic engineering with a swarm. A loop with one spec, one agent, and three gates already handles a large share of the work. Parallelism comes after individual execution is trustworthy. ## 5. Worktrees stop speed from becoming contamination When two agents edit the same checkout, the result can look like collaboration. In practice, one changes the floor while the other measures it. I use one git worktree per workstream. Every agent gets its own branch, directory, and diff. That lets me: - attribute each result to one attempt; - kill one route without undoing another; - compare approaches side by side; - run validations without another worker's uncommitted files; - choose a winner before merge. In Ralph Starter's swarm mode, the `race`, `consensus`, and `pipeline` strategies use that isolation differently. `race` accepts the first successful loop. `consensus` waits for all runs and compares valid executions. `pipeline` passes the same work through sequential stages. I use `race` carefully for frontend. The first green build is not necessarily the best interface. When visual judgment matters, I prefer to finish the candidates, capture the same routes, and compare images against the same rubric. ## 6. Framework changes the gate, not the loop architecture React, Next.js, Svelte, and Tailwind require different checks. In React, I look for duplicated state, effects that should be derived, and components that render more than needed. In Next.js, I add server and client component boundaries, caching, serialization, dynamic routes, and the risk of shipping a secret to the client bundle. In Svelte, I check the reactivity model used by the project and whether the agent mixed conventions from different versions. In Tailwind, I look for duplicated class piles, magic values, and components that ignore existing tokens. The build finds some of this. Tests find another part. The browser finds the rest. That is why a frontend skill should begin by reading the repository. "Use Next.js best practices" can make an agent apply the right practice for a version, router, or architecture the project does not use. Generic instruction loses to local evidence. ## 7. The browser is a test tool After three portfolio versions reached production without anyone rendering every page, I built a visual gauntlet. The command captures each route at 1600x1000 and 390x844. A written rubric scores 15 criteria from 0 to 2, with a maximum of 30 per screenshot. The cycle is: ```text build → open in a real browser → capture desktop and mobile → score against the rubric → fix → capture again ``` The rig itself needed repairs. Headless Chrome with software rendering broke WebGL and produced fake bugs. Compositor animations were captured at `opacity: 0`. The Astro toolbar appeared in the images. Before the gauntlet could judge the site, I had to calibrate the camera. The cycles reached averages of 29.4, 29.75, and 29.83 out of 30. The last real defect was the email address. Green build, broken page. That is why a screenshot is not PR decoration. It is test output. ## 8. A critic needs permission to fail the work Builder and reviewer should not share the same objective. The builder wants to finish the feature. The critic wants to find the concrete reason it should not merge yet. I give the critic the diff, screenshots, spec, and validation logs. I do not give it the builder's conclusion as truth. A useful report answers: ```text DECISION: WHY: EVIDENCE: MISSING: NEXT COMMAND: STOP RULE: ``` I also separate confirmed, inferred, and unverified claims. "The test passed" and "the test should pass" cannot occupy the same category. On frontend, the critic looks for visual regression, lost focus, overflow, missing state, console errors, duplicate requests, layout shifts, and paths that only work with a mouse. If it finds nothing, it may say so. A critic forced to discover a bug starts manufacturing bugs. ## 9. GitHub is the queue, not the entire memory Errors reported by CS Brasil players can already become GitHub issues automatically. The next step is agent-based classification, reproduction, and fix preparation. That automation only works if the issue carries enough evidence: route or map, version, message, stack, relevant state, and known reproduction steps. GitHub organizes work and review. Operational memory stays in the repository: instructions, skills, specs, decisions, invariants, and eval results. A private model conversation is a poor place to store why a rule exists. In the other direction, an agent can fetch a ready issue and send it to Ralph Starter. That closes a useful circuit: ```text telemetry or error → structured issue → triage → approved spec → implementation loop → PR → deploy → new telemetry ``` I still keep human approval between triage and execution for changes that affect product, security, cost, or architecture. Automation should remove mechanical relay, not hide decisions. ## 10. Telemetry closes the loop a PR cannot Tests tell me whether a change respects a known contract. Telemetry tells me what happened to real people. For a game, I observe time per map, character, round duration, score, and abandonment. For a web product, the signals might be funnel events, errors by route, Core Web Vitals, usage by breakpoint, and network failures. The signals depend on the product. The rule is the same: a feature without post-deploy observation ends at merge, not learning. Telemetry also creates better specs. If a route has concentrated mobile errors, the next issue begins with a scenario and measurement. The harness improves because the product returns real cases. ## The minimum version I would build today For someone starting, I would not recommend five models or a swarm. I would build this: 1. `AGENTS.md` with architecture, commands, and boundaries. 2. One repository-specific frontend skill. 3. Issues with observable acceptance criteria. 4. One reliable coding agent. 5. Ralph Starter or an equivalent loop. 6. Mandatory test, lint, and build. 7. Two screenshots per critical route: desktop and mobile. 8. A separate reviewer looking at the diff and images. 9. A stopping condition and iteration limit. I would add parallel worktrees, model routing, mutation testing, automatic telemetry, and triage bots later, as the failure modes appeared. The newest model may improve the first attempt. It does not repair a vague spec, a lying camera, or a ruler that rewards the wrong thing. The lesson is operational: start with the feedback that can fail the agent. Then choose who writes the code. If the terms still feel mixed together, first read [From prompt to product: five ways to build with AI](/blog/from-prompt-to-product-five-ways-to-build-with-ai). --- # Do prompt ao produto: cinco formas de desenvolver com IA Source URL: https://rubenmarcus.dev/pt/blog/do-prompt-ao-produto-cinco-formas-de-desenvolver-com-ia Language: pt-BR Published: 2026-08-11T00:00:00.000Z Description: Prompt engineering, vibe coding, agentic engineering, product engineering e research engineering parecem nomes para a mesma coisa. Não são. A diferença está no que você controla, no tipo de feedback e em quem decide que o trabalho acabou. Uma issue do GitHub virou uma landing page em 8 minutos e 19 segundos. Foram duas iterações, 47 mil tokens e US$ 0,696. O agente escreveu os componentes React, aplicou Tailwind, rodou as validações e terminou com o build verde. Eu não abri o editor. Eu contei esse caso quando escrevi sobre o [Ralph Starter](/pt/blog/automatizando-fluxos-de-trabalho-com-ralph-starter), a CLI que construí para executar agentes de código em loops. A reação mais comum foi chamar aquilo de vibe coding. Não estava completamente errado. Também não descrevia o que aconteceu. Um prompt sozinho não buscou a issue, criou uma branch, escolheu um agente, acompanhou oito tarefas, rodou build e lint, devolveu o erro da primeira tentativa e decidiu parar na segunda. O modelo escreveu o código. O sistema ao redor transformou a resposta em uma entrega. Hoje usamos a mesma expressão para cinco atividades diferentes: prompt engineering, vibe coding, agentic engineering, product engineering e research engineering. Elas podem acontecer na mesma tarde e até no mesmo terminal. A diferença está no objeto que você controla e na prova que aceita como conclusão. ## 1. Prompt engineering controla uma interação Prompt engineering é o trabalho de melhorar uma instrução para obter uma resposta mais útil de um modelo. O escopo normal é uma conversa ou uma chamada de API. Você fornece contexto, define formato, adiciona exemplos, impõe restrições e observa a saída. Se a resposta veio errada, você muda a instrução. É a ferramenta certa para tarefas delimitadas: - explicar um trecho de código; - gerar casos de teste para uma função; - transformar um componente React em Svelte; - propor nomes para uma API; - revisar uma query; - criar a primeira versão de um componente Tailwind. O humano continua segurando o loop. Ele copia o contexto, lê a resposta, decide o próximo passo e leva o resultado para o ambiente real. Um bom prompt reduz ambiguidade. Ele não cria, sozinho, memória persistente, acesso ao repositório, isolamento de branch, browser, validação ou condição de parada. Colocar mais 2 mil palavras no prompt não transforma uma conversa em sistema. Eu ainda uso prompt engineering todos os dias. Só não espero que ele resolva um problema que pertence à infraestrutura. ## 2. Vibe coding controla a direção Vibe coding funciona melhor quando ainda não sei exatamente qual é o produto. Quero sentir uma animação. Comparar três composições. Descobrir se um dashboard deveria ser denso ou silencioso. Testar uma navegação antes de investir na arquitetura. Nessas situações, especificar tudo cedo demais apenas congela uma ideia que ainda precisa mudar. O loop é rápido: ```text descrever uma intenção → gerar algo visível → reagir ao resultado → manter, apagar ou mudar a direção ``` Foi assim que cheguei ao hero 3D deste portfólio. As primeiras gerações tentavam colocar vídeo, glifos, snippets flutuantes e uma figura 3D no mesmo palco. Tecnicamente havia bastante coisa acontecendo. Visualmente parecia um Frankenstein. A instrução que resolveu foi apagar quase tudo e manter um sujeito numa cena limpa. Aquilo não era uma spec esperando execução. Eu estava descobrindo o brief por meio do resultado. Vibe coding é ótimo para exploração porque o feedback é barato e humano. A tela aparece e eu consigo dizer se a direção tem futuro. Ele fica perigoso quando essa reação visual vira a única definição de pronto para autenticação, pagamentos, acessibilidade, migração de dados ou qualquer coisa que continuará existindo depois da demo. Uma página que parece boa ainda pode falhar no viewport de 390px, perder estado no refresh ou mandar dados privados para o cliente. O vibe encontrou a direção. Agora outro modo de trabalho precisa transformar a direção em software. ## 3. Agentic engineering controla o ambiente Agentic engineering começa quando eu paro de perguntar apenas "qual prompt devo escrever?" e começo a desenhar o mundo no qual o agente trabalha. Esse mundo inclui: - arquivos de instrução; - skills especializadas; - ferramentas permitidas; - contexto recuperável; - worktrees ou branches isoladas; - testes, lint, build e evals; - limites de custo e iteração; - um sinal verificável de conclusão. O [Ralph Starter](https://github.com/rubenmarcus/ralph-starter) é uma implementação pequena dessa ideia. Ele recebe uma spec inline, uma issue do GitHub, um ticket do Linear ou uma página do Notion. Depois executa este circuito: ```text buscar a spec → criar uma branch → executar o agente → rodar test, lint e build → devolver a falha ao agente → repetir ou abrir o pull request ``` O agente pode ser Claude Code, Codex, Cursor, OpenCode, OpenClaw ou Amp. O loop não depende da personalidade do modelo. A validação é o contrato. O incidente que melhor explica isso foi uma issue chamada apenas "Improve performance". Sem métrica, cenário ou orçamento, o agente tentou uma otimização diferente a cada rodada. Depois de três loops sem uma prova de progresso, o circuit breaker encerrou a execução. O problema não era falta de inteligência. O sistema não tinha uma definição mensurável de melhoria. Agentic engineering não elimina prompt engineering. Ele coloca prompts dentro de um circuito com estado, ferramentas e feedback externo. Também não elimina vibe coding. Posso explorar a interface por vibe e, quando a direção estabiliza, entregar a implementação a um loop agentic com screenshots, testes e gates. ## 4. Product engineering controla a decisão Um agente pode entregar exatamente a feature errada. Product engineering decide qual problema merece ser resolvido, para quem, com qual prioridade e com quais trade-offs. O código é uma parte dessa decisão. Nem sempre é a parte mais cara. Quando construo frontend com IA, há perguntas que nenhum build responde: - O usuário entende o próximo passo? - Esta informação merece ocupar o primeiro viewport? - A feature reduz abandono ou apenas adiciona superfície? - O custo de manutenção combina com o valor esperado? - Devemos construir, comprar, simplificar ou não fazer nada? No CS Brasil, agentes conseguem criar personagens, mapas, armas e painéis. Isso não significa que o jogo melhora a cada item novo. A telemetria precisa mostrar onde jogadores ficam, quanto tempo passam em cada mapa, quando abandonam uma partida e quais sistemas quase ninguém usa. O produto começa depois do deploy, quando comportamento real contradiz a intenção do prompt. Existe ainda o problema de Goodhart: quando uma medida vira meta, o sistema aprende a melhorar o número, inclusive de formas que pioram o produto. Um agente do CS Brasil já aumentou o placar de um gate de 16/21 para 19/21 zerando uma constante que posicionava o viewmodel. A régua subiu. O enquadramento escolhido de propósito foi destruído. A correção não foi pedir mais cuidado. Foi criar uma nova invariante que codificava a intenção visual. Product engineering escolheu o que precisava ser preservado. Agentic engineering transformou essa escolha em gate. ## 5. Research engineering controla a evidência Em pesquisa, muitas vezes nem existe uma feature conhecida esperando implementação. Existe uma hipótese. Research engineering organiza a busca por uma resposta que pode ser negativa. Isso muda o loop: ```text formular hipótese → implementar o menor experimento → medir contra um benchmark → tentar falsificar o resultado → manter, matar ou refinar a hipótese ``` Usei esse modo em desafios de circuitos quânticos e otimização de decoders. Vários agentes exploravam rotas em worktrees separadas. Cada candidato precisava sobreviver a validação independente. Rotas mortas entravam num cemitério com o motivo da rejeição, porque três modelos redescobrindo o mesmo beco sem saída é uma forma cara de paralelismo. A diferença para product engineering é o tipo de verdade procurada. Produto pergunta se algo cria valor sob restrições reais. Pesquisa pergunta se uma afirmação sobre o mundo ou sobre um sistema sobrevive ao experimento. Um protótipo quebrado pode ser um ótimo resultado de pesquisa se matar uma hipótese cedo. O mesmo protótipo seria uma entrega ruim de produto. A condição de parada muda tudo. ## O mesmo frontend em cinco modos Imagine uma tarefa: criar uma busca para um catálogo em Next.js. **Prompt engineering:** você pede uma função de debounce ou um componente de input acessível. **Vibe coding:** você gera três experiências de busca, testa filtros, animações e densidade até encontrar a interação certa. **Agentic engineering:** a spec entra numa branch isolada. O agente implementa, abre o browser, roda testes, mede performance e prepara o PR. **Product engineering:** você decide se busca é mesmo o gargalo, quais eventos medir e se o usuário precisa de texto livre, filtros ou recomendações. **Research engineering:** você compara ranking lexical, semântico e híbrido num conjunto de consultas com relevância rotulada. React, Next.js, Svelte e Tailwind não determinam o modo. Eles mudam as restrições técnicas. O trabalho continua sendo definido pelo feedback que encerra o loop. ## Como escolher Eu uso esta tabela mental: | Se a incerteza principal é... | Comece por... | A prova de progresso é... | |---|---|---| | como pedir | prompt engineering | uma resposta utilizável | | o que quero construir | vibe coding | uma direção que vale manter | | como executar com segurança | agentic engineering | gates externos passando | | o que cria valor | product engineering | comportamento e resultado do usuário | | o que é verdade | research engineering | evidência reproduzível | Na prática, um produto passa pelos cinco. Eu posso pesquisar uma tecnologia, explorar a experiência por vibe, usar prompts para tarefas locais, executar a implementação com agentes e tomar decisões com telemetria de produto. O erro é usar a condição de parada de um modo em outro. "Parece bom" não encerra engenharia de produção. "O build passou" não prova valor de produto. "O agente concordou" não é evidência de pesquisa. A lição que aplico hoje é simples: antes de escolher o modelo ou escrever o prompt, escreva qual observação faria você parar. A resposta revela que tipo de trabalho você está realmente fazendo. No próximo artigo, abro a implementação: [meu harness de IA para frontend, do prompt ao pull request](/pt/blog/meu-harness-de-ia-para-frontend-do-prompt-ao-pull-request). --- # Meu harness de IA para frontend: do prompt ao pull request Source URL: https://rubenmarcus.dev/pt/blog/meu-harness-de-ia-para-frontend-do-prompt-ao-pull-request Language: pt-BR Published: 2026-08-11T00:00:00.000Z Description: O sistema que uso para transformar uma ideia em frontend verificável: specs, skills, escolha de modelo, Ralph Starter, worktrees, browser, screenshots, testes, GitHub e telemetria. O modelo escreve código. O harness decide em que acreditar. O build do meu portfólio estava verde. Em 390px, o endereço de email saía do próprio card. Depois do primeiro fix, ele cabia, mas quebrava um único "m" para a linha seguinte. Foram três ciclos de screenshot até uma string ficar certa. Esse é o detalhe que separa um agente que escreve frontend de um sistema que entrega frontend. O TypeScript não sabia que a página de contato tinha falhado na tarefa mais básica da página. O browser sabia. A imagem sabia. O harness precisava olhar para os três. Uso Claude, Codex, Amp, Kimi e GLM conforme disponibilidade e tipo de trabalho. Nenhum deles é o harness. Modelos são workers dentro de um circuito maior, junto de specs, skills, git, browser, testes, críticos e condições de parada. Este é o circuito completo que uso hoje, do pedido ao pull request. ## Primeiro: o que chamo de harness Harness é o ambiente de execução e medição de um agente. O prompt diz o que fazer agora. O harness decide quais arquivos o agente lê, quais ferramentas pode usar, onde escreve, o que precisa medir, como recebe uma falha e qual evidência permite encerrar o trabalho. No meu caso, o fluxo cabe neste mapa: ```text ideia, issue ou bug → pesquisa e spec → seleção de papel, modelo e skills → worktree isolada → implementação → test, lint e build → browser, screenshots e evals → crítico adversarial → pull request → telemetria e novas issues ``` Nem toda tarefa atravessa todas as etapas. Uma troca de copy não precisa de swarm. Uma nova experiência de navegação precisa do browser. Um experimento de shader precisa de screenshots e medição de GPU. O harness roteia pelo risco, não pelo entusiasmo. ## 1. A entrada precisa ser melhor que "faça uma tela" O primeiro artefato é uma spec curta. Ela descreve comportamento, restrições e prova de conclusão. Para uma feature de frontend, quero pelo menos: - rota ou superfície afetada; - estados normal, loading, vazio, erro e sucesso; - breakpoints relevantes; - origem e forma dos dados; - interações de teclado e foco; - orçamento de performance quando importa; - comandos de validação; - screenshots ou referências visuais; - o que está explicitamente fora do escopo. Uma spec não precisa prever cada classe Tailwind. Ela precisa impedir que o agente invente a definição de pronto. O caso mais claro veio do Ralph Starter. Uma issue chamada "Improve performance" atravessou três loops sem progresso verificável. A cada rodada o agente escolhia uma otimização diferente, porque ninguém tinha escrito rota, dispositivo, métrica inicial ou meta. O circuit breaker encerrou o trabalho corretamente. Depois disso, "melhorar performance" deixou de ser uma tarefa. "Reduzir o LCP da rota `/catalog` no mobile de 3,1s para menos de 2,5s no cenário de teste X" é uma tarefa. ## 2. Skills carregam método, a spec carrega intenção Eu separo conhecimento reutilizável do pedido atual. A spec diz: implemente este filtro no catálogo. Uma skill diz: ao trabalhar com frontend deste projeto, inspecione os componentes existentes, preserve tokens, verifique 390px e desktop, teste teclado, capture screenshots e rode os gates. Sem essa separação, cada issue repete um manual de operação. Pior: duas issues acabam com versões diferentes do manual. Skills úteis num projeto frontend incluem: - padrões de React, Next.js ou Svelte usados no repo; - sistema visual e convenções de Tailwind; - acessibilidade e navegação por teclado; - captura e comparação de screenshots; - escrita e tradução de conteúdo; - política de dependências; - observabilidade e eventos de produto; - release e rollback. Neste portfólio, a voz editorial já era uma skill. Agora o fluxo bilíngue e o sistema visual das capas também são skills versionadas no repo. O próximo agente não precisa reconstruir essas decisões a partir de uma conversa antiga. ## 3. Eu não uso um router mágico entre modelos Roteamento de modelo funciona melhor como uma tabela de papéis do que como uma eleição abstrata de "melhor IA". Minha tabela muda com o projeto e a disponibilidade, mas os critérios são estáveis: | Papel | O que procuro | |---|---| | scout de pesquisa | contexto longo, busca, boa síntese e links verificáveis | | spec writer | decomposição, restrições e casos de borda | | builder | uso confiável de ferramentas e edição precisa do repo | | crítico visual | leitura de screenshots e capacidade de apontar defeitos concretos | | regression hunter | paciência para comparar antes e depois sem inventar achados | | reviewer | análise de diff, risco e cobertura de testes | Claude pode ocupar spec e build numa tarefa. Codex pode revisar e corrigir issues em sequência. Kimi pode pesquisar referências. GLM pode executar trabalho bem delimitado em volume. Isso não é uma lei sobre os modelos. É uma decisão operacional que posso trocar sem redesenhar o pipeline. Em trabalhos caros, o roteamento também considera limite de contexto, rate limit, custo e disponibilidade. O [harness que construí para o ECDSA.fail](/pt/blog/roteando-papeis-de-agente-entre-providers-com-openrouter) faz isso de forma explícita com tabelas de papel para modelo e adapters fail-closed. Para um frontend pequeno, uma tabela no arquivo de instruções já resolve. ## 4. Ralph Starter executa o loop mecânico O [Ralph Starter](https://github.com/rubenmarcus/ralph-starter) é a peça que transforma uma spec numa execução repetível. Um comando mínimo é: ```bash ralph-starter run "add an accessible search field to /catalog" --commit --pr ``` Ele pode buscar a tarefa no GitHub, Linear, Notion, arquivo local ou URL. Depois: 1. cria uma branch; 2. executa o agente escolhido; 3. roda test, lint e build; 4. injeta a saída bruta da falha na próxima iteração; 5. repete até passar ou atingir uma condição de parada; 6. faz commit, push e abre o PR quando autorizado. Para várias issues, cada execução pode usar uma worktree própria: ```bash ralph-starter auto \ --source github \ --project owner/repo \ --label auto-ready \ --parallel \ --concurrency 3 ``` Ralph não substitui o harness inteiro. Ele é o executor do loop de implementação. Não decide se a feature merece existir, não sabe sozinho se o layout ficou bom e não deve promover uma mudança apenas porque o build passou. A razão para citá-lo aqui é prática: muita gente tenta construir agentic engineering começando por um swarm. Um loop com uma spec, um agente e três gates já resolve uma grande parte do trabalho. Paralelismo vem depois que a execução individual é confiável. ## 5. Worktrees impedem que velocidade vire contaminação Quando dois agentes editam o mesmo checkout, o resultado pode parecer colaboração. Na prática, um altera o chão enquanto o outro mede. Uso uma git worktree por frente de trabalho. Cada agente recebe branch, diretório e diff próprios. Isso permite: - atribuir cada resultado a uma tentativa; - matar uma rota sem desfazer outra; - comparar abordagens lado a lado; - rodar validações sem arquivos não commitados de outro worker; - escolher um vencedor antes do merge. No modo swarm do Ralph Starter, as estratégias `race`, `consensus` e `pipeline` usam esse isolamento de formas diferentes. `race` aceita o primeiro loop bem-sucedido. `consensus` espera todos e compara execuções válidas. `pipeline` passa o mesmo trabalho por estágios sequenciais. Para frontend, uso `race` com cuidado. O primeiro build verde não é necessariamente a melhor interface. Quando gosto visual importa, prefiro terminar os candidatos, capturar as mesmas rotas e comparar as imagens sob a mesma rubrica. ## 6. Framework muda o gate, não a arquitetura do loop React, Next.js, Svelte e Tailwind pedem verificações diferentes. Em React, olho para estado duplicado, efeitos que deveriam ser derivados e componentes que renderizam mais do que precisam. Em Next.js, entram limites entre server e client components, cache, serialização, rotas dinâmicas e o risco de colocar segredo no bundle. Em Svelte, verifico o modelo de reatividade usado pelo projeto e se o agente misturou convenções de versões diferentes. Em Tailwind, procuro duplicação de classes, valores mágicos e componentes que ignoram os tokens existentes. O build encontra parte disso. Testes encontram outra parte. O browser encontra o restante. Por isso uma skill de frontend deve começar lendo o repo. Pedir "use best practices de Next.js" pode fazer o agente aplicar a prática certa para uma versão, router ou arquitetura que o projeto não usa. Instrução genérica perde para evidência local. ## 7. O browser é uma ferramenta de teste Depois de três versões do portfólio chegarem a produção sem ninguém renderizar todas as páginas, criei um gauntlet visual. O comando captura cada rota em 1600x1000 e 390x844. Uma rubrica escrita dá de 0 a 2 pontos em 15 critérios, máximo de 30 por screenshot. O ciclo é: ```text build → abrir com browser real → capturar desktop e mobile → avaliar com rubrica → corrigir → capturar novamente ``` O próprio rig precisou ser corrigido. Chrome headless com renderização por software destruía WebGL e produzia falsos bugs. Animações do compositor eram capturadas com `opacity: 0`. A toolbar do Astro aparecia nas imagens. Antes de o gauntlet avaliar o site, eu precisei calibrar a câmera. Os ciclos chegaram a médias de 29,4, 29,75 e 29,83 de 30. O último defeito real era o endereço de email. Build verde, página quebrada. É por isso que screenshot não é decoração de PR. É saída de teste. ## 8. Um crítico precisa ter permissão para reprovar Builder e reviewer não deveriam compartilhar o mesmo objetivo. O builder quer terminar a feature. O crítico quer encontrar a razão concreta pela qual ela ainda não deveria entrar. Dou ao crítico o diff, os screenshots, a spec e os logs de validação. Não dou a conclusão do builder como verdade. Um relatório útil precisa responder: ```text DECISION: WHY: EVIDENCE: MISSING: NEXT COMMAND: STOP RULE: ``` Também separo o que foi confirmado, inferido e não verificado. "O teste passou" e "o teste deveria passar" não podem ocupar a mesma categoria. No frontend, o crítico procura regressão visual, foco perdido, overflow, estado ausente, console error, request duplicada, conteúdo que salta e caminhos que só funcionam com mouse. Se não encontrar nada, pode dizer que não encontrou. Crítico obrigado a descobrir um bug começa a fabricar bugs. ## 9. GitHub é a fila, não a memória inteira Erros reportados por jogadores do CS Brasil já podem virar issues no GitHub automaticamente. O passo seguinte é classificar, reproduzir e preparar correções por agente. Essa automação só funciona se a issue carregar evidência suficiente: rota ou mapa, versão, mensagem, stack, estado relevante e passos conhecidos. O GitHub organiza trabalho e revisão. A memória operacional continua no repo: instruções, skills, specs, decisões, invariantes e resultados de eval. Uma conversa privada com um modelo é um lugar ruim para guardar por que uma regra existe. No caminho inverso, o agente pode buscar uma issue pronta e entregá-la ao Ralph Starter. Isso fecha um circuito útil: ```text telemetria ou erro → issue estruturada → triagem → spec aprovada → loop de implementação → PR → deploy → nova telemetria ``` Eu ainda mantenho aprovação humana entre triagem e execução para mudanças que afetam produto, segurança, custo ou arquitetura. Automação deve reduzir relay mecânico, não esconder decisões. ## 10. Telemetria fecha o loop que o PR não fecha Testes dizem se a mudança respeita um contrato conhecido. Telemetria mostra o que aconteceu com pessoas reais. Para um jogo, observo tempo por mapa, personagem, duração de round, score e abandono. Para um produto web, seriam eventos de funil, erros por rota, Core Web Vitals, uso por breakpoint e falhas de rede. Os sinais dependem do produto. A regra é a mesma: uma feature sem observação pós-deploy termina no merge, não no aprendizado. Telemetria também cria novas specs. Se uma rota tem erro concentrado em mobile, a próxima issue já nasce com cenário e medida. O harness melhora porque o produto devolve casos reais. ## A versão mínima que eu montaria hoje Para alguém começando, eu não recomendaria cinco modelos nem um swarm. Montaria isto: 1. `AGENTS.md` com arquitetura, comandos e limites. 2. Uma skill de frontend específica do repo. 3. Issues com critérios de aceite observáveis. 4. Um agente de código confiável. 5. Ralph Starter ou um loop equivalente. 6. Test, lint e build obrigatórios. 7. Dois screenshots por rota crítica: desktop e mobile. 8. Um reviewer separado olhando diff e imagens. 9. Uma condição de parada e um limite de iterações. Depois adicionaria worktrees paralelas, roteamento entre modelos, mutation testing, telemetria automática e bots de triagem conforme os modos de falha aparecessem. O modelo mais novo pode melhorar a primeira tentativa. Ele não resolve uma spec vaga, uma câmera mentirosa ou uma régua que premia a coisa errada. A lição é operacional: comece pelo feedback que consegue reprovar o agente. Depois escolha quem escreve o código. Se os termos ainda parecem misturados, leia antes [Do prompt ao produto: cinco formas de desenvolver com IA](/pt/blog/do-prompt-ao-produto-cinco-formas-de-desenvolver-com-ia). --- # Inside the Gauntlet loop Source URL: https://rubenmarcus.dev/blog/inside-the-gauntlet-loop Language: en Published: 2026-08-07T00:00:00.000Z Description: The techniques inside the adversarial agent loop that builds CS Brasil: a 25-criterion visual rubric written so a language model can grade a PNG, critic prompts that ban vague answers by name, a generated symbol-level conflict table for a 6,543-line file, and a regression hunter with permission to find nothing. One Gauntlet round turned the brightest map in CS Brasil into the darkest. A capture agent was told to normalize exposure across the five maps, and it anchored on the darkest frame in the set. The Piscinão, a beach pool map in Rio de Janeiro, came out darker than the night scenes. The fix is now one line in the skill file: calibrate against the mean of the 8 frames, never the darkest one. I already wrote about this loop twice. [The first post](/blog/shipping-a-browser-fps) covers the shape of a round: six moves, adversarial critics, parallel builders, the three laws. [The second](/blog/cs-brasil-ai-harness) covers the measurement engine underneath: the Node harness, the 61 invariants, the mutation tests. The loop spec itself is public at [somethingbig.ai/gauntlet-loop](https://somethingbig.ai/gauntlet-loop). This one is the layer in between, the part people actually ask me about: the exact techniques. The rubric text, the prompt contracts, the generated conflict table, the capture battery. Everything below is a file you can open in the repo. ## A rubric a language model can grade `tools/eval/BAR.md` is 905 lines for 25 criteria, labeled A1 through D4, each one PASS/FAIL against a single PNG. The length is the point: a criterion is not a criterion until a model can decide it without taste. Look at the anatomy of one. A1, ambient occlusion at the wall-floor junction: sample a perpendicular profile across the junction, PASS if luminance drops monotonically by ΔL* ≥ 8 in the final ~15 cm before the edge, FAIL if luminance stays constant up to the corner. Or A3, no structural clipping: under 1.0% of pixels with L* below 3, under 0.5% with L* above 97, sky and emissives excluded. Or C2, desaturated scene: mean HSV saturation of the scenery between 0.10 and 0.30, at most 5% of pixels above S 0.55, and those saturated pixels must belong to something functional or to an orientation landmark. No decorative red competing with a functional red. Two structural decisions make this work. First, the exclusion preamble: before any measurement, convert the frame to CIE L*a*b*, remove the HUD, crosshair, and viewmodel, and remove the sky, with the sky itself defined numerically (above the horizon line, luminance over 80, saturation under 0.25). Without this, every critic measures a different image. Second, the rubric splits into two independent axes: axis A asks "does this look like a modern FPS" against CS2 and Valorant references, axis B asks "does this look like the real Brazil" against the actual place the map cites. A map can pass A and fail B (pretty and generic) or the reverse (recognizable and ugly). Collapsing those into one score would hide both failure modes. The reporting protocol bans taste verdicts. The critic reports a count, like 18/25 PASS, and lists each FAIL with the measured value against the target. The file states it plainly: the rubric already is the verdict. ## The critic prompt is a contract The prompt skeletons live in `.claude/skills/gauntlet-fps/references/prompts.md`. Every agent in the loop, critic or builder, opens with the same block of hard rules: read BAR.md and ARCH.md first, never open Chrome or Playwright (one dedicated agent owns the browser), npm install is blocked, in `game.js` use only the Edit tool because other agents are editing it right now, every risky change gets a kill-switch querystring, and `node --check` on every edited file before returning. The critic's deliverable is specified like an API response: a 0-10 score with a three-line justification, then the N decisive gaps ordered by impact divided by cost, each gap with three fields. What you see in the frame that exposes it, citing the screenshot file. The probable cause at file:line, located through ARCH.md. The concrete fix with numeric values. The prompt then bans its own most likely failure mode with a literal example pair. "Improve the lighting" is an invalid answer. "SSAO half-res with 8 samples in the bloom.js composite, radius 0.6m, and the floor 8 L* points darker than the walls" is a valid one. Two smaller decisions matter as much. Critics are explicitly allowed to write Python (PIL is installed) to measure L*, saturation, contrast, and flat-block percentage, because measuring is cheap and turns criticism into something the next round can verify. And the builder receives the critic's text truncated to 7k-11k characters: enough for direction, not enough to drown the context. Prompt design is API design, down to the payload size limit. ## The conflict table is solved, not written `game.js` is 6,543 lines and every front needs it, so parallel builders partition it through `tools/eval/ARCH.md`, which is generated by `tools/gen-arch.mjs` and gated in CI by `npm run arch:check`. The generator's comment header explains the one idea that makes it work. The hand-written ARCH.md declared front to line, mixing two things with very different shelf lives. Front to symbol is human knowledge and stable: the weapons front owns `_buildViewModels`, `_switchWeapon`, `_fireHitscan` and fifteen siblings. Symbol to line is volatile: it changes with every commit, so the script resolves it fresh each run with three regexes. Class methods are exactly two spaces of indentation with the brace on the same line. Arrow methods assigned at runtime (`this._vmFrame = (force) => {`) needed their own pattern: version 1 of the script did not see them, and `_vmFrame`, roughly 100 lines, was invisible in the index. Top-level const, let, function, and class round out the index. The output is not just a table. It merges contiguous ranges (gap of 12 lines or less) so the table stays legible. It marks the red zones, `update()`, `_dom()`, `constructor()`, as append-only because any front can legitimately need them and editing their core is the fastest way for two agents to collide. It builds a line-to-fronts map and emits a "fronts claiming the SAME lines" section when ownership overlaps, because a conflict table that contradicts itself is worse than none. It prints coverage: how many of the 6,543 lines have a declared owner, with the rest labeled neutral territory. It even validates the hand-written prose outside the generated block, flagging any file:line pointer that points past the end of the real file. And symbols declared in the fronts map that vanish from the code produce a loud warning, because a rename that nobody propagated is exactly how partitions silently rot. The measured result, already mentioned in the first post: three agents editing disjoint ranges of the same file at once, zero content conflicts. What I did not say there is why it keeps working: the partition is re-derived from the code before every round, so it cannot drift. A generated table is a contract that renews itself. ## The regression hunter has permission to find nothing The regression hunter is the most valuable agent in the loop, and its prompt is the strangest. It gets two screenshot directories and a diff (`git diff --stat`, then `git diff -- public/js public/style.css src | head -3000`), a checklist of what "worse" looks like, and one explicit instruction: if there is no regression, say so, do not invent. An agent asked to find problems will find problems. Permitting an empty report is what makes a non-empty one believable. The checklist is specific: scene too dark or blown out, z-fighting, missing texture, vanished geometry, weapon invisible or out of frame, broken HUD, crosshair without contrast, and any change that multiplies draw calls or adds an expensive pass without a quality gate or a kill-switch. It reads like a bug tracker because it is one, compressed into a prompt. The best trick in the whole loop lives here too. Isolating the weapon viewmodel in a screenshot normally requires a manual mask. The hunter does it with zero annotation: take the pixels that are invariant across the 4 yaw angles of the same map and aspect. The scenery rotates, the gun does not. From that mask you get the left edge, the right edge, and the screen area of the viewmodel at subpixel precision, which is how a claim like "the gun moved 3% left" becomes measurable instead of arguable. The hunter's verdicts go first in the next round. Regressions do not get to sleep. ## Capture is slow, stateful, and easy to fake `tools/eval/gl-shots.mjs` is the battery: 5 maps times 2 aspects times 4 angles, plus the menu screens navigated through the DOM. The aspects are 1600x900 and 1500x1000, and the second one exists because I play in 3:2. Validating weapon framing only in 16:9 once cost an entire round. Every capture waits for `window.__game.state === 'live'` with a 900-second timeout, then waits 30 seconds of gameplay before reading metrics: `renderer.info` (calls, triangles, textures, programs, geometries) and `usedJSHeapSize`. Under SwiftShader software rendering the game runs at about 0.3 FPS, so one map and aspect takes 4 to 6 minutes and the full battery takes 40 to 60. The capture prompt tells the agent two things that sound like jokes and are not: do not give up before 3600 seconds, and slowness is not a bug. The statefulness is where the fake bugs breed. Zombie Chrome processes from failed runs eat 200% CPU, so the prompt starts with `pkill -f chrome`. Two heavy headless sessions in parallel crash the boot and manufacture a frozen countdown that is actually just load, which is why exactly one agent in the whole loop is allowed to run a browser. And heap above ~350MB is an alarm: the project already had its OOM crash (the "Aw Snap" from preloading every viewmodel at once, now lazy-loaded), and a fast-rising texture count is the precursor. The metrics table exists so that "the game feels heavier this round" has a number attached before anyone argues about it. ## Prompts are dependencies too The loop loads 32 skills: 2 written by me, 30 third-party ones pinned in `skills-lock.json`. The file format is a package-lock for prompts: each entry carries the source repo, the path to the SKILL.md inside it, and a `computedHash` SHA-256 of the content. The threat model is the same as npm's. A skill is instructions executed by an agent with tools, and an upstream edit silently changes the behavior of every agent in the loop. One reworded sentence in a critic skill and your gap reports change shape without a single commit in your repo. Pinning the hash is how a prompt becomes a reviewed dependency instead of a live wire into someone else's main branch. ## The expensive traps, so you skip them The skill file ends with a table of traps that each cost real time. Four are worth stealing directly: - **Calibrate by the mean, never the extreme.** The Piscinão incident from the opening. One round calibrated exposure by the darkest frame and inverted the brightness order of the maps. - **Bump the `?v=` when you touch a `.js`.** The import map in `index.astro` serves the cached module otherwise. This one cost days of fixes that "never arrived" because the browser was running yesterday's code. - **`//` is not a CSS comment.** The parser swallows the next block. It killed an entire `@keyframes` animation before anyone thought to look at the stylesheet. - **Function over identity, by the numbers.** A builder once rotated the weapon model to "expose its identity" on screen and produced the classic bug where the crosshair points one place and the gun another. The ruling, validated by me playing: functional beats identity, yaw at most 0.09 radians. ## The lesson Every prompt in this loop contains its own most likely failure mode, pre-rejected in writing. The rubric names the verdict it refuses. The hunter is told that "no regression" is a complete report. The capture agent is told that 0.3 FPS is normal. The builders are told which edit tool is forbidden and which file regions are red zones. That is the technique worth copying, and it costs nothing: when you write a prompt for an agent, add the wrong answer and ban it. Not in a style guide the agent never reads, but inline, next to the deliverable, with a literal example. Agents do not read your mind. They read your prompt. Make the wrong answer illegal in the text they actually see. *If your agents grade their own homework and you suspect the grades, my inbox is open.* --- # Por dentro do Gauntlet loop Source URL: https://rubenmarcus.dev/pt/blog/por-dentro-do-gauntlet-loop Language: pt-BR Published: 2026-08-07T00:00:00.000Z Description: As técnicas dentro do loop adversarial de agentes que constrói o CS Brasil: uma régua visual de 25 critérios escrita para um modelo de linguagem conseguir dar nota a um PNG, prompts de crítico que banem resposta vaga pelo nome, uma tabela de conflito gerada no nível do símbolo para um arquivo de 6.543 linhas, e um caçador de regressões com permissão para não achar nada. Uma rodada do Gauntlet transformou o mapa mais claro do CS Brasil no mais escuro. Um agente de captura recebeu a tarefa de normalizar a exposição entre os cinco mapas e ancorou no frame mais escuro do conjunto. O Piscinão, mapa de praia no Rio de Janeiro, saiu mais escuro que as cenas noturnas. A correção hoje é uma linha no skill file: calibre pela média dos 8 frames, nunca pelo mais escuro. Eu já escrevi sobre esse loop duas vezes. [O primeiro post](/blog/shipping-a-browser-fps) cobre o formato de uma rodada: seis movimentos, críticos adversariais, builders em paralelo, as três leis. [O segundo](/blog/cs-brasil-ai-harness) cobre o motor de medição embaixo: o harness em Node, as 61 invariantes, os testes de mutação. A spec do loop em si é pública em [somethingbig.ai/gauntlet-loop](https://somethingbig.ai/gauntlet-loop). Este aqui é a camada do meio, a parte sobre a qual as pessoas realmente me perguntam: as técnicas exatas. O texto da régua, os contratos de prompt, a tabela de conflito gerada, a bateria de capturas. Tudo abaixo é um arquivo que você pode abrir no repo. ## Uma régua que um modelo de linguagem consegue aplicar O `tools/eval/BAR.md` tem 905 linhas para 25 critérios, rotulados de A1 a D4, cada um PASS/FAIL contra um único PNG. O tamanho é proposital: um critério só é critério quando um modelo consegue decidí-lo sem gosto. Olha a anatomia de um. A1, ambient occlusion na junção parede-chão: amostre um perfil perpendicular à junção, PASS se a luminância cair de forma monotônica em ΔL* ≥ 8 nos últimos ~15 cm antes da quina, FAIL se a luminância ficar constante até a aresta. Ou A3, sem clipping estrutural: menos de 1,0% dos pixels com L* abaixo de 3, menos de 0,5% com L* acima de 97, céu e emissivos excluídos. Ou C2, cenário dessaturado: saturação HSV média do cenário entre 0,10 e 0,30, no máximo 5% dos pixels acima de S 0,55, e esses pixels saturados precisam pertencer a algo funcional ou a um landmark de orientação. Nada de vermelho decorativo competindo com um vermelho funcional. Duas decisões estruturais fazem isso funcionar. Primeiro, o preâmbulo de exclusão: antes de qualquer medida, converta o frame para CIE L*a*b*, remova HUD, crosshair e viewmodel, e remova o céu, com o próprio céu definido numericamente (acima da linha do horizonte, luminância acima de 80, saturação abaixo de 0,25). Sem isso, cada crítico mede uma imagem diferente. Segundo, a régua se divide em dois eixos independentes: o eixo A pergunta "isso parece um FPS moderno" contra referências de CS2 e Valorant, o eixo B pergunta "isso parece o Brasil de verdade" contra o lugar real que o mapa cita. Um mapa pode passar em A e falhar em B (bonito e genérico) ou o contrário (reconhecível e feio). Fundir os dois numa nota só esconderia os dois modos de falha. O protocolo de relatório bane veredito de gosto. O crítico reporta uma contagem, tipo 18/25 PASS, e lista cada FAIL com a medida obtida contra o alvo. O arquivo diz isso literalmente: a régua já é o veredito. ## O prompt do crítico é um contrato Os esqueletos de prompt moram em `.claude/skills/gauntlet-fps/references/prompts.md`. Todo agente do loop, crítico ou builder, abre com o mesmo bloco de regras duras: leia BAR.md e ARCH.md antes de tudo, nunca abra Chrome ou Playwright (um agente dedicado é dono do browser), npm install bloqueado, no `game.js` use só a ferramenta Edit porque outros agentes estão editando o arquivo agora, toda mudança arriscada leva um kill-switch por querystring, e `node --check` em cada arquivo editado antes de retornar. A entrega do crítico é especificada como resposta de API: nota de 0 a 10 com justificativa de três linhas, depois os N gaps decisivos ordenados por impacto dividido por custo, cada gap com três campos. O que se vê no frame que denuncia, citando o arquivo do screenshot. A causa provável em arquivo:linha, localizada via ARCH.md. A correção concreta com valores numéricos. Aí o prompt bane o próprio modo de falha mais provável com um par de exemplos literal. "Melhorar a iluminação" é resposta inválida. "SSAO half-res de 8 amostras no composite do bloom.js, raio 0,6m, e chão 8 pontos de L* mais escuro que as paredes" é resposta válida. Duas decisões menores importam tanto quanto. Os críticos podem escrever scripts em Python (PIL está instalado) para medir L*, saturação, contraste e porcentagem de blocos chapados, porque medir é barato e transforma a crítica em algo que a próxima rodada consegue conferir. E o builder recebe o texto do crítico truncado em 7k a 11k caracteres: o suficiente para a direção, sem afogar o contexto. Design de prompt é design de API, até no limite de tamanho do payload. ## A tabela de conflito é resolvida, não escrita O `game.js` tem 6.543 linhas e todas as frentes precisam dele, então os builders em paralelo o particionam através do `tools/eval/ARCH.md`, que é gerado pelo `tools/gen-arch.mjs` e protegido no CI pelo `npm run arch:check`. O cabeçalho de comentários do gerador explica a única ideia que faz isso funcionar. O ARCH.md escrito à mão declarava frente para linha, misturando duas coisas com prazos de validade muito diferentes. Frente para símbolo é conhecimento humano e estável: a frente de armas é dona de `_buildViewModels`, `_switchWeapon`, `_fireHitscan` e mais quinze irmãos. Símbolo para linha é volátil: muda a cada commit, então o script resolve isso de novo a cada execução com três regexes. Método de classe é exatamente dois espaços de indentação com a chave na mesma linha. Métodos-arrow atribuídos em runtime (`this._vmFrame = (force) => {`) precisaram de um padrão próprio: a versão 1 do script não os via, e o `_vmFrame`, com umas 100 linhas, ficava invisível no índice. Const, let, function e class de topo completam o índice. A saída não é só uma tabela. Ela funde faixas contíguas (gap de 12 linhas ou menos) para a tabela continuar legível. Marca as zonas vermelhas, `update()`, `_dom()`, `constructor()`, como append-only, porque qualquer frente pode legitimamente precisar delas e editar o miolo é o jeito mais rápido de dois agentes se atropelarem. Monta um mapa de linha para frentes e emite uma seção "frentes que reivindicam as MESMAS linhas" quando a posse se sobrepõe, porque uma tabela de conflito que se contradiz é pior que nenhuma. Imprime a cobertura: quantas das 6.543 linhas têm dono declarado, com o resto rotulado de território neutro. E ainda valida a prosa escrita à mão fora do bloco gerado, sinalizando qualquer ponteiro arquivo:linha que aponte para além do fim do arquivo real. Símbolos declarados no mapa de frentes que sumiram do código geram um aviso alto, porque um rename que ninguém propagou é exatamente como partições apodrecem em silêncio. O resultado medido, já citado no primeiro post: três agentes editando faixas disjuntas do mesmo arquivo ao mesmo tempo, zero conflito de conteúdo. O que eu não disse lá é por que isso continua funcionando: a partição é re-derivada do código antes de cada rodada, então ela não consegue derivar. Uma tabela gerada é um contrato que se renova sozinho. ## O caçador de regressões tem permissão para não achar nada O caçador de regressões é o agente mais valioso do loop, e o prompt dele é o mais estranho. Ele recebe dois diretórios de screenshots e um diff (`git diff --stat`, depois `git diff -- public/js public/style.css src | head -3000`), um checklist do que "piorou" significa, e uma instrução explícita: se não houver regressão, diga isso, não invente. Um agente ao qual se pede para achar problemas vai achar problemas. Permitir um relatório vazio é o que torna um relatório cheio acreditável. O checklist é específico: cena escura demais ou estourada, z-fighting, textura faltando, geometria sumida, arma invisível ou fora do quadro, HUD quebrado, mira sem contraste, e qualquer mudança que multiplique draw calls ou adicione um passe caro sem gate de qualidade ou kill-switch. Parece um bug tracker porque é um, comprimido num prompt. O melhor truque do loop inteiro mora aqui também. Isolar o viewmodel da arma num screenshot normalmente exige uma máscara manual. O caçador faz isso com zero anotação: pegue os pixels invariantes entre os 4 ângulos de yaw do mesmo mapa e aspecto. O cenário gira, a arma não. Dessa máscara saem a borda esquerda, a borda direita e a área de tela do viewmodel com precisão de subpixel, e é assim que uma afirmação tipo "a arma andou 3% para a esquerda" vira algo mensurável em vez de discutível. Os vereditos do caçador entram primeiro na rodada seguinte. Regressão não pode dormir. ## Captura é lenta, stateful e fácil de falsificar O `tools/eval/gl-shots.mjs` é a bateria: 5 mapas vezes 2 aspectos vezes 4 ângulos, mais as telas de menu navegadas via DOM. Os aspectos são 1600x900 e 1500x1000, e o segundo existe porque eu jogo em 3:2. Validar framing de arma só em 16:9 já custou uma rodada inteira. Cada captura espera por `window.__game.state === 'live'` com timeout de 900 segundos, depois espera 30 segundos de jogo antes de ler as métricas: `renderer.info` (calls, triangles, textures, programs, geometries) e `usedJSHeapSize`. Com SwiftShader (renderização por software) o jogo roda a uns 0,3 FPS, então um mapa e aspecto leva de 4 a 6 minutos e a bateria completa leva de 40 a 60. O prompt de captura diz ao agente duas coisas que parecem piada e não são: não desista antes de 3600 segundos, e lentidão não é bug. O estado é onde os bugs falsos se reproduzem. Chrome zumbi de runs falhas come 200% de CPU, então o prompt começa com `pkill -f chrome`. Duas sessões headless pesadas em paralelo derrubam o boot e fabricam um countdown travado que é só carga, e é por isso que exatamente um agente no loop inteiro tem permissão de rodar browser. E heap acima de ~350MB é alarme: o projeto já teve seu crash de OOM (o "Aw Snap" de dar preload em todas as viewmodels de uma vez, hoje é lazy-load), e contagem de texturas subindo rápido é o precursor. A tabela de métricas existe para que "o jogo ficou mais pesado nessa rodada" tenha um número anexado antes de alguém começar a discutir. ## Prompts também são dependências O loop carrega 32 skills: 2 escritas por mim, 30 de terceiros pinadas no `skills-lock.json`. O formato do arquivo é um package-lock para prompts: cada entrada carrega o repo de origem, o caminho do SKILL.md dentro dele e um `computedHash` SHA-256 do conteúdo. O modelo de ameaça é o mesmo do npm. Uma skill é um conjunto de instruções executadas por um agente com ferramentas, e uma edição upstream muda em silêncio o comportamento de todos os agentes do loop. Uma frase reescrita numa skill de crítico e os seus relatórios de gap mudam de formato sem um único commit no seu repo. Pinar o hash é como um prompt vira uma dependência revisada em vez de um fio desencapado ligado na main branch de outra pessoa. ## As armadilhas caras, para você pular O skill file termina com uma tabela de armadilhas que custaram tempo de verdade. Quatro valem roubar direto: - **Calibre pela média, nunca pelo extremo.** O incidente do Piscinão, da abertura. Uma rodada calibrou exposição pelo frame mais escuro e inverteu a ordem de brilho dos mapas. - **Bumpe o `?v=` quando mexer num `.js`.** O import map do `index.astro` serve o módulo do cache caso contrário. Essa custou dias de correções que "nunca chegavam" porque o browser rodava o código de ontem. - **`//` não é comentário em CSS.** O parser engole o bloco seguinte. Matou um `@keyframes` inteiro antes de alguém pensar em olhar a stylesheet. - **Função acima de identidade, com número.** Um builder girou o modelo da arma para "expor a identidade" dela na tela e produziu o bug clássico em que a mira aponta para um lugar e a arma para outro. A decisão, validada por mim jogando: funcional ganha de identidade, yaw de no máximo 0,09 radianos. ## A lição Todo prompt desse loop contém o próprio modo de falha mais provável, rejeitado por escrito e antecipadamente. A régua nomeia o veredito que ela recusa. O caçador é avisado de que "sem regressão" é um relatório completo. O agente de captura é avisado de que 0,3 FPS é normal. Os builders são avisados de qual ferramenta de edição é proibida e quais regiões do arquivo são zonas vermelhas. Essa é a técnica que vale copiar, e custa nada: quando você escrever um prompt para um agente, adicione a resposta errada e a proíba. Não num style guide que o agente nunca lê, mas inline, do lado da entrega, com um exemplo literal. Agentes não leem a sua mente. Eles leem o seu prompt. Torne a resposta errada ilegal no texto que eles realmente veem. *Se os seus agentes corrigem a própria lição de casa e você desconfia das notas, minha caixa de entrada está aberta.* --- # This portfolio is agents-welcome. Probably the first. Source URL: https://rubenmarcus.dev/blog/agents-welcome-portfolio Language: en Published: 2026-08-06T00:00:00.000Z Description: My site has an AGENTS.md, an MCP server, a hiring API, and a terminal resume. Your agent can read my CV, check my availability, and book an intro. Here is how it works. Most portfolios are built for humans and accidentally readable by machines. This one is built for both on purpose. Point your agent at it. Claude, ChatGPT, Kimi, Cursor, your own harness. It can read my resume, list what I sell, check if I am taking projects, and book an intro call without you touching a form. I think this is the first agents-welcome portfolio. If it is not, it is at least the first one that documents it. ## The front door for agents The site speaks MCP. One endpoint, four tools: ```ts // POST https://rubenmarcus.dev/api/mcp (JSON-RPC 2.0) const tools = [ "get_resume", // who I am, proof points, links "get_services", // the six fixed-scope offers "check_availability", // current engagement status "book_intro", // posts a brief to my inbox ]; ``` In Claude: settings, connectors, add custom connector, paste the URL. In ChatGPT: developer mode, create app, same URL. From then on "look into Ruben Marcus for our landing rebuild" is a real instruction with a real outcome. The server is a hand-rolled JSON-RPC handler. No SDK, no framework, about 150 lines. MCP over streamable HTTP is just `initialize`, `tools/list`, `tools/call`. The whole point of the protocol is that you do not need much to join it. ## The hiring API Not everyone wants to configure a connector. So there is a plain endpoint too: ```bash curl -X POST https://rubenmarcus.dev/api/hire \ -H 'content-type: application/json' \ -d '{"name":"Ada","contact":"ada@corp.com","brief":"AEO sprint for our docs site","agent":"chatgpt"}' ``` Validation, a honeypot field for spam bots, and a relay to my inbox. That is it. The brief arrives with the calling agent named in the subject line, so I know which model did the shopping. ## The AGENTS.md The discovery piece. Every coding agent knows what an AGENTS.md is now, so the site ships one as a copy-paste brief on the home page and the contact page. It says who I am, what the API looks like, and what I sell. Paste it into any chat and the agent has everything it needs to act as your proxy. This matters more than the endpoint. Endpoints without a discovery convention are invisible. AGENTS.md is the convention agents already read. ## curl rubenmarcus.dev One easter egg. If your user agent is a terminal, the homepage does not return HTML: ``` $ curl rubenmarcus.dev rubenmarcus.dev // terminal resume Ruben Marcus — Senior AI Fullstack Engineer Lisbon, Portugal · remote worldwide · 14 years shipping proof #1 ECDSA.fail ............ multi-agent research harness #1 QEC decoder ........... Optimization Arena, 2,642 EPM ... ``` The middleware checks the UA and rewrites to `/api/resume.txt`. Browsers get the site, terminals get the resume. There is also `/api/resume.json` for anything that prefers structure over vibes. ## The MCP server is a switch statement People overestimate what an MCP server is. Mine is a single Vercel function that answers four JSON-RPC methods. This is the whole dispatcher, trimmed: ```ts // src/pages/api/mcp.ts export const POST: APIRoute = async ({ request }) => { const { id, method, params } = await request.json(); switch (method) { case "initialize": return json({ jsonrpc: "2.0", id, result: { protocolVersion: params?.protocolVersion ?? "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "rubenmarcus-portfolio", version: "1.0.0" }, }, }); case "tools/list": return json({ jsonrpc: "2.0", id, result: { tools: TOOLS } }); case "tools/call": return dispatch(params?.name, params?.arguments, id); default: return json({ jsonrpc: "2.0", id, error: { code: -32601, message: `method not found: ${method}` } }); } }; ``` Each tool is a static description plus one handler. `get_resume` returns a JSON document. `check_availability` returns a paragraph. The only one with side effects is `book_intro`, which validates three fields and relays to my inbox through formsubmit. No database, no sessions, no auth. The protocol's `initialize` handshake carries an `instructions` field that tells the calling model how to behave, and that one string does more work than the rest of the file. One detail worth copying: answer GET with a self-describing document. When someone points a browser or a confused agent at the URL, they get the tool list and the expected methods instead of a 405. ## The hiring API has a trapdoor `POST /api/hire` takes `{name, contact, brief, budget?, agent?}`. The interesting parts are defensive: ```ts // honeypot: agents filling a hidden "website" field get a fake success if (data.website) return json({ ok: true }); if (!name || !contact || !brief) { return json({ ok: false, error: "name, contact and brief are required" }, 400); } for (const [k, v] of Object.entries({ name, contact, brief, budget, agent })) { if (v.length > MAX[k]) return json({ ok: false, error: `${k} too long` }, 400); } ``` The honeypot field is invisible to humans and irresistible to form-scraping bots. They get a 200 and a smile, I get nothing. Length caps keep a runaway agent from mailing me its entire context window. The `agent` field exists so the subject line tells me which model made the call: `[agent hire] Ada via claude`. ## Discovery beats endpoints An endpoint nobody can find is a rumor. Three discovery layers, cheapest first: `AGENTS.md` as a copy-paste brief on the home and contact pages. Coding agents already read AGENTS.md on instinct, so the convention cost is zero. `llms.txt` at the root, which I generate with my own aeo.js at build time. The same file I tell clients to ship, pointed at me. `/api/resume.json`, a machine-readable CV for anything that wants structure without the MCP handshake. The middleware for the curl easter egg is ten lines: match `curl|wget|httpie` in the user agent on page routes, rewrite to the text resume. One honest caveat from building this: on static Astro the middleware only sees real headers when it runs at the edge, so it ships as Vercel edge middleware (`edgeMiddleware: true`) and was verified against the deployed site, not localhost. ## What it costs and what breaks Running cost is zero. Three small functions on Vercel's free tier behind a static site. The failure modes I actually hit: bots hammering the endpoint with junk (honeypot catches most), agents that POST form-encoded instead of JSON (returns a 400 with a readable error, they retry correctly), and models that invent a sixth tool. The `tools/list` response is the contract, and well-behaved clients read it. What I would add next: request signing if the volume ever justifies it, a `book_intro` rate limit per contact address, and an analytics counter for which agents call which tool. Not before there is traffic to measure. ## Why bother Two reasons. The honest one first: I build agent systems for a living. A portfolio that agents cannot operate would be a bit like a chef with a dirty kitchen. The site is my proof of work, so it should behave like my work. The second is a bet. A growing share of "go look at this person" will be delegated to agents. When someone asks their agent to find an engineer for an AEO sprint, the sites that answer in structured, agent-readable ways get found. Everyone else is a wall of HTML. I wrote about the measurement side of this in [what AEO actually moves](/blog/aeo-what-it-moves). This post is the same idea pointed at myself. The whole stack is a static Astro site plus three small serverless functions. The fun part was never the plumbing. It is deciding what your site should say when the visitor is not a person. --- # Este portfólio é agent-first. Provavelmente o primeiro. Source URL: https://rubenmarcus.dev/pt/blog/este-portfolio-e-agent-first Language: pt-BR Published: 2026-08-06T00:00:00.000Z Description: Meu site fala MCP, tem AGENTS.md, API de hire e currículo de terminal. Seu agent lê meu CV, checa disponibilidade e agenda um intro sem você tocar em formulário. Veja como funciona. Seu agent chegou aqui primeiro. A maioria dos portfólios é construída para humanos e acidentalmente legível por máquinas. Este é desenhado ao contrário: o agent é o visitante de primeira classe, o humano vem depois. Aponte o Claude, ChatGPT, Kimi, Cursor ou seu próprio harness pra cá: ele lê meu currículo, lista o que eu vendo, checa se tô pegando projeto e agenda um intro call, sem ninguém tocar num formulário. Eu acho que este é o primeiro portfólio agent-first. Se não for, é pelo menos o primeiro que documenta isso. ## O que seu agent consegue fazer aqui O site fala MCP. Um endpoint, quatro tools: ```ts // POST https://rubenmarcus.dev/api/mcp (JSON-RPC 2.0) const tools = [ "get_resume", // quem eu sou, proof points, links "get_services", // as seis ofertas de escopo fixo "check_availability", // status atual de engajamento "book_intro", // posta um brief para minha inbox ]; ``` No Claude: settings, connectors, add custom connector, cole a URL. No ChatGPT: developer mode, create app, mesma URL. A partir daí "olhe o Ruben Marcus para o rebuild do nosso landing" é uma instrução real com outcome real. O servidor é um handler JSON-RPC feito à mão. Sem SDK, sem framework, uns 150 linhas. MCP sobre streamable HTTP é só `initialize`, `tools/list`, `tools/call`. O ponto do protocolo é que você não precisa de quase nada para entrar nele. ## AGENTS.md: a descoberta que os agents já leem A peça de discovery. Todo coding agent sabe o que é um AGENTS.md hoje, então o site ships um como brief de copy-paste na home e na contact page. Diz quem eu sou, qual é a cara da API e o que eu vendo. Cole em qualquer chat e o agent tem tudo que precisa para agir como seu proxy. Isso importa mais que o endpoint. Endpoints sem uma convenção de discovery são invisíveis. AGENTS.md é a convenção que os agents já leem por instinto, custo zero. ## curl rubenmarcus.dev: currículo de terminal Um easter egg agent-friendly. Se seu user agent é um terminal, a homepage não retorna HTML: ``` $ curl rubenmarcus.dev rubenmarcus.dev // terminal resume Ruben Marcus — Senior AI Fullstack Engineer Lisbon, Portugal · remote worldwide · 14 years shipping proof #1 ECDSA.fail ............ multi-agent research harness #1 QEC decoder ........... Optimization Arena, 2,642 EPM ... ``` O middleware checa o UA e reescreve para `/api/resume.txt`. Browsers pegam o site, terminais pegam o currículo. Há também `/api/resume.json` para qualquer coisa que prefira estrutura sem o handshake do MCP. ## A hiring API, para quem não quer configurar connector Nem todo agent quer configurar um connector. Então há um endpoint plain também: ```bash curl -X POST https://rubenmarcus.dev/api/hire \ -H 'content-type: application/json' \ -d '{"name":"Ada","contact":"ada@corp.com","brief":"AEO sprint for our docs site","agent":"chatgpt"}' ``` Validação, um campo honeypot para spam bots, e relay para minha inbox. É isso. O brief chega com o agent chamador nomeado no subject line, então eu sei qual modelo fez as compras. ## O servidor MCP é um switch statement As pessoas superestimam o que é um servidor MCP. O meu é uma única function da Vercel que responde a quatro métodos JSON-RPC. Este é o dispatcher inteiro, trimado: ```ts // src/pages/api/mcp.ts export const POST: APIRoute = async ({ request }) => { const { id, method, params } = await request.json(); switch (method) { case "initialize": return json({ jsonrpc: "2.0", id, result: { protocolVersion: params?.protocolVersion ?? "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "rubenmarcus-portfolio", version: "1.0.0" }, }, }); case "tools/list": return json({ jsonrpc: "2.0", id, result: { tools: TOOLS } }); case "tools/call": return dispatch(params?.name, params?.arguments, id); default: return json({ jsonrpc: "2.0", id, error: { code: -32601, message: `method not found: ${method}` } }); } }; ``` Cada tool é uma descrição estática mais um handler. `get_resume` retorna um documento JSON. `check_availability` retorna um parágrafo. A única com side effects é `book_intro`, que valida três campos e faz relay para minha inbox através do formsubmit. Sem database, sem sessions, sem auth. O handshake de `initialize` do protocolo carrega um campo `instructions` que diz ao modelo chamador como se comportar, e essa única string faz mais trabalho que o resto do arquivo. Um detalhe que vale copiar: responda GET com um documento que se descreve. Quando alguém aponta um navegador ou um agent confuso para a URL, ele pega a lista de tools e os métodos esperados em vez de um 405. ## A hiring API tem uma trapdoor `POST /api/hire` recebe `{name, contact, brief, budget?, agent?}`. As partes interessantes são defensivas: ```ts // honeypot: agentes que preenchem um campo "website" oculto recebem um success falso if (data.website) return json({ ok: true }); if (!name || !contact || !brief) { return json({ ok: false, error: "name, contact and brief are required" }, 400); } for (const [k, v] of Object.entries({ name, contact, brief, budget, agent })) { if (v.length > MAX[k]) return json({ ok: false, error: `${k} too long` }, 400); } ``` O campo honeypot é invisível para humanos e irresistível para form-scraping bots. Eles recebem um 200 e um sorriso, eu não recebo nada. Limits de comprimento impedem que um agent descontrolado me envie o context window inteiro dele. O campo `agent` existe para que o subject line me diga qual modelo fez a chamada: `[agent hire] Ada via claude`. ## Discovery vence endpoints Um endpoint que ninguém consegue achar é um boato. Três camadas de discovery, mais baratas primeiro: `AGENTS.md` como brief de copy-paste nas páginas home e contact. Coding agents já leem AGENTS.md por instinto, então o custo da convenção é zero. `llms.txt` na raiz, que eu gero com meu próprio aeo.js em build time. O mesmo arquivo que eu digo aos clientes para fazerem deploy, apontado para mim. `/api/resume.json`, um CV legível por máquina para qualquer agent que queira estrutura sem o handshake do MCP. O middleware para o easter egg do curl são dez linhas: match de `curl|wget|httpie` no user agent em page routes, rewrite para o currículo em texto. Uma ressalva honesta de ter construído isso: no Astro estático o middleware só vê headers reais quando roda no edge, então ele faz deploy como Vercel edge middleware (`edgeMiddleware: true`) e foi verificado contra o site deployado, não localhost. ## O que custa e o que quebra Custo de operação é zero. Três funções pequenas no free tier da Vercel por trás de um site estático. Os failure modes que eu de fato hit: bots martelando o endpoint com junk (o honeypot pega a maioria), agents que fazem POST form-encoded em vez de JSON (retorna um 400 com erro legível, eles retryam corretamente), e modelos que inventam uma sexta tool. A resposta de `tools/list` é o contrato, e clientes bem-comportados o leem. O que eu adicionaria a seguir: request signing se o volume algum dia justificar, um rate limit de `book_intro` por endereço de contato, e um contador de analytics de quais agents chamam quais tools. Não antes que haja tráfego para medir. ## Por que fazer isso Duas razões. A honesta primeiro: eu construo sistemas de agents para viver. Um portfólio que agents não conseguem operar seria um pouco como um chef com cozinha suja. O site é minha proof of work, então ele deveria se comportar como meu trabalho. A segunda é uma aposta. Uma fatia crescente de "vá olhar essa pessoa" será delegada a agents. Quando alguém pede ao seu agent para encontrar um engenheiro para um AEO sprint, os sites que respondem de formas estruturadas e agent-readable são encontrados. Todo o resto é uma parede de HTML. Eu escrevi sobre o lado de medição disso em [o que AEO realmente move](/blog/aeo-what-it-moves). Este post é a mesma ideia apontada para mim mesmo. O stack inteiro é um site Astro estático mais três funções serverless pequenas. A parte divertida nunca foi o encanamento. É decidir o que seu site deveria dizer quando o visitante não é uma pessoa e, cada vez mais, quando o visitante é um agent. --- # The AI harness behind the CS Brasil game Source URL: https://rubenmarcus.dev/blog/cs-brasil-ai-harness Language: en Published: 2026-08-04T00:00:00.000Z Description: The machine that builds CS Brasil is no longer a folder of markdown. It is a measurement engine: the real game booted in pure Node, 61 invariants with mutation tests, generated docs that fail CI on drift, and four laws learned the expensive way. I already wrote the [build-side retrospective for CS Brasil](/blog/shipping-a-browser-fps): the Gauntlet loop, the adversarial critics, the parallel builders. That post is about how the game gets changed. This one is about how a change gets *believed*: the measurement engine underneath the loop. The repo is public, so everything here is a path you can open. The previous version of this post described a folder of markdown. That was true at the time and isn't anymore. Since then the harness grew a spine: `tools/eval/` now holds 154 scripts, with 43 more pipeline scripts in `tools/`, all reachable through 41 npm scripts. Markdown still matters. It just no longer decides anything alone. Numbers do. ## The game boots without a browser The most valuable file in the harness is `tools/eval/harness.mjs`. It boots the real, production `Game` class (the same code players run) inside plain Node, with the DOM and canvas stubbed out. No browser, no GPU, no rendering at all. This is only possible because the game is zero-build vanilla JS: plain modules you can `import` anywhere, no bundler in the way. On top of that sits `botsim.mjs`, which plays full bot matches deterministically: 60 seconds, across all five maps, with fixed random seeds. Same seed, same match, every time. That one property turned game testing from archaeology into science. The old Playwright path cost about ten minutes per map at 0.3 FPS under software rendering, and two captures in parallel crashed the boot and manufactured bugs that were actually load. The Node harness answers in seconds, and its answers are reproducible. ## The gate is a list of invariants `tools/eval/invariants.mjs` declares 61 invariant IDs (executable statements about what must always be true) and exits with code 1 if any critical one fails, which makes it a CI gate. It currently sits at 39 of 52 critical checks passing, and the reds are tracked in the open, not hidden. The header of that file is the best documentation I've ever written, because it's a list of my own bug reports translated into physics. "The hands are floating" becomes a ceiling on hand-to-grip distance. "The gun points at the floor" becomes a maximum barrel angle. "Sniper without zoom" becomes: scoped field of view must be smaller than hip field of view. And the file carries the rule that keeps it honest: every new bug I report becomes an invariant. That is how a bug never comes back. My favorite is AUD1, a meta-invariant: it checks that the ruler agrees with the game. It exists because the gate once measured a stale snapshot of the viewmodel and confidently reported red on code that was fine: the gate was lying, and now there's an invariant whose only job is catching the gate lying. ## Every ruler ships with a mutant Mutation testing, for the uninitiated: you deliberately break the code (that's the mutant) and check that your tests catch it. If nothing goes red, your tests are decoration. The harness applies this to the rulers themselves: every invariant must ship with a mutation that turns it red. The rule exists because of a humiliating result. A mutant that removed a real fix passed 20 of 22 checks GREEN. The invariant was reading the constant's *declaration* (still sitting pretty in the file) instead of its *use*. The fix was gone, the number was still there, and the ruler applauded. As the repo puts it: *uma régua que não reprova a versão anterior do próprio arquivo não é régua, é decoração*. A ruler that can't fail the previous version of its own file is decoration. ## The docs are generated, and CI checks Every number in the README, the agent instructions, and the docs lives inside a generated block, delimited by `BEGIN:GERADO` markers and written by `tools/gen-docs.mjs`. `npm run docs:check` regenerates all of it and fails CI on any drift. This was born from a real incident: a skill file claimed `game.js` had 3,234 lines while the file had quietly doubled in size. No rule watched that number, so it kept lying, politely, in the exact document every agent reads first. The rule that came out of it: **a number derivable from code is never written by hand.** ## Four laws, paid in full The measurement engine runs on four laws, each with an incident receipt: 1. **Goodhart is undefeated.** An agent once raised the gate score from 16/21 to 19/21, and silently zeroed `VM_OFF`, the constant that positioned the entire viewmodel, destroying the look we had deliberately chosen. The agent didn't cheat. As the incident note says: *o agente não trapaceou, ele otimizou honestamente a única coisa que estava medida.* It honestly optimized the only thing being measured. The fix was VM12, an invariant that encodes intent, not just numbers. 2. **A ceiling without provenance is an opinion.** I spent three days fixing weapon framing against asserted numbers ("the muzzle sits at 0.66 of screen height") that nobody had ever measured in any pixel. The round only ended when we measured actual Counter-Strike 1.6 frames and replaced every asserted number with a measured one, plus the script that reproduces it. *Teto sem procedência é opinião.* 3. **Mutations or decoration.** Covered above. 4. **Generate the figure and LOOK at it.** Numbers without images fooled this project four separate times. A metric can go green while the frame is garbage. A loop that doesn't end with a human looking at a picture ends as a cautionary blog post. Like this one. ## Then and now The delta since the last version of this post: eval scripts 106 → 154. Invariants 24 → 61. The invariant gate wasn't in CI at all; it now runs on every PR. `ARCH.md`, the line-level index of the 6,543-line `game.js`, was hand-written and wrong; it's now generated by `tools/gen-arch.mjs` with an `arch:check` gate. The 84KB append-only handoff log was replaced by a `STATUS.md` capped at 100 lines. ## What gets automated next The harness is now big enough to need its own harness. The roadmap is a real file in the repo, and everything in it is developer-experience work. None of it touches game code: - **A full mutation catalog.** Today every mutation is run by hand, one at a time, which means the gate can rot without anyone noticing. The plan is `tools/eval/mutate.mjs`: a declarative catalog mapping each invariant to the patch that should turn it red. Apply, run, restore, report. A mutant that kills its ruler is the normal case. A mutant that survives means a blind invariant, and that is the finding. The acceptance test is that it catches the holes we already know about. - **A lessons file.** Every production bug becomes one written line in `docs/LICOES.md`, with the real case that generated it, read at the start of every agent session. Today that memory lives in my head and in scattered comments, and every new agent rediscovers the same traps at full price. - **Cost accounting per front.** Tokens, tool calls, and gate delta logged per work front, in a plain JSONL file. A front that burns 300K tokens without moving the gate becomes visible as what it is: a front that produced text. - **Hash verification for the pinned skills.** `skills-lock.json` pins SHA-256 hashes for 30 third-party skills, and nothing verifies them. A lock nobody checks is documentation, not a guarantee. One `skills:check` script fixes that, and it goes into the fast gate. - **Subagents as configs, not prompts.** The critic, the builders, and the regression hunter currently exist as paragraphs of prompt text. They become real config files with their own tool restrictions (the critic gets no Write), their own worktree isolation, and their own models, because mechanical work doesn't need a frontier model. - **Stop hooks.** A hook that blocks the end of the turn until `invariants.mjs` exits 0. The loop checks itself before the human even looks. The pattern across all six: the instrumentation is becoming a product of its own, and it gets the same treatment as the game. Rulers with mutations, generated docs, gates in CI. The DX of the AI team gets the same rigor as the DX of the game, because the AI team is where the bugs are born now. The lesson is short. Any check that depends on a human remembering to run it is already broken. You just haven't noticed yet. *If you've ever watched a metric go green while the product got worse, my inbox is open.* --- # O harness de IA por trás do CS Brasil Source URL: https://rubenmarcus.dev/pt/blog/harness-de-ia-por-tras-do-cs-brasil Language: pt-BR Published: 2026-08-04T00:00:00.000Z Description: A máquina que constrói o CS Brasil não é mais uma pasta de markdown. É um motor de medição: o jogo real dando boot em Node puro, 61 invariantes com testes de mutação, docs gerados que reprovam o CI quando derivam, e quatro leis aprendidas do jeito caro. Eu já escrevi a [retrospectiva do lado de build do CS Brasil](/blog/shipping-a-browser-fps): o Gauntlet loop, os críticos adversariais, os builders em paralelo. Aquele post é sobre como o jogo muda. Este é sobre como uma mudança passa a ser *acreditada*: o motor de medição embaixo do loop. O repo é público, então tudo aqui é um caminho que você pode abrir. A versão anterior deste post descrevia uma pasta de markdown. Era verdade na época e não é mais. Desde então o harness criou uma espinha: `tools/eval/` agora tem 154 scripts, com mais 43 scripts de pipeline em `tools/`, todos alcançáveis por 41 npm scripts. Markdown ainda importa. Só não decide mais nada sozinho. Números decidem. ## O jogo dá boot sem navegador O arquivo mais valioso do harness é `tools/eval/harness.mjs`. Ele sobe a classe `Game` real, de produção (o mesmo código que os jogadores rodam), dentro de Node puro, com o DOM e o canvas stubados. Sem navegador, sem GPU, sem renderização nenhuma. Isso só é possível porque o jogo é vanilla JS zero-build: módulos puros que você pode dar `import` em qualquer lugar, sem bundler no caminho. Em cima disso fica o `botsim.mjs`, que joga partidas inteiras de bots de forma determinística: 60 segundos, nos cinco mapas, com seeds fixas. Mesma seed, mesma partida, toda vez. Essa propriedade transformou teste de jogo de arqueologia em ciência. O caminho antigo com Playwright custava uns dez minutos por mapa a 0,3 FPS em renderização por software, e duas capturas em paralelo derrubavam o boot e fabricavam bugs que eram, na verdade, carga. O harness em Node responde em segundos, e as respostas são reproduzíveis. ## O portão é uma lista de invariantes O `tools/eval/invariants.mjs` declara 61 IDs de invariantes (afirmações executáveis sobre o que precisa ser sempre verdade) e sai com código 1 se qualquer crítica falhar, o que o torna um gate de CI. Hoje ele está em 39 de 52 checagens críticas passando, e os vermelhos são rastreados em aberto, não escondidos. O cabeçalho desse arquivo é a melhor documentação que já escrevi, porque é uma lista dos meus próprios bug reports traduzidos para física. "As mãos estão soltas no ar" vira um teto para a distância mão↔grip. "A arma aponta pro chão" vira um ângulo máximo de cano. "Sniper sem zoom" vira: o campo de visão mirando tem que ser menor que o de quadril. E o arquivo carrega a regra que o mantém honesto: todo bug novo que eu reporto vira uma invariante. É assim que um bug nunca volta. Minha favorita é a AUD1, uma invariante meta: ela checa se a régua concorda com o jogo. Existe porque o portão uma vez mediu um snapshot velho do viewmodel e reportou vermelho com toda confiança em código que estava certo: o portão estava mentindo, e agora existe uma invariante cujo único trabalho é pegar o portão mentindo. ## Toda régua shipa com um mutante Teste de mutação, para os não-infectados: você quebra o código de propósito (esse é o mutante) e confere se seus testes pegam. Se nada fica vermelho, seus testes são decoração. O harness aplica isso às próprias réguas: toda invariante precisa shipar com uma mutação que a deixe vermelha. A regra existe por causa de um resultado humilhante. Um mutante que removia um fix de verdade passou em 20 de 22 checagens, GREEN. A invariante lia a *declaração* da constante (ainda linda no arquivo) em vez do *uso* dela. O fix tinha sumido, o número continuava lá, e a régua aplaudiu. Como diz o repo: **uma régua que não reprova a versão anterior do próprio arquivo não é régua, é decoração.** ## As docs são geradas, e o CI confere Todo número no README, nas instruções de agente e nas docs vive dentro de um bloco gerado, delimitado por marcadores `BEGIN:GERADO` e escrito pelo `tools/gen-docs.mjs`. O `npm run docs:check` regenera tudo e reprova o CI se qualquer coisa derivou. Isso nasceu de um incidente real: um arquivo de skill afirmava que o `game.js` tinha 3.234 linhas enquanto o arquivo dobrava de tamanho em silêncio. Nenhuma regra vigiava aquele número, então ele continuou mentindo, educadamente, no exato documento que todo agente lê primeiro. A regra que saiu disso: **número derivável de código nunca é escrito à mão.** ## Quatro leis, pagas integralmente O motor de medição roda em cima de quatro leis, cada uma com recibo de incidente: 1. **Goodhart é imbatível.** Um agente uma vez subiu o placar do gate de 16/21 para 19/21, e silenciosamente zerou o `VM_OFF`, a constante que posicionava o viewmodel inteiro, destruindo o visual que a gente tinha escolhido de propósito. O agente não trapaceou. Ele otimizou honestamente a única coisa que estava medida. O fix foi a VM12, uma invariante que codifica intenção, não só números. 2. **Teto sem procedência é opinião.** Passei três dias corrigindo o enquadramento das armas contra números asseridos ("a boca da arma fica a 0,66 da altura da tela") que ninguém tinha medido em pixel nenhum. A rodada só terminou quando medimos frames reais do Counter-Strike 1.6 e substituímos cada número asserido por um medido, junto com o script que o reproduz. 3. **Mutações ou decoração.** Coberto acima. 4. **Gere a figura e OLHE.** Números sem imagens enganaram este projeto quatro vezes. Uma métrica pode ficar verde enquanto o frame é lixo. Um loop que não termina com um humano olhando para uma imagem termina como post de blog cautelar. Como este. ## Antes e agora O delta desde a última versão deste post: scripts de eval 106 → 154. Invariantes 24 → 61. O gate de invariantes não estava no CI; agora roda em todo PR. O `ARCH.md`, o índice linha a linha do `game.js` de 6.543 linhas, era escrito à mão e errado; agora é gerado pelo `tools/gen-arch.mjs` com um gate `arch:check`. O log de handoff de 84KB, append-only, foi substituído por um `STATUS.md` limitado a 100 linhas. ## O que entra de automação agora O harness ficou grande o suficiente para precisar do próprio harness. O roadmap é um arquivo de verdade no repo, e tudo nele é trabalho de developer experience. Nada toca código de jogo: - **Um catálogo completo de mutantes.** Hoje cada mutação roda à mão, uma por vez, o que significa que o portão pode apodrecer sem ninguém notar. O plano é o `tools/eval/mutate.mjs`: um catálogo declarativo mapeando cada invariante para o patch que deveria deixá-la vermelha. Aplica, roda, restaura, reporta. Mutante que mata a régua é o caso normal. Mutante que sobrevive é invariante cega, e esse é o achado. O critério de aceite é ele pegar os buracos que a gente já conhece. - **Um arquivo de lições.** Todo bug de produção vira uma linha escrita em `docs/LICOES.md`, com o caso real que gerou a lição, lida no começo de toda sessão de agente. Hoje essa memória mora na minha cabeça e em comentários espalhados, e todo agente novo redescobre as mesmas armadilhas pagando preço cheio. - **Contabilidade de custo por frente.** Tokens, chamadas de ferramenta e delta do portão registrados por frente de trabalho, num JSONL simples. Uma frente que queima 300 mil tokens sem mexer no portão fica visível como o que ela é: uma frente que produziu texto. - **Verificação de hash das skills pinadas.** O `skills-lock.json` guarda o SHA-256 de 30 skills de terceiros, e nada verifica. Um cadeado que ninguém confere é documentação, não garantia. Um script `skills:check` resolve, e entra no gate rápido. - **Subagents como config, não como prompt.** O crítico, os builders e o caçador de regressões hoje existem como parágrafos de prompt. Viram arquivos de configuração de verdade, com restrições de ferramenta próprias (o crítico não recebe Write), isolamento em worktree e modelos próprios, porque trabalho mecânico não precisa de modelo frontier. - **Stop hooks.** Um hook que bloqueia o fim do turno até o `invariants.mjs` sair com código 0. O loop se verifica sozinho antes de o humano olhar. O padrão nos seis itens: a instrumentação está virando um produto próprio, e recebe o mesmo tratamento do jogo. Réguas com mutações, docs gerados, gates no CI. A DX do time de IA recebe o mesmo rigor da DX do jogo, porque é no time de IA que os bugs nascem agora. A lição é curta. Qualquer checagem que depende de um humano lembrar de rodar já está quebrada. Você só ainda não percebeu. *Se você já viu uma métrica ficar verde enquanto o produto piorava, minha inbox está aberta.* --- # A command center for agent swarms, in markdown Source URL: https://rubenmarcus.dev/blog/agent-command-center Language: en Published: 2026-08-02T00:00:00.000Z Description: I run a swarm of coding agents — Codex, Claude, Amp, Kimi — plus humans, coordinated entirely through markdown files. No database, no dashboard-as-source-of-truth, chat history explicitly banned. Here is the shared brain: control room, task queue, candidate cemetery, and fail-closed automation. For a while now I have been running a competitive optimization challenge (the kind where you throw compute and ingenuity at a hard target and the leaderboard keeps score) using a swarm of coding agents. Codex, Claude, Amp, Kimi, plus the occasional human who wandered in. The agents come and go, hit rate limits, lose their context windows, and forget everything between sessions. So the coordination layer cannot live in any of them. It lives in files. Markdown is the database, and chat history is explicitly, deliberately *not* the source of truth. If an agent discovers something and does not write it down, it did not happen. This post is the anatomy of that shared brain. ## Why not a database Because agents already read and write markdown natively, git diffs are the audit log for free, and I can inspect the entire system state with `cat`. Every fancier option (a SQLite store, a task API, a web dashboard) adds a layer the agents have to be taught to use and I have to maintain. A directory of markdown files has zero onboarding cost: point any agent at it and it already knows the interface. ## control-room/: the shared brain The `control-room/` directory is the state of the whole operation: - **CURRENT_CONTEXT.md**: what is happening right now, rewritten whenever it changes. Any agent or human reads this first and is oriented in thirty seconds. - **ROUTES.md**: the queue of active attack routes, each with a verdict. This is the work board. - **LEDGER.md**: append-only. Every run, every result, every cost, one line each, never edited. The ledger is the memory that survives every context wipe. - **CONTROL.md**: the submit gate. Nothing goes out without passing through it. - **A compact learning digest**: auto-generated from the ledger, so a fresh agent does not spend paid tokens rediscovering dead routes the hard way. This one file pays for itself daily: without it, every new session is a goldfish with a credit card. ## swarm/: the task queue The `swarm/` directory is the machinery: task JSONs sit in `queue/`, prompt snapshots in `outbox/`, and results land in `results/`. A Python dispatcher with a CLI adapter per agent (each of these tools has a different personality and a different flag syntax; the adapters absorb that) pops tasks and runs them. A launchd scheduler queues exactly one bounded task per 30-minute cycle. One task per cycle is a deliberate governor, not a limitation. It caps the blast radius of any confused agent, spreads cost predictably, and gives me a natural checkpoint to read results. An unbounded queue with autonomous agents is just a very efficient way to convert money into regressions. Every task is bounded before it starts: a falsifier (what result would prove this route wrong), a validator command, a max cost, a wall-clock limit, and a kill condition. If a task cannot state its falsifier, it is not a task, it is a vibe, and vibes do not get queued. ## agents/: role prompts Agents get role prompts the way employees get job descriptions. There is a **dissector** (tear apart the problem and existing attempts), an **engineer** (implement the route), an **analyst** (read results and extract lessons), a **scout** (explore untested levers), and an **orchestrator** (decide what gets queued next). Same underlying models, different jobs. A generalist prompt produces generalist wandering; a role prompt produces a deliverable. ## skills/: the output format police One skills file enforces a rigid output format on every agent report: ``` DECISION: WHY: EVIDENCE: MISSING: NEXT COMMAND: STOP RULE: ``` Evidence must be labeled CONFIRMED, INFERRED, or UNKNOWN. This is the single highest-leverage file in the whole system. Before it, agent reports were confident prose where "the test passed" and "I assume the test would pass" looked identical. After it, an agent has to commit, in writing, to what it actually observed. Hallucinations do not disappear, but they get a lot harder to launder into CONFIRMED. ## intelligence/: the map of the maze The `intelligence/` directory holds the branch library and the lever taxonomy, every known way to attack the problem, each in one of three states: `untested`, `pending`, or `killed`. Killed is a first-class state, and that is the point: a dead route written down is worth more than a live one, because the most expensive outcome in a multi-agent system is three different agents rediscovering the same dead end across three different sessions. Which brings me to my favorite directory: the **candidate cemetery**. Every dead candidate gets a headstone: what it was, the verdict that killed it, and the exact conditions under which it may be reopened. The cemetery is what stops the swarm from re-litigating settled history. It is a graveyard as an optimization. ## Worktrees and fail-closed automation Execution hygiene: one git worktree per route, so routes never contaminate each other and every result is attributable to a diff. And the automation is fail-closed. Automation may *prepare* (queue tasks, draft prompts, aggregate results) but it may never improvise spend. There are hard stops wired in: a `GPU_APPROVAL_NEEDED` gate for anything that costs real compute, and a plain `STOP` file that halts the scheduler dead. The STOP file is deliberately stupid: no conditionals, no parsing, if it exists nothing runs. When the thing supervising your agents is also software, the emergency brake should be the dumbest possible object in the repo. ## The dashboard observes, it does not decide There is an internal observability dashboard: a context-pressure proxy (how close each agent is to the top of its window), prompt history, and a findings log of death-loops and hallucinations the system has caught. But the dashboard is a read-only view over the markdown. The files are the truth; the dashboard is the instrument panel. The moment a dashboard becomes the source of truth, you are debugging your dashboard. ## What I actually learned Multi-agent systems do not fail because the agents are dumb. They fail because the coordination is vibes. Agents forget, hallucinate, duplicate work, and confidently report things they did not check, and every one of those failure modes is survivable if the state of the world lives in boring, inspectable, append-only files that no single agent owns. The whole command center is a directory of markdown, a Python dispatcher, a scheduler, and a graveyard. It has survived every model swap I have thrown at it, because it does not depend on any model. Markdown outlives context windows. That is the entire trick. --- # Um centro de comando para swarms de agentes, em markdown Source URL: https://rubenmarcus.dev/pt/blog/centro-de-comando-para-swarms-de-agentes-em-markdown Language: pt-BR Published: 2026-08-02T00:00:00.000Z Description: Eu rodo um swarm de agentes de codificação — Codex, Claude, Amp, Kimi — mais humanos, coordenados inteiramente por arquivos markdown. Sem banco de dados, sem dashboard como fonte da verdade, histórico de chat explicitamente banido. Aqui está o cérebro compartilhado: sala de controle, fila de tarefas, cemitério de candidatos e automação fail-closed. Há um tempo venho rodando um desafio competitivo de otimização (o tipo em que você joga computação e engenhosidade contra um alvo difícil e o leaderboard marca os pontos) usando um swarm de agentes de codificação. Codex, Claude, Amp, Kimi, mais um ou outro humano que apareceu. Os agentes vêm e vão, batem no rate limit, estouram a janela de contexto e esquecem tudo entre sessões. Então a camada de coordenação não pode viver em nenhum deles. Ela vive em arquivos. Markdown é o banco de dados, e o histórico de chat é explícita e deliberadamente *não* a fonte da verdade. Se um agente descobriu algo e não escreveu, isso não aconteceu. Este post é a anatomia desse cérebro compartilhado. ## Por que não um banco de dados Porque agentes já leem e escrevem markdown nativamente, git diffs são o log de auditoria de graça, e eu consigo inspecionar o estado inteiro do sistema com `cat`. Qualquer opção mais sofisticada (um SQLite, uma API de tarefas, um dashboard web) adiciona uma camada que os agentes precisam ser ensinados a usar e que eu tenho que manter. Um diretório de arquivos markdown tem custo zero de onboarding: aponte qualquer agente para ele e ele já conhece a interface. ## control-room/: o cérebro compartilhado O diretório `control-room/` é o estado de toda a operação: - **CURRENT_CONTEXT.md**: o que está acontecendo agora, reescrito sempre que muda. Qualquer agente ou humano lê isso primeiro e está orientado em trinta segundos. - **ROUTES.md**: a fila de rotas de ataque ativas, cada uma com um veredito. Este é o quadro de trabalho. - **LEDGER.md**: append-only. Cada execução, cada resultado, cada custo, uma linha cada, nunca editado. O ledger é a memória que sobrevive a todo wipe de contexto. - **CONTROL.md**: o portão de submissão. Nada sai sem passar por ele. - **Um digest compacto de aprendizados**: gerado automaticamente a partir do ledger, para que um agente novo não gaste tokens pagos redescobrindo rotas mortas da forma difícil. Esse arquivo se paga diariamente: sem ele, toda sessão nova é um peixe dourado com um cartão de crédito. ## swarm/: a fila de tarefas O diretório `swarm/` é a maquinaria: JSONs de tarefa ficam em `queue/`, snapshots de prompt em `outbox/`, e resultados aterrissam em `results/`. Um dispatcher em Python com um adaptador de CLI por agente (cada uma dessas ferramentas tem uma personalidade e uma sintaxe de flags diferente; os adaptadores absorvem isso) retira tarefas da fila e as executa. Um scheduler no launchd enfileira exatamente uma tarefa limitada por ciclo de 30 minutos. Uma tarefa por ciclo é um regulador deliberado, não uma limitação. Ele limita o raio de explosão de qualquer agente confuso, distribui o custo de forma previsível e me dá um checkpoint natural para ler resultados. Uma fila sem limite com agentes autônomos é só um jeito muito eficiente de converter dinheiro em regressões. Toda tarefa é delimitada antes de começar: um falsificador (que resultado provaria que esta rota está errada), um comando validador, um custo máximo, um limite de wall-clock e uma condição de kill. Se uma tarefa não consegue declarar seu falsificador, ela não é uma tarefa, é uma vibe, e vibes não entram na fila. ## agents/: prompts de papel Agentes recebem prompts de papel do mesmo jeito que funcionários recebem descrições de cargo. Há um **dissector** (desmonta o problema e as tentativas existentes), um **engineer** (implementa a rota), um **analyst** (lê resultados e extrai lições), um **scout** (explora alavancas não testadas) e um **orchestrator** (decide o que entra na fila a seguir). Mesmos modelos por baixo, trabalhos diferentes. Um prompt generalista produz divagação generalista; um prompt de papel produz um entregável. ## skills/: a polícia do formato de output Um arquivo de skills impõe um formato rígido de output em todo relatório de agente: ``` DECISION: WHY: EVIDENCE: MISSING: NEXT COMMAND: STOP RULE: ``` A evidência precisa ser rotulada como CONFIRMED, INFERRED ou UNKNOWN. Este é o arquivo de maior alavancagem do sistema inteiro. Antes dele, relatórios de agente eram prosa confiante onde "o teste passou" e "imagino que o teste passaria" pareciam idênticos. Depois dele, o agente precisa se comprometer, por escrito, com o que ele realmente observou. Alucinações não desaparecem, mas fica bem mais difícil lavá-las para CONFIRMED. ## intelligence/: o mapa do labirinto O diretório `intelligence/` guarda a biblioteca de branches e a taxonomia de alavancas, toda forma conhecida de atacar o problema, cada uma em um de três estados: `untested`, `pending` ou `killed`. Killed é um estado de primeira classe, e esse é o ponto: uma rota morta registrada vale mais que uma viva, porque o resultado mais caro num sistema multi-agente é três agentes diferentes redescobrindo o mesmo beco sem saída em três sessões diferentes. O que me traz ao meu diretório favorito: o **cemitério de candidatos**. Todo candidato morto ganha uma lápide: o que era, o veredito que o matou e as condições exatas sob as quais pode ser reaberto. O cemitério é o que impede o swarm de re-litigar história encerrada. É um cemitério como otimização. ## Worktrees e automação fail-closed Higiene de execução: um git worktree por rota, então rotas nunca contaminam umas às outras e todo resultado é atribuível a um diff. E a automação é fail-closed. A automação pode *preparar* (enfileirar tarefas, rascunhar prompts, agregar resultados), mas nunca pode improvisar gastos. Há hard stops ligados no circuito: um portão `GPU_APPROVAL_NEEDED` para qualquer coisa que custe computação de verdade, e um simples arquivo `STOP` que mata o scheduler na hora. O arquivo STOP é deliberadamente burro: sem condicionais, sem parsing, se ele existe nada roda. Quando a coisa que supervisiona seus agentes também é software, o freio de emergência deve ser o objeto mais burro possível no repositório. ## O dashboard observa, não decide Existe um dashboard interno de observabilidade: um proxy de pressão de contexto (quão perto cada agente está do topo da janela), histórico de prompts e um log de achados de death-loops e alucinações que o sistema pegou. Mas o dashboard é uma visão read-only sobre o markdown. Os arquivos são a verdade; o dashboard é o painel de instrumentos. No momento em que um dashboard vira a fonte da verdade, você está debugando seu dashboard. ## O que eu realmente aprendi Sistemas multi-agente não falham porque os agentes são burros. Falham porque a coordenação é vibes. Agentes esquecem, alucinam, duplicam trabalho e relatam com confiança coisas que não verificaram, e cada um desses modos de falha é sobrevivível se o estado do mundo vive em arquivos chatos, inspecionáveis e append-only que nenhum agente individual possui. O centro de comando inteiro é um diretório de markdown, um dispatcher em Python, um scheduler e um cemitério. Ele sobreviveu a toda troca de modelo que eu joguei nele, porque não depende de modelo nenhum. Markdown sobrevive a janelas de contexto. Esse é o truque inteiro. --- # I built my portfolio with a fleet of AI agents Source URL: https://rubenmarcus.dev/blog/i-built-my-portfolio-with-a-fleet-of-ai-agents Language: en Published: 2026-07-30T00:00:00.000Z Description: The making-of of this site: AI-generated 3D models that kept coming out as busts, a shader pipeline that morphs me into wireframe and ASCII, a visual gauntlet that grades every pixel, and what I learned about art-directing machines. On the third cycle of the visual gauntlet I run against this site, the mean score across 12 screenshots was 29.83 out of 30. The missing fraction was a single email address that refused to fit inside its own card on a 390px viewport. A contact page that can't display the contact is a special kind of broken, and no green CI checkmark was going to catch it. ## The brief I gave myself My old portfolio was a Next.js page from 2021 with a "new posts coming soon" that had been lying for four years. I build AI tooling for a living, and my own corner of the internet looked abandoned mid-sprint. Every recruiter, client, and collaborator landed there first. So I gave myself a brief: a portfolio that does three jobs: prove I can ship product, prove I can orchestrate AI agents, and be weird enough that people screenshot it. So of course I built it with agents. What I didn't expect: the hardest part wasn't the code. It was **taste**. Agents will happily ship mediocre work with total confidence. This is the story of rejecting them until they got it right, with receipts: files, commands, and scores. ## Act 1: The bust problem The hero concept: a stylized 3D me, sitting at a desk, coding, rendered as glowing green contour lines that dissolve into wireframe and ASCII as you scroll. The model would come from an AI 3D generator (Tripo, via an MCP pipeline). Every generation came back as a bust. A giant floating head. I have a shaved head covered in tattoos, and the generator saw the reference photo and decided the head WAS the product. Three generations, three heads, credits burning. The fix wasn't a better prompt about my face; it was removing the face entirely. A text-only prompt ("engineer sitting at a desk typing, character occupies at most 60% of frame") produced the full scene on the first try. **Lesson 1: when an AI anchors on the wrong thing, take the anchor away instead of arguing with it.** ## Act 2: "This looks amateur" First assembled hero: layers on layers. A video background, an ASCII glyph field, floating code snippets, the 3D figure. Technically impressive, visually a mess. I looked at the screenshot and wrote back to the agent: "this is a Frankenstein, kill everything except the figure." That instruction (one clean stage, one subject, cinematic lighting) is what turned the corner. The agent rewrote the hero around a single contour-shaded figure on a black stage, with fog for depth and a contact glow under the desk. AAA design is mostly deleting. **Lesson 2: agents add; art direction subtracts. You are the taste layer.** ## Act 3: The typing problem I wanted the character to actually type. The generated model was a single fused mesh, no bones, no rig. The answer: generate a separate character, auto-rig it (41 joints), and retarget a stock animation onto it. There's no "typing" preset in the library, but there's `play_video_game`, which is hands-forward-fingers-moving, close enough that it reads as typing at hero distance. Then the real bug: my custom shaders (contour isolines, wireframe glow, point-cloud dissolve) didn't support skinned meshes. The character would T-pose through the animation like a haunted mannequin. The fix was rewriting the shader pipeline to inject the skinning chunks: the character now gets its own skinning-capable green treatment while the desk morphs through the full state machine. **Lesson 3: "it doesn't support X" is where the actual engineering starts. Agents are great at the happy path; you own the edge cases.** ## Act 4: The gauntlet Three iterations shipped "blind" before I learned my lesson: agents reporting "done" on work nobody had rendered. So I built a gauntlet. `node scripts/visual-gauntlet.mjs --cycle N` screenshots every page at 1600x1000 and 390x844 in headless Chrome, and a written rubric (`qa/VISUAL_RUBRIC.md`, 15 items scored 0 to 2, max 30 per shot) grades every pixel. Every change goes capture → score → fix → re-capture, keeping only what doesn't regress. The harness itself needed calibrating before it could judge anything. Headless Chrome with `--disable-gpu` falls back to SwiftShader, and SwiftShader can't render this site: WebGL contexts failed outright (`BindToCurrentSequence failed`), shader derivatives came out mangled (my contour bands rendered as dense mesh), and DOM text layers vanished. The fix was `--use-angle=metal`, which sends shots through the real GPU path. Then a subtler race: compositor-driven CSS animations run on a different clock than `virtual-time-budget 10000`, so entrance animations (`heroRise`, the GSAP reveals) sometimes captured at opacity 0. `--force-prefers-reduced-motion` makes every shot a deterministic settled frame. The vortex intro, which only exists with motion on, gets verified through dedicated unforced shots instead. Even then, the first runs lied. Astro's dev toolbar pill photobombed every screenshot until I disabled it in `astro.config.mjs`. An "overflow" flagged on the About page turned out to be a 490px harness artifact, not a site bug. A QA rig that cries wolf teaches you to ignore it, so false positives got fixed as aggressively as real defects. The real numbers: cycle 1 averaged 29.4, cycle 2 averaged 29.75, cycle 3 averaged 29.83. The stubborn remainder was my own email address. It crossed the card border on desktop and clipped mid-string ("@gmai…") on mobile. First fix: `clamp(1.5rem,3vw,2.1rem)` down to `clamp(1.15rem,2.6vw,1.75rem)` plus `overflow-wrap: anywhere`. Cycle 2: contained, but wrapping an orphan "m" onto its own line. Second fix: the floor down to `1rem`. Cycle 3: one line, inside the card, 30 out of 30. One string, three cycles. The only points still on the table are environmental. Repeated gauntlet runs burn through GitHub's unauthenticated API rate limit, the requests start coming back 403, and the HUD telemetry card degrades to placeholder dashes, exactly as designed. The rubric's console-error item catches it every cycle. Graceful degradation, verified by accident, twelve times per cycle. **Lesson 4: "the build passes" is not "it looks right". Automate the looking, and calibrate the camera before you trust the photo.** ## Act 5: Linting the AI's prose The gauntlet covers pixels. The words needed a gate too, because agents write prose the way they write CSS: fluent, confident, full of tells. `scripts/text-gate.mjs` scans 50 files (both blog collections, plus the about, ai, and agents pages in EN and PT) for the patterns that make text read as machine-generated. The banned list is a regex array called RULES, and I can't quote most of it here, because quoting it trips the gate (the linter has no concept of irony). The short version: em-dashes and en-dashes anywhere in prose or frontmatter, the single most recognizable AI tell; the cliché transitions every LLM reaches for; hype verbs that promise without saying what changes; announcing your own honesty; exclamation marks in technical prose. Any violation exits 1 with file:line. The error messages are in Portuguese, because I'm the one reading them. This post passes the gate, and so does its PT twin. Writing about AI-generated work while an AI-tell linter watches your draft is a decent approximation of pair programming with a very literal colleague. **Lesson 5: if a tell can be regexed, it can be gated. A style guide that lives in a doc gets ignored; a style guide that exits 1 gets obeyed.** ## Act 6: The site talks to agents directly If agents are going to research me on behalf of recruiters, the site might as well speak their protocol. `src/pages/api/mcp.ts` is a Model Context Protocol endpoint hand-rolled on a Vercel function: JSON-RPC 2.0 over streamable HTTP, no SDK, no dependencies. It implements `initialize`, `ping`, `tools/list`, and `tools/call`, with proper error codes (-32700 for parse errors, -32601 for unknown methods). Four tools: `get_resume`, `get_services`, `check_availability`, and `book_intro`. `book_intro` is the interesting one. It lets someone's AI assistant book a project intro on their behalf, relaying the brief to my email through a formsubmit POST. Every field is capped server-side (name 120 chars, contact 160, brief 4000) because an agent will happily paste an entire RFC into a form field. Any MCP client (Claude, ChatGPT, Cursor, Kimi) can add rubenmarcus.dev as a connector and interview the site directly. There's a quieter easter egg in `src/middleware.ts`, 17 lines: if your user agent matches curl, wget, httpie, or libcurl and you hit a page route, the middleware rewrites you to `/api/resume.txt`. Browsers get the site. Terminals get the resume. **Lesson 6: agents are users now. Give them an API instead of making them scrape your DOM.** ## Act 7: The player that died silently The site has a bottom-right audio deck: a YouTube IFrame API player with a fixed playlist, terminal-styled, persisted across page transitions with Astro's `transition:persist`. Except persistence has a trap. Astro moves persisted elements into the new document on navigation, and moving an `