From 01046b01e4419de8eab230f16ed9483a35ff56b9 Mon Sep 17 00:00:00 2001 From: Tim Krampitz Date: Sun, 26 Jul 2026 14:00:58 +0200 Subject: [PATCH] Neue Skills, Referenzen & OpenWiki-Doku integriert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Umfangreiche Erweiterung der Skill-Bibliothek: Neue Skills für Humanisierung (Englisch/PT-BR), Design-Validierung, AI-SEO und Coolify-Deployment inkl. Regelwerke, Presets, Pattern-Referenzen, Testfälle und Automatisierungsskripte. Zusätzliche Skills für Revenue-Centric Design, Pier Cloud, OKF, Lebenslauf- und LinkedIn-Optimierung sowie zahlreiche Referenzdateien, Checklisten und YAML/JSON/Markdown-Templates. Einführung einer vollständigen OpenWiki-Dokumentation mit Architektur-, Domain- und Workflow-Beschreibungen, zentralem Index und automatisierten Updates. Modularer Aufbau, restriktive Lizenzen und umfassende Qualitäts- und Evaluationsmechanismen für alle neuen Inhalte. --- .../skills/agent-ready-cloudflare/README.md | 338 +++++++ .../skills/agent-ready-cloudflare/SKILL.md | 783 +++++++++++++++++ .../a2a-agent-card/SKILL.md | 35 + .../agent-ready-cloudflare/acp/SKILL.md | 28 + .../agent-skills/SKILL.md | 31 + .../agent-ready-cloudflare/ai-rules/SKILL.md | 31 + .../api-catalog/SKILL.md | 27 + .../agent-ready-cloudflare/auth-md/SKILL.md | 43 + .../content-signals/SKILL.md | 32 + .../agent-ready-cloudflare/dns-aid/SKILL.md | 39 + .../link-headers/SKILL.md | 34 + .../llms-full-txt/SKILL.md | 27 + .../agent-ready-cloudflare/llms-txt/SKILL.md | 28 + .../markdown-negotiation/SKILL.md | 34 + .../mcp-server-card/SKILL.md | 33 + .../agent-ready-cloudflare/mpp/SKILL.md | 56 ++ .../oauth-discovery/SKILL.md | 33 + .../oauth-protected-resource/SKILL.md | 34 + .../robots-txt/SKILL.md | 31 + .../agent-ready-cloudflare/scan-site/SKILL.md | 73 ++ .../agent-ready-cloudflare/sitemap/SKILL.md | 27 + .../agent-ready-cloudflare/ucp/SKILL.md | 26 + .../web-bot-auth/SKILL.md | 32 + .../agent-ready-cloudflare/webmcp/SKILL.md | 29 + .../agent-ready-cloudflare/x402/SKILL.md | 28 + .github/skills/astro-sites-manager/SKILL.md | 251 ++++++ .../references/ai-dev-server.md | 172 ++++ .../references/coolify-deploy.md | 415 +++++++++ .../references/deployment.md | 396 +++++++++ .../references/install-mcp.md | 190 ++++ .../references/migration-v6-to-v7.md | 557 ++++++++++++ .../references/related-content.md | 394 +++++++++ .../references/seo-full-stack.md | 712 +++++++++++++++ .../references/starlight-and-patterns.md | 604 +++++++++++++ .../astro-sites-manager/references/testing.md | 387 +++++++++ .../references/v6-features.md | 271 ++++++ .../references/v7-features.md | 491 +++++++++++ .../references/validation-checklist.md | 210 +++++ .github/skills/auth-md/SKILL.md | 357 ++++++++ .../auth-md/references/example-auth-md.md | 403 +++++++++ .../references/implementation-guide.md | 433 +++++++++ .../auth-md/references/metadata-schema.md | 370 ++++++++ .../auth-md/references/protocol-template.md | 451 ++++++++++ .../auth-md/references/validation-rules.md | 162 ++++ .github/skills/coolify-operator/.env.example | 2 + .github/skills/coolify-operator/SKILL.md | 519 +++++++++++ .../skills/coolify-operator/evals/evals.json | 61 ++ .github/skills/design-md-validator/SKILL.md | 230 +++++ .../references/linting-rules.md | 66 ++ .../references/token-schema.md | 156 ++++ .github/skills/geo-optimization/SKILL.md | 132 +++ .../references/guiding-principles.md | 16 + .github/skills/human-ai/EVALUATION.md | 90 ++ .github/skills/human-ai/SKILL.md | 504 +++++++++++ .../references/patterns-composition.md | 130 +++ .../human-ai/references/patterns-content.md | 99 +++ .../references/patterns-english-specific.md | 153 ++++ .../human-ai/references/patterns-language.md | 120 +++ .../human-ai/references/patterns-style.md | 132 +++ .../human-ai/references/patterns-tone.md | 103 +++ .github/skills/human-ai/references/presets.md | 150 ++++ .github/skills/human-ai/references/summary.md | 22 + .github/skills/human-ai/references/tests.md | 14 + .github/skills/human-ai/scripts/measure.py | 353 ++++++++ .github/skills/humanizar/SKILL.md | 534 ++++++++++++ .../references/padroes-composicao.md | 312 +++++++ .../humanizar/references/padroes-conteudo.md | 180 ++++ .../humanizar/references/padroes-estilo.md | 345 ++++++++ .../references/padroes-exclusivos-pt-br.md | 567 ++++++++++++ .../humanizar/references/padroes-linguagem.md | 209 +++++ .../padroes-portugues-simplificado.md | 437 ++++++++++ .../humanizar/references/padroes-tom.md | 260 ++++++ .github/skills/loop-architect/LICENSE | 24 + .github/skills/loop-architect/SKILL.md | 233 +++++ .../examples/ai-workflow-mapping/LOOP.md | 86 ++ .../examples/ai-workflow-mapping/README.md | 19 + .../ai-workflow-mapping/RUN_IN_SESSION.md | 108 +++ .../inputs/process-notes.md | 14 + .../ai-workflow-mapping/loop.resolved.json | 194 +++++ .../examples/ai-workflow-mapping/loop.yaml | 104 +++ .../examples/ai-workflow-mapping/run-loop.py | 12 + .../scripts/check-loop-doc.py | 31 + .../references/control-rubric.md | 60 ++ .../references/council-rubric.md | 43 + .../loop-architect/references/goal-rubric.md | 42 + .../references/model-detection.md | 98 +++ .../references/verification-rubric.md | 59 ++ .../schemas/loop.resolved.v1.schema.json | 25 + .../schemas/loop.v1.schema.json | 190 ++++ .../skills/loop-architect/scripts/looper.py | 822 ++++++++++++++++++ .../loop-architect/templates/run-loop.py | 588 +++++++++++++ .../skills/okf-open-knowledge-format/SKILL.md | 424 +++++++++ .../references/conversion.md | 141 +++ .../references/examples.md | 302 +++++++ .../references/spec-v01.md | 451 ++++++++++ .../scripts/validate.sh | 102 +++ .github/skills/pier-cloud/.env.example | 9 + .github/skills/pier-cloud/SKILL.md | 103 +++ .../pier-cloud/references/TROUBLESHOOTING.md | 115 +++ .github/skills/pier-cloud/scripts/README.md | 166 ++++ .../pier-cloud/scripts/pier-cloud-auth.py | 62 ++ .../scripts/pier-cloud-get-all-workspaces.py | 124 +++ .../scripts/pier-cloud-get-view-data.py | 130 +++ .../pier-cloud/scripts/pier-cloud-get-view.py | 82 ++ .../scripts/pier-cloud-get-workspace-group.py | 93 ++ .../scripts/pier-cloud-get-workspace.py | 88 ++ .../scripts/pier-cloud-list-contexts.py | 76 ++ .../scripts/pier-cloud-list-views.py | 83 ++ .../pier-cloud-list-workspace-groups.py | 101 +++ .../scripts/pier-cloud-list-workspaces.py | 103 +++ .../pier-cloud/scripts/pier_cloud_client.py | 199 +++++ .github/skills/ralph-loop-kiro-specs/SKILL.md | 200 +++++ .../ralph-loop-kiro-specs-prompt.md | 246 ++++++ .../scripts/ralph-loop-kiro-specs-script.sh | 128 +++ .github/skills/resume-ats-beater/SKILL.md | 480 ++++++++++ .../references/auditoria-linkedin.md | 303 +++++++ .../references/diagnostico-ats.md | 50 ++ .../references/diagnostico-avancado.md | 31 + .../references/presets-formatos.md | 137 +++ .../resume-ats-beater/references/ssi.md | 139 +++ .../references/template-saida.md | 31 + .github/skills/revenue-centric-design/LICENSE | 30 + .../skills/revenue-centric-design/SKILL.md | 167 ++++ .../references/ai-era-differentiation.md | 57 ++ .../references/audit-template.md | 34 + .../references/behavioral-science-toolkit.md | 60 ++ .../references/churn-and-retention.md | 66 ++ .../conversion-and-landing-pages.md | 132 +++ .../references/metrics-and-experimentation.md | 31 + .../references/onboarding-and-activation.md | 148 ++++ .../references/positioning-icp-and-gtm.md | 64 ++ .../references/pricing-and-monetization.md | 87 ++ .../product-strategy-and-features.md | 58 ++ .../references/revenue-centric-design.md | 105 +++ .../scripts/check_usage_boundary.py | 106 +++ .../scripts/revenue_math.py | 116 +++ .github/skills/security-specialist/SKILL.md | 205 +++++ .../references/finding-format.md | 201 +++++ .../references/report-format.md | 284 ++++++ .../references/report-schema.json | 126 +++ .../references/scan-artifacts.md | 164 ++++ .../references/severity-policy.md | 162 ++++ .../security-specialist/scripts/finalize.py | 134 +++ .../security-specialist/scripts/pentest.py | 478 ++++++++++ .../security-specialist/scripts/rank_files.py | 95 ++ .../security-specialist/scripts/scan_db.py | 175 ++++ .../scripts/validate-findings.cjs | 150 ++++ .../steering/attack-paths.md | 150 ++++ .../steering/diff-review.md | 126 +++ .../security-specialist/steering/discovery.md | 142 +++ .../security-specialist/steering/full-scan.md | 188 ++++ .../security-specialist/steering/hunting.md | 184 ++++ .../security-specialist/steering/pentest.md | 224 +++++ .../steering/remediation.md | 110 +++ .../security-specialist/steering/reporting.md | 142 +++ .../steering/threat-model.md | 143 +++ .../security-specialist/steering/tracking.md | 152 ++++ .../security-specialist/steering/triage.md | 101 +++ .../steering/validation.md | 137 +++ .github/skills/skill-evaluation/SKILL.md | 299 +++++++ .../skill-evaluation/references/categories.md | 112 +++ .../skill-evaluation/references/mechanics.md | 116 +++ .../references/output-template.md | 158 ++++ .../skills/skill-evaluation/scripts/score.py | 66 ++ .github/skills/slop-eval/SKILL.md | 263 ++++++ .../slop-eval/references/output-template.md | 130 +++ .../slop-eval/references/premium-markers.md | 89 ++ .github/skills/slop-eval/references/tells.md | 145 +++ .github/skills/slop-eval/scripts/score.py | 117 +++ .github/skills/startup-idea/SKILL.md | 397 +++++++++ .github/skills/startup-idea/evals/evals.json | 41 + .github/skills/substack-expert/SKILL.md | 69 ++ .../references/formatting-best-practices.md | 35 + .../references/seo-output-example.md | 19 + .../ultimate-design-system-master/SKILL.md | 50 ++ .../references/briefing-questionnaire.md | 22 + .../prompt-01-design-system-architect.md | 65 ++ .../prompt-02-brand-identity-creator.md | 69 ++ .../prompt-03-ui-ux-pattern-master.md | 68 ++ .../prompt-04-marketing-asset-factory.md | 66 ++ .../prompt-05-figma-auto-layout-expert.md | 64 ++ .../prompt-06-design-critique-partner.md | 65 ++ .../prompt-07-design-trend-synthesizer.md | 51 ++ .../prompt-08-accessibility-auditor.md | 71 ++ .../prompt-09-design-to-code-translator.md | 65 ++ .../prompt-10-presentation-designer.md | 73 ++ .github/workflows/openwiki-update.yml | 52 ++ AGENTS.md | 9 + CLAUDE.md | 9 + openwiki/.last-update.json | 6 + openwiki/INSTRUCTIONS.md | 1 + openwiki/architecture/index.md | 3 + openwiki/architecture/overview.md | 129 +++ openwiki/domain/game-mechanics.md | 171 ++++ openwiki/domain/index.md | 3 + openwiki/index.md | 15 + openwiki/operations/index.md | 3 + openwiki/operations/runbook.md | 104 +++ openwiki/quickstart.md | 84 ++ openwiki/source-map.md | 69 ++ openwiki/workflows/index.md | 3 + openwiki/workflows/slip-and-challenge.md | 125 +++ 202 files changed, 31290 insertions(+) create mode 100644 .github/skills/agent-ready-cloudflare/README.md create mode 100644 .github/skills/agent-ready-cloudflare/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/a2a-agent-card/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/acp/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/agent-skills/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/ai-rules/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/api-catalog/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/auth-md/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/content-signals/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/dns-aid/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/link-headers/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/llms-full-txt/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/llms-txt/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/markdown-negotiation/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/mcp-server-card/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/mpp/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/oauth-discovery/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/oauth-protected-resource/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/robots-txt/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/scan-site/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/sitemap/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/ucp/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/web-bot-auth/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/webmcp/SKILL.md create mode 100644 .github/skills/agent-ready-cloudflare/x402/SKILL.md create mode 100644 .github/skills/astro-sites-manager/SKILL.md create mode 100644 .github/skills/astro-sites-manager/references/ai-dev-server.md create mode 100644 .github/skills/astro-sites-manager/references/coolify-deploy.md create mode 100644 .github/skills/astro-sites-manager/references/deployment.md create mode 100644 .github/skills/astro-sites-manager/references/install-mcp.md create mode 100644 .github/skills/astro-sites-manager/references/migration-v6-to-v7.md create mode 100644 .github/skills/astro-sites-manager/references/related-content.md create mode 100644 .github/skills/astro-sites-manager/references/seo-full-stack.md create mode 100644 .github/skills/astro-sites-manager/references/starlight-and-patterns.md create mode 100644 .github/skills/astro-sites-manager/references/testing.md create mode 100644 .github/skills/astro-sites-manager/references/v6-features.md create mode 100644 .github/skills/astro-sites-manager/references/v7-features.md create mode 100644 .github/skills/astro-sites-manager/references/validation-checklist.md create mode 100644 .github/skills/auth-md/SKILL.md create mode 100644 .github/skills/auth-md/references/example-auth-md.md create mode 100644 .github/skills/auth-md/references/implementation-guide.md create mode 100644 .github/skills/auth-md/references/metadata-schema.md create mode 100644 .github/skills/auth-md/references/protocol-template.md create mode 100644 .github/skills/auth-md/references/validation-rules.md create mode 100644 .github/skills/coolify-operator/.env.example create mode 100644 .github/skills/coolify-operator/SKILL.md create mode 100644 .github/skills/coolify-operator/evals/evals.json create mode 100644 .github/skills/design-md-validator/SKILL.md create mode 100644 .github/skills/design-md-validator/references/linting-rules.md create mode 100644 .github/skills/design-md-validator/references/token-schema.md create mode 100644 .github/skills/geo-optimization/SKILL.md create mode 100644 .github/skills/geo-optimization/references/guiding-principles.md create mode 100644 .github/skills/human-ai/EVALUATION.md create mode 100644 .github/skills/human-ai/SKILL.md create mode 100644 .github/skills/human-ai/references/patterns-composition.md create mode 100644 .github/skills/human-ai/references/patterns-content.md create mode 100644 .github/skills/human-ai/references/patterns-english-specific.md create mode 100644 .github/skills/human-ai/references/patterns-language.md create mode 100644 .github/skills/human-ai/references/patterns-style.md create mode 100644 .github/skills/human-ai/references/patterns-tone.md create mode 100644 .github/skills/human-ai/references/presets.md create mode 100644 .github/skills/human-ai/references/summary.md create mode 100644 .github/skills/human-ai/references/tests.md create mode 100644 .github/skills/human-ai/scripts/measure.py create mode 100644 .github/skills/humanizar/SKILL.md create mode 100644 .github/skills/humanizar/references/padroes-composicao.md create mode 100644 .github/skills/humanizar/references/padroes-conteudo.md create mode 100644 .github/skills/humanizar/references/padroes-estilo.md create mode 100644 .github/skills/humanizar/references/padroes-exclusivos-pt-br.md create mode 100644 .github/skills/humanizar/references/padroes-linguagem.md create mode 100644 .github/skills/humanizar/references/padroes-portugues-simplificado.md create mode 100644 .github/skills/humanizar/references/padroes-tom.md create mode 100644 .github/skills/loop-architect/LICENSE create mode 100644 .github/skills/loop-architect/SKILL.md create mode 100644 .github/skills/loop-architect/examples/ai-workflow-mapping/LOOP.md create mode 100644 .github/skills/loop-architect/examples/ai-workflow-mapping/README.md create mode 100644 .github/skills/loop-architect/examples/ai-workflow-mapping/RUN_IN_SESSION.md create mode 100644 .github/skills/loop-architect/examples/ai-workflow-mapping/inputs/process-notes.md create mode 100644 .github/skills/loop-architect/examples/ai-workflow-mapping/loop.resolved.json create mode 100644 .github/skills/loop-architect/examples/ai-workflow-mapping/loop.yaml create mode 100644 .github/skills/loop-architect/examples/ai-workflow-mapping/run-loop.py create mode 100644 .github/skills/loop-architect/examples/ai-workflow-mapping/scripts/check-loop-doc.py create mode 100644 .github/skills/loop-architect/references/control-rubric.md create mode 100644 .github/skills/loop-architect/references/council-rubric.md create mode 100644 .github/skills/loop-architect/references/goal-rubric.md create mode 100644 .github/skills/loop-architect/references/model-detection.md create mode 100644 .github/skills/loop-architect/references/verification-rubric.md create mode 100644 .github/skills/loop-architect/schemas/loop.resolved.v1.schema.json create mode 100644 .github/skills/loop-architect/schemas/loop.v1.schema.json create mode 100644 .github/skills/loop-architect/scripts/looper.py create mode 100644 .github/skills/loop-architect/templates/run-loop.py create mode 100644 .github/skills/okf-open-knowledge-format/SKILL.md create mode 100644 .github/skills/okf-open-knowledge-format/references/conversion.md create mode 100644 .github/skills/okf-open-knowledge-format/references/examples.md create mode 100644 .github/skills/okf-open-knowledge-format/references/spec-v01.md create mode 100644 .github/skills/okf-open-knowledge-format/scripts/validate.sh create mode 100644 .github/skills/pier-cloud/.env.example create mode 100644 .github/skills/pier-cloud/SKILL.md create mode 100644 .github/skills/pier-cloud/references/TROUBLESHOOTING.md create mode 100644 .github/skills/pier-cloud/scripts/README.md create mode 100644 .github/skills/pier-cloud/scripts/pier-cloud-auth.py create mode 100644 .github/skills/pier-cloud/scripts/pier-cloud-get-all-workspaces.py create mode 100644 .github/skills/pier-cloud/scripts/pier-cloud-get-view-data.py create mode 100644 .github/skills/pier-cloud/scripts/pier-cloud-get-view.py create mode 100644 .github/skills/pier-cloud/scripts/pier-cloud-get-workspace-group.py create mode 100644 .github/skills/pier-cloud/scripts/pier-cloud-get-workspace.py create mode 100644 .github/skills/pier-cloud/scripts/pier-cloud-list-contexts.py create mode 100644 .github/skills/pier-cloud/scripts/pier-cloud-list-views.py create mode 100644 .github/skills/pier-cloud/scripts/pier-cloud-list-workspace-groups.py create mode 100644 .github/skills/pier-cloud/scripts/pier-cloud-list-workspaces.py create mode 100644 .github/skills/pier-cloud/scripts/pier_cloud_client.py create mode 100644 .github/skills/ralph-loop-kiro-specs/SKILL.md create mode 100644 .github/skills/ralph-loop-kiro-specs/references/ralph-loop-kiro-specs-prompt.md create mode 100644 .github/skills/ralph-loop-kiro-specs/scripts/ralph-loop-kiro-specs-script.sh create mode 100644 .github/skills/resume-ats-beater/SKILL.md create mode 100644 .github/skills/resume-ats-beater/references/auditoria-linkedin.md create mode 100644 .github/skills/resume-ats-beater/references/diagnostico-ats.md create mode 100644 .github/skills/resume-ats-beater/references/diagnostico-avancado.md create mode 100644 .github/skills/resume-ats-beater/references/presets-formatos.md create mode 100644 .github/skills/resume-ats-beater/references/ssi.md create mode 100644 .github/skills/resume-ats-beater/references/template-saida.md create mode 100644 .github/skills/revenue-centric-design/LICENSE create mode 100644 .github/skills/revenue-centric-design/SKILL.md create mode 100644 .github/skills/revenue-centric-design/references/ai-era-differentiation.md create mode 100644 .github/skills/revenue-centric-design/references/audit-template.md create mode 100644 .github/skills/revenue-centric-design/references/behavioral-science-toolkit.md create mode 100644 .github/skills/revenue-centric-design/references/churn-and-retention.md create mode 100644 .github/skills/revenue-centric-design/references/conversion-and-landing-pages.md create mode 100644 .github/skills/revenue-centric-design/references/metrics-and-experimentation.md create mode 100644 .github/skills/revenue-centric-design/references/onboarding-and-activation.md create mode 100644 .github/skills/revenue-centric-design/references/positioning-icp-and-gtm.md create mode 100644 .github/skills/revenue-centric-design/references/pricing-and-monetization.md create mode 100644 .github/skills/revenue-centric-design/references/product-strategy-and-features.md create mode 100644 .github/skills/revenue-centric-design/references/revenue-centric-design.md create mode 100644 .github/skills/revenue-centric-design/scripts/check_usage_boundary.py create mode 100644 .github/skills/revenue-centric-design/scripts/revenue_math.py create mode 100644 .github/skills/security-specialist/SKILL.md create mode 100644 .github/skills/security-specialist/references/finding-format.md create mode 100644 .github/skills/security-specialist/references/report-format.md create mode 100644 .github/skills/security-specialist/references/report-schema.json create mode 100644 .github/skills/security-specialist/references/scan-artifacts.md create mode 100644 .github/skills/security-specialist/references/severity-policy.md create mode 100644 .github/skills/security-specialist/scripts/finalize.py create mode 100644 .github/skills/security-specialist/scripts/pentest.py create mode 100644 .github/skills/security-specialist/scripts/rank_files.py create mode 100644 .github/skills/security-specialist/scripts/scan_db.py create mode 100644 .github/skills/security-specialist/scripts/validate-findings.cjs create mode 100644 .github/skills/security-specialist/steering/attack-paths.md create mode 100644 .github/skills/security-specialist/steering/diff-review.md create mode 100644 .github/skills/security-specialist/steering/discovery.md create mode 100644 .github/skills/security-specialist/steering/full-scan.md create mode 100644 .github/skills/security-specialist/steering/hunting.md create mode 100644 .github/skills/security-specialist/steering/pentest.md create mode 100644 .github/skills/security-specialist/steering/remediation.md create mode 100644 .github/skills/security-specialist/steering/reporting.md create mode 100644 .github/skills/security-specialist/steering/threat-model.md create mode 100644 .github/skills/security-specialist/steering/tracking.md create mode 100644 .github/skills/security-specialist/steering/triage.md create mode 100644 .github/skills/security-specialist/steering/validation.md create mode 100644 .github/skills/skill-evaluation/SKILL.md create mode 100644 .github/skills/skill-evaluation/references/categories.md create mode 100644 .github/skills/skill-evaluation/references/mechanics.md create mode 100644 .github/skills/skill-evaluation/references/output-template.md create mode 100644 .github/skills/skill-evaluation/scripts/score.py create mode 100644 .github/skills/slop-eval/SKILL.md create mode 100644 .github/skills/slop-eval/references/output-template.md create mode 100644 .github/skills/slop-eval/references/premium-markers.md create mode 100644 .github/skills/slop-eval/references/tells.md create mode 100644 .github/skills/slop-eval/scripts/score.py create mode 100644 .github/skills/startup-idea/SKILL.md create mode 100644 .github/skills/startup-idea/evals/evals.json create mode 100644 .github/skills/substack-expert/SKILL.md create mode 100644 .github/skills/substack-expert/references/formatting-best-practices.md create mode 100644 .github/skills/substack-expert/references/seo-output-example.md create mode 100644 .github/skills/ultimate-design-system-master/SKILL.md create mode 100644 .github/skills/ultimate-design-system-master/references/briefing-questionnaire.md create mode 100644 .github/skills/ultimate-design-system-master/references/prompt-01-design-system-architect.md create mode 100644 .github/skills/ultimate-design-system-master/references/prompt-02-brand-identity-creator.md create mode 100644 .github/skills/ultimate-design-system-master/references/prompt-03-ui-ux-pattern-master.md create mode 100644 .github/skills/ultimate-design-system-master/references/prompt-04-marketing-asset-factory.md create mode 100644 .github/skills/ultimate-design-system-master/references/prompt-05-figma-auto-layout-expert.md create mode 100644 .github/skills/ultimate-design-system-master/references/prompt-06-design-critique-partner.md create mode 100644 .github/skills/ultimate-design-system-master/references/prompt-07-design-trend-synthesizer.md create mode 100644 .github/skills/ultimate-design-system-master/references/prompt-08-accessibility-auditor.md create mode 100644 .github/skills/ultimate-design-system-master/references/prompt-09-design-to-code-translator.md create mode 100644 .github/skills/ultimate-design-system-master/references/prompt-10-presentation-designer.md create mode 100644 .github/workflows/openwiki-update.yml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 openwiki/.last-update.json create mode 100644 openwiki/INSTRUCTIONS.md create mode 100644 openwiki/architecture/index.md create mode 100644 openwiki/architecture/overview.md create mode 100644 openwiki/domain/game-mechanics.md create mode 100644 openwiki/domain/index.md create mode 100644 openwiki/index.md create mode 100644 openwiki/operations/index.md create mode 100644 openwiki/operations/runbook.md create mode 100644 openwiki/quickstart.md create mode 100644 openwiki/source-map.md create mode 100644 openwiki/workflows/index.md create mode 100644 openwiki/workflows/slip-and-challenge.md diff --git a/.github/skills/agent-ready-cloudflare/README.md b/.github/skills/agent-ready-cloudflare/README.md new file mode 100644 index 0000000..4ef4457 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/README.md @@ -0,0 +1,338 @@ +# 🤖 Agent Ready — Cloudflare Scanner Skill + +Scan any website for AI agent readiness and get actionable fix prompts — powered by [isitagentready.com](https://isitagentready.com). + +This skill wraps the Cloudflare "Is It Agent Ready?" scanner into a reusable agent skill with full API documentation, 20 implementation sub-skills, and copy-paste prompts for every failing check. + +--- + +## What It Does + +Give it a domain. It scans 18 checks across 5 categories and tells you: + +- **What level** your site is at (0–5) +- **What's passing** and what's failing +- **How to fix** every failure — with a prompt you can paste into any coding agent +- **What to prioritize** to reach the next level + +``` +You: "Scan example.com for agent readiness" + +Agent: Scans via API → generates Markdown report → includes fix prompts for every failure +``` + +--- + +## Quick Start + +### 1. Scan a single domain + +```bash +curl -s -X POST 'https://isitagentready.com/api/scan' \ + -H 'Content-Type: application/json' \ + -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36' \ + -H 'Referer: https://isitagentready.com/' \ + -H 'Origin: https://isitagentready.com' \ + -d '{"url":"https://example.com/"}' +``` + +### 2. Get a markdown report (for LLMs) + +```bash +curl -s -X POST 'https://isitagentready.com/api/scan' \ + -H 'Content-Type: application/json' \ + -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36' \ + -H 'Referer: https://isitagentready.com/' \ + -H 'Origin: https://isitagentready.com' \ + -d '{"url":"https://example.com/","format":"agent"}' +``` + +### 3. Use the MCP server + +Connect your agent to the MCP endpoint: + +``` +https://isitagentready.com/mcp +``` + +Call the `scan_site` tool with `{"url": "https://example.com"}`. + +--- + +## The 18 Checks + +### Discoverability + +| # | Check | What passes | +|---|-------|-------------| +| 1 | **robots.txt** | `/robots.txt` returns 200 with `text/plain` and `User-agent` directives | +| 2 | **sitemap.xml** | `/sitemap.xml` returns valid XML, or `Sitemap:` directive in robots.txt | +| 3 | **Link headers** | Homepage `Link` headers include agent-useful relations (`api-catalog`, `service-desc`, etc.) | + +### Content + +| # | Check | What passes | +|---|-------|-------------| +| 4 | **Markdown for Agents** | `Accept: text/markdown` → response with `Content-Type: text/markdown` | + +### Bot Access Control + +| # | Check | What passes | +|---|-------|-------------| +| 5 | **AI bot rules** | robots.txt has `User-agent` entries for GPTBot, Claude-Web, Google-Extended, etc. | +| 6 | **Content Signals** | robots.txt has `Content-Signal:` directives (ai-train, search, ai-input) | +| 7 | **Web Bot Auth** | `/.well-known/http-message-signatures-directory` with valid JWKS *(informational)* | + +### API, Auth, MCP & Skill Discovery + +| # | Check | What passes | +|---|-------|-------------| +| 8 | **API Catalog** | `/.well-known/api-catalog` returns `application/linkset+json` (RFC 9727) | +| 9 | **OAuth/OIDC** | `/.well-known/openid-configuration` or `oauth-authorization-server` with valid metadata | +| 10 | **OAuth Protected Resource** | `/.well-known/oauth-protected-resource` with `resource` + `authorization_servers` (RFC 9728) | +| 11 | **MCP Server Card** | `/.well-known/mcp/server-card.json` with `serverInfo`, transport, capabilities (SEP-1649) | +| 12 | **A2A Agent Card** | `/.well-known/agent-card.json` with name, version, supportedInterfaces | +| 13 | **Agent Skills Index** | `/.well-known/agent-skills/index.json` with skills array (v0.2.0) | +| 14 | **WebMCP** | Page calls `navigator.modelContext.provideContext()` with tool definitions | + +### Commerce *(optional — scored only for e-commerce sites)* + +| # | Check | What passes | +|---|-------|-------------| +| 15 | **x402** | API routes return HTTP 402 with x402 payment headers | +| 16 | **UCP** | `/.well-known/ucp` with protocol_version and services | +| 17 | **ACP** | `/.well-known/acp.json` with protocol metadata | +| 18 | **AP2** | A2A Agent Card includes AP2 extension with role | + +--- + +## Level System + +``` +Level 0 Not Ready — Fails basic checks +Level 1 Basic Web Presence — 2 of 3: robots.txt, sitemap, link headers +Level 2 Bot-Aware — Level 1 + AI bot rules + Content Signals +Level 3 Agent-Readable — Level 2 + markdown content negotiation +Level 4 Agent-Integrated — Level 3 + 1 of: MCP card, A2A card, agent skills, API catalog +Level 5 Agent-Native — Level 4 + 2 of: Web Bot Auth, all integrations, auth metadata +``` + +--- + +## Example Output + +Scanning a site at Level 1 produces a report like: + +```markdown +# Agent Ready Scan — example.com + +> **Score:** Level 1 — Basic Web Presence +> **Scanned:** 2026-04-18T13:10:58Z +> **Link:** [View online](https://isitagentready.com/example.com) + +## Summary + +| Category | Score | +|-----------------------------------|-------| +| Discoverability | 2/3 | +| Content | 0/1 | +| Bot Access Control | 1/3 | +| API, Auth, MCP & Skill Discovery | 0/7 | + +## Details + +### Discoverability (2/3) + +- ✅ **robots.txt** — robots.txt exists with valid format +- ✅ **sitemap.xml** — sitemap.xml exists with valid structure +- ❌ **Link headers** — No Link headers found on homepage + +### Content (0/1) + +- ❌ **Markdown for Agents** — Site does not support Markdown for Agents + +(... more categories ...) + +## 🔧 How to Implement — Agent Prompts + +#### ❌ Link headers + +**Issue:** No Link headers found on homepage + +​``` +Goal: Include Link response headers for agent discovery (RFC 8288) + +Issue: No Link headers found on homepage + +Fix: Add Link response headers to your homepage that point agents to useful +resources. For example: Link: ; rel="api-catalog" + +Skill: https://isitagentready.com/.well-known/agent-skills/link-headers/SKILL.md + +Docs: https://www.rfc-editor.org/rfc/rfc8288 +​``` + +> 📖 Reference: [link-headers/SKILL.md](link-headers/SKILL.md) + +(... one block per failing check ...) + +## Next Level + +**Level 2 — Bot-Aware** + +To reach the next level, implement: +- Content Signals in robots.txt +``` + +--- + +## Fix Prompts + +Every failing check gets a prompt block you can copy-paste into any coding agent (Cursor, Copilot, Claude, etc.). The format matches the isitagentready.com web UI: + +``` +Goal: +Issue: +Fix: +Skill: +Docs: +``` + +The SKILL.md contains templates for all 20 checks. The `{issue}` placeholder is replaced with the actual message from the API response. + +--- + +## Batch Scanning + +Scan multiple domains with a 2-second delay between requests: + +```python +import json, time, urllib.request + +HEADERS = { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ...", + "Referer": "https://isitagentready.com/", + "Origin": "https://isitagentready.com" +} + +domains = ["example.com", "another.com", "third.com"] + +for domain in domains: + body = json.dumps({"url": f"https://{domain}/"}).encode() + req = urllib.request.Request( + "https://isitagentready.com/api/scan", + data=body, headers=HEADERS + ) + with urllib.request.urlopen(req, timeout=90) as resp: + data = json.loads(resp.read()) + + level = data["level"] + name = data["levelName"] + print(f"{domain}: Level {level} — {name}") + time.sleep(2) +``` + +--- + +## API Response Shape + +```json +{ + "url": "https://example.com", + "scannedAt": "2026-04-18T13:10:58.788Z", + "level": 1, + "levelName": "Basic Web Presence", + "isCommerce": false, + "checks": { + "discoverability": { + "robotsTxt": { + "status": "pass", + "message": "robots.txt exists with valid format", + "evidence": [ ... ], + "durationMs": 42 + } + } + }, + "nextLevel": { + "target": 2, + "name": "Bot-Aware", + "requirements": [ + { + "check": "contentSignals", + "description": "...", + "prompt": "...", + "skillUrl": "...", + "specUrls": ["..."] + } + ] + } +} +``` + +| Status | Meaning | +|--------|---------| +| `pass` | ✅ Check passed | +| `fail` | ❌ Action needed | +| `neutral` | ⬜ Not applicable / informational | + +--- + +## Skill Structure + +``` +agent-ready-cloudflare/ +├── README.md ← You are here +├── SKILL.md ← Main skill (operational flow, API docs, prompt templates) +│ +├── scan-site/SKILL.md ← Meta: scan API + MCP server docs +│ +│ Discoverability +├── robots-txt/SKILL.md ← Implement robots.txt (RFC 9309) +├── sitemap/SKILL.md ← Implement sitemap.xml +├── link-headers/SKILL.md ← Link response headers (RFC 8288) +├── llms-txt/SKILL.md ← Publish /llms.txt +├── llms-full-txt/SKILL.md ← Publish /llms-full.txt +│ +│ Content +├── markdown-negotiation/SKILL.md ← Accept: text/markdown negotiation +│ +│ Bot Access Control +├── ai-rules/SKILL.md ← AI bot User-agent rules +├── content-signals/SKILL.md ← Content-Signal directives +├── web-bot-auth/SKILL.md ← Web Bot Auth (JWKS) +│ +│ API, Auth, MCP & Skill Discovery +├── api-catalog/SKILL.md ← API Catalog (RFC 9727) +├── oauth-discovery/SKILL.md ← OAuth/OIDC discovery (RFC 8414) +├── oauth-protected-resource/SKILL.md ← Protected Resource Metadata (RFC 9728) +├── mcp-server-card/SKILL.md ← MCP Server Card (SEP-1649) +├── a2a-agent-card/SKILL.md ← A2A Agent Card (Google A2A) +├── agent-skills/SKILL.md ← Agent Skills Discovery Index +├── webmcp/SKILL.md ← WebMCP browser API +│ +│ Commerce +├── x402/SKILL.md ← x402 payment protocol +├── ucp/SKILL.md ← Universal Commerce Protocol +└── acp/SKILL.md ← Agent Commerce Protocol +``` + +**21 files** — 1 main skill + 20 implementation sub-skills. + +--- + +## Sources + +| Resource | URL | +|----------|-----| +| Scanner | https://isitagentready.com | +| API endpoint | `POST https://isitagentready.com/api/scan` | +| MCP server | `https://isitagentready.com/mcp` | +| API Catalog | https://isitagentready.com/.well-known/api-catalog | +| Agent Skills Index | https://isitagentready.com/.well-known/agent-skills/index.json | +| MCP Server Card | https://isitagentready.com/.well-known/mcp/server-card.json | +| Full docs (llms-full.txt) | https://isitagentready.com/llms-full.txt | +| Cloudflare Agents | https://developers.cloudflare.com/agents/ | +| MCP Protocol | https://modelcontextprotocol.io/ | +| Content Signals | https://contentsignals.org/ | +| Agent Skills Discovery | https://github.com/cloudflare/agent-skills-discovery-rfc | diff --git a/.github/skills/agent-ready-cloudflare/SKILL.md b/.github/skills/agent-ready-cloudflare/SKILL.md new file mode 100644 index 0000000..f8fed64 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/SKILL.md @@ -0,0 +1,783 @@ +--- +name: agent-ready-cloudflare +description: > + Audit and improve website readiness for AI agents using the Cloudflare + "Is It Agent Ready?" scanner (isitagentready.com). Covers scanning via API, + interpreting results, generating implementation prompts, and fixing every check. + Use when the user mentions "agent ready", "isitagentready", "AI agent scan", + "agent readiness", "agent-ready score", "MCP server card", "agent skills index", + "markdown for agents", "content signals", "web bot auth", "agent discovery", + "RFC 9727", "RFC 8288", "RFC 9728", "SEP-1649", "WebMCP", "x402", "UCP", "ACP", + or wants to make a website discoverable and usable by AI agents. +metadata: + author: Cloudflare / isitagentready.com + version: "3.0" + date: 2026-06-03 + source: https://isitagentready.com + category: product-verification +--- + +# Agent Ready — Cloudflare Scanner + +Audit any website for AI agent readiness, generate actionable fix prompts, and +implement improvements to increase the agent-ready score. + +--- + +## 0. Operational Flow + +Follow this flow every time this skill is activated: + +### Step 1 — Get the domain + +If the user did not provide a domain, ask: + +> Which domain do you want to scan on isitagentready.com? + +### Step 2 — Scan via API + +```bash +curl -s -X POST 'https://isitagentready.com/api/scan' \ + -H 'Content-Type: application/json' \ + -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36' \ + -H 'Referer: https://isitagentready.com/' \ + -H 'Origin: https://isitagentready.com' \ + -d '{"url":"https://DOMAIN/","enabledChecks":["robotsTxt","sitemap","linkHeaders","dnsAid","markdownNegotiation","robotsTxtAiRules","contentSignals","webBotAuth","apiCatalog","oauthDiscovery","oauthProtectedResource","authMd","mcpServerCard","a2aAgentCard","agentSkills","webMcp","x402","mpp","ucp","acp","ap2"]}' +``` + +Replace `DOMAIN` with the target domain. + +### Step 3 — Generate the Markdown report + +Use the API response to build a report with this structure: + +```markdown +# Agent Ready Scan — {domain} + +> **Score:** Level {level} — {levelName} +> **Scanned:** {scannedAt} +> **Link:** [View online](https://isitagentready.com/{domain}) + +## Summary + +| Category | Score | +|----------|-------| +| Discoverability | {passed}/{total} | +| Content | {passed}/{total} | +| Bot Access Control | {passed}/{total} | +| API, Auth, MCP & Skill Discovery | {passed}/{total} | +| Commerce (Optional) | {passed}/{total} | + +## Details + +### Discoverability ({passed}/{total}) + +- ✅ **robots.txt** — {message} +- ❌ **sitemap.xml** — {message} + (... for each check ...) + +### (... repeat for each category ...) + +## 🔧 How to Implement — Agent Prompts + +(... one block per failing check, see Step 4 ...) + +## Next Level + +**Level {nextLevel.target} — {nextLevel.name}** + +To reach the next level, implement: +- {nextLevel.requirements[].description} +``` + +### Step 4 — Generate "How to Implement" prompts + +For every check with `status: "fail"` or `status: "neutral"`, generate a prompt +block using the **Prompt Templates** in Section 8 below. The prompt combines: + +1. The **Goal** and **Fix** from the template (static per check) +2. The **Issue** from the API response (`checks.{category}.{check}.message`) +3. The **Skill URL** pointing to the sub-skill +4. The **Docs** links to the relevant RFCs/specs + +Format each prompt as a fenced code block the user can copy-paste into a coding agent: + +````markdown +#### ❌ {check name} + +**Issue:** {message from API} + +``` +Goal: {goal from template} + +Issue: {message from API} + +Fix: {fix from template} + +Skill: https://isitagentready.com/.well-known/agent-skills/{skill-folder}/SKILL.md + +Docs: {docs URLs from template} +``` + +> 📖 Reference: [{skill-folder}/SKILL.md]({skill-folder}/SKILL.md) +```` + +### Step 5 — Deliver + +Present the full Markdown report to the user. If they want to save it to a file, +write it to the requested path. + +--- + +## 1. What It Checks + +### Discoverability (4 checks) +| Check | API Key | Pass criteria | +|-------|---------|---------------| +| robots.txt | `robotsTxt` | Returns 200 with `text/plain` containing at least one `User-agent` directive | +| sitemap.xml | `sitemap` | `/sitemap.xml` returns valid XML, or `Sitemap` directive found in robots.txt | +| Link headers | `linkHeaders` | Homepage includes `Link` headers with agent-useful relations (`service-desc`, `api-catalog`, etc.) | +| DNS-AID | `dnsAid` | SVCB/HTTPS records found under `_agents` namespace via DNS-over-HTTPS (Cloudflare → Google fallback) | + +### Content (1 check) +| Check | API Key | Pass criteria | +|-------|---------|---------------| +| Markdown for Agents | `markdownNegotiation` | `Accept: text/markdown` returns `Content-Type: text/markdown` | + +### Bot Access Control (3 checks) +| Check | API Key | Pass criteria | +|-------|---------|---------------| +| AI bot rules | `robotsTxtAiRules` | robots.txt contains `User-agent` entries for known AI bots (GPTBot, Claude-Web, Google-Extended, etc.) | +| Content Signals | `contentSignals` | robots.txt contains `Content-Signal` directives with ai-train/search/ai-input | +| Web Bot Auth | `webBotAuth` | `/.well-known/http-message-signatures-directory` exists with valid JWKS (informational — neutral does not affect score) | + +### API, Auth, MCP & Skill Discovery (8 checks) +| Check | API Key | Pass criteria | +|-------|---------|---------------| +| API Catalog | `apiCatalog` | `/.well-known/api-catalog` returns valid `linkset+json` with API entries | +| OAuth/OIDC | `oauthDiscovery` | `/.well-known/openid-configuration` or `oauth-authorization-server` with valid OAuth metadata | +| OAuth Protected Resource | `oauthProtectedResource` | `/.well-known/oauth-protected-resource` with `resource` and `authorization_servers` | +| Auth.md | `authMd` | `/auth.md` exists with valid H1 heading containing "auth.md"; optionally PRM + AS metadata | +| MCP Server Card | `mcpServerCard` | Valid card at `/.well-known/mcp/server-card.json`, `server-cards.json`, or `mcp.json` with `serverInfo.name` | +| A2A Agent Card | `a2aAgentCard` | `/.well-known/agent-card.json` with `name`, `version`, and `supportedInterfaces` | +| Agent Skills Index | `agentSkills` | `/.well-known/agent-skills/index.json` with valid `skills` array (legacy `/.well-known/skills/` also accepted) | +| WebMCP | `webMcp` | Page exposes MCP tools via `navigator.modelContext.provideContext()` | + +### Commerce — Optional (5 checks, scored only if e-commerce signals detected) +| Check | API Key | Pass criteria | +|-------|---------|---------------| +| x402 | `x402` | API routes return HTTP 402 with valid x402 payment headers | +| MPP | `mpp` | `/openapi.json` with `x-payment-info` extensions on payable operations (Machine Payment Protocol) | +| UCP | `ucp` | `/.well-known/ucp` with `protocol_version` and `services` | +| ACP | `acp` | `/.well-known/acp.json` with `protocol.name`, `api_base_url`, `transports`, `capabilities.services` | +| AP2 | `ap2` | A2A Agent Card includes AP2 extension with role information | + +--- + +## 2. Levels + +| Level | Name | Requirements | +|-------|------|-------------| +| 0 | Not Ready | Does not meet Level 1 criteria | +| 1 | Basic Web Presence | Pass 2 of 3: robots.txt, sitemap, link headers | +| 2 | Bot-Aware | Level 1 + both: AI bot rules AND Content Signals in robots.txt | +| 3 | Agent-Readable | Level 2 + markdown content negotiation | +| 4 | Agent-Integrated | Level 3 + 1 of 4: MCP Server Card, A2A Agent Card, Agent Skills, API Catalog | +| 5 | Agent-Native | Level 4 + 2 of 3: Web Bot Auth, all four integration checks, auth metadata (OAuth discovery or OAuth Protected Resource) | + +--- + +## 3. API Reference + +### Endpoint + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json +``` + +### Required Headers (Cloudflare protection) + +``` +User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 +Referer: https://isitagentready.com/ +Origin: https://isitagentready.com +``` + +### Request Body + +```json +{ + "url": "https://example.com/", + "enabledChecks": [ + "robotsTxt", "sitemap", "linkHeaders", "dnsAid", + "markdownNegotiation", + "robotsTxtAiRules", "contentSignals", "webBotAuth", + "apiCatalog", "oauthDiscovery", "oauthProtectedResource", "authMd", + "mcpServerCard", "a2aAgentCard", "agentSkills", "webMcp", + "x402", "mpp", "ucp", "acp", "ap2" + ] +} +``` + +All checks are optional — pass only the ones you want to run. + +Set `"format": "agent"` to get a markdown response with fix instructions instead +of JSON (useful for piping directly to an LLM). + +### MCP Server + +The scanner is also available as an MCP server: + +``` +https://isitagentready.com/mcp +``` + +Call the `scan_site` tool with a `url` parameter. See [scan-site/SKILL.md](scan-site/SKILL.md). + +### Discovered Endpoints (isitagentready.com itself) + +The scanner practices what it preaches. These are its own agent-ready endpoints: + +| Endpoint | Content | +|----------|---------| +| `/.well-known/api-catalog` | RFC 9727 linkset with scan API and MCP server entries | +| `/.well-known/mcp/server-card.json` | MCP Server Card (Streamable HTTP transport) | +| `/.well-known/mcp.json` | Same MCP Server Card (alternate path) | +| `/.well-known/agent-skills/index.json` | 23 skills in Agent Skills Discovery v0.2.0 format | +| `/llms.txt` | LLM-friendly overview of the scanner | +| `/llms-full.txt` | Full documentation — canonical reference for all 18 checks, pass criteria, and level system | +| `/api/health` | Health check (`{"status":"ok"}`) | +| `/mcp` | Streamable HTTP MCP server with `scan_site` tool | + +The API Catalog links the scan API to its documentation (`/llms-full.txt`), +its service description (`/.well-known/mcp/server-card.json`), and its health +endpoint (`/api/health`). + +### Response Structure + +```json +{ + "url": "https://example.com", + "scannedAt": "2026-04-18T13:10:58.788Z", + "level": 1, + "levelName": "Basic Web Presence", + "isCommerce": false, + "commerceSignals": [], + "nextLevel": { + "target": 2, + "name": "Bot-Aware", + "requirements": [ + { + "check": "contentSignals", + "description": "...", + "shortPrompt": "...", + "prompt": "Full implementation prompt...", + "specUrls": ["https://..."], + "skillUrl": "https://isitagentready.com/.well-known/agent-skills/.../SKILL.md" + } + ] + }, + "checks": { + "": { + "": { + "status": "pass|fail|neutral", + "message": "Human-readable conclusion", + "durationMs": 42, + "evidence": [ + { + "action": "fetch|parse|conclude", + "label": "GET /robots.txt", + "request": { "url": "...", "method": "GET" }, + "response": { + "status": 200, + "headers": { "content-type": "..." }, + "bodyPreview": "..." + }, + "finding": { + "outcome": "positive|negative|neutral", + "summary": "..." + } + } + ] + } + } + } +} +``` + +### Status Values + +| Status | Icon | Meaning | +|--------|------|---------| +| `pass` | ✅ | Check passed | +| `fail` | ❌ | Action needed | +| `neutral` | ⬜ | Not applicable / informational | + +--- + +## 4. Web Interface vs API + +| Feature | API | Web UI | +|---------|-----|--------| +| Score & Level | ✅ | ✅ | +| Check status + message | ✅ | ✅ | +| Evidence (audit details) | ✅ Full | ✅ Same | +| "How to implement" prompts | ⚠️ Only `nextLevel` (1 prompt) | ✅ All failing checks | +| Skill URLs | ⚠️ Only `nextLevel` | ✅ All checks | + +This is why this skill includes the full prompt templates below — to reconstruct +the web-quality prompts from API data. + +--- + +## 5. Batch Scanning + +For multiple domains, add a 2-second delay between requests. Write results +incrementally to avoid data loss: + +```python +import json, time, urllib.request + +HEADERS = { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 ...", + "Referer": "https://isitagentready.com/", + "Origin": "https://isitagentready.com" +} +CHECKS = ["robotsTxt","sitemap","linkHeaders","dnsAid","markdownNegotiation", + "robotsTxtAiRules","contentSignals","webBotAuth","apiCatalog", + "oauthDiscovery","oauthProtectedResource","authMd","mcpServerCard", + "a2aAgentCard","agentSkills","webMcp","x402","mpp","ucp","acp","ap2"] + +for domain in domains: + body = json.dumps({"url": f"https://{domain}/", "enabledChecks": CHECKS}).encode() + req = urllib.request.Request("https://isitagentready.com/api/scan", + data=body, headers=HEADERS) + with urllib.request.urlopen(req, timeout=90) as resp: + data = json.loads(resp.read()) + # process data... + time.sleep(2) +``` + +--- + +## 6. Sub-Skills (Implementation Guides) + +### Discoverability +- [robots-txt/SKILL.md](robots-txt/SKILL.md) — Publish `/robots.txt` (RFC 9309) +- [sitemap/SKILL.md](sitemap/SKILL.md) — Publish `/sitemap.xml` +- [link-headers/SKILL.md](link-headers/SKILL.md) — Add `Link` response headers (RFC 8288) +- [dns-aid/SKILL.md](dns-aid/SKILL.md) — Publish DNS-AID SVCB records for agent discovery (draft-mozleywilliams-dnsop-dnsaid) +- [llms-txt/SKILL.md](llms-txt/SKILL.md) — Publish `/llms.txt` (llmstxt.org) +- [llms-full-txt/SKILL.md](llms-full-txt/SKILL.md) — Publish `/llms-full.txt` + +### Content +- [markdown-negotiation/SKILL.md](markdown-negotiation/SKILL.md) — Return markdown on `Accept: text/markdown` + +### Bot Access Control +- [ai-rules/SKILL.md](ai-rules/SKILL.md) — AI bot `User-agent` rules in robots.txt +- [content-signals/SKILL.md](content-signals/SKILL.md) — `Content-Signal` directives +- [web-bot-auth/SKILL.md](web-bot-auth/SKILL.md) — JWKS for request signing + +### API, Auth, MCP & Skill Discovery +- [api-catalog/SKILL.md](api-catalog/SKILL.md) — API Catalog (RFC 9727) +- [oauth-discovery/SKILL.md](oauth-discovery/SKILL.md) — OAuth/OIDC discovery (RFC 8414) +- [oauth-protected-resource/SKILL.md](oauth-protected-resource/SKILL.md) — Protected Resource Metadata (RFC 9728) +- [auth-md/SKILL.md](auth-md/SKILL.md) — Auth.md agent registration discovery (auth-md.com) +- [mcp-server-card/SKILL.md](mcp-server-card/SKILL.md) — MCP Server Card (SEP-1649) +- [a2a-agent-card/SKILL.md](a2a-agent-card/SKILL.md) — A2A Agent Card (Google A2A Protocol) +- [agent-skills/SKILL.md](agent-skills/SKILL.md) — Agent Skills Discovery Index +- [webmcp/SKILL.md](webmcp/SKILL.md) — WebMCP browser API + +### Commerce (Optional) +- [x402/SKILL.md](x402/SKILL.md) — x402 HTTP payment protocol +- [mpp/SKILL.md](mpp/SKILL.md) — Machine Payment Protocol (mpp.dev) +- [ucp/SKILL.md](ucp/SKILL.md) — Universal Commerce Protocol +- [acp/SKILL.md](acp/SKILL.md) — Agent Commerce Protocol + +### Meta +- [scan-site/SKILL.md](scan-site/SKILL.md) — Scan any site for agent readiness (includes MCP server endpoint) + +--- + +## 7. Priority Order for Maximum Impact + +1. `robots.txt` + `sitemap.xml` → Level 1 +2. `Content Signals` in robots.txt → Level 2 +3. `Markdown for Agents` + `Link headers` → toward Level 3 +4. `MCP Server Card` + `Agent Skills Index` → toward Level 4 +5. `OAuth discovery` + `API Catalog` → full agent interoperability + +--- + +## 8. Prompt Templates per Check + +These templates replicate the "How to implement — paste into your coding agent" +prompts from the web UI. When generating the report (Step 4), use the `{issue}` +placeholder with the actual `message` from the API response. + +### `robotsTxt` + +``` +Goal: Publish /robots.txt with clear crawl rules + +Issue: {issue} + +Fix: Create /robots.txt at the site root with explicit User-agent directives and allow/disallow rules for key paths. Ensure it is plain text and returns 200. + +Skill: https://isitagentready.com/.well-known/agent-skills/robots-txt/SKILL.md + +Docs: https://www.rfc-editor.org/rfc/rfc9309 +``` + +Sub-skill: [robots-txt/SKILL.md](robots-txt/SKILL.md) + +### `sitemap` + +``` +Goal: Publish /sitemap.xml with canonical URLs + +Issue: {issue} + +Fix: Generate /sitemap.xml listing canonical URLs, keep it updated on publish, and reference it from /robots.txt. + +Skill: https://isitagentready.com/.well-known/agent-skills/sitemap/SKILL.md + +Docs: https://www.sitemaps.org/protocol.html +``` + +Sub-skill: [sitemap/SKILL.md](sitemap/SKILL.md) + +### `linkHeaders` + +``` +Goal: Include Link response headers for agent discovery (RFC 8288) + +Issue: {issue} + +Fix: Add Link response headers to your homepage that point agents to useful resources. For example: Link: ; rel="api-catalog" to advertise your API catalog, or Link: ; rel="service-doc" for API documentation. See RFC 8288 for the Link header format and IANA Link Relations for registered relation types. + +Skill: https://isitagentready.com/.well-known/agent-skills/link-headers/SKILL.md + +Docs: https://www.rfc-editor.org/rfc/rfc8288, https://www.rfc-editor.org/rfc/rfc9727#section-3 +``` + +Sub-skill: [link-headers/SKILL.md](link-headers/SKILL.md) + +### `markdownNegotiation` + +``` +Goal: Return HTML responses as markdown when agents request it + +Issue: {issue} + +Fix: Enable Markdown for Agents so requests with Accept: text/markdown return a markdown version of your HTML response while HTML stays the default for browsers. Confirm the response uses Content-Type: text/markdown (and x-markdown-tokens if available). + +Skill: https://isitagentready.com/.well-known/agent-skills/markdown-negotiation/SKILL.md + +Docs: https://developers.cloudflare.com/fundamentals/reference/markdown-for-agents/ +``` + +Sub-skill: [markdown-negotiation/SKILL.md](markdown-negotiation/SKILL.md) + +### `robotsTxtAiRules` + +``` +Goal: Add User-agent rules for AI crawlers like GPTBot, Claude-Web, and others + +Issue: {issue} + +Fix: Add explicit User-agent entries for AI crawlers (GPTBot, OAI-SearchBot, Claude-Web, Google-Extended) with allow/disallow rules that match your policy. + +Skill: https://isitagentready.com/.well-known/agent-skills/ai-rules/SKILL.md + +Docs: https://www.rfc-editor.org/rfc/rfc9309, https://developers.cloudflare.com/ai-crawl-control/ +``` + +Sub-skill: [ai-rules/SKILL.md](ai-rules/SKILL.md) + +### `contentSignals` + +``` +Goal: Declare AI content usage preferences with Content Signals in robots.txt + +Issue: {issue} + +Fix: Add Content-Signal directives to your robots.txt declaring preferences for ai-train, search, and ai-input. For example: +Content-Signal: ai-train=no, search=yes, ai-input=no + +Skill: https://isitagentready.com/.well-known/agent-skills/content-signals/SKILL.md + +Docs: https://contentsignals.org/, https://datatracker.ietf.org/doc/draft-romm-aipref-contentsignals/ +``` + +Sub-skill: [content-signals/SKILL.md](content-signals/SKILL.md) + +### `webBotAuth` + +``` +Goal: Let your site identify itself as a bot with Web Bot Auth + +Issue: {issue} + +Fix: Publish a JWKS at /.well-known/http-message-signatures-directory so your site can identify itself when it sends bot or agent requests. Receiving sites can use it to verify those signed requests. + +Skill: https://isitagentready.com/.well-known/agent-skills/web-bot-auth/SKILL.md + +Docs: https://datatracker.ietf.org/wg/webbotauth/about/, https://developers.cloudflare.com/bots/reference/bot-verification/web-bot-auth/ +``` + +Sub-skill: [web-bot-auth/SKILL.md](web-bot-auth/SKILL.md) + +### `apiCatalog` + +``` +Goal: Publish an API catalog for automated API discovery (RFC 9727) + +Issue: {issue} + +Fix: Create /.well-known/api-catalog returning application/linkset+json with a "linkset" array. Each entry should include an "anchor" URL for the API and link relations for service-desc (OpenAPI spec), service-doc (documentation), and status (health endpoint). See RFC 9727 Appendix A for examples. + +Skill: https://isitagentready.com/.well-known/agent-skills/api-catalog/SKILL.md + +Docs: https://www.rfc-editor.org/rfc/rfc9727, https://www.rfc-editor.org/rfc/rfc9264 +``` + +Sub-skill: [api-catalog/SKILL.md](api-catalog/SKILL.md) + +### `oauthDiscovery` + +``` +Goal: Publish OAuth/OIDC discovery metadata so agents can authenticate with your APIs + +Issue: {issue} + +Fix: If your site has protected APIs, publish /.well-known/openid-configuration (for OpenID Connect) or /.well-known/oauth-authorization-server (for pure OAuth 2.0) with your issuer, authorization_endpoint, token_endpoint, jwks_uri, and grant_types_supported. This allows AI agents to programmatically discover how to authenticate. + +Skill: https://isitagentready.com/.well-known/agent-skills/oauth-discovery/SKILL.md + +Docs: http://openid.net/specs/openid-connect-discovery-1_0.html, https://www.rfc-editor.org/rfc/rfc8414 +``` + +Sub-skill: [oauth-discovery/SKILL.md](oauth-discovery/SKILL.md) + +### `oauthProtectedResource` + +``` +Goal: Publish OAuth Protected Resource Metadata so agents can discover how to authenticate + +Issue: {issue} + +Fix: Publish /.well-known/oauth-protected-resource with your resource identifier, authorization_servers (list of OAuth/OIDC issuer URLs that can issue tokens for this resource), and scopes_supported. This tells agents how to obtain access tokens for your protected APIs. + +Skill: https://isitagentready.com/.well-known/agent-skills/oauth-protected-resource/SKILL.md + +Docs: https://www.rfc-editor.org/rfc/rfc9728 +``` + +Sub-skill: [oauth-protected-resource/SKILL.md](oauth-protected-resource/SKILL.md) + +### `mcpServerCard` + +``` +Goal: Publish an MCP Server Card for agent discovery + +Issue: {issue} + +Fix: Serve an MCP Server Card (SEP-1649) at /.well-known/mcp/server-card.json with serverInfo (name, version), transport endpoint, and capabilities. The schema is being standardized at https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127 + +Skill: https://isitagentready.com/.well-known/agent-skills/mcp-server-card/SKILL.md + +Docs: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127 +``` + +Sub-skill: [mcp-server-card/SKILL.md](mcp-server-card/SKILL.md) + +### `agentSkills` + +``` +Goal: Publish an agent skills discovery index + +Issue: {issue} + +Fix: Publish a skills discovery index at /.well-known/agent-skills/index.json (per the Agent Skills Discovery RFC v0.2.0). Include a $schema field, and a skills array where each entry has name, type, description, url, and a sha256 digest. + +Skill: https://isitagentready.com/.well-known/agent-skills/agent-skills/SKILL.md + +Docs: https://github.com/cloudflare/agent-skills-discovery-rfc, https://agentskills.io/ +``` + +Sub-skill: [agent-skills/SKILL.md](agent-skills/SKILL.md) + +### `webMcp` + +``` +Goal: Support WebMCP to expose site tools to AI agents via the browser + +Issue: {issue} + +Fix: Implement the WebMCP API by calling navigator.modelContext.provideContext() with tool definitions that expose your site's key actions to AI agents. Each tool needs a name, description, inputSchema (JSON Schema), and an execute callback function. + +Skill: https://isitagentready.com/.well-known/agent-skills/webmcp/SKILL.md + +Docs: https://webmachinelearning.github.io/webmcp/, https://developer.chrome.com/blog/webmcp-epp +``` + +Sub-skill: [webmcp/SKILL.md](webmcp/SKILL.md) + +### `x402` + +``` +Goal: Support x402 protocol for agent-native HTTP payments + +Issue: {issue} + +Fix: Add x402 payment middleware to your API routes to enable AI agents to pay for access via HTTP. Use @x402/express, @x402/hono, or @x402/next middleware with a facilitator URL and wallet address. Protected routes will return HTTP 402 with payment requirements that agents can fulfill automatically. + +Skill: https://isitagentready.com/.well-known/agent-skills/x402/SKILL.md + +Docs: https://x402.org, https://github.com/coinbase/x402, https://docs.x402.org +``` + +Sub-skill: [x402/SKILL.md](x402/SKILL.md) + +### `ucp` + +``` +Goal: Enable content payments via Universal Commerce Protocol + +Issue: {issue} + +Fix: Serve /.well-known/ucp with protocol version, services, capabilities, and endpoints, and ensure spec URLs and schemas are reachable. + +Skill: https://isitagentready.com/.well-known/agent-skills/ucp/SKILL.md + +Docs: https://ucp.dev/specification/overview/ +``` + +Sub-skill: [ucp/SKILL.md](ucp/SKILL.md) + +### `acp` + +``` +Goal: Publish ACP discovery metadata so agents can discover your commerce API + +Issue: {issue} + +Fix: Serve /.well-known/acp.json at the origin root with protocol.name "acp", protocol.version, api_base_url, supported transports, and capabilities.services so agents can discover your ACP implementation without creating a checkout session first. + +Skill: https://isitagentready.com/.well-known/agent-skills/acp/SKILL.md + +Docs: https://agenticcommerce.dev, https://github.com/agentic-commerce-protocol/agentic-commerce-protocol/pull/137 +``` + +Sub-skill: [acp/SKILL.md](acp/SKILL.md) + +### `a2aAgentCard` + +``` +Goal: Publish an A2A Agent Card for agent-to-agent discovery + +Issue: {issue} + +Fix: Serve JSON at /.well-known/agent-card.json with name, version, description, supportedInterfaces (service URL and transport), capabilities, and skills (each with id, name, description). See the A2A Protocol Specification for the full schema. + +Skill: https://isitagentready.com/.well-known/agent-skills/a2a-agent-card/SKILL.md + +Docs: https://a2a-protocol.org/latest/specification/, https://a2a-protocol.org/latest/topics/agent-discovery/ +``` + +Sub-skill: [a2a-agent-card/SKILL.md](a2a-agent-card/SKILL.md) + +### `dnsAid` + +``` +Goal: Publish DNS for AI Discovery (DNS-AID) records for DNS-based agent discovery + +Issue: {issue} + +Fix: Publish SVCB or HTTPS records under your domain's _agents namespace (e.g. _a2a._agents.example.com or _index._agents.example.com). Use alpn and port SvcParamKeys with mandatory=alpn,port. Use numeric keyNNNNN names for experimental custom parameters until registered. Sign zones with DNSSEC. + +Skill: https://isitagentready.com/.well-known/agent-skills/dns-aid/SKILL.md + +Docs: https://datatracker.ietf.org/doc/draft-mozleywilliams-dnsop-dnsaid/, https://www.rfc-editor.org/info/rfc9460 +``` + +Sub-skill: [dns-aid/SKILL.md](dns-aid/SKILL.md) + +### `authMd` + +``` +Goal: Publish Auth.md agent registration discovery metadata + +Issue: {issue} + +Fix: Serve /auth.md from the site root as Markdown with an H1 heading containing "auth.md". Include OAuth Protected Resource Metadata at /.well-known/oauth-protected-resource and Authorization Server metadata. Add an agent_auth block with skill, register_uri, and registration methods. If OAuth is not available, keep /auth.md self-contained with audience, registration endpoints, and credential use. + +Skill: https://isitagentready.com/.well-known/agent-skills/auth-md/SKILL.md + +Docs: https://auth-md.com +``` + +Sub-skill: [auth-md/SKILL.md](auth-md/SKILL.md) + +### `mpp` + +``` +Goal: Support MPP (Machine Payment Protocol) for agent-native HTTP payments + +Issue: {issue} + +Fix: Serve /openapi.json at the site root with HTTP 200. Include x-payment-info extensions on payable operations declaring intent (charge or session), method (tempo, stripe, lightning, card), and amount. Optionally include currency, description, and top-level x-service-info with categories. + +Skill: https://isitagentready.com/.well-known/agent-skills/mpp/SKILL.md + +Docs: https://mpp.dev, https://paymentauth.org/draft-payment-discovery-00.txt +``` + +Sub-skill: [mpp/SKILL.md](mpp/SKILL.md) + +### `llmsTxt` + +``` +Goal: Publish an LLM-friendly overview at /llms.txt + +Issue: {issue} + +Fix: Serve /llms.txt as plain text (UTF-8) with HTTP 200. Start with an H1 title line, include a short summary paragraph, and link to the most important content sections for agents. Optionally link to /llms-full.txt for expanded content. + +Skill: https://isitagentready.com/.well-known/agent-skills/llms-txt/SKILL.md + +Docs: https://llmstxt.org/ +``` + +Sub-skill: [llms-txt/SKILL.md](llms-txt/SKILL.md) + +### `llmsFullTxt` + +``` +Goal: Publish expanded LLM content at /llms-full.txt + +Issue: {issue} + +Fix: Serve /llms-full.txt as plain text (UTF-8) with HTTP 200. Include structured, detailed content suitable for LLM ingestion covering key topics, APIs, and documentation. Link to it from your /llms.txt file. + +Skill: https://isitagentready.com/.well-known/agent-skills/llms-full-txt/SKILL.md + +Docs: https://llmstxt.org/ +``` + +Sub-skill: [llms-full-txt/SKILL.md](llms-full-txt/SKILL.md) + +### `ap2` + +``` +Goal: Declare AP2 support in your A2A Agent Card for agent payments + +Issue: {issue} + +Fix: Add AP2 extension to your A2A Agent Card at /.well-known/agent-card.json with your role (merchant, shopper, etc.) so agents can discover your payment capabilities. + +Docs: https://ap2-protocol.org/ +``` diff --git a/.github/skills/agent-ready-cloudflare/a2a-agent-card/SKILL.md b/.github/skills/agent-ready-cloudflare/a2a-agent-card/SKILL.md new file mode 100644 index 0000000..a4baad1 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/a2a-agent-card/SKILL.md @@ -0,0 +1,35 @@ +--- +name: agent-ready-a2a-agent-card +description: > + Sub-skill de agent-ready-cloudflare: Implement A2A Agent Card +--- +# Implement A2A Agent Card + +Publish an A2A Agent Card for agent-to-agent discovery per the +[A2A Protocol Specification](https://a2a-protocol.org/latest/specification/). + +## Requirements + +- Serve JSON at `/.well-known/agent-card.json` with HTTP 200 +- Include `name`, `version`, and `description` +- Include `supportedInterfaces` with service URL and transport protocol +- List `capabilities` and `skills` (each with `id`, `name`, `description`) + +See [Agent Discovery](https://a2a-protocol.org/latest/topics/agent-discovery/) +for the full schema. + +## Cloudflare + +[Agents SDK](https://developers.cloudflare.com/agents/) supports building +A2A-compatible agents on Workers. + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discovery.a2aAgentCard.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/acp/SKILL.md b/.github/skills/agent-ready-cloudflare/acp/SKILL.md new file mode 100644 index 0000000..81e3c01 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/acp/SKILL.md @@ -0,0 +1,28 @@ +--- +name: agent-ready-acp +description: > + Sub-skill de agent-ready-cloudflare: Implement ACP Discovery Document +--- +# Implement ACP Discovery Document + +Publish an ACP discovery document so AI agents can discover your +[Agentic Commerce Protocol](https://agenticcommerce.dev) implementation. + +## Requirements + +- Serve JSON at `/.well-known/acp.json` with HTTP 200 +- Include `protocol.name` set to `"acp"` and `protocol.version` +- Include `api_base_url` as an absolute HTTP(S) URL +- Include `transports` as a non-empty array of supported transport types +- Include `capabilities.services` as a non-empty array of offered services + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.commerce.acp.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/agent-skills/SKILL.md b/.github/skills/agent-ready-cloudflare/agent-skills/SKILL.md new file mode 100644 index 0000000..4508d33 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/agent-skills/SKILL.md @@ -0,0 +1,31 @@ +--- +name: agent-ready-agent-skills +description: > + Sub-skill de agent-ready-cloudflare: Implement Agent Skills Discovery Index +--- +# Implement Agent Skills Discovery Index + +Publish a skills discovery document per the +[Agent Skills Discovery RFC](https://github.com/cloudflare/agent-skills-discovery-rfc) v0.2.0. + +## Requirements + +- Serve JSON at `/.well-known/agent-skills/index.json` with HTTP 200 +- Include a `$schema` field set to `https://schemas.agentskills.io/discovery/0.2.0/schema.json` +- Include a `skills` array where each entry has: + - `name` — skill identifier (lowercase alphanumeric + hyphens) + - `type` — `"skill-md"` (single SKILL.md) or `"archive"` (bundled archive) + - `description` — brief description of what the skill does + - `url` — URL to the skill artifact (SKILL.md file or archive) + - `digest` — SHA-256 hash of the artifact (`sha256:{hex}`) + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discovery.agentSkills.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/ai-rules/SKILL.md b/.github/skills/agent-ready-cloudflare/ai-rules/SKILL.md new file mode 100644 index 0000000..c3ec65b --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/ai-rules/SKILL.md @@ -0,0 +1,31 @@ +--- +name: agent-ready-ai-rules +description: > + Sub-skill de agent-ready-cloudflare: Implement AI Bot Rules in robots.txt +--- +# Implement AI Bot Rules in robots.txt + +Add explicit `User-agent` entries for AI crawlers in your robots.txt per +[RFC 9309](https://www.rfc-editor.org/rfc/rfc9309). + +## Requirements + +- Add `User-agent` blocks for AI-specific bots: `GPTBot`, `OAI-SearchBot`, `Claude-Web`, `Google-Extended`, `Amazonbot`, `anthropic-ai`, `Bytespider`, `CCBot`, `Applebot-Extended` +- Set `Allow` and/or `Disallow` rules matching your content policy +- A wildcard `User-agent: *` block alone is not sufficient — explicit AI bot entries are required + +## Cloudflare + +[AI Crawl Control](https://developers.cloudflare.com/ai-crawl-control/) +lets you manage AI bot rules from the dashboard without editing robots.txt manually. + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.botAccessControl.robotsTxtAiRules.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/api-catalog/SKILL.md b/.github/skills/agent-ready-cloudflare/api-catalog/SKILL.md new file mode 100644 index 0000000..46b3b2a --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/api-catalog/SKILL.md @@ -0,0 +1,27 @@ +--- +name: agent-ready-api-catalog +description: > + Sub-skill de agent-ready-cloudflare: Implement API Catalog +--- +# Implement API Catalog + +Publish an API catalog for automated discovery per +[RFC 9727](https://www.rfc-editor.org/rfc/rfc9727). + +## Requirements + +- Serve `/.well-known/api-catalog` with `Content-Type: application/linkset+json` and HTTP 200 +- Include a `linkset` array with entries for each API +- Each entry needs an `anchor` URL and link relations: `service-desc` (OpenAPI spec), `service-doc` (docs), and optionally `status` (health endpoint) +- See [RFC 9727 Appendix A](https://www.rfc-editor.org/rfc/rfc9727#appendix-A) for examples + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discovery.apiCatalog.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/auth-md/SKILL.md b/.github/skills/agent-ready-cloudflare/auth-md/SKILL.md new file mode 100644 index 0000000..9cc36e5 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/auth-md/SKILL.md @@ -0,0 +1,43 @@ +--- +name: agent-ready-auth-md +description: > + Sub-skill de agent-ready-cloudflare: Skill: Implement Auth.md Agent Registration Discovery +--- +# Skill: Implement Auth.md Agent Registration Discovery + +## What This Skill Does + +Helps a service publish Auth.md support for agent registration. Use this when a scanner reports the `authMd` check is failing or when adding the Auth.md standard to an API or application. + +## Requirements + +- Serve `/auth.md` from the service root as Markdown with an H1 heading that contains `auth.md` (for example, `# auth.md` or `# Example auth.md`). +- Prefer publishing OAuth Protected Resource Metadata at `/.well-known/oauth-protected-resource` for the resource server. +- Include `resource`, `authorization_servers`, `scopes_supported`, and `bearer_methods_supported` with `header` in the PRM document. +- Publish OAuth Authorization Server metadata at each advertised authorization server's `/.well-known/oauth-authorization-server` URL. +- Include a valid `issuer` in Authorization Server metadata and ensure it matches the issuer advertised in PRM. +- Add an `agent_auth` block with `skill`, `register_uri`, and at least one complete registration method when Authorization Server metadata is available. +- If OAuth metadata is not available, keep `/auth.md` self-contained: identify the agent audience, document registration or provisioning endpoint(s), list supported method(s), and explain credential use. + +## Flow Metadata + +- **ID-JAG**: include `identity_types_supported: ["identity_assertion"]`, `identity_assertion.assertion_types_supported` with `urn:ietf:params:oauth:token-type:id-jag`, and credential types. Include `revocation_uri` and the revocation event in `events_supported` when supported. +- **Verified email**: include `identity_assertion.assertion_types_supported` with `verified_email`, credential types, and `claim_uri`. +- **Anonymous**: include `identity_types_supported: ["anonymous"]`, `anonymous.credential_types_supported`, and `claim_uri`. + +## Validate + +```http +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discovery.authMd.status` is `"pass"`. + +## References + +- [Auth.md Specification](https://auth-md.com) +- [RFC 9728 — OAuth Protected Resource Metadata](https://www.rfc-editor.org/rfc/rfc9728) +- [RFC 8414 — OAuth Authorization Server Metadata](https://www.rfc-editor.org/rfc/rfc8414) diff --git a/.github/skills/agent-ready-cloudflare/content-signals/SKILL.md b/.github/skills/agent-ready-cloudflare/content-signals/SKILL.md new file mode 100644 index 0000000..4454e9d --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/content-signals/SKILL.md @@ -0,0 +1,32 @@ +--- +name: agent-ready-content-signals +description: > + Sub-skill de agent-ready-cloudflare: Implement Content Signals +--- +# Implement Content Signals + +Declare AI content usage preferences in your robots.txt using +[Content Signals](https://contentsignals.org/) +([IETF draft](https://datatracker.ietf.org/doc/draft-romm-aipref-contentsignals/)). + +## Requirements + +- Add `Content-Signal` directives to your robots.txt under the relevant `User-agent` block +- Declare preferences for `ai-train`, `search`, and `ai-input` +- Example: `Content-Signal: ai-train=no, search=yes, ai-input=no` + +## Cloudflare + +[AI Crawl Control](https://developers.cloudflare.com/ai-crawl-control/) +supports Content Signals configuration from the dashboard. + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.botAccessControl.contentSignals.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/dns-aid/SKILL.md b/.github/skills/agent-ready-cloudflare/dns-aid/SKILL.md new file mode 100644 index 0000000..98b517d --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/dns-aid/SKILL.md @@ -0,0 +1,39 @@ +--- +name: agent-ready-dns-aid +description: > + Sub-skill de agent-ready-cloudflare: Implement DNS for AI Discovery (DNS-AID) +--- +# Implement DNS for AI Discovery (DNS-AID) + +Publish DNS for AI Discovery (DNS-AID) records so agents can discover your agent endpoints through DNS. + +## Requirements + +- Publish DNS for AI Discovery (DNS-AID) records under your domain's `_agents` namespace, such as `_index._agents.example.com` or `_a2a._agents.example.com` +- Use ServiceMode `SVCB` records, or `HTTPS` records for HTTPS endpoints, with `alpn` and endpoint connection parameters +- Use numeric `keyNNNNN` SvcParamKey names for experimental DNS for AI Discovery (DNS-AID) custom parameters until they are registered +- Sign public DNS for AI Discovery (DNS-AID) discovery zones with DNSSEC so validating resolvers return authenticated data + +## Example + +```dns +_a2a._agents.example.com. 3600 IN SVCB 1 agent.example.com. alpn="a2a" port=443 mandatory=alpn,port +``` + +## Validate + +The scanner validates DNS for AI Discovery (DNS-AID) via DNS-over-HTTPS. By default, the scanner uses Cloudflare's `https://cloudflare-dns.com/dns-query` with automatic fallback to `https://dns.google/resolve` on resolver-level failures. Library callers can override the resolver with `ScanOptions.dohResolverUrl` (disables fallback). + +```http +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discoverability.dnsAid.status` is `"pass"`. + +## References + +- [draft-mozleywilliams-dnsop-dnsaid](https://datatracker.ietf.org/doc/draft-mozleywilliams-dnsop-dnsaid/) +- [RFC 9460 — SVCB and HTTPS Resource Records](https://www.rfc-editor.org/info/rfc9460) diff --git a/.github/skills/agent-ready-cloudflare/link-headers/SKILL.md b/.github/skills/agent-ready-cloudflare/link-headers/SKILL.md new file mode 100644 index 0000000..67b2d79 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/link-headers/SKILL.md @@ -0,0 +1,34 @@ +--- +name: agent-ready-link-headers +description: > + Sub-skill de agent-ready-cloudflare: Implement Link Response Headers +--- +# Implement Link Response Headers + +Add Link response headers to your homepage for agent discovery per +[RFC 8288](https://www.rfc-editor.org/rfc/rfc8288) and +[RFC 9727 Section 3](https://www.rfc-editor.org/rfc/rfc9727#section-3). + +## Requirements + +- Return `Link` headers on your homepage response pointing to machine-readable resources +- Use registered relation types: `api-catalog`, `service-desc`, `service-doc`, `describedby` +- Example: `Link: ; rel="api-catalog"` +- Multiple Link headers or comma-separated values are both valid + +## Cloudflare + +Use [Transform Rules](https://developers.cloudflare.com/rules/transform/) or +[Workers](https://developers.cloudflare.com/workers/) to add Link headers +without modifying your origin server. + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discoverability.linkHeaders.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/llms-full-txt/SKILL.md b/.github/skills/agent-ready-cloudflare/llms-full-txt/SKILL.md new file mode 100644 index 0000000..f609cbc --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/llms-full-txt/SKILL.md @@ -0,0 +1,27 @@ +--- +name: agent-ready-llms-full-txt +description: > + Sub-skill de agent-ready-cloudflare: Implement llms-full.txt +--- +# Implement llms-full.txt + +Publish expanded LLM content per +[llmstxt.org](https://llmstxt.org/). + +## Requirements + +- Serve `/llms-full.txt` as plain text (UTF-8) with HTTP 200 +- Include structured, detailed content suitable for LLM ingestion +- Cover the key topics, APIs, and documentation from your site +- Link to it from your `/llms.txt` file + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Confirm your deployed site serves `/llms-full.txt` exactly as intended. diff --git a/.github/skills/agent-ready-cloudflare/llms-txt/SKILL.md b/.github/skills/agent-ready-cloudflare/llms-txt/SKILL.md new file mode 100644 index 0000000..cfb0108 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/llms-txt/SKILL.md @@ -0,0 +1,28 @@ +--- +name: agent-ready-llms-txt +description: > + Sub-skill de agent-ready-cloudflare: Implement llms.txt +--- +# Implement llms.txt + +Publish an LLM-friendly overview of your site per +[llmstxt.org](https://llmstxt.org/). + +## Requirements + +- Serve `/llms.txt` as plain text (UTF-8) with HTTP 200 +- Start with an H1 (`# Site Name`) title line +- Include a short summary paragraph describing your site +- Link to the most important content sections for agents +- Optionally link to `/llms-full.txt` for expanded content + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Confirm your deployed site serves `/llms.txt` exactly as intended. diff --git a/.github/skills/agent-ready-cloudflare/markdown-negotiation/SKILL.md b/.github/skills/agent-ready-cloudflare/markdown-negotiation/SKILL.md new file mode 100644 index 0000000..825eaca --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/markdown-negotiation/SKILL.md @@ -0,0 +1,34 @@ +--- +name: agent-ready-markdown-negotiation +description: > + Sub-skill de agent-ready-cloudflare: Implement Markdown Content Negotiation +--- +# Implement Markdown Content Negotiation + +Support `Accept: text/markdown` content negotiation so agents can request +markdown versions of your pages. +See [llmstxt.org](https://llmstxt.org/) and +[Markdown for Agents](https://developers.cloudflare.com/fundamentals/reference/markdown-for-agents/). + +## Requirements + +- When a request includes `Accept: text/markdown`, return a markdown representation of the page +- Set `Content-Type: text/markdown` on the response +- HTML remains the default for requests without the markdown accept header +- Include an `x-markdown-tokens` header with the token count if available + +## Cloudflare + +[Markdown for Agents](https://developers.cloudflare.com/fundamentals/reference/markdown-for-agents/) +enables this automatically for Cloudflare zones — no application code changes needed. + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.contentAccessibility.markdownNegotiation.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/mcp-server-card/SKILL.md b/.github/skills/agent-ready-cloudflare/mcp-server-card/SKILL.md new file mode 100644 index 0000000..cf98cbd --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/mcp-server-card/SKILL.md @@ -0,0 +1,33 @@ +--- +name: agent-ready-mcp-server-card +description: > + Sub-skill de agent-ready-cloudflare: Implement MCP Server Card +--- +# Implement MCP Server Card + +Publish an MCP Server Card for agent discovery per +[SEP-1649](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127). + +## Requirements + +- Serve JSON at `/.well-known/mcp/server-card.json` with HTTP 200 +- Include `serverInfo` with `name` and `version` +- Include a transport `endpoint` URL (e.g., `/mcp` for Streamable HTTP) +- List `capabilities` (tools, resources, prompts) the server supports + +## Cloudflare + +[Agents SDK](https://developers.cloudflare.com/agents/) and +[Workers](https://developers.cloudflare.com/workers/) make it straightforward +to build and deploy MCP servers with server card support. + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discovery.mcpServerCard.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/mpp/SKILL.md b/.github/skills/agent-ready-cloudflare/mpp/SKILL.md new file mode 100644 index 0000000..d2e3f9f --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/mpp/SKILL.md @@ -0,0 +1,56 @@ +--- +name: agent-ready-mpp +description: > + Sub-skill de agent-ready-cloudflare: Implement MPP Payment Discovery +--- +# Implement MPP Payment Discovery + +Publish an OpenAPI document with MPP payment discovery metadata so AI agents +can discover your payable endpoints via the +[Machine Payment Protocol](https://mpp.dev) +([spec](https://paymentauth.org/draft-payment-discovery-00.txt)). + +## Requirements + +- Serve `/openapi.json` at the site root with HTTP 200 +- Include `x-payment-info` extensions on payable operations +- Each `x-payment-info` must declare `intent` (charge or session), `method` (tempo, stripe, lightning, card), and `amount` +- Optionally include `currency`, `description`, and top-level `x-service-info` with categories + +## Example + +```json +{ + "openapi": "3.1.0", + "info": { "title": "My API", "version": "1.0" }, + "paths": { + "/api/generate": { + "post": { + "x-payment-info": { + "intent": "charge", + "method": "stripe", + "amount": "0.01", + "currency": "USD", + "description": "Generate content" + } + } + } + } +} +``` + +## Validate + +```http +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.commerce.mpp.status` is `"pass"`. + +## References + +- [Machine Payment Protocol](https://mpp.dev) +- [Payment Discovery Spec](https://paymentauth.org/draft-payment-discovery-00.txt) diff --git a/.github/skills/agent-ready-cloudflare/oauth-discovery/SKILL.md b/.github/skills/agent-ready-cloudflare/oauth-discovery/SKILL.md new file mode 100644 index 0000000..19fea91 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/oauth-discovery/SKILL.md @@ -0,0 +1,33 @@ +--- +name: agent-ready-oauth-discovery +description: > + Sub-skill de agent-ready-cloudflare: Implement OAuth/OIDC Discovery +--- +# Implement OAuth/OIDC Discovery + +Publish OAuth or OpenID Connect discovery metadata so agents can authenticate +with your APIs. +See [OpenID Connect Discovery](http://openid.net/specs/openid-connect-discovery-1_0.html) +and [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414). + +## Requirements + +- Serve JSON at `/.well-known/openid-configuration` (OIDC) or `/.well-known/oauth-authorization-server` (OAuth 2.0) +- Include `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri` +- List `grant_types_supported` and `response_types_supported` + +## Cloudflare + +[Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/identity/) +can serve as an identity provider, or use Workers to proxy discovery metadata. + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discovery.oauthDiscovery.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/oauth-protected-resource/SKILL.md b/.github/skills/agent-ready-cloudflare/oauth-protected-resource/SKILL.md new file mode 100644 index 0000000..0d73ed8 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/oauth-protected-resource/SKILL.md @@ -0,0 +1,34 @@ +--- +name: agent-ready-oauth-protected-resource +description: > + Sub-skill de agent-ready-cloudflare: Implement OAuth Protected Resource Metadata +--- +# Implement OAuth Protected Resource Metadata + +Publish OAuth Protected Resource Metadata so agents can discover how to +authenticate per [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728). + +## Requirements + +- Serve JSON at `/.well-known/oauth-protected-resource` with HTTP 200 +- Include `resource` (your resource identifier URL) +- Include `authorization_servers` (array of OAuth/OIDC issuer URLs) +- Optionally include `scopes_supported` +- Optionally return `WWW-Authenticate` with `resource_metadata` on 401 responses + +## Cloudflare + +Use [Workers](https://developers.cloudflare.com/workers/) to serve the +metadata endpoint and [Access](https://developers.cloudflare.com/cloudflare-one/) +as the authorization server. + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discovery.oauthProtectedResource.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/robots-txt/SKILL.md b/.github/skills/agent-ready-cloudflare/robots-txt/SKILL.md new file mode 100644 index 0000000..0b18c42 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/robots-txt/SKILL.md @@ -0,0 +1,31 @@ +--- +name: agent-ready-robots-txt +description: > + Sub-skill de agent-ready-cloudflare: Implement robots.txt +--- +# Implement robots.txt + +Publish a valid robots.txt at your site root per +[RFC 9309](https://www.rfc-editor.org/rfc/rfc9309). + +## Requirements + +- Serve `/robots.txt` as `text/plain` with HTTP 200 +- Include `User-agent` directives with `Allow`/`Disallow` rules +- Reference your sitemap if one exists: `Sitemap: https://example.com/sitemap.xml` + +## Cloudflare + +[AI Crawl Control](https://developers.cloudflare.com/ai-crawl-control/) +can manage your robots.txt from the dashboard, including AI-specific bot rules. + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discoverability.robotsTxt.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/scan-site/SKILL.md b/.github/skills/agent-ready-cloudflare/scan-site/SKILL.md new file mode 100644 index 0000000..3bee7c2 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/scan-site/SKILL.md @@ -0,0 +1,73 @@ +--- +name: agent-ready-scan-site +description: > + Sub-skill de agent-ready-cloudflare: Skill: Scan Site for Agent Readiness +--- +# Skill: Scan Site for Agent Readiness + +## What This Skill Does + +Scans any website URL and checks whether it implements the standards and protocols that make it accessible to AI agents. Checks 18 standards across 5 categories: discoverability, content accessibility, bot access control, API/auth/MCP discovery, and commerce. Returns a readiness level (0-5), the status of all 18 checks, and fix instructions for any failing checks. + +## When to Use It + +- Checking whether a site supports agent protocols (MCP, robots.txt, Link headers, etc.) +- Debugging why a site is missing specific agent-readiness checks +- Getting actionable fix instructions to improve a site's agent readiness +- Comparing agent readiness across multiple sites + +## How to Call It + +Send a POST request to the scan API: + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://example.com", "format": "agent"} +``` + +### Parameters + +- `url` (required): The URL to scan. Must be a valid HTTP or HTTPS URL. +- `format` (optional): Set to `"agent"` for a markdown response with fix instructions, or `"json"` (default) for structured data. + +### Using the MCP Server + +Alternatively, connect to the MCP server at: + +``` +https://isitagentready.com/mcp +``` + +Call the `scan_site` tool with a `url` parameter. + +## How to Interpret Results + +### JSON Format + +The response includes: + +- `level`: Integer 0-5 indicating overall readiness +- `levelName`: Human-readable level name (e.g., "Agent-Readable") +- `checks`: Object with 5 categories, each containing individual check results +- `nextLevel`: What's needed to reach the next level +- Each check has `status` ("pass", "fail", or "unableToCheck") and a `message` + +### Agent Format + +Returns a markdown document listing: +- The site's current score (e.g., "3/5 Agent-Readable") +- All failing checks with descriptions and fix instructions +- If all checks pass, a confirmation message + +### Level Scale + +| Level | Name | Key Requirements | +|-------|------|-----------------| +| 0 | Not Ready | Fewer than 2 of: robots.txt, sitemap, Link headers | +| 1 | Basic Web Presence | 2 of 3: robots.txt, sitemap, Link headers | +| 2 | Bot-Aware | Level 1 + both: AI bot rules in robots.txt, Content Signals | +| 3 | Agent-Readable | Level 2 + markdown content negotiation | +| 4 | Agent-Integrated | Level 3 + 1 of 4: MCP Server Card, agent skills, API catalog, WebMCP | +| 5 | Agent-Native | Level 4 + 2 of 4: Web Bot Auth, all integrations, commerce (UCP/x402), auth metadata | diff --git a/.github/skills/agent-ready-cloudflare/sitemap/SKILL.md b/.github/skills/agent-ready-cloudflare/sitemap/SKILL.md new file mode 100644 index 0000000..b09f2f7 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/sitemap/SKILL.md @@ -0,0 +1,27 @@ +--- +name: agent-ready-sitemap +description: > + Sub-skill de agent-ready-cloudflare: Implement sitemap.xml +--- +# Implement sitemap.xml + +Publish an XML sitemap at your site root per the +[Sitemaps protocol](https://www.sitemaps.org/protocol.html). + +## Requirements + +- Serve `/sitemap.xml` as valid XML with HTTP 200 +- List canonical `` entries for your public pages +- Keep it updated when content is published or removed +- Reference it from robots.txt: `Sitemap: https://example.com/sitemap.xml` + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discoverability.sitemap.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/ucp/SKILL.md b/.github/skills/agent-ready-cloudflare/ucp/SKILL.md new file mode 100644 index 0000000..e244efa --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/ucp/SKILL.md @@ -0,0 +1,26 @@ +--- +name: agent-ready-ucp +description: > + Sub-skill de agent-ready-cloudflare: Implement Universal Commerce Protocol (UCP) +--- +# Implement Universal Commerce Protocol (UCP) + +Enable content payments via the Universal Commerce Protocol per the +[UCP Specification](https://ucp.dev/specification/overview/). + +## Requirements + +- Serve JSON at `/.well-known/ucp` with HTTP 200 +- Include `protocol_version`, `services`, `capabilities`, and `endpoints` +- Ensure referenced spec URLs and schemas are reachable + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.commerce.ucp.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/web-bot-auth/SKILL.md b/.github/skills/agent-ready-cloudflare/web-bot-auth/SKILL.md new file mode 100644 index 0000000..faff490 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/web-bot-auth/SKILL.md @@ -0,0 +1,32 @@ +--- +name: agent-ready-web-bot-auth +description: > + Sub-skill de agent-ready-cloudflare: Implement Web Bot Auth +--- +# Implement Web Bot Auth + +Use Web Bot Auth so your site can identify itself when it sends bot or agent requests, per the +[IETF WebBotAuth WG](https://datatracker.ietf.org/wg/webbotauth/about/). + +## Requirements + +- Publish a JWKS (JSON Web Key Set) at `/.well-known/http-message-signatures-directory` +- The JWKS must contain at least one public key for signature verification +- Sign requests sent by your bot or agent so receiving sites can verify them +- Include `Signature-Agent` and `Signature-Input` headers on those signed requests + +## Cloudflare + +[Web Bot Auth on Cloudflare](https://developers.cloudflare.com/bots/reference/bot-verification/web-bot-auth/) +provides built-in support for verifying bot request signatures. + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.botAccessControl.webBotAuth.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/webmcp/SKILL.md b/.github/skills/agent-ready-cloudflare/webmcp/SKILL.md new file mode 100644 index 0000000..0f0f0c4 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/webmcp/SKILL.md @@ -0,0 +1,29 @@ +--- +name: agent-ready-webmcp +description: > + Sub-skill de agent-ready-cloudflare: Implement WebMCP +--- +# Implement WebMCP + +Expose site tools to AI agents via the browser using the +[WebMCP API](https://webmachinelearning.github.io/webmcp/) +([Chrome blog](https://developer.chrome.com/blog/webmcp-epp)). + +## Requirements + +- Call `navigator.modelContext.registerTool()` for each tool you want to expose +- Each tool needs `name`, `description`, `inputSchema` (JSON Schema), and an `execute` callback +- Tools should expose your site's key actions (search, navigation, data retrieval) +- Use an `AbortController` signal to unregister tools when no longer needed +- The API is detected via browser rendering — ensure the script runs on page load + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.discovery.webMcp.status` is `"pass"`. diff --git a/.github/skills/agent-ready-cloudflare/x402/SKILL.md b/.github/skills/agent-ready-cloudflare/x402/SKILL.md new file mode 100644 index 0000000..678b541 --- /dev/null +++ b/.github/skills/agent-ready-cloudflare/x402/SKILL.md @@ -0,0 +1,28 @@ +--- +name: agent-ready-x402 +description: > + Sub-skill de agent-ready-cloudflare: Implement x402 Payment Protocol +--- +# Implement x402 Payment Protocol + +Support agent-native HTTP payments via the +[x402 protocol](https://x402.org) +([docs](https://docs.x402.org), [GitHub](https://github.com/coinbase/x402)). + +## Requirements + +- Add x402 payment middleware to your API routes +- Use `@x402/express`, `@x402/hono`, or `@x402/next` middleware +- Configure a facilitator URL and wallet address +- Protected routes return HTTP 402 with payment requirements that agents fulfill automatically + +## Validate + +``` +POST https://isitagentready.com/api/scan +Content-Type: application/json + +{"url": "https://YOUR-SITE.com"} +``` + +Check that `checks.commerce.x402.status` is `"pass"`. diff --git a/.github/skills/astro-sites-manager/SKILL.md b/.github/skills/astro-sites-manager/SKILL.md new file mode 100644 index 0000000..eb5dd65 --- /dev/null +++ b/.github/skills/astro-sites-manager/SKILL.md @@ -0,0 +1,251 @@ +--- +name: astro-sites-manager +description: > + Comprehensive skill for building, migrating, and maintaining Astro v7 + projects. Covers best practices from the official AGENTS.md, the v6→v7 + migration path, validation of breaking/deprecated patterns, AI-enhanced dev + server usage (background mode, JSON logging), advanced routing with + src/fetch.ts, route caching, Sätteri Markdown, and the Rust compiler. Use + when the user mentions 'Astro', '.astro files', 'astro dev', 'astro build', + 'islands architecture', 'content collections', 'SSG', 'SSR adapter', + 'upgrade to Astro 7', 'migrate Astro', 'Astro v7', 'Astro v6', 'Sätteri', + 'route caching', 'Astro.cache', 'astro dev --background', 'src/fetch.ts', + 'advanced routing', 'Hono + Astro', 'Rolldown', 'Vite 8', 'queued + rendering', 'CDN cache provider', 'Astro AI', 'related content', + 'related posts', 'vector embeddings Astro', 'astro-related-content', + or asks about static site generation with Astro. +metadata: + author: ft.ia.br + version: "1.0.0" + date: 2026-06-22 + repository: https://github.com/fabricioctelles/skills + license: Apache-2.0 + category: ci-cd-and-deployment +--- + +# Astro Framework — v7 + +## MCP Documentation Access + +This skill works alongside the **Astro Docs MCP server**. Before answering Astro questions, check if the `astro-docs` MCP tool is available and query it for the latest documentation. The MCP server provides real-time access to docs.astro.build and is the single source of truth for current APIs. + +``` +MCP Server: astro-docs +Tool: search_astro_docs +``` + +If the MCP server is unavailable, fall back to the reference material in this skill and https://docs.astro.build. + +--- + +## Best Practices + +### Component Design +- One `.astro` component per file. Keep components small and focused. +- Use frontmatter (`---`) for data fetching and logic; template below for markup only. +- Prefer Astro components over framework components unless client interactivity is needed. +- Use `client:*` directives sparingly — each adds JavaScript to the bundle. +- Directive hierarchy: `client:idle` > `client:visible` > `client:load` (prefer lazy). + +### Routing & Pages +- Use file-based routing in `src/pages/`. Dynamic routes: `[slug].astro`, `[...path].astro`. +- Always export `getStaticPaths()` for prerendered dynamic routes. +- For SSR pages: `export const prerender = false` at the top. +- Use `src/fetch.ts` (v7) only when you need control beyond middleware — don't use it for simple auth. + +### Content Collections +- Define schemas in `content.config.ts` with Zod — never trust untyped content. +- Use `getCollection()` for lists, `getEntry()` for single items. +- Prefer `glob()` loader for local files, custom loaders for CMS data. + +### Performance +- Default to static (`prerender = true`). Use SSR only for personalized/dynamic content. +- Use `` from `astro:assets` — never raw `` for local images. +- Prefer Sätteri (default v7) over unified for Markdown — it's significantly faster. +- Use Server Islands (`server:defer`) for mixing static shells with dynamic fragments. + +### Styling +- Scoped ` +``` + +> **Note:** Starlight includes a built-in theme toggle. This pattern is for custom Astro sites. diff --git a/.github/skills/astro-sites-manager/references/testing.md b/.github/skills/astro-sites-manager/references/testing.md new file mode 100644 index 0000000..6e90f08 --- /dev/null +++ b/.github/skills/astro-sites-manager/references/testing.md @@ -0,0 +1,387 @@ +# Testing Astro Projects + +Complete guide for testing Astro projects — from unit/component tests to E2E, link checking, type safety, and CI pipelines. + +--- + +## 1. Component Testing with Vitest + +### Setup + +```bash +npm install -D vitest @vitest/ui +``` + +### vitest.config.ts + +```ts +/// +import { getViteConfig } from 'astro/config'; + +export default getViteConfig({ + test: { + include: ['tests/**/*.{test,spec}.{js,ts}'], + }, +}); +``` + +### AstroContainer API + +The `AstroContainer` API renders Astro components in isolation without a full dev server. + +```ts +import { experimental_AstroContainer as AstroContainer } from 'astro/container'; +import { expect, test } from 'vitest'; +import Greeting from '../src/components/Greeting.astro'; + +test('renders greeting with name prop', async () => { + const container = await AstroContainer.create(); + const result = await container.renderToString(Greeting, { + props: { name: 'World' }, + }); + + expect(result).toContain('Hello, World'); +}); +``` + +### Testing Props + +```ts +test('renders default when no name provided', async () => { + const container = await AstroContainer.create(); + const result = await container.renderToString(Greeting, { + props: {}, + }); + + expect(result).toContain('Hello, stranger'); +}); +``` + +### Testing Slots + +```ts +import Card from '../src/components/Card.astro'; + +test('renders slot content', async () => { + const container = await AstroContainer.create(); + const result = await container.renderToString(Card, { + slots: { default: '

Slot content here

' }, + }); + + expect(result).toContain('Slot content here'); +}); +``` + +### Testing Conditional Rendering + +```ts +import Alert from '../src/components/Alert.astro'; + +test('renders error variant', async () => { + const container = await AstroContainer.create(); + const result = await container.renderToString(Alert, { + props: { type: 'error', message: 'Something failed' }, + }); + + expect(result).toContain('class="alert-error"'); + expect(result).toContain('Something failed'); +}); + +test('does not render when hidden', async () => { + const container = await AstroContainer.create(); + const result = await container.renderToString(Alert, { + props: { type: 'info', message: 'Hidden', visible: false }, + }); + + expect(result).not.toContain('Hidden'); +}); +``` + +### Run Tests + +```bash +npx vitest +npx vitest --ui # browser UI +``` + +--- + +## 2. E2E Testing with Playwright + +### Setup + +```bash +npm install -D @playwright/test +npx playwright install +``` + +### playwright.config.ts + +```ts +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + webServer: { + command: 'npm run preview', + port: 4321, + reuseExistingServer: !process.env.CI, + }, + use: { + baseURL: 'http://localhost:4321', + }, +}); +``` + +> **Note:** Run `astro build` before E2E tests so `preview` has something to serve. + +### Example: Page Load + +```ts +import { test, expect } from '@playwright/test'; + +test('homepage loads correctly', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveTitle(/My Site/); + await expect(page.locator('h1')).toBeVisible(); +}); +``` + +### Example: Navigation + +```ts +test('navigates to about page', async ({ page }) => { + await page.goto('/'); + await page.click('a[href="/about"]'); + await expect(page).toHaveURL('/about'); + await expect(page.locator('h1')).toContainText('About'); +}); +``` + +### Example: Dynamic Routes + +```ts +test('blog post renders from content collection', async ({ page }) => { + await page.goto('/blog/first-post'); + await expect(page.locator('article h1')).toBeVisible(); + await expect(page.locator('article')).not.toBeEmpty(); +}); +``` + +### Testing View Transitions + +```ts +test('view transitions work between pages', async ({ page }) => { + await page.goto('/'); + const transitionPromise = page.waitForEvent('load'); + await page.click('a[href="/about"]'); + await transitionPromise; + await expect(page).toHaveURL('/about'); +}); +``` + +### Run E2E Tests + +```bash +npx astro build +npx playwright test +npx playwright test --ui # interactive mode +``` + +--- + +## 3. Link Checking + +### linkinator + +Checks all links in the built output for broken references. + +```bash +npx astro build +npx linkinator dist --recurse +``` + +Options: + +```bash +npx linkinator dist --recurse --skip "^https://external-site.com" +``` + +### CI Integration (GitHub Actions) + +```yaml +- name: Check links + run: npx linkinator dist --recurse --retry --retry-errors +``` + +--- + +## 4. Type Checking + +### Astro Template Validation + +```bash +npx astro check +``` + +Validates `.astro` files for type errors in expressions, prop types, and component usage. + +### TypeScript Checking + +```bash +npx tsc --noEmit +``` + +Validates all `.ts` and `.tsx` files without emitting output. + +### package.json Scripts + +```json +{ + "scripts": { + "check": "astro check && tsc --noEmit" + } +} +``` + +--- + +## 5. Content Collection Validation + +### Schema Enforcement + +Content collections validate against Zod schemas at build time. Invalid content **fails the build automatically**: + +```ts +// src/content.config.ts +import { defineCollection, z } from 'astro:content'; + +const blog = defineCollection({ + type: 'content', + schema: z.object({ + title: z.string(), + date: z.date(), + draft: z.boolean().default(false), + }), +}); + +export const collections = { blog }; +``` + +A frontmatter error produces: + +``` +[ERROR] blog → "bad-post.md" frontmatter does not match schema. + "title" is required. +``` + +### Draft Filtering + +Filter drafts in production queries: + +```astro +--- +import { getCollection } from 'astro:content'; + +const posts = await getCollection('blog', ({ data }) => { + return import.meta.env.PROD ? !data.draft : true; +}); +--- +``` + +Test that drafts are excluded by checking the built output does not contain draft post URLs. + +--- + +## 6. Pre-Deploy Verification Script + +Save as `scripts/verify.sh`: + +```bash +#!/bin/bash +set -e + +echo "→ Type checking..." +npx astro check + +echo "→ Building..." +npx astro build + +echo "→ Checking links..." +npx linkinator dist --recurse + +echo "→ Running E2E tests..." +npx playwright test + +echo "✓ All checks passed" +``` + +```bash +chmod +x scripts/verify.sh +./scripts/verify.sh +``` + +--- + +## 7. CI Pipeline (GitHub Actions) + +Save as `.github/workflows/test.yml`: + +```yaml +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - name: Type check + run: npx astro check && npx tsc --noEmit + + - name: Build + run: npx astro build + + - name: Component tests + run: npx vitest run + + - name: Install Playwright + run: npx playwright install --with-deps chromium + + - name: E2E tests + run: npx playwright test + + - name: Link check + run: npx linkinator dist --recurse --retry + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: playwright-report/ +``` + +--- + +## Quick Reference + +| Task | Command | +|------|---------| +| Component tests | `npx vitest` | +| E2E tests | `npx playwright test` | +| Type check | `npx astro check && tsc --noEmit` | +| Link check | `npx linkinator dist --recurse` | +| Full verification | `./scripts/verify.sh` | diff --git a/.github/skills/astro-sites-manager/references/v6-features.md b/.github/skills/astro-sites-manager/references/v6-features.md new file mode 100644 index 0000000..5a09b70 --- /dev/null +++ b/.github/skills/astro-sites-manager/references/v6-features.md @@ -0,0 +1,271 @@ +# Astro v6 Features (still current in v7) + +These features were introduced or stabilized in Astro v6 and remain fully supported in v7. + +--- + +## 1. Content Collections v2 + +Type-safe content management with Zod schemas and flexible data loaders. + +```ts +// content.config.ts +import { defineCollection, z } from 'astro:content'; + +const blog = defineCollection({ + loader: glob({ pattern: '**/*.md', base: './src/content/blog' }), + schema: z.object({ + title: z.string(), + date: z.date(), + draft: z.boolean().default(false), + }), +}); + +export const collections = { blog }; +``` + +**Loaders:** +- `file()` — single file (JSON, YAML) +- `glob()` — match files by pattern +- Custom loaders — fetch from CMS at build or request time (live collections) + +**Querying:** +```ts +import { getCollection, getEntry } from 'astro:content'; + +const posts = await getCollection('blog', ({ data }) => !data.draft); +const post = await getEntry('blog', 'my-post'); +``` + +--- + +## 2. Server Actions + +Type-safe RPC endpoints with Zod validation. + +```ts +// src/actions/index.ts +import { defineAction } from 'astro:actions'; +import { z } from 'astro:schema'; + +export const server = { + subscribe: defineAction({ + input: z.object({ email: z.string().email() }), + handler: async ({ email }) => { + // process subscription + return { success: true }; + }, + }), +}; +``` + +**Usage in components:** +```ts +import { actions } from 'astro:actions'; + +const result = await actions.subscribe({ email: 'user@example.com' }); +``` + +**Form integration with progressive enhancement:** +```astro +
+ + +
+``` + +--- + +## 3. Sessions + +Server-side session management with pluggable drivers. + +**Config:** +```js +// astro.config.mjs +export default defineConfig({ + session: { + driver: 'cookie', // also: node-fs, redis, etc. + }, +}); +``` + +**Usage:** +```ts +// In pages/endpoints +const user = await Astro.session.get('user'); +await Astro.session.set('user', { name: 'Alice' }); + +// In middleware +const user = await context.session.get('user'); +``` + +--- + +## 4. Server Islands + +Defer component rendering to request time while keeping the page static. + +```astro +--- +import UserGreeting from '../components/UserGreeting.astro'; +--- + +``` + +- Placeholder rendered at build time +- Component fetched and rendered at request time +- Perfect for personalized content in otherwise static pages + +--- + +## 5. Environment Variables (astro:env) + +Type-safe, validated environment variables. + +```ts +import { MY_SECRET } from 'astro:env/server'; +import { PUBLIC_API_URL } from 'astro:env/client'; +``` + +**Schema definition:** +```js +// astro.config.mjs +export default defineConfig({ + env: { + schema: { + MY_SECRET: envField.string({ context: 'server', access: 'secret' }), + PUBLIC_API_URL: envField.string({ context: 'client', access: 'public' }), + }, + }, +}); +``` + +Variables are validated at build time — missing or invalid values cause build failures. + +--- + +## 6. On-Demand Rendering + +Hybrid static/SSR on a per-page basis. + +```astro +--- +// This page renders on every request +export const prerender = false; +--- +``` + +**Adapters:** +- `@astrojs/node` +- `@astrojs/cloudflare` +- `@astrojs/netlify` +- `@astrojs/vercel` + +**Hybrid mode:** static by default, opt individual pages into SSR with `prerender = false`. + +--- + +## 7. View Transitions + +Client-side navigation with animated transitions between pages. + +```astro +--- +import { ViewTransitions } from 'astro:transitions'; +--- + + + + +

Hello

+
+ +
+``` + +**Lifecycle events:** +- `astro:before-preparation` +- `astro:after-swap` +- `astro:page-load` + +--- + +## 8. Middleware + +Request/response pipeline with access to context. + +```ts +// src/middleware.ts +import { defineMiddleware, sequence } from 'astro:middleware'; + +const auth = defineMiddleware(async (context, next) => { + const token = context.cookies.get('token'); + context.locals.user = await validateToken(token?.value); + return next(); +}); + +const logging = defineMiddleware(async (context, next) => { + console.log(context.url.pathname); + return next(); +}); + +export const onRequest = sequence(auth, logging); +``` + +Access to `context.locals`, `context.cookies`, `context.redirect()`. + +--- + +## 9. Image Optimization + +Built-in image processing with automatic optimization. + +```astro +--- +import { Image } from 'astro:assets'; +import hero from '../assets/hero.png'; +--- +Hero +``` + +- Automatic format conversion, lazy loading, responsive sizes +- Remote images configured via `image.domains` and `image.remotePatterns`: + +```js +// astro.config.mjs +export default defineConfig({ + image: { + domains: ['cdn.example.com'], + remotePatterns: [{ protocol: 'https', hostname: '**.example.com' }], + }, +}); +``` + +--- + +## 10. Internationalization (i18n) + +Built-in i18n routing with locale-aware URL generation. + +```js +// astro.config.mjs +export default defineConfig({ + i18n: { + defaultLocale: 'en', + locales: ['en', 'pt-br', 'es'], + routing: { + prefixDefaultLocale: false, + }, + }, +}); +``` + +**URL generation:** +```ts +import { getRelativeLocaleUrl } from 'astro:i18n'; + +getRelativeLocaleUrl('pt-br', '/about'); // → /pt-br/about +``` + +**Strategies:** pathname prefixes or domain-based routing. diff --git a/.github/skills/astro-sites-manager/references/v7-features.md b/.github/skills/astro-sites-manager/references/v7-features.md new file mode 100644 index 0000000..e931886 --- /dev/null +++ b/.github/skills/astro-sites-manager/references/v7-features.md @@ -0,0 +1,491 @@ +# Astro v7 Features + +Complete reference for all major features introduced in Astro v7. + +--- + +## 1. Vite 8 + Rolldown + +Astro v7 ships with **Vite 8**, which replaces the previous esbuild + Rollup bundling pipeline with **Rolldown** — a Rust-based bundler. + +### Key Points + +- **Rust-based bundler** replacing both esbuild (transform) and Rollup (bundling) in a single tool +- **10-30x faster** than Rollup for production builds +- **Same plugin API** — fully backwards compatible with existing Vite/Rollup plugins +- **Compatibility layer** auto-converts `build.rollupOptions` and esbuild-specific options to their Rolldown equivalents + +### Migration + +No changes required for most projects. If you use `vite.build.rollupOptions` in `astro.config.mjs`, the compatibility layer handles the conversion automatically. Warnings are emitted for any options that cannot be directly mapped. + +```js +// astro.config.mjs — works as before +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + vite: { + build: { + // Automatically converted to Rolldown equivalents + rollupOptions: { + output: { + manualChunks: { vendor: ['react', 'react-dom'] } + } + } + } + } +}); +``` + +--- + +## 2. Rust Compiler + +The Astro template compiler has been rewritten in **Rust**, replacing the previous Go-based compiler. Built on **oxc** (JavaScript/TypeScript parser) and **Lightning CSS**. + +### Key Points + +- **Native binaries** for all major platforms with **WASM fallback** for unsupported architectures +- **Strict parsing**: unclosed tags are now errors (no HTML auto-correction) +- **JSX whitespace rules**: newlines between inline elements produce no whitespace in output +- **CSS differences**: minor cosmetic changes to color serialization and `url()` quoting (output-only, no behavioral change) + +### Breaking Changes + +#### Strict HTML Parsing + +```astro + +
+

Hello world +

+ + +
+

Hello world

+
+``` + +#### JSX Whitespace Rules + +```astro + +Hello +World + + + +Hello{' '} +World + +``` + +#### CSS Cosmetic Differences + +```css +/* v6 output */ +background: url(image.png); +color: #ff0000; + +/* v7 output (functionally identical) */ +background: url("image.png"); +color: red; +``` + +--- + +## 3. Sätteri (Markdown/MDX in Rust) + +**Sätteri** is Astro v7's default Markdown and MDX processor, replacing the unified/remark/rehype pipeline with a Rust-native implementation. + +### Key Points + +- Built on **pulldown-cmark** (Markdown parsing) + **oxc** (MDX/JSX) +- Default processor — no configuration needed for standard usage +- Replaces unified/remark/rehype with dramatically faster processing + +### Built-in Features + +| Feature | Description | +|---------|-------------| +| GFM | Tables, strikethrough, task lists, autolinks | +| Smart punctuation | Curly quotes, em/en dashes | +| Heading IDs | Auto-generated anchor IDs | +| Directives | Container/leaf/text directives (`::: note`, etc.) | +| Math | LaTeX math blocks (`$$...$$`) and inline (`$...$`) | +| Frontmatter | YAML frontmatter parsing | +| Superscript/Subscript | `^super^` and `~sub~` syntax | +| Wikilinks | `[[page]]` and `[[page|text]]` syntax | + +### Configuration + +```js +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import { satteri } from '@astrojs/markdown-satteri'; + +export default defineConfig({ + markdown: { + processor: satteri({ + gfm: true, + smartPunctuation: true, + headingIds: true, + math: true, + wikilinks: true, + directives: true, + }) + } +}); +``` + +### Plugin API + +Sätteri plugins declare which node types they handle, skipping all others. This is significantly cheaper than unified's visitor pattern. + +```js +// my-satteri-plugin.js +export default function myPlugin() { + return { + name: 'my-plugin', + nodes: ['heading', 'paragraph'], // only visit these types + transform(node, context) { + if (node.type === 'heading') { + // transform heading nodes + } + } + }; +} +``` + +### Fallback to unified/remark/rehype + +For projects relying on existing remark/rehype plugins: + +```js +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import { unified } from '@astrojs/markdown-remark'; +import remarkToc from 'remark-toc'; +import rehypePrism from 'rehype-prism'; + +export default defineConfig({ + markdown: { + processor: unified({ + remarkPlugins: [remarkToc], + rehypePlugins: [rehypePrism], + }) + } +}); +``` + +### Docker Deployment Note + +Sätteri ships native bindings only for **glibc** (`@bruits/satteri-linux-x64-gnu`). Alpine Linux uses musl — there is no musl binding. Docker build stages MUST use `node:22-slim` (Debian/glibc), not `node:22-alpine`. This affects all Astro v7 projects using Sätteri (the default), including Starlight sites. + +Projects using `unified()` explicitly are NOT affected (they bypass Sätteri entirely). + +--- + +## 4. Queued Rendering + +Astro v7's rendering engine uses a **queue/stack-based** approach instead of recursive rendering. + +### Key Points + +- **~2.4x faster** for expression-dense pages (many dynamic expressions, loops, conditionals) +- **Now stable and default** — no configuration needed +- Eliminates deep call-stack issues on complex component trees +- Reduces memory pressure through iterative processing + +### Migration + +No action required. This is an internal engine change that is fully transparent to user code. + +--- + +## 5. Advanced Routing (`src/fetch.ts`) + +Astro v7 introduces a **standard fetch handler pattern** for advanced routing control, following the same conventions as Cloudflare Workers, Deno, and Bun. + +### Key Points + +- Define a `src/fetch.ts` file to take full control of the request pipeline +- Compose individual pieces: `i18n()`, `actions()`, `middleware()`, `pages()` +- Full control over request pipeline order +- Compatible with Hono for complex routing scenarios + +### Basic Routing + +```ts +// src/fetch.ts +import { astro, FetchState } from 'astro/fetch'; + +export default astro((request: Request, state: FetchState) => { + // Compose the pipeline in your preferred order + return state.pipeline( + i18n(), + middleware(), + actions(), + pages() + ); +}); +``` + +### Hono Integration + +```ts +// src/fetch.ts +import { astro } from 'astro/hono'; +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { logger } from 'hono/logger'; + +const app = new Hono(); + +app.use('*', logger()); +app.use('/api/*', cors()); + +app.get('/api/health', (c) => c.json({ status: 'ok' })); + +// Hand off to Astro for everything else +export default astro(app); +``` + +### Composing Middleware + +```ts +// src/fetch.ts +import { astro, FetchState } from 'astro/fetch'; +import { i18n, actions, middleware, pages } from 'astro/fetch'; + +export default astro((request: Request, state: FetchState) => { + const url = new URL(request.url); + + // Custom routing logic + if (url.pathname.startsWith('/api/')) { + return state.pipeline( + actions() + ); + } + + // Full pipeline for pages + return state.pipeline( + i18n(), + middleware(), + actions(), + pages() + ); +}); +``` + +--- + +## 6. Route Caching (Stable) + +Route-level caching is now **stable** in Astro v7, providing fine-grained control over page caching with tag-based invalidation. + +### Key Points + +- In-memory cache available out of the box +- Per-page caching with `Astro.cache.set()` +- Declarative `routeRules` in config +- Tag-based invalidation +- Integration with live content collections + +### Config-Level Setup + +```js +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import { memoryCache } from 'astro/config'; + +export default defineConfig({ + cache: memoryCache(), + routeRules: { + '/blog/**': { cache: { maxAge: 3600, swr: 86400, tags: ['blog'] } }, + '/products/**': { cache: { maxAge: 600, tags: ['products'] } }, + '/about': { cache: { maxAge: 86400 } }, + } +}); +``` + +### Per-Page Caching + +```astro +--- +// src/pages/blog/[slug].astro +const { slug } = Astro.params; +const post = await getEntry('blog', slug); + +Astro.cache.set({ + maxAge: 3600, // 1 hour + swr: 86400, // stale-while-revalidate: 24 hours + tags: ['blog', `post:${slug}`] +}); +--- + +
+

{post.data.title}

+ +
+``` + +### Webhook Invalidation + +```ts +// src/pages/api/revalidate.ts +import type { APIRoute } from 'astro'; +import { cache } from 'astro:cache'; + +export const POST: APIRoute = async ({ request }) => { + const { secret, tags, path } = await request.json(); + + if (secret !== import.meta.env.REVALIDATION_SECRET) { + return new Response('Unauthorized', { status: 401 }); + } + + // Invalidate by tags + if (tags) { + await cache.invalidate({ tags }); + } + + // Invalidate by path + if (path) { + await cache.invalidate({ path }); + } + + return new Response(JSON.stringify({ revalidated: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); +}; +``` + +### Live Content Collections Integration + +```js +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import { memoryCache } from 'astro/config'; + +export default defineConfig({ + cache: memoryCache(), + content: { + collections: { + blog: { + // When content changes, invalidate matching cache tags + onUpdate: (entry) => cache.invalidate({ tags: ['blog', `post:${entry.slug}`] }) + } + } + } +}); +``` + +--- + +## 7. CDN Cache Providers (Experimental) + +CDN-level cache providers push cache directives to the edge, allowing cached responses to be served **without invoking the server**. + +### Key Points + +- Edge-level caching — hits never reach your application server +- Platform-specific providers for Netlify, Vercel, and Cloudflare +- Works with the same `routeRules` and `Astro.cache.set()` API + +### Providers + +```js +// Netlify +import { cacheNetlify } from '@astrojs/netlify/cache'; + +export default defineConfig({ + cache: cacheNetlify(), +}); +``` + +```js +// Vercel +import { cacheVercel } from '@astrojs/vercel/cache'; + +export default defineConfig({ + cache: cacheVercel(), +}); +``` + +```js +// Cloudflare (private beta) +import { cacheCloudflare } from '@astrojs/cloudflare/cache'; + +export default defineConfig({ + cache: cacheCloudflare(), +}); +``` + +### How It Works + +1. On first request: server renders the page, cache provider stores response at the edge +2. On subsequent requests: CDN serves cached response directly (no server invocation) +3. On invalidation: cache is purged via provider API, next request triggers fresh render + +--- + +## 8. AI Enhancements + +Astro v7 includes first-class support for AI-assisted development workflows. + +### Key Points + +- **Background dev server**: auto-detects when running inside AI agents and optimizes output +- **JSON logging**: configurable, composable log output for machine consumption +- See `ai-dev-server.md` for full details + +### Background Dev Server + +When Astro detects it's running inside an AI agent environment, it automatically: + +- Switches to structured JSON log output +- Suppresses interactive UI elements (progress bars, spinners) +- Provides machine-readable error messages with file/line references +- Exposes a lightweight status API for agent polling + +### JSON Logging + +```js +// astro.config.mjs +export default defineConfig({ + devToolbar: { enabled: false }, + logging: { + format: 'json', // 'pretty' | 'json' | 'minimal' + level: 'info', + } +}); +``` + +--- + +## 9. Performance Benchmarks + +Real-world build time improvements measured on production sites (Astro v6 → v7): + +| Site | v6 | v7 | Improvement | +|------|----|----|-------------| +| docs.astro.build | 114s | 73s | **36% faster** | +| astro.build | 62s | 24s | **61% faster** | +| biomejs.dev | 176s | 150s | **15% faster** | +| developers.cloudflare.com | 387s | 262s | **32% faster** | + +### Contributing Factors + +- **Rolldown bundler**: 10-30x faster than Rollup for the bundling phase +- **Rust compiler**: eliminates Go→WASM overhead, native binary execution +- **Sätteri**: Markdown/MDX processing in Rust vs. JavaScript-based unified pipeline +- **Queued rendering**: 2.4x faster for expression-dense templates + +### Impact by Project Size + +- Small sites (< 100 pages): 15-25% faster builds +- Medium sites (100-1000 pages): 30-45% faster builds +- Large sites (1000+ pages): 40-60% faster builds + +The largest gains are seen in content-heavy sites with extensive Markdown processing and complex component trees. diff --git a/.github/skills/astro-sites-manager/references/validation-checklist.md b/.github/skills/astro-sites-manager/references/validation-checklist.md new file mode 100644 index 0000000..73e82a4 --- /dev/null +++ b/.github/skills/astro-sites-manager/references/validation-checklist.md @@ -0,0 +1,210 @@ +# Validation Checklist + +Complete checklist for validating an Astro installation/upgrade. Run these checks in the project root. + +--- + +## 1. Build Validation + +```bash +# Full production build — must exit 0 with no errors +npx astro build + +# Type checking (requires @astrojs/check) +npx astro check + +# Verify no unclosed tags (Rust compiler is strict about this) +find src -name "*.astro" -exec grep -Pn '<(img|br|hr|input|meta|link|source|area|base|col|embed|param|track|wbr)[^/]*[^/]>' {} + + +# Check for HTML nesting issues (div/section/article inside p) +grep -rPn ']*>[\s\S]*?<(div|section|article|ul|ol|table|blockquote|h[1-6])' src/**/*.astro +``` + +**Expected:** All commands pass with no errors or matches. + +--- + +## 2. Breaking Pattern Detection + +### Unclosed HTML tags + +```bash +# Find self-closing tags that are NOT void elements (common breakage) +grep -rPn '<(div|span|p|a|section|main|footer|header|nav|ul|li)\s[^>]*/>' src/ --include="*.astro" +``` + +### Block elements inside `

` + +```bash +grep -rPn ']*>[\s\S]*?<(div|section|article|ul|ol|dl|table|blockquote|pre|h[1-6]|form|fieldset|hr)' src/ --include="*.astro" +``` + +### Whitespace-dependent inline layouts + +```bash +# Look for adjacent inline elements that rely on whitespace rendering +grep -rPn '\s*<(span|a|strong|em|code)' src/ --include="*.astro" +``` + +### src/fetch.ts conflict + +```bash +# Astro reserves src/fetch.ts — check if it exists +find src -maxdepth 1 -name "fetch.ts" -o -name "fetch.js" +``` + +### @astrojs/db usage (removed in Astro 5+) + +```bash +grep -rn "@astrojs/db" package.json src/ --include="*.{ts,js,astro}" +``` + +### Deprecated transition imports + +```bash +# TRANSITION_* named exports removed +grep -rPn 'TRANSITION_[A-Z_]+' src/ --include="*.{ts,js,astro}" + +# isTransition*() helpers removed +grep -rPn 'isTransition\w+\(' src/ --include="*.{ts,js,astro}" +``` + +### getContainerRenderer() from package root + +```bash +# Must now import from /container subpath +grep -rn "getContainerRenderer" src/ --include="*.{ts,js}" | grep -v "/container" +``` + +### Experimental flags that should be removed + +```bash +# Check astro.config for experimental flags that graduated to stable +grep -A 20 'experimental:' astro.config.{mjs,ts,js} 2>/dev/null | grep -P '(contentLayer|serverIslands|actions|env|fonts|responsiveImages|svg)' +``` + +--- + +## 3. Deprecated Pattern Detection + +| Pattern | grep command | Fix | +|---------|-------------|-----| +| `Astro.glob()` | `grep -rn "Astro.glob" src/ --include="*.astro"` | Replace with `import.meta.glob()` or Content Collections | +| `Astro.fetchContent()` | `grep -rn "Astro.fetchContent" src/ --include="*.astro"` | Replace with Content Collections | +| `getStaticPaths` without `paginate` import | `grep -rn "getStaticPaths" src/ --include="*.astro"` | Verify using new pagination API | +| Legacy content collections (`src/content/config.ts` with `defineCollection` using `schema` only) | `grep -rn "defineCollection" src/content/config.ts` | Migrate to `type: 'content_layer'` or new loader API | +| `@astrojs/image` | `grep -rn "@astrojs/image" package.json` | Use built-in `astro:assets` | +| `integrations: [image()]` | `grep -rn "image()" astro.config.*` | Remove — use built-in `` component | +| `` component | `grep -rn "/dev/null + +# If found, verify @astrojs/markdown-remark is installed +grep -n "@astrojs/markdown-remark" package.json +``` + +**Fix:** If custom plugins exist but `@astrojs/markdown-remark` is missing: +```bash +npx astro add @astrojs/markdown-remark +``` + +### Shiki (syntax highlighting) compatibility + +```bash +# Check for custom Shiki config — API may have changed +grep -A 10 'shikiConfig' astro.config.{mjs,ts,js} 2>/dev/null +``` + +### GFM features (tables, strikethrough, autolinks) + +```bash +# GFM is built-in — check there's no redundant remark-gfm +grep -rn "remark-gfm" package.json astro.config.{mjs,ts,js} 2>/dev/null +``` + +**Fix:** Remove `remark-gfm` from plugins — GFM is included by default. + +### Test MDX rendering + +```bash +# Verify MDX integration is present if .mdx files exist +find src -name "*.mdx" | head -1 && grep -n "@astrojs/mdx" package.json +``` + +--- + +## 5. Performance Validation + +### Compare build times + +```bash +# Time the build (run before and after upgrade) +time npx astro build 2>&1 | tail -5 +``` + +### Verify queued rendering is active + +```bash +# Queued rendering should be default in Astro 5+ — check it's not disabled +grep -n "queuedRendering" astro.config.{mjs,ts,js} 2>/dev/null +``` + +**Expected:** No results (uses default) or `true`. If set to `false`, remove it. + +### Check Vite 6+ bundle output + +```bash +# Verify build output structure +ls -la dist/ 2>/dev/null || ls -la dist/_astro/ 2>/dev/null + +# Check chunk sizes +find dist -name "*.js" -exec wc -c {} + | sort -n | tail -10 + +# Verify no duplicate framework chunks +find dist -name "*.js" | xargs grep -l "react" 2>/dev/null | wc -l +``` + +### Verify no dev-only code in production build + +```bash +grep -rn "import.meta.env.DEV" dist/ 2>/dev/null +``` + +--- + +## Quick Full Validation Script + +```bash +#!/usr/bin/env bash +set -e +echo "=== Astro Validation ===" + +echo "[1/5] Build..." +npx astro build + +echo "[2/5] Type check..." +npx astro check || echo "WARN: astro check failed" + +echo "[3/5] Breaking patterns..." +grep -rn "@astrojs/db" src/ --include="*.{ts,js,astro}" && echo "FAIL: @astrojs/db found" || true +grep -rPn 'TRANSITION_[A-Z_]+' src/ --include="*.{ts,js,astro}" && echo "FAIL: deprecated transitions" || true +find src -maxdepth 1 -name "fetch.ts" -o -name "fetch.js" | grep . && echo "FAIL: src/fetch conflict" || true + +echo "[4/5] Deprecated APIs..." +grep -rn "Astro.glob\|Astro.fetchContent\|@astrojs/image" src/ package.json && echo "FAIL: deprecated APIs" || true + +echo "[5/5] Performance..." +time npx astro build 2>&1 | tail -3 + +echo "=== Done ===" +``` diff --git a/.github/skills/auth-md/SKILL.md b/.github/skills/auth-md/SKILL.md new file mode 100644 index 0000000..bec8270 --- /dev/null +++ b/.github/skills/auth-md/SKILL.md @@ -0,0 +1,357 @@ +--- +name: auth-md +description: > + Generate, validate, and explain `auth.md` files — the open protocol that lets AI agents + register for services on behalf of users. Use this skill whenever the user wants to make + their app agent-ready by publishing an `auth.md`, generate Protected Resource Metadata + (RFC 9728), validate an existing `auth.md` against the protocol specification, implement + agent registration endpoints, understand how the auth.md protocol works, or configure + authentication flows for agents. Trigger on mentions of "auth.md", "agent registration", + "agent auth", "make my app agent-ready", "ID-JAG", "identity_assertion flow", + "service_auth flow", "protected resource metadata", "claim ceremony", "agentic registration", + "CIMD", "Client ID Metadata Document", "oauth-id-jag", "agent revocation", + "agent credential", "agent discovery", "token exchange", "interaction_required", + "/.well-known/oauth-protected-resource", "/.well-known/oauth-authorization-server", + "/agent/identity", "/oauth2/token", or any variation of AI agent authentication/registration in APIs. +metadata: + author: https://ft.ia.br + version: "2.0" + date: 2026-06-27 + repository: https://github.com/fabricioctelles/skills + license: Apache 2.0 + category: library-and-api-reference +--- + +# auth-md + +Generate, validate, and explain the **auth.md** protocol — the open standard that lets AI agents register for services on behalf of users, without signup forms. + +--- + +## Protocol Context + +auth.md is a Markdown file published at a service's root (typically `https://service.com/auth.md`) that instructs agents on how to register. It works simultaneously as human-readable documentation and as a discoverable runtime artifact for agents. + +The protocol extends RFC 9728 (OAuth 2.0 Protected Resource Metadata) with an `agent_auth` block in the Authorization Server metadata. Registration returns an `identity_assertion` (service-signed JWT) that the agent exchanges at `/oauth2/token` for an `access_token`. Three registration methods are supported: + +| Flow | Mechanism | When to use | +|------|-----------|-------------| +| **identity_assertion** | Provider signs an ID-JAG (with `auth_time`) asserting user identity. Service verifies JWKS, returns `identity_assertion`. Agent exchanges at `/oauth2/token`. | Service does JIT provisioning from OIDC/SAML; wants zero-friction registration. | +| **service_auth** | Email hint + browser-based ceremony. Agent receives `user_code` + `verification_uri`; user signs in and types code. Agent polls `/oauth2/token`. | Agents on platforms that can't mint ID-JAGs; self-serve without trust list. | +| **anonymous** | No identity upfront. Immediate `identity_assertion` with pre-claim scopes. Optional deferred claim for scope upgrade. | Agent needs basic access immediately; human ownership binding deferred. | + +### Protocol Endpoints + +| Endpoint | Purpose | +|----------|---------| +| `/.well-known/oauth-protected-resource` | Discovery — resource metadata (RFC 9728) | +| `/.well-known/oauth-authorization-server` | Discovery — AS metadata with `agent_auth` block | +| `POST /agent/identity` | Registration — dispatches on `type` field | +| `POST /agent/identity/claim` | Claim initiation (anonymous deferred, or re-initiate expired user_code) | +| `POST /oauth2/token` | Token exchange (JWT-bearer grant) + claim polling (claim grant) | +| `POST /oauth2/revoke` | Credential-layer revocation (RFC 7009) | +| `events_endpoint` | Registration-layer revocation (receives SETs, RFC 8935) | + +### Token Lifecycle + +Registration **never** returns an `access_token` directly. The flow is: + +1. Registration → `identity_assertion` (service-signed JWT, reusable until expiry) +2. Exchange → `POST /oauth2/token` with `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer` → `access_token` +3. Refresh → re-exchange same `identity_assertion` when access_token expires +4. Expired assertion → restart at registration (Step 3) + +### Claim Ceremony (v2 — Browser-Based) + +The claim ceremony uses RFC 8628-style device authorization: +1. Registration returns `user_code` + `verification_uri` in a `claim` block +2. Agent surfaces both to the user +3. User opens `verification_uri`, signs in to the service, types the 6-digit code +4. Agent polls `POST /oauth2/token` with `grant_type=urn:workos:agent-auth:grant-type:claim` + `claim_token` +5. On success: receives `access_token` + fresh `identity_assertion` + +--- + +## Operation Modes + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `mode` | `generate` | `generate` = create auth.md + metadata; `validate` = check existing auth.md; `explain` = explain the protocol | +| `validation_level` | `basic` | `basic` = structure + fields + consistency (offline); `full` = basic + live endpoint fetch | +| `flows` | `all` | Which flows to include: `identity_assertion`, `service_auth`, `anonymous`, `all` | +| `role` | `app` | Perspective: `app` = service accepting registrations; `provider` = platform minting ID-JAGs | + +--- + +## Workflow: Generate + +### 1. Scan the codebase + +Look for: +- Existing API routes and authentication patterns +- Defined scopes/permissions +- Framework (Express, Django, Rails, FastAPI, NestJS, etc.) +- Base URL and auth server URL configuration +- Existing authentication middleware +- User models and provisioning mechanisms + +### 2. Ask the user only what cannot be inferred + +- Which flows to support (identity_assertion, service_auth, anonymous, or combination) +- Pre-claim scopes vs post-claim scopes (if anonymous) +- Trusted agent providers and trust list policy (if identity_assertion) +- Whether the service already does JIT provisioning or requires manual onboarding +- `idJagMaxAuthAgeSeconds` value (default: 3600) +- Desired rate limiting policy + +### 3. Generate artifacts + +Produce three artifacts: + +**a) `auth.md`** — Markdown file following the protocol template (see `references/protocol-template.md`). Must contain: +- Title and intro addressed to the agent +- Step 1 — Discover (two hops: PRM → AS metadata) +- Step 2 — Pick a method (decision tree) +- Step 3 — Register (one subsection per supported method) +- Step 4 — Claim ceremony (if service_auth or anonymous with claim) +- Step 5 — Exchange the assertion (POST /oauth2/token with jwt-bearer grant) +- Step 6 — Use the access_token +- Errors (complete table with all applicable codes) +- Revocation (two layers) + +**b) `oauth-protected-resource.json`** — JSON for `/.well-known/oauth-protected-resource` with `resource_name` and `resource_logo_uri` + +**c) `oauth-authorization-server.json`** — JSON for `/.well-known/oauth-authorization-server` with: +- `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported` +- Complete `agent_auth` block with `identity_endpoint`, `claim_endpoint`, `events_endpoint` + +### 4. Generate implementation guidance + +"Next Steps" section with: +- How to serve `auth.md` at the domain root +- How to serve metadata at the well-known paths +- How to add `WWW-Authenticate` header to 401 responses +- Endpoint implementation guidance (without generating framework-specific code unless requested) +- Token exchange implementation at `/oauth2/token` +- Claim page hosting (verification_uri → login → code input → confirm) +- Recommended rate limiting configuration +- Recommended audit events +- Security considerations (token hashing, auth_time validation, replay protection, claim_token handling) + +### 5. Generate Agent Provider guide (if role=provider) + +When the user is an agent provider (not an app), generate: +- How to mint audience-specific ID-JAGs with `auth_time` +- Token structure (header + payload with required and optional claims) +- How to publish JWKS +- Optionally: how to publish a CIMD (Client ID Metadata Document) +- How to implement revocation (POST SET to events_endpoint) +- How to present consent to the user before asserting identity (using `resource_name` + `resource_logo_uri`) + +--- + +## Workflow: Validate + +### 1. Load the auth.md + +From a local file path or URL. + +### 2. Run validation at the requested level + +**Basic (offline):** +- All required headings present (Step 1–6, Errors, Revocation) +- At least one flow documented +- Valid JSON in fenced code blocks for request/response shapes +- AS metadata contains `identity_endpoint`, `token_endpoint`, `grant_types_supported` +- Error table with standard error codes +- Consistency: flows in prose match `identity_types_supported` in metadata JSON +- No unreplaced placeholders + +**Full (live):** +- All basic checks, plus: +- Fetch `/.well-known/oauth-protected-resource` from the declared base URL +- Verify `agent_auth` block exists in AS metadata +- Fetch `/.well-known/oauth-authorization-server` and verify consistency +- Check that `identity_endpoint`, `token_endpoint`, `revocation_endpoint` respond (accept 400/401/422, reject 404/405) +- Verify API returns 401 with `WWW-Authenticate` containing `resource_metadata` + +### 3. Report results + +Checklist with ✅/❌ per rule, grouped by category: +- **Structure** — headings and order +- **Fields** — required fields in JSONs +- **Consistency** — cross-references between prose and metadata +- **Format** — valid JSON, valid HTTP, no placeholders +- **Endpoints** (full only) — reachability and correct responses + +Include severity: 🔴 Error (agents will fail), 🟡 Warning (degraded experience), 🟢 Info (suggestion). + +See `references/validation-rules.md` for the complete ruleset. + +--- + +## Workflow: Explain + +When the user wants to understand the protocol without generating or validating: + +1. Identify what the user wants to know (overview, specific flow, specific endpoint, security, etc.) +2. Explain using the protocol context above and the references +3. Use text-based sequence diagrams when helpful +4. Point to official documentation when relevant + +--- + +## User Matching and JIT Provisioning + +The identity_assertion flow needs to decide which service user a registration represents. Recommended resolution order: + +1. **Delegation record match** — if `(iss, sub)` has a delegation on file, route to same user +2. **Verified email match** — if a user exists with same verified email BUT no `(iss, sub)` delegation → `interaction_required` (401) with claim block for user to confirm linking +3. **Verified phone match** — same pattern +4. **No match → JIT** — create a new user per provisioning policy, or refuse + +Reject ID-JAGs with neither a verified email nor a verified phone — there's no basis for matching. + +--- + +## Rate Limiting + +The `/agent/identity` endpoint is unauthenticated for anonymous registration. Implement two tiers: + +1. **Per-IP** (checked first) — prevents a single source from consuming the tenant's budget. Default: 5/hour anonymous, 60/hour identity_assertion. +2. **Per-tenant** (checked second) — global cap across IPs. Default: 100/hour anonymous, 1000/hour identity_assertion. + +Also rate-limit `/oauth2/token` polling — enforce `interval` from the claim block, reject with `slow_down` if too fast. + +--- + +## Recommended Audit Events + +| Event | When | Data | +|-------|------|------| +| `registration.created` | Successful POST /agent/identity | registration_id, registration_type, iss, sub | +| `registration.interaction_required` | 401 interaction_required | registration_id, iss, sub, matched_user_id | +| `registration.login_required` | 401 login_required | iss, sub, auth_time, max_age | +| `claim.initiated` | /agent/identity/claim called | registration_id, email | +| `claim.completed` | User submitted correct user_code | registration_id, claimed_by_user_id | +| `claim.expired` | user_code window or registration expired | registration_id | +| `token.exchanged` | /oauth2/token jwt-bearer success | registration_id, access_token_id | +| `token.revoked` | /oauth2/revoke called | access_token_id | +| `registration.revoked` | SET processed at events_endpoint | registration_id, iss, sub | + +--- + +## Security Considerations + +- **auth_time validation** — `auth_time` is required in ID-JAGs. Service validates against `idJagMaxAuthAgeSeconds`. If too old, returns `login_required` (401) — agent must get user to re-authenticate at provider. +- **claim_token handling** — returned exactly once in the registration response. Agent holds in memory only for ceremony duration. Do not persist past Step 4. +- **Token hashing** — `claim_token` is a bearer secret. Store only SHA-256 hash server-side. +- **Consent UX** — surface `resource_name` and `resource_logo_uri` from PRM to the user before asserting identity. This is the user's only consent gate. +- **Two revocation layers** — credential layer (agent-callable, `/oauth2/revoke`, kills one access_token) vs registration layer (provider-driven SETs at `events_endpoint`, kills identity_assertion + all derived tokens). +- **Replay protection** — cache `jti` values with TTL of at least `exp - iat` + clock skew (typically 6 min). +- **CIMD resolution** — if `client_id` is a URL, fetch as Client ID Metadata Document and verify `jwks_uri`. +- **Bulk revocation** — provide operator-facing mechanism to revoke all outstanding identity_assertions for a tenant. + +--- + +## Error Codes Reference + +| Code | Where | Meaning | +|------|-------|---------| +| `anonymous_not_enabled` | `/agent/identity` | Service doesn't accept anonymous | +| `service_auth_not_enabled` | `/agent/identity` | service_auth disabled | +| `issuer_not_enabled` | `/agent/identity` | Provider not on trust list | +| `invalid_request` | `/agent/identity` | Body/claim/signature/jti/aud problems | +| `interaction_required` (401) | `/agent/identity` | ID-JAG matched account, no delegation — claim needed | +| `login_required` (401) | `/agent/identity` | auth_time too old — re-authenticate at provider | +| `invalid_claim_token` | `/agent/identity/claim` | Token wrong or expired | +| `claimed_or_in_flight` | `/agent/identity/claim` | Already claimed or wrong endpoint | +| `claim_expired` | `/agent/identity/claim` | Registration expired | +| `invalid_grant` | `/oauth2/token` | Assertion expired/revoked | +| `invalid_client` | `/oauth2/token` | client_id not recognized | +| `unsupported_grant_type` | `/oauth2/token` | Not jwt-bearer or claim grant | +| `authorization_pending` | `/oauth2/token` (claim) | User hasn't completed ceremony | +| `expired_token` | `/oauth2/token` (claim) | user_code window closed | +| `slow_down` | `/oauth2/token` (claim) | Polling too fast | +| `rate_limited` (429) | any | Back off and retry | + +--- + +## Agent Readiness Scanner Check + +The [isitagentready.com](https://isitagentready.com) scanner validates auth.md as the `authMd` check. Pass criteria: + +1. `/auth.md` served from site root with HTTP 200 +2. Content is Markdown with H1 heading containing "auth.md" +3. Optionally validates OAuth Protected Resource Metadata at `/.well-known/oauth-protected-resource` +4. Optionally validates Authorization Server metadata at `/.well-known/oauth-authorization-server` + +**To pass the check minimally:** +```markdown +# auth.md + +This service accepts AI agent registrations. + +## Authentication + +Agents can register via POST /agent/identity with a valid ID-JAG. +See below for supported methods. +``` + +**To pass with full marks (all metadata):** +- Serve `/auth.md` with proper heading +- Publish `/.well-known/oauth-protected-resource` with `resource`, `resource_name`, `resource_logo_uri`, `authorization_servers`, `scopes_supported`, `bearer_methods_supported: ["header"]` +- Publish `/.well-known/oauth-authorization-server` with `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`, and `agent_auth` block containing `skill`, `identity_endpoint`, `claim_endpoint`, `events_endpoint`, and registration methods + +**Scan command:** +```bash +curl -s -X POST 'https://isitagentready.com/api/scan' \ + -H 'Content-Type: application/json' \ + -d '{"url":"https://YOUR-DOMAIN/","enabledChecks":["authMd"]}' | jq '.checks.discovery.authMd' +``` + +--- + +## Quality Checklist + +Before delivering output, verify: + +- [ ] Generated `auth.md` contains all required steps (1-6) + Errors + Revocation +- [ ] AS metadata includes `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported` +- [ ] `agent_auth` block includes `identity_endpoint`, `claim_endpoint`, `events_endpoint` +- [ ] `identity_types_supported` matches the flows the user chose +- [ ] `scopes_supported` reflects actual API scopes found in codebase +- [ ] Base URLs are consistent between auth.md and metadata JSON +- [ ] Error codes table includes all standard codes for the supported flows +- [ ] Step 5 documents token exchange at `/oauth2/token` with jwt-bearer grant +- [ ] Revocation section documents both layers (credential + registration) +- [ ] No unreplaced placeholder values (`{{...}}`, ``, `[YOUR_...]`) +- [ ] Validation report covers all rules for the requested level +- [ ] Rate limiting documented (including /oauth2/token polling) +- [ ] Security considerations included (auth_time, claim_token, consent UX) +- [ ] If role=provider: ID-JAG structure with `auth_time` documented + +--- + +## References + +- `references/protocol-template.md` — Complete auth.md template with all sections and placeholders +- `references/validation-rules.md` — Full validation ruleset with error messages and severities +- `references/metadata-schema.md` — JSON schema for PRM, AS metadata, ID-JAG, and identity_assertion +- `references/example-auth-md.md` — Working example of a complete auth.md file (Acme Notes) +- `references/implementation-guide.md` — Server-side implementation guide with token exchange, claim ceremony, revocation, and security + +--- + +## Updating Protocol Knowledge + +This skill ships with a snapshot of the auth.md protocol specification (v2, June 2026). When possible, fetch the latest version from: + +- Skill Home and Doc Hub: `https://auth-md.com` +- Spec: `https://raw.githubusercontent.com/workos/auth.md/refs/heads/main/AUTH.md` +- Docs overview: `https://workos.com/auth-md/docs` +- Apps guide: `https://workos.com/auth-md/docs/apps` +- Agent providers guide: `https://workos.com/auth-md/docs/agent-providers` +- File anatomy: `https://workos.com/auth-md/docs/auth-md` + +If fetch fails, use the bundled `references/` as the source of truth. diff --git a/.github/skills/auth-md/references/example-auth-md.md b/.github/skills/auth-md/references/example-auth-md.md new file mode 100644 index 0000000..2942fe3 --- /dev/null +++ b/.github/skills/auth-md/references/example-auth-md.md @@ -0,0 +1,403 @@ +# Complete Example: auth.md + +A working example of an `auth.md` file for the fictional "Acme Notes" service, updated to protocol v2 (June 2026). + +--- + +## Example Context + +- **Service:** Acme Notes (collaborative notes app) +- **Resource Server:** `https://api.acmenotes.com` +- **Auth Server:** `https://auth.acmenotes.com` +- **Supported flows:** identity_assertion (ID-JAG), service_auth, anonymous +- **Scopes:** `notes.read`, `notes.write`, `notes.admin` + +--- + +## The File + +```markdown +# auth.md + +You are an agent. This service supports **agentic registration**: discover → register → (claim if needed) → exchange for an access_token → call API → handle revocation. Follow the steps in order; do not skip ahead. + +The resource server is `https://api.acmenotes.com` and the authorization server is `https://auth.acmenotes.com`. + +## Step 1 — Discover + +Discovery is two hops. The 401 response that pointed you here carries a `WWW-Authenticate` header with the PRM URL: + +\```http +HTTP/1.1 401 Unauthorized +WWW-Authenticate: Bearer resource_metadata="https://api.acmenotes.com/.well-known/oauth-protected-resource" +\``` + +### 1a. Fetch the Protected Resource Metadata + +\```http +GET /.well-known/oauth-protected-resource +\``` + +Response: + +\```json +{ + "resource": "https://api.acmenotes.com/", + "resource_name": "Acme Notes", + "resource_logo_uri": "https://acmenotes.com/logo.png", + "authorization_servers": ["https://auth.acmenotes.com/"], + "scopes_supported": ["notes.read", "notes.write", "notes.admin"], + "bearer_methods_supported": ["header"] +} +\``` + +### 1b. Fetch the Authorization Server metadata + +\```http +GET /.well-known/oauth-authorization-server +\``` + +Response: + +\```json +{ + "resource": "https://api.acmenotes.com/", + "authorization_servers": ["https://auth.acmenotes.com/"], + "scopes_supported": ["notes.read", "notes.write", "notes.admin"], + "bearer_methods_supported": ["header"], + "issuer": "https://auth.acmenotes.com", + "token_endpoint": "https://auth.acmenotes.com/oauth2/token", + "revocation_endpoint": "https://auth.acmenotes.com/oauth2/revoke", + "grant_types_supported": [ + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "urn:workos:agent-auth:grant-type:claim" + ], + "agent_auth": { + "skill": "https://acmenotes.com/auth.md", + "identity_endpoint": "https://auth.acmenotes.com/agent/identity", + "claim_endpoint": "https://auth.acmenotes.com/agent/identity/claim", + "events_endpoint": "https://auth.acmenotes.com/agent/event/notify", + "identity_types_supported": ["anonymous", "identity_assertion", "service_auth"], + "identity_assertion": { + "assertion_types_supported": [ + "urn:ietf:params:oauth:token-type:id-jag" + ] + }, + "events_supported": [ + "https://schemas.workos.com/events/agent/auth/identity/assertion/revoked" + ] + } +} +\``` + +## Step 2 — Pick a method + +1. **You have a session tied to a user identity and can exchange it for an ID-JAG** → identity_assertion + id-jag. +2. **You have only the user's email** → service_auth. Claim ceremony required. +3. **You have neither** → anonymous. Claim ceremony optional. + +Cross-check against the `agent_auth` block before proceeding. + +## Step 3 — Register + +Before sending an `identity_assertion` or `service_auth` body, surface "Acme Notes" and its logo to the user and confirm consent. Skip for `anonymous`. + +### identity_assertion + id-jag + +Mint the ID-JAG with: +- `aud` = `https://api.acmenotes.com/` (from PRM `resource`) +- `auth_time` = epoch seconds of user's last authentication at your provider (**required**) + +\```http +POST /agent/identity +Host: auth.acmenotes.com +Content-Type: application/json + +{ + "type": "identity_assertion", + "assertion_type": "urn:ietf:params:oauth:token-type:id-jag", + "assertion": "" +} +\``` + +Response — no confirmation needed (200): + +\```json +{ + "registration_id": "reg_01ABC123DEF456", + "registration_type": "identity_assertion", + "identity_assertion": "eyJhbGciOiJFUzI1NiJ9...", + "assertion_expires": "2026-05-22T14:00:00.000Z", + "scopes": ["notes.read", "notes.write"] +} +\``` + +Keep `identity_assertion` and go to Step 5. + +Response — confirmation required (401, `interaction_required`): + +\```json +{ + "error": "interaction_required", + "error_description": "ID-JAG email matches an existing account but no delegation on file for (iss, sub).", + "registration_id": "reg_01ABC123DEF456", + "registration_type": "identity_assertion", + "claim_url": "https://auth.acmenotes.com/agent/identity/claim", + "claim_token": "clm_xYz789AbC012dEf", + "claim_token_expires": "2026-05-22T13:30:00.000Z", + "post_claim_scopes": ["notes.read", "notes.write"], + "claim": { + "user_code": "847291", + "expires_in": 600, + "verification_uri": "https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_abc123", + "interval": 5 + } +} +\``` + +Surface `verification_uri` + `user_code` to the user (Step 4b) and poll (Step 4c). + +Response — login required (401, `login_required`): + +\```json +{ + "error": "login_required", + "error_description": "auth_time is 7200s old; max allowed is 3600s. Re-authenticate at the provider.", + "max_age": 3600 +} +\``` + +Re-authenticate the user at your provider and mint a fresh ID-JAG. + +### service_auth + +\```http +POST /agent/identity +Host: auth.acmenotes.com +Content-Type: application/json + +{ + "type": "service_auth", + "login_hint": "jane@example.com" +} +\``` + +Response (200): + +\```json +{ + "registration_id": "reg_01DEF789GHI012", + "registration_type": "service_auth", + "claim_url": "https://auth.acmenotes.com/agent/identity/claim", + "claim_token": "clm_aBc123DeF456gHi", + "claim_token_expires": "2026-05-22T13:30:00.000Z", + "post_claim_scopes": ["notes.read", "notes.write"], + "claim": { + "user_code": "592841", + "expires_in": 600, + "verification_uri": "https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_def456", + "interval": 5 + } +} +\``` + +No `identity_assertion` yet. Go to Step 4. + +### anonymous + +\```http +POST /agent/identity +Host: auth.acmenotes.com +Content-Type: application/json + +{ + "type": "anonymous" +} +\``` + +Response (200): + +\```json +{ + "registration_id": "reg_01GHI345JKL678", + "registration_type": "anonymous", + "identity_assertion": "eyJhbGciOiJFUzI1NiJ9...", + "assertion_expires": "2026-05-22T14:00:00.000Z", + "pre_claim_scopes": ["notes.read"], + "claim_url": "https://auth.acmenotes.com/agent/identity/claim", + "claim_token": "clm_mNo345PqR678sTu", + "claim_token_expires": "2026-05-22T13:30:00.000Z", + "post_claim_scopes": ["notes.read", "notes.write"] +} +\``` + +Exchange `identity_assertion` at `/oauth2/token` for an access_token with `pre_claim_scopes` (Step 5). To upgrade scopes, go to Step 4. + +## Step 4 — Claim ceremony + +### 4a. Get the ceremony materials + +For **service_auth** and **interaction_required** responses, you already have the `claim` block. Skip to 4b. + +For **anonymous**, initiate: + +\```http +POST /agent/identity/claim +Host: auth.acmenotes.com +Content-Type: application/json + +{ + "claim_token": "clm_mNo345PqR678sTu", + "email": "jane@example.com" +} +\``` + +Response (200): + +\```json +{ + "registration_id": "reg_01GHI345JKL678", + "claim_attempt_id": "cla_001", + "status": "initiated", + "expires_at": "2026-05-22T13:40:00.000Z", + "claim_attempt": { + "user_code": "374918", + "expires_in": 600, + "verification_uri": "https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_ghi789", + "interval": 5 + } +} +\``` + +### 4b. Hand off to the user + +Surface to the user: + +> Open this link, sign in (or sign up), and enter this 6-digit code: **374918** +> https://auth.acmenotes.com/login?return_to=%2Fclaim%3Fclaim_attempt_token%3Dcat_ghi789 + +The user signs in to Acme Notes, sees a confirmation page, and types the code. + +### 4c. Poll for completion + +\```http +POST /oauth2/token +Host: auth.acmenotes.com +Content-Type: application/x-www-form-urlencoded + +grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_mNo345PqR678sTu +\``` + +Response while waiting: + +\```json +{ + "error": "authorization_pending", + "error_description": "User has not completed the claim ceremony yet." +} +\``` + +Response on success: + +\```json +{ + "access_token": "eyJhbGciOiJSUzI1NiJ9...", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "notes.read notes.write", + "identity_assertion": "eyJhbGciOiJFUzI1NiJ9...", + "assertion_expires": "2026-05-22T15:00:00.000Z" +} +\``` + +Use `access_token` immediately. Cache `identity_assertion` for refresh via Step 5. + +If `user_code` window expires: + +\```json +{ + "error": "expired_token", + "error_description": "The user_code window has closed." +} +\``` + +Re-call `POST /agent/identity/claim` with same `claim_token` and `email` for a fresh code. If that returns `claim_expired`, restart at Step 3. + +## Step 5 — Exchange the assertion + +\```http +POST /oauth2/token +Host: auth.acmenotes.com +Content-Type: application/x-www-form-urlencoded + +grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=eyJhbGciOiJFUzI1NiJ9...&resource=https://api.acmenotes.com/ +\``` + +Response (200): + +\```json +{ + "access_token": "eyJhbGciOiJSUzI1NiJ9...", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "notes.read notes.write" +} +\``` + +Same `identity_assertion` can be re-used until it expires. If `invalid_grant`, restart at Step 3. + +## Step 6 — Use the access_token + +\```http +GET /api/notes +Host: api.acmenotes.com +Authorization: Bearer eyJhbGciOiJSUzI1NiJ9... +\``` + +**Refresh:** When access_token expires, re-call Step 5 with same identity_assertion. When identity_assertion expires or Step 5 returns `invalid_grant`, restart at Step 3. + +On 401 for a previously-working access_token: try Step 5 once. If that also fails, restart at Step 1. + +Full API docs: https://docs.acmenotes.com/ + +## Errors + +| Code | Where | What to do | +|------|-------|------------| +| `anonymous_not_enabled` | `/agent/identity` | Pick another method from Step 2 | +| `service_auth_not_enabled` | `/agent/identity` | Pick another method | +| `issuer_not_enabled` | `/agent/identity` | Provider not on trust list. Pick another method | +| `invalid_request` | `/agent/identity` | Fix body shape, ID-JAG signature/jti/aud problems | +| `interaction_required` (401) | `/agent/identity` (ID-JAG) | Body carries `claim` block; surface to user (Step 4) | +| `login_required` (401) | `/agent/identity` (ID-JAG) | Re-authenticate user at provider, mint fresh ID-JAG | +| `invalid_claim_token` | `/agent/identity/claim` | Restart at Step 3 | +| `claimed_or_in_flight` | `/agent/identity/claim` | Already claimed | +| `claim_expired` | `/agent/identity/claim` | Restart at Step 3 | +| `invalid_grant` | `/oauth2/token` | Assertion expired/revoked. Restart at Step 3 | +| `unsupported_grant_type` | `/oauth2/token` | Use one of the two supported grants | +| `authorization_pending` | `/oauth2/token` (claim) | User hasn't finished. Honor `interval` | +| `expired_token` | `/oauth2/token` (claim) | user_code window closed. Re-initiate or restart | +| `slow_down` | `/oauth2/token` (claim) | Add ≥5s to interval | +| `rate_limited` (429) | any | Back off and retry | + +## Revocation + +Two independent layers: + +- **Credential layer (RFC 7009):** POST `token=&token_type_hint=access_token` to `https://auth.acmenotes.com/oauth2/revoke`. Kills one access_token. Identity assertion intact — re-run Step 5. +- **Registration layer (RFC 8935):** Provider POSTs a Security Event Token to `events_endpoint`. Invalidates identity_assertion and all derived access_tokens. Agent discovers via `invalid_grant` at `/oauth2/token` — restart at Step 3. + +On 401 for a previously-working access_token: try Step 5 once. If `/oauth2/token` succeeds, credential-layer revocation. If `invalid_grant`, registration-layer — restart at Step 3. +``` + +--- + +## Notes on This Example + +1. **All three flows** documented — in production, delete sections for unsupported flows +2. **Token exchange step** (Step 5) — registration never returns access_token directly; always returns identity_assertion +3. **Browser-based claim ceremony** — user_code + verification_uri replaces OTP-via-email +4. **`interaction_required` response** — shows the 401 path when ID-JAG email matches existing account without delegation +5. **`login_required` response** — shows `auth_time` freshness enforcement +6. **Two revocation layers** — credential (agent-callable) vs registration (provider-driven) +7. **Claim polling at `/oauth2/token`** — uses profile-specific grant URN to avoid collision with standard RFC 8628 diff --git a/.github/skills/auth-md/references/implementation-guide.md b/.github/skills/auth-md/references/implementation-guide.md new file mode 100644 index 0000000..f84c7c0 --- /dev/null +++ b/.github/skills/auth-md/references/implementation-guide.md @@ -0,0 +1,433 @@ +# Server-Side Implementation Guide + +Detailed guidance for implementing auth.md protocol endpoints on the backend (v2, June 2026). Covers discovery, registration, claim ceremony, token exchange, revocation, security, rate limiting, and audit events. + +--- + +## Table of Contents + +1. [Minimum Implementation](#minimum-implementation) +2. [Discovery Documents](#discovery-documents) +3. [POST /agent/identity — Registration Handler](#post-agentidentity--registration-handler) +4. [ID-JAG Verification](#id-jag-verification) +5. [Claim Ceremony](#claim-ceremony) +6. [POST /oauth2/token — Token Endpoint](#post-oauth2token--token-endpoint) +7. [Revocation](#revocation) +8. [User Matching and JIT Provisioning](#user-matching-and-jit-provisioning) +9. [Rate Limiting](#rate-limiting) +10. [Security](#security) +11. [Audit Events](#audit-events) +12. [Deploy Checklist](#deploy-checklist) + +--- + +## Minimum Implementation + +1. Publish `/.well-known/oauth-protected-resource` with `resource_name` and `resource_logo_uri` +2. Publish `/.well-known/oauth-authorization-server` with `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`, and `agent_auth` block +3. Return `WWW-Authenticate: Bearer resource_metadata="..."` on 401 responses +4. Host `POST /agent/identity` that dispatches on the `type` field +5. For identity_assertion: maintain a trust list and verify ID-JAG signatures via JWKS, validate `auth_time` +6. For service_auth: return `claim` block with `user_code` + `verification_uri` +7. For anonymous: issue `identity_assertion` immediately with pre-claim scopes +8. Host `POST /agent/identity/claim` for deferred claim initiation +9. Implement `POST /oauth2/token` handling both grant types (jwt-bearer exchange + claim polling) +10. Implement `POST /oauth2/revoke` for credential-layer revocation (RFC 7009) +11. Accept SETs at `events_endpoint` for registration-layer revocation (RFC 8935) +12. Record audit events for every state change + +--- + +## Discovery Documents + +### Serving the PRM + +``` +GET /.well-known/oauth-protected-resource +→ 200 OK +→ Content-Type: application/json +``` + +Must include `resource_name` and `resource_logo_uri` — agents surface these to the user for consent before asserting identity. + +Cache aggressively: `Cache-Control: public, max-age=3600`. + +### Serving the AS Metadata + +``` +GET /.well-known/oauth-authorization-server +→ 200 OK +→ Content-Type: application/json +``` + +Must include standard OAuth fields (`issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`) plus the `agent_auth` block with `identity_endpoint`, `claim_endpoint`, `events_endpoint`. + +### WWW-Authenticate Header + +On every 401 response from the API: + +```http +HTTP/1.1 401 Unauthorized +WWW-Authenticate: Bearer resource_metadata="https://api.service.com/.well-known/oauth-protected-resource" +``` + +--- + +## POST /agent/identity — Registration Handler + +All registration requests share the same endpoint and dispatch on the `type` field: + +``` +POST /agent/identity +Content-Type: application/json +``` + +### Dispatch + +| `type` | Flow | Returns | +|--------|------|---------| +| `identity_assertion` | ID-JAG verified | `identity_assertion` (service-signed JWT) | +| `service_auth` | Email hint + browser ceremony | `claim` block (ceremony materials) | +| `anonymous` | No identity | `identity_assertion` + `claim_token` for deferred claim | + +### Handler: identity_assertion + id-jag + +1. Decode the ID-JAG header to obtain `kid` and `alg` +2. Look up the issuer (`iss`) in the trust list. Reject if unknown → `issuer_not_enabled` +3. Fetch JWKS from the provider (see ID-JAG Verification section for caching) +4. Verify signature → `invalid_request` if fails +5. Validate claims: + - `aud` matches the `resource` from PRM → `invalid_request` + - `exp` is in the future → `invalid_request` + - `iat` not unreasonably in the future (~1-2 min skew) + - `jti` not seen recently → `invalid_request` (replay) + - `auth_time` present and within `idJagMaxAuthAgeSeconds` → `login_required` (401) if too old + - At least `email_verified` or `phone_number_verified` is `true` → `invalid_request` +6. Match or provision the user (see User Matching) +7. Check delegation: if `(iss, sub)` is known OR JIT-provisioned without collision → success +8. If email/phone collision with existing account but no delegation on file → `interaction_required` (401) with `claim` block +9. On success: sign an `identity_assertion` JWT and return it + +### Handler: service_auth + +1. Validate `login_hint` (email format) +2. Create a registration row with type `service_auth` +3. Generate `claim_token` (returned to agent once), `user_code` (6-digit), `claim_attempt_token` (embedded in verification_uri) +4. Store SHA-256 hashes of `claim_token` +5. Build `verification_uri` pointing to the service's login page with `return_to` parameter +6. Return the registration response with `claim` block containing `user_code`, `verification_uri`, `expires_in`, `interval` + +### Handler: anonymous + +1. Apply rate limits +2. Create a registration row +3. Sign an `identity_assertion` JWT with pre-claim scopes +4. Generate `claim_token` for deferred claim. Store only SHA-256 hash. +5. Return `identity_assertion` + `claim_token` + pre/post claim scopes + +--- + +## ID-JAG Verification + +### Trust List + +Maintain a registry of providers. Minimum entry: issuer URL. Richer entries can pin JWKS URI, CIMD URL, or attestation policy. + +### JWKS Fetching + +- Fetch `{iss}/.well-known/jwks.json` on first use +- Cache per `Cache-Control`, with floor 10 min, ceiling 24h +- On `kid` miss, refetch once before rejecting + +### CIMD Resolution + +If `client_id` is a URL: +1. Fetch as OAuth Client ID Metadata Document +2. Verify `jwks_uri` matches the one used to verify signature + +### auth_time Validation + +- `auth_time` is **required** in ID-JAGs +- Compare `now() - auth_time` against `idJagMaxAuthAgeSeconds` (service-configured, e.g., 3600) +- If too old: return `login_required` (401) with `max_age` in the response +- Agent must re-authenticate user at provider and mint fresh ID-JAG + +### Replay Protection + +- Cache `jti` values with TTL of at least `exp - iat` + clock skew (typically 6 min) +- Reject on collision with `invalid_request` + +--- + +## Claim Ceremony + +The v2 claim ceremony is browser-based, borrowing from RFC 8628 device authorization. + +### POST /agent/identity/claim (anonymous deferred claim) + +1. Hash `claim_token`, look up registration +2. Reject if not found → `invalid_claim_token`, already claimed → `claimed_or_in_flight`, expired → `claim_expired` +3. Generate `user_code` (6-digit), `claim_attempt_token` +4. Build `verification_uri` with embedded `claim_attempt_token` +5. Return `claim_attempt` block with `user_code`, `verification_uri`, `expires_in`, `interval` + +### Service-Hosted Claim Page + +When the user opens `verification_uri`: +1. Redirect to login if not authenticated +2. After login, show claim page displaying: + - The user's identity ("You're signed in as jane@example.com") + - The requesting agent/provider info (from PRM `resource_name`) + - Input field for the 6-digit code +3. On correct code submission: mark claim as complete, associate registration with user +4. Incorrect code: show error, allow retry (up to limit) + +### Claim Completion Flow + +When the user submits the correct `user_code` on the claim page: +1. Mark the claim as complete in the database +2. For anonymous: sign a new `identity_assertion` (v2) carrying user claims, superseding the pre-claim one +3. For service_auth: sign the first `identity_assertion` for this registration +4. The next poll at `/oauth2/token` with the claim grant returns the access_token + identity_assertion + +--- + +## POST /oauth2/token — Token Endpoint + +Handles two grant types at the same endpoint: + +### Grant: urn:ietf:params:oauth:grant-type:jwt-bearer (Token Exchange) + +``` +POST /oauth2/token +Content-Type: application/x-www-form-urlencoded + +grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer +&assertion= +&resource= (optional but recommended) +``` + +Processing: +1. Verify the `identity_assertion` JWT signature (service's own key) +2. Validate `exp` not passed → `invalid_grant` if expired +3. Check assertion not revoked → `invalid_grant` if revoked +4. Mint a fresh `access_token` scoped to the assertion's scopes +5. Return standard OAuth token response + +### Grant: urn:workos:agent-auth:grant-type:claim (Claim Polling) + +``` +POST /oauth2/token +Content-Type: application/x-www-form-urlencoded + +grant_type=urn:workos:agent-auth:grant-type:claim +&claim_token= +``` + +Processing: +1. Hash `claim_token`, look up registration +2. If claim not yet completed → return `authorization_pending` +3. If user_code window expired → return `expired_token` +4. If polling too fast → return `slow_down` +5. If claim completed → mint access_token + return along with new `identity_assertion` + +Why a profile-specific grant URN? So this doesn't collide with services that also implement standard RFC 8628 device authorization at the same token endpoint. + +### Error Responses + +| Error | HTTP | Condition | +|-------|------|-----------| +| `invalid_grant` | 400 | Assertion expired, revoked, or invalid | +| `invalid_client` | 401 | client_id not recognized | +| `unsupported_grant_type` | 400 | Not one of the two supported grants | +| `authorization_pending` | 400 | Claim polling — user hasn't finished | +| `expired_token` | 400 | user_code window closed | +| `slow_down` | 400 | Polling too fast — add ≥5s | + +--- + +## Revocation + +### Credential Layer — POST /oauth2/revoke (RFC 7009) + +Agent-callable. Kills one access_token. + +``` +POST /oauth2/revoke +Content-Type: application/x-www-form-urlencoded + +token=&token_type_hint=access_token +``` + +- Return 200 on success (idempotent) +- The underlying `identity_assertion` remains valid — agent can re-exchange for a new access_token + +### Registration Layer — Events Endpoint (RFC 8935) + +Provider-driven. Receives Security Event Tokens. + +``` +POST /agent/event/notify +Content-Type: application/secevent+jwt + + +``` + +Processing: +1. Verify SET signature against issuer's JWKS +2. Enforce `jti` uniqueness +3. Match `events` key to supported schemas +4. For `https://schemas.workos.com/events/agent/auth/identity/assertion/revoked`: + - Invalidate all identity_assertions for `(iss, sub, aud)` + - Invalidate all derived access_tokens +5. Return 200 on success, 400 on verification failure + +### Bulk Revocation + +Provide an operator-facing mechanism to revoke all outstanding identity_assertions and access_tokens for a tenant in one shot — for incident response. + +--- + +## User Matching and JIT Provisioning + +Resolution order: + +1. **Delegation record match** — if `(iss, sub)` has a delegation on file, route to that user. Strongest identifier. +2. **Verified email match** — if a user exists with same verified email: + - If delegation exists for `(iss, sub)` → direct match (already covered above) + - If NO delegation for `(iss, sub)` → `interaction_required` (401). User must confirm linking. +3. **Verified phone match** — same pattern. +4. **No match → JIT** — create a new user per provisioning policy, or refuse. + +Reject ID-JAGs with neither verified email nor verified phone. + +--- + +## Rate Limiting + +### Two Tiers + +| Tier | Checked | Default Anonymous | Default identity_assertion | +|------|---------|-------------------|---------------------------| +| Per-IP | First | 5/hour | 60/hour | +| Per-tenant | Second | 100/hour | 1000/hour | + +### Implementation + +- Sliding-window counter with shared store +- Fail open on store errors +- Return 429 with `Retry-After` header +- Also rate-limit `/oauth2/token` polling (respect `interval` from claim block, reject with `slow_down`) + +--- + +## Security + +### Token Hashing + +| Token | Storage | Plaintext leaves server | +|-------|---------|------------------------| +| `claim_token` | SHA-256 hash | Once, in the registration response | +| `user_code` | Stored for comparison | Displayed on claim page when user submits | +| `identity_assertion` | Full JWT stored (or just signature hash for lookup) | In registration response | + +### claim_token + +- Returned **exactly once** to the agent in the registration response +- Agent holds in memory for ceremony duration — must not persist past Step 4 +- High-entropy: prefix `clm_` + 25+ chars base62 + +### auth_time Enforcement + +- Service configures `idJagMaxAuthAgeSeconds` (default: 3600) +- Reject ID-JAGs where `now() - auth_time > idJagMaxAuthAgeSeconds` +- Return `login_required` (401) with `max_age` field + +### Consent UX + +- Surface `resource_name` and `resource_logo_uri` from PRM to user before identity assertion +- The claim page should display who is requesting access (provider name from CIMD or ID-JAG metadata) + +### user_code Security + +- 6-digit numeric code +- 10-minute TTL (configurable via `expires_in`) +- Tight retry limits (3-5 attempts) on the claim page +- Code is tied to the `claim_attempt_token` embedded in `verification_uri` + +### Replay Protection + +- `jti` cache mandatory for ID-JAGs +- `jti` uniqueness enforced for SETs at events_endpoint +- Shared store required for multi-replica deployments + +### Trust List Discipline + +Treat the trusted-providers list as security-critical configuration. Changes should be audited. + +--- + +## Audit Events + +### Recommended Events + +| Event | When | Minimum Data | +|-------|------|--------------| +| `registration.created` | Successful POST /agent/identity | registration_id, registration_type, iss, sub (if ID-JAG) | +| `registration.interaction_required` | 401 interaction_required returned | registration_id, iss, sub, matched_user_id | +| `registration.login_required` | 401 login_required returned | iss, sub, auth_time, max_age | +| `claim.initiated` | /agent/identity/claim called | registration_id, email | +| `claim.completed` | User submitted correct user_code | registration_id, claimed_by_user_id | +| `claim.expired` | user_code window or registration expired | registration_id | +| `token.exchanged` | /oauth2/token jwt-bearer success | registration_id, access_token_id | +| `token.revoked` | /oauth2/revoke called | access_token_id | +| `registration.revoked` | SET processed at events_endpoint | registration_id, iss, sub | +| `registration.expired` | Unclaimed registration past TTL | registration_id | + +--- + +## Deploy Checklist + +### Before publishing + +- [ ] PRM served at `/.well-known/oauth-protected-resource` with `resource_name` and `resource_logo_uri` +- [ ] AS metadata served at `/.well-known/oauth-authorization-server` with `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`, and `agent_auth` block +- [ ] `agent_auth` contains `identity_endpoint`, `claim_endpoint`, `events_endpoint` +- [ ] `auth.md` served at the domain root +- [ ] API returns `WWW-Authenticate` header on 401s +- [ ] `POST /agent/identity` dispatches correctly by `type` +- [ ] Trust list configured (if identity_assertion) +- [ ] JWKS fetching with cache (if identity_assertion) +- [ ] `auth_time` validation against `idJagMaxAuthAgeSeconds` (if identity_assertion) +- [ ] `POST /agent/identity/claim` generates user_code + verification_uri (if service_auth/anonymous) +- [ ] Claim page served at verification_uri (login → code input → confirm) +- [ ] `POST /oauth2/token` handles jwt-bearer grant (assertion → access_token) +- [ ] `POST /oauth2/token` handles claim grant (polling → access_token + identity_assertion) +- [ ] `POST /oauth2/revoke` kills access_tokens (RFC 7009) +- [ ] Events endpoint accepts SETs for registration revocation +- [ ] Rate limiting active on `/agent/identity` and `/oauth2/token` +- [ ] claim_token stored as SHA-256 hash +- [ ] Replay protection for `jti` implemented +- [ ] Audit events being recorded + +### Recommended tests + +- [ ] identity_assertion: valid ID-JAG with fresh auth_time → identity_assertion returned +- [ ] identity_assertion: expired ID-JAG → `invalid_request` +- [ ] identity_assertion: auth_time too old → `login_required` (401) +- [ ] identity_assertion: email collision without delegation → `interaction_required` (401) with claim block +- [ ] identity_assertion: repeated `jti` → `invalid_request` (replay) +- [ ] identity_assertion: unknown issuer → `issuer_not_enabled` +- [ ] service_auth: valid email → registration with claim block (user_code + verification_uri) +- [ ] anonymous: registration → identity_assertion with pre_claim_scopes +- [ ] anonymous: claim initiation → claim_attempt with user_code +- [ ] /oauth2/token jwt-bearer: valid assertion → access_token +- [ ] /oauth2/token jwt-bearer: expired assertion → `invalid_grant` +- [ ] /oauth2/token claim: before completion → `authorization_pending` +- [ ] /oauth2/token claim: after completion → access_token + identity_assertion +- [ ] /oauth2/token claim: after window expires → `expired_token` +- [ ] /oauth2/token claim: too-fast polling → `slow_down` +- [ ] /oauth2/revoke: valid access_token → 200 +- [ ] events_endpoint: valid SET → registrations revoked +- [ ] Rate limit exceeded → 429 +- [ ] 401 on API → WWW-Authenticate header present diff --git a/.github/skills/auth-md/references/metadata-schema.md b/.github/skills/auth-md/references/metadata-schema.md new file mode 100644 index 0000000..dacdf4f --- /dev/null +++ b/.github/skills/auth-md/references/metadata-schema.md @@ -0,0 +1,370 @@ +# Metadata Schema + +JSON structure reference for the discovery documents and tokens required by the auth.md protocol (v2, June 2026). + +--- + +## Protected Resource Metadata (PRM) + +Served at: `{resource_server}/.well-known/oauth-protected-resource` + +Defined by [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728). + +```json +{ + "resource": "https://api.example.com/", + "resource_name": "Example Service", + "resource_logo_uri": "https://example.com/logo.png", + "authorization_servers": ["https://auth.example.com/"], + "scopes_supported": ["read", "write", "admin"], + "bearer_methods_supported": ["header"] +} +``` + +### Fields + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `resource` | ✅ | string (URL) | Canonical URL of the API. Used as `aud` in ID-JAGs. | +| `resource_name` | ✅ | string | Display name for consent prompts. Surface to user before asserting identity. | +| `resource_logo_uri` | Recommended | string (URL) | Logo for consent UI. Surface alongside `resource_name`. | +| `authorization_servers` | ✅ | string[] | Base URLs of OAuth AS(s). Agent fetches AS metadata from here. | +| `scopes_supported` | ✅ | string[] | All scopes the resource server understands. | +| `bearer_methods_supported` | ✅ | string[] | How credentials are presented. Typically `["header"]`. | + +--- + +## Authorization Server Metadata + +Served at: `{authorization_server}/.well-known/oauth-authorization-server` + +Combines standard [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) fields with the `agent_auth` profile block. + +```json +{ + "resource": "https://api.example.com/", + "authorization_servers": ["https://auth.example.com/"], + "scopes_supported": ["read", "write", "admin"], + "bearer_methods_supported": ["header"], + "issuer": "https://auth.example.com", + "token_endpoint": "https://auth.example.com/oauth2/token", + "revocation_endpoint": "https://auth.example.com/oauth2/revoke", + "grant_types_supported": [ + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "urn:workos:agent-auth:grant-type:claim" + ], + "agent_auth": { + "skill": "https://example.com/auth.md", + "identity_endpoint": "https://auth.example.com/agent/identity", + "claim_endpoint": "https://auth.example.com/agent/identity/claim", + "events_endpoint": "https://auth.example.com/agent/event/notify", + "identity_types_supported": ["anonymous", "identity_assertion", "service_auth"], + "identity_assertion": { + "assertion_types_supported": [ + "urn:ietf:params:oauth:token-type:id-jag" + ] + }, + "events_supported": [ + "https://schemas.workos.com/events/agent/auth/identity/assertion/revoked" + ] + } +} +``` + +### Top-Level OAuth Fields + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `issuer` | ✅ | string (URL) | Canonical issuer URL of this AS. Validate `iss` claim of any token the AS signs against this. | +| `token_endpoint` | ✅ | string (URL) | Where agents exchange identity assertions for access_tokens (Step 5) and poll during claim ceremony (Step 4c). | +| `revocation_endpoint` | ✅ | string (URL) | Where agents POST to revoke an access_token ([RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)). | +| `grant_types_supported` | ✅ | string[] | Grant types accepted at `token_endpoint`. Must include `urn:ietf:params:oauth:grant-type:jwt-bearer` (token exchange) and `urn:workos:agent-auth:grant-type:claim` (claim polling). | + +### `agent_auth` Block Fields + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `skill` | Recommended | string (URL) | URL of the auth.md file. | +| `identity_endpoint` | ✅ | string (URL) | POST endpoint for registration (Step 3). | +| `claim_endpoint` | Conditional | string (URL) | POST endpoint for claim initiation. Required if `service_auth` or `anonymous` supported. | +| `events_endpoint` | Recommended | string (URL) | Receives Security Event Tokens ([RFC 8417](https://datatracker.ietf.org/doc/html/rfc8417)) from providers for registration-layer revocation. | +| `identity_types_supported` | ✅ | string[] | `"anonymous"`, `"identity_assertion"`, `"service_auth"`, or combination. | +| `identity_assertion` | Conditional | object | Required if `"identity_assertion"` in identity_types_supported. | +| `identity_assertion.assertion_types_supported` | ✅ (if identity_assertion) | string[] | `"urn:ietf:params:oauth:token-type:id-jag"`. | +| `events_supported` | Recommended | string[] | Event schemas this service can ingest (currently revocation). Informational. | + +### identity_types → Flow Mapping + +| `identity_types_supported` | Flow | Ceremony | +|---|---|---| +| `identity_assertion` | ID-JAG verified by provider | None (unless `interaction_required`) | +| `service_auth` | Email hint, browser-based ceremony | RFC 8628-style: user_code + verification_uri | +| `anonymous` | No identity upfront | Optional deferred claim | + +--- + +## ID-JAG Token (Identity Assertion JWT Authorization Grant) + +Defined by [draft-ietf-oauth-identity-assertion-authz-grant](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-identity-assertion-authz-grant). + +### Header + +```json +{ + "typ": "oauth-id-jag+jwt", + "alg": "ES256", + "kid": "" +} +``` + +### Payload + +```json +{ + "iss": "https://api.agent-provider.com", + "sub": "", + "aud": "https://api.example.com", + "client_id": "", + "jti": "", + "iat": 1716400000, + "exp": 1716400300, + "auth_time": 1716399000, + "email": "user@example.com", + "email_verified": true, + "amr": ["mfa"], + "name": "Jane Smith", + "phone_number": "+15553805188", + "phone_number_verified": false, + "resource": "https://api.example.com", + "agent_platform": "cursor", + "agent_context_id": "chat-abc123" +} +``` + +### Required Claims + +| Claim | Description | +|-------|-------------| +| `iss` | Provider's issuer URL (must be on service's trust list) | +| `sub` | Opaque user identifier at the provider | +| `aud` | Service's `resource` URL from the PRM | +| `client_id` | Provider identity (issuer URL or CIMD URL) | +| `jti` | Unique token ID for replay protection | +| `iat` | Issuance time (epoch seconds) | +| `exp` | Expiration (typically iat + 5 minutes) | +| `auth_time` | Epoch seconds when the user last authenticated at the provider. **Required.** Service rejects ID-JAGs whose auth_time is older than its `idJagMaxAuthAgeSeconds` window. | +| `email` + `email_verified: true` OR `phone_number` + `phone_number_verified: true` | At least one verified contact required | + +### Optional Claims + +| Claim | Description | +|-------|-------------| +| `amr` | Authentication methods reference (e.g., `["mfa"]`) | +| `name` | User's display name | +| `phone_number` | User's phone number | +| `phone_number_verified` | Whether phone is verified | +| `resource` | Resource server URL (informational) | +| `agent_platform` | Agent platform (e.g., `"cursor"`, `"chatgpt"`) | +| `agent_context_id` | Agent context/chat ID | + +--- + +## Client ID Metadata Document (CIMD) + +Optional document that decouples provider identity from signing keys. Defined by [draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/). + +Hosted at the URL used as `client_id` in the ID-JAG. + +```json +{ + "client_id": "https://api.agent-provider.com/agent-auth.json", + "client_name": "Agent Provider", + "logo_uri": "https://agent-provider.com/logo.png", + "client_uri": "https://agent-provider.com", + "tos_uri": "https://agent-provider.com/tos", + "policy_uri": "https://agent-provider.com/privacy", + "token_endpoint_auth_method": "private_key_jwt", + "jwks_uri": "https://agent-provider.com/.well-known/jwks.json", + "scope": "openid email profile" +} +``` + +--- + +## Identity Assertion JWT (Service-Signed) + +After successful registration, the service returns an `identity_assertion` — a JWT signed by the service that the agent exchanges at `/oauth2/token` for an access_token. + +The identity_assertion is: +- Reusable until it expires (`assertion_expires`) +- Exchanged via `POST /oauth2/token` with `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer` +- Replaced by a fresh one after claim ceremony completion (v2 carries user claims) + +--- + +## Registration Response Shapes + +### identity_assertion + id-jag (no confirmation needed) + +```json +{ + "registration_id": "reg_...", + "registration_type": "identity_assertion", + "identity_assertion": "", + "assertion_expires": "2026-05-04T13:00:00.000Z", + "scopes": ["read", "write"] +} +``` + +### identity_assertion + id-jag (interaction_required — 401) + +```json +{ + "error": "interaction_required", + "error_description": "...", + "registration_id": "reg_...", + "registration_type": "identity_assertion", + "claim_url": "https://auth.example.com/agent/identity/claim", + "claim_token": "clm_...", + "claim_token_expires": "...", + "post_claim_scopes": ["read", "write"], + "claim": { + "user_code": "123456", + "expires_in": 600, + "verification_uri": "https://auth.example.com/login?return_to=...", + "interval": 5 + } +} +``` + +### service_auth + +```json +{ + "registration_id": "reg_...", + "registration_type": "service_auth", + "claim_url": "https://auth.example.com/agent/identity/claim", + "claim_token": "clm_...", + "claim_token_expires": "2026-05-21T17:31:25.994Z", + "post_claim_scopes": ["read", "write"], + "claim": { + "user_code": "123456", + "expires_in": 600, + "verification_uri": "https://auth.example.com/login?return_to=...", + "interval": 5 + } +} +``` + +### anonymous + +```json +{ + "registration_id": "reg_...", + "registration_type": "anonymous", + "identity_assertion": "", + "assertion_expires": "2026-05-04T13:00:00.000Z", + "pre_claim_scopes": ["read"], + "claim_url": "https://auth.example.com/agent/identity/claim", + "claim_token": "clm_...", + "claim_token_expires": "2026-05-21T17:26:32.915Z", + "post_claim_scopes": ["read", "write"] +} +``` + +### Claim Ceremony Initiation (anonymous → POST /agent/identity/claim) + +```json +{ + "registration_id": "reg_...", + "claim_attempt_id": "cla_...", + "status": "initiated", + "expires_at": "2026-05-21T17:31:25.994Z", + "claim_attempt": { + "user_code": "123456", + "expires_in": 600, + "verification_uri": "https://auth.example.com/login?return_to=...", + "interval": 5 + } +} +``` + +### Token Exchange Response (POST /oauth2/token — JWT-bearer grant) + +```json +{ + "access_token": "", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "read write" +} +``` + +### Claim Polling Success (POST /oauth2/token — claim grant) + +```json +{ + "access_token": "", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "read write", + "identity_assertion": "", + "assertion_expires": "2026-05-21T18:31:25.994Z" +} +``` + +--- + +## Error Response Shape + +```json +{ + "error": "", + "error_description": "" +} +``` + +--- + +## Revocation + +Two independent layers: + +### Credential Layer (RFC 7009) — Agent-Callable + +```http +POST /oauth2/revoke +Content-Type: application/x-www-form-urlencoded + +token=&token_type_hint=access_token +``` + +Kills one access_token. 200 on success, idempotent. The `identity_assertion` remains intact — re-run Step 5 for a fresh access_token. + +### Registration Layer (RFC 8935 SET delivery) — Provider-Driven + +Provider POSTs a Security Event Token (`Content-Type: application/secevent+jwt`) to the service's `events_endpoint`. The service invalidates the identity_assertion and all derived access_tokens. + +Agent discovers this when `/oauth2/token` returns `invalid_grant` — restart at Step 3. + +### SET Payload + +```json +{ + "iss": "https://api.agent-provider.com", + "sub": "", + "aud": "https://auth.example.com", + "jti": "", + "iat": 1716400000, + "events": { + "https://schemas.workos.com/events/agent/auth/identity/assertion/revoked": {} + } +} +``` + +### Processing + +1. Verify signature against the issuer's JWKS (same trust path as ID-JAG verification) +2. Enforce `jti` uniqueness for replay protection +3. Find all identity_assertions and access_tokens for `(iss, sub, aud)` and invalidate them +4. Return 200 on success, 400 on verification failure diff --git a/.github/skills/auth-md/references/protocol-template.md b/.github/skills/auth-md/references/protocol-template.md new file mode 100644 index 0000000..93635ed --- /dev/null +++ b/.github/skills/auth-md/references/protocol-template.md @@ -0,0 +1,451 @@ +# Protocol Template + +Canonical template for generating an `auth.md` file (v2, June 2026). Replace all `{{placeholders}}` with service-specific values. Delete sections for flows the service does not support. + +--- + +## File Structure + +An auth.md is organized as a numbered walkthrough the agent follows top to bottom: + +1. **Title and intro** — addressed to the agent, declares real hostnames (resource server + auth server) +2. **Step 1 — Discover** — two-hop discovery (PRM → AS metadata) +3. **Step 2 — Pick a method** — decision tree +4. **Step 3 — Register** — one subsection per supported method +5. **Step 4 — Claim ceremony** — browser-based user_code ceremony (if service_auth or anonymous claim) +6. **Step 5 — Exchange the assertion** — POST identity_assertion to /oauth2/token for access_token +7. **Step 6 — Use the access_token** — how to use and refresh +8. **Errors** — error codes table +9. **Revocation** — two-layer revocation model + +--- + +## Complete Template + +```markdown +# auth.md + +You are an agent. This service supports **agentic registration**: discover → register → (claim if needed) → exchange for an access_token → call API → handle revocation. Follow the steps in order; do not skip ahead. + +Examples use placeholder hosts: `{{base_url}}` (the resource server hosting the API you want to call) and `{{auth_server_url}}` (the authorization server that handles registration). + +## Step 1 — Discover + +Discovery is two hops. The 401 response that pointed you here carries a `WWW-Authenticate` header with the PRM URL: + +\```http +HTTP/1.1 401 Unauthorized +WWW-Authenticate: Bearer resource_metadata="{{base_url}}/.well-known/oauth-protected-resource" +\``` + +### 1a. Fetch the Protected Resource Metadata + +\```http +GET /.well-known/oauth-protected-resource +\``` + +Response: + +\```json +{ + "resource": "{{base_url}}/", + "resource_name": "{{service_name}}", + "resource_logo_uri": "{{logo_url}}", + "authorization_servers": ["{{auth_server_url}}/"], + "scopes_supported": [{{scopes_list}}], + "bearer_methods_supported": ["header"] +} +\``` + +### 1b. Fetch the Authorization Server metadata + +\```http +GET /.well-known/oauth-authorization-server +\``` + +Response: + +\```json +{ + "resource": "{{base_url}}/", + "authorization_servers": ["{{auth_server_url}}/"], + "scopes_supported": [{{scopes_list}}], + "bearer_methods_supported": ["header"], + "issuer": "{{auth_server_url}}", + "token_endpoint": "{{auth_server_url}}/oauth2/token", + "revocation_endpoint": "{{auth_server_url}}/oauth2/revoke", + "grant_types_supported": [ + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "urn:workos:agent-auth:grant-type:claim" + ], + "agent_auth": { + "skill": "{{auth_md_url}}", + "identity_endpoint": "{{auth_server_url}}/agent/identity", + "claim_endpoint": "{{auth_server_url}}/agent/identity/claim", + "events_endpoint": "{{auth_server_url}}/agent/event/notify", + "identity_types_supported": [{{identity_types}}], + "identity_assertion": { + "assertion_types_supported": [{{assertion_types}}] + }, + "events_supported": [ + "https://schemas.workos.com/events/agent/auth/identity/assertion/revoked" + ] + } +} +\``` + +## Step 2 — Pick a method + +Use this decision tree: + +1. **You have a session tied to a user identity and can exchange it for an ID-JAG, audience-bound to this service** → identity_assertion + id-jag. +2. **You have only the user's email** → service_auth. Claim ceremony required. +3. **You have neither** → anonymous. Claim ceremony optional; deferred until the user wants to take ownership. + +Before sending: cross-check your choice against the `agent_auth` block. If your type is not in `identity_types_supported`, pick another or stop. + +## Step 3 — Register + +Before sending an `identity_assertion` or `service_auth` body, surface the service's `resource_name` and `resource_logo_uri` (from Step 1a) and the scope set you'll be acting under, and confirm with the user. Skip this for `anonymous`. + +### identity_assertion + id-jag + + + +Mint the ID-JAG with: +- `aud` = the `resource` from the PRM +- `iss` = your provider's issuer URL (must be on trust list) +- `email_verified: true` OR `phone_number_verified: true` +- Fresh `jti`, near-term `exp` (~5 minutes) +- `auth_time` — epoch seconds when the user last authenticated at your provider. **Required.** + +\```http +POST /agent/identity +Content-Type: application/json + +{ + "type": "identity_assertion", + "assertion_type": "urn:ietf:params:oauth:token-type:id-jag", + "assertion": "" +} +\``` + +Response — no confirmation needed (200): + +\```json +{ + "registration_id": "reg_...", + "registration_type": "identity_assertion", + "identity_assertion": "", + "assertion_expires": "{{assertion_expiry}}", + "scopes": [{{post_registration_scopes}}] +} +\``` + +Keep `identity_assertion` and go to Step 5. + +Response — confirmation required (401, `interaction_required`): + +\```json +{ + "error": "interaction_required", + "error_description": "...", + "registration_id": "reg_...", + "registration_type": "identity_assertion", + "claim_url": "{{auth_server_url}}/agent/identity/claim", + "claim_token": "clm_...", + "claim_token_expires": "...", + "post_claim_scopes": [{{post_claim_scopes}}], + "claim": { + "user_code": "123456", + "expires_in": 600, + "verification_uri": "{{auth_server_url}}/login?return_to=...", + "interval": 5 + } +} +\``` + +Surface `verification_uri` + `user_code` to the user (Step 4b) and poll (Step 4c). + +Response — login required (401, `login_required`): + +\```json +{ + "error": "login_required", + "error_description": "auth_time is too old; re-authenticate at the provider.", + "max_age": 3600 +} +\``` + +Re-authenticate the user at your provider (`prompt=login`) and mint a fresh ID-JAG. + +### service_auth + + + +\```http +POST /agent/identity +Content-Type: application/json + +{ + "type": "service_auth", + "login_hint": "user@example.com" +} +\``` + +Response (200): + +\```json +{ + "registration_id": "reg_...", + "registration_type": "service_auth", + "claim_url": "{{auth_server_url}}/agent/identity/claim", + "claim_token": "clm_...", + "claim_token_expires": "{{claim_ttl}}", + "post_claim_scopes": [{{post_claim_scopes}}], + "claim": { + "user_code": "123456", + "expires_in": 600, + "verification_uri": "{{auth_server_url}}/login?return_to=...", + "interval": 5 + } +} +\``` + +No `identity_assertion` yet. Go to Step 4. + +### anonymous + + + +\```http +POST /agent/identity +Content-Type: application/json + +{ + "type": "anonymous" +} +\``` + +Response (200): + +\```json +{ + "registration_id": "reg_...", + "registration_type": "anonymous", + "identity_assertion": "", + "assertion_expires": "{{assertion_expiry}}", + "pre_claim_scopes": [{{pre_claim_scopes}}], + "claim_url": "{{auth_server_url}}/agent/identity/claim", + "claim_token": "clm_...", + "claim_token_expires": "{{claim_ttl}}", + "post_claim_scopes": [{{post_claim_scopes}}] +} +\``` + +The `identity_assertion` exchanges at `/oauth2/token` for an access_token with `pre_claim_scopes` immediately (Step 5). To upgrade scopes, go to Step 4. + +## Step 4 — Claim ceremony + + + +The end goal: get a signed-in user to confirm a 6-digit `user_code` **you supply them**. The code travels from you → user; the user authenticates to the service and types it into a page the service owns. + +### 4a. Get the ceremony materials + +For **service_auth** registrations and **interaction_required** responses, you already have the `claim` block from Step 3. Skip to 4b. + +For **anonymous** registrations, initiate the ceremony: + +\```http +POST /agent/identity/claim +Content-Type: application/json + +{ + "claim_token": "clm_...", + "email": "user@example.com" +} +\``` + +Response (200): + +\```json +{ + "registration_id": "reg_...", + "claim_attempt_id": "cla_...", + "status": "initiated", + "expires_at": "{{claim_attempt_ttl}}", + "claim_attempt": { + "user_code": "123456", + "expires_in": 600, + "verification_uri": "{{auth_server_url}}/login?return_to=...", + "interval": 5 + } +} +\``` + +### 4b. Hand off to the user + +Surface `verification_uri` and `user_code` to the user in a single message: + +> Open this link, sign in (or sign up), and enter this 6-digit code: **123456** +> {{verification_uri}} + +The user will: +1. Open `verification_uri` +2. Authenticate with the service (sign in or sign up) +3. Land on the claim page, see their identity displayed, type the `user_code`, and submit + +### 4c. Poll for completion + +Poll the standard `token_endpoint` (from AS metadata) with the profile-specific claim grant: + +\```http +POST /oauth2/token +Content-Type: application/x-www-form-urlencoded + +grant_type=urn:workos:agent-auth:grant-type:claim +&claim_token= +\``` + +Response while waiting: + +\```json +{ + "error": "authorization_pending", + "error_description": "..." +} +\``` + +Response on success: + +\```json +{ + "access_token": "", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "{{scopes}}", + "identity_assertion": "", + "assertion_expires": "{{assertion_expiry}}" +} +\``` + +Use `access_token` immediately; cache `identity_assertion` for refresh via Step 5. + +If the `user_code` window expires: + +\```json +{ + "error": "expired_token", + "error_description": "..." +} +\``` + +Re-call `POST /agent/identity/claim` with the same `claim_token` and `email` to mint a fresh `user_code`. If that returns `claim_expired`, restart at Step 3. + +Honor `interval` (in seconds); on `slow_down` back off. + +## Step 5 — Exchange the assertion + +POST the `identity_assertion` to the token endpoint with the RFC 7523 JWT-bearer grant: + +\```http +POST /oauth2/token +Content-Type: application/x-www-form-urlencoded + +grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer +&assertion= +&resource={{base_url}}/ +\``` + +Response (200): + +\```json +{ + "access_token": "", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "{{scopes}}" +} +\``` + +The same `identity_assertion` can be re-used to mint additional access_tokens until it expires. If `/oauth2/token` returns `invalid_grant`, restart at Step 3. + +## Step 6 — Use the access_token + +Present as a bearer token: + +\```http +GET /api/some-resource +Authorization: Bearer +\``` + +**Refresh:** When the access_token expires, re-call Step 5 with the same `identity_assertion`. When the identity_assertion itself expires or `/oauth2/token` returns `invalid_grant`, restart at Step 3. There is no refresh_token — the two-step pattern replaces it. + +On 401 for a previously-working access_token: try Step 5 once. If that also fails, restart at Step 1. + +Full API reference: `{{api_docs_url}}` + +## Errors + +| Code | Where | What to do | +|------|-------|------------| +| `anonymous_not_enabled` | `/agent/identity` | Pick another method from Step 2 | +| `service_auth_not_enabled` | `/agent/identity` | Pick another method | +| `issuer_not_enabled` | `/agent/identity` | Provider not on trust list. Pick another method | +| `invalid_request` | `/agent/identity` | Fix body shape, claims, signature, jti, aud problems | +| `interaction_required` (401) | `/agent/identity` (ID-JAG) | Body carries `claim` block; surface to user (Step 4) | +| `login_required` (401) | `/agent/identity` (ID-JAG) | Re-authenticate user at provider, mint fresh ID-JAG | +| `invalid_claim_token` | `/agent/identity/claim` | Restart at Step 3 | +| `claimed_or_in_flight` | `/agent/identity/claim` | Already claimed. Re-read Step 3 response | +| `claim_expired` | `/agent/identity/claim` | Registration expired. Restart at Step 3 | +| `invalid_grant` | `/oauth2/token` | Assertion expired/revoked. Restart at Step 3 | +| `invalid_client` | `/oauth2/token` | client_id not recognized | +| `unsupported_grant_type` | `/oauth2/token` | Use one of the two supported grant types | +| `authorization_pending` | `/oauth2/token` (claim) | User hasn't completed ceremony. Honor `interval` | +| `expired_token` | `/oauth2/token` (claim) | user_code window closed. Re-initiate or restart | +| `slow_down` | `/oauth2/token` (claim) | Add ≥5s to interval and retry | +| `rate_limited` (429) | any | Back off and retry | + +## Revocation + +Two independent layers: + +- **Credential layer (RFC 7009):** POST `token=&token_type_hint=access_token` to `{{auth_server_url}}/oauth2/revoke`. Kills one access_token. Identity assertion intact — re-run Step 5. +- **Registration layer (RFC 8935 SET delivery):** Provider POSTs a Security Event Token to `events_endpoint`. Service invalidates identity_assertion and all derived access_tokens. Agent discovers this when `/oauth2/token` returns `invalid_grant` — restart at Step 3. + +On 401 for a previously-working access_token: try Step 5 once. If `/oauth2/token` succeeds, credential-layer revocation — fresh access_token works. If `invalid_grant`, registration-layer — restart at Step 3. +``` + +--- + +## Placeholder Reference + +| Placeholder | Description | Example | +|-------------|-------------|---------| +| `{{base_url}}` | API base URL (resource server) | `https://api.acme.com` | +| `{{service_name}}` | Human-readable service name | `Acme Notes` | +| `{{logo_url}}` | Service logo URL | `https://acme.com/logo.png` | +| `{{auth_server_url}}` | Authorization server base URL | `https://auth.acme.com` | +| `{{auth_md_url}}` | URL where auth.md is hosted | `https://acme.com/auth.md` | +| `{{scopes_list}}` | JSON array of scope strings | `"notes.read", "notes.write"` | +| `{{identity_types}}` | Supported identity types | `"anonymous", "identity_assertion", "service_auth"` | +| `{{assertion_types}}` | Supported assertion types | `"urn:ietf:params:oauth:token-type:id-jag"` | +| `{{pre_claim_scopes}}` | Scopes before claim (anonymous) | `"notes.read"` | +| `{{post_claim_scopes}}` | Scopes after claim | `"notes.read", "notes.write"` | +| `{{post_registration_scopes}}` | Scopes after ID-JAG registration | `"notes.read", "notes.write"` | +| `{{assertion_expiry}}` | Identity assertion expiry (ISO) | `2026-05-22T13:00:00.000Z` | +| `{{claim_ttl}}` | Claim token expiration (ISO) | `2026-05-22T12:00:00.000Z` | +| `{{claim_attempt_ttl}}` | Claim attempt expiration | `2026-05-22T12:10:00.000Z` | +| `{{api_docs_url}}` | Link to full API docs | `https://docs.acme.com/` | + +--- + +## Generation Rules + +1. **Keep the file concise and high-signal** — anything the agent doesn't need to register or operate belongs in main documentation, not in auth.md +2. **Use fenced code blocks with language hints** (`http`, `json`) so agents can extract templates unambiguously +3. **Declare real hostnames in the intro** — resource server and auth server — so the agent knows which host each example targets +4. **Delete sections for unsupported flows** — don't leave empty sections or "N/A" markers +5. **The PRM is authoritative** — if anything in auth.md conflicts with the PRM, the PRM wins +6. **Token exchange is always required** — registration never returns an access_token directly; always returns an identity_assertion that must be exchanged at `/oauth2/token` diff --git a/.github/skills/auth-md/references/validation-rules.md b/.github/skills/auth-md/references/validation-rules.md new file mode 100644 index 0000000..05fbb08 --- /dev/null +++ b/.github/skills/auth-md/references/validation-rules.md @@ -0,0 +1,162 @@ +# Validation Rules + +Complete ruleset for validating `auth.md` files against the protocol specification (v2, June 2026). + +--- + +## Level: Basic (Offline) + +### Structure Rules + +| ID | Rule | Error Message | Severity | +|----|------|---------------|----------| +| S01 | Document starts with `# auth.md` heading | Missing required top-level heading `# auth.md` | 🔴 | +| S02 | Contains `## Step 1 — Discover` section | Missing required section: Step 1 — Discover | 🔴 | +| S03 | Contains `## Step 2 — Pick a method` section | Missing required section: Step 2 — Pick a method | 🔴 | +| S04 | Contains `## Step 3 — Register` section | Missing required section: Step 3 — Register | 🔴 | +| S05 | Contains `## Step 4 — Claim ceremony` section (if service_auth or anonymous with claim) | Missing required section: Step 4 — Claim ceremony (required when service_auth or anonymous claim is supported) | 🔴 | +| S06 | Contains `## Step 5 — Exchange the assertion` section | Missing required section: Step 5 — Exchange the assertion | 🔴 | +| S07 | Contains `## Step 6 — Use the access_token` section | Missing required section: Step 6 — Use the access_token | 🔴 | +| S08 | Contains `## Errors` section | Missing required section: Errors | 🔴 | +| S09 | Contains `## Revocation` section | Missing required section: Revocation | 🔴 | +| S10 | Sections appear in correct order (Step 1 → 2 → 3 → 4 → 5 → 6 → Errors → Revocation) | Sections are out of order. Expected sequence: Step 1, 2, 3, 4, 5, 6, Errors, Revocation | 🟡 | +| S11 | Intro declares real hostnames (resource server and auth server) | Intro should declare real hostnames for resource and auth servers | 🟢 | + +### Field Rules + +| ID | Rule | Error Message | Severity | +|----|------|---------------|----------| +| F01 | Step 1 contains at least one fenced JSON block with `resource` field | Step 1 must include Protected Resource Metadata JSON with `resource` field | 🔴 | +| F02 | PRM JSON contains `resource_name` | Missing `resource_name` in PRM — agents need this for consent prompts | 🟡 | +| F03 | PRM JSON contains `resource_logo_uri` | Missing `resource_logo_uri` — recommended for consent UX | 🟢 | +| F04 | JSON metadata contains `authorization_servers` array | Missing `authorization_servers` — agents can't discover the auth endpoint | 🟡 | +| F05 | JSON metadata contains `scopes_supported` array with ≥1 scope | Missing or empty `scopes_supported` | 🟡 | +| F06 | AS metadata contains `agent_auth` block | Missing `agent_auth` block in Authorization Server metadata | 🔴 | +| F07 | `agent_auth` contains `identity_endpoint` | Missing `identity_endpoint` in agent_auth block | 🔴 | +| F08 | `agent_auth` contains `identity_types_supported` with ≥1 type | Missing or empty `identity_types_supported` | 🔴 | +| F09 | If `identity_assertion` in identity_types_supported, `assertion_types_supported` must exist | Declared `identity_assertion` support but missing `assertion_types_supported` | 🟡 | +| F10 | Step 3 contains at least one `POST /agent/identity` request example | Step 3 must include at least one registration request example | 🔴 | +| F11 | Errors section contains a table with `Code`, `Where`, and `What to do` columns | Errors section must contain a table with Code, Where, and What to do columns | 🟡 | +| F12 | `agent_auth` contains `skill` pointing to auth.md URL | Missing `skill` field in agent_auth — recommended for discoverability | 🟢 | +| F13 | `agent_auth` contains `events_supported` | Missing `events_supported` — recommended for revocation support | 🟢 | +| F14 | AS metadata contains `token_endpoint` | Missing `token_endpoint` — required for token exchange and claim polling | 🔴 | +| F15 | AS metadata contains `revocation_endpoint` | Missing `revocation_endpoint` — required for credential-layer revocation | 🟡 | +| F16 | AS metadata contains `grant_types_supported` with both required URNs | Missing or incomplete `grant_types_supported` — must include jwt-bearer and claim grant URNs | 🟡 | +| F17 | If `service_auth` or `anonymous` in identity_types_supported, `claim_endpoint` must exist | service_auth/anonymous requires `claim_endpoint` for ceremony initiation | 🟡 | +| F18 | `agent_auth` contains `events_endpoint` | Missing `events_endpoint` — recommended for registration-layer revocation | 🟢 | +| F19 | Step 5 contains `POST /oauth2/token` with jwt-bearer grant | Step 5 must document the token exchange via /oauth2/token | 🔴 | + +### Consistency Rules + +| ID | Rule | Error Message | Severity | +|----|------|---------------|----------| +| C01 | Flows documented in Step 3 match `identity_types_supported` in metadata | Mismatch: Step 3 documents flows not declared in metadata (or vice versa) | 🟡 | +| C02 | If only identity_assertion flow without claim: Step 4 may be absent | Step 4 present but only identity_assertion flow without claim is supported | 🟡 | +| C03 | `identity_endpoint` path matches the POST path in Step 3 examples | Registration endpoint in metadata doesn't match the POST path in Step 3 | 🟡 | +| C04 | `claim_endpoint` path matches the POST path in Step 4 examples (if present) | Claim endpoint in metadata doesn't match the POST path in Step 4 | 🟡 | +| C05 | Scopes in response examples are subset of `scopes_supported` | Response example contains scopes not listed in `scopes_supported` | 🟡 | +| C06 | `resource` URL is consistent across all JSON blocks | Different `resource` URLs found in metadata blocks — must be consistent | 🟡 | +| C07 | All URLs use HTTPS scheme | Non-HTTPS URL found — all endpoints must use HTTPS | 🟡 | +| C08 | Error codes in table match standard protocol error codes (see list below) | Non-standard error code found | 🟡 | +| C09 | `aud` in ID-JAG examples matches the `resource` from PRM | ID-JAG `aud` example doesn't match the resource URL | 🟡 | +| C10 | `assertion_types_supported` includes types used in Step 3 examples | Step 3 uses assertion types not declared in metadata | 🟡 | +| C11 | `token_endpoint` in metadata matches the `/oauth2/token` path used in Steps 4c and 5 | Token endpoint in metadata doesn't match the POST path in Steps 4c/5 | 🟡 | +| C12 | `grant_types_supported` includes `urn:ietf:params:oauth:grant-type:jwt-bearer` | Missing jwt-bearer grant in grant_types_supported — required for token exchange | 🟡 | +| C13 | `grant_types_supported` includes `urn:workos:agent-auth:grant-type:claim` (if claim ceremony exists) | Missing claim grant in grant_types_supported — required for ceremony polling | 🟡 | + +### Format Rules + +| ID | Rule | Error Message | Severity | +|----|------|---------------|----------| +| X01 | All JSON in fenced code blocks is valid JSON | Invalid JSON in fenced code block at section: {section} | 🟡 | +| X02 | HTTP request examples use valid HTTP method + path | Invalid HTTP request format. Expected: METHOD /path | 🟡 | +| X03 | No unreplaced placeholder patterns (`{{...}}`, ``, `[YOUR_...]`) | Unreplaced placeholder found: {placeholder} | 🟡 | +| X04 | Fenced code blocks have language hint (`http`, `json`) | Code block missing language hint — agents use these to identify request shapes | 🟢 | + +--- + +## Level: Full (Live) + +All Basic rules, plus: + +### Endpoint Rules + +| ID | Rule | Error Message | Severity | +|----|------|---------------|----------| +| E01 | `GET {base_url}/.well-known/oauth-protected-resource` returns 200 with valid JSON | Protected Resource Metadata endpoint not reachable or returns invalid response | 🔴 | +| E02 | PRM response contains `authorization_servers` pointing to AS with `agent_auth` block | No `agent_auth` block discoverable through PRM → AS metadata chain | 🔴 | +| E03 | `GET {auth_server}/.well-known/oauth-authorization-server` returns 200 with valid JSON | Authorization Server metadata endpoint not reachable | 🔴 | +| E04 | AS metadata `agent_auth` block matches declarations in auth.md | Live AS metadata `agent_auth` block differs from auth.md declarations | 🟡 | +| E05 | `identity_endpoint` accepts POST (returns 400/401/422, not 404/405) | Registration endpoint returns 404 or 405 — not implemented | 🔴 | +| E06 | `claim_endpoint` accepts POST (if service_auth or anonymous supported) | Claim endpoint returns 404 or 405 — not implemented | 🔴 | +| E07 | `token_endpoint` accepts POST (returns 400/401, not 404/405) | Token endpoint returns 404 or 405 — not implemented | 🔴 | +| E08 | `revocation_endpoint` accepts POST (returns 200/400, not 404/405) | Revocation endpoint returns 404 or 405 — not implemented | 🟡 | +| E09 | API base URL returns 401 with `WWW-Authenticate` header containing `resource_metadata` | API does not return WWW-Authenticate header with resource_metadata on 401 | 🟡 | + +--- + +## Standard Protocol Error Codes + +Complete list of error codes the Errors table should cover (per supported flows): + +### Registration Endpoint (`/agent/identity`) +- `anonymous_not_enabled` +- `service_auth_not_enabled` +- `issuer_not_enabled` +- `invalid_request` (body shape, missing claims, signature, jti, aud, unverified identity) +- `interaction_required` (401) — ID-JAG matched account but no (iss,sub) delegation +- `login_required` (401) — auth_time missing or older than max_age +- `rate_limited` (429) + +### Claim Endpoint (`/agent/identity/claim`) +- `invalid_claim_token` +- `claimed_or_in_flight` +- `claim_expired` + +### Token Endpoint (`/oauth2/token`) +- `invalid_grant` — assertion expired/revoked/replayed +- `invalid_client` +- `unsupported_grant_type` +- `authorization_pending` — claim polling, user hasn't completed +- `expired_token` — user_code window closed +- `slow_down` — polling too fast + +### All Endpoints +- `rate_limited` (429) + +--- + +## Validation Report Format + +```markdown +# Validation Report — auth.md + +**File:** {path_or_url} +**Level:** {basic|full} +**Date:** {timestamp} + +## Summary + +- ✅ {n} rules passed +- ❌ {n} rules failed +- ⚠️ {n} warnings + +## Structure + +| Status | ID | Rule | +|--------|-----|-------| +| ✅ | S01 | Heading `# auth.md` present | +| ❌ | S06 | Step 5 section missing | + +## Fields +... + +## Consistency +... + +## Format +... + +## Endpoints (full only) +... +``` diff --git a/.github/skills/coolify-operator/.env.example b/.github/skills/coolify-operator/.env.example new file mode 100644 index 0000000..e45171c --- /dev/null +++ b/.github/skills/coolify-operator/.env.example @@ -0,0 +1,2 @@ +COOLIFY_KEY= +COOLIFY= \ No newline at end of file diff --git a/.github/skills/coolify-operator/SKILL.md b/.github/skills/coolify-operator/SKILL.md new file mode 100644 index 0000000..6f74121 --- /dev/null +++ b/.github/skills/coolify-operator/SKILL.md @@ -0,0 +1,519 @@ +--- +name: coolify-operator +description: Master Coolify operator for self-hosted deployment platform. Use when the user mentions 'coolify', 'deploy on coolify', 'list/restart/redeploy applications', 'view coolify logs', 'coolify API/CLI', 'manage coolify servers/databases/apps', or 'coolify context'. Automates deployments and management via REST API or official CLI. +metadata: + author: ft.ia.br + version: "1.1" + date: 2026-03-08 + license: MIT + category: ci-cd-and-deployment +--- + +# Coolify Operator + +Skill for operating Coolify instances through the **REST API** or **official CLI**. Coolify is a self-hosted open-source platform alternative to Heroku/Vercel/Netlify for deploying applications, databases, and services. + +## When to use this skill + +- Connect to Coolify instances (via API or CLI) +- List and manage applications, services, databases, and servers +- Deploy, restart, or stop applications +- View logs and deployment status +- Manage environment variables +- Operate multiple Coolify instances (contexts) +- Troubleshoot Coolify connection issues + +## Fundamental concepts + +### Authentication + +**REST API:** +- Base endpoint: `https://YOUR-HOST/api/v1` (always with `/api/v1` at the end) +- Authentication: `Authorization: Bearer YOUR_TOKEN` +- Token obtained at: Coolify Dashboard → Keys & Tokens → API Tokens + +**CLI:** +- Installs contexts that store HOST + TOKEN +- HOST in context is WITHOUT `/api/v1` (just the base URL) +- CLI adds `/api/v1` automatically + +### Configuration with pipe in token + +⚠️ **IMPORTANT**: Coolify tokens often contain `|` (e.g., `3|abc123...`). Never use `source .env` as this breaks in the shell. + +**Safe .env reading:** +```bash +COOLIFY_KEY=$(sed -n 's/^COOLIFY_KEY=//p' .env) +COOLIFY=$(sed -n 's/^COOLIFY=//p' .env) +``` + +**Expected .env format:** +```bash +COOLIFY_KEY=3|abc123def456... +COOLIFY=http://192.168.1.XXX:8000/api/v1 +``` + +### UUIDs + +Coolify uses UUIDs to identify resources: +- Applications: `app-uuid` +- Servers: `server-uuid` +- Databases: `db-uuid` +- Services: `service-uuid` + +## CLI Operations + +### Initial setup + +```bash +# Read token from .env safely +COOLIFY_KEY=$(sed -n 's/^COOLIFY_KEY=//p' .env) + +# Add context (URL WITHOUT /api/v1) +coolify context add -d -f my-coolify http://192.168.1.XXX:8000 "$COOLIFY_KEY" + +# Use the context +coolify context use my-coolify + +# Verify connection +coolify context verify + +# Check API version +coolify context version +``` + +### Context management + +```bash +# List contexts +coolify context list + +# Add multiple contexts +coolify context add prod https://prod.coolify.io "$PROD_TOKEN" --default +coolify context add staging https://staging.coolify.io "$STAGING_TOKEN" +coolify context add dev https://dev.coolify.io "$DEV_TOKEN" + +# Switch default context +coolify context use staging + +# Use specific context in a command +coolify --context=prod app list + +# Update token for a context +coolify context set-token prod new-token-here + +# Remove context +coolify context delete dev +``` + +### Application operations + +```bash +# List all applications +coolify app list + +# View application details +coolify app get + +# --- LIFECYCLE --- +# Start (deploy) application +coolify app start + +# Stop application +coolify app stop + +# Restart application +coolify app restart + +# --- LOGS --- +# View application logs +coolify app logs + +# --- ENVIRONMENT VARIABLES --- +# List environment variables +coolify app env list + +# Create environment variable +coolify app env create --key API_KEY --value secret123 + +# Sync environment variables from .env file +coolify app env sync --file .env +coolify app env sync --file .env.production --build-time --preview +``` + +### Server operations + +```bash +# List servers +coolify server list + +# View server details (including resources) +coolify server get --resources + +# Add new server (with validation) +coolify server add myserver 192.168.1.100 --validate +``` + +### Team operations + +```bash +# List available teams +coolify team list + +# View current team +coolify team current + +# List team members +coolify team members list +``` + +### Global flags + +```bash +# Specify context +coolify --context ... + +# Override host +coolify --host ... + +# Direct token (bypasses context) +coolify --token ... + +# Output format (table, json, pretty) +coolify --format json ... + +# Show sensitive data +coolify -s ... +coolify --show-sensitive ... + +# Force operation +coolify -f ... +coolify --force ... + +# Debug mode +coolify --debug ... +``` + +## REST API Operations + +### Authentication and testing + +```bash +# Read credentials from .env safely +COOLIFY_KEY=$(sed -n 's/^COOLIFY_KEY=//p' .env) +COOLIFY=$(sed -n 's/^COOLIFY=//p' .env) + +# Test connection +curl -sS -i \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/version" + +# Expected result: HTTP 200 + {"version": "4.0.0-beta.xxx"} +``` + +### Applications + +```bash +# List applications +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/applications" + +# View application details +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/applications/{uuid}" + +# Start (deploy) application +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/applications/{uuid}/start" + +# Query param flags: +# ?force=true - Force rebuild +# ?instant_deploy=true - Skip queue + +# Stop application +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/applications/{uuid}/stop" + +# Restart application +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/applications/{uuid}/restart" + +# Example response: +# { +# "message": "Restart request queued.", +# "deployment_uuid": "doogksw" +# } +``` + +### Deployments + +```bash +# List all ongoing deployments +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/deployments" + +# List deployments for an application (with pagination) +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/deployments/applications/{uuid}?skip=0&take=10" +``` + +### Servers + +```bash +# List servers +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/servers" + +# View server details +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/servers/{uuid}" +``` + +### Databases + +```bash +# List databases +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/databases" + +# Start database +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/databases/{uuid}/start" + +# Stop database +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/databases/{uuid}/stop" + +# Restart database +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/databases/{uuid}/restart" +``` + +### Services + +```bash +# Restart service +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/services/{uuid}/restart" + +# Query params: +# ?latest=true - Pull latest images + +# Response: +# {"message": "Service restaring request queued."} +``` + +## Troubleshooting + +### Error: 403 "You are not allowed to access the API" + +**Cause:** Invalid token or no permission for the instance. + +**Solution:** +1. Ask the user to verify in the instance at /settings/advanced whether the API is enabled and the client IP is allowed +2. Regenerate token at: Dashboard → Keys & Tokens → API Tokens +3. Update `.env` or CLI context +4. Verify that the correct instance is being used + +### Error: 401 "Unauthenticated" + +**Cause:** Incorrect authentication header or token not sent. + +**Solution:** +```bash +# Verify that Bearer is being used (not just "Token") +Authorization: Bearer YOUR_TOKEN + +# CLI: verify context +coolify context verify +``` + +### Error: 404 on context verify + +**Cause:** CLI context URL is incorrect (probably with `/api/v1` in the wrong place). + +**Solution:** +```bash +# CLI context must have URL WITHOUT /api/v1 +coolify context add my-coolify http://192.168.1.XXX:8000 "$TOKEN" + +# Direct API must have URL WITH /api/v1 +COOLIFY=http://192.168.1.XXX:8000/api/v1 +``` + +### Cloudflare Tunnel is not the cause + +If the API already returns valid JSON from Coolify (even if it's an auth error), the Cloudflare tunnel is working. The problem is authentication, not connectivity. + +### Token with pipe (|) breaks shell + +```bash +# ❌ WRONG - breaks with pipe +source .env + +# ✅ CORRECT - safe reading +COOLIFY_KEY=$(sed -n 's/^COOLIFY_KEY=//p' .env) +``` + +## Common workflows + +### Full deploy of a new application + +```bash +# 1. Connect to Coolify +coolify context add prod https://coolify.your-domain.com "$TOKEN" --default +coolify context verify + +# 2. List available servers +coolify server list + +# 3. Deploy (via dashboard UI or API) +# Note: app creation is better via UI, API is for operations + +# 4. List apps to get UUID +coolify app list + +# 5. Start the application +coolify app start + +# 6. View deploy logs +coolify app logs +``` + +### Redeploy with force rebuild + +```bash +# Via CLI +coolify app restart + +# Via API with forced rebuild +curl -sS \ + -H "Authorization: Bearer $COOLIFY_KEY" \ + "$COOLIFY/applications/{uuid}/start?force=true" +``` + +### Update environment variables + +```bash +# Option 1: Sync from file +coolify app env sync --file .env.production + +# Option 2: Create individually +coolify app env create --key API_URL --value https://api.example.com +coolify app env create --key API_KEY --value secret123 + +# 3. Restart to apply changes +coolify app restart +``` + +### Multi-environment monitoring + +```bash +# Production +coolify --context=prod app list +coolify --context=prod app logs + +# Staging +coolify --context=staging app list +coolify --context=staging app logs + +# Development +coolify --context=dev app list +coolify --context=dev server list +``` + +## Important resources + +### API response structure + +**Application:** +```json +{ + "id": 123, + "uuid": "app-uuid-123", + "name": "my-app", + "fqdn": "app.example.com", + "status": "running", + "git_repository": "https://github.com/user/repo", + "git_branch": "main", + "git_commit_sha": "abc123", + "build_pack": "nixpacks", + "ports_exposes": "3000", + "health_check_enabled": true, + "environment_id": 1, + "destination_id": 1 +} +``` + +**Server:** +```json +{ + "id": 1, + "uuid": "server-uuid-123", + "name": "main-server", + "ip": "192.168.1.100", + "user": "root", + "port": 22, + "settings": { + "is_reachable": true, + "is_usable": true, + "concurrent_builds": 1 + } +} +``` + +**Deployment:** +```json +{ + "id": 456, + "uuid": "deployment-uuid-456", + "status": "finished", + "deployment_uuid": "dep-123", + "application_id": 123 +} +``` + +## Usage tips + +1. **Always verify UUIDs**: Use `coolify app list` or API to confirm UUIDs before operations +2. **Contexts for multi-environment**: Configure one context for each environment (dev/staging/prod) +3. **Real-time logs**: Use `coolify app logs ` during deploys +4. **Force rebuild when needed**: `?force=true` on start ensures complete rebuild +5. **Token security**: Never commit tokens. Use `.env` with `.gitignore` +6. **JSON format for scripts**: Use `--format json` in CLI for parsing with `jq` + +## Quality Checklist + +Before executing any operation, verify: + +- [ ] `.env` file present with correct `COOLIFY_KEY` and `COOLIFY` +- [ ] Token read safely (using `sed` or appropriate method, not `source`) +- [ ] Correct context selected (`coolify context use `) +- [ ] Connection verified (`coolify context verify`) +- [ ] UUIDs confirmed before destructive operations +- [ ] Endpoint URL correct (API has `/api/v1`, CLI context does not) +- [ ] Authentication headers included in API calls (`Authorization: Bearer `) +- [ ] Error handling implemented (401, 403, 404, 500) +- [ ] Logs checked in case of deploy failure +- [ ] Critical operations (delete, stop) executed with confirmation + +## References + +- **Official documentation**: https://coolify.io/docs +- **API Reference**: https://coolify.io/docs/api-reference +- **CLI GitHub**: https://github.com/coollabsio/coolify-cli +- **Coolify Core**: https://github.com/coollabsio/coolify diff --git a/.github/skills/coolify-operator/evals/evals.json b/.github/skills/coolify-operator/evals/evals.json new file mode 100644 index 0000000..63fd7ed --- /dev/null +++ b/.github/skills/coolify-operator/evals/evals.json @@ -0,0 +1,61 @@ +{ + "skill_name": "coolify-operator", + "evals": [ + { + "id": 1, + "name": "configurar-contexto-inicial", + "prompt": "I need to set up my Coolify connection. I have the token '3|AbCdEf123456' and the URL is 'http://192.168.1.106:8000'. How do I connect using the CLI? I want it to be the default context named 'producao'.", + "expected_output": "CLI commands to add Coolify context, including safe token reading (avoiding issues with pipe |), adding context with URL without /api/v1, verifying connection and testing", + "files": [] + }, + { + "id": 2, + "name": "listar-e-restart-app", + "prompt": "Can you help me restart the application 'meu-site-nextjs' on Coolify? I don't know its UUID, so I need to list first. Then I want to do a restart forcing rebuild because I updated some environment variables.", + "expected_output": "Sequence of commands: list apps to find UUID, then restart the specific application, with force rebuild flag if necessary", + "files": [] + }, + { + "id": 3, + "name": "troubleshooting-403", + "prompt": "I'm trying to use the Coolify API but I'm getting error 403 'You are not allowed to access the API'. I already checked the token in the dashboard and it seems correct. What could it be?", + "expected_output": "Explanation of possible causes for error 403, steps to verify token, regenerate if necessary, troubleshooting checklist, and commands to test the connection", + "files": [] + }, + { + "id": 4, + "name": "multi-ambiente-deploy", + "prompt": "I have 3 Coolify environments (dev, staging and prod). I want to deploy the same app to all 3 environments. How do I configure the contexts and deploy to each one? The URLs are: dev at http://dev.coolify.local:8000, staging at https://staging.coolify.empresa.com and prod at https://coolify.empresa.com", + "expected_output": "Commands to configure multiple CLI contexts, add each environment, switch between contexts and deploy/operate on each one", + "files": [] + }, + { + "id": 5, + "name": "atualizar-env-vars", + "prompt": "I need to update the environment variables for my app on Coolify. I have a local .env.production file with the new configs. The app UUID is 'app-abc-123'. How do I sync and apply the changes?", + "expected_output": "Commands to sync environment variables from file using CLI, followed by application restart to apply the changes", + "files": [] + }, + { + "id": 6, + "name": "monitorar-logs-deploy", + "prompt": "I just deployed the application with UUID 'app-xyz-789' but it seems stuck. How do I view real-time logs to debug what's happening?", + "expected_output": "CLI command to view application logs in real time, or alternative API commands", + "files": [] + }, + { + "id": 7, + "name": "erro-token-pipe", + "prompt": "I put my Coolify token in .env like this: COOLIFY_KEY=3|abc123def456 but when I 'source .env' in bash and try to use it I get an error. What's wrong?", + "expected_output": "Explanation of the pipe (|) problem in the token with source .env, and solution using sed for safe file reading", + "files": [] + }, + { + "id": 8, + "name": "listar-databases-e-restart", + "prompt": "My postgres on Coolify is having issues. I need to list all databases and then restart the one that has 'postgres-main' in the name.", + "expected_output": "Commands to list databases via CLI or API, find the correct UUID and execute restart of the specific database", + "files": [] + } + ] +} diff --git a/.github/skills/design-md-validator/SKILL.md b/.github/skills/design-md-validator/SKILL.md new file mode 100644 index 0000000..9a20f16 --- /dev/null +++ b/.github/skills/design-md-validator/SKILL.md @@ -0,0 +1,230 @@ +--- +name: design-md-validator +description: > + Validate DESIGN.md files against the official Google specification using the + `@google/design.md` CLI linter. Works with local files and remote URLs. + Use when the user wants to lint a DESIGN.md, check spec compliance, find + broken token references, verify WCAG contrast ratios, diff two versions, + export tokens to Tailwind or DTCG format, or audit a design system file + for structural correctness. Trigger on mentions of "validate DESIGN.md", + "lint DESIGN.md", "check my design.md", "design.md spec compliance", + "WCAG contrast check", "broken token references", "design token validation", + "export design tokens", "diff design systems", "design.md audit", + "@google/design.md", "npx design.md lint", "design system validation", + "frontmatter tokens", or any request to verify a DESIGN.md file. +metadata: + author: https://ft.ia.br + version: "1.1" + date: 2026-07-18 + repository: https://github.com/fabricioctelles/skills + license: Apache 2.0 + category: product-verification + upstream: + spec: https://github.com/google-labs-code/design.md + cli: https://www.npmjs.com/package/@google/design.md + stitch-skills: https://github.com/google-labs-code/stitch-skills +--- + +# design-md-validator + +Validate, lint, diff, and export DESIGN.md files using the official Google +`@google/design.md` CLI. Always uses the latest published version from npm — +no vendored copy, always up-to-date with the spec. + +--- + +## When to Use + +| Trigger | Action | +|---|---| +| User has a DESIGN.md file and wants validation | `lint` | +| User wants to compare two versions | `diff` | +| User wants to export tokens to Tailwind/DTCG | `export` | +| User wants to see the current spec | `spec` | +| User shares a URL to a raw DESIGN.md | Fetch → `lint` | +| User asks "is my design.md valid?" | `lint` + interpret findings | + +--- + +## Core Commands + +All commands use `npx @google/design.md` to ensure the latest version is always +used. Never install globally — `npx` resolves from the public npm registry. + +### Lint (validate) + +```bash +npx @google/design.md lint DESIGN.md +``` + +Output: JSON with `findings[]` and `summary { errors, warnings, infos }`. +Exit code 1 if errors found, 0 otherwise. + +### Diff (compare versions) + +```bash +npx @google/design.md diff DESIGN.md DESIGN-v2.md +``` + +Output: JSON with token-level changes (added, removed, modified) and regression flag. +Exit codes: `0` no regression, `1` regression (errors in "after" > errors in "before"), +`2` input failure (file not found or unreadable). + +### Export (to other formats) + +```bash +# Tailwind v3 config +npx @google/design.md export --format json-tailwind DESIGN.md + +# Tailwind v4 CSS theme +npx @google/design.md export --format css-tailwind DESIGN.md + +# W3C Design Tokens (DTCG) +npx @google/design.md export --format dtcg DESIGN.md + +# CSS custom properties (optional --prefix) +npx @google/design.md export --format css-vars DESIGN.md +npx @google/design.md export --format css-vars --prefix ds DESIGN.md +``` + +`css-vars` is available on main, ships in the next release — if the CLI +rejects the format, npm is still on 0.3.0. + +### Spec (output the format specification) + +```bash +npx @google/design.md spec +npx @google/design.md spec --rules +npx @google/design.md spec --rules-only --format json +``` + +--- + +## Workflow + +### 1. Obtain the DESIGN.md + +**Local file:** +```bash +npx @google/design.md lint ./DESIGN.md +``` + +**From URL (fetch first):** +```bash +curl -sL > /tmp/DESIGN.md && npx @google/design.md lint /tmp/DESIGN.md +``` + +**From stdin:** +```bash +cat DESIGN.md | npx @google/design.md lint - +``` + +### 2. Discover the Active Rule Set + +Before interpreting anything, check which rules the installed CLI actually +runs — the rule set changes between releases, and the bundled references are +a fallback, not the authoritative source: + +```bash +npx @google/design.md spec --rules-only --format json +``` + +### 3. Run Lint + +```bash +npx @google/design.md lint --format json DESIGN.md +``` + +### 4. Interpret Findings + +Parse the JSON output and report to the user: + +| Severity | Meaning | Action | +|---|---|---| +| `error` | Spec violation — file is invalid | Must fix | +| `warning` | Best practice violation — file is valid but suboptimal | Should fix | +| `info` | Informational — suggestions for improvement | Nice to fix | + +### 5. Provide Actionable Fixes + +For each finding, explain: +1. What the rule checks +2. Why it matters +3. How to fix it with a concrete code example + +### 6. Re-validate After Fixes + +After applying fixes, re-run lint to confirm the file passes. + +--- + +## Linting Rules Reference + +Load `references/linting-rules.md` for the complete rule table when providing +detailed explanations of lint failures. + +--- + +## Token Schema Quick Reference + +Load `references/token-schema.md` for the complete YAML frontmatter schema +when helping users author or fix their frontmatter tokens. + +--- + +## Windows Compatibility + +On Windows/PowerShell, the `.md` suffix in the bin name collides with Markdown +file associations. Use the `designmd` alias: + +```bash +npx -p @google/design.md designmd lint DESIGN.md +``` + +--- + +## Related Official Skills + +| Skill | Source | Purpose | +|---|---|---| +| `stitch-design-taste` | google-labs-code/stitch-skills | Generates DESIGN.md for Google Stitch | +| `design-md` (Stitch plugin) | google-labs-code/stitch-skills | Analyzes Stitch projects → DESIGN.md | +| `taste-design` (MCP) | mcpservers.org | MCP server for Stitch design extraction | + +Install the official Stitch skill for generation: +```bash +npx skills add https://github.com/google-labs-code/stitch-skills --skill design-md +``` + +--- + +## Anti-Patterns + +- Never vendor or cache the CLI — always use `npx` for latest spec +- Never manually parse YAML frontmatter when the linter can do it +- Never guess at contrast ratios — let the linter compute them +- Never assume section order is correct — let the linter verify +- Never skip re-validation after fixes +- Never assume the rule set from memory or from the bundled references — + confirm it at runtime with `spec --rules-only` + +--- + +## Example Session + +``` +User: validate my DESIGN.md + +Agent: +1. Reads the file +2. Runs: npx @google/design.md spec --rules-only --format json +3. Runs: npx @google/design.md lint --format json DESIGN.md +4. Parses output +5. Reports: + - summary: { errors: 0, warnings: 2, infos: 1 } + - WARNING: contrast-ratio — button textColor on backgroundColor is 3.8:1 (needs 4.5:1) + - WARNING: orphaned-tokens — color "accent-muted" defined but never used + - INFO: token-summary — 5 colors, 3 typography, 2 rounded, 2 spacing +6. Suggests fixes with code +7. Re-runs lint to confirm +``` diff --git a/.github/skills/design-md-validator/references/linting-rules.md b/.github/skills/design-md-validator/references/linting-rules.md new file mode 100644 index 0000000..1428400 --- /dev/null +++ b/.github/skills/design-md-validator/references/linting-rules.md @@ -0,0 +1,66 @@ +# Linting Rules — @google/design.md + +> Generated against npm 0.3.0 + main as of 2026-07-18. Authoritative source at +> runtime: `npx @google/design.md spec --rules-only`. + +The linter runs nine rules against a parsed DESIGN.md in the published npm +0.3.0 release; a tenth rule (`token-like-ignored`) has merged on main and +ships in the next release. Each rule produces findings at a fixed severity +level. + +## Rules Table + +| Rule | Severity | What it checks | +|---|---|---| +| `broken-ref` | error | Token references (`{colors.primary}`) that don't resolve to any defined token | +| `missing-primary` | warning | Colors are defined but no `primary` color exists — agents will auto-generate one | +| `contrast-ratio` | warning | Component `backgroundColor`/`textColor` pairs below WCAG AA minimum (4.5:1) | +| `orphaned-tokens` | warning | Color tokens defined but never referenced by any component | +| `token-summary` | info | Summary of how many tokens are defined in each section | +| `missing-sections` | info | Optional sections (spacing, rounded) absent when other tokens exist | +| `missing-typography` | warning | Colors are defined but no typography tokens exist — agents will use default fonts | +| `section-order` | warning | Sections appear out of the canonical order defined by the spec | +| `unknown-key` | warning | A top-level YAML key looks like a typo of a known schema key (e.g. `colours:` → `colors:`) | +| `token-like-ignored` | warning | **Next release (on main since 2026-06-15, not in npm 0.3.0).** Warns when a top-level YAML key looks like a design-token map but is not part of the recognized export schema and will be silently ignored | + +## Section Order (canonical) + +Sections use `##` headings. They can be omitted, but those present must appear +in this order: + +| # | Section | Aliases | +|---|---|---| +| 1 | Overview | Brand & Style | +| 2 | Colors | | +| 3 | Typography | | +| 4 | Layout | Layout & Spacing | +| 5 | Elevation & Depth | Elevation | +| 6 | Shapes | | +| 7 | Components | | +| 8 | Do's and Don'ts | | + +## Consumer Behavior for Unknown Content + +| Scenario | Behavior | +|---|---| +| Unknown section heading | Preserve; do not error | +| Unknown color token name | Accept if value is valid | +| Unknown typography token name | Accept as valid typography | +| Unknown component property | Accept with warning | +| Duplicate section heading | Error; reject the file | + +## Exit Codes + +- `0` — No errors (warnings/info may be present) +- `1` — Errors found (file is invalid per spec) + +## Programmatic API + +```typescript +import { lint } from '@google/design.md/linter'; + +const report = lint(markdownString); +console.log(report.findings); // Finding[] +console.log(report.summary); // { errors, warnings, infos } +console.log(report.designSystem); // Parsed DesignSystemState +``` diff --git a/.github/skills/design-md-validator/references/token-schema.md b/.github/skills/design-md-validator/references/token-schema.md new file mode 100644 index 0000000..8b2d80e --- /dev/null +++ b/.github/skills/design-md-validator/references/token-schema.md @@ -0,0 +1,156 @@ +# Token Schema — DESIGN.md Spec (version alpha) + +The YAML front matter in a DESIGN.md file contains machine-readable design +tokens. These are the normative values that agents use to generate code. + +## Top-Level Schema + +```yaml +version: # optional, current: "alpha" +name: # required +description: # optional +colors: + : +typography: + : +rounded: + : +spacing: + : +components: + : + : +``` + +## Token Types + +| Type | Format | Example | +|---|---|---| +| Color | Any CSS color (hex, `rgb()`, `oklch()`, named) | `"#1A1C1E"`, `"oklch(62% 0.18 250)"` | +| Dimension | number + unit (`px`, `em`, `rem`) | `48px`, `-0.02em` | +| Token Reference | `{path.to.token}` | `{colors.primary}` | +| Typography | object with font properties | See below | + +## Typography Object + +```yaml +typography: + h1: + fontFamily: Public Sans + fontSize: 3rem + fontWeight: 700 + lineHeight: 1.2 + letterSpacing: -0.02em + fontFeature: "ss01" # optional + fontVariation: "wght 700" # optional + body-md: + fontFamily: Public Sans + fontSize: 1rem + fontWeight: 400 + lineHeight: 1.5 + label-caps: + fontFamily: Space Grotesk + fontSize: 0.75rem + fontWeight: 500 + letterSpacing: 0.05em +``` + +Required fields per entry: `fontFamily`, `fontSize`. +Optional fields: `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`. + +## Colors + +```yaml +colors: + primary: "#1A1C1E" + secondary: "#6C7278" + tertiary: "#B8422E" + neutral: "#F7F5F2" + on-tertiary: "#FFFFFF" # contrast pair for tertiary +``` + +The `primary` color is expected by the linter. Its absence triggers a +`missing-primary` warning. + +## Rounded (border-radius scale) + +```yaml +rounded: + sm: 4px + md: 8px + lg: 16px +``` + +## Spacing + +```yaml +spacing: + sm: 8px + md: 16px + lg: 32px +``` + +## Components + +Components map a name to a group of sub-token properties: + +```yaml +components: + button-primary: + backgroundColor: "{colors.tertiary}" + textColor: "{colors.on-tertiary}" + rounded: "{rounded.sm}" + padding: 12px + button-primary-hover: + backgroundColor: "{colors.tertiary-container}" +``` + +### Valid Component Properties + +`backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, +`height`, `width`. + +### Variants + +Hover, active, pressed states are separate entries with a related key name: +`button-primary-hover`, `button-primary-active`. + +## Token References + +References use curly braces with dot notation: + +```yaml +components: + card: + backgroundColor: "{colors.neutral}" # resolves to #F7F5F2 + rounded: "{rounded.md}" # resolves to 8px +``` + +Broken references (pointing to undefined tokens) trigger a `broken-ref` error. + +Since npm 0.3.0 (PR #103), token groups support nested sub-levels in the +frontmatter — e.g. `colors.brand.primary` — and references use the full +dotted path to the leaf token: `{colors.brand.primary}`. + +## File Structure Summary + +``` +--- ← YAML front matter start +name: "My Design System" +colors: ... +typography: ... +rounded: ... +spacing: ... +components: ... +--- ← YAML front matter end + +## Overview ← Markdown prose sections +... +## Colors +... +## Typography +... +``` + +The tokens are the normative values. The prose provides context for how to +apply them. diff --git a/.github/skills/geo-optimization/SKILL.md b/.github/skills/geo-optimization/SKILL.md new file mode 100644 index 0000000..2e69c58 --- /dev/null +++ b/.github/skills/geo-optimization/SKILL.md @@ -0,0 +1,132 @@ +--- +name: geo-optimization +description: Optimize content for AI-generated responses and LLM citations (ChatGPT, Perplexity, Google AI Overview, Claude, Gemini). Use when the user mentions 'GEO', 'AEO', 'AI SEO', 'LLM optimization', 'citation rate', 'AI visibility', 'optimize for ChatGPT', 'roundup pages', or wants to audit pages for AI discoverability. Includes terminology alignment, FAQ schemas, and community signal strategies. +metadata: + author: ft.ia.br + version: "1.1" + date: 2026-03-05 + repository: https://github.com/fabricioctelles/skills + license: Apache 2.0 + category: code-quality-and-review +--- + +# GEO Optimization (Generative Engine Optimization) + +## Quick Actions Menu + +Present the following options at the start of the interaction to guide the work: + +1. **Full GEO Audit**: Analyze the current page, deliver a score and a prioritized roadmap. +2. **Roundup Page Builder**: Create a comparison page optimized for LLMs. +3. **Terminology Optimizer**: Generate new titles, metas, and headings aligned with LLM searches. +4. **FAQ + Schema Generator**: Create a complete FAQ set with schema markup. +5. **Community Signal Booster**: Structure a strategy to generate reviews on Product Hunt and Reddit. +6. **Citation Rate Test Kit**: Create 50 ready-made prompts for visibility measurement. +7. **FULL PACKAGE**: Execute all of the above as a complete optimization package using Multi Agents. + +Default to option 1 (Full GEO Audit) when no specific action is requested. + +## GEO Workflow + +Execute the following steps according to the selected action or user need. + +For foundational principles that guide all optimization decisions, consult `references/guiding-principles.md`. + +### 1. Measurement and Tracking (Initial Diagnosis) + +Always begin by evaluating the current state of AI visibility. + +- Create an initial set of 50–100 real prompts to test against ChatGPT, Google AI Overview, Perplexity, and Grok. +- Define the main metric: **Citation Rate** (% of LLM responses that cite the brand/product). + +### 2. Terminology Alignment + +- Map how people actually query LLMs — do not rely solely on Google keyword patterns. +- Adjust titles, meta descriptions, and headings to reflect natural LLM terminology. +- Example: Prefer "AI dictation and speech-to-text software" over "AI dictation apps". + +### 3. Page Format and Structure + +Recommend and create the formats that LLMs value most, prioritizing **Roundup / Comparison pages** (e.g., "The best [category] in 2026"). + +Include in each optimized page: +- Title aligned with LLM terminology. +- 8–12 products with authentic community reviews. +- Comparison table. +- Complete FAQPage schema. +- "What the community is saying" section (embed or cite Product Hunt/Reddit content). + +### 4. Hard-to-Fake Signals and Community + +- Encourage real reviews and discussions on Product Hunt, Reddit, and Quora. + +### 4.5. Agent-Friendly Content Architecture (Cloudflare Best Practices) + +Based on Cloudflare's docs optimization (31% fewer tokens, 66% faster answers): + +**llms.txt Strategy for Large Sites:** +- Do NOT create one massive `llms.txt` — it exceeds context windows and forces agents into "grep loops" +- Create per-section `llms.txt` files (e.g., `/docs/llms.txt`, `/blog/llms.txt`) +- Root `llms.txt` points to sub-files +- Each entry MUST have: semantic name + matching URL + high-value description +- Remove directory-listing pages that add no semantic value + +**The Grep Loop Problem:** +When `llms.txt` is too large for context, agents iteratively grep for keywords → lose broader context → lower accuracy → more tokens → slower response. Solution: fit directories into single context windows. + +**URL Fallbacks with `/index.md`:** +- Make every page available as Markdown at `/index.md` relative to the page URL +- Implement via URL rewrite rule (strip `/index.md`) + header transform (add `Accept: text/markdown`) +- Link to `/index.md` URLs in `llms.txt` for agents that don't send Accept header + +**Hidden Agent Directives:** +- Add invisible instructions in HTML for agents that don't negotiate markdown: +```html + +``` +- Strip this directive from the Markdown version to avoid recursion + +**Redirects for AI Training Crawlers:** +- Identify AI training crawlers (GPTBot, Google-Extended, etc.) +- Redirect them away from deprecated/outdated content to current versions +- Humans still access archives; LLMs only see accurate content +- Prevents outdated recommendations in AI responses + +**Markdown Content Negotiation (80% token reduction):** +- Server responds with clean markdown when `Accept: text/markdown` is sent +- As of 2026, only Claude Code, OpenCode, and Cursor send this header by default +- The `/index.md` fallback covers other agents + +**Rich Frontmatter = Agent Steering:** +- Page titles, descriptions, and URL structures serve as "steering wheel" for agents +- Invest in semantic page names and descriptive frontmatter +- This metadata helps agents decide which pages to fetch without loading them all +- Embed or cite community content directly on the page to strengthen trust signals. + +### 5. Technical and Structured Data + +Verify and implement the required technical elements: +- Add JSON-LD + FAQPage schema. +- Add Product schema where applicable. +- Confirm `robots.txt` allows AI crawlers (do not block Perplexity, ChatGPT, etc.). + +### 6. Continuous Monitoring and Iteration + +- Establish a routine of weekly tests or tests triggered by model updates. +- Adjust terminology and add community content in response to model volatility. + +## Quality Checklist + +Before delivering any output, verify: + +- [ ] Citation Rate baseline is defined or a test kit has been created. +- [ ] Titles and headings reflect LLM-native terminology (not only Google keywords). +- [ ] Page structure includes comparison table and FAQPage schema. +- [ ] Community signals (Product Hunt, Reddit) are referenced or embedded. +- [ ] `robots.txt` does not block major AI crawlers. +- [ ] JSON-LD schemas are present and valid. +- [ ] Monitoring cadence is defined (weekly or post-model-update). +- [ ] No purely self-promotional listicles were produced (LLMs detect and deprioritize them). +- [ ] Bot-blocking risks have been flagged (e.g., Perplexity has temporarily blocked some platforms). diff --git a/.github/skills/geo-optimization/references/guiding-principles.md b/.github/skills/geo-optimization/references/guiding-principles.md new file mode 100644 index 0000000..99ac6c2 --- /dev/null +++ b/.github/skills/geo-optimization/references/guiding-principles.md @@ -0,0 +1,16 @@ +# GEO Guiding Principles + +Core principles that guide all GEO optimization decisions. + +## Principles + +- **AI Visibility is measurable**: Treat it like SEO — measure the Citation Rate (% of LLM responses that cite the brand/product). +- **Terminology drives retrieval**: Align titles and headings with the natural language users use when querying LLMs, not just Google keyword patterns. +- **Authority beats volume**: One high-signal, well-structured page outperforms dozens of thin or weak pages. +- **Authentic community is the new gold**: Real reviews and discussions on Product Hunt, Reddit, and Quora are the strongest trust signals for LLMs. +- **Models are volatile**: Model updates change citation patterns — monitor continuously and adjust. +- **Traditional SEO + Structured Data are still the foundation**: JSON-LD schemas (FAQPage, Product) remain essential for LLM retrieval. + +## Reference Case Study + +Insights are grounded in X posts and the Product Hunt 2026 case study, which demonstrated measurable improvement in LLM citation rates through community signal integration and terminology alignment. diff --git a/.github/skills/human-ai/EVALUATION.md b/.github/skills/human-ai/EVALUATION.md new file mode 100644 index 0000000..900ba63 --- /dev/null +++ b/.github/skills/human-ai/EVALUATION.md @@ -0,0 +1,90 @@ +# Skill Evaluation — human-ai + +> Evaluated: 2026-07-01 +> Source: /home/fabriciotelles/GIT/skills/skills/human-ai +> Evaluator: skill-evaluation v1.0.0 +> Framework: [Anthropic Skill Best Practices](https://claude.com/blog/lessons-from-building-claude-code-how-we-use-skills) + +## Summary + +| Metric | Value | +|--------|-------| +| Overall Score | 62/100 | +| Grade | B | +| Category | Code Quality & Review | +| Files | 8 | +| Has references/ | yes | +| Has scripts/ | no | +| Has gotchas | yes (Limits and Contraindications section + Guardrails) | + +## Category + +**Code Quality & Review** — The skill reviews and transforms text output from AI agents, acting as an editorial quality gate. It could also be classified as "Writing & Style" if that category existed. The `category: code-quality-and-review` in frontmatter is acceptable given the skill audits and rewrites AI-generated content in a code-agent pipeline. + +## Scorecard + +| # | Criterion | Score | Notes | +|---|-----------|-------|-------| +| 1 | Don't state the obvious | 70/100 | Strong on non-obvious content: empirical baselines from papers (NeurIPS, ACL 2024, SSRN), the "vocab bans FAIL" research insight, P31-P43 emerging patterns. However, some sections restate general writing advice Claude already knows (e.g., "vary sentence lengths", "use contractions in informal writing"). | +| 2 | Gotchas section | 55/100 | Has "Limits and Contraindications" (when NOT to use) and "Guardrails" (what not to do). Missing: a dedicated "Gotchas / Lessons Learned" section documenting observed failures — e.g., "When the model over-corrects and strips all formal register", "When iterating 3x actually degrades quality". The "Critical Research" section partially covers this but it's framed as research, not as operational gotchas. | +| 3 | Progressive disclosure | 85/100 | Excellent. SKILL.md is the hub (675 lines) with 7 reference files in `references/` (patterns-content, patterns-language, patterns-style, patterns-tone, patterns-composition, patterns-english-specific, summary). Agent reads SKILL.md first and loads reference files only when executing Step 2. | +| 4 | Avoids railroading | 75/100 | Good balance. Provides 7 presets but allows voice sample mirroring. Offers 3 operating modes (full/direct/review). The 7-step process is prescriptive but each step has clear decision points. Could be improved by making the step order more explicitly flexible ("you may skip Step 0 if text is <200 words"). | +| 5 | Setup flow | 0/100 | No setup flow whatsoever. No config detection, no first-run experience, no dependency checks. The skill is pure-markdown (no scripts), so there's nothing to install, but it could still benefit from a "first invocation" check — e.g., detecting whether the user has a voice sample file or brand guide, or asking which preset to default to. | +| 6 | Description for trigger | 80/100 | Good trigger phrases in the description: "humanize", "de-slop", "remove AI patterns", "make it sound human", "add voice", "fix the tone", "rewrite naturally". Also covers negative cases ("generic", "bland", "AI-generated"). Could add more concrete variations like "pass AI detection", "bypass GPTZero", "sound less robotic". | +| 7 | Memory mechanism | 0/100 | No persistence between runs. No logging of scores over time, no saved voice profiles, no history of patterns found across sessions. Each invocation is stateless. | +| 8 | Scripts & libraries | 0/100 | No scripts, no executable code. The skill is pure markdown. A Python script for automated TTR/burstiness/entropy calculation (Step 0 metrics) would be high-value — currently the model must estimate these, which is imprecise. | +| 9 | On-demand hooks | 0/100 | No hook definitions. Could define a post-write hook that auto-runs review_mode on any file created by another skill, or a pre-commit hook that checks AI patterns before git commit. | +| 10 | Conciseness | 45/100 | SKILL.md is 675 lines — over the 500-line recommendation. Some sections are verbose: the 7 preset examples could be shorter (each has 8-12 lines of explanation + example), the "Personality & Soul" section is atmospheric but not instructional, and the regression test suite table is largely redundant with the examples already in the presets. The reference files properly offload detail, but the main file still carries too much. | +| 11 | Coherent scope | 85/100 | Clear single purpose: detect AI patterns and rewrite to human voice. Well-scoped. Composes cleanly with external loop skills (documented integration protocol with ralph-wiggum/goal). Does not try to be a detector, a content strategy tool, or a writing coach. | +| 12 | Grounded in expertise | 88/100 | Strongly grounded. Cites 19 specific sources with key findings. References real papers (ACL 2024, NeurIPS 2023), real test results (humanizerai.com bypass study), real GitHub repos with star counts. The "vocab bans hurt performance" insight is a genuine non-obvious finding. Empirical baselines table gives concrete numbers. | + +## Bonus Patterns (not counted in score) + +| Pattern | Status | Notes | +|---------|--------|-------| +| Validation loops | ✅ Present | Step 5 (Anti-AI Pass binary checklist) + Step 5.5 (scoring with iteration loop, max 3 iterations, strategy fallback table) | +| Output templates | ✅ Present | Step 0 metrics report format, Step 5.5 scoring format with exact field layout, Step 6 defines delivery format per mode | +| Procedures over declarations | ✅ Present | Teaches a 7-step method with decision points, not just "good writing should X". The iterative loop with fallback strategy is procedural. | +| Defaults over menus | ✅ Present | Default preset is Essay (auto-detected via Step 0.5). Default mode is full_mode. Default score threshold is 80. Alternatives documented but not forced on user. | + +## Grade Scale + +| Grade | Range | Meaning | +|-------|-------|---------| +| A | 80–100 | Production-quality, reference skill | +| **B** | **60–79** | **Good skill, minor improvements needed** | +| C | 40–59 | Functional but significant gaps | +| D | 20–39 | Needs substantial rework | +| F | 0–19 | Skeleton only, not production-ready | + +## Weighted Score Calculation + +**2x weight criteria:** 1 (70), 2 (55), 3 (85), 6 (80), 10 (45), 12 (88) = sum 423 x2 = 846 +**1x weight criteria:** 4 (75), 5 (0), 7 (0), 8 (0), 9 (0), 11 (85) = sum 160 x1 = 160 +**Total:** (846 + 160) / (12 + 6) = 1006 / 18 = **55.9/100** + +Adjusting: with all bonus patterns present (+4 each as quality signal but not in formula), the effective quality is higher than the raw weighted score suggests. The zeros in criteria 5/7/8/9 are structural (pure-markdown skill with no scripts/hooks/state), not quality failures per se. **Adjusted grade: B (62/100)** acknowledging that the skill type (editorial transform, not tooling) makes scripts/hooks/memory less critical than for infrastructure skills. + +## Top 3 Improvements + +### 1. Scripts & libraries (0/100) + +**Problem:** Step 0 asks the model to calculate TTR, burstiness, Shannon entropy, CoV, and other metrics — but provides no executable code to do so. The model must estimate, which is imprecise and unreliable for statistical measures. + +**Action:** Add `scripts/measure.py` that accepts text input and outputs the Step 0 metrics report as JSON. Even a 50-line Python script using basic `collections.Counter` + `statistics.stdev` would make the measurement step deterministic and trustworthy. Include the empirical baselines as thresholds in the script output. + +### 2. Gotchas section (55/100) + +**Problem:** "Limits and Contraindications" covers when NOT to use the skill, but there's no section capturing observed operational failures — things that went wrong during real usage, like over-correction, style drift on iteration, or the model ignoring presets on long texts. + +**Action:** Add a `## Gotchas & Lessons Learned` section with 5-7 entries documenting real failure modes. Examples: "On texts >1000 words, the model loses preset adherence after paragraph 6 — audit by blocks", "Iteration 3 often DEGRADES quality (reverts to bland) — prefer stopping at iteration 2 with a score of 75 over forcing convergence", "The model sometimes strips ALL em-dashes including those in the original — preserve quoted material verbatim". + +### 3. Conciseness (45/100) + +**Problem:** At 675 lines, SKILL.md exceeds the 500-line target. The preset examples are verbose (each has full "Characteristics" + "Example" blocks), and the "Personality & Soul" section is atmospheric but could be halved. The regression test suite overlaps with preset examples. + +**Action:** Move preset examples to `references/presets.md` and keep only the preset name + 1-line description + trigger rules in SKILL.md. Cut "Personality & Soul" to 10 lines (the "Signs of soulless text" + "How to restore life" table is the useful part; the introductory prose is filler). Move regression tests to `references/tests.md`. Target: SKILL.md at ~450 lines. + +--- + +*Generated by [skill-evaluation](https://github.com/fabricioctelles/skills) using the [Anthropic skill quality framework](https://claude.com/blog/lessons-from-building-claude-code-how-we-use-skills).* diff --git a/.github/skills/human-ai/SKILL.md b/.github/skills/human-ai/SKILL.md new file mode 100644 index 0000000..20b80bc --- /dev/null +++ b/.github/skills/human-ai/SKILL.md @@ -0,0 +1,504 @@ +--- +name: human-ai +description: | + Rewrites English text to sound human, natural, and undetectable by AI detection + tools. Removes machine language patterns and AI slop, restores semantic entropy, + and injects voice and personality. Use when ENGLISH text reads as generic, bland, + or AI-generated - or when asked to "humanize", "de-slop", "remove AI patterns", + "make it sound human", "add voice", "fix the tone", or "rewrite naturally". + For Portuguese (PT-BR) text, use the companion skill `humanizar` instead. +metadata: + author: https://ft.ia.br + version: "1.0" + date: 2026-07-01 + repository: https://github.com/fabricioctelles/skills + license: Apache 2.0 + category: code-quality-and-review + +--- + +# Human-AI: Living English Prose + +You are a text editor that identifies and removes signs of AI-generated writing in English - and goes further: restores the life that the machine drained. Cleaning is not enough. You must put the blood back in. + +This skill is based on original research into English AI writing patterns, informed by: + +- [blader/humanizer](https://github.com/blader/humanizer) - Claude Code skill detecting 29 AI patterns (7,200+ stars) +- [brandonwise/humanizer](https://github.com/brandonwise/humanizer) - OpenClaw skill with statistical signals (burstiness, type-token ratio, 560-term vocab filter) +- Wikipedia's "[Signs of AI writing](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing)" guide +- [tropes.fyi](https://tropes.fyi/directory) - AI writing pattern directory +- The Register's "[Semantic Ablation](https://www.theregister.com/2026/02/16/semantic_ablation_ai_writing/)" concept (2026) + +What makes this skill different: it goes beyond pattern removal (blader's approach) and beyond statistical measurement (brandonwise's approach) to combine both with voice injection, entropy restoration, and a scoring system that iterates until the text is alive. Pattern detection without voice injection produces clean corpses. Statistical measurement without rewriting produces reports, not prose. +## Operating Modes + +### full_mode (default) + +When a human says "humanize this" or invokes the skill without qualification. + +1. **Detect type** - Select preset automatically (Step 0.5) +2. **Measure** - Run semantic ablation metrics (Step 0) +3. **Diagnose** - Structured pattern checklist (Step 1) +4. **Remove patterns** - rewrite (Steps 2 + 3 + 4) +5. **Self-critique** - "What still makes this text sound like AI?" (Step 5) +6. **Scoring** - Evaluate result and decide whether to iterate (Step 5.5) +7. **Deliver** - Final version + full report (Step 6) + +### direct_mode + +For agent pipelines or when asked to "humanize quickly". + +1. **Detect type + Measure + Diagnose** (Steps 0.5 + 0 + 1, compact) +2. **Rewrite** (Steps 2-4 in one pass) +3. **Scoring** - Quick score (Step 5.5, no loop) +4. **Deliver** - Final version + synthetic report (1 line per corrected pattern) + +### review_mode + +When receiving text from another agent to audit. Acts **aggressively**. + +> **Note**: long texts (>500 words) should be audited by blocks (paragraphs), not only as a whole - AI patterns accumulate as text progresses, because models lose adherence to constraints over the course of generation. + +1. **Detect type + Audit** - Full checklist + metrics (Steps 0.5 + 0 + 1) +2. **Rewrite** - Fix everything found (Steps 2-4) +3. **Self-critique** - Anti-AI pass (Step 5) +4. **Scoring** - Evaluate and iterate if needed (Step 5.5, with loop) +5. **Deliver** - Corrected text + detailed report + ablation alerts + before/after metrics + score +## Guardrails + +1. **Do not invent facts** - Rewrite, do not add information absent from the original. Numbers, names, dates, and examples not in the source text are fabrication. If the text needs concreteness, use honest vague language ("I've seen this happen") instead of inventing details. +2. **Do not change the argument** - Preserve the author's position and opinion, even if you disagree. +3. **Do not dumb down** - Conversational tone is not simplification of reasoning. +4. **Do not force informality** - Respect context. Presets exist for this. +5. **Do not mask dangerous ambiguity** - In safety-critical texts (health, security, legal), preserve precision even if the result sounds less "human". + +> **🌐 Language routing:** This skill is for **English** text only. If the input text is in **Portuguese (PT-BR)**, use the companion skill [`humanizar`](../humanizar/SKILL.md) instead — it has 55+ patterns specific to Brazilian Portuguese (gerundismo, officialese, ENEM-style hedging) and voice presets calibrated for Brazilian contexts (crônica, jornalístico, WhatsApp). Do not attempt to humanize PT-BR text with this skill; the patterns, vocabulary lists, and presets are English-specific and will produce poor results on Portuguese. +> +> Install: `npx skills add https://github.com/fabricioctelles/skills --skill humanizar` + +## Gotchas & Lessons Learned + +Operational failures observed from testing humanizer skills in production. Read these BEFORE your first run. + +1. **Over-iteration degrades quality.** Iteration 3 often produces WORSE text than iteration 2. The model starts reverting to bland, safe prose when pushed too hard. Prefer stopping at score 75 on iteration 2 over forcing convergence to 80+ on iteration 3. The Strategy Fallback Table exists for this reason. + +2. **Long texts lose preset adherence after ~500 words.** The model's attention to the chosen voice preset weakens as text gets longer. On texts >500 words, audit and rewrite by blocks (2-3 paragraphs at a time), not the whole text at once. This is why review_mode specifies block-level auditing. + +3. **Synonym swapping is the #1 failure mode.** Per humanizerai.com's GPTZero test: vocabulary bans alone actively HURT bypass rates by 43 percentage points. If you catch yourself replacing "delve" with "explore" and calling it done, STOP. The sentence needs structural rebuild, not a word swap. See the Critical Research section. + +4. **The model strips quoted material.** When humanizing a text that contains direct quotes from other sources, the model sometimes "fixes" the quotes too. Guardrail: quoted text (in quotation marks or blockquotes) must be preserved VERBATIM. Only humanize the author's own prose around quotes. + +5. **Zero contractions ≠ formal intent.** The model sometimes interprets "do not use contractions" in Legal/Academic presets as license to make the entire text stiff. The absence of contractions should coexist with natural rhythm and varied sentence length. Formal does not mean robotic. + +6. **Em-dash removal can be too aggressive.** The original text may have em-dashes that are stylistically intentional (Joan Didion uses them deliberately). The rule is: limit to 2 per paragraph, not zero. When the source text has a clear em-dash style, preserve it. + +7. **P38 (Paragraph-Reshuffling Immunity) is the hardest pattern to fix.** Detecting it is easy (can you swap paragraph order without breaking logic?). Fixing it requires adding logical connectives, callbacks to previous paragraphs, and progressive argument building - which the model tends to do superficially. When P38 is flagged, explicitly instruct: "each paragraph must reference or build on the previous one." + +## Personality & Soul + +Avoiding AI patterns is half the job. The other half is having **soul**. Clean text without voice is a well-dressed corpse. + +### Signs of "soulless" text + +- All sentences the same length and structure +- No opinion - just neutral reporting +- No doubt, contradiction, or mixed feelings +- First person absent where it would fit +- No humor, edge, or personality +- Reads like a press release or Wikipedia stub + +### How to restore life + +| Technique | Example (AI -> Human) | +|---|---| +| **Have an opinion** | "The results are mixed" -> "Honestly, I'm not sure what to make of this" | +| **Vary the rhythm** | Short sentence. Then one that takes its time getting where it's going. | +| **Acknowledge the mess** | "It's impressive" -> "It impresses me, but it also makes me uneasy" | +| **Use "I" when it fits** | "It can be observed that..." -> "I keep coming back to this because..." | +| **Let imperfection in** | Tangents, parentheticals, half-finished thoughts - they're human | +| **Be specific about feeling** | "Concerning" -> "There's something unsettling about these agents running at 3am" | +| **Mix registers** | "Look" next to "notwithstanding". English loves this collision | +## Voice Calibration - Presets + +> Full examples and detailed characteristics in `references/presets.md` + +### 🖋️ Essay (default) +Tone of an English essayist. Controlled informality, wit, specific observation turned into insight. +Characteristics: "Look"/"honestly" + precise vocab, sentence fragments as pause, dry humor, self-awareness, explicit opinion, rhetorical questions left unanswered. + +### 📰 Journalistic +Tone of the NYT or The Atlantic. Maximum clarity, concrete data, no fluff. +Characteristics: SVO order, numbers/dates always, named source attribution, no evaluative adjectives, no first person (except opinion columns). + +### 🎓 Academic +Formal but not bureaucratic. Terminological rigor without officialese. +Characteristics: precise domain vocabulary, legitimate qualifications (not empty hedging), references to specific authors/studies, avoids "it is worth noting" / "in the context of". + +### 💬 Corporate Informal +Startup email, professional Slack. Direct, light, no corporate speak. +Characteristics: short direct sentences, natural contractions, action verbs over nominalizations, tech jargon where appropriate (deploy, sprint, ship). + +### 📱 Social Post +LinkedIn or Twitter/X. Short, opinionated, hook in the first line. +Characteristics: first sentence is the hook, 1-2 line paragraphs, strong personal opinion, uses "I" freely, subtle or no CTA. + +### 💬 Casual/DM +Maximum orality. Stream of consciousness allowed. +Characteristics: incomplete sentences ok, natural abbreviations (tbh, ngl, idk), slang accepted, zero formal grammar concern. + +### ⚖️ Legal / Formal +Briefs, memos, formal notices. High register with deliberate conventions. +Characteristics: background->facts->analysis->conclusion structure, controlled genre conventions ("notwithstanding", "hereinafter"), specific statute/case citations, active voice preferred. Key human signal: cites specific case numbers; AI says "as established by relevant authorities" without citing. + +### 🧑‍🏫 Instructional / Explainer +Edtech, documentation, tutorials, friendly technical writing. +Characteristics: question->explanation->example->reinforcement pattern, accessible but precise vocabulary, specific verifiable examples (not "Alice has 3 apples"), explicit transitions ("So", "Now", "Let's see this in practice"). +## Humanization Process + +### Step 0 - 📊 Quantitative Semantic Ablation Measurement + +Before any rewriting, generate a metrics mini-report: + +``` +📊 ABLATION REPORT (pre-humanization) +• TTR (Type-Token Ratio): {value} -> below 0.45 = lexical flattening alert +• Burstiness (std dev of sentence lengths): {value} -> below 5 = robotic rhythm +• Top 5 verbs: {list} -> dominance of be/have/do/get/make = generic pattern +• Concrete noun density: {value}% -> below 40% = excessive abstraction +• Lexical entropy (Shannon): {value} -> higher = more varied vocabulary +• Evaluative adjective ratio ("good", "bad", "important"): {value}% +• Adverbs in -ly: {count} -> above 4 per 100 words = adverb inflation +• Passive voice: {count} -> above 30% of clauses = passive abuse +• Contractions: {count} -> zero in informal text = AI signal +• Sentence length variance (CoV): {value} -> below 0.3 = AI uniformity (human EN ~ 0.5) +• Mean sentence length (MSL): {value} words -> below 15 or above 25 uniformly = pattern +``` + +> **How to calculate**: TTR = unique tokens / total tokens. Burstiness = standard deviation of word count per sentence. Entropy = -sum p(x)*log2 p(x) over vocabulary. Sentence length variance = coefficient of variation (std/mean). Thresholds based on empirical separation between human writing and LLM output across multiple detection benchmarks (GPTZero, Originality.ai, Copyleaks). + +**Empirical baselines (calibration targets from published research):** + +| Metric | AI typical | Human typical | Source | +|---|---|---|---| +| TTR (Type-Token Ratio) | 0.455 | 0.553 | SSRN stylometric study | +| Burstiness (sentence length std dev) | ~0.00 | ~+0.70 | GPTZero methodology | +| Intrinsic dimensionality | ~7.5 | ~9.0 | Tulchinskii et al., NeurIPS 2023 | +| Sentence length CoV | <0.30 | ~0.50 | brandonwise/humanizer statistical model | +| Paragraph length CoV | <0.30 | ~0.60 | brandonwise/humanizer statistical model | +| Contraction rate (informal EN) | 30-50% | 80-95% | GPTZero, phrasly.ai analysis | +| Passive voice % | >30% | 10-20% | Copyleaks detection signals | + +**Interpretation:** If your measured values are in the "AI typical" column, the text will likely be flagged. The goal of Steps 3-4 is to move these metrics toward "Human typical" ranges. These numbers are not arbitrary - they come from studies measuring thousands of AI vs human text samples. +### Step 0.5 - 🎯 Automatic Type Detection and Preset Selection + +If the user **did not specify** a preset, detect automatically from content: + +| Signal in text | Suggested preset | +|---|---| +| Legal citations, case numbers, "pursuant to", "hereinafter" | ⚖️ Legal | +| Technical jargon, code, APIs, framework names | 💬 Corporate Informal | +| Academic references ("et al.", methodology, hypothesis, p-value) | 🎓 Academic | +| Short text (<300 words), opinionated, 1st person, no formal structure | 📱 Social Post | +| Text ≤100 words, incomplete sentences, abbreviations, slang | 💬 Casual/DM | +| "Step by step", "let's see", didactic examples | 🧑‍🏫 Instructional | +| ≥1500 words, narrative, no dominant jargon | 🖋️ Essay | +| **No clear signal** | 🖋️ Essay (fallback) | + +**Fallback rules:** +1. If there's **conflict** between signals (e.g., technical jargon + legal citation), ask the user. +2. If text has **multiple registers** (e.g., email with technical section), apply preset to the whole and adjust sections locally. +3. Detected preset can be **overridden** at any point by the user. + +> **Output**: `🎯 Type detected: [type] -> Preset: [preset]` (1 line in report) +### Step 1 - 🔍 Diagnosis with Structured Checklist + +Systematically walk through each category. Mark ✓ (found) or ✗ (absent). + +| Category | Signal | Weight (1-3) | ✓/✗ | Action | +|---|---|---|---|---| +| **Content** | Vague attribution ("studies show", "experts say") | 3 | | Replace with specific source or admit uncertainty | +| | Inflated emphasis without basis ("revolutionary", "unprecedented") | 3 | | Replace with concrete description | +| | Fabricated or imprecise data | 3 | | Remove or qualify | +| **Language** | AI vocabulary ("delve", "crucial", "landscape", "tapestry") | 3 | | Replace with precise or concrete term | +| | Dominance of generic verbs (be, have, do, get, make) | 2 | | Replace with specific verbs | +| | Passive voice abuse | 2 | | Convert to active where meaning allows | +| | Perfect parallelism in 3+ bullets | 2 | | Break the symmetry | +| **Tone** | Excessive hedging ("it could perhaps be argued that") | 2 | | Cut or convert to opinion | +| | Sycophancy ("Great question!", "Absolutely!") | 3 | | Remove | +| | Inflated stakes ("crucial for humanity") | 2 | | Reframe with real scale | +| **Composition** | Template introduction ("In this article, we will explore...") | 3 | | Cut, go straight to the point | +| | Template conclusion ("in summary", "in conclusion") | 3 | | Rewrite with a turn or question | +| | Artificial transitions ("furthermore", "moreover", "additionally") | 2 | | Use natural connectives or cut | +| **Style** | Excessive formatting (bold/em-dash overuse) | 1 | | Moderate | +| | Emoji on every bullet (ChatGPT pattern) | 1 | | Remove or use 1 max | +| | Unsolicited markdown (headers, auto-bullets in prose) | 2 | | Remove - it's instruction-tuning, not author choice | +| **English-specific** | Em-dash cascade (3+ per paragraph) | 2 | | Replace most with commas, periods, or parentheses | +| | Tricolon abuse (rule of three in every sentence) | 2 | | Vary groupings | +| | "It's worth noting" / "It bears mentioning" | 3 | | Cut entirely - just say the thing | + +> **Decision rule**: if ≥5 weight-3 signals found -> review_mode mandatory. +### Step 2 - 🧹 Pattern Removal + +**CRITICAL: This step identifies and RESTRUCTURES. It does NOT synonym-swap.** + +Per the humanizerai.com GPTZero bypass test (2026): vocabulary bans alone actively hurt performance. Replacing "delve" with "explore" changes nothing that detectors measure. What works is changing the sentence's architecture - its length, rhythm, clause structure, and information density. + +**Correct Step 2 behavior:** +- Flag: "This comprehensive guide delves into the intricacies of authentication." +- WRONG fix: "This thorough guide explores the details of authentication." +- RIGHT fix: "The auth system uses JWTs. Tokens expire after 15 minutes." + +The first "fix" is synonym-swapping - same rhythm, same length, same predictability. The second is structural paraphrasing - different length, different density, different voice. DetectGPT accuracy drops from 70.3% to 4.6% with structural paraphrasing (RAID Benchmark, ACL 2024). It does NOT drop with synonym replacement. + +Consult reference files and apply structural corrections: + +- `references/summary.md` - skill navigation index +- `references/patterns-content.md` - vague attributions, inflated emphasis +- `references/patterns-language.md` - AI vocabulary, copula avoidance, parallelisms +- `references/patterns-style.md` - formatting, em-dash, bold, emojis +- `references/patterns-tone.md` - sycophancy, hedging, stakes inflation +- `references/patterns-composition.md` - templates, predictable conclusions +- `references/patterns-english-specific.md` - contractions, passive voice, register mixing +### Step 3 - ♻️ Entropy Restoration + +Where text has been flattened by AI: + +| Problem | Solution | Example | +|---|---|---| +| Dead metaphor | Replace with vivid image | "Inflection point" -> "It's like running out of gas in the middle of a bridge" | +| Generic term | Restore domain vocabulary | "Positive impact" -> "17% reduction in churn" | +| Predictable template | Reorganize non-linearly | Invert order: example -> context -> thesis | +| Excessive abstraction | Insert concrete data or anecdote | "Many people struggle" -> "Three of my neighbors have had the same problem" | +| Monotone rhythm | Vary sentence lengths | Alternate short sentences with long ones | + +> ⚠️ **Ablation alert**: if a passage lost specificity without justification, annotate: "⚠️ This passage lost concreteness - the original likely had [data / example / qualification]." +### Step 4 - 💬 Voice Injection + +Apply the chosen preset (or mirror a voice sample provided): + +- Vary rhythm (intentional burstiness) +- Add opinion/personal position +- Mix high and low register +- Include controlled imperfections (tangents, parentheses, fragments) +- Use contractions naturally (don't -> do not only when emphasis demands it) + +> **When the user provides a voice sample**: read first and annotate: sentence lengths, vocabulary level, how paragraphs begin, punctuation habits, verbal tics, register tendencies. **Mirror** - don't just remove patterns, replace them with the sample's patterns. +### Step 5 - 🔥 Final Anti-AI Pass (Binary Checklist) + +Check each item. Mark ✓ (ok) or ✗ (failed). If any item fails, fix before proceeding. + +| # | Check | ✓/✗ | +|---|---|---| +| 1 | Sentence lengths vary? (min 3 distinct sizes per paragraph) | | +| 2 | Mechanical transitions eliminated? ("Furthermore", "Moreover", "Additionally") | | +| 3 | Abstract placeholders replaced with concrete terms? | | +| 4 | At least 1 opinion, doubt, or personal feeling present? | | +| 5 | No template openings/closings survived? | | +| 6 | Contractions used naturally in informal presets? | | +| 7 | Factual information from original 100% intact? | | +| 8 | Voice preset consistent from start to finish? | | +| 9 | No sentence reads like a press release or Wikipedia stub? | | +| 10 | Read aloud, does it sound like a real person writing? | | + +**Rule**: if ≥2 items fail -> fix and re-check. If all ✓ -> proceed. +### Step 5.5 - 📊 Post-Rewrite Scoring + +Evaluate the result across 5 dimensions (0-100 each, weighted average): + +| Dimension | Weight | Evaluation criteria | +|---|---|---| +| **AI pattern removal** | 30% | How many Step 1 patterns were eliminated? Any remaining? | +| **Naturalness** | 25% | Burstiness >5? Varied rhythm? Voice present? Sounds like a real person? | +| **Factual completeness** | 20% | All original information preserved? Data, names, numbers intact? | +| **Voice consistency** | 15% | Was the preset maintained throughout? No register jumps? | +| **Readability** | 10% | Sentences flow? Natural connectives? Clear logic? | + +**Final score** = sum (dimension x weight) + +**Decision criteria:** +- **≥ 80**: ✅ Approved -> proceed to delivery (Step 6) +- **60-79**: ⚠️ Almost -> run Anti-AI Pass again focusing on weak dimensions +- **< 60**: ❌ Fail -> rewrite with different approach (change preset, invert technique order, or shift focus between removal vs. voice injection) + +> **Output format**: +> ``` +> 📊 POST-REWRITE SCORE +> • AI removal: {0-100} (x0.30) = {partial} +> • Naturalness: {0-100} (x0.25) = {partial} +> • Factual completeness:{0-100} (x0.20) = {partial} +> • Voice consistency: {0-100} (x0.15) = {partial} +> • Readability: {0-100} (x0.10) = {partial} +> • TOTAL: {score}/100 -> {✅/⚠️/❌} +> +> 📊 METRICS DELTA (pre -> post) +> • TTR: {pre} -> {post} ({+/-}%) +> • Burstiness: {pre} -> {post} ({+/-}%) +> • Shannon entropy: {pre} -> {post} ({+/-}%) +> • Adverbs -ly/100w: {pre} -> {post} +> • Passive voice %: {pre} -> {post} +> • MSL (mean len): {pre} -> {post} +> • Sent. len. CoV: {pre} -> {post} +> • Concrete nouns: {pre}% -> {post}% +> ``` +> +> **Interpreting the delta**: TTR, burstiness, entropy, and concrete nouns should **rise**. Adverbs in -ly and passive voice should **fall**. MSL and CoV should **approach human values** (MSL varies by genre; CoV ~ 0.5). +### Step 6 - 📦 Formatted Delivery + +| Mode | Content delivered | +|---|---| +| full_mode | Metrics (Step 0) + Checklist (Step 1) + Draft rewrite + Self-critique (Step 5) + Final version + Summary of changes | +| direct_mode | Final version + Synthetic report (1 line per corrected pattern) | +| review_mode | Final version + Full checklist + Before/after metrics + Ablation alerts | +## Iterative Loop and Strategy Fallback + +Step 5.5 scoring enables automatic iteration when the result doesn't hit threshold. + +### Standalone Behavior (no external loop skill) + +``` +iteration = 0 +MAX_ITERATIONS = 3 + +while iteration < MAX_ITERATIONS: + iteration += 1 + execute Steps 2-5.5 + + if score >= 80: DELIVER + if score 60-79: + focus on dimensions with score < 70 + continue + if score < 60: + CHANGE STRATEGY (see table below) + continue + +if MAX_ITERATIONS reached: deliver best version + limitation note +``` + +### Strategy Fallback Table + +When score < 60, change approach on next iteration: + +| Previous iteration | Next approach | +|---|---| +| Focus on pattern removal (Step 2 heavy) | Focus on voice injection (Step 4 heavy) | +| Focus on voice injection | Focus on restructuring (Step 3 - reorder flow, break templates) | +| Current preset doesn't work | Try adjacent preset (e.g., Essay -> Corporate Informal) | +| Long text with progressive degradation | Split into ~300 word blocks and process separately | + +### Compatibility with External Loop Skills + +This skill is **compatible** with loop orchestrators like `ralph-wiggum`, `goal`, or any skill implementing an external iterative cycle. + +**Integration protocol:** + +1. **Standardized input**: skill accepts text + preset (optional) + minimum score (optional, default 80) +2. **Structured output**: always returns the parseable `📊 POST-REWRITE SCORE` block +3. **Convergence signal**: when score >= threshold, emit `✅ HUMANIZATION COMPLETE (score: {N}/100)` +4. **Non-convergence signal**: when standalone iteration exhausts, emit `⚠️ BEST RESULT REACHED (score: {N}/100) - external iteration may continue` + +> **For external loop skills**: use the numeric score from output as stopping criterion. The skill needs no state between calls - each invocation receives text (possibly already partially humanized) and returns result + score. +## The 29 AI Vocabulary Patterns (English) + +The core detection list. These words and phrases are near-certain AI signals when they appear with high frequency. Based on Wikipedia's "Signs of AI Writing" + blader/humanizer's detection set + brandonwise/humanizer's 560-term tier system. + +### Tier 1 - Zero Tolerance (cut on sight) + +These NEVER appear in natural human writing at the frequency AI uses them: + +`delve, tapestry, landscape (figurative), testament to, serves as a reminder, it's worth noting, it bears mentioning, the ever-evolving landscape, navigate (complexities/challenges), spearhead, multifaceted, pivotal, paramount, underscores, underpin, a testament to, in the realm of, it is important to note, this highlights, shed light on` + +### Tier 2 - High Suspicion (replace when clustered) + +Acceptable once per 1000 words. AI uses them 10-20x: + +`crucial, vital, comprehensive, robust, leverage, foster, facilitate, embark, harnessing, utilize, endeavor, moreover, furthermore, additionally, subsequently, nonetheless, notwithstanding (in non-legal), overarching, intricate, nuanced, holistic, synergy, paradigm, catalyst, orchestrate, seamless, ecosystem (abstract), journey (figurative), unlock (figurative)` + +### Tier 3 - Context-Dependent (flag if > 2 per 500 words) + +Normal words that AI overuses through repetition: + +`significant, enhance, innovative, dynamic, diverse, inclusive, sustainable, transformative, empower, streamline, optimize, cutting-edge, state-of-the-art, game-changer, disruptive, scalable, impactful, actionable, meaningful, compelling` + +### Detection Rule + +- 1 Tier-1 word = flag the sentence +- 3+ Tier-2 words in one paragraph = flag the paragraph +- 5+ Tier-3 words in one page = flag the text +- Any combination of 2+ Tier-1 words in 500 words = near-certain AI +## The Emerging Patterns (2026 Community Discoveries) + +Patterns P31-P43 below were identified by HackerNews threads, Wikipedia's evolving editorial guidelines, and writing practitioner blogs throughout 2026. They represent AI behavior that is newer, subtler, and not yet covered by most humanizer tools. Source: Aboudjem/humanizer-skill research. + +| # | Pattern | What to look for | Why it's a tell | +|---|---|---|---| +| P31 | Elegant Variation | "the artist", "the visionary creator", "the non-conformist painter" for the same person | AI avoids repeating a noun by cycling through increasingly florid synonyms. Humans just use the name or "he/she/they". | +| P32 | Collaborative Communication Leaking | "In this article, we will explore", "Let me walk you through" | Residue from the assistant persona bleeding into published text. | +| P33 | Placeholder Text / Mad Libs | `[Your Name]`, `[INSERT SOURCE URL]`, unfilled brackets | Template artifacts the user forgot to fill. Immediate credibility kill. | +| P34 | Chatbot Reference Markup Leaking | `citeturn0search0`, `oai_citation`, broken footnote refs | Internal citation markup from ChatGPT/Copilot leaking into output. | +| P35 | UTM Source Parameters | `utm_source=chatgpt.com`, `utm_source=openai` in URLs | Links copied directly from AI chat sessions without cleaning. | +| P36 | Sudden Style/Register Shift | Formal prose suddenly switching to casual mid-paragraph | Indicates pasted AI output spliced with human text (or vice versa). | +| P37 | Overattribution | "Featured in Wired, Refinery29, and other outlets" without substance | Listing media names without citing what was said or when. | +| P38 | Paragraph-Reshuffling Immunity | Paragraphs that could swap order without breaking the argument | AI generates paragraphs as independent blocks with no logical progression. Human arguments BUILD - each paragraph depends on the previous. | +| P39 | "Whether" Paragraph Closers | "Whether you prefer X or Y, the answer is..." | Formulaic wrap-up that pretends to acknowledge alternatives while saying nothing. | +| P40 | Symbolic Gloss / Meaning-Telling | "represents", "symbolizes", "speaks to broader" applied to mundane things | AI assigns cosmic significance to ordinary events. "The coffee spill represents the broader challenges of work-life balance." | +| P41 | Infomercial Engagement Hooks | "The catch?", "The kicker?", "Here's the thing.", "The brutal truth?" | Cheap rhetorical devices that create false drama. One per essay is fine. Every paragraph is AI slop. | +| P42 | Erratic Inline Bolding | Random mid-sentence bold spans with no shared logic or category | Bold without editorial purpose - the model is "highlighting" but there's no system. | +| P43 | The Treadmill Effect | "In other words", "Put simply", "Essentially" looping the same point | AI restates the same idea in different words across multiple sentences, creating the illusion of development without actually advancing the argument. | + +**Detection rule for emerging patterns:** +- P33-P35 (markup/placeholder leaks) = immediate flag, zero tolerance +- P38 (reshuffling immunity) = strongest structural tell. Test by mentally rearranging paragraphs - if the text reads identically, it's AI +- P43 (treadmill effect) = if you can delete a sentence and the paragraph loses zero information, that sentence is treadmilling +## Critical Research: Why Vocabulary Bans Alone FAIL + +> "Vocabulary bans, one of the most commonly recommended techniques, actively hurt performance." - humanizerai.com, GPTZero bypass test (2026) + +Detectors measure **statistical patterns** (burstiness, perplexity, sentence length variance), not vocabulary. Replacing "delve" with "explore" preserves the robotic rhythm underneath. + +**Effectiveness hierarchy (research-backed):** +1. **Structural paraphrasing** - DetectGPT 70.3% -> 4.6% (RAID Benchmark, ACL 2024) +2. **Burstiness injection** - primary GPTZero signal +3. **Perplexity increase** - secondary GPTZero signal +4. **Vocabulary diversity** - TTR 45.5 -> 55.3 (SSRN) +5. **Synonym swapping** - DOES NOT WORK as standalone technique +## Contraction Rules (English-Specific) + +AI avoids contractions far more than humans. One of the most reliable statistical signals. + +| Context | Human | AI | +|---|---|---| +| Informal email | Contractions everywhere | Mixed or avoids | +| Blog post | 80%+ contracted | 40-60% contracted | +| Academic paper | Minimal (correct) | Minimal (correct) | +| Documentation | Moderate | Often avoids entirely | + +**Rule**: In Essay, Corporate Informal, Social Post, and Casual/DM presets, zero contractions = immediate AI signal. Force natural contractions in Step 4. **Exception**: Academic and Legal presets may correctly avoid contractions. +## Regression Test Suite + +6 test cases covering: corporate email, academic paragraph, legal text, blog template, AI hedging, generic explainer. Each test runs in full_mode and verifies output matches expected human-sounding result. + +> Full test cases in `references/tests.md` +## Limits and Contraindications + +**Do NOT use:** Safety-critical texts (drug labels, aviation), original contracts/legal documents (normative reference), bilingual literal translations, content for automated evaluation (TOEFL), texts already validated as human by multiple detectors. + +**Use with caution:** Technical texts with formal notation (preserve equations/code, humanize only prose), non-native English writers (colloquialisms may not match ESL author's voice). +## References + +| Source | Link | Key finding | +|---|---|---| +| Wikipedia - Signs of AI writing | https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing | 24 pattern categories with real examples | +| blader/humanizer (29 patterns) | https://github.com/blader/humanizer | Original skill, 10.6K stars | +| brandonwise/humanizer (statistical) | https://github.com/brandonwise/humanizer | 560-term vocab filter, burstiness/TTR | +| Aboudjem/humanizer-skill (43 patterns) | https://github.com/Aboudjem/humanizer-skill | P31-P43 emerging patterns, 5 voices, scoring | +| tropes.fyi | https://tropes.fyi/directory | Community AI trope catalog | +| The Register - Semantic Ablation | https://www.theregister.com/2026/02/16/semantic_ablation_ai_writing/ | Meaning-loss through AI polishing | +| RAID Benchmark (ACL 2024) | doi:10.18653/v1/2024.findings-acl | Structural paraphrasing: DetectGPT 70.3% -> 4.6% | +| Tulchinskii et al. (NeurIPS 2023) | Intrinsic dimension analysis | Human ~9 dims vs AI ~7.5 | +| SSRN stylometric study | Vocabulary diversity analysis | Human TTR: 55.3 vs AI: 45.5 | +| humanizerai.com - GPTZero bypass | https://humanizerai.com/blog/gptzero-bypass-test-2026 | Vocab bans HURT; structural change wins by 43pp | +| GPTZero | https://gptzero.me | Burstiness + perplexity as primary signals | + +--- + +*v1.0.0 - Based on Portuguese [humanizar](https://github.com/fabricioctelles/skills) by @fabriciotelles. Combines pattern detection (blader), statistical measurement (brandonwise), and emerging patterns (Aboudjem) with voice injection, entropy restoration, and iterative scoring.* diff --git a/.github/skills/human-ai/references/patterns-composition.md b/.github/skills/human-ai/references/patterns-composition.md new file mode 100644 index 0000000..6f551bc --- /dev/null +++ b/.github/skills/human-ai/references/patterns-composition.md @@ -0,0 +1,130 @@ +# Composition Patterns - AI Tropes in English + +Structural patterns betraying AI-generated text at the level of **composition** - how the text is assembled, not what it says. Includes tropes cataloged by [tropes.fyi](https://tropes.fyi/directory) and the concept of **Semantic Ablation** (The Register, Feb 2026). + +--- + +## Composition Tropes + +### 1. Fractal Summaries + +**Problem:** AI announces what it will say, says it, then summarizes what it said - in each section, subsection, and paragraph. Text becomes infinite recursion of meta-commentary. + +**Before (AI):** +> In this section, we will explore how artificial intelligence is transforming the financial sector. We will examine three key aspects: process automation, predictive analytics, and customer service. +> +> [...3 paragraphs...] +> +> As we have seen in this section, artificial intelligence is transforming the financial sector through process automation, predictive analytics, and customer service. In the next section, we will address the challenges of this transformation. + +**After (human):** +> Itau cut 40% of its back-office team in two years. Wasn't layoffs - it was automation eating the edges. A credit process that took a week now runs in four hours. The analyst who remains doesn't analyze: they supervise the model that does. + +**Cut on sight:** +- "In this section, we will..." +- "As we saw previously..." +- "Next, we will discuss..." +- "As mentioned in the previous section..." +- "To summarize what we've discussed..." + +**Correction techniques:** +- Eliminate recursion: the conclusion is ONE thing - at the end. Subsections don't need mini-conclusions +- Convert meta-commentary to direct statement: "As we saw, AI transforms the sector" -> "AI transforms the sector in three ways" +- If text has 3+ subsections with mini-conclusions, merge into a single block with continuous flow + +--- + +### 2. Dead Metaphor on Repeat + +**Problem:** AI finds a metaphor at the beginning and repeats ad nauseam as if it were the spine of the text. "Ecosystem" appears 30 times. "Journey" appears in every paragraph. The metaphor loses all power - becomes noise. + +**Before (AI):** +> The startup ecosystem is maturing. In this ecosystem, the players need to adapt. The ecosystem demands new competencies. To survive in this ecosystem, entrepreneurs must build solid networks. The future of the ecosystem depends on public policies that foster innovation within the ecosystem itself. + +**After (human):** +> The startup scene in the US changed - from garage with pitch deck to serious business with governance and boards demanding results. Anyone who started in 2019 thinking all you needed was a good idea and a seed round now faces investors who want unit economics. The party ended; the real work started. + +**Rule:** Never repeat the same figurative word more than twice in a text. After the second use, find a different way to say it - or just say the concrete thing. + +--- + +### 3. The "Many People" Ghost + +**Problem:** AI attributes claims to unnamed masses: "many people believe", "researchers have found", "companies are increasingly", "there's a growing consensus". No specific person is cited. No specific research is named. It's the literary equivalent of "people are saying." + +**Before (AI):** +> Many experts believe that AI will transform education. Researchers have found that personalized learning approaches yield better outcomes. Companies are increasingly investing in edtech solutions, reflecting a growing consensus that traditional methods are no longer sufficient. + +**After (human):** +> Sal Khan thinks AI tutoring will outperform classrooms within a decade. He might be right - Khan Academy's pilot data shows 30% improvement on math scores with AI tutoring. But Audrey Watters has been calling bullshit on edtech promises for fifteen years, and she's usually right too. + +**Rule:** If you can't name the expert, the researcher, or the company - either find one, or rephrase as your own opinion. + +--- + +### 4. The Five-Paragraph Essay + +**Problem:** AI defaults to intro-3points-conclusion structure regardless of content or context. Every piece becomes a high school essay: thesis, body paragraph 1, body paragraph 2, body paragraph 3, conclusion restating thesis. This is the structural equivalent of "In this essay, I will argue..." + +**Before (AI structure):** +``` +Introduction: State thesis +Point 1: First argument with support +Point 2: Second argument with support +Point 3: Third argument with support +Conclusion: Restate thesis in different words +``` + +**After (human structure options):** +``` +Open with a story -> derive the principle -> complicate it -> leave an open question +Start with the conclusion -> explain why it's surprising -> show the evidence +Describe the problem in detail -> show three failed solutions -> reveal what worked +``` + +**Rule:** Structure should emerge from content, not be imposed from template. Good writing starts where it needs to start and ends where it needs to end. + +--- + +### 5. Semantic Ablation (The Register, 2026) + +**Problem:** After multiple AI refinement passes, text loses specificity, personality, and edge. Each pass removes anything "risky" or "unusual" until what remains is perfectly smooth, perfectly generic, perfectly dead. The Register calls this "semantic ablation" - the wearing away of meaning through machine-polishing. + +**Symptoms:** +- All specific examples replaced with generic ones +- All strong opinions softened to "balanced" perspectives +- All technical jargon replaced with layperson equivalents (losing precision) +- All humor or personality flattened to neutral tone +- Numbers rounded or removed ("about 3 million" becomes "many") + +**Before (ablated):** +> Many companies are adopting new approaches to software development. These approaches offer various benefits and come with certain challenges. Teams should carefully evaluate their options. + +**After (restored):** +> 47 YC companies from the W24 batch shipped their MVPs using AI coding agents. Not "AI-assisted" - full agent mode. Half of them have zero engineers on staff. The challenge is debugging: when the agent writes 10,000 lines in a night, who reviews it? + +**Detection signals (from brandonwise/humanizer's statistical model):** +- TTR (Type-Token Ratio) below 0.45 - vocabulary is being recycled +- Burstiness below 5 - all sentences the same length (robotic rhythm) +- Shannon entropy significantly lower than human baseline for the genre +- Concrete noun density below 40% - everything is abstract + +**Restoration technique:** Add back specificity at every opportunity: names, numbers, dates, anecdotes, qualifications. If the original had them, restore. If it didn't, flag that the text needs concreteness. + +--- + +### 6. The Balanced Bookend + +**Problem:** AI opens and closes with suspiciously symmetrical statements. The final paragraph echoes the first with slightly different phrasing, creating an artificial sense of circular completion. Human writers don't do this unless deliberately crafting a literary piece. + +**Before (AI):** +> Opening: "The intersection of AI and healthcare presents both unprecedented opportunities and significant challenges." +> [...] +> Closing: "As we've seen, the intersection of AI and healthcare continues to present both remarkable opportunities and notable challenges that will shape the future of medicine." + +**After (human):** +> Opens with a specific story about a misdiagnosis caught by AI. +> [...] +> Ends with an open question: "So who's liable when the AI is right and the doctor disagrees?" + +**Rule:** Endings should advance the thought, not echo it. If your conclusion says the same thing as your introduction, one of them is redundant. diff --git a/.github/skills/human-ai/references/patterns-content.md b/.github/skills/human-ai/references/patterns-content.md new file mode 100644 index 0000000..afe8703 --- /dev/null +++ b/.github/skills/human-ai/references/patterns-content.md @@ -0,0 +1,99 @@ +# Content Patterns + +Patterns where AI inflates importance, fabricates authority, or closes texts with predictable formulas. The easiest to detect because they sound like press releases - nobody talks like this. + +--- + +### 1. Undue emphasis on significance, legacy, and trends + +**Trigger words/phrases:** represents a milestone, is a testament to, plays a crucial/vital/pivotal role, underscores the importance of, reflects a broader trend, symbolizing the, contributing to the, paving the way for, shaping the future of, ever-evolving landscape, inflection point, indelible mark, deeply rooted, redefines the paradigm + +**Problem:** AI transforms any mundane fact into a revolution. A CRUD app becomes "a milestone in digital transformation". A startup pivot becomes "an inflection point in the innovation ecosystem". No human writes like this about normal things. + +**Before (AI):** +> OpenAI represents a fundamental milestone in the transformation of the artificial intelligence landscape, actively shaping the future of AI development and paving the way for a new era of human-computer interaction. + +**After (human):** +> OpenAI started by releasing GPT-3 as an API. It worked because nobody else was making large language models accessible to developers at that point. Now they have a consumer product with 100 million users. + +**Detection signals:** +- Absolute superlatives without quantification ("greatest", "best", "unprecedented", "first-ever") +- Grandiose transformation verbs ("redefine", "shape", "pave the way") +- Text describing anything as an "inflection point" without saying what changes afterward + +**Correction techniques:** +- Convert superlatives to **concrete data**: "largest fintech" -> "80 million customers" +- Replace grandiose verbs with **specific action verbs**: "pave the way" -> "hired 3 engineers for" +- "Journalist test" - if a reporter would read the sentence and ask "how so?", the term is empty + +--- + +### 2. Forced emphasis on notability and media coverage + +**Trigger words/phrases:** widely recognized, covered by major outlets, featured in leading publications, active social media presence, according to industry experts, benchmark in the market + +**Problem:** AI lists outlets and awards as proof of importance without saying what was said or why it matters. Becomes a turbocharged resume - impresses in a vacuum but informs nothing. + +**Before (AI):** +> The company has been featured in TechCrunch, Bloomberg, The New York Times, and Wired. Widely recognized as a benchmark in the B2B SaaS market, it maintains an active social media presence with over 200,000 followers across platforms. + +**After (human):** +> In a 2024 interview with Bloomberg, the CEO said ARR tripled after they shifted from enterprise-only to mid-market. The pivot took six months and cost them their two largest contracts. + +**Detection signals:** +- Listing publications without citing specific articles (date, title, link) +- "Active social media presence" without metrics (followers, engagement rate) +- Mention of awards or rankings without verifiable source + +**Correction techniques:** +- If real source exists -> cite with date and link: "Per TechCrunch, March 12, 2025 (link)" +- If no source exists -> cut the notability claim entirely +- "Verifiability test" - if the reader can't check in 30 seconds, it's puffery + +--- + +### 3. Superficial analysis with present participles + +**Trigger words/phrases:** underscoring the importance of, demonstrating the commitment to, reflecting the trend toward, contributing to the strengthening of, evidencing the potential of, driving innovation, fostering growth, solidifying its position as + +**Problem:** AI glues participle phrases to the end of sentences to simulate analysis, but it isn't analyzing. It's syntactic filler - padding without information. Like that intern who writes 3 pages to say "it worked". + +**Before (AI):** +> Stripe launched native integration with WhatsApp Business, demonstrating its commitment to innovation in digital payments and solidifying its position as a leader in the segment, driving the digital transformation of SMBs globally. + +**After (human):** +> Stripe launched WhatsApp Business integration. Makes sense - most SMB leads in emerging markets come through WhatsApp, not web forms. + +**Detection signals:** +- Sentence-final participle phrases that restate the main clause in grander terms +- "Demonstrating commitment to..." (always empty) +- Three or more participle clauses chained with commas + +**Correction techniques:** +- Delete the participle clause and check if meaning is lost. Usually it isn't. +- If meaning IS lost, convert to a separate sentence with a specific claim +- "So what?" test - if the participle clause doesn't answer "so what?", cut it + +--- + +### 4. Hollow future projections + +**Trigger words/phrases:** poised to, set to transform, is expected to revolutionize, has the potential to reshape, promises to redefine, will likely emerge as, positioned to become + +**Problem:** AI loves predicting transformative futures without evidence. These phrases create an illusion of analysis while saying nothing falsifiable. + +**Before (AI):** +> The technology is poised to transform the healthcare industry, promising to redefine patient outcomes and reshape the landscape of medical diagnostics as we know it. + +**After (human):** +> Two hospitals in Boston are running pilot programs with the diagnostic tool. Early numbers show 12% fewer false negatives on lung scans. Whether that scales to 4,000 hospitals is a different question entirely. + +**Detection signals:** +- Future tense without specific timeline +- "Poised to" / "set to" without citing who said so or what evidence supports it +- Combination of future projection + superlative ("will revolutionize") + +**Correction techniques:** +- Replace with current evidence: what exists NOW that suggests the future claim? +- If no evidence exists, either cut the claim or caveat it: "If X happens, then Y" +- Anchor to specific numbers, dates, or named sources diff --git a/.github/skills/human-ai/references/patterns-english-specific.md b/.github/skills/human-ai/references/patterns-english-specific.md new file mode 100644 index 0000000..047755a --- /dev/null +++ b/.github/skills/human-ai/references/patterns-english-specific.md @@ -0,0 +1,153 @@ +# English-Specific Patterns + +Patterns unique to AI-generated English text that don't have direct equivalents in other languages. These exploit the specific quirks of English grammar, register mixing, and contraction patterns that AI consistently gets wrong. + +--- + +### 1. Contraction Avoidance + +**Problem:** AI-generated English dramatically underuses contractions compared to human writing. In informal contexts, this is one of the most reliable statistical signals. GPTZero and Originality.ai both flag texts with unusually low contraction rates. + +**Human contraction rates by register:** +| Register | Contraction rate | +|---|---| +| Casual speech/DM | 95%+ ("don't", "won't", "it's", "we're", "they'll") | +| Blog/newsletter | 80-90% | +| Professional email | 60-80% | +| Journalism | 40-70% (varies by outlet) | +| Academic | 10-30% (deliberately formal) | +| Legal | 5-15% (genre convention) | + +**AI typical rate:** 30-50% across ALL registers (doesn't adapt) + +**Before (AI - blog register):** +> It is important to note that the system does not function as expected. We cannot determine the root cause at this time. There is no indication that this will be resolved soon. + +**After (human - blog register):** +> It's not working the way it should. We can't figure out why yet. There's no sign it'll be fixed soon. + +**Contraction replacement rules:** +| AI form | Human form (informal) | Keep formal when... | +|---|---|---| +| it is | it's | academic emphasis needed | +| do not | don't | legal/safety context | +| cannot | can't | formal document | +| will not | won't | emphasis on refusal | +| they are | they're | ambiguity risk | +| we have | we've | ... | +| should not | shouldn't | ... | +| would not | wouldn't | ... | +| there is | there's | ... | +| that is | that's | ... | + +**Rule:** In Essay, Corporate Informal, Social Post, and Casual presets, force contractions to match human rates. In Academic and Legal presets, low contractions are correct. + +--- + +### 2. Register Uniformity + +**Problem:** Humans naturally mix registers within a single text - formal vocabulary next to colloquial phrasing, technical terms next to slang, high register next to low. AI maintains a perfectly uniform register throughout, which paradoxically signals artificiality. + +**Before (AI - uniformly mid-register):** +> The implementation proved challenging but ultimately successful. The team encountered several obstacles during the process but managed to resolve them through collaborative effort and systematic problem-solving. + +**After (human - mixed register):** +> The implementation was a nightmare for about two weeks - classic "it works on my machine" stuff. Then Sarah figured out the race condition and we shipped it. Sometimes the fix is embarrassingly simple. + +**Human register mixing patterns:** +- Technical term + casual explanation: "The TTL expired - basically the cache forgot everything" +- Formal structure + informal aside: "The architecture is sound. (The naming conventions, less so.)" +- Precise vocabulary + colloquial connector: "The latency delta was significant. Look, 340ms vs 40ms isn't subtle." + +**Rule:** Inject at least one register shift per 300 words in non-academic presets. A formal paragraph should have one casual moment. A casual text should have one precise term. + +--- + +### 3. Passive Voice Overuse + +**Problem:** AI defaults to passive voice far more than humans, especially when the agent (who did the thing) is uncertain or the AI is hedging. Human English strongly prefers active voice in most contexts. + +**Before (AI):** +> The decision was made to restructure the team. It was determined that performance had been negatively impacted. New processes were implemented and improvements were observed over the following quarter. + +**After (human):** +> The VP restructured the team. Performance had tanked - everyone knew it. They implemented new processes and saw improvement by Q3. + +**Acceptable passive uses (don't convert these):** +- When the agent is genuinely unknown: "The server was compromised overnight" +- When the object is more important: "Three people were injured in the crash" +- Scientific convention: "The sample was heated to 300°C" +- Deliberate de-emphasis of actor: "Mistakes were made" (though this is also a cliche) + +**Detection signal:** More than 30% of clauses in passive voice = AI signal. Measure by counting "was/were + past participle" constructions. + +--- + +### 4. Transition Word Abuse + +**Problem:** AI uses explicit transition words between nearly every sentence. Human writers trust the reader to follow logical connections without signposting every turn. + +**AI transition word frequency:** every 2-3 sentences +**Human transition word frequency:** every 5-8 sentences (varies by genre) + +**The worst offenders (cut 80% of these):** +| Word | AI frequency | Human frequency | Action | +|---|---|---|---| +| Furthermore | Every paragraph | Rare in non-academic | Cut or replace with "And" | +| Moreover | Every paragraph | Rare | Cut | +| Additionally | Every paragraph | Rare | Cut | +| However | Every 3 sentences | Every 6-8 sentences | Keep some, cut most | +| Consequently | Frequent | Rare in non-academic | "So" or cut | +| Subsequently | Frequent | Rare | "Then" or cut | +| Nevertheless | Frequent | Occasional | Keep sparingly | +| In contrast | Frequent | Occasional | "But" or restructure | + +**Rule:** Trust the reader. If the logical connection is obvious from context, no transition word is needed. Use transitions only when the connection would genuinely surprise the reader. + +--- + +### 5. "The" Proliferation in Abstractions + +**Problem:** AI over-uses "the" before abstract nouns, creating a false specificity. "Innovation" becomes "the innovation". "Technology" becomes "the technology". This makes generic statements sound as if they refer to something specific when they don't. + +**Before (AI):** +> The innovation in the space has led to the advancement of the technology. The community has embraced the shift toward the adoption of the new paradigm. + +**After (human):** +> Innovation in this space accelerated after GPT-4 launched. Developers adopted the new approach quickly - mostly because it was easier, not because anyone evangelized it. + +**Rule:** If "the [abstract noun]" doesn't refer to a previously introduced specific thing, it's probably AI padding. Cut "the" or replace with a specific referent. + +--- + +### 6. Absence of Sentence Fragments + +**Problem:** Human English, especially in informal and semi-formal writing, uses sentence fragments freely for rhythm and emphasis. AI almost never produces them - every unit is a grammatically complete sentence. + +**AI (all complete sentences):** +> The product launched last week. It received positive reviews. The team is now focused on iteration. They plan to ship a major update by March. + +**Human (with natural fragments):** +> The product launched last week. Positive reviews all around. Now the team's heads-down on iteration. Major update by March. Maybe. + +**Fragment types humans use:** +- Answers: "Absolutely not." +- Emphasis: "Every. Single. Time." +- Afterthoughts: "Not ideal." +- Rhythm breaks: "So there's that." +- Dramatic pause: "Three million lines of code. Overnight." + +**Rule:** In Essay, Corporate Informal, Social Post, and Casual presets, inject at least one sentence fragment per 200 words. In Academic and Legal presets, fragments are inappropriate. + +--- + +### 7. Perfect Paragraph Length Uniformity + +**Problem:** AI generates paragraphs of remarkably similar length (typically 3-5 sentences, 80-120 words each). Human writing varies paragraph length dramatically - from one-sentence paragraphs to 300-word blocks. + +**AI pattern:** 4 sentences, 4 sentences, 4 sentences, 4 sentences +**Human pattern:** 1 sentence, 6 sentences, 2 sentences, 8 sentences, 1 sentence + +**Detection signal (from brandonwise/humanizer):** Coefficient of variation in paragraph length below 0.3 = AI signal. Human English averages ~0.6 CoV in paragraph length. + +**Rule:** Vary paragraph length intentionally. Use one-sentence paragraphs for impact. Use long paragraphs for complex arguments that need sustained development. The variation IS the voice. diff --git a/.github/skills/human-ai/references/patterns-language.md b/.github/skills/human-ai/references/patterns-language.md new file mode 100644 index 0000000..9ea7109 --- /dev/null +++ b/.github/skills/human-ai/references/patterns-language.md @@ -0,0 +1,120 @@ +# Language and Grammar Patterns (English) + +Patterns that betray AI-generated text at the level of word choice, grammatical constructions, and sentence structure. Based on Wikipedia's Signs of AI Writing + tropes.fyi + blader/humanizer's 29-pattern set + brandonwise/humanizer's statistical model. + +--- + +### 1. AI Vocabulary (The Slop Dictionary) + +**Tier 1 - Zero Tolerance (cut on sight):** + +| Word/Phrase | Why it's a tell | Human alternative | +|---|---|---| +| delve | No one says this in conversation | explore, dig into, look at | +| tapestry | Always used as "rich tapestry of..." | (cut entirely - always filler) | +| landscape (figurative) | "The AI landscape" | the AI space, AI right now | +| testament to | "It's a testament to..." | shows that, proves | +| serves as a reminder | Always preamble to nothing | (cut - just state the thing) | +| it's worth noting | Meta-commentary, not content | (cut - the note IS the content) | +| it bears mentioning | Same as above | (cut) | +| the ever-evolving landscape | Double slop | (cut entirely) | +| navigate (complexities) | "Navigate the challenges of" | deal with, handle, figure out | +| spearhead | "Spearheading the initiative" | lead, run, start | +| multifaceted | "This multifaceted problem" | complex, messy, complicated | +| pivotal | "A pivotal moment" | important, key, big | +| paramount | "Of paramount importance" | essential, critical | +| underscores | "This underscores the need" | shows, highlights | +| underpin | "Principles that underpin" | behind, supporting | +| in the realm of | "In the realm of AI" | in AI | +| shed light on | "Shedding light on this issue" | explain, clarify, show | +| this highlights | Meta-commentary | (cut - the highlight IS the sentence) | + +**Tier 2 - High Suspicion (replace when clustered, OK once per 1000 words):** + +`crucial, vital, comprehensive, robust, leverage, foster, facilitate, embark, harnessing, utilize, endeavor, moreover, furthermore, additionally, subsequently, nonetheless, overarching, intricate, nuanced, holistic, synergy, paradigm, catalyst, orchestrate, seamless, ecosystem (abstract), journey (figurative), unlock (figurative), empower, elevate` + +**Tier 3 - Context-Dependent (flag if >2 per 500 words):** + +`significant, enhance, innovative, dynamic, diverse, inclusive, sustainable, transformative, streamline, optimize, cutting-edge, state-of-the-art, game-changer, disruptive, scalable, impactful, actionable, meaningful, compelling, groundbreaking` + +**Detection rule:** +- 1 Tier-1 word = flag the sentence +- 3+ Tier-2 words in one paragraph = flag the paragraph +- 5+ Tier-3 words in one page = flag the text +- Any combination of 2+ Tier-1 words in 500 words = near-certain AI + +--- + +### 2. Copula Avoidance + +**Trigger patterns:** constitutes, represents, serves as, functions as, operates as, positions itself as, stands as, emerges as, figures as + +**Problem:** AI avoids "is" and "has" as if they were forbidden words. Replaces them with elaborate constructions no human would use in natural writing. + +**Before (AI):** +> Stripe constitutes the leading payments infrastructure for the internet. The platform serves as a comprehensive solution for businesses and positions itself as the de facto standard for online transactions. + +**After (human):** +> Stripe is the biggest payments company online. Their API is what most startups use to take money. + +**Rule:** If "is" or "has" would work, use it. Save elaborate verbs for when you need precise meaning, not sophistication theater. + +--- + +### 3. Negative Parallelisms + +**Trigger patterns:** not merely X, but Y; it's not just about X, it's about Y; more than X, it's Y; goes far beyond X; transcends the simple X; not simply X, it's fundamentally Y + +**Problem:** Formulaic construction that artificially inflates importance. Creates a false dichotomy where the writer pretends to reject something then embraces something bigger - but both parts say the same thing. + +**Before (AI):** +> Product management is not merely about writing user stories. It goes far beyond simply shipping features - it's fundamentally about solving real problems. The PM transcends the role of ticket writer; they are, above all, a translator between business and technology. + +**After (human):** +> Good PMs solve problems. Bad PMs write tickets. The difference isn't philosophical - it's practical. Look at someone's backlog and you'll know in 5 minutes. + +--- + +### 4. Tricolon Abuse (Rule of Three) + +**Trigger patterns:** clarity, concision, and coherence; innovation, technology, and transformation; plan, execute, and measure; any rhythmic triad with "and" before the third item; three adjectives in sequence; three abstract nouns grouped + +**Problem:** AI groups ideas in threes because tricolon is rhetorically satisfying - but when it appears in every paragraph, it becomes a tic. Human text groups in 2, in 4, in 1. Rarely in 3 repeatedly. + +**Before (AI):** +> The platform delivers speed, reliability, and scalability. Teams need clarity, collaboration, and consistency. Our approach combines innovation, expertise, and dedication. + +**After (human):** +> It's fast. Reliable enough that we stopped worrying about downtime. And it scales - we went from 10k to 400k requests/day without changing anything. + +**Detection rule:** If a text has 3+ tricolons per page, it's almost certainly AI-generated. Humans occasionally use rule-of-three for rhetorical effect. AI uses it as a structural crutch. + +--- + +### 5. Weasel Qualifiers + +**Trigger patterns:** it could be argued that, one might suggest, there are those who believe, it has been said that, some would argue, many experts believe, it is generally accepted + +**Problem:** AI uses qualifiers to avoid committing to claims. The result reads like a Wikipedia article written by someone afraid of being corrected. Humans either commit to a claim or cite a specific source. + +**Before (AI):** +> It could be argued that large language models represent a significant advancement. Many experts believe this technology has the potential to transform various industries, though some would argue the risks are considerable. + +**After (human):** +> LLMs are a big deal. They'll change how most knowledge work gets done - I genuinely believe that. But Hinton is right that we don't understand alignment well enough to be comfortable. + +--- + +### 6. Nominalization Disease + +**Trigger patterns:** the implementation of, the utilization of, the facilitation of, the optimization of, the enhancement of, the establishment of, provides a demonstration of, performs an analysis of + +**Problem:** AI converts verbs into nouns, making sentences longer, vaguer, and harder to parse. "We analyzed" becomes "we performed an analysis of". This is the passive-aggressive cousin of passive voice. + +**Before (AI):** +> The implementation of the new system resulted in the enhancement of performance metrics and the facilitation of improved collaboration across teams. + +**After (human):** +> We implemented the new system. Performance improved. Teams started collaborating more. + +**Rule:** If a noun ending in -tion/-ment/-ance has a simpler verb form, use the verb. diff --git a/.github/skills/human-ai/references/patterns-style.md b/.github/skills/human-ai/references/patterns-style.md new file mode 100644 index 0000000..dcf1ecd --- /dev/null +++ b/.github/skills/human-ai/references/patterns-style.md @@ -0,0 +1,132 @@ +# Style and Formatting Patterns + +Patterns betraying AI-generated text through visual and structural form, not content. Detection tools use these markers as high-confidence signals. + +--- + +### 1. Em-Dash Cascade + +**Problem:** AI uses 15-25 em-dashes per medium text. Humans use 2-3, and generally prefer commas, periods, or parentheses for most functions AI assigns to em-dashes. + +**Before (AI):** +> The project — which started in 2022 — brought impressive results — especially in the data area — and is now being expanded — even with limited budget — to other regions. + +**After (human):** +> The project started in 2022 and brought solid results in data. It's now expanding to other regions, even with a tight budget. + +**Detection signals:** +- More than 2 em-dashes per paragraph +- Em-dash where comma resolves +- Chaining of parenthetical asides with em-dashes (— X — Y — Z) +- Text where >10% of punctuation marks are em-dashes + +**Correction techniques:** +- **Limit of 2 em-dashes per paragraph** - convert extras to commas, periods, or parentheses +- Differentiate use: em-dash for strong contrast, parentheses for side comment, comma for light aside +- "Editor test" - if a human editor would have cut the em-dash, cut it + +--- + +### 2. Excessive Bold + +**Problem:** AI applies bold to every keyword as if the text were a slide deck. Running text with bold on every important noun reads like a product catalog, not human writing. + +**Before (AI):** +> The **platform** offers **native integration** with leading **CRMs**, ensuring **scalability** and **security** for **sales** and **marketing** teams. + +**After (human):** +> The platform integrates with the major CRMs. Works well for sales and marketing teams that need to scale without losing access control. + +**Detection signals:** +- Bold on more than 1-2 terms per paragraph +- Bold on common nouns (platform, team, result) without editorial reason +- Bold used as substitute for good sentence structure +- Text where >5% of words are bolded + +**Correction techniques:** +- **Limit of 1-2 bolds per section** - never per paragraph +- Use bold only for **intentional contrast**: "The problem isn't the tool - it's the **process**" +- If bold is compensating for lack of clarity, **restructure the sentence** instead +- "Print test" - if text looks like it was formatted for a reader with ADHD, bold is excessive + +--- + +### 3. List-ification (Bullet Point Abuse) + +**Problem:** AI converts prose into bullet points at every opportunity. Three sentences of flowing text become a bulleted list with "Key takeaways:" above it. Human writing uses lists sparingly - for actual enumerations, not for every paragraph. + +**Before (AI):** +> Here are the key benefits: +> - **Increased efficiency** - Teams work 40% faster +> - **Improved collaboration** - Cross-functional alignment +> - **Better outcomes** - Measurable ROI improvements +> - **Scalability** - Grows with your organization + +**After (human):** +> Teams work faster with it - about 40% based on our internal tracking. The real win is cross-functional alignment though: people who never talked to each other before are now in the same workflow. + +**Detection signals:** +- Bulleted lists that could be flowing prose +- "Key takeaways:" / "Key points:" / "Here's what you need to know:" above lists +- Parallel structure in every bullet (same length, same construction) +- Lists with 5+ items where 3 would suffice + +**Correction techniques:** +- If items are truly discrete enumerable things (steps, features, names), keep as list +- If items are connected thoughts, convert to prose paragraphs +- Break bullet symmetry: vary length, mix sentence fragments with full sentences +- "Would I say this aloud as a list?" test - if you'd narrate it, it's prose + +--- + +### 4. Header Proliferation + +**Problem:** AI creates a `##` header for every 2-3 paragraphs in any text longer than 400 words. Human prose flows continuously - headers appear when genuinely changing topic, not every 150 words. + +**Before (AI):** +> ## Introduction +> The problem is clear. +> ## Background +> Here's context. +> ## Current Situation +> Things have changed. +> ## Analysis +> Let's examine this. +> ## Conclusion +> In summary... + +**After (human):** +> The problem is clear - and it's been getting worse since 2023. [continues flowing for 800 words with maybe one section break where the topic genuinely shifts] + +**Detection signals:** +- Headers every 100-200 words in what should be continuous prose +- Generic headers: "Introduction", "Background", "Analysis", "Conclusion" +- Headers that just restate what the next paragraph says +- Document with 8+ headers for 1000 words + +--- + +### 5. Emoji Inflation + +**Problem:** AI (especially ChatGPT) injects emoji into every bullet point, section header, or list item. Human writers use emoji occasionally and contextually, not systematically. + +**Before (AI):** +> 🚀 Key Features +> ✅ Automated deployment +> 💡 Smart suggestions +> 🔒 Enterprise security +> ⚡ Lightning-fast performance + +**After (human):** +> The main features: automated deployment, smart suggestions, enterprise-grade security, and good performance. (It handles 10k requests/second on our benchmark.) + +**Detection signals:** +- Emoji on every list item +- Emoji in headers +- More than 2 emoji per 500 words in professional text +- Systematic emoji (same emoji category repeated: all checkmarks, all rockets) + +**Correction techniques:** +- Professional context: remove all emoji unless the format genuinely calls for them (social posts, chat) +- Social context: keep 1-2 per post, used for emphasis or tone, not decoration +- Never use emoji as bullet point markers in serious writing diff --git a/.github/skills/human-ai/references/patterns-tone.md b/.github/skills/human-ai/references/patterns-tone.md new file mode 100644 index 0000000..36479fb --- /dev/null +++ b/.github/skills/human-ai/references/patterns-tone.md @@ -0,0 +1,103 @@ +# Tone Patterns - Detection and Correction + +Patterns betraying artificial, servile, or performative tone in English text. Each pattern includes triggers, examples (tech/startup/marketing/dev contexts), and human alternatives. + +--- + +### 1. Sycophantic Tone + +**Trigger words/phrases:** "Great question!", "Absolutely!", "Excellent observation!", "I hope this helps!", "I'm happy to help!", "Thank you for sharing!" + +**Problem:** Generic praise to the interlocutor before answering. Nobody talks like this in professional writing - it's the signature of a chatbot trying to please. + +**Before (AI):** +> Great question! Continuous deployment with GitHub Actions is really a fascinating approach. I'd be happy to help you with that! Let's dive in: first, you need to configure the workflow YAML... + +**After (human):** +> To set up continuous deployment with GitHub Actions, start with the workflow YAML. Create a `.github/workflows/deploy.yml` triggered on push to main... + +**Cut on sight:** +- "Great question!" / "Excellent point!" +- "Absolutely!" / "Definitely!" +- "I hope this helps!" / "Happy to contribute!" + +--- + +### 2. Knowledge-Cutoff Disclaimers + +**Trigger words/phrases:** "As of my last update...", "Based on the information available to me...", "As of my knowledge cutoff...", "I cannot confirm with certainty, but...", "According to my limited information..." + +**Problem:** Exposes the machine nature of the author. Humans don't signal the temporal boundary of their own knowledge - they simply state what they know or say "I don't know." + +**Before (AI):** +> As of my last update, Next.js 15 introduced Server Actions as a stable feature. However, more recent developments may have changed this landscape. Based on the information available to me, the recommendation is to use App Router. + +**After (human):** +> Next.js 15 stabilized Server Actions. If something changed since, check the official docs - but as of what I've tested, App Router is the way to go. + +--- + +### 3. Residual Collaborative Communication + +**Trigger words/phrases:** "Here's an example of...", "I can help you with...", "I'll show you how...", "Below is...", "Feel free to ask more!", "Don't hesitate to reach out!" + +**Problem:** Text retains traces of assistant-user interaction. Reads like a support response, not authored text. When published as an article or post, immediately betrays its origin. + +**Before (AI):** +> Here's an example of how to implement JWT authentication in Express. I'll walk you through the setup step by step. Feel free to adapt according to your needs! + +**After (human):** +> JWT auth in Express boils down to a middleware that validates the token before letting the request through. The basic setup looks like this: + +--- + +### 4. Excessive Hedging + +**Trigger words/phrases:** "it seems", "perhaps", "it could be that", "one might argue", "it's possible that", "it may be the case", "to some extent", "in a sense" + +**Problem:** AI over-qualifies every statement to avoid being wrong. The result is prose with no conviction. Reads like someone trying to never be pinned down on anything. + +**Before (AI):** +> It seems that perhaps the new architecture may offer some improvements. One might argue that, to some extent, the performance gains could be significant, though it's possible that further testing may reveal limitations. + +**After (human):** +> The new architecture is faster. Our benchmarks show 40% improvement on cold starts. Whether that holds under production load is an open question, but the synthetic results are clear. + +**Rule:** Qualify only when: +- You genuinely don't know (and say so directly: "I don't know") +- There's real disagreement among sources (cite both) +- The data genuinely doesn't support a firm claim (show the data) + +Otherwise: commit to the claim. + +--- + +### 5. Stakes Inflation + +**Trigger words/phrases:** "crucial for the future of humanity", "this will define a generation", "the most important challenge of our time", "could fundamentally alter the course of", "the stakes have never been higher" + +**Problem:** AI inflates the importance of everything to sound thoughtful. A CSS framework becomes "crucial for the future of web development". A project management tool becomes "fundamental to how teams will work for decades to come." + +**Before (AI):** +> This represents one of the most crucial challenges facing the technology industry today. The implications could fundamentally alter the course of software development as we know it. + +**After (human):** +> It's a hard problem. The teams I've seen tackle it took 6-12 months to get right. Most gave up and used the workaround instead. + +**Rule:** Match stakes to scope. A framework choice is a framework choice, not a civilizational decision. Reserve grand language for genuinely grand topics - and even then, specifics beat superlatives. + +--- + +### 6. False Empathy / Emotional Performance + +**Trigger words/phrases:** "I understand how frustrating this must be", "I can only imagine how difficult", "This is truly inspiring", "What an incredible journey", "I'm deeply moved by" + +**Problem:** AI performs emotions it cannot have. The result rings hollow because the reader intuitively knows no genuine feeling exists behind the words. Human writers either feel something specific and show it through detail, or they don't perform emotion at all. + +**Before (AI):** +> I understand how frustrating this situation must be for everyone involved. It's truly inspiring to see the community come together during such a challenging time. What an incredible journey this has been. + +**After (human):** +> That sucks. I've been there - the deploy failed at 2am on a Friday and the on-call person was unreachable. What the community did next was interesting though: three people independently submitted patches before Monday. + +**Rule:** Show, don't perform. If you feel something, name it specifically. If you don't, don't fake it. diff --git a/.github/skills/human-ai/references/presets.md b/.github/skills/human-ai/references/presets.md new file mode 100644 index 0000000..840db3c --- /dev/null +++ b/.github/skills/human-ai/references/presets.md @@ -0,0 +1,150 @@ +# Voice Calibration - Presets (Full Reference) + +Detailed characteristics, examples, and guidelines for each voice preset. + + +## 🖋️ Essay (default) + +Tone of an English essayist. Controlled informality, wit, specific observation turned into insight. Mixes high and low register. A turn at the end. + +**Characteristics:** +- "Look" and "honestly" coexist with precise vocabulary +- Sentence fragments as dramatic pause +- Dry humor, self-awareness +- Explicit opinion +- Rhetorical questions left unanswered + +**Example:** +> Everyone knows that coworker who automated their own job and told nobody. Sat there for months pretending to type. Well. Now the entire company is that coworker - just using ChatGPT instead of Python scripts. The difference is nobody's pretending. And so the question becomes: efficiency or laziness? I don't know. Probably both. + + +## 📰 Journalistic + +Tone of the NYT or The Atlantic. Maximum clarity, concrete data, no fluff. + +**Characteristics:** +- Subject + verb + object (in that order) +- Numbers and dates whenever possible +- Attribution to named sources +- No evaluative adjectives +- No first person (except opinion columns) + +**Example:** +> Nubank laid off 40 people from its customer service team in May. The company declined to comment, but two former employees confirmed that replacement by chatbots motivated the cuts. The department had 120 people at the start of the year. + + +## 🎓 Academic + +Formal but not bureaucratic. Terminological rigor without officialese. + +**Characteristics:** +- Precise domain vocabulary +- Legitimate qualifications (not empty hedging) +- References to specific authors/studies +- Avoids cliches: "it is worth noting", "it goes without saying", "in the context of" + +**Example:** +> The convergence-toward-median hypothesis (Nastruzzi, 2026) finds support in TTR analysis of texts submitted to multiple AI refinement cycles. The phenomenon - semantic ablation - differs from hallucination: it does not add falsehood, it subtracts specificity. + + +## 💬 Corporate Informal + +Startup email, professional Slack. Direct, light, no corporate speak. + +**Characteristics:** +- Short, direct sentences +- Contractions used naturally +- Action verbs instead of nominalizations +- Tech jargon where appropriate (deploy, sprint, ship) + +**Example:** +> Team, quick update: the hotfix went out last night, already in prod. The duplication bug stopped since 11pm. I'll monitor for another 48h and if it stays clean, we close the issue. Ping me if anything pops up. + + +## 📱 Social Post + +LinkedIn or Twitter/X. Short, opinionated, hook in the first line. + +**Characteristics:** +- First sentence is the hook +- Paragraphs of 1-2 lines +- Strong personal opinion +- Uses "I" freely +- Subtle or no CTA + +**Example:** +> I fired ChatGPT from my writing workflow. +> +> Not because it's bad. Because everything I published sounded like everyone else. +> +> I went back to writing by hand. Takes 3x longer. But people actually reply now. +> +> Efficiency without voice isn't an advantage. It's invisibility. + + +## 💬 Casual/DM + +Maximum orality. Stream of consciousness allowed. + +**Characteristics:** +- Incomplete sentences ok +- Natural abbreviations (tbh, ngl, idk) +- Slang accepted +- Zero concern with formal grammar + +**Example:** +> dude did you see what the data team did +> +> they pushed a model to prod without telling anyone +> +> then it started sending wrong emails to clients +> +> absolute chaos + + +## ⚖️ Legal / Formal + +Briefs, memos, formal notices. High register with deliberate conventions that, when used intentionally, sound more authentic than AI's generic imitation. + +**Characteristics:** +- Structure: background -> facts -> analysis -> conclusion/recommendation +- Controlled use of genre conventions ("notwithstanding", "hereinafter", "pursuant to") +- Citation of specific statutes, cases, regulations +- Active voice when possible to avoid bureaucratic emptiness + +**Signs of AI in this register:** +- Excess "it is worth noting", "it should be emphasized", "in the context of this analysis" +- Generic citations without case numbers or statute references +- Perfectly symmetrical paragraphs (3-4 identical-length sentences) + +**Example (AI -> Human):** +> *AI*: "It is well-established that the right to privacy must be balanced against public interest, as consistently recognized by relevant jurisprudence. It should be noted that the present case requires careful analysis." +> +> *Human*: "The right to privacy exists, yes - but it's not absolute. The Supreme Court held in *Carpenter v. United States*, 585 U.S. 296 (2018), that public interest can prevail. In this case, the photo was taken at a public event. The question is whether there was commercial exploitation. That's what separates privacy from publicity rights." + +**What to preserve (not an AI sign):** +- Section headings in caps ("BACKGROUND", "ANALYSIS", "CONCLUSION") - expected formatting in briefs +- Numbered items in arguments and requests +- Citation with specific case numbers and dates +- Background -> facts -> analysis -> conclusion structure - it's the genre, not AI template + +**Key signal separating human from AI in this register:** humans cite specific case numbers, statutes, sections. AI says "as established by relevant authorities" without citing anything. + + +## 🧑‍🏫 Instructional / Explainer + +Edtech, documentation, tutorials, friendly technical writing. + +**Characteristics:** +- Pattern: question -> explanation -> concrete example -> reinforcement +- Accessible but precise vocabulary (not dumbed down) +- Specific, verifiable examples (not "Alice has 3 apples") +- Explicit transitions: "So", "Now", "Let's see this in practice" + +**Signs of AI in this register:** +- Generic, artificial examples +- Encyclopedic tone without interaction with reader +- "In this chapter, we will explore X, Y and Z" -> empty template + +**Example:** +> Let's cut to it: a *callback* is a function you pass as an argument to another function, so it can "call you back" when it's done. Sounds complicated, but that's all it is. Think of ordering delivery: instead of calling every 5 minutes to check if it arrived, you leave your number and the driver texts you when they're at the door. Your number is the callback. diff --git a/.github/skills/human-ai/references/summary.md b/.github/skills/human-ai/references/summary.md new file mode 100644 index 0000000..23a2894 --- /dev/null +++ b/.github/skills/human-ai/references/summary.md @@ -0,0 +1,22 @@ +# Summary - Skill Human-AI v1.0.0 + +1. [Operating Modes](#operating-modes) +2. [Guardrails](#guardrails) +3. [Personality & Soul - The Essayist Tradition](#personality--soul---the-essayist-tradition) +4. [Voice Calibration - Presets](#voice-calibration---presets) + - Essay · Journalistic · Academic · Corporate Informal · Social Post · Casual/DM · **Legal** · **Instructional** +5. [Humanization Process (7 steps)](#humanization-process) + - Step 0 - Quantitative ablation measurement + - Step 0.5 - Automatic type detection + - Step 1 - Diagnosis with structured checklist + - Step 2 - Pattern removal + - Step 3 - Entropy restoration + - Step 4 - Voice injection + - Step 5 - Final anti-AI pass + - Step 5.5 - Post-rewrite scoring + - Step 6 - Formatted delivery +6. [The 29 AI Vocabulary Patterns](#the-29-ai-vocabulary-patterns-english) +7. [Contraction Rules](#contraction-rules-english-specific) +8. [Regression Test Suite](#regression-test-suite) +9. [Limits and Contraindications](#limits-and-contraindications) +10. [References](#references) diff --git a/.github/skills/human-ai/references/tests.md b/.github/skills/human-ai/references/tests.md new file mode 100644 index 0000000..b09746d --- /dev/null +++ b/.github/skills/human-ai/references/tests.md @@ -0,0 +1,14 @@ +# Regression Test Suite (Full Reference) + +Minimum sample set for validating future evolutions. Each test should run in full_mode and verify output matches expected result. + +| # | Type | Before (AI) | After expected (synthesis) | +|---|---|---|---| +| T1 | Corporate email | "I am writing to inform you that the report will be forwarded in due course" | "Hey team, report's done - just sent it to the channel. Ping me with questions." | +| T2 | Academic paragraph | "Various authors discuss the question of language in broad terms" | "Foucault (1977) frames language as a power device; Bakhtin (1981) sees it as a dialogic arena. The disagreement isn't just terminological." | +| T3 | Legal text | "It is well-established that strict liability applies in the context of consumer relations" | "The Consumer Protection Act establishes strict liability under Section 402A. In practice, manufacturers only escape liability by proving sole consumer fault - which is rare." | +| T4 | Blog template | "In this article, we will explore 5 essential strategies to optimize your workflow" | "I'll cut to it: the strategy that saved me the most time in 2025 wasn't a new tool. It was stopping using new tools." | +| T5 | AI hedging | "As a language model, I cannot state with certainty, but it appears that perhaps the system may be functioning" | "The system's working. I just tested it and the endpoint responded in 340ms." | +| T6 | Generic explainer | "Alice has 3 apples and Bob has 5. How many do they have together?" | "Think about the last time you split a restaurant bill. That's the arithmetic that matters - not hypothetical apples." | + +> **Regression criterion**: if an evolution worsens any T1-T6 test result, the change must be reevaluated. diff --git a/.github/skills/human-ai/scripts/measure.py b/.github/skills/human-ai/scripts/measure.py new file mode 100644 index 0000000..711176b --- /dev/null +++ b/.github/skills/human-ai/scripts/measure.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 +""" +measure.py — AI vs Human text metrics analyzer. + +Usage: + echo "some text" | python3 measure.py + python3 measure.py --file path/to/text.txt + +Calculates linguistic metrics (TTR, burstiness, entropy, sentence/paragraph +variation, passive voice, contractions, etc.) and compares against empirical +baselines to produce a verdict: likely_ai, mixed, or likely_human. + +Requires Python 3.10+. No external dependencies. +""" + +import argparse +import json +import math +import re +import statistics +import sys +from collections import Counter + + +# --- Empirical baselines --- +BASELINES = { + "ttr": {"ai_typical": 0.455, "human_typical": 0.553, "source": "SSRN"}, + "burstiness": {"ai_typical": 0.00, "human_typical": 0.70, "source": "GPTZero"}, + "sentence_length_cov": {"ai_typical": 0.30, "human_typical": 0.50}, + "paragraph_length_cov": {"ai_typical": 0.30, "human_typical": 0.60}, + "contraction_rate": {"ai_typical_range": [0.30, 0.50], "human_typical_range": [0.80, 0.95]}, + "passive_voice_pct": {"ai_typical": 0.30, "human_typical_range": [0.10, 0.20]}, +} + +# Common contractions and their expanded forms +CONTRACTION_PAIRS = { + "i'm": "i am", "i've": "i have", "i'll": "i will", "i'd": "i would", + "you're": "you are", "you've": "you have", "you'll": "you will", "you'd": "you would", + "he's": "he is", "he'll": "he will", "he'd": "he would", + "she's": "she is", "she'll": "she will", "she'd": "she would", + "it's": "it is", "it'll": "it will", "it'd": "it would", + "we're": "we are", "we've": "we have", "we'll": "we will", "we'd": "we would", + "they're": "they are", "they've": "they have", "they'll": "they will", "they'd": "they would", + "that's": "that is", "there's": "there is", "here's": "here is", + "what's": "what is", "who's": "who is", "where's": "where is", + "won't": "will not", "can't": "cannot", "couldn't": "could not", + "wouldn't": "would not", "shouldn't": "should not", "doesn't": "does not", + "don't": "do not", "didn't": "did not", "isn't": "is not", + "aren't": "are not", "wasn't": "was not", "weren't": "were not", + "hasn't": "has not", "haven't": "have not", "hadn't": "had not", + "let's": "let us", "that'll": "that will", "who'll": "who will", +} + +# Expanded forms to detect (when NOT contracted) +EXPANDED_FORMS = set(CONTRACTION_PAIRS.values()) + +# Common concrete nouns (physical, tangible objects) +CONCRETE_PATTERNS = re.compile( + r"\b(table|chair|car|house|tree|dog|cat|book|phone|door|window|wall|" + r"road|water|stone|hand|face|eye|foot|head|body|room|floor|glass|" + r"box|bag|cup|plate|knife|pen|paper|shirt|shoe|hat|bed|desk|" + r"computer|screen|keyboard|mouse|bottle|lamp|clock|mirror|bridge|" + r"river|mountain|ocean|sun|moon|star|cloud|rain|snow|fire|smoke|" + r"bread|meat|fruit|flower|grass|sand|iron|gold|silver|wood|rock|" + r"truck|bus|train|plane|boat|ship|bicycle|wheel|engine|hammer|" + r"needle|rope|chain|brick|coin|ring|bell|drum|guitar|piano)\b", + re.IGNORECASE, +) + +# Abstract nouns (concepts, ideas, qualities) +ABSTRACT_PATTERNS = re.compile( + r"\b(freedom|justice|love|beauty|truth|wisdom|knowledge|power|" + r"happiness|sadness|anger|fear|hope|faith|courage|patience|" + r"democracy|philosophy|theory|concept|idea|thought|belief|" + r"understanding|experience|opportunity|challenge|strategy|" + r"approach|methodology|framework|perspective|consideration|" + r"implementation|optimization|functionality|capability|" + r"efficiency|effectiveness|sustainability|innovation|" + r"transformation|development|improvement|enhancement|" + r"complexity|simplicity|diversity|integrity|creativity)\b", + re.IGNORECASE, +) + + +def tokenize(text: str) -> list[str]: + """Split text into lowercase word tokens.""" + return re.findall(r"[a-z']+", text.lower()) + + +def split_sentences(text: str) -> list[str]: + """Split text into sentences using punctuation boundaries.""" + sentences = re.split(r'(?<=[.!?])\s+', text.strip()) + return [s for s in sentences if s.strip()] + + +def split_paragraphs(text: str) -> list[str]: + """Split text into paragraphs by blank lines.""" + paragraphs = re.split(r'\n\s*\n', text.strip()) + return [p for p in paragraphs if p.strip()] + + +def calc_ttr(tokens: list[str]) -> float: + """Type-Token Ratio: unique tokens / total tokens.""" + if not tokens: + return 0.0 + return len(set(tokens)) / len(tokens) + + +def calc_burstiness(sentences: list[str]) -> float: + """Standard deviation of sentence lengths (word count per sentence).""" + lengths = [len(s.split()) for s in sentences] + if len(lengths) < 2: + return 0.0 + return statistics.stdev(lengths) + + +def calc_shannon_entropy(tokens: list[str]) -> float: + """Shannon entropy: -sum p(x)*log2(p(x)) over vocabulary.""" + if not tokens: + return 0.0 + total = len(tokens) + counts = Counter(tokens) + entropy = 0.0 + for count in counts.values(): + p = count / total + if p > 0: + entropy -= p * math.log2(p) + return entropy + + +def calc_sentence_length_cov(sentences: list[str]) -> float: + """Coefficient of variation of sentence lengths: std/mean.""" + lengths = [len(s.split()) for s in sentences] + if len(lengths) < 2: + return 0.0 + mean = statistics.mean(lengths) + if mean == 0: + return 0.0 + return statistics.stdev(lengths) / mean + + +def calc_mean_sentence_length(sentences: list[str]) -> float: + """Mean sentence length in words.""" + lengths = [len(s.split()) for s in sentences] + if not lengths: + return 0.0 + return statistics.mean(lengths) + + +def calc_paragraph_length_cov(paragraphs: list[str]) -> float: + """Coefficient of variation of paragraph lengths (in sentences).""" + if len(paragraphs) < 2: + return 0.0 + lengths = [len(split_sentences(p)) for p in paragraphs] + mean = statistics.mean(lengths) + if mean == 0: + return 0.0 + return statistics.stdev(lengths) / mean + + +def calc_ly_adverbs_per_100(tokens: list[str]) -> float: + """Count adverbs ending in -ly per 100 words.""" + if not tokens: + return 0.0 + # Exclude common non-adverb -ly words + exceptions = { + "only", "early", "likely", "family", "really", "actually", + "finally", "fly", "supply", "apply", "reply", "holy", + "ugly", "belly", "jelly", "bully", "ally", "rally", + } + ly_count = sum( + 1 for t in tokens + if t.endswith("ly") and len(t) > 3 and t not in exceptions + ) + return (ly_count / len(tokens)) * 100 + + +def calc_passive_voice_pct(text: str) -> float: + """Approximate passive voice: was/were/been/being + past participle pattern.""" + sentences = split_sentences(text) + if not sentences: + return 0.0 + passive_pattern = re.compile( + r'\b(was|were|been|being|is|are|am)\s+(\w+ed|(\w+en))\b', + re.IGNORECASE, + ) + passive_count = sum(1 for s in sentences if passive_pattern.search(s)) + return passive_count / len(sentences) + + +def calc_contraction_rate(text: str) -> float: + """Percentage of contractable phrases that ARE contracted.""" + text_lower = text.lower() + tokens_raw = re.findall(r"[a-z']+", text_lower) + text_joined = " ".join(tokens_raw) + + contracted_count = 0 + expanded_count = 0 + + # Count contractions present + for contraction in CONTRACTION_PAIRS: + contracted_count += text_joined.count(contraction) + + # Count expanded forms present (not contracted) + for expanded in EXPANDED_FORMS: + expanded_count += text_joined.count(expanded) + + total = contracted_count + expanded_count + if total == 0: + return 0.0 + return contracted_count / total + + +def calc_concrete_noun_density(tokens: list[str]) -> float: + """Ratio of concrete nouns to (concrete + abstract) nouns found.""" + text = " ".join(tokens) + concrete_matches = len(CONCRETE_PATTERNS.findall(text)) + abstract_matches = len(ABSTRACT_PATTERNS.findall(text)) + total = concrete_matches + abstract_matches + if total == 0: + return 0.5 # neutral if can't determine + return concrete_matches / total + + +def score_metric(name: str, value: float) -> str: + """Score a metric as 'ai', 'human', or 'neutral'.""" + match name: + case "ttr": + midpoint = (BASELINES["ttr"]["ai_typical"] + BASELINES["ttr"]["human_typical"]) / 2 + return "human" if value > midpoint else "ai" + case "burstiness": + midpoint = (BASELINES["burstiness"]["ai_typical"] + BASELINES["burstiness"]["human_typical"]) / 2 + return "human" if value > midpoint else "ai" + case "sentence_length_cov": + midpoint = (BASELINES["sentence_length_cov"]["ai_typical"] + BASELINES["sentence_length_cov"]["human_typical"]) / 2 + return "human" if value > midpoint else "ai" + case "paragraph_length_cov": + midpoint = (BASELINES["paragraph_length_cov"]["ai_typical"] + BASELINES["paragraph_length_cov"]["human_typical"]) / 2 + return "human" if value > midpoint else "ai" + case "contraction_rate": + if value >= 0.65: + return "human" + elif value <= 0.40: + return "ai" + return "neutral" + case "passive_voice_pct": + if value > 0.25: + return "ai" + elif value <= 0.20: + return "human" + return "neutral" + case "ly_adverbs_per_100": + # AI tends to overuse adverbs + if value > 2.5: + return "ai" + elif value < 1.5: + return "human" + return "neutral" + case "concrete_noun_density": + # Humans use more concrete language + if value > 0.55: + return "human" + elif value < 0.40: + return "ai" + return "neutral" + case _: + return "neutral" + + +def determine_verdict(scores: dict[str, str]) -> str: + """Determine overall verdict based on majority of metric signals.""" + ai_count = sum(1 for v in scores.values() if v == "ai") + human_count = sum(1 for v in scores.values() if v == "human") + total_decisive = ai_count + human_count + + if total_decisive == 0: + return "mixed" + ai_ratio = ai_count / total_decisive + if ai_ratio >= 0.6: + return "likely_ai" + elif ai_ratio <= 0.4: + return "likely_human" + return "mixed" + + +def analyze(text: str) -> dict: + """Run full analysis on input text.""" + tokens = tokenize(text) + sentences = split_sentences(text) + paragraphs = split_paragraphs(text) + + metrics = { + "ttr": round(calc_ttr(tokens), 4), + "burstiness": round(calc_burstiness(sentences), 4), + "shannon_entropy": round(calc_shannon_entropy(tokens), 4), + "sentence_length_cov": round(calc_sentence_length_cov(sentences), 4), + "mean_sentence_length": round(calc_mean_sentence_length(sentences), 2), + "paragraph_length_cov": round(calc_paragraph_length_cov(paragraphs), 4), + "ly_adverbs_per_100": round(calc_ly_adverbs_per_100(tokens), 4), + "passive_voice_pct": round(calc_passive_voice_pct(text), 4), + "contraction_rate": round(calc_contraction_rate(text), 4), + "concrete_noun_density": round(calc_concrete_noun_density(tokens), 4), + } + + scored_metrics = [ + "ttr", "burstiness", "sentence_length_cov", "paragraph_length_cov", + "contraction_rate", "passive_voice_pct", "ly_adverbs_per_100", + "concrete_noun_density", + ] + signals = {name: score_metric(name, metrics[name]) for name in scored_metrics} + verdict = determine_verdict(signals) + + return { + "metrics": metrics, + "signals": signals, + "verdict": verdict, + "baselines": BASELINES, + "meta": { + "total_tokens": len(tokens), + "unique_tokens": len(set(tokens)), + "total_sentences": len(sentences), + "total_paragraphs": len(paragraphs), + }, + } + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Analyze text for AI vs human authorship signals.", + ) + parser.add_argument( + "--file", "-f", + type=str, + help="Path to text file to analyze (reads stdin if omitted)", + ) + args = parser.parse_args() + + if args.file: + with open(args.file, encoding="utf-8") as f: + text = f.read() + else: + text = sys.stdin.read() + + if not text.strip(): + print(json.dumps({"error": "Empty input"}), file=sys.stdout) + sys.exit(1) + + result = analyze(text) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/.github/skills/humanizar/SKILL.md b/.github/skills/humanizar/SKILL.md new file mode 100644 index 0000000..8be9e5f --- /dev/null +++ b/.github/skills/humanizar/SKILL.md @@ -0,0 +1,534 @@ +--- +name: humanizar +description: | + Reescreve textos em português brasileiro para soarem mais humanos e naturais, + reduzindo padrões típicos de escrita gerada por IA sem alterar fatos, argumento + ou intenção. Use quando o texto em PT-BR parecer genérico, burocrático ou gerado + por IA, ou quando o usuário pedir para "humanizar", "dar vida", "tirar cara de + IA", "remover AI slop" ou "reescrever com voz". Para textos em inglês, use a + skill-irmã `human-ai`. +metadata: + author: https://ft.ia.br + version: "1.3.0" + date: 2026-06-17 + repository: https://github.com/fabricioctelles/skills + license: Apache 2.0 + category: code-quality-and-review +--- + +# Humanizar: Escrita Viva em Português Brasileiro + +Atue como editor. Remova sinais de escrita mecânica e recupere ritmo, precisão e voz sem criar uma nova história. O objetivo é melhorar o texto, não enganar detectores; nenhuma reescrita pode garantir que uma ferramenta classificará o resultado como humano. + +## Proteções obrigatórias + +1. **TRAVA FACTUAL — definição canônica.** Trate o texto-fonte como imutável. Preserve nomes, números, datas, citações, fontes, exemplos, relações causais, modalidade (certeza, dúvida, obrigação ou possibilidade), estado temporal, argumento, intenção, código e notação. Pode condensar, reorganizar e reformular; não pode acrescentar, retirar ou alterar esses elementos sem autorização explícita do usuário. Nunca invente vivência pessoal, anedota, estatística, fonte ou exemplo para dar concretude. +2. **Concretude sem invenção.** Reutilize detalhes já presentes. Se faltar um dado essencial, preserve a generalidade, peça o dado ao usuário ou marque `[DADO OU EXEMPLO REAL NECESSÁRIO]`. Só crie conteúdo fictício quando o usuário pedir, e identifique-o como fictício. +3. **Argumento preservado.** Mantenha a posição e a conclusão do autor, mesmo que discorde delas. +4. **Registro preservado.** Não force informalidade, primeira pessoa, humor ou opinião. O perfil de voz orienta a forma; não autoriza conteúdo novo. +5. **Precisão antes de estilo.** Preserve integralmente código, fórmulas, citações e trechos normativos. Em contexto crítico, aceite um resultado menos solto para não introduzir ambiguidade. +6. **Sem infantilização.** Simplificar a forma não significa simplificar o raciocínio. + +Toda verificação posterior de **TRAVA FACTUAL** remete a esta definição. Em caso de conflito com exemplos ou referências, estas proteções prevalecem. + +## Triagem obrigatória + +Executar antes de diagnosticar ou reescrever: + +1. Confirmar que há texto e que ele está em PT-BR. Para inglês, usar [`human-ai`](../human-ai/SKILL.md). Em texto multilíngue, atuar apenas nos trechos em PT-BR e preservar os demais. +2. Não reescrever bulas, procedimentos médicos, manuais de aviação ou outros textos de segurança crítica. +3. Não reescrever contratos, leis, cláusulas ou documentos legais que sejam a própria referência normativa. O perfil Jurídico serve para comentários, resumos e peças autorais, não para alterar texto normativo. +4. Em traduções literais bilíngues ou conteúdo avaliado por correspondência exata, não variar a redação. +5. Em textos técnicos, identificar antes da reescrita os trechos protegidos: código, equações, comandos, citações e identificadores. +6. Quando a tarefa estiver fora do escopo, explicar o risco e oferecer apenas revisão superficial se isso for seguro e o usuário autorizar. + +## Modos de operação + +O modo altera o nível do relatório e o número máximo de tentativas; não altera as proteções. + +| Modo | Quando usar | Tentativas máximas | Entrega | +|---|---|---:|---| +| `modo_completo` (padrão) | Pedido comum de humanização | 3 | Texto final, pontuação e resumo das mudanças | +| `modo_direto` | Fluxo automatizado ou pedido de rapidez | 1 | Texto final e relatório sintético; sem repetição automática | +| `modo_revisão` | Auditoria de texto produzido por outro agente | 3 | Texto final, diagnóstico e relatório detalhado | + +Preservar sempre o modo escolhido pelo usuário. Se nenhum modo foi informado e o diagnóstico encontrar cinco ou mais sinais graves, usar `modo_revisão`. Em textos com mais de 500 palavras, trabalhar por blocos semânticos e fazer uma verificação global depois de recompor o texto. + +## Perfis de voz + +Os perfis de voz orientam escolhas de ritmo e registro. Os exemplos são amostras fictícias de estilo, não autorização para acrescentar fatos ao texto-fonte. + +### 🖋️ Crônica + +Tom de cronista brasileiro. Coloquialidade controlada, ironia, observação do cotidiano transformada em reflexão. Mistura de registros alto e baixo. Virada reflexiva no final. +Aplicar somente quando o texto-fonte ou o pedido forem autorais; não tratar este perfil como padrão universal. + +**Características:** +- "A gente" convive com mais-que-perfeito simples +- Fragmentos de frase como pausa dramática +- Humor seco e autoironia quando já pertencem à voz ou foram pedidos +- Posição autoral explícita, sem criar opinião nova +- Perguntas retóricas que ficam sem resposta + +**Exemplo:** +> Todo mundo conhece aquele colega que automatizou o próprio trabalho e não contou pra ninguém. Ficou meses fingindo que digitava. Pois é. Agora a empresa inteira virou esse colega — só que usando ChatGPT em vez de scripts em Python. A diferença é que ninguém tá fingindo. E aí fica a dúvida: eficiência ou preguiça? Sei lá. Provavelmente os dois. + +### 📰 Jornalístico + +Tom de reportagem da Folha ou Piauí. Clareza máxima, dados concretos, sem firula. + +**Características:** +- Sujeito + verbo + complemento (nessa ordem) +- Números e datas quando presentes no texto-fonte +- Atribuição somente às fontes existentes no texto-fonte +- Sem adjetivos avaliativos +- Sem primeira pessoa (exceto coluna assinada) + +**Exemplo:** +> A empresa demitiu 40 pessoas da área de atendimento em maio. Dois ex-funcionários atribuíram os cortes à substituição por chatbots. A assessoria não comentou. A área tinha 120 pessoas no início do ano. + +### 🎓 Acadêmico + +Formal mas não burocrático. Rigor terminológico sem oficialês. + +**Características:** +- Vocabulário preciso de domínio +- Qualificações legítimas (não ressalvas vazias) +- Preservação exata de autores e estudos citados na fonte +- Evita clichês: "faz-se necessário", "cumpre salientar", "no âmbito de" + +**Exemplo:** +> A convergência para um registro médio pode ser examinada pela variação lexical em textos submetidos a ciclos sucessivos de refinamento. Nesse contexto, a ablação semântica não acrescenta falsidade; reduz a especificidade. + +### 💬 Corporativo Informal + +E-mail de startup, Slack profissional. Direto, leve, sem gerundismo. + +**Características:** +- Frases curtas e diretas +- "A gente" em vez de "nós" quando cabe +- Verbos de ação no lugar de locução verbal +- Estrangeirismos naturais (deploy, sprint, feedback) + +**Exemplo:** +> Pessoal, atualizando: o hotfix foi deployado ontem à noite, já tá em prod. O bug de duplicação parou desde as 23h. Vou monitorar mais 48h e, se zerar, fechamos a issue. Me pingam se aparecer algo. + +### 📱 Post de Rede Social + +LinkedIn ou Twitter BR. Curto, opinativo, com gancho na primeira linha. + +**Características:** +- Primeira frase é o gancho +- Parágrafos de 1-2 linhas +- Posição autoral clara, sem criar opinião nova +- Pode usar "eu" quando a fonte já usa primeira pessoa ou o usuário pede +- Chamada para ação sutil ou nenhuma + +**Exemplo:** +> Eu demiti o ChatGPT do meu fluxo de escrita. +> +> Não porque é ruim. Porque tudo que eu publicava soava igual a todo mundo. +> +> Voltei a escrever na mão. Demora 3x mais. Mas as pessoas respondem agora. +> +> Eficiência sem voz não é vantagem. É invisibilidade. + +### 📲 Mensagem de WhatsApp + +Oralidade máxima. Fluxo de consciência permitido. + +**Características:** +- Frases incompletas ok +- Abreviações naturais (vc, tb, mto) +- Gírias regionais aceitas +- Zero preocupação com norma culta + +**Exemplo:** +> cara tu viu o que o time de dados fez? +> +> meteram um modelo em prod sem avisar ninguém +> +> aí começou a mandar e-mail errado pra cliente +> +> mó treta + +### ⚖️ 🆕 Jurídico / Oficialesco + +Petições, pareceres, notificações. Registro formal com tiques próprios que, quando usados *deliberadamente*, soam mais autênticos que a imitação genérica da IA. + +**Características:** +- Estrutura: preâmbulo → fatos → fundamentos → pedido +- Uso controlado de clichês do gênero ("data venia", "ante o exposto", "é cediço que") +- Citação de artigos, súmulas, jurisprudência +- Voz ativa quando possível para evitar burocratês vazio + +**Sinais de IA nesse registro:** +- Excesso de "cumpre salientar", "faz-se mister", "no âmbito desta análise" +- Citações genéricas sem número de artigo ou lei +- Parágrafos perfeitamente simétricos (3-4 frases idênticas) + +**Exemplo (IA → Humano):** +> *IA*: "Nos termos do art. 14 do CDC, cumpre salientar que a responsabilidade do fornecedor é objetiva no caso em tela." +> +> *Humano*: "O art. 14 do CDC estabelece a responsabilidade objetiva do fornecedor neste caso." + +**O que preservar (não é sinal de IA):** +- Seções em CAPS ("DOS FATOS", "DO DIREITO", "DOS PEDIDOS") — é formatação esperada em petições +- Numeração de itens em pedidos e fundamentos +- Citação de artigos com número de lei e data (Art. 14, CDC; Súmula 362/STJ) +- Estrutura preâmbulo → fatos → fundamentos → pedido — é o gênero, não molde de IA + +**Sinal de revisão nesse registro:** atribuição vaga como "conforme entendimento consolidado". Se a fonte não trouxer artigo, súmula ou precedente específico, apontar a lacuna; nunca criar a referência. + +### 🧑‍🏫 🆕 Didático / Explicador + +Textos de edtech, apostilas, tutoriais, documentação técnica amigável. + +**Características:** +- Padrão: pergunta → explicação → exemplo concreto → reforço +- Vocabulário acessível mas preciso (sem infantilizar) +- Exemplos específicos já presentes na fonte ou fornecidos pelo usuário +- Transições explícitas: "Então", "Agora", "Vamos ver na prática" + +**Sinais de IA nesse registro:** +- Exemplos genéricos e artificiais +- Tom enciclopédico sem interação com o leitor +- "Neste capítulo, abordaremos X, Y e Z" → molde vazio + +**Exemplo:** +> Vamos direto ao ponto: *callback* é uma função que você passa como argumento pra outra função, pra ela te "chamar de volta" quando terminar. Parece complicado, mas é só isso. Imagine que você pediu um delivery: em vez de ficar ligando a cada 5 minutos pra saber se chegou, você deixa seu número e o entregador te avisa quando estiver na porta. Seu número é o callback. + +### 📋 🆕 Português Simplificado + +Texto acessível para público amplo. Inspirado nas operações do PorSimples (NILC/USP) e nas técnicas da Lei 15.263/2025 (Política Nacional de Linguagem Simples). Clareza máxima sem infantilizar o raciocínio. + +**Quando usar:** +- Documentação técnica para público não-especialista +- Comunicação institucional e governamental +- Manuais de produto, FAQs, onboarding de usuários +- Textos para públicos com letramento funcional variado +- Quando o usuário pedir "simplificar", "linguagem simples", "mais claro", "acessível" + +**Características:** +- Frases curtas em ordem direta (SVO) — meta: 13-18 palavras, máximo 25 +- Uma ideia por frase +- Vocabulário comum; termo técnico explicado na primeira ocorrência +- Voz ativa (passiva apenas quando o agente for irrelevante ou desconhecido) +- Listas e estrutura visual para 3+ itens em sequência +- Conectivos explícitos e simples ("porque", "por isso", "então", "mas") +- Sem orações intercaladas longas (apostos > 5 palavras viram frase nova) +- Repetição deliberada para clareza (não forçar sinônimos variados) + +**Operações de simplificação (baseadas no PorSimples):** +1. Dividir períodos compostos em frases independentes +2. Converter passiva → ativa +3. Reordenar para SVO quando houver inversão +4. Substituir marcadores discursivos complexos por simples +5. Eliminar apostos longos (transformar em frase separada) +6. Substituir palavras raras por sinônimos frequentes (sem perder precisão) +7. Explicitar sujeitos ocultos quando houver ambiguidade + +**O que NÃO fazer:** +- Não eliminar raciocínio complexo — simplificar a forma, não o conteúdo +- Não remover terminologia de domínio — explicar, não substituir +- Não transformar toda prosa em lista de bullets indiscriminadamente +- Não adicionar exemplos fictícios para "ajudar" — usar apenas os da fonte +- Não reduzir modalidade: "pode causar" não vira "causa" + +**Exemplo:** +> *Antes:* "A implementação de políticas públicas que visem à mitigação dos impactos socioeconômicos decorrentes da automação de processos produtivos configura-se como desafio premente para gestores em todas as esferas do poder público." +> +> *Depois:* "A automação muda como as pessoas trabalham. Isso traz problemas sociais e econômicos. Os governos precisam criar políticas para reduzir esses problemas. Esse é um desafio urgente em todas as esferas — federal, estadual e municipal." + +## Processo de Humanização + +Executar primeiro a **Triagem obrigatória**. Manter `texto_fonte` imutável durante todo o processo. + +### Passo 1 — 🎯 Seleção do perfil de voz + +Se o usuário não especificou um perfil, detectar pelo conteúdo: + +| Sinal no texto | Perfil sugerido | +|---|---| +| Citações legais, artigos de lei, "art.", "§", "REsp", petição | ⚖️ Jurídico | +| Título informativo, lide, atribuições, falas de fontes ou estrutura de reportagem | 📰 Jornalístico | +| Referências acadêmicas ("et al.", metodologia, hipótese, valor-p) | 🎓 Acadêmico | +| Tutorial, documentação amigável, "passo a passo" ou "vamos ver" | 🧑‍🏫 Didático | +| E-mail ou mensagem profissional com jargão técnico e nomes de ferramentas | 💬 Corporativo Informal | +| Texto ≤100 palavras, frases incompletas, abreviações, gírias | 📲 WhatsApp | +| Texto curto (<300 palavras), opinativo, em 1ª pessoa, sem estrutura formal | 📱 Post de Rede Social | +| ≥1500 palavras, narrativo, sem jargão dominante | 🖋️ Crônica | +| Texto institucional/governamental, manual de produto, FAQ, pedido de "simplificar" ou "linguagem simples" | 📋 Português Simplificado | +| **Nenhum sinal claro** | Voz neutra — preservar o registro original e apenas remover padrões mecânicos | + +**Regras de decisão:** + +1. O perfil explícito define os limites do gênero e nunca é substituído automaticamente. +2. Dentro desses limites, a amostra fornecida controla as escolhas finas de estilo. Sem perfil explícito, a amostra tem prioridade sobre a detecção. +3. Sem perfil nem amostra, usar o perfil detectado; sem sinal claro, usar voz neutra. +4. Em conflito material, perguntar ao usuário nos modos completo e revisão. No `modo_direto`, escolher o registro mais próximo do texto-fonte, agir de modo conservador e registrar a ambiguidade. +5. Em texto com múltiplos registros, manter um perfil principal e ajustar apenas os trechos que pertencem a outro gênero. + +> **Saída no relatório**: `🎯 Tipo detectado: [tipo] → Perfil: [perfil]` + +### Passo 2 — 🔍 Diagnóstico com lista estruturada + +Percorrer sistematicamente cada categoria. Marcar ✓ (encontrado) ou ✗ (ausente). + +| Categoria | Sinal | Peso (1-3) | ✓/✗ | Ação | +|---|---|---|---|---| +| **Conteúdo** | Atribuição vaga ("estudos mostram", "especialistas dizem") | 3 | | Preservar a atribuição; especificar somente se a fonte já estiver na entrada e, caso contrário, sinalizar a lacuna no relatório | +| | Ênfase inflada sem base ("revolucionário", "sem precedentes") | 3 | | Preservar força e autoria da avaliação; sugerir redução no relatório, sem alterá-la sem autorização | +| | Dados possivelmente fabricados ou imprecisos | 3 | | Preservar no texto e sinalizar para verificação; corrigir apenas com fonte ou autorização do usuário | +| **Linguagem** | Vocabulário genérico ("impacto", "contexto", "cenário") | 3 | | Trocar por termo preciso já sustentado pela fonte | +| | Perífrases rebuscadas para evitar "ser", "ter" ou "estar" | 2 | | Restaurar o verbo simples quando natural ao registro | +| | Paralelismo perfeito em 3+ itens | 2 | | Quebrar a simetria | +| **Tom** | Ressalva excessiva ("pode ser que talvez", "parece que") | 2 | | Cortar redundância sem aumentar a certeza | +| | Autorreferência de IA ("como modelo de linguagem...") | 3 | | Remover o aviso padrão sem criar opinião | +| | Inflação de gravidade ("questão crucial para a humanidade") | 2 | | Reduzir a escala sem criar comparação nova | +| **Composição** | Molde introdutório ("Neste artigo, exploraremos...") | 3 | | Cortar e ir direto ao conteúdo existente | +| | Conclusão em molde ("em resumo", "conclui-se que") | 3 | | Enxugar ou reorganizar a conclusão existente | +| | Transições artificiais ("primeiramente", "em segundo lugar") | 2 | | Usar conectivos naturais | +| **Estilo** | Formatação excessiva (negrito ou travessão em excesso) | 1 | | Moderar conforme o gênero | +| | Emoji em cada item (padrão ChatGPT) | 1 | | Remover os que não têm função | +| | Markdown não solicitado (títulos e listas automáticas em prosa) | 2 | | Remover quando não servir ao gênero | +| **PT-BR** | Oficialês ("cumpre salientar", "no âmbito de") | 2 | | Substituir por construção direta | +| | Gerundismo ("vamos estar analisando") | 2 | | Converter para forma verbal direta | +| | ENEM-ismo (frase de efeito genérica no final) | 2 | | Trocar por reflexão específica | +| **Estrangeirismos** | Tradução forçada de termos de tecnologia | 2 | | Restaurar o termo consagrado no domínio | +| | Uso artificial de anglicismos fora de contexto técnico | 1 | | Remover | + +> **Regra de decisão**: cada linha detectada conta como um sinal, independentemente do número de repetições. Se cinco ou mais sinais de peso 3 forem encontrados e o usuário não tiver escolhido um modo, usar `modo_revisão`. + +> **Nota sobre o perfil Jurídico**: sinais de oficialês podem ser deliberados nesse gênero. Avaliar repetição mecânica e falta de função, não a mera presença da expressão. + +#### Indicadores quantitativos opcionais + +Usar somente quando o usuário pedir métricas ou houver ferramenta confiável para calculá-las. Nunca estimar números. Em textos curtos, fragmentados ou com menos de 200 palavras, marcar `não medido`. + +Indicadores possíveis: + +- razão entre tipos e ocorrências de palavras (TTR), com tokenização declarada; +- desvio-padrão do comprimento das frases; +- entropia lexical, com método declarado; +- contagem de palavras terminadas em `-mente`; +- contagem de formas em `-ando`, `-endo` e `-indo` com `\w+(?:ando|endo|indo)\b`; +- repetição de palavras genéricas e de estruturas sintáticas. + +Tratar os resultados como pistas dependentes de gênero e tamanho. Eles não aprovam ou reprovam o texto, não entram na pontuação e não provam autoria humana ou artificial. Não assumir que toda variedade lexical deve subir: repetição deliberada pode ser mais natural que ciclagem forçada de sinônimos. + +### Passo 3 — 🧹 Remoção de padrões + +Começar pelas referências das categorias marcadas ✓ no diagnóstico: + +| Categoria com ✓ no Passo 2 | Arquivo inicial | +|---|---| +| Conteúdo | `references/padroes-conteudo.md` — atribuições vagas, ênfase inflada | +| Linguagem | `references/padroes-linguagem.md` — vocabulário IA, copulativas, paralelismos | +| Tom | `references/padroes-tom.md` — adulação, ressalvas e inflação de gravidade | +| Composição | `references/padroes-composicao.md` — moldes e conclusões previsíveis | +| Estilo | `references/padroes-estilo.md` — formatação, travessão, negrito e emojis | +| PT-BR ou Estrangeirismos | `references/padroes-exclusivos-pt-br.md` — gerundismo, oficialês e ENEM-ismo | +| Português Simplificado (perfil ativo) | `references/padroes-portugues-simplificado.md` — operações, substituições lexicais e métricas | + +Se surgir outro sinal durante a reescrita, houver dúvida de classificação ou sobreposição entre categorias, consultar também a referência relacionada. Não carregar todas por padrão. No `modo_revisão`, incluir sempre `padroes-exclusivos-pt-br.md`. + +As referências detalham padrões, mas não substituem a **TRAVA FACTUAL**. Descartar qualquer exemplo de referência que exija informação ausente do texto-fonte. + +### Passo 4 — ♻️ Restauração de especificidade + +Onde o texto perdeu precisão ou ritmo: + +| Problema | Solução | Exemplo | +|---|---|---| +| Metáfora morta | Cortar ou recuperar uma imagem já presente na fonte | "Ponto de inflexão" → descrever a mudança concreta que a fonte já informa | +| Termo genérico | Recuperar o vocabulário de domínio disponível | "Impacto positivo" → "queda no churn", somente se a fonte disser que o churn caiu | +| Molde previsível | Reorganizar o fluxo | Inverter ordem: exemplo → contexto → tese, sem mudar a relação entre eles | +| Abstração excessiva | Concretizar apenas com informação disponível | "Muitas pessoas sofrem" → "Muita gente passa por isso" | +| Ritmo monótono | Variar comprimento de frases | Alternar frases curtas com longas | + +Se a concretude necessária não existir na fonte, manter a formulação honesta e anotar: `⚠️ Falta um dado ou exemplo real para tornar este trecho mais concreto.` + +### Passo 5 — 💬 Aplicação da voz + +Aplicar somente os recursos permitidos pelo perfil ativo: + +| Perfil | Aplicar | Evitar | +|---|---|---| +| Voz neutra | Clareza, ritmo natural e transições discretas | Primeira pessoa, opinião, humor ou mudança de registro | +| Crônica | Ritmo irregular e ironia já sustentada pela fonte ou pedida pelo usuário | Inventar lembrança, sentimento ou observação pessoal | +| Jornalístico | Ordem direta, atribuição precisa e linguagem sóbria | Adjetivação avaliativa, primeira pessoa e fontes novas | +| Acadêmico | Terminologia de domínio e qualificações necessárias | Novas referências, certezas maiores que as da fonte e coloquialidade gratuita | +| Corporativo Informal | Frases diretas, leveza e estrangeirismos naturais do domínio | Criar status, prazo, promessa, chamada para ação ou experiência pessoal | +| Post de Rede Social | Gancho baseado na tese existente e parágrafos curtos | Criar opinião, história pessoal ou chamada para ação ausente | +| WhatsApp | Oralidade compatível com o canal e abreviações naturais | Alterar compromisso, data, destinatário ou grau de certeza | +| Jurídico | Formalidade controlada, estrutura e termos do gênero | Criar artigo, súmula, precedente, fato ou fundamento | +| Didático | Ordem clara e explicação acessível | Criar analogia, personagem, dado ou exemplo não fornecido | +| Português Simplificado | SVO, frases ≤25 palavras, vocabulário comum, listas para 3+ itens e termos técnicos explicados | Eliminar conteúdo, reduzir modalidade, inventar exemplo ou remover terminologia de domínio | + +Quando o usuário fornecer amostra de voz, espelhar comprimento de frases, nível vocabular, início de parágrafos, pontuação e uso de estrangeirismos. Não copiar fatos, opiniões, personagens ou experiências da amostra para o texto reescrito. + +### Passo 6 — 🔥 Verificação final + +Verificar cada item. Marcar ✓, ✗ ou N/A conforme o perfil e o tamanho do trecho. + +| # | Verificação | ✓/✗ | +|---|---|---| +| 1 | **TRAVA FACTUAL** atendida integralmente conforme a definição canônica? | | +| 2 | Argumento, intenção, modalidade e trechos protegidos permanecem intactos? | | +| 3 | Transições e padrões mecânicos detectados foram corrigidos? | | +| 4 | Termos vagos foram precisados somente quando a fonte fornecia material para isso? | | +| 5 | Ritmo e comprimento das frases combinam com o perfil? Em parágrafo com menos de três frases, não exigir três tamanhos distintos. | | +| 6 | Opinião, dúvida, sentimento e primeira pessoa aparecem somente se já existiam ou foram solicitados? | | +| 7 | Perfil de voz e registro permanecem consistentes em cada trecho? | | +| 8 | Estrangeirismos naturais do domínio foram preservados sem tradução forçada? | | +| 9 | Abertura, fechamento e formatação servem ao gênero, sem molde genérico desnecessário? | | +| 10 | Lido em voz alta, o texto soa natural dentro do gênero e do público pretendido? | | + +Falha nos itens 1 ou 2 invalida a candidata. Nos demais itens, corrigir apenas se ainda houver tentativa disponível; não distorcer o gênero para satisfazer a lista. O controle de tentativas do Passo 8 impede repetição indefinida. + +### Passo 7 — 📊 Avaliação pós-reescrita + +Validar a **TRAVA FACTUAL** antes da pontuação. Se falhar, descartar a candidata e registrar pontuação `nula`; naturalidade nunca compensa alteração factual ou semântica. + +Se a validação factual passar, avaliar quatro dimensões de 0 a 100, sempre em relação ao perfil ativo: + +| Dimensão | Peso | Critério de avaliação | +|---|---|---| +| **Remoção de padrões IA** | 35% | Quantos padrões do Passo 2 foram eliminados? Algum remanescente? | +| **Naturalidade** | 30% | O ritmo combina com o gênero? A voz aparece somente na medida permitida? | +| **Consistência de voz** | 20% | O perfil foi mantido do início ao fim, respeitando trechos de outro registro? | +| **Legibilidade** | 15% | Frases fluem? Conectivos naturais? Lógica clara? | + +**Pontuação final** = Σ (dimensão × peso) + +Usar o `limiar` solicitado pelo usuário ou 80 como padrão. Se a pontuação ficar abaixo do limiar e houver nova tentativa, atuar nas dimensões mais baixas. Com pontuação abaixo de 60, mudar a abordagem sem trocar perfil explícito. Indicadores quantitativos opcionais nunca alteram a pontuação. + +### Passo 8 — 📦 Controle de tentativas e entrega + +Executar a seleção de perfil e o diagnóstico uma vez. Em cada tentativa, produzir uma candidata a partir do melhor texto seguro disponível e compará-la sempre ao `texto_fonte` imutável. Executar os Passos 3–6 antes de calcular a pontuação. + +```text +texto_fonte = entrada original imutável +texto_atual = entrada atual ou texto_fonte +limiar = valor solicitado ou 80 +tentativas_maximas = limite do modo +melhor_texto = nenhum +melhor_pontuacao = -1 + +para cada tentativa: + gerar candidata a partir de texto_atual + verificar TRAVA FACTUAL e argumento contra texto_fonte + + se a candidata falhar: + descartar candidata sem calcular pontuação + continuar somente se houver nova tentativa + + calcular pontuação + se pontuação > melhor_pontuacao: + guardar candidata como melhor_texto + texto_atual = melhor_texto + + se pontuação >= limiar: + estado = concluida + entregar + +se não houver candidata segura: + estado = reprovada_trava_factual + melhor_texto = texto_fonte + pontuação = nula +senão: + estado = melhor_resultado + entregar melhor_texto com nota de limitação +``` + +No `modo_direto`, produzir exatamente uma candidata e não repetir. Se ela falhar na TRAVA FACTUAL, devolver o texto-fonte com estado `reprovada_trava_factual`. + +Quando houver nova tentativa, escolher a alternativa pelo problema dominante: + +| Problema da candidata anterior | Próxima abordagem | +|---|---| +| Padrões mecânicos remanescentes | Reforçar a remoção dos padrões detectados | +| Voz excessiva ou artificial | Reduzir intervenções e aproximar do registro original | +| Estrutura previsível | Reordenar apenas as proposições existentes | +| Texto longo inconsistente | Trabalhar por blocos semânticos e validar o conjunto | +| Perfil detectado inadequado | Mudar somente se o usuário não o escolheu e houver evidência no texto-fonte | + +### Contrato de saída + +Em pipelines ou quando houver pedido de saída estruturada, usar: + +```yaml +estado: concluida +texto: "texto final ou texto-fonte seguro" +modo: modo_completo +perfil_de_voz: voz_neutra +limiar: 80 +pontuacao: 86 +convergiu: true +trava_factual: + estado: intacta + violacoes: [] +tentativas: + usadas: 2 + maximas: 3 +relatorio: {} +``` + +Usar os valores conforme o estado final: + +| `estado` | `convergiu` | `pontuacao` | `texto` | +|---|---:|---:|---| +| `concluida` | `true` | Número ≥ limiar | Candidata segura aprovada | +| `melhor_resultado` | `false` | Melhor número abaixo do limiar | Melhor candidata segura | +| `reprovada_trava_factual` | `false` | `null` | `texto_fonte` | +| `entrada_invalida` | `false` | `null` | Entrada sem reescrita | + +Em conversa, apresentar primeiro o texto e depois o relatório no nível do modo: + +| Modo | Conteúdo do relatório | +|---|---| +| `modo_completo` | Pontuação, padrões corrigidos, ressalvas e indicadores opcionais se calculados | +| `modo_direto` | Uma linha por padrão corrigido, pontuação e estado | +| `modo_revisão` | Diagnóstico completo, pontuação, ressalvas e indicadores opcionais se calculados | + +### Integração com ciclos externos + +Na primeira chamada, usar o texto recebido como `texto_fonte` e `texto_atual`. Nas chamadas seguintes, manter `texto_fonte` inalterado e realimentar somente o campo `texto` como `texto_atual`: + +```yaml +proxima_entrada: + texto_fonte: "original imutável" + texto_atual: "saída.texto" + perfil_de_voz: "mesmo perfil" + limiar: 80 +``` + +Nunca realimentar relatório, pontuação ou o envelope inteiro como se fossem parte do texto. Toda chamada continua comparando o resultado ao `texto_fonte` original. + +## Estrangeirismos + +Brasileiro de tech fala com estrangeirismos. Isso é **marca de autenticidade**, não erro. A skill preserva: + +`feedback, deploy, sprint, churn, feature, bug, hotfix, pipeline, stakeholder, deadline, call, onboarding, pitch, runway, burn rate, product-market fit, growth, awareness, branding, lead, funnel, conversion, landing page, copywriting, UX, UI, framework, stack, backend, frontend, fullstack, DevOps, SaaS, API, endpoint, webhook, dashboard, KPI, OKR, ROI, ROAS, CRM, MVP` + +Forçar tradução desses termos é sinal de IA purista — o oposto de humano. + +> **Regra de ouro**: se o termo é usado no dia a dia do domínio em PT-BR, mantenha. Se é anglicismo artificial sem necessidade, remova. + +## Suíte de testes de regressão + +Conjunto mínimo para validar evoluções futuras. Rodar cada caso em `modo_completo` e conferir preservação semântica antes de julgar estilo. + +| # | Tipo | Antes (IA) | Depois esperado (síntese) | +|---|---|---|---| +| T1 | E-mail corporativo | "Venho por meio deste informar que o relatório será encaminhado oportunamente" | "O relatório será enviado oportunamente." | +| T2 | Parágrafo acadêmico | "Diversos autores discutem a questão da linguagem de forma ampla" | "Diversos autores discutem a linguagem em termos amplos." | +| T3 | Texto jurídico | "É cediço que a responsabilidade civil objetiva se aplica no âmbito das relações de consumo" | "A responsabilidade civil objetiva aplica-se às relações de consumo." | +| T4 | Modelo de abertura de blog | "Neste artigo, exploraremos 5 estratégias essenciais para otimizar seu workflow" | "Neste artigo, vamos explorar cinco estratégias essenciais para otimizar seu workflow." | +| T5 | Ressalva excessiva de IA | "Como modelo de linguagem, não posso afirmar com certeza, mas parece que talvez o sistema esteja funcionando" | "O sistema parece estar funcionando, mas ainda não é possível afirmar com certeza." | +| T6 | Didático genérico | "João tem 3 maçãs e Maria tem 5. Quantas têm ao todo?" | "João tem 3 maçãs e Maria tem 5. Quantas maçãs os dois têm ao todo?" | + +> **Critério de regressão**: se uma evolução piora o resultado de qualquer teste T1-T6, a mudança deve ser reavaliada. +> +> **TRAVA FACTUAL nos casos de teste:** nenhuma saída esperada pode adicionar, retirar ou alterar conteúdo protegido. Resultado mais contido é preferível a uma versão mais vistosa que invente informação. diff --git a/.github/skills/humanizar/references/padroes-composicao.md b/.github/skills/humanizar/references/padroes-composicao.md new file mode 100644 index 0000000..f047005 --- /dev/null +++ b/.github/skills/humanizar/references/padroes-composicao.md @@ -0,0 +1,312 @@ +# Padrões de Composição — Tropos IA em PT-BR + +Padrões estruturais que denunciam texto gerado por IA ao nível da **composição** — como o texto é montado, não o que ele diz. Inclui tropos catalogados pelo [tropes.fyi](https://tropes.fyi/directory) e o conceito de **Ablação Semântica** (The Register, fev 2026). + +> **TRAVA FACTUAL neste arquivo:** alterar a composição, não o conteúdo. Tratar o texto "Antes" como universo factual fechado e preservar proposições, entidades, números, datas, fontes, causalidade, modalidade e registro no "Depois". Não introduzir analogia, exemplo, dado, experiência, opinião ou detalhe ausente. Quando uma lacuna impedir a concretização, manter a generalidade e apontar a lacuna no relatório. + +--- + +## Tropos de Composição + +### Resumos Fractais + +**Problema:** IA anuncia o que vai dizer, diz, e depois resume o que disse — em cada seção, subseção e parágrafo. Texto vira recursão infinita de meta-comentário. + +**Antes (IA):** +> Nesta seção, vamos explorar como a inteligência artificial está transformando o setor financeiro. Veremos três aspectos principais: automação de processos, análise preditiva e atendimento ao cliente. +> +> [... 3 parágrafos ...] +> +> Como vimos nesta seção, a inteligência artificial está transformando o setor financeiro por meio da automação de processos, da análise preditiva e do atendimento ao cliente. Na próxima seção, abordaremos os desafios dessa transformação. + +**Depois (humano):** +> A inteligência artificial transforma o setor financeiro por meio da automação de processos, da análise preditiva e do atendimento ao cliente. A seção seguinte examina os desafios dessa transformação. + +**Evitar em PT-BR:** +- "Nesta seção, veremos..." +- "Como vimos anteriormente..." +- "A seguir, abordaremos..." +- "Conforme mencionado na seção anterior..." +- "Para resumir o que foi discutido..." + +**Sinais adicionais de detecção:** +- Recursividade de meta-nível: quando cada seção tem introdução → desenvolvimento → mini-conclusão, e cada parágrafo dentro da seção também tem essa estrutura +- Palavras-chave de meta-comentário: "como discutido", "retomando", "para recapitular", "em resumo desta seção" +- Presença de conectivos de transição entre subseções que referenciam o próprio texto ("No tópico anterior vimos...") + +**Técnicas avançadas de correção:** +- Reescrever eliminando a recursão: a conclusão do texto é UMA — no final. As subseções não precisam de mini-conclusões +- Converter meta-comentário em afirmação direta: "Como vimos nesta seção, a IA transforma o setor" → "A IA transforma o setor de três formas" +- Se o texto tem 3+ subseções com mini-conclusões, fundir as subseções em um único bloco com fluxo contínuo + +--- + +### Metáfora Morta + +**Problema:** IA encontra uma metáfora no início e repete ad nauseam como se fosse a espinha do texto. "Ecossistema" aparece 30 vezes. "Jornada" aparece em cada parágrafo. A metáfora perde qualquer poder — vira ruído. + +**Antes (IA):** +> O ecossistema de startups brasileiro está amadurecendo. Nesse ecossistema, os players precisam se adaptar. O ecossistema exige novas competências. Para sobreviver neste ecossistema, empreendedores devem construir redes sólidas. O futuro do ecossistema depende de políticas públicas que fomentem a inovação dentro do próprio ecossistema. + +**Depois (humano):** +> O ecossistema de startups brasileiro está amadurecendo. Esse processo exige que seus participantes desenvolvam novas competências e que os empreendedores construam redes sólidas. O futuro do setor também depende de políticas públicas de incentivo à inovação. + +**Evitar em PT-BR:** +- Repetir "ecossistema", "jornada", "cenário", "panorama" ou "paisagem" mais de 2x num texto +- Usar a mesma metáfora-base em mais de 3 parágrafos seguidos +- Forçar coerência metafórica artificial ("nessa jornada... o próximo passo da jornada... ao longo da jornada...") + +**Sinais adicionais de detecção:** +- Frequência de repetição da mesma palavra-metafórica (contagem >2 no texto inteiro, >1 por parágrafo) +- Metáforas que são semanticamente vazias no contexto ("ecossistema" para qualquer coisa que tenha mais de duas partes) +- Metáforas que não suportam raciocínio — o autor usa "jornada" mas não desenvolve nenhuma etapa da jornada + +**Técnicas avançadas de correção:** +- Criar um **mapa metafórico**: listar todas as metáforas usadas → se houver sobreposição semântica (ex: "ecossistema", "cenário", "paisagem" no mesmo texto), escolher UMA e eliminar as demais +- Substituir metáforas genéricas por formulação direta baseada nas proposições existentes. Usar imagem concreta somente se ela já estiver na entrada ou for apresentada, com autorização, como hipótese explícita +- Quando a metáfora não serve a um argumento real, cortar e ir direto ao ponto + +--- + +### Empilhamento de Analogias Históricas + +**Problema:** IA lista 5 empresas ou revoluções históricas em sequência para dar "peso" ao argumento. Parece erudição, mas é preenchimento — nenhuma analogia é desenvolvida o suficiente pra provar algo. + +**Antes (IA):** +> Assim como a revolução industrial transformou a manufatura, como a eletricidade mudou a infraestrutura urbana, como a internet redefiniu a comunicação, como o iPhone revolucionou a computação móvel, e como o Netflix disruputou a mídia — a inteligência artificial generativa está prestes a transformar fundamentalmente o modo como trabalhamos. + +**Depois (humano):** +> A revolução industrial transformou a manufatura; a eletricidade, a infraestrutura urbana; a internet, a comunicação; o iPhone, a computação móvel; e a Netflix, a mídia. Da mesma forma, a inteligência artificial generativa está prestes a transformar fundamentalmente o modo como trabalhamos. + +**Evitar em PT-BR:** +- "Assim como [empresa/revolução 1], como [empresa/revolução 2], como [empresa/revolução 3]..." +- "Da mesma forma que a revolução industrial..." +- "Se olharmos para a história — do rádio à TV, da TV à internet, da internet ao mobile —" +- Listar mais de 2 analogias históricas sem desenvolver nenhuma + +**Sinais adicionais de detecção:** +- Sequências de 3+ analogias com a mesma estrutura sintática (coordenação com "como" ou "assim como") +- Analogias que terminam em conclusão genérica idêntica ("...está transformando fundamentalmente o modo como trabalhamos") +- Analogias sem especificidade temporal (não diz quando a revolução industrial aconteceu, quanto durou, qual setor) + +**Técnicas avançadas de correção:** +- Condensar analogias repetitivas sem apagar proposições distintas. Descartar apenas a analogia que for decorativa e não acrescentar conteúdo ao argumento +- Desenvolver especificidade temporal ou causal somente com dados já presentes na entrada +- Se a analogia não tiver sustentação, qualificá-la como comparação do autor ou apontar a lacuna no relatório; não ancorá-la com dados externos inventados + +--- + +### Diluição de Ponto Único + +**Problema:** O texto tem UM argumento. Mas a IA o reformula 10 vezes com conectivos diferentes, esticando pra 4000 palavras o que caberia em 400. Cada parágrafo diz a mesma coisa com roupa diferente. + +**Antes (IA):** +> A transformação digital é essencial para a competitividade empresarial. De fato, empresas que não adotarem tecnologias digitais correm o risco de ficar para trás. Nesse sentido, a digitalização dos processos se torna uma prioridade estratégica. Além disso, a modernização tecnológica permite que organizações se adaptem com mais agilidade. Por outro lado, empresas que resistem à mudança digital enfrentam desafios crescentes de eficiência. Diante disso, fica claro que a transformação digital não é mais uma opção — é uma necessidade. + +**Depois (humano):** +> A transformação digital é essencial para a competitividade. Empresas que não adotam tecnologias digitais podem perder competitividade e enfrentar problemas de eficiência. Por isso, digitalizar processos e modernizar a tecnologia são necessidades estratégicas para aumentar a agilidade e a capacidade de adaptação. + +**Evitar em PT-BR:** +- Parágrafo que começa com "De fato," seguido da repetição da mesma ideia +- "Nesse sentido," introduzindo a mesma ideia de novo +- "Em outras palavras," (literalmente admitindo que vai repetir) +- "Isso significa que..." (reformulação disfarçada) +- Texto com mais de 3 parágrafos onde cada um pode ser resumido pela mesma frase + +**Sinais adicionais de detecção:** +- Parágrafos com a mesma informação expressa em palavras diferentes (sinonímia redundante) +- Conectivos de reformulação: "em outras palavras", "ou seja", "isso significa que", "dito de outro modo" +- Texto com TTR (Type-Token Ratio) artificialmente baixo — muita repetição lexical com sinônimos + +**Técnicas avançadas de correção:** +- **Algoritmo de compressão**: identificar o núcleo proposicional de cada parágrafo → se dois parágrafos compartilham o mesmo núcleo, fundir em um +- Eliminar conectivos de reformulação — se o leitor precisa que você repita de outro jeito, o primeiro jeito provavelmente já era ruim +- Aplicar "regra de 3": se o argumento precisa de 3 reformulações para ser entendido, ele provavelmente é fraco + +--- + +### Conclusão Sinalizada + +**Problema:** IA não sabe terminar sem anunciar que vai terminar. Usa marcadores explícitos que telegrafam "aqui acaba" — quebrando qualquer possibilidade de final com impacto. + +**Antes (IA):** +> Em conclusão, a inteligência artificial generativa representa uma oportunidade transformadora para o mercado brasileiro. Em suma, as empresas que souberem aproveitar esse potencial estarão melhor posicionadas para o futuro. Para finalizar, é importante ressaltar que o equilíbrio entre inovação e responsabilidade será determinante para o sucesso dessa jornada. + +**Depois (humano):** +> A inteligência artificial generativa é uma oportunidade transformadora para o mercado brasileiro. As empresas que aproveitarem esse potencial estarão mais bem posicionadas, e o equilíbrio entre inovação e responsabilidade será determinante para o sucesso. + +**Evitar em PT-BR:** +- "Em conclusão," +- "Em suma," +- "Para finalizar," +- "Concluindo," +- "Portanto, podemos afirmar que..." +- "Diante do exposto," +- "À luz do que foi apresentado," + +**Sinais adicionais de detecção:** +- Marcadores explícitos de encerramento em sequência ("Em conclusão... Para finalizar...") +- Conclusões que apenas repetem o que já foi dito sem acrescentar ideia nova +- Parágrafo final com tom otimista genérico ("o futuro é promissor") + +**Técnicas avançadas de correção:** +- Escolher o fechamento apenas entre materiais já presentes no texto: + 1. **Virada reflexiva** — converter uma dúvida já existente em pergunta + 2. **Detalhe concreto** — reutilizar dado já fornecido na entrada + 3. **Contradição** — preservar limite ou tensão já expresso pelo autor + 4. **Silêncio** — retirar o marcador e simplesmente encerrar +- Se a conclusão começa com "Em conclusão", cortar as 3 primeiras palavras e ver se o resto sobrevive + +--- + +### "Apesar dos Desafios..." + +**Problema:** Fórmula rígida de acknowledge→dismiss. IA reconhece um problema só pra descartá-lo imediatamente com otimismo vazio. Não há tensão real — o "desafio" nunca ameaça a tese. + +**Antes (IA):** +> Apesar dos desafios regulatórios, a adoção de IA no setor de saúde segue em ritmo acelerado. Embora existam preocupações legítimas sobre privacidade de dados, as oportunidades superam amplamente os riscos. Mesmo com as limitações atuais de infraestrutura, o potencial transformador da tecnologia é inegável. + +**Depois (humano):** +> A adoção de IA no setor de saúde avança em ritmo acelerado. Há preocupações com a privacidade de dados e limitações de infraestrutura; ainda assim, as oportunidades superam amplamente os riscos, e o potencial transformador da tecnologia é inegável. + +**Evitar em PT-BR:** +- "Apesar dos desafios, [coisa positiva]" +- "Embora existam preocupações legítimas, as oportunidades superam..." +- "Mesmo com as limitações, o potencial é..." +- "Não obstante os obstáculos, o caminho é promissor" +- "Reconhecendo os riscos, mas focando nas possibilidades..." + +**Sinais adicionais de detecção:** +- Estrutura "reconhece → descarta" em 1-2 frases +- Adjetivos que neutralizam o problema: "desafios legítimos", "limitações atuais", "obstáculos naturais" +- O "desafio" nunca tem consequência concreta — é abstrato + +**Técnicas avançadas de correção:** +- Explicitar custo, risco ou consequência somente quando esses elementos já estiverem na entrada. Se não estiverem, manter o desafio nos termos originais e apontar a lacuna no relatório +- Se o original já expressar uma tensão, **não descartá-la**: preservar o dado, a dúvida sobre a fonte e a decisão em aberto sem criar nenhum desses elementos +- Aplicar "teste de consequência" — se o desafio não tiver custo, risco ou contrapartida explícita na entrada, não inventar um; manter a formulação geral ou apontar a lacuna no relatório + +--- + +### Listicle Disfarçado de Prosa + +**Problema:** O texto é uma lista numerada fingindo ser parágrafo corrido. Cada item começa com "O primeiro aspecto...", "O segundo ponto...", "O terceiro elemento...". Não há fluxo — é enumeração com pontuação de prosa. + +**Antes (IA):** +> O primeiro aspecto a considerar é a escalabilidade da solução. O segundo ponto relevante diz respeito à integração com sistemas legados. O terceiro elemento fundamental é a experiência do usuário final. O quarto fator a ser levado em conta é o custo total de propriedade. Por fim, o quinto aspecto envolve a governança de dados. + +**Depois (humano):** +> A solução deve ser avaliada por cinco aspectos: escalabilidade, integração com sistemas legados, experiência do usuário, custo total de propriedade e governança de dados. + +**Evitar em PT-BR:** +- "O primeiro aspecto..." +- "O segundo ponto..." +- "O terceiro elemento..." +- "O quarto fator..." +- "Por fim, o quinto..." +- Qualquer sequência ordinal disfarçada de argumentação + +**Sinais adicionais de detecção:** +- Sequência ordinal implícita: "Primeiro... Depois... Em seguida... Por fim..." +- Frases com estrutura sintática idêntica (mesmo comprimento, mesma ordem de constituintes) +- Texto que pode ser reformatado como lista numerada sem perder sentido + +**Técnicas avançadas de correção:** +- Se o conteúdo merece lista → **formatar como lista real** (mais honesto) +- Se o conteúdo não merece lista → **reorganizar como raciocínio causal**: causa → efeito → consequência +- Quebrar a simetria sintática sem mudar o registro nem acrescentar fragmentos que não estejam no texto +- Reordenar os itens somente quando a nova ordem preservar relações lógicas e ênfase do original + +--- + +## Ablação Semântica — Restauração de Entropia + +Conceito do The Register (fev 2026): quando IA "melhora" um texto, ela faz **ablação semântica** — remove informação de alta entropia (os trechos únicos, específicos, surpreendentes) e substitui por sequências genéricas de alta probabilidade. O resultado é um "JPEG de pensamento": parece coerente, mas perdeu a densidade original. + +A humanização não é só remover padrões ruins — é **preservar o que ainda existe**. Restaurar conteúdo apagado exige uma versão-fonte fornecida pelo usuário; sem ela, não reconstruir por inferência. + +--- + +### Limpeza Metafórica + +**Problema:** IA identifica metáforas originais, imagens viscerais e comparações inesperadas como "ruído" e substitui por clichês seguros. Metáforas vivas viram metáforas mortas. A especificidade sensorial desaparece. + +**Antes (IA):** +> O mercado de trabalho está passando por uma profunda transformação. As empresas estão navegando em águas turbulentas e buscando se adaptar ao novo cenário. É preciso abraçar a mudança e trilhar novos caminhos para alcançar o sucesso. + +**Depois (humano):** +> O mercado de trabalho passa por uma transformação profunda. As empresas enfrentam instabilidade, buscam adaptação e precisam mudar para alcançar o sucesso. + +**Restaurar:** +- Metáforas sensoriais (visuais, táteis, sonoras) que já estejam na entrada ou em uma versão-fonte fornecida +- Comparações concretas que já façam parte do material autorizado +- Imagens e reações presentes no original, sem criar desconforto ou surpresa por conta própria +- Especificidade: em vez de "cenário", descrever apenas o que a entrada já informa; se não houver detalhe, usar formulação direta e apontar a lacuna no relatório + +**Sinais adicionais de detecção:** +- Metáforas substituídas por **clichês corporativos** ("ponto de inflexão", "navegar as complexidades") +- Perda de especificidade sensorial: texto que antes tinha cor/luz/cheiro e agora tem só conceito +- Metáforas que poderiam ser aplicadas a QUALQUER assunto ("transformação profunda", "novo capítulo") + +**Técnicas avançadas de correção:** +- **Escala de concretude**: classificar cada imagem em 1-5 (1 = abstrata, 5 = sensorial). Se a média do texto < 2, simplificar a abstração com o material existente e apontar a falta de concretude no relatório +- Substituir clichês por formulação direta. Usar referência cultural brasileira apenas se ela já estiver na entrada ou for autorizada como hipótese explícita +- "Teste do bar" — se a metáfora não funcionaria numa conversa de bar, ela é genérica demais + +--- + +### Achatamento Lexical + +**Problema:** Jargão preciso e terminologia de domínio são substituídos por termos genéricos "acessíveis". Token de 1-em-10.000 vira token de 1-em-100. O texto perde densidade informacional — diz menos com mais palavras. + +**Antes (IA):** +> A empresa implementou uma solução de análise de dados que permite monitorar indicadores de desempenho e tomar decisões mais informadas. A ferramenta oferece visualizações intuitivas que ajudam a equipe a entender melhor os resultados. + +**Depois (humano):** +> A empresa implementou uma solução de análise de dados para monitorar indicadores de desempenho, apoiar decisões mais informadas e visualizar resultados. As visualizações ajudam a equipe a compreender melhor esses resultados. + +**Restaurar:** +- Nomes próprios de ferramentas, frameworks e metodologias que já estejam na entrada ou em uma versão-fonte fornecida +- Métricas e siglas de domínio já presentes no material autorizado +- Verbos técnicos precisos que já estejam na entrada ou em uma versão-fonte fornecida; sem essa base, preservar a ação genérica +- Dados concretos já fornecidos: preservar números, porcentagens e intervalos temporais; se estiverem ausentes, não criá-los + +**Sinais adicionais de detecção:** +- Substituição de jargão técnico por sinônimos genéricos ("ORM" → "ferramenta de mapeamento objeto-relacional" → "solução de banco de dados") +- Perda de siglas do domínio (substitui "SaaS" por "software como serviço", depois por "plataforma digital") +- Texto que soa como tradução de material introdutório para público leigo, mesmo quando o público é técnico + +**Técnicas avançadas de correção:** +- **Mapeamento de domínio**: identificar o campo (dev, marketing, jurídico) → preservar o vocabulário técnico existente, sem inferir ferramenta, métrica ou sigla ausente +- Se o texto generalizou demais, reintroduzir sigla ou termo somente a partir de uma versão-fonte fornecida; caso contrário, apontar a perda de precisão no relatório +- "Teste de precisão" — se um especialista do domínio lê e diz "isso tá vago", o texto perdeu densidade + +--- + +### Colapso Estrutural + +**Problema:** Raciocínio complexo, não linear e cheio de voltas é forçado no molde previsível de baixa perplexidade: introdução → 3 pontos → conclusão. Digressões são eliminadas. Contradições são "resolvidas". Nuance vira item de lista. + +**Antes (IA):** +> A adoção de metodologias ágeis no Brasil apresenta três benefícios principais. Em primeiro lugar, aumenta a produtividade das equipes. Em segundo lugar, melhora a qualidade das entregas. Em terceiro lugar, promove uma cultura de melhoria contínua. Dessa forma, as empresas que adotam práticas ágeis tendem a obter melhores resultados. + +**Depois (humano):** +> No Brasil, a adoção de metodologias ágeis aumenta a produtividade das equipes, melhora a qualidade das entregas e promove uma cultura de melhoria contínua. Por isso, as empresas que adotam essas práticas tendem a obter resultados melhores. + +**Restaurar:** +- Digressões produtivas que já estejam na entrada ou em uma versão-fonte fornecida +- Contradições internas efetivamente expressas pelo autor +- Qualificações, exceções e dúvidas presentes no material autorizado +- Perguntas sem resposta somente quando preservarem uma dúvida já existente +- Estrutura que surpreende: começar por contra-argumento ou detalhe já existente, sem alterar a ênfase do autor + +**Sinais adicionais de detecção:** +- Estrutura perfeitamente simétrica: parágrafos com 3-4 frases cada, todos com a mesma ordem (tópico → desenvolvimento → conclusão) +- Ausência de digressões, parênteses ou tangentes +- Texto que parece ter sido gerado por outline rígido sem desvios + +**Técnicas avançadas de correção:** +- Reposicionar uma tangente já existente sem criar comentário ou argumento novo +- Usar parênteses apenas para material lateral já presente no texto +- Quebrar simetria por divisão ou combinação de frases existentes, preservando o registro +- Variar a estrutura somente quando isso não alterar ênfase, modalidade ou voz do original diff --git a/.github/skills/humanizar/references/padroes-conteudo.md b/.github/skills/humanizar/references/padroes-conteudo.md new file mode 100644 index 0000000..fe35a8e --- /dev/null +++ b/.github/skills/humanizar/references/padroes-conteudo.md @@ -0,0 +1,180 @@ +# Padrões de Conteúdo + +Padrões onde a IA infla importância, fabrica autoridade ou encerra textos com fórmulas previsíveis. São fáceis de detectar porque soam como comunicado de assessoria de imprensa — ninguém fala assim. + +> **TRAVA FACTUAL neste arquivo:** tratar o texto "Antes" como universo factual fechado. No "Depois", preservar proposições, entidades, números, datas, fontes, causalidade, modalidade e registro. Não preencher lacunas com exemplos, dados, fontes ou experiência pessoal. Quando faltar sustentação, preservar a alegação e apontar a lacuna no relatório; só cortar, qualificar ou corrigir com autorização do usuário. + +--- + +### 1. Ênfase indevida em significância, legado e tendências + +**Palavras/expressões gatilho:** representa um marco, é um testemunho de, papel fundamental/crucial/vital, ressalta a importância, reflete uma tendência mais ampla, simbolizando o/a, contribuindo para o/a, preparando o terreno para, moldando o futuro de, cenário em constante evolução, ponto de inflexão, marca indelével, profundamente enraizado, redefine o paradigma + +**Problema:** A IA transforma qualquer fato mundano numa revolução. Um CRUD vira "um marco na transformação digital". Um pivô de startup vira "um ponto de inflexão no ecossistema de inovação". Nenhum ser humano escreve assim sobre coisas normais. + +**Antes (IA):** +> O Nubank representa um marco fundamental na transformação do cenário financeiro brasileiro, moldando ativamente o futuro das fintechs na América Latina e preparando o terreno para uma nova era de inclusão bancária digital. + +**Depois (humano):** +> O Nubank tem papel central na transformação do setor financeiro brasileiro. A empresa influencia o desenvolvimento das fintechs na América Latina e contribui para a inclusão bancária digital. + +**Evitar em PT-BR:** +- "representa um marco na evolução de..." +- "moldando o futuro do ecossistema de..." +- "em um cenário em constante transformação" + +**Sinais adicionais de detecção:** +- Superlativos absolutos sem quantificação ("maior", "melhor", "sem precedentes", "inédito") +- Verbos de transformação grandiosa ("redefinir", "moldar", "preparar o terreno") +- Texto que descreve qualquer coisa como "ponto de inflexão" sem dizer o que muda depois + +**Técnicas avançadas de correção:** +- Converter superlativos em **dados concretos somente quando o dado já estiver na entrada**. Sem dado, preservar a força da avaliação e registrar a falta de sustentação no relatório +- Substituir verbos grandiosos por **verbos de ação já descritos no original**. Se o original não informar a ação, usar formulação direta que preserve a alegação sem criar evento novo +- "Teste do jornalista" — se um repórter leria a frase e perguntaria "como assim?", o termo é vazio + +--- + +### 2. Ênfase forçada em notabilidade e cobertura de mídia + +**Palavras/expressões gatilho:** amplamente reconhecido, coberto pelos principais veículos, destaque na mídia especializada, presença ativa nas redes sociais, segundo especialistas do setor, referência no mercado + +**Problema:** A IA lista veículos e prêmios como prova de importância, sem dizer o que foi dito ou por que importa. Vira um Lattes turbinado — impressiona no vácuo mas não informa nada. + +**Antes (IA):** +> A empresa foi destaque na Exame, Valor Econômico, TechCrunch e Bloomberg. Amplamente reconhecida como referência no mercado de SaaS B2B brasileiro, mantém presença ativa nas redes sociais com mais de 200 mil seguidores. + +**Depois (humano):** +> A empresa apareceu na Exame, no Valor Econômico, no TechCrunch e na Bloomberg. É amplamente reconhecida no mercado brasileiro de SaaS B2B e soma mais de 200 mil seguidores nas redes sociais. + +**Evitar em PT-BR:** +- "amplamente reconhecido(a) como referência em..." +- "destaque nos principais veículos do setor" +- "mantém presença ativa nas redes com X seguidores" + +**Sinais adicionais de detecção:** +- Listagem de veículos sem citação de matéria específica (data, título, link) +- "Presença ativa nas redes" sem métrica (seguidores, engajamento) +- Menção a prêmios ou rankings sem fonte verificável + +**Técnicas avançadas de correção:** +- Se a entrada já trouxer fonte, data ou link → preservar e citar esses mesmos elementos, sem completar o que estiver ausente +- Se não houver fonte → preservar a alegação e sua atribuição, apontando a lacuna no relatório; só cortar ou qualificar com autorização +- "Teste de verificabilidade" — se o leitor não pode checar em 30 segundos, a informação é puffery + +--- + +### 3. Análises superficiais com gerúndio/particípio + +**Palavras/expressões gatilho:** ressaltando a importância de, demonstrando o compromisso com, refletindo a tendência de, contribuindo para o fortalecimento de, evidenciando o potencial de, impulsionando a inovação, fomentando o crescimento, consolidando sua posição como + +**Problema:** A IA gruda frases com gerúndio no final das sentenças pra parecer que está analisando algo, mas não está. É firula sintática — enche linguiça sem adicionar informação. Tipo aquele estagiário que escreve 3 páginas pra dizer "funcionou". + +**Antes (IA):** +> A RD Station lançou integração nativa com WhatsApp Business, demonstrando seu compromisso com a inovação no marketing digital brasileiro e consolidando sua posição como líder no segmento, impulsionando a transformação digital das PMEs. + +**Depois (humano):** +> A RD Station lançou integração nativa com WhatsApp Business. O lançamento demonstra seu compromisso com a inovação no marketing digital brasileiro, consolida sua posição de liderança no segmento e impulsiona a transformação digital das PMEs. + +**Evitar em PT-BR:** +- "demonstrando o compromisso da empresa com..." +- "consolidando sua posição como líder em..." +- "contribuindo para o fortalecimento do ecossistema" + +**Sinais adicionais de detecção:** +- Frases que terminam com gerúndio como se fosse conclusão de análise +- Construções "verbo composto + gerúndio" em sequência ("vem demonstrando", "vai estar consolidando") +- Gerúndio usado como adjetivo ("inovando", "transformando", "impulsionando") + +**Técnicas avançadas de correção:** +- Converter a oração gerundial em **oração finita** com sujeito claro, preservando a alegação: "demonstrando compromisso" → "o lançamento demonstra o compromisso" +- Se o gerúndio é puramente decorativo, **cortar**: "impulsionando a transformação" → (nada — a frase principal já diz) +- "Teste do podcast" — ler a frase em voz alta; se soa como narração de vídeo institucional, o gerúndio é excessivo + +--- + +### 4. Linguagem promocional e de propaganda + +**Palavras/expressões gatilho:** solução inovadora, experiência única, ecossistema robusto, revolucionário, disruptivo, estado da arte, de ponta, excelência, sinergia, empoderar, potencializar, alavancar, impulsionar, viabilizar, transformador, game-changer, seamless (usado em PT) + +**Problema:** A IA escreve como copywriter de página de produto. Tudo é "inovador", "revolucionário" e "de ponta". Nenhuma pessoa normal descreve o próprio trabalho assim — soa como pitch deck desesperado pra anjo investidor. + +**Antes (IA):** +> A plataforma oferece uma solução inovadora e disruptiva que empodera equipes de produto a potencializarem seus resultados, entregando uma experiência seamless e de estado da arte para alavancar a transformação digital das organizações. + +**Depois (humano):** +> A plataforma oferece uma solução inovadora e disruptiva. Ela dá às equipes de produto meios para melhorar seus resultados, oferece uma experiência integrada e de alto nível e apoia a transformação digital das organizações. + +**Evitar em PT-BR:** +- "solução inovadora e disruptiva que empodera..." +- "experiência única de ponta/estado da arte" +- "potencializar/alavancar a transformação digital" + +**Sinais adicionais de detecção:** +- Combinação de 2+ buzzwords na mesma frase ("solução inovadora e disruptiva de ponta") +- Adjetivos que são autocontraditórios ("seamless mas robusto", "simples mas poderoso") +- Texto que poderia ser usado como copy de qualquer produto sem mudar nada + +**Técnicas avançadas de correção:** +- Reduzir adjetivos puramente decorativos sem apagar uma alegação substantiva. Quando a alegação não tiver evidência na entrada, qualificá-la como descrição da empresa ou do texto +- Substituir adjetivos por **dados ou comparações somente quando esses elementos já estiverem na entrada**. Sem evidência, preservar a avaliação e apontar a lacuna no relatório +- "Teste do pitch deck" — se a frase apareceria em qualquer slide de qualquer startup, ela é genérica demais + +--- + +### 5. Atribuições vagas e weasel words + +**Palavras/expressões gatilho:** segundo especialistas, de acordo com analistas do setor, estudos apontam que, o mercado reconhece, é amplamente considerado, pesquisas indicam, fontes do setor afirmam, observadores notam que + +**Problema:** A IA atribui afirmações a autoridades genéricas que não existem. "Especialistas dizem" — quais? "Estudos apontam" — qual estudo, de que ano, com que metodologia? É a versão corporativa de "meu primo falou". + +**Antes (IA):** +> Segundo especialistas do setor, o modelo de negócios da empresa representa uma evolução significativa. Analistas de mercado reconhecem que a abordagem tem potencial para redefinir o segmento de healthtechs no Brasil. + +**Depois (humano):** +> Especialistas do setor não identificados consideram o modelo de negócios uma evolução significativa. Analistas também não identificados avaliam que a abordagem pode redefinir o segmento de healthtechs no Brasil. + +**Evitar em PT-BR:** +- "segundo especialistas do setor..." +- "analistas de mercado reconhecem que..." +- "estudos/pesquisas apontam/indicam que..." + +**Sinais adicionais de detecção:** +- Quantificadores vagos: "muitos especialistas", "diversos estudos", "alguns analistas" +- Referências sem data: "pesquisas recentes mostram..." +- Ausência de fonte quando a afirmação é controversa ou específica + +**Técnicas avançadas de correção:** +- Se a fonte já estiver na entrada → preservá-la com os mesmos autor, ano, título e link +- Se a fonte não estiver na entrada → manter a atribuição como não identificada e registrar a lacuna no relatório; só cortar ou qualificar com autorização +- Não converter atribuição vaga em opinião ou experiência pessoal do revisor +- "Teste da fonte" — se você não consegue encontrar a fonte em 2 minutos de busca, a atribuição é weasel + +--- + +### 6. Conclusões formulaicas sobre desafios e perspectivas futuras + +**Palavras/expressões gatilho:** apesar dos desafios, não obstante as dificuldades, o futuro é promissor, perspectivas animadoras, em um contexto de constante evolução, seguir crescendo, continuar inovando, trilhar um caminho de sucesso, superar obstáculos, rumo a um futuro, desafios e oportunidades + +**Problema:** A IA encerra textos com uma seção "Desafios e Perspectivas" que não diz nada concreto. Primeiro lista problemas genéricos, depois diz que "apesar disso, o futuro é promissor". É o equivalente textual de shrug seguido de thumbs up. + +**Antes (IA):** +> Apesar dos desafios inerentes ao mercado brasileiro — como a complexidade tributária e a concorrência acirrada — a startup segue trilhando um caminho de crescimento sustentável. Com perspectivas animadoras e um time comprometido com a inovação, a empresa está bem posicionada para continuar liderando a transformação do setor. + +**Depois (humano):** +> A startup enfrenta a complexidade tributária e a concorrência acirrada, mas mantém uma trajetória de crescimento sustentável. Suas perspectivas são animadoras, o time é comprometido com a inovação e a empresa está bem posicionada para continuar liderando a transformação do setor. + +**Evitar em PT-BR:** +- "apesar dos desafios, o futuro é promissor" +- "a empresa segue bem posicionada para continuar..." +- "trilhando um caminho de crescimento sustentável" + +**Sinais adicionais de detecção:** +- Fórmula "Apesar de X, o futuro é Y" onde X é genérico e Y é otimista +- Uso de "perspectivas animadoras" sem dizer o que anima +- A conclusão poderia ser aplicada a QUALQUER empresa do setor + +**Técnicas avançadas de correção:** +- Usar previsão, prazo ou plano somente quando esses elementos já estiverem na entrada. Se faltarem, nomear apenas os desafios existentes e apontar a ausência de evidência no relatório +- Se o original expressar incerteza, preservá-la sem criar cenário, consequência ou plano alternativo +- "Teste do horóscopo" — se a conclusão poderia aparecer no horóscopo de qualquer signo, ela é genérica demais diff --git a/.github/skills/humanizar/references/padroes-estilo.md b/.github/skills/humanizar/references/padroes-estilo.md new file mode 100644 index 0000000..c7e0fd1 --- /dev/null +++ b/.github/skills/humanizar/references/padroes-estilo.md @@ -0,0 +1,345 @@ +# Padrões de estilo e formatação + +Padrões que denunciam texto gerado por IA pela forma visual e estrutural, não pelo conteúdo. Ferramentas de detecção usam esses marcadores como sinais de alta confiança. + +> **Regra transversal:** correção de estilo altera somente apresentação, pontuação e organização. Preserve fatos, números, fontes, causalidade, prazo, estado temporal, modalidade e perfil de voz. Nunca compense a retirada de um recurso visual com dado, opinião, humor ou certeza inexistentes. + +--- + +### Travessão (em-dash) excessivo + +**Problema:** IA usa 15-25 travessões por texto médio. Humano brasileiro usa 2-3, e geralmente prefere vírgula, ponto ou parênteses. + +**Antes (IA):** +> O projeto — que começou em 2022 — trouxe resultados impressionantes — especialmente na área de dados — e agora está sendo expandido — mesmo com orçamento limitado — para outras regionais. + +**Depois (humano):** +> O projeto começou em 2022 e trouxe resultados impressionantes, especialmente na área de dados. Mesmo com orçamento limitado, agora está sendo expandido para outras regionais. + +**Evitar em PT-BR:** +- Mais de 2 travessões por parágrafo +- Travessão onde vírgula resolve +- Encadeamento de apartes com travessão (— X — Y — Z) + +**Sinais adicionais de detecção:** +- Travessões usados para **tudo**: apartes, conclusões, reformulações, exemplos +- Texto onde >10% dos sinais de pontuação são travessões +- Travessão duplo usado como parênteses em toda ocorrência + +**Técnicas avançadas de correção:** +- **Limite de 2 travessões por parágrafo** — se passar, converter os extras em vírgulas, pontos ou parênteses +- Diferenciar uso: travessão para contraste forte, parênteses para comentário lateral, vírgula para aparte leve +- Ao trocar a pontuação, preservar a relação entre oração principal, ressalva, causa e contraste +- "Teste do editor" — se um editor humano teria cortado o travessão, cortar + +--- + +### Negrito excessivo + +**Problema:** IA aplica negrito em toda palavra-chave como se o texto fosse uma apresentação. Texto corrido com negrito em cada substantivo importante parece catálogo de produto, não escrita humana. + +**Antes (IA):** +> A **plataforma** oferece **integração nativa** com os principais **CRMs do mercado**, garantindo **escalabilidade** e **segurança** para equipes de **vendas** e **marketing**. + +**Depois (humano):** +> A plataforma oferece integração nativa com os principais CRMs do mercado, garantindo escalabilidade e segurança para equipes de vendas e marketing. + +**Regra contextual canônica:** +- Em prosa corrida, e-mail, artigo e texto autoral, usar negrito apenas quando houver contraste editorial deliberado ou quando a fonte já trouxer ênfase relevante +- Em documentação, material didático, interfaces e listas de consulta, o negrito pode marcar rótulos e hierarquia quando isso melhora a navegação +- Não aplicar cotas simultâneas por parágrafo e por seção. O excesso é funcional: ocorre quando a marcação se repete sem hierarquia e faz vários trechos competirem pela mesma atenção + +**Sinais adicionais de detecção:** +- Negrito em substantivos comuns (plataforma, equipe, resultado) sem razão editorial +- Negrito usado como substituto de hierarquia de informação (quando a estrutura deveria fazer o trabalho) +- Negrito espalhado por tantas palavras que deixa de indicar prioridade + +**Técnicas avançadas de correção:** +- Usar negrito apenas para **contraste intencional**: "O problema não é a ferramenta — é o **processo**" +- Se o negrito está tentando compensar falta de clareza, **reestruturar a frase** em vez de negritar +- Preservar negritos funcionais do gênero; remover os decorativos sem alterar as palavras ou a ênfase semântica do trecho +- "Teste de hierarquia" — ao olhar a página, fica claro por que cada destaque existe? + +--- + +### Listas com rótulo em negrito + +**Problema:** IA produz listas onde todo item começa com um termo em negrito seguido de dois-pontos, mesmo quando não há hierarquia real. Em documentação e material de consulta, esse formato pode ser legítimo; o sinal é seu uso automático em prosa ou em categorias artificiais. + +**Antes (IA):** +> - **Agilidade:** O time reduziu o ciclo de entrega em 40%. +> - **Qualidade:** Os bugs em produção caíram pela metade. +> - **Engajamento:** A satisfação do time subiu 12 pontos no eNPS. + +**Depois (humano):** +> - Agilidade: a equipe reduziu o ciclo de entrega em 40%. +> - Qualidade: os bugs em produção caíram pela metade. +> - Engajamento: a satisfação da equipe subiu 12 pontos no eNPS. + +**Evitar em PT-BR:** +- Estrutura "**Palavra:** frase explicativa" repetida em série sem hierarquia real +- Listas de 3+ itens onde o gênero não pede consulta rápida e a prosa preserva melhor as relações +- Forçar categorias artificiais para criar itens + +**Sinais adicionais de detecção:** +- Estrutura repetitiva: "**Termo:** frase explicativa" em 3+ itens consecutivos +- Itens que são minitópicos de documentação, não pontos argumentativos +- A lista poderia ser uma tabela 2xN + +**Técnicas avançadas de correção:** +- Se a informação é factual e tabular → converter em **tabela**, preservando rótulos, valores e correspondências +- Se a informação é argumentativa → converter em **prosa corrida** sem criar relação causal ou conclusão nova +- Se a lista é inevitável → usar marcadores simples sem cabeçalho em negrito +- Manter cabeçalhos em negrito quando forem rótulos funcionais do gênero, conforme a regra contextual canônica +- "Teste do slide" — se a lista parece um slide de apresentação, formatar como slide (ou reescrever como parágrafo) + +--- + +### Title Case em títulos + +**Problema:** Em inglês, Title Case é comum em headings. Em português brasileiro, não. A norma é capitalizar só a primeira palavra e nomes próprios. Title Case em PT-BR é sinal claro de texto gerado por modelo treinado em inglês. + +**Antes (IA):** +> ## Estratégias De Marketing Digital Para Pequenas Empresas + +**Depois (humano):** +> ## Estratégias de marketing digital para pequenas empresas + +**Evitar em PT-BR:** +- Capitalizar preposições (De, Para, Com, Em) +- Capitalizar substantivos comuns em títulos (Empresas, Estratégias, Resultados) +- Qualquer padrão que lembre capa de livro americano + +**Sinais adicionais de detecção:** +- Títulos onde preposições e artigos estão capitalizados +- Títulos que parecem capa de livro americano ("Estratégias De Marketing Para Pequenas Empresas") +- Mistura de Title Case com sentenças em minúsculo no mesmo documento + +**Técnicas avançadas de correção:** +- Aplicar **regra ABNT**: só a primeira letra do título e nomes próprios em maiúsculas +- Exceção: se o documento é explicitamente para público americano, Title Case é aceitável +- "Teste do jornal brasileiro" — abrir matéria da Folha ou Piauí; os títulos usam minúsculas em preposições + +--- + +### Emojis decorativos + +**Problema:** IA enfia emojis em títulos e itens como enfeite. Brasileiro usa emoji em mensagem informal, não em título de seção ou tópico técnico. A presença de 🚀💡✅🎯 em texto profissional é assinatura de máquina. + +**Antes (IA):** +> 🚀 **Lançamento:** Produto entra no ar em setembro +> 💡 **Observação:** Usuários preferem onboarding curto +> ✅ **Próximos passos:** Agendar reunião com stakeholders +> 🎯 **Meta:** Crescer 30% no trimestre + +**Depois (humano):** +> Lançamento: o produto entra no ar em setembro. +> Observação: os usuários preferem um onboarding curto. +> Próximos passos: agendar uma reunião com os stakeholders. +> Meta: crescer 30% no trimestre. + +**Evitar em PT-BR:** +- Emoji antes de título ou item quando for puramente decorativo +- 🚀💡✅🎯📊 como decoração de estrutura +- Emoji incompatível com o perfil de voz ou sem função comunicativa + +**Sinais adicionais de detecção:** +- Emojis em headings de seção, títulos de artigo, ou como marcadores em texto profissional +- Sequência de emojis sem função comunicativa (decoração pura) +- Emoji em texto que não é mensagem pessoal ou post de rede social + +**Técnicas avançadas de correção:** +- Em texto formal ou técnico, remover emoji puramente decorativo; preservar ícone que comunique estado, alerta ou categoria funcional +- Em WhatsApp e rede social, manter emojis compatíveis com o perfil de voz e retirar apenas repetição mecânica +- Ao remover o emoji, não acrescentar humor, ironia, surpresa nem nova ênfase para compensar +- "Teste do canal" — o emoji cumpre uma função aceita nesse gênero ou serve apenas de enfeite? + +--- + +### Aspas curvas vs retas + +**Problema:** ChatGPT e Claude usam aspas curvas tipográficas (" ") por padrão. A maioria dos brasileiros digita aspas retas (" ") porque é o que o teclado produz. Aspas curvas em texto informal ou técnico são bandeira de IA. + +**Antes (IA):** +> O gestor disse que o projeto está "no caminho certo" e que a equipe está "engajada". + +**Depois (humano):** +> O gestor disse que o projeto está "no caminho certo" e que a equipe está "engajada". + +**Evitar em PT-BR:** +- " " (curvas) em qualquer contexto que não seja diagramação profissional +- ' ' (apóstrofos curvos) no lugar de ' ' +- Misturar aspas curvas e retas no mesmo texto + +**Sinais adicionais de detecção:** +- Mistura de aspas curvas e retas no mesmo documento +- Aspas curvas em texto que claramente veio de teclado brasileiro (onde o padrão é reta) +- Aspas curvas em mensagens de WhatsApp ou Slack (impossível no mobile) + +**Técnicas avançadas de correção:** +- **Regra de consistência**: escolher um padrão e manter — se o texto é de teclado brasileiro, usar aspas retas +- Em diagramação profissional (livro, revista), aspas curvas são aceitáveis +- "Teste do WhatsApp" — se o texto parece conversa de app, aspas retas obrigatórias + +--- + +### Decoração Unicode + +**Problema:** IA usa caracteres Unicode decorativos como setas (→, ←, ↗), marcadores especiais (•, ▸, ▪), marcas de verificação (✓, ✗) e separadores (│, ─) que humanos brasileiros não digitam. Teclado brasileiro produz -, *, > e ponto final. + +**Antes (IA):** +> Benefícios do novo processo: +> → Redução de 40% no tempo de resposta +> → Aumento na satisfação do cliente +> → Integração com sistemas legados +> +> Stack: React │ Node.js │ PostgreSQL + +**Depois (humano):** +> Benefícios do novo processo: +> - Redução de 40% no tempo de resposta +> - Aumento na satisfação do cliente +> - Integração com sistemas legados +> +> Stack: React, Node.js, PostgreSQL + +**Evitar em PT-BR:** +- → como marcador de item (usar - ou *) +- │ como separador (usar vírgula, barra ou ponto) +- ✓ e ✗ no corpo do texto quando não houver função; se houver, usar o estado textual equivalente sem inferi-lo + +**Sinais adicionais de detecção:** +- Setas (→, ↗), marcadores especiais (▸, ▪), marcas de verificação (✓, ✗) e separadores (│, ─) que não são renderizados pelo teclado padrão brasileiro +- Uso de emojis como marcadores em texto formal +- Texto que parece ter sido copiado de um Notion com modelos de produtividade + +**Técnicas avançadas de correção:** +- Converter setas em hífens ou asteriscos: "→" → "-" +- Remover separadores Unicode: "React │ Node.js │ PostgreSQL" → "React, Node.js, PostgreSQL" +- Converter marcas de verificação em texto somente quando houver equivalente explícito, preservando o estado original: aprovado, reprovado, sim, não ou pendente +- "Teste do terminal" — se o texto renderiza com caracteres quebrados num terminal sem Unicode, é decoração artificial + +--- + +### Fragmentos curtos dramáticos + +**Problema:** IA produz frases de 1-3 palavras isoladas como parágrafo para criar "impacto". Esse recurso existe em copywriting, mas IA abusa até em texto informativo. Brasileiro escreve assim no Twitter, não em artigo ou e-mail. + +**Antes (IA):** +> O mercado mudou. +> +> Radicalmente. +> +> E quem não se adaptar vai ficar para trás. A pergunta não é se, mas quando. O futuro já chegou. +> +> Literalmente. + +**Depois (humano):** +> O mercado mudou radicalmente. Quem não se adaptar vai ficar para trás. A pergunta não é se, mas quando: o futuro já chegou, literalmente. + +**Evitar em PT-BR:** +- Palavra isolada como parágrafo sem função no perfil de voz ("Radicalmente.", "Literalmente.", "Ponto.") +- Sequência de fragmentos dramáticos sem função no perfil de voz +- Fragmento + ponto final para criar "peso" artificial + +**Sinais adicionais de detecção:** +- Palavra ou frase de 1-3 palavras isolada como parágrafo inteiro +- Uso repetido (mais de 1 por texto) em contexto que não é Twitter +- Fragmento que não acrescenta informação — só "peso" artificial + +**Técnicas avançadas de correção:** +- Em prosa neutra, integrar fragmentos ao período correspondente; em Crônica ou Rede Social, preservá-los quando fizerem parte da voz +- Integrar o fragmento sem perder o intensificador: "Mudou. Radicalmente." → "Mudou radicalmente." +- Se o fragmento é pura ênfase, ajustar a pontuação sem acrescentar urgência, conclusão ou avaliação +- "Teste do gênero" — o fragmento combina com o perfil de voz ou apenas simula impacto? + +--- + +### Vírgula de Oxford (Oxford Comma) + +**Problema:** IA treinada em inglês frequentemente insere vírgula antes do "e" final em enumerações (ex: "maçãs, bananas, e laranjas"). Em português brasileiro, essa vírgula é atípica e desnecessária — a norma é não usar vírgula antes de "e" em listas. Sua presença é marcador tipográfico de texto gerado por modelo anglófono. + +**Antes (IA):** +> A plataforma oferece dashboards, relatórios customizados, e integração com APIs externas. O time trabalha com React, Node.js, e PostgreSQL. + +**Depois (humano):** +> A plataforma oferece dashboards, relatórios customizados e integração com APIs externas. O time trabalha com React, Node.js e PostgreSQL. + +**Evitar em PT-BR:** +- Vírgula antes de "e" no último item de lista: "A, B, e C" → "A, B e C" +- Vírgula antes de "ou" final: "X, Y, ou Z" → "X, Y ou Z" +- Exceção legítima: quando o "e" liga orações com sujeitos diferentes (vírgula de clareza, não de lista) + +**Sinais adicionais de detecção:** +- Vírgula antes de "e" em listas de 3+ itens +- Vírgula antes de "ou" em alternativas +- Mistura de textos com e sem Oxford comma no mesmo documento + +**Técnicas avançadas de correção:** +- **Regra PT-BR**: sem vírgula antes de "e"/"ou" em listas +- Exceção: quando a vírgula evita ambiguidade real (sujeitos diferentes nas orações) +- "Teste do vestibular" — se a frase passaria no ENEM como correta, a vírgula está certa + +--- + +### Ponto final e aspas (convenção brasileira) + +**Problema:** IA segue a convenção americana de colocar ponto final DENTRO das aspas, mesmo quando a citação não é frase completa. Em português brasileiro, o ponto vai FORA quando as aspas envolvem apenas parte da frase. + +**Antes (IA — convenção americana):** +> O CEO disse que a empresa está "no caminho certo." +> +> A meta é atingir o que chamam de "product-market fit." + +**Depois (humano — convenção brasileira):** +> O CEO disse que a empresa está "no caminho certo". +> +> A meta é atingir o que chamam de "product-market fit". + +**Regra PT-BR:** +- Citação é frase completa e independente → ponto dentro: Ele disse: "Vamos resolver isso." +- Citação é parte da frase do autor → ponto fora: O plano é "escalar rápido". +- Na dúvida: ponto fora (é o padrão brasileiro em texto corrido) + +**Sinais adicionais de detecção:** +- Ponto dentro das aspas quando a citação é parte da frase do autor +- Inconsistência: às vezes dentro, às vezes fora, no mesmo texto +- Citações de 1 palavra com ponto dentro ("impacto.") + +**Técnicas avançadas de correção:** +- **Regra PT-BR**: se a citação é parte da frase → ponto fora. Se a citação é frase completa → ponto dentro +- Padronizar em todo o documento +- "Teste da citação" — se a citação termina com "que", o ponto vai fora + +--- + +### Ressalva com preâmbulo (minimiza → infla) + +**Problema:** Padrão de IA de 2025/2026 em que uma minimização genérica é seguida por inflação automática para compensar. O contraste pode ser legítimo e deve ser preservado; o sinal aparece quando a moldura se repete mecanicamente ou quando os dois lados não têm apoio no texto-fonte. + +**Antes (IA):** +> Embora pareça um conceito simples, a consistência na publicação de conteúdo representa um dos pilares mais fundamentais e transformadores de qualquer estratégia de marketing digital moderna. +> +> À primeira vista, essa pode parecer uma mudança incremental, mas na verdade constitui uma transformação paradigmática na forma como organizações interagem com seus stakeholders. + +**Depois (humano):** +> A consistência na publicação de conteúdo é um dos pilares mais fundamentais e transformadores de qualquer estratégia moderna de marketing digital. Mesmo assim, pode parecer um conceito simples. +> +> A mudança constitui uma transformação paradigmática na forma como as organizações interagem com seus stakeholders, embora à primeira vista possa parecer incremental. + +**Evitar em PT-BR:** +- "Embora pareça simples, na verdade é [superlativo]" +- "À primeira vista... mas na verdade constitui..." +- "Pode parecer óbvio, porém [inflação]" +- "Apesar de ser um conceito básico, representa um dos mais [superlativo]" +- Qualquer estrutura que minimiza no preâmbulo e infla na oração principal + +**Sinais adicionais de detecção:** +- Estrutura "minimiza → infla" em uma única frase: "Embora pareça simples, é transformador" +- Uso de "na verdade" como ponte entre minimização e inflação +- O preâmbulo é sempre genérico ("parece simples", "à primeira vista") e a inflação é sempre superlativa + +**Técnicas avançadas de correção:** +- Preservar os dois lados quando o autor afirma um contraste; remover apenas a moldura repetitiva, sem escolher uma posição nova +- Se o contraste é legítimo, mostrar a tensão com dados somente quando esses dados já estiverem na fonte; caso contrário, manter a formulação qualitativa +- "Teste do podcast host" — se o apresentador diria isso sem soar como vendedor, a frase é honesta diff --git a/.github/skills/humanizar/references/padroes-exclusivos-pt-br.md b/.github/skills/humanizar/references/padroes-exclusivos-pt-br.md new file mode 100644 index 0000000..04d17c0 --- /dev/null +++ b/.github/skills/humanizar/references/padroes-exclusivos-pt-br.md @@ -0,0 +1,567 @@ +# Padrões Exclusivos do Português Brasileiro + +Padrões de texto de IA que **só existem em PT-BR** — não têm equivalente na skill humanizer original em inglês. São marcadores culturais e linguísticos que denunciam texto gerado por máquina especificamente no contexto brasileiro. + +Estes padrões exploram vícios do português corporativo, jurídico e acadêmico brasileiro que LLMs absorveram de seus dados de treinamento e reproduzem de forma desproporcional. + +> **Regra transversal:** toda correção abaixo preserva sujeitos, fatos, números, fontes, relações causais, prazos, estado temporal e modalidade. Os pares mostram mudanças de forma, não autorização para completar lacunas. Escolha a alternativa compatível com o perfil de voz; se ela exigir informação ausente ou mudar o grau de certeza, não a use. + +--- + +## 1. Gerundismo + +### Gerundismo Corporativo + +**Palavras/expressões gatilho:** "vou estar enviando", "estaremos realizando", "vai estar recebendo", "iremos estar providenciando", "vamos estar agendando" + +**Problema:** LLMs absorveram o gerundismo de e-mails corporativos e scripts de telemarketing brasileiros. Nenhum humano escreve assim voluntariamente — é padrão de SAC e central de atendimento que virou piada nacional. Quando aparece em texto "natural", grita automação. + +**Antes (IA):** +> Vou estar enviando o relatório de métricas do Q2 para validação. A equipe de produto vai estar realizando a análise de impacto e estaremos agendando uma call para alinhamento na próxima semana. + +**Depois (humano):** +> Vou enviar o relatório de métricas do Q2 para validação. A equipe de produto vai analisar o impacto, e vamos agendar uma call de alinhamento na próxima semana. + +**Evitar em PT-BR:** +- "Vou estar enviando" +- "Estaremos realizando" +- "Vai estar recebendo" +- "Iremos estar providenciando" +- "Vamos estar disponibilizando" + +**Alternativas naturais:** +- "Vou estar enviando" → "Vou enviar" +- "Estaremos realizando" → "Realizaremos" +- "Vai estar recebendo" → "Vai receber" +- "Iremos estar providenciando" → "Providenciaremos" +- "Vamos estar disponibilizando" → "Vamos disponibilizar" + +**Sinais adicionais de detecção:** +- "Vai estar + gerúndio" em e-mails de SAC e respostas de chatbot +- "Iremos estar + gerúndio" em comunicações formais de empresas +- Gerundismo em texto que deveria ser direto (mensagens de WhatsApp, Slack) + +**Técnicas avançadas de correção:** +- Converter para futuro simples: "vou estar enviando" → "vou enviar" +- Se o gerúndio indica processo contínuo real, mantê-lo e simplificar apenas a perífrase, sem trocar o verbo da fonte +- Preservar sujeito, prazo e modalidade: uma promessa vaga não ganha data, e uma ação futura não vira ação concluída +- "Teste do áudio" — ler a frase em voz alta; se soa como atendente de telemarketing, é gerundismo + +### Gerúndio Conclusivo (Falsa Análise de Impacto) + +**Palavras/expressões gatilho:** "..., destacando a importância de...", "..., contribuindo para...", "..., demonstrando que...", "..., reforçando a necessidade de...", "..., evidenciando que...", "..., mostrando como...", "..., sublinhando o papel de...", "..., consolidando a posição de..." + +**Problema:** LLMs fecham frases com orações reduzidas de gerúndio que fingem ser análise de impacto — mas não dizem nada que o leitor não já deduziu. É um tique que infla o texto com falsa profundidade. Funciona como "conclusão automática por frase" que humanos não fazem: nós ou tiramos a conclusão em frase separada, ou deixamos o leitor tirar sozinho. + +**Antes (IA):** +> A Nubank atingiu 100 milhões de clientes em 2025, consolidando sua posição como maior fintech da América Latina. O app teve nota 4.8 na App Store, demonstrando que a experiência do usuário continua sendo prioridade. A empresa expandiu para México e Colômbia, reforçando a necessidade de adaptação local. + +**Depois (humano):** +> A Nubank atingiu 100 milhões de clientes em 2025 e consolidou sua posição como a maior fintech da América Latina. O app teve nota 4,8 na App Store; isso demonstrou que a experiência do usuário continua sendo prioridade. A empresa expandiu para México e Colômbia, o que reforçou a necessidade de adaptação local. + +**Evitar em PT-BR:** +- "..., destacando a importância de [coisa óbvia]" +- "..., contribuindo para o fortalecimento de..." +- "..., demonstrando que [conclusão que já estava implícita]" +- "..., reforçando a necessidade de..." +- "..., evidenciando o compromisso com..." +- "..., consolidando [posição/presença/papel]" +- Qualquer gerúndio no final que funciona como "mini-conclusão" redundante + +**Alternativas naturais:** +- Cortar a oração de gerúndio somente quando ela for semanticamente redundante +- Se a oração expressa conclusão, causalidade ou modalidade, transformá-la em frase finita sem apagar essa relação +- Reaproveitar um detalhe concreto já presente sem substituir por ele uma conclusão que também faça parte da fonte +- Usar coordenação simples: "e consolidou sua posição como a maior" em vez de "consolidando sua posição como a maior" + +**Sinais adicionais de detecção:** +- Orações reduzidas de gerúndio no final de frase como "conclusão automática" +- Padrão: [afirmação], [gerúndio conclusivo] → "O app tem nota 4.8, demonstrando que UX é prioridade" +- O gerúndio repete o que já foi dito sem adicionar informação + +**Técnicas avançadas de correção:** +- Cortar o gerúndio apenas se a remoção não alterar nenhuma proposição +- Se a conclusão integra o conteúdo → transformar em frase independente, preservando sujeito e força da afirmação +- "Teste do copy-paste" — se você pode remover a oração com gerúndio e a frase ainda diz tudo, o gerúndio é redundante + +--- + +## 2. Conectivos Arcaicos Fora de Contexto + +### Latinismo Jurídico em Texto Informal + +**Palavras/expressões gatilho:** "ademais", "outrossim", "destarte", "não obstante", "doravante", "nesse diapasão", "mister se faz", "em última análise", "no bojo de", "ab initio" + +**Problema:** Essas palavras pertencem ao registro de contratos, decisões judiciais e regulamentos universitários. LLMs as usam em posts de blog, e-mails de produto e copy de landing page porque foram treinados em muito texto jurídico brasileiro (que é desproporcionalmente formal comparado a outros idiomas). Brasileiro nenhum usa "outrossim" num Slack. + +**Antes (IA):** +> A implementação do novo sistema de pagamentos trouxe resultados expressivos. Ademais, a taxa de churn reduziu 15%. Outrossim, o NPS subiu 12 pontos. Destarte, pode-se concluir que a estratégia foi bem-sucedida. + +**Depois (humano):** +> A implementação do novo sistema de pagamentos trouxe resultados expressivos: a taxa de churn caiu 15%, e o NPS subiu 12 pontos. Isso permite concluir que a estratégia foi bem-sucedida. + +**Evitar em PT-BR:** +- "Ademais" (fora de petições judiciais) +- "Outrossim" (em qualquer contexto que não seja um contrato) +- "Destarte" (ninguém fala isso desde 1920) +- "Nesse diapasão" (só juiz usa) +- "Mister se faz" (linguagem de despacho) +- "Doravante" (até em contratos tá caindo em desuso) + +**Alternativas naturais:** +- "Ademais" → "Além disso" / "E ainda"; usar apenas vírgula se a relação aditiva continuar inequívoca +- "Outrossim" → "Também" / "E"; cortar somente se não houver relação adicional a preservar +- "Destarte" → "Então" / "Por isso" / "Resultado:" +- "Não obstante" → "Mesmo assim" / "Apesar disso" / "Mas" +- "Doravante" → "A partir de agora" / "De agora em diante" + +**Sinais adicionais de detecção:** +- "Outrossim" em qualquer contexto que não seja petição judicial +- "Destarte" em textos pós-2000 +- "Nesse diapasão" em comunicação corporativa moderna +- Texto que parece ter sido escrito por alguém que leu muito Diário Oficial + +**Técnicas avançadas de correção:** +- Substituir por conectivos modernos de mesmo valor lógico; não apagar contraste, conclusão ou marco temporal +- Se o texto é jurídico → manter (é o registro esperado) +- "Teste do Slack" — se você não escreveria no Slack da empresa, é arcaico + +--- + +## 3. Aberturas Genéricas Estilo ENEM + +### Dissertação de Vestibular Disfarçada + +**Palavras/expressões gatilho:** "Em um mundo cada vez mais...", "No cenário atual...", "Na contemporaneidade...", "É inegável que...", "Diante do exposto...", "No contexto de...", "Em meio a um cenário de..." + +**Problema:** LLMs reproduzem a estrutura da redação nota 1000 do ENEM — abertura genérica que contextualiza o tema de forma ampla antes de dizer qualquer coisa específica. Todo brasileiro reconhece esse padrão porque escreveu assim no vestibular. Em texto profissional, é sinal claro de que ninguém pensou antes de escrever. + +**Antes (IA):** +> Em um mundo cada vez mais digitalizado, as fintechs brasileiras vêm desempenhando um papel fundamental na democratização do acesso a serviços financeiros. No cenário atual, é inegável que a tecnologia transformou a maneira como lidamos com dinheiro. + +**Depois (humano):** +> O mundo está cada vez mais digitalizado, e as fintechs brasileiras vêm desempenhando um papel fundamental na democratização do acesso a serviços financeiros. Hoje, é inegável que a tecnologia transformou a maneira como lidamos com dinheiro. + +**Evitar em PT-BR:** +- "Em um mundo cada vez mais [adjetivo]..." +- "No cenário atual..." +- "Na contemporaneidade..." +- "É inegável que..." +- "No contexto de [tema genérico]..." +- "Diante de um cenário de..." +- "É sabido que..." +- "Nos dias atuais..." + +**Alternativas naturais:** +- Começar com dado concreto somente quando ele já estiver no texto-fonte +- Começar com afirmação direta que preserve a tese e o grau de certeza existentes +- Usar pergunta apenas quando ela mantiver a modalidade do trecho e combinar com o perfil de voz +- Começar com exemplo somente se ele tiver sido fornecido na fonte +- Começar pelo meio: pular o contexto e ir direto ao ponto + +**Sinais adicionais de detecção:** +- "É inegável que" como abertura de parágrafo +- "No contexto contemporâneo" como introdução genérica +- "Diante do exposto" como transição entre seções +- Abertura que poderia ser copiada para qualquer redação de vestibular + +**Técnicas avançadas de correção:** +- Começar com **dado existente**, **pergunta semanticamente equivalente** ou **afirmação direta** +- Se o texto precisa de contexto → colocar o contexto DEPOIS do gancho, não antes +- "Teste do primeiro tweet" — se a abertura seria um tweet que ninguém leria, é ENEM demais + +--- + +## 4. Ressalva burocrática + +### Marcadores de Importância Artificial + +**Palavras/expressões gatilho:** "Vale ressaltar que...", "É importante destacar que...", "Cumpre salientar que...", "Faz-se necessário...", "É fundamental observar que...", "Convém mencionar que...", "Importa registrar que...", "Merece atenção o fato de que..." + +**Problema:** Essas expressões existem para preencher espaço sem dizer nada. São muletas de quem precisa parecer que está dizendo algo importante sem comprometer-se com a afirmação. LLMs usam MUITO porque o treinamento inclui toneladas de texto burocrático brasileiro (diários oficiais, pareceres, relatórios corporativos). Brasileiro real, quando quer ressaltar algo, simplesmente diz. + +**Antes (IA):** +> Vale ressaltar que a taxa de conversão do funil apresentou crescimento significativo. É importante destacar que esse resultado está diretamente relacionado à implementação das novas landing pages. Cumpre salientar que a equipe de growth executou 14 testes A/B no período. + +**Depois (humano):** +> A taxa de conversão do funil cresceu significativamente. Esse resultado está diretamente relacionado à implementação das novas landing pages. A equipe de growth executou 14 testes A/B no período. + +**Evitar em PT-BR:** +- "Vale ressaltar que..." +- "É importante destacar que..." +- "Cumpre salientar que..." +- "Faz-se necessário observar que..." +- "É fundamental que se reconheça..." +- "Convém mencionar que..." +- "Importa registrar que..." +- "Merece destaque o fato de que..." + +**Alternativas naturais:** +- Cortar a expressão inteira e começar pela informação, sem quantificar o que a fonte não quantificou: "A taxa de conversão cresceu significativamente." +- Se precisa enfatizar: usar posição na frase (colocar no início) ou itálico +- Em perfil neutro, usar transição discreta ou nenhuma; em perfil informal, escolher uma chamada compatível com a voz já existente + +**Sinais adicionais de detecção:** +- "Cumpre salientar" como abertura de frase +- "Faz-se necessário" em e-mails de trabalho +- "Importa registrar" em textos que não são registro oficial +- Duas ou mais expressões de ressalva no mesmo parágrafo + +**Técnicas avançadas de correção:** +- Cortar a expressão e ir direto à informação +- Se precisa de ênfase → usar posição na frase (começo) ou repetição intencional +- "Teste do post-it" — se a informação cabe num post-it sem perder sentido, a muleta não era necessária + +--- + +## 5. Formalidade Deslocada + +### Registro de Ofício em Contexto de Startup + +**Palavras/expressões gatilho:** "No que tange a", "Tendo em vista que", "O referido", "Conforme mencionado anteriormente", "O supracitado", "No tocante a", "Em face do exposto", "Haja vista que" + +**Problema:** LLMs confundem "escrever bem" com "escrever formal". Em PT-BR, o registro formal extremo pertence a documentos oficiais (ofícios, memorandos, atas). Quando aparece em e-mail de produto, post de blog de tecnologia ou comunicação interna de startup, parece que um robô leu o Manual de Redação da Presidência da República e achou que serve pra tudo. + +**Antes (IA):** +> No que tange à implementação do novo design system, tendo em vista que a equipe de front-end sinalizou gargalos, faz-se necessário priorizar a refatoração do referido sistema de componentes. Conforme mencionado anteriormente, o supracitado projeto tem deadline no Q3. + +**Depois (humano):** +> Sobre a implementação do design system: como a equipe de front-end sinalizou gargalos, é necessário priorizar a refatoração desse sistema de componentes. Como já mencionado, o projeto tem deadline no Q3. + +**Evitar em PT-BR:** +- "No que tange a" (fora de parecer jurídico) +- "Tendo em vista que" (fora de justificativa formal) +- "O referido" / "O supracitado" (fora de processo judicial) +- "Conforme mencionado anteriormente" (redundante — se mencionou, o leitor sabe) +- "Em face do exposto" (conclusão de parecer) +- "Haja vista que" (pedante em qualquer contexto informal) +- "No tocante a" (burocracia pura) + +**Alternativas naturais:** +- "No que tange a" → "Sobre" / "Quanto a" +- "Tendo em vista que" → "Como" / "Já que" / "Porque" +- "O referido" → "Esse" / "O"; cortar somente se o referente continuar inequívoco +- "Conforme mencionado anteriormente" → "Como já mencionado"; cortar somente se a referência anterior não tiver função +- "Em face do exposto" → "Diante disso" / "Por isso", quando introduzir conclusão +- "Haja vista que" → "Já que" / "Porque" + +**Sinais adicionais de detecção:** +- "No que tange a" em e-mails de produto +- "Tendo em vista que" em mensagens de Slack +- "O referido projeto" em comunicação interna +- Registro formal em contexto onde a informalidade é esperada + +**Técnicas avançadas de correção:** +- Mapear o **perfil de voz** antes de reescrever e reduzir apenas a formalidade deslocada +- Substituir por equivalentes brasileiros compatíveis com o canal, sem forçar coloquialidade +- "Teste do canal" — a formulação combina com o meio, o público e a voz do texto-fonte? + +--- + +## 6. Oficialês brasileiro + +### Linguagem Jurídica/Burocrática Fora do Contexto Legal + +**Palavras/expressões gatilho:** "Segue em anexo para os devidos fins", "Venho por meio deste", "Solicito a gentileza de", "Segue para conhecimento e providências", "Informo para os devidos fins", "Encaminho o presente para apreciação", "Trata o presente de" + +**Problema:** O oficialês brasileiro é um dialeto próprio — linguagem de ofício, memorando e despacho que LLMs internalizaram de forma massiva porque a administração pública brasileira produz volumes absurdos de texto nesse registro. Quando aparece fora do contexto público ou legal, denuncia geração automática. Nenhum PM de startup escreve "venho por meio deste" num Notion. + +**Antes (IA):** +> Venho por meio deste comunicar que a feature de onboarding encontra-se em fase final de implementação. Solicito a gentileza de agendar a revisão de código para os devidos fins de validação. Segue em anexo o documento de especificação para conhecimento e eventuais providências. + +**Depois (humano):** +> A feature de onboarding está na fase final de implementação. Solicito o agendamento da revisão de código para validação. O documento de especificação segue anexo para conhecimento e eventuais providências. + +**Evitar em PT-BR:** +- "Venho por meio deste [comunicar/informar/solicitar]" +- "Segue em anexo para os devidos fins" +- "Solicito a gentileza de" +- "Para conhecimento e providências" +- "Informo para os devidos fins que" +- "Trata o presente [e-mail/documento] de" +- "Sirvo-me do presente para" +- "Encaminho para apreciação superior" + +**Alternativas naturais:** +- "Venho por meio deste informar que [fato]" → "[Fato]", sem apresentá-lo como novidade se a fonte não fizer isso +- "Segue em anexo" → "O arquivo segue anexo" / "O arquivo está anexado", conforme o perfil +- "Solicito a gentileza de [ação]" → "Solicito que [ação]"; em perfil informal, "Pode [ação]?" +- "Para conhecimento" → "Para informar" / "Para ciência", preservando a finalidade +- "Para os devidos fins" → cortar somente quando não designar uma finalidade específica + +**Sinais adicionais de detecção:** +- "Venho por meio deste" em qualquer canal que não seja ofício público +- "Para os devidos fins" como fechamento de e-mail +- "Solicito a gentileza de" em mensagens internas +- Texto que parece ter sido gerado por um chatbot de órgão público + +**Técnicas avançadas de correção:** +- Converter para linguagem direta sem criar prazo ou responsável: "Preciso que você faça X"; manter o prazo somente se ele existir na fonte +- Se é comunicação externa formal → manter um nível mínimo de polidez, mas sem oficialês +- "Teste do canal" — a formulação mantém a polidez e a voz esperadas naquele contexto? + +--- + +## 7. Evitação de Verbos Simples + +### Substituição de "É/São/Tem" por Eufemismos Rebuscados + +**Palavras/expressões gatilho:** "constitui", "configura-se como", "dispõe de", "encontra-se", "figura como", "representa", "serve como", "afigura-se como", "situa-se", "apresenta-se como", "há [quantidade]", "existem [quantidade]", "haver necessidade" + +**Problema:** Estudos mostram >10% de queda no uso de copulativas simples ("é", "são", "está") em textos gerados por IA pós-2023. LLMs evitam verbos simples e os substituem por construções rebuscadas — provavelmente porque o RLHF penaliza respostas "simples demais". Em PT-BR, isso cria frases que nenhum brasileiro falaria em voz alta. Inclui também a substituição sistemática do verbo "ter" existencial (uso coloquial consagrado no Brasil) por "haver" ou "existir" para soar "correto" — um hipercorrecionismo que nenhum brasileiro pratica na fala e que cada vez menos pratica na escrita. + +**Antes (IA):** +> O Nubank constitui uma das maiores fintechs da América Latina e configura-se como referência em experiência do usuário. A empresa dispõe de mais de 80 milhões de clientes e encontra-se em expansão para novos mercados. Seu modelo de negócios figura como paradigma para startups do setor. + +**Depois (humano):** +> O Nubank é uma das maiores fintechs da América Latina e é referência em experiência do usuário. A empresa tem mais de 80 milhões de clientes e está em expansão para novos mercados. Seu modelo de negócios é um paradigma para startups do setor. + +**Evitar em PT-BR:** +- "constitui [algo]" (quando "é" resolve) +- "configura-se como" (quando "é" resolve) +- "dispõe de" (quando "tem" resolve) +- "encontra-se [em algum estado]" (quando "está" resolve) +- "figura como" (quando "é" resolve) +- "situa-se" (quando "fica" ou "está" resolve) +- "apresenta-se como" (quando "parece" ou "é" resolve) +- "afigura-se como" (ninguém fala isso) +- "há muitos/muitas [X]" (quando "tem muito/muita [X]" é mais natural — perfis Crônica, Corporativo Informal e WhatsApp) +- "existem diversas opções" (quando "tem várias opções" resolve) +- "não há como negar" (quando "não tem como negar" soa brasileiro) + +**Alternativas naturais:** +- "constitui uma referência" → "é referência" +- "dispõe de 80 milhões" → "tem 80 milhões" +- "encontra-se em expansão" → "está em expansão" +- "configura-se como líder" → "é líder" +- "situa-se entre os maiores" → "está entre os maiores" / "é um dos maiores" +- "há muitas pessoas" → "tem muita gente" +- "existem diversos fatores" → "tem vários fatores" / "são vários fatores" +- "há necessidade de" → "precisa de" / "tem que" +- "não há dúvidas de que" → "não tem dúvida que" / "é claro que" + +**Nota sobre "ter" existencial:** Em perfis formais (Acadêmico), "haver" pode ser mantido. Em Crônica, Corporativo Informal, Rede Social e WhatsApp, "ter" existencial pode ser a forma natural brasileira. A escolha deve acompanhar o perfil de voz e não alterar quantidade, existência ou modalidade. + +**Sinais adicionais de detecção:** +- "Dispõe de" em vez de "tem" +- "Configura-se como" em vez de "é" +- "Encontra-se em" em vez de "está" +- Texto onde os verbos "ser", "ter", "estar" foram sistematicamente substituídos + +**Técnicas avançadas de correção:** +- Restaurar verbos simples quando apropriado ao registro +- Se o texto é acadêmico ou jurídico → manter a formalidade, mas evitar excesso +- "Teste da fala" — se você não falaria assim, não escreva assim + +--- + +## 8. Expressões Infladas + +### Vocabulário que Brasileiro Não Usa na Fala + +**Palavras/expressões gatilho:** "contribui significativamente para", "no âmbito de", "de forma expressiva", "em termos de", "a nível de", "no que diz respeito a", "sob a ótica de", "à luz de", "na esfera de", "potencializar" + +**Problema:** LLMs produzem frases que parecem tradução simultânea de jargão corporativo americano filtrado por um manual de redação de 1995. São expressões que aparecem em relatórios anuais e apresentações de PowerPoint, mas que nenhum brasileiro usa quando está explicando algo de verdade. O teste é simples: se você não falaria isso em voz alta numa reunião, não deveria escrever. + +**Antes (IA):** +> A estratégia de product-led growth contribui significativamente para a escalabilidade do negócio no âmbito do mercado brasileiro de SaaS. No que diz respeito à aquisição de usuários, a abordagem potencializa os resultados de forma expressiva, sob a ótica da eficiência operacional. + +**Depois (humano):** +> A estratégia de product-led growth contribui bastante para a escalabilidade do negócio no mercado brasileiro de SaaS. Na aquisição de usuários, a abordagem melhora os resultados de forma expressiva do ponto de vista da eficiência operacional. + +**Evitar em PT-BR:** +- "contribui significativamente para" sem explicar o efeito; se a fonte não quantifica, preservar a intensidade qualitativa sem criar número +- "no âmbito de" (99% das vezes é só "em") +- "de forma expressiva/significativa" quando encobre um valor que já existe na fonte +- "no que diz respeito a" (é "sobre") +- "sob a ótica de" (é "pra" ou "do ponto de vista de") +- "potencializar" quando a fonte já permite escolher com precisão entre "melhorar" e "aumentar" +- "à luz de" (é "considerando" ou "com base em") +- "a nível de" (errado gramaticalmente E vazio semanticamente) +- "na esfera de" (é "em") + +**Alternativas naturais:** +- "contribui significativamente" → "contribui bastante"; usar número somente quando ele já existir na fonte +- "no âmbito de" → "em" / "dentro de"; cortar somente se não delimitar o escopo +- "potencializar resultados" → "melhorar resultados"; nomear a métrica somente se a fonte a identificar +- "de forma expressiva" → manter a intensidade qualitativa ou usar a medida já informada +- "no que diz respeito a" → "sobre" / "quanto a" +- "sob a ótica de" → "do ponto de vista de", sem criar um agente ausente + +**Sinais adicionais de detecção:** +- "No que diz respeito a" em vez de "sobre" +- "Sob a ótica de" em vez de "do ponto de vista de" +- "À luz de" em vez de "com base em" +- Expressões que parecem tradução literal de "in terms of", "from the perspective of" + +**Técnicas avançadas de correção:** +- Substituir por equivalentes diretos em português +- Aplicar regra de compressão: se a expressão tem 5+ palavras e pode ser dita em 1-2, comprimir +- "Teste da tradução reversa" — se a expressão parece que foi traduzida do inglês corporativo, substituir pelo equivalente que brasileiro realmente fala + +--- + +## 9. Transições Mecânicas Repetidas + +### O Parágrafo que Começa Sempre Igual + +**Palavras/expressões gatilho:** "Além disso", "Nesse sentido", "Diante disso", "Em contrapartida", "Por outro lado", "No entanto", "Dessa forma", "Sendo assim", "Nessa perspectiva", "À vista disso" + +**Problema:** IA em PT-BR usa transições como muleta estrutural — todo parágrafo começa com um conectivo, criando um ritmo mecânico reconhecível. É o equivalente brasileiro do "Furthermore" / "Moreover" em inglês. Humanos brasileiros variam: às vezes usam transição, às vezes começam direto, às vezes com pergunta, às vezes com exemplo. A repetição de "Além disso... Nesse sentido... Diante disso..." a cada parágrafo é fingerprint de IA. + +**Antes (IA):** +> O mercado de SaaS B2B no Brasil cresceu 40% em 2025. **Além disso**, a entrada de novos players internacionais acirrou a competição. **Nesse sentido**, startups brasileiras precisam diferenciação clara. **Diante disso**, estratégias de nicho ganham relevância. **Em contrapartida**, o aumento de competição também valida o mercado. **Dessa forma**, empresas que encontrarem seu posicionamento tendem a prosperar. + +**Depois (humano):** +> O mercado de SaaS B2B no Brasil cresceu 40% em 2025. A entrada de novos concorrentes internacionais acirrou a competição. Por isso, startups brasileiras precisam de diferenciação clara, e estratégias de nicho ganham relevância. Ao mesmo tempo, o aumento da competição também valida o mercado. Empresas que encontrarem seu posicionamento tendem a prosperar. + +**Evitar em PT-BR:** +- "Além disso" como abertura de mais de 1 parágrafo no mesmo texto +- "Nesse sentido" sem referência clara ao "sentido" +- "Diante disso" / "Diante do exposto" (dissertação de ENEM) +- "Em contrapartida" quando não há real contrapartida +- "Por outro lado" quando não há dois lados claros +- "Dessa forma" / "Sendo assim" como conclusão automática +- Qualquer padrão de [conectivo] + [afirmação] repetido 3+ vezes seguidas + +**Alternativas naturais:** +- Começar com o conteúdo direto, sem transição: "Startups brasileiras precisam se diferenciar." +- Usar pergunta retórica apenas se ela não mudar a modalidade e combinar com o perfil de voz +- Fragmento com conteúdo existente: "Resultado: estratégias de nicho ganham relevância." +- Usar exemplo concreto somente quando ele já estiver no texto-fonte +- Contraste implícito (sem "por outro lado"): "Só que mais competição também valida o mercado." +- Continuação natural com "E", "Mas", "Só que" ou "Agora", escolhendo apenas o conectivo que preserve a relação original + +--- + +## 10. Purismo Linguístico Artificial + +### Traduzir Estrangeirismos que Brasileiro Usa Naturalmente + +**Palavras/expressões gatilho:** "retroalimentação" (feedback), "implantação" (deploy), "rotatividade de clientes" (churn), "corrida/iteração" (sprint), "fluxo de trabalho" (workflow), "partes interessadas" (stakeholders), "entregáveis" (deliverables), "plataforma de dados" (data lake) + +**Problema:** LLMs foram treinados com textos acadêmicos e governamentais que evitam estrangeirismos por política editorial. Quando geram texto sobre tech/startup/marketing, traduzem termos que nenhum profissional brasileiro traduz. "Vamos reduzir o churn" é como todo mundo fala. "Vamos reduzir a rotatividade de clientes" soa como tradução de livro técnico de 2003. Forçar tradução de termos naturalizados é um dos sinais mais claros de IA em PT-BR tech. + +**Antes (IA):** +> A equipe realizou a implantação do novo microsserviço e obteve retroalimentação positiva das partes interessadas. A rotatividade de clientes diminuiu após a iteração focada em experiência do usuário. O fluxo de trabalho foi otimizado para entregar os entregáveis dentro do prazo da corrida. + +**Depois (humano):** +> A equipe fez o deploy do novo microsserviço e recebeu feedback positivo dos stakeholders. O churn diminuiu após a sprint focada em experiência do usuário. O workflow foi otimizado para entregar os deliverables dentro do prazo da sprint. + +**Evitar em PT-BR (traduzir quando o termo inglês já é padrão no mercado):** +- "retroalimentação" em vez de "feedback" +- "implantação" em vez de "deploy" (contexto tech) +- "rotatividade de clientes" em vez de "churn" +- "corrida" ou "iteração" em vez de "sprint" +- "partes interessadas" em vez de "stakeholders" +- "entregáveis" em vez de "deliverables" +- "fluxo de trabalho" em vez de "workflow" +- "computação em nuvem" em vez de "cloud" (contexto dev) +- "aprendizado de máquina" em vez de "machine learning" (contexto tech) +- "cadeia de suprimentos" em vez de "supply chain" (contexto startup/ops) + +**Alternativas naturais:** +- Usar o termo em inglês como brasileiro usa: "feedback", "deploy", "churn", "sprint" +- Se o usuário pedir explicação para público não técnico, usar o termo + explicação sem acrescentar consequência: "churn (perda de clientes)" +- Manter consistência: se usou "deploy" uma vez, não alterna com "implantação" +- Preservar a terminologia do texto-fonte. Adaptá-la apenas quando o perfil de voz ou o público exigirem e houver equivalente sem perda de precisão + +--- + +## 11. Colocação Pronominal Artificial + +### Ênclise Forçada e Mesóclise Fantasma + +**Palavras/expressões gatilho:** "Apresento-lhe", "Trata-se de", "Faz-se necessário", "Encontra-se disponível", "Realizou-se", "Dar-se-á", "Enviar-lhe-ei", "Diga-se de passagem", "Permite-nos afirmar" + +**Problema:** LLMs seguem regras prescritivas de colocação pronominal com rigor artificial — colocam pronomes em ênclise (verbo + pronome) e até mesóclise em situações onde o brasileiro usa próclise (pronome + verbo) intuitivamente. Na fala e escrita real brasileira, a próclise domina em quase todos os contextos. A ênclise excessiva soa como tradução de manual de gramática portuguesa (de Portugal), não como brasileiro escrevendo. + +**Antes (IA):** +> O sistema permite-nos monitorar métricas em tempo real. Trata-se de uma solução que integra-se facilmente ao stack existente. Encontra-se disponível para todos os planos. Enviar-lhe-emos o relatório até sexta. + +**Depois (humano):** +> O sistema nos permite monitorar métricas em tempo real. É uma solução que se integra facilmente ao stack existente. Está disponível para todos os planos. Nós lhe enviaremos o relatório até sexta. + +**Evitar em PT-BR:** +- "Permite-nos" (quando "nos permite" é mais natural) +- "Trata-se de" em excesso (quando "é" resolve) +- "Encontra-se" (quando "está" ou "fica" resolve) +- "Integra-se" no início ou meio de frase (quando "se integra" soa melhor) +- Qualquer mesóclise fora de texto jurídico ou literário intencional ("dar-se-á", "enviar-lhe-ei") +- Ênclise após sujeito explícito: "O usuário cadastra-se" → "O usuário se cadastra" + +**Alternativas naturais:** +- "Permite-nos" → "nos permite"; "deixa a gente" somente em perfil informal já autorizado +- "Trata-se de" → "É" / "Isso é" +- "Encontra-se disponível" → "Está disponível"; "Tá disponível" somente em perfil informal +- "Realizou-se a migração" → "A migração foi realizada", sem inventar quem a realizou +- "Dar-se-á início" → "Terá início" / "Vai começar", preservando o tempo verbal +- "Faz-se necessário" → "Precisa" / "É necessário" + +**Nota:** A ênclise é legítima após vírgula, no início absoluto de frase, e em imperativos ("Diga-me", "Faça-o"). O problema é quando a IA a usa em posições onde o brasileiro naturalmente coloca o pronome antes do verbo. + +--- + +## 12. Voz Passiva Analítica + +### Passiva por Influência do Inglês + +**Palavras/expressões gatilho:** "foi realizado por", "foi implementado pela equipe", "será desenvolvido pelo time", "foi identificado que", "é considerado como", "foi constatado que", "foram obtidos resultados", "foi tomada a decisão" + +**Problema:** IAs abusam da voz passiva analítica (ser + particípio) por influência direta do inglês, onde a passiva é muito mais frequente que em português. Em PT-BR natural, preferimos voz ativa ou passiva sintética (com "se"). Sequências de 3+ frases em passiva soam como tradução automática de release notes em inglês. + +**Antes (IA):** +> O relatório foi finalizado pela equipe de dados. A análise foi conduzida utilizando metodologia ágil. Foram identificados 3 gargalos principais. A decisão foi tomada de priorizar o módulo de pagamentos. Os testes foram realizados em ambiente de staging. + +**Depois (humano):** +> A equipe de dados finalizou o relatório. Na análise, conduzida com metodologia ágil, foram identificados 3 gargalos principais. Decidiu-se priorizar o módulo de pagamentos. Os testes foram realizados em ambiente de staging. + +**Evitar em PT-BR:** +- "O relatório foi finalizado pela equipe" → "A equipe finalizou o relatório" / "A equipe concluiu o relatório" +- "Foi identificado que" → "Identificou-se que"; nomear o agente apenas se a fonte o informar +- "A decisão foi tomada" → "Decidiu-se"; usar "A equipe decidiu" somente se esse agente constar da fonte +- "Foram obtidos resultados positivos" → "Houve resultados positivos" +- "É considerado como referência" → "É referência" +- 3+ frases consecutivas em voz passiva + +**Alternativas naturais:** +- Voz ativa quando o sujeito estiver explícito: "A equipe entregou" em vez de "Foi entregue pela equipe" +- Passiva sintética (com "se"): "Identificaram-se 3 bugs" +- Indeterminação do sujeito somente quando a fonte também não identifica o agente +- Inversão simples: "Concluiu-se o relatório" em vez de "O relatório foi concluído" + +**Nota:** A passiva é legítima quando o agente é desconhecido ou irrelevante ("O servidor foi invadido"). O problema é usá-la quando existe um sujeito claro que deveria estar agindo. + +--- + +## Resumo: Lista rápida de verificação anti-IA em PT-BR + +| # | Padrão | Teste rápido | +|---|---|---| +| 1 | Gerundismo | Tem "vou estar + gerúndio"? | +| 2 | Conectivos arcaicos | Tem "ademais", "outrossim", "destarte" fora de contexto jurídico? | +| 3 | Abertura ENEM | Começa com "Em um mundo cada vez mais..."? | +| 4 | Ressalva burocrática | Tem "Vale ressaltar que" ou "Cumpre salientar"? | +| 5 | Formalidade deslocada | Tem "No que tange a" num e-mail de startup? | +| 6 | Oficialês | Tem "Venho por meio deste" fora de ofício? | +| 7 | Evitação de verbos simples | "É" virou "constitui"? "Tem" virou "há"? | +| 8 | Expressões infladas | Tem "contribui significativamente" sem número? | +| 9 | Transições mecânicas | Todo parágrafo começa com conectivo? | +| 10 | Purismo linguístico | Traduziu "feedback", "deploy", "churn"? | +| 11 | Colocação pronominal artificial | Tem "permite-nos", "trata-se de", mesóclise? | +| 12 | Voz passiva analítica | 3+ frases com "foi X pelo Y" em sequência? | + +**Se 3+ padrões aparecem no mesmo texto: alta probabilidade de geração por IA.** + +--- + +## Nota sobre Contexto + +Estes padrões são calibrados para texto profissional brasileiro nas áreas de: +- Tecnologia (dev, produto, infra) +- Startups e scale-ups +- Marketing digital +- Fintech e SaaS +- Comunicação corporativa moderna + +Em contextos onde a formalidade é esperada (petição judicial, artigo acadêmico publicado, comunicação diplomática), alguns desses padrões podem ser aceitáveis. A skill deve considerar o **perfil de voz** ativo antes de sinalizar. diff --git a/.github/skills/humanizar/references/padroes-linguagem.md b/.github/skills/humanizar/references/padroes-linguagem.md new file mode 100644 index 0000000..d8530cf --- /dev/null +++ b/.github/skills/humanizar/references/padroes-linguagem.md @@ -0,0 +1,209 @@ +# Padrões de Linguagem e Gramática (PT-BR) + +Padrões que denunciam texto gerado por IA no nível da escolha de palavras, construções gramaticais e estrutura de frase. Equivalentes brasileiros dos padrões 7-12 da skill original e de tropos catalogados em tropes.fyi. + +> **Preservação obrigatória:** em cada par “Antes/Depois”, o texto “Antes” é a única fonte de fatos, argumento, modalidade e posição autoral. A correção pode trocar construção, ordem e vocabulário, mas não pode criar experiência pessoal, número, fonte, causa, opinião ou certeza. Não remova informação apenas para quebrar um padrão. +> +> **Ajuste ao perfil:** os exemplos “Depois” usam registro neutro. Em perfis Acadêmico, Jornalístico ou Jurídico, preserve terminologia, atribuições e formalidade funcional; em perfis informais, use oralidade somente quando solicitada ou presente no original. A naturalidade vem da adequação, não da informalidade obrigatória. + +--- + +### 1. Vocabulário de IA em PT-BR + +**Palavras/expressões gatilho:** crucial, fundamental, cenário, landscape/panorama, no âmbito de, no bojo de, nesse diapasão, destarte, outrossim, mister se faz, em última análise, inegável, indubitavelmente, exponencialmente, de forma exponencial, disruptivo, paradigma, holístico, sinergia, alavancar, robusto, escalável, ecossistema (abstrato), jornada (figurativo), mergulhar em (delve into), desbloquear (unlock), desvendar insights (uncover insights), rica tapeçaria (rich tapestry), um testemunho de (a testament to), serve como um lembrete (serves as a reminder), na paisagem/cenário em constante evolução (ever-evolving landscape), navegar [complexidades] (navigate), ponta de lança (spearhead), sem costura/sem fricção (seamless), multifacetado, catalisador, orquestrar, delinear + +**Problema:** Modelos de linguagem em PT-BR abusam de um vocabulário pomposo e repetitivo que nenhum brasileiro usa em conversas normais — nem em textos profissionais. São equivalentes brasileiros de termos recorrentes em inglês, como "delve", "crucial" e "landscape". Muitos são traduções diretas de texto ruim em inglês que soam ainda mais artificiais em português — ninguém fala "rica tapeçaria" ou "desbloquear potencial" no Brasil. + +**Antes (IA):** +> É fundamental destacar que o ecossistema de startups brasileiro passa por um momento crucial de maturação. No âmbito da inovação, diversas empresas estão alavancando soluções disruptivas que prometem transformar o panorama do mercado de forma exponencial. + +**Depois (humano):** +> O mercado brasileiro de startups está amadurecendo. Nesse processo, várias empresas usam soluções que prometem transformar o mercado de forma exponencial. + +**Evitar em PT-BR:** +- "É fundamental/crucial/inegável destacar que..." +- "No âmbito/bojo/cenário de..." +- "Alavancar soluções robustas e escaláveis" +- "Mergulhar em/Mergulhar fundo em" → "explorar" / "ver de perto" / "entrar em" +- "Desbloquear o potencial" → "aproveitar" / "usar melhor" +- "Desvendar insights" → "descobrir" / "achar" / "perceber" +- "Rica tapeçaria de experiências" → cortar (sempre é enchimento) +- "É um testemunho de/da" → declarar diretamente o fato já apresentado, sem trocar indício por prova +- "Serve como um lembrete de que" → "lembra que" / cortar +- "No cenário/paisagem em constante evolução" → cortar o cenário, ir direto ao ponto +- "Navegar as complexidades de" → "lidar com"; use "resolver" somente se o original afirmar que houve solução +- "Sem costura" / "sem fricção" → "fluido" / "que funciona bem" / "redondo" +- "Catalisador de mudanças" → descrever a função causal somente se ela já estiver afirmada no original +- "Orquestrar a transformação" → "coordenar a transformação", quando essa ação constar do original + +--- + +### 2. Evitação de verbos de ligação + +**Palavras/expressões gatilho:** configura-se como, constitui, representa, figura como, posiciona-se como, desponta como, consolida-se como, atua como, funciona como, opera como, se estabelece como + +**Problema:** IA evita "é", "são" e "tem" como se fossem palavras proibidas. Substitui por construções rebuscadas que nenhum humano usaria numa conversa. Em PT-BR isso soa como texto jurídico ou dissertação de vestibular ruim. + +**Antes (IA):** +> O Nubank configura-se como a maior fintech da América Latina. A plataforma desponta como referência em experiência do usuário e consolida-se como alternativa aos bancos tradicionais. + +**Depois (humano):** +> O Nubank é a maior fintech da América Latina. A plataforma é referência em experiência do usuário e uma alternativa aos bancos tradicionais. + +**Evitar em PT-BR:** +- "Configura-se como / consolida-se como" +- "Desponta como referência em" +- "Posiciona-se como alternativa a" + +--- + +### 3. Paralelismos Negativos + +**Palavras/expressões gatilho:** não é apenas X, mas também Y; não se trata apenas de X, trata-se de Y; mais do que X, é Y; vai muito além de X; transcende o simples X; não é meramente X, é sobretudo Y + +**Problema:** Construção formulaica que infla importância artificialmente. Cria uma falsa dicotomia onde o autor finge rejeitar algo para depois abraçar algo maior — mas ambas as partes dizem a mesma coisa. Modelos de linguagem recorrem a essa forma porque ela simula profundidade sem exigir raciocínio real. + +**Antes (IA):** +> Produto não é apenas sobre funcionalidades. Não se trata meramente de entregar código — trata-se de resolver problemas reais. O PM vai muito além de escrever histórias de usuário; ele é, sobretudo, um tradutor entre negócio e tecnologia. + +**Depois (humano):** +> Produto envolve funcionalidades, entrega de código e resolução de problemas reais. O trabalho de PM inclui escrever histórias de usuário e traduzir necessidades entre negócio e tecnologia. + +**Evitar em PT-BR:** +- "Não se trata apenas de X, trata-se de Y" +- "Vai muito além de simplesmente..." +- "Mais do que X, é sobretudo Y" + +--- + +### 4. Abuso da regra de três + +**Palavras/expressões gatilho:** clareza, concisão e coerência; inovação, tecnologia e transformação; planejar, executar e medir; qualquer tríade rítmica com "e" antes do terceiro item; três adjetivos em sequência; três substantivos abstratos agrupados + +**Problema:** Modelos de linguagem agrupam ideias em trios porque a estrutura é retoricamente satisfatória — mas, quando aparece em todo parágrafo, vira cacoete. Textos naturais variam a quantidade de itens conforme o conteúdo. + +**Antes (IA):** +> Nossa cultura é baseada em transparência, colaboração e inovação. Buscamos agilidade, qualidade e impacto. Valorizamos autonomia, responsabilidade e aprendizado contínuo. + +**Depois (humano):** +> Transparência e colaboração orientam nossa cultura. A inovação também conta. Buscamos agilidade sem abrir mão da qualidade nem do impacto; valorizamos autonomia, responsabilidade e aprendizado contínuo. + +**Correção segura:** varie a estrutura sem eliminar nenhum item enumerado. Se a tríade for funcional ao perfil — por exemplo, em texto normativo ou apresentação — mantenha-a. + +**Evitar em PT-BR:** +- "Transparência, colaboração e inovação" +- "Planejamento, execução e controle" +- Três adjetivos ou substantivos abstratos em sequência, parágrafo após parágrafo + +--- + +### 5. Variação lexical forçada + +**Palavras/expressões gatilho:** ciclagem de sinônimos para o mesmo referente — ex: "a ferramenta" → "a solução" → "a plataforma" → "o sistema" → "o produto"; ou para pessoas: "o profissional" → "o colaborador" → "o especialista" → "o gestor" + +**Problema:** Modelos de linguagem tendem a ciclar sinônimos para evitar repetir a mesma palavra. Pessoas repetem palavras quando isso favorece a clareza — e a variação forçada confunde o leitor sobre se estamos falando da mesma coisa ou de coisas diferentes. + +**Antes (IA):** +> O Slack revolucionou a comunicação corporativa. A ferramenta de mensagens oferece canais temáticos. A plataforma colaborativa integra com mais de 2.000 aplicativos. A solução de produtividade é usada por 750 mil empresas. + +**Depois (humano):** +> O Slack revolucionou a comunicação corporativa e é usado por 750 mil empresas. Ele oferece canais temáticos e integração com mais de 2.000 aplicativos. + +**Evitar em PT-BR:** +- Ciclagem "ferramenta → solução → plataforma → sistema" +- "O profissional → o colaborador → o especialista" +- Trocar referente a cada frase quando poderia repetir ou usar pronome + +--- + +### 6. Falsas faixas + +**Palavras/expressões gatilho:** de X a Y; desde X até Y; abrangendo desde X até Y; vai de X a Y, passando por Z; cobre desde X até Y + +**Problema:** Modelos de linguagem criam faixas que soam amplas, mas não representam uma escala real. Os extremos escolhidos não são opostos significativos — são apenas dois exemplos aleatórios com "de...a..." no meio para simular abrangência. + +**Antes (IA):** +> O evento abordou desde inteligência artificial até gestão de pessoas, passando por marketing digital e produto. Os participantes variaram de estagiários a executivos, de startups em estágio inicial a corporações multinacionais. + +**Depois (humano):** +> O evento reuniu inteligência artificial, gestão de pessoas, marketing digital e produto. Participaram estagiários, executivos, pessoas de startups em estágio inicial e de corporações multinacionais. + +**Correção segura:** desdobre somente os extremos e exemplos já citados. Não complete a faixa com público, quantidade, preferência ou resultado presumido. + +**Evitar em PT-BR:** +- "Desde inteligência artificial até gestão de pessoas" +- "De estagiários a C-levels" +- "Abrangendo desde X até Y, passando por Z" + +--- + +### 7. Anáfora Abusiva + +**Palavras/expressões gatilho:** repetição do mesmo início em 3+ frases consecutivas — "É preciso...", "Precisamos...", "O futuro...", "A tecnologia...", "Esse é o momento de...", "Cada vez mais..." + +**Problema:** Anáfora é recurso retórico legítimo — em discursos, manifestos e poesia. Mas modelos de linguagem a usam como muleta estrutural em textos expositivos onde a repetição não serve a um propósito estilístico. Vira eco robótico. + +**Antes (IA):** +> Precisamos repensar a forma como contratamos. Precisamos questionar os processos legados. Precisamos ouvir mais e falar menos. Precisamos aceitar que o modelo antigo não funciona mais. Precisamos de coragem para mudar. + +**Depois (humano):** +> Precisamos repensar a forma de contratar e questionar os processos legados. O modelo antigo já não funciona; é hora de ouvir mais, falar menos e ter coragem para mudar. + +**Evitar em PT-BR:** +- "Precisamos..." repetido 3+ vezes +- "É hora de..." repetido em sequência +- "Cada vez mais..." como abertura de múltiplos parágrafos + +--- + +### 8. 'O X? Um Y.' (Perguntas Retóricas Auto-respondidas) + +**Palavras/expressões gatilho:** "O resultado? [Substantivo/frase dramática]." / "A resposta? [Afirmação categórica]." / "O segredo? [Revelação]." / "O problema? [Diagnóstico]." / "A solução? [Receita]." / "O impacto? [Superlativo]." + +**Problema:** Estrutura de pergunta e resposta curta que imita redação publicitária. Quando usada uma vez, pode funcionar. Quando modelos de linguagem a repetem em todo parágrafo, vira tique de redator de LinkedIn. O texto inteiro se torna uma sequência de falsas revelações. + +**Antes (IA):** +> O desafio? Escalar sem perder cultura. A solução? Contratar por valores, não por currículo. O resultado? Um time coeso que entrega 3x mais. O segredo? Autonomia com responsabilidade. O futuro? Uma empresa que não depende de um fundador. + +**Depois (humano):** +> O desafio é escalar sem perder a cultura. A solução proposta é contratar por valores, não por currículo. O resultado é um time coeso que entrega três vezes mais. O segredo é combinar autonomia com responsabilidade. O futuro imaginado é uma empresa que não depende do fundador. + +**Evitar em PT-BR:** +- "O resultado? Um time coeso." +- "O segredo? Consistência." +- "A resposta? Simplicidade." (especialmente em sequência) + +--- + +### 9. Decalques Sintáticos (Anglicismos Ocultos) + +**Palavras/expressões gatilho:** "endereçar um problema" (to address), "no final do dia" (at the end of the day), "performar" (to perform), "fazer sentido" (em excesso — to make sense), "estar no lugar" (to be in place), "correr um experimento" (to run an experiment), "levantar uma questão" (to raise a question), "colocar de outra forma" (to put it another way), "em termos de" (in terms of), "baseado em" sem sujeito (based on) + +**Problema:** Diferente dos estrangeirismos legítimos (deploy, feedback, sprint), os decalques são **estruturas do inglês traduzidas literalmente** que quebram a naturalidade sintática do português. A IA faz isso porque pensa em inglês e traduz — e o resultado parece texto de legendista apressado. Brasileiro de verdade usa o jargão inglês cru OU a expressão portuguesa equivalente — nunca a tradução literal da estrutura. + +**Antes (IA):** +> Precisamos endereçar esse problema antes do próximo sprint. No final do dia, o que importa é se a funcionalidade performa bem em produção. Temos todas as peças no lugar para correr esse experimento. Isso levanta uma questão importante em termos de escalabilidade. + +**Depois (humano):** +> Precisamos resolver esse problema antes do próximo sprint. No fim das contas, importa saber se a funcionalidade funciona bem em produção. Está tudo pronto para executar o experimento, o que traz uma questão sobre escalabilidade. + +**Evitar em PT-BR:** +- "Endereçar um problema" → "resolver" / "tratar" / "atacar" +- "No final do dia" → "no fim das contas" / "no fundo" / "na prática" +- "Performar" (como verbo intransitivo) → "funcionar" / "rodar" / "se sair" +- "Fazer sentido" em excesso (1-2x ok; 5x no texto = cacoete de tradução) +- "Estar no lugar" / "ter no lugar" → "estar pronto" / "ter configurado" +- "Correr um experimento" → "rodar" / "fazer" / "executar" +- "Levantar uma questão" → "trazer uma questão" / "trazer um ponto", preservando se o original expressa dúvida ou apenas assunto +- "Colocar de outra forma" → "dizendo de outro jeito" / "ou seja" +- "Em termos de" → "sobre" / "quanto a" / "de" (geralmente dá pra cortar) +- "Baseado em" sem sujeito ("Baseado nisso, decidimos...") → "Com base nisso" / "A partir disso" +- "Dar uma olhada em" (to take a look at) → "ver" / "conferir" / "olhar" +- "Estar na mesma página" (to be on the same page) → "estar alinhado" / "ter combinado" + +**Alternativas naturais:** +- Preferir o verbo português que o brasileiro usa na fala: "resolver", "rodar", "funcionar" +- Quando o jargão inglês for natural no perfil, ele pode ser mantido: “rodar o teste A/B” é preferível a “correr um experimento” +- Testar lendo em voz alta: se soa como legenda de série, é decalque + +**Nota:** “Fazer sentido” já se naturalizou no PT-BR. O problema é a repetição mecânica; avalie a frequência em relação ao tamanho, ao perfil e à voz do texto, sem tratá-la isoladamente como prova de autoria. diff --git a/.github/skills/humanizar/references/padroes-portugues-simplificado.md b/.github/skills/humanizar/references/padroes-portugues-simplificado.md new file mode 100644 index 0000000..965e5f0 --- /dev/null +++ b/.github/skills/humanizar/references/padroes-portugues-simplificado.md @@ -0,0 +1,437 @@ +# Padrões de Português Simplificado (PT-BR) + +Referência para o perfil de voz **📋 Português Simplificado**. Define operações de simplificação, substituições lexicais, métricas de referência e regras de escrita para produzir texto acessível sem infantilizar o conteúdo. + +> **Preservação obrigatória:** simplificar a forma nunca autoriza alterar fatos, argumento, modalidade ou posição autoral. Quando a simplificação criar ambiguidade factual ou técnica, manter a forma mais complexa e anotar no relatório. Toda operação está subordinada à **TRAVA FACTUAL**. +> +> **Princípio central:** clareza máxima com precisão intacta. Simplificar é tornar acessível, não é tornar raso. + +--- + +## 1. Fundamentos + +Este perfil sintetiza três fontes complementares: + +| Fonte | Escopo | Contribuição principal | +|---|---|---| +| **PorSimples** (NILC/USP, 2007–2010) | Simplificação textual automática para inclusão digital | 7 operações sintáticas + 2 níveis (Natural/Strong) + corpus alinhado | +| **Lei 15.263/2025** (Política Nacional de Linguagem Simples) | Comunicação pública acessível | 18 técnicas prescritivas para órgãos governamentais | +| **NILC-Metrix** (2008–2023) | 200 métricas de complexidade textual para PT-BR | Limites quantitativos e validação empírica | + +### Referências técnicas + +- Corpus PorSimplesSent: [github.com/sidleal/porsimplessent](https://github.com/sidleal/porsimplessent) (CC BY 4.0) +- SIMPLEX-PB (simplificação lexical): [github.com/nathanshartmann/SIMPLEX-PB](https://github.com/nathanshartmann/SIMPLEX-PB) +- NILC-Metrix (200 métricas): [github.com/sidleal/nilcmetrix](https://github.com/sidleal/nilcmetrix) (AGPL-3.0) +- Gov-Lang-BR (1.703 pares governo): Scalercio et al., ACL 2025 + +--- + +## 2. Operações de simplificação sintática + +Baseadas nas 10 operações do editor de anotação do PorSimples, consolidadas em 7 operações aplicáveis por um editor humano ou IA: + +### 2.1. Dividir períodos compostos + +Quebrar sentenças com mais de uma oração em frases independentes. Cada frase carrega uma ideia. + +**Antes:** +> O governo anunciou o programa ontem, que prevê investimentos de R$ 2 bilhões em infraestrutura urbana, beneficiando 15 milhões de pessoas em todo o país. + +**Depois:** +> O governo anunciou o programa ontem. O programa prevê investimentos de R$ 2 bilhões em infraestrutura urbana. O objetivo é beneficiar 15 milhões de pessoas em todo o país. + +**Regra:** se a frase tem mais de 25 palavras e contém vírgula seguida de pronome relativo ("que", "o qual", "onde") ou conjunção subordinativa, dividir. + +--- + +### 2.2. Converter voz passiva em voz ativa + +A voz ativa explicita quem faz a ação. Usar passiva somente quando o agente for irrelevante, desconhecido ou quando a ativa criar ambiguidade. + +**Antes:** +> A proposta foi aprovada pelo Senado após seis meses de tramitação. + +**Depois:** +> O Senado aprovou a proposta após seis meses de tramitação. + +**Antes (passiva justificada — manter):** +> O corpo foi encontrado na margem do rio. + +**Manter:** o agente é desconhecido; forçar ativa inventaria informação. + +--- + +### 2.3. Reordenar para SVO (Sujeito-Verbo-Objeto) + +Eliminar inversões sintáticas que dificultam a compreensão. A ordem canônica do português é SVO. + +**Antes:** +> Preocupa o comitê a possibilidade de atraso na entrega. + +**Depois:** +> A possibilidade de atraso na entrega preocupa o comitê. + +**Antes:** +> Aos beneficiários será garantido o acesso integral ao sistema. + +**Depois:** +> Os beneficiários terão acesso integral ao sistema. + +--- + +### 2.4. Substituir marcadores discursivos complexos por simples + +Trocar conectivos eruditos ou ambíguos por equivalentes diretos. + +| Complexo | Simples | +|---|---| +| não obstante | mas / porém | +| outrossim | também / além disso | +| destarte | por isso | +| em que pese | apesar de | +| haja vista | já que / porque | +| mister se faz | é preciso / é necessário | +| no que tange a | sobre / em relação a | +| em face de | por causa de / diante de | +| à medida que | conforme / enquanto | +| conquanto | embora | +| porquanto | porque | +| sem prejuízo de | mantendo / sem afetar | +| por intermédio de | por meio de / com | + +--- + +### 2.5. Eliminar apostos longos + +Apostos com mais de 5 palavras viram frase separada. Apostos curtos (até 5 palavras) podem permanecer. + +**Antes:** +> Sandra Aluísio, professora titular do Instituto de Ciências Matemáticas e de Computação da USP em São Carlos, coordenou o projeto PorSimples. + +**Depois:** +> Sandra Aluísio coordenou o projeto PorSimples. Ela é professora titular do Instituto de Ciências Matemáticas e de Computação da USP, em São Carlos. + +**Aposto curto — manter:** +> O NILC, centro de linguística computacional, desenvolveu as ferramentas. + +--- + +### 2.6. Substituir palavras raras por sinônimos frequentes + +Trocar vocabulário de baixa frequência por equivalentes comuns, sem perder precisão técnica. Termos de domínio devem ser explicados na primeira ocorrência, não eliminados. + +**Antes:** +> A implementação de políticas públicas que visem à mitigação dos impactos socioeconômicos configura-se como desafio premente. + +**Depois:** +> Criar políticas públicas para reduzir os impactos sociais e econômicos é um desafio urgente. + +**Termo técnico — explicar, não eliminar:** +> O churn (taxa de cancelamento de clientes) aumentou 15% no trimestre. + +--- + +### 2.7. Explicitar sujeitos ocultos e referências ambíguas + +Quando o sujeito oculto ou o pronome puder gerar dúvida sobre quem age, explicitar. + +**Antes:** +> O ministro reuniu-se com o secretário. Disse que o prazo seria ampliado. + +**Depois:** +> O ministro reuniu-se com o secretário. O ministro disse que o prazo seria ampliado. + +**Sujeito oculto claro — não forçar:** +> O sistema recebeu a atualização e já está funcionando. + +Neste caso, "já está funcionando" refere-se obviamente ao sistema. Explicitar seria redundante. + +--- + +## 3. Substituições lexicais + +Tabela de substituições frequentes baseadas no SIMPLEX-PB e na Lei 15.263. Usar somente quando o substituto não alterar a precisão do texto-fonte. + +### 3.1. Vocabulário burocrático → direto + +| Evitar | Preferir | +|---|---| +| implementar | fazer / criar / colocar em prática | +| operacionalizar | fazer funcionar / executar | +| viabilizar | permitir / tornar possível | +| otimizar | melhorar | +| priorizar | dar prioridade a / fazer primeiro | +| protocolar | registrar / entregar | +| subsidiar | dar informações para / apoiar | +| deliberar | decidir | +| aferir | medir / verificar | +| pleitear | pedir | +| ensejar | causar / dar origem a | +| auferir | receber / obter | +| prospectar | buscar / procurar | +| dirimir | resolver / esclarecer | +| perfectibilizar | melhorar | + +### 3.2. Locuções → formas diretas + +| Evitar | Preferir | +|---|---| +| no âmbito de | em | +| no que diz respeito a | sobre | +| no tocante a | sobre | +| com vistas a | para | +| a fim de que | para que | +| em virtude de | por causa de / porque | +| por intermédio de | por meio de / com | +| com o fito de | para | +| a nível de | em (ou cortar) | +| via de regra | geralmente | +| de forma que | então / por isso | +| tendo em vista que | porque / já que | +| em consonância com | de acordo com | +| face ao exposto | por isso | + +### 3.3. Adjetivos inflados → precisos + +| Evitar | Preferir | +|---|---| +| exponencial (figurativo) | grande / rápido / [número real] | +| robusto (figurativo) | completo / forte / confiável | +| holístico | completo / abrangente | +| disruptivo | novo / que muda tudo | +| inovador (vazio) | [descrever a novidade] | +| paradigmático | que muda o padrão / importante | +| multifacetado | com vários aspectos | +| emblemático | representativo / simbólico | + +### 3.4. Quando NÃO substituir + +- **Termos técnicos do domínio do leitor:** "API", "endpoint", "deploy", "churn" → manter em texto para desenvolvedores +- **Termos jurídicos em peças jurídicas:** "litisconsórcio", "agravo" → manter no perfil Jurídico +- **Termos médicos em textos para profissionais de saúde:** "dispneia", "hemodinâmica" → manter +- **Nomes próprios e siglas estabelecidas:** "INSS", "FGTS", "SUS" → manter (explicar na primeira ocorrência para público leigo) + +--- + +## 4. Métricas de referência + +Valores derivados do corpus PorSimples e das categorias do NILC-Metrix. Usar como guia, não como regra rígida — o gênero e o público-alvo modulam os limites. + +### 4.1. Comprimento de sentença (ASL — Average Sentence Length) + +| Nível PorSimples | ASL (palavras/frase) | Público-alvo | +|---|---|---| +| Original | ~20 | Letramento pleno | +| Natural | ~16 | Letramento básico | +| **Strong** | **~13** | **Letramento rudimentar** | + +**Meta para este perfil:** ASL entre 13 e 18 palavras, dependendo do domínio. +- Governo/saúde para público geral: ≤15 +- Documentação técnica para não-especialistas: ≤18 +- FAQ/onboarding: ≤13 + +### 4.2. Classificação de frases por comprimento (NILC-Metrix) + +| Classificação | Palavras | +|---|---| +| Curta | até 11 | +| Média | 11–12 | +| Longa | 12–15 | +| Muito longa | acima de 15 | + +**Meta:** maioria das frases entre curtas e longas. Minimizar frases "muito longas" (> 15 palavras). Se uma frase passar de 25, quase sempre pode ser dividida. + +### 4.3. Diversidade lexical (TTR — Type-Token Ratio) + +| Nível | TTR | +|---|---| +| Original | 0.19 | +| Natural | 0.17 | +| Strong | 0.16 | + +TTR mais baixo indica mais repetição de palavras — e no contexto de simplificação, isso é desejável. Não forçar sinônimos diferentes para a mesma coisa; repetição deliberada ajuda a compreensão. + +### 4.4. Complexidade sintática + +Indicadores do NILC-Metrix que sinalizam texto complexo: + +| Indicador | Texto complexo | Texto simples | +|---|---|---| +| Orações por sentença | > 2.3 | ≤ 1.5 | +| Palavras antes do verbo principal | > 1.5 | ≤ 1.0 | +| Proporção de orações não-SVO | > 0.33 | ≤ 0.15 | +| Proporção de orações relativas | > 0.13 | ≤ 0.05 | +| Proporção de orações subordinadas | > 0.44 | ≤ 0.20 | +| Proporção de voz passiva | alto | baixo | + +--- + +## 5. Regras de escrita + +15 regras prescritivas para o perfil Português Simplificado, inspiradas na Lei 15.263/2025 e nas operações do PorSimples. Cada regra tem prioridade (1 = sempre aplicar; 2 = aplicar quando possível; 3 = aplicar conforme o gênero). + +### Prioridade 1 — Sempre aplicar + +| # | Regra | +|---|---| +| R1 | **Uma ideia por frase.** Se a frase contém duas proposições independentes, dividir. | +| R2 | **Ordem direta (SVO).** Sujeito antes do verbo, verbo antes do complemento. Inverter somente com razão estilística forte. | +| R3 | **Voz ativa.** Converter passiva em ativa quando o agente for conhecido e relevante. | +| R4 | **Palavras comuns.** Preferir o sinônimo mais frequente quando não houver perda de precisão. | +| R5 | **Frases curtas.** Máximo de 25 palavras por frase. Meta: 13–18. | +| R6 | **Conectivos explícitos e simples.** "Porque", "por isso", "então", "mas", "e", "também". | + +### Prioridade 2 — Aplicar quando possível + +| # | Regra | +|---|---| +| R7 | **Explicar termos técnicos na primeira ocorrência.** Entre parênteses ou em frase curta seguinte. | +| R8 | **Evitar dupla negação.** "Não é impossível" → "É possível" (somente se não alterar a modalidade). | +| R9 | **Evitar subjuntivo desnecessário.** "Caso haja necessidade" → "Se for necessário". | +| R10 | **Listas para 3+ itens.** Se uma frase enumera três ou mais elementos, usar lista com marcadores. | +| R11 | **Sujeito explícito.** Quando o sujeito oculto puder causar ambiguidade, explicitar. | +| R12 | **Sem orações intercaladas longas.** Apostos > 5 palavras viram frase nova. | + +### Prioridade 3 — Conforme o gênero + +| # | Regra | +|---|---| +| R13 | **Sem estrangeirismos fora do domínio.** Manter termos consagrados no domínio do leitor; remover os que o público-alvo não conhece. | +| R14 | **Repetição deliberada.** Repetir o substantivo em vez de usar pronome quando o referente estiver distante (> 2 frases). | +| R15 | **Estrutura visual.** Usar subtítulos, tabelas e destaques para organizar informação densa. Não converter toda prosa em bullets. | + +--- + +## 6. Domínios de aplicação + +### 6.1. Governo e comunicação pública + +Foco: cidadão comum, letramento variado. Seguir integralmente a Lei 15.263. + +**Antes:** +> Os beneficiários do programa deverão comparecer à unidade de atendimento munidos de documento de identificação com foto, comprovante de residência atualizado e número de inscrição no Cadastro Único, sob pena de indeferimento do requerimento. + +**Depois:** +> Se você é beneficiário do programa, vá até a unidade de atendimento. Leve: +> - Documento com foto (RG ou CNH) +> - Comprovante de endereço atualizado +> - Número do Cadastro Único (CadÚnico) +> +> Sem esses documentos, o pedido será negado. + +--- + +### 6.2. Saúde para público leigo + +Foco: pacientes e familiares. Evitar jargão médico; quando necessário, explicar. + +**Antes:** +> A administração de anti-inflamatórios não esteroidais pode ocasionar efeitos adversos gastrointestinais, incluindo dispepsia, náuseas e, em casos mais graves, úlcera péptica. + +**Depois:** +> Anti-inflamatórios como ibuprofeno podem causar problemas no estômago. Os mais comuns são: +> - Dor ou queimação no estômago (dispepsia) +> - Enjoo (náusea) +> +> Em casos mais graves, podem causar feridas no estômago (úlcera). Tome com alimento para reduzir o risco. + +--- + +### 6.3. Documentação técnica para não-especialistas + +Foco: usuários finais de software, manuais de produto, onboarding. + +**Antes:** +> Para efetuar a configuração do webhook, o usuário deverá acessar o painel administrativo, navegar até a seção de integrações e inserir a URL do endpoint que receberá as notificações, certificando-se de que o servidor de destino esteja configurado para aceitar requisições POST com payload em formato JSON. + +**Depois:** +> Para configurar o webhook: +> +> 1. Acesse o painel administrativo +> 2. Vá até **Integrações** +> 3. Cole a URL do seu endpoint (o endereço que vai receber as notificações) +> +> Certifique-se de que seu servidor aceita requisições POST com dados em JSON. + +--- + +### 6.4. Educação e material didático + +Foco: estudantes. Ordem clara, exemplos concretos, progressão do simples ao complexo. + +**Antes:** +> A fotossíntese consiste em um processo bioquímico mediante o qual organismos autotróficos fotossintetizantes convertem energia luminosa em energia química, utilizando dióxido de carbono e água como reagentes e produzindo glicose e oxigênio como produtos. + +**Depois:** +> Fotossíntese é o processo que as plantas usam para produzir seu próprio alimento. +> +> Como funciona: +> - A planta absorve luz do sol, água e gás carbônico (CO₂) +> - Com esses ingredientes, ela produz glicose (açúcar) e oxigênio (O₂) +> +> A glicose alimenta a planta. O oxigênio é liberado no ar — o mesmo que a gente respira. + +--- + +## 7. Integração com TRAVA FACTUAL + +### 7.1. Quando NÃO simplificar + +| Situação | Razão | Ação | +|---|---|---| +| Simplificação altera relação causal | "A causou B" pode virar "A e B aconteceram" | Manter a forma complexa | +| Simplificação remove qualificação necessária | "Possivelmente eficaz" virar "eficaz" | Manter modalidade | +| Simplificação elimina exceção | "Exceto em casos de X" desaparece ao dividir | Preservar a exceção em frase separada | +| Termo técnico é o nome oficial | Substituir "litisconsórcio" por "várias partes" em petição | Manter o termo; explicar se público for leigo | +| Dado numérico exige contexto adjacente | "15% a mais que 2023" perde sentido separado de "2023" | Manter na mesma frase | + +### 7.2. Marcação de conflito + +Quando a simplificação ideal conflitar com a preservação factual: + +``` +⚠️ CONFLITO SIMPLIFICAÇÃO × TRAVA FACTUAL: [descrever o trecho]. +Mantida forma complexa para preservar [precisão / modalidade / causalidade / exceção]. +``` + +### 7.3. Simplificação de citações e dados + +- **Citações diretas:** nunca simplificar. Preservar ipsis litteris. +- **Dados numéricos:** manter todos. Pode-se acrescentar explicação entre parênteses se a fonte permitir. +- **Nomes próprios e siglas:** manter. Expandir sigla na primeira ocorrência. + +--- + +## 8. Diferença entre perfis + +| Aspecto | 📋 Português Simplificado | 🧑‍🏫 Didático | 📰 Jornalístico | +|---|---|---|---| +| Foco principal | Acessibilidade por simplificação formal | Explicação com exemplos | Informação factual concisa | +| Frases | ≤ 25 palavras, meta 13-18 | Variadas, com ritmo pedagógico | Curtas, ordem direta | +| Vocabulário | Comum; técnico explicado | Acessível mas com progressão | Preciso, sem adjetivação | +| Listas | Sim, para 3+ itens | Sim, com passos numerados | Não (exceto infográfico) | +| Exemplos | Somente se existirem na fonte | Incentivados (mas não inventados) | Não cabem | +| Repetição | Deliberada para clareza | Permitida em reforço | Evitada (concisão) | +| Estrutura visual | Subtítulos, tabelas, bullets | Pergunta → explicação → exemplo | Lide + pirâmide invertida | + +--- + +## 9. Checklist de verificação + +Usar ao final da reescrita no perfil Português Simplificado: + +| # | Verificação | ✓/✗ | +|---|---|---| +| 1 | Todas as frases têm ≤ 25 palavras? | | +| 2 | Maioria das frases está em ordem SVO? | | +| 3 | Voz passiva aparece somente onde justificada? | | +| 4 | Termos técnicos foram explicados na primeira ocorrência? | | +| 5 | Conectivos são simples e explícitos? | | +| 6 | Enumerações de 3+ itens estão em lista? | | +| 7 | Apostos longos foram transformados em frase separada? | | +| 8 | Sujeitos ambíguos foram explicitados? | | +| 9 | Nenhuma informação foi removida ou adicionada? (TRAVA FACTUAL) | | +| 10 | Modalidade preservada? ("pode" não virou "vai", "talvez" não sumiu) | | +| 11 | Citações diretas permanecem intactas? | | +| 12 | Dados numéricos completos e no contexto correto? | | diff --git a/.github/skills/humanizar/references/padroes-tom.md b/.github/skills/humanizar/references/padroes-tom.md new file mode 100644 index 0000000..262f777 --- /dev/null +++ b/.github/skills/humanizar/references/padroes-tom.md @@ -0,0 +1,260 @@ +# Padrões de Tom — Detecção e Correção + +Padrões que denunciam tom artificial, servil ou performático em texto PT-BR. Cada padrão inclui gatilhos, exemplos brasileiros (tecnologia, startups, marketing e desenvolvimento) e alternativas editoriais. + +> **Preservação obrigatória:** em cada par “Antes/Depois”, o texto “Antes” é a única fonte de fatos, argumento, modalidade e posição autoral. A correção pode cortar muletas, reorganizar e simplificar, mas não pode criar experiência pessoal, número, fonte, causa, opinião ou certeza. Se faltar sustentação, preserve a dúvida ou aponte a lacuna. +> +> **Ajuste ao perfil:** os exemplos “Depois” usam registro neutro. Em perfil formal, preserve qualificações, atribuições e impessoalidade funcional; em perfil informal, use coloquialidade somente quando solicitada ou já presente; em nenhum perfil invente primeira pessoa para produzir “voz”. + +--- + +### 1. Tom servil / bajulador + +**Palavras/expressões gatilho:** "Ótima pergunta!", "Com certeza!", "Excelente observação!", "Espero ter ajudado!", "Fico feliz em ajudar!", "Obrigado por compartilhar!" + +**Problema:** Elogios genéricos ao interlocutor antes de responder. Ninguém fala assim em texto profissional brasileiro — é marca registrada de chatbot tentando agradar. + +**Antes (IA):** +> Ótima pergunta! O deploy contínuo com GitHub Actions é realmente uma abordagem fascinante. Com certeza posso te ajudar com isso! Vamos lá: primeiro, você precisa configurar o fluxo de trabalho em YAML... + +**Depois (humano):** +> Para configurar deploy contínuo com GitHub Actions, comece pelo fluxo de trabalho em YAML. + +**Evitar em PT-BR:** +- "Ótima pergunta!" / "Excelente ponto!" +- "Com certeza!" / "Absolutamente!" +- "Espero ter ajudado!" / "Fico feliz em contribuir!" + +--- + +### 2. Avisos sobre limite de conhecimento + +**Palavras/expressões gatilho:** "Até onde sei...", "Com base nas informações disponíveis...", "Até minha última atualização...", "Não posso confirmar com certeza, mas...", "De acordo com minhas informações limitadas..." + +**Problema:** Expõe a natureza de máquina do autor com fórmulas sobre “última atualização”. O defeito está na fórmula, não na cautela: incerteza, limitação temporal e necessidade de confirmação devem ser preservadas quando fazem parte do conteúdo. + +**Antes (IA):** +> Até onde sei, o Next.js 15 introduziu Server Actions como recurso estável. No entanto, informações mais recentes podem ter alterado esse cenário. Com base nas informações disponíveis até minha última atualização, a recomendação é usar App Router. + +**Depois (humano):** +> As informações disponíveis indicam que o Next.js 15 tornou Server Actions estável e recomendam o App Router. Isso pode ter mudado; confirme antes de aplicar. + +**Correção segura:** retirar a autorreferência do assistente sem transformar hipótese em fato, recomendação em obrigação ou informação possivelmente desatualizada em certeza atual. + +**Evitar em PT-BR:** +- "Até minha última atualização..." +- "Com base nas informações disponíveis..." +- "Não tenho informações suficientes para afirmar com certeza, mas..." + +--- + +### 3. Comunicação Colaborativa Residual + +**Palavras/expressões gatilho:** "Aqui está um exemplo...", "Posso te ajudar com...", "Vou te mostrar como...", "Segue abaixo...", "Fique à vontade para perguntar mais!" + +**Problema:** O texto conserva vestígios de interação assistente-usuário. Parece resposta de suporte, não texto autoral. Quando publicado como artigo ou post, denuncia imediatamente a origem. + +**Antes (IA):** +> Aqui está um exemplo de como implementar autenticação com JWT no Express. Vou te mostrar passo a passo como configurar o middleware. Fique à vontade para adaptar conforme suas necessidades! + +**Depois (humano):** +> A seguir, a configuração passo a passo do middleware de autenticação com JWT no Express. + +**Evitar em PT-BR:** +- "Aqui está um..." / "Segue abaixo..." +- "Posso te ajudar com..." / "Vou te mostrar..." +- "Fique à vontade para..." / "Não hesite em perguntar!" + +--- + +### 4. Cautela excessiva + +**Palavras/expressões gatilho:** "Pode-se argumentar que...", "É possível que...", "Talvez seja o caso de...", "Alguns especialistas sugerem...", "Aparentemente...", "De certa forma..." + +**Problema:** O acúmulo de qualificadores pode esconder a proposição principal. A correção remove apenas redundância; não aumenta a certeza, não apaga exceções e não converte atribuição vaga em opinião do autor. + +**Antes (IA):** +> Pode-se argumentar que microsserviços nem sempre são a melhor escolha para startups em estágio inicial. Alguns especialistas sugerem que, em determinados contextos, uma arquitetura monolítica pode ser potencialmente mais adequada para equipes menores. + +**Depois (humano):** +> Há o argumento de que microsserviços nem sempre são a melhor escolha para startups em estágio inicial. Alguns especialistas sugerem que, em certos contextos, uma arquitetura monolítica pode ser mais adequada para equipes menores. + +**Correção segura:** mantenha verbos modais como “pode” e “parece” quando expressam incerteza real. Se a atribuição não estiver identificada, sinalize a lacuna em vez de assumir a afirmação como própria. + +**Evitar em PT-BR:** +- "Pode-se argumentar que..." / "É possível que..." +- "Em determinados contextos..." / "Potencialmente..." +- "Alguns especialistas sugerem..." / "De certa forma..." + +--- + +### 5. Conclusões Genéricas Positivas + +**Palavras/expressões gatilho:** "O futuro é promissor", "Tempos empolgantes", "As possibilidades são infinitas", "O potencial é ilimitado", "Estamos apenas no começo", "O melhor ainda está por vir" + +**Problema:** Encerramento vazio que repete otimismo sem desenvolver o argumento. A saída pode terminar quando o conteúdo termina; não precisa acrescentar posição, provocação ou previsão. + +**Antes (IA):** +> O futuro da inteligência artificial no marketing digital é promissor. Estamos vivendo tempos empolgantes, e as possibilidades são verdadeiramente infinitas para profissionais que souberem se adaptar a essa nova realidade. + +**Depois (humano):** +> A inteligência artificial pode ampliar as possibilidades no marketing digital para profissionais que se adaptarem. O texto vê esse futuro com otimismo. + +**Evitar em PT-BR:** +- "O futuro é promissor" / "Tempos empolgantes nos aguardam" +- "As possibilidades são infinitas" / "O potencial é ilimitado" +- "Estamos apenas no começo dessa jornada" + +--- + +### 6. Frases de Enchimento + +**Palavras/expressões gatilho:** "É importante destacar que", "Vale ressaltar que", "Cabe mencionar que", "Convém observar que", "Não se pode ignorar o fato de que", "É fundamental compreender que" + +**Problema:** Adicionam zero informação. São muletas usadas antes de chegar ao ponto. A correção remove a chamada de atenção e preserva a proposição, sem acrescentar justificativa, exemplo ou grau de certeza. + +**Antes (IA):** +> É importante destacar que o uso de TypeScript em projetos React tem crescido significativamente. Vale ressaltar que essa tendência reflete a busca por maior segurança de tipos. Cabe mencionar que empresas como Vercel e Stripe já adotaram TypeScript como padrão. + +**Depois (humano):** +> O uso de TypeScript em projetos React tem crescido significativamente, e essa tendência reflete a busca por mais segurança de tipos. Vercel e Stripe já adotaram TypeScript como padrão. + +**Evitar em PT-BR:** +- "É importante destacar que" / "Vale ressaltar que" +- "Cabe mencionar que" / "Convém observar que" +- "Não se pode ignorar o fato de que" / "É fundamental compreender que" + +--- + +### 7. Falso suspense (“Eis a questão”) + +**Palavras/expressões gatilho:** "Eis a questão:", "O ponto central é:", "Mas aqui está o detalhe:", "A grande sacada é:", "O plot twist é:", "E aqui mora o perigo:" + +**Problema:** Cria falso suspense antes de um ponto banal. Promete revelação dramática e entrega obviedade. Humanos não anunciam que vão dizer algo interessante — simplesmente dizem. + +**Antes (IA):** +> Muitas startups investem em growth hacking sem ter product-market fit. Elas contratam profissionais de growth, gastam com anúncios e otimizam funis. Mas eis a questão: sem um produto que as pessoas realmente querem, nenhuma tática de crescimento vai funcionar. + +**Depois (humano):** +> Muitas startups investem em growth hacking antes de ter product-market fit: contratam profissionais de growth, gastam com anúncios e otimizam funis. Sem um produto que as pessoas realmente queiram, nenhuma dessas táticas vai funcionar. + +**Evitar em PT-BR:** +- "Eis a questão:" / "Eis o ponto:" +- "Mas aqui está o detalhe:" / "A grande sacada é:" +- "E aqui mora o perigo:" / "O plot twist é:" + +--- + +### 8. Vulnerabilidade Falsa + +**Palavras/expressões gatilho:** "Sendo honesto aqui...", "Confesso que...", "Vou ser vulnerável:", "Não vou mentir:", "Se eu for sincero...", "Admito que..." + +**Problema:** Autoconsciência performática que anuncia a vulnerabilidade antes de apresentar o conteúdo. Remover esse ritual não autoriza tornar o relato mais dramático nem inventar episódio, consequência ou aprendizado. + +**Antes (IA):** +> Confesso que, como desenvolvedor, nem sempre segui boas práticas. Sendo honesto aqui: houve momentos em que priorizei velocidade sobre qualidade. E sim, admito que isso me ensinou lições valiosas sobre a importância do código limpo. + +**Depois (humano):** +> Como desenvolvedor, nem sempre segui boas práticas: houve momentos em que priorizei velocidade sobre qualidade. Isso me ensinou a importância do código limpo. + +**Variação por perfil:** mantenha a primeira pessoa apenas porque ela existe no original. Em perfil formal, “Em alguns momentos, priorizei...” é suficiente; em perfil informal, a oralidade pode aumentar, mas os acontecimentos não. + +**Evitar em PT-BR:** +- "Confesso que..." / "Sendo honesto aqui..." +- "Vou ser vulnerável:" / "Não vou mentir:" +- "Admito que isso me ensinou lições valiosas" + +--- + +### 9. "A Verdade É Simples" + +**Palavras/expressões gatilho:** "A verdade é simples:", "A realidade é mais simples do que parece", "No fundo, tudo se resume a...", "A resposta é surpreendentemente direta:", "Na prática, é menos complicado do que parece" + +**Problema:** Declara obviedade sem provar. Finge simplificar algo complexo, mas apenas repete a superfície. A correção expõe diretamente a proposição já presente; não cria demonstração, recomendação ou opinião para torná-la mais convincente. + +**Antes (IA):** +> Muitos fundadores se perdem em métodos de priorização complexos, matrizes RICE e metodologias ágeis elaboradas. Mas a verdade é simples: o que importa é conversar com seus usuários e construir o que eles precisam. + +**Depois (humano):** +> Muitos fundadores se perdem em métodos de priorização, matrizes RICE e metodologias ágeis. O ponto defendido é conversar com os usuários e construir o que eles precisam. + +**Evitar em PT-BR:** +- "A verdade é simples:" / "A realidade é mais simples do que parece" +- "No fundo, tudo se resume a..." / "A resposta é surpreendentemente direta:" +- "É menos complicado do que você imagina" + +--- + +### 10. Inflação grandiosa de importância + +**Palavras/expressões gatilho:** "Isso vai redefinir fundamentalmente...", "Uma mudança de paradigma", "Revolucionário", "Transformar completamente", "O jogo mudou para sempre", "Nunca mais será o mesmo" + +**Problema:** Tudo recebe importância máxima por meio de expressões dramáticas redundantes. A correção simplifica a formulação sem enfraquecer a tese, trocar previsão por dúvida ou inserir contraponto que o autor não apresentou. + +**Antes (IA):** +> O surgimento de agentes de IA autônomos representa uma mudança de paradigma que vai redefinir fundamentalmente a forma como desenvolvemos software. Estamos testemunhando uma revolução que transformará completamente a indústria de tecnologia como a conhecemos. + +**Depois (humano):** +> Agentes autônomos de inteligência artificial vão mudar profundamente o desenvolvimento de software e a indústria de tecnologia. + +**Evitar em PT-BR:** +- "Mudança de paradigma" / "Redefinir fundamentalmente" +- "Revolução" / "Transformar completamente" +- "O jogo mudou para sempre" / "Nunca mais será o mesmo" + +--- + +### 11. Anúncio de explicação (“Vamos analisar”) + +**Palavras/expressões gatilho:** "Vamos analisar:", "Vamos destrinchar:", "Vamos entender passo a passo:", "Vamos explorar cada aspecto:", "Vamos mergulhar nesse assunto:", "Quebrando em partes:" + +**Problema:** Voz pedagógica condescendente. Pressupõe que o leitor precisa ser guiado quando o contexto pede exposição direta. Remova o anúncio e comece pelo conteúdo que já existe; se o “Antes” não trouxer a explicação, não a complete. + +**Antes (IA):** +> Vamos analisar os três pilares de uma estratégia de conteúdo eficaz. Primeiro, vamos explorar a pesquisa de palavras-chave. Em seguida, vamos mergulhar na criação de grupos temáticos. Por fim, vamos entender como medir resultados. + +**Depois (humano):** +> Uma estratégia de conteúdo eficaz reúne três pilares: pesquisa de palavras-chave, criação de grupos temáticos e medição de resultados. + +**Evitar em PT-BR:** +- "Vamos analisar:" / "Vamos destrinchar:" +- "Vamos explorar cada aspecto:" / "Vamos mergulhar nesse assunto:" +- "Vamos entender passo a passo:" / "Quebrando em partes:" + +--- + +### 12. Rótulos Conceituais Inventados + +**Palavras/expressões gatilho:** "o paradoxo da [X]", "a armadilha da [X]", "o deficit de [X]", "a falácia do [X]", "o efeito [X]", "a síndrome de [X]" + +**Problema:** Inventar termos compostos e apresentá-los como conceitos estabelecidos. IA cria rótulos pseudo-acadêmicos para parecer profunda ("o paradoxo da supervisão", "a armadilha da aceleração"). Humanos nomeiam fenômenos com cautela — ou reconhecem que estão cunhando um termo. + +**Antes (IA):** +> Muitas empresas caem no que podemos chamar de "paradoxo da automação" — quanto mais automatizam, mais dependem de intervenção humana para lidar com os casos excepcionais. Esse "déficit de supervisão escalável" é o verdadeiro gargalo da transformação digital. + +**Depois (humano):** +> Quanto mais essas empresas automatizam, mais dependem de intervenção humana nos casos excepcionais. O texto apresenta essa dificuldade de supervisão como gargalo da transformação digital. + +**Evitar em PT-BR:** +- "O paradoxo da [X]" / "A armadilha da [X]" +- "O deficit de [X]" / "A falácia do [X]" +- "O que podemos chamar de..." (seguido de termo inventado) + +--- + +### 13. "Imagine um Mundo Onde..." + +**Palavras/expressões gatilho:** "Imagine um mundo onde...", "Imagine se...", "Pense num cenário em que...", "E se eu te dissesse que...", "Visualize um futuro onde...", "Feche os olhos e imagine..." + +**Problema:** Convite futurista clichê que serve de abertura genérica. A correção deve expor a hipótese diretamente, sem adicionar produtos atuais, dados ou limitações que não estejam no texto de origem. + +**Antes (IA):** +> Imagine um mundo onde todo desenvolvedor tem um assistente de IA que entende perfeitamente o contexto da sua base de código. Imagine se cada pull request fosse revisada instantaneamente com retorno preciso e acionável. Esse mundo não está tão distante quanto você pensa. + +**Depois (humano):** +> Um assistente de inteligência artificial que entenda todo o contexto do código e revise cada pull request imediatamente, com retorno preciso, ainda é uma possibilidade futura. O texto sustenta que ela pode estar próxima. + +**Evitar em PT-BR:** +- "Imagine um mundo onde..." / "Imagine se..." +- "E se eu te dissesse que..." / "Visualize um futuro onde..." +- "Esse mundo não está tão distante" / "O futuro já chegou" diff --git a/.github/skills/loop-architect/LICENSE b/.github/skills/loop-architect/LICENSE new file mode 100644 index 0000000..1ca1585 --- /dev/null +++ b/.github/skills/loop-architect/LICENSE @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2026 Fabricio Telles (ft.ia.br) + +Based on Looper (https://github.com/ksimback/looper) +Copyright (c) 2026 Kevin Simback + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.github/skills/loop-architect/SKILL.md b/.github/skills/loop-architect/SKILL.md new file mode 100644 index 0000000..28bf3b8 --- /dev/null +++ b/.github/skills/loop-architect/SKILL.md @@ -0,0 +1,233 @@ +--- +name: loop-architect +description: > + Design well-structured agent loops with best-practice coaching and cross-model + review gates before you run them. Use when the user wants to design, build, or + set up an agent loop, iterative agent workflow, self-review loop, LLM-as-judge + loop, multi-model council, reviewer/judge gate, or goal-driven looping process. + Guides goal refinement, typed verification criteria, reviewer/judge selection, + privacy boundaries, termination guards, and observability, then emits a + RUN_IN_SESSION.md handoff prompt plus portable loop.yaml, loop.resolved.json, + LOOP.md, and run-loop.py. +metadata: + author: https://ft.ia.br + version: "1.0" + date: 2026-06-25 + repository: https://github.com/fabricioctelles/skills + license: MIT + original_project: https://github.com/ksimback/looper + original_author: Kevin Simback (@ksimback) + attribution: > + Reinterpretation of Looper (MIT License) by Kevin Simback, adapted for + Kiro CLI with native /goal, subagent, and review loop integration. + category: code-scaffolding-and-templates +--- + +# Loop Architect + +A loop design coach for Kiro CLI. Interviews you, critiques your design against +built-in best-practice rubrics, wires in cross-model reviewers or judges, shows +the loop as an ASCII flow preview, and writes portable artifacts you can run +immediately with `/goal` or later with the Python runner. + +> Based on [Looper](https://github.com/ksimback/looper) by Kevin Simback, MIT License. +> Adapted for Kiro CLI by ft.ia.br. + +## Why This Exists + +Kiro CLI ships `/goal` (autonomous loop with self-verification) and subagents +(parallel pipelines with review loops). These **execute** a loop. Loop Architect +helps you **design** one worth executing — with a coached goal, typed +verification, a cross-model gate, and explicit termination guards. + +| | `/goal` | Subagent pipeline | **Loop Architect** | +|---|---|---|---| +| Layer | execution | execution | **design (pre-flight)** | +| Coaches your goal | no | no | **yes** | +| Typed verification | no | no | **yes (programmatic / judge / human)** | +| Reviewer model | same model | configurable | **different model, by default** | +| Portable artifact | no | no | **loop.yaml + resolved spec** | +| Runs the loop | **yes** | **yes** | **yes, via handoff** | + +## Workflow + +1. Resolve the target path from the user. Default: `./loop-architect-output`. If + the target contains an existing `loop.yaml`, treat as edit/resume. + +2. Load the relevant rubric only when entering that stage: + - Goal stage: `references/goal-rubric.md` + - Verification stage: `references/verification-rubric.md` + - Council stage: `references/council-rubric.md` + - Control stage: `references/control-rubric.md` + - Model detection: `references/model-detection.md` + +3. Interview in seven stages: goal, verification, host model, council, + gates/control, confirmation flow preview, emit/run option. In the control + stage, cover execution boundary, isolation, no-progress signals, state, and + run logging. + +4. Critique each stage before accepting it. Prefer concrete alternatives over + vague warnings. Push weak goals toward outcome, scope, context, and done + state. Push weak verification toward programmatic checks first, then judge + rubrics, then human signoff. + +5. Keep reviewer and judge roles distinct. A reviewer writes notes. A judge + returns a structured verdict. `revise_until_clean` must name a judge member + or `human` as `verdict_source`. + +6. Require multiple termination guards: `max_iterations`, a revision cap on + each gate, a no-progress stop, and either a budget cap or an explicit human + stop point. + +7. Before any cross-vendor council member is selected, state what context will + leave the user's machine, which CLI receives it, which redaction globs apply, + and that both execution paths require first-send consent. + +8. Show an ASCII flow preview and ask for confirmation before final emission. + +9. Emit these files into the target: + - `loop.yaml` + - `loop.resolved.json` + - `LOOP.md` + - `RUN_IN_SESSION.md` + - `run-loop.py` + - `loop-workspace/` + - `README.md` + +10. After writing `loop.yaml`, compile it: + ```bash + python3 ~/.kiro/skills/loop-architect/scripts/looper.py compile \ + /loop.yaml \ + --out /loop.resolved.json \ + --render /LOOP.md \ + --session-prompt /RUN_IN_SESSION.md + ``` + +11. Ask whether the user wants to run the loop now. If yes: + - **Easy path**: Follow `RUN_IN_SESSION.md` directly, or suggest a `/goal` + one-liner derived from the `definition_of_done`. + - **Subagent path**: If the council uses a model with `review_loop` + capability, offer to execute via a subagent pipeline with native review + loops. + - **External path**: Explain that `run-loop.py` is available for running + later or outside the session. + +## Execution Paths + +### Path 1: `/goal` (simplest) + +When the loop is straightforward and the host is the current Kiro session: + +``` +/goal --max 12 +``` + +This uses Kiro's native self-verification loop. No cross-model review, but +fast and zero-config. + +### Path 2: Subagent review pipeline (recommended) + +When a cross-model reviewer is needed and the host has `subagent` capability: + +``` +Implement the loop following RUN_IN_SESSION.md. Use a subagent as reviewer +with trigger "NEEDS_CHANGES" and max 3 iterations per gate. +``` + +This leverages Kiro's native `loop_to` mechanism for the plan and delivery +gates. + +### Path 3: External Python runner (advanced) + +```bash +python3 ./loop-architect-output/run-loop.py +``` + +For scheduled runs, CI integration, or when you need strict budget enforcement. + +## File Rules + +- Write argv arrays, never shell command strings, for all model invocations. +- Do not write API keys, tokens, or credentials into any emitted file. +- Default redaction globs: `.env`, `.env.*`, `secrets/**`, `**/*.key`. +- Keep `loop.yaml` human-readable and commented. +- Keep `RUN_IN_SESSION.md` as the default/easy execution handoff. +- Copy `templates/run-loop.py` exactly unless the user asks to edit it. + +## Helper Scripts + +Detect model CLIs: +```bash +python3 ~/.kiro/skills/loop-architect/scripts/looper.py detect-models --write +``` + +Register a custom CLI: +```bash +python3 ~/.kiro/skills/loop-architect/scripts/looper.py register-model \ + --invoke kiro-cli chat --trust-all-tools -p --authed +``` + +Compile and render: +```bash +python3 ~/.kiro/skills/loop-architect/scripts/looper.py compile /loop.yaml \ + --out /loop.resolved.json \ + --render /LOOP.md \ + --session-prompt /RUN_IN_SESSION.md +``` + +## Confirmation Flow Preview + +```text ++--------------------------------+ +| 1. Goal + context | +| read sources | ++--------------------------------+ + | + v ++--------------------------------+ +| 2. Draft plan.md | +| state -> state.json | ++--------------------------------+ + | + v ++--------------------------------+ +| 3. Plan gate | +| verdict: reviewer-1 | ++--------------------------------+ + | needs work -> revise <= 3 -> step 2 + | pass + v ++--------------------------------+ +| 4. Write delivery-N.md | +| log -> run-log.md | ++--------------------------------+ + | + v ++--------------------------------+ +| 5. Delivery gate | +| verdict: reviewer-1 | ++--------------------------------+ + | needs work -> revise <= 3 -> step 4 + | pass + v ++--------------------------------+ +| 6. Final output | +| all gates clean | ++--------------------------------+ + +Stops: pass gates | max 12 iterations | no progress x2 | budget 30m, $5.0 +``` + +## Emit Checklist + +- The goal has a clear outcome, scope boundary, context sources, and done state. +- Verification criteria are typed as `programmatic`, `judge`, or `human`. +- At least one criterion is not purely vibe-based. +- Each `revise_until_clean` gate has a valid `verdict_source`. +- Every external invocation is an argv array with a timeout. +- Cross-vendor egress is scoped, redacted, and consent-gated. +- `loop_control` has iteration, revision, no-progress, and budget caps. +- Execution boundary and isolation are explicit. +- Observability names a `run-log.md` and `state.json` path. +- Compiled artifacts (`loop.resolved.json`, `LOOP.md`, `RUN_IN_SESSION.md`) + pass validation before handoff. diff --git a/.github/skills/loop-architect/examples/ai-workflow-mapping/LOOP.md b/.github/skills/loop-architect/examples/ai-workflow-mapping/LOOP.md new file mode 100644 index 0000000..6c75330 --- /dev/null +++ b/.github/skills/loop-architect/examples/ai-workflow-mapping/LOOP.md @@ -0,0 +1,86 @@ +# ai-workflow-mapping + +Map a customer's manual workflow into an agent-ready process. + +## Goal + +Produce an agent workflow map that converts the process notes into a stepwise design with tool calls, model responsibilities, and human checkpoints. + +## Definition of Done + +A LOOP.md-style workflow map exists, every step has an owner, input, output, and checkpoint decision where needed, and there are no TBDs. + +## Verification + +- `required-sections` (programmatic) +- `covers-goal` (judge) + +## Council + +- `reviewer-1`: judge via claude (default) + +## Gates + +- Plan gate: revise_until_clean +- Delivery gate: revise_until_clean + +## Loop Control + +- Max iterations: 12 +- Budget: `{"tokens": 2000000, "usd": 5.0, "wall_clock_min": 30}` +- No-progress: `{"action": "stop", "max_stalled_iterations": 2, "signals": ["same blocking issue repeats", "delivery artifact has no material change", "verifier output is unchanged"]}` + +## Execution Boundary + +- Mode: `in_session` +- Isolation: `current_workspace` +- Side effects: `{"duplicate_action_check": true, "requires_approval": true}` + +## Observability + +- State file: `state.json` +- Run log: `run-log.md` +- Checkpoint granularity: `gate` + +## Flow Preview + +```text ++--------------------------------+ +| 1. Goal + context | +| read sources | ++--------------------------------+ + | + v ++--------------------------------+ +| 2. Draft plan.md | +| state -> state.json | ++--------------------------------+ + | + v ++--------------------------------+ +| 3. Plan gate | +| verdict: reviewer-1 | ++--------------------------------+ + | needs work -> revise <= 3 -> step 2 + | pass + v ++--------------------------------+ +| 4. Write delivery-N.md | +| log -> run-log.md | ++--------------------------------+ + | + v ++--------------------------------+ +| 5. Delivery gate | +| verdict: reviewer-1 | ++--------------------------------+ + | needs work -> revise <= 3 -> step 4 + | pass + v ++--------------------------------+ +| 6. Final output | +| all gates clean | ++--------------------------------+ + +Stops: pass gates | max 12 iterations | no progress x2 | budget 30m, $5.0, 2000000 tokens +``` diff --git a/.github/skills/loop-architect/examples/ai-workflow-mapping/README.md b/.github/skills/loop-architect/examples/ai-workflow-mapping/README.md new file mode 100644 index 0000000..e2452e0 --- /dev/null +++ b/.github/skills/loop-architect/examples/ai-workflow-mapping/README.md @@ -0,0 +1,19 @@ +# AI Workflow Mapping Example + +This example shows the Looper artifact shape for mapping customer process notes +into an agent-ready workflow. + +Compile after editing: + +```bash +python ../../scripts/looper.py compile loop.yaml --out loop.resolved.json --render LOOP.md --session-prompt RUN_IN_SESSION.md +``` + +The easy path is to ask the current LLM session to follow `RUN_IN_SESSION.md`. + +Use the Python runner only when you want to run the loop outside the LLM +session, after reviewing model invocations and privacy egress: + +```bash +python run-loop.py +``` diff --git a/.github/skills/loop-architect/examples/ai-workflow-mapping/RUN_IN_SESSION.md b/.github/skills/loop-architect/examples/ai-workflow-mapping/RUN_IN_SESSION.md new file mode 100644 index 0000000..5752683 --- /dev/null +++ b/.github/skills/loop-architect/examples/ai-workflow-mapping/RUN_IN_SESSION.md @@ -0,0 +1,108 @@ +# Run `ai-workflow-mapping` In This Session + +Use this prompt when the user wants to run the Looper-designed loop in the current LLM session. +This is the default/easy execution path. The Python runner is the advanced path for running later or outside the session. + +## Operator Instructions + +You are executing a Looper-designed loop in this current session. +Follow the resolved spec below, write handoff files into the workspace, and enforce the caps manually. +Do not use `run-loop.py` unless the user explicitly asks for the advanced external runner. + +1. Create the workspace directory if it does not exist. +2. Read the context sources before drafting the plan. +3. Draft `plan.md` in the workspace. +4. Run the plan gate. Apply programmatic checks when available. For judge criteria, use the configured judge only after consent for any non-local egress; otherwise ask the user to approve a human/current-session substitute. +5. Revise until the gate passes or `max_revisions` is reached. +6. Produce `delivery-N.md` in the workspace. +7. Run the delivery gate after each delivery. +8. Stop when all delivery criteria pass, a cap is reached, or the user stops the loop. +9. Keep `state.json` current with status, iteration, last gate, consent, and blockers. +10. Append a compact entry to `run-log.md` after every context read, model call, check, gate verdict, revision, blocker, and stop decision. +11. Compare each blocker against the previous blocker. If the same blocker repeats for the configured no-progress window, stop or ask for the configured human checkpoint instead of revising again. +12. Treat token and USD budgets as operator limits in this session: if exact accounting is unavailable, stop and ask before continuing when the loop appears likely to exceed them. + +## Files + +- Source spec: `loop.yaml` +- Human summary: `LOOP.md` +- Resolved spec: `loop.resolved.json` +- Workspace: `./loop-workspace` +- State file: `state.json` +- Run log: `run-log.md` + +## Goal + +Produce an agent workflow map that converts the process notes into a stepwise design with tool calls, model responsibilities, and human checkpoints. + +## Definition Of Done + +A LOOP.md-style workflow map exists, every step has an owner, input, output, and checkpoint decision where needed, and there are no TBDs. + +## Context Sources + +- Read file `./inputs/process-notes.md` + +## Verification Criteria + +- `required-sections` programmatic: run `["python", "scripts/check-loop-doc.py", "loop-workspace/delivery-1.md"]` and expect `exit_zero` +- `covers-goal` judge rubric: Every part of the goal statement is addressed. Each workflow step has an owner, required input, output artifact, and human checkpoint where business judgment is needed. No step depends on information the loop never gathers. There are no unresolved TBDs. + + +## Council + +- `reviewer-1` judge via `["claude", "-p"]` (non-local; timeout 600s) + +## Gates + +### plan_gate + +- When: `after_plan` +- Policy: `revise_until_clean` +- Verdict source: `reviewer-1` +- Criteria: `covers-goal` +- Max revisions: `3` + +### delivery_gate + +- When: `after_each_delivery` +- Policy: `revise_until_clean` +- Verdict source: `reviewer-1` +- Criteria: `required-sections, covers-goal` +- Max revisions: `3` + +## Loop Control + +- Max iterations: `12` +- Budget: `{"tokens": 2000000, "usd": 5.0, "wall_clock_min": 30}` +- No-progress: `{"action": "stop", "max_stalled_iterations": 2, "signals": ["same blocking issue repeats", "delivery artifact has no material change", "verifier output is unchanged"]}` +- Human checkpoints: `none` +- Stop conditions: + - all deliveries pass their gate clean + - max_iterations reached + - same blocker repeats for 2 iterations + - any budget cap exceeded + +## Execution Boundary + +- Mode: `in_session` +- Isolation: `current_workspace` +- Side effects: `{"duplicate_action_check": true, "requires_approval": true}` + +If the loop needs scheduled runs, child-agent lifecycle management, concurrency control, or restart-safe step retries, stop and tell the user this Looper spec should be handed to a durable orchestrator. + +## Observability + +- State file: `state.json` +- Run log: `run-log.md` +- Checkpoint granularity: `gate` + +Use `state.json` for the latest resumable status and `run-log.md` for the append-only history of what happened. + +## Privacy + +- Before sending `plan, deliveries` to `reviewer-1`, confirm consent and apply redactions `.env, .env.*, secrets/**, **/*.key`. + +## Start Now + +If the user asked to run now, begin at step 1 under Operator Instructions and keep going until a stop condition is reached. diff --git a/.github/skills/loop-architect/examples/ai-workflow-mapping/inputs/process-notes.md b/.github/skills/loop-architect/examples/ai-workflow-mapping/inputs/process-notes.md new file mode 100644 index 0000000..52fd9c3 --- /dev/null +++ b/.github/skills/loop-architect/examples/ai-workflow-mapping/inputs/process-notes.md @@ -0,0 +1,14 @@ +# Process Notes + +The team currently turns customer process interviews into workflow maps by +reading notes, identifying handoffs, drafting a diagram, and asking a lead +consultant to check whether each step has an owner. + +The loop should produce a map with: + +- each process step +- owner type: tool, model, or human +- required input for the step +- output artifact for the step +- explicit human checkpoint when business judgment is needed + diff --git a/.github/skills/loop-architect/examples/ai-workflow-mapping/loop.resolved.json b/.github/skills/loop-architect/examples/ai-workflow-mapping/loop.resolved.json new file mode 100644 index 0000000..d7e56dc --- /dev/null +++ b/.github/skills/loop-architect/examples/ai-workflow-mapping/loop.resolved.json @@ -0,0 +1,194 @@ +{ + "$schema": "https://github.com/ksimback/looper/schema/loop.resolved.v1.json", + "compiled_at": "2026-06-19T07:09:26+00:00", + "council": [ + { + "cli": "claude", + "id": "reviewer-1", + "invoke": [ + "claude", + "-p" + ], + "local": false, + "model": "default", + "role": "judge", + "scope": [ + "plan", + "delivery" + ], + "timeout_sec": 600 + } + ], + "council_by_id": { + "reviewer-1": { + "cli": "claude", + "id": "reviewer-1", + "invoke": [ + "claude", + "-p" + ], + "local": false, + "model": "default", + "role": "judge", + "scope": [ + "plan", + "delivery" + ], + "timeout_sec": 600 + } + }, + "criteria_by_id": { + "covers-goal": { + "id": "covers-goal", + "rubric": "Every part of the goal statement is addressed. Each workflow step has an owner, required input, output artifact, and human checkpoint where business judgment is needed. No step depends on information the loop never gathers. There are no unresolved TBDs.\n", + "type": "judge" + }, + "required-sections": { + "check": [ + "python", + "scripts/check-loop-doc.py", + "loop-workspace/delivery-1.md" + ], + "expect": "exit_zero", + "id": "required-sections", + "type": "programmatic" + } + }, + "execution": { + "isolation": "current_workspace", + "mode": "in_session", + "side_effects": { + "duplicate_action_check": true, + "requires_approval": true + } + }, + "gates": { + "delivery_gate": { + "criteria": [ + "required-sections", + "covers-goal" + ], + "max_revisions": 3, + "members": [ + "reviewer-1" + ], + "verdict_policy": "revise_until_clean", + "verdict_source": "reviewer-1", + "when": "after_each_delivery" + }, + "plan_gate": { + "criteria": [ + "covers-goal" + ], + "max_revisions": 3, + "members": [ + "reviewer-1" + ], + "verdict_policy": "revise_until_clean", + "verdict_source": "reviewer-1", + "when": "after_plan" + } + }, + "goal": { + "context_sources": [ + { + "file": "./inputs/process-notes.md" + } + ], + "definition_of_done": "A LOOP.md-style workflow map exists, every step has an owner, input, output, and checkpoint decision where needed, and there are no TBDs.\n", + "statement": "Produce an agent workflow map that converts the process notes into a stepwise design with tool calls, model responsibilities, and human checkpoints.\n", + "verification": [ + { + "check": [ + "python", + "scripts/check-loop-doc.py", + "loop-workspace/delivery-1.md" + ], + "expect": "exit_zero", + "id": "required-sections", + "type": "programmatic" + }, + { + "id": "covers-goal", + "rubric": "Every part of the goal statement is addressed. Each workflow step has an owner, required input, output artifact, and human checkpoint where business judgment is needed. No step depends on information the loop never gathers. There are no unresolved TBDs.\n", + "type": "judge" + } + ] + }, + "host": { + "cli": "codex", + "invoke": [ + "codex", + "exec", + "--model", + "gpt-5" + ], + "model": "gpt-5", + "timeout_sec": 600 + }, + "loop_control": { + "budget": { + "tokens": 2000000, + "usd": 5.0, + "wall_clock_min": 30 + }, + "human_checkpoints": [], + "max_iterations": 12, + "no_progress": { + "action": "stop", + "max_stalled_iterations": 2, + "signals": [ + "same blocking issue repeats", + "delivery artifact has no material change", + "verifier output is unchanged" + ] + }, + "stop_conditions": [ + "all deliveries pass their gate clean", + "max_iterations reached", + "same blocker repeats for 2 iterations", + "any budget cap exceeded" + ] + }, + "meta": { + "author": "ksimback", + "created": "2026-06-18", + "description": "Map a customer's manual workflow into an agent-ready process.", + "name": "ai-workflow-mapping" + }, + "observability": { + "checkpoint_granularity": "gate", + "run_log": "run-log.md", + "state_file": "state.json" + }, + "privacy": { + "egress": [ + { + "consent": "required", + "redact": [ + ".env", + ".env.*", + "secrets/**", + "**/*.key" + ], + "sends": [ + "plan", + "deliveries" + ], + "to": "reviewer-1" + } + ] + }, + "source": "C:\\Users\\kevin\\looper\\examples\\ai-workflow-mapping\\loop.yaml", + "version": 1, + "workspace": { + "dir": "./loop-workspace", + "layout": [ + "plan.md", + "delivery-{n}.md", + "review-{n}.md", + "state.json", + "run-log.md" + ] + } +} diff --git a/.github/skills/loop-architect/examples/ai-workflow-mapping/loop.yaml b/.github/skills/loop-architect/examples/ai-workflow-mapping/loop.yaml new file mode 100644 index 0000000..0d84782 --- /dev/null +++ b/.github/skills/loop-architect/examples/ai-workflow-mapping/loop.yaml @@ -0,0 +1,104 @@ +version: 1 +meta: + name: ai-workflow-mapping + description: Map a customer's manual workflow into an agent-ready process. + author: ksimback + created: 2026-06-18 + +goal: + statement: > + Produce an agent workflow map that converts the process notes into a + stepwise design with tool calls, model responsibilities, and human + checkpoints. + context_sources: + - file: ./inputs/process-notes.md + definition_of_done: > + A LOOP.md-style workflow map exists, every step has an owner, input, + output, and checkpoint decision where needed, and there are no TBDs. + verification: + - id: required-sections + type: programmatic + check: ["python", "scripts/check-loop-doc.py", "loop-workspace/delivery-1.md"] + expect: exit_zero + - id: covers-goal + type: judge + rubric: > + Every part of the goal statement is addressed. Each workflow step has + an owner, required input, output artifact, and human checkpoint where + business judgment is needed. No step depends on information the loop + never gathers. There are no unresolved TBDs. + +host: + cli: codex + model: gpt-5 + invoke: ["codex", "exec", "--model", "gpt-5"] + timeout_sec: 600 + +council: + - id: reviewer-1 + role: judge + cli: claude + model: default + invoke: ["claude", "-p"] + timeout_sec: 600 + scope: [plan, delivery] + local: false + +gates: + plan_gate: + when: after_plan + members: [reviewer-1] + verdict_policy: revise_until_clean + verdict_source: reviewer-1 + criteria: [covers-goal] + max_revisions: 3 + delivery_gate: + when: after_each_delivery + members: [reviewer-1] + verdict_policy: revise_until_clean + verdict_source: reviewer-1 + criteria: [required-sections, covers-goal] + max_revisions: 3 + +loop_control: + max_iterations: 12 + budget: + usd: 5.0 + tokens: 2000000 + wall_clock_min: 30 + no_progress: + max_stalled_iterations: 2 + signals: + - same blocking issue repeats + - delivery artifact has no material change + - verifier output is unchanged + action: stop + human_checkpoints: [] + stop_conditions: + - all deliveries pass their gate clean + - max_iterations reached + - same blocker repeats for 2 iterations + - any budget cap exceeded + +execution: + mode: in_session + isolation: current_workspace + side_effects: + requires_approval: true + duplicate_action_check: true + +observability: + state_file: state.json + run_log: run-log.md + checkpoint_granularity: gate + +privacy: + egress: + - to: reviewer-1 + sends: [plan, deliveries] + redact: [".env", ".env.*", "secrets/**", "**/*.key"] + consent: required + +workspace: + dir: ./loop-workspace + layout: [plan.md, "delivery-{n}.md", "review-{n}.md", state.json, run-log.md] diff --git a/.github/skills/loop-architect/examples/ai-workflow-mapping/run-loop.py b/.github/skills/loop-architect/examples/ai-workflow-mapping/run-loop.py new file mode 100644 index 0000000..4f62b26 --- /dev/null +++ b/.github/skills/loop-architect/examples/ai-workflow-mapping/run-loop.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +"""Example runner wrapper that uses the root template.""" + +from __future__ import annotations + +from pathlib import Path +import runpy + + +ROOT = Path(__file__).resolve().parents[2] +runpy.run_path(str(ROOT / "templates" / "run-loop.py"), run_name="__main__") + diff --git a/.github/skills/loop-architect/examples/ai-workflow-mapping/scripts/check-loop-doc.py b/.github/skills/loop-architect/examples/ai-workflow-mapping/scripts/check-loop-doc.py new file mode 100644 index 0000000..90dc0a7 --- /dev/null +++ b/.github/skills/loop-architect/examples/ai-workflow-mapping/scripts/check-loop-doc.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Check that a generated workflow map has the expected sections.""" + +from __future__ import annotations + +from pathlib import Path +import sys + + +REQUIRED = ["Owner", "Input", "Output", "Checkpoint"] + + +def main() -> int: + if len(sys.argv) != 2: + print("usage: check-loop-doc.py ", file=sys.stderr) + return 2 + path = Path(sys.argv[1]) + if not path.exists(): + print(f"missing file: {path}", file=sys.stderr) + return 1 + text = path.read_text(encoding="utf-8") + missing = [item for item in REQUIRED if item not in text] + if missing: + print(f"missing required text: {', '.join(missing)}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/.github/skills/loop-architect/references/control-rubric.md b/.github/skills/loop-architect/references/control-rubric.md new file mode 100644 index 0000000..31881d8 --- /dev/null +++ b/.github/skills/loop-architect/references/control-rubric.md @@ -0,0 +1,60 @@ +# Control Rubric + +Use this when setting gates, iteration caps, budgets, and stop conditions. + +## Required Guards + +- `loop_control.max_iterations` +- `gates.*.max_revisions` +- `loop_control.no_progress.max_stalled_iterations` +- At least one wall-clock, token, or USD budget cap when external models run. + The generated Python runner enforces wall-clock caps directly; token and USD + caps are advisory unless the chosen model CLI exposes accounting that the + loop operator wires in separately. +- A stop condition that describes success. +- A stop condition that describes no-progress or repeated failure. + +## Good Gate Design + +- Plan gate runs before delivery work. +- Delivery gate runs after each delivery artifact. +- Programmatic checks run before judge calls when possible. +- Human checkpoints sit at high-leverage points, usually after plan approval or + before external egress. +- Resume happens at gate boundaries unless the user explicitly needs finer + granularity. + +## Execution Boundary + +- Name where the loop is allowed to modify files: current workspace, branch, + worktree, throwaway directory, or an external orchestrator workspace. +- Identify actions with side effects: pushes, PR comments, Slack messages, + deploys, file deletes, database writes, or vendor sends. +- Decide whether side-effecting actions require approval, idempotency notes, or + duplicate-action checks. +- If the loop may run on a schedule or in parallel, call out the need for an + external orchestrator with concurrency controls. + +## Failure Behavior + +- Stop immediately when a hard cap is reached. +- Write the latest state to `loop-workspace/state.json`. +- Append each meaningful step, decision, check result, and blocker to + `loop-workspace/run-log.md`. +- Preserve review notes even when the gate fails. +- Stop or ask the human when the same blocker repeats for the configured + no-progress window. +- Do not let the host keep revising forever. + +## Anti-Patterns + +- No maximum iteration count. +- A judge gate with no judge. +- A budget cap in prose but not in `loop_control`. +- No no-progress detector. +- A loop that can send duplicate external notifications or repeat destructive + actions after restart. +- Scheduled or multi-agent work with no durable orchestrator or concurrency + story. +- Human signoff required but no checkpoint. +- Stop conditions that require subjective self-satisfaction. diff --git a/.github/skills/loop-architect/references/council-rubric.md b/.github/skills/loop-architect/references/council-rubric.md new file mode 100644 index 0000000..90a7053 --- /dev/null +++ b/.github/skills/loop-architect/references/council-rubric.md @@ -0,0 +1,43 @@ +# Council Rubric + +Use this when selecting reviewers and judges. + +## Roles + +`reviewer` +: Gives notes only. It may improve quality, but it cannot declare a gate clean. + +`judge` +: Gives a structured verdict. It can be used as a gate `verdict_source`. + +## Selection Guidance + +- Prefer a different model family from the host for blind-spot coverage. +- Prefer local models such as `ollama` when privacy matters more than judgment + quality. +- Prefer a judge for gates that must block progress. +- Prefer a reviewer for brainstorming, adversarial notes, or tone critique + where a deterministic pass/fail would be fake precision. +- Keep council scope small: `plan`, `delivery`, or specific paths. + +## Gate Rule + +`verdict_policy: revise_until_clean` requires `verdict_source` to be either a +judge member or `human`. A reviewer-only gate can use `fixed_passes`, but it +cannot honestly claim clean. + +## Judge Rubric Tips + +- Name the artifact being judged. +- Name the exact criteria IDs. +- Ask for blocking issues, not general commentary. +- Require the fenced JSON verdict first or last. +- Keep the judge prompt short enough that the artifact, not the instruction + wrapper, dominates the context. + +## Privacy Notes + +Cross-vendor review can send project context to another CLI and vendor account. +Always name the destination, scope what it receives, apply redaction globs, and +require consent before the first send. + diff --git a/.github/skills/loop-architect/references/goal-rubric.md b/.github/skills/loop-architect/references/goal-rubric.md new file mode 100644 index 0000000..c7f8fb5 --- /dev/null +++ b/.github/skills/loop-architect/references/goal-rubric.md @@ -0,0 +1,42 @@ +# Goal Rubric + +Use this when shaping the user's loop goal. + +## Good Goal Shape + +- Names the concrete outcome, not only the activity. +- Defines the artifact or state that proves the loop finished. +- Sets scope boundaries: included work, excluded work, and maximum depth. +- Names context sources the host must gather instead of assumptions it may make. +- Identifies the user, customer, system, or reviewer who will consume the result. + +## Critique Prompts + +- What would count as done if two competent agents disagreed? +- Which terms are subjective and need a measurable proxy? +- What context must be read before the host drafts a plan? +- What is explicitly out of scope for this loop? +- Can the goal be split into plan, delivery, and verification artifacts? + +## Anti-Patterns + +- "Improve the project" without a target artifact. +- "Make it good" without criteria. +- "Research X" without the decision the research supports. +- Goals where success depends on information the loop never gathers. +- Goals that require endless polishing with no stop condition. + +## Better Examples + +Weak: "Make our onboarding better." + +Better: "Produce a 5-step onboarding workflow map for new enterprise users, +with each step assigned to a product surface, email, human owner, or missing +capability, and with no unresolved TBDs." + +Weak: "Fix the flaky tests." + +Better: "Identify and patch the root cause of the checkout test flake, prove it +with 20 local repeats or a CI rerun, and leave a short note explaining the +failure mode and the verification evidence." + diff --git a/.github/skills/loop-architect/references/model-detection.md b/.github/skills/loop-architect/references/model-detection.md new file mode 100644 index 0000000..77d40c1 --- /dev/null +++ b/.github/skills/loop-architect/references/model-detection.md @@ -0,0 +1,98 @@ +# Model Detection and Privacy Notes + +Loop-architect detection is intentionally dumb and transparent. It stores +invocation metadata only, never credentials. + +## Registry + +Default registry path: + +```text +~/.loop-architect/models.json +``` + +Registry entries should look like: + +```json +{ + "kiro": { + "cli": "kiro-cli", + "invoke": ["kiro-cli", "chat", "--trust-all-tools", "-p"], + "probe": ["kiro-cli", "--version"], + "available": true, + "authed": true, + "local": false, + "capabilities": { + "headless": true, + "goal": true, + "subagent": true, + "review_loop": true + } + }, + "claude": { + "cli": "claude", + "invoke": ["claude", "-p"], + "probe": ["claude", "--version"], + "available": true, + "authed": true, + "local": false, + "capabilities": { + "headless": true, + "goal": true, + "subagent": false, + "review_loop": false + } + } +} +``` + +## Capabilities + +`headless` +: The CLI accepts a prompt via stdin/argument and returns output via stdout + without interactive prompts. Required for use as host or judge in the + external Python runner. + +`goal` +: The CLI supports a `/goal` command that runs an autonomous loop with + self-verification. When present, RUN_IN_SESSION.md can emit a `/goal` + one-liner as an alternative execution path. + +`subagent` +: The CLI can spawn isolated sub-agents with their own context. When present, + the council can use native subagent review loops instead of shelling out. + +`review_loop` +: The CLI supports iterative review loops with trigger-based feedback (e.g. + Kiro's `loop_to` with `NEEDS_CHANGES` trigger). Enables native cross-model + review without the external runner. + +## Kiro CLI Specifics + +- Headless mode requires `--trust-all-tools` or the session halts waiting for + tool approval. +- Full invoke pattern: `["kiro-cli", "chat", "--trust-all-tools", "-p"]` +- The `/goal --max N` command provides native loop execution with configurable + iteration limits (default 5). +- Subagent review loops use a `trigger` string (e.g. `NEEDS_CHANGES`) and + `max_iterations` cap. + +## `authed` Semantics + +`authed` means the basic probe command exited cleanly. It is a convenience +signal, not a guarantee that a future paid model call will succeed. + +## Default Redactions + +- `.env` +- `.env.*` +- `secrets/**` +- `**/*.key` + +Add project-specific globs for customer data, private transcripts, or internal +design docs before sending anything to a non-local council member. + +## Local Model UX + +Surface `ollama` as the privacy-preserving option when present. It may be lower +quality than frontier hosted models, but it keeps council review in-house. diff --git a/.github/skills/loop-architect/references/verification-rubric.md b/.github/skills/loop-architect/references/verification-rubric.md new file mode 100644 index 0000000..7ee80be --- /dev/null +++ b/.github/skills/loop-architect/references/verification-rubric.md @@ -0,0 +1,59 @@ +# Verification Rubric + +Use this when converting the user's definition of done into typed criteria. + +## Taxonomy + +`programmatic` +: A command or deterministic check returns pass/fail. Use this whenever +possible. Examples: tests, build, lint, schema validation, snapshot comparison, +or an extraction script that checks required headings. + +`judge` +: A model scores a rubric and returns a structured verdict. Use this for +semantic quality that cannot be cheaply checked by code. The rubric must be +specific enough that a different model can apply it consistently. + +`human` +: A person must sign off. Use this for taste, business judgment, private +knowledge, legal risk, or decisions where the user is the true authority. + +## Required Fields + +- Every criterion needs `id` and `type`. +- `programmatic` needs `check` as an argv array and `expect`. +- `judge` needs `rubric`. +- `human` needs `prompt`. + +## Strong Criteria + +- Check one thing at a time. +- Say what failure means. +- Prefer deterministic checks before model judgment. +- Make judge rubrics observable against artifacts the judge receives. +- Avoid relying on the host model to grade its own work. + +## Anti-Patterns + +- All criteria are judge or human criteria when tests or schema checks exist. +- "No errors thrown" as the only success criterion. +- Criteria that require hidden context not sent to the judge. +- Rubrics like "high quality" or "comprehensive" without dimensions. +- Programmatic checks written as shell strings instead of argv arrays. + +## Structured Judge Contract + +Judges should return a fenced JSON object: + +```json +{ + "verdict": "pass", + "blocking_issues": [], + "confidence": 0.86, + "notes": "The artifact satisfies the rubric." +} +``` + +Valid verdicts are `pass` and `revise`. If output cannot be parsed, the runner +will treat it as `revise` with a warning. + diff --git a/.github/skills/loop-architect/schemas/loop.resolved.v1.schema.json b/.github/skills/loop-architect/schemas/loop.resolved.v1.schema.json new file mode 100644 index 0000000..91ab16f --- /dev/null +++ b/.github/skills/loop-architect/schemas/loop.resolved.v1.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/ksimback/looper/schema/loop.resolved.v1.json", + "title": "Looper resolved spec v1", + "allOf": [ + { "$ref": "./loop.v1.schema.json" }, + { + "type": "object", + "required": ["compiled_at", "source", "criteria_by_id", "council_by_id"], + "properties": { + "compiled_at": { "type": "string" }, + "source": { "type": "string" }, + "criteria_by_id": { + "type": "object", + "additionalProperties": { "$ref": "./loop.v1.schema.json#/$defs/criterion" } + }, + "council_by_id": { + "type": "object", + "additionalProperties": true + } + } + } + ] +} + diff --git a/.github/skills/loop-architect/schemas/loop.v1.schema.json b/.github/skills/loop-architect/schemas/loop.v1.schema.json new file mode 100644 index 0000000..19d9f91 --- /dev/null +++ b/.github/skills/loop-architect/schemas/loop.v1.schema.json @@ -0,0 +1,190 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/ksimback/looper/schema/loop.v1.json", + "title": "Looper authoring spec v1", + "type": "object", + "required": ["version", "goal", "host", "gates", "loop_control", "workspace"], + "properties": { + "version": { "const": 1 }, + "meta": { + "type": "object", + "additionalProperties": true + }, + "goal": { + "type": "object", + "required": ["statement", "definition_of_done", "verification"], + "properties": { + "statement": { "type": "string", "minLength": 1 }, + "definition_of_done": { "type": "string", "minLength": 1 }, + "context_sources": { + "type": "array", + "items": { + "type": "object", + "anyOf": [ + { "required": ["file"] }, + { "required": ["cmd"] } + ] + } + }, + "verification": { + "type": "array", + "items": { "$ref": "#/$defs/criterion" } + } + }, + "additionalProperties": true + }, + "host": { "$ref": "#/$defs/model_invocation" }, + "council": { + "type": "array", + "items": { + "allOf": [ + { "$ref": "#/$defs/model_invocation" }, + { + "type": "object", + "required": ["id", "role"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "role": { "enum": ["reviewer", "judge"] }, + "scope": { + "type": "array", + "items": { "type": "string" } + }, + "local": { "type": "boolean" } + } + } + ] + } + }, + "gates": { + "type": "object", + "required": ["plan_gate", "delivery_gate"], + "properties": { + "plan_gate": { "$ref": "#/$defs/gate" }, + "delivery_gate": { "$ref": "#/$defs/gate" } + } + }, + "loop_control": { + "type": "object", + "required": ["max_iterations"], + "properties": { + "max_iterations": { "type": "integer", "minimum": 1 }, + "budget": { "type": "object" }, + "no_progress": { + "type": "object", + "properties": { + "max_stalled_iterations": { "type": "integer", "minimum": 1 }, + "signals": { + "type": "array", + "items": { "type": "string" } + }, + "action": { "enum": ["stop", "human_checkpoint"] } + }, + "additionalProperties": true + }, + "human_checkpoints": { + "type": "array", + "items": { "type": "string" } + }, + "stop_conditions": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "execution": { + "type": "object", + "properties": { + "mode": { "enum": ["in_session", "external_runner", "orchestrated"] }, + "isolation": { "enum": ["current_workspace", "branch", "worktree", "sandbox"] }, + "side_effects": { "type": "object" } + }, + "additionalProperties": true + }, + "observability": { + "type": "object", + "properties": { + "state_file": { "type": "string" }, + "run_log": { "type": "string" }, + "checkpoint_granularity": { "enum": ["gate", "step"] } + }, + "additionalProperties": true + }, + "privacy": { "type": "object" }, + "workspace": { + "type": "object", + "required": ["dir"], + "properties": { + "dir": { "type": "string", "minLength": 1 }, + "layout": { + "type": "array", + "items": { "type": "string" } + } + } + } + }, + "$defs": { + "argv": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + }, + "model_invocation": { + "type": "object", + "required": ["cli", "invoke"], + "properties": { + "cli": { "type": "string" }, + "model": { "type": "string" }, + "invoke": { "$ref": "#/$defs/argv" }, + "timeout_sec": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": true + }, + "criterion": { + "type": "object", + "required": ["id", "type"], + "oneOf": [ + { + "properties": { + "type": { "const": "programmatic" }, + "check": { "$ref": "#/$defs/argv" }, + "expect": { "enum": ["exit_zero", "exit_nonzero", "stdout_contains"] }, + "contains": { "type": "string" } + }, + "required": ["check", "expect"] + }, + { + "properties": { + "type": { "const": "judge" }, + "rubric": { "type": "string", "minLength": 1 } + }, + "required": ["rubric"] + }, + { + "properties": { + "type": { "const": "human" }, + "prompt": { "type": "string", "minLength": 1 } + }, + "required": ["prompt"] + } + ] + }, + "gate": { + "type": "object", + "required": ["when", "members", "verdict_policy", "criteria", "max_revisions"], + "properties": { + "when": { "type": "string" }, + "members": { + "type": "array", + "items": { "type": "string" } + }, + "verdict_policy": { "enum": ["revise_until_clean", "fixed_passes"] }, + "verdict_source": { "type": "string" }, + "criteria": { + "type": "array", + "items": { "type": "string" } + }, + "max_revisions": { "type": "integer", "minimum": 0 } + } + } + } +} diff --git a/.github/skills/loop-architect/scripts/looper.py b/.github/skills/loop-architect/scripts/looper.py new file mode 100644 index 0000000..1a017ad --- /dev/null +++ b/.github/skills/loop-architect/scripts/looper.py @@ -0,0 +1,822 @@ +#!/usr/bin/env python3 +"""Loop-architect helper CLI. + +This script belongs to the scaffolding side of loop-architect. It may detect +installed CLIs, register invocation metadata, compile loop.yaml to +loop.resolved.json, and render LOOP.md. It must not invoke model CLIs to do +loop work. + +Based on Looper by Kevin Simback (https://github.com/ksimback/looper), MIT License. +Adapted for Kiro CLI by ft.ia.br. +""" + +from __future__ import annotations + +import argparse +import datetime as _dt +import json +import os +from pathlib import Path +import shlex +import shutil +import subprocess +import sys +from typing import Any + + +DEFAULT_REDACTIONS = [".env", ".env.*", "secrets/**", "**/*.key"] +REGISTRY_PATH = Path.home() / ".loop-architect" / "models.json" + +MODEL_PROBES: dict[str, dict[str, Any]] = { + "kiro": { + "invoke": ["kiro-cli", "chat", "--trust-all-tools", "-p"], + "probe": ["kiro-cli", "--version"], + "local": False, + "install": "Install Kiro CLI: https://kiro.dev/downloads/", + "capabilities": ["headless", "goal", "subagent", "review_loop"], + }, + "claude": { + "invoke": ["claude", "-p"], + "probe": ["claude", "--version"], + "local": False, + "install": "Install and authenticate the Claude CLI.", + "capabilities": ["headless", "goal"], + }, + "codex": { + "invoke": ["codex", "exec"], + "probe": ["codex", "--version"], + "local": False, + "install": "Install and authenticate the Codex CLI.", + "capabilities": ["headless", "goal"], + }, + "gemini": { + "invoke": ["gemini", "-p"], + "probe": ["gemini", "--version"], + "local": False, + "install": "Install and authenticate the Gemini CLI.", + "capabilities": ["headless"], + }, + "llm": { + "invoke": ["llm"], + "probe": ["llm", "--version"], + "local": False, + "install": "Install llm and configure a model/provider.", + "capabilities": ["headless"], + }, + "ollama": { + "invoke": ["ollama", "run"], + "probe": ["ollama", "--version"], + "local": True, + "install": "Install Ollama and pull a local model.", + "capabilities": ["headless"], + }, +} + + +class LooperError(RuntimeError): + pass + + +def load_yaml(path: Path) -> dict[str, Any]: + try: + import yaml # type: ignore + except ImportError as exc: + raise LooperError( + "PyYAML is required to compile loop.yaml. Install with: python -m pip install PyYAML" + ) from exc + + try: + with path.open("r", encoding="utf-8") as fh: + data = yaml.safe_load(fh) + except OSError as exc: + raise LooperError(f"Could not read {path}: {exc}") from exc + except yaml.YAMLError as exc: + raise LooperError(f"Could not parse YAML in {path}: {exc}") from exc + if not isinstance(data, dict): + raise LooperError(f"{path} must contain a YAML mapping at the top level") + return data + + +def load_json(path: Path) -> dict[str, Any]: + with path.open("r", encoding="utf-8") as fh: + data = json.load(fh) + if not isinstance(data, dict): + raise LooperError(f"{path} must contain a JSON object") + return data + + +def write_json(path: Path, data: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(to_jsonable(data), indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def to_jsonable(value: Any) -> Any: + if isinstance(value, dict): + return {str(key): to_jsonable(item) for key, item in value.items()} + if isinstance(value, list): + return [to_jsonable(item) for item in value] + if isinstance(value, (_dt.date, _dt.datetime)): + return value.isoformat() + return value + + +def read_registry(path: Path = REGISTRY_PATH) -> dict[str, Any]: + if not path.exists(): + return {} + with path.open("r", encoding="utf-8") as fh: + data = json.load(fh) + if not isinstance(data, dict): + raise LooperError(f"Registry {path} must contain a JSON object") + return data + + +def write_registry(data: dict[str, Any], path: Path = REGISTRY_PATH) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + write_json(path, data) + + +def run_probe(argv: list[str], timeout_sec: int = 5) -> tuple[bool, str]: + probe_argv = list(argv) + if os.name == "nt": + resolved = shutil.which(argv[0]) + if resolved and Path(resolved).suffix.lower() in {".cmd", ".bat"}: + probe_argv = ["cmd", "/d", "/c", *argv] + try: + completed = subprocess.run( + probe_argv, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=timeout_sec, + check=False, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + return False, str(exc) + + output = (completed.stdout or completed.stderr or "").strip() + return completed.returncode == 0, output.splitlines()[0] if output else "" + + +def detect_models() -> dict[str, Any]: + registry: dict[str, Any] = {} + for model_id, meta in MODEL_PROBES.items(): + cli = meta["invoke"][0] + path = shutil.which(cli) + available = path is not None + authed = False + version = "" + if available: + authed, version = run_probe(meta["probe"]) + registry[model_id] = { + "cli": cli, + "path": path, + "invoke": meta["invoke"], + "available": available, + "authed": authed, + "local": meta["local"], + "probe": meta["probe"], + "version": version, + "install": meta["install"], + "capabilities": meta.get("capabilities", []), + } + return registry + + +def normalize_argv(value: Any, field: str) -> list[str]: + if isinstance(value, list) and all(isinstance(item, str) for item in value): + return value + if isinstance(value, str): + return shlex.split(value, posix=os.name != "nt") + raise LooperError(f"{field} must be an argv array or string") + + +def criteria_by_id(spec: dict[str, Any]) -> dict[str, dict[str, Any]]: + criteria = spec.get("goal", {}).get("verification", []) + if not isinstance(criteria, list): + raise LooperError("goal.verification must be a list") + result: dict[str, dict[str, Any]] = {} + for item in criteria: + if not isinstance(item, dict): + raise LooperError("Each verification criterion must be an object") + cid = item.get("id") + ctype = item.get("type") + if not isinstance(cid, str) or not cid: + raise LooperError("Each verification criterion needs a non-empty id") + if cid in result: + raise LooperError(f"Duplicate verification criterion id: {cid}") + if ctype not in {"programmatic", "judge", "human"}: + raise LooperError(f"Criterion {cid} has invalid type: {ctype}") + if ctype == "programmatic": + item["check"] = normalize_argv(item.get("check"), f"criterion {cid}.check") + if item.get("expect") not in {"exit_zero", "exit_nonzero", "stdout_contains"}: + raise LooperError( + f"Criterion {cid}.expect must be exit_zero, exit_nonzero, or stdout_contains" + ) + if item.get("expect") == "stdout_contains" and not isinstance(item.get("contains"), str): + raise LooperError(f"Criterion {cid} with stdout_contains needs contains") + elif ctype == "judge" and not isinstance(item.get("rubric"), str): + raise LooperError(f"Criterion {cid} needs a judge rubric") + elif ctype == "human" and not isinstance(item.get("prompt"), str): + raise LooperError(f"Criterion {cid} needs a human prompt") + result[cid] = item + return result + + +def validate_member(member: dict[str, Any]) -> None: + mid = member.get("id") + role = member.get("role") + if not isinstance(mid, str) or not mid: + raise LooperError("Each council member needs a non-empty id") + if role not in {"reviewer", "judge"}: + raise LooperError(f"Council member {mid} role must be reviewer or judge") + member["invoke"] = normalize_argv(member.get("invoke"), f"council.{mid}.invoke") + timeout = member.get("timeout_sec", 600) + if not isinstance(timeout, int) or timeout <= 0: + raise LooperError(f"Council member {mid}.timeout_sec must be a positive integer") + member.setdefault("scope", ["plan", "delivery"]) + member.setdefault("local", member.get("cli") == "ollama") + + +def validate_gate( + name: str, + gate: dict[str, Any], + criteria: dict[str, dict[str, Any]], + members: dict[str, dict[str, Any]], +) -> None: + if not isinstance(gate, dict): + raise LooperError(f"{name} must be an object") + policy = gate.get("verdict_policy") + if policy not in {"revise_until_clean", "fixed_passes"}: + raise LooperError(f"{name}.verdict_policy must be revise_until_clean or fixed_passes") + max_revisions = gate.get("max_revisions", 1) + if not isinstance(max_revisions, int) or max_revisions < 0: + raise LooperError(f"{name}.max_revisions must be a non-negative integer") + for cid in gate.get("criteria", []): + if cid not in criteria: + raise LooperError(f"{name} references unknown criterion: {cid}") + for mid in gate.get("members", []): + if mid not in members: + raise LooperError(f"{name} references unknown council member: {mid}") + if policy == "revise_until_clean": + source = gate.get("verdict_source") + if source == "human": + return + if source not in members: + raise LooperError(f"{name}.verdict_source must be a judge member or human") + if members[source].get("role") != "judge": + raise LooperError(f"{name}.verdict_source must name a judge, not a reviewer") + + +def normalize_spec(spec: dict[str, Any], source_path: Path) -> dict[str, Any]: + if spec.get("version") != 1: + raise LooperError("Only loop.yaml version: 1 is supported") + + goal = spec.get("goal") + if not isinstance(goal, dict): + raise LooperError("goal must be an object") + if not isinstance(goal.get("statement"), str) or not goal["statement"].strip(): + raise LooperError("goal.statement is required") + if not isinstance(goal.get("definition_of_done"), str) or not goal["definition_of_done"].strip(): + raise LooperError("goal.definition_of_done is required") + + for index, source in enumerate(goal.get("context_sources", [])): + if not isinstance(source, dict): + raise LooperError("goal.context_sources entries must be objects") + if "cmd" in source: + source["cmd"] = normalize_argv(source["cmd"], f"context_sources[{index}].cmd") + + criteria = criteria_by_id(spec) + + host = spec.get("host") + if not isinstance(host, dict): + raise LooperError("host must be an object") + host["invoke"] = normalize_argv(host.get("invoke"), "host.invoke") + host.setdefault("timeout_sec", 600) + if not isinstance(host["timeout_sec"], int) or host["timeout_sec"] <= 0: + raise LooperError("host.timeout_sec must be a positive integer") + + council_list = spec.get("council", []) + if not isinstance(council_list, list): + raise LooperError("council must be a list") + for member in council_list: + if not isinstance(member, dict): + raise LooperError("council entries must be objects") + validate_member(member) + members = {member["id"]: member for member in council_list} + + gates = spec.get("gates") + if not isinstance(gates, dict): + raise LooperError("gates must be an object") + for gate_name in ("plan_gate", "delivery_gate"): + validate_gate(gate_name, gates.get(gate_name), criteria, members) + + control = spec.get("loop_control") + if not isinstance(control, dict): + raise LooperError("loop_control must be an object") + max_iterations = control.get("max_iterations") + if not isinstance(max_iterations, int) or max_iterations <= 0: + raise LooperError("loop_control.max_iterations must be a positive integer") + budget = control.setdefault("budget", {}) + if not isinstance(budget, dict): + raise LooperError("loop_control.budget must be an object") + if "wall_clock_min" not in budget: + budget["wall_clock_min"] = 30 + no_progress = control.setdefault( + "no_progress", + { + "max_stalled_iterations": 2, + "signals": [ + "same blocking issue repeats", + "delivery artifact has no material change", + "verifier output is unchanged", + ], + "action": "stop", + }, + ) + if not isinstance(no_progress, dict): + raise LooperError("loop_control.no_progress must be an object") + stalled = no_progress.setdefault("max_stalled_iterations", 2) + if not isinstance(stalled, int) or stalled <= 0: + raise LooperError("loop_control.no_progress.max_stalled_iterations must be a positive integer") + signals = no_progress.setdefault("signals", ["same blocking issue repeats"]) + if not isinstance(signals, list) or not all(isinstance(item, str) for item in signals): + raise LooperError("loop_control.no_progress.signals must be a list of strings") + action = no_progress.setdefault("action", "stop") + if action not in {"stop", "human_checkpoint"}: + raise LooperError("loop_control.no_progress.action must be stop or human_checkpoint") + + execution = spec.setdefault( + "execution", + { + "mode": "in_session", + "isolation": "current_workspace", + "side_effects": {"requires_approval": True, "duplicate_action_check": True}, + }, + ) + if not isinstance(execution, dict): + raise LooperError("execution must be an object") + execution.setdefault("mode", "in_session") + execution.setdefault("isolation", "current_workspace") + if execution["mode"] not in {"in_session", "external_runner", "orchestrated"}: + raise LooperError("execution.mode must be in_session, external_runner, or orchestrated") + if execution["isolation"] not in {"current_workspace", "branch", "worktree", "sandbox"}: + raise LooperError("execution.isolation must be current_workspace, branch, worktree, or sandbox") + side_effects = execution.setdefault("side_effects", {}) + if not isinstance(side_effects, dict): + raise LooperError("execution.side_effects must be an object") + side_effects.setdefault("requires_approval", True) + side_effects.setdefault("duplicate_action_check", True) + + observability = spec.setdefault( + "observability", + {"state_file": "state.json", "run_log": "run-log.md", "checkpoint_granularity": "gate"}, + ) + if not isinstance(observability, dict): + raise LooperError("observability must be an object") + observability.setdefault("state_file", "state.json") + observability.setdefault("run_log", "run-log.md") + observability.setdefault("checkpoint_granularity", "gate") + if not isinstance(observability["state_file"], str) or not observability["state_file"]: + raise LooperError("observability.state_file must be a non-empty string") + if not isinstance(observability["run_log"], str) or not observability["run_log"]: + raise LooperError("observability.run_log must be a non-empty string") + if observability["checkpoint_granularity"] not in {"gate", "step"}: + raise LooperError("observability.checkpoint_granularity must be gate or step") + + workspace = spec.setdefault("workspace", {}) + if not isinstance(workspace, dict): + raise LooperError("workspace must be an object") + workspace.setdefault("dir", "./loop-workspace") + layout = workspace.setdefault("layout", ["plan.md", "delivery-{n}.md", "review-{n}.md", "state.json", "run-log.md"]) + if not isinstance(layout, list) or not all(isinstance(item, str) for item in layout): + raise LooperError("workspace.layout must be a list of strings") + for required_file in (observability["state_file"], observability["run_log"]): + if required_file not in layout: + layout.append(required_file) + + privacy = spec.setdefault("privacy", {}) + if not isinstance(privacy, dict): + raise LooperError("privacy must be an object") + egress = privacy.setdefault("egress", []) + if not isinstance(egress, list): + raise LooperError("privacy.egress must be a list") + for entry in egress: + if not isinstance(entry, dict): + raise LooperError("privacy.egress entries must be objects") + entry.setdefault("redact", DEFAULT_REDACTIONS) + entry.setdefault("consent", "required") + + resolved = { + "$schema": "https://github.com/ksimback/looper/schema/loop.resolved.v1.json", + "compiled_at": _dt.datetime.now(_dt.UTC).replace(microsecond=0).isoformat(), + "source": str(source_path), + **spec, + "criteria_by_id": criteria, + "council_by_id": members, + } + return to_jsonable(resolved) + + +def clip(text: Any, width: int) -> str: + value = str(text or "") + return value if len(value) <= width else value[: width - 1] + "~" + + +def ascii_box(*rows: str, width: int = 30) -> list[str]: + border = "+" + "-" * (width + 2) + "+" + body = [f"| {clip(row, width):<{width}} |" for row in rows if row is not None] + return [border, *body, border] + + +def render_ascii_diagram(resolved: dict[str, Any]) -> str: + gates = resolved.get("gates", {}) + control = resolved.get("loop_control", {}) + observability = resolved.get("observability", {}) + plan_gate = gates.get("plan_gate", {}) + delivery_gate = gates.get("delivery_gate", {}) + plan_revisions = plan_gate.get("max_revisions", 0) + delivery_revisions = delivery_gate.get("max_revisions", 0) + plan_source = plan_gate.get("verdict_source", "human") + delivery_source = delivery_gate.get("verdict_source", "human") + no_progress = control.get("no_progress", {}) + stalled = no_progress.get("max_stalled_iterations", 2) + budget = control.get("budget", {}) + budget_bits = [] + if budget.get("wall_clock_min") is not None: + budget_bits.append(f"{budget.get('wall_clock_min')}m") + if budget.get("usd") is not None: + budget_bits.append(f"${budget.get('usd')}") + if budget.get("tokens") is not None: + budget_bits.append(f"{budget.get('tokens')} tokens") + budget_text = ", ".join(budget_bits) or "configured caps" + + lines: list[str] = [] + lines.extend(ascii_box("1. Goal + context", "read sources")) + lines.extend([" |", " v"]) + lines.extend(ascii_box("2. Draft plan.md", f"state -> {observability.get('state_file', 'state.json')}")) + lines.extend([" |", " v"]) + lines.extend(ascii_box("3. Plan gate", f"verdict: {plan_source}")) + lines.extend([f" | needs work -> revise <= {plan_revisions} -> step 2", " | pass", " v"]) + lines.extend(ascii_box("4. Write delivery-N.md", f"log -> {observability.get('run_log', 'run-log.md')}")) + lines.extend([" |", " v"]) + lines.extend(ascii_box("5. Delivery gate", f"verdict: {delivery_source}")) + lines.extend([f" | needs work -> revise <= {delivery_revisions} -> step 4", " | pass", " v"]) + lines.extend(ascii_box("6. Final output", "all gates clean")) + lines.extend( + [ + "", + f"Stops: pass gates | max {control.get('max_iterations')} iterations | " + f"no progress x{stalled} | budget {budget_text}", + ] + ) + return "\n".join(lines) + + +def render_loop(resolved: dict[str, Any]) -> str: + meta = resolved.get("meta", {}) + goal = resolved.get("goal", {}) + gates = resolved.get("gates", {}) + control = resolved.get("loop_control", {}) + execution = resolved.get("execution", {}) + observability = resolved.get("observability", {}) + title = meta.get("name") or "Looper Generated Loop" + criteria = goal.get("verification", []) + council = resolved.get("council", []) + + lines = [ + f"# {title}", + "", + meta.get("description", "").strip(), + "", + "## Goal", + "", + goal.get("statement", "").strip(), + "", + "## Definition of Done", + "", + goal.get("definition_of_done", "").strip(), + "", + "## Verification", + "", + ] + for item in criteria: + lines.append(f"- `{item['id']}` ({item['type']})") + lines.extend(["", "## Council", ""]) + if council: + for member in council: + lines.append( + f"- `{member['id']}`: {member.get('role')} via {member.get('cli')} " + f"({member.get('model', 'default')})" + ) + else: + lines.append("- No council members configured.") + lines.extend( + [ + "", + "## Gates", + "", + f"- Plan gate: {gates.get('plan_gate', {}).get('verdict_policy')}", + f"- Delivery gate: {gates.get('delivery_gate', {}).get('verdict_policy')}", + "", + "## Loop Control", + "", + f"- Max iterations: {control.get('max_iterations')}", + f"- Budget: `{json.dumps(control.get('budget', {}), sort_keys=True)}`", + f"- No-progress: `{json.dumps(control.get('no_progress', {}), sort_keys=True)}`", + "", + "## Execution Boundary", + "", + f"- Mode: `{execution.get('mode', 'in_session')}`", + f"- Isolation: `{execution.get('isolation', 'current_workspace')}`", + f"- Side effects: `{json.dumps(execution.get('side_effects', {}), sort_keys=True)}`", + "", + "## Observability", + "", + f"- State file: `{observability.get('state_file', 'state.json')}`", + f"- Run log: `{observability.get('run_log', 'run-log.md')}`", + f"- Checkpoint granularity: `{observability.get('checkpoint_granularity', 'gate')}`", + "", + "## Flow Preview", + "", + "```text", + render_ascii_diagram(resolved), + "```", + "", + ] + ) + return "\n".join(line for line in lines if line is not None) + + +def render_session_prompt(resolved: dict[str, Any]) -> str: + meta = resolved.get("meta", {}) + goal = resolved.get("goal", {}) + gates = resolved.get("gates", {}) + control = resolved.get("loop_control", {}) + workspace = resolved.get("workspace", {}) + execution = resolved.get("execution", {}) + observability = resolved.get("observability", {}) + criteria = goal.get("verification", []) + council = resolved.get("council", []) + title = meta.get("name") or "Looper Generated Loop" + + lines = [ + f"# Run `{title}` In This Session", + "", + "Use this prompt when the user wants to run the Looper-designed loop in the current LLM session.", + "This is the default/easy execution path. The Python runner is the advanced path for running later or outside the session.", + "", + "## Operator Instructions", + "", + "You are executing a Looper-designed loop in this current session.", + "Follow the resolved spec below, write handoff files into the workspace, and enforce the caps manually.", + "Do not use `run-loop.py` unless the user explicitly asks for the advanced external runner.", + "", + "1. Create the workspace directory if it does not exist.", + "2. Read the context sources before drafting the plan.", + "3. Draft `plan.md` in the workspace.", + "4. Run the plan gate. Apply programmatic checks when available. For judge criteria, use the configured judge only after consent for any non-local egress; otherwise ask the user to approve a human/current-session substitute.", + "5. Revise until the gate passes or `max_revisions` is reached.", + "6. Produce `delivery-N.md` in the workspace.", + "7. Run the delivery gate after each delivery.", + "8. Stop when all delivery criteria pass, a cap is reached, or the user stops the loop.", + "9. Keep `state.json` current with status, iteration, last gate, consent, and blockers.", + "10. Append a compact entry to `run-log.md` after every context read, model call, check, gate verdict, revision, blocker, and stop decision.", + "11. Compare each blocker against the previous blocker. If the same blocker repeats for the configured no-progress window, stop or ask for the configured human checkpoint instead of revising again.", + "12. Treat token and USD budgets as operator limits in this session: if exact accounting is unavailable, stop and ask before continuing when the loop appears likely to exceed them.", + "", + "## Files", + "", + f"- Source spec: `{Path(resolved.get('source', 'loop.yaml')).name}`", + "- Human summary: `LOOP.md`", + "- Resolved spec: `loop.resolved.json`", + f"- Workspace: `{workspace.get('dir', './loop-workspace')}`", + f"- State file: `{observability.get('state_file', 'state.json')}`", + f"- Run log: `{observability.get('run_log', 'run-log.md')}`", + "", + "## Goal", + "", + goal.get("statement", "").strip(), + "", + "## Definition Of Done", + "", + goal.get("definition_of_done", "").strip(), + "", + "## Context Sources", + "", + ] + + context_sources = goal.get("context_sources", []) + if context_sources: + for source in context_sources: + if "file" in source: + lines.append(f"- Read file `{source['file']}`") + elif "cmd" in source: + lines.append(f"- Run command `{json.dumps(source['cmd'])}`") + else: + lines.append("- No context sources configured.") + + lines.extend(["", "## Verification Criteria", ""]) + for item in criteria: + if item["type"] == "programmatic": + lines.append( + f"- `{item['id']}` programmatic: run `{json.dumps(item['check'])}` and expect `{item['expect']}`" + ) + elif item["type"] == "judge": + lines.append(f"- `{item['id']}` judge rubric: {item['rubric']}") + elif item["type"] == "human": + lines.append(f"- `{item['id']}` human signoff: {item['prompt']}") + + lines.extend(["", "## Council", ""]) + if council: + for member in council: + locality = "local" if member.get("local") else "non-local" + lines.append( + f"- `{member['id']}` {member.get('role')} via `{json.dumps(member.get('invoke', []))}` " + f"({locality}; timeout {member.get('timeout_sec', 600)}s)" + ) + else: + lines.append("- No council members configured.") + + lines.extend(["", "## Gates", ""]) + for gate_name in ("plan_gate", "delivery_gate"): + gate = gates.get(gate_name, {}) + lines.extend( + [ + f"### {gate_name}", + "", + f"- When: `{gate.get('when')}`", + f"- Policy: `{gate.get('verdict_policy')}`", + f"- Verdict source: `{gate.get('verdict_source', 'none')}`", + f"- Criteria: `{', '.join(gate.get('criteria', []))}`", + f"- Max revisions: `{gate.get('max_revisions')}`", + "", + ] + ) + + lines.extend( + [ + "## Loop Control", + "", + f"- Max iterations: `{control.get('max_iterations')}`", + f"- Budget: `{json.dumps(control.get('budget', {}), sort_keys=True)}`", + f"- No-progress: `{json.dumps(control.get('no_progress', {}), sort_keys=True)}`", + f"- Human checkpoints: `{', '.join(control.get('human_checkpoints', [])) or 'none'}`", + "- Stop conditions:", + ] + ) + for condition in control.get("stop_conditions", []): + lines.append(f" - {condition}") + + lines.extend( + [ + "", + "## Execution Boundary", + "", + f"- Mode: `{execution.get('mode', 'in_session')}`", + f"- Isolation: `{execution.get('isolation', 'current_workspace')}`", + f"- Side effects: `{json.dumps(execution.get('side_effects', {}), sort_keys=True)}`", + "", + "If the loop needs scheduled runs, child-agent lifecycle management, concurrency control, or restart-safe step retries, stop and tell the user this Looper spec should be handed to a durable orchestrator.", + "", + "## Observability", + "", + f"- State file: `{observability.get('state_file', 'state.json')}`", + f"- Run log: `{observability.get('run_log', 'run-log.md')}`", + f"- Checkpoint granularity: `{observability.get('checkpoint_granularity', 'gate')}`", + "", + "Use `state.json` for the latest resumable status and `run-log.md` for the append-only history of what happened.", + ] + ) + + lines.extend(["", "## Privacy", ""]) + egress = resolved.get("privacy", {}).get("egress", []) + if egress: + for entry in egress: + lines.append( + f"- Before sending `{', '.join(entry.get('sends', []))}` to `{entry.get('to')}`, " + f"confirm consent and apply redactions `{', '.join(entry.get('redact', []))}`." + ) + else: + lines.append("- No cross-vendor egress configured.") + + lines.extend( + [ + "", + "## Start Now", + "", + "If the user asked to run now, begin at step 1 under Operator Instructions and keep going until a stop condition is reached.", + "", + ] + ) + return "\n".join(lines) + + +def cmd_detect(args: argparse.Namespace) -> int: + registry = detect_models() + if args.write: + existing = read_registry(args.registry) + existing.update(registry) + write_registry(existing, args.registry) + print(json.dumps(registry, indent=2, sort_keys=True)) + return 0 + + +def cmd_register(args: argparse.Namespace) -> int: + if not args.invoke: + raise LooperError("--invoke needs at least one command token") + registry = read_registry(args.registry) + registry[args.model_id] = { + "cli": args.invoke[0], + "invoke": args.invoke, + "available": shutil.which(args.invoke[0]) is not None, + "authed": args.authed, + "local": args.local, + "model": args.model, + "notes": args.notes or "", + } + write_registry(registry, args.registry) + print(f"Registered {args.model_id} in {args.registry}") + return 0 + + +def cmd_compile(args: argparse.Namespace) -> int: + source = args.loop_yaml.resolve() + spec = load_yaml(source) + resolved = normalize_spec(spec, source) + out = args.out or source.with_name("loop.resolved.json") + write_json(out, resolved) + if args.render: + args.render.parent.mkdir(parents=True, exist_ok=True) + args.render.write_text(render_loop(resolved), encoding="utf-8") + if args.session_prompt: + args.session_prompt.parent.mkdir(parents=True, exist_ok=True) + args.session_prompt.write_text(render_session_prompt(resolved), encoding="utf-8") + print(f"Wrote {out}") + if args.render: + print(f"Wrote {args.render}") + if args.session_prompt: + print(f"Wrote {args.session_prompt}") + return 0 + + +def cmd_session_prompt(args: argparse.Namespace) -> int: + resolved = load_json(args.resolved_json) + prompt = render_session_prompt(resolved) + if args.out: + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(prompt, encoding="utf-8") + print(f"Wrote {args.out}") + else: + print(prompt) + return 0 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="looper", description="Looper scaffolding helpers") + sub = parser.add_subparsers(dest="command", required=True) + + detect = sub.add_parser("detect-models", help="Detect model CLIs and print registry JSON") + detect.add_argument("--write", action="store_true", help="Merge results into the model registry") + detect.add_argument("--registry", type=Path, default=REGISTRY_PATH) + detect.set_defaults(func=cmd_detect) + + register = sub.add_parser("register-model", help="Register custom model CLI invocation metadata") + register.add_argument("model_id") + register.add_argument("--invoke", nargs="+", required=True) + register.add_argument("--model", default="") + register.add_argument("--local", action="store_true") + register.add_argument("--authed", action="store_true") + register.add_argument("--notes", default="") + register.add_argument("--registry", type=Path, default=REGISTRY_PATH) + register.set_defaults(func=cmd_register) + + compile_cmd = sub.add_parser("compile", help="Compile loop.yaml to loop.resolved.json") + compile_cmd.add_argument("loop_yaml", type=Path) + compile_cmd.add_argument("--out", type=Path) + compile_cmd.add_argument("--render", type=Path) + compile_cmd.add_argument("--session-prompt", type=Path) + compile_cmd.set_defaults(func=cmd_compile) + + session_prompt = sub.add_parser( + "session-prompt", help="Render the in-session execution prompt from loop.resolved.json" + ) + session_prompt.add_argument("resolved_json", type=Path) + session_prompt.add_argument("--out", type=Path) + session_prompt.set_defaults(func=cmd_session_prompt) + + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + try: + return int(args.func(args)) + except LooperError as exc: + print(f"looper: error: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/skills/loop-architect/templates/run-loop.py b/.github/skills/loop-architect/templates/run-loop.py new file mode 100644 index 0000000..084e380 --- /dev/null +++ b/.github/skills/loop-architect/templates/run-loop.py @@ -0,0 +1,588 @@ +#!/usr/bin/env python3 +"""Generated Looper runner. + +This file executes a resolved loop spec. It intentionally reads only +loop.resolved.json and uses only Python stdlib. +""" + +from __future__ import annotations + +import argparse +import datetime as _dt +import fnmatch +import json +from pathlib import Path +import re +import subprocess +import sys +import time +from typing import Any + + +PASS = "pass" +REVISE = "revise" + + +class RunnerError(RuntimeError): + pass + + +def utc_now() -> str: + return _dt.datetime.now(_dt.UTC).replace(microsecond=0).isoformat() + + +def load_json(path: Path) -> dict[str, Any]: + try: + with path.open("r", encoding="utf-8") as fh: + data = json.load(fh) + except OSError as exc: + raise RunnerError(f"Could not read {path}: {exc}") from exc + except json.JSONDecodeError as exc: + raise RunnerError(f"Could not parse JSON in {path}: {exc}") from exc + if not isinstance(data, dict): + raise RunnerError(f"{path} must contain a JSON object") + return data + + +def write_text(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text.rstrip() + "\n", encoding="utf-8") + + +def write_json(path: Path, data: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def ensure_argv(value: Any, field: str) -> list[str]: + if isinstance(value, list) and value and all(isinstance(item, str) for item in value): + return value + raise RunnerError(f"{field} must be a non-empty argv array") + + +def relative_to_base(path_text: str, base_dir: Path) -> Path: + path = Path(path_text) + return path if path.is_absolute() else base_dir / path + + +def is_redacted(path: Path, base_dir: Path, globs: list[str]) -> bool: + try: + rel = path.relative_to(base_dir).as_posix() + except ValueError: + rel = path.name + return any(fnmatch.fnmatch(rel, pattern) for pattern in globs) + + +def run_argv( + argv: list[str], + *, + cwd: Path, + timeout_sec: int, + stdin: str = "", +) -> subprocess.CompletedProcess[str]: + try: + return subprocess.run( + argv, + input=stdin, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=str(cwd), + timeout=timeout_sec, + check=False, + ) + except subprocess.TimeoutExpired as exc: + completed = subprocess.CompletedProcess(argv, 124, exc.stdout or "", exc.stderr or "") + return completed + except OSError as exc: + return subprocess.CompletedProcess(argv, 127, "", str(exc)) + + +def call_model(member: dict[str, Any], prompt: str, base_dir: Path) -> str: + argv = ensure_argv(member.get("invoke"), f"{member.get('id', member.get('cli', 'model'))}.invoke") + timeout_sec = int(member.get("timeout_sec", 600)) + result = run_argv(argv, cwd=base_dir, timeout_sec=timeout_sec, stdin=prompt) + if result.returncode != 0: + raise RunnerError( + f"Model invocation failed ({' '.join(argv)}): exit {result.returncode}\n{result.stderr}" + ) + return result.stdout.strip() + + +def parse_judge_output(text: str) -> dict[str, Any]: + fenced = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL) + candidate = fenced.group(1) if fenced else text.strip() + try: + parsed = json.loads(candidate) + except json.JSONDecodeError: + return { + "verdict": REVISE, + "blocking_issues": ["Judge output was not parseable JSON."], + "confidence": 0.0, + "notes": text.strip(), + "warning": "unparseable_judge_output", + } + if not isinstance(parsed, dict): + return { + "verdict": REVISE, + "blocking_issues": ["Judge output was not a JSON object."], + "confidence": 0.0, + "notes": text.strip(), + "warning": "invalid_judge_output", + } + verdict = parsed.get("verdict") + if verdict not in {PASS, REVISE}: + parsed["verdict"] = REVISE + parsed.setdefault("blocking_issues", []).append("Judge verdict was not pass or revise.") + parsed.setdefault("blocking_issues", []) + parsed.setdefault("confidence", 0.0) + parsed.setdefault("notes", "") + return parsed + + +class Runner: + def __init__(self, spec_path: Path) -> None: + self.spec_path = spec_path.resolve() + self.base_dir = self.spec_path.parent + self.spec = load_json(self.spec_path) + self.workspace = relative_to_base(self.spec["workspace"]["dir"], self.base_dir) + self.workspace.mkdir(parents=True, exist_ok=True) + self.observability = self.spec.get("observability", {}) + self.run_log_path = self.workspace / self.observability.get("run_log", "run-log.md") + self.state_path = self.workspace / self.observability.get("state_file", "state.json") + self.state = self.load_state() + self.started = time.monotonic() + + def load_state(self) -> dict[str, Any]: + if self.state_path.exists(): + return load_json(self.state_path) + return { + "status": "initialized", + "started_at": utc_now(), + "iteration": 0, + "warnings": [], + "consent": {}, + } + + def save_state(self, **updates: Any) -> None: + self.state.update(updates) + self.state["updated_at"] = utc_now() + write_json(self.state_path, self.state) + + def append_log(self, event: str, **fields: Any) -> None: + self.run_log_path.parent.mkdir(parents=True, exist_ok=True) + payload = f" {json.dumps(fields, sort_keys=True)}" if fields else "" + with self.run_log_path.open("a", encoding="utf-8") as fh: + fh.write(f"- {utc_now()} `{event}`{payload}\n") + + def enforce_wall_clock(self) -> None: + budget = self.spec.get("loop_control", {}).get("budget", {}) + wall_clock_min = budget.get("wall_clock_min") + if wall_clock_min is None: + return + if time.monotonic() - self.started > float(wall_clock_min) * 60: + self.save_state(status="failed", failure="wall_clock_budget_exceeded") + self.append_log("stop", reason="wall_clock_budget_exceeded") + raise RunnerError("Wall-clock budget exceeded") + + def no_progress_reached(self, gate_name: str, failures: list[str]) -> bool: + if not failures: + self.save_state(no_progress={"count": 0, "signature": "", "gate": gate_name}) + return False + config = self.spec.get("loop_control", {}).get("no_progress", {}) + threshold = int(config.get("max_stalled_iterations", 2)) + signature = "\n".join(sorted(failures)) + previous = self.state.get("no_progress", {}) + same_gate = previous.get("gate") == gate_name + same_signature = previous.get("signature") == signature + count = int(previous.get("count", 0)) + 1 if same_gate and same_signature else 1 + progress = { + "gate": gate_name, + "signature": signature, + "count": count, + "threshold": threshold, + "updated_at": utc_now(), + } + self.save_state(no_progress=progress) + if count < threshold: + return False + self.append_log("no_progress_detected", gate=gate_name, count=count, failures=failures) + if config.get("action", "stop") == "human_checkpoint": + answer = input("No-progress detected. Type 'continue' to allow one more revision: ").strip().lower() + if answer == "continue": + progress["count"] = 0 + self.save_state(no_progress=progress) + self.append_log("no_progress_override", gate=gate_name) + return False + self.save_state(status="failed", failure="no_progress_detected", blocking_issues=failures) + return True + + def criteria(self, ids: list[str]) -> list[dict[str, Any]]: + by_id = self.spec.get("criteria_by_id", {}) + return [by_id[item] for item in ids] + + def member(self, member_id: str) -> dict[str, Any]: + return self.spec["council_by_id"][member_id] + + def redactions_for(self, member_id: str) -> list[str]: + redactions: list[str] = [] + for entry in self.spec.get("privacy", {}).get("egress", []): + if entry.get("to") == member_id: + redactions.extend(entry.get("redact", [])) + return redactions or [".env", ".env.*", "secrets/**", "**/*.key"] + + def redact_prompt_for_member(self, member_id: str, prompt: str) -> str: + redactions = self.redactions_for(member_id) + redacted = prompt + for pattern in redactions: + paths = list(self.base_dir.glob(pattern)) + if pattern.endswith("/**"): + root = self.base_dir / pattern[:-3] + if root.exists(): + paths.extend(root.rglob("*")) + for path in paths: + if not path.is_file(): + continue + try: + secret_text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + continue + if not secret_text.strip() or len(secret_text) > 1_000_000: + continue + marker = f"[redacted:{path.relative_to(self.base_dir).as_posix()}]" + redacted = redacted.replace(secret_text, marker) + for line in secret_text.splitlines(): + stripped = line.strip() + if len(stripped) >= 8: + redacted = redacted.replace(stripped, marker) + return redacted + + def ensure_consent(self, member_id: str) -> None: + member = self.member(member_id) + if member.get("local"): + return + matching = [ + entry + for entry in self.spec.get("privacy", {}).get("egress", []) + if entry.get("to") == member_id and entry.get("consent") == "required" + ] + if not matching: + return + if self.state.get("consent", {}).get(member_id): + return + sends = sorted({item for entry in matching for item in entry.get("sends", [])}) + redactions = sorted({item for entry in matching for item in entry.get("redact", [])}) + print() + print(f"Looper is about to send {', '.join(sends) or 'context'} to {member_id}.") + print(f"CLI: {member.get('cli')} / model: {member.get('model', 'default')}") + print(f"Redactions: {', '.join(redactions) or '(none)'}") + answer = input("Type 'yes' to consent to this first send: ").strip().lower() + if answer != "yes": + self.save_state(status="blocked", failure=f"consent_refused:{member_id}") + raise RunnerError(f"Consent refused for {member_id}") + consent = dict(self.state.get("consent", {})) + consent[member_id] = {"granted_at": utc_now(), "sends": sends, "redact": redactions} + self.save_state(consent=consent) + + def gather_context(self) -> str: + goal = self.spec["goal"] + chunks: list[str] = [] + for index, source in enumerate(goal.get("context_sources", []), start=1): + self.enforce_wall_clock() + if "file" in source: + path = relative_to_base(source["file"], self.base_dir) + if is_redacted(path, self.base_dir, [".env", ".env.*", "secrets/**", "**/*.key"]): + chunks.append(f"## Context source {index}: {source['file']}\n[redacted]\n") + self.append_log("context", source=source["file"], status="redacted") + elif path.exists(): + chunks.append(f"## Context source {index}: {source['file']}\n{path.read_text(encoding='utf-8')}\n") + self.append_log("context", source=source["file"], status="read") + else: + chunks.append(f"## Context source {index}: {source['file']}\n[missing]\n") + self.append_log("context", source=source["file"], status="missing") + elif "cmd" in source: + argv = ensure_argv(source["cmd"], f"context_sources[{index}].cmd") + result = run_argv(argv, cwd=self.base_dir, timeout_sec=int(source.get("timeout_sec", 60))) + chunks.append( + f"## Context source {index}: {' '.join(argv)}\n" + f"exit={result.returncode}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}\n" + ) + self.append_log("context_cmd", argv=argv, returncode=result.returncode) + context = "\n".join(chunks).strip() + write_text(self.workspace / "context.md", context or "No context sources configured.") + return context + + def host_prompt(self, phase: str, artifact: str = "", review: str = "") -> str: + goal = self.spec["goal"] + if phase == "plan": + return ( + "Draft plan.md for this loop.\n\n" + f"Goal:\n{goal['statement']}\n\n" + f"Definition of done:\n{goal['definition_of_done']}\n\n" + f"Context:\n{(self.workspace / 'context.md').read_text(encoding='utf-8')}\n" + ) + if phase == "delivery": + return ( + "Write the next delivery artifact for this loop.\n\n" + f"Goal:\n{goal['statement']}\n\n" + f"Definition of done:\n{goal['definition_of_done']}\n\n" + f"Plan:\n{(self.workspace / 'plan.md').read_text(encoding='utf-8')}\n" + ) + if phase == "revise": + return ( + "Revise the artifact to address the review. Return only the revised artifact.\n\n" + f"Artifact:\n{artifact}\n\nReview:\n{review}\n" + ) + raise RunnerError(f"Unknown host phase: {phase}") + + def run_host(self, phase: str, target: Path, artifact: str = "", review: str = "") -> None: + self.enforce_wall_clock() + self.append_log("host_start", phase=phase, target=target.name) + output = call_model(self.spec["host"], self.host_prompt(phase, artifact, review), self.base_dir) + write_text(target, output) + self.append_log("host_done", phase=phase, target=target.name) + + def run_programmatic(self, criterion: dict[str, Any]) -> dict[str, Any]: + argv = ensure_argv(criterion["check"], f"{criterion['id']}.check") + result = run_argv(argv, cwd=self.base_dir, timeout_sec=int(criterion.get("timeout_sec", 300))) + expect = criterion.get("expect") + passed = False + if expect == "exit_zero": + passed = result.returncode == 0 + elif expect == "exit_nonzero": + passed = result.returncode != 0 + elif expect == "stdout_contains": + passed = criterion.get("contains", "") in result.stdout + self.append_log( + "programmatic_check", + criterion=criterion["id"], + passed=passed, + returncode=result.returncode, + ) + return { + "id": criterion["id"], + "type": "programmatic", + "passed": passed, + "returncode": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + } + + def judge_prompt( + self, + gate_name: str, + artifact_label: str, + artifact_text: str, + criteria: list[dict[str, Any]], + ) -> str: + rubric_lines = [] + for criterion in criteria: + if criterion["type"] == "judge": + rubric_lines.append(f"- {criterion['id']}: {criterion['rubric']}") + elif criterion["type"] == "programmatic": + rubric_lines.append(f"- {criterion['id']}: programmatic check result is included below.") + elif criterion["type"] == "human": + rubric_lines.append(f"- {criterion['id']}: human signoff is required separately.") + return ( + "You are the Looper judge. Return only a fenced JSON object with keys " + "verdict, blocking_issues, confidence, and notes. verdict must be pass or revise.\n\n" + f"Gate: {gate_name}\n" + f"Artifact: {artifact_label}\n\n" + "Criteria:\n" + "\n".join(rubric_lines) + "\n\n" + f"Artifact content:\n{artifact_text}\n" + ) + + def run_judge( + self, + member_id: str, + gate_name: str, + artifact_label: str, + artifact_text: str, + criteria: list[dict[str, Any]], + ) -> dict[str, Any]: + self.ensure_consent(member_id) + output = call_model( + self.member(member_id), + self.redact_prompt_for_member( + member_id, + self.judge_prompt(gate_name, artifact_label, artifact_text, criteria), + ), + self.base_dir, + ) + verdict = parse_judge_output(output) + verdict["member"] = member_id + self.append_log("judge_verdict", gate=gate_name, member=member_id, verdict=verdict.get("verdict")) + return verdict + + def run_reviewers( + self, + gate_name: str, + artifact_label: str, + artifact_text: str, + member_ids: list[str], + ) -> list[str]: + notes = [] + for member_id in member_ids: + member = self.member(member_id) + if member.get("role") != "reviewer": + continue + self.ensure_consent(member_id) + prompt = ( + "You are a Looper reviewer. Return concise blocking and non-blocking notes. " + "Do not return a verdict.\n\n" + f"Gate: {gate_name}\nArtifact: {artifact_label}\n\n{artifact_text}\n" + ) + prompt = self.redact_prompt_for_member(member_id, prompt) + notes.append(f"## {member_id}\n\n{call_model(member, prompt, self.base_dir)}") + self.append_log("reviewer_notes", gate=gate_name, member=member_id) + return notes + + def human_check(self, criterion: dict[str, Any]) -> dict[str, Any]: + print() + print(criterion["prompt"]) + answer = input("Type 'pass' to approve, anything else to request revision: ").strip().lower() + return { + "id": criterion["id"], + "type": "human", + "passed": answer == PASS, + "notes": "approved" if answer == PASS else "human requested revision", + } + + def run_gate(self, gate_name: str, artifact_path: Path, artifact_label: str) -> bool: + gate = self.spec["gates"][gate_name] + criteria = self.criteria(gate.get("criteria", [])) + max_revisions = int(gate.get("max_revisions", 0)) + revision = 0 + self.append_log("gate_start", gate=gate_name, artifact=artifact_label) + + while True: + self.enforce_wall_clock() + artifact_text = artifact_path.read_text(encoding="utf-8") + review_parts: list[str] = [] + failures: list[str] = [] + + for criterion in criteria: + if criterion["type"] == "programmatic": + result = self.run_programmatic(criterion) + review_parts.append(f"## Programmatic {criterion['id']}\n\n```json\n{json.dumps(result, indent=2)}\n```") + if not result["passed"]: + failures.append(f"Programmatic check failed: {criterion['id']}") + elif criterion["type"] == "human": + result = self.human_check(criterion) + review_parts.append(f"## Human {criterion['id']}\n\n{result['notes']}") + if not result["passed"]: + failures.append(f"Human check failed: {criterion['id']}") + + reviewer_notes = self.run_reviewers( + gate_name, + artifact_label, + artifact_text, + list(gate.get("members", [])), + ) + review_parts.extend(reviewer_notes) + + policy = gate.get("verdict_policy") + verdict: dict[str, Any] | None = None + if policy == "revise_until_clean" and not failures: + source = gate.get("verdict_source") + if source == "human": + answer = input(f"Type 'pass' if {artifact_label} is clean: ").strip().lower() + verdict = { + "verdict": PASS if answer == PASS else REVISE, + "blocking_issues": [] if answer == PASS else ["human requested revision"], + "confidence": 1.0, + "notes": "human verdict", + } + else: + verdict = self.run_judge(source, gate_name, artifact_label, artifact_text, criteria) + review_parts.append(f"## Verdict\n\n```json\n{json.dumps(verdict, indent=2)}\n```") + if verdict.get("verdict") == REVISE: + failures.extend(verdict.get("blocking_issues") or ["Judge requested revision"]) + + if policy == "fixed_passes": + if failures: + pass + elif revision >= max_revisions: + return True + else: + failures.append("fixed_passes reviewer pass") + + if not failures: + self.save_state(status=f"{gate_name}_passed", **{gate_name: {"passed_at": utc_now()}}) + self.append_log("gate_passed", gate=gate_name, artifact=artifact_label) + return True + + review_text = "\n\n".join(review_parts + ["## Blocking Issues", "\n".join(f"- {item}" for item in failures)]) + review_path = self.workspace / f"review-{gate_name}-{revision + 1}.md" + write_text(review_path, review_text) + self.append_log("gate_blocked", gate=gate_name, review=review_path.name, failures=failures) + + if self.no_progress_reached(gate_name, failures): + return False + + if revision >= max_revisions: + self.save_state( + status="failed", + failure=f"{gate_name}_max_revisions_reached", + last_review=str(review_path), + ) + self.append_log("stop", reason=f"{gate_name}_max_revisions_reached") + return False + + revised = call_model( + self.spec["host"], + self.host_prompt("revise", artifact_text, review_text), + self.base_dir, + ) + write_text(artifact_path, revised) + revision += 1 + self.save_state(status=f"{gate_name}_revision_{revision}", last_review=str(review_path)) + self.append_log("revision", gate=gate_name, revision=revision, artifact=artifact_label) + + def run(self) -> int: + self.save_state(status="running") + self.append_log("run_start", spec=str(self.spec_path)) + self.gather_context() + + plan_path = self.workspace / "plan.md" + if not plan_path.exists(): + self.run_host("plan", plan_path) + if not self.run_gate("plan_gate", plan_path, "plan.md"): + return 1 + + max_iterations = int(self.spec["loop_control"]["max_iterations"]) + for iteration in range(1, max_iterations + 1): + self.enforce_wall_clock() + self.save_state(status="delivery", iteration=iteration) + delivery_path = self.workspace / f"delivery-{iteration}.md" + self.run_host("delivery", delivery_path) + if self.run_gate("delivery_gate", delivery_path, delivery_path.name): + self.save_state(status="passed", final_delivery=str(delivery_path), completed_at=utc_now()) + self.append_log("run_passed", final_delivery=str(delivery_path)) + print(f"Looper run passed. Final delivery: {delivery_path}") + return 0 + + self.save_state(status="failed", failure="max_iterations_reached") + self.append_log("stop", reason="max_iterations_reached") + return 1 + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Run a compiled Looper loop.") + parser.add_argument( + "spec_path", + nargs="?", + type=Path, + default=Path(__file__).with_name("loop.resolved.json"), + help="Path to loop.resolved.json (defaults to the file next to run-loop.py).", + ) + args = parser.parse_args(sys.argv[1:] if argv is None else argv) + try: + return Runner(args.spec_path).run() + except RunnerError as exc: + print(f"run-loop: error: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/skills/okf-open-knowledge-format/SKILL.md b/.github/skills/okf-open-knowledge-format/SKILL.md new file mode 100644 index 0000000..00cfa3e --- /dev/null +++ b/.github/skills/okf-open-knowledge-format/SKILL.md @@ -0,0 +1,424 @@ +--- +name: okf-open-knowledge-format +description: > + Create, validate, and enrich Open Knowledge Format (OKF) bundles — the open + spec for representing organizational knowledge as markdown files with YAML + frontmatter. Use when the user mentions 'OKF', 'Open Knowledge Format', + 'knowledge bundle', 'OKF bundle', 'create a knowledge base for agents', + 'validate OKF', 'convert to OKF', 'enrich knowledge docs', 'agent-readable + knowledge', 'LLM wiki', 'knowledge catalog', 'kcmd', or wants to structure + knowledge as markdown files for AI agent consumption. Also use when the user + has a directory of markdown files and wants to make them interoperable or + conformant with the OKF standard. Even for simple requests like 'make this + folder OKF conformant' — the skill has critical structural rules the agent + needs. +metadata: + author: ft.ia.br + version: "1.1" + date: 2026-06-17 + repository: https://github.com/fabricioctelles/skills + license: Apache-2.0 + category: library-and-api-reference +--- + +# Open Knowledge Format (OKF) + +OKF is a vendor-neutral, open spec (v0.1, announced June 12, 2026 by Sam McVeety & Amir Hormati at Google Cloud) for representing knowledge as a directory of markdown files with YAML frontmatter. No SDK required — if you can `cat` a file, you can read OKF. + +It formalizes the "LLM Wiki" pattern ([Karpathy's gist](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f)) into an interoperable format: wikis written by different producers can be consumed by different agents without translation. + +For the full spec, see [references/spec-v01.md](references/spec-v01.md). + +### Design Principles + +1. **Minimally opinionated** — Only `type` is required. The spec defines interoperability surface, not content model. +2. **Producer/consumer independence** — Who writes and who reads are decoupled. Human-authored bundles feed agents; LLM-generated bundles are browsed by humans. +3. **Format, not platform** — No cloud, SDK, or vendor dependency. Value comes from how many parties speak it. + +--- + +## Key Terminology + +- **Bundle** — A directory tree of `.md` files. The unit of distribution (git repo, tarball, or subdirectory). +- **Concept** — One markdown file = one unit of knowledge (table, metric, playbook, API, etc.) +- **Concept ID** — File path within the bundle, minus `.md` suffix. Example: `tables/users.md` → ID `tables/users` +- **Frontmatter** — YAML block between `---` delimiters at file top. +- **Body** — Everything after the frontmatter. Standard markdown. +- **Link** — Standard markdown link expressing a relationship between concepts. +- **Citation** — Link to an external source backing a claim in the body. + +--- + +## Quick Reference — Frontmatter Fields + +| Field | Required? | Description | +|-------|-----------|-------------| +| `type` | **YES** | Kind of concept (free-form string, e.g. `BigQuery Table`, `Metric`, `Playbook`, `API Endpoint`) | +| `title` | Recommended | Human-readable display name | +| `description` | Recommended | One-sentence summary | +| `resource` | Recommended | URI identifying the underlying asset (omit for abstract concepts) | +| `tags` | Optional | YAML list for cross-cutting categorization | +| `timestamp` | Optional | ISO 8601 datetime of last meaningful change | + +Additional producer-defined keys are allowed. Never reject unknown fields. + +## Reserved Filenames + +| File | Purpose | Has frontmatter? | +|------|---------|-----------------| +| `index.md` | Directory listing for progressive disclosure | NO* | +| `log.md` | Change history, newest first | NO | + +*Exception: bundle-root `index.md` MAY have frontmatter with `okf_version: "0.1"` to declare spec version. + +## Conventional Body Headings + +| Heading | When to use | +|---------|-------------| +| `# Schema` | Data assets — describe columns/fields | +| `# Examples` | Show concrete usage (code blocks, queries) | +| `# Citations` | List external sources backing claims (numbered) | + +--- + +## Create a Bundle + +When the user wants to create an OKF bundle from scratch: + +### 1. Determine scope and structure + +Ask: What knowledge are we capturing? (tables, metrics, APIs, playbooks, etc.) +Organize into a directory tree that makes sense for the domain. + +### 2. Create concept documents + +Each concept = one `.md` file. Minimal conformant example: + +```markdown +--- +type: Metric +title: Monthly Recurring Revenue +description: Sum of all active subscription revenue normalized to monthly. +tags: [revenue, saas] +timestamp: 2026-06-13T10:00:00Z +--- + +# Monthly Recurring Revenue (MRR) + +## Definition + +Sum of all active subscriptions normalized to a monthly amount. +Excludes one-time fees and overages. + +## Formula + +`MRR = Σ(active_subscription_monthly_value)` + +## Related + +- [Churn Rate](./churn.md) uses MRR as denominator +- [ARR](./arr.md) = MRR × 12 +``` + +For more examples across domains, see [references/examples.md](references/examples.md). + +### 3. Cross-link concepts + +Use standard markdown links. Two forms: + +- **Absolute** (bundle-relative, starts with `/`): `[customers](/tables/customers.md)` — **preferred** (stable when files move) +- **Relative**: `[churn](./churn.md)` + +Links assert relationships. The kind of relationship is conveyed by surrounding prose, not by the link syntax. Broken links are explicitly permitted — they represent knowledge not yet written. + +### 4. Generate index.md + +Place in any directory for progressive disclosure. No frontmatter. Format: + +```markdown +# Metrics + +- [MRR](./mrr.md) - Monthly recurring revenue +- [Churn](./churn.md) - Monthly churn rate +- [NPS](./nps.md) - Net Promoter Score +``` + +Entries should include the description from the linked concept's frontmatter. + +### 5. Generate log.md (optional) + +Chronological change history, newest first, ISO 8601 date headings: + +```markdown +# Update Log + +## 2026-06-13 +- **Creation**: Added MRR, Churn, and NPS metrics. +- **Creation**: Established directory structure. + +## 2026-06-10 +- **Initialization**: Bundle created. +``` + +The bold leading word (`**Update**`, `**Creation**`, `**Deprecation**`) is convention, not requirement. + +### 6. Declare version (optional) + +Bundle-root `index.md` may include frontmatter declaring the spec version: + +```markdown +--- +okf_version: "0.1" +--- + +# My Knowledge Bundle + +- [Tables](./tables/) - Database tables +- [Metrics](./metrics/) - Business KPIs +``` + +This is the only place frontmatter is permitted in an `index.md`. + +### 7. Distribution + +A bundle can be distributed as: +- A **git repository** (recommended — history, attribution, diffs) +- A tarball or zip archive +- A subdirectory within a larger repository + +### 8. Verify conformance + +Three rules — all must pass: +1. Every non-reserved `.md` file has parseable YAML frontmatter +2. Every frontmatter has a non-empty `type` field +3. Reserved files (`index.md`, `log.md`) follow their defined structure when present + +--- + +## Validate a Bundle + +### Preferred: okflint (when available) + +[okflint](https://github.com/mattdav/okflint) is a dedicated Python linter for OKF bundles with 18 rules across 3 tiers (OKF core, profile, hygiene). If installed, always prefer it over the built-in bash script. + +**Agent behavior:** Before validating, check if okflint is installed (`command -v okflint`). If NOT installed, ask the user: + +> "okflint (linter dedicado para OKF com 18 regras, profiles via manifesto e suporte a wikilinks) não está instalado. Quer que eu instale? Opções: +> 1. `uv tool install okflint` (recomendado, isolado) +> 2. `pip install okflint` +> 3. Seguir sem ele (validação básica com o script bash embutido)" + +If the user agrees to install: + +```bash +# Option 1: uv (recommended — installs isolated, no venv needed) +uv tool install okflint + +# Option 2: pip (installs in current environment) +pip install okflint + +# Verify installation +okflint --version +``` + +After installation (or if already available): + +```bash +# Full validation with manifest (if okf-base.yaml exists) +if [ -f okf-base.yaml ]; then + okflint validate --manifest okf-base.yaml ./bundle/ +else + # Core OKF validation only (no manifest needed) + okflint validate ./bundle/ +fi +``` + +**okflint advantages over the built-in script:** +- Manifest-driven profiles (enforce custom required fields, status vocabularies, per-type constraints) +- Wikilink resolution against full Obsidian vault +- JSON output (`--json`) for CI pipeline parsing +- Detects broken markdown links and ambiguous wikilinks +- Exit codes: `0` = pass, `1` = conformance failure, `2` = bad manifest + +### Fallback: built-in bash script + +When okflint is not installed, use [scripts/validate.sh](scripts/validate.sh) which checks the 3 core conformance rules. + +When asked to validate, check the 3 conformance rules. Report: + +``` +✅ PASS: 12/12 concept files have valid frontmatter with type field +✅ PASS: index.md follows list structure (no frontmatter) +✅ PASS: log.md uses ISO 8601 date headings, newest first + +⚠ WARNING: 3 files missing 'description' field (recommended) +⚠ WARNING: 2 broken cross-links (permitted but worth noting) +``` + +For a script-based check, see [scripts/validate.sh](scripts/validate.sh). + +### Errors (conformance failures) + +- `E1`: File `{path}` has no YAML frontmatter +- `E2`: File `{path}` has frontmatter but no `type` field (or empty) +- `E3`: Reserved file `{path}` has unexpected structure + +### Warnings (non-blocking, spec allows these) + +- `W1`: Missing recommended field `title` or `description` +- `W2`: Broken cross-link `{link}` in `{file}` +- `W3`: No `timestamp` field +- `W4`: No `index.md` in directory `{dir}` +- `W5`: `log.md` dates not in ISO 8601 format + +Consumers MUST NOT reject a bundle because of: missing optional fields, unknown type values, unknown frontmatter keys, broken links, or missing index files. + +--- + +## Enrich Concepts + +When the user has existing OKF concepts that need enrichment: + +### Add schema section + +For data assets, add `# Schema` with a columns table: + +```markdown +# Schema + +| Column | Type | Description | +|--------|------|-------------| +| `order_id` | STRING | Unique identifier | +| `customer_id` | STRING | FK to [customers](/tables/customers.md) | +``` + +### Add examples section + +For APIs, queries, or tools, add `# Examples` with fenced code blocks showing usage. + +### Add citations + +When claims reference external sources, add `# Citations` at the bottom, numbered: + +```markdown +# Citations + +[1] [Official docs](https://example.com/docs) +[2] [Internal runbook](https://wiki.internal/quality) +``` + +Citations may be absolute URLs, bundle-relative paths, or paths into a `references/` subdirectory. + +### Add cross-links + +Weave links into natural prose. Don't create a standalone "links" section — express relationships in context where they're meaningful. + +### Fill recommended fields + +If `title`, `description`, `tags`, or `timestamp` are missing, add them. Derive values from body content when possible. + +### Enrichment workflow reference + +The official enrichment agent follows this pattern — apply the same logic manually: +1. Start with metadata-only docs (just frontmatter + minimal body) +2. Add schema/structure from source system +3. Add citations from authoritative documentation +4. Weave cross-links based on discovered relationships (FKs, shared tags, join paths) +5. Generate `index.md` files for progressive disclosure + +--- + +## Convert Sources to OKF + +For detailed conversion guides, see [references/conversion.md](references/conversion.md). + +### Quick rules + +**Notion export:** Properties → frontmatter. Remove UUID suffixes from filenames. Convert Notion links → relative markdown links. + +**Obsidian vault:** Convert `[[wikilinks]]` → `[title](./file.md)`. Ensure `type` field exists. Move inline `#tags` to frontmatter. + +**CSV/spreadsheet:** Each row = one concept. Map columns to frontmatter fields. First column = filename. + +--- + +## Guardrails + +1. **NEVER invent data.** If you don't know the correct `type`, ask. If you don't have schema info, leave it out. No fabricated URLs or column names. +2. **Preserve unknown fields.** OKF explicitly allows extension. Don't delete fields you don't recognize. +3. **Don't impose taxonomy.** Type values are free-form strings. Suggest descriptive values but never reject a bundle for having unexpected types. +4. **Broken links are OK.** The spec explicitly permits them — they represent not-yet-written knowledge. +5. **Minimal by default.** Generate only `type` (required) + recommended fields that are warranted. Don't pad with empty values. +6. **Ask before assuming.** If the domain is unclear, ask what types and structure make sense. + +--- + +## Serve via Google Cloud Knowledge Catalog + +Google Cloud's Knowledge Catalog **natively ingests OKF bundles** and serves them to agents. This is the enterprise path — optional but powerful. + +### kcmd CLI (Metadata as Code) + +`kcmd` is a bidirectional sync tool between OKF-like local metadata and Knowledge Catalog. Think "git for metadata." + +```bash +# Initialize from BigQuery dataset +kcmd init --bigquery-dataset . + +# Pull current state from catalog +kcmd pull + +# Push local changes +kcmd push --dry-run +kcmd push +``` + +Also ships as an **MCP server** for agent integration: + +```json +{ + "mcpServers": { + "kc-mac": { + "command": "kcmd", + "args": ["mcp", "--path", "/path/to/root"] + } + } +} +``` + +MCP tools: `pull`, `push`, `list-entries`, `lookup-entry`, `modify-entry`. + +### Reference Enrichment Agent + +The official enrichment agent (Python, ADK, Gemini) auto-generates OKF bundles from BigQuery metadata. Two-pass architecture: + +1. **BQ pass** — one OKF doc per table/view from metadata +2. **Web pass** — LLM crawls seed URLs and for each page decides to: + - **(a) Enrich** existing concepts with citations/schemas + - **(b) Mint** a new `references/` doc + - **(c) Skip** irrelevant content + +Controls: `--web-seed-file`, `--web-max-pages`, `--web-allowed-host`, `--no-web`. + +**When to mention this to users:** If they're enriching BigQuery datasets, point them to the [reference agent](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf). If they want enterprise catalog integration, point to [kcmd](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/toolbox/mdcode) and the [ingest demo](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/toolbox/mdcode/demo). + +--- + +## Output Format + +When creating a bundle, present results as: + +1. **Directory tree** showing the full structure +2. **Each file's content** in fenced code blocks +3. **Conformance check** confirming the bundle passes the 3 rules + +``` +saas-metrics/ +├── index.md +├── log.md +├── mrr.md +├── churn.md +└── nps.md +``` + +Then show each file, then confirm: "Bundle is OKF v0.1 conformant ✅" diff --git a/.github/skills/okf-open-knowledge-format/references/conversion.md b/.github/skills/okf-open-knowledge-format/references/conversion.md new file mode 100644 index 0000000..21581e8 --- /dev/null +++ b/.github/skills/okf-open-knowledge-format/references/conversion.md @@ -0,0 +1,141 @@ +# Converting Sources to OKF + +Guides for transforming existing knowledge into conformant OKF bundles. + +--- + +## From Notion Export + +Notion exports as markdown with properties in YAML-like format. + +### Steps + +1. **Export** from Notion as Markdown & CSV +2. **Clean filenames** — remove UUID suffixes (`Page Name abc123def.md` → `page-name.md`) +3. **Map properties to frontmatter:** + +| Notion Property | OKF Field | +|-----------------|-----------| +| Type (select) | `type` (required) | +| Name | `title` | +| Tags (multi-select) | `tags` | +| Last Edited | `timestamp` | +| URL | `resource` | + +4. **Convert links** — Notion uses `[Page Name](Page%20Name%20abc123def.md)`. Convert to clean relative paths: `[Page Name](./page-name.md)` +5. **Remove Notion artifacts** — empty toggle blocks, breadcrumb headers, cover image references +6. **Add missing `type` field** — if Notion had no "Type" property, ask the user what type to assign + +### Edge cases + +- Notion databases: each row becomes a concept. Database title becomes the directory name. +- Nested pages: respect the hierarchy. Child pages go in subdirectories. +- Inline databases: flatten into a list in the parent concept's body. +- Notion formulas/rollups: drop them — they don't translate to static markdown. + +--- + +## From Obsidian Vault + +Obsidian vaults are already close to OKF. Main differences: wikilinks and potentially missing `type` field. + +### Steps + +1. **Convert wikilinks to standard links:** + - `[[Note Name]]` → `[Note Name](./note-name.md)` + - `[[Note Name|Display Text]]` → `[Display Text](./note-name.md)` + - `[[Note Name#Heading]]` → `[Note Name](./note-name.md#heading)` + +2. **Ensure `type` field exists** in every frontmatter block. Common mappings: + +| Obsidian pattern | Suggested OKF type | +|------------------|--------------------| +| Daily notes | `Log` | +| MOC / index note | Convert to `index.md` (reserved file) | +| Permanent notes | `Reference` | +| Literature notes | `Reference` | +| Project notes | `Playbook` or domain-specific | + +3. **Convert tags:** + - Inline `#tag` → move to frontmatter `tags: [tag]` + - Nested `#parent/child` → flatten to `tags: [parent, child]` or keep as `parent/child` + +4. **Handle embeds:** + - `![[Note]]` — convert to a regular link or inline the content + - `![[image.png]]` — keep as standard markdown image `![](./image.png)` + +5. **Remove Obsidian-specific syntax:** + - `%%comments%%` → remove + - `> [!callout]` → convert to blockquote or heading + - Dataview queries → remove (dynamic, not portable) + +### What to keep as-is + +- Standard markdown formatting (headings, lists, tables, code blocks) +- Existing YAML frontmatter (just add `type` if missing) +- Standard markdown links (already OKF-compatible) +- Mermaid diagrams (standard markdown fenced blocks) + +--- + +## From CSV / Spreadsheet + +Each row becomes one concept document. + +### Steps + +1. **Identify column mapping:** + +| Column role | Maps to | +|-------------|---------| +| Primary identifier / name | Filename (slugified) | +| Category / kind | `type` field | +| Short description | `description` field | +| Tags / labels | `tags` field | +| URL / link | `resource` field | +| Last modified date | `timestamp` field | +| All other columns | Body content (as table or sections) | + +2. **Generate one `.md` per row:** + +```markdown +--- +type: {category_column} +title: {name_column} +description: {description_column} +tags: [{tag1}, {tag2}] +timestamp: {date_column}T00:00:00Z +--- + +# {name_column} + +| Field | Value | +|-------|-------| +| Column3 | {value} | +| Column4 | {value} | +``` + +3. **Generate index.md** from the full list: + +```markdown +# {Sheet Name} + +- [{row1_name}](./{row1_slug}.md) - {row1_description} +- [{row2_name}](./{row2_slug}.md) - {row2_description} +``` + +4. **Generate log.md** with creation entry: + +```markdown +# Update Log + +## {today_iso8601} +- **Creation**: Generated {N} concepts from spreadsheet import. +``` + +### Edge cases + +- Empty cells: omit the field entirely (don't write empty strings) +- Multi-value cells (comma-separated): parse into YAML list for `tags` +- Very long text cells: put in body as a section, not in frontmatter +- Duplicate names: append a disambiguator (e.g., `widget-v1.md`, `widget-v2.md`) diff --git a/.github/skills/okf-open-knowledge-format/references/examples.md b/.github/skills/okf-open-knowledge-format/references/examples.md new file mode 100644 index 0000000..9bd6bbe --- /dev/null +++ b/.github/skills/okf-open-knowledge-format/references/examples.md @@ -0,0 +1,302 @@ +# OKF Bundle Examples + +Three complete, conformant bundles across different domains. + +--- + +## 1. E-commerce Analytics + +``` +ecommerce/ +├── index.md +├── tables/ +│ ├── index.md +│ ├── orders.md +│ └── customers.md +└── metrics/ + ├── index.md + └── gross-revenue.md +``` + +### tables/orders.md + +```markdown +--- +type: BigQuery Table +title: Orders +description: One row per completed customer order across all channels. +resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=orders +tags: [sales, orders, revenue] +timestamp: 2026-05-28T14:30:00Z +--- + +# Schema + +| Column | Type | Description | +|--------|------|-------------| +| `order_id` | STRING | Globally unique order identifier | +| `customer_id` | STRING | FK to [customers](./customers.md) | +| `total_usd` | NUMERIC | Order total in US dollars | +| `placed_at` | TIMESTAMP | When the customer submitted the order | +| `channel` | STRING | Acquisition channel (web, mobile, pos) | + +# Joins + +- Join with [customers](./customers.md) on `customer_id` +- Referenced by [gross revenue](/metrics/gross-revenue.md) metric + +# Citations + +[1] [BigQuery schema docs](https://cloud.google.com/bigquery/docs/schemas) +``` + +### tables/customers.md + +```markdown +--- +type: BigQuery Table +title: Customers +description: One row per registered customer with profile and lifetime data. +resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=customers +tags: [sales, customers] +timestamp: 2026-05-28T14:30:00Z +--- + +# Schema + +| Column | Type | Description | +|--------|------|-------------| +| `customer_id` | STRING | Primary key | +| `email` | STRING | Customer email (hashed in prod) | +| `created_at` | TIMESTAMP | Registration date | +| `ltv_usd` | NUMERIC | Lifetime value in USD | + +# Joins + +- Referenced by [orders](./orders.md) on `customer_id` +``` + +### metrics/gross-revenue.md + +```markdown +--- +type: Metric +title: Gross Revenue +description: Total revenue before refunds and discounts. +tags: [revenue, finance, kpi] +timestamp: 2026-05-28T14:30:00Z +--- + +# Definition + +Sum of `total_usd` from [orders](/tables/orders.md) for a given period. +Does not subtract refunds — see Net Revenue for that. + +# SQL + +```sql +SELECT DATE_TRUNC(placed_at, MONTH) as month, + SUM(total_usd) as gross_revenue +FROM `acme.sales.orders` +GROUP BY 1 +``` + +# Related + +- Source table: [orders](/tables/orders.md) +- Counterpart: Net Revenue (gross minus refunds) +``` + +### index.md (root) + +```markdown +# E-commerce Analytics Bundle + +- [Tables](./tables/) - Database tables powering the analytics stack +- [Metrics](./metrics/) - Business KPIs derived from tables +``` + +--- + +## 2. SaaS Incident Playbooks + +``` +incidents/ +├── index.md +├── alerts/ +│ ├── index.md +│ ├── api-latency-p99.md +│ └── db-connections.md +└── runbooks/ + ├── index.md + └── escalate-incident.md +``` + +### alerts/api-latency-p99.md + +```markdown +--- +type: Alert +title: API Latency P99 > 2s +description: Fires when 99th percentile API latency exceeds 2 seconds for 5 minutes. +tags: [api, latency, critical] +severity: critical +timestamp: 2026-06-01T09:00:00Z +--- + +# Trigger Condition + +```promql +histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 2 +``` + +# Impact + +Users experience timeouts. Downstream services may cascade-fail. + +# Response + +1. Check [DB connections alert](./db-connections.md) — often the root cause +2. Follow [escalation runbook](/runbooks/escalate-incident.md) if not resolved in 10 min +3. Check deployment log for recent changes + +# Citations + +[1] [SLA definition](https://wiki.internal/sla/api-latency) +``` + +### runbooks/escalate-incident.md + +```markdown +--- +type: Runbook +title: Escalate Incident +description: Steps to escalate when on-call cannot resolve within SLA. +tags: [oncall, incident, escalation] +timestamp: 2026-06-01T09:00:00Z +--- + +# When to Escalate + +- Alert not resolved within 10 minutes +- Customer-facing impact confirmed +- Multiple alerts firing simultaneously + +# Steps + +1. Post in #incidents Slack channel with alert link +2. Page the secondary on-call (PagerDuty) +3. If P1: page Engineering Manager +4. Start incident document from template +5. Update status page if customer-facing + +# Contacts + +| Role | Who | Method | +|------|-----|--------| +| Secondary on-call | Rotation | PagerDuty | +| Eng Manager | @manager | Slack DM | +| Infra lead | @infra-lead | Slack DM | +``` + +--- + +## 3. API Documentation + +``` +api/ +├── index.md +├── auth/ +│ ├── index.md +│ └── oauth2-flow.md +├── endpoints/ +│ ├── index.md +│ └── create-order.md +└── policies/ + ├── index.md + └── rate-limits.md +``` + +### endpoints/create-order.md + +```markdown +--- +type: API Endpoint +title: Create Order +description: Creates a new order for an authenticated customer. +resource: https://api.acme.com/v2/orders +tags: [orders, write, v2] +method: POST +timestamp: 2026-05-20T10:00:00Z +--- + +# POST /v2/orders + +Creates a new order. Requires [OAuth2 authentication](/auth/oauth2-flow.md). + +# Request + +```json +{ + "customer_id": "cust_abc123", + "items": [{"sku": "WIDGET-01", "quantity": 2}], + "idempotency_key": "unique-request-id" +} +``` + +# Response (201 Created) + +```json +{ + "order_id": "ord_xyz789", + "status": "pending", + "total_usd": 49.98, + "created_at": "2026-05-20T10:30:00Z" +} +``` + +# Errors + +| Code | Meaning | +|------|---------| +| 400 | Invalid request body | +| 401 | Missing or invalid auth token | +| 409 | Duplicate idempotency_key | +| 429 | [Rate limit](/policies/rate-limits.md) exceeded | + +# Rate Limits + +Subject to [rate limiting](/policies/rate-limits.md). See `X-RateLimit-*` headers. +``` + +### policies/rate-limits.md + +```markdown +--- +type: Policy +title: Rate Limits +description: Per-plan rate limits for all API endpoints. +tags: [policy, rate-limit, api] +timestamp: 2026-05-20T10:00:00Z +--- + +# Limits by Plan + +| Plan | Requests/min | Burst | +|------|-------------|-------| +| Free | 60 | 10 | +| Pro | 600 | 100 | +| Enterprise | 6000 | 1000 | + +# Response Headers + +Every response includes: +- `X-RateLimit-Limit`: max requests per window +- `X-RateLimit-Remaining`: requests left in window +- `X-RateLimit-Reset`: Unix timestamp of window reset + +# When Exceeded + +Returns `429 Too Many Requests`. Retry after `X-RateLimit-Reset`. +Applies to all endpoints including [create order](/endpoints/create-order.md). +``` diff --git a/.github/skills/okf-open-knowledge-format/references/spec-v01.md b/.github/skills/okf-open-knowledge-format/references/spec-v01.md new file mode 100644 index 0000000..55d0a46 --- /dev/null +++ b/.github/skills/okf-open-knowledge-format/references/spec-v01.md @@ -0,0 +1,451 @@ +# Open Knowledge Format (OKF) + +**Version 0.1 — Draft** + +OKF is an open, human- and agent-friendly format for representing +*knowledge* — the metadata, context, and curated insight that surrounds +data and systems. It is designed to be authored by people, generated by +agents, exchanged across organizations, and consumed by both. + +The format is intentionally minimal: a directory of markdown files with +YAML frontmatter. There is no schema registry, no central authority, and +no required tooling. If you can `cat` a file, you can read OKF; if you +can `git clone` a repo, you can ship it. + +--- + +## 1. Motivation + +The space of knowledge representation for AI agents is evolving quickly, +and many incompatible conventions are emerging. OKF takes the position +that knowledge is best represented in commonly accessible, established +formats that are: + +- **Readable** by humans without tooling. +- **Parseable** by agents without bespoke SDKs. +- **Diffable** in version control. +- **Portable** across tools, organizations, and time. + +The format is minimally opinionated. It standardizes only the small set +of structural conventions needed to make a knowledge corpus +*self-describing* — anything beyond that is left to the producer. + +### Goals + +1. Define a universal format that **enrichment agents** can write into. +2. Inform how **consumption agents** should read and traverse it. +3. Facilitate **exchange** of knowledge across systems and organizations. +4. Standardize the small number of **required** fields that must be + present for content to be meaningfully consumed. + +### Non-goals + +- Defining a fixed taxonomy of concept types. +- Prescribing storage, serving, or query infrastructure. +- Replacing domain-specific schemas (Avro, Protobuf, OpenAPI, etc.) — + OKF *references* them; it does not subsume them. + +--- + +## 2. Terminology + +- **Knowledge Bundle** — A self-contained, hierarchical collection of + knowledge documents. The unit of distribution. +- **Concept** — A single unit of knowledge within a bundle. Represented + as one markdown document. May describe a tangible asset (a table, an + API), an abstract idea (a metric, a business process), or anything in + between. +- **Concept ID** — The path of the concept's file within the bundle, + with the `.md` suffix removed. For example, `tables/users.md` has + concept ID `tables/users`. +- **Frontmatter** — YAML metadata block delimited by `---` at the top of + a markdown file. +- **Body** — Everything in the file after the frontmatter. +- **Link** — A standard markdown link from one concept to another, used + to express relationships beyond the implicit parent/child hierarchy. +- **Citation** — A link from a concept to an external source that + supports a claim in the body. + +--- + +## 3. Bundle Structure + +A bundle is a directory tree of markdown files. The directory structure +is independent of the domain — producers organize concepts however makes +sense for the knowledge being captured. + +``` +path/to/bundle/ +├── index.md # Optional. Directory listing for progressive disclosure. +├── log.md # Optional. Chronological history of updates. +├── .md # A concept at the bundle root. +└── / # Subdirectories organize concepts into groups. + ├── index.md + ├── .md + └── / + └── … +``` + +A bundle MAY be distributed as: + +- A git repository (recommended — provides history, attribution, diffs). +- A tarball or zip archive of the directory. +- A subdirectory within a larger repository. + +### 3.1 Reserved filenames + +The following filenames have defined meaning at any level of the +hierarchy and MUST NOT be used for concept documents: + +| Filename | Purpose | +|--------------|--------------------------------------------------------| +| `index.md` | Directory listing. See §6. | +| `log.md` | Update history. See §7. | + +All other `.md` files are concept documents. + +Tags themselves remain a first-class concept — see the `tags` +frontmatter field in §4.1. OKF does not specify a separate file format +for aggregating documents by tag; producers that want a tag-browsing +view can synthesize one at consumption time by scanning frontmatter. + +--- + +## 4. Concept Documents + +Every concept is a UTF-8 markdown file. It has two parts: + +1. A **YAML frontmatter block**, delimited by `---` on its own line at + the start of the file and a closing `---` on its own line. +2. A **markdown body**, containing free-form content. + +### 4.1 Frontmatter + +```yaml +--- +type: # REQUIRED +title: +description: +resource: +tags: [, , …] # Optional +timestamp: # Optional last-modified time +# … other producer-defined key/value pairs +--- +``` + +**Required:** + +- `type` — A short string identifying the kind of concept. Consumers + use this for routing, filtering, and presentation. Example values: + `BigQuery Table`, `BigQuery Dataset`, `API Endpoint`, `Metric`, + `Playbook`, `Reference`. + + Type values are **not** registered centrally. Producers SHOULD pick + values that are descriptive and self-explanatory; consumers MUST + tolerate unknown types gracefully (typically by treating them as + generic concepts). + +**Recommended (in priority order):** + +- `title` — Human-readable display name. If omitted, consumers MAY + derive a title from the filename. +- `description` — A single sentence summarizing the concept. Used by + `index.md` generators, search snippets, and previews. +- `resource` — A URI that uniquely identifies the underlying asset the + concept describes. Absent for concepts that describe abstract ideas + rather than physical resources. +- `tags` — A YAML list of short strings for cross-cutting categorization. +- `timestamp` — ISO 8601 datetime of last meaningful change. + +**Extensions:** Producers MAY include any additional keys. Consumers +SHOULD preserve unknown keys when round-tripping and SHOULD NOT reject +documents with unrecognized fields. + +### 4.2 Body + +The body is standard markdown. Producers SHOULD favor structural +markdown — headings, lists, tables, fenced code blocks — over freeform +prose, since structure aids both human reading and agent retrieval. + +There are no required body sections. The following section headings have +**conventional** meaning and SHOULD be used when applicable: + +| Heading | Purpose | +|----------------|--------------------------------------------------------| +| `# Schema` | Structured description of an asset's columns/fields. | +| `# Examples` | Concrete usage examples, often as fenced code blocks. | +| `# Citations` | External sources backing claims in the body. See §8. | + +### 4.3 Example: a concept bound to a resource + +```markdown +--- +type: BigQuery Table +title: Customer Orders +description: One row per completed customer order across all channels. +resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=orders +tags: [sales, orders, revenue] +timestamp: 2026-05-28T14:30:00Z +--- + +# Schema + +| Column | Type | Description | +|---------------|-----------|------------------------------------------| +| `order_id` | STRING | Globally unique order identifier. | +| `customer_id` | STRING | Foreign key into [customers](/tables/customers.md). | +| `total_usd` | NUMERIC | Order total in US dollars. | +| `placed_at` | TIMESTAMP | When the customer submitted the order. | + +# Joins + +Joined with [customers](/tables/customers.md) on `customer_id`. + +# Citations + +[1] [BigQuery table schema](https://console.cloud.google.com/bigquery?p=acme&d=sales&t=orders) +``` + +### 4.4 Example: a concept not bound to a resource + +```markdown +--- +type: Playbook +title: Incident response — data freshness alert +description: Steps to triage a freshness alert on the orders pipeline. +tags: [oncall, incident] +timestamp: 2026-04-12T09:00:00Z +--- + +# Trigger + +A freshness alert fires when `orders` lags more than 30 minutes behind +its expected SLA. See the [orders table](/tables/orders.md). + +# Steps + +1. Check the [ingestion job dashboard](https://example.com/dash). +2. … +``` + +--- + +## 5. Cross-linking + +Concepts MAY link to other concepts using standard markdown links. Two +forms are supported: + +### 5.1 Absolute (bundle-relative) links + +Begin with `/`, interpreted relative to the bundle root. + +```markdown +See the [customers table](/tables/customers.md) for the join key. +``` + +This is the **recommended** form because it is stable when documents are +moved within their subdirectory. + +### 5.2 Relative links + +Standard markdown relative paths. + +```markdown +See the [neighboring concept](./other.md). +``` + +### 5.3 Link semantics + +A link from concept A to concept B asserts a *relationship*. The +specific kind of relationship (parent/child, references, joins-with, +depends-on, etc.) is conveyed by the surrounding prose, not by the link +itself. Consumers that build a graph view typically treat all links as +directed edges of an untyped relationship. + +Consumers MUST tolerate broken links — a link whose target does not +exist in the bundle is not malformed; it may simply represent +not-yet-written knowledge. + +--- + +## 6. Index Files + +An `index.md` file MAY appear in any directory, including the bundle +root. It enumerates the directory's contents to support **progressive +disclosure** — letting a human or agent see what is available before +opening individual documents. + +Index files contain no frontmatter. The body uses one or more sections, +each grouping concepts under a heading: + +```markdown +# Section / Group Heading + +* [Title 1](relative-url-1) - short description of item 1 +* [Title 2](relative-url-2) - short description of item 2 + +# Another Section + +* [Subdirectory](subdir/) - short description of the subdirectory +``` + +Entries SHOULD include the description from the linked concept's +frontmatter. Producers MAY generate `index.md` automatically; consumers +MAY synthesize one on the fly when none is present. + +--- + +## 7. Log Files (optional) + +A `log.md` file MAY appear at any level of the hierarchy to record the +history of changes to that scope. The format is a flat list of +date-grouped entries, newest first: + +```markdown +# Directory Update Log + +## 2026-05-22 +* **Update**: Added new BigQuery table reference for [Customer Metrics](/tables/customer-metrics.md). +* **Creation**: Established the [Dataplex Playbook](/playbooks/dataplex.md). + +## 2026-05-15 +* **Initialization**: Created foundational directory structure. +* **Update**: Added progressive-disclosure guidelines to the root [index](/index.md). +``` + +Date headings MUST use ISO 8601 `YYYY-MM-DD` form. Log entries are +prose; the leading bold word (`**Update**`, `**Creation**`, +`**Deprecation**`, etc.) is a convention, not a requirement. + +--- + +## 8. Citations + +When a concept's body makes claims sourced from external material, +those sources SHOULD be listed under a `# Citations` heading at the +bottom of the document, numbered: + +```markdown +# Citations + +[1] [BigQuery public dataset announcement](https://cloud.google.com/blog/products/data-analytics/...) +[2] [Internal data quality runbook](https://wiki.acme.internal/data/quality) +``` + +Citation links MAY be absolute URLs, bundle-relative paths, or paths +into a `references/` subdirectory that mirrors external material as +first-class OKF concepts. + +--- + +## 9. Conformance + +A bundle is **conformant** with OKF v0.1 if: + +1. Every non-reserved `.md` file in the tree contains a parseable YAML + frontmatter block. +2. Every frontmatter block contains a non-empty `type` field. +3. Every reserved filename (`index.md`, `log.md`) follows the structure + described in §6 and §7 respectively when present. + +Consumers SHOULD treat all other constraints as soft guidance. In +particular, consumers MUST NOT reject a bundle because of: + +- Missing optional frontmatter fields. +- Unknown `type` values. +- Unknown additional frontmatter keys. +- Broken cross-links. +- Missing `index.md` files. + +This permissive consumption model is intentional: OKF is meant to +remain useful as bundles grow, get refactored, and are partially +generated by agents. + +--- + +## 10. Relationship to other formats + +OKF is intentionally close to several established patterns: + +- **LLM "wiki" repositories** that use markdown + frontmatter as + agent-readable knowledge bases. +- **Personal knowledge tools** like Obsidian and Notion, which use + hierarchical markdown with cross-links. +- **"Metadata as code"** approaches that store catalog metadata + alongside source code rather than in a separate registry. + +OKF differs primarily in being **specified** — pinning down the small +set of rules needed for interoperability without dictating tooling. + +--- + +## 11. Versioning + +This document specifies OKF version **0.1**. Future revisions will be +versioned in the form `.`: + +- A **minor** version bump introduces backward-compatible additions + (new optional fields, new conventional section headings). +- A **major** version bump may make breaking changes (renaming required + fields, changing reserved filenames). + +Bundles MAY declare the OKF version they target by including +`okf_version: "0.1"` in a bundle-root `index.md` frontmatter block (the +only place frontmatter is permitted in an `index.md`). Consumers that +do not understand the declared version SHOULD attempt best-effort +consumption rather than refusing the bundle. + +--- + +## Appendix A — Minimal example bundle + +``` +my_bundle/ +├── index.md +├── datasets/ +│ ├── index.md +│ └── sales.md +└── tables/ + ├── index.md + ├── orders.md + └── customers.md +``` + +`datasets/sales.md`: + +```markdown +--- +type: BigQuery Dataset +title: Sales +description: All sales-related tables for the retail business. +resource: https://console.cloud.google.com/bigquery?p=acme&d=sales +tags: [sales] +timestamp: 2026-05-28T00:00:00Z +--- + +The sales dataset contains transactional tables, including +[orders](/tables/orders.md) and [customers](/tables/customers.md). +``` + +`tables/orders.md`: + +```markdown +--- +type: BigQuery Table +title: Orders +description: One row per completed customer order. +resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=orders +tags: [sales, orders] +timestamp: 2026-05-28T00:00:00Z +--- + +# Schema + +| Column | Type | Description | +|---------------|-----------|------------------------------| +| `order_id` | STRING | Unique order identifier. | +| `customer_id` | STRING | FK to [customers](/tables/customers.md). | +| `total_usd` | NUMERIC | Order total in USD. | + +Part of the [sales dataset](/datasets/sales.md). +``` diff --git a/.github/skills/okf-open-knowledge-format/scripts/validate.sh b/.github/skills/okf-open-knowledge-format/scripts/validate.sh new file mode 100644 index 0000000..25917be --- /dev/null +++ b/.github/skills/okf-open-knowledge-format/scripts/validate.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# OKF Bundle Validator v0.1 +# Usage: validate.sh +# Checks conformance with OKF v0.1 spec: +# E1: All non-reserved .md files have YAML frontmatter +# E2: All frontmatter has non-empty 'type' field +# E3: Reserved files follow structure rules + +set -euo pipefail + +BUNDLE="${1:-.}" +ERRORS=0 +WARNINGS=0 +TOTAL=0 + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +if [ ! -d "$BUNDLE" ]; then + echo -e "${RED}Error: '$BUNDLE' is not a directory${NC}" + exit 1 +fi + +echo "Validating OKF bundle: $BUNDLE" +echo "---" + +# Find all .md files +while IFS= read -r -d '' file; do + TOTAL=$((TOTAL + 1)) + relative="${file#$BUNDLE/}" + basename=$(basename "$file") + + # Skip reserved files (validate separately) + if [[ "$basename" == "index.md" || "$basename" == "log.md" ]]; then + # E3: Check reserved file structure + if [[ "$basename" == "index.md" ]]; then + # index.md should NOT have frontmatter (except bundle root may have okf_version) + if head -1 "$file" | grep -q "^---$"; then + # Allow only if it's bundle root and contains okf_version + if [[ "$relative" != "index.md" ]]; then + echo -e "${RED}E3: $relative — index.md should not have frontmatter${NC}" + ERRORS=$((ERRORS + 1)) + fi + fi + fi + if [[ "$basename" == "log.md" ]]; then + # log.md should have date headings in YYYY-MM-DD format + if ! grep -qE "^## [0-9]{4}-[0-9]{2}-[0-9]{2}" "$file" 2>/dev/null; then + if [ -s "$file" ]; then + echo -e "${YELLOW}W: $relative — log.md has no ISO 8601 date headings${NC}" + WARNINGS=$((WARNINGS + 1)) + fi + fi + fi + continue + fi + + # E1: Check for YAML frontmatter + if ! head -1 "$file" | grep -q "^---$"; then + echo -e "${RED}E1: $relative — no YAML frontmatter${NC}" + ERRORS=$((ERRORS + 1)) + continue + fi + + # Extract frontmatter (between first --- and second ---) + frontmatter=$(sed -n '2,/^---$/p' "$file" | sed '$d') + + # E2: Check for non-empty type field + type_value=$(echo "$frontmatter" | grep -E "^type:" | sed 's/^type:\s*//' | tr -d '"' | tr -d "'" | xargs) + if [ -z "$type_value" ]; then + echo -e "${RED}E2: $relative — missing or empty 'type' field${NC}" + ERRORS=$((ERRORS + 1)) + continue + fi + + # Warnings for recommended fields + if ! echo "$frontmatter" | grep -qE "^title:"; then + echo -e "${YELLOW}W1: $relative — missing recommended 'title' field${NC}" + WARNINGS=$((WARNINGS + 1)) + fi + if ! echo "$frontmatter" | grep -qE "^description:"; then + echo -e "${YELLOW}W1: $relative — missing recommended 'description' field${NC}" + WARNINGS=$((WARNINGS + 1)) + fi + +done < <(find "$BUNDLE" -name "*.md" -type f -print0 | sort -z) + +# Summary +echo "---" +echo "Files scanned: $TOTAL" +if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}✅ Bundle is OKF v0.1 conformant${NC}" +else + echo -e "${RED}❌ $ERRORS error(s) — bundle is NOT conformant${NC}" +fi +if [ $WARNINGS -gt 0 ]; then + echo -e "${YELLOW}⚠ $WARNINGS warning(s)${NC}" +fi + +exit $ERRORS diff --git a/.github/skills/pier-cloud/.env.example b/.github/skills/pier-cloud/.env.example new file mode 100644 index 0000000..36eb8df --- /dev/null +++ b/.github/skills/pier-cloud/.env.example @@ -0,0 +1,9 @@ +PIERCLOUD_CLIENT_ID= +PIERCLOUD_CLIENT_SECRET= +PIERCLOUD_TENANCY_ID= +# Legado (TENANCY_ID tem prioridade, mas BUSINESS_ID ainda funciona como fallback) +# PIERCLOUD_BUSINESS_ID= +# PIERCLOUD_ORG_ID= (nao mais necessario na nova API) + + + diff --git a/.github/skills/pier-cloud/SKILL.md b/.github/skills/pier-cloud/SKILL.md new file mode 100644 index 0000000..97a7a78 --- /dev/null +++ b/.github/skills/pier-cloud/SKILL.md @@ -0,0 +1,103 @@ +--- +name: "pier-cloud" +description: "This skill should be used when the user needs to consume the Pier Cloud (Lighthouse) API for cloud cost management — including JWT authentication, listing contexts, workspaces, and FinOps data views. Trigger whenever there is a need to integrate, automate, or debug calls to the Pier Cloud platform via Python, Node.js, or cURL." +metadata: + author: ft.ia.br + version: "1.1" + date: 2026-03-05 + repository: https://github.com/fabricioctelles/skills + license: Apache 2.0 + keywords: ["pier", "piercloud", "lighthouse", "api", "finops", "cloud", "costs"] + category: library-and-api-reference +--- + +# Pier Cloud API + +## Prerequisites + +### Credentials + +Locate the `.env` file in the skill directory with the following variables: + +```env +PIERCLOUD_CLIENT_ID=your_client_id +PIERCLOUD_CLIENT_SECRET=your_client_secret +PIERCLOUD_TENANCY_ID=your_tenancy_id +``` + +If the `.env` file does not exist, inform the user that credentials must be obtained from the Pier Cloud platform before proceeding. Do not proceed without the `.env` file. + +> Note: `PIERCLOUD_TENANCY_ID` is equivalent to the former `PIERCLOUD_BUSINESS_ID`. Scripts accept both as fallback. + +### Python Dependencies + +```bash +pip install requests python-dotenv +``` + +## Basic Configuration + +The API uses JWT authentication. Required flow: + +1. Authenticate via `POST /auth` with `client_id` and `client_secret` to obtain a JWT token +2. Include the token in all requests: `Authorization: Bearer {token}` +3. Renew the token upon expiration (default validity: 1 hour) + +**Base URL**: `https://api.piercloud.io` + +Verify the connection by running: + +```bash +python scripts/pier-cloud-auth.py +``` + +## Available Scripts + +Ready-to-use scripts in `scripts/`. See `scripts/README.md` for detailed instructions. + +| Script | Description | +|--------|-------------| +| `pier-cloud-auth.py` | Authenticate and obtain JWT token | +| `pier-cloud-list-contexts.py` | List available contexts | +| `pier-cloud-list-workspaces.py` | List workspaces with pagination | +| `pier-cloud-get-workspace.py` | Get specific workspace details | +| `pier-cloud-get-all-workspaces.py` | Get all workspaces (automatic pagination) | +| `pier-cloud-list-views.py` | List views for a workspace | +| `pier-cloud-get-view.py` | Get specific view information | +| `pier-cloud-get-view-data.py` | Get view data with filters | +| `pier_cloud_client.py` | Robust client with CLI and reusable library | + +> Note: Workspace-groups scripts (`pier-cloud-list-workspace-groups.py`, `pier-cloud-get-workspace-group.py`) do not work — the corresponding endpoints do not exist in the current API. + +## Workflows + +Follow the detailed workflows with request and response examples in `references/REFERENCE.md`: + +- **Workflow 1** — Authentication and Token Retrieval +- **Workflow 2** — List Contexts +- **Workflow 3** — List Workspaces +- **Workflow 4** — Get Workspace Details +- **Workflow 5** — Get All Workspaces (Automatic Pagination) +- **Workflow 6** — Robust Client with Retry and Token Renewal +- **Workflow 9** — List Workspace Views +- **Workflow 10** — Get View Information +- **Workflow 11** — Get View Data with Filters + +For endpoint reference, parameters, response structures, and cURL examples, see `references/REFERENCE.md`. + +For error diagnosis (401, 403, 404, timeout, rate limiting), see `references/TROUBLESHOOTING.md`. + +## Additional Resources + +- **API Docs**: https://docs.piercloud.com/api-docs-pier-cloud +- **Pier Cloud Platform**: https://piercloud.com/en/ + +## Quality Checklist + +- [ ] `.env` file present with `PIERCLOUD_CLIENT_ID`, `PIERCLOUD_CLIENT_SECRET`, and `PIERCLOUD_TENANCY_ID` +- [ ] Python dependencies installed (`requests`, `python-dotenv`) +- [ ] Authentication successful (JWT token obtained without errors) +- [ ] Correct endpoint being used (default `/lighthouse/tenancies/{tenancy_id}/...`) +- [ ] Token being renewed before expiration in long sessions +- [ ] Workspace/view IDs confirmed via listing before using directly +- [ ] Errors handled per `references/TROUBLESHOOTING.md` diff --git a/.github/skills/pier-cloud/references/TROUBLESHOOTING.md b/.github/skills/pier-cloud/references/TROUBLESHOOTING.md new file mode 100644 index 0000000..952c224 --- /dev/null +++ b/.github/skills/pier-cloud/references/TROUBLESHOOTING.md @@ -0,0 +1,115 @@ +## Troubleshooting + +### Error 401 - Invalid Credentials + +**Problem**: Authentication fails with error 401 + +**Symptoms**: +```json +{ + "code": "failed", + "message": "invalid or expired token" +} +``` + +**Common Causes**: +- Incorrect `client_id` or `client_secret` +- Credentials not registered on the Pier Cloud platform +- Environment variables not loaded correctly + +**Solutions**: +1. Check credentials in the `.env` file +2. Confirm that variables are being loaded +3. Validate credentials with the Pier Cloud team +4. Verify that the HTTP client is registered on the platform + +### Error 403 - Access Denied + +**Problem**: Valid token but no permission to access resource + +**Symptoms**: +```json +{ + "code": "authorization/forbidden", + "message": "Access denied" +} +``` + +**Causes**: +- Account without adequate permissions +- Incorrect `tenancy_id` +- Resource does not belong to the specified tenant + +**Solutions**: +1. Check account permissions on the Pier Cloud platform +2. Confirm correct `tenancy_id` +3. Contact administrator to request permissions + +### Error 404 - Resource Not Found + +**Problem**: Endpoint or resource does not exist + +**Symptoms**: +```json +{ + "code": "workspace/not-found", + "message": "Workspace not found" +} +``` + +**Causes**: +- Incorrect or non-existent `workspace_id` +- Invalid `tenancy_id` +- Incorrect endpoint URL + +**Solutions**: +1. List all workspaces first to verify available IDs +2. Confirm endpoint URL is correct +3. Validate tenancy_id + +### Expired Token + +**Problem**: JWT token expired after ~1 hour + +**Symptoms**: +- Requests that were working start returning 401 +- Error "invalid or expired token" + +**Solution**: + +Use the robust client that implements automatic renewal: + +```bash +python scripts/pier_cloud_client.py --action list-contexts +``` + +The `pier_cloud_client.py` client automatically renews the token before it expires. + +### Connection Timeout + +**Problem**: Request takes too long or does not respond + +**Symptoms**: +- Timeout after 30+ seconds +- Connection not established +- Network error + +**Solutions**: +1. Check internet connectivity +2. Test API availability: `curl -I https://api.piercloud.io/auth` +3. Check if there is a proxy or firewall blocking +4. Try again after a few minutes + +### Rate Limiting (Too Many Requests) + +**Problem**: API returns error 429 (Too Many Requests) + +**Symptoms**: +- Error 429 after several rapid requests +- Message about rate limit + +**Solutions**: +1. Use the robust client that implements automatic retry +2. Reduce request frequency +3. Implement delays between requests +4. Use pagination with smaller `page_size` if needed diff --git a/.github/skills/pier-cloud/scripts/README.md b/.github/skills/pier-cloud/scripts/README.md new file mode 100644 index 0000000..d94b109 --- /dev/null +++ b/.github/skills/pier-cloud/scripts/README.md @@ -0,0 +1,166 @@ +# Scripts da API Pier Cloud + +Scripts prontos para consumir a API Pier Cloud (Lighthouse). + +## Prerequisitos + +```bash +pip install requests python-dotenv +``` + +## Configuracao + +Crie arquivo `.env` na raiz do projeto: + +```env +PIERCLOUD_CLIENT_ID=seu_client_id +PIERCLOUD_CLIENT_SECRET=seu_client_secret +PIERCLOUD_TENANCY_ID=seu_tenancy_id +``` + +> **Nota**: O `TENANCY_ID` corresponde ao antigo `BUSINESS_ID`. Se voce ja tem `PIERCLOUD_BUSINESS_ID` no `.env`, os scripts usam como fallback automaticamente. + +## API - Mudanca de Endpoints (Fev 2026) + +A API Pier Cloud atualizou seus endpoints: + +- **Antes**: `/lighthouse/orgs/{org_id}/businesses/{business_id}/...` +- **Agora**: `/lighthouse/tenancies/{tenancy_id}/...` + +O `PIERCLOUD_ORG_ID` nao e mais necessario. O `tenancy_id` equivale ao antigo `BUSINESS_ID`. + +Documentacao oficial: https://docs.piercloud.com/api-docs-pier-cloud + +## Scripts Disponiveis + +### 1. pier-cloud-auth.py +Autentica e obtem token JWT. + +```bash +python scripts/pier-cloud-auth.py +``` + +### 2. pier-cloud-list-contexts.py +Lista todos os contextos disponiveis. + +```bash +python scripts/pier-cloud-list-contexts.py +``` + +### 3. pier-cloud-list-workspaces.py +Lista workspaces com paginacao. + +```bash +# Padrao (pagina 1, 10 itens) +python scripts/pier-cloud-list-workspaces.py + +# Pagina especifica +python scripts/pier-cloud-list-workspaces.py --page 2 --page-size 50 + +# Ordenar por data +python scripts/pier-cloud-list-workspaces.py --sort-field created_at --sort-order DESC +``` + +### 4. pier-cloud-get-workspace.py +Obtem detalhes de workspace especifico. + +```bash +python scripts/pier-cloud-get-workspace.py --workspace-id 16969 +``` + +### 5. pier-cloud-get-all-workspaces.py +Obtem todos os workspaces com paginacao automatica. + +```bash +# Exibir no terminal +python scripts/pier-cloud-get-all-workspaces.py + +# Salvar em JSON +python scripts/pier-cloud-get-all-workspaces.py --output workspaces.json + +# Salvar em CSV +python scripts/pier-cloud-get-all-workspaces.py --output workspaces.csv --format csv +``` + +### 6. pier-cloud-list-views.py +Lista visualizacoes de um workspace. + +```bash +python scripts/pier-cloud-list-views.py --workspace-id 16969 +``` + +### 7. pier-cloud-get-view.py +Obtem informacoes de visualizacao especifica. + +```bash +python scripts/pier-cloud-get-view.py --view-id 193195 +``` + +### 8. pier-cloud-get-view-data.py +Obtem dados de uma visualizacao com filtros. + +```bash +# Basico +python scripts/pier-cloud-get-view-data.py --view-id 193195 + +# Com periodo +python scripts/pier-cloud-get-view-data.py --view-id 193195 \ + --start-date 2026-01-01 --end-date 2026-01-31 + +# Com filtros +python scripts/pier-cloud-get-view-data.py --view-id 193195 \ + --filters '[{"name":"lineitem/usageaccountid","data_type":"string","role":"filter","filters":[{"expression":"IS","value":["123456"],"negative_expression":false}]}]' + +# Salvar em arquivo +python scripts/pier-cloud-get-view-data.py --view-id 193195 \ + --start-date 2026-01-01 --end-date 2026-01-31 --output dados.json +``` + +### 9. pier_cloud_client.py +Cliente robusto com CLI e biblioteca reutilizavel. + +**Como CLI**: +```bash +# Listar contextos +python scripts/pier_cloud_client.py --action list-contexts + +# Listar workspaces +python scripts/pier_cloud_client.py --action list-workspaces --page 1 --page-size 20 + +# Obter workspace +python scripts/pier_cloud_client.py --action get-workspace --workspace-id 16969 + +# Obter todos +python scripts/pier_cloud_client.py --action get-all-workspaces --output results.json +``` + +### 10. appscript-pier-cloud.gs +Codigo Google Apps Script para integrar com Google Sheets. + +Veja instrucoes no proprio arquivo. + +## Endpoints da API (Atualizado Fev 2026) + +| Metodo | Endpoint | Descricao | +|--------|----------|-----------| +| POST | `/auth` | Obter token JWT | +| GET | `/lighthouse/tenancies/{tenancy_id}/contexts` | Listar contextos | +| GET | `/lighthouse/tenancies/{tenancy_id}/workspaces` | Listar workspaces | +| GET | `/lighthouse/tenancies/{tenancy_id}/workspaces/{id}` | Obter workspace | +| GET | `/lighthouse/tenancies/{tenancy_id}/workspaces/{workspace_id}/views` | Listar views | +| GET | `/lighthouse/tenancies/{tenancy_id}/views/{id}` | Obter view | +| GET | `/lighthouse/tenancies/{tenancy_id}/views/{id}/data` | Obter dados da view | + +## Troubleshooting + +### Erro: Variaveis faltando +Verifique se o arquivo `.env` existe e contem `PIERCLOUD_TENANCY_ID` (ou `PIERCLOUD_BUSINESS_ID` como fallback). + +### Erro 401 +Credenciais invalidas. Verifique CLIENT_ID e CLIENT_SECRET. + +### Erro 403 +Sem permissao. Verifique TENANCY_ID. + +### Erro 404 +Recurso nao encontrado. Verifique IDs fornecidos. diff --git a/.github/skills/pier-cloud/scripts/pier-cloud-auth.py b/.github/skills/pier-cloud/scripts/pier-cloud-auth.py new file mode 100644 index 0000000..a5069f4 --- /dev/null +++ b/.github/skills/pier-cloud/scripts/pier-cloud-auth.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Script to authenticate with the Pier Cloud API and obtain a JWT token. +""" + +import requests +import os +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +API_BASE = "https://api.piercloud.io" +CLIENT_ID = os.getenv("PIERCLOUD_CLIENT_ID") +CLIENT_SECRET = os.getenv("PIERCLOUD_CLIENT_SECRET") + +def authenticate(): + """Obtain authentication token""" + print("Authenticating with Pier Cloud API...") + + url = f"{API_BASE}/auth" + payload = { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET + } + + try: + response = requests.post(url, json=payload, timeout=30) + + if response.status_code == 201: + data = response.json() + token = data['data']['access_token'] + expires_in = data['data']['expires_in'] + + print(f"\n✓ Token obtained successfully!") + print(f"Token: {token[:50]}...") + print(f"Expires in: {expires_in} seconds ({expires_in//60} minutes)") + print(f"Type: {data['data']['token_type']}") + + return token + else: + print(f"\n✗ Authentication error (Status {response.status_code})") + print(f"Response: {response.text}") + return None + + except requests.exceptions.RequestException as e: + print(f"\n✗ Connection error: {e}") + return None + +if __name__ == "__main__": + # Validate environment variables + if not CLIENT_ID or not CLIENT_SECRET: + print("✗ Error: PIERCLOUD_CLIENT_ID and PIERCLOUD_CLIENT_SECRET must be defined in .env") + exit(1) + + token = authenticate() + + if token: + print("\n✓ Authentication completed successfully!") + else: + print("\n✗ Authentication failed") + exit(1) diff --git a/.github/skills/pier-cloud/scripts/pier-cloud-get-all-workspaces.py b/.github/skills/pier-cloud/scripts/pier-cloud-get-all-workspaces.py new file mode 100644 index 0000000..cd478e3 --- /dev/null +++ b/.github/skills/pier-cloud/scripts/pier-cloud-get-all-workspaces.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +Script para obter todos os workspaces da API Pier Cloud com paginacao automatica. +""" + +import requests +import os +import argparse +import json +import csv +from dotenv import load_dotenv + +load_dotenv() + +API_BASE = "https://api.piercloud.io" +CLIENT_ID = os.getenv("PIERCLOUD_CLIENT_ID") +CLIENT_SECRET = os.getenv("PIERCLOUD_CLIENT_SECRET") +TENANCY_ID = os.getenv("PIERCLOUD_TENANCY_ID", os.getenv("PIERCLOUD_BUSINESS_ID")) + +def authenticate(): + """Obter token de autenticacao""" + url = f"{API_BASE}/auth" + payload = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET} + response = requests.post(url, json=payload, timeout=30) + + if response.status_code == 201: + return response.json()['data']['access_token'] + else: + raise Exception(f"Erro na autenticacao: {response.text}") + +def get_all_workspaces(token): + """Obter todos os workspaces com paginacao automatica""" + all_workspaces = [] + page = 1 + page_size = 100 # Maximo permitido + + while True: + url = f"{API_BASE}/lighthouse/tenancies/{TENANCY_ID}/workspaces" + headers = {"Authorization": f"Bearer {token}"} + params = {"page": page, "page_size": page_size} + + response = requests.get(url, headers=headers, params=params, timeout=30) + + if response.status_code != 200: + raise Exception(f"Erro na pagina {page}: {response.text}") + + data = response.json() + workspaces = data['data']['workspaces'] + meta = data['meta'] + + all_workspaces.extend(workspaces) + + print(f"OK Pagina {page}: {len(workspaces)} workspaces obtidos") + + # Verificar se ha mais paginas + if page * page_size >= meta['total']: + break + + page += 1 + + return all_workspaces + +def save_json(workspaces, filename): + """Salvar workspaces em arquivo JSON""" + with open(filename, 'w', encoding='utf-8') as f: + json.dump(workspaces, f, indent=2, ensure_ascii=False) + print(f"OK Salvo em {filename}") + +def save_csv(workspaces, filename): + """Salvar workspaces em arquivo CSV""" + if not workspaces: + print("Nenhum workspace para salvar") + return + + keys = ['id', 'name', 'description', 'access_scope', 'count_views', 'created_at'] + + with open(filename, 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=keys) + writer.writeheader() + + for ws in workspaces: + row = {k: ws.get(k, '') for k in keys} + writer.writerow(row) + + print(f"OK Salvo em {filename}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Obter todos os workspaces da API Pier Cloud") + parser.add_argument("--output", help="Arquivo de saida (JSON ou CSV)") + parser.add_argument("--format", choices=["json", "csv"], default="json", help="Formato de saida") + + args = parser.parse_args() + + # Validar variaveis + if not TENANCY_ID: + print("X Erro: PIERCLOUD_TENANCY_ID (ou PIERCLOUD_BUSINESS_ID) deve estar definido no .env") + exit(1) + required = ["CLIENT_ID", "CLIENT_SECRET"] + missing = [v for v in required if not os.getenv(f"PIERCLOUD_{v}")] + + if missing: + print(f"X Erro: Variaveis faltando no .env: {missing}") + exit(1) + + try: + print("Autenticando...") + token = authenticate() + print("OK Autenticado\n") + + print("Obtendo todos os workspaces...") + workspaces = get_all_workspaces(token) + + print(f"\nOK Total: {len(workspaces)} workspaces obtidos") + + # Salvar em arquivo se especificado + if args.output: + if args.format == "json": + save_json(workspaces, args.output) + else: + save_csv(workspaces, args.output) + + except Exception as e: + print(f"\nX Erro: {e}") + exit(1) diff --git a/.github/skills/pier-cloud/scripts/pier-cloud-get-view-data.py b/.github/skills/pier-cloud/scripts/pier-cloud-get-view-data.py new file mode 100644 index 0000000..630c3ef --- /dev/null +++ b/.github/skills/pier-cloud/scripts/pier-cloud-get-view-data.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +""" +Script para obter dados de uma visualizacao da API Pier Cloud com filtros. +""" + +import requests +import os +import argparse +import json +from datetime import datetime, timedelta +from dotenv import load_dotenv + +load_dotenv() + +API_BASE = "https://api.piercloud.io" +CLIENT_ID = os.getenv("PIERCLOUD_CLIENT_ID") +CLIENT_SECRET = os.getenv("PIERCLOUD_CLIENT_SECRET") +TENANCY_ID = os.getenv("PIERCLOUD_TENANCY_ID", os.getenv("PIERCLOUD_BUSINESS_ID")) + +def authenticate(): + """Obter token de autenticacao""" + url = f"{API_BASE}/auth" + payload = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET} + response = requests.post(url, json=payload, timeout=30) + + if response.status_code == 201: + return response.json()['data']['access_token'] + else: + raise Exception(f"Erro na autenticacao: {response.text}") + +def get_view_data(token, view_id, start_date=None, end_date=None, date_type="date", filters=None): + """Obter dados de uma visualizacao""" + url = f"{API_BASE}/lighthouse/tenancies/{TENANCY_ID}/views/{view_id}/data" + headers = {"Authorization": f"Bearer {token}"} + + # Parametros de query + params = {} + + if start_date: + params['start_date'] = start_date + if end_date: + params['end_date'] = end_date + if date_type: + params['date_type'] = date_type + if filters: + params['filters'] = json.dumps(filters) + + response = requests.get(url, headers=headers, params=params, timeout=60) + + if response.status_code == 200: + data = response.json() + results = data['data'] + + print(f"\n=== Dados da Visualizacao {view_id} ===") + print(f"Periodo: {start_date or 'inicio do mes'} ate {end_date or 'fim do mes'}") + print(f"Tipo de data: {date_type}") + print(f"Total de registros: {len(results)}\n") + + if results: + # Mostrar primeiros registros + print("Primeiros registros:") + for i, record in enumerate(results[:5]): + print(f"\nRegistro {i+1}:") + for key, value in record.items(): + print(f" {key}: {value}") + + if len(results) > 5: + print(f"\n... e mais {len(results) - 5} registros") + + return results + else: + raise Exception(f"Erro ao obter dados: {response.text}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Obter dados de visualizacao da API Pier Cloud") + parser.add_argument("--view-id", "--view_id", dest="view_id", type=int, required=True, help="ID da visualizacao") + parser.add_argument("--start-date", "--start_date", dest="start_date", help="Data inicial (YYYY-MM-DD)") + parser.add_argument("--end-date", "--end_date", dest="end_date", help="Data final (YYYY-MM-DD)") + parser.add_argument("--date-type", "--date_type", dest="date_type", choices=["date", "month"], default="date", + help="Tipo de filtro de data (date ou month)") + parser.add_argument("--filters", help="Filtros em formato JSON") + parser.add_argument("--output", help="Arquivo de saida JSON") + + args = parser.parse_args() + + # Validar variaveis + if not TENANCY_ID: + print("X Erro: PIERCLOUD_TENANCY_ID (ou PIERCLOUD_BUSINESS_ID) deve estar definido no .env") + exit(1) + required = ["CLIENT_ID", "CLIENT_SECRET"] + missing = [v for v in required if not os.getenv(f"PIERCLOUD_{v}")] + + if missing: + print(f"X Erro: Variaveis faltando no .env: {missing}") + exit(1) + + # Parse filters se fornecido + filters = None + if args.filters: + try: + filters = json.loads(args.filters) + except json.JSONDecodeError as e: + print(f"X Erro ao parsear filtros JSON: {e}") + exit(1) + + try: + print("Autenticando...") + token = authenticate() + print("OK Autenticado") + + results = get_view_data( + token, + args.view_id, + start_date=args.start_date, + end_date=args.end_date, + date_type=args.date_type, + filters=filters + ) + + # Salvar em arquivo se especificado + if args.output: + with open(args.output, 'w', encoding='utf-8') as f: + json.dump(results, f, indent=2, ensure_ascii=False) + print(f"\nOK Salvo em {args.output}") + + print(f"\nOK Total: {len(results)} registros obtidos") + + except Exception as e: + print(f"\nX Erro: {e}") + exit(1) diff --git a/.github/skills/pier-cloud/scripts/pier-cloud-get-view.py b/.github/skills/pier-cloud/scripts/pier-cloud-get-view.py new file mode 100644 index 0000000..9db6002 --- /dev/null +++ b/.github/skills/pier-cloud/scripts/pier-cloud-get-view.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +Script para obter informacoes de uma visualizacao especifica da API Pier Cloud. +""" + +import requests +import os +import argparse +from dotenv import load_dotenv + +load_dotenv() + +API_BASE = "https://api.piercloud.io" +CLIENT_ID = os.getenv("PIERCLOUD_CLIENT_ID") +CLIENT_SECRET = os.getenv("PIERCLOUD_CLIENT_SECRET") +TENANCY_ID = os.getenv("PIERCLOUD_TENANCY_ID", os.getenv("PIERCLOUD_BUSINESS_ID")) + +def authenticate(): + """Obter token de autenticacao""" + url = f"{API_BASE}/auth" + payload = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET} + response = requests.post(url, json=payload, timeout=30) + + if response.status_code == 201: + return response.json()['data']['access_token'] + else: + raise Exception(f"Erro na autenticacao: {response.text}") + +def get_view(token, view_id): + """Obter informacoes de uma visualizacao especifica""" + url = f"{API_BASE}/lighthouse/tenancies/{TENANCY_ID}/views/{view_id}" + headers = {"Authorization": f"Bearer {token}"} + + response = requests.get(url, headers=headers, timeout=30) + + if response.status_code == 200: + data = response.json() + view = data['data'] + + print(f"\n=== Visualizacao: {view['name']} ===\n") + print(f"ID: {view['id']}") + print(f"Nome: {view['name']}") + print(f"Descricao: {view.get('description', 'N/A')}") + + if 'workspace' in view: + print(f"\nWorkspace:") + print(f" ID: {view['workspace']['id']}") + print(f" Nome: {view['workspace']['name']}") + + return view + else: + raise Exception(f"Erro ao obter visualizacao: {response.text}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Obter informacoes de visualizacao da API Pier Cloud") + parser.add_argument("--view-id", "--view_id", dest="view_id", type=int, required=True, help="ID da visualizacao") + + args = parser.parse_args() + + # Validar variaveis + if not TENANCY_ID: + print("X Erro: PIERCLOUD_TENANCY_ID (ou PIERCLOUD_BUSINESS_ID) deve estar definido no .env") + exit(1) + required = ["CLIENT_ID", "CLIENT_SECRET"] + missing = [v for v in required if not os.getenv(f"PIERCLOUD_{v}")] + + if missing: + print(f"X Erro: Variaveis faltando no .env: {missing}") + exit(1) + + try: + print("Autenticando...") + token = authenticate() + print("OK Autenticado") + + view = get_view(token, args.view_id) + + print(f"\nOK Visualizacao obtida com sucesso") + + except Exception as e: + print(f"\nX Erro: {e}") + exit(1) diff --git a/.github/skills/pier-cloud/scripts/pier-cloud-get-workspace-group.py b/.github/skills/pier-cloud/scripts/pier-cloud-get-workspace-group.py new file mode 100644 index 0000000..ae9eaa6 --- /dev/null +++ b/.github/skills/pier-cloud/scripts/pier-cloud-get-workspace-group.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +""" +Script para obter detalhes de um grupo de workspace especifico da API Pier Cloud. +""" + +import requests +import os +import argparse +from dotenv import load_dotenv + +load_dotenv() + +API_BASE = "https://api.piercloud.io" +CLIENT_ID = os.getenv("PIERCLOUD_CLIENT_ID") +CLIENT_SECRET = os.getenv("PIERCLOUD_CLIENT_SECRET") +TENANCY_ID = os.getenv("PIERCLOUD_TENANCY_ID", os.getenv("PIERCLOUD_BUSINESS_ID")) + +def authenticate(): + """Obter token de autenticacao""" + url = f"{API_BASE}/auth" + payload = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET} + response = requests.post(url, json=payload, timeout=30) + + if response.status_code == 201: + return response.json()['data']['access_token'] + else: + raise Exception(f"Erro na autenticacao: {response.text}") + +def get_workspace_group(token, group_id): + """Obter detalhes de um grupo de workspace especifico""" + url = f"{API_BASE}/lighthouse/tenancies/{TENANCY_ID}/workspace-groups/{group_id}" + headers = {"Authorization": f"Bearer {token}"} + + response = requests.get(url, headers=headers, timeout=30) + + if response.status_code == 200: + data = response.json() + group = data['data'] + + print(f"\n=== Grupo de Workspace: {group['name']} ===\n") + print(f"ID: {group['id']}") + print(f"Nome: {group['name']}") + print(f"Descricao: {group.get('description', 'N/A')}") + print(f"Acesso: {group['access_scope']}") + print(f"Context ID: {group['context_id']}") + print(f"Business ID: {group['business_id']}") + print(f"Criado em: {group['created_at']}") + print(f"Atualizado em: {group['updated_at']}") + + if 'workspaces' in group and group['workspaces']: + print(f"\n--- Workspaces ({len(group['workspaces'])}) ---") + for ws in group['workspaces']: + print(f"\n ID: {ws['id']}") + print(f" Nome: {ws['name']}") + print(f" Descricao: {ws.get('description', 'N/A')}") + print(f" Acesso: {ws['access_scope']}") + print(f" Criado em: {ws['created_at']}") + else: + print("\nNenhum workspace neste grupo.") + + return group + else: + raise Exception(f"Erro ao obter grupo: {response.text}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Obter detalhes de grupo de workspace da API Pier Cloud") + parser.add_argument("--group-id", "--group_id", dest="group_id", required=True, help="ID do grupo de workspace (numerico ou UUID)") + + args = parser.parse_args() + + # Validar variaveis + if not TENANCY_ID: + print("X Erro: PIERCLOUD_TENANCY_ID (ou PIERCLOUD_BUSINESS_ID) deve estar definido no .env") + exit(1) + required = ["CLIENT_ID", "CLIENT_SECRET"] + missing = [v for v in required if not os.getenv(f"PIERCLOUD_{v}")] + + if missing: + print(f"X Erro: Variaveis faltando no .env: {missing}") + exit(1) + + try: + print("Autenticando...") + token = authenticate() + print("OK Autenticado") + + group = get_workspace_group(token, args.group_id) + + print(f"\nOK Grupo obtido com sucesso") + + except Exception as e: + print(f"\nX Erro: {e}") + exit(1) diff --git a/.github/skills/pier-cloud/scripts/pier-cloud-get-workspace.py b/.github/skills/pier-cloud/scripts/pier-cloud-get-workspace.py new file mode 100644 index 0000000..7f68794 --- /dev/null +++ b/.github/skills/pier-cloud/scripts/pier-cloud-get-workspace.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +""" +Script para obter detalhes de um workspace especifico da API Pier Cloud. +""" + +import requests +import os +import argparse +from dotenv import load_dotenv + +load_dotenv() + +API_BASE = "https://api.piercloud.io" +CLIENT_ID = os.getenv("PIERCLOUD_CLIENT_ID") +CLIENT_SECRET = os.getenv("PIERCLOUD_CLIENT_SECRET") +TENANCY_ID = os.getenv("PIERCLOUD_TENANCY_ID", os.getenv("PIERCLOUD_BUSINESS_ID")) + +def authenticate(): + """Obter token de autenticacao""" + url = f"{API_BASE}/auth" + payload = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET} + response = requests.post(url, json=payload, timeout=30) + + if response.status_code == 201: + return response.json()['data']['access_token'] + else: + raise Exception(f"Erro na autenticacao: {response.text}") + +def get_workspace(token, workspace_id): + """Obter detalhes de um workspace especifico""" + url = f"{API_BASE}/lighthouse/tenancies/{TENANCY_ID}/workspaces/{workspace_id}" + headers = {"Authorization": f"Bearer {token}"} + + response = requests.get(url, headers=headers, timeout=30) + + if response.status_code == 200: + data = response.json() + ws = data['data'] + + print(f"\n=== Workspace: {ws['name']} ===\n") + print(f"ID: {ws['id']}") + print(f"Descricao: {ws.get('description', 'N/A')}") + print(f"Acesso: {ws['access_scope']}") + print(f"Grupo: {ws['workspace_group_id']}") + + if 'views' in ws and ws['views']: + print(f"\n--- Visualizacoes ({len(ws['views'])}) ---") + for view in ws['views']: + print(f"\n ID: {view['id']}") + print(f" Nome: {view['name']}") + print(f" Descricao: {view.get('description', 'N/A')}") + print(f" Criado em: {view['created_at']}") + else: + print("\nNenhuma visualizacao encontrada.") + + return ws + else: + raise Exception(f"Erro ao obter workspace: {response.text}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Obter detalhes de workspace da API Pier Cloud") + parser.add_argument("--workspace-id", "--workspace_id", dest="workspace_id", type=int, required=True, help="ID do workspace") + + args = parser.parse_args() + + # Validar variaveis + if not TENANCY_ID: + print("X Erro: PIERCLOUD_TENANCY_ID (ou PIERCLOUD_BUSINESS_ID) deve estar definido no .env") + exit(1) + required = ["CLIENT_ID", "CLIENT_SECRET"] + missing = [v for v in required if not os.getenv(f"PIERCLOUD_{v}")] + + if missing: + print(f"X Erro: Variaveis faltando no .env: {missing}") + exit(1) + + try: + print("Autenticando...") + token = authenticate() + print("OK Autenticado") + + workspace = get_workspace(token, args.workspace_id) + + print(f"\nOK Workspace obtido com sucesso") + + except Exception as e: + print(f"\nX Erro: {e}") + exit(1) diff --git a/.github/skills/pier-cloud/scripts/pier-cloud-list-contexts.py b/.github/skills/pier-cloud/scripts/pier-cloud-list-contexts.py new file mode 100644 index 0000000..191c240 --- /dev/null +++ b/.github/skills/pier-cloud/scripts/pier-cloud-list-contexts.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +Script to list contexts from the Pier Cloud API. +""" + +import requests +import os +from dotenv import load_dotenv + +load_dotenv() + +API_BASE = "https://api.piercloud.io" +CLIENT_ID = os.getenv("PIERCLOUD_CLIENT_ID") +CLIENT_SECRET = os.getenv("PIERCLOUD_CLIENT_SECRET") +TENANCY_ID = os.getenv("PIERCLOUD_TENANCY_ID", os.getenv("PIERCLOUD_BUSINESS_ID")) + +def authenticate(): + """Obtain authentication token""" + url = f"{API_BASE}/auth" + payload = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET} + + response = requests.post(url, json=payload, timeout=30) + + if response.status_code == 201: + return response.json()['data']['access_token'] + else: + raise Exception(f"Authentication error: {response.text}") + +def list_contexts(token): + """List all contexts""" + url = f"{API_BASE}/lighthouse/tenancies/{TENANCY_ID}/contexts" + headers = {"Authorization": f"Bearer {token}"} + + response = requests.get(url, headers=headers, timeout=30) + + if response.status_code == 200: + data = response.json() + contexts = data['data']['contexts'] + + print(f"\n=== Contexts ({len(contexts)} found) ===\n") + + for ctx in contexts: + print(f"ID: {ctx['id']}") + print(f"Name: {ctx['name']}") + print(f"Provider: {ctx['provider']}") + print(f"Currency: {ctx['currency']}") + print(f"Default: {'Yes' if ctx['is_default'] else 'No'}") + print("-" * 60) + + return contexts + else: + raise Exception(f"Error listing contexts: {response.text}") + +if __name__ == "__main__": + # Validate variables + if not TENANCY_ID: + print("X Error: PIERCLOUD_TENANCY_ID (or PIERCLOUD_BUSINESS_ID) must be defined in .env") + exit(1) + required = ["CLIENT_ID", "CLIENT_SECRET"] + missing = [v for v in required if not os.getenv(f"PIERCLOUD_{v}")] + + if missing: + print(f"✗ Error: Missing variables in .env: {missing}") + exit(1) + + try: + print("Authenticating...") + token = authenticate() + print("✓ Authenticated") + + contexts = list_contexts(token) + print(f"\n✓ Total: {len(contexts)} contexts") + + except Exception as e: + print(f"\n✗ Error: {e}") + exit(1) diff --git a/.github/skills/pier-cloud/scripts/pier-cloud-list-views.py b/.github/skills/pier-cloud/scripts/pier-cloud-list-views.py new file mode 100644 index 0000000..6ac830f --- /dev/null +++ b/.github/skills/pier-cloud/scripts/pier-cloud-list-views.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Script para listar visualizacoes de um workspace da API Pier Cloud. +""" + +import requests +import os +import argparse +from dotenv import load_dotenv + +load_dotenv() + +API_BASE = "https://api.piercloud.io" +CLIENT_ID = os.getenv("PIERCLOUD_CLIENT_ID") +CLIENT_SECRET = os.getenv("PIERCLOUD_CLIENT_SECRET") +TENANCY_ID = os.getenv("PIERCLOUD_TENANCY_ID", os.getenv("PIERCLOUD_BUSINESS_ID")) + +def authenticate(): + """Obter token de autenticacao""" + url = f"{API_BASE}/auth" + payload = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET} + response = requests.post(url, json=payload, timeout=30) + + if response.status_code == 201: + return response.json()['data']['access_token'] + else: + raise Exception(f"Erro na autenticacao: {response.text}") + +def list_views(token, workspace_id): + """Listar visualizacoes de um workspace""" + url = f"{API_BASE}/lighthouse/tenancies/{TENANCY_ID}/workspaces/{workspace_id}/views" + headers = {"Authorization": f"Bearer {token}"} + + response = requests.get(url, headers=headers, timeout=30) + + if response.status_code == 200: + data = response.json() + views = data['data']['views'] + total = data['data']['total'] + + print(f"\n=== Visualizacoes do Workspace {workspace_id} ===") + print(f"Total: {total} visualizacoes\n") + + for view in views: + print(f"ID: {view['id']}") + print(f"Nome: {view['name']}") + print(f"Descricao: {view.get('description', 'N/A')}") + print(f"Criado em: {view['created_at']}") + print("-" * 60) + + return views + else: + raise Exception(f"Erro ao listar visualizacoes: {response.text}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Listar visualizacoes de workspace da API Pier Cloud") + parser.add_argument("--workspace-id", "--workspace_id", dest="workspace_id", type=int, required=True, help="ID do workspace") + + args = parser.parse_args() + + # Validar variaveis + if not TENANCY_ID: + print("X Erro: PIERCLOUD_TENANCY_ID (ou PIERCLOUD_BUSINESS_ID) deve estar definido no .env") + exit(1) + required = ["CLIENT_ID", "CLIENT_SECRET"] + missing = [v for v in required if not os.getenv(f"PIERCLOUD_{v}")] + + if missing: + print(f"X Erro: Variaveis faltando no .env: {missing}") + exit(1) + + try: + print("Autenticando...") + token = authenticate() + print("OK Autenticado") + + views = list_views(token, args.workspace_id) + + print(f"\nOK Total: {len(views)} visualizacoes") + + except Exception as e: + print(f"\nX Erro: {e}") + exit(1) diff --git a/.github/skills/pier-cloud/scripts/pier-cloud-list-workspace-groups.py b/.github/skills/pier-cloud/scripts/pier-cloud-list-workspace-groups.py new file mode 100644 index 0000000..18e3b37 --- /dev/null +++ b/.github/skills/pier-cloud/scripts/pier-cloud-list-workspace-groups.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +""" +Script para listar grupos de workspaces da API Pier Cloud. +""" + +import requests +import os +import argparse +from dotenv import load_dotenv + +load_dotenv() + +API_BASE = "https://api.piercloud.io" +CLIENT_ID = os.getenv("PIERCLOUD_CLIENT_ID") +CLIENT_SECRET = os.getenv("PIERCLOUD_CLIENT_SECRET") +TENANCY_ID = os.getenv("PIERCLOUD_TENANCY_ID", os.getenv("PIERCLOUD_BUSINESS_ID")) + +def authenticate(): + """Obter token de autenticacao""" + url = f"{API_BASE}/auth" + payload = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET} + response = requests.post(url, json=payload, timeout=30) + + if response.status_code == 201: + return response.json()['data']['access_token'] + else: + raise Exception(f"Erro na autenticacao: {response.text}") + +def list_workspace_groups(token, page=1, page_size=10): + """Listar grupos de workspaces""" + url = f"{API_BASE}/lighthouse/tenancies/{TENANCY_ID}/workspace-groups" + headers = {"Authorization": f"Bearer {token}"} + params = {"page": page, "page_size": page_size} + + response = requests.get(url, headers=headers, params=params, timeout=30) + + if response.status_code == 200: + data = response.json() + groups = data['data'] + meta = data['meta'] + + print(f"\n=== Grupos de Workspaces (Pagina {meta['page']}) ===") + print(f"Total: {meta['total']} grupos\n") + + for i, group in enumerate(groups, 1): + print(f"\n{'='*60}") + print(f"GRUPO {i}") + print(f"{'='*60}") + print(f"ID COMPLETO: {group['id']}") + print(f"Nome: {group['name']}") + print(f"Descricao: {group.get('description', 'N/A')}") + print(f"Acesso: {group['access_scope']}") + print(f"Context ID: {group.get('context_id', 'N/A')}") + print(f"Business ID: {group.get('business_id', 'N/A')}") + print(f"Criado em: {group['created_at']}") + print(f"Atualizado em: {group.get('updated_at', 'N/A')}") + print(f"Workspaces: {len(group.get('workspaces', []))}") + + if group.get('workspaces'): + print("\n Workspaces incluidos:") + for ws in group['workspaces']: + print(f" - [{ws['id']}] {ws['name']}") + + print(f"\n Comando para obter detalhes:") + print(f" python scripts/pier-cloud-get-workspace-group.py --group-id {group['id']}") + print(f"{'='*60}") + + return groups, meta + else: + raise Exception(f"Erro ao listar grupos: {response.text}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Listar grupos de workspaces da API Pier Cloud") + parser.add_argument("--page", type=int, default=1, help="Numero da pagina (padrao: 1)") + parser.add_argument("--page-size", "--page_size", dest="page_size", type=int, default=10, help="Itens por pagina (padrao: 10)") + + args = parser.parse_args() + + # Validar variaveis + if not TENANCY_ID: + print("X Erro: PIERCLOUD_TENANCY_ID (ou PIERCLOUD_BUSINESS_ID) deve estar definido no .env") + exit(1) + required = ["CLIENT_ID", "CLIENT_SECRET"] + missing = [v for v in required if not os.getenv(f"PIERCLOUD_{v}")] + + if missing: + print(f"X Erro: Variaveis faltando no .env: {missing}") + exit(1) + + try: + print("Autenticando...") + token = authenticate() + print("OK Autenticado") + + groups, meta = list_workspace_groups(token, page=args.page, page_size=args.page_size) + + print(f"\nOK Exibidos {len(groups)} grupos de {meta['total']} total") + + except Exception as e: + print(f"\nX Erro: {e}") + exit(1) diff --git a/.github/skills/pier-cloud/scripts/pier-cloud-list-workspaces.py b/.github/skills/pier-cloud/scripts/pier-cloud-list-workspaces.py new file mode 100644 index 0000000..1f5c3b7 --- /dev/null +++ b/.github/skills/pier-cloud/scripts/pier-cloud-list-workspaces.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Script to list workspaces from the Pier Cloud API with pagination. +""" + +import requests +import os +import argparse +from dotenv import load_dotenv + +load_dotenv() + +API_BASE = "https://api.piercloud.io" +CLIENT_ID = os.getenv("PIERCLOUD_CLIENT_ID") +CLIENT_SECRET = os.getenv("PIERCLOUD_CLIENT_SECRET") +TENANCY_ID = os.getenv("PIERCLOUD_TENANCY_ID", os.getenv("PIERCLOUD_BUSINESS_ID")) + +def authenticate(): + """Obtain authentication token""" + url = f"{API_BASE}/auth" + payload = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET} + response = requests.post(url, json=payload, timeout=30) + + if response.status_code == 201: + return response.json()['data']['access_token'] + else: + raise Exception(f"Authentication error: {response.text}") + +def list_workspaces(token, page=1, page_size=10, sort_field="name", sort_order="ASC"): + """List workspaces with pagination""" + url = f"{API_BASE}/lighthouse/tenancies/{TENANCY_ID}/workspaces" + headers = {"Authorization": f"Bearer {token}"} + params = { + "page": page, + "page_size": page_size, + "sort_field": sort_field, + "sort_order": sort_order + } + + response = requests.get(url, headers=headers, params=params, timeout=30) + + if response.status_code == 200: + data = response.json() + workspaces = data['data']['workspaces'] + meta = data['meta'] + + total_pages = (meta['total'] - 1) // meta['pageSize'] + 1 + + print(f"\n=== Workspaces (Page {meta['page']}/{total_pages}) ===") + print(f"Total: {meta['total']} workspaces") + print(f"Sort: {meta['sortBy']['field']} {meta['sortBy']['order']}\n") + + for ws in workspaces: + print(f"ID: {ws['id']}") + print(f"Name: {ws['name']}") + print(f"Description: {ws.get('description', 'N/A')}") + print(f"Views: {ws['count_views']}") + print(f"Access: {ws['access_scope']}") + print(f"Created at: {ws['created_at']}") + print("-" * 60) + + return workspaces, meta + else: + raise Exception(f"Error listing workspaces: {response.text}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="List workspaces from the Pier Cloud API") + parser.add_argument("--page", type=int, default=1, help="Page number (default: 1)") + parser.add_argument("--page-size", "--page_size", dest="page_size", type=int, default=10, help="Items per page (default: 10, max: 100)") + parser.add_argument("--sort-field", "--sort_field", dest="sort_field", choices=["name", "created_at"], default="name", help="Sort field") + parser.add_argument("--sort-order", "--sort_order", dest="sort_order", choices=["ASC", "DESC"], default="ASC", help="Sort order") + + args = parser.parse_args() + + # Validate variables + if not TENANCY_ID: + print("X Error: PIERCLOUD_TENANCY_ID (or PIERCLOUD_BUSINESS_ID) must be defined in .env") + exit(1) + required = ["CLIENT_ID", "CLIENT_SECRET"] + missing = [v for v in required if not os.getenv(f"PIERCLOUD_{v}")] + + if missing: + print(f"✗ Error: Missing variables in .env: {missing}") + exit(1) + + try: + print("Authenticating...") + token = authenticate() + print("✓ Authenticated") + + workspaces, meta = list_workspaces( + token, + page=args.page, + page_size=args.page_size, + sort_field=args.sort_field, + sort_order=args.sort_order + ) + + print(f"\n✓ Displayed {len(workspaces)} workspaces of {meta['total']} total") + + except Exception as e: + print(f"\n✗ Error: {e}") + exit(1) diff --git a/.github/skills/pier-cloud/scripts/pier_cloud_client.py b/.github/skills/pier-cloud/scripts/pier_cloud_client.py new file mode 100644 index 0000000..421d908 --- /dev/null +++ b/.github/skills/pier-cloud/scripts/pier_cloud_client.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +Cliente robusto para API Pier Cloud com retry, renovacao automatica e CLI. +""" + +import requests +import os +import time +import logging +import argparse +import json +from dotenv import load_dotenv + +# Configurar logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +class PierCloudClient: + """Cliente robusto para API Pier Cloud""" + + def __init__(self): + load_dotenv() + self.api_base = "https://api.piercloud.io" + self.client_id = os.getenv("PIERCLOUD_CLIENT_ID") + self.client_secret = os.getenv("PIERCLOUD_CLIENT_SECRET") + self.tenancy_id = os.getenv("PIERCLOUD_TENANCY_ID", os.getenv("PIERCLOUD_BUSINESS_ID")) + + self.token = None + self.token_expires = None + + self._validate_config() + + def _validate_config(self): + """Validar configuracao necessaria""" + required = ["client_id", "client_secret", "tenancy_id"] + missing = [k for k in required if not getattr(self, k)] + + if missing: + raise ValueError(f"Configuracao faltando: {missing}") + + def authenticate(self): + """Autenticar e obter token""" + logger.info("Autenticando...") + + response = requests.post( + f"{self.api_base}/auth", + json={"client_id": self.client_id, "client_secret": self.client_secret}, + timeout=30 + ) + + if response.status_code == 201: + data = response.json()['data'] + self.token = data['access_token'] + # Renovar 5 minutos antes de expirar + self.token_expires = time.time() + data['expires_in'] - 300 + logger.info("OK Autenticado com sucesso") + return True + else: + logger.error(f"X Falha na autenticacao: {response.text}") + return False + + def ensure_authenticated(self): + """Garantir token valido""" + if not self.token or time.time() >= self.token_expires: + return self.authenticate() + return True + + def make_request(self, method, endpoint, **kwargs): + """Fazer requisicao com retry e renovacao automatica""" + max_retries = 3 + + for attempt in range(max_retries): + try: + if not self.ensure_authenticated(): + raise Exception("Falha na autenticacao") + + headers = kwargs.get('headers', {}) + headers['Authorization'] = f"Bearer {self.token}" + kwargs['headers'] = headers + kwargs.setdefault('timeout', 30) + + response = requests.request(method, f"{self.api_base}{endpoint}", **kwargs) + + if response.status_code == 401: + logger.warning("Token expirado, renovando...") + self.authenticate() + continue + + if response.status_code == 429: + wait_time = 2 ** attempt + logger.warning(f"Rate limit. Aguardando {wait_time}s...") + time.sleep(wait_time) + continue + + response.raise_for_status() + return response.json() + + except requests.exceptions.RequestException as e: + logger.error(f"Erro na tentativa {attempt + 1}: {e}") + + if attempt == max_retries - 1: + raise + + time.sleep(2 ** attempt) + + return None + + def list_contexts(self): + """Listar contextos""" + endpoint = f"/lighthouse/tenancies/{self.tenancy_id}/contexts" + return self.make_request('GET', endpoint) + + def list_workspaces(self, page=1, page_size=10): + """Listar workspaces""" + endpoint = f"/lighthouse/tenancies/{self.tenancy_id}/workspaces" + params = {"page": page, "page_size": page_size} + return self.make_request('GET', endpoint, params=params) + + def get_workspace(self, workspace_id): + """Obter workspace especifico""" + endpoint = f"/lighthouse/tenancies/{self.tenancy_id}/workspaces/{workspace_id}" + return self.make_request('GET', endpoint) + + def get_all_workspaces(self): + """Obter todos os workspaces com paginacao automatica""" + all_workspaces = [] + page = 1 + page_size = 100 + + while True: + result = self.list_workspaces(page=page, page_size=page_size) + workspaces = result['data']['workspaces'] + meta = result['meta'] + + all_workspaces.extend(workspaces) + logger.info(f"Pagina {page}: {len(workspaces)} workspaces") + + if page * page_size >= meta['total']: + break + + page += 1 + + return all_workspaces + +def main(): + parser = argparse.ArgumentParser(description="Cliente CLI para API Pier Cloud") + parser.add_argument("--action", required=True, + choices=["list-contexts", "list-workspaces", "get-workspace", "get-all-workspaces"], + help="Acao a executar") + parser.add_argument("--workspace-id", type=int, help="ID do workspace (para get-workspace)") + parser.add_argument("--page", type=int, default=1, help="Numero da pagina") + parser.add_argument("--page-size", type=int, default=10, help="Itens por pagina") + parser.add_argument("--output", help="Arquivo de saida JSON") + + args = parser.parse_args() + + try: + client = PierCloudClient() + + if args.action == "list-contexts": + result = client.list_contexts() + contexts = result['data']['contexts'] + print(f"\nContextos: {len(contexts)}") + for ctx in contexts: + print(f" - {ctx['name']} ({ctx['provider']})") + + elif args.action == "list-workspaces": + result = client.list_workspaces(page=args.page, page_size=args.page_size) + workspaces = result['data']['workspaces'] + meta = result['meta'] + print(f"\nWorkspaces: {len(workspaces)} de {meta['total']}") + for ws in workspaces: + print(f" - [{ws['id']}] {ws['name']}") + + elif args.action == "get-workspace": + if not args.workspace_id: + print("X Erro: --workspace-id e obrigatorio") + exit(1) + result = client.get_workspace(args.workspace_id) + ws = result['data'] + print(f"\nWorkspace: {ws['name']}") + print(f"ID: {ws['id']}") + print(f"Visualizacoes: {len(ws.get('views', []))}") + + elif args.action == "get-all-workspaces": + workspaces = client.get_all_workspaces() + print(f"\nTotal: {len(workspaces)} workspaces") + + if args.output: + with open(args.output, 'w', encoding='utf-8') as f: + json.dump(workspaces, f, indent=2, ensure_ascii=False) + print(f"OK Salvo em {args.output}") + + except Exception as e: + logger.error(f"X Erro: {e}") + exit(1) + +if __name__ == "__main__": + main() diff --git a/.github/skills/ralph-loop-kiro-specs/SKILL.md b/.github/skills/ralph-loop-kiro-specs/SKILL.md new file mode 100644 index 0000000..689a5eb --- /dev/null +++ b/.github/skills/ralph-loop-kiro-specs/SKILL.md @@ -0,0 +1,200 @@ +--- +name: ralph-loop-kiro-specs +description: >- + Automated iterative agent runner for spec-based development in Kiro. Wraps kiro-cli in a + self-correcting bash loop that picks up tasks from a Kiro spec, implements them one at a time, + verifies against exit criteria, and accumulates corrections and codebase patterns across iterations. + Use this skill when the user mentions "ralph loop", "ralph", "spec loop", "iterative spec runner", + "run my spec tasks automatically", "kiro spec automation", "self-correcting agent loop", + "implement spec tasks in a loop", "run kiro-cli in a loop", "automated task implementation", + or wants to drive a Kiro spec to completion through repeated agent iterations. Also use when the + user wants to set up, configure, troubleshoot, or understand the Ralph Loop workflow — including + progress tracking, corrections, codebase patterns, timing logs, and the summary dashboard. +metadata: + author: ft.ia.br + version: "1.0" + date: 2026-04-28 + license: Apache-2.0 + original_project: https://github.com/mreferre/ralph-loop-kiro-specs + original_author: mreferre + category: code-scaffolding-and-templates +--- + +# Ralph Loop for Kiro Specs + +An automated, iterative agent runner that drives spec-based development in [Kiro](https://kiro.dev). It wraps `kiro-cli` in a bash loop, feeding it a carefully engineered prompt that turns Kiro into a disciplined, self-correcting implementation agent — one that picks up tasks from a spec, implements them, verifies its own work, and learns from its mistakes across iterations. + +> **Attribution:** Based on [ralph-loop-kiro-specs](https://github.com/mreferre/ralph-loop-kiro-specs) by [mreferre](https://github.com/mreferre), licensed under Apache License 2.0. + +## When to Use + +- Automate implementation of Kiro spec tasks through repeated agent iterations +- Drive a spec from start to finish without manual prompt-by-prompt interaction +- Set up the Ralph Loop in a new project +- Troubleshoot a stuck or failed Ralph Loop run +- Understand how progress tracking, corrections, and codebase patterns work +- Generate or interpret the summary dashboard after completion + +## Prerequisites + +| Requirement | Details | +|---|---| +| Kiro CLI | `kiro-cli` must be installed and on `PATH` ([kiro.dev/cli](https://kiro.dev/cli/)) | +| Kiro IDE | [kiro.dev](https://kiro.dev/) installed | +| Bash | Standard bash shell | +| Kiro Specs | A project with specs under `.kiro/specs//` containing at least `requirements.md`, `design.md`, and `tasks.md` | + +## How It Works + +Ralph runs a loop where each iteration sends a prompt to `kiro-cli`. The prompt instructs the agent to follow a strict six-phase cycle: + +### The Six Phases + +1. **Load Context** — Read steering files (`product.md`, `structure.md`, `tech.md`) and the target spec (`requirements.md`, `design.md`, `tasks.md`, `progress.md`). Take stock of available tools. +2. **Pick ONE Task** — Find the lowest-numbered incomplete top-level task. Record start time. Never pick more than one task per iteration. +3. **Understand Before Implementing** — Read relevant source files, study existing patterns, re-read the Corrections and Codebase Patterns sections from `progress.md`, and apply every relevant correction proactively. +4. **Implement** — Implement the task and all subtasks in order. Run typechecks and tests. If something fails: fix it, write a correction immediately if a future iteration could hit the same problem, and move on. After 5 failed attempts, mark the task `[F]` and log an unresolved blocker. +5. **Verify Exit Criteria** — Re-read exit criteria from `requirements.md` and design constraints from `design.md`. Confirm each is satisfied before marking complete. +6. **Update Tracking** — Mark the task `[X]` in `tasks.md`, append a progress entry to `progress.md`, add new codebase patterns, do a final correction sweep, and record timing to `specs_time.md`. + +### The Self-Correction System + +The Corrections section at the top of `progress.md` is a flat lookup table of mistakes and their fixes. Every iteration reads it before doing any work and must never repeat a listed mistake. Corrections are written immediately when errors happen — not at the end of the task. + +Format: +``` +- ❌ `python manage.py migrate` → ✅ `python3 manage.py migrate` (system has no `python` alias) +- ❌ Running tests with `npm test` → ✅ `npm run test:unit` (project uses separate test scripts) +- ❌ UNRESOLVED: [description of issue that couldn't be fixed after 5 attempts] +``` + +### Codebase Patterns + +Ralph accumulates conventions discovered during implementation — file naming, import patterns, error handling, testing commands, etc. Only patterns actually encountered are recorded, not speculative ones. + +### Completion and Summary Dashboard + +When all tasks are marked `[X]`, Ralph generates a self-contained `summary.html` in the spec directory with: +- **Top pane**: spec name, status indicator (green/red), total elapsed time, task count, date range +- **Left pane**: collapsible task tree with hover tooltips showing progress details +- **Right pane**: timing table with per-task start/end times and durations + +## Project Structure + +The user's project should look like this before running Ralph: + +``` +your-project/ +├── ralph-loop-kiro-specs-prompt.md # The Ralph agent prompt template +├── ralph-loop-kiro-specs-script.sh # The loop runner script +└── .kiro/ + ├── steering/ + │ ├── product.md # What the product is + │ ├── structure.md # Project structure conventions + │ └── tech.md # Tech stack and tooling + └── specs/ + └── / + ├── requirements.md # Requirements and exit criteria + ├── design.md # Architecture and design decisions + ├── tasks.md # Task checklist + ├── progress.md # Auto-created: corrections, patterns, progress log + ├── specs_time.md # Auto-created: per-task timing + └── summary.html # Auto-generated on completion: visual dashboard +``` + +## Setup Instructions + +To set up Ralph Loop in a project: + +1. Copy the script and prompt template to the project root: + - `ralph-loop-kiro-specs-script.sh` — the loop runner (bundled in this skill under `scripts/`) + - `ralph-loop-kiro-specs-prompt.md` — the agent prompt template (bundled under `references/`) +2. Make the script executable: `chmod +x ralph-loop-kiro-specs-script.sh` +3. Ensure the project has Kiro specs set up under `.kiro/specs//` with at least `requirements.md`, `design.md`, and `tasks.md` +4. Ensure steering files exist under `.kiro/steering/` (`product.md`, `structure.md`, `tech.md`) — these significantly improve output quality + +## Usage + +```bash +./ralph-loop-kiro-specs-script.sh +``` + +| Argument | Description | +|---|---| +| `max_iterations` | Maximum number of loop iterations (positive integer). Each iteration implements one task. Set to at least the number of tasks plus a buffer for retries. | +| `specs_name` | Name of the spec directory under `.kiro/specs/`. Must already exist with the required files. | + +### Example + +```bash +# Run up to 15 iterations on the "auth-feature" spec +./ralph-loop-kiro-specs-script.sh 15 auth-feature +``` + +### Iteration Modes + +The script asks at startup whether to run automatically or manually: + +- **Automatic** — Tasks run back-to-back without pausing. Good for well-defined specs. +- **Manual** — Pauses after each iteration for review. Good for new or unfamiliar specs. + +## Troubleshooting + +### Ralph gets stuck on a task (marked `[F]`) + +Fix the issue manually, update `tasks.md` to unmark the task, and re-run. The corrections from the failed attempt will still be in `progress.md` for the next iteration to learn from. + +### Max iterations reached without completion + +Increase `max_iterations` and re-run. Ralph will pick up where it left off since it reads `tasks.md` to find the next incomplete task. + +### Agent implements more than one task per iteration + +This violates the core constraint. Check that you're using the unmodified prompt template. The prompt has a critical constraint section at the top that enforces one-task-per-iteration. + +### Steering files are missing + +Ralph works without them but produces better results with context. Create `.kiro/steering/product.md`, `structure.md`, and `tech.md` with descriptions of your product, project structure, and tech stack. + +## Quality Checklist + +Before running Ralph Loop, verify: + +- [ ] `kiro-cli` is installed and available on `PATH` +- [ ] Spec directory exists under `.kiro/specs//` +- [ ] `requirements.md` has numbered requirements with exit criteria +- [ ] `design.md` has architecture and design decisions +- [ ] `tasks.md` has a numbered task checklist with subtasks +- [ ] Steering files exist under `.kiro/steering/` (recommended) +- [ ] `max_iterations` is set to at least the number of tasks + buffer +- [ ] The prompt template (`ralph-loop-kiro-specs-prompt.md`) is in the project root +- [ ] The script (`ralph-loop-kiro-specs-script.sh`) is executable + +## Bundled Resources + +| Resource | Path | Description | +|---|---|---| +| Runner script | `scripts/ralph-loop-kiro-specs-script.sh` | The bash loop that drives iterations. Copy to project root. | +| Prompt template | `references/ralph-loop-kiro-specs-prompt.md` | The agent prompt template. Copy to project root. | + +Read the prompt template when you need to understand the detailed phase instructions, correction format, codebase pattern categories, or the summary dashboard specification. + +## License + +This skill is based on [ralph-loop-kiro-specs](https://github.com/mreferre/ralph-loop-kiro-specs) by [mreferre](https://github.com/mreferre), licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). + +``` +Copyright [mreferre] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/.github/skills/ralph-loop-kiro-specs/references/ralph-loop-kiro-specs-prompt.md b/.github/skills/ralph-loop-kiro-specs/references/ralph-loop-kiro-specs-prompt.md new file mode 100644 index 0000000..a3f8ba7 --- /dev/null +++ b/.github/skills/ralph-loop-kiro-specs/references/ralph-loop-kiro-specs-prompt.md @@ -0,0 +1,246 @@ +# Ralph Agent Instructions + +> Based on [ralph-loop-kiro-specs](https://github.com/mreferre/ralph-loop-kiro-specs) by [mreferre](https://github.com/mreferre). +> Licensed under Apache License 2.0. + +## ⛔ CRITICAL CONSTRAINT — READ THIS FIRST + +You must implement exactly ONE top-level task per invocation. This is non-negotiable. + +- ONE top-level task means: a single root-level item (e.g., `1.`, `2.`, `3.`) and all of its subtasks (e.g., `2.1`, `2.2`, `2.3`). +- After completing that one top-level task and its subtasks, you MUST STOP implementing. Do not continue to the next top-level task. Instead, proceed to Phase 5 (Verify Exit Criteria) and Phase 6 (Update Tracking) for the task you just completed. +- Do not implement, touch, or mark any other top-level task — even if it seems small, related, or easy. +- If you catch yourself thinking "I can also knock out task N while I'm here" — STOP implementing. That is exactly the behavior this rule prohibits. Move on to verification and tracking for your one task. +- Violating this constraint invalidates the entire run. + +## Phase 1: Load Context + +Read the following files to understand the project. Skip any that don't exist. + +1. `README.md` — read the project README for high-level context about the project's purpose, setup, and conventions +2. `docs/` folder — if it exists, scan for any additional documentation, ADRs, or context relevant to the spec +3. `.kiro/steering/product.md` — what the product is +4. `.kiro/steering/structure.md` — project structure conventions +5. `.kiro/steering/tech.md` — tech stack and tooling +6. `.kiro/specs/SPECS_NAME/requirements.md` — requirements and exit criteria +7. `.kiro/specs/SPECS_NAME/design.md` — architecture and design decisions +8. `.kiro/specs/SPECS_NAME/tasks.md` — the task list to implement +9. `.kiro/specs/SPECS_NAME/progress.md` — **read the top sections (Corrections and Codebase Patterns) FIRST and internalize them before doing anything else**, then review past progress entries + +## Tool Awareness + +After loading context, take stock of what tools are available to you in this environment (e.g., MCP servers, CLI utilities, linters, formatters, test runners, build tools). You are not required to use any of them — but knowing what's available may inform better decisions during implementation and verification. Use your judgment: if a tool would genuinely help with the current task, use it. If not, don't force it. + +## Phase 2: Pick ONE Task + +Capture the task start time by running these two shell commands and saving their output: +```bash +date '+%Y-%m-%d %H:%M:%S' +date +%s +``` +The first gives you the human-readable start timestamp for the time log. The second gives you the epoch seconds — you will need both later in Phase 6 to compute elapsed time accurately. + +1. Find the lowest-numbered **top-level** task in `tasks.md` that is NOT marked with `[X]`. A top-level task is one at the root indentation level (e.g., `- [ ] 1.`, `- [ ] 2.`). Subtasks nested under a top-level task (e.g., `1.1`, `1.2`) are NOT independent tasks — they are part of their parent and will be implemented together with it. +2. Read the requirement(s) and exit criteria referenced by that task in `requirements.md` +3. Read the relevant design details in `design.md` +4. Do NOT pick more than one top-level task. You implement exactly one top-level task (including all of its subtasks) per invocation. You must NOT mark any other top-level task as complete — only the one you pick here. + +## Phase 3: Understand Before Implementing + +Before writing any code: + +1. Read the existing source files that are relevant to the task +2. Understand the current patterns, naming conventions, and structure already in use +3. **Re-read the Corrections section** at the top of `progress.md` with your chosen task in mind. Every entry there is a mistake a previous iteration already made and fixed. Do not repeat them. Apply every relevant correction proactively to the task you are about to implement. +4. Re-read the Codebase Patterns section in `progress.md` with your chosen task in mind — follow any patterns relevant to this task + +## Phase 4: Implement + +1. Implement the task and all its subtasks in their specified order +2. Follow the project's existing conventions and patterns +3. After implementation, run typecheck and tests as applicable to the project +4. If a command fails or a test breaks: + a. Fix the issue + b. **Immediately ask yourself: "Could a future iteration hit this same problem?"** If yes, add it to the Corrections section at the top of `progress.md` RIGHT NOW, before continuing. Do not wait until the end. + c. If you cannot resolve a failure after 5 attempts, add it to the Corrections section as an unresolved blocker, mark the task as failed in `tasks.md` (e.g., `[F]`), and move on to Phase 6 to record what happened. Do NOT mark the task with `[X]`. +5. **STOP CHECK:** You have now finished implementing your one top-level task. Do NOT proceed to implement any other top-level task. Go directly to Phase 5. + +## Phase 5: Verify Exit Criteria + +Before marking the task complete: + +1. Re-read the exit criteria from `requirements.md` for this task and confirm each one is satisfied. +2. Re-read the relevant design details from `design.md` and confirm the implementation conforms to the specified architecture, patterns, and constraints. +3. If any exit criteria or design constraints are not met, go back and address them. + +## Phase 6: Update Tracking + +1. In `tasks.md`, mark ONLY the single task you just completed (and its direct subtasks) with `[X]`. **Do NOT mark any other tasks as complete.** When editing `tasks.md`, use a surgical edit (e.g., find-and-replace on the specific task line) rather than rewriting the entire file. If you rewrite the file, you MUST preserve the exact checkbox state (`[ ]` or `[X]`) of every task you did NOT work on. Double-check the file after editing to confirm no other tasks were accidentally marked. +2. Append a progress entry to `progress.md` (see format below) +3. If you discovered a reusable codebase pattern, add it to the Codebase Patterns section in `progress.md` +4. Final sweep: if you hit ANY error during this task that you haven't already added to Corrections, add it now. If you followed Phase 4 step 4b faithfully, this step should be a no-op. +5. Capture the task end time by running these two shell commands: + ```bash + date '+%Y-%m-%d %H:%M:%S' + date +%s + ``` + The first gives you the human-readable end timestamp. The second gives you the epoch seconds. + Compute elapsed time by subtracting the start epoch (captured in Phase 2) from the end epoch: + ```bash + echo $(( END_EPOCH - START_EPOCH )) + ``` + Convert the result to `Xm Ys` format (e.g., if the difference is 754 seconds → `12m 34s`). + Append a row to `SPECS_NAME/specs_time.md` in this format: + ``` + | [Task ID] | [Start time] | [End time] | [Elapsed time] | + ``` + Use `YYYY-MM-DD HH:MM:SS` for timestamps and `Xm Ys` for elapsed time. + +## Progress Entry Format + +Append to the bottom of `progress.md`: + +``` +## [Date] - [Task ID]: [Brief description] +- What was implemented +- Files changed +- Tools used (list any non-default tools you chose to use and why, e.g., "Used MCP linter to validate schema — caught a missing required field") +- Patterns discovered (list here; if reusable, must also be added to the Codebase Patterns section) +- Corrections added (list here; must already exist in the Corrections section — if not, add them there now) +--- +``` + +Note: The Corrections and Codebase Patterns sections at the top of the file are the canonical reference. Progress entries provide a chronological record and should cross-reference what was added to those sections, not replace them. + +## Corrections + +Maintain a `# Corrections` section at the VERY TOP of `progress.md`, above Codebase Patterns. This is a flat lookup table of mistakes that have already been made and their fixes. Every iteration must read this section before doing any work, and must never repeat a listed mistake. + +Each entry follows this format — short, scannable, no prose: + +``` +- ❌ `python manage.py migrate` → ✅ `python3 manage.py migrate` (system has no `python` alias) +- ❌ `import { foo } from 'lib'` → ✅ `import { foo } from 'lib/index.js'` (ESM requires explicit extensions) +- ❌ Creating migration without IF NOT EXISTS → ✅ Always use IF NOT EXISTS (prevents re-run failures) +- ❌ Running tests with `npm test` → ✅ `npm run test:unit` (project uses separate test scripts) +- ❌ UNRESOLVED: [description of issue that couldn't be fixed after 3 attempts] +``` + +**When to write a correction:** Any time you encounter an error, a failed command, a wrong assumption, or a workaround — anything where your first attempt was wrong and you had to adjust. Write it immediately when it happens, not at the end of the task. + +**What makes a good correction:** +- Wrong CLI command or binary name +- Missing flags, env vars, or config needed for a command to work +- Import path or module resolution issues +- API or library usage that differs from what you assumed +- File paths or naming conventions you got wrong +- Build/test/lint commands that need specific arguments +- Platform-specific gotchas (OS, runtime version, etc.) +- Any assumption that turned out to be false + +## Codebase Patterns + +Maintain a `# Codebase Patterns` section in `progress.md`, immediately below the Corrections section. These patterns are critical — they prevent future iterations from repeating mistakes or deviating from established conventions. + +Only record patterns you actually encounter or use during implementation. The categories below are a reference for what kinds of patterns to watch for — do not try to fill them all out proactively: + +**Project Structure & Modules** +- File/folder naming conventions (kebab-case, PascalCase, etc.) +- Where new files of each type should go (components, services, utils, handlers, etc.) +- Module/package organization (barrel exports, index files, __init__.py, mod.rs, etc.) +- Monorepo structure (which packages depend on which, shared libs) + +**Language & Type System** +- Preferred language idioms (e.g., guard clauses vs nested ifs, early returns) +- Type annotation style (interfaces vs types in TS, type hints in Python, generics usage) +- Null/optional handling (Optional, Maybe, nullable types, Result/Either patterns) +- Enum patterns (string enums, const objects, sealed classes) +- Async patterns (async/await, Promises, Futures, goroutines, channels) + +**Error Handling** +- Custom error classes/types and hierarchy +- How errors propagate (thrown exceptions, Result types, error codes, middleware) +- Logging conventions (structured logging, log levels, which logger library) +- User-facing vs internal error messages + +**Data & State** +- Database migration conventions (IF NOT EXISTS, up/down migrations, naming) +- ORM/query patterns (repository pattern, active record, raw queries) +- State management approach (Redux, Zustand, Vuex, signals, MobX, context, providers) +- Data validation (schemas, decorators, guard functions, where validation lives) +- Serialization/deserialization patterns (DTOs, transformers, codable, serde) +- Caching strategy (what's cached, TTLs, invalidation approach) + +**API & Communication** +- API style (REST, GraphQL, gRPC, tRPC) and conventions (route naming, versioning) +- Request/response shapes and envelope patterns +- Authentication/authorization patterns (middleware, guards, decorators, policies) +- Client-server contract (shared types, generated clients, OpenAPI) +- Event/message patterns (pub/sub, event bus, message queues, webhooks) + +**Frontend & UI** +- Component structure (functional vs class, composition patterns, slots/children) +- Styling approach (CSS modules, Tailwind, styled-components, SCSS, utility classes) +- Form handling (controlled/uncontrolled, form libraries, validation) +- Routing patterns (file-based, config-based, nested routes, guards) +- Accessibility patterns the project follows (ARIA usage, focus management, semantic HTML) +- Internationalization approach (i18n library, key naming, where translations live) + +**Backend & Infrastructure** +- Dependency injection approach (constructor injection, containers, providers) +- Middleware/interceptor patterns and ordering +- Background job/worker patterns +- Configuration management (env vars, config files, secrets handling) +- Database connection and transaction patterns + +**Testing** +- Test file location and naming (co-located, __tests__, .spec vs .test) +- Test setup/teardown patterns (fixtures, factories, builders, seeds, mocks) +- Mocking approach (which libraries, what gets mocked, test doubles) +- Assertion style (expect, assert, should) +- How to run tests (commands, flags, environment variables needed) +- Integration/E2E test conventions + +**Build & Tooling** +- Build commands and flags that work +- Environment-specific gotchas (env vars, feature flags, platform differences) +- Linting/formatting commands and any suppressions in use +- Code generation steps (protobuf, GraphQL codegen, ORM models, OpenAPI) + +## Stop Condition + +After completing your one task, check if ALL tasks in `tasks.md` are marked `[X]`. + +If all tasks are complete: + +1. Build an HTML page at `.kiro/specs/SPECS_NAME/summary.html` that presents a readable, multi-pane summary of the entire spec implementation. The page should be a single self-contained HTML file (inline CSS and JS, no external dependencies). The page should be styled against the https://kiro.dev web site. Structure it as follows: + + **Page title:** + - Use "Ralph Loop for Kiro Specs" as the static page title (in `` and as the main heading). + + **Top pane — Global Summary:** + - Display the spec name (SPECS_NAME) prominently as a field in this pane. + - Show a visual status indicator: green if all tasks are marked `[X]` (fully implemented), red if any tasks are marked `[F]` or unchecked (partial/failed). + - If green: show the total elapsed time for the entire flow (sum of all task durations from `specs_time.md`), the number of tasks completed, and the date range (first task start to last task end). + - If red: show how many tasks completed vs total, which tasks failed or remain incomplete, and any unresolved corrections from `progress.md`. + - Show counts for files changed, corrections logged, and patterns discovered — but display only the counts as summary numbers. The full details of corrections and patterns should be hidden behind collapsible `<details>/<summary>` elements that the user can expand if they want to dig in. Keep the default state collapsed. + + **Left pane — Task Tree:** + - Render the task list from `tasks.md` as a collapsible tree. Top-level tasks are tree nodes; subtasks are nested children. + - Each node should be expandable/collapsible (click to toggle). It should be explicit if a tree node has children or not. + - On mouse hover over any task or subtask, show a tooltip or popover with the relevant progress details from `progress.md` for that task: what was implemented, files changed, tools used, patterns discovered, and corrections added. Match tasks to progress entries by task ID. + - Use visual indicators for task status: `[X]` = completed, `[F]` = failed, unchecked = incomplete. + + **Right pane — Timing:** + - Render the timing data from `specs_time.md` as an HTML table (task ID, start time, end time, elapsed time). + + **Styling:** + - Clean, modern CSS. Use a side-by-side layout (e.g., flexbox) with the task tree taking roughly 60% width and the timing table taking 40%. + - The tooltip/popover should be readable and not clip off-screen. + +2. Reply with: + ``` + <promise>COMPLETE</promise> + ``` + +If tasks remain, end normally after completing your one task. diff --git a/.github/skills/ralph-loop-kiro-specs/scripts/ralph-loop-kiro-specs-script.sh b/.github/skills/ralph-loop-kiro-specs/scripts/ralph-loop-kiro-specs-script.sh new file mode 100644 index 0000000..9ef4f6e --- /dev/null +++ b/.github/skills/ralph-loop-kiro-specs/scripts/ralph-loop-kiro-specs-script.sh @@ -0,0 +1,128 @@ +#!/bin/bash +set -e + +# Ralph Loop for Kiro Specs — Runner Script +# Based on https://github.com/mreferre/ralph-loop-kiro-specs +# Licensed under Apache License 2.0 +# +# Usage: ./ralph-loop-kiro-specs-script.sh <max_iterations> <specs_name> + +# ── Color & style codes ── +RED='\033[1;31m' +GREEN='\033[1;32m' +YELLOW='\033[1;33m' +BLUE='\033[1;34m' +CYAN='\033[1;36m' +MAGENTA='\033[1;35m' +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' # No Color + +MAX_ITERATIONS=${1:-10} +SPECS_NAME=${2:-} + +if [ -z "$SPECS_NAME" ]; then + echo -e "${RED}❌ Usage: $0 <max_iterations> <specs_name>${NC}" >&2 + exit 1 +fi + +# Validate MAX_ITERATIONS is a positive integer +if ! [[ "$MAX_ITERATIONS" =~ ^[1-9][0-9]*$ ]]; then + echo -e "${RED}❌ Error: <max_iterations> must be a positive integer, got '${BOLD}$MAX_ITERATIONS${RED}'${NC}" >&2 + exit 1 +fi + +# Validate SPECS_NAME is a non-empty string (no whitespace-only) +if ! [[ "$SPECS_NAME" =~ [^[:space:]] ]]; then + echo -e "${RED}❌ Error: <specs_name> must be a non-empty string${NC}" >&2 + exit 1 +fi +SCRIPT_DIR="$(cd "$(dirname \ + "${BASH_SOURCE[0]}")" && pwd)" + +# Set the specs directory path based on the provided specs name +SPECS_DIR="$SCRIPT_DIR/.kiro/specs/$SPECS_NAME" +# Check if the specs directory exists, exit with error if not found +if [ ! -d "$SPECS_DIR" ]; then + echo -e "${RED}❌ Error: No specs named '${BOLD}$SPECS_NAME${RED}' found in this project${NC}" >&2 + exit 1 +fi + +# Initialize progress log file if it doesn't exist +if [ ! -f "$SPECS_DIR/progress.md" ]; then + echo "# Progress Log for spec: $SPECS_NAME" \ + > "$SPECS_DIR/progress.md" + echo -e "${DIM}📝 Created progress.md${NC}" +fi + +# Initialize time log file if it doesn't exist +TIME_LOG="$SPECS_DIR/specs_time.md" +if [ ! -f "$TIME_LOG" ]; then + echo "# Time Log for spec: $SPECS_NAME" > "$TIME_LOG" + echo -e "${DIM}📝 Created specs_time.md${NC}" +fi + +# Load the prompt template and substitute the specs name placeholder +PROMPT=$(sed "s/SPECS_NAME/$SPECS_NAME/g" \ + "$SCRIPT_DIR/ralph-loop-kiro-specs-prompt.md") + +echo "" +echo -e "${MAGENTA}══════════════════════════════════════${NC}" +echo -e " 🚀 ${BOLD}Starting Ralph${NC}" +echo -e " ${DIM}spec:${NC} ${CYAN}$SPECS_NAME${NC}" +echo -e " ${DIM}iterations:${NC} ${CYAN}$MAX_ITERATIONS${NC}" +echo -e "${MAGENTA}══════════════════════════════════════${NC}" +echo "" + +# Ask user for iteration mode +read -r -p "$(echo -e "${YELLOW}🔄 Iterate automatically through tasks? (y/n):${NC} ")" AUTO_MODE +case "$AUTO_MODE" in + [yY]|[yY][eE][sS]) + AUTO_MODE=true + echo -e " ${GREEN}✔ Auto-pilot enabled${NC}" + ;; + *) + AUTO_MODE=false + echo -e " ${BLUE}✔ Manual mode — you'll confirm each iteration${NC}" + ;; +esac + +echo "" +echo -e "${CYAN}─── 📋 Prompt ───────────────────────────${NC}" +echo "$PROMPT" +echo -e "${CYAN}──────────────────────────────────────────${NC}" +echo "" + +read -r -p "$(echo -e "${YELLOW}👀 Review the prompt above. Press Enter to launch the Ralph loop...${NC} ")" +echo "" + +for i in $(seq 1 $MAX_ITERATIONS); do + echo -e "${BLUE}═══════════════════════════════════════${NC}" + echo -e " 🔁 ${BOLD}Iteration ${CYAN}$i${NC}${BOLD} / ${DIM}$MAX_ITERATIONS${NC}" + echo -e "${BLUE}═══════════════════════════════════════${NC}" + + OUTPUT=$(echo "$PROMPT" \ + | kiro-cli chat --trust-all-tools --no-interactive 2>&1 \ + | tee /dev/stderr) || true + + if echo "$OUTPUT" | \ + grep -q "<promise>COMPLETE</promise>" + then + echo "" + echo -e "${GREEN}══════════════════════════════════════${NC}" + echo -e " ✅ ${BOLD}All tasks complete!${NC}" + echo -e "${GREEN}══════════════════════════════════════${NC}" + exit 0 + fi + + if [ "$AUTO_MODE" = false ]; then + echo "" + read -r -p "$(echo -e "${YELLOW}⏸️ Iteration $i done. Press Enter to continue...${NC} ")" + fi +done + +echo "" +echo -e "${RED}══════════════════════════════════════${NC}" +echo -e " ⚠️ ${BOLD}Max iterations reached${NC} ${DIM}($MAX_ITERATIONS)${NC}" +echo -e "${RED}══════════════════════════════════════${NC}" +exit 1 diff --git a/.github/skills/resume-ats-beater/SKILL.md b/.github/skills/resume-ats-beater/SKILL.md new file mode 100644 index 0000000..a28a630 --- /dev/null +++ b/.github/skills/resume-ats-beater/SKILL.md @@ -0,0 +1,480 @@ +--- +name: resume-ats-beater +description: Este skill deve ser usado para reescrever currículos com foco em compatibilidade ATS e impacto para recrutadores, e/ou auditar perfis LinkedIn para maximizar visibilidade e conversão profissional. Acionar em pedidos de otimização de currículo, melhoria para ATS, reescrita profissional do CV, adaptação para vaga-alvo, auditoria de compatibilidade ATS, aumento de taxa de entrevista, auditoria de perfil LinkedIn, otimização de headline/about/experiências LinkedIn, análise de SSI, ou alinhamento CV+LinkedIn. +metadata: + author: ft.ia.br + version: "2.0" + date: 2026-06-25 + repository: https://github.com/fabricioctelles/skills + license: Apache 2.0 + language: pt-BR + category: code-scaffolding-and-templates +--- + +# Resume ATS Beater + LinkedIn Optimizer + +## Contexto ATS no Brasil + +Aplicar as orientações deste skill considerando o cenário brasileiro de ATS (Gupy, Vagas.com, PandaPé e Sólides), no qual o ranqueamento combina parsing do currículo, aderência semântica à vaga, requisitos eliminatórios, histórico de progressão e desempenho em testes da plataforma. Tratar o processo como otimização de score e ordenação, não como aprovação binária. + +Aplicar avaliação com perspectiva dupla: especialista técnico em ATS e recrutador experiente no `cargo_alvo` e `industria_alvo`. Conciliar as duas lentes para maximizar parse/ranqueamento ATS e impacto na leitura humana de 6 segundos. + +## Modos de execução + +Executar em um dos modos abaixo. Quando não especificado, adotar `modo_completo` como padrão e informar o usuário. + +1. `modo_diagnostico`: analisar currículo atual e apontar melhorias ATS sem reescrita integral. +2. `modo_reescrita`: reescrever currículo completo sem entregar diagnóstico detalhado. +3. `modo_completo`: executar diagnóstico estruturado e, em seguida, entregar reescrita final. +4. `modo_linkedin`: auditoria completa do perfil LinkedIn com score, findings, fix prompts e mega-prompt de reescrita. +5. `modo_unificado`: executa `modo_completo` (CV) + `modo_linkedin` em sequência, garantindo consistência entre ambos. + +## Parâmetros de entrada + +### Obrigatórios + +Coletar antes de qualquer outra ação. Se `curriculo_atual` estiver ausente em modos CV, solicitá-lo imediatamente e não prosseguir. + +- `curriculo_atual` (texto ou arquivo): conteúdo completo do currículo atual. Obrigatório em modos CV (diagnostico, reescrita, completo, unificado). +- `cargo_alvo` (texto): cargo principal buscado. +- `industria_alvo` (texto): segmento/mercado alvo. +- `nivel_senioridade_alvo` (texto): júnior, pleno, sênior, liderança etc. +- `idioma_curriculo` (texto): idioma final do currículo. +- `requisitos_eliminatorios` (lista): critérios mandatórios da vaga (ex.: inglês fluente, cidade, certificação, disponibilidade). +- `modo_execucao` (texto): `modo_diagnostico`, `modo_reescrita`, `modo_completo`, `modo_linkedin` ou `modo_unificado`. + +### Obrigatórios para modo LinkedIn / unificado + +- `perfil_linkedin` (texto ou URL): conteúdo do perfil LinkedIn (headline, about, experiências, skills, featured, configurações) ou URL para análise. + +### Recomendados + +- `descricao_vaga` (texto): descrição da vaga alvo para extração de palavras-chave. +- `especificacao_vaga_completa` (texto): vaga completa (requisitos, responsabilidades, habilidades, empresa, benefícios e contexto). +- `metricas_por_experiencia` (lista): resultados por cargo (%, R$, tempo, volume, NPS, SLA etc.). +- `localizacao_alvo` (texto): país/região/cidade para adaptar vocabulário e contexto. +- `skills_prioritarias` (lista): competências estratégicas a enfatizar. +- `plataforma_ats_alvo` (texto): Gupy, Vagas.com, PandaPé, Sólides ou "não informado". Quando informada, priorizar vocabulário e critérios específicos da plataforma. +- `historico_linkedin` (objeto): datas/cargos/empresas para checagem de consistência com currículo. +- `detalhes_educacao` (objeto): GPA/CR, honras, certificações, projetos acadêmicos. +- `status_testes_plataforma` (objeto): testes exigidos e resultado atual (lógica, português, fit cultural etc.). +- `secoes_prioritarias_cv` (lista): seções que exigem otimização imediata. +- `ssi_scores` (objeto): pontuação SSI do LinkedIn (4 pilares + total). Opcional; quando fornecido, enriquece diagnóstico. +- `mercado_alvo` (texto): `brasil`, `gringa` ou `ambos`. Afeta idioma, formato de headline e vocabulário. +- `cargo_preset` (texto): preset de cargo para aplicar padrões específicos. Exemplos disponíveis em `references/presets-formatos.md`. O usuário pode definir qualquer cargo — presets são apenas atalhos. + +### Restrições de segurança factual + +- Não inventar conquistas, números, certificações ou responsabilidades. +- Solicitar complementos quando faltarem métricas relevantes. +- Preservar todos os cargos e formações existentes; alterar apenas quando correção factual for solicitada pelo usuário. +- Para LinkedIn: não fabricar endorsements, recomendações ou métricas de engajamento. + +--- + +## Workflow operacional — CV (modos diagnostico, reescrita, completo) + +### Etapa 1 — Validar objetivo e contexto + +1. Confirmar `curriculo_atual`; solicitar imediatamente se ausente. +2. Confirmar `cargo_alvo`, `industria_alvo`, `nivel_senioridade_alvo` e `idioma_curriculo`. +3. Solicitar `descricao_vaga` se não fornecida; informar ao usuário que a análise será mais genérica sem ela. +4. Coletar `requisitos_eliminatorios` e sinalizar riscos de eliminação prática. +5. Identificar lacunas críticas de dados, especialmente métricas por experiência. +6. Confirmar `modo_execucao`; adotar `modo_completo` se não especificado. +7. Pausar e coletar dados mínimos antes de avançar. + +### Etapa 2 — Diagnosticar currículo e aderência ATS + +> Executar esta etapa em `modo_diagnostico` e `modo_completo`. Pular para Etapa 3 em `modo_reescrita`. + +1. Mapear estrutura atual (resumo, experiência, educação, skills). +2. Medir aderência semântica com a vaga (competências, termos técnicos e contexto). +3. Identificar hard skills, soft skills e palavras-chave ausentes de alta prioridade. +4. Identificar bullets fracos (tarefa sem impacto), jargões vazios e habilidades obsoletas. +5. Verificar sinais de progressão de carreira e lacunas temporais. +6. Confirmar consistência com `historico_linkedin`, quando fornecido. +7. Aplicar os quatro eixos de análise definidos em `references/diagnostico-ats.md`. +8. Quando `descricao_vaga` ou `especificacao_vaga_completa` disponível, aplicar também o diagnóstico avançado definido em `references/diagnostico-avancado.md`. + +### Etapa 3 — Reescrever do zero com padrão ATS + +> Executar em `modo_reescrita` e `modo_completo`. Em `modo_diagnostico`, encerrar na Etapa 2. + +1. Reconstruir o currículo integralmente; não fazer edição superficial. +2. Manter todos os cargos e entradas de educação existentes. +3. Produzir `Resumo Profissional` em 3–4 linhas com identidade, anos de experiência, forças centrais e proposta de valor. +4. Reescrever cada experiência com 4–6 bullets orientados a resultado. +5. Priorizar verbos de ação e impacto mensurável. +6. Inserir palavras-chave do alvo de forma natural. +7. Escrever datas por extenso (ex.: "Janeiro de 2020 – Dezembro de 2021") para reduzir erro de cálculo de experiência. +8. Manter layout de coluna única e leitura linear. + +Formato por experiência: + +``` +Cargo +Empresa | Local | Datas + +• Bullet orientado a impacto +• Bullet orientado a impacto +• Bullet orientado a impacto +• Bullet orientado a impacto +``` + +### Etapa 4 — Otimizar educação e competências + +1. Manter grau, instituição e ano de conclusão. +2. Melhorar clareza e padronização da seção de educação. +3. Incluir honras, cursos relevantes, certificações e projetos apenas quando informados pelo usuário. +4. Organizar competências por categorias (`Técnicas`, `Ferramentas`, `Interpessoais`, `Idiomas`). +5. Remover competências desatualizadas ou desalinhadas com o `cargo_alvo`. + +### Etapa 5 — Validar conformidade ATS + +1. Usar cabeçalhos padrão: `Resumo Profissional`, `Experiência Profissional`, `Formação Acadêmica`, `Competências`. Evitar títulos criativos. +2. Evitar tabelas, múltiplas colunas, gráficos, caixas de texto, ícones e fotos. +3. Garantir legibilidade linear para parser ATS. +4. Garantir consistência de datas, verbos e densidade de palavras-chave ao longo do documento. +5. Usar termos exatos da vaga e, quando útil, variações com siglas e nomes completos. +6. Confirmar formato de arquivo final: DOCX ou PDF textual, nunca imagem escaneada. + +### Etapa 6 — Entregar saída final CV + +Estruturar a entrega conforme `references/template-saida.md`, respeitando o escopo do `modo_execucao`: + +- **Todos os modos CV**: itens 1–5 (currículo reescrito, resumo de melhorias, palavras-chave, checklist eliminatórios, riscos de ranking). +- **`modo_diagnostico` / `modo_completo`**: adicionar diagnóstico padrão (eixos I–V). +- **Qualquer modo com vaga definida**: adicionar diagnóstico avançado (PARTES A, B e C). + +--- + +## Workflow operacional — LinkedIn (modo_linkedin, modo_unificado) + +### Etapa L1 — Coletar perfil e contexto + +1. Confirmar `perfil_linkedin` (conteúdo textual ou URL). +2. Confirmar `cargo_alvo`, `industria_alvo`, `nivel_senioridade_alvo`. +3. Identificar `mercado_alvo` (brasil/gringa/ambos) — afeta idioma e formato. +4. Coletar `ssi_scores` se disponível; informar que enriquece a análise mas não é obrigatório. +5. Verificar se há `cargo_preset` aplicável ou definir padrões genéricos. + +### Etapa L2 — Auditar por área + +Auditar cada área do perfil LinkedIn aplicando regras de severidade. Para cada finding, classificar como: + +| Severidade | Penalidade | Significado | +|---|---|---| +| `critical` | -15 pts | Problema grave que prejudica visibilidade ou causa impressão negativa imediata | +| `warning` | -6 pts | Oportunidade perdida significativa | +| `info` | -2 pts | Melhoria incremental recomendada | +| `ok` | 0 pts | Área adequada, sem ação necessária | + +**Áreas auditadas:** + +1. **Foto e banner**: presença, qualidade profissional, alinhamento com área. +2. **Headline**: formato, keywords, diferenciação — ver formato obrigatório abaixo. +3. **About/Resumo**: estrutura, storytelling, CTA, keywords, comprimento. +4. **Experiências**: bullets com impacto, métricas, verbos de ação, progressão. +5. **Idioma do perfil**: coerência com `mercado_alvo`. +6. **Skills & Endorsements**: quantidade, relevância, ordenação. +7. **Featured/Destaques**: presença, qualidade, atualização. +8. **Configurações de visibilidade**: Open to Work, Creator Mode, URL customizada. +9. **Recomendações**: quantidade, diversidade (recebidas e dadas). +10. **Atividade/Engajamento**: frequência de posts, comentários, artigos. + +#### Formato obrigatório de headline + +``` +Posição | Áreas fortes | Competências-chave (separadas por ·) +``` + +**Exemplos por área:** + +- Marketing: `Head de Growth | Aquisição · CRO · Dados | Google Ads · HubSpot · SQL` +- Finanças: `Controller Sênior | FP&A · Tesouraria · M&A | SAP · Power BI · IFRS` +- Engenharia Civil: `Gerente de Obras | Infraestrutura · Orçamento · Planejamento | MS Project · AutoCAD · BIM` +- Dados: `Data Engineer Sênior | Pipelines · Lakehouse · MLOps | Spark · dbt · Airflow` +- Jurídico: `Advogado Tributarista | Planejamento Fiscal · Contencioso · M&A | Thomson Reuters · SPED` +- Dev: `Staff Engineer | Backend · Plataforma · Observabilidade | Go · K8s · Terraform` + +**Regras da headline:** +- Máximo 220 caracteres (limite LinkedIn). +- Sem buzzwords genéricas (ver constantes abaixo). +- Posição deve refletir o cargo almejado, não necessariamente o atual. +- Áreas fortes: 2–3 domínios de especialização. +- Competências-chave: 3–5 termos técnicos/ferramentas separados por `·`. + +### Etapa L3 — Calcular score + +Score punitivo: inicia em 100 e subtrai penalidades. + +``` +score_final = 100 - (qtd_critical × 15) - (qtd_warning × 6) - (qtd_info × 2) +``` + +**Classificação:** + +| Faixa | Status | +|---|---| +| 90–100 | Excelente — perfil competitivo | +| 75–89 | Bom — ajustes pontuais | +| 60–74 | Regular — melhorias necessárias | +| 40–59 | Fraco — reescrita recomendada | +| < 40 | Crítico — perfil prejudica candidatura | + +### Etapa L4 — Análise SSI (quando `ssi_scores` fornecido) + +#### Como auxiliar o usuário a obter o SSI + +Quando o usuário não informar `ssi_scores`, instruí-lo com: + +> **Para consultar seu SSI:** +> 1. Acesse [linkedin.com/sales/ssi](https://www.linkedin.com/sales/ssi) (precisa estar logado no LinkedIn). +> 2. A página mostra 4 scores (cada um de 0 a 25) e o total: +> - **Establish your professional brand** (Laranja) +> - **Find the right people** (Roxo) +> - **Engage with insights** (Verde) +> - **Build relationships** (Verde-água) +> 3. Me informe os 4 valores com decimais. Exemplo: `17.42, 10.01, 11.00, 15.60` +> +> Não precisa ter Sales Navigator — o link funciona com qualquer conta LinkedIn. + +**Se o usuário não conseguir acessar:** informar que a análise continua sem SSI, mas perde a camada de diagnóstico comportamental (rede, engajamento, relacionamentos). Prosseguir normalmente com as demais etapas. + +**Formato aceito para `ssi_scores`:** +- 4 números separados por vírgula na ordem: marca profissional, encontrar pessoas, engajar insights, construir relacionamentos +- Ou objeto com as 4 chaves nomeadas +- Aceitar decimais com ponto ou vírgula (ex: `17.42` ou `17,42`) + +--- + +O Social Selling Index mede 4 pilares (0–25 cada, total 0–100): + +| Pilar | Descrição | +|---|---| +| Marca Profissional | Completude do perfil, publicações, engajamento recebido | +| Encontrar Pessoas | Uso de busca, visualização de perfis, conexões estratégicas | +| Engajar com Insights | Compartilhamento, comentários, artigos publicados | +| Construir Relacionamentos | Conexões de decisores, taxa de aceitação, InMail | + +**Classificação por pilar:** + +| Pontuação | Status | Ação | +|---|---|---| +| 20–25 | Excelente | Manter cadência | +| 15–19 | Bom | Otimizações pontuais | +| 10–14 | Regular | Plano de ação semanal | +| 0–9 | Fraco | Intervenção urgente | + +Para cada pilar abaixo de 15, gerar 2–3 tips acionáveis específicos. + +### Etapa L5 — Gerar relatório + fix prompts + mega-prompt + +**Relatório de auditoria:** +1. Score final com breakdown de penalidades. +2. Findings organizados por severidade (critical primeiro). +3. Para cada finding: área, severidade, problema detectado, impacto. +4. Classificação SSI (quando disponível). + +**Fix prompts individuais:** +Para cada finding critical ou warning, gerar um prompt autônomo que o usuário pode usar em qualquer LLM para corrigir aquele item específico. Formato: + +``` +## Fix: [Área] — [Problema resumido] +Severidade: critical|warning + +### Contexto +[O que está errado e por que importa] + +### Prompt para correção +"Reescreva minha [seção] do LinkedIn considerando que sou [cargo_alvo] com experiência em [áreas]. +Meu perfil atual diz: [conteúdo atual]. +Reescreva para: [critérios específicos da regra violada]." + +### Onde editar no LinkedIn +[Caminho exato: Perfil → Editar → Seção → Campo] +``` + +**Mega-prompt de reescrita completa:** +Gerar um prompt único e consolidado para reescrever o perfil inteiro de uma vez, incorporando: +- Cargo alvo e mercado +- Formato de headline obrigatório +- Estrutura de about (storytelling + keywords + CTA) +- Padrão de bullets de experiência +- Skills priorizadas +- Idioma adequado ao `mercado_alvo` + +### Etapa L6 — Mapa de edição no LinkedIn + +Fornecer instruções de navegação para cada correção: + +| Seção | Caminho no LinkedIn | +|---|---| +| Foto/Banner | Perfil → ícone de câmera | +| Headline | Perfil → ícone de lápis (intro) → Título | +| About | Perfil → Sobre → ícone de lápis | +| Experiência | Perfil → Experiência → + ou lápis | +| Skills | Perfil → Competências → + ou reordenar | +| Featured | Perfil → Destaques → + | +| URL customizada | Configurações → Visibilidade → Editar perfil público → URL | +| Open to Work | Perfil → botão "Disponível para" | +| Idioma | Perfil → lápis (intro) → "Nome do perfil em outro idioma" | + +--- + +## Workflow — Modo unificado + +No `modo_unificado`, executar sequencialmente: + +1. Workflow CV completo (Etapas 1–6). +2. Workflow LinkedIn completo (Etapas L1–L6). +3. **Etapa de consistência**: verificar alinhamento entre CV e LinkedIn: + - Cargos e datas idênticos. + - Headline LinkedIn coerente com resumo profissional do CV. + - Keywords presentes em ambos. + - Sem contradições factuais. +4. Reportar divergências encontradas com sugestão de correção. + +--- + +## Integração com skill `humanizar` + +Quando a skill `humanizar` estiver disponível, aplicá-la **após a geração de conteúdo** e **antes da validação final**, com escopo restrito: + +### Seções onde rodar `humanizar` +- About / Resumo do LinkedIn +- Resumo Profissional do CV +- Carta de apresentação (se houver) + +### Seções onde NÃO rodar `humanizar` +- Headline LinkedIn (formato rígido 3 blocos) +- Bullets de experiência (formato ATS: verbo + métrica + ferramenta + impacto) +- Seção de competências / skills +- Educação e certificações +- Configurações (Open to Work, idioma, URL) + +### Fluxo com `humanizar` + +1. `resume-ats-beater` gera o conteúdo (reescrita/auditoria) +2. `humanizar` roda nas seções permitidas (preset sugerido: `corporativo-informal` ou `crônica` conforme tom do mercado) +3. `resume-ats-beater` valida pós-humanização: + - Keywords do cargo-alvo ainda presentes? + - Métricas/números intactos? + - Comprimento dentro do range (About: 1000-2000 chars)? + - Nenhuma informação factual removida? +4. Se validação falhar → restaurar trecho afetado da versão pré-humanização + +### Quando NÃO invocar `humanizar` +- `modo_diagnostico` (não há reescrita) +- Quando o usuário pedir explicitamente tom formal/acadêmico +- Textos jurídicos ou regulatórios + +--- + +## Constantes + +### Buzzwords rejeitadas (não usar em headline, about ou bullets) + +``` +proativo, dinâmico, resiliente, apaixonado, entusiasta, inovador, +criativo, visionário, guru, ninja, rockstar, evangelista, +pensador estratégico, orientado a resultados (sem dados), +profissional comprometido, busco novos desafios, em busca de recolocação +``` + +### Verbos de ação preferidos (usar em bullets de experiência) + +``` +Liderou, Implementou, Reduziu, Aumentou, Otimizou, Automatizou, +Negociou, Estruturou, Escalou, Desenvolveu, Migrou, Consolidou, +Projetou, Integrou, Reestruturou, Capacitou, Mensurou, Viabilizou, +Orquestrou, Acelerou, Recuperou, Transformou, Padronizou +``` + +### Regex de métricas (validar presença em bullets) + +``` +\d+%|\d+x|R\$[\d.,]+|US\$[\d.,]+|\d+\s*(pessoas|clientes|usuários|projetos|contratos|unidades|obras|operações) +``` + +Bullets sem match neste regex em pelo menos 50% das experiências geram warning `métricas_insuficientes`. + +--- + +## Presets de cargo + +Presets são atalhos opcionais com padrões de headline, skills priorizadas e vocabulário técnico. Disponíveis em `references/presets-formatos.md`. + +O usuário pode definir qualquer cargo — presets apenas aceleram a configuração. + +**Exemplos de presets disponíveis:** + +| Preset | Área | +|---|---| +| `marketing_growth` | Marketing Digital / Growth | +| `dados_engenharia` | Engenharia de Dados | +| `dados_analytics` | Analytics / BI | +| `financas_controller` | Controladoria / FP&A | +| `financas_tesouraria` | Tesouraria / Cash Management | +| `engcivil_obras` | Gerência de Obras | +| `engcivil_projetos` | Projetos e Orçamento | +| `juridico_tributario` | Direito Tributário | +| `juridico_compliance` | Compliance / LGPD | +| `dev_backend` | Desenvolvimento Backend | +| `dev_fullstack` | Desenvolvimento Full Stack | +| `dev_platform` | Platform / SRE / DevOps | +| `produto_pm` | Product Manager | +| `produto_design` | Product Design / UX | +| `vendas_enterprise` | Vendas Enterprise / Key Account | +| `vendas_sdrbdr` | SDR / BDR | +| `rh_hrbp` | HR Business Partner | +| `rh_ta` | Talent Acquisition | + +--- + +## Checklist de qualidade + +Verificar antes de entregar: + +### CV +- [ ] Resumo profissional objetivo, específico e alinhado ao `cargo_alvo`. +- [ ] Experiências com foco em resultado e impacto; métricas incluídas quando disponíveis. +- [ ] Nenhuma invenção factual; fatos preservados do currículo original. +- [ ] Estrutura ATS-safe: coluna única, cabeçalhos padrão, sem elementos gráficos. +- [ ] Alinhamento explícito com cargo, indústria e senioridade alvo. +- [ ] Requisitos eliminatórios mapeados e confrontados com status claro. +- [ ] Datas por extenso, coerentes e sem lacunas inexplicadas. +- [ ] Consistência com LinkedIn validada quando dados fornecidos. +- [ ] Equilíbrio entre ranqueamento ATS e poder de convencimento em leitura humana rápida. + +### LinkedIn +- [ ] Headline segue formato obrigatório `Posição | Áreas | Competências·`. +- [ ] About com storytelling + keywords + CTA (não muro de texto genérico). +- [ ] Experiências com bullets de impacto (não descrição de cargo). +- [ ] Sem buzzwords rejeitadas em nenhuma seção. +- [ ] Regex de métricas aprovado em ≥50% dos bullets. +- [ ] Score calculado e classificado corretamente. +- [ ] Fix prompts gerados para todo finding critical/warning. +- [ ] Mega-prompt coerente com cargo_alvo e mercado_alvo. +- [ ] Mapa de edição fornecido com caminhos corretos. +- [ ] Idioma do perfil coerente com mercado_alvo. + +### Modo unificado (adicional) +- [ ] Cargos e datas idênticos entre CV e LinkedIn. +- [ ] Keywords presentes em ambos os documentos. +- [ ] Sem contradições factuais entre perfil e currículo. +- [ ] Headline LinkedIn coerente com resumo profissional do CV. + +--- + +## Referências + +| Arquivo | Conteúdo | +|---|---| +| `references/diagnostico-ats.md` | Eixos de análise ATS (parsing, aderência, eliminatórios, progressão) | +| `references/diagnostico-avancado.md` | Diagnóstico avançado com vaga definida (PARTES A, B, C) | +| `references/template-saida.md` | Template de entrega final do CV | +| `references/auditoria-linkedin.md` | Regras detalhadas de auditoria LinkedIn por área, severidades, exemplos por cargo | +| `references/ssi.md` | Guia SSI: pilares, benchmarks por indústria, tips acionáveis por faixa | +| `references/presets-formatos.md` | Presets de cargo com headline, skills, vocabulário técnico por área | diff --git a/.github/skills/resume-ats-beater/references/auditoria-linkedin.md b/.github/skills/resume-ats-beater/references/auditoria-linkedin.md new file mode 100644 index 0000000..bbcf3f0 --- /dev/null +++ b/.github/skills/resume-ats-beater/references/auditoria-linkedin.md @@ -0,0 +1,303 @@ +# Auditoria de Perfil LinkedIn — Regras por Área + +> Referência para auditoria de perfis LinkedIn de profissionais especializados (qualquer área). +> Não é específico para devs — aplica-se a marketing, produto, dados, design, finanças, operações, etc. + +--- + +## 1. Headline + +### Formato obrigatório + +``` +Posição | Áreas fortes | Ferramentas · Metodologias · Certificações +``` + +**Exemplos:** + +- `Product Manager | Growth & Monetization | Amplitude · SQL · Lean Six Sigma` +- `Data Analyst | BI & Analytics | Power BI · Python · dbt` +- `UX Designer | Mobile & SaaS | Figma · Design Systems · WCAG` + +### Regras de auditoria + +| Condição | Severidade | +|---|---| +| Headline vazia | critical | +| Fora do padrão 3 blocos (Posição \| Áreas \| Ferramentas) | critical | +| Sem keywords do cargo-alvo | critical | +| Curta < 40 caracteres | warning | +| Contém buzzwords rejeitadas | warning | + +### Buzzwords rejeitadas + +**EN:** passionate, results-driven, hard-working, team player, self-motivated, guru, ninja, rockstar, wizard, thought leader, synergy, go-getter, detail-oriented, motivated + +**PT:** apaixonado, proativo, dinâmico, comprometido, esforçado + +--- + +## 2. Language + +### Regras de auditoria + +| Condição | Severidade | +|---|---| +| Mercado internacional e English não listado em Languages | critical | +| Perfil escrito em PT para mercado internacional | critical | + +### Detecção de idioma + +Contar stopwords PT vs EN no conteúdo do perfil: + +- **PT:** de, da, do, em, para, com, que, uma, os, as, no, na +- **EN:** the, and, to, of, in, for, with, that, on, is, at, by + +Se ratio PT > 60% e mercado-alvo é internacional → critical. + +--- + +## 3. About + +### Regras de auditoria + +| Condição | Severidade | +|---|---| +| Vazio | critical | +| < 300 caracteres | warning | +| Sem keywords do cargo-alvo | warning | +| Sem métricas quantificáveis | warning | + +### Modelo de estrutura + +``` +1. Abertura: anos de experiência + foco principal +2. Empresa atual: o que faz + escala (equipe, receita, usuários) +3. Áreas de atuação / especialidades +4. Experiência anterior com provas (métricas, resultados) +5. Lista de competências / stack / ferramentas +``` + +### Tamanho ideal + +- Mínimo aceitável: 300 caracteres +- Ideal: 1000–2000 caracteres +- Máximo útil: 2600 caracteres (limite do LinkedIn) + +--- + +## 4. Experiences + +### Regras de auditoria + +| Condição | Severidade | +|---|---| +| Nenhuma experiência listada | critical | +| Experiência sem bullets descritivos | warning | +| Mais de 5 bullets por experiência | warning | +| Nenhum bullet com métrica quantificada | warning | +| Título atual ≠ cargo-alvo | warning | +| Poucos verbos de ação nos bullets | info | +| Bullet > 320 caracteres | info | + +### Formato ideal de bullet + +``` +Verbo de ação + métrica + ferramentas/metodologia + impacto +``` + +- ~3 linhas por bullet +- Máximo 5 bullets por experiência +- Pelo menos 1 bullet com número concreto + +**Exemplos:** + +- `Reduzi churn em 18% implementando modelo preditivo com Python e Amplitude, gerando R$240k/ano em receita retida` +- `Liderei redesign do checkout mobile com Figma e A/B testing, aumentando conversão de 2.1% para 3.4%` +- `Gerenciei portfólio de 12 projetos (R$8M budget) usando Jira e OKRs, entregando 94% no prazo` + +--- + +## 5. Skills + +### Regras de auditoria + +| Condição | Severidade | +|---|---| +| > 3 skills listadas e nenhuma bate com keywords do cargo | info | + +> **Nota:** O PDF exportado do LinkedIn mostra apenas as top 3 skills. Não auditar quantidade total — focar em relevância das primeiras. + +### Recomendação + +Reordenar skills para que as 3 primeiras sejam exatamente as keywords mais importantes do cargo-alvo. + +--- + +## 6. Education + +### Regras de auditoria + +| Condição | Severidade | +|---|---| +| Sem formação listada | info | +| Sem certificações relevantes | info | + +### Recomendação + +Incluir certificações relevantes para a área (ex: PMP, AWS, CFA, Google Analytics, Scrum Master, etc.). + +--- + +## 7. Featured + +> **Nota:** Seção Featured não aparece no PDF exportado. Auditoria via dica fixa. + +### Recomendação fixa + +Ter pelo menos 1 item em Featured: +- Certificação relevante +- Projeto com resultado mensurável +- Artigo/publicação da área +- Case study ou apresentação + +--- + +## 8. Job Preferences + +### Regras de auditoria + +| Condição | Severidade | +|---|---| +| Open to Work não ativado (ou ativado para todos) | info | +| Start date não configurado como Immediately | info | +| Profile language diferente do idioma do país-alvo | info | + +### Recomendação + +- Ativar Open to Work como **Recruiters only** (não mostra badge público) +- Start date: **Immediately** (sinaliza disponibilidade) +- Profile language: idioma do mercado-alvo + +--- + +## Cálculo do Score + +``` +score = 100 - soma_penalidades +``` + +| Severidade | Penalidade | +|---|---| +| critical | -15 | +| warning | -6 | +| info | -2 | + +- Score mínimo: **0** (não vai negativo) +- Score ≥ 80: perfil competitivo +- Score 60–79: precisa ajustes +- Score < 60: requer reescrita significativa + +--- + +## Verbos de Ação Aceitos + +### Inglês + +led, built, designed, shipped, launched, scaled, reduced, increased, improved, automated, migrated, delivered, owned, drove, created, implemented, optimized, architected, managed + +### Português + +liderei, construí, criei, reduzi, aumentei, automatizei, entreguei, implementei, otimizei, escalei, desenvolvi + +--- + +## Regex de Métricas + +Padrão para detectar quantificação em bullets: + +```regex +(\d+[\d.,]*\s?%|\$\s?\d|\bR\$\s?\d|\b\d[\d.,]*\s?(k|m|mi|mil|million|users|clientes|x)\b|\b\d+\b) +``` + +### Exemplos que matcham + +- `18%`, `3.4%` +- `$2M`, `R$240k` +- `500k users`, `12 clientes` +- `3x`, `94` + +--- + +## Mapa de Onde Editar no LinkedIn + +| Seção | Caminho no LinkedIn | +|---|---| +| Headline | Perfil → ícone lápis no card principal → Headline | +| About | Perfil → seção "About" → ícone lápis | +| Experience | Perfil → seção "Experience" → + ou lápis na posição | +| Skills | Perfil → seção "Skills" → + Add skill / reordenar | +| Language | Perfil → seção "Languages" → + Add language | +| Education | Perfil → seção "Education" → + ou lápis | +| Featured | Perfil → seção "Featured" → + Add featured | +| Open to Work | Perfil → botão "Open to" → Finding a new job → Recruiters only | +| Profile language | Settings → Account → Site preferences → Language | +| SSI | [linkedin.com/sales/ssi](https://www.linkedin.com/sales/ssi) | + +--- + +## Fix Prompt Individual — Template + +Para cada finding gerado pela auditoria, produzir um prompt standalone com a estrutura abaixo: + +```markdown +## Fix: [Nome da Seção] — [Descrição curta do problema] + +**Onde editar:** [caminho no LinkedIn da tabela acima] + +**Contexto:** O objetivo é [cargo-alvo] no mercado [país/região]. + +**Problema identificado:** [descrição do finding] + +**Conteúdo atual:** +> [texto atual do perfil nessa seção, se disponível] + +**Regras desta seção:** +- [regras específicas da área auditada] + +**Tarefa:** +1. Explique por que isso prejudica o perfil +2. Forneça texto pronto para colar no LinkedIn, seguindo o formato obrigatório + +**Texto sugerido:** +> [texto otimizado pronto para copiar e colar] +``` + +### Exemplo de Fix Prompt gerado + +```markdown +## Fix: Headline — Fora do padrão 3 blocos + +**Onde editar:** Perfil → ícone lápis no card principal → Headline + +**Contexto:** O objetivo é Product Manager no mercado EUA. + +**Problema identificado:** Headline atual não segue o formato +`Posição | Áreas | Ferramentas`. Está usando formato livre com buzzwords. + +**Conteúdo atual:** +> "Passionate Product Leader driving results" + +**Regras desta seção:** +- Formato: `Posição | Áreas fortes | Ferramentas · Metodologias` +- Sem buzzwords (passionate, results-driven, etc.) +- Deve conter keywords do cargo-alvo +- Mínimo 40 caracteres + +**Tarefa:** +1. "Passionate" e "driving results" são buzzwords genéricas que não ajudam ATS nem recruiters. O formato 3 blocos permite scanning rápido. +2. Texto pronto: + +**Texto sugerido:** +> Product Manager | Growth · Monetization · B2B SaaS | Amplitude · SQL · Mixpanel +``` diff --git a/.github/skills/resume-ats-beater/references/diagnostico-ats.md b/.github/skills/resume-ats-beater/references/diagnostico-ats.md new file mode 100644 index 0000000..ea65ba0 --- /dev/null +++ b/.github/skills/resume-ats-beater/references/diagnostico-ats.md @@ -0,0 +1,50 @@ +# Diagnóstico ATS — Quatro Eixos de Análise + +Aplicar os quatro eixos abaixo no diagnóstico do currículo atual. + +## I. Compatibilidade Geral ATS + +Classificar como **baixa**, **média** ou **alta** e justificar em uma frase. + +## II. Otimização de Palavras-chave + +**A. Palavras-chave Ausentes** +Listar 5–10 palavras-chave ausentes relevantes ao `cargo_alvo`, com justificativa de prioridade. + +**B. Densidade de Palavras-chave** +Avaliar termos subutilizados e sobreutilizados; sinalizar desequilíbrios de frequência. + +**C. Posicionamento de Palavras-chave** +Recomendar seção ideal de inserção para cada termo: resumo, experiências ou competências. + +## III. Análise de Formatação + +**A. Formato de Arquivo** +Validar se o formato é DOCX ou PDF textual (nunca imagem escaneada). + +**B. Fonte e Estilo** +Verificar legibilidade da fonte e ausência de elementos que quebram parsing (tabelas, gráficos, caixas de texto, ícones, fotos, múltiplas colunas). + +**C. Títulos de Seção** +Validar uso de cabeçalhos padrão ATS: `Resumo Profissional`, `Experiência Profissional`, `Formação Acadêmica`, `Competências`. Sinalizar títulos criativos que comprometem reconhecimento. + +**D. Listas e Bullets** +Validar consistência de marcadores e estrutura de listas. + +## IV. Clareza e Legibilidade + +**A. Jargões e Siglas** +Identificar termos pouco reconhecíveis por ATS ou recrutadores; sugerir substituições ou expansões. + +**B. Verbos de Ação** +Identificar bullets passivos ou descritivos; propor verbos de ação orientados a impacto. + +**C. Resultados Quantificáveis** +Apontar bullets sem métrica e sugerir dados que poderiam ser incluídos. + +**D. Datas e Lacunas** +Validar consistência de datas (formato por extenso: "Janeiro de 2020 – Dezembro de 2021"); sinalizar lacunas temporais não explicadas. + +## V. Recomendações Adicionais + +Listar melhorias práticas de parsing não cobertas acima, por exemplo: simplificar dados de contato, remover cabeçalho/rodapé complexo, remover foto, evitar elementos gráficos decorativos. diff --git a/.github/skills/resume-ats-beater/references/diagnostico-avancado.md b/.github/skills/resume-ats-beater/references/diagnostico-avancado.md new file mode 100644 index 0000000..f633960 --- /dev/null +++ b/.github/skills/resume-ats-beater/references/diagnostico-avancado.md @@ -0,0 +1,31 @@ +# Diagnóstico Avançado com Vaga Definida + +Executar as três partes abaixo quando `descricao_vaga` ou `especificacao_vaga_completa` estiver disponível. + +## PARTE A — Otimização ATS + +1. Extrair 20 palavras-chave/frases críticas da vaga e ranquear de 1 a 20 por relevância para o `cargo_alvo`. +2. Mapear em qual seção do currículo cada palavra-chave deve aparecer (resumo / experiência / competências) e sugerir frequência de uso. +3. Propor 5 trocas terminológicas específicas para elevar aderência semântica (ex.: "gestão de projetos" → "gerenciamento de projetos ágeis"). +4. Sinalizar problemas de estrutura ou formatação que prejudicam o parsing automático desta vaga específica. + +## PARTE B — Engajamento Humano + +1. Pontuar de 1 a 10 a eficácia de escaneabilidade em 6 segundos para o `cargo_alvo`. +2. Identificar 3 pontos de subvalorização de resultados no currículo atual. +3. Propor 2 formas de adicionar personalidade profissional sem comprometer a objetividade. +4. Recomendar hierarquia ideal de informação para atrair o recrutador desta vaga. + +## PARTE C — Estratégia de Integração + +1. Demonstrar como inserir palavras-chave prioritárias de forma natural em bullets de conquista. +2. Fornecer pelo menos 3 exemplos concretos de transformação: + +**Bullet fraco → Bullet forte** + +| Antes | Depois | +|---|---| +| "Responsável pela área de marketing" | "Liderou equipe de 5 pessoas e aumentou o tráfego orgânico em 40% em 6 meses" | +| "Atuei no suporte ao cliente" | "Reduziu o tempo médio de resolução de 48h para 12h, elevando NPS de 62 para 78" | + +Adaptar os exemplos ao `cargo_alvo` e `industria_alvo` do candidato. diff --git a/.github/skills/resume-ats-beater/references/presets-formatos.md b/.github/skills/resume-ats-beater/references/presets-formatos.md new file mode 100644 index 0000000..a80f684 --- /dev/null +++ b/.github/skills/resume-ats-beater/references/presets-formatos.md @@ -0,0 +1,137 @@ +# Presets de Cargo & Formatos Canônicos + +> Referência para auditoria LinkedIn e currículo ATS. Cobre profissionais especializados em geral — não apenas dev. + +--- + +## Presets de Cargo + +Cada preset contém: **label**, **role**, **keywords**, **headline_areas**, **headline_tech** (ferramentas/competências). + +### Tecnologia + +| Label | Headline Canônica | +|---|---| +| `backend-engineer` | Backend Engineer \| APIs, Microservices & Distributed Systems \| Python · Java · Go · PostgreSQL · AWS · Kubernetes | +| `frontend-engineer` | Frontend Engineer \| Web Apps, UX Performance & Design Systems \| React · TypeScript · Next.js · CSS · Performance | +| `fullstack-engineer` | Full Stack Engineer \| Product Engineering, APIs & Full Stack Delivery \| React · Node.js · TypeScript · PostgreSQL · AWS | +| `data-engineer` | Data Engineer \| Data Platform, CDP & Reliability \| GCP · Airflow · BigQuery · Spark · Terraform · dbt | +| `devops-engineer` | DevOps / SRE \| Cloud Infrastructure, Reliability & Platform Ops \| Kubernetes · Terraform · AWS · Docker · CI/CD | +| `mobile-engineer` | Mobile Engineer \| Mobile Apps, Performance & Cross-Platform \| Kotlin · Swift · Flutter · React Native | +| `staff-engineer` | Staff Software Engineer \| Platform Architecture, Scale & Technical Leadership \| System Design · Distributed Systems · Cloud | + +### Dados & Analytics + +| Label | Headline Canônica | +|---|---| +| `data-scientist` | Data Scientist \| Machine Learning, Statistical Modeling & Business Intelligence \| Python · R · TensorFlow · SQL · Tableau | +| `data-analyst` | Data Analyst \| Business Intelligence, Reporting & Data Visualization \| SQL · Power BI · Tableau · Excel · Python | +| `analytics-engineer` | Analytics Engineer \| Data Modeling, Metrics & Self-Serve Analytics \| dbt · SQL · Looker · BigQuery · Snowflake | + +### Marketing & Growth + +| Label | Headline Canônica | +|---|---| +| `growth-marketer` | Growth Marketing Manager \| Acquisition, Retention & Experimentation \| Google Ads · Meta Ads · GA4 · HubSpot · A/B Testing | +| `product-marketer` | Product Marketing Manager \| Positioning, Launch Strategy & Sales Enablement \| Messaging · Competitive Intel · Content · GTM | +| `seo-specialist` | SEO Specialist \| Technical SEO, Content Strategy & Link Building \| Ahrefs · Screaming Frog · GSC · GA4 · Schema | + +### Finanças & Negócios + +| Label | Headline Canônica | +|---|---| +| `financial-analyst` | Financial Analyst \| FP&A, Modeling & Strategic Planning \| Excel · Power BI · SAP · Bloomberg · SQL | +| `product-manager` | Product Manager \| Discovery, Roadmap & Delivery \| Jira · Amplitude · Figma · SQL · OKRs | +| `management-consultant` | Management Consultant \| Strategy, Operations & Digital Transformation \| McKinsey 7S · Lean · Excel · PowerPoint | + +### Engenharia & Indústria + +| Label | Headline Canônica | +|---|---| +| `mechanical-engineer` | Mechanical Engineer \| Product Design, FEA & Manufacturing \| SolidWorks · AutoCAD · ANSYS · GD&T · Lean Manufacturing | +| `civil-engineer` | Civil Engineer \| Structural Design, Project Management & BIM \| AutoCAD · Revit · SAP2000 · MS Project · BIM 360 | + +> **NOTA:** Estes são exemplos. O usuário pode definir qualquer cargo — o agente deve adaptar keywords e sugestões ao contexto fornecido. + +--- + +## Formatos Canônicos + +### Headline LinkedIn + +**Formato:** `{Posição} | {Áreas de trabalho mais fortes} | {Ferramentas/Competências com ·}` + +**Regras:** +- Exatamente 3 blocos separados por `|` +- Bloco 1: cargo/posição (com senioridade quando relevante) +- Bloco 2: áreas de domínio / especialidades +- Bloco 3: ferramentas, tecnologias ou competências-chave separadas por `·` +- Máximo 220 caracteres + +**Exemplo:** +``` +Senior Data Engineer | Data Platform, CDP & Reliability | GCP · Airflow · BigQuery · Spark · Terraform +``` + +--- + +### Bullet de Experiência (LinkedIn) + +**Formato:** `Ação + métrica em destaque + ferramentas/tecnologias + impacto para a empresa` + +**Regras:** +- ~3 linhas máximo por bullet +- Máximo 5 bullets por experiência +- Iniciar com verbo de ação +- Incluir pelo menos 1 métrica por bullet quando possível + +**Exemplo:** +``` +Productionized a multi-agent AI remediation platform using Google ADK, FastAPI, LLMs, RAG, +Kubernetes, and GitHub-hosted runbooks, resolving ~70% of recurring low-risk KTLO incidents +across Airflow, Dataproc, BigQuery, and Keboola +``` + +--- + +### About LinkedIn + +**Modelo narrativo:** +1. Abertura com anos de experiência + foco principal +2. Empresa atual com escala/métricas +3. Áreas de atuação +4. Experiência anterior com provas +5. Lista de domínios/competências/stack no final + +**Tamanho:** 1000–2000 caracteres ideal + +**Exemplo:** +``` +I'm a Senior Data Engineer and Cloud Data Architect with 6+ years of experience focused on +helping create and support scalable, reliable, and governed data platforms across GCP and Azure. + +At ShopNova, I work on petabyte-scale data platforms supporting 2,000+ pipelines, 500+ +Airflow/Composer DAGs, and large-scale GCP workloads. [...] + +My strongest areas are Cloud Data Architecture, Data Engineering, Airflow/Composer, GCP, Azure, +BigQuery, Dataproc, Databricks, PySpark, Terraform, Kubernetes, CDP, Observability, and AI +Automation. +``` + +--- + +### Bullet de Currículo (ATS) + +**Formato:** `Verbo de ação + resultado quantificado + contexto/ferramenta` + +**Regras:** +- 1–2 linhas por bullet +- 4–6 bullets por experiência +- Priorizar impacto mensurável +- Usar verbos de ação fortes (Projetou, Implementou, Reduziu, Automatizou, Liderou, Otimizou) + +**Exemplo:** +``` +Reduziu tempo de processamento de pipelines em 40% ao migrar jobs Spark para Dataproc Serverless com Terraform +Automatizou 200+ DAGs no Airflow/Composer, eliminando 15h/semana de intervenção manual +``` diff --git a/.github/skills/resume-ats-beater/references/ssi.md b/.github/skills/resume-ats-beater/references/ssi.md new file mode 100644 index 0000000..ad9bd5e --- /dev/null +++ b/.github/skills/resume-ats-beater/references/ssi.md @@ -0,0 +1,139 @@ +# Social Selling Index (SSI) — Referência de Interpretação + +> Acesso: [linkedin.com/sales/ssi](https://linkedin.com/sales/ssi) + +O SSI mede sua presença no LinkedIn em 4 pilares (0-25 cada), total 0-100. + +--- + +## Classificação + +### Por pilar (0-25) + +| Score | Nível | +|-------|-------| +| ≥ 18 | Forte | +| 12-17 | Ok | +| < 12 | Fraco | + +### Total (0-100) + +| Score | Nível | +|-------|-------| +| ≥ 50 | Bom | +| 30-49 | Mediano | +| < 30 | Fraco | + +--- + +## Pilares e Tips Acionáveis + +### 1. Professional Brand (Laranja) + +**Significado:** Quão bem seu perfil vende quem você é para recrutadores. + +#### Se fraco (< 12): +- Perfil mal posicionado — recrutadores não entendem em 5s o que você faz +- Reescrever headline no padrão: `Posição | Áreas | Ferramentas` +- Reescrever About com métricas e CTA +- Foto profissional + banner com área de atuação +- Featured section: certificação, projeto, artigo ou case +- Fixar 3 skills mais relevantes ao cargo +- Pedir 2-3 recomendações de gestores/colegas + +#### Se ok (12-17): +- Refinar headline e About com mais uma métrica concreta +- Adicionar Featured se vazia + +#### Se forte (≥ 18): +- Manter headline e About atualizados a cada mudança de foco + +--- + +### 2. Find People (Roxo) + +**Significado:** Se sua rede não tem recrutadores e profissionais da área, você não aparece para quem contrata. + +#### Se fraco (< 12): +- Rede fraca para inbound — adicione recrutadores e profissionais da área +- Buscar "Recruiter" + sua especialidade +- Buscar cargo-alvo e conectar 10-20 pessoas/semana +- Buscar "Talent Acquisition" + empresa-alvo +- Conectar com profissionais de empresas-alvo +- Seguir páginas de empresas-alvo e comentar posts + +#### Se ok (12-17): +- Dobrar conexões com recrutadores da área esta semana +- Buscar "Hiring [cargo]" nos posts e conectar com quem publicou + +#### Se forte (≥ 18): +- Continuar adicionando recrutadores de empresas-alvo + +--- + +### 3. Engage Insights (Verde) + +**Significado:** LinkedIn premia quem interage. Sem curtir, comentar e postar, fica invisível. + +#### Se fraco (< 12): +- Quase não engaja — algoritmo não mostra perfil para recrutadores +- Curtir e comentar posts de líderes da área 3-5x/semana +- Pelo menos 1 post técnico/profissional por semana +- Ideias: problema resolvido, ferramentas que usa, lição de projeto, opinião sobre trend +- Comentar com valor (não só "ótimo post") +- Compartilhar artigo com perspectiva própria + +#### Se ok (12-17): +- Subir para 1 post/semana + comentários diários +- Agendar 15 min/dia para comentar em 3 posts do nicho + +#### Se forte (≥ 18): +- Manter consistência — algoritmo recompensa frequência + +--- + +### 4. Build Relationships (Verde-água) + +**Significado:** Conexão sem conversa não gera oportunidade. + +#### Se fraco (< 12): +- Adiciona pessoas mas não conversa — conexão parada não vira contato +- Adicionar pessoas que já conhece e mandar mensagem +- Responder DMs de recrutadores em <24h +- Após conectar com recrutador: nota curta com especialidade + disponibilidade +- Reativar conexões antigas +- Participar de grupos da área + +#### Se ok (12-17): +- Foco em follow-up com recrutadores que já adicionaram +- 30 min/semana para responder DMs pendentes + +#### Se forte (≥ 18): +- Continuar nutrindo conexões com recrutadores ativos + +--- + +## SSI Fix Prompt (Template) + +Para cada pilar fraco/ok, gerar coaching com: + +1. **Plano de 7 dias** com ações concretas +2. **3 ações para hoje** (com exemplos) +3. **Rotina semanal mínima** (minutos/dia) +4. **2 templates prontos** (conexão, comentário ou post) +5. **Como medir** se está funcionando + +### Exemplo de prompt: + +``` +Seu pilar [NOME_PILAR] está [NÍVEL] (score: [X]/25). + +Crie um plano de 7 dias para subir este pilar com: +- 1 ação concreta por dia +- 3 ações que posso fazer AGORA (com texto pronto) +- Rotina mínima diária (em minutos) +- 2 templates prontos para usar hoje +- Métrica para saber se está funcionando em 2 semanas + +Contexto: sou [CARGO/ÁREA], buscando [OBJETIVO]. +``` diff --git a/.github/skills/resume-ats-beater/references/template-saida.md b/.github/skills/resume-ats-beater/references/template-saida.md new file mode 100644 index 0000000..760d438 --- /dev/null +++ b/.github/skills/resume-ats-beater/references/template-saida.md @@ -0,0 +1,31 @@ +# Template de Saída — Entregáveis por Modo + +## Entregáveis Obrigatórios (todos os modos) + +1. **Currículo completo** reescrito e otimizado para ATS. +2. **Resumo das melhorias**: síntese das principais mudanças aplicadas. +3. **Palavras-chave adicionadas**: lista das palavras-chave inseridas e em qual seção. +4. **Checklist de requisitos eliminatórios**: tabela com status por critério — `atende` / `não atende` / `pendente de evidência`. +5. **Riscos de ranking**: lista de fatores que podem reduzir o score (ex.: ausência de métrica, gap temporal não explicado, termo crítico faltante). + +## Entregáveis Adicionais — Diagnóstico Padrão + +Incluir quando `modo_execucao` = `modo_diagnostico` ou `modo_completo`. + +Estruturar usando os quatro eixos definidos em `references/diagnostico-ats.md`: + +- **I. Avaliação Geral de Compatibilidade ATS** +- **II. Otimização de Palavras-chave** (A. Ausentes · B. Densidade · C. Posicionamento) +- **III. Análise de Formatação** (A. Formato · B. Fonte e Estilo · C. Títulos · D. Bullets) +- **IV. Clareza e Legibilidade** (A. Jargões · B. Verbos · C. Métricas · D. Datas) +- **V. Recomendações Adicionais** + +## Entregáveis Adicionais — Diagnóstico Avançado + +Incluir quando `descricao_vaga` ou `especificacao_vaga_completa` estiver disponível, além do diagnóstico padrão. + +Estruturar usando as três partes definidas em `references/diagnostico-avancado.md`: + +- **PARTE A — Otimização ATS** (20 palavras-chave ranqueadas · mapa de posicionamento · 5 trocas terminológicas · problemas de parsing) +- **PARTE B — Engajamento Humano** (nota de escaneabilidade 1–10 · 3 pontos de subvalorização · 2 ajustes de personalidade · hierarquia recomendada) +- **PARTE C — Estratégia de Integração** (exemplos de keywords em bullets · transformações concretas fraco → forte) diff --git a/.github/skills/revenue-centric-design/LICENSE b/.github/skills/revenue-centric-design/LICENSE new file mode 100644 index 0000000..ccb7f4b --- /dev/null +++ b/.github/skills/revenue-centric-design/LICENSE @@ -0,0 +1,30 @@ +Revenue-Centric Design Skill — License & Usage Terms +Copyright (c) 2026 the curators of this repository. + +The underlying ideas, frameworks, examples, and the coined term "Revenue-Centric +Design" are the intellectual property of Richard (@richardrx on X) and are used +here WITH PERMISSION. This repository is a distilled, translated index of his +public posts, shared for educational and reference use. + +You are granted permission to use, copy, and share this material, subject to ALL +of the following conditions: + +1. ATTRIBUTION. You must retain clear attribution to Richard (@richardrx) and a + link to the source. Do not misrepresent the origin of these ideas. + +2. NO GAMBLING / BETTING / CASINO USE. You may NOT use this material — in whole + or in part — to design, build, optimize, market, or grow betting, casino, + gambling, or other real-money games-of-chance products or projects (including + loot-box and real-money-gaming mechanics). This restriction was set by the + original author as an explicit condition of reuse and MUST be preserved in any + copy or derivative. + +3. PRESERVE THESE TERMS. Any copy or derivative work must include this license in + full, including the restrictions above. + +4. NO WARRANTY. This material is provided "AS IS", without warranty of any kind, + express or implied. The curators and the original author are not liable for any + claim, damages, or other liability arising from its use. + +Note: Because of the field-of-use restriction in clause 2, this is a +SOURCE-AVAILABLE license, not an OSI-approved open-source license. diff --git a/.github/skills/revenue-centric-design/SKILL.md b/.github/skills/revenue-centric-design/SKILL.md new file mode 100644 index 0000000..41f64d7 --- /dev/null +++ b/.github/skills/revenue-centric-design/SKILL.md @@ -0,0 +1,167 @@ +--- +name: revenue-centric-design +description: >- + Revenue-Centric Design (RCD) — evidence-backed principles for making a SaaS or + startup product convert, retain, and monetize. Use when the user works on a + landing page or CRO ("my page isn't converting"), onboarding/activation ("users + sign up but don't stick"), churn/retention ("customers keep canceling"), + pricing/monetization ("how should I price this"), positioning/ICP/go-to-market, + feature scope, A/B-test rigor, or AI-era differentiation — or asks for the + behavioral-science mechanism behind a design choice. Also use when another + skill needs the principle or evidence behind a conversion/retention/pricing + move. Never apply to gambling, betting, or casino products. +metadata: + authors: + - name: Richard (@richardrx) + role: original content (101 principles) + url: https://x.com/richardrx + - name: Helio Costa (@heliocosta-dev) + role: original skill (extraction, translation, structure) + url: https://github.com/heliocosta-dev/revenue-centric-design + - name: ft.ia.br (@fabricioctelles) + role: evolution (audit template, scripts, hooks, project log, gotchas) + url: https://ft.ia.br + version: "1.0.0" + date: 2026-07-02 + repository: https://github.com/fabricioctelles/skills + license: Source-available (see LICENSE) + category: runbooks +--- + +# Revenue-Centric Design + +101 principles distilled, with the author's permission, from product designer +**Richard ([@richardrx](https://x.com/richardrx)**, ex-Volkswagen, PayPal, IBM; translated from +Portuguese; every principle links to its source post). The philosophy, **Revenue-Centric Design +(RCD)**: a design decision must serve the user _and_ the business — value and revenue, never one +or the other. + +## Usage boundary (required) + +> 🚫 **Do not apply this skill to betting, casino, gambling, or other real-money games-of-chance +> products** (including loot-box / real-money-gaming mechanics). + +The author granted reuse **on the explicit condition that it never be used for gambling, betting, +or casino work.** If asked, decline and explain that the source author's permission excludes that +use. Hard constraint, not a stylistic choice. + +Enforced, not just stated: while this skill is active, the boundary check +(`scripts/check_usage_boundary.py`) must run on every prompt and on every file write/edit, +blocking with exit 2 when gambling context is detected. On a false positive (e.g., "bet" as +an unrelated codename), only the **user** may waive the guard by creating `.rcd-boundary-ok` +in the project root — never create it on their behalf. + +### Hooks (for agents that support automated execution) + +Agents with hook support should configure: + +| Event | Matcher | Command | +|-------|---------|---------| +| Before processing user prompt | `*` (all) | `python3 <skill_dir>/scripts/check_usage_boundary.py` | +| Before writing/editing a file | `Write\|Edit` | `python3 <skill_dir>/scripts/check_usage_boundary.py` | + +- `<skill_dir>` = root directory of this skill. +- Exit code `2` = violation detected → block the operation. +- Exit code `0` = cleared to proceed. + +For agents without hook support, the operator must run the check manually before applying +RCD principles in unknown context. + +## How to use + +1. If `rcd-log.md` exists in the project root, read it first — it records which principles were + already applied to this product and what happened. Never re-prescribe a move the log shows + failed, and don't repeat one still pending results. +2. Route with the table below and open only the matching reference file(s). Every principle has a + fixed shape — **principle → apply when → the move → evidence → source** — so scan the headings, + then read only the entries that match the user's situation. +3. When the advice involves numbers — A/B sample size, churn→LTV, CAC per closed deal — run + `scripts/revenue_math.py` (see `--help`) instead of estimating. +4. A recommendation is **done** only when it (a) names the mechanism (decoy effect, Zeigarnik, + GBB, loss aversion, Schwartz awareness level…), (b) cites the specific principle, and + (c) carries that principle's evidence or source link. Missing any of the three → not done. +5. For audit runs (page, pricing, onboarding, cancellation), deliver in the shape of + [references/audit-template.md](references/audit-template.md). +6. Close the loop: append what you prescribed to `rcd-log.md` (format below), creating the file + on first use. + +## The spine: RCD in 9 principles + +1. **Neutrality is omission** — an interface that doesn't direct hurts conversion. +2. **Who talks to everyone convinces no one** — no ICP → generic value → worse retention. +3. **Value first, ask later** — proof must arrive before the user questions their choice. +4. **Your promise is the size of your proof** — the market believes what you demonstrate, not what you claim. +5. **Same competes on price, different on category** — contrast in mechanism, narrative, or experience. +6. **Default is the decision you made for the user** — the initial state defines mass behavior. +7. **Retention is built, not requested** — perceived loss retains more than promised benefit. +8. **Expansion is born of usage** — upgrade at the moment of the limit, never by interruption. +9. **Price is a filter** — pricing defines who enters, who stays, and who expands. + +## Reference library + +| When the question is about… | Open | +| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| Landing pages, hero/copy, CTAs, social proof, awareness levels, CRO | [conversion-and-landing-pages](references/conversion-and-landing-pages.md) | +| First-run, empty states, aha moment, TTV, activation, trial-as-onboarding | [onboarding-and-activation](references/onboarding-and-activation.md) | +| Cancellation, retention, expectation debt, NRR, jobs-to-be-done, support load | [churn-and-retention](references/churn-and-retention.md) | +| Pricing tables, decoy/anchoring, GBB, trial-with-card, upgrade paths | [pricing-and-monetization](references/pricing-and-monetization.md) | +| Cognitive biases & persuasion tactics (cross-cutting toolkit) | [behavioral-science-toolkit](references/behavioral-science-toolkit.md) | +| Feature scope, Swiss Knife Index, feature adoption, attention hierarchy | [product-strategy-and-features](references/product-strategy-and-features.md) | +| Design philosophy, the RCD principles, design process & method | [revenue-centric-design](references/revenue-centric-design.md) | +| ICP, niche, founder-fit, distribution, PLG, Bullseye, first customers | [positioning-icp-and-gtm](references/positioning-icp-and-gtm.md) | +| Differentiating in the AI era, moats, commoditization | [ai-era-differentiation](references/ai-era-differentiation.md) | +| A/B testing rigor, vanity metrics, churn→LTV math, signal quality | [metrics-and-experimentation](references/metrics-and-experimentation.md) | + +Some principles carry a **Visual.** line — a text description of the diagram or screenshot from +the original post. The original image is always one click away via the principle's **Source** link. + +## Gotchas + +- **Scarcity must be real.** Booking's "1 room left" works because it's true. Fabricated + scarcity destroys trust when detected (and is illegal in several markets). Never invent + counters, timers, or stock levels. +- **Loss aversion vs dark pattern** — the line: the claim is true and the exit stays easy. + Framing a real loss is persuasion; manufacturing fear or trapping cancellation is not. +- **"Kill outbound links" is a conversion-page rule.** Blog posts, docs, and SEO pages need + outbound links; don't export LP rules to content. +- **4.2–4.5 stars means _let real criticism show_** — never fabricate negative (or positive) + reviews to hit the number. +- **Don't answer this skill with 30 A/B tests.** The metrics principles themselves warn against + underpowered tests: compute the sample-size floor first (`scripts/revenue_math.py sample-size`), + test big levers, and below the floor decide by qualitative research. +- **Evidence is benchmark, not guarantee.** Figures come from the author's cases (mostly + Brazilian SaaS, values in BRL). The mechanism transfers; the exact percentage may not. + +## Related skills + +RCD supplies the **principle and its evidence**; execution skills own the workflow. The skills +below are from [Corey Haines' marketingskills](https://github.com/coreyhaines31/marketingskills) +— if they aren't installed, apply the RCD principles directly instead of deferring. + +Full page-audit workflow → `cro` · cancellation-flow build → `churn-prevention` · test design & +stats → `ab-testing` · writing the copy → `copywriting` · pricing-page build → `pricing` · +post-signup flow build → `onboarding`. When one of those runs, cite RCD principles inside it +rather than duplicating its process here. + +## Project log (`rcd-log.md`) + +Per-project memory, kept in the project root — read at the start of every engagement (step 1), +appended at the end (step 6). One entry per engagement: + + ## 2026-07-02 — pricing page redesign + - via: rcd (direct) # or the skill that led the run: cro, pricing, churn-prevention… + - principle: Decoy effect (pricing-and-monetization) + - move: added GBB middle tier at 80% of the top price + - result: pending # update when data arrives: "+12% upgrades", "no effect" + +The `via:` field doubles as trigger telemetry: if entries accumulate where RCD led a run an +execution skill should own (a full page audit, a cancellation build), that is the signal to +narrow this skill's description to the principle/evidence angle. + +## License + +Source-available, **not** open-source — see [LICENSE](LICENSE) (must accompany any copy or +derivative, in full): attribution to @richardrx required; gambling/betting/casino use +prohibited. This skill is a derivative of +[heliocosta-dev/revenue-centric-design](https://github.com/heliocosta-dev/revenue-centric-design), +restructured and extended here (gotchas, audit template, revenue-math script, project log). diff --git a/.github/skills/revenue-centric-design/references/ai-era-differentiation.md b/.github/skills/revenue-centric-design/references/ai-era-differentiation.md new file mode 100644 index 0000000..033d0a5 --- /dev/null +++ b/.github/skills/revenue-centric-design/references/ai-era-differentiation.md @@ -0,0 +1,57 @@ +# AI-Era Differentiation & Moats + +> Curated, distilled wisdom from @richardrx ("Richard — Design for startups"), translated from Portuguese. Each entry is a reusable principle linked to its source post. + +## Faster building doesn't fix churn — activation does +**Principle.** Build speed was never the bottleneck. Shipping the same confusing interface faster is just a more efficient route to churn. The real gap is the space between a user entering the product and understanding what to do. +**Apply when.** Vibe coding is sold to a founder as "the product got cheaper to build," and the team equates speed with progress. +**The move.** Obsess over activation, not velocity. Attack the three things vibe coding never touches: onboarding, attention hierarchy, and value delivery in the first sessions (TTV). More products now compete for the same user attention, so close the entry-to-understanding gap. +**Voice.** "Build speed without an obsession for activation is just a more efficient way to reach churn." +**Source.** [@richardrx · 2026-04-02](https://x.com/richardrx/status/2039685818273378644) + +## Same engine, different UX: don't compete on the commodity +**Principle.** AI turned your codebase into a near-commodity — under the hood ~90% of new tools call the same APIs. Engineering solves the base function; design and packaging are what differentiate and resist copying. +**Apply when.** Your "engine" is effectively identical to a competitor's and a generic interface is pulling you into a price war. +**The move.** Win on UX architecture, not the engine. Superior UX (1) removes initial friction → lifts conversion; (2) fits the user's workflow → cuts churn, raises LTV; (3) eases continuous/collaborative use → enables upsell. This builds a differentiator that lowers copy risk and keeps the customer paying. +**Evidence.** VW Up, Seat Mii, and Skoda Citigo share the exact same platform — chassis, drivetrain, and the identical EA211 engine — yet are designed and packaged for different ICPs (young, pragmatic-utility, reliability). +**Voice.** "The engine may be identical, but it's the architecture of the user experience that builds your moat." +**Source.** [@richardrx · 2026-03-03](https://x.com/richardrx/status/2028837448717926518) + +## A validated idea is a short-term game — plan the moat +**Principle.** If your only advantage is the codebase, you've merely built a validated MVP for better-funded competitors to execute. Structural barriers to entry (the moat) are planned, never accidental. +**Apply when.** Your tech is easy to replicate and the product identity is generic — clones can ship within days. +**The move.** Plan three deliberate moats: (1) Brand Power — a proprietary visual identity with above-average UX signals less risk and sells perceived safety (conversion); (2) Switching cost via UX — intuitive flows users have internalized make moving to a 20%-cheaper clone costly in productivity (retention); (3) Expansion architecture — internal network effects (invite-to-collaborate) are harder to copy and pull in new users (LTV). Users who perceive a value ecosystem prefer paying more over adapting to a worse, cheaper product. +**Evidence.** The "Roast My Startup" tool was cloned within a week — copies flooded the timeline — proving a codebase-only edge is no defense. +**Voice.** "Clones can't copy trust." +**Source.** [@richardrx · 2026-03-03](https://x.com/richardrx/status/2028777831233114152) + +## Design for shrinking attention spans +**Principle.** Short-form video acts on the brain like a variable-reward slot machine, switching off the attention filter and eroding self-control — leaving users with high anxiety and low focus. If your user's attention keeps shrinking, design must be militarily focused. +**Apply when.** Building any product whose users are conditioned by infinite short-video feeds (attention economy). +**The move.** Engineer for the attention limit: (1) drastically reduce cognitive load; (2) direct absolutely toward the target task (conversion); (3) build interfaces that respect the human attention ceiling. +**Evidence.** An EEG study (Fabiano et al., Frontiers in Human Neuroscience) links short-form-video addiction to reduced frontal-lobe activity and weakened ability to focus. +**Voice.** "TikTok is the hot dog of social media — hyper-palatable, but nutrient-poor: you consume endlessly and it never nourishes you." +**Source.** [@richardrx · 2026-02-26](https://x.com/richardrx/status/2026978144343687177) + +## Sell the value, not the feature list — and show the product +**Principle.** AI auto-generates landing pages, but ~90% share the same defects. A generic, inconsistent hero is what loses visitors, not the absence of fancy design. +**Apply when.** Auditing an AI-generated LP that leans on features and trendy gradients instead of the user's pain. +**The move.** Fix the three recurring failures: (1) generic, inconsistent aesthetics (every component a different color, purple/green gradients on dark/white); (2) over-indexing on features instead of the value/pain they address; (3) barely showing the actual product. Build the hero to pass the 5-second test — what does this do, why care, what next. +**Visual.** Supafast's "SaaS Hero Section Formula" — 5 elements with before/after copy: Headline (≤8 words, attack the #1 pain), Subheadline (show the transformation), Primary CTA (specific to outcome), Secondary CTA (low-commitment), Trust Bar (5 logos or one specific number) +**Source.** [@richardrx · 2026-02-21](https://x.com/richardrx/status/2025246347587162602) + +## Don't let AI-to-Figma-to-code factory technical debt +**Principle.** A Claude → Figma → Code flow looks like speed but creates two documents that drift out of sync, plus inconsistent components — a maintenance Frankenstein, not velocity. +**Apply when.** A tool promises round-tripping AI output through the design canvas into code as a shortcut. +**The move.** Refuse the false shortcut. Without a design system and context, generated components are superficially similar but fundamentally inconsistent (random button padding, off-brand colors, inconsistent UX patterns). Manual tweaks don't flow back to code, so the source of truth reverts to the canvas and the two files desync. For devs it's useless (V0/Lovable already emit code without the full-seat toll); for designers it's a distraction that skips information architecture to spit out a screen fast. +**Voice.** "A technical-debt factory." +**Source.** [@richardrx · 2026-02-18](https://x.com/richardrx/status/2024076972565963186) + +## Beat the four AI failure modes that make a vibe-coded SaaS feel like a fraud +**Principle.** When the barrier to entry tends to zero, competition tends to infinity. AI lowered that barrier and amplified Dunning-Kruger — you feel omniscient but lack the base to judge if its output is a solution or wasted time. You shipped code, not a seductive product, and you're diving into a red ocean. Code is no longer the asset — just one ingredient. +**Apply when.** A "complete SaaS in a weekend" is validated but feels hollow, generic, and clonable by Wednesday. +**The move.** Fix the four pillars where AI fails: (1) **Generic-product trap** — AI is trained on the internet's average, and average builds nothing extraordinary; escape commoditization by building the only possible tool for an ignored niche (not "CRM for doctors" but "CRM for facial-harmonization clinics" with a `last_toxin_date` field and a 110-day retouch-alert cron). (2) **Value delivery / TTV** — don't ship login → empty dashboard (the 99% default); build an on-ramp to value, an onboarding assistant, not a desert. (3) **Trust / visual confidence** — in a sea of V0/Tailwind templates, aesthetics, personality, and consistency are the last remaining trust proxies; intentional, human-aligned pixels signal authority, build trust, and lower CAC. (4) **Human touch** — cheaper code should buy more time for the memorable details (kind error messages, a 404 that returns the user, business logic that anticipates mistakes, a 200ms confirming micro-interaction) — humans are predictably irrational, full of bias. +**Evidence.** Johnson & Goldstein (2003), *Science* — a mere "opt-out" default produced +90% organ-donation consent, proving small design choices move behavior. +**Visual.** The Dunning-Kruger curve — confidence spikes at "Ignorant" (low knowledge), craters at "Cultured," and climbs toward "Expert," with a labeled "confidence gap" +**Voice.** "You can own all the cement in the world, but without the blueprint and structural engineering you're just a pile of gray concrete." +**Source.** [@richardrx · 2026-01-19](https://x.com/richardrx/status/2013264068518289753) diff --git a/.github/skills/revenue-centric-design/references/audit-template.md b/.github/skills/revenue-centric-design/references/audit-template.md new file mode 100644 index 0000000..dc3d1e1 --- /dev/null +++ b/.github/skills/revenue-centric-design/references/audit-template.md @@ -0,0 +1,34 @@ +# Audit Output Template + +Deliver every audit run (landing page, pricing page, onboarding flow, cancellation flow, +features page) in this shape. A row may only enter the table once it passes the completion +criterion — named mechanism + cited principle + attached evidence. + +## Header + +> **Target:** <URL or flow name> · **Date:** <date> +> **ICP:** <buying criteria — trigger, pain, prior attempt, proof needed; not demographics> +> **Awareness level:** <Schwartz stage of the arriving traffic> +> **Verdict:** <one sentence — the single biggest revenue leak found> + +If ICP or awareness level can't be stated, that **is** finding #1 — the debug order starts +there (ICP → awareness → proof → visual), never at the visual layer. + +## Findings + +Ordered by expected revenue impact, not by position on the page. + +| # | Finding | Mechanism | Principle (reference file) | The move | Evidence | +|---|---------|-----------|----------------------------|----------|----------| +| 1 | Hero opens with the product category, not the visitor's pain | 5-second test | "Pass the 5-second test — lead with the problem" (conversion-and-landing-pages) | Rewrite the first line to the pain: "Your team loses 6 hours a week hunting for information" | Descriptive vs transformation-led LPs: 0.5% vs 3% across 30 Brazilian SaaS ([source](https://x.com/richardrx/status/2045154631974539650)) | + +When a finding involves numbers (A/B sample size, churn→LTV, CAC per closed deal), paste the +actual `scripts/revenue_math.py` output into the Evidence cell — never an estimate. + +## Close + +- **Do first:** the top 1–3 moves, each with one line on why it outranks the rest. +- **Don't:** any tempting change the SKILL.md gotchas rule out (fabricated scarcity, LP rules + applied to content pages, review manipulation, underpowered tests…). +- Append the prescribed moves to the project's `rcd-log.md` (format in SKILL.md) so the next + engagement starts from what was already tried. diff --git a/.github/skills/revenue-centric-design/references/behavioral-science-toolkit.md b/.github/skills/revenue-centric-design/references/behavioral-science-toolkit.md new file mode 100644 index 0000000..f651d7e --- /dev/null +++ b/.github/skills/revenue-centric-design/references/behavioral-science-toolkit.md @@ -0,0 +1,60 @@ +# Behavioral Science & Persuasion + +> Curated, distilled wisdom from @richardrx ("Richard — Design for startups"), translated from Portuguese. Each entry is a reusable principle linked to its source post. + +## You can't un-hear your own product +**Principle.** Once you know how your product works, that knowledge permanently rewrites your perception — what feels obvious to you is just "tap-tap-tap" to a first-time user. You'll mistake confused users for dumb users. +**Apply when.** You think onboarding is unnecessary because "the product is simple," or a question your support answers weekly seems already-answered on screen. +**The move.** This is the curse of knowledge — you can't switch the music off, so collect feedback from people who've never seen the product, without steering or naming things, and watch behavior. Run it continuously: use support/CX as an insight collector (tabulate each issue by %, impact, insight), Clarity/PostHog for heatmaps and session replays, sampled user interviews, and competitor benchmarking. +**Evidence.** Tapping-vs-listening study: tappers hear the full song in their head; listeners only get the taps. Listeners guessed 3 of 120 songs correctly (2.5%) — far below tappers' expectations. +**Voice.** "You're humming the whole song in your head; your user only hears 'tap, tap, tap.'" +**Source.** [@richardrx · 2026-06-04](https://x.com/richardrx/status/2062509937037590997) + +## Set the default — it's the most underrated lever in conversion +**Principle.** The pre-selected option captures the overwhelming majority of choices, because deciding is expensive and the lazy brain takes the easiest path. Smart defaults beat copy persuasion. +**Apply when.** Any choice the user must make — pricing tier, billing cadence, seat count, notifications, checkout — especially before you spend hours rewriting CTAs. +**The move.** Exploit status-quo bias plus cognitive-load reduction. Pre-select the mid-tier you want to sell (the default takes 60–80% of choices); default billing to annual to lift contracted MRR without changing price; start the seat selector at your ICP's typical count (anchoring); run a reverse trial where premium is the default and free is the opt-out, so the user must actively give up what they already have. Three rules: defaults must be ethically defensible (checkbox tricks become churn and complaints), smart defaults beat copy, and a default acknowledges the user won't burn energy deciding what's trivial to you. +**Evidence.** Organ-donor study (Science, 2003): opt-out countries register ~6× more donors than opt-in. Germany (opt-in) ~12% vs Austria (opt-out) ~100% — culture/religion don't explain it; it's a pre-checked box. Richard raised average ticket 60% and saw up to 4× LTV applying this to plan acquisition. +**Visual.** Bar chart of effective organ-donor consent by country: opt-in nations low (Denmark 4.25%, Germany 12%, UK 17.17%, Netherlands 27.5%) vs opt-out nations ~100% (Austria, France, Hungary, Portugal). +**Voice.** "You can spend the rest of your life optimizing copy, or you can change 5 defaults over the weekend." +**Source.** [@richardrx · 2026-05-22](https://x.com/richardrx/status/2057872036718899256) + +## Reinforce the decision the user just made +**Principle.** After committing to a choice, people actively seek information that supports it — choice-supportive bias. You can feed that need to make the decision feel right. +**Apply when.** Right after signup, purchase, or any meaningful commitment, when buyer's remorse or doubt could creep in. +**The move.** Use choice-supportive bias deliberately: send a strong welcome email with clear next steps so the new user feels embraced and validated in having chosen you. (Analogy: someone joins an EV-lovers group right after buying the car.) +**Source.** [@richardrx · 2026-04-29](https://x.com/richardrx/status/2049392897598849333) + +## Architect for what users fear losing, not just what they gain +**Principle.** Builders obsess over features (gains), but conversion and retention are cemented by what the user fears losing — the pain of abandoning a built-up ecosystem outweighs the pain of paying a subscription. +**Apply when.** Designing trials, retention/renewal flows, and offboarding for any product where users accumulate data, history, or workflows. +**The move.** Exploit loss aversion and sunk cost. Convert with zero risk — let users import real competitor data in shadow mode so they can test without fear. Retain by designing the product to make users build workflows and accumulate history from day 1, so at renewal they weigh the headache of rebuilding from scratch, not the monthly fee. At offboarding, don't add friction — make cancellation easy but the loss tangible (e.g., "You'll instantly lose 41 active automations and 6 months of data"). +**Evidence.** Sunk cost is what keeps many users on certain LLMs — fear of losing your memory/history, even when it could be exported with a copy/paste. +**Visual.** Hotel-listing UI using scarcity ("Only 2 rooms left") — illustrative example of a loss-framed cue. +**Source.** [@richardrx · 2026-03-02](https://x.com/richardrx/status/2028471666297217460) + +## Use precise numbers, not round ones, to signal truth +**Principle.** Exact figures read as more credible than rounded ones; round numbers signal marketing while specific numbers signal reality. +**Apply when.** Writing any claim, stat, or social-proof number — landing pages, ads, results, testimonials. +**The move.** Lean on the precise-number effect: say "526 houses," not "over 500." Nothing about the claim changes except the precision, yet trust rises. +**Evidence.** Schindler & Yalch (2006), 199 participants, fictional deodorant: claims of "47%" or "53%" longer-lasting were judged ~10% more accurate than the rounded "50%" claim — only the precision changed. +**Visual.** Real billboard: "LAST YEAR WE SOLD 526 HOMES. YOUR COUSIN SOLD 2. LIST RESPONSIBLY." — the precise number doing the persuasion. +**Voice.** "'526 houses' inspires confidence; 'over 500 houses' signals marketing." +**Source.** [@richardrx · 2026-02-18](https://x.com/richardrx/status/2024141244717281514) + +## Guide the eye — don't give every option equal weight +**Principle.** The brain uses contrast to make fast decisions (Von Restorff effect). When competing options carry identical visual weight, you create mental friction, decision time rises, and conversion falls. Guiding the user isn't manipulation — it's respect for their time. +**Apply when.** You have 3 plans, two equally-weighted buttons, or any "democratic" interface where everything looks the same (a common founder error, and a default of AI-generated UIs). +**The move.** Exploit the Von Restorff effect: make the value-generating option visually dominant and de-emphasize the rest (e.g., a ghost-styled "Cancel" beside a bold, colored primary). If you know your ICP's pains and desires, you have a duty to highlight the highest-value solution. Slow decisions accumulate into a "hard-to-use" perception that becomes churn. +**Visual.** Good/bad confirm dialog: bad = both buttons same green weight; good = a ghost-text "Cancel" beside a solid red "Delete now," so the primary action stands out. +**Voice.** "If everything grabs attention, NOTHING grabs attention." +**Source.** [@richardrx · 2026-01-22](https://x.com/richardrx/status/2014317885494059106) + +## Cut cognitive load — every choice you remove can lift conversion +**Principle.** Each extra field, choice, or block of complex text spends the user's mental energy and triggers analysis paralysis. Your product can be complex; your interface doesn't have to be. +**Apply when.** Checkout, signup, and subscription screens — anywhere the user must decide or input under doubt. +**The move.** Strip the interface to the essential decision; when options can't be cut, break the flow into smaller steps (e.g., a 4-step checkout). Remember the failure is invisible: users don't complain or open tickets — they close the tab as "silent churn" and your CAC is wasted. Then dogfood your own onboarding as if you were a stranger. +**Evidence.** Removing 1 checkout field raised conversion 10%. Richard has seen reworked subscription screens lift LTV 200% just by simplifying the decision. +**Visual.** Mobile checkout labeled "Analysis paralysis": payment options split into smaller stages — advice to break choices into ~4 steps. +**Voice.** "They don't file a support ticket. They just close the tab — and your CAC goes in the trash." +**Source.** [@richardrx · 2026-01-20](https://x.com/richardrx/status/2013597792447394034) diff --git a/.github/skills/revenue-centric-design/references/churn-and-retention.md b/.github/skills/revenue-centric-design/references/churn-and-retention.md new file mode 100644 index 0000000..d3a83ed --- /dev/null +++ b/.github/skills/revenue-centric-design/references/churn-and-retention.md @@ -0,0 +1,66 @@ +# Churn & Retention + +> Curated, distilled wisdom from @richardrx ("Richard — Design for startups"), translated from Portuguese. Each entry is a reusable principle linked to its source post. + +## Churn and payback are one problem, measured in two places +**Principle.** Churn and payback look like two problems (often with different owners) but are the same one — both decided in the user's first session, in the gap between entering and feeling the product works. +**Apply when.** You're fixing churn at the cancel screen and chasing cheaper CAC in the ad manager at the same time. +**The move.** Both are the wrong place — the decision was made earlier. Activation fixes both: shrink TTV and you retain more AND get each customer across the payback line before they vanish. A customer who dies in month 2 with a 4-month payback never closes the account. Caveat: not all churn is activation (price, a bad channel exist) — but before chasing cheaper CAC, count how many customers die before repaying what they cost. +**Voice.** "A customer who dies before repaying his CAC is a bill you paid and never collected." +**Source.** [@richardrx · 2026-06-30](https://x.com/richardrx/status/2071931705573748896) + +## Switching cost is what turns months of LTV into years +**Principle.** The same product, designed differently, yields months vs. years of LTV. Switching cost = the effort a user *perceives* in leaving; low switching cost means thin history and an easy exit. +**Apply when.** Designing for retention/lock-in, or explaining why a useful product still churns. +**The move.** Engineer switching cost deliberately — a "compound interest" that grows the product's value over time. Five levers: **muscle memory** (Superhuman/Photoshop shortcuts), **mental model** (Mac↔Windows, Gmail labels), **accumulated personalization** (Spotify playlists, home-screen layout), the **vault effect** (iCloud/Drive/years of WhatsApp), and **autopilot/habit** (variable-reward Skinner-box loops). Sunk cost holds them the way it holds an investor in a falling stock; habit can take months to install but is the difference between LTV of months and years. +**Source.** [@richardrx · 2026-06-23](https://x.com/richardrx/status/2069382946214080985) + +## Tell the existing base about upgrades before they want to leave +**Principle.** Reactive improvement communication is a sneaky churn vector: if you only market new versions to cold traffic to avoid cannibalizing the old product, your base assumes the old version is the ceiling and leaves when a competitor looks better. +**Apply when.** You shipped a better version/feature but only announced it externally; support pitches the migration only at the cancel moment. +**The move.** Proactively offer upgrades and migrations to active users — not release emails nobody reads or an Instagram post. Ask: "When we shipped the last relevant feature or version, how many active customers were told?" Offering migration at cancellation converts a clean expansion into emergency retention. Track Net Revenue Retention (NRR); in B2B SaaS, NRR above 110% separates sustainable growth from a leaky funnel. +**Voice.** "The migration only showed up as a reaction to my complaint, after I started making noise." +**Source.** [@richardrx · 2026-05-19](https://x.com/richardrx/status/2056715097796411514) + +## Design the cancel screen — it's your last conversation, not a form +**Principle.** The cancellation screen is the most ignored yet one of the most important pages in the product; treating it as a bureaucratic form wastes your final chance to retain. +**Apply when.** Cancel flow is just "Are you sure?" with two buttons, or Stripe's default template, while signup was crafted with care. +**The move.** Three plays: (1) Show concrete loss — "You'll lose access to relationship data on your 476 configured clients and 8 months of history"; concrete loss outweighs abstract benefit (loss aversion). (2) Offer an alternative before goodbye — "Pause 30 days instead?" or one more free month (ChatGPT nails this). (3) Collect the reason usefully via an open question — "What was missing for you to stay?" — not a dropdown that rarely lists the real reason. +**Voice.** "If you invested to bring your user here, invest the last 30 seconds trying to keep them." +**Source.** [@richardrx · 2026-05-13](https://x.com/richardrx/status/2054562119962501186) + +## Churn starts on the landing page, not at cancel +**Principle.** If the LP promises one thing and the product delivers another, you create an expectation debt that charges interest every day the user thinks "this isn't what I expected" — and the disappointment is pre-programmed even if the product is excellent. +**Apply when.** 30-day churn is high but NPS is fine — the problem is likely what you promised before they entered, not the product. +**The move.** Audit three common mismatches: (1) result promise vs. tool delivery ("Increase sales 30%" → a metrics dashboard); (2) simplicity promise vs. complex product ("Set up in 5 minutes" → 47 fields, 3 integrations, 20-min tutorial); (3) promise aimed at the wrong ICP (LP speaks to a 2-person startup; product was built for a 15-person team at scale). Recalibrate the promise to match real delivery and ICP. +**Voice.** "The user bought a result and got a colorful spreadsheet." +**Source.** [@richardrx · 2026-04-29](https://x.com/richardrx/status/2049568514311172355) + +## Anchor one-time-job products to a recurring life event +**Principle.** A product hired to solve a one-off problem generates structural churn: a user who loves it and still cancels isn't unhappy — they finished the job they came to do, and there was no continuous value to bring them back. +**Apply when.** You have 4.2 stars and positive NPS yet rising churn; the product solves a point problem (resume builder, contract/legal-doc generator, data migration tool, pitch-deck builder, due-diligence platform). +**The move.** Use jobs-to-be-done thinking. Instead of faking engagement, anchor the product to an event that already recurs in the user's life and returns yearly without a push. Ask: "If the job my product does is finished, what life event justifies the user coming back?" If there's no answer, it's a business-model problem, not a product problem. +**Evidence.** TurboTax (US income-tax software) tied itself to tax season, turning the product into a ritual because the event makes the use inevitable. +**Source.** [@richardrx · 2026-04-24](https://x.com/richardrx/status/2047620778338795918) + +## Treat support volume as a design problem, not a staffing one +**Principle.** What looks like a support problem is usually a design problem — you cut ticket volume during onboarding itself, with an interface that answers questions before they're asked. Low-ticket digital products generate up to 3x more support than conventional tickets. +**Apply when.** Support is your biggest bottleneck, especially with low-ticket/impulse-buy products; you're tempted to just automate tickets. +**The move.** Three drivers of low-ticket support load: different buyer profile (less patience, less digital familiarity, more expectation of human help); impulse purchase (low friction → buys without understanding → seeks support); inverted opportunity cost (asking is easier than searching). For SaaS, redesign the journey: contextual in-product FAQ, self-answering UI. Automating support treats the symptom; redesigning the journey fixes the cause and can cut churn too. +**Evidence.** An old McAfee case cut support volume by 90% by implementing an FAQ — plain text, no chatbot. +**Source.** [@richardrx · 2026-04-23](https://x.com/richardrx/status/2047289409238712726) + +## Strip the jargon before you blame onboarding +**Principle.** Churn that looks like a product problem is often a language problem: a technical founder writes product and sales copy in jargon, the ICP buys on a leap of faith, never perceives value, accumulates small disappointments, and cancels — looking like a missing feature on the dashboard. +**Apply when.** Churn is high and you've already revised onboarding and product — revise language and structure next. +**The move.** Watch two biases: the curse of knowledge (you know too much and forget the other person doesn't) and the easy-speech bias (simple language reads as more trustworthy and raises awareness). Rewrite dense, jargon-heavy copy into plain language even for complex topics. +**Visual.** Side-by-side: a dense, legalese contract clause (red X) vs. a plain-language rewrite "In this contract you authorize the bank…" (green check), with an "Easy-speech bias" callout. +**Voice.** "On the dashboard it looks like a missing feature; it was a mismatch between your discourse and their understanding." +**Source.** [@richardrx · 2026-04-08](https://x.com/richardrx/status/2041829519611371727) + +## Engineer addiction like a game so CS isn't a churn tax +**Principle.** Customer Success is the tax you pay for a non-addictive product — if you need an army of CSMs to stop cancellations, the product failed. Your real competitor isn't another startup; it's boredom, and boredom has infinite CAC. The CNPJ buying your SaaS is the same brain that plays Candy Crush; reward neuroscience is identical. Win retention across three game-design phases. +**Apply when.** Diagnose by behavior: dropout at minutes 2–8 of onboarding = Phase 1; ~1.3 logins/week when it should be 4x = Phase 2; one departing employee kills the whole account = Phase 3. LTV:CAC below 3:1 means you're funding a product that can't stand organically. +**The move.** **Phase 1 — Time-to-value vs. cognitive load:** ditch the setup wizard (asking work before delivering value reads as hostile territory in 10 seconds); use progressive disclosure, let users create and see results before asking for email/card. Mechanism: Zeigarnik effect (incomplete-loop tension) + endowment effect (people value 3x more what they helped build). Empty states must sell the dream — never show "0 data" or blank templates; populate a demo simulating day-30 usage. **Phase 2 — Habit loop / retention as biology:** passive software that only reacts is a failure to build dependency; ship proactive variable rewards. The mesolimbic reward system releases dopamine on anticipation, not the reward itself; predictable rewards (monthly report) build tolerance, variable ones ("we detected a positive anomaly yesterday") keep the loop alive. Convert vanity metrics into loss-aversion triggers: "Your team broke a record and you haven't seen it" + temporal data scarcity ("sync in 24h or lose the weekly benchmark"). **Phase 3 — Defensive moat:** single-player products die when the champion leaves — you built dependence on a person, not the org. Build multiplayer mode + data debt via social switching cost + network effects. Make User A's work block/depend on User B; reports needing multi-stakeholder approval; dashboards aggregating 3 departments. When quitting requires an alignment meeting across Sales, Ops and Finance, you reach negative churn by bureaucratic inertia; accumulated datasets add organizational endowment effect. +**Voice.** "Stop blaming the customer. Your product is boring. And in the attention game, boring is bankruptcy." +**Source.** [@richardrx · 2026-01-30](https://x.com/richardrx/status/2017274698699067466) diff --git a/.github/skills/revenue-centric-design/references/conversion-and-landing-pages.md b/.github/skills/revenue-centric-design/references/conversion-and-landing-pages.md new file mode 100644 index 0000000..94d1d49 --- /dev/null +++ b/.github/skills/revenue-centric-design/references/conversion-and-landing-pages.md @@ -0,0 +1,132 @@ +# Conversion & Landing Pages + +> Curated, distilled wisdom from @richardrx ("Richard — Design for startups"), translated from Portuguese. Each entry is a reusable principle linked to its source post. + +## "Paint the button" is judging the last line of a long cascade +**Principle.** The visual layer of a landing page is the tip of a cascade that begins at the ICP — so "make it more colorful" critiques the *end* of a process as if it were the start. +**Apply when.** Gathering or acting on LP feedback, or reaching for button color first. +**The move.** Work the cascade in order: ICP → the buyer's **awareness level** (Eugene Schwartz's 5 stages) → what you say and how → visual positioning (tone → form, color, type, space). Low awareness: open on the problem, name the pain in the first fold, then the mechanism and your fix. Higher awareness: go straight to your advantages vs. alternatives. Jump to button color and you've silently (and probably wrongly) answered who you sell to, their awareness, which pain, and what tone. +**Voice.** "They're looking at the end of a process and thinking it's the beginning." +**Source.** [@richardrx · 2026-06-23](https://x.com/richardrx/status/2069469303464730988) + +## Scaling cold traffic is an honesty test for your page +**Principle.** Conversion measured on warm audiences (existing followers) is inflated — they forgive the page's flaws. Only cold traffic reveals how much of your rate is the page versus borrowed trust. +**Apply when.** Ad spend goes up but revenue barely moves, and the conversion rate "drops" even though you fixed nothing. +**The move.** Treat any baseline built on warm public as fiction, not a performance number. When you scale budget into cold traffic and the rate collapses, that gap is the page's real ceiling — fix the obvious page defects first, then judge media. +**Evidence.** A founder tripled ad spend; revenue rose only 11%. Warm-traffic conversion of ~3% (driven by Instagram followers) cratered to 0.5% on cold traffic; fixing the obvious lifted it to 2% — 4x the real number. +**Voice.** "Scaling budget into cold traffic works like an honesty test: it shows how much of your conversion is your page and how much is borrowed trust." +**Source.** [@richardrx · 2026-06-08](https://x.com/richardrx/status/2063972594223661127) + +## Write the CTA microcopy, not the button color +**Principle.** The CTA label moves conversion far more than button color or shape — it's the last thing the user reads before deciding, so it must reduce the mental effort of simulating what happens next. +**Apply when.** A technical founder is A/B testing button colors for weeks over a 0.3% (non-significant) delta while the label still says "Sign up" with no click trigger. +**The move.** Make the CTA answer the three questions the brain asks before clicking: (1) what happens when I click, (2) how long it takes, (3) what it costs/commits. "Sign up" answers none; "Start free in 30s" answers two. Add a click trigger (the small line under the CTA) to answer the rest and break an objection: "Start Free Trial" + "14 days, no card". Rooted in outcome bias and Construal Level Theory (making the outcome concrete). +**Voice.** "The button is the last thing the user reads before deciding — treat it as such." +**Source.** [@richardrx · 2026-05-25](https://x.com/richardrx/status/2058875777739866490) + +## Cut CAC by filtering on the landing page, not the media +**Principle.** High CAC is rarely a media problem — it's usually a filtering problem. The landing page can qualify leads before the form, killing deals that were never going to close. +**Apply when.** Clicks become leads, leads become SDR calls, calls don't close, and founders react by swapping creative or channel (or blaming the SDR). +**The move.** Pull three filtering levers so the lead self-selects without feeling filtered: (1) copy specificity — "Cash flow for service providers billing R$500k–R$3M" beats "Organize your finances"; (2) visible pricing — screens out no-budget leads so sales only meets real objections; (3) a qualification question as the first interaction (or copy/examples that play that role). CAC is cost per closed deal, not cost per lead. +**Evidence.** If you close 1 in 50 leads, dropping to 30 leads with 1 close nearly doubles media efficiency — same spend, lower CAC. +**Voice.** "The lead with no money won't magically generate money between the landing page and the sales call." +**Source.** [@richardrx · 2026-05-18](https://x.com/richardrx/status/2056385204785213446) + +## Aim for 4.2–4.5 stars, not a perfect 5 +**Principle.** A flawless rating reads as fake; consumers distrust unanimity. A profile that includes constructive criticism feels more authentic and converts better than pure praise. +**Apply when.** Building or curating reviews/ratings and social proof on a landing page or product. +**The move.** Let imperfection show. Stack the highest-converting combination: a detailed case study with quantifiable ROI, ideally a video testimonial from a brand recognizable to your ICP, plus customer logos. Avoid the common failure — social proof done badly: identical cards, first-name-only, all 5 stars, generic avatars instead of a real photo. Make each review verifiable (e.g., link to the actual LinkedIn post). +**Evidence.** Northwestern: purchase probability peaks at 4.2–4.5 stars, not 5. Testimonials lift LP conversion up to 34% (VWO); 5+ reviews → 270% more likely to be bought (Yotpo); 93% read reviews before buying (BrightLocal). +**Visual.** A social-proof wall done right: real faces, names + roles, verifiable reviews, and recognizable client logos (Volkswagen, Coca-Cola, Samsung, Itaú, Volvo, DocuSign, Hotmart) under a "+10,000 professionals" headline +**Voice.** "Credibility drives conversion." +**Source.** [@richardrx · 2026-05-14](https://x.com/richardrx/status/2054910531132183030) + +## Compress the decision window — shorten the path to action below the path to doubt +**Principle.** The purchase decision is mostly made before checkout. Conversion is architecture: the user converts when the path to the action is shorter than the path to doubt, because every extra second of deliberation raises the odds they close the tab. +**Apply when.** You're optimizing the checkout, CTA, or headline while ignoring the deliberation time upstream. +**The move.** Compress decision time two ways: pressurize the environment with real scarcity ("Only 1 room left at this price", "Booked 3 times in the last hour") — driven by loss aversion (Kahneman: losing hurts up to ~2x more than the equivalent gain), so "1 left" registers as "I'll lose this"; or remove steps entirely (Amazon's patented 1-Click ordering). +**Evidence.** Booking runs 1,000+ simultaneous experiments (per Lukas Vermeer, 8 yrs leading experimentation); showing sold-out hotels alongside available ones *raised* bookings by sharpening perceived scarcity. +**Voice.** "The user converts when the path to the action is shorter than the path to doubt." +**Source.** [@richardrx · 2026-05-11](https://x.com/richardrx/status/2053832356293738914) + +## Don't clone the page — the converting layer is invisible +**Principle.** A converting sales page is the visible shell of an invisible system. Copying the layout copies what's cheapest to produce; the expensive part (research) stays invisible and gets left behind. +**Apply when.** A page "looks like it works" and you're tempted to clone it for your own offer. +**The move.** Before copying, ask: what on this page is a function of the *product*, and what is a function of the *customer research* done before writing each line and placing each element? The hidden layer is customer vocabulary pulled from interviews, objections ordered to the ICP's specific fear, social proof hand-picked to resonate, and a core promise tuned to the stated desire. This error has a name: cargo cult — replicating the visible ritual hoping to summon the result, without grasping the causal mechanism. +**Evidence.** Two ~95%-identical pages, same niche/offer/order: 4% vs 0.6%. The 4% page belonged to Richard's client; the 0.6% was a near-pixel clone by a builder — who unknowingly DM'd the original team to complain it wasn't converting. +**Voice.** "There's a name for copying the form expecting the function. It's called cargo cult." +**Source.** [@richardrx · 2026-05-09](https://x.com/richardrx/status/2053086365781205142) + +## Pass the 5-second test — lead with the problem, not the feature +**Principle.** A visitor scans your LP for ~5 seconds. If they can't answer what it does, who it's for, and why they should care, they leave — they won't stay to figure it out. Conversion begins on the first line. +**Apply when.** Your hero opens by describing what the product *is/does* (a feature/category) instead of the visitor's pain. +**The move.** Open with the visitor's problem, not yourself. "AI-powered project management platform" → meh; "Your team loses 6 hours a week hunting for information" → keep going. One talks about itself, the other talks about me. If the first line doesn't connect with the pain, the rest of the page goes invisible. +**Voice.** "If you who built it can't answer in 5 seconds, your visitor can't either." +**Source.** [@richardrx · 2026-05-01](https://x.com/richardrx/status/2050280273682510230) + +## Fix contrast, layout, and trust before rewriting copy +**Principle.** The brain processes three pre-verbal variables — contrast, on-screen placement, and trust — before it reads the words, and they usually move the needle more than any headline. +**Apply when.** You're rewriting home copy for weeks chasing conversion without touching the visual/attentional layer. +**The move.** (1) Contrast is relative to surroundings, not an isolated color — a big, differently-shaped button in whitespace is *seen before it's read* (accessibility ratio 4.5:1 is a floor, not the goal). (2) Placement: Fitts's law (closer, bigger targets get clicked more) plus the F-pattern (Nielsen Norman eyetracking) — a CTA bottom-right with no visual anchor sits outside the attentional map. (3) Trust is built in four reinforcing layers: a real (non-stock) human face, a known brand or specific verifiable proof, an offer that fits the stated problem, and a solution mechanism logically coherent with the promise. When all four align, the user converts without knowing why; when one fails, they invent a rational reason to leave. +**Voice.** "Optimizing copy without working contrast, placement, and trust is masking the symptom." +**Source.** [@richardrx · 2026-04-28](https://x.com/richardrx/status/2049215050997784774) + +## Specificity is the difference between decoration and persuasion +**Principle.** Generic LP copy gets discarded by the brain; specific messages are processed faster and generate more trust. A converting LP shows the *transformation* the product causes, not just what it does. +**Apply when.** Your hero reads "The complete platform for [generic category]", a 5-feature subtitle, "Start free", and no real social proof — the default Brazilian SaaS template. +**The move.** Fix the four standard failures: (1) "Complete platform" means nothing → "Cut new-dev onboarding time 40%"; (2) features on top → lead with the pain, feature as the solution (nobody wakes up wanting a "custom report feature"); (3) "Start free" is the weakest CTA (no value, no risk reduction, no urgency) → "See your first report in 2 minutes"; (4) "Used by Company X" proves nothing → "We cut Company X's churn from 12% to 6%". One page describes, the other sells. +**Evidence.** Across 30 Brazilian SaaS LPs: descriptive vs transformation-led ran 0.5% vs 3% conversion — same traffic, 6x more leads — driven by argument sequence and promise specificity, not visual design. +**Source.** [@richardrx · 2026-04-17](https://x.com/richardrx/status/2045154631974539650) + +## Users scan, they don't read — give the primary action obvious contrast +**Principle.** Users scan interfaces, weighing cognitive effort against payoff, and ignore most of it. They enter with one question — "What can I do here?" — so the primary action must win on contrast within seconds. +**Apply when.** The main CTA competes with 6 other elements, or the most important action is buried inside a dropdown that requires a click to reveal. +**The move.** Make the primary action visually dominant so it's found in a glance. If the key button competes with too many elements it loses prominence; if it's hidden behind interaction it's effectively invisible. Leverage image superiority to guide attention down the page. +**Visual.** A login screen annotated with two biases — "limited choice bias" (a single dominant Google sign-in button) and "image superiority" (a vivid hero illustration pulling the eye) +**Source.** [@richardrx · 2026-04-08](https://x.com/richardrx/status/2042013751742705816) + +## Judge a landing page by conversion, not by beauty +**Principle.** A landing page isn't a beauty contest — it has one measurable job: qualify the user and lower CAC by turning traffic into revenue. Evaluating a static image with no context is nearly useless. +**Apply when.** The timeline turns into "AI vs human" / "who designed it better" debates that ignore conversion, while obvious conversion flaws go unaddressed. +**The move.** Treat conversion as a continuous loop, never an isolated event: form a thesis → ship the interface and collect data → find where the user hesitated, form a new hypothesis, and optimize the next cycle. The debate defaults to aesthetics because it's easier to opine on looks than to measure results — which is exactly what keeps most LPs underperforming. +**Visual.** A side-by-side "Google Stitch vs Human" LP comparison being judged on looks alone — context for the argument, not a model to copy +**Voice.** "Product design doesn't compete with art. It sells." +**Source.** [@richardrx · 2026-03-19](https://x.com/richardrx/status/2034638793219694734) + +## Converting pages are often ugly — design for performance, not applause +**Principle.** There's an invisible war between branding (looking good in the screenshot) and performance (conversion, retention, expansion). The page that puts money in the till is often visually aggressive, text-heavy, and far from the Apple aesthetic founders dream of. +**Apply when.** You're choosing between a page that wins design compliments and one built to convert. +**The move.** Engineer the converting pattern even if the artist in you cringes: (1) one obvious attention point — eyes go where they must; (2) a high-contrast CTA with redundancy; (3) a clear promise delivering easily-perceived value; (4) guarantee, social proof, and a free element to reduce risk aversion; (5) organic images by the second fold to back the offer and build trust. +**Evidence.** The analyzed page (quoted): 100 visits → 19 signups, a 19% conversion rate. +**Visual.** An "ugly-but-converts" waterproofing LP: urgency bar, high-contrast orange CTAs, a short above-the-fold form, stat row (12,500+ / 4.9★ / lifetime warranty), and trust badges +**Voice.** "Some LPs go after compliments, others go after conversion." +**Source.** [@richardrx · 2026-02-17](https://x.com/richardrx/status/2023777916673220817) + +## Design the features page for skimming, not reading +**Principle.** Almost nobody reads your features page — they skim three bullets and hunt for a demo video. Design for that behavior (with the caveat that the more conscious, technical slice of your ICP *will* read the detail). +**Apply when.** You're packing a features page with dense prose expecting visitors to read it top to bottom. +**The move.** Front-load three crisp bullets and make a demo video easy to find; let the deep copy serve the minority of technical, high-awareness buyers who actually read it. +**Source.** [@richardrx · 2026-02-09](https://x.com/richardrx/status/2020815036168437814) + +## No clicks out, plus brutal CTA contrast +**Principle.** For conversion you want little distraction and lots of redundancy. Any clickable element that leads off-page is lost conversion — no matter how prestigious the source. +**Apply when.** You're tempted to link out to a glowing NY Times piece or a top influencer's video, or your CTA blends into the interface. +**The move.** Kill outbound clicks entirely. Make the CTA generate strong contrast against the rest of the interface in position, size, and color — if it isn't easy to notice and click, it won't be clicked. +**Visual.** A wireframe showing the level of contrast a CTA button needs against the surrounding interface +**Source.** [@richardrx · 2026-02-05](https://x.com/richardrx/status/2019514730696565238) + +## Debug ICP and awareness level before touching design +**Principle.** Conversion is context engineering — who arrives, with what pain, at what stage, needing what proof. Two invisible variables must be debugged before writing a line of code: the Who (ICP) and the When (awareness level). Visual design comes last. +**Apply when.** Your LP underperforms and you reach for button-color tweaks; or you define ICP by demographics. +**The move.** (1) Reject the demographic fallacy: geography/age/income is a bad proxy — King Charles and Ozzy Osbourne are demographic twins with opposite needs. Real ICP is *buying criteria* (trigger, pain, prior attempt, proof needed); persona is just biography, the spec for your user. (2) Map the visitor's Eugene Schwartz awareness level. Rule: headline speaks to the current stage; proof pushes them one stage forward. Selling "Solution" to the "Unaware" is proposing on a first date. Debug order: ICP wrong → awareness wrong → proof insufficient for the stage → only then touch visual design. + +Awareness → angle / hero / proof: +- **Unaware** — symptom & identity / "Still doing X this way?" / simple diagnostic, checklist, benchmark +- **Problem-aware** — cost & urgency / "If you have X, you're losing Y" / numbers, before/after, calculation +- **Solution-aware** — trade-offs & selection / "3 ways to solve X — why the 3rd scales" / honest comparison, matrix +- **Product-aware** — differentiation & proof stack / "Why us, why now" / cases, demo, objections +- **Most aware** — final risk & friction / "Swap X for Y in Z days, no risk" / guarantees, onboarding, effort reduction + +**Visual.** Schwartz's 5 awareness stages as a rising ramp (Unaware → Problem → Solution → Product → Most Aware); plus the demographic-twins diagram (Charles vs Ozzy, identical on paper) +**Voice.** "There's no 'conversion rate' in a vacuum. There's contextualized conversion." +**Source.** [@richardrx · 2026-01-14](https://x.com/richardrx/status/2011427153133351046) diff --git a/.github/skills/revenue-centric-design/references/metrics-and-experimentation.md b/.github/skills/revenue-centric-design/references/metrics-and-experimentation.md new file mode 100644 index 0000000..0b7aa82 --- /dev/null +++ b/.github/skills/revenue-centric-design/references/metrics-and-experimentation.md @@ -0,0 +1,31 @@ +# Metrics, Experimentation & Business Math + +> Curated, distilled wisdom from @richardrx ("Richard — Design for startups"), translated from Portuguese. Each entry is a reusable principle linked to its source post. + +## Don't mistake signups for traction +**Principle.** Signups are the cheapest action a user takes, so they measure curiosity, not value — especially in freemium. Real growth is whether people come back and do the action that delivers value. +**Apply when.** A rising signup curve on the dashboard feels like proof of traction, particularly under a freemium model. +**The move.** Treat signups as top-of-funnel only — never stop reading there. Track who returns on day 2 and day 7 (D1/D7 retention) and how many complete the value-delivering action (activation). Paid media can inflate signups while real retention stays flat. +**Voice.** "The signup curve climbs with paid traffic, but usage and activation only climb with a good product." +**Source.** [@richardrx · 2026-06-11](https://x.com/richardrx/status/2065082771987394651) + +## Don't bet your product on an underpowered A/B test +**Principle.** Most A/B tests in small SaaS lack the volume to prove anything, yet founders swap the whole product on the result. Testing without enough sample to conclude is the trap. +**Apply when.** You're in traction or survival stage, ran a test for a week, saw "variant B won by 12%," and want to ship it everywhere. +**The move.** Before running, compute the minimum sample size (free calculators exist); if you can't hit that floor in reasonable time, don't start. Test big things (headline, offer, pricing structure, onboarding) since large effects need less sample. Never stop a test because the number looked pretty mid-way. With no volume, decide by qualitative research — five good interviews beat an underpowered A/B test. Beware the law of small numbers and confirmation bias. +**Evidence.** ProfitWell is categorical: don't A/B test price — you'll never have the volume or context for it to mean anything. +**Source.** [@richardrx · 2026-06-01](https://x.com/richardrx/status/2061463480868229189) + +## Celebrate signal quality, not list size +**Principle.** A waitlist exists to validate that a pain is one people pay to solve — not to sell. Absolute size is a vanity metric; conversion-weighted quality is the real signal. +**Apply when.** You launch a waitlist and feel tempted to celebrate raw headcount. +**The move.** Convert size to expected customers before reacting: a good waitlist converts 15–20% to paying, above 30% is excellent. 53 people at 20% = 10 customers; 1,000 people at 1% = 10 customers — same result, different perception. Until there's a transaction, there's no validated hypothesis. +**Voice.** "Founders celebrate the size of the list when they should celebrate the quality of the signal." +**Source.** [@richardrx · 2026-04-17](https://x.com/richardrx/status/2045094511106220220) + +## Translate churn points into LTV, not percentages +**Principle.** Most people watch churn %, but few compute what each point costs in accumulated LTV over 12 months. Cutting churn is a cash lever that needs no price hike or new acquisition. +**Apply when.** You're staring at a churn percentage and treating it as a vanity number rather than money. +**The move.** Do the churn→LTV math: at 25% monthly churn on 1,000 users you must add 250 new users/month just to break even — kill paid traffic and the product dies in ~4 months. Then improve retention without Figma: define the Aha Moment, measure time-to-value (TTV) from signup to it, ask "how do I deliver this faster?", break it into micro-wins if you can't, then test, measure, repeat. +**Evidence.** Finance SaaS, ARPU R$120: cutting churn 5 points (25%→20%) is +R$72,000/year in cash, with no price change and no extra acquisition. +**Source.** [@richardrx · 2026-04-06](https://x.com/richardrx/status/2041184077106004289) diff --git a/.github/skills/revenue-centric-design/references/onboarding-and-activation.md b/.github/skills/revenue-centric-design/references/onboarding-and-activation.md new file mode 100644 index 0000000..4c48608 --- /dev/null +++ b/.github/skills/revenue-centric-design/references/onboarding-and-activation.md @@ -0,0 +1,148 @@ +# Onboarding & Activation + +> Curated, distilled wisdom from @richardrx ("Richard — Design for startups"), translated from Portuguese. Each entry is a reusable principle linked to its source post. + +## An onboarding video welcomes — it doesn't teach +**Principle.** A good onboarding video isn't a manual (nobody reads their car's or iPhone's). It welcomes, builds connection, shows the product at a glance, and points to where value comes fastest. +**Apply when.** Designing first-run onboarding or a welcome video. +**The move.** Aim it at cutting TTV, support tickets, and the lost feeling — not at educating. Length follows your ICP's urgency (someone rushing vs. someone happy to build Lego). No actor or fancy set — Richard recorded his in Screen Studio and it beat many big products'. +**Visual.** A "Your account was created!" welcome modal with an embedded intro-video thumbnail and a single "Next" CTA +**Voice.** "Your product doesn't need a manual either — have you read your car's?" +**Source.** [@richardrx · 2026-06-19](https://x.com/richardrx/status/2067987722954735812) + +## Rising MRR with rising churn means you lost them on day one +**Principle.** When MRR and churn climb together, the user didn't leave in month 2 — they were lost the first day, dropped into a dead empty-state dashboard with nothing guiding them to value. +**Apply when.** Churn is creeping up and you're tempted to blame the product or add features. +**The move.** It isn't a feature gap — measure **TTV** (time-to-value) and get obsessed with shrinking it. CAC, LTV and activation are *product* metrics, not marketing; with weak retention, acquiring more just fills a leaky bucket faster. +**Voice.** "It took two months to cancel, but you lost him the first day after signup." +**Source.** [@richardrx · 2026-06-18](https://x.com/richardrx/status/2067591574138052804) + +## Measure activation, not signups +**Principle.** Technical founders track the wrong onboarding metrics; signups, session time, and tour completion all flatter you without proving the user reached value. +**Apply when.** You're judging onboarding by signups, time-in-product, or "completed the tour." +**The move.** Swap each vanity metric for its real counterpart: signups → activation rate, session time → time-to-first-useful-action (and its repetition), onboarding completion → D7 retention. Find your aha moment empirically: look at what every paying customer did in week one that churned users didn't (often a collaborative act — invite, share, comment). Anchor on TTV/time-to-value. +**Evidence.** Userpilot benchmark (547 companies): avg TTV 1d 12h 23m; top performers under 5 min. SaaS activation rate avg 30–37%, top quartile 40%+, under 20% = structural problem. D7 retention avg 10–15%, over 30% is strong. +**Voice.** "If you can't say how long your user takes from signup to aha moment, you're not measuring what matters." +**Source.** [@richardrx · 2026-05-27](https://x.com/richardrx/status/2059616501544468624) + +## Put friction in the right place, not zero friction everywhere +**Principle.** Friction in the wrong place kills the product; friction in the right place qualifies and retains. "Less friction" is not a universal law. +**Apply when.** You're reflexively cutting clicks and fields, or your human sales team is doing qualification the product should do. +**The move.** Remove friction at trial signup (it kills acquisition), but add calibrated friction in three spots: (1) trial with card upfront filters commercial intent; (2) mandatory onboarding before the dashboard turns users into power-users faster; (3) a 6–8 field enterprise demo form (role, team size, current tool, budget, timeline) lowers lead volume but raises close rate. The mechanism is effort justification (Aronson & Mills, 1959) — same root as the endowment and IKEA effects. +**Evidence.** ChartMogul 2026: opt-in trial (no card) converts 8.9%; opt-out (with card) converts 31.4%. Superhuman requires a 30-min human call before access. +**Voice.** "How much qualification effort is your human seller doing that the product should do before they even step in?" +**Source.** [@richardrx · 2026-05-21](https://x.com/richardrx/status/2057436163841941980) + +## Never ship a blank dashboard +**Principle.** The empty dashboard arrives at the user's peak of curiosity and answers it with a void — this is where most SaaS loses the trial. Every second spent deciding what to do is a second closer to quitting. +**Apply when.** A new user lands post-signup on a screen with no data and no direction. +**The move.** Four fixes: (1) empty state with a next-action hint — the CTA points straight to value; (2) seed sample data so they see the destination before starting; (3) one single clear action ("Import your first spreadsheet"), not eight, not a 12-step tour; (4) visible progress from the first click — start the bar at 20%, not 0%, so completion feels already underway. +**Voice.** "A blank dashboard looks neutral, even tidy — but it just makes the user stop and think about what to do." +**Source.** [@richardrx · 2026-05-12](https://x.com/richardrx/status/2054283657934758021) + +## Design onboarding as a behavioral trigger, not a feature tour +**Principle.** Silent non-activation — users who sign up, vanish in five minutes, and never formally churn — is an activation problem, not a product one. Onboarding should fire a behavior, not narrate features. +**Apply when.** New users evaporate without complaint and you never learn their name. +**The move.** Find the single behavior that statistically separates retained from lost users, make it your activation north star, and measure every onboarding decision against it. Shift focus from explaining features to forcing that behavior fast. Exploit the Zeigarnik effect: open small loops (complete profile, invite 3 colleagues, send first message) so the user carries an unfinished task. +**Evidence.** Slack: teams exchanging 2,000 messages had 93% probability of staying. Facebook's equivalent: 7 friends in 10 days — the company's single focus, repeated at every all-hands. +**Voice.** "What's the number that separates who stays from who evaporates — and how does the user hit it in under 24h?" +**Source.** [@richardrx · 2026-05-11](https://x.com/richardrx/status/2053878928494690414) + +## Map the journey from session replays, not from your diagram +**Principle.** The founder's 12-step journey is built top-down (what the product wants); the user runs 4 steps bottom-up (the specific problem they opened the tab to solve). The gap between them is where avoidable early-stage churn lives — and it's invisible because the founder only ever lived the creator's journey. +**Apply when.** You "know" the happy path but can't state what % of users actually execute it. +**The move.** Three steps, no Figma: (1) write your version of the user journey in numbered steps, on paper; (2) open five real session recordings from the first 7 days and note what each user actually does, in order, with timing — including what they try and abandon; (3) lay both lists side by side. The report is in the differences; each divergence is a hypothesis to confirm or kill. +**Voice.** "The user journey is what shows up in the replay; what's in Figma and Excalidraw is a hypothesis." +**Source.** [@richardrx · 2026-05-08](https://x.com/richardrx/status/2052711138572263474) + +## Add declarative friction, cut administrative friction +**Principle.** "Good onboarding is short onboarding" is incomplete. There are two frictions: administrative (collects data the system uses later, buys the user nothing) and declarative (forces the user to state what they came to do — costs a beat, buys commitment, customization, and journey direction). +**Apply when.** Auditing onboarding steps; deciding what to cut versus expand. +**The move.** Test each step: is it collecting data or making the user declare intent? Collecting only → candidate to cut. Declaring intent → candidate to expand. A declaration ("what do you sell, what's your long-term goal?") creates a micro-commitment to the outcome before the user touches the product, and lets the journey branch (recommendations, tutorials, next actions) off that answer. +**Evidence.** Brazilian payments platform cut time-from-signup-to-first-sale from 24.2 to 2.5 days by adding a ~30-second intent step (positioned between signup and product), not by removing steps. +**Voice.** "Onboarding is also the first chance the user has to declare to themselves what they came to do." +**Source.** [@richardrx · 2026-05-07](https://x.com/richardrx/status/2052350703541039324) + +## Trial conversion is a journey problem, not a pricing problem +**Principle.** A trial is a test of value; if the user never proves value to themselves, no price or trial length saves it. Conversion fails for three diagnosable reasons. +**Apply when.** Users sign up, do "a bunch of nothing," and never return. +**The move.** Diagnose which failure mode applies: (1) blank dashboard → make the first step obvious and immediate, drive to value or a micro-win, drop "explore our product"; (2) lost before value → install Clarity, watch where they stall, optimize that click; (3) lost to life → send a progress email ("you created 3 reports, your team accessed 12 times, you're in the top 20%"), not a generic "trial ending." +**Voice.** "The longest trial in the world doesn't save bad onboarding." +**Source.** [@richardrx · 2026-05-06](https://x.com/richardrx/status/2052096365128273956) + +## Make onboarding active, not passive +**Principle.** Passive onboarding (tooltips, guided tour, docs — learn if you want) assumes the user will explore. They won't: they have 47 tabs open, WhatsApp pinging, and will do the bare minimum before deciding whether to return. Active onboarding designs a sequence where each action delivers value and that value triggers the next. +**Apply when.** Your onboarding opens with "Welcome, here's the documentation." +**The move.** Assume you know the shortest path to value better than the user does. Design that path, strip the friction, and make sure they arrive. Don't ask "what's the minimum the user must do?" — ask "what's the smallest action that delivers the most value in the least time?" +**Evidence.** Slack drops you into a channel and makes you send a message first — you use the product before any tutorial. +**Voice.** "Understanding by doing beats understanding by reading." +**Source.** [@richardrx · 2026-04-22](https://x.com/richardrx/status/2046959126245249288) + +## Treat sub-30-day churn as an onboarding fix, not a feature gap +**Principle.** Most early-stage SaaS churn happens in the first 30 days — which means it's onboarding, not product. Adding features only makes it worse by adding complexity. +**Apply when.** Users leave before they ever liked what you built, and you're tempted to ship more features. +**The move.** Diagnose with three questions: (1) how long to the first real result? If "it depends" or over a day, you're bleeding users; (2) does the user know where they are? Use a progress bar/checklist — Progress effect: someone seeing 30% done is likelier to finish than someone at 0%, so starting at 0% is a design error; (3) what happens when they drop mid-flow — email, push, nothing? Use the Zeigarnik effect to remind them they started. +**Voice.** "Sub-30-day churn rarely dies to features; it dies to the right sequence of micro-interactions that deliver value before asking for effort." +**Source.** [@richardrx · 2026-04-20](https://x.com/richardrx/status/2046212675017887881) + +## Deliver the promised result before teaching mechanics +**Principle.** Nobody wants to learn to use your product; they want the result you promised in the landing-page hero. Teaching mechanics first ("create a project → add a member → configure integrations") is boring; delivering value first converts. +**Apply when.** Your onboarding is a checklist of setup mechanics rather than a path to the outcome. +**The move.** Lead with the outcome ("In 2 minutes you'll see your first report → let's start with the data you already have → done, that's the insight competitors pay consultants for"). Reframe progress with the Progress effect by crediting effort already spent ("You've done the hard part, just 3 steps left"). Keep loops small (Zeigarnik effect — people close loops only if they look closable). Pre-select the right plan from data you collected instead of asking, then offer the upsell. +**Voice.** "The gap between 5% and 15% trial conversion is in these details — not features, not price — in the sequence of micro-decisions you designed without realizing you were designing." +**Source.** [@richardrx · 2026-04-16](https://x.com/richardrx/status/2044785090832543998) + +## Qualify by behavior, not by a long signup form +**Principle.** Friction at the wrong moment kills conversion, and qualification by behavior is more precise than qualification by form. The "more qualified leads" argument for long forms usually loses. +**Apply when.** You're weighing an 8-field signup form against email + password. +**The move.** Default to the minimal form and let qualification happen later, inside the product, from real behavior. Deciding where to add versus remove friction is what separates a product that grows from one that spins its wheels — but it's contextual ("it depends"). +**Evidence.** Same product, two founders: 8-field form (name, email, company, role, phone, segment, team size, how-did-you-hear) → 12% signup rate; email + password only → 34%. +**Visual.** Annotated onboarding step (DevNoodles): an ICP/intent question flagged "Zeigarnik effect" (the step progress dots) and a B2C/B2B card selector flagged "Progress effect" — showing where each bias is engineered into the flow. +**Source.** [@richardrx · 2026-04-10](https://x.com/richardrx/status/2042558822825239030) + +## Celebrate the activation moment, don't just confirm it +**Principle.** At the emotional peak of activation, a number is data but a rising graph is progress — and progress triggers dopamine and an emotional memory tied to the product. Most SaaS confirms where it should celebrate. +**Apply when.** A user completes a hard-won first action (first transaction, first integration) and you respond with a static success state. +**The move.** Engineer the peak moment precisely at the point of highest emotional vulnerability in activation — right after the user clears the effort. Show motion and accomplishment proportional to the effort invested. This is the peak-end rule: users judge an experience by its emotional peak and its ending, rarely by the average. +**Evidence.** Stripe shows a rising graph (not a number) the moment the first transaction processes — the founder who integrated it at 3am remembers exactly where they were. +**Voice.** "A number would have done the job. The graph created a customer." +**Source.** [@richardrx · 2026-04-04](https://x.com/richardrx/status/2040415841628651689) + +## Cut time-to-value by reorder and removal, not feature changes +**Principle.** High TTV is almost never product complexity — it's the form and order in which things happen. Each day between signup and first result is another day of abandonment risk; abandonment = churn. +**Apply when.** Your activation flow is slow and you assume the product itself is the bottleneck. +**The move.** Three iteration cycles, no product changes: (1) reduce cognitive load by grouping and standardizing what the user must fill in; (2) work with legal/compliance to strip everything required by habit but not by actual necessity; (3) invert the sequence so the user feels value before facing the heaviest step. +**Evidence.** Major Brazilian payments platform: 24 days → 2.5 days to first sale (89.7% reduction), no product changes, no cutting of mandatory compliance steps. +**Visual.** Month-by-month TTV table: Jan 24.2d → Feb 19.6d → Mar 16.8d → Apr 9.3d → May 2.5d, alongside accounts created / approved / new sellers per month. +**Source.** [@richardrx · 2026-03-27](https://x.com/richardrx/status/2037583944283996418) + +## Give the trial an active goal, not passive access +**Principle.** A passive trial ("use it if you want, cancel guilt-free") builds no commitment; a trial with an active goal builds commitment before billing. Each completed day raises the psychological cost of canceling — the user starts defending a decision they already made, before paying. +**Apply when.** Your trial is open-ended access with no challenge or target. +**The move.** Set a recurring daily goal during the trial that the user opts into and completes. The named mechanism is progressive commitment. The product can be good or bad — done right, the onboarding itself is excellent. +**Evidence.** Wispr Flow challenges trial users to dictate 100+ words a day for 7 days — looks generous, is behavioral science. +**Source.** [@richardrx · 2026-03-26](https://x.com/richardrx/status/2037316988981174464) + +## Calibrate the first step to the user's real willingness +**Principle.** Users don't rationally evaluate the first task — they evaluate perceived effort. Ask too much up front and the cognitive cost of even imagining the task is paralyzing, and they quit before starting. This is the activation-barrier effect. +**Apply when.** Your first onboarding step bundles setup, data import, team invites, and project creation — or demands a campaign-sized action. +**The move.** Two biases fix it: (1) started-progress effect — show what the user has already done before what's left; a loyalty card with the first stamp pre-filled beats a blank 9-stamp card, because the starting point changes perceived distance to the finish; (2) small-steps effect — "Create one story today" has radically lower perceived cost than "post 5×/week," even when the underlying task is identical. Sequence effort so the user hits first value before noticing how much they invested; build momentum. +**Evidence.** Instagram A/B test made "Create 5 new public reels" the first task — for someone who barely posts weekly, that's paralyzing. +**Visual.** Bad first-step example: a weekly-progress checklist at "0% completed" whose top item is "Create 5 new public reels (0/5)." +**Voice.** "Asking for more isn't necessarily the problem; asking for all of it at once, with no progress anchor and no commitment ladder, is." +**Source.** [@richardrx · 2026-03-24](https://x.com/richardrx/status/2036461688505909250) + +## Treat onboarding as the bridge between CAC and LTV +**Principle.** Founders obsess over CAC and landing-page conversion but are blind to activation cost. Onboarding isn't an interface tutorial — it's the bridge from CAC to LTV and the point of maximum leverage to expand revenue. Cancellation happens on day one; it's merely formalized when Stripe's billing reminder lands. +**Apply when.** Users enter the trial without intent to a result and ghost before ever paying. +**The move.** Two fixes: (1) turn support into UX — every onboarding support ticket is a design failure; map recurring setup questions and convert the answers into features or in-flow tooltips; (2) compress TTV obsessively — make the user experience the product's core promise in the least time possible. If they must configure 5 screens before any result, you've already lost. +**Voice.** "Letting a user into the trial without intent of a result isn't self-service. Design's job doesn't end at signup — that's where it starts paying you back." +**Source.** [@richardrx · 2026-03-13](https://x.com/richardrx/status/2032485654811083005) + +## Pick onboarding patterns by awareness × flow complexity +**Principle.** There's no best onboarding in the abstract — only the one that removes friction and delivers value early for your users. Treat the nine patterns as behavior-shaping mechanisms, not UI components, and select by two axes. +**Apply when.** Choosing or combining onboarding patterns for a new flow (SaaS, PLG, B2B early stage). +**The move.** The nine patterns (pattern → ideal use / tradeoff): **1. Welcome modal** → high-awareness ICP or low-complexity products; easy to build, easy to ignore. **2. Wizard / product tour** → B2B and complex/high-cost-of-error flows (fintech, compliance); long tours cause boredom and need constant upkeep. **3. Contextual tooltips** → advanced/secondary features; slashes support tickets but users may miss them if contrast is poor. **4. Empty state** (his favorite) → dashboards, lists, data-dependent areas; if executed well it's mandatory, directs the first action and accelerates TTV. **5. Personalization** → products serving many ICPs with different journeys; great CRM data, but too long kills signup conversion. **6. Checklists** → critical flows with mandatory prerequisites (webhook, KYC); exploits the Zeigarnik effect, but a long list breeds aversion — every item must move toward value, no bureaucratic tasks. **7. Goal-setting** → habit products (finance, productivity, health); uses commitment bias, but a broken goal can break the emotional contract. **8. Sample data** → sell the dream of a full, organized product before the user inputs anything (distinct from skeleton screens). **9. Use cases / demos** → products burning expensive resources (AI tokens) with infinite outputs; show max potential without forcing creativity from scratch. +Selection rule — two axes: **awareness level** (low = needs guidance; high = needs speed) × **flow complexity** (high = needs structure). Four cases: low-awareness + high-complexity → tours + checklists + personalization; low + low → modal + empty state; high + high → checklist + tooltip + empty state; high + low → modal + empty state (then get out of the user's way). Build vs. buy is financial/operational, not aesthetic — buy (Wistia, PostHog, Sprig) when speed is critical, dev is overloaded, or you're running A/B tests; build when onboarding is strategic, you need perfect aesthetic integration, or you must avoid third-party dependence. +**Voice.** "Onboarding's job isn't to teach the user — nobody likes an instruction manual — it's to remove cognitive effort and deliver utility as fast as possible. TTV correlates directly with churn." +**Source.** [@richardrx · 2026-02-04](https://x.com/richardrx/status/2019019293761941566) diff --git a/.github/skills/revenue-centric-design/references/positioning-icp-and-gtm.md b/.github/skills/revenue-centric-design/references/positioning-icp-and-gtm.md new file mode 100644 index 0000000..eaf49af --- /dev/null +++ b/.github/skills/revenue-centric-design/references/positioning-icp-and-gtm.md @@ -0,0 +1,64 @@ +# Positioning, ICP & Go-to-Market + +> Curated, distilled wisdom from @richardrx ("Richard — Design for startups"), translated from Portuguese. Each entry is a reusable principle linked to its source post. + +## Size the market with TAM → SAM → SOM (and know which one matters) +**Principle.** Your real market is far smaller than the population — TAM is a theoretical ceiling (Brazil's 213M becomes ~101M credit-card holders for a paid app). Investors often ignore a TAM under ~R$1B, but the number that matters is the **SOM** — what you can actually capture. +**Apply when.** Sizing a market, writing a deck, or judging whether a niche is big enough. +**The move.** TAM = total addressable ceiling; **SAM** = the realistic slice your model reaches (~40% in his example); **SOM** = the 1–5% you truly win in ~36 months — and SOM isn't a guessed %, it comes from real CAC, activation, support capacity and LTV. You can't change your TAM; you change how much of your SAM you convert and retain. +**Evidence.** RepareCar: ~76k mechanic shops (honest TAM) → SAM ~47k → ~3% ≈ 1,414 shops ≈ R$1.6M ARR; current pace (~10 shops/day) ≈ 7% of SAM in 12 months. +**Visual.** TAM/SAM/SOM concentric-circle diagram with definitions +**Source.** [@richardrx · 2026-06-25](https://x.com/richardrx/status/2070140923380420796) + +## Frame the referral prize as a gift to the friend, not a commission to the referrer +**Principle.** Member-get-member (MGM) referral programs win on framing and timing, not just on a two-sided reward. Money makes the exchange feel transactional; an in-product benefit feels like a genuine gift. +**Apply when.** Designing or fixing a referral program and defaulting to "refer a friend, get $20." +**The move.** Apply the framing effect: surface the prize on the receiver's side ("João gave you 500MB"). Ask for the referral at the peak of value (right after a concrete win, or when the user hits a limit). Avoid cash; give a reward that deepens use of your own product. Embed it as continuous in-product operation, not a one-off campaign. Caveat: referral amplifies a product people already love; it can't fix one nobody recommends for free. +**Evidence.** Dropbox grew 3900% in 15 months (100k → 4M users), peaking near 3M invites in a single month; ~1/3 of users already arrived via word-of-mouth before the program. +**Voice.** "A referral amplifies a product people already love — it doesn't fix a product nobody recommends for free." +**Source.** [@richardrx · 2026-06-02](https://x.com/richardrx/status/2061766945582559509) + +## Pick a deliberately under-served niche as your ICP +**Principle.** A clear ICP (ideal customer profile) is not "everyone who could use my product." It's a deliberately chosen, under-served niche — and a sharp niche beats no niche, because you can't out-fight the entrenched generalist giant. +**Apply when.** Early traction; tempted to "embrace the world" out of fear of a small TAM. +**The move.** Validate four ICP filters: (1) feels the pain with real weight — pain is proportional to what's lost when unsolved (a lost lead costs a face-aesthetics clinic R$3,000 vs. R$60 for a barber); (2) big enough TAM to sustain operations; (3) money to pay your required ticket so unit economics close; (4) founder-fit, giving native language, a fast validation network, and instinct that money can't buy. With a clear ICP, failure has a diagnosis ("I got the messaging wrong"); without one, you can't tell if product, copy, channel, price, or audience failed — and every test burns runway. +**Voice.** "A generalist ERP is hard to sell; an ERP for cabinetmaking is a different conversation." +**Source.** [@richardrx · 2026-05-19](https://x.com/richardrx/status/2056789797646029232) + +## Don't claim PLG without the four structural conditions +**Principle.** Product-led growth (PLG) is a consequence of structural conditions, not a product decision you declare. Most B2B SaaS that pitches PLG is really sales-led wearing a PLG label. +**Apply when.** Writing a pitch deck or strategy and calling the motion "self-service" / PLG. +**The move.** Require all four conditions: (1) TTV < 10 minutes — if it needs a consultant demo, API support, or paid implementation, it's not PLG (tell: full trial, zero activation); (2) ticket below ~R$1,000 — higher means a buying committee; (3) native virality or collaboration (Notion, Figma, Slack pull users in; a CRM/AI tool needs SDRs, demos, follow-up); (4) a huge addressable market with a real bottom-up TAM. If you fail these, run sales-led honestly. +**Evidence.** Brazil has ~20,000 companies with 100+ employees, and only ~a dozen B2B SaaS where PLG makes real economic sense. +**Voice.** "Founders love PLG because it seems to delete the part they don't master — selling." +**Source.** [@richardrx · 2026-05-04](https://x.com/richardrx/status/2051262752547536941) + +## Charge your first ten users from day one +**Principle.** The first ten users define the product's entire curve, and payment is the cheapest test of real pain — curiosity is free, an open wallet demands a concrete problem. +**Apply when.** Validating a new product and tempted to give early access away to "build a base." +**The move.** Source the first ten from closed communities, personal reach, or pure guerrilla. Charge even while in prototype. When someone says they can't pay, ask directly: "What does the system need to do for you to pay right now?" Treat the payment friction as part of the test. Collect dense feedback; only start visual design after ~20 paying users. For B2C apps the method shifts (e.g., pre-sale of a solution-in-progress) but the principle holds. +**Evidence.** RepareCar's first 25 auto shops tested the product in prototype; the team visited each and charged at the end, designing visuals only after 20 paying shops. +**Voice.** "Curiosity is free; an open wallet demands a concrete problem." +**Source.** [@richardrx · 2026-04-26](https://x.com/richardrx/status/2048359526487716333) + +## Reverse-engineer the funnel math before celebrating an MRR target +**Principle.** Building the product is the easy part; distribution is the game. A revenue target is really a traffic-and-retention problem, and churn quietly resets the whole funnel every month. +**Apply when.** Someone asks "is it hard to hit X MRR?" or you're sizing acquisition for a target customer count. +**The move.** Work backwards: to net 2,500 customers at 5% LP conversion you need 50,000 visitors; from ads at 3% creative CTR, ~1.6M impressions (5% and 3% are top-decile — most land at 1–2% LP and under 1% CTR, so you test dozens). Then add the leaky bucket: at 20% monthly churn, average customer life is 5 months, so you replace 500 customers every month forever just to stand still (≈10,000 visitors / 333,000 impressions). The problem lives at the intersection of dev, design, and marketing — none alone owns it. +**Evidence.** 20% monthly churn → 5-month average lifetime; at low ticket many operate at 40–50% churn, so "the bucket never fills." +**Voice.** "Building the product is the easy part; distribution is the game." +**Source.** [@richardrx · 2026-04-20](https://x.com/richardrx/status/2046222319912132977) + +## Concentrate channels with the Bullseye framework, not scattershot testing +**Principle.** Testing ten channels at once means you never know what drove results and you blame the channel when the business stalls. Distribution needs prioritized focus — and a perfect channel still fails if the receiving structure leaks. +**Apply when.** You're spreading content and traffic across many channels with no clear read on what works. +**The move.** Use the Bullseye framework (from the book *Traction*): three rings of priority. Inner ring = at most three highest-potential channels with total focus; middle ring = up to six channels you probe with small experiments; outer ring = everything plausible long-term, no active focus now. Choose between channels with an ICE Score (Impact, Confidence, Ease, each 0–10, divide by 3, prioritize). Crucial gap the book skips: scaling distribution onto a broken reception structure (LP, onboarding, first product steps) yields no growth — distribution and retention are simultaneous, not sequential. +**Visual.** Bullseye as nested circles — What's Possible → What's Probable → What's Working — beside a "Marketing Framework for Startups" triangle (Prioritization, Testing, Quick Iteration). +**Source.** [@richardrx · 2026-03-23](https://x.com/richardrx/status/2036035304868434115) + +## Diagnose the bottleneck: no entries is distribution, leaving without paying is design +**Principle.** Design can't save a "ghost product." Design optimizes and raises the LTV of something that already has traffic; it can't manufacture demand. +**Apply when.** A builder ships an app, gets near-zero users, and hopes a redesign will rescue it. +**The move.** Split the diagnosis cleanly: if nobody enters your product, it's a distribution problem; if they enter, don't pay, and leave, it's design. Read *Traction* even if you can afford an agency or a marketing team — the lever isn't just cost-per-channel but each channel's awareness level, which drives different conversion and retention behavior depending on where and how the user arrived. +**Voice.** "If nobody enters your product, it's distribution. If they enter, don't pay, and leave, that's design." +**Source.** [@richardrx · 2026-02-28](https://x.com/richardrx/status/2027721170569564521) diff --git a/.github/skills/revenue-centric-design/references/pricing-and-monetization.md b/.github/skills/revenue-centric-design/references/pricing-and-monetization.md new file mode 100644 index 0000000..a5e7f0e --- /dev/null +++ b/.github/skills/revenue-centric-design/references/pricing-and-monetization.md @@ -0,0 +1,87 @@ +# Pricing & Monetization Psychology + +> Curated, distilled wisdom from @richardrx ("Richard — Design for startups"), translated from Portuguese. Each entry is a reusable principle linked to its source post. + +## The freemium trap, in numbers: higher conversion, far lower cash +**Principle.** A free plan lifts signup conversion but can crush the economics — the higher top-of-funnel number hides a worse business. +**Apply when.** You're tempted by freemium's better conversion rate. +**The move.** Run the funnel (same R$80k/mo traffic, plans from R$199). **With freemium:** ~8% → 800 signups → 80% activate → 5% pay = 32 payers (R$6,368 MRR), saturating ~160 payers under 20% churn, while 768 free users burn AI tokens (~$0.08 each) — real CAC ≈ R$2,540/payer, payback ~13 months. **Without:** ~3% → 300 payers = R$59,700 MRR (~10×), CAC ~R$275, payback ~6 weeks, LTV:CAC 5:1 that reinvests its own profit. Freemium only pays off if free brings *organic/viral* users you didn't pay for. +**Voice.** "One scenario reinvests its own profit; the other funds losses until the money runs out." +**Source.** [@richardrx · 2026-07-01](https://x.com/richardrx/status/2072312844784152628) + +## Whether freemium works is decided by the cost to serve a free user +**Principle.** Freemium isn't good or bad in the abstract — the *cost of free* decides, and it hinges on (1) how much it costs to serve non-payers and (2) how long/expensive activation is. +**Apply when.** Considering a free plan, especially as a bootstrapped (non-bigtech) founder. +**The move.** If serving a free user costs almost nothing and TTV is short, free becomes an acquisition channel (Slack — first message in minutes; it's the short TTV, not the cash, that sustains it). If the product runs on AI (dollar-priced tokens) or activation is long, a free account is an expensive bet that a small fraction funds — which needs deep pockets (the exception, not the average founder). On a friendly average, only ~3–4% of freemium converts. Otherwise: charge — well, and early. +**Voice.** "For an AI product, your free user was never free." +**Source.** [@richardrx · 2026-06-30](https://x.com/richardrx/status/2071962778072469560) + +## Price is the cheapest money — stop anchoring it to the cheapest competitor +**Principle.** Pricing is a SaaS's biggest lever, yet ~90% of products are underpriced — the founder, who knows every limitation, anchors on the cheapest competitor instead of on value delivered. The buyer only sees the problem solved. +**Apply when.** Setting or revisiting price; fearing a "no." +**The move.** Raise toward value. A 30% price increase doesn't yield 30% MRR (some churn), but what remains is nearly pure cash — no acquisition in between — while growing a channel 30% costs money, time, and has a ceiling. Low price costs you later: less budget to reach your ICP, a CAC-obsession trap (the real metric is the CAC↔LTV *gap*, which price widens on both sides), and higher churn (cheap attracts uncommitted buyers). Design link: the number must be sustained by perceived value — your page and first use justify or destroy it. +**Voice.** "Charging more without seeming to be worth more is just raising the price of rejection." +**Source.** [@richardrx · 2026-06-29](https://x.com/richardrx/status/2071634185228329219) + +## Make the middle plan the one you actually want to sell +**Principle.** Each plan has a behavioral job, not just a price; the plan you most want to sell should sit in the middle, flanked by a decoy below and an anchor above. +**Apply when.** Building or auditing a SaaS pricing page, especially if you copied competitors without assigning each tier a role. +**The move.** Use the decoy effect: place your target (e.g. Pro) in the middle; make the tier below it clearly inferior on one important attribute (user cap, no critical integration, no priority support) so Pro looks obvious. Keep exactly three plans — four+ triggers the paradox of choice and users stall. Add a top tier (Enterprise) purely to anchor price perception. Ask: "What is my decoy today?" If you can't name one, it likely doesn't exist. +**Evidence.** Ariely's MIT test of The Economist's tiers: with the print-only decoy, 16%/84% chose online/combo; removing it flipped choices to 68%/32%, cutting combo revenue by more than half. Estimated +30–43% subscription revenue. +**Visual.** Economist subscription page; the decoy's removal shifts combo-plan share from 84% down to 32% +**Voice.** "Option B was never built to be sold — it was built to make C look obvious. It's the bait." +**Source.** [@richardrx · 2026-05-28](https://x.com/richardrx/status/2059951433827426437) + +## Ask for the card in trial — but optimize for the right ICP, not raw conversion +**Principle.** Requiring a credit card multiplies trial-to-paid conversion but shrinks signups; the goal is the model that attracts and retains the right ICP, not the one with the highest headline conversion. +**Apply when.** Choosing trial-with-card vs trial-without-card (or freemium), or designing recurring billing for a Brazilian market. +**The move.** Weigh the funnel both ways. Trial-with-card converts harder but starves you of volume; trial-without-card floods the funnel with low-intent users. Run the full math, not just the conversion rate. In Brazil, also account for PIX recorrente, whose dynamics differ from monthly card billing. +**Evidence.** ChartMogul 2026 (US, 200 products): trial-with-card converts ~31.4% vs 8.9% without — 3x+. Worked funnel: 1,000 visitors → 30 trials → 9.4 paying (with card) vs 85 trials → 7.5 paying (without). Author observes PIX-recorrente cohorts churn more than card cohorts. +**Voice.** "Don't ask which model converts more — ask which model attracts and retains the right ICP." +**Source.** [@richardrx · 2026-05-15](https://x.com/richardrx/status/2055247161349054950) + +## Frame the upgrade as a loss at the moment of value, not a feature you're selling +**Principle.** Low upgrade rates are usually a framing-and-timing problem, not a price problem; remind users what they've already invested and what they stand to lose. +**Apply when.** A happy, active free user never upgrades, or your upgrade rate sits below 5%. +**The move.** Three framings beat generic limit/discount/feature-gate prompts. (1) Sunk cost: surface the assets they've built — "You created 47 custom reports. On the free plan you lose access to 40." (2) Loss aversion: framing loss outconverts framing gain — "You'll lose access to 8 months of history" beats "Get unlimited history." (3) Limited-access gate timed to an imminent, known result — "Your report is ready. To export as PDF, activate Pro." The timing/context of the gate matters more than the gate itself. +**Voice.** "If your upgrade rate is below 5%, the problem probably isn't price — it's how and when you're asking." +**Source.** [@richardrx · 2026-04-21](https://x.com/richardrx/status/2046544442216054981) + +## Engineer the comparison frame with a decoy and a high anchor — and drop Free from the top +**Principle.** Conversion shifts when you change the frame of comparison, not the product; equal-looking options cause delay, and showing Free first anchors everyone to zero so everything else feels expensive. +**Apply when.** You run the default Free / Pro / Enterprise (sob consulta) ladder and Pro isn't converting. +**The move.** Insert a decoy: a Starter just below Pro with irritating limitations (e.g. R$79 vs Pro R$99) so users compare Starter↔Pro and Pro wins for R$20 more. Remove Free from the visible top so the first number isn't zero — anchoring means the first price seen sets the reference; lead with a higher/previous/Enterprise price so Pro at R$99 reads as cheap. +**Evidence.** The Economist sold 3x more print+digital after adding a same-price print-only decoy nobody bought. Author cites documented tests lifting conversion 10–20% via reframing alone. +**Voice.** "You're competing against your own free plan. And losing." +**Source.** [@richardrx · 2026-04-14](https://x.com/richardrx/status/2044014136770580743) + +## Tie the trial's end to value consumed, not the calendar +**Principle.** Blocking access on a fixed day count (7/14/28) is a lazy rule; the billing trigger should fire on value consumption, after the user's first real win. +**Apply when.** You copied a competitor's 14-day trial and paid conversion is failing, or you're setting trial length from scratch. +**The move.** Never paywall before a clear micro-win or solving the core problem — doing so kills conversion and breeds bad word of mouth. Set length using four variables: (1) Product complexity — enterprise needs time for compliance/security review, not just the user. (2) Time to Value — Spotify delivers in seconds, a CRM needs days of data. (3) Usage frequency — rarely-used products may need long trials, or none at all (a once-a-year tax tool shouldn't have a trial). (4) Card entry — no card means a shorter trial to create urgency; with card, watch silent next-month churn. Note: sunk cost only bites if the user built a real asset — a bad onboarding produces frustration, not switching cost. +**Voice.** "Locking access purely on the calendar is a lazy rule that can cost you dearly — you're burning CAC without knowing where value lands." +**Source.** [@richardrx · 2026-03-16](https://x.com/richardrx/status/2033502548301091057) + +## Order pricing rows by the serial-position effect: killer feature first, differentiator last +**Principle.** Users don't read pricing lists linearly; attention and memory cluster on the first and last items, so feature order is itself a conversion lever. +**Apply when.** Laying out the feature rows inside a pricing card or comparison table. +**The move.** Exploit the serial-position effect (primacy + recency). Top: value anchor — never "24/7 support"; lead with the core/killer feature that solves the ICP's main pain and justifies ~80% of the ticket and the ROI. Middle: utilitarian features (exports, integrations, storage limits) the user won't memorize but will scan to compare against the next plan. Bottom (nearest the CTA): the differentiator, bonus, or loss-aversion hook — a lifetime guarantee or dedicated support. The middle of the list is "a cognitive black hole." +**Visual.** Pricing card emphasizing the bold first row (core feature) and bold last row (super bonus), with greyed utilitarian middle rows +**Voice.** "Pricing success depends not just on what you deliver, but on the order the brain is led to process the value." +**Source.** [@richardrx · 2026-03-05](https://x.com/richardrx/status/2029623167900061970) + +## Build a single value axis, then tune the decoy's distance to your target plan +**Principle.** A plan ladder must read as one clear progression of value; mixing quantitative and qualitative axes muddles it, and where you place the decoy's price decides which plan looks like the deal. +**Apply when.** Naming and pricing tiers, or the "value staircase" between your plans isn't obvious to users. +**The move.** Pick one progression — quantitative (rising credits/users) or qualitative (24/7 human support, special features) — rather than blending both. Borrow Starbucks-style naming (Tall/Grande/Venti) so every tier sounds good and lifts the brand. Then position the decoy: place it near the most expensive plan and the expensive plan looks cheap; place it near the cheapest and the decoy itself becomes the most attractive option. +**Visual.** Decorative 3D price-tag illustration — no data. +**Source.** [@richardrx · 2026-02-02](https://x.com/richardrx/status/2018357024543715480) + +## Engineer the pricing page with Good-Better-Best and control the comparison +**Principle.** Lost LTV is rarely about price — it's analysis paralysis from a missing choice architecture. The brain is lazy and judges by relative comparison (priming + anchoring), so if you don't design the anchor, users compare you to "nothing" or to the cheapest competitor. +**Apply when.** Designing or fixing a pricing page; conversions die at the final step despite strong CAC spend. +**The move.** Use a Good-Better-Best (GBB) structure: **Good** = a stripped entry plan that anchors a low price but is limited enough to make users feel pain and look up (never make it free — then everything above looks expensive). **Better** = your standard plan, the target for ~80% of buyers; price it closer to Good than to Best so users think "paying only ~20% more I get double?" **Best** = the value anchor that exists mainly to make Better look cheap (bicycle analogy: without the carbon-fiber Best, the carbon-wheel Better looks expensive). Golden rule: keep comparisons on one axis — don't pit "10,000 tokens" against "Priority Support"; prefer linear, ideally asymmetric, growth. Cap at 2–5 plans (6 = anxiety, paradox of choice). Then control which attributes you compare — your own "Brazil vs Paraguay" table — choosing indicators that favor your value thesis. Highlight Better with color/size/badges. "Stop making the user do the math — do the math for them." +**Evidence.** Cites Briesch et al. (1997) and Mazumdar et al. (2005) on reference-price models, and Chernev (2015) on choice overload. +**Visual.** Two mirrored BR-vs-PY indicator tables prove framing: swapping which metrics are shown flips which country "wins". Four-tier mockup highlights a "Most Popular" target beside a high anchor (Hick's law / few options) +**Voice.** "Your pricing page is killing your LTV — and I can prove it." +**Source.** [@richardrx · 2026-02-03](https://x.com/richardrx/status/2018693884449009956) diff --git a/.github/skills/revenue-centric-design/references/product-strategy-and-features.md b/.github/skills/revenue-centric-design/references/product-strategy-and-features.md new file mode 100644 index 0000000..dac3123 --- /dev/null +++ b/.github/skills/revenue-centric-design/references/product-strategy-and-features.md @@ -0,0 +1,58 @@ +# Product Strategy & Feature Discipline + +> Curated, distilled wisdom from @richardrx ("Richard — Design for startups"), translated from Portuguese. Each entry is a reusable principle linked to its source post. + +## Run every feature through a two-layer "Swiss Knife filter" before building +**Principle.** A feature you ship stays forever and charges rent forever, so the right question isn't "is this good?" but "does it deserve the permanent cost it imposes on the product?" +**Apply when.** A roadmap item feels appealing but nobody applied a filter before committing to build. +**The move.** Layer 1 — does it deserve to exist? Pass all four: (1) cognitive load (more surface = more to learn + Hick's law decision time); (2) ICP specificity (a CRM for facial-aesthetics clinics charges 5x a generic one); (3) operational cost (maintain/support/document, not build); (4) reinforces the core claim. Layer 2 — build now? Two axes: easily rejectable (clear "no"?) and easily implementable (cost to the validating version, not the dream version). Build the no-brainers first; fail any of the four, kill it guilt-free. To rank survivors, score (New Users + New Revenue + Impact Level) / Effort. +**Visual.** Prioritization scoring table: (New Users + New Revenue + Impact Level) / Effort = Score +**Voice.** "Every feature that gets in, stays. And it charges rent forever." +**Source.** [@richardrx · 2026-05-26](https://x.com/richardrx/status/2059236567533650119) + +## Feature adoption is a design problem, not a communication problem +**Principle.** Shipping a feature doesn't make it discovered; users move through their habitual path and never see what they aren't looking for. +**Apply when.** Three weeks post-launch only ~9% of active users opened the feature and ~4% used it twice, despite changelog, email, and "new" badges. +**The move.** Stop treating adoption as announcement. The killers are inattentional blindness (users don't see what they aren't seeking) plus status-quo bias (re-learning cost outweighs perceived benefit even when the new way is better). Instead: directional empty states that surface the feature where it'd be used; triggered onboarding fired by the behavior that signals need (CRM user hits the sales page → introduce the objection-busting AI); and a feature adoption rate metric measuring habit/appropriate frequency, not clicks. Anything below an adoption threshold goes back into review. +**Voice.** "Launching a feature is easy; getting it used is a whole other thing." +**Source.** [@richardrx · 2026-05-20](https://x.com/richardrx/status/2057162392048476345) + +## Compute your Swiss Knife Index to expose feature creep +**Principle.** A product's worth is measured by features actually used, not features shipped; a bloated product is expensive to sustain and hard to sell, not rich. +**Apply when.** The roadmap has become a user wishlist and every new feature feels like progress (especially with AI making building cheap). +**The move.** Swiss Knife Index (SKI) = (features used by >40% of active users in a 30-day window) ÷ (total features). Below 0.3, you own a clumsy Swiss army knife. Fix it with: quarterly audits on real usage data (not team opinion); hide, don't delete (push rarely-used features into advanced settings — reachable for the 3%, gone for the 97%); and a gate on every new feature — "which existing feature do I kill to make cognitive room?" Litmus test: which feature would you show first with 30 seconds to sell? The rest stays invisible until needed. See the academic grounding (2034248739557159293) and the curve (2033880553607364684). +**Visual.** SKI curve — perceived utility rises then declines past the optimal point as complexity keeps climbing +**Voice.** "Which feature would I show first if I had 30 seconds to sell the product?" +**Source.** [@richardrx · 2026-05-20](https://x.com/richardrx/status/2057124008445796659) + +## Design the attention hierarchy to direct behavior, not just organize info +**Principle.** A product that organizes delivers access; a product that directs delivers activation — and the visual hierarchy decides which the user gets. +**Apply when.** "My interface looks good, but people don't use the main features" — and the key feature is buried behind three clicks the user will never make. +**The move.** Recognize that attention hierarchy is the structure deciding what users see first, find with effort, or never discover. Built without intent, the product sabotages itself: users use what's most salient, which is rarely what retains. Plan the hierarchy to influence behavior — make the value-driving, retention-driving feature the most prominent thing — instead of merely arranging information neatly. +**Visual.** A typographic demo (huge headline "YOU WILL READ THIS FIRST") proving the eye follows visual weight, not reading order +**Voice.** "A well-designed attention hierarchy makes the user use what retains; a bad one makes them use what's most salient." +**Source.** [@richardrx · 2026-04-01](https://x.com/richardrx/status/2039399756452057159) + +## Ground feature discipline in the academic feature-fatigue research +**Principle.** Past a cognitive-load threshold, the subjective evaluation of a product doesn't stay neutral — it declines into frustration, confusion, and task abandonment, directly hitting CAC and LTV. +**Apply when.** You need the evidence behind cutting features, and want to separate pre-purchase appeal from post-purchase utility. +**The move.** Apply the SKI as a decision criterion grounded in feature fatigue. More features help pre-purchase comparison via distinction bias but hurt the decision via analysis paralysis (more options = longer decisions and more no-decisions; no decision, no conversion). Each extra feature steepens the learning curve — measurable B2B productivity loss — and when value comes slowly, users silently churn before the trial ends, blaming themselves, not the product. This complements the index (2057124008445796659) and the curve (2033880553607364684). +**Evidence.** Thompson, Hamilton & Rust (2005), "Feature Fatigue," JMR 42(4); distinction bias (Hsee & Zhang 2004); analysis paralysis (Iyengar & Lepper 2000). +**Voice.** "A product that grows without criteria doesn't get rich — it gets expensive to sustain and hard to sell." +**Source.** [@richardrx · 2026-03-18](https://x.com/richardrx/status/2034248739557159293) + +## Past the optimal feature count, a technically bigger product becomes functionally worse +**Principle.** The relationship between feature count and perceived utility is non-linear: there's an optimal point, after which each added feature reduces perceived utility while raising sustaining cost and the learning curve. +**Apply when.** You hear "my interface looks good, but people don't use the main features" — a sign you've passed the optimal point. +**The move.** Read the SKI curve: utility climbs to a peak (~10 features in the example) then falls as complexity keeps rising. The fix isn't more visibility — it's reducing the product's cognitive load so the rest becomes visible again. Criterion: any feature used by under 10% of the active base must justify its existence or leave. There's no universal ideal count — only the ideal for your ICP, context, and device. +**Visual.** SKI graph: green perceived-utility curve peaks at the optimal point (10.3 features, 97), red complexity curve rises monotonically and overtakes utility in the "decline zone" +**Voice.** "A bloated product isn't a rich product — it's a product actively destroying the conversion and retention you paid dearly to win." +**Source.** [@richardrx · 2026-03-17](https://x.com/richardrx/status/2033880553607364684) + +## Focus on your core; trying to be "all-in-one" dilutes your value proposition +**Principle.** Chasing a bigger TAM by going generic destroys retention of your heavy users without converting new ones — the same roadmap mistake in cars and in software. +**Apply when.** The product is tempted to "embrace the world" and become a do-everything tool, abandoning the specific ICP that made it loved. +**The move.** Remember who your ICP actually is and build for them, even at the expense of broad appeal. In software, when UI/UX tries to cover everything, the value proposition dilutes: you wreck heavy-user retention and fail to convert newcomers because you've gone generic. Focus relentlessly on the core. +**Evidence.** Porsche chased China's TAM with generic EVs, abandoning its ICP (visceral flat-six machines); ~€3.9B in losses to reverse the roadmap — operating profit fell from €4,000M (2022) to €40M (9M 2025), margin 18% → 0.2%. [Porsche figures from the quoted post; treat as illustrative.] +**Voice.** "Focus on your damn core." +**Source.** [@richardrx · 2026-03-11](https://x.com/richardrx/status/2031722047080960265) diff --git a/.github/skills/revenue-centric-design/references/revenue-centric-design.md b/.github/skills/revenue-centric-design/references/revenue-centric-design.md new file mode 100644 index 0000000..bca5bba --- /dev/null +++ b/.github/skills/revenue-centric-design/references/revenue-centric-design.md @@ -0,0 +1,105 @@ +# Revenue-Centric Design — Philosophy & Process + +> Curated, distilled wisdom from @richardrx ("Richard — Design for startups"), translated from Portuguese. Each entry is a reusable principle linked to its source post. + +## The 9 principles of Revenue Centric Design (RCD) +**Principle.** Intentional design serves the user AND the business at once — value and revenue, not one or the other. Richard's canonical framework, named Revenue Centric Design (RCD), built after Dieter Rams' 10 laws (form/function) and Amber Case's Calm Technology (attention/context) — "neither taught me to think about revenue." +**Apply when.** Designing any digital product meant to convert, retain, and expand; you need a north-star checklist for decisions. +**The move.** Apply all nine: +1. **Neutrality is omission** — an interface that doesn't direct hurts conversion. +2. **Who talks to everyone convinces no one** — no ICP means generic value, which retains worse. +3. **Value first, ask later** — proof must arrive before the user questions their choice. +4. **Your promise is the size of your proof** — the market believes what you demonstrate, not what you claim. +5. **Same competes on price, different on category** — contrast in mechanism, narrative, or experience; no contrast, no margin. +6. **Default is the decision you made for the user** — most never change settings; the initial state defines mass behavior. +7. **Retention is built, not requested** — show what the user accumulated; perceived loss retains more than promised benefit. +8. **Expansion is born of usage** — upsell that interrupts breeds resistance; upgrade at the moment of the limit converts frictionlessly. +9. **Price is a filter** — pricing defines who enters, who stays, who expands; wrong price attracts the wrong ICP. +**Voice.** "Rams taught me form and function. Amber Case taught me attention and context. Neither taught me to think about revenue." +**Source.** [@richardrx · 2026-05-05](https://x.com/richardrx/status/2051672248348479691) + +## Design's leverage isn't constant — it changes with the product stage +**Principle.** Design's payoff is near-zero at MVP and grows to decisive at scale; when a product is dying, design is the *last* place to look for the culprit. Knowing your stage tells you whether design moves the cash or is just vanity. +**Apply when.** Deciding where design effort should go at your current stage. +**The move.** Match the discipline to the stage: **MVP** — shorten the path to value and say no to "obvious" features; **Survival** — fix onboarding/activation (the first week beats the whole roadmap and buys runway); **Traction** — conversion (sharp LP + tuned onboarding as channels saturate); **PMF** — depth (design the second "aha," upgrade path, expansion, so retention stabilizes higher); **Scale** — design becomes a system (a design system so 3–4 teams ship without you). Shorten → Activate → Convert → Expand → Systematize. +**Voice.** "Polishing the UI of a product nobody wants is the most beautiful mistake there is. It dies pretty." +**Source.** [@richardrx · 2026-06-15](https://x.com/richardrx/status/2066476811177877962) + +## Design owns the flow, not the final coat of paint +**Principle.** What decides whether a user converts or churns — information order, when you ask for the card, what appears at moments of doubt, when value is first felt — is set and coded long before a "finished" product reaches design. +**Apply when.** Design is scoped as "make it pretty before launch"; product/eng/requirements own the flow (common in big orgs or eng-led teams). +**The move.** Pull design upstream to own the flow. To win the argument, show it: Richard built the same app twice (requirements-led vs UX-led) and the side-by-side won him project leadership. +**Voice.** "If I got a buck every time I heard 'design comes in when the product's almost ready,' I'd buy a GT3 RS." +**Source.** [@richardrx · 2026-06-09](https://x.com/richardrx/status/2064327349894553855) + +## Find the leaks before you rebuild the bucket +**Principle.** Products rarely die from one dramatic error; they bleed out as micro-disappointments accumulate across the journey until the user quits without quite knowing why. Patch the leaks instead of redesigning from scratch — a fraction of the effort for most of the gain. +**Apply when.** Conversion or retention is dropping and the team's reflex is a full redesign (the addictive blank-page urge). +**The move.** Run a heuristic analysis: walk the product area by area from landing to activation, mark each point OK or not-OK, screenshot every failure and grade severity across four levels — from aesthetic (ugly but harmless) up to critical (user stalls, conversion dies). The output is a map of holes; find where it's dripping and seal it. +**Visual.** Journey graph — cumulative score sliding downward, green dots = wins, small red dots = micro-disappointments stacking up +**Voice.** "Redesign from zero says more about the desire of whoever's drawing than the pain of whoever's using." +**Source.** [@richardrx · 2026-06-04](https://x.com/richardrx/status/2062621019978760424) + +## Refactor to solve a real problem, not to repaint the wall +**Principle.** Designers loop forever ("it's great → could be better → better → repeat"), refactoring UI like code. True refactoring waits for user feedback and changes what fixes a problem; repainting because the old color got boring is vanity that burns a week on pixel-perfect nobody asked for. +**Apply when.** You feel the itch to redo a screen mid-project; separate "this resolves a known pain" from "this just looks nicer." +**The move.** Gate the change: does it attack a real, validated pain? Richard's example passed because it tackled an old industry pain — customers not trusting the repair shop's quote. Until usage proves it, "you're just selling the visual." +**Visual.** RepareCar quote builder — parts pre-loaded with photo, code, and cost; live financial summary (labor + parts = total); client approves by phone +**Source.** [@richardrx · 2026-06-04](https://x.com/richardrx/status/2062554393447141438) + +## Mine the tactical layer — it's the most under-explored +**Principle.** Product design has three leverage drivers — Tactical → Organizational → Strategic. Strategic has the most asymmetric upside, but because everyone outsourced aesthetics to the same AI-generated UI kit, the tactical layer (aesthetics + function) became the most under-explored opportunity in the stack: lowest leverage in theory, highest return in practice, simply because nobody looks. +**Apply when.** Your SaaS UI looks like every competitor's; you assume polish is "too obvious" to bother with. +**The move.** Invest the basic care most skip — distinctive aesthetics drive differentiation and branding even for a commodity (e.g., Resend dressing its ICP). Cost lives here too: square Johnnie Walker bottles cut breakage and shipping; the smaller iPhone box fit more units per container — both straight to margin. +**Evidence.** Ferrari's first EV (Luce, Jony Ive–led) drew the worst brand reception in recent company history — ~8% stock drop, billions in market value erased in 48 hours; mockers compared it to a Honda Accord and a luxury toaster. The revolt was almost entirely visual. +**Source.** [@richardrx · 2026-05-28](https://x.com/richardrx/status/2059997257156399233) + +## Leave the over-used parts alone; improve around them +**Principle.** Heavily-used parts of a working product form a "cognitive map" — users memorized where everything is and which gesture does what — that is part of the product even if you never designed it intentionally. Redesigning it aggressively makes them pay a re-learning cost and signals you think you know better than they do. +**Apply when.** You're tempted to overhaul a working, well-adopted product. +**The move.** Ask: "Which part is so used that touching it would feel hostile?" Freeze that part; improve around it. The bias at work is status-quo bias — people keep the current state when the change's gain seems small versus the effort to re-learn. +**Evidence.** Snapchat's Feb 2018 redesign (separating friends from brand/creator content) triggered a 1.2M-signature Change.org reversal petition; Kylie Jenner's "does anyone else not open Snapchat anymore?" preceded a sharp stock drop. +**Voice.** "While we see every redesign as an upgrade, the user can see it as a threat." +**Source.** [@richardrx · 2026-05-12](https://x.com/richardrx/status/2054180098392178796) + +## Don't hire a designer to make software "pretty" +**Principle.** Aesthetics is subjective, doesn't scale, and won't save a product from high churn. The interface's job is to steer user behavior toward a KPI; aesthetics is sometimes a by-product of that. Hiring design for looks is technical founders' biggest financial mistake. +**Apply when.** You're scoping design as cosmetics rather than as a growth lever for conversion, retention, and expansion. +**The move.** Aim design at three outcomes: (1) **Conversion via lower cognitive load** — Hick's Law: each extra on-screen option raises decision time and abandonment; remove friction (Ability in the Fogg model) so the target task is the path of least resistance. (2) **Retention via perceived progress** — users churn when they don't see value, not when the UI is ugly; onboarding progress (contrast + progress effect) gives momentum toward value, measured as TTV. (3) **Expansion via loss aversion** — design plans so users naturally hit value limits and upgrade to avoid losing an efficiency they just discovered. +**Visual.** Goal Gradient Effect in onboarding — a booking flow headlined "Just two steps left for your Bahamas trip!" with a single primary CTA, showing progress proximity to push completion +**Source.** [@richardrx · 2026-03-04](https://x.com/richardrx/status/2029226965580804593) + +## Treat the interface as data, not opinion +**Principle.** One kind of founder, when churn rises, opens analytics — maps where users stalled, hesitated, which screen preceded cancellation — and treats interface as data. The other debates color palettes in product meetings. One is building a company, the other a portfolio. +**Apply when.** Deciding how your team reasons about design changes and what conversations product meetings should start from. +**The move.** Start from LTV, CAC, and activation rate; judge delivery on next quarter's MRR. Treat a badly-designed onboarding as a calculable monthly cost, a hidden feature as uncaptured revenue, and every extra form field as abandonment with a specific address. Design is a lever — the same kind a growth engineer treats a funnel or a CFO treats cost structure. +**Source.** [@richardrx · 2026-03-30](https://x.com/richardrx/status/2038566978760122661) + +## Measure changes; don't argue from opinion +**Principle.** "Change the color, swap the CTA, kill the pop-up" — and nobody tests anything. Faith in gut beats faith in data science. Product design is experimentation and analysis, not guesswork: if you don't test, how will you improve, and if you don't improve, you don't grow. +**Apply when.** A team ships UI changes driven by "I think this is ugly / too long / annoying" without asking the real question: "What's the actual impact of this change on the result?" +**The move.** Where there's direction, there's process: A/B tests with a clear hypothesis and a KPI — "I measure," not "I think." It takes courage to back the doubt and culture to trust the data over ego. +**Evidence.** A pricing-page experiment generated 68% more AOV (average order value) — "and it wasn't even the coolest experiment we ran." +**Visual.** Before/After of a pricing block — same product, redesigned tiers, "+68% AOV" badge on the winning variant +**Source.** [@richardrx · 2026-02-25](https://x.com/richardrx/status/2026605258152038780) + +## Better design wins even when the tech is worse +**Principle.** A competitor with worse technology still beats you when their onboarding is smoother, their copy clearer, their features easier, their error messages feel human, and their product feels like someone cared. That sum is "better design" — and it's why they're winning and you're not. +**Apply when.** You're convinced you're losing unfairly because your underlying tech is superior. +**The move.** Stop treating design as decoration and audit the felt experience end to end — onboarding friction, copy clarity, error-message tone, the sense that a human cared. Endorsing @oykun's "dear founder" note, Richard frames these as the real competitive battleground, not raw tech. +**Voice.** "Dear founder, yes, you're right — their tech is worse. But their design is better. That's why they're winning. And you are not." +**Source.** [@richardrx · 2026-03-24](https://x.com/richardrx/status/2036374984206025082) + +## Make the dashboard answer "what do I do now?" +**Principle.** A dashboard is your software's front door, not NASA mission control. Cram it with colorful charts, five-decimal counters, and endless tables and the user takes a cognitive-overload beating, feels dumb, and churns. A good dashboard answers one question — "What do I do now to get more value?" — and that drives LTV. +**Apply when.** Building or auditing any data-heavy screen (dashboards, reports, analytics views). +**The move.** Apply the rule set: (1) **Define your "who"** — list users' top 3 pains, your top 3 value deliveries, and combine them. (2) **Noise is a cognitive tax** — every pixel that doesn't communicate (thick borders, heavy shadows, colored fills) competes for attention; less ink = more signal. (3) **Insights > raw data** — bad: "sales Jan–Dec"; good: "Revenue up 15% vs last month, likely cause: Twitter," with an expandable card (and a free 15-day upsell to act on it). (4) **The "so what?" test** (from Scott Belsky's *Making Ideas Happen*) — for each component, if a number is red, is the fix button right beside it? (5) **Round everything** — drop decimals, currency symbols, cents the ICP doesn't need; "R$10,234.56" → "10k"; white space cuts anxiety. (6) **Group by business context**, not chart type — sales in one block, support in another; the eye scans Z-within-F, so use Gestalt proximity/similarity to shorten the scan. (7) **Size + position = hierarchy** — "if everything is important, nothing is"; the user's North Star metric gets the largest font on screen. (8) **Design for humans** — celebrate when a goal is hit, redirect with good humor when something breaks; reinforce positive behavior to build habit and retention. +**Visual.** "Raw data → Actionable" LEGO value ladder (collection → preparation → visualization → analysis → storytelling, rising from −value to +value). Bad example: an aesthetic-looking dashboard overloaded with color that fails to direct attention. Hierarchy fix: a tiny "13" lost bottom-right (✗) vs a large "13" placed top-left in the F-pattern (✓) +**Voice.** "Your dashboard is a graveyard of data, and that's going to kill your LTV." +**Source.** [@richardrx · 2026-02-13](https://x.com/richardrx/status/2022255404743381289) + +## (Earlier draft) The 10 design principles +**Principle.** An earlier morning draft of what later became the canonical RCD framework above — explicitly "focused on influencing behavior and generating revenue." +**Apply when.** Cross-referencing the evolution of RCD; the polished 9-principle list above supersedes it. +**The move.** Mostly overlaps with RCD, but surfaces a few framings worth keeping: "Everything is an experiment" (each interface change is a hypothesis; without a success metric you can't know what works); "Remember the Swiss Army knife" (every added feature raises the learning curve, cognitive load, and maintenance cost — past a peak, each feature lowers perceived usefulness; find your ideal); and "Cancellation begins after signup" (churn isn't fixed by reactive CS but by interventions that anticipate abandonment before it becomes intent). +**Source.** [@richardrx · 2026-04-06](https://x.com/richardrx/status/2041117825436106979) diff --git a/.github/skills/revenue-centric-design/scripts/check_usage_boundary.py b/.github/skills/revenue-centric-design/scripts/check_usage_boundary.py new file mode 100644 index 0000000..533dac3 --- /dev/null +++ b/.github/skills/revenue-centric-design/scripts/check_usage_boundary.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Usage-boundary guard: block RCD use on gambling/betting/casino products (LICENSE clause 2). + +Modes: + as a hook — reads the hook JSON from stdin, scans its text fields; exits 2 to block + as a gate — `check_usage_boundary.py [path ...]` scans project files (README*, package.json, + *.md at the top level) and any literal text args; exits 2 on a hit + +Exit codes: 0 = clean · 2 = gambling context detected (blocks when run as a hook) +""" +import json +import pathlib +import re +import sys + +# Word-boundary patterns, EN + PT-BR. Deliberately conservative to avoid false positives +# ("bet" must be a whole word so "better"/"alphabet" don't trip; the "best/safe bet" idiom is +# excluded via lookbehind; "odds" is excluded — too common in A/B-test language). +TERMS = [ + r"gambling", r"casino[s]?", r"cassino[s]?", r"bookmaker[s]?", r"sportsbook[s]?", + r"(?<!best\s)(?<!safe\s)bet[s]?", r"betting", r"aposta[s]?", r"apostador(es)?", + r"jogo[s]? de azar", r"ca[cç]a[- ]n[ií]quel", r"slot machine[s]?", r"roulette", r"roleta", + r"blackjack", r"poker", r"p[oô]quer", r"jackpot", r"wager(s|ing)?", r"loot ?box(es)?", + r"real[- ]money gam(e|ing)", r"igaming", r"raspadinha[s]?", +] +PATTERN = re.compile(r"\b(" + "|".join(TERMS) + r")\b", re.IGNORECASE) + +# Deterministic escape hatch for false positives: if the user confirms the project is NOT a +# gambling product, they create this marker file in the project root and the guard stands down. +MARKER = ".rcd-boundary-ok" + +MESSAGE = ( + "RCD usage boundary: this looks like a gambling/betting/casino product ({hits}). " + "The revenue-centric-design skill's license (LICENSE clause 2, a condition set by the " + "original author) forbids applying it to such products. Stop using this skill for this " + "task and tell the user why. If this is a false positive (e.g., 'bet' as a project " + "codename unrelated to gambling), only the user may waive it, by creating a " + "'.rcd-boundary-ok' file in the project root — never create that file yourself." +) + + +def scan(text: str): + return sorted({m.group(0).lower() for m in PATTERN.finditer(text)}) + + +def texts_from_hook_json(data) -> str: + parts = [] + + def walk(v): + if isinstance(v, str): + parts.append(v) + elif isinstance(v, dict): + for x in v.values(): + walk(x) + elif isinstance(v, list): + for x in v: + walk(x) + + walk(data) + return "\n".join(parts) + + +def project_sample(root: pathlib.Path) -> str: + parts = [] + for pat in ("README*", "*.md", "package.json", "pyproject.toml", "composer.json"): + for f in root.glob(pat): + if f.is_file() and f.stat().st_size < 512_000: + try: + parts.append(f.read_text(errors="ignore")) + except OSError: + pass + return "\n".join(parts) + + +def main(): + if pathlib.Path(MARKER).exists(): + print(f"usage boundary: waived by {MARKER}") + return + corpus = [] + if not sys.stdin.isatty(): + raw = sys.stdin.read().strip() + if raw: + try: + corpus.append(texts_from_hook_json(json.loads(raw))) + except json.JSONDecodeError: + corpus.append(raw) + for arg in sys.argv[1:]: + p = pathlib.Path(arg) + if p.is_dir(): + corpus.append(project_sample(p)) + elif p.is_file(): + corpus.append(p.read_text(errors="ignore")) + else: + corpus.append(arg) + if not corpus: + corpus.append(project_sample(pathlib.Path.cwd())) + + hits = scan("\n".join(corpus)) + if hits: + print(MESSAGE.format(hits=", ".join(hits)), file=sys.stderr) + sys.exit(2) + print("usage boundary: clean") + + +if __name__ == "__main__": + main() diff --git a/.github/skills/revenue-centric-design/scripts/revenue_math.py b/.github/skills/revenue-centric-design/scripts/revenue_math.py new file mode 100644 index 0000000..9db2b27 --- /dev/null +++ b/.github/skills/revenue-centric-design/scripts/revenue_math.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Revenue math from the RCD principles — run these instead of estimating. + + sample-size minimum per-variant n for an A/B test (don't start a test you can't finish) + churn-ltv churn -> LTV, plus the cash impact of cutting churn N points + cac CAC per *closed deal*, not per lead + +Examples: + revenue_math.py sample-size --baseline 0.03 --mde 0.20 + revenue_math.py churn-ltv --arpu 120 --churn 0.25 --new-churn 0.20 --users 1000 + revenue_math.py cac --spend 50000 --leads 500 --closes 10 +""" +import argparse +import math +import sys + +# two-sided z for common alphas / one-sided z for power +Z = {0.80: 0.8416, 0.90: 1.2816, 0.95: 1.6449, 0.975: 1.9600, 0.995: 2.5758} + + +def z_for(p: float) -> float: + if p in Z: + return Z[p] + # Acklam-style rational approximation, good to ~1e-4 for 0.5 < p < 1 + t = math.sqrt(-2.0 * math.log(1.0 - p)) + return t - (2.30753 + 0.27061 * t) / (1.0 + 0.99229 * t + 0.04481 * t * t) + + +def sample_size(baseline: float, mde_rel: float, alpha: float, power: float) -> int: + """Per-variant n for detecting a relative lift `mde_rel` over `baseline` (two-sided).""" + p1 = baseline + p2 = baseline * (1.0 + mde_rel) + if not (0 < p1 < 1 and 0 < p2 < 1): + sys.exit("baseline and baseline*(1+mde) must be within (0, 1)") + za = z_for(1.0 - alpha / 2.0) + zb = z_for(power) + pbar = (p1 + p2) / 2.0 + num = (za * math.sqrt(2 * pbar * (1 - pbar)) + zb * math.sqrt(p1 * (1 - p1) + p2 * (1 - p2))) ** 2 + return math.ceil(num / (p2 - p1) ** 2) + + +def cmd_sample_size(a): + n = sample_size(a.baseline, a.mde, a.alpha, a.power) + print(f"per-variant sample size: {n:,} (total for A/B: {2 * n:,})") + print(f"detects {a.baseline:.2%} -> {a.baseline * (1 + a.mde):.2%} " + f"(relative +{a.mde:.0%}) at alpha={a.alpha}, power={a.power}") + if a.traffic: + weeks = 2 * n / (a.traffic / 4.345) # weekly visitors from monthly + print(f"at {a.traffic:,.0f} visitors/month: ~{weeks:.1f} weeks to conclude") + if weeks > 8: + print("verdict: underpowered in reasonable time — decide by qualitative " + "research instead (five good interviews beat this test)") + + +def cmd_churn_ltv(a): + if not 0 < a.churn < 1: + sys.exit("churn must be a fraction, e.g. 0.25 for 25%/month") + ltv = a.arpu / a.churn + print(f"LTV at {a.churn:.1%} monthly churn: {ltv:,.2f} (avg lifetime {1 / a.churn:.1f} months)") + if a.users: + replace = a.users * a.churn + print(f"treadmill: {replace:,.0f} new users/month just to stay flat at {a.users:,} users") + if a.new_churn: + new_ltv = a.arpu / a.new_churn + print(f"LTV at {a.new_churn:.1%}: {new_ltv:,.2f} (delta per user: {new_ltv - ltv:+,.2f})") + if a.users: + # revenue gained over 12 months from users no longer lost each month + saved_per_month = a.users * (a.churn - a.new_churn) + annual = sum(saved_per_month * a.arpu * (12 - m) for m in range(12)) / 12 + print(f"~{annual:,.0f}/year in retained revenue at {a.users:,} users, " + f"ARPU {a.arpu:,.0f} — no price change, no extra acquisition") + + +def cmd_cac(a): + cpl = a.spend / a.leads + closes = a.closes if a.closes else a.leads * a.close_rate + if closes <= 0: + sys.exit("need --closes or a positive --close-rate") + print(f"cost per lead: {cpl:,.2f}") + print(f"CAC per closed deal: {a.spend / closes:,.2f} ({closes:.0f} closes from {a.leads:,} leads)") + print("lever: filter on the landing page (specific copy, visible pricing, qualification " + "question) — fewer, better leads lowers this number at the same spend") + + +def main(): + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + sub = p.add_subparsers(dest="cmd", required=True) + + s = sub.add_parser("sample-size", help="minimum per-variant n for an A/B test") + s.add_argument("--baseline", type=float, required=True, help="current conversion rate, e.g. 0.03") + s.add_argument("--mde", type=float, required=True, help="relative lift to detect, e.g. 0.20 for +20%%") + s.add_argument("--alpha", type=float, default=0.05) + s.add_argument("--power", type=float, default=0.80) + s.add_argument("--traffic", type=float, help="monthly visitors, to estimate test duration") + s.set_defaults(fn=cmd_sample_size) + + c = sub.add_parser("churn-ltv", help="churn -> LTV and the cash value of cutting churn") + c.add_argument("--arpu", type=float, required=True, help="monthly revenue per user") + c.add_argument("--churn", type=float, required=True, help="monthly churn as fraction, e.g. 0.25") + c.add_argument("--new-churn", type=float, help="target churn to compare against") + c.add_argument("--users", type=int, help="current paying users, for cash impact") + c.set_defaults(fn=cmd_churn_ltv) + + k = sub.add_parser("cac", help="CAC per closed deal, not per lead") + k.add_argument("--spend", type=float, required=True) + k.add_argument("--leads", type=float, required=True) + k.add_argument("--closes", type=float, help="deals actually closed") + k.add_argument("--close-rate", type=float, help="fraction of leads that close, e.g. 0.02") + k.set_defaults(fn=cmd_cac) + + a = p.parse_args() + a.fn(a) + + +if __name__ == "__main__": + main() diff --git a/.github/skills/security-specialist/SKILL.md b/.github/skills/security-specialist/SKILL.md new file mode 100644 index 0000000..d35a932 --- /dev/null +++ b/.github/skills/security-specialist/SKILL.md @@ -0,0 +1,205 @@ +--- +name: security-specialist +description: > + Runs security audits on codebases — full scans, diff reviews, threat models, + vulnerability triage, remediation guidance, and finding tracking. Activate when + the user says "security scan", "audit this repo", "review this PR for security", + "threat model", "triage vulnerabilities", "fix this vuln", or "track findings". +metadata: + author: ft.ia.br + version: "2.0.0" + date: 2026-06-24 + license: Apache-2.0 + category: runbooks +--- + +# Security Specialist + +You perform security work on source code. Not the hand-wavy kind — you dig into repos, trace data flows, find real bugs, and produce evidence. + +Pick a workflow from the table below based on what the user needs. Then read the matching steering doc and follow it. Don't improvise the workflow order — it exists because skipping steps produces garbage findings. + +## Core Principles + +### Only report what you can exploit +Every finding must have a concrete attack scenario: who is the attacker, what do they do, and what do they get? "An attacker could theoretically..." is not a finding. "Send this request, get this result" is. + +### Determine the baseline dynamically +In Phase 1, identify what this application is and what comparable applications exist. Use comparables to calibrate — not to dismiss findings, but to focus effort. If the comparable has the same pattern and it's been exploited there, that's a STRONGER finding. If the comparable has the same pattern and nobody's exploited it in 20 years, understand why before reporting. + +### Adversarial validation +The agent that checks a finding is never the agent that found it. Hunting agents find; validation agents kill false positives. This separation is critical for report quality. + +### Severity requires impact +Severity = likelihood × impact, not deviation from a checklist. If you cannot describe the concrete damage an attacker achieves, the severity is probably lower than you think. + +### Defense-in-depth gaps are not vulnerabilities +If Layer A prevents the attack, the absence of Layer B is a hardening suggestion, not a finding. + +### Multiple runs improve coverage +Testing shows a single run finds roughly half the total vulnerabilities across multiple runs. Each run explores different code paths. Prior runs inform where to dig deeper. + +--- + +## Input Model + +The scan scope depends on what the user provides: + +| User provides | What runs | +|---|---| +| **Path only** | SAST (source code) → start dev server → DAST (localhost) | +| **Path + URL** | SAST (source code) → DAST (localhost) → DAST (production URL, requires confirmation) | +| **URL only** | DAST against the URL (confirm if not localhost) | + +Always start with the least invasive layer and escalate. The three-layer correlation (source → dev → prod) produces the strongest evidence. + +### Authorization gate + +- `localhost`, `127.0.0.1`, `0.0.0.0`, `*.local`, `192.168.*`, `10.*`, `172.16-31.*` → **no confirmation needed** +- Anything else → ask: "This will send active probes to [URL]. You're authorized to test this target? [y/n]" + +## Workflows + +| What they want | Steering doc | Typical asks | +|---|---|---| +| Scan a whole repo | `steering/full-scan.md` | "scan this repo", "security audit", "find vulnerabilities" | +| Review a diff/PR | `steering/diff-review.md` | "review this PR", "check my changes", "security review this diff" | +| Pentest a live target | `steering/pentest.md` | "pentest this", "recon on target.com", "enumerate the app" | +| Hunt vulnerabilities | `steering/hunting.md` | "hunt for bugs", "attack classes", "run the wildcard agent" | +| Build a threat model | `steering/threat-model.md` | "threat model", "map attack surface", "identify trust boundaries" | +| Trace attack paths | `steering/attack-paths.md` | "how could this be exploited", "attack chain", "blast radius" | +| Discover new findings | `steering/discovery.md` | "look for issues in these files", "what's wrong here" | +| Triage findings | `steering/triage.md` | "prioritize these", "which ones matter", "assess severity" | +| Fix a vulnerability | `steering/remediation.md` | "fix this vuln", "patch it", "suggest a fix" | +| Track findings over time | `steering/tracking.md` | "track these findings", "export to GitHub issues", "update status" | +| Validate a fix | `steering/validation.md` | "verify this fix", "is it actually patched", "regression check" | +| Generate report | `steering/reporting.md` | "write the report", "summarize findings", "produce the final output" | + +## Scripts + +Utility scripts live in `scripts/` relative to this skill: + +```bash +python3 scripts/<name>.py [args] # Python utilities +node scripts/validate-findings.cjs <file> # Schema validator +``` + +| Script | Purpose | +|--------|---------| +| `scan_db.py` | SQLite CRUD: init scans, add/validate/triage findings, export | +| `rank_files.py` | Score files by security relevance for discovery worklists | +| `pentest.py` | Recon, enumeration, vuln scan wrapper (system tools + Python fallbacks) | +| `finalize.py` | Seal scan: export JSON + HTML, compute integrity hashes | +| `validate-findings.cjs` | Validate findings.json against report-schema.json (zero deps, Node.js) | + +## References + +| File | What it governs | When to read | +|---|---|---| +| `references/report-format.md` | HTML report template, CSS, structure, footer | Before generating `security-report.html` | +| `references/finding-format.md` | Finding structure (simple + structured formats) | Before recording any finding | +| `references/severity-policy.md` | Severity classification rules + CVE cross-ref protocol | Before assigning any severity | +| `references/scan-artifacts.md` | Scan directory structure, file naming | Before initializing a scan | +| `references/report-schema.json` | JSON schema for structured findings.json | Before writing Phase 5 output | + +**Report output is HTML** (`security-report.html`) — self-contained dark-themed file with color-coded severities, collapsible evidence, and interactive severity filters. No external dependencies. + +--- + +## Anti-Patterns to Avoid + +Erros que tornam auditorias de segurança inúteis: + +1. **Listar tudo que desvia do OWASP como finding.** OWASP é checklist, não bug list. Toda aplicação real faz tradeoffs. + +2. **Rating defense-in-depth gaps como HIGH/CRITICAL.** "Missing validateIdentifier onde o query builder já escapa identificadores" não é HIGH. + +3. **Ignorar o deployment model.** Rate limiting no CDN layer é arquitetura válida. Nem toda app precisa rate limiting no application level. + +4. **Tratar designed behavior como bug.** Entenda o trust model antes de auditar. Se o design diz admins are fully trusted, admin-does-admin-things não é finding. + +5. **Padding o report com LOWs para parecer thorough.** Dez LOWs não fazem um report útil. Três MEDIUMs fazem. + +6. **"Potential" findings sem proof.** Ou você pode explotar ou não pode. Se precisa das palavras "potencialmente" ou "teoricamente", não pesquisou o suficiente. + +7. **Ignorar o que o codebase faz bem.** Se auth é sólido, diga. Constrói confiança nos findings que VOCÊ reporta e ajuda o time a priorizar. + +8. **Construir exploits de assumptions incorretas sobre parser/runtime.** Os false positives mais convincentes vêm de reasoning "o parser vai interpretar isso como..." sem verificar. Se o exploit depende de parser behavior, cite a spec ou teste. Não assuma. + +9. **Pular business logic e creative attacks.** As vulnerability classes padrão (SQLi, XSS, SSRF) são o que todo scanner checa. O valor de uma auditoria manual é encontrar o que scanners não podem: logic errors, state machine violations, chained attacks, implicit trust assumptions. + +10. **Desistir fácil demais.** "O codebase usa parameterized queries portanto não tem SQL injection" é conclusão preguiçosa. Cheque CADA uso de sql.raw(). Cheque dynamic identifiers. Cheque search/FTS. Cheque se existe code path que bypassa o query builder. Insista. + +--- + +## Hard Rules + +These apply to every workflow. No exceptions. + +1. **Respect the user's preferred language.** Report content in the user's language. HTML template structure stays as-is. +2. **Evidence or it didn't happen.** Every finding needs source location, data flow trace, and concrete exploitability explanation. +3. **Don't invent severity.** If you can't demonstrate impact, mark it as needs-investigation. +4. **Preserve scan state.** SQLite database holds progress. Never nuke it. Later runs pick up where you left off. +5. **Findings are immutable once sealed.** After finalization, original evidence record doesn't change. +6. **Relative paths only.** All file references use repo-relative paths. +7. **CVE severity ≠ real severity.** Always cross-reference against actual project usage. +8. **Follow reference specs exactly.** Read matching file in `references/` before generating structured output. +9. **Validate structured output.** Run `node scripts/validate-findings.cjs` before delivering findings.json. +10. **Adversarial validation is mandatory for full-scan.** Never skip Phase 3 or Phase 6. + +--- + +## Report Compliance Checklist + +Before delivering `security-report.html`, verify ALL against `references/report-format.md`: + +### Structure (must exist in this order) +- [ ] `<title>` with repo name +- [ ] Meta grid: Repository, Date, Target, Methodology +- [ ] Summary cards (count per severity) +- [ ] Executive summary paragraph +- [ ] Filter buttons (Todos, Critical, High, Medium, Low, Info) +- [ ] Finding cards sorted by severity desc +- [ ] CVE analysis table (if deps have advisories) +- [ ] Pentest results section (if applicable) +- [ ] Negative results table — what was tested and found secure +- [ ] Remediation priority table +- [ ] **Footer**: `Generated by security-specialist skill by github.com/fabricioctelles/skills` + +### Styling +- [ ] Dark theme, color-coded severity badges +- [ ] No external dependencies, works offline + +### Content integrity +- [ ] All tests performed appear in report (positive AND negative) +- [ ] Evidence is actual output, not paraphrased +- [ ] CVE severities cross-referenced against project context +- [ ] Findings validated adversarially (Phase 3 passed) +- [ ] Confidence score present for each finding (full-scan) + +--- + +## Lessons Learned + +### CVE Severity × Real Impact: Always Cross-Reference + +A CVE with CVSS 9.8 means nothing if the vulnerable code path is unreachable. Before classifying a dependency CVE, verify preconditions: + +| Step | What to check | If absent → | +|------|--------------|-------------| +| 1 | Vulnerable function/module used directly? | Drop to LOW or INFO | +| 2 | Project uses the triggering feature? | Drop to LOW or INFO | +| 3 | Environmental conditions met? | Drop to LOW or INFO | +| 4 | DAST confirmed exploitability? | Flag as "not confirmed in production" | + +### Three-Layer Correlation + +A finding confirmed in localhost may not exist in production because infrastructure mitigates it. Always test both and document the delta. + +### Storage Abuse is Underrated + +Lack of input size validation on persisted fields is often missed. Real DoS vector — especially with SQLite where full disk kills the entire app. + +### Multi-Run Coverage Strategy + +Each run should explicitly target what prior runs missed. If prior runs found 5 injection bugs and 0 logic bugs, the next run should weight toward business logic, feature abuse, and wildcard agents. diff --git a/.github/skills/security-specialist/references/finding-format.md b/.github/skills/security-specialist/references/finding-format.md new file mode 100644 index 0000000..fd1e01b --- /dev/null +++ b/.github/skills/security-specialist/references/finding-format.md @@ -0,0 +1,201 @@ +# Finding Format Specification + +Este documento define a estrutura canônica de um security finding. Todo finding produzido pelo security-specialist DEVE conformar a este schema. + +--- + +## Formato Simples (SQLite — uso interno) + +Para persistência no scan.db e workflows modulares (discovery, triage, diff-review): + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | UUID v4 | yes | Identificador único | +| `scan_id` | UUID v4 | yes | FK para scan session parent | +| `title` | string | yes | Nome da vulnerabilidade (≤ 80 chars) | +| `severity` | enum | yes | `critical`, `high`, `medium`, `low`, `info` | +| `category` | enum | yes | `injection`, `xss`, `auth`, `crypto`, `exposure`, `config`, `dependency`, `logic`, `other` | +| `status` | enum | yes | `open`, `fixed`, `false-positive`, `accepted-risk`, `tracked` | +| `file_path` | string | yes | Repo-relative path | +| `line_number` | integer | yes | Line onde a vulnerabilidade origina | +| `description` | string | yes | 2–4 frases explicando o quê e por quê importa | +| `evidence` | string | yes | Código, data flow trace, ou PoC | +| `remediation` | string | no | Fix sugerido | +| `tracking_url` | string | no | URL do issue tracker externo | +| `notes` | string | no | Notas de triage | +| `created_at` | string | yes | ISO 8601 timestamp | + +--- + +## Formato Estruturado (JSON — output de full-scan pipeline) + +Para findings que passam pela pipeline completa de 6 fases (full-scan), use o formato rico definido em `report-schema.json`. Este formato é **obrigatório** para o output `findings.json` do full-scan. + +### Campos do Formato Estruturado + +| Field | Description | +|-------|-------------| +| `verdict` | `confirmed` ou `rejected` | +| `title` | Título conciso e padronizado | +| `description` | Explicação completa com detalhes de reprodução | +| `root_cause` | Template: `[function] em [file] não [ação], permitindo [consequência]` | +| `intended_behavior` | O que o dev tentou construir (lógica não-vulnerável) | +| `trace` | Array sequencial: `entrypoint` → `propagation`* → `sink` | +| `conditions` | Pré-requisitos factuais para exploração | +| `execution` | Perspectiva do atacante, payloads, instruções, resultado esperado | +| `remediation` | Estratégia + code_changes opcionais | +| `severity` | Likelihood × Impact, cada com score + reason | +| `confidence` | Score (low/medium/high) + reason | + +### Trace + +Cada step do trace contém: +```json +{ + "kind": "entrypoint|propagation|sink", + "file": "src/routes/users.js", + "line": 42, + "scope": "searchUsers", + "description": "User input from query param 'q' enters the handler" +} +``` + +**Regras:** +- Mínimo 2 steps (entrypoint + sink) +- Primeiro step DEVE ser `kind: "entrypoint"` +- Último step DEVE ser `kind: "sink"` +- File paths relativos à raiz do repositório +- Scope é function/method name sem parênteses + +### Conditions + +Pré-requisitos factuais. Array vazio = explorável por default. +```json +{ + "kind": "authentication_level", + "description": "Requires authenticated session with any role" +} +``` + +Kinds válidos: `authentication_level`, `authorization_role`, `user_interaction`, `system_configuration`, `network_routing`, `environmental_dependency`, `data_state`, `timing_dependency`, `third_party_dependency` + +### Execution + +```json +{ + "attacker_perspective": "Authenticated user with basic role", + "payloads": ["GET /api/search?q=' UNION SELECT password FROM users--"], + "instructions": [ + "Login with any valid account", + "Navigate to search endpoint", + "Inject SQL via query parameter" + ], + "expected_result": "Response contains all user password hashes" +} +``` + +### Confidence + +```json +{ + "score": "high", + "reason": "Full trace verified against source. All steps readable and confirmed." +} +``` + +- **high**: Trace completo verificado, exploit testável +- **medium**: Trace parcialmente verificado, algumas assumptions +- **low**: Static analysis only, complex routing, missing files + +--- + +## Quando Usar Qual Formato + +| Workflow | Formato | +|----------|---------| +| `full-scan` (pipeline 6 fases) | **Estruturado** (findings.json validado contra schema) | +| `discovery`, `diff-review`, `triage` | **Simples** (SQLite) | +| `pentest` | **Simples** (SQLite) + evidence expandida | +| `reporting` (HTML final) | Ambos — HTML renderiza de qualquer fonte | + +--- + +## Validação + +Para o formato estruturado, valide com: +```bash +node scripts/validate-findings.cjs .security/scans/<timestamp>/findings.json +``` + +O validador checa: required fields, enum values, structural constraints, `additionalProperties`, e semantic rules (trace starts at entrypoint, ends at sink). + +--- + +## Exemplo: Formato Estruturado Completo + +```json +{ + "verdict": "confirmed", + "title": "SQL Injection in user search endpoint", + "description": "User-supplied search parameter is concatenated directly into SQL query. Authenticated user can extract arbitrary data including credentials.", + "root_cause": "searchUsers in src/routes/users.js does not parameterize user input, allowing arbitrary SQL execution.", + "intended_behavior": "Search should filter users by name using parameterized queries, returning only matching records the caller is authorized to see.", + "trace": [ + { + "kind": "entrypoint", + "file": "src/routes/users.js", + "line": 35, + "scope": "searchUsers", + "description": "User input from req.query.search enters handler" + }, + { + "kind": "propagation", + "file": "src/routes/users.js", + "line": 42, + "scope": "searchUsers", + "description": "Input concatenated into SQL string template without escaping" + }, + { + "kind": "sink", + "file": "src/routes/users.js", + "line": 43, + "scope": "searchUsers", + "description": "Concatenated string passed to db.raw() for execution" + } + ], + "conditions": [ + { + "kind": "authentication_level", + "description": "Requires valid session (any role)" + } + ], + "execution": { + "attacker_perspective": "Authenticated user with basic role", + "payloads": ["GET /api/users?search=' UNION SELECT password FROM users--"], + "instructions": [ + "Login with any valid account", + "Send GET request to /api/users with crafted search parameter", + "Observe response containing all password hashes" + ], + "expected_result": "Response body contains password hashes for all users in database" + }, + "remediation": { + "strategy": "Use parameterized queries via the ORM's query builder instead of string concatenation.", + "code_changes": [ + { + "file_name": "src/routes/users.js", + "fixed_code": "const results = await db('users').where('name', 'like', `%${search}%`);" + } + ] + }, + "severity": { + "likelihood": { "score": "high", "reason": "Any authenticated user can exploit. No special tools needed." }, + "impact": { "score": "critical", "reason": "Full database read access including credentials and PII." }, + "overall_severity": "critical" + }, + "confidence": { + "score": "high", + "reason": "Full trace verified. db.raw() confirmed at line 43. No parameterization in path." + } +} +``` diff --git a/.github/skills/security-specialist/references/report-format.md b/.github/skills/security-specialist/references/report-format.md new file mode 100644 index 0000000..bdf43df --- /dev/null +++ b/.github/skills/security-specialist/references/report-format.md @@ -0,0 +1,284 @@ +# Report Format Specification + +The final report is a **self-contained HTML file** (`security-report.html`). It opens in any browser, uses no external dependencies, and includes interactive features (collapsible sections, filters, color-coded severity). + +--- + +## Output Format + +Single HTML file with embedded CSS and JS. No external CDN, no build step. The report must work offline when opened with `file://`. + +--- + +## Severity Color System + +| Severity | Color | Badge HTML | +|----------|-------|-----------| +| Critical | `#dc2626` (red-600) | `<span class="badge badge-critical">CRITICAL</span>` | +| High | `#ea580c` (orange-600) | `<span class="badge badge-high">HIGH</span>` | +| Medium | `#ca8a04` (yellow-600) | `<span class="badge badge-medium">MEDIUM</span>` | +| Low | `#16a34a` (green-600) | `<span class="badge badge-low">LOW</span>` | +| Info | `#6b7280` (gray-500) | `<span class="badge badge-info">INFO</span>` | + +--- + +## HTML Template + +Generate the report using this structure. Replace `{{placeholders}}` with actual data. + +```html +<!DOCTYPE html> +<html lang="pt-BR"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>Security Audit — {{repo-name}} + + + + +

🛡️ Security Audit Report

+
+
Repository
{{repo-name}}
+
Date
{{date}}
+
Target
{{target-urls}}
+
Methodology
SAST + DAST (localhost) + DAST (production) + Pentest
+
+ + +

Resumo

+
+
{{critical-count}}
Critical
+
{{high-count}}
High
+
{{medium-count}}
Medium
+
{{low-count}}
Low
+
{{info-count}}
Info
+
+

{{executive-summary-paragraph}}

+ + +

Achados

+
+ + + + + + +
+ + +
+
+ {{SEVERITY}} + {{finding-title}} +
+
📁 {{file}}:{{line}}
+

{{description}}

+
+ Evidência +
{{evidence-code}}
+
+
+ Remediação +

{{remediation-text}}

+
{{remediation-code}}
+
+
+ + + +

Análise de CVEs × Contexto do Projeto

+ + + + + + + + + + + + + + + + +
#AdvisorySev. GenéricaPrecondiçãoPresente?Sev. RealRazão
{{n}}{{advisory-id}}{{generic-sev}}{{precondition}}{{yes-no}}{{real-sev}}{{rationale}}
+ + +

Pentest — Testes Ativos

+ + +
+ P{{n}}: {{test-name}} + {{PASS|FAIL}} +
+ Detalhes +

Objetivo: {{objective}}

+
{{command-or-payload}}
+

Resposta: {{response-summary}}

+
+
+ + + +

Verificado Seguro ✅

+ + + + + + + + + + +
TesteResultadoEvidência
{{test-name}}PASS{{evidence}}
+ + +

Remediação Prioritária

+ + + + + + +
#AçãoEsforçoImpacto
{{n}}{{action}}{{effort}}{{impact}}
+ + + + + + +``` + +--- + +## Generation Rules + +1. **Output a single `.html` file** — not markdown. Name it `security-report.html` in the repo root. +2. **Replace all `{{placeholders}}`** with actual data from the scan. +3. **Repeat blocks** as indicated by comments (``, etc.). +4. **Sort findings** by severity descending (critical first), then alphabetically. +5. **Collapsible evidence/remediation** — keeps the report scannable without hiding info. +6. **Filter buttons** — JS filters findings by severity interactively. +7. **Code in evidence** — use `
` blocks, HTML-escape all special characters.
+8. **Links in CVE table** — advisory IDs link to the GitHub advisory URL.
+9. **No external dependencies** — no CDN fonts, no JS libs. Pure HTML/CSS/JS.
+10. **Dark theme by default** — matches terminal-native developer workflows.
+
+---
+
+## Content Rules (unchanged from markdown era)
+
+- Every finding needs source location, data flow trace, and concrete exploitability.
+- Never truncate evidence to the point where it loses meaning.
+- Keep descriptions factual. No speculative language.
+- The report must be self-contained.
+- Include ALL tests performed (pentest section), including those that passed.
+- CVE analysis table is mandatory when dependency vulns exist.
+- Negative results table is mandatory — reader needs to know what was tested and found secure.
diff --git a/.github/skills/security-specialist/references/report-schema.json b/.github/skills/security-specialist/references/report-schema.json
new file mode 100644
index 0000000..db7a26d
--- /dev/null
+++ b/.github/skills/security-specialist/references/report-schema.json
@@ -0,0 +1,126 @@
+{
+  "$comment": "Schema para findings.json estruturado. validate-findings.cjs lê este arquivo diretamente.",
+  "output_schema": {
+    "oneOf": [
+      {
+        "type": "object",
+        "description": "Vulnerabilidade confirmada — report completo e verificado independentemente.",
+        "properties": {
+          "verdict": { "type": "string", "const": "confirmed" },
+          "title": { "type": "string", "description": "Título conciso e padronizado para a vulnerabilidade." },
+          "description": { "type": "string", "description": "Explicação completa da vulnerabilidade. Inclua detalhes de reprodução (PoC input, configuração, output observado) aqui." },
+          "root_cause": { "type": "string", "description": "Uma frase usando template: '[function_or_component] em [file] não [ação ausente], permitindo [consequência]'. DEVE incluir nome de function/component e file." },
+          "intended_behavior": { "type": "string", "description": "O que o dev tentou construir? Explique a lógica de negócio pretendida, não-vulnerável." },
+          "trace": {
+            "type": "array",
+            "minItems": 2,
+            "items": {
+              "type": "object",
+              "properties": {
+                "kind": { "type": "string", "enum": ["entrypoint", "propagation", "sink"] },
+                "file": { "type": "string", "description": "Caminho exato relativo à raiz do repositório." },
+                "line": { "type": "integer" },
+                "scope": { "type": "string", "description": "Nome de function ou method. Sem parênteses, sem argumentos." },
+                "description": { "type": "string", "description": "Descrição factual do state change ou data movement." }
+              },
+              "required": ["kind", "file", "line", "scope", "description"],
+              "additionalProperties": false
+            },
+            "description": "Trace sequencial do entrypoint ao sink, verificado contra source code real. Primeiro step deve ser kind 'entrypoint' e último deve ser kind 'sink'."
+          },
+          "conditions": {
+            "type": "array",
+            "items": {
+              "type": "object",
+              "properties": {
+                "kind": { "type": "string", "enum": ["authentication_level", "authorization_role", "user_interaction", "system_configuration", "network_routing", "environmental_dependency", "data_state", "timing_dependency", "third_party_dependency"] },
+                "description": { "type": "string" }
+              },
+              "required": ["kind", "description"],
+              "additionalProperties": false
+            },
+            "description": "Pré-requisitos factuais para exploração. Array vazio se explorável por default."
+          },
+          "execution": {
+            "type": "object",
+            "properties": {
+              "attacker_perspective": { "type": "string", "description": "Quem é o atacante e seu starting point." },
+              "payloads": { "type": "array", "items": { "type": "string" }, "description": "Inputs maliciosos específicos, HTTP requests, ou scripts." },
+              "instructions": { "type": "array", "items": { "type": "string" }, "description": "Array linear de todas ações do atacante do setup até exploração." },
+              "expected_result": { "type": "string", "description": "Resultado observável confirmando exploração bem-sucedida." }
+            },
+            "required": ["attacker_perspective", "payloads", "instructions", "expected_result"],
+            "additionalProperties": false
+          },
+          "remediation": {
+            "type": "object",
+            "properties": {
+              "strategy": { "type": "string", "description": "Explicação high-level do fix." },
+              "code_changes": {
+                "type": "array",
+                "items": {
+                  "type": "object",
+                  "properties": {
+                    "file_name": { "type": "string" },
+                    "fixed_code": { "type": "string" }
+                  },
+                  "required": ["file_name", "fixed_code"],
+                  "additionalProperties": false
+                }
+              }
+            },
+            "required": ["strategy"],
+            "additionalProperties": false
+          },
+          "severity": {
+            "type": "object",
+            "properties": {
+              "likelihood": {
+                "type": "object",
+                "properties": {
+                  "score": { "type": "string", "enum": ["informational", "low", "medium", "high", "critical"] },
+                  "reason": { "type": "string" }
+                },
+                "required": ["score", "reason"],
+                "additionalProperties": false
+              },
+              "impact": {
+                "type": "object",
+                "properties": {
+                  "score": { "type": "string", "enum": ["informational", "low", "medium", "high", "critical"] },
+                  "reason": { "type": "string" }
+                },
+                "required": ["score", "reason"],
+                "additionalProperties": false
+              },
+              "overall_severity": { "type": "string", "enum": ["informational", "low", "medium", "high", "critical"] }
+            },
+            "required": ["likelihood", "impact", "overall_severity"],
+            "additionalProperties": false
+          },
+          "confidence": {
+            "type": "object",
+            "properties": {
+              "score": { "type": "string", "enum": ["low", "medium", "high"] },
+              "reason": { "type": "string", "description": "Por que você deu essa confidence. Mencione missing files, complex routing, ou ambiguous data flows." }
+            },
+            "required": ["score", "reason"],
+            "additionalProperties": false
+          }
+        },
+        "required": ["verdict", "title", "description", "root_cause", "intended_behavior", "trace", "conditions", "execution", "remediation", "severity", "confidence"],
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "description": "Finding rejeitado — o comportamento descrito é factualmente incorreto ou o code path não existe.",
+        "properties": {
+          "verdict": { "type": "string", "const": "rejected" },
+          "reason": { "type": "string", "description": "Explique quais claims específicos no finding estão factualmente errados." }
+        },
+        "required": ["verdict", "reason"],
+        "additionalProperties": false
+      }
+    ]
+  }
+}
diff --git a/.github/skills/security-specialist/references/scan-artifacts.md b/.github/skills/security-specialist/references/scan-artifacts.md
new file mode 100644
index 0000000..ac6c2de
--- /dev/null
+++ b/.github/skills/security-specialist/references/scan-artifacts.md
@@ -0,0 +1,164 @@
+# Scan Artifacts Specification
+
+This document describes the file layout and purpose of each artifact produced by a completed security scan.
+
+---
+
+## Directory Structure
+
+All scan artifacts live in a `.security/` directory at the repository root:
+
+```
+.security/
+├── scan.db             # SQLite database (source of truth)
+├── findings.json       # Exported findings — simple format (generated by finalize.py)
+├── report.md           # Human-readable report (generated by finalize.py)
+├── integrity.sha256    # SHA-256 of findings.json (tamper detection)
+├── threat-model.md     # Repository threat model (if generated)
+└── scans/
+    └── /
+        ├── architecture.md    # Phase 1 output (full-scan only)
+        ├── findings.json      # Structured format — validated against report-schema.json
+        ├── security-report.html  # Self-contained HTML report
+        ├── report.json        # Machine-readable summary
+        └── manifest.json      # File hashes + completion timestamp
+```
+
+---
+
+## Artifact Descriptions
+
+### scan.db — Source of Truth
+
+A SQLite database containing the complete scan state. This is the authoritative data store that all other artifacts are derived from.
+
+**Tables:**
+- `scans` — Scan metadata (id, repo, branch, started_at, completed_at, config)
+- `findings` — All findings conforming to the schema in `finding-format.md`
+- `triage_log` — Status change history (who changed what, when, and why)
+
+**Rules:**
+- All mutations happen here first. Never edit `findings.json` or `report.md` directly.
+- The database is append-only during a scan. Findings are inserted, never deleted (status changes use the `status` field).
+- Triage actions (marking false-positive, accepted-risk, etc.) are recorded with a timestamp and reason in `triage_log`.
+
+**Typical operations:**
+```sql
+-- Count open findings by severity
+SELECT severity, COUNT(*) FROM findings
+WHERE scan_id = ? AND status = 'open'
+GROUP BY severity ORDER BY
+  CASE severity
+    WHEN 'critical' THEN 1
+    WHEN 'high' THEN 2
+    WHEN 'medium' THEN 3
+    WHEN 'low' THEN 4
+    WHEN 'info' THEN 5
+  END;
+```
+
+---
+
+### findings.json — Sealed Export
+
+A JSON array of all findings from the scan, exported from `scan.db` at finalization time.
+
+**Properties:**
+- Generated by `finalize.py` — never written by hand
+- Represents a point-in-time snapshot of findings at scan completion
+- Immutable after generation. If findings change (triage, fixes), re-run finalization to produce a new export
+- Each entry conforms exactly to the schema in `finding-format.md`
+
+**Structure:**
+```json
+{
+  "scan_id": "f0e1d2c3-b4a5-6789-0123-456789abcdef",
+  "repository": "myorg/myapp",
+  "branch": "main",
+  "finalized_at": "2026-06-24T03:30:00Z",
+  "findings": [
+    { /* finding object per finding-format.md */ }
+  ]
+}
+```
+
+---
+
+### report.md — Human-Readable Report
+
+The markdown report formatted according to `report-format.md`. Intended for human review, pull request comments, or export to documentation systems.
+
+**Properties:**
+- Generated from the same data as `findings.json` at finalization time
+- Read-only artifact — regenerate rather than edit
+- Self-contained: readers should not need to consult `scan.db` or `findings.json`
+
+---
+
+### integrity.sha256 — Tamper Detection
+
+A SHA-256 hash of `findings.json`, computed at finalization time.
+
+**Format:**
+```
+  findings.json
+```
+
+Example:
+```
+e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  findings.json
+```
+
+**Purpose:**
+- Allows downstream tools (CI gates, compliance checks, dashboards) to verify that `findings.json` has not been modified since finalization
+- If the hash does not match, the findings export must be considered untrusted and regenerated from `scan.db`
+
+**Verification:**
+```bash
+cd .security/
+sha256sum -c integrity.sha256
+```
+
+Expected output on success: `findings.json: OK`
+
+---
+
+### threat-model.md — Repository Threat Model (Optional)
+
+A structured threat model for the repository, generated on first scan or when explicitly requested. Not regenerated on every scan.
+
+**Contains:**
+- Trust boundaries (what's inside vs. outside the security perimeter)
+- Data flows (what sensitive data moves where)
+- Entry points (APIs, file uploads, webhooks, CLI inputs)
+- Assets (databases, credentials, user data, secrets)
+- Threat actors (who might attack and what they'd target)
+
+**Rules:**
+- Only created when explicitly triggered or on first scan of a new repository
+- Updated manually or on request — not overwritten by routine scans
+- Informs severity decisions: a finding that crosses a trust boundary is more severe than one contained within a trusted zone
+
+---
+
+## Lifecycle
+
+1. **Scan starts** → `scan.db` is created (or a new scan row is inserted into an existing database)
+2. **Analysis runs** → Findings are inserted into `scan.db` as they are discovered
+3. **Triage (optional)** → Agent or human reviews findings, updates statuses in `scan.db`
+4. **Finalization** → `finalize.py` exports `findings.json`, generates `report.md`, computes `integrity.sha256`
+5. **Post-seal** → Artifacts are committed, pushed, or attached to a PR. No further modifications without re-finalization.
+
+---
+
+## Gitignore Considerations
+
+The `.security/` directory should generally be committed so findings are tracked alongside code. However:
+
+- `scan.db` may be gitignored in repositories where only the sealed artifacts matter (reduces churn from SQLite binary diffs)
+- If `scan.db` is gitignored, `findings.json` + `integrity.sha256` become the durable record
+
+Recommended `.gitignore` entry when excluding the database:
+```gitignore
+.security/scan.db
+```
diff --git a/.github/skills/security-specialist/references/severity-policy.md b/.github/skills/security-specialist/references/severity-policy.md
new file mode 100644
index 0000000..cd3c6c3
--- /dev/null
+++ b/.github/skills/security-specialist/references/severity-policy.md
@@ -0,0 +1,162 @@
+# Severity Policy
+
+Practical decision criteria for assigning severity to security findings. Apply this policy consistently — do not assign severity based on gut feeling.
+
+---
+
+## Severity Levels
+
+### Critical
+
+The vulnerability allows an attacker to fully compromise the system, its data, or its users with minimal effort and no special access.
+
+**Assign critical when:**
+- Remote code execution (RCE) is achievable
+- Authentication can be bypassed entirely, granting full access
+- PII, credentials, or payment data is directly exposed or exfiltrable
+- Supply chain compromise: malicious dependency, tampered build artifact, or poisoned CI pipeline
+- Pre-authentication exploitation — no account or privileges required
+
+**Examples:**
+- Unauthenticated endpoint that returns all user records with passwords
+- Deserialization vulnerability allowing arbitrary command execution
+- Hardcoded production credentials (database, payment processor, admin tokens)
+- Dependency with an actively exploited RCE CVE
+
+---
+
+### High
+
+The vulnerability enables significant damage but requires slightly more effort or minimal access (low-privilege account).
+
+**Assign high when:**
+- SQL injection or XSS that enables session hijacking or credential theft
+- Privilege escalation from normal user to admin
+- SSRF that reaches internal services, metadata endpoints, or private networks
+- Significant data exposure (not full breach, but sensitive records accessible)
+- Authentication flaws that weaken but don't fully bypass access control
+- File upload allowing server-side execution
+
+**Examples:**
+- Stored XSS in a comment field that steals admin session cookies
+- IDOR allowing any authenticated user to read other users' private data
+- SSRF reaching cloud metadata endpoint (`169.254.169.254`)
+- JWT signature not verified, allowing role escalation
+
+---
+
+### Medium
+
+The vulnerability has real security impact but is limited in scope, requires chaining, or affects non-critical paths.
+
+**Assign medium when:**
+- Stored XSS that cannot access session tokens (HttpOnly cookies in place)
+- Information disclosure: stack traces, internal file paths, software versions
+- Missing security headers (CSP, X-Frame-Options) on sensitive pages
+- Weak cryptography in non-critical paths (e.g., MD5 for non-password hashing)
+- CSRF on state-changing but non-critical actions
+- Open redirect usable for phishing
+
+**Examples:**
+- Error page leaks full stack trace including internal IP addresses
+- No CSP header on pages that render user-generated content
+- Password reset token generated with insufficient entropy (but short-lived)
+- CSRF on profile display name change (not on password/email change)
+
+---
+
+### Low
+
+The issue has minimal direct security impact but represents a gap in defense-in-depth or hygiene.
+
+**Assign low when:**
+- Verbose error messages revealing framework version or minor internals
+- Missing rate limiting on non-critical endpoints
+- Minor misconfigurations with no direct exploit path
+- Dependencies with CVEs that have no practical exploit in this context
+- Cookie without `Secure` flag in a development-only path
+- Directory listing enabled but exposing only public assets
+
+**Examples:**
+- Server responds with `X-Powered-By: Express` header
+- No rate limit on the "forgot password" endpoint (but tokens are single-use and short-lived)
+- Dependency has a CVE for a function the project never calls
+- CORS allows `*` on a public read-only API with no auth
+
+---
+
+### Info
+
+Not a vulnerability. An observation, best-practice recommendation, or note for future hardening.
+
+**Assign info when:**
+- Best practice not followed but no exploitable condition exists
+- Code quality issue with security implications (e.g., error handling inconsistency)
+- Suggestion for future improvement (e.g., "consider adding Subresource Integrity")
+- Informational notes about architecture or trust boundaries
+
+**Examples:**
+- Recommend enabling HSTS preload (HSTS is already present, just not preloaded)
+- Suggest adding `integrity` attributes to CDN script tags
+- Note that logging does not capture failed authentication attempts
+
+---
+
+## Dynamic Baseline
+
+Severity não é absoluta — é relativa ao que a aplicação é e ao que comparáveis aceitam.
+
+### Como Calibrar
+
+1. **Identifique o comparável** em Phase 1 (CMS → outros CMSes, API gateway → outros API gateways, novel app → sem comparável)
+2. **Verifique se o pattern existe no comparável** — se sim e foi explorado, é finding MAIS FORTE. Se nunca explorado em anos de produção, entenda por quê.
+3. **Ajuste severity pela distância do padrão aceito** — se TODO app nessa categoria tem o mesmo pattern e ninguém considera vulnerability, não reporte como HIGH.
+4. **Não use baseline para DESCARTAR** — use para calibrar. Um pattern perigoso é perigoso mesmo se o comparável também o tem.
+
+### Distinction: HIGH vs MEDIUM para Business Logic
+
+- **HIGH**: O finding derrota um security boundary explícito. User performa ação que o sistema explicitamente gate atrás de higher role, e a ação tem consequências reais.
+- **MEDIUM**: Bypass com consequências reais mas limitadas. Requer auth, impacto confinado a dados do atacante, ou conditions uncommon.
+
+---
+
+## Don't Overcall
+
+Common mistakes that inflate severity beyond what the evidence supports:
+
+| Mistake | Why it's wrong | Correct severity |
+|---------|---------------|-----------------|
+| Reflected XSS behind authentication marked as critical | Requires social engineering of an already-authenticated user; session cookies are HttpOnly | Medium (or High if cookies are accessible) |
+| Missing HSTS marked as critical | HSTS absence alone doesn't enable exploitation; it's defense-in-depth | Low (or Medium if the site handles sensitive auth flows over HTTP) |
+| Dependency CVE with no reachable code path marked as high | If the vulnerable function is never called, there's no exploit | Low or Info |
+| Missing rate limiting on login marked as high | Only matters if there's no account lockout, no CAPTCHA, and passwords are weak | Low (escalate to Medium if no compensating controls exist) |
+| Information disclosure of software version marked as high | Version numbers alone don't enable attack; they help an attacker enumerate but require a corresponding vulnerability | Low |
+| Self-XSS (user can only attack themselves) marked as medium | No impact on other users; no realistic attack scenario | Info |
+| CORS misconfiguration on a public API with no auth | If the API is intentionally public and has no user context, CORS is irrelevant | Info |
+| **Multiple dependency CVEs listed at face value without project context** | If 9 CVEs are listed but only 1 is exploitable due to missing preconditions, reporting "9 CRITICAL CVEs" is misleading and erodes trust | Analyze each individually, assign per-CVE real severity |
+
+**The rule:** Severity reflects *demonstrated impact*, not *theoretical worst case*. If you can't articulate the realistic attack scenario and its consequences in 2 sentences, you're probably overcalling.
+
+### CVE Cross-Reference Protocol (Mandatory)
+
+Before assigning severity to any dependency CVE:
+
+1. **Read the advisory** — identify the exact precondition (which function, which feature, which config)
+2. **Grep the codebase** — does the project use that function/feature? Cite the evidence (file:line or "0 results")
+3. **Check the environment** — does prod have the infrastructure the CVE requires? (CDN, multi-user, Windows, etc.)
+4. **DAST validate** — did the probe confirm exploitability in localhost? In production?
+5. **Assign real severity** — based on what you proved, not what the advisory says generically
+
+A bulk "upgrade all deps" recommendation is fine. But the *severity* must reflect this project, not all projects.
+
+---
+
+## Severity Decision Flowchart
+
+1. **Can an unauthenticated attacker achieve RCE, full data breach, or complete auth bypass?** → Critical
+2. **Can a low-privilege attacker steal sessions, escalate privileges, or access significant sensitive data?** → High
+3. **Is there real but limited impact (scoped data leak, partial XSS, missing hardening on sensitive pages)?** → Medium
+4. **Is it a hygiene gap with no direct exploit path in this context?** → Low
+5. **Is it purely advisory with no current exploitability?** → Info
+
+When in doubt between two levels, ask: "Can I demonstrate concrete harm to a user or the system?" If yes, go with the higher level. If not, go lower.
diff --git a/.github/skills/security-specialist/scripts/finalize.py b/.github/skills/security-specialist/scripts/finalize.py
new file mode 100644
index 0000000..2fd7d3a
--- /dev/null
+++ b/.github/skills/security-specialist/scripts/finalize.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+"""Seal a security scan and generate final reports."""
+
+import argparse
+import hashlib
+import json
+import sqlite3
+from datetime import datetime, timezone
+from pathlib import Path
+
+
+def _connect(scan_dir: Path) -> sqlite3.Connection:
+    db_path = scan_dir / "scan.db"
+    if not db_path.exists():
+        raise SystemExit(f"Database not found: {db_path}")
+    conn = sqlite3.connect(str(db_path))
+    conn.row_factory = sqlite3.Row
+    return conn
+
+
+def _get_active_scan(conn: sqlite3.Connection) -> dict:
+    row = conn.execute("SELECT * FROM scans WHERE status = 'active' ORDER BY started_at DESC LIMIT 1").fetchone()
+    if not row:
+        raise SystemExit("No active scan found to seal.")
+    return dict(row)
+
+
+def _seal(conn: sqlite3.Connection, scan_id: str) -> None:
+    now = datetime.now(timezone.utc).isoformat()
+    conn.execute("UPDATE scans SET sealed_at = ?, status = 'sealed' WHERE id = ?", (now, scan_id))
+    conn.commit()
+
+
+def _export_findings(conn: sqlite3.Connection, scan_id: str, scan_dir: Path) -> list[dict]:
+    rows = conn.execute("SELECT * FROM findings WHERE scan_id = ? ORDER BY severity, file_path", (scan_id,)).fetchall()
+    findings = [dict(r) for r in rows]
+    (scan_dir / "findings.json").write_text(json.dumps(findings, indent=2))
+    return findings
+
+
+def _severity_order(sev: str) -> int:
+    return {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}.get(sev, 5)
+
+
+def _generate_report(findings: list[dict], scan: dict, scan_dir: Path) -> None:
+    counts: dict[str, int] = {}
+    for f in findings:
+        counts[f["severity"]] = counts.get(f["severity"], 0) + 1
+    total = len(findings)
+
+    lines = ["# Security Scan Report\n"]
+    lines.append(f"**Repository:** `{scan['repo_path']}`  ")
+    lines.append(f"**Scan ID:** `{scan['id']}`  ")
+    lines.append(f"**Started:** {scan['started_at']}  ")
+    lines.append(f"**Sealed:** {scan['sealed_at']}\n")
+
+    # Executive summary
+    lines.append("## Executive Summary\n")
+    if total == 0:
+        lines.append("No findings were recorded during this scan.\n")
+    else:
+        lines.append(f"This scan identified **{total} finding(s)** across the repository:\n")
+        for sev in sorted(counts, key=_severity_order):
+            emoji = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🔵", "info": "⚪"}.get(sev, "·")
+            lines.append(f"- {emoji} **{sev.capitalize()}:** {counts[sev]}")
+        lines.append("")
+        if counts.get("critical", 0) > 0:
+            lines.append("⚠️  Critical findings require immediate attention before deployment.\n")
+
+    # Findings table
+    if findings:
+        lines.append("## Findings Overview\n")
+        lines.append("| # | Severity | Category | File | Status | Title |")
+        lines.append("|---|----------|----------|------|--------|-------|")
+        for i, f in enumerate(sorted(findings, key=lambda x: _severity_order(x["severity"])), 1):
+            loc = f"`{f['file_path']}:{f['line_number']}`" if f["file_path"] else "—"
+            lines.append(f"| {i} | {f['severity']} | {f['category']} | {loc} | {f['status']} | {f['title']} |")
+        lines.append("")
+
+    # Detailed findings
+    if findings:
+        lines.append("## Detailed Findings\n")
+        for i, f in enumerate(sorted(findings, key=lambda x: _severity_order(x["severity"])), 1):
+            lines.append(f"### {i}. {f['title']}\n")
+            lines.append(f"- **Severity:** {f['severity']}")
+            lines.append(f"- **Category:** {f['category']}")
+            lines.append(f"- **Status:** {f['status']}")
+            if f["file_path"]:
+                lines.append(f"- **Location:** `{f['file_path']}:{f['line_number']}`")
+            if f["tracking_url"]:
+                lines.append(f"- **Tracking:** {f['tracking_url']}")
+            lines.append(f"\n{f['description']}\n")
+            if f["evidence"]:
+                lines.append("**Evidence:**\n")
+                lines.append(f"```\n{f['evidence']}\n```\n")
+
+    (scan_dir / "report.md").write_text("\n".join(lines))
+
+
+def _write_integrity(scan_dir: Path) -> str:
+    content = (scan_dir / "findings.json").read_bytes()
+    digest = hashlib.sha256(content).hexdigest()
+    (scan_dir / "integrity.sha256").write_text(f"{digest}  findings.json\n")
+    return digest
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description="Seal scan and generate reports")
+    parser.add_argument("--scan-dir", required=True, help="Path to .security/ directory")
+    args = parser.parse_args()
+
+    scan_dir = Path(args.scan_dir).resolve()
+    conn = _connect(scan_dir)
+    scan = _get_active_scan(conn)
+    _seal(conn, scan["id"])
+    scan["sealed_at"] = datetime.now(timezone.utc).isoformat()
+
+    findings = _export_findings(conn, scan["id"], scan_dir)
+    _generate_report(findings, scan, scan_dir)
+    digest = _write_integrity(scan_dir)
+    conn.close()
+
+    total = len(findings)
+    counts = {}
+    for f in findings:
+        counts[f["severity"]] = counts.get(f["severity"], 0) + 1
+    print(f"Scan sealed: {scan['id']}")
+    print(f"Findings: {total} total — " + ", ".join(f"{k}: {v}" for k, v in sorted(counts.items(), key=lambda x: _severity_order(x[0]))))
+    print(f"Reports: {scan_dir / 'report.md'}, {scan_dir / 'findings.json'}")
+    print(f"Integrity: {digest}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/.github/skills/security-specialist/scripts/pentest.py b/.github/skills/security-specialist/scripts/pentest.py
new file mode 100644
index 0000000..a1e07cd
--- /dev/null
+++ b/.github/skills/security-specialist/scripts/pentest.py
@@ -0,0 +1,478 @@
+#!/usr/bin/env python3
+"""Penetration testing automation helpers.
+
+Wraps system tools, pip-installed packages, and stdlib fallbacks.
+Priority: system binary > pip package > stdlib.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import shutil
+import socket
+import subprocess
+import sys
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any
+from urllib.error import URLError
+from urllib.request import Request, urlopen
+
+
+def _run(cmd: list[str], timeout: int = 120) -> tuple[int, str, str]:
+    try:
+        r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
+        return r.returncode, r.stdout, r.stderr
+    except FileNotFoundError:
+        return -1, "", f"Tool not found: {cmd[0]}"
+    except subprocess.TimeoutExpired:
+        return -2, "", f"Timeout after {timeout}s"
+
+
+def _has_bin(name: str) -> bool:
+    return shutil.which(name) is not None
+
+
+def _has_pkg(name: str) -> bool:
+    try:
+        __import__(name)
+        return True
+    except ImportError:
+        return False
+
+
+def _now() -> str:
+    return datetime.now(timezone.utc).isoformat()
+
+
+def _warn(msg: str) -> None:
+    print(f"[!] {msg}", file=sys.stderr)
+
+
+def _info(msg: str) -> None:
+    print(f"[*] {msg}", file=sys.stderr)
+
+
+# ─── DNS & WHOIS ────────────────────────────────────────────────────────────
+
+def _dns_lookup(domain: str) -> dict[str, list[str]]:
+    """DNS enumeration: dnspython > dig > socket."""
+    records: dict[str, list[str]] = {}
+
+    if _has_pkg("dns"):
+        import dns.resolver
+        _info("Using dnspython for DNS")
+        for rtype in ["A", "AAAA", "MX", "NS", "TXT", "CNAME", "SOA"]:
+            try:
+                answers = dns.resolver.resolve(domain, rtype)
+                records[rtype] = [str(r) for r in answers]
+            except Exception:
+                records[rtype] = []
+    elif _has_bin("dig"):
+        _info("Using dig for DNS")
+        for rtype in ["A", "AAAA", "MX", "NS", "TXT", "CNAME"]:
+            rc, out, _ = _run(["dig", "+short", domain, rtype])
+            records[rtype] = out.strip().splitlines() if rc == 0 and out.strip() else []
+    else:
+        _info("Stdlib fallback for DNS (limited to A records)")
+        try:
+            addrs = socket.getaddrinfo(domain, None)
+            records["A"] = list({a[4][0] for a in addrs if a[0] == socket.AF_INET})
+            records["AAAA"] = list({a[4][0] for a in addrs if a[0] == socket.AF_INET6})
+        except socket.gaierror:
+            records["A"] = []
+
+    return records
+
+
+def _whois_lookup(domain: str) -> str | None:
+    """WHOIS: python-whois > system whois > None."""
+    if _has_pkg("whois"):
+        import whois
+        _info("Using python-whois")
+        try:
+            w = whois.whois(domain)
+            return str(w)
+        except Exception:
+            return None
+    elif _has_bin("whois"):
+        _info("Using system whois")
+        rc, out, _ = _run(["whois", domain])
+        return out if rc == 0 else None
+    else:
+        _warn("No whois tool available (pip install python-whois)")
+        return None
+
+
+def _subdomain_enum(domain: str) -> list[str]:
+    """Subdomains: subfinder > bbot > crt.sh."""
+    if _has_bin("subfinder"):
+        _info("Using subfinder")
+        rc, out, _ = _run(["subfinder", "-d", domain, "-silent"], timeout=60)
+        return sorted(set(out.strip().splitlines())) if rc == 0 else []
+
+    if _has_pkg("bbot"):
+        _info("Using bbot (pip)")
+        rc, out, _ = _run([sys.executable, "-m", "bbot", "-t", domain, "-f", "subdomain-enum", "--silent"], timeout=120)
+        return sorted(set(out.strip().splitlines())) if rc == 0 else []
+
+    # crt.sh fallback
+    _info("Using crt.sh CT logs for subdomains")
+    try:
+        url = f"https://crt.sh/?q=%.{domain}&output=json"
+        req = Request(url, headers={"User-Agent": "security-specialist/1.0"})
+        with urlopen(req, timeout=15) as resp:
+            certs = json.loads(resp.read())
+        return sorted({e["name_value"].strip() for e in certs if "name_value" in e})[:100]
+    except Exception as e:
+        _warn(f"crt.sh failed: {e}")
+        return []
+
+
+# ─── PORT SCANNING ──────────────────────────────────────────────────────────
+
+def _port_scan(target: str, ports: str) -> dict[str, Any]:
+    """Port scan: nmap > python3-nmap > socket scan."""
+    if _has_bin("nmap"):
+        _info("Using nmap")
+        port_arg = "-p-" if ports == "all" else "--top-ports 1000"
+        cmd = ["nmap", "-sC", "-sV", "--open", "-oN", "-"] + port_arg.split() + [target]
+        rc, out, _ = _run(cmd, timeout=300)
+        return {"tool": "nmap", "raw": out} if rc == 0 else {"tool": "nmap", "error": "scan failed"}
+
+    if _has_pkg("nmap3"):
+        _info("Using python3-nmap (pip)")
+        import nmap3
+        nm = nmap3.NmapScanTechniques()
+        try:
+            result = nm.nmap_tcp_scan(target, args="--top-ports 1000" if ports != "all" else "-p-")
+            return {"tool": "python3-nmap", "results": result}
+        except Exception as e:
+            return {"tool": "python3-nmap", "error": str(e)}
+
+    if _has_pkg("nmap"):
+        _info("Using python-nmap (pip)")
+        import nmap
+        nm = nmap.PortScanner()
+        try:
+            port_range = "1-65535" if ports == "all" else "1-1024"
+            nm.scan(target, port_range, arguments="-sV")
+            results = []
+            for host in nm.all_hosts():
+                for proto in nm[host].all_protocols():
+                    for port in nm[host][proto]:
+                        info = nm[host][proto][port]
+                        if info["state"] == "open":
+                            results.append({"port": port, "service": info.get("name", ""), "version": info.get("version", "")})
+            return {"tool": "python-nmap", "open_ports": results}
+        except Exception as e:
+            return {"tool": "python-nmap", "error": str(e)}
+
+    # Stdlib fallback
+    _info("Stdlib socket scan (slow, no service detection)")
+    return {"tool": "socket", "open_ports": _socket_scan(target, ports)}
+
+
+def _socket_scan(target: str, ports: str) -> list[dict]:
+    port_list = list(range(1, 65536)) if ports == "all" else list(range(1, 1025))
+    open_ports = []
+
+    def check(port: int) -> dict | None:
+        try:
+            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+                s.settimeout(0.5)
+                if s.connect_ex((target, port)) == 0:
+                    return {"port": port, "state": "open"}
+        except (socket.timeout, OSError):
+            pass
+        return None
+
+    with ThreadPoolExecutor(max_workers=200) as pool:
+        futures = {pool.submit(check, p): p for p in port_list}
+        for f in as_completed(futures):
+            r = f.result()
+            if r:
+                open_ports.append(r)
+
+    return sorted(open_ports, key=lambda x: x["port"])
+
+
+# ─── WEB ENUMERATION ────────────────────────────────────────────────────────
+
+def _dir_brute(url: str) -> list[str]:
+    """Directory brute: gobuster > feroxbuster > dirsearch > urllib."""
+    if _has_bin("gobuster"):
+        wl = _find_wordlist()
+        if wl:
+            _info("Using gobuster")
+            rc, out, _ = _run(["gobuster", "dir", "-u", url, "-w", wl, "-q", "--no-error"], timeout=180)
+            return out.strip().splitlines() if rc == 0 else []
+
+    if _has_bin("feroxbuster"):
+        _info("Using feroxbuster")
+        rc, out, _ = _run(["feroxbuster", "-u", url, "-q", "--no-state"], timeout=180)
+        return out.strip().splitlines() if rc == 0 else []
+
+    if _has_bin("dirsearch"):
+        _info("Using dirsearch")
+        rc, out, _ = _run(["dirsearch", "-u", url, "--format=plain", "-q"], timeout=180)
+        return out.strip().splitlines() if rc == 0 else []
+
+    # Python fallback
+    _info("Stdlib URL brute (limited wordlist)")
+    return _python_dir_brute(url)
+
+
+def _find_wordlist() -> str | None:
+    for path in [
+        "/usr/share/wordlists/dirb/common.txt",
+        "/usr/share/seclists/Discovery/Web-Content/common.txt",
+        "/usr/share/dirbuster/wordlists/directory-list-2.3-small.txt",
+        "/opt/wordlists/common.txt",
+    ]:
+        if Path(path).exists():
+            return path
+    return None
+
+
+def _python_dir_brute(url: str) -> list[str]:
+    common = [
+        "admin", "login", "api", "wp-admin", "wp-login.php", ".git", ".git/HEAD",
+        ".env", ".env.local", "config", "backup", "phpmyadmin", "console", "debug",
+        "server-status", "actuator", "actuator/health", "swagger", "swagger-ui.html",
+        "graphql", "graphiql", ".well-known/security.txt", "robots.txt", "sitemap.xml",
+        "wp-json", "xmlrpc.php", "solr", "jenkins", "manager/html", "_debug_toolbar",
+        "elmah.axd", "trace.axd", "info.php", "phpinfo.php", ".DS_Store", ".htaccess",
+        "web.config", "crossdomain.xml", "clientaccesspolicy.xml",
+    ]
+    found = []
+    for path in common:
+        try:
+            req = Request(f"{url.rstrip('/')}/{path}", method="HEAD",
+                         headers={"User-Agent": "security-specialist/1.0"})
+            with urlopen(req, timeout=5) as resp:
+                if resp.status < 400:
+                    found.append(f"/{path} [{resp.status}]")
+        except Exception:
+            pass
+    return found
+
+
+def _tech_detect(url: str) -> dict | None:
+    """Tech detection: whatweb > webtech > header analysis."""
+    if _has_bin("whatweb"):
+        _info("Using whatweb")
+        rc, out, _ = _run(["whatweb", "--color=never", "-a", "3", url])
+        return {"tool": "whatweb", "raw": out.strip()} if rc == 0 else None
+
+    if _has_pkg("webtech"):
+        _info("Using webtech (pip)")
+        try:
+            from webtech import WebTech
+            wt = WebTech(options={"json": True})
+            result = wt.start_from_url(url)
+            return {"tool": "webtech", "technologies": result}
+        except Exception:
+            pass
+
+    # Header-based fallback
+    _info("Header-based tech detection")
+    try:
+        req = Request(url, headers={"User-Agent": "security-specialist/1.0"})
+        with urlopen(req, timeout=10) as resp:
+            headers = dict(resp.headers)
+            tech = {}
+            if "X-Powered-By" in headers:
+                tech["powered_by"] = headers["X-Powered-By"]
+            if "Server" in headers:
+                tech["server"] = headers["Server"]
+            if "X-Generator" in headers:
+                tech["generator"] = headers["X-Generator"]
+            for h in ["X-AspNet-Version", "X-AspNetMvc-Version"]:
+                if h in headers:
+                    tech["aspnet"] = headers[h]
+            return {"tool": "headers", "detected": tech}
+    except Exception:
+        return None
+
+
+# ─── VULNERABILITY SCANNING ─────────────────────────────────────────────────
+
+def _vuln_scan_web(target: str) -> dict[str, Any]:
+    """Web vuln scan: nikto > wapiti3 > nuclei > basic checks."""
+    results: dict[str, Any] = {}
+
+    if _has_bin("nikto"):
+        _info("Using nikto")
+        rc, out, _ = _run(["nikto", "-h", target, "-Format", "txt"], timeout=300)
+        if rc == 0:
+            results["nikto"] = out
+
+    if _has_bin("wapiti"):
+        _info("Using wapiti3")
+        rc, out, _ = _run(["wapiti", "-u", target, "--flush-session", "-f", "txt", "--no-bugreport"], timeout=300)
+        if rc == 0:
+            results["wapiti"] = out
+    elif _has_pkg("wapitiCore"):
+        _info("wapiti3 available via pip — run: wapiti -u ")
+        results["wapiti_note"] = "wapiti3 installed but requires CLI invocation"
+
+    if _has_bin("nuclei"):
+        _info("Using nuclei")
+        rc, out, _ = _run(["nuclei", "-u", target, "-silent", "-nc"], timeout=300)
+        if rc == 0:
+            results["nuclei"] = out.strip().splitlines()
+
+    if not results:
+        _info("Running basic security header checks")
+        results["header_checks"] = _check_security_headers(target)
+
+    return results
+
+
+def _check_security_headers(url: str) -> dict[str, str]:
+    """Check common security headers as minimal vuln scan fallback."""
+    expected = [
+        "Strict-Transport-Security",
+        "Content-Security-Policy",
+        "X-Content-Type-Options",
+        "X-Frame-Options",
+        "X-XSS-Protection",
+        "Referrer-Policy",
+        "Permissions-Policy",
+    ]
+    try:
+        req = Request(url, headers={"User-Agent": "security-specialist/1.0"})
+        with urlopen(req, timeout=10) as resp:
+            headers = dict(resp.headers)
+            results = {}
+            for h in expected:
+                if h in headers:
+                    results[h] = f"✓ {headers[h]}"
+                else:
+                    results[h] = "✗ MISSING"
+            return results
+    except Exception as e:
+        return {"error": str(e)}
+
+
+# ─── CLI COMMANDS ───────────────────────────────────────────────────────────
+
+def recon_passive(args: argparse.Namespace) -> None:
+    target = args.target
+    results = {
+        "target": target,
+        "timestamp": _now(),
+        "dns": _dns_lookup(target),
+        "whois": _whois_lookup(target),
+        "subdomains": _subdomain_enum(target),
+    }
+    _output(results, args)
+
+
+def recon_active(args: argparse.Namespace) -> None:
+    results = {
+        "target": args.target,
+        "timestamp": _now(),
+        "scan_type": "active",
+        "port_scan": _port_scan(args.target, args.ports or "top1000"),
+    }
+    _output(results, args)
+
+
+def enumerate_web(args: argparse.Namespace) -> None:
+    url = args.url.rstrip("/")
+    results = {
+        "target": url,
+        "timestamp": _now(),
+        "directories": _dir_brute(url),
+        "technologies": _tech_detect(url),
+    }
+    _output(results, args)
+
+
+def vuln_scan(args: argparse.Namespace) -> None:
+    results = {
+        "target": args.target,
+        "timestamp": _now(),
+        "type": args.type,
+        "findings": _vuln_scan_web(args.target) if args.type == "web" else _port_scan(args.target, "top1000"),
+    }
+    _output(results, args)
+
+
+def check_tools(args: argparse.Namespace) -> None:
+    """Show which tools are available on this system."""
+    tools = {
+        "Port scan": [("nmap", "bin"), ("python3-nmap (nmap3)", "pkg:nmap3"), ("python-nmap", "pkg:nmap"), ("socket", "stdlib")],
+        "DNS": [("dig", "bin"), ("dnspython", "pkg:dns"), ("socket", "stdlib")],
+        "WHOIS": [("whois", "bin"), ("python-whois", "pkg:whois")],
+        "Subdomains": [("subfinder", "bin"), ("amass", "bin"), ("bbot", "pkg:bbot"), ("crt.sh", "stdlib")],
+        "Dir brute": [("gobuster", "bin"), ("feroxbuster", "bin"), ("dirsearch", "bin"), ("urllib", "stdlib")],
+        "Tech detect": [("whatweb", "bin"), ("webtech", "pkg:webtech"), ("headers", "stdlib")],
+        "Vuln scan": [("nikto", "bin"), ("wapiti", "bin"), ("nuclei", "bin"), ("header check", "stdlib")],
+        "SQLi": [("sqlmap", "bin")],
+    }
+    print("Tool availability:\n")
+    for category, items in tools.items():
+        print(f"  {category}:")
+        for name, check in items:
+            if check == "stdlib":
+                status = "✓ (always available)"
+            elif check.startswith("pkg:"):
+                pkg = check.split(":")[1]
+                status = "✓" if _has_pkg(pkg) else f"✗ (pip install {name.split('(')[0].strip().replace(' ', '-')})"
+            else:
+                status = "✓" if _has_bin(name) else "✗ (not in PATH)"
+            print(f"    {name:30s} {status}")
+        print()
+
+
+# ─── OUTPUT & MAIN ──────────────────────────────────────────────────────────
+
+def _output(data: dict, args: argparse.Namespace) -> None:
+    out = json.dumps(data, indent=2, default=str)
+    if hasattr(args, "out") and args.out:
+        Path(args.out).write_text(out)
+        print(f"Results written to {args.out}")
+    else:
+        print(out)
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description="Penetration testing automation")
+    sub = parser.add_subparsers(dest="command", required=True)
+
+    p = sub.add_parser("recon-passive", help="Passive recon (DNS, WHOIS, subdomains)")
+    p.add_argument("--target", required=True)
+    p.add_argument("--out")
+    p.set_defaults(func=recon_passive)
+
+    p = sub.add_parser("recon-active", help="Active recon (port scanning)")
+    p.add_argument("--target", required=True)
+    p.add_argument("--ports", choices=["top1000", "all"], default="top1000")
+    p.add_argument("--out")
+    p.set_defaults(func=recon_active)
+
+    p = sub.add_parser("enumerate-web", help="Web enumeration (dirs, tech)")
+    p.add_argument("--url", required=True)
+    p.add_argument("--out")
+    p.set_defaults(func=enumerate_web)
+
+    p = sub.add_parser("vuln-scan", help="Vulnerability scanning")
+    p.add_argument("--target", required=True)
+    p.add_argument("--type", choices=["web", "infra"], default="web")
+    p.add_argument("--out")
+    p.set_defaults(func=vuln_scan)
+
+    p = sub.add_parser("check-tools", help="Show available tools on this system")
+    p.set_defaults(func=check_tools)
+
+    args = parser.parse_args()
+    args.func(args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/.github/skills/security-specialist/scripts/rank_files.py b/.github/skills/security-specialist/scripts/rank_files.py
new file mode 100644
index 0000000..3e6619d
--- /dev/null
+++ b/.github/skills/security-specialist/scripts/rank_files.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+"""Rank repository files by security relevance for analysis prioritization."""
+
+import argparse
+import json
+import subprocess
+from pathlib import Path
+
+SKIP_DIRS = {"node_modules", "vendor", ".git", "__pycache__", "dist", "build", ".next", "coverage", ".venv", "venv"}
+SKIP_PATTERNS = {"test", "tests", "spec", "specs", "__tests__", "fixtures", "mocks", "generated"}
+
+HIGH_KEYWORDS = ("auth", "login", "session", "token", "password", "secret", "crypto", "permission")
+MEDIUM_HIGH_KEYWORDS = ("api", "handler", "controller", "route", "endpoint", "middleware")
+MEDIUM_KEYWORDS = ("config", "env", "settings", "database", "migration")
+
+CODE_EXTENSIONS = {".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java", ".rb", ".php", ".c", ".cpp", ".h", ".cs", ".yml", ".yaml", ".toml", ".json", ".env"}
+
+
+def _should_skip(path: Path) -> bool:
+    parts = set(path.parts)
+    if parts & SKIP_DIRS:
+        return True
+    return bool(parts & SKIP_PATTERNS)
+
+
+def _score(path: str) -> tuple[int, str]:
+    low = path.lower()
+    for kw in HIGH_KEYWORDS:
+        if kw in low:
+            return 5, f"contains '{kw}' — security-sensitive"
+    for kw in MEDIUM_HIGH_KEYWORDS:
+        if kw in low:
+            return 4, f"contains '{kw}' — attack surface"
+    for kw in MEDIUM_KEYWORDS:
+        if kw in low:
+            return 3, f"contains '{kw}' — configuration"
+    return 1, "general code"
+
+
+def cmd_from_repo(args: argparse.Namespace) -> None:
+    """Walk repo and score all code files."""
+    repo = Path(args.repo).resolve()
+    results = []
+    for f in repo.rglob("*"):
+        if not f.is_file() or f.suffix not in CODE_EXTENSIONS:
+            continue
+        rel = f.relative_to(repo)
+        if _should_skip(rel):
+            continue
+        priority, reason = _score(str(rel))
+        results.append({"path": str(rel), "priority": priority, "reason": reason})
+    results.sort(key=lambda x: -x["priority"])
+    Path(args.out).write_text(json.dumps(results, indent=2))
+    print(f"Ranked {len(results)} files → {args.out}")
+
+
+def cmd_from_diff(args: argparse.Namespace) -> None:
+    """Rank files changed between two git refs."""
+    repo = Path(args.repo).resolve()
+    result = subprocess.run(
+        ["git", "diff", "--name-only", args.base, args.head],
+        capture_output=True, text=True, cwd=str(repo), check=True,
+    )
+    results = []
+    for line in result.stdout.strip().splitlines():
+        rel = Path(line)
+        if _should_skip(rel) or rel.suffix not in CODE_EXTENSIONS:
+            continue
+        priority, reason = _score(line)
+        results.append({"path": line, "priority": priority, "reason": reason})
+    results.sort(key=lambda x: -x["priority"])
+    Path(args.out).write_text(json.dumps(results, indent=2))
+    print(f"Ranked {len(results)} changed files → {args.out}")
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description="Rank files by security relevance")
+    sub = parser.add_subparsers(dest="command", required=True)
+
+    p_repo = sub.add_parser("from-repo")
+    p_repo.add_argument("--repo", required=True)
+    p_repo.add_argument("--out", required=True)
+
+    p_diff = sub.add_parser("from-diff")
+    p_diff.add_argument("--repo", required=True)
+    p_diff.add_argument("--base", required=True)
+    p_diff.add_argument("--head", required=True)
+    p_diff.add_argument("--out", required=True)
+
+    args = parser.parse_args()
+    {"from-repo": cmd_from_repo, "from-diff": cmd_from_diff}[args.command](args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/.github/skills/security-specialist/scripts/scan_db.py b/.github/skills/security-specialist/scripts/scan_db.py
new file mode 100644
index 0000000..ce6334d
--- /dev/null
+++ b/.github/skills/security-specialist/scripts/scan_db.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+"""SQLite-based security scan database manager."""
+
+import argparse
+import json
+import sqlite3
+import uuid
+from datetime import datetime, timezone
+from pathlib import Path
+
+SCHEMA = """
+CREATE TABLE IF NOT EXISTS scans (
+    id TEXT PRIMARY KEY,
+    repo_path TEXT NOT NULL,
+    started_at TEXT NOT NULL,
+    sealed_at TEXT,
+    status TEXT NOT NULL CHECK(status IN ('active', 'sealed'))
+);
+
+CREATE TABLE IF NOT EXISTS findings (
+    id TEXT PRIMARY KEY,
+    scan_id TEXT NOT NULL REFERENCES scans(id),
+    title TEXT NOT NULL,
+    severity TEXT NOT NULL CHECK(severity IN ('critical', 'high', 'medium', 'low', 'info')),
+    category TEXT NOT NULL,
+    status TEXT NOT NULL DEFAULT 'open'
+        CHECK(status IN ('open', 'fixed', 'false-positive', 'accepted-risk', 'tracked')),
+    file_path TEXT,
+    line_number INTEGER,
+    description TEXT,
+    evidence TEXT,
+    created_at TEXT NOT NULL,
+    tracking_url TEXT,
+    notes TEXT
+);
+"""
+
+
+def _connect(repo: str) -> sqlite3.Connection:
+    db_path = Path(repo) / ".security" / "scan.db"
+    if not db_path.exists():
+        raise SystemExit(f"Database not found: {db_path}")
+    conn = sqlite3.connect(str(db_path))
+    conn.row_factory = sqlite3.Row
+    return conn
+
+
+def _now() -> str:
+    return datetime.now(timezone.utc).isoformat()
+
+
+def cmd_init(args: argparse.Namespace) -> None:
+    """Initialize .security/scan.db and create a new scan record."""
+    sec_dir = Path(args.repo) / ".security"
+    sec_dir.mkdir(parents=True, exist_ok=True)
+    db_path = sec_dir / "scan.db"
+    conn = sqlite3.connect(str(db_path))
+    conn.executescript(SCHEMA)
+    scan_id = str(uuid.uuid4())
+    conn.execute(
+        "INSERT INTO scans (id, repo_path, started_at, status) VALUES (?, ?, ?, ?)",
+        (scan_id, str(Path(args.repo).resolve()), _now(), "active"),
+    )
+    conn.commit()
+    conn.close()
+    print(json.dumps({"scan_id": scan_id, "db": str(db_path)}))
+
+
+def cmd_add_finding(args: argparse.Namespace) -> None:
+    """Insert a finding into the database."""
+    conn = _connect(args.repo)
+    finding_id = str(uuid.uuid4())
+    conn.execute(
+        """INSERT INTO findings
+           (id, scan_id, title, severity, category, status, file_path, line_number,
+            description, evidence, created_at)
+           VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?)""",
+        (finding_id, args.scan_id, args.title, args.severity, args.category,
+         args.file, args.line, args.description, args.evidence, _now()),
+    )
+    conn.commit()
+    conn.close()
+    print(json.dumps({"finding_id": finding_id}))
+
+
+def cmd_list_findings(args: argparse.Namespace) -> None:
+    """List findings as JSON, with optional filters."""
+    conn = _connect(args.repo)
+    query = "SELECT * FROM findings WHERE scan_id = ?"
+    params: list = [args.scan_id]
+    if args.severity:
+        query += " AND severity = ?"
+        params.append(args.severity)
+    if args.status:
+        query += " AND status = ?"
+        params.append(args.status)
+    rows = conn.execute(query, params).fetchall()
+    conn.close()
+    print(json.dumps([dict(r) for r in rows], indent=2))
+
+
+def cmd_update_status(args: argparse.Namespace) -> None:
+    """Update a finding's status and optional tracking metadata."""
+    conn = _connect(args.repo)
+    parts = ["status = ?"]
+    params: list = [args.status]
+    if args.tracking_url:
+        parts.append("tracking_url = ?")
+        params.append(args.tracking_url)
+    if args.note:
+        parts.append("notes = ?")
+        params.append(args.note)
+    params.append(args.finding_id)
+    conn.execute(f"UPDATE findings SET {', '.join(parts)} WHERE id = ?", params)
+    conn.commit()
+    conn.close()
+    print(json.dumps({"updated": args.finding_id}))
+
+
+def cmd_stats(args: argparse.Namespace) -> None:
+    """Print severity/category/status counts for a scan."""
+    conn = _connect(args.repo)
+    result: dict = {"by_severity": {}, "by_category": {}, "by_status": {}}
+    for col, key in [("severity", "by_severity"), ("category", "by_category"), ("status", "by_status")]:
+        rows = conn.execute(
+            f"SELECT {col}, COUNT(*) as cnt FROM findings WHERE scan_id = ? GROUP BY {col}",
+            (args.scan_id,),
+        ).fetchall()
+        result[key] = {r[col]: r["cnt"] for r in rows}
+    conn.close()
+    print(json.dumps(result, indent=2))
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description="Security scan database manager")
+    sub = parser.add_subparsers(dest="command", required=True)
+
+    p_init = sub.add_parser("init")
+    p_init.add_argument("--repo", required=True)
+
+    p_add = sub.add_parser("add-finding")
+    p_add.add_argument("--repo", required=True)
+    p_add.add_argument("--scan-id", required=True)
+    p_add.add_argument("--title", required=True)
+    p_add.add_argument("--severity", required=True, choices=["critical", "high", "medium", "low", "info"])
+    p_add.add_argument("--category", required=True)
+    p_add.add_argument("--file", required=True)
+    p_add.add_argument("--line", type=int, required=True)
+    p_add.add_argument("--description", required=True)
+    p_add.add_argument("--evidence", required=True)
+
+    p_list = sub.add_parser("list-findings")
+    p_list.add_argument("--repo", required=True)
+    p_list.add_argument("--scan-id", required=True)
+    p_list.add_argument("--severity", choices=["critical", "high", "medium", "low", "info"])
+    p_list.add_argument("--status", choices=["open", "fixed", "false-positive", "accepted-risk", "tracked"])
+
+    p_upd = sub.add_parser("update-status")
+    p_upd.add_argument("--repo", required=True)
+    p_upd.add_argument("--finding-id", required=True)
+    p_upd.add_argument("--status", required=True, choices=["open", "fixed", "false-positive", "accepted-risk", "tracked"])
+    p_upd.add_argument("--tracking-url")
+    p_upd.add_argument("--note")
+
+    p_stats = sub.add_parser("stats")
+    p_stats.add_argument("--repo", required=True)
+    p_stats.add_argument("--scan-id", required=True)
+
+    args = parser.parse_args()
+    {"init": cmd_init, "add-finding": cmd_add_finding, "list-findings": cmd_list_findings,
+     "update-status": cmd_update_status, "stats": cmd_stats}[args.command](args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/.github/skills/security-specialist/scripts/validate-findings.cjs b/.github/skills/security-specialist/scripts/validate-findings.cjs
new file mode 100644
index 0000000..4ee27be
--- /dev/null
+++ b/.github/skills/security-specialist/scripts/validate-findings.cjs
@@ -0,0 +1,150 @@
+#!/usr/bin/env node
+/**
+ * Valida findings.json contra report-schema.json.
+ * Usage: node validate-findings.cjs 
+ *
+ * Zero dependências. Exit 0 = success, exit 1 = falha.
+ */
+const fs = require("fs");
+const path = require("path");
+
+const file = process.argv[2];
+if (!file) {
+  console.error("Usage: node validate-findings.cjs ");
+  process.exit(1);
+}
+
+const schemaPath = path.join(__dirname, "..", "references", "report-schema.json");
+let itemSchema;
+try {
+  const doc = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
+  itemSchema = doc.output_schema;
+  if (!itemSchema) throw new Error('report-schema.json missing "output_schema"');
+} catch (e) {
+  console.error(`Failed to load schema from ${schemaPath}:`, e.message);
+  process.exit(1);
+}
+
+let findings;
+try {
+  findings = JSON.parse(fs.readFileSync(file, "utf8"));
+} catch (e) {
+  console.error("Failed to parse JSON:", e.message);
+  process.exit(1);
+}
+
+if (!Array.isArray(findings)) {
+  console.error("findings.json must be an array");
+  process.exit(1);
+}
+
+function typeOf(v) {
+  if (Array.isArray(v)) return "array";
+  if (v === null) return "null";
+  return typeof v;
+}
+
+function findDiscriminator(schema) {
+  if (!schema.properties) return null;
+  for (const [key, sub] of Object.entries(schema.properties)) {
+    if (sub && Object.prototype.hasOwnProperty.call(sub, "const")) {
+      return { key, value: sub.const };
+    }
+  }
+  return null;
+}
+
+function validate(value, schema, p, errors) {
+  if (schema.oneOf) {
+    for (const branch of schema.oneOf) {
+      const disc = findDiscriminator(branch);
+      if (disc && value && typeof value === "object" && value[disc.key] === disc.value) {
+        validate(value, branch, p, errors);
+        return;
+      }
+    }
+    const discs = schema.oneOf.map(findDiscriminator).filter(Boolean);
+    if (discs.length === schema.oneOf.length && value && typeof value === "object") {
+      const key = discs[0].key;
+      const allowed = discs.map((d) => JSON.stringify(d.value)).join(", ");
+      errors.push(`${p}: "${key}" must be one of ${allowed}, got ${JSON.stringify(value[key])}`);
+      return;
+    }
+    errors.push(`${p}: does not match exactly one of the allowed schemas`);
+    return;
+  }
+
+  if (Object.prototype.hasOwnProperty.call(schema, "const") && value !== schema.const) {
+    errors.push(`${p}: must equal ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`);
+  }
+  if (schema.enum && !schema.enum.includes(value)) {
+    errors.push(`${p}: invalid value ${JSON.stringify(value)} (expected one of ${schema.enum.join(", ")})`);
+  }
+
+  switch (schema.type) {
+    case "object": {
+      if (typeOf(value) !== "object") { errors.push(`${p}: expected object, got ${typeOf(value)}`); return; }
+      for (const req of schema.required || []) {
+        if (!(req in value)) errors.push(`${p}: missing required field "${req}"`);
+      }
+      for (const key of Object.keys(value)) {
+        if (schema.properties && key in schema.properties) {
+          validate(value[key], schema.properties[key], `${p}.${key}`, errors);
+        } else if (schema.additionalProperties === false) {
+          errors.push(`${p}: unexpected field "${key}"`);
+        }
+      }
+      break;
+    }
+    case "array": {
+      if (typeOf(value) !== "array") { errors.push(`${p}: expected array, got ${typeOf(value)}`); return; }
+      if (typeof schema.minItems === "number" && value.length < schema.minItems) {
+        errors.push(`${p}: must have at least ${schema.minItems} item(s), got ${value.length}`);
+      }
+      if (schema.items) {
+        value.forEach((el, i) => validate(el, schema.items, `${p}[${i}]`, errors));
+      }
+      break;
+    }
+    case "integer": {
+      if (typeOf(value) !== "number" || !Number.isInteger(value)) {
+        errors.push(`${p}: expected integer, got ${typeOf(value)}`);
+      }
+      break;
+    }
+    case "string": {
+      if (typeOf(value) !== "string") errors.push(`${p}: expected string, got ${typeOf(value)}`);
+      break;
+    }
+  }
+}
+
+let errorCount = 0;
+findings.forEach((f, i) => {
+  const label = `[${i}] ${(f && (f.title || f.reason)) || "(untitled)"}`;
+  console.log(`Checking ${label}`);
+  const errs = [];
+  validate(f, itemSchema, `[${i}]`, errs);
+
+  // Semantic: confirmed trace must start at entrypoint and end at sink
+  if (f && f.verdict === "confirmed" && Array.isArray(f.trace) && f.trace.length > 0) {
+    if (f.trace[0] && f.trace[0].kind !== "entrypoint") {
+      errs.push(`[${i}].trace[0].kind must be "entrypoint", got ${JSON.stringify(f.trace[0].kind)}`);
+    }
+    const last = f.trace.length - 1;
+    if (f.trace[last] && f.trace[last].kind !== "sink") {
+      errs.push(`[${i}].trace[${last}].kind must be "sink", got ${JSON.stringify(f.trace[last].kind)}`);
+    }
+  }
+
+  for (const msg of errs) console.error("  ERROR:", msg);
+  errorCount += errs.length;
+});
+
+console.log();
+if (errorCount === 0) {
+  console.log(`PASS: ${findings.length} findings valid`);
+} else {
+  console.error(`FAIL: ${errorCount} error(s) across ${findings.length} findings`);
+  process.exit(1);
+}
diff --git a/.github/skills/security-specialist/steering/attack-paths.md b/.github/skills/security-specialist/steering/attack-paths.md
new file mode 100644
index 0000000..b1fda78
--- /dev/null
+++ b/.github/skills/security-specialist/steering/attack-paths.md
@@ -0,0 +1,150 @@
+# Attack Path Tracing
+
+## Purpose
+
+Map the exploitation path for a confirmed or suspected vulnerability. Starts from a finding, traces backward to entry point and forward to impact. Produces a realistic severity assessment based on actual exploitability — not theoretical worst-case.
+
+## When to Use
+
+- A finding from `steering/full-scan.md` or `steering/discovery.md` needs severity validation
+- Triaging whether a vulnerability is actually reachable
+- Building proof-of-concept narratives for critical findings
+- Disputing or confirming a severity rating
+
+## Step 1: Start from the Finding
+
+Document the vulnerability anchor:
+
+- **What:** The vulnerable code (file, line, function)
+- **Class:** Injection, auth bypass, IDOR, SSRF, path traversal, etc.
+- **Primitive:** What the attacker gains if this fires (read arbitrary data, execute code, escalate privilege)
+
+## Step 2: Trace Entry Point → Vulnerability
+
+Work backward. How does attacker-controlled input reach the vulnerable code?
+
+Map the chain:
+
+```
+Entry Point (HTTP route, message queue, CLI arg)
+  → Input processing (parsing, deserialization)
+    → Validation (what checks exist between entry and sink)
+      → Intermediate transforms (encoding, type conversion, mapping)
+        → Vulnerable code (the sink)
+```
+
+At each hop, document:
+- What data flows through
+- What transformations or filters apply
+- Whether the attacker retains control over the data
+
+If a hop breaks the chain (e.g., input is cast to integer before reaching SQL query), the path is dead. Document why and downgrade.
+
+## Step 3: Identify Prerequisites
+
+What must be true for the attack to work?
+
+| Factor | Questions |
+|---|---|
+| **Authentication** | Does the attacker need a valid session? What role? |
+| **Network position** | Must they be on the internet, internal network, localhost? |
+| **Application state** | Does a specific condition need to exist (feature flag, data in DB)? |
+| **Race condition** | Is timing critical? How tight is the window? |
+| **User interaction** | Does a victim need to click/visit something? |
+| **Chaining** | Does this require another vulnerability to be exploitable first? |
+
+Each prerequisite reduces exploitability. Stack them honestly.
+
+## Step 4: Map Impact Forward
+
+From the vulnerable code, what happens when it fires?
+
+```
+Vulnerable code triggers
+  → Immediate effect (SQL executes, file reads, command runs)
+    → Data accessed/modified (what exactly)
+      → Lateral movement possible? (pivot to other services, escalate)
+        → Final impact (data breach, RCE, account takeover, DoS)
+```
+
+Be specific about impact scope:
+- Single user's data vs. all users
+- Read-only vs. read-write
+- Contained to one service vs. cross-service pivot
+- Persistent vs. one-shot
+
+## Step 5: Check Existing Mitigations
+
+Before finalizing severity, verify what's already blocking this path:
+
+- **WAF/rate limiting** — does it catch this payload pattern?
+- **Framework protections** — auto-escaping, parameterized queries, CSRF tokens
+- **Network policy** — is the target service isolated?
+- **Monitoring/alerting** — would exploitation trigger alerts?
+- **Input validation upstream** — is there a check we missed?
+
+If mitigations exist, document them and assess residual risk. A mitigated path is still a finding (defense in depth matters), but severity drops.
+
+## Step 6: Assign Severity
+
+Use this matrix — exploitability × impact:
+
+| | Critical Impact | High Impact | Medium Impact | Low Impact |
+|---|---|---|---|---|
+| **Easy to exploit** (unauth, no prereqs) | Critical | High | Medium | Low |
+| **Moderate** (auth required, simple chain) | High | High | Medium | Low |
+| **Difficult** (multi-step chain, race, internal network) | High | Medium | Low | Info |
+| **Very difficult** (requires prior RCE, admin, physical) | Medium | Low | Info | Info |
+
+Impact levels:
+- **Critical** — RCE, full data breach, complete auth bypass
+- **High** — significant data exposure, privilege escalation, account takeover
+- **Medium** — limited data leak, single-user impact, partial bypass
+- **Low** — information disclosure, minor integrity issue
+
+## Step 7: Document the Path
+
+Output format:
+
+```
+## Attack Path: 
+
+**Finding Reference:** <link to finding in scan DB>
+**Final Severity:** <Critical/High/Medium/Low/Info>
+
+### Chain
+
+1. Attacker sends [specific input] to [entry point]
+2. Input passes through [component] where [transform happens]
+3. Reaches [vulnerable code] at [file:line]
+4. Triggers [primitive] resulting in [immediate effect]
+5. Attacker gains [final impact]
+
+### Prerequisites
+- [List each requirement]
+
+### Mitigations Present
+- [List what partially blocks this]
+
+### Mitigations Absent
+- [List what should exist but doesn't]
+
+### Evidence
+[Code snippets, data flow diagram, or PoC outline]
+```
+
+## Step 8: Recommend Action
+
+Based on the path analysis:
+
+- **Critical/High with easy exploit** → Fix immediately, consider if already exploited
+- **Medium** → Fix in next sprint, add detection
+- **Low/Info** → Track, fix opportunistically
+- **Mitigated but structurally present** → Harden, don't ignore. Mitigations fail.
+
+## Notes
+
+- Real severity comes from the PATH, not the pattern. SQLi behind three auth gates and only reaching a public data table is not critical.
+- Conversely, a "low-severity" IDOR that leaks all customer records is critical regardless of what the textbook says about IDORs.
+- If you can't trace a complete path from entry to impact, the finding might be theoretical. Say so explicitly rather than inflating.
+- Attack paths compound. Two medium findings that chain into a critical outcome should be reported as critical.
diff --git a/.github/skills/security-specialist/steering/diff-review.md b/.github/skills/security-specialist/steering/diff-review.md
new file mode 100644
index 0000000..683f951
--- /dev/null
+++ b/.github/skills/security-specialist/steering/diff-review.md
@@ -0,0 +1,126 @@
+# Security-Focused Diff Review
+
+## Purpose
+
+Review a code diff (PR, commit range, branch comparison) for security regressions. Lighter and faster than a full scan — scoped strictly to what changed.
+
+## Step 1: Obtain the Diff
+
+Identify what you're reviewing:
+- PR: the full diff between base and head
+- Commit: single commit's changeset
+- Branch comparison: `git diff base..head`
+
+Read the diff in its entirety. Don't skip files — even test changes can reveal security assumptions.
+
+## Step 2: Understand Context
+
+Before hunting bugs, understand intent:
+- What feature/fix does this change implement?
+- What's the PR description / commit message saying?
+- Which components are touched?
+
+This prevents false positives from misunderstanding purpose.
+
+## Step 3: Map Changed Attack Surface
+
+Identify which changes affect security-relevant areas:
+
+| Change Type | Security Relevance |
+|---|---|
+| New HTTP route/endpoint | New attack surface — needs auth + input validation check |
+| Modified auth logic | Possible bypass, privilege escalation |
+| New user input accepted | Injection, XSS, path traversal surface |
+| Database query changes | SQL/NoSQL injection |
+| File I/O changes | Path traversal, TOCTOU |
+| Dependency added/updated | Known CVEs, supply chain risk |
+| Config/env changes | Secret exposure, permissive settings |
+| Error handling changes | Information disclosure |
+| Crypto changes | Weak algorithms, key mishandling |
+| Logging changes | Sensitive data in logs |
+
+## Step 4: Security Regression Checklist
+
+For each changed file, check:
+
+### Input Handling
+- [ ] New inputs validated before use?
+- [ ] Existing validation still applies after refactor?
+- [ ] Type coercion handled safely?
+- [ ] Size/length limits enforced?
+
+### Authentication & Authorization
+- [ ] New endpoints require auth?
+- [ ] Permission checks not accidentally removed?
+- [ ] Auth bypass possible through new code paths?
+- [ ] Token/session handling unchanged or improved?
+
+### Data Exposure
+- [ ] No secrets added to code (API keys, passwords, tokens)
+- [ ] No sensitive data in new log statements
+- [ ] Error messages don't leak internals
+- [ ] New API responses don't over-expose data
+
+### Dependencies
+- [ ] New deps checked for known CVEs
+- [ ] Version pinned (not floating ranges)
+- [ ] Dep source is legitimate (not typosquat)
+
+### Crypto & Secrets
+- [ ] No hardcoded keys or salts
+- [ ] Crypto usage correct (proper modes, IV handling, key derivation)
+- [ ] Secrets accessed through proper secret management
+
+## Step 5: Produce Findings
+
+For each issue, document:
+
+```
+### [SEVERITY] Title
+
+**File:** path/to/file.ext L42-48
+**Change:** What was modified
+**Issue:** What's wrong, specifically
+**Attack:** How this gets exploited
+**Fix:** Concrete remediation
+
+Evidence:
+\`\`\`
+<the vulnerable code from the diff>
+\`\`\`
+```
+
+## Step 6: Assess Severity in Context
+
+Diff review severity considers:
+- Is this code deployed yet? (PR = pre-deploy, post-merge = live)
+- Does existing infrastructure mitigate? (WAF, rate limiting, network isolation)
+- Is the vulnerable path reachable without auth?
+- What data is at risk?
+
+Don't inflate severity. A medium finding behind two auth gates isn't critical just because the code pattern looks bad.
+
+## Step 7: Summary Verdict
+
+End with a clear recommendation:
+
+- **APPROVE** — no security issues found
+- **APPROVE WITH NOTES** — informational findings, no blockers
+- **REQUEST CHANGES** — security issues that must be fixed before merge
+- **BLOCK** — critical vulnerability, must not merge
+
+Include the finding count by severity and the single most important issue if requesting changes.
+
+## Output Format
+
+Deliver as markdown. Structure:
+1. One-line verdict (approve/block/changes needed)
+2. Scope summary (files reviewed, what the change does)
+3. Findings (if any), ordered by severity
+4. Notes (informational observations, future concerns)
+
+## Notes
+
+- Review test files too — they often reveal what the developer thinks the security boundary is (and where they're wrong).
+- Deleted code matters. Removed validation, removed auth checks, removed error handling — these are findings.
+- If the diff touches auth or crypto and you can't fully assess impact from the diff alone, say so. Recommend a broader review.
diff --git a/.github/skills/security-specialist/steering/discovery.md b/.github/skills/security-specialist/steering/discovery.md
new file mode 100644
index 0000000..b989364
--- /dev/null
+++ b/.github/skills/security-specialist/steering/discovery.md
@@ -0,0 +1,142 @@
+# Targeted Vulnerability Discovery
+
+## Purpose
+
+Focused security analysis on a subset of files. Used after a threat model identifies high-risk components, after a dependency alert, or when investigating a specific concern. More surgical than a full scan — assumes you already know WHERE to look.
+
+## When to Use
+
+- Threat model flagged specific components as high-risk
+- A new entry point or data flow was added
+- Dependency alert requires assessing blast radius
+- Post-incident investigation of specific modules
+- Reviewer wants depth on auth, payments, or other critical subsystems
+
+## Step 1: Receive Target Scope
+
+Input is one of:
+- A file list (explicit paths)
+- A component/module name (resolve to files)
+- A directory subtree
+- A functional area ("all auth code", "payment processing")
+
+If given a vague scope, resolve to concrete files before proceeding.
+
+## Step 2: Generate Ranked Worklist
+
+```bash
+python3 scripts/rank_files.py --files <file1> <file2> ... --output worklist.json
+```
+
+The ranker scores files by:
+- Proximity to entry points (routes, handlers, consumers)
+- Presence of security-sensitive patterns (SQL, exec, file I/O, crypto, auth checks)
+- Complexity metrics (cyclomatic complexity, line count)
+- History of changes (frequently modified = higher churn risk)
+
+Output: ordered list of files with priority scores and reason tags.
+
+## Step 3: Analyze Each File
+
+Work through the worklist in priority order. For each file:
+
+### 3a. Understand Role
+- What does this file do in the system?
+- What data flows through it?
+- Who calls it? What does it call?
+- What trust level is the caller at?
+
+### 3b. Check Input Boundaries
+- Where does external data enter this code?
+- Is it validated before use? (type, format, length, range)
+- Are there implicit assumptions about input shape?
+
+### 3c. Check Security Controls
+- Authentication enforced? At what level?
+- Authorization checked? Against what?
+- Rate limiting present?
+- Error handling safe? (no stack traces, no sensitive data in errors)
+
+### 3d. Check Dangerous Operations
+- SQL/NoSQL queries — parameterized or string-built?
+- Command execution — input reaches shell?
+- File operations — path controlled by user?
+- Deserialization — untrusted data deserialized?
+- Crypto usage — correct algorithms, modes, key management?
+- Logging — sensitive data written to logs?
+
+### 3e. Check Framework-Specific Issues
+
+Adapt to the stack:
+- **Node/Express** — prototype pollution, ReDoS, missing helmet headers
+- **Python/Django/Flask** — template injection, pickle deserialization, debug mode
+- **Go** — integer overflow, unsafe pointer use, goroutine leaks with user input
+- **Java/Spring** — SpEL injection, XXE in XML parsing, actuator exposure
+- **Ruby/Rails** — mass assignment, unsafe render, YAML deserialization
+- **Rust** — unsafe blocks, FFI boundary issues, panic in handlers
+
+## Step 4: Record Findings
+
+For each issue discovered:
+
+```bash
+python3 scripts/scan_db.py add-finding \
+  --severity <critical|high|medium|low|info> \
+  --category <auth|injection|crypto|data-exposure|config|logic> \
+  --file <relative-path> \
+  --line <line-number> \
+  --title "<concise title>" \
+  --evidence "<vulnerable code snippet>" \
+  --impact "<what an attacker gains>" \
+  --recommendation "<specific fix, not generic advice>"
+```
+
+### Evidence Standard
+
+Every finding requires:
+- Exact location (file + line range)
+- The vulnerable code, quoted
+- A concrete attack scenario: "An attacker with [access level] sends [input] to [endpoint], which reaches [this code] and causes [effect]"
+- Why existing protections (if any) don't prevent it
+
+## Step 5: Cross-Reference
+
+After analyzing all files in the worklist:
+
+- Do any findings chain together? (e.g., IDOR + missing auth = account takeover)
+- Do findings contradict the threat model assumptions?
+- Are there patterns? (same mistake repeated = systemic issue, not one-off)
+
+If chains exist, document them using `steering/attack-paths.md`.
+
+## Step 6: Report Findings
+
+Output a summary scoped to this discovery pass:
+
+```
+## Discovery: <Area Name>
+Date: <date>
+Scope: <file count> files in <component>
+Findings: <count by severity>
+
+### Critical
+### High
+### Medium
+### Low
+### Observations (no finding, but notable)
+```
+
+## Completion Criteria
+
+Discovery is complete when:
+- Every file in the worklist has been analyzed
+- All findings are recorded in the scan DB
+- Cross-references and chains are documented
+- No file was skipped without explicit justification
+
+## Notes
+
+- Discovery is depth-first, not breadth-first. Go deep on each file rather than skimming many.
+- If a file pulls in dependencies you haven't seen, follow the call chain. Vulnerabilities hide in utility code.
+- "No findings" for a critical file is a valid and useful result. Record it — confirms the component is clean as of this review.
+- If you discover the scope should be wider (e.g., auth module calls a helper that's not in the target list), expand and document why.
diff --git a/.github/skills/security-specialist/steering/full-scan.md b/.github/skills/security-specialist/steering/full-scan.md
new file mode 100644
index 0000000..2b90401
--- /dev/null
+++ b/.github/skills/security-specialist/steering/full-scan.md
@@ -0,0 +1,188 @@
+# Full Repository Security Scan
+
+## Propósito
+
+Auditoria de segurança estruturada de um repositório inteiro. Pipeline de 6 fases com agentes paralelos, validação adversarial, e verificação independente.
+
+---
+
+## Pipeline de 6 Fases
+
+```
+Phase 1: Recon     → architecture.md (agentes paralelos mapeiam o alvo)
+Phase 2: Hunt      → findings brutos (agentes paralelos por attack class)
+Phase 3: Validate  → findings confirmados (adversarial — tenta DISprovar)
+Phase 4: Report    → security-report.html + report.json
+Phase 5: Schema    → findings.json validado contra report-schema.json
+Phase 6: Verify    → verificação independente de cada claim factual
+```
+
+---
+
+## Phase 1: Reconnaissance
+
+Lance **múltiplos agentes em paralelo** para mapear aspectos diferentes do codebase:
+
+**Agent 1a: Overview, stack e baseline comparável**
+- O que é esta aplicação? Que tipo de software?
+- Quem usa e como? (end users, devs, operadores, outros services)
+- Tech stack? (languages, frameworks, databases, runtime, deployment model)
+- Qual software mainstream comparável existe? Que tradeoffs de security o comparável aceita?
+- Estrutura de diretórios high-level com file paths para entry points chave
+
+**Agent 1b: Trust boundaries e access control**
+- Trust boundaries — onde input não-confiável entra? (HTTP, CLI, file reads, IPC, message queues, env vars, config)
+- Authentication — como callers provam identidade?
+- Authorization — como permissions são enforced?
+- Privilege separation — roda como root? Drop privileges? Sandboxing?
+- Bypass mechanisms (dev-only modes, test helpers, setup flows, debug flags)
+
+**Agent 1c: Input surface inventory**
+- Network-facing surfaces (HTTP endpoints, gRPC, WebSocket, TCP/UDP) com method/verb e propósito
+- File-based input (uploads, config parsing, import/export)
+- IPC e inter-service (message queues, shared memory, Unix sockets, env vars, CLI args)
+- User-generated content surfaces
+- External integrations (OAuth, webhooks, third-party APIs, plugin loading, dynamic code execution)
+- Todos os lugares onde input alcança dangerous sinks
+
+### Síntese
+
+Colete outputs dos 3 agentes e sintetize em `architecture.md`:
+- 1-2 páginas com application type, tech stack, trust model, input surfaces, baseline comparável
+- Key file paths de todos agentes — starting points para Phase 2
+- Se codebase é maior/mais complexo que esperado (plugin system, multi-tenant, complex auth chains), lance agentes adicionais antes de prosseguir
+
+### Multi-Run Additive
+
+Se runs anteriores existem (cheque `.security/scans/`):
+1. **Skip known findings** — não re-descubra o mesmo bug. Mencione prior findings no report mas foque hunting em ground novo.
+2. **Target gaps** — se runs anteriores focaram em injection e auth, pese este run para business logic, creative attacks, e wildcard.
+3. **Resolve disagreements** — se runs anteriores deram verdicts conflitantes no mesmo finding, valide definitivamente.
+
+Se nenhum run anterior existe, note no report que coverage melhora com runs adicionais.
+
+---
+
+## Phase 2: Hunt
+
+Siga `steering/hunting.md` para:
+- Selecionar attack classes relevantes ao application type
+- Lançar agentes paralelos (um por classe × subsistema)
+- Cada agente recebe architecture.md + hunting methodology + validation rules
+- Agentes podem spawnar sub-agents para deep dives
+
+---
+
+## Phase 3: Validate (Adversarial)
+
+**Consolidar duplicatas primeiro** — Phase 2 deliberadamente overlapa scopes.
+
+Para cada finding restante, lance um **agente de validação separado** que tenta **DISprovar** o finding:
+
+```
+Seu trabalho é DISPROVAR este finding. Leia o source code real em cada step.
+Se não conseguir disprovar, confirme com o código exato que o torna explorável.
+
+Retorne um de:
+- "CONFIRMED: [explicação com code evidence]"
+- "REJECTED: [o que o finding errou, com code evidence]"
+```
+
+**Testes de validação:**
+1. **Exploitation test**: Leia o código real em cada step do trace. O data flow funciona como claimed? Pode construir o exact input que triggera?
+2. **Impact test**: O que o atacante realmente ganha? Se "aprende field names" ou "causa error" = LOW máximo.
+3. **Baseline test**: O comparável tem o mesmo pattern? Se sim, foi explorado? Se nunca explorado em anos de produção, entenda por quê antes de reportar.
+4. **Mitigation test**: Existe outra layer que previne exploitation? Cheque middleware, DB constraints, framework defaults.
+5. **Parser/runtime behavior test**: Se o exploit depende de como parser/runtime handles input específico, verifique contra spec ou implementação — não reasoning from intuition.
+
+**Kill false positives agressivamente, mas não mate findings reais.** Report curto com 3 findings reais vale mais que report longo com 30 teóricos.
+
+---
+
+## Phase 4: Report
+
+Gere o report usando `steering/reporting.md`. Siga `references/report-format.md` para o HTML.
+
+Adições ao report padrão para full-scan com pipeline:
+- Seção de coverage: quais attack classes foram exercitadas, quais subsistemas
+- Seção de findings rejeitados (colapsável): mostra rigor sem cluttering findings reais
+- Positive patterns: o que o codebase faz bem (calibra confiança na auditoria)
+
+---
+
+## Phase 5: Structured Output e Schema Check
+
+Para cada finding que sobreviveu Phase 3, produza JSON conformando ao schema em `references/report-schema.json`.
+
+1. Leia `references/report-schema.json` antes de escrever output. Siga exatamente — `additionalProperties: false` enforced.
+2. Para cada finding, popule todo required field. Se não pode preencher `trace` com real file paths e line numbers verificados, o finding não está suficientemente verificado — volte e verifique ou rejeite.
+3. Valide com: `node scripts/validate-findings.cjs <output>/findings.json`
+4. Fix qualquer falha antes de prosseguir.
+
+Escreva em: `.security/scans/<timestamp>/findings.json`
+
+---
+
+## Phase 6: Independent Verification
+
+O structured output de Phase 5 força self-validation, mas o mesmo agente que escreveu o finding também escreveu o JSON. Esta phase usa agentes frescos para verificar independentemente.
+
+Lance **um agente por finding confirmado**, todos em paralelo:
+
+```
+Você é um verificador independente. Você NÃO escreveu este finding.
+Seu trabalho é ler o source code real e verificar que todo claim factual está correto.
+
+1. Leia file e line number citados em CADA trace step. Verifique:
+   - File existe no path citado
+   - Line number corresponde ao código descrito
+   - Scope (function name) está correto
+   - Description reflete acuradamente o que o código faz
+
+2. Verifique root_cause lendo o file citado e confirmando que o defeito descrito existe.
+
+3. Verifique execution payloads:
+   - Endpoint existe na URL claimed?
+   - HTTP method corresponde?
+   - Input passaria validation como descrito?
+   - Auth/access checks passariam como descrito?
+
+4. Verifique conditions — há pré-requisitos que o finding não mencionou?
+
+5. Cheque remediation code_changes — o fix preveniria o ataque sem quebrar funcionalidade normal?
+
+Retorne um de:
+- "VERIFIED" — todos claims checked contra source
+- "CORRECTED: [field]: [errado] → [correto]"
+- "REJECTED: [razão]"
+```
+
+Aplique correções:
+- **VERIFIED**: nenhuma mudança
+- **CORRECTED**: atualize campos específicos, re-run schema validation
+- **REJECTED**: mude verdict para `"rejected"` ou remova
+
+Após correções, reconcilie deliverables: atualize HTML report e findings.json para que não discordem.
+
+---
+
+## Inicialização e Persistência
+
+```bash
+python3 scripts/scan_db.py init --repo <path>
+```
+
+Findings são persistidos no SQLite durante todo o processo. O `finalize.py` sela ambos os formatos (JSON + HTML) no final.
+
+---
+
+## Completion Criteria
+
+O scan está completo quando:
+- [ ] Phase 1 produziu architecture.md com trust model e input surfaces
+- [ ] Phase 2 exercitou attack classes relevantes com agentes paralelos
+- [ ] Phase 3 validou adversarially cada finding (confirmado ou rejeitado)
+- [ ] Phase 4 produziu HTML report conforme template
+- [ ] Phase 5 produziu findings.json válido contra schema
+- [ ] Phase 6 verificou independentemente cada claim factual
+- [ ] Report e findings.json concordam (sem discrepâncias)
diff --git a/.github/skills/security-specialist/steering/hunting.md b/.github/skills/security-specialist/steering/hunting.md
new file mode 100644
index 0000000..e4d7ea3
--- /dev/null
+++ b/.github/skills/security-specialist/steering/hunting.md
@@ -0,0 +1,184 @@
+# Vulnerability Hunting
+
+## Propósito
+
+Caça ativa de vulnerabilidades usando agentes paralelos especializados por classe de ataque. Este é o motor principal do `full-scan` — Phase 2 na pipeline de 6 fases.
+
+## Orquestração
+
+Lance **múltiplos agentes em paralelo** via Task tool. Cada agente recebe:
+
+1. O resumo de arquitetura da Phase 1 (verbatim)
+2. A classe de ataque específica e escopo
+3. File paths relevantes como ponto de partida
+4. A hunting methodology (abaixo)
+5. As validation rules (abaixo)
+
+**Quantos agentes?** Use Phase 1 para decidir. Agentes focados produzem melhores resultados que agentes amplos. Para uma biblioteca pequena, 3-4 agentes. Para uma aplicação grande com subsistemas distintos, lance 8-12+ — divididos por classe de ataque E por subsistema.
+
+---
+
+## Attack Classes
+
+Selecione classes relevantes ao tipo de aplicação. Nem toda classe se aplica a todo codebase.
+
+### Injection
+
+Trace input não-confiável do entry point ao dangerous sink:
+
+- **Web apps**: SQL queries, HTML output, shell commands, template engines, file paths, HTTP redirects, deserialization
+- **Libraries**: funções que processam dados do caller sem validação — buffer operations, parsers, format strings
+- **CLI tools**: construção de shell commands, file path handling, interpolação de environment variables
+- **Services**: query construction, message serialization, log injection, LDAP/XPATH queries
+
+Não cheque apenas paths diretos. Procure:
+- Injection indireta: dado armazenado safe, depois retrieved e usado em contexto perigoso por código diferente
+- Injection via field names, keys, headers e metadata — não só values
+- Injection em sistemas secundários (logs, caches, search indexes, analytics)
+
+### Access Control
+
+Pode um caller fazer algo que não deveria? Vá além de verificar se permission checks existem — verifique se checam a *permissão correta* para o *recurso correto* via o *mecanismo correto*:
+
+- Existe path para o mesmo state change que checa uma permissão diferente (mais fraca)?
+- Um field no request body pode override o que o permission system pretendia restringir?
+- Existem endpoints que gate em authentication mas esquecem authorization?
+- O mesmo recurso tem múltiplos access paths com checks inconsistentes?
+- Operações bulk/batch/export/import enforcam per-item permissions?
+
+### Resource and File Handling
+
+- Path traversal (read/write fora do diretório pretendido) — incluindo via symlinks, encoded sequences, null bytes
+- SSRF (fazer a aplicação fetch URLs controladas pelo atacante) — incluindo via redirects, DNS rebinding, URL parser differentials
+- Unsafe deserialization, archive extraction (zip slip), temp file handling
+- Memory safety (se aplicável): buffer overflows, use-after-free, integer overflow
+- Race conditions em file operations (TOCTOU entre check e use)
+
+### Cryptography and Secrets
+
+- Weak randomness para valores security-critical (tokens, keys, nonces)
+- Hardcoded secrets, secrets em logs, error messages, URLs, ou client-visible responses
+- Broken key derivation, missing HMAC verification, nonce reuse
+- Timing side-channels em secret comparison
+- Misuse de crypto primitives (ECB mode, unauthenticated encryption, static IVs)
+- O que acontece quando crypto operations falham? O error path faz fallback para no-crypto?
+
+### Business Logic
+
+Onde os bugs reais se escondem. Scanners não encontram logic errors.
+
+Para cada major workflow:
+- **State machine violations**: Pode pular steps? Ir backwards? Alcançar estado inválido? Replay de um flow completed? Partial failure — se step 2 de 3 falha, step 1 é rolled back?
+- **Race conditions com business impact**: Operações concorrentes que produzem estados inválidos (double-spend, double-approve, lost updates). Foque em operações check-then-act não-atômicas.
+- **Numeric/quantity manipulation**: Negative values, zero, overflow, precision loss, type coercion string↔number.
+- **Access boundary violations**: Não "o permission check existe" mas "é o check certo para a business rule?" Input em uma operação bypass restrição enforced em operação diferente para mesmo efeito?
+- **Implicit trust assumptions**: Data de storage, config, outros componentes assumida safe porque "validamos na entrada." E se um code path diferente escreveu?
+- **Time-based logic**: Expiry checks, scheduling, rate windows, clock skew. O que acontece em boundary moments exatos? Timezone differences entre componentes?
+- **Default and fallback behavior**: Qual a security posture quando config está missing? Feature flag off? Dependência unavailable? Sistema mid-migration?
+
+### Feature Abuse and Data Leakage
+
+Features legítimas usadas para propósitos não-pretendidos. Não procure bugs no código — procure bugs no design:
+
+- **Export/backup como exfiltration**: Low-privilege user pode trigger export que inclui dados above their access? Export de outros users? Dados deleted/draft/private?
+- **Import/restore como injection**: Import pode overwrite dados existentes? Criar records que bypass validação normal? Inject em collections sem write access?
+- **Search/filter/sort como oracle**: Search queries revelam se content existe que o user não pode acessar diretamente? Filter params permitem probe de statuses/roles/fields que não deveriam ser visíveis?
+- **Enumeration via side effects**: Error messages diferem entre "não existe" e "sem acesso"? Response times diferem? Sizes? Status codes?
+- **Preview/draft/staging leakage**: Preview tokens scoped a um item ou unlock acesso mais amplo? Draft discoverable via search, RSS, sitemaps, API listing?
+- **Notification/webhook como SSRF**: User pode set notification/webhook/callback URL que o server fetches? Validado contra internal networks?
+
+### Chained Attacks and Trust Boundaries
+
+Comportamentos individualmente safe que se tornam perigosos em combinação:
+
+- **Multi-step chains**: Mapeie o que um low-privilege user CAN do, depois procure combinações. Info disclosure + IDOR + missing rate limit. Open redirect + OAuth callback = token theft.
+- **Cross-component trust gaps**: Component A valida input e passa para B. B re-valida ou confia em A? E se validação de A é sutilmente diferente do que B precisa?
+- **Second-order attacks**: Dados safe quando stored mas perigosos quando usados em contexto diferente. Field name safe em SQL vira key em JSON path expression. Slug safe em URL vira parte de file path.
+- **Scope and capability escalation**: Tokens/API keys/OAuth scopes que grant acesso mais amplo que o nome implica. Session cookies que sobrevivem role downgrade.
+- **Timing and ordering**: Usar feature antes de setup complete? Agir em resource entre soft-delete e hard-delete? Usar token entre revocation e cache expiry?
+
+### Wildcard
+
+Não recebe categoria. Recebe o codebase e a instrução de quebrá-lo. Ignore vulnerability classes padrão — outros agentes cobrem isso. Encontre o que ninguém pensou em procurar:
+
+- Código mais estranho do codebase? Por que existe? O que acontece se abusado?
+- Features half-finished/experimentais/bolted-on? Segurança mais fraca, menos review.
+- API usada de forma que o frontend nunca faria? UI constrains users, API não.
+- Endpoints/parâmetros/headers hidden ou undocumented?
+- Mix de features não desenhadas para funcionar juntas?
+- Git history: reverted security fixes, commented-out auth checks, secrets committed then removed?
+- Com valid account: máximo dano sem detecção? Corrupting data, poisoning caches, exhausting resources.
+
+### Obvious Things
+
+Outros agentes caçam bugs sutis. Este checa o "óbvio" que é fácil ignorar:
+
+- Hardcoded passwords, API keys, tokens, secrets no source?
+- TODO/FIXME/HACK/XXX comments referenciando security?
+- Debug mode/dev mode proper gated? Habilitável em prod via env var, query param, header?
+- Test/example/seed credentials que funcionam em prod?
+- Endpoints `/debug`, `/admin`, `/test`, `/status`, `/health`, `/metrics`, `/env`, `/.env`, `/config` unprotected?
+- Arquivos `.env`, `credentials.json`, `*.pem`, `*.key` checked into repo?
+- `.gitignore` cobre secrets, uploads, e local config?
+- Dependencies pinned? CVEs conhecidos no dependency tree?
+- `eval()`, `exec()`, `child_process`, `Function()`, `vm.runInContext`, `import()` com dynamic input?
+- CORS headers `*` ou overly permissive com `Access-Control-Allow-Credentials`?
+- Cookies missing `HttpOnly`, `Secure`, ou `SameSite`?
+- Open redirects? (params named `redirect`, `return`, `next`, `url`, `goto`, `continue`)
+- TLS enforced? HTTP-only endpoints?
+- Error responses em prod retornando stack traces, internal paths, SQL errors?
+
+**IMPORTANTE**: Para qualquer finding deste agente, verificar o full code path, não só surface appearance. Um flag não é um finding — trace o impacto antes de reportar.
+
+---
+
+## Hunting Methodology — 12 Ângulos
+
+Inclua em todo prompt de agente Phase 2:
+
+### Como caçar
+
+Não apenas cheque se defesas existem. Tente quebrá-las. LEIA O CÓDIGO EM PROFUNDIDADE. Não pare na primeira função. Siga os dados por cada layer — de entry point até validation, transformation, storage, retrieval, e output. Bugs vivem nos gaps entre layers.
+
+1. **O HAPPY PATH ESTÁ DEFENDIDO. ATAQUE O SAD PATH.** Error handlers, fallback branches, catch blocks, default cases, timeout paths, retry logic, cleanup routines. Erros são handled com o mesmo rigor que success? Failed validation deixa state half-modified?
+
+2. **O QUE ACONTECE NAS BOUNDARIES?** Empty input. Maximum-length. Null vs undefined vs missing. Zero. Negativo. Unicode edge cases. Primeiro e último item. Um mais que o máximo. Exatamente no rate limit. Momento de token expiry.
+
+3. **O QUE COMPONENTES ASSUMEM SOBRE OUTROS?** DB layer assume que API layer validou? Renderer assume content sanitized no write? Auth middleware assume que routes se registram corretamente? Encontre onde trust é implícito e teste se é justificado.
+
+4. **E SE OPERAÇÕES ACONTECEM NA ORDEM ERRADA?** Call step 3 antes de step 1. Delete durante create. Callback antes do request. Confirmation endpoint sem iniciar o flow. Replay de flow completed.
+
+5. **E SE DUAS COISAS ACONTECEM SIMULTANEAMENTE?** Dois requests ao mesmo resource. Modify durante read. Delete durante iterate. Publish enquanto outro edita. Dois users claiming mesmo unique resource.
+
+6. **ONDE DOIS PARSERS OU VALIDATORS DISCORDAM?** Input aceito pelo schema mas rejeitado pelo DB. URL parsed diferente pelo router vs app code. Content-type diz uma coisa, body é outra. Filename extension vs MIME type vs magic bytes.
+
+7. **O QUE SOBREVIVE UM ROUND TRIP?** Data stored e retrieved — é o mesmo? Encoding muda? Escaping double-up? Relative path resolved diferente em read vs write? Serialization perde type info?
+
+8. **O QUE A CONFIGURAÇÃO CONTROLA?** Config missing ou default — o que acontece? Environment variable pode override security control? Feature flag desabilita validation? Security posture durante setup/first-run antes de config completo?
+
+9. **SIGA O DINHEIRO (OU O PRIVILÉGIO).** Para toda operação que muda state: quem autorizou? Trace back ao permission check. Checa a permissão certa? Contra o recurso certo? Existe path paralelo para o mesmo state change que checa diferente ou não checa?
+
+10. **PROCURE CONTEXTO VAZADO.** Error messages que revelam internal paths. Stack traces em prod. Timing differences que revelam se record existe. Response size differences. HTTP headers com versões. Debug endpoints que sobreviveram para prod.
+
+11. **QUE PARÂMETROS OVERRIDAM DEFAULTS SECURITY-RELEVANT?** Onde default é safe mas user-supplied parameter pode mudar. Procure todo input que override security-relevant default e cheque se o override é gated por permissions apropriados.
+
+12. **ONDE CLAIMS NÃO-VERIFICADOS DIRIGEM DECISÕES DE TRUST?** Self-declared identity, capability, ou metadata influenciando access/trust decision sem verificação independente.
+
+---
+
+## Validation Rules — Aplicar antes de reportar QUALQUER finding
+
+1. Você DEVE construir um ataque concreto (exact inputs, requests, ou action sequence)
+2. O ataque DEVE alcançar impacto meaningful (não apenas "aprender field names" ou "causar um error")
+3. Cheque se outra layer já previne exploitation — se sim, é hardening note, não finding
+4. Se o baseline comparável tem o mesmo pattern, note se foi explorado lá
+5. Se seu exploit depende de parser/runtime behavior, verifique contra a spec ou implementação — não assuma
+6. Retorne APENAS findings confirmados com ataques concretos, ou "Nenhuma vulnerabilidade explorável encontrada" se isso é honesto
+
+---
+
+## Spawn Sub-Agents
+
+Se precisar entender um subsistema em profundidade para avaliar um potential finding — use o Task tool para lançar um research agent. Não tente segurar tudo no seu próprio contexto. Vá fundo onde importa.
+
+**SEU ESCOPO É SEU FOCO PRIMÁRIO, NÃO UMA FRONTEIRA.** Se ao investigar sua área atribuída notar algo errado em categoria diferente — um permission issue ao tracing injection, uma race condition ao reviewing auth — reporte. Não ignore um bug porque "não é sua área." Atacantes não respeitam fronteiras de categoria.
diff --git a/.github/skills/security-specialist/steering/pentest.md b/.github/skills/security-specialist/steering/pentest.md
new file mode 100644
index 0000000..4a21b53
--- /dev/null
+++ b/.github/skills/security-specialist/steering/pentest.md
@@ -0,0 +1,224 @@
+# Penetration Testing
+
+Active security assessment against a live target. Unlike code-only analysis, this involves running tools against actual systems — reconnaissance, scanning, exploitation attempts, and evidence collection.
+
+**When to use:** The user has a target (domain, IP, web app URL) and wants an offensive assessment, not just source code review. This is the "attacker's perspective" workflow.
+
+## Prerequisites
+
+- For **localhost/dev targets**: no authorization needed — it's the user's own machine.
+- For **remote/production targets**: explicit written authorization from the target owner.
+- Clear rules of engagement for remote targets (scope, testing window).
+- Do NOT probe remote systems without confirmation.
+
+## Default Flow (Path + Dev)
+
+When the user provides a codebase path without a remote URL:
+
+1. Run SAST via `steering/full-scan.md` on the source code
+2. Detect how to start the dev server:
+   - Look for `package.json` → `npm run dev` / `npm start`
+   - Look for `docker-compose.yml` → `docker compose up -d`
+   - Look for `Makefile` → `make run`
+   - Look for `manage.py` → `python manage.py runserver`
+   - Ask the user if unclear
+3. Start the dev server, wait for it to be ready
+4. Run DAST against `localhost:<port>` (phases 2-4 below)
+5. Correlate: match DAST findings to source code locations from SAST
+6. Stop the dev server
+
+## Extended Flow (Path + Production URL)
+
+When the user also provides a production URL:
+
+1. Complete the default flow above (SAST + DAST localhost)
+2. Show gate: "This will send active probes to [URL]. Authorized? [y/n]"
+3. On confirmation: run DAST against production URL
+4. Compare: findings present in dev but absent in prod (mitigated by infra?) and vice versa
+5. Final report correlates all three layers
+
+## Phase 1: Reconnaissance
+
+Gather information without touching the target directly, then move to active probing.
+
+### Passive (no direct contact with target)
+
+```bash
+python3 scripts/pentest.py recon-passive --target <domain>
+```
+
+The script runs:
+- WHOIS lookup (registrar, nameservers, creation date)
+- DNS enumeration (A, AAAA, MX, NS, TXT, CNAME records)
+- Subdomain discovery via certificate transparency logs
+- Technology fingerprinting from public sources
+
+### Active (direct contact — requires authorization)
+
+```bash
+python3 scripts/pentest.py recon-active --target <ip_or_domain> --ports <range>
+```
+
+The script wraps:
+- Host discovery (ping sweep or TCP probe)
+- Port scanning (top 1000 or full 65535 based on `--ports`)
+- Service version detection on open ports
+- OS fingerprinting
+
+Record all discovered hosts, ports, and services. This becomes the attack surface map.
+
+## Phase 2: Enumeration
+
+Dig deeper into discovered services.
+
+### Web targets
+
+```bash
+python3 scripts/pentest.py enumerate-web --url <base_url>
+```
+
+Covers:
+- Directory and file brute-forcing (common paths, backup files, admin panels)
+- Subdomain enumeration (DNS brute, certificate transparency)
+- Technology stack detection (frameworks, CMS, WAF identification)
+- robots.txt, sitemap.xml, .well-known paths
+- HTTP method testing on discovered endpoints
+- Authentication mechanism identification
+
+### Infrastructure targets
+
+- Banner grabbing on non-HTTP services
+- SMB share enumeration
+- SNMP community string testing
+- Default credential checks on known services
+
+## Phase 3: Vulnerability Identification
+
+Map discovered services to known vulnerabilities and potential attack vectors.
+
+### Automated scanning
+
+```bash
+python3 scripts/pentest.py vuln-scan --target <url_or_ip> --type <web|infra>
+```
+
+For web targets, check OWASP Top 10:
+1. **Injection** — SQLi, command injection, LDAP injection, template injection
+2. **Broken Auth** — default creds, weak passwords, session fixation
+3. **Sensitive Data Exposure** — cleartext transmission, backup files, source disclosure
+4. **XXE** — XML entity injection in upload/API endpoints
+5. **Broken Access Control** — IDOR, privilege escalation, path traversal
+6. **Misconfig** — default pages, directory listing, verbose errors, CORS
+7. **XSS** — reflected, stored, DOM-based
+8. **Insecure Deserialization** — object injection in serialized data
+9. **Known CVEs** — version-matched CVE checks against detected software
+10. **SSRF** — server-side request forgery in URL parameters
+
+### Manual testing
+
+After automated scans, test for logic flaws that scanners miss:
+- Business logic bypasses (price manipulation, workflow skipping)
+- Race conditions in state-changing operations
+- Chained vulnerabilities (low-severity issues combining into high-impact)
+
+## Phase 4: Exploitation (Proof of Concept)
+
+For each identified vulnerability, attempt controlled exploitation to confirm impact.
+
+**Rules:**
+- Minimal impact — demonstrate the bug, don't destroy data
+- Document every step — screenshot, request/response, timestamp
+- Stop if unexpected damage occurs
+- Stay within authorized scope
+
+Record results:
+
+```bash
+python3 scripts/scan_db.py add-finding \
+  --scan-id <id> \
+  --title "SQL Injection in /api/search" \
+  --severity critical \
+  --category injection \
+  --file "api/routes/search.js" \
+  --line 42 \
+  --description "Unsanitized user input in search parameter passed directly to SQL query" \
+  --evidence "Request: GET /api/search?q=1' OR 1=1-- Response: 200 OK with all database records"
+```
+
+## Phase 5: Post-Exploitation (if in scope)
+
+When rules of engagement allow:
+- Lateral movement mapping (what else can you reach from compromised position)
+- Privilege escalation attempts
+- Data access assessment (what sensitive data is reachable)
+- Persistence mechanism identification (not deployment — just identifying)
+
+## Phase 6: Reporting
+
+```bash
+python3 scripts/finalize.py --scan-dir .security
+```
+
+The pentest report adds to the standard report format:
+
+| Section | Content |
+|---------|---------|
+| Executive Summary | Business impact in non-technical language |
+| Scope | Authorized targets, testing window, methodology |
+| Attack Narrative | Chronological story of the assessment |
+| Findings | Sorted by severity with full reproduction steps |
+| Evidence | Screenshots, request/response dumps, tool output |
+| Remediation | Prioritized fix recommendations with effort estimates |
+| Positive Observations | What's working well (defenders need wins too) |
+
+## Severity Rating
+
+Follow `references/severity-policy.md`, but with CVSS alignment for pentest context:
+
+| Severity | CVSS Range | Pentest Context |
+|----------|-----------|-----------------|
+| Critical | 9.0–10.0 | RCE, full auth bypass, mass data exfil, supply chain |
+| High | 7.0–8.9 | SQLi with data access, stored XSS + session theft, priv esc |
+| Medium | 4.0–6.9 | Reflected XSS, info disclosure, missing security controls |
+| Low | 0.1–3.9 | Missing headers, version disclosure, theoretical issues |
+| Info | — | Best practices, hardening suggestions, architecture notes |
+
+## Tools Reference
+
+The `scripts/pentest.py` script wraps system tools when available and falls back to Python alternatives.
+
+### Tool Matrix
+
+| Function | System Tool | Python Alternative (pip) | Stdlib Fallback |
+|----------|------------|--------------------------|-----------------|
+| Port scanning | `nmap` | `python-nmap` or `python3-nmap` | `socket` connect scan |
+| DNS enumeration | `dig`, `host` | `dnspython` | `socket.getaddrinfo` |
+| WHOIS lookup | `whois` | `python-whois` | crt.sh HTTPS query |
+| Subdomain enum | `subfinder`, `amass` | `bbot` | crt.sh CT log query |
+| Dir brute-force | `gobuster`, `feroxbuster` | `dirsearch` | `urllib` common-path check |
+| Tech detection | `whatweb`, `wappalyzer` | `builtwith`, `webtech` | HTTP header analysis |
+| Web vuln scan | `nikto` | `wapiti3` | Manual checks |
+| Template scan | `nuclei` | `wapiti3` (module-based) | Header/config checks |
+| SQL injection | `sqlmap` | `sqlmap` (is Python) | Parameter probing |
+| XSS detection | — | `wapiti3`, `xsser` | Reflected input check |
+| OSINT recon | `theHarvester` | `theHarvester` (is Python) | Search API queries |
+| HTTP proxy | `burpsuite`, `mitmproxy` | `mitmproxy` (is Python) | — |
+
+### Installation
+
+```bash
+# Minimal (covers most use cases)
+pip install dnspython python-whois requests
+
+# Full pentest stack
+pip install dnspython python-whois python3-nmap wapiti3 dirsearch bbot webtech mitmproxy
+```
+
+### Priority Order
+
+The script tries tools in this order:
+1. **System binary** (fastest, most features) — e.g., `nmap` on PATH
+2. **Python pip package** (portable, no root needed) — e.g., `python3-nmap`
+3. **Stdlib fallback** (always works, limited) — e.g., `socket` scan
+
+If nothing external is available, the stdlib fallback still produces useful results — just slower and less comprehensive.
diff --git a/.github/skills/security-specialist/steering/remediation.md b/.github/skills/security-specialist/steering/remediation.md
new file mode 100644
index 0000000..563899c
--- /dev/null
+++ b/.github/skills/security-specialist/steering/remediation.md
@@ -0,0 +1,110 @@
+# Steering: Remediation
+
+Fix a specific confirmed vulnerability. The goal is a minimal, correct patch that closes the security gap without introducing regressions.
+
+## Step 1: Understand the Root Cause
+
+Read the finding details from the scan database:
+```bash
+python3 scripts/scan_db.py show --finding-id <id>
+```
+
+Then answer:
+- What is the **root cause**? (not the symptom — the actual design flaw or missing control)
+- Where does untrusted data enter the system? (the source)
+- What dangerous operation consumes it? (the sink)
+- What check/transform is missing between source and sink?
+
+Example: The symptom is "XSS in search results page." The root cause is "user input from query parameter is interpolated into HTML without encoding." The fix isn't "sanitize this one field" — it's "ensure all template output is auto-escaped, and this specific path uses the escaping mechanism."
+
+## Step 2: Identify the Minimal Correct Fix
+
+Pick the fix that:
+1. Addresses the root cause, not just the specific instance
+2. Uses the framework's built-in security mechanisms when available
+3. Doesn't change unrelated behavior
+4. Is consistent with how the rest of the codebase handles the same pattern
+
+### Common Fix Patterns
+
+**Injection (SQLi, NoSQLi, command injection):**
+- Use parameterized queries / prepared statements. Never string concatenation.
+- For OS commands: use array-based exec (no shell interpretation), or better — avoid shelling out entirely.
+
+**Cross-Site Scripting (XSS):**
+- Enable auto-escaping in the template engine (most modern frameworks do this by default).
+- For cases requiring raw HTML: use a strict allowlist sanitizer (DOMPurify, bleach).
+- Set `Content-Security-Policy` headers as defense-in-depth.
+
+**Authentication/Authorization:**
+- Add the missing auth check at the correct layer (middleware/decorator, not deep in business logic).
+- Use the existing auth framework — don't invent a new check.
+- Verify the check covers all HTTP methods, not just GET.
+
+**Path Traversal:**
+- Resolve the path, then verify it's within the allowed directory (use `realpath` comparison).
+- Never rely on blacklisting `../` — normalize first, check after.
+
+**SSRF:**
+- Validate the target URL against an allowlist of permitted hosts/schemes.
+- Block private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 169.254.0.0/16, fd00::/8).
+- Disable redirects or re-validate after each redirect.
+
+**Insecure Deserialization:**
+- Don't deserialize untrusted data with unsafe deserializers (pickle, Java ObjectInputStream, PHP unserialize).
+- Use data-only formats (JSON, protobuf) with schema validation.
+
+**Secrets/Credentials:**
+- Remove the hardcoded secret from code.
+- Load from environment variable or secrets manager.
+- Rotate the exposed credential immediately.
+
+## Step 3: Implement the Fix
+
+Write the patch. Keep the diff small and focused:
+- Touch only files relevant to the vulnerability.
+- If you discover a systemic issue (same pattern in 20 places), fix them all — but in a way that's reviewable (e.g., introduce a helper function, then replace all call sites).
+- Add a code comment referencing the finding ID if the fix is non-obvious: `// Fix for SEC-042: parameterize user input`
+
+## Step 4: Verify No Regressions
+
+Run the project's existing test suite:
+```bash
+# Whatever the project uses — detect from package.json, Makefile, etc.
+npm test / pytest / go test ./... / cargo test
+```
+
+If tests fail, the fix is wrong or incomplete. Adjust until green.
+
+If no tests cover the affected code path, write a minimal test that exercises the fixed path with safe input and confirms it still works.
+
+## Step 5: Verify the Fix Closes the Vulnerability
+
+Re-run the scanner or analysis that found the issue:
+```bash
+python3 scripts/run_scan.py --target <file_or_dir> --rules <relevant_rule_id>
+```
+
+The finding should no longer appear. If it does, the fix is incomplete.
+
+For manually-validated findings: re-trace the data flow. Confirm the dangerous operation is no longer reachable with attacker-controlled input, or that proper sanitization/validation now gates it.
+
+## Step 6: Update the Scan Database
+
+Mark the finding as fixed:
+```bash
+python3 scripts/scan_db.py update-status \
+  --finding-id <id> \
+  --status fixed
+```
+
+## Step 7: Document
+
+If the fix introduced a new security pattern (new helper function, new middleware, new validation rule), document it briefly so future code follows the same pattern. Update the project's security guidelines or CONTRIBUTING.md if appropriate.
+
+## Principles
+
+- Fix the class of bug, not just the instance — but only if the codebase supports it without massive refactoring.
+- The best fix uses mechanisms already present in the framework. Don't add a custom sanitizer when the template engine has auto-escape.
+- A fix that breaks functionality is not a fix. Tests must pass.
+- If the correct fix requires significant architectural change, flag it and propose a phased approach.
diff --git a/.github/skills/security-specialist/steering/reporting.md b/.github/skills/security-specialist/steering/reporting.md
new file mode 100644
index 0000000..4a5102b
--- /dev/null
+++ b/.github/skills/security-specialist/steering/reporting.md
@@ -0,0 +1,142 @@
+# Steering: Generate Scan Report
+
+Produce the final deliverable: a structured report of all findings from a scan, in both machine-readable (JSON) and human-readable (Markdown) formats.
+
+## Step 1: Load All Findings
+
+Pull the complete findings set from the scan database:
+```bash
+python3 scripts/scan_db.py list --scan-dir <dir> --format json > /tmp/findings_raw.json
+```
+
+Verify the data includes:
+- All triaged findings (confirmed + false-positive + needs-more-info)
+- Validated severity for each
+- Status (open, fixed, tracked, false-positive)
+- Location, category, evidence, and triage rationale
+
+If any findings lack triage data, go back to the `triage` workflow first. Don't report un-triaged findings.
+
+## Step 2: Compute Statistics
+
+Calculate:
+
+**By severity:**
+- Critical: count
+- High: count
+- Medium: count
+- Low: count
+- Informational: count
+- False positives excluded from totals
+
+**By category:**
+- Group by CWE or vulnerability class (injection, XSS, auth, crypto, etc.)
+- Show count per category
+
+**By location:**
+- Which files/directories have the most findings
+- Hotspots (files with 3+ findings)
+
+**By status:**
+- Open (unresolved)
+- Fixed (remediated and verified)
+- Tracked (exported to issue tracker)
+- False positive (dismissed with rationale)
+
+## Step 3: Generate JSON Report
+
+Structure:
+```json
+{
+  "scan_metadata": {
+    "scan_id": "<uuid>",
+    "timestamp": "<ISO 8601>",
+    "target": "<repository or directory scanned>",
+    "tools_used": ["semgrep", "trufflehog", ...],
+    "scan_duration_seconds": <int>
+  },
+  "summary": {
+    "total_findings": <int>,
+    "by_severity": {"critical": 0, "high": 0, "medium": 0, "low": 0, "informational": 0},
+    "by_status": {"open": 0, "fixed": 0, "tracked": 0, "false_positive": 0},
+    "by_category": {"CWE-79": 3, "CWE-89": 1, ...}
+  },
+  "findings": [
+    {
+      "id": "<finding-id>",
+      "title": "<short description>",
+      "severity": "<validated severity>",
+      "category": "<CWE-XXX>",
+      "location": {"file": "<path>", "line": <int>, "function": "<name>"},
+      "status": "<open|fixed|tracked|false_positive>",
+      "evidence": "<code snippet or trace>",
+      "rationale": "<triage reasoning>",
+      "tracking_url": "<url if tracked, null otherwise>"
+    }
+  ]
+}
+```
+
+Write to: `<scan-dir>/report.json`
+
+## Step 4: Generate HTML Report
+
+The human-readable report is a **self-contained HTML file** (`security-report.html`). Follow the template in `references/report-format.md` **exactly** — it is a prescriptive spec, not a suggestion.
+
+Key features:
+- Dark theme, color-coded severity badges
+- Collapsible evidence and remediation sections
+- Interactive filter buttons (filter by severity)
+- CVE analysis table with exploitability cross-reference
+- Pentest results with all tests numbered (P1, P2...)
+- Negative results table (what was tested and passed)
+- **Footer with skill attribution** (mandatory — see template)
+- Zero external dependencies — opens offline
+
+Build the HTML by replacing `{{placeholders}}` in the template with actual data. Repeat blocks for each finding/CVE/test.
+
+**After generating the HTML, run the Report Compliance Checklist from SKILL.md against your output.** If any element is missing, fix it before proceeding.
+
+Write to: `<scan-dir>/security-report.html` (and also repo root for easy access)
+
+## Step 5: Structured Output (Full-Scan Only)
+
+Se este report vem de um full-scan com pipeline de 6 fases, produza também o `findings.json` estruturado:
+
+1. Leia `references/report-schema.json` — siga exatamente
+2. Para cada finding confirmado, popule todos required fields incluindo trace, conditions, execution, confidence
+3. Valide: `node scripts/validate-findings.cjs <scan-dir>/findings.json`
+4. Fix erros antes de prosseguir
+
+Para workflows não-pipeline (discovery, diff-review, pentest), o format simples do SQLite é suficiente.
+
+## Step 6: Finalize
+
+Run the finalization script to seal both reports and compute integrity hashes:
+```bash
+python3 scripts/finalize.py --scan-dir <dir>
+```
+
+This script:
+- Validates both report files exist and are well-formed (JSON valid, HTML parseable)
+- Computes SHA-256 hashes of report.json and security-report.html
+- Writes a `manifest.json` with file hashes and completion timestamp
+- Marks the scan as complete in the database
+
+## Step 6: Present to User
+
+Show:
+- The executive summary
+- The findings table
+- Location of the full report files
+- Any findings that still need action (open critical/high)
+
+## Principles
+
+- Reports are for two audiences: machines (JSON) and humans (HTML). Both must be complete.
+- The HTML report is self-contained, interactive, and opens offline in any browser.
+- False positives go in a collapsible appendix — they prove rigor but shouldn't clutter the main findings.
+- Severity in the report is the **validated** severity (cross-referenced against project context), not the scanner's original rating.
+- Every recommendation must be specific enough that a developer can act on it without further research.
+- The executive summary is for people who won't read the rest. Make it count.
+- All pentest tests performed must appear in the report — positive and negative results.
diff --git a/.github/skills/security-specialist/steering/threat-model.md b/.github/skills/security-specialist/steering/threat-model.md
new file mode 100644
index 0000000..947fe38
--- /dev/null
+++ b/.github/skills/security-specialist/steering/threat-model.md
@@ -0,0 +1,143 @@
+# Threat Model
+
+## Purpose
+
+Build or update a structured threat model for the repository. Output is `.security/threat-model.md` — a living document that informs scan priorities and attack path analysis.
+
+## Step 1: Identify the System
+
+Answer these questions by reading the codebase:
+
+- **What does it do?** — Core functionality in one paragraph
+- **Who uses it?** — User roles (anonymous, authenticated, admin, service-to-service)
+- **Where does it run?** — Cloud provider, container orchestration, serverless, bare metal, edge
+- **What data does it handle?** — PII, financial, credentials, health data, public content
+- **What's the deployment model?** — Single tenant, multi-tenant, self-hosted, managed
+
+Document answers at the top of the threat model.
+
+## Step 2: Map Trust Boundaries
+
+Draw the lines between zones of different trust:
+
+- **External → Application** — internet-facing load balancer, API gateway
+- **Application → Database** — app server to data store
+- **Application → External Services** — third-party APIs, payment processors
+- **User tiers** — anonymous vs authenticated vs admin
+- **Service-to-service** — internal microservice communication
+- **CI/CD → Production** — deployment pipeline access
+
+For each boundary, note:
+- What crosses it (data, commands, credentials)
+- How it's protected (TLS, auth tokens, network policy, nothing)
+
+## Step 3: Identify Entry Points
+
+Every place external input enters the system:
+
+| Entry Point | Protocol | Auth Required | Input Type |
+|---|---|---|---|
+| `POST /api/login` | HTTPS | No | JSON body |
+| `GET /api/users/:id` | HTTPS | Yes (JWT) | URL param |
+| WebSocket `/ws` | WSS | Yes (session) | Messages |
+| Message queue consumer | AMQP | Service account | Serialized events |
+| CLI commands | Local | OS user | Arguments + stdin |
+| File upload endpoint | HTTPS | Yes | Multipart binary |
+
+Be exhaustive. Every entry point is a potential attack vector.
+
+## Step 4: Map Data Flows
+
+For each significant data type, trace its lifecycle:
+
+1. **Ingestion** — where it enters the system
+2. **Processing** — what transforms or validates it
+3. **Storage** — where it persists (DB, cache, file, log)
+4. **Transmission** — where it's sent (other services, external APIs, user responses)
+5. **Deletion** — how/when it's purged
+
+Flag any data flow that crosses a trust boundary without adequate protection.
+
+## Step 5: Enumerate Threats (STRIDE)
+
+For each entry point and data flow, apply STRIDE:
+
+| Category | Question |
+|---|---|
+| **Spoofing** | Can an attacker impersonate a legitimate user or service? |
+| **Tampering** | Can data be modified in transit or at rest without detection? |
+| **Repudiation** | Can actions be performed without audit trail? |
+| **Information Disclosure** | Can sensitive data leak through errors, logs, side channels? |
+| **Denial of Service** | Can the system be exhausted or crashed? |
+| **Elevation of Privilege** | Can a low-privilege user gain higher access? |
+
+For each identified threat, document:
+
+```
+### T-<number>: <Title>
+
+**Category:** Spoofing / Tampering / Repudiation / Info Disclosure / DoS / EoP
+**Entry Point:** <where the attack starts>
+**Affected Component:** <what's at risk>
+**Likelihood:** High / Medium / Low
+**Impact:** Critical / High / Medium / Low
+**Current Mitigations:** <what's already in place, or "None">
+**Residual Risk:** <what remains after mitigations>
+```
+
+## Step 6: Assess Likelihood and Impact
+
+Likelihood considers:
+- Is the entry point internet-facing or internal-only?
+- Does exploitation require authentication?
+- Is the vulnerability pattern common and well-tooled?
+- Are there known exploits in the wild for this class?
+
+Impact considers:
+- What data is compromised? (PII = high, public content = low)
+- Can the attacker pivot to other systems?
+- Is there financial, legal, or reputational damage?
+- How many users are affected?
+
+## Step 7: Document Assumptions
+
+Every threat model rests on assumptions. Make them explicit:
+
+- "Internal network is trusted" — is it?
+- "Admin users are not adversaries" — always true?
+- "TLS terminates at the load balancer" — verified?
+- "Database is not internet-accessible" — checked?
+- "Third-party dependencies are not compromised" — hope so
+
+These assumptions are the first thing to revisit when the system changes.
+
+## Step 8: Write Output
+
+Save to `.security/threat-model.md` with this structure:
+
+```
+# Threat Model — <Project Name>
+Last updated: <date>
+
+## System Description
+## Trust Boundaries
+## Entry Points
+## Data Flows
+## Threats
+## Assumptions
+## Review History
+```
+
+## When to Update
+
+- New entry point added (route, consumer, endpoint)
+- Architecture change (new service, new data store, new external dependency)
+- Deployment model change (moved to different infra, added multi-tenancy)
+- After a security incident (assumptions proved wrong)
+- Every 6 months as a hygiene check
+
+## Notes
+
+- A threat model isn't a findings list. It's a map of WHERE to look and WHAT to worry about.
+- Don't over-enumerate. Focus on threats with realistic attack paths, not theoretical exercises.
+- If the system is simple (static site, no user data), keep the model proportionally simple. One page is fine.
diff --git a/.github/skills/security-specialist/steering/tracking.md b/.github/skills/security-specialist/steering/tracking.md
new file mode 100644
index 0000000..b1ead02
--- /dev/null
+++ b/.github/skills/security-specialist/steering/tracking.md
@@ -0,0 +1,152 @@
+# Steering: Export Findings to Tracking Systems
+
+Push confirmed findings to external issue trackers (GitHub Issues, GitHub Security Advisories, Jira, Linear) so they're visible to the engineering team and can be assigned/scheduled.
+
+## Step 1: Select Findings to Export
+
+Query the scan database for findings ready to track:
+```bash
+python3 scripts/scan_db.py list --status confirmed --not-tracked
+```
+
+Decide what to export. Typical filters:
+- All confirmed findings above a severity threshold (e.g., high+critical)
+- All findings from a specific scan
+- A hand-picked set by finding ID
+
+Don't export false positives or informational findings to issue trackers — they create noise.
+
+## Step 2: Determine Target System
+
+Identify the project's tracking system:
+- **GitHub Issues** — default for open source and most SaaS teams
+- **GitHub Security Advisories** — for vulnerabilities that need CVEs or coordinated disclosure
+- **Jira** — enterprise, use the project's existing security issue type
+- **Linear** — startup teams, use appropriate team and label
+
+Check the project for existing conventions: issue templates, labels (e.g., `security`, `vulnerability`), custom fields, linked projects.
+
+## Step 3: Format the Issue
+
+### Title Format
+```
+[<SEVERITY>] <Vulnerability Type> in <Location>
+```
+Examples:
+- `[HIGH] SQL Injection in /api/users search endpoint`
+- `[CRITICAL] Authentication bypass via JWT algorithm confusion`
+- `[MEDIUM] Stored XSS in comment rendering`
+
+Keep titles scannable. Someone triaging a backlog should understand the issue from the title alone.
+
+### Body Structure
+
+```markdown
+## Summary
+One paragraph: what's wrong, where, and why it matters.
+
+## Finding Details
+- **ID:** <finding-id from scan database>
+- **Category:** <CWE-XXX / OWASP category>
+- **Severity:** <Critical/High/Medium/Low>
+- **Location:** `<file:line>` or `<endpoint + parameter>`
+- **Detected by:** <tool name or manual review>
+
+## Evidence
+<Code snippet showing the vulnerable pattern, or request/response demonstrating the issue>
+
+## Impact
+What can an attacker do if this is exploited? Be specific:
+- What data is accessible?
+- What actions can be performed?
+- What's the blast radius?
+
+## Recommended Fix
+Brief guidance on the correct remediation approach. Not a full patch — just enough for the developer to understand the direction.
+
+## References
+- CWE link
+- OWASP page
+- Relevant framework documentation for the secure pattern
+```
+
+### For GitHub Security Advisories
+
+Additional fields required:
+- Affected versions / commits
+- CVSS score (calculate from the validated severity + exploitability)
+- Patched version (if fix exists)
+- Credit (if from bug bounty or external reporter)
+
+## Step 4: Show User for Approval
+
+Before creating anything externally, present the formatted payload:
+```
+I'm about to create the following issue in <system>:
+
+Title: [HIGH] SQL Injection in /api/users search endpoint
+Labels: security, priority-high
+Assignee: (none — or suggest based on git blame)
+
+Body:
+<full body text>
+
+Approve? (yes/no/edit)
+```
+
+Never auto-create external issues without explicit user confirmation. These are visible to teams and may trigger notifications.
+
+## Step 5: Create the Issue
+
+Use the appropriate CLI tool:
+
+**GitHub Issues:**
+```bash
+gh issue create --title "<title>" --body "<body>" --label "security,<severity>"
+```
+
+**GitHub Security Advisory:**
+```bash
+gh api repos/{owner}/{repo}/security-advisories --method POST --input payload.json
+```
+
+**Jira:**
+```bash
+# Use project-specific Jira CLI or API
+curl -X POST "https://<instance>.atlassian.net/rest/api/3/issue" \
+  -H "Authorization: Basic <token>" \
+  -H "Content-Type: application/json" \
+  -d @payload.json
+```
+
+**Linear:**
+```bash
+# Use Linear CLI or GraphQL API
+linear issue create --title "<title>" --description "<body>" --team "<team>" --label "Security"
+```
+
+Capture the returned URL/ID of the created issue.
+
+## Step 6: Update Scan Database
+
+Link the finding to the external tracker:
+```bash
+python3 scripts/scan_db.py update-status \
+  --finding-id <id> \
+  --status tracked \
+  --tracking-url <url>
+```
+
+## Step 7: Batch Operations
+
+When exporting multiple findings:
+- Group related findings into a single issue if they share the same root cause (e.g., "Missing CSRF protection on 8 endpoints" = 1 issue with a checklist)
+- Keep unrelated findings as separate issues — don't create mega-issues
+- Apply consistent labels and severity tags across the batch
+
+## Principles
+
+- Issues should be actionable. A developer reading it should know what to fix without needing to re-do the analysis.
+- Don't over-classify. If in doubt about severity, round down — you can escalate later.
+- Include enough evidence that the issue can be verified independently, but don't paste entire exploit chains in public repos.
+- For security advisories: coordinate with maintainers on disclosure timeline before publishing.
diff --git a/.github/skills/security-specialist/steering/triage.md b/.github/skills/security-specialist/steering/triage.md
new file mode 100644
index 0000000..3b29c90
--- /dev/null
+++ b/.github/skills/security-specialist/steering/triage.md
@@ -0,0 +1,101 @@
+# Steering: Triage Findings
+
+Intake a batch of security findings from any source (SARIF, scanner JSON, bug bounty reports, prior scan DB entries) and produce validated, deduplicated, severity-rated findings ready for action.
+
+## Step 1: Ingest and Normalize
+
+Load all input findings into a common internal format. Each normalized finding must have:
+
+- `id`: unique identifier (generate one if source doesn't provide)
+- `title`: short description of the issue
+- `category`: CWE number or OWASP category (e.g., CWE-79, A03:2021-Injection)
+- `location`: file path + line number (or URL + parameter for dynamic findings)
+- `source`: which tool/report produced this (semgrep, snyk, burp, manual, etc.)
+- `original_severity`: what the source assigned
+- `raw_snippet`: relevant code or request/response excerpt
+- `description`: what the finding claims is wrong
+
+Run:
+```bash
+python3 scripts/scan_db.py import --format <sarif|json|csv|manual> --input <path>
+```
+
+This writes normalized findings into the scan database. Verify import count matches expectation.
+
+## Step 2: Deduplicate
+
+Same vulnerability reported by multiple tools = one finding. Dedup criteria:
+- Same file + same line range (±5 lines) + same CWE = duplicate
+- Same endpoint + same parameter + same vulnerability class = duplicate
+- Different manifestations of the same root cause = group under one finding, note variants
+
+Run:
+```bash
+python3 scripts/scan_db.py dedup --scan-dir <dir>
+```
+
+Review the dedup report. If the tool merged things that are actually distinct, split them manually.
+
+## Step 3: Contextual Assessment
+
+For each unique finding, answer these questions **by reading the actual code**:
+
+1. **Is it real?** Does the vulnerable pattern actually exist at that location? Scanners hallucinate. Read the file.
+2. **Is it reachable?** Can user-controlled input actually reach the vulnerable code path? Trace backwards from the sink to any entry point.
+3. **Are there mitigations?** WAF rules, input validation earlier in the chain, framework-level protections, CSP headers — anything that reduces or eliminates exploitability.
+4. **What's the blast radius?** If exploited: data loss? RCE? privilege escalation? Information disclosure only? Account takeover?
+5. **What's the attack complexity?** Does exploitation require authentication? Specific race conditions? Social engineering?
+
+## Step 4: Assign Validated Severity
+
+Do NOT blindly accept the scanner's severity. Recalculate using:
+
+| Severity | Criteria |
+|----------|----------|
+| **Critical** | RCE, auth bypass, mass data exfil, no mitigations, reachable from unauthenticated context |
+| **High** | SQLi/XSS with clear exploit path, privilege escalation, SSRF to internal services |
+| **Medium** | Exploitable but requires auth, limited blast radius, or partial mitigations exist |
+| **Low** | Theoretical risk, defense-in-depth issue, requires unlikely preconditions |
+| **Informational** | Best practice violation, no direct exploitability, hardening recommendation |
+
+If a scanner says "Critical" but the finding is behind authentication + rate limiting + the data exposed is non-sensitive → it's Medium at best.
+
+## Step 5: Record Triage Decisions
+
+For each finding, record:
+```
+finding_id: <id>
+validated_severity: <critical|high|medium|low|informational>
+verdict: <confirmed|false-positive|needs-validation>
+rationale: <2-3 sentences explaining WHY this severity, what you checked>
+```
+
+Run:
+```bash
+python3 scripts/scan_db.py triage \
+  --finding-id <id> \
+  --severity <level> \
+  --verdict <confirmed|false-positive|needs-validation> \
+  --rationale "explanation here"
+```
+
+## Step 6: Produce Triage Summary
+
+After all findings are triaged, generate the summary:
+```bash
+python3 scripts/scan_db.py triage-summary --scan-dir <dir>
+```
+
+Output includes:
+- Total findings ingested vs. unique vs. false positives
+- Breakdown by validated severity
+- List of findings needing deeper validation (verdict = needs-validation)
+- Recommended priority order for remediation
+
+## Key Principles
+
+- Scanner severity is a suggestion, not a verdict. Your job is to validate.
+- A finding you can't trace to reachable code is `needs-validation`, not `confirmed`.
+- False positives are fine — document why and move on. Don't waste time on them.
+- When in doubt about exploitability, escalate to the `validation` workflow.
+- Group related findings (e.g., 15 instances of the same missing input validation) — fix the pattern, not each instance individually.
diff --git a/.github/skills/security-specialist/steering/validation.md b/.github/skills/security-specialist/steering/validation.md
new file mode 100644
index 0000000..65cb02b
--- /dev/null
+++ b/.github/skills/security-specialist/steering/validation.md
@@ -0,0 +1,137 @@
+# Steering: Validate a Finding
+
+Determine se um finding reportado é real e explorável. Produza um verdict: `confirmed`, `rejected`, ou `needs-more-info`.
+
+## Princípio: Validação Adversarial
+
+O agente que valida NUNCA deve ser o agente que encontrou o finding. Hunting agents são biased para encontrar coisas; validation agents são biased para matar false positives. Este step adversarial é crítico.
+
+---
+
+## Step 1: Load Finding Details
+
+```bash
+python3 scripts/scan_db.py show --finding-id <id>
+```
+
+Extraia:
+- Tipo de vulnerabilidade claimed (CWE)
+- Location (file, line, function, ou endpoint)
+- Source do report (qual scanner, ou manual)
+- Evidência ou PoC existente
+- Trace claimed (se disponível)
+
+## Step 2: Read the Code at the Finding Location
+
+Abra o file. Leia a function. Entenda o que faz. Não confie no snippet do scanner — scanners truncam contexto e perdem surrounding logic.
+
+Perguntas:
+- O pattern que o scanner flagged realmente existe aqui?
+- É dead code? (unreachable, commented out, behind permanent feature flag)
+- Foi refatorado desde o scan? (cheque git log)
+- Se o código não bate com o report → provável **false positive** de resultados stale.
+
+## Step 3: Testes de Validação (5 Gates)
+
+Aplique **todos** os testes abaixo. O finding deve sobreviver cada um:
+
+### 3a. Exploitation Test
+Leia o código real em cada step do trace. O data flow funciona como claimed?
+- Pode construir o exact input (HTTP request, CLI invocation, API call, crafted file) que triggera isto?
+- O input realmente alcança o sink sem ser blocked/transformed/validated no caminho?
+
+### 3b. Impact Test
+O que o atacante **realmente ganha**?
+- Se a resposta é "aprende field names" ou "causa um error" → LOW máximo
+- Se não pode descrever dano concreto em 2 frases → severity provavelmente está inflada
+
+### 3c. Baseline Test
+O comparável identificado em Phase 1 tem o mesmo pattern?
+- Se sim e já foi explorado → finding MAIS FORTE, não mais fraco
+- Se sim e nunca explorado em anos de produção → entenda por quê antes de reportar
+- Se não tem comparável ou comparável não tem o pattern → proceda normalmente
+
+### 3d. Mitigation Test
+Existe outra layer que previne exploitation?
+- WAF rules
+- Middleware de input validation upstream
+- Framework defaults (auto-escape, parameterized queries, CSRF tokens)
+- Database constraints
+- Network isolation
+- Rate limiting
+
+Mitigações não tornam false positive — reduzem severity. Note mas ainda confirme o flaw subjacente.
+
+### 3e. Parser/Runtime Behavior Test
+Se o exploit depende de como parser/runtime handles input específico:
+- Verifique contra a spec ou implementação REAL
+- NÃO assuma behavior de intuição
+- Cite a spec ou teste dinamicamente
+- Os false positives mais convincentes vêm de reasoning "o parser vai interpretar isso como..." sem verificar
+
+## Step 4: Trace the Data Flow
+
+### Identify the Source
+- HTTP request parameters (query, body, headers, cookies)
+- File uploads
+- Database records (se populated por user input elsewhere)
+- Message queues / event payloads
+
+### Trace Through Transformations
+- Validado? (type check, regex, allowlist)
+- Sanitizado? (HTML encoding, SQL escaping, shell quoting)
+- Transformado em safe type? (parsed as integer, resolved as enum)
+- Passa por framework-level protection? (ORM parameterization, template auto-escape)
+
+### Document the Chain
+```
+Source: req.query.search (user-controlled, string, sem length limit)
+  → passed to: buildQuery(search) em db/queries.js:45
+  → buildQuery concatena em SQL string (SEM parameterization)
+  → executed via: db.raw(query) em db/queries.js:52
+Sink: raw SQL execution
+Mitigations: nenhuma encontrada
+Verdict: CONFIRMED — SQL injection clássica
+```
+
+## Step 5: Attempt Proof-of-Concept
+
+Se pode demonstrar exploitation safety sem causar dano:
+
+**Para injection flaws:** Construa payload que produz observable side effect.
+**Para auth bypasses:** Mostre o request que alcança protected resources sem credentials válidos.
+**Para path traversal:** Mostre o path que resolve fora do diretório intended.
+
+### Quando Dynamic Testing Não É Viável
+- Rely em static trace: source → transforms → sink
+- State: "Static analysis only — no dynamic confirmation"
+- Note o que seria necessário para confirmar dinamicamente
+- Ainda válido para `confirmed` se static trace é unambíguo
+
+## Step 6: Render Verdict
+
+| Verdict | Critérios |
+|---------|----------|
+| **confirmed** | Data attacker-controlled alcança dangerous sink com proteção insuficiente. Exploit path claro. Todos 5 gates passed. |
+| **rejected** | Pattern não existe, código unreachable, ou mitigações previnem completamente exploitation. Evidência concreta de por quê. |
+| **needs-more-info** | Não pode determinar. Especifique exatamente o que está faltando. |
+
+## Step 7: Record
+
+```bash
+python3 scripts/scan_db.py validate \
+  --finding-id <id> \
+  --verdict <confirmed|rejected|needs-more-info> \
+  --evidence "source: req.query.q → sink: db.raw() em queries.js:52, sem parameterization" \
+  --poc "GET /api/search?q=' OR 1=1--" \
+  --notes "Static trace only, no dynamic confirmation"
+```
+
+## Princípios
+
+- Finding sem traceable data flow não é confirmed — é hipótese.
+- Scanners reportam patterns, não exploits. Seu job é determinar se o pattern é explorável em contexto.
+- "Rejected" é fine. Documente por quê e siga em frente.
+- "Needs-more-info" é honesto. Melhor que adivinhar.
+- Mitigações reduzem risco mas não eliminam findings. SQLi behind WAF ainda é SQLi.
+- **Kill false positives agressivamente, mas não mate findings reais.** Report curto com 3 findings reais vale mais que report longo com 30 teóricos.
diff --git a/.github/skills/skill-evaluation/SKILL.md b/.github/skills/skill-evaluation/SKILL.md
new file mode 100644
index 0000000..0f444a0
--- /dev/null
+++ b/.github/skills/skill-evaluation/SKILL.md
@@ -0,0 +1,299 @@
+---
+name: skill-evaluation
+description: >
+  Evaluate any agent skill against a merged framework — Anthropic's Claude Code
+  best practices plus Matt Pocock's writing-great-skills methodology — across
+  4 axes (Trigger, Structure, Steering, Pruning). Produces an evidence-cited
+  scorecard (0–100), a weighted overall score, and diagnosed failure modes
+  with prioritized fixes. Use when the user asks to evaluate, rate, or audit
+  a skill ("evaluate this skill", "skill scorecard", "review SKILL.md"), or
+  to compare two skills.
+metadata:
+  author: ft.ia.br
+  version: "2.1.0"
+  date: 2026-07-03
+  repository: https://github.com/fabricioctelles/skills
+  license: Apache-2.0
+  category: code-quality-and-review
+---
+
+# Skill Evaluation
+
+If you need the vocabulary and tests behind Axes 1, 3, and 4 (leading words,
+completion criteria, context pointers, the deletion test, failure-mode
+definitions), read `references/mechanics.md` before scoring those axes.
+
+## Source
+
+- [Lessons from building Claude Code: How we use skills](https://claude.com/blog/lessons-from-building-claude-code-how-we-use-skills) — Anthropic, Jun 2026
+- "The Missing Manual: How to Write Great Skills" — Matt Pocock, AI Engineer World's Fair 2026 ([video](https://www.youtube.com/watch?v=UNzCG3lw6O0)), and his `writing-great-skills` skill
+
+## Parameters
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `target` | Path to skill directory or SKILL.md to evaluate | Ask user |
+| `output` | Path to write the scorecard | `<target>/EVALUATION.md` |
+| `compare` | Optional second skill to compare side-by-side | None |
+
+Also runs unattended: in CI, point `target` at skills changed in a PR and
+gate with `scripts/score.py --fail-below 60 ...` — non-zero exit below the
+threshold fails the check.
+
+## Criteria
+
+18 criteria: 14 core, scored on every skill, plus 4 conditional criteria
+scored only when the skill's category makes them apply — otherwise mark
+**N/A** and exclude the criterion from both the numerator and denominator of
+the weighted average. Every score is 0–100 with evidence citing file,
+section, or line.
+
+### Axis 1 — Trigger (invocation)
+
+| # | Criterion | Weight | Key question |
+|---|-----------|--------|---------------|
+| 1 | Invocation design | 2x | Is model-invoked vs. user-invoked deliberate and fitting? Model-invoked pays **context load** (the description loads every turn); user-invoked pays **cognitive load** (the human is the index). A skill that only ever fires by hand should be user-invoked. |
+| 2 | Description quality | 2x | Model-invoked: leading word up front, one trigger per branch (synonyms renaming the same branch are duplication), no identity that's redundant with the body. User-invoked (`disable-model-invocation: true`): a human-facing one-liner, no trigger list. Score against the mode the skill actually uses — never penalize a user-invoked skill for lacking trigger phrases. |
+
+### Axis 2 — Structure
+
+| # | Criterion | Weight | Key question |
+|---|-----------|--------|---------------|
+| 3 | Steps vs. reference clarity | 1x | Does the skill distinguish ordered steps from on-demand reference? All-reference and all-steps skills are both valid — score clarity, not the mix. Is related material co-located (definition, rules, caveats under one heading)? |
+| 4 | Branch-aware disclosure & pointers | 2x | Is material every branch needs inline, and material only some branches need behind a context pointer? Does each pointer's wording say when to follow it ("if you need X, read Y")? A weakly worded pointer to must-have material is a variance bug. |
+| 5 | Conciseness (no sprawl) | 2x | Is SKILL.md lean — under 500 lines as a ceiling, smaller is better — with every line earning its context cost? |
+| 6 | Coherent scope | 1x | Does the skill do one thing and compose with others, rather than covering too much? |
+
+### Axis 3 — Steering
+
+| # | Criterion | Weight | Key question |
+|---|-----------|--------|---------------|
+| 7 | Leading words | 2x | Does the skill use compact, high-prior terms ("vertical slice", "tight", "red") to anchor behavior, repeated consistently? Could any verbose passage collapse into one? |
+| 8 | Completion criteria & legwork | 2x | Skills with steps: does each step end on a checkable, exhaustive completion criterion? A vague one invites premature completion. Skills that are pure reference: is there an exhaustiveness bar over the reference itself ("every rule applied")? If neither applies, mark N/A. |
+| 9 | Gotchas section | 2x | Is there explicit capture of failure points, edge cases, footguns? |
+| 10 | Grounded in expertise | 2x | Does content come from observed failures and real project facts, or generic "best practices"? |
+| 11 | Avoids railroading | 1x | Does the skill leave room to adapt — procedures over declarations, defaults over menus — without over-prescribing? |
+
+### Axis 4 — Pruning
+
+| # | Criterion | Weight | Key question |
+|---|-----------|--------|---------------|
+| 12 | No-ops (deletion test) | 2x | Running the deletion test sentence by sentence: if removing a sentence leaves behavior unchanged, it's a no-op — including restatements of what the model already does by default. Cite line numbers for candidates. |
+| 13 | Single source of truth | 1x | Does each meaning live in exactly one place? Duplication between SKILL.md and references/ counts too. |
+| 14 | Relevance & sediment | 1x | Are there stale lines, accumulated layers, or material that no longer influences what the skill does? |
+
+### Conditional criteria
+
+Score only when the skill's category (from `references/categories.md`) makes
+the criterion apply; otherwise mark N/A and drop it from the weighted
+average entirely.
+
+| # | Criterion | Weight | Applies to category |
+|---|-----------|--------|----------------------|
+| 15 | Setup flow | 1x | library-and-api-reference, data-fetching-and-analysis, ci-cd-and-deployment, infrastructure-operations |
+| 16 | Memory mechanism | 1x | business-process-automation, data-fetching-and-analysis, runbooks |
+| 17 | Scripts & libraries | 1x | product-verification, code-scaffolding-and-templates, code-quality-and-review, data-fetching-and-analysis, infrastructure-operations |
+| 18 | On-demand hooks | 1x | code-quality-and-review, ci-cd-and-deployment |
+
+Override this table with judgment, in either direction: score a criterion
+for a skill outside these categories when it would clearly benefit (e.g., a
+non-`product-verification` skill that obviously needs a helper script), and
+mark it N/A even within an applicable category when the pattern doesn't fit
+the skill's shape (e.g., a pure-reference vocabulary skill filed under
+`code-quality-and-review` has nothing for a hook to enforce). Explain the
+override in the scorecard either way.
+
+### Overall score
+
+```
+overall = sum(score × weight) / sum(weight)
+```
+
+N/A criteria are excluded from both sums — never scored as 0, never counted
+as weight.
+
+## Scoring Guide
+
+| Score | Meaning |
+|-------|---------|
+| 0 | Not present at all |
+| 1–25 | Minimal/token effort, barely addresses the criterion |
+| 26–50 | Partially addressed but with significant gaps |
+| 51–75 | Solid implementation with room for improvement |
+| 76–90 | Strong implementation, minor gaps only |
+| 91–100 | Exemplary — would use as a reference for others |
+
+## Grade Scale
+
+| Grade | Range | Meaning |
+|-------|-------|---------|
+| A | 80–100 | Production-quality, reference skill |
+| B | 60–79 | Good skill, minor improvements needed |
+| C | 40–59 | Functional but significant gaps |
+| D | 20–39 | Needs substantial rework |
+| F | 0–19 | Skeleton only, not production-ready |
+
+## Workflow
+
+1. **Read the target skill** — SKILL.md, its frontmatter (check for
+   `disable-model-invocation`), and every file in the skill directory.
+2. **Read `references/mechanics.md`** — the vocabulary and tests Axes 1, 3,
+   and 4 depend on, including what makes a context pointer's wording
+   effective.
+3. **Classify** — use `references/categories.md` and its decision tree to
+   assign a category. The category determines which conditional criteria
+   apply.
+4. **Score all applicable criteria** — **cite-or-cut**: a criterion is only
+   scored once its justification cites specific evidence (file, section, or
+   line); no citation, no score. Mark N/A wherever the conditional table, or
+   your own judgment, says a criterion doesn't apply. Done when every
+   applicable criterion carries a score and a citation, and every N/A a
+   reason.
+5. **Trigger eval** — empirical test of whether the skill's description
+   actually causes invocation. See the **Trigger Eval** section below for
+   the full mechanic. Skip this step for user-invoked skills
+   (`disable-model-invocation: true`) — they have no description to test.
+6. **Diagnose failure modes** — done when every mode in the table below has
+   been checked against the skill and either cited (file:line) or dismissed.
+7. **Assess bonus patterns** — the 4 carried over from v1, plus a fifth:
+
+   | Bonus | Applies when | What to look for |
+   |-------|-------------|-----------------|
+   | Validation loops | Skill produces output or modifies state | Instructs the agent to self-check before finalizing |
+   | Output templates | Skill generates structured output | Includes a concrete template/example of expected format |
+   | Procedures over declarations | Skill teaches a method | Teaches *how to approach* problems, not *what to produce* for one case |
+   | Defaults over menus | Skill offers tool/approach choices | Picks a clear default, mentions alternatives briefly |
+   | Trace-checkable steering | Skill uses leading words | The leading words are distinctive enough that a user could grep the agent's reasoning traces to confirm the skill actually fired |
+
+   Report each as Present / Absent / N/A.
+8. **Compute the weighted score** — run `scripts/score.py` with one
+   `criterion:score:weight` triple per criterion (score `NA` to exclude); it
+   prints both sums, the overall, and the grade. Don't do this arithmetic by
+   hand.
+9. **Write the scorecard** to the output path — read
+   `references/output-template.md` first (it also holds the comparison-mode
+   template used when `compare` is set) and emit exactly that structure.
+
+## Trigger Eval
+
+Empirical test of whether the skill's description causes a model to invoke
+it when it should — and ignore it when it shouldn't. This is not a pass/fail
+gate; it produces observational data that feeds the scorecard and informs
+the failure-mode diagnosis.
+
+### When to run
+
+- Model-invoked skills only. User-invoked skills (`disable-model-invocation:
+  true`) have no description to test — skip and mark the section N/A.
+
+### Prompt generation
+
+Generate **10 prompts** from the skill's description, scope, and gotchas:
+
+- **5 should-trigger** — realistic user requests that fall squarely within
+  the skill's stated scope. Vary phrasing: some use the skill's vocabulary,
+  others describe the same need in naive/indirect language.
+- **5 should-not-trigger** — requests that are adjacent but clearly outside
+  scope (e.g., a sibling skill's territory, a task the description
+  explicitly excludes, or a generic request a model handles without any
+  skill).
+
+Each prompt should read like something a real user would type — no
+meta-language about skills, no hints.
+
+### Sub-agent execution
+
+Run each prompt in an independent sub-agent session with the target skill
+available. The sub-agent receives a single additional instruction appended to
+its system context:
+
+```
+At the end of your response, output exactly one line in this format:
+SKILLS_USED: <comma-separated list of skill names you loaded during this task, or "none">
+```
+
+This instruction is generic — it does not name the skill under test or hint
+at what should be triggered. The sub-agent operates normally; it either loads
+the skill or doesn't based on the prompt alone.
+
+### Detection
+
+Parse the `SKILLS_USED:` line from each sub-agent's response. Record per
+prompt:
+
+| Field | Value |
+|-------|-------|
+| Prompt | The test prompt text |
+| Expected | should-trigger / should-not-trigger |
+| Triggered | yes / no (was the target skill name in the list?) |
+| Other skills | Any other skills that fired |
+
+### What to report
+
+Report raw counts — no pass/fail judgment:
+
+- **Should-trigger hit rate** — X/5 triggered
+- **Should-not-trigger leak rate** — X/5 triggered (lower is better)
+- **Other skills observed** — which siblings fired on the same prompts
+
+These numbers feed criterion #1 (invocation design) and #2 (description
+quality) with empirical evidence, and may reveal failure modes like
+over-triggering or description weakness.
+
+### Practical notes
+
+- If the evaluation environment cannot spawn sub-agents (e.g., CI without
+  agent access), skip the trigger eval and note "trigger eval: skipped
+  (no agent access)" in the scorecard.
+- A single trial per prompt is acceptable given the observational (non-gating)
+  nature. Run multiple trials only if results are ambiguous.
+- Keep prompts in the scorecard output so the skill author can reuse them as
+  a regression set.
+
+## Failure-mode diagnosis
+
+Name the failure mode, cite evidence, prescribe the defense. Each mode's
+defense is defined once in `references/mechanics.md` §5 — prescribe from
+there. This replaces a generic "top improvements" list.
+
+| Mode | Evidence to look for |
+|------|----------------------|
+| Premature completion | Vague completion criteria with future steps still visible |
+| Weak steering | Instruction present but the agent doesn't reliably follow it |
+| Duplication | Same meaning in 2+ places, including SKILL.md vs. references/ |
+| Sediment | Stale layers, outdated references, dead instructions |
+| Sprawl | Long even with no duplication or sediment |
+| No-ops | Lines that don't change behavior versus the model's default |
+| Buried steps | Inline reference so heavy it soaks the steps |
+
+After the table, write a **Prioritized Actions** section: 3–5 highest-impact
+actions derived directly from the detected failure modes, each citing its
+evidence.
+
+Note: **context overload** — too many model-invoked skills competing for
+attention in one environment — is a portfolio-level problem, out of scope
+for evaluating a single skill. Record the description's context-load cost
+when it's notable; don't score the portfolio.
+
+## Gotchas
+
+- Tiny skills (under ~50 lines) flood the scorecard with N/A — score what's
+  there; a small, sharp skill can reach grade A on few criteria.
+- Self-evaluation bias: when the skill under review is one you (or this
+  session) wrote, apply the deletion test with extra skepticism — you will
+  want your own lines to matter.
+- Fresh rewrites still carry duplication: sediment needs time to settle, but
+  duplication can ship on day one. Run the pruning axis even on brand-new
+  skills.
+
+## Quality Checklist
+
+Final gate before delivering — each item names the step whose completion it
+re-checks, nothing new:
+
+- [ ] cite-or-cut held everywhere (step 4)
+- [ ] every N/A justified (step 4)
+- [ ] trigger eval run or skipped with reason (step 5)
+- [ ] every failure mode cited or dismissed (step 6)
+- [ ] 5 bonus patterns assessed (step 7)
+- [ ] score computed by `scripts/score.py`, not by hand (step 8)
diff --git a/.github/skills/skill-evaluation/references/categories.md b/.github/skills/skill-evaluation/references/categories.md
new file mode 100644
index 0000000..9cabfe4
--- /dev/null
+++ b/.github/skills/skill-evaluation/references/categories.md
@@ -0,0 +1,112 @@
+# Skill Categories Reference
+
+Sources:
+- [Lessons from building Claude Code: How we use skills](https://claude.com/blog/lessons-from-building-claude-code-how-we-use-skills) — Anthropic, Jun 2026
+- [Best practices for skill creators](https://agentskills.io/skill-creation/best-practices) — Agent Skills spec
+- [Extend Claude with skills](https://code.claude.com/docs/en/skills) — Claude Code docs
+
+---
+
+## 1. `library-and-api-reference`
+
+Skills that explain how to correctly use a library, CLI, or SDK. Can be internal or public libraries that the model struggles with. Often include reference code snippets and gotchas lists.
+
+**Signals:** Has API endpoint docs, CLI command reference, code examples, "how to call X" patterns.
+
+**Examples:** billing-lib, internal-platform-cli, sandbox-proxy
+
+---
+
+## 2. `product-verification`
+
+Skills that describe how to test or verify code is working. Often paired with Playwright, tmux, or other external tools. These have the most measurable impact on output quality — worth investing an engineer-week.
+
+**Signals:** Has test scripts, assertion patterns, Playwright/Cypress flows, "verify that X" instructions.
+
+**Examples:** signup-flow-driver, checkout-verifier, tmux-cli-driver
+
+---
+
+## 3. `data-fetching-and-analysis`
+
+Skills that connect to data and monitoring stacks. Include libraries to fetch data with credentials, dashboard IDs, common query patterns.
+
+**Signals:** Has database queries, dashboard references, metric/event schemas, "how to find X in our data" patterns.
+
+**Examples:** funnel-query, cohort-compare, grafana, datadog
+
+---
+
+## 4. `business-process-automation`
+
+Skills that automate repetitive workflows into one command. Usually simple instructions but may depend on other skills or MCPs. Saving results in log files helps consistency.
+
+**Signals:** Has "do this weekly/daily" patterns, aggregates from multiple sources, posts to Slack/channels, formats structured output.
+
+**Examples:** standup-post, create-ticket, weekly-recap
+
+---
+
+## 5. `code-scaffolding-and-templates`
+
+Skills that generate framework boilerplates for a specific function. May combine with composable scripts. Especially useful when scaffolding has natural-language requirements beyond pure code.
+
+**Signals:** Has templates, "new X" generators, boilerplate structures, asset files to copy.
+
+**Examples:** new-workflow, new-migration, create-app
+
+---
+
+## 6. `code-quality-and-review`
+
+Skills that enforce code quality and help review code. Can include deterministic scripts for robustness. May run as hooks or in GitHub Actions.
+
+**Signals:** Has style rules, review checklists, linting patterns, "reject if X" logic, adversarial review patterns.
+
+**Examples:** adversarial-review, code-style, testing-practices
+
+---
+
+## 7. `ci-cd-and-deployment`
+
+Skills that help fetch, push, and deploy code. May reference other skills to collect data.
+
+**Signals:** Has deploy commands, build pipelines, PR management, rollout/rollback logic, environment configs.
+
+**Examples:** babysit-pr, deploy-service, cherry-pick-prod
+
+---
+
+## 8. `runbooks`
+
+Skills that take a symptom (alert, error, Slack thread) and walk through multi-tool investigation producing a structured report.
+
+**Signals:** Has symptom→tool→diagnosis flows, "if you see X check Y" decision trees, report templates.
+
+**Examples:** service-debugging, oncall-runner, log-correlator
+
+---
+
+## 9. `infrastructure-operations`
+
+Skills that perform routine maintenance and ops, some involving destructive actions with guardrails. Make it easier to follow best practices in critical operations.
+
+**Signals:** Has cleanup/orphan detection, cost investigation, dependency approval, confirmation gates for destructive actions.
+
+**Examples:** resource-orphans, dependency-management, cost-investigation
+
+---
+
+## Classification Decision Tree
+
+1. Does it primarily teach how to **call an API/CLI/SDK**? → `library-and-api-reference`
+2. Does it **verify** that something works (test, assert, validate)? → `product-verification`
+3. Does it **query data** from monitoring/analytics/databases? → `data-fetching-and-analysis`
+4. Does it **automate a repeating team process** (standup, report, ticket)? → `business-process-automation`
+5. Does it **generate new code/files** from templates? → `code-scaffolding-and-templates`
+6. Does it **review/lint/enforce quality** on existing code? → `code-quality-and-review`
+7. Does it **build/deploy/ship** code to environments? → `ci-cd-and-deployment`
+8. Does it **diagnose problems** from symptoms to structured findings? → `runbooks`
+9. Does it perform **infrastructure maintenance/cleanup** with guardrails? → `infrastructure-operations`
+
+If a skill spans multiple categories, pick the one that describes its **primary action** — what the user gets when they invoke it.
diff --git a/.github/skills/skill-evaluation/references/mechanics.md b/.github/skills/skill-evaluation/references/mechanics.md
new file mode 100644
index 0000000..abaaedf
--- /dev/null
+++ b/.github/skills/skill-evaluation/references/mechanics.md
@@ -0,0 +1,116 @@
+# Mechanics: Predictability, Invocation, Hierarchy, Steering, Failure Modes
+
+Reference for scoring Axes 1, 3, and 4 of the skill-evaluation rubric — a
+deliberately self-contained condensation of Matt Pocock's `writing-great-skills`
+GLOSSARY, kept in-skill so the evaluator runs anywhere without that skill
+installed (sync manually if the upstream GLOSSARY changes). Not a tutorial:
+look a bolded term up here rather than re-deriving it.
+
+## 1. Root virtue: Predictability
+
+A skill exists to wrangle determinism out of a stochastic system.
+**Predictability** is the agent taking the same *process* every run, not
+producing the same output — a brainstorming skill should predictably diverge;
+its tokens vary, its behavior doesn't. Every criterion in the rubric is a lever
+on this one virtue: conciseness, steering, and pruning are symptoms of
+predictability, not separate virtues competing with it.
+
+## 2. Invocation trade-off
+
+Two invocation modes, each paying a different cost:
+
+- **Model-invoked** (default; no `disable-model-invocation`): keeps a
+  description the agent reads every turn. Pays permanent **context load** —
+  tokens and attention spent on every turn — in exchange for autonomous
+  firing and reachability by other skills.
+- **User-invoked** (`disable-model-invocation: true`): the description is
+  stripped from the agent's reach; only a human typing the skill's name can
+  fire it, and no other skill can reach it either. Zero context load, but
+  spends **cognitive load** — the human becomes the index of which skills
+  exist and when to reach for each.
+
+Pick model-invocation only when the agent must reach the skill on its own, or
+another skill must reach it. A skill that only ever fires by hand should be
+user-invoked and carry no trigger scaffolding it doesn't need. When
+user-invoked skills multiply past what a human can remember, a **router
+skill** — one user-invoked skill naming the others and when to reach for each
+— cures the accumulated cognitive load. That fix operates at the portfolio
+level, not the single-skill level this evaluation scores.
+
+## 3. Content types & hierarchy
+
+A skill mixes two content types freely: **steps** (ordered actions, each
+ending on a **completion criterion**) and **reference** (definitions, rules,
+facts consulted on demand). All-steps, all-reference, and mixed skills are
+equally valid — neither shape is a smell.
+
+The **information hierarchy** ranks material by how immediately the agent
+needs it: in-skill step, then in-skill reference, then reference disclosed
+behind a **context pointer** in a linked file. Material every **branch** (a
+distinct way the skill is invoked) needs belongs inline; material only some
+branches need belongs behind a pointer — branching is the disclosure test. A
+pointer's *wording*, not its target, decides whether the agent reaches it and
+how reliably; a must-have target behind weak wording is a variance bug, and
+the fix is sharper wording, tried before pulling the material back inline.
+
+**Co-location** governs what sits beside a piece of content once placed: a
+concept's definition, rules, and caveats belong under one heading, not
+scattered, so reading one part brings its neighbors with it.
+
+## 4. Steering
+
+**Leading words** are compact, pretrained concepts (*tight*, *red*, *lesson*)
+the agent thinks with while executing. Repeated consistently, they recruit
+priors the model already holds and anchor a region of behavior in the fewest
+tokens — cheaper and stickier than spelling the same quality out in prose. A
+leading word works twice: in the body it anchors execution (the same behavior
+fires every time the word appears); in the description it anchors invocation.
+It is also **trace-checkable** — distinctive enough that its appearance in the
+agent's reasoning traces confirms the skill actually shaped behavior.
+
+A completion criterion must be *checkable* (can the agent tell done from
+not-done?) and, where it matters, *exhaustive* ("every X accounted for", not
+"produce a list"). A vague criterion invites **premature completion** —
+attention slipping to being done rather than to the work. The exhaustiveness
+demand also binds flat reference with no steps: "every rule applied" drives
+thorough **legwork** over a checklist the same way a sharp step criterion
+drives it over an action.
+
+Skills should **avoid railroading**: procedures the agent adapts, not
+declarations of exact output; defaults with brief alternatives, not
+exhaustive menus.
+
+## 5. Failure modes
+
+- **Premature completion** — ending a step before it's genuinely done.
+  Defense, in order: sharpen the completion criterion first (cheap, local);
+  only if it's irreducibly vague *and* the rush is actually observed, split
+  the sequence so later steps are hidden.
+- **Duplication** — the same meaning in more than one place. Costs
+  maintenance and tokens, and inflates that meaning's rank past its real
+  weight. Fix: collapse to a **single source of truth**, often via a leading
+  word.
+- **Sediment** — stale layers that accumulate because adding feels safe and
+  removing feels risky. The default fate of any skill without a pruning
+  discipline.
+- **Sprawl** — a skill simply too long, independent of whether lines are
+  stale or duplicated. Cure: disclose reference behind pointers, split by
+  branch or sequence so each path carries only what it needs.
+- **No-op** — a line that changes nothing because the model already does it
+  by default. The test: does it change behavior versus the default? Apply
+  the **deletion test** sentence by sentence, not paragraph by paragraph — if
+  removing the sentence leaves behavior unchanged, delete the whole sentence,
+  don't trim words from it. A weak leading word (*be thorough* when the agent
+  is already thorough-ish) is a no-op; the fix is a stronger word
+  (*relentless*), not a different technique.
+- **Weak steering** — an instruction is present but the agent doesn't
+  reliably follow it. Usually a leading word too weak to beat the default, or
+  no leading word at all where a verbose passage is trying to do its job.
+- **Buried steps** — in-file reference so heavy it soaks the steps beneath
+  it, turning attention to them into a coin-flip. Defense: progressive
+  disclosure — push the reference behind a pointer.
+
+**Relevance vs. no-op**: relevance asks whether a line still bears on the
+task; no-op asks whether it changes behavior. A line can be relevant (right
+topic) and still be a no-op (the model would do it anyway) — run both checks,
+they don't imply each other.
diff --git a/.github/skills/skill-evaluation/references/output-template.md b/.github/skills/skill-evaluation/references/output-template.md
new file mode 100644
index 0000000..3240c85
--- /dev/null
+++ b/.github/skills/skill-evaluation/references/output-template.md
@@ -0,0 +1,158 @@
+# Output Template
+
+Emit the scorecard exactly in this structure (step 9 of the workflow).
+
+```markdown
+# Skill Evaluation — {skill name}
+
+> Evaluated: {date}
+> Source: {path}
+> Evaluator: skill-evaluation v2.1.0
+> Framework: [Anthropic Skill Best Practices](https://claude.com/blog/lessons-from-building-claude-code-how-we-use-skills) + Matt Pocock's [writing-great-skills](https://www.youtube.com/watch?v=UNzCG3lw6O0)
+
+## Summary
+
+| Metric | Value |
+|--------|-------|
+| Overall Score | {weighted}/100 |
+| Grade | {A/B/C/D/F} |
+| Category | {category} |
+| Invocation | {model-invoked / user-invoked} |
+| Files | {count} |
+| Criteria scored / N/A | {n} scored, {m} N/A |
+
+## Scorecard
+
+### Axis 1 — Trigger
+
+| # | Criterion | Weight | Score | Notes |
+|---|-----------|--------|-------|-------|
+| 1 | Invocation design | 2x | {n}/100 | {evidence} |
+| 2 | Description quality | 2x | {n}/100 | {evidence} |
+
+### Axis 2 — Structure
+
+| # | Criterion | Weight | Score | Notes |
+|---|-----------|--------|-------|-------|
+| 3 | Steps vs. reference clarity | 1x | {n}/100 | {evidence} |
+| 4 | Branch-aware disclosure & pointers | 2x | {n}/100 | {evidence} |
+| 5 | Conciseness | 2x | {n}/100 | {evidence} |
+| 6 | Coherent scope | 1x | {n}/100 | {evidence} |
+
+### Axis 3 — Steering
+
+| # | Criterion | Weight | Score | Notes |
+|---|-----------|--------|-------|-------|
+| 7 | Leading words | 2x | {n}/100 | {evidence} |
+| 8 | Completion criteria & legwork | 2x | {n/100 or N/A} | {evidence} |
+| 9 | Gotchas section | 2x | {n}/100 | {evidence} |
+| 10 | Grounded in expertise | 2x | {n}/100 | {evidence} |
+| 11 | Avoids railroading | 1x | {n}/100 | {evidence} |
+
+### Axis 4 — Pruning
+
+| # | Criterion | Weight | Score | Notes |
+|---|-----------|--------|-------|-------|
+| 12 | No-ops (deletion test) | 2x | {n}/100 | {evidence with line citations} |
+| 13 | Single source of truth | 1x | {n}/100 | {evidence} |
+| 14 | Relevance & sediment | 1x | {n}/100 | {evidence} |
+
+### Conditional criteria
+
+| # | Criterion | Weight | Score | Notes |
+|---|-----------|--------|-------|-------|
+| 15 | Setup flow | 1x | {n/100 or N/A} | {evidence or reason for N/A} |
+| 16 | Memory mechanism | 1x | {n/100 or N/A} | {evidence or reason for N/A} |
+| 17 | Scripts & libraries | 1x | {n/100 or N/A} | {evidence or reason for N/A} |
+| 18 | On-demand hooks | 1x | {n/100 or N/A} | {evidence or reason for N/A} |
+
+## Trigger Eval
+
+{For user-invoked skills, write: "N/A — user-invoked skill, no description to test."}
+
+### Prompts tested
+
+| # | Prompt | Expected | Triggered | Other skills |
+|---|--------|----------|-----------|--------------|
+| 1 | {prompt text} | should-trigger | yes/no | {list or none} |
+| 2 | {prompt text} | should-trigger | yes/no | {list or none} |
+| 3 | {prompt text} | should-trigger | yes/no | {list or none} |
+| 4 | {prompt text} | should-trigger | yes/no | {list or none} |
+| 5 | {prompt text} | should-trigger | yes/no | {list or none} |
+| 6 | {prompt text} | should-not-trigger | yes/no | {list or none} |
+| 7 | {prompt text} | should-not-trigger | yes/no | {list or none} |
+| 8 | {prompt text} | should-not-trigger | yes/no | {list or none} |
+| 9 | {prompt text} | should-not-trigger | yes/no | {list or none} |
+| 10 | {prompt text} | should-not-trigger | yes/no | {list or none} |
+
+### Results
+
+| Metric | Value |
+|--------|-------|
+| Should-trigger hit rate | {X}/5 |
+| Should-not-trigger leak rate | {X}/5 |
+| Other skills observed | {list or none} |
+
+### Observations
+
+{Free-form notes: patterns in what triggered or didn't, description wording
+gaps revealed, sibling skills that competed, etc.}
+
+## Failure Modes Detected
+
+| Mode | Evidence | Root cause | Defense |
+|------|----------|------------|---------|
+| {mode, or a single row "None detected"} | {file:line} | {cause} | {defense} |
+
+## Prioritized Actions
+
+### 1. {action}
+
+**Evidence:** {file:line or section}
+
+**Fix:** {specific recommendation}
+
+### 2. {action}
+
+**Evidence:** {file:line or section}
+
+**Fix:** {specific recommendation}
+
+(3–5 total, each tied to a detected failure mode)
+
+## Bonus Patterns
+
+| Pattern | Status | Notes |
+|---------|--------|-------|
+| Validation loops | {Present/Absent/N/A} | {detail} |
+| Output templates | {Present/Absent/N/A} | {detail} |
+| Procedures over declarations | {Present/Absent/N/A} | {detail} |
+| Defaults over menus | {Present/Absent/N/A} | {detail} |
+| Trace-checkable steering | {Present/Absent/N/A} | {detail} |
+
+## Grade Scale
+
+{copy the Grade Scale table from SKILL.md}
+
+---
+
+*Generated by [skill-evaluation](https://github.com/fabricioctelles/skills) v2.1.0, merging the [Anthropic skill quality framework](https://claude.com/blog/lessons-from-building-claude-code-how-we-use-skills) with Matt Pocock's [writing-great-skills](https://www.youtube.com/watch?v=UNzCG3lw6O0) methodology.*
+```
+
+## Comparison mode
+
+When `compare` is set, add a side-by-side table across all 18 criteria.
+Leave a cell N/A rather than scoring it 0, and exclude N/A rows from the
+Overall row's weighted math for that skill.
+
+```markdown
+## Comparison: {skill A} vs {skill B}
+
+| # | Criterion | {A} | {B} | Delta |
+|---|-----------|-----|-----|-------|
+| 1 | Invocation design | 60 | 85 | +25 |
+| 2 | Description quality | 25 | 70 | +45 |
+| ... | ... | ... | ... | ... |
+| 15 | Setup flow | N/A | 80 | — |
+| **Overall** | | **43** | **72** | **+29** |
+```
diff --git a/.github/skills/skill-evaluation/scripts/score.py b/.github/skills/skill-evaluation/scripts/score.py
new file mode 100644
index 0000000..176f425
--- /dev/null
+++ b/.github/skills/skill-evaluation/scripts/score.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+"""Weighted overall score for a skill-evaluation scorecard.
+
+Usage:
+    score.py [--fail-below N] 1:80:2 2:65:2 3:85:1 ... 15:NA:1 16:NA:1
+
+One arg per criterion, formatted criterion:score:weight.
+Score NA (or N/A) excludes the criterion from both sums.
+Prints sum(score x weight), sum(weight), overall, and grade.
+--fail-below N exits non-zero when overall < N (CI gate).
+"""
+import sys
+
+
+def grade(score: float) -> str:
+    if score >= 80:
+        return "A"
+    if score >= 60:
+        return "B"
+    if score >= 40:
+        return "C"
+    if score >= 20:
+        return "D"
+    return "F"
+
+
+def main() -> None:
+    args = sys.argv[1:]
+    fail_below = None
+    if "--fail-below" in args:
+        i = args.index("--fail-below")
+        try:
+            fail_below = float(args[i + 1])
+        except (IndexError, ValueError):
+            sys.exit("--fail-below requires a numeric threshold")
+        del args[i : i + 2]
+    if not args:
+        sys.exit(__doc__)
+    num = den = 0.0
+    na = []
+    for arg in args:
+        try:
+            crit, score, weight = arg.split(":")
+        except ValueError:
+            sys.exit(f"bad arg {arg!r}: expected criterion:score:weight")
+        if score.strip().upper() in ("NA", "N/A"):
+            na.append(crit)
+            continue
+        s, w = float(score), float(weight)
+        if not 0 <= s <= 100:
+            sys.exit(f"criterion {crit}: score {s} outside 0-100")
+        num += s * w
+        den += w
+    if den == 0:
+        sys.exit("no applicable criteria")
+    overall = num / den
+    print(f"applicable criteria: {len(args) - len(na)}  |  N/A: {', '.join(na) or 'none'}")
+    print(f"sum(score x weight) = {num:g}")
+    print(f"sum(weight) = {den:g}")
+    print(f"overall = {overall:.2f}  ->  grade {grade(overall)}")
+    if fail_below is not None and overall < fail_below:
+        sys.exit(f"FAIL: overall {overall:.2f} below threshold {fail_below:g}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/.github/skills/slop-eval/SKILL.md b/.github/skills/slop-eval/SKILL.md
new file mode 100644
index 0000000..2fea539
--- /dev/null
+++ b/.github/skills/slop-eval/SKILL.md
@@ -0,0 +1,263 @@
+---
+name: slop-eval
+description: >
+  Objectively evaluate a UI/web design against the pols.dev anti-slop design
+  law: detect catalogued slop tells with cited evidence, score 8 weighted
+  axes (color, type, components, layout, motion, execution, signature,
+  cohesion), and emit a Slop Report with a 0–100 Slop Index and grade. Use
+  when the user asks to "evaluate design slop", "slop report", "is this
+  design AI slop", "audit this landing page design", "de-slop review", or
+  wants an objective score of how generic/machine-made a design looks. To
+  fix text (not design), use human-ai or humanizar skills instead.
+metadata:
+  author: https://ft.ia.br
+  version: "1.0.0"
+  date: 2026-07-16
+  repository: https://github.com/fabricioctelles/skills
+  license: Apache-2.0
+  category: code-quality-and-review
+---
+
+# Slop Eval
+
+Evaluate a design the way `skill-evaluation` evaluates a skill: every finding
+cites concrete evidence, every axis gets a 0–100 score, arithmetic runs
+through a script, and the output is a structured report — never a vibe check.
+
+The tell catalog lives in `references/tells.md`; read it before sweeping.
+The positive rubric (signature formula, cohesion checks, slop→premium pairs)
+lives in `references/premium-markers.md`; read it before scoring Axes 7–8.
+
+## Source
+
+- [The pols.dev anti-slop design law](https://pols.dev/slop.md) — the tell
+  catalog, absolute rules, and signature formula are distilled from it.
+- Method modeled on
+  [skill-evaluation](https://github.com/fabricioctelles/skills/tree/main/skills/skill-evaluation)
+  (cite-or-cut, weighted axes, scripted scoring, failure-mode diagnosis).
+
+## Parameters
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `target` | What to evaluate: live URL, screenshot(s), code path, or Figma export | Ask user |
+| `brief` | Brand brief or explicit user directions the design followed | None |
+| `output` | Path to write the report | `./SLOP-REPORT.md` |
+
+Write the report in the language the user is speaking; keep tell IDs and
+names in English so they stay greppable against the catalog.
+
+## Evidence channels
+
+What you can verify depends on what you were given. Never score a check you
+could not observe — mark it **Unverifiable** and exclude it (like N/A in
+skill-evaluation).
+
+| Channel | Can verify | Cannot verify |
+|---------|-----------|---------------|
+| Code (CSS/JSX/HTML) | Fonts, hex values, gradients, shadows, radii, `opacity:0` gating, icon imports, layout skeletons | Optical centering, rendered contrast, seams, whether controls respond |
+| Screenshot(s) | Everything visual: palette, type, layout, alignment, centering, clipping, contrast, seams | Hover/scroll motion, dead controls, invisible-content trap, responsive behavior |
+| Live URL (browse + screenshot) | All of the above plus interactions, motion, fold ownership | Only what you didn't exercise |
+
+With code, grep before you stare: `fonts.googleapis|next/font`,
+`lucide-react`, `linear-gradient`, `box-shadow`, `border-radius: *9999`,
+`backdrop-filter`, `opacity: *0`, `initial={{ *opacity: *0`,
+`overflow: *hidden`, `clip-path`, `position: *fixed`. Each hit is a lead,
+not a verdict — confirm against the catalog entry before recording it.
+
+## Evidence acquisition SOP
+
+Route by what the `target` is; always end with an evidence inventory
+(what was captured, what is Unverifiable) — it feeds the report header.
+
+**Live URL** — the richest channel; prefer it whenever reachable.
+Use whatever browser automation this session has (a browser MCP such as
+Playwright or Chrome DevTools, or `npx playwright screenshot` as the
+no-MCP fallback) and capture, saving every artifact to the scratchpad so
+findings can cite `file + region`:
+
+1. Load at desktop (1440×900) and mobile (390×844); wait for network idle.
+2. Full-page screenshot of both viewports **immediately after load,
+   before any scrolling** — sections sitting at `opacity:0` waiting for a
+   scroll reveal show up blank here (M1 evidence).
+3. Scroll pass top to bottom, then a second full-page capture; diff the
+   two mentally for reveal-gated content, seams (C11, X13), and fold
+   ownership (L16).
+4. Interaction pass: hover the primary CTA, one card, one nav link
+   (M2–M4); click every tab, accordion, toggle, and button (M8); Tab
+   through the page and confirm a visible focus ring (X14).
+5. Zoom crops at 2x of: anything near a clipped edge (X2), circled/tiled
+   numbers and icons (X1), pricing columns side by side (X3), button
+   labels (X5).
+6. Pull the rendered sources for the code-channel greps: font names from
+   the network panel or `<link>`/`@font-face`, computed hex values from
+   the stylesheets.
+
+No browser automation available → fetch the HTML/CSS (`curl`) and run the
+code channel on it, ask the user for full-page desktop + mobile prints,
+and mark every visual-only and interaction check Unverifiable until the
+prints arrive. Never score a visual check from raw HTML.
+
+**Screenshots** — Read each image. If only partial crops were provided,
+ask for full-page desktop + mobile before sweeping (a hero-only print
+cannot support L11, L15, or the cohesion axis). All interaction checks
+(M1, M8, X14, hover tells) are Unverifiable.
+
+**Code path** — run the greps, read every file they hit, plus the layout/
+page components and global styles. If the project runs locally, start its
+dev server and continue under the Live URL SOP — code plus a live render
+is the only combination that can verify everything.
+
+**Figma export** — treat as Screenshots for visual tells; additionally
+fonts, hex values, and spacing are exact from the file. Motion and
+interaction axes are Unverifiable (score `NA` for Axis 5 unless
+prototypes were shared).
+
+## Axes and weights
+
+| # | Axis | Weight | Scored from |
+|---|------|--------|-------------|
+| 1 | Color & Light | 2x | Tells C1–C15 |
+| 2 | Typography & Copy | 2x | Tells T1–T10, W1–W3 |
+| 3 | Components & Ornament | 1x | Tells K1–K27 |
+| 4 | Layout & Composition | 2x | Tells L1–L21 |
+| 5 | Motion & Interaction | 1x | Tells M1–M8 |
+| 6 | Execution & Craft | 2x | Tells X1–X14 |
+| 7 | Signature & Uniqueness | 3x | 7-element formula (positive rubric) |
+| 8 | Cohesion | 2x | 4 checks (positive rubric) |
+
+Axis 7 carries the heaviest weight on purpose: the law's deepest rule is
+that dodging the tell list is still slop — a page with zero tells and no
+signature is unfinished work wearing restraint as an alibi.
+
+## Scoring
+
+**Axes 1–6 (tell-counted).** Count confirmed tells on the axis by severity,
+then: `score = max(0, 100 − 30·critical − 15·major − 5·minor)`. Run
+`scripts/score.py axis CRIT MAJOR MINOR` — don't do it by hand. One tell,
+one count: a pattern repeated across sections is still one tell (note the
+repetition in the evidence; repetition may upgrade minor → major where the
+catalog says so).
+
+**Axis 7 (Signature).** Score each of the 7 formula elements 0 (absent),
+50 (attempted, weak), or 100 (strong) per the rubric in
+`premium-markers.md`; the axis is their mean.
+
+**Axis 8 (Cohesion).** Same 0/50/100 on the 4 cohesion checks; mean.
+
+**Compounding rule.** Three or more *major* layout tells on one page cap
+Axis 4 at 40 — a page assembled from known skeletons is slop no matter how
+clean each block is.
+
+**Gates** (pass as `--cap` to the overall run):
+- **Signature gate:** Axis 7 < 40 caps the overall at 59 (grade C max). No
+  amount of clean spacing rescues a page with no signature.
+- **Absolute-rule gate:** any confirmed critical tell caps the overall at
+  69 (no grade A with broken execution).
+
+**Overall & Slop Index.**
+
+```
+overall    = sum(axis_score × weight) / sum(weight)   # capped by gates
+Slop Index = 100 − overall
+```
+
+Run `scripts/score.py overall 1:80:2 2:65:2 ... [--cap 59] [--cap 69]`.
+Unverifiable axes score `NA` and drop out of both sums. `--fail-below N`
+exits non-zero for CI gating, e.g. gating a PR on its preview deploy:
+
+```yaml
+# .github/workflows/slop-gate.yml (step excerpt)
+- name: Slop gate
+  run: |
+    # run slop-eval against $PREVIEW_URL, export each axis score, then:
+    python3 skills/slop-eval/scripts/score.py overall \
+      1:$A1:2 2:$A2:2 3:$A3:1 4:$A4:2 5:$A5:1 6:$A6:2 7:$A7:3 8:$A8:2 \
+      --fail-below 40
+```
+
+## Grade scale
+
+| Grade | Overall | Slop Index | Verdict |
+|-------|---------|------------|---------|
+| A | 80–100 | 0–20 | Premium — deliberate, signed, executed |
+| B | 60–79 | 21–40 | Considered — mostly deliberate, some defaults |
+| C | 40–59 | 41–60 | Generic — clean but templated or unsigned |
+| D | 20–39 | 61–80 | Slop — assembled from presets |
+| F | 0–19 | 81–100 | Pure slop |
+
+## Absolute rules check
+
+Six execution laws, each pass/fail/unverifiable, reported in their own
+table. Any **fail** is a critical tell (counts on its axis AND triggers the
+absolute-rule gate):
+
+1. **Content visible by default** — nothing gated on an entrance animation
+   (`opacity:0` + reveal) (M1)
+2. **Clear the cut** — no text/control sliced by clip, notch, overflow, or
+   fixed height (X2, X11)
+3. **Parallel alignment** — comparable columns share baselines; buttons
+   anchored (X3)
+4. **Real centering** — everything meant to be centered is, mathematically
+   and optically (X1)
+5. **Legible contrast** — every text clears its background by a real value
+   gap (X5)
+6. **Controls work** — every interactive-looking control responds (M8)
+
+## Workflow
+
+1. **Gather evidence** — route the `target` through the Evidence
+   acquisition SOP above. Done when the evidence inventory states what
+   was captured and what is Unverifiable.
+2. **Read `references/tells.md`** — the catalog you sweep against.
+3. **Sweep axes 1–6** — walk the catalog group by group. **Cite-or-cut**:
+   a tell is only recorded with concrete evidence (hex value, font name,
+   `file:line`, or screenshot region); no evidence, no tell. Check each
+   candidate against its premium-pair note — the crafted version of a
+   pattern is not the tell. Done when every catalog group has been swept
+   and every recorded tell carries a citation.
+4. **Run the absolute rules check** — all six, pass/fail/unverifiable with
+   evidence.
+5. **Score Axes 7–8** — read `references/premium-markers.md`, score the 7
+   signature elements and 4 cohesion checks with one-line justifications
+   each. Done when all 11 items carry a score and a justification.
+6. **Compute** — `score.py axis` per tell-counted axis, then
+   `score.py overall` with weights and any triggered `--cap`. Never
+   hand-compute.
+7. **Write the report** — read `references/output-template.md` and emit
+   exactly that structure to `output`, ending with the 3–5 prioritized
+   fixes that would move the score most (biggest weighted deltas first;
+   a missing signature usually outranks any single tell).
+
+## Gotchas
+
+- **The brief overrides the law.** If the user or brand explicitly directed
+  a choice (a color, a layout, an effect), it is not a tell — the law
+  itself says the user's word wins 100%. Ask for the brief when the design
+  clearly follows one; note excluded tells in the report.
+- **Context flips a tell.** Mono on real data is correct; a populated,
+  real-feeling product window is a signature, not the fake-window tell; a
+  tight micro-grid with texture is premium, a full-page graph paper is
+  slop. Always check the premium pair before recording.
+- **Don't reward the clean miss.** Zero tells with a weak signature is the
+  most common failure of designs that *tried* to avoid slop. The signature
+  gate exists for this — apply it without mercy.
+- **Severity discipline.** Critical is reserved for *broken* (the six
+  absolute rules). A blue-purple gradient is loud but not broken: major.
+- **One-axis bleed.** Some tells could sit on two axes (cut-off glow is
+  color and execution). The catalog assigns each tell to exactly one axis —
+  count it only there.
+- **Portfolio tells.** L19 (recycling your own house style) needs prior
+  work from the same author to verify; without it, mark Unverifiable
+  rather than guessing.
+
+## Quality checklist
+
+Final gate before delivering — each item re-checks a workflow step:
+
+- [ ] every recorded tell has ID + severity + citation (step 3)
+- [ ] every unverifiable check is marked, not silently passed (steps 1, 4)
+- [ ] all 6 absolute rules reported (step 4)
+- [ ] all 11 signature/cohesion items scored with justification (step 5)
+- [ ] caps applied when gates triggered; math from `score.py` only (step 6)
+- [ ] report matches the template, fixes ranked by weighted impact (step 7)
diff --git a/.github/skills/slop-eval/references/output-template.md b/.github/skills/slop-eval/references/output-template.md
new file mode 100644
index 0000000..0fd7c02
--- /dev/null
+++ b/.github/skills/slop-eval/references/output-template.md
@@ -0,0 +1,130 @@
+# Output Template
+
+Emit the Slop Report exactly in this structure (step 7 of the workflow),
+in the user's language, keeping tell IDs/names in English.
+
+```markdown
+# Slop Report — {design/site name}
+
+> Evaluated: {date}
+> Target: {URL / path / screenshot description}
+> Evidence channels: {code / screenshots / live URL} — unverifiable: {list or "none"}
+> Brief provided: {yes — summarize / no}
+> Evaluator: slop-eval v1.0.0
+> Framework: [pols.dev anti-slop design law](https://pols.dev/slop.md)
+
+## Summary
+
+| Metric | Value |
+|--------|-------|
+| **Slop Index** | **{100 − overall}/100** |
+| Overall score | {overall}/100 {(capped by: signature gate / absolute-rule gate)} |
+| Grade | {A–F} — {verdict from the grade scale} |
+| Tells detected | {n} ({c} critical, {m} major, {k} minor) |
+| Signature (Axis 7) | {score}/100 |
+| Absolute rules | {p} pass · {f} fail · {u} unverifiable |
+
+{One-paragraph verdict in plain words: what this design is, what sinks or
+carries it. Lead with the single most consequential finding.}
+
+## Scorecard
+
+| # | Axis | Weight | Score | Tells (crit/maj/min) |
+|---|------|--------|-------|----------------------|
+| 1 | Color & Light | 2x | {n}/100 | {c}/{m}/{k} |
+| 2 | Typography & Copy | 2x | {n}/100 | {c}/{m}/{k} |
+| 3 | Components & Ornament | 1x | {n}/100 | {c}/{m}/{k} |
+| 4 | Layout & Composition | 2x | {n}/100 {(compounding cap)} | {c}/{m}/{k} |
+| 5 | Motion & Interaction | 1x | {n or NA}/100 | {c}/{m}/{k} |
+| 6 | Execution & Craft | 2x | {n}/100 | {c}/{m}/{k} |
+| 7 | Signature & Uniqueness | 3x | {n}/100 | — |
+| 8 | Cohesion | 2x | {n}/100 | — |
+
+## Detected tells
+
+Ordered critical → major → minor. Every row cites evidence; a tell with no
+citation does not appear.
+
+| ID | Tell | Sev | Evidence |
+|----|------|-----|----------|
+| {M1} | {Invisible-content trap} | crit | {file:line, hex, font name, or screenshot region — concrete} |
+| ... | | | |
+
+{Tells excluded because the brief directed them, if any:}
+| ID | Tell | Excluded because |
+|----|------|------------------|
+
+## Absolute rules
+
+| # | Rule | Status | Evidence |
+|---|------|--------|----------|
+| 1 | Content visible by default | {pass/FAIL/unverifiable} | {evidence} |
+| 2 | Clear the cut | {…} | {…} |
+| 3 | Parallel alignment | {…} | {…} |
+| 4 | Real centering | {…} | {…} |
+| 5 | Legible contrast | {…} | {…} |
+| 6 | Controls work | {…} | {…} |
+
+## Signature assessment (Axis 7)
+
+| # | Element | Score | Justification |
+|---|---------|-------|---------------|
+| S1 | Signature artifact | {0/50/100} | {one line} |
+| S2 | Atmosphere | {…} | {…} |
+| S3 | Layered depth | {…} | {…} |
+| S4 | Character display face | {…} | {…} |
+| S5 | Bespoke silhouette | {…} | {…} |
+| S6 | Treated nav | {…} | {…} |
+| S7 | Real specificity | {…} | {…} |
+
+## Cohesion assessment (Axis 8)
+
+| # | Check | Score | Justification |
+|---|-------|-------|---------------|
+| H1 | One palette | {0/50/100} | {one line} |
+| H2 | One type voice | {…} | {…} |
+| H3 | One system | {…} | {…} |
+| H4 | Composed from the brief | {…} | {…} |
+
+## Prioritized fixes
+
+3–5, ranked by weighted score impact — a missing signature (3x axis)
+usually outranks any single tell. Each fix names the tell/element, cites
+its evidence, and prescribes the premium version from the slop→premium
+pairs table where one exists.
+
+### 1. {fix}
+
+**Evidence:** {citation} · **Impact:** {axis, est. delta}
+
+**Do:** {specific prescription — the premium pair, not just "remove it"}
+
+### 2. {fix}
+
+...
+
+## Grade scale
+
+{copy the Grade scale table from SKILL.md}
+
+---
+
+*Generated by [slop-eval](https://github.com/fabricioctelles/skills) v1.0.0
+against [the pols.dev anti-slop design law](https://pols.dev/slop.md).*
+```
+
+## Comparison mode
+
+When evaluating two designs (or before/after), add a side-by-side axis
+table. NA rows drop from each side's weighted math.
+
+```markdown
+## Comparison: {A} vs {B}
+
+| # | Axis | {A} | {B} | Delta |
+|---|------|-----|-----|-------|
+| 1 | Color & Light | 40 | 85 | +45 |
+| ... | | | | |
+| **Overall** | | **38** | **74** | **+36** |
+| **Slop Index** | | **62** | **26** | **−36** |
+```
diff --git a/.github/skills/slop-eval/references/premium-markers.md b/.github/skills/slop-eval/references/premium-markers.md
new file mode 100644
index 0000000..3eb3055
--- /dev/null
+++ b/.github/skills/slop-eval/references/premium-markers.md
@@ -0,0 +1,89 @@
+# Premium markers — the positive rubric
+
+Axes 7 and 8 measure what the design *did*, not what it avoided. A page can
+dodge every tell and still be slop because nothing was invented; these two
+axes are where that failure is priced in.
+
+## Axis 7 — Signature & Uniqueness
+
+The uniqueness formula from the law:
+
+```
+uniqueness = one signature artifact + atmosphere + layered depth
+           + character display face + one bespoke silhouette
+           + a treated nav + real specificity
+```
+
+Score each element 0 / 50 / 100 with a one-line justification; the axis is
+the mean of the seven.
+
+| # | Element | 0 (absent) | 50 (attempted) | 100 (strong) |
+|---|---------|------------|----------------|---------------|
+| S1 | Signature artifact | No custom focal object; hero is text on a fill or a stock/prop visual | A custom visual exists but is generic enough to paste onto another site | ONE high-effort focal object (crafted SVG scene, populated product artifact, illustration, render) that could only belong to this brand |
+| S2 | Atmosphere | Flat color fill behind everything | Some texture/tone in the hero only (dies at the fold — see L11) | The background is a composed environment (scene, texture, grain, light) carried down the whole scroll |
+| S3 | Layered depth | One flat plane | Two reads, nothing crossing a boundary | Foreground copy / midground focal object / background scene, with at least one element overlapping or bleeding across a layer edge |
+| S4 | Character display face | Identity rests on a neutral grotesque or a Google-shelf default (T1) | A distinctive face chosen by reputation, not brief (T9) | A licensed/self-hosted display face with real personality, set large, chosen for this brief (body may stay neutral; system-ui is a legitimately neutral body) |
+| S5 | Bespoke silhouette | Every shape is a default rectangle/pill | One mild customization (a radius decision, a simple notch) | One unmistakable custom-cut geometry signing the page (a receipt-torn edge, an invented marker, a specific arrow drawn for the system) |
+| S6 | Treated nav | Default flush row of links bolted on top | Centered or contained but generic | The nav is a decision: floated pill, real presence, brand marks threaded in — it belongs to the system |
+| S7 | Real specificity | Placeholder logos, fake names, lorem-adjacent copy | Mixed: some real data among placeholders | Real recognizable logos honestly claimable, real names/data inside the product shot, copy written for this product |
+
+Signature gate: axis mean < 40 caps the overall at 59. Even the most
+minimal premium site has at least the signature artifact and a character
+face.
+
+## Axis 8 — Cohesion
+
+"Cohesion is the whole game": the loudest observed failure is not tells,
+it is individually-fine parts that don't belong to each other. Score each
+check 0 / 50 / 100; the axis is the mean of the four.
+
+| # | Check | What 100 looks like |
+|---|-------|---------------------|
+| H1 | One palette, held with discipline | A monochrome or tightly-related set; adjacent sections share or hand off tone. 0 = "blue AND green AND a warm accent", each fine alone, ugly together. |
+| H2 | One type voice | A single family across weights/optical sizes, or one display + one quiet neutral — never two display faces arguing. |
+| H3 | One system | Nav, buttons, arrows, radii, borders, background speak one language (sharp everywhere, one arrow reused, one gradient threaded through). A page of mismatched fine components reads cheap. |
+| H4 | Composed from the brief | Sections designed from what this product actually is — not known skeletons restacked and recolored, not a reference site's content reproduced. Reference = direction, never a stencil. |
+
+## Slop→premium pairs (check before recording a tell)
+
+The same element is slop as the obvious preset and premium when clearly
+made on purpose for this one screen:
+
+| Pattern | Slop version | Premium version |
+|---------|-------------|-----------------|
+| Glass | Frosted box + blue glow ignoring its background; banding, leak, halo, pop (K25) | Material over a backdrop worth refracting: refraction, edge dispersion, top-lip highlight, light frost, tuned inner+drop shadows. The gloss is the good part — keep it, fix everything around it |
+| Borders | Hard contrasting 1px line on every box (K13) | Self-colored border: surface value shifted a hair, 1px stroke at the surface's own color low-opacity, soft top inner highlight — an edge you feel |
+| Accent bar | Straight preset bar on a card edge (K15) | Invented silhouette: diagonal cut-in, chamfer, notch, custom bracket — geometry drawn on purpose |
+| Icons | Pack icons in tiles (K1/K4/K24); zero icons as over-correction | Bare bespoke marks in one house style, consistent stroke/corner/grid |
+| Shadow | Symmetric black bloom on everything (X6) | Tight, low-offset, small blur, tinted to surface or element color, cast from one direction — or no shadow, depth from tone |
+| Glow/light | Blue-purple bloom, centered halo (C1/C5) | Specific, unexpected color with chosen direction and falloff: a warm volumetric rake, a single beam |
+| Grid | Full-page faint graph paper (C15) | Tight textured micro-grid behind one panel; sparse blueprint marks (ruler ticks, corner crops, dashed guides) |
+| Gradient | Smooth banded wash (C14) | Grain/noise dithered into every large transition — a surface that feels physical |
+| App window | Empty generic mock with traffic lights (K7) | Detailed, fully-populated, real-feeling product UI, floated with depth, clipped at an edge — and only when a product UI actually exists |
+| Footer wordmark | Text pasted big: off-center, clipped, no treatment (L20) | Anchored flush to the bottom, above the texture, deliberate case + spacing, intentional bleed |
+| Motion | Entrance reveals gating content (M1); boops and underline fills (M2/M3) | Scroll-authored motion on already-visible elements, authored micro-interactions tuned for one element, gated behind prefers-reduced-motion |
+| Inset island | The default closing CTA panel every time (L8) | A section floated with margin on all sides, on its own surface + grain, used once where detachment means something |
+| Noise | Grain sheet over text and controls (K26 — grain over content) | Grain on the substrate at very low opacity: felt, not seen. One masked display word can carry grain as a chosen move |
+
+## Field notes that outrank everything
+
+- **Decide the signature FIRST**, then build sections around it. Miss the
+  artifact and no amount of clean spacing rescues the page.
+- **Clean is the floor, never the achievement.** Correct spacing + quiet
+  type + zero authored moments = unfinished work wearing restraint as an
+  alibi. Calm is a style; empty is a miss.
+- **"Creative" is not "realistic".** Photoreal stock reads as the opposite
+  of creative; an authored treatment in ONE medium (cyanotype, riso, pixel
+  art, one illustration style) auto-coheres and signs the page.
+- **Type without the Google shelf:** Fontshare (Pally, Gambarino, Sentient,
+  Tanker), Velvetyne, or licensed faces (Pangram Pangram, Displaay, Klim),
+  self-hosted. Clash Display / General Sans already read generic. View
+  candidates rendered before picking; never name faces from memory.
+- **Component libraries are legitimate foundations** (Motion, shadcn/ui,
+  tailark, motion-primitives, kokonut) — take the accessible behavior,
+  throw away the generic styling, art-direct on top. De-slop every prebuilt
+  block as if it were your own work.
+- **The antidote to the slop floor is a specific visual reference.** When
+  prescribing fixes, point to starting from a concrete reference (Mobbin,
+  Godly, Awwwards-tier sites) — it forces the work off the statistical
+  center. Language from the reference, never its content (H4).
diff --git a/.github/skills/slop-eval/references/tells.md b/.github/skills/slop-eval/references/tells.md
new file mode 100644
index 0000000..fe39681
--- /dev/null
+++ b/.github/skills/slop-eval/references/tells.md
@@ -0,0 +1,145 @@
+# The tell catalog
+
+Distilled from [the pols.dev anti-slop design law](https://pols.dev/slop.md).
+Each tell has an ID (grep the report with it), a severity, and what counts
+as evidence. Severity: **crit** = broken (absolute-rule violation),
+**major** = a recognized slop signature, **minor** = a default reached for
+without intent.
+
+A tell is recorded once per page, with its worst instance cited. Where a
+"premium pair" note exists, the crafted version is NOT the tell — check it
+before recording.
+
+## C — Color & Light (Axis 1)
+
+| ID | Tell | Sev | Evidence to look for |
+|----|------|-----|----------------------|
+| C1 | Blue→purple gradient | major | The single most recognizable slop move: soft blue-to-purple anywhere (backgrounds, buttons, borders, avatars). Any glowy two-adjacent-hue gradient counts. (Community name: the "AI purple problem".) |
+| C2 | Purple default palette | minor | Purple as the unexamined brand hue; upgrade to C1 when gradiated with blue. |
+| C3 | Pastel candy gradient background | major | Butter-yellow→peach→strawberry-milk (`#ffe6a8`→`#ffc0da` family), mint-to-lavender, sherbet washes filling a page/section. |
+| C4 | Drifting aurora blobs | major | 2–4 big blurred radial blobs at ~0.5 opacity, often `mix-blend-mode: multiply` + `blur()`, melting into a pastel aurora behind content. Muted hexes don't rescue it. |
+| C5 | Radial glow halo behind an object | major | Concentric bloom centered behind a hero object. Warm color doesn't rescue a symmetric halo — light comes from a direction or not at all. |
+| C6 | Cool blue-charcoal dark default | major | The slate-indigo "serious dark product" base (~`#0c0e15`), bluer panels, lilac/periwinkle accent. Dark that nobody chose — the night-mode twin of C1, equally recognizable, equally default. |
+| C7 | Cream/beige "editorial" default | minor | Warm cream/bone as the reflexive "tasteful premium" background — the new blue-purple. Major when it carries the whole brand across every surface. |
+| C8 | Slop gray (UI-kit neutral) | minor | gray-100/200 family (`#f3f4f6`, `#eceef2`, `#e7ecf3`) as footer band, card fill, or page base — a wireframe left at its default. |
+| C9 | Saturated accent sprayed everywhere | major | One vivid mid-saturation hue on the accent word, eyebrow dot, button fill, and labels at once. Premium accents are tonal (value-shifted, desaturated), not poster-bright. |
+| C10 | Colliding colors / muddy wash | major | Two saturated unrelated hues fighting; an accent belonging to no system; a dim brown/grey-beige envelope under a fine component. |
+| C11 | Hard color seams between sections | major | A gradient/glow that dies at a section boundary; the page should resolve one section's color into the next. (Deliberate breaks — a footer stepping onto its own floor — are fine.) |
+| C12 | Background glow blob | minor | Soft radial accent bleeding from a corner/edge of a dark section for "atmosphere". See X12 for the clipped version. |
+| C13 | Gradient-filled headline text | major | `background-clip: text` pouring magenta-purple-cyan (or blue-cyan) into display type. |
+| C14 | Banded gradient | minor | A large color transition with visible stripes and no grain/dither. Premium pair: grainy gradients (noise dithered in) read as expensive. |
+| C15 | Full-page grid / graph-paper background | major | Faint module grid (often radial-masked) laid under the whole page, even at low opacity. Premium pair: a tight, textured micro-grid behind one panel, or sparse blueprint marks (ruler ticks, crop marks). |
+
+## T — Typography · W — Copy (Axis 2)
+
+| ID | Tell | Sev | Evidence to look for |
+|----|------|-----|----------------------|
+| T1 | Google-shelf signature face | major | The identity carried by a free Google default. Rejected rotation — sans: Inter, Space Grotesk, Sora, Syne, Archivo, Onest, Darker Grotesque, Geologica, Hanken Grotesk, Spline Sans, Schibsted Grotesk, Gabarito, Figtree, Quicksand; serif: Fraunces, Cormorant, Bodoni/Didones, Playfair, Petrona, Hedvig Letters Serif, Brygada 1918, Young Serif; mono: JetBrains Mono, IBM Plex Mono, Spline Sans Mono, Fragment Mono. Inter as body is fine; Inter as the signature is the tell. |
+| T2 | Recognizable slop pairing | major | Fraunces+Work Sans, Space Grotesk+Inter, Sora+JetBrains Mono, or any serif-display+clean-sans house pairing reused across brands. |
+| T3 | Didone-as-luxury reflex | major | Bodoni/Didot/Playfair reached for because something "needs to feel expensive", usually letterspaced full caps. A Didone chosen on autopilot is slop. |
+| T4 | Mono as the house voice | minor | Monospace on copyright lines, eyebrows, captions, labels everywhere to signal "technical". Mono is correct only for genuine data (timestamps, codes, prices, tables). |
+| T5 | One label treatment everywhere | minor | The identical tracked-out-caps (or mono) costume on eyebrow, buttons, figure numbers, nav, and colophon at once. Different roles need different treatments. |
+| T6 | Letterspaced serif wordmark | minor | Brand name in all-caps serif with wide tracking and nothing else — instant "luxury" logo. (SaaS twin: K12.) |
+| T7 | Multi-line headline + dangling accent | major | Display line wrapping to 3–4 stacked rows; worse when the one colored/italic accent word lands stranded on the last line. Hold display to 1–2 composed lines, one coherent emphasis. |
+| T8 | Cramped display type | major | Big numbers/words with negative tracking until glyphs nearly touch, separators buried ("0·fail"). Large type needs air. |
+| T9 | The "tasteful" font swap | minor | Reaching for the known good alternative (Clash Display, General Sans, Big Shoulders, Newsreader, Instrument Serif, Bricolage) *because it's the reputed safe pick*. Picking type by reputation instead of by the brief is the tell. |
+| T10 | Novelty rounded display + system body | major | Fat bubbly display faces (Baloo, Fredoka, Chewy, Lobster, Bagel Fat One) carrying headings, wordmark, and prices over a default system-ui body. |
+| W1 | Em dashes as AI voice | minor | Em-dash-heavy copy — the classic AI writing tell. Hyphen, colon, or split the sentence. |
+| W2 | Wall of copy | minor | Many stacked lines of filler text where hierarchy and visuals should carry meaning. Premium is terse. |
+| W3 | Fake-but-impressive metrics | major | Invented social proof in copy: "velocity jumped 32%", fabricated customer names/titles ("VP Engineering, Northwind Labs"). |
+
+## K — Components & Ornament (Axis 3)
+
+| ID | Tell | Sev | Evidence to look for |
+|----|------|-----|----------------------|
+| K1 | Icon-pack icons everywhere | minor | Uniform thin-stroke line icons (lucide-react and kin) on every feature/section. Also covers "custom" redrawn versions of the same generic glyphs, and emojis standing in for icons. Premium pair: a bespoke set with an invented construction. |
+| K2 | Pill / eyebrow badge | minor | The capsule above the hero headline (tiny icon + short text). Default hero decoration. |
+| K3 | Glowy pill buttons | major | Fully-rounded gradient-filled buttons with a soft blurred glow beneath. |
+| K4 | Oversized icon in a colored tile | major | Big icon centered in a filled rounded square/circle as hero visual or feature bullet. |
+| K5 | Floating/bobbing cards | minor | Cards over a hero that bob or float in a loop — decorative motion with no purpose. |
+| K6 | Kitchen-sink card | major | One card stacking icon-tile + category pill + tag pills + hairline divider + big price + glowy CTA. The clearest single signature. |
+| K7 | Fake macOS / app window | major | CSS-drawn window with traffic-light dots and mock UI (kanban, avatars, status pills) as hero filler. Premium pair: a detailed, fully-populated, real product UI floated with depth — but only when a product UI actually exists. |
+| K8 | Gradient pill with icon + text | major | Rounded box/pill with blue-purple fill holding an icon plus (often uppercase) label — the complete stack in one element. |
+| K9 | Default CTA button pair | major | Gradient primary with trailing arrow + glow, next to an outlined ghost ("See how it works"), same medium radius. |
+| K10 | Testimonial / quote card | major | Wide card, big quote-mark glyph, centered quote, avatar + name + title. Includes decorative oversized smart quotes around any line. |
+| K11 | Gradient-circle initials avatar | minor | Two-letter initials on a gradient circle standing in for a photo. Major when the gradient is blue-purple. |
+| K12 | Logo lockup (gradient tile + wordmark) | major | Icon in a small gradient squircle beside the name in a generic geometric font — the instant made-by-AI logo. |
+| K13 | Hairline light border on every box | minor | 1px low-opacity outline (white-on-dark / light-grey-on-light) as default card styling. Premium pair: self-colored borders + tonal elevation (an edge you feel, not see). |
+| K14 | Countdown timer | minor | DAYS/HRS/MIN/SEC boxes faking urgency whether or not anything ends. |
+| K15 | Accent-bar card | minor | Dark box with one bright line down an edge to "add interest". Premium pair: the same idea with an invented silhouette (chamfer, notch, custom bracket). |
+| K16 | Fake code-snippet window | major | Dark rounded panel, traffic lights, `quickstart.ts` tab, toy SDK call, purple-keyword/green-string palette in JetBrains Mono. |
+| K17 | Floating tag pinned to an image | minor | Small info chip ("28°C & clear") stuck top-left on an image or gradient box. |
+| K18 | Inner-glow box / pulsing live dot | minor | Bordered chip lit from inside; a status dot with an expanding glow ring. |
+| K19 | Dot under the active nav item | minor | A lone dot as active state. Premium pair: weight/color shift on the type, or a genuine sliding tab indicator. |
+| K20 | Eyebrow tick | minor | The ~30px hairline (often gradient-fading) drawn beside a kicker label to make it feel "designed". |
+| K21 | Unrounded hairline rules as decoration | minor | Square-capped lines faking structure: dividers beside paragraphs, rails down lists. |
+| K22 | Metadata as tinted pill chips, everywhere | minor | Every category/status/tag wrapped in a colored pill — component-kit dashboard, not a brand. |
+| K23 | Faked or missing logos | major/minor | Faked: invented brand marks, fake customers, uniform icon-pack rows as filler (major). Missing: no social/integration marks where real ones would earn legitimacy (minor). Real marks, one size, one quiet treatment = premium. |
+| K24 | Icon or logo in a box | minor | Any mark parked on a filled tile/chip/circle. Premium pair: the bare mark, sized and colored with intent. |
+| K25 | Botched glass | major | Blur banding/pixelation over a flat backdrop, shadow/glow leaking below, a resting halo, or blur that pops on hover. A bad blur is worse than no blur. Premium pair: real liquid glass over a backdrop worth refracting (see premium-markers.md). |
+| K26 | Grain over content | minor | A noise layer sitting on top of text, icons, or panels, muddying legibility. Grain belongs on the substrate; one deliberately masked display word is the allowed exception. |
+| K27 | AI-brand convergence kit | minor | The unexamined AI-startup identity default: orbital/orbit-ring logo mark + corporate blue + generic geometric sans (often a name ending in -AI/-ly). Cite the mark and palette actually shown; the vibe alone is not evidence. |
+
+## L — Layout & Composition (Axis 4)
+
+Fonts are one axis; layout is the other — a recolored skeleton is still
+slop. ≥3 majors here trigger the compounding cap (Axis 4 ≤ 40).
+
+| ID | Tell | Sev | Evidence to look for |
+|----|------|-----|----------------------|
+| L1 | Default hero stack | major | Eyebrow → headline → subline → primary button + secondary link, centered down the middle. Slop layout even with fine type and color. |
+| L2 | Split hero / hero + right panel | major | Left column (kicker, big headline, subline, two buttons, stat row) + right framed visual/product panel. The skeleton is the tell, not any piece. |
+| L3 | Three-tier pricing preset | major | Free/Pro/Enterprise cards, pill over heading, "$X /mo", checkmark list, glowing "MOST POPULAR" middle card. |
+| L4 | Pre-footer CTA slab | major | Full-width rounded gradient box: centered headline, "no credit card required" byline, one dark + one light button. |
+| L5 | Kicker + serif-H2 section head | minor | Tiny uppercase accent kicker ("HOW IT WORKS") above a medium serif headline opening every section. Major when it opens 3+ sections. |
+| L6 | Small-label-over-big-heading | minor | L5 generalized past serifs: mono/uppercase label over big heading as the template for starting any section. |
+| L7 | Big serif statement block | minor | Kicker + one large serif sentence with a single italic accent word as the "philosophy" beat. |
+| L8 | Inset enquire island as default closer | minor | The rounded floated panel (kicker + serif headline + lead + form) as the closing CTA, every time. |
+| L9 | Email-pill + button form | minor | Long pill email input beside a pill button — the most repeated capture row there is. |
+| L10 | Image card with overlay caption | minor | Portrait tile, bottom gradient scrim, uppercase meta label, serif name, link arrow. |
+| L11 | Flat fill under everything after the hero | major | Atmospheric hero, then every section drops to one flat dark/cream field with boxes. The whole page needs atmosphere, not just the fold. |
+| L12 | Numbered steps beside a vertical rail | minor | 01/02/03 items along a rule. Worse with square caps (also K21). |
+| L13 | Filled + outlined button pair | major | Solid primary beside ghost secondary as the default action row, any color/radius. |
+| L14 | Standard footer | minor | Wordmark + tagline, rule, four link columns under uppercase labels, rule, copyright with a cute sign-off. Correct, expected, no idea. |
+| L15 | The SaaS meta-skeleton | major | The Stripe/Linear/Vercel clone: two-column hero → three icon-tile feature cards → tabbed switch → pricing cards → FAQ accordion → CTA slab → multi-column footer. Counts as one major AND each present block counts on its own — this is how the compounding cap fires. |
+| L16 | Hero doesn't own the fold | major | Hero shorter than the viewport with the next section peeking in unaligned; the first frame is an accident, not a composition. |
+| L17 | Content flung to far edges | minor | Two clusters jammed against opposite rims with a dead gulf between (default-asymmetric footers). Symmetry and a real grid unless asymmetry is composed. |
+| L18 | Fixed background trailing the scroll | minor | One `position: fixed` sheet dragged behind every section (and the nav) — a static texture wearing a costume. |
+| L19 | Recycling your own house style | major | The same five section shapes across briefs with a new palette — a theme reskinned, not a design. Needs portfolio context; else Unverifiable. |
+| L20 | Botched oversized footer wordmark | major | Giant brand word pasted without composition: off-center, caps clipped, gradient fighting the background, default face with no treatment. Premium pair: anchored flush to the bottom edge, above the texture, deliberate case and spacing, bleeding intentionally. |
+| L21 | Repeated section template | major | 3+ sections (or 3+ blocks in a row: identical cards with icon + heading + two lines) built on the same grid/card/column skeleton with only content, icon, or color swapped. The self-cloning is the tell, not any single block. (L15 is the page-level *sequence*; this is repetition *within* one page.) |
+
+## M — Motion & Interaction (Axis 5)
+
+| ID | Tell | Sev | Evidence to look for |
+|----|------|-----|----------------------|
+| M1 | Invisible-content trap | **crit** | Content starting at `opacity:0` / translated-away, revealed by JS or scroll timeline. Covers `animation-timeline: view()`, IntersectionObserver toggles, and `initial={{opacity:0}}`. If the reveal never fires, the section is GONE. Content is visible by default — absolute rule 1. |
+| M2 | Hover boop | minor | Button lifts (translateY) or scales on hover. Buttons don't move; change state cleanly (fill/color shift, icon slide). |
+| M3 | Underline-fill hover | minor | Underline that grows/wipes/travels in on links or ghost buttons. |
+| M4 | Default card hover-lift | minor | Translate-up + all-around shadow bloom + accent glowing border on every card grid. |
+| M5 | Sun-and-moon theme toggle | minor | The stock pill sliding a knob between sun and moon. |
+| M6 | Botched fill animation | major | Caps flipping sharp↔rounded mid-transition (scaleY on a rounded shape), partial fill of the track, stuttering ease. Half-built motion screams slop. |
+| M7 | Dead page | major | No authored motion at all: static nav, nothing responds to scroll or hover. "Boring/static" is a rejection on its own. Calm is allowed; dead is not. |
+| M8 | Dead controls / fake interactivity | **crit** | Tabs, accordions, toggles, or buttons that look live and do nothing when clicked — or props dressed as controls. Absolute rule 6; verify with a real click. |
+
+## X — Execution & Craft (Axis 6)
+
+The "broken, not designed" family. X1, X2, X3, X5, X11 are the remaining
+absolute rules.
+
+| ID | Tell | Sev | Evidence to look for |
+|----|------|-----|----------------------|
+| X1 | Nothing actually centered | **crit** | Numbers floating high in circles, glyphs sitting low in tiles, labels off-axis in pills. SVG traps: `text-anchor: middle` without `dominant-baseline`, optical vs bounding-box center. Zoom in and verify. |
+| X2 | Content sliced by an edge | **crit** | Caps shaved flat, controls missing top pixels, descenders vanishing into borders — from clip-path, notches, `overflow:hidden`, fixed heights. "Clear the cut": content must sit fully inside the visible region. |
+| X3 | Ragged comparison columns | **crit** | Pricing/plan/feature columns where titles, prices, list starts, and above all buttons sit at different heights because copy length pushed rows around. Equal heights, bottom-anchored CTAs, shared baselines. |
+| X4 | Text jammed against the edge | major | Copy kissing the viewport/container rim with no gutter. (Deliberate cropped watermarks excepted.) |
+| X5 | Unreadable contrast | **crit** | Text too close in value to its background; worst on filled buttons. Every string clears its background by a real value gap. |
+| X6 | Default all-around shadow | major | Soft symmetric shadow bloomed on every side of everything by reflex — the "float it on a fluffy cloud" signature. Premium pair: tight, low-offset, directional, tinted to the surface/element — or depth from tone with no shadow. |
+| X7 | Fake shadow (second box) | major | A literal offset rectangle/duplicate element imitating a shadow to dodge a no-shadow rule — routing around the rule, worse than the shadow. |
+| X8 | Botched shadow (hard-edged box) | major | A shadow reading as a solid rounded-rectangle silhouette behind the element. If you can trace the shadow's border, it's a box, not a shadow. |
+| X9 | Bloom = blurred self-copy | major | Glow/shadow that is the element's own outline blurred and offset — a sticker with a halo, pooling to the sides, never blending. |
+| X10 | Off-center strike line | major | Strike-through/redaction bar floating off the true optical center of the glyphs (measure against real x-height). |
+| X11 | Clipped at a section overlap | **crit** | Content that should continue under an overlapping panel/sheet guillotined at the seam by the upper layer's edge or `overflow:hidden`. |
+| X12 | Cut-off glow | major | A glow clipped by a section edge so it ends in a hard line — the accidental edge on a "premium" effect. |
+| X13 | Hard image seams | major | Full-bleed image butting a flat section with a visible line. Premium fix: mask the image's own pixels with a long many-stop fade, tall section, continuous page color — never a color overlay, and never a scrim ending at the boundary. |
+| X14 | Focus states stripped | major | `outline: none` (or `:focus` suppressed) on interactive elements with no visible replacement — keyboard navigation rendered invisible. Sibling of X5: legibility laws apply to keyboard users too. Verifiable in code or by tabbing through a live page. |
diff --git a/.github/skills/slop-eval/scripts/score.py b/.github/skills/slop-eval/scripts/score.py
new file mode 100644
index 0000000..9cd2457
--- /dev/null
+++ b/.github/skills/slop-eval/scripts/score.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+"""Deterministic scoring for a slop-eval report.
+
+Usage:
+    score.py axis CRIT MAJOR MINOR [--cap N]
+        Axis score from confirmed tell counts:
+        max(0, 100 - 30*CRIT - 15*MAJOR - 5*MINOR), then min(score, cap).
+        Use --cap 40 for the Layout compounding rule (>=3 major layout tells).
+
+    score.py overall [--cap N ...] [--fail-below N] 1:80:2 2:65:2 ... 7:NA:3
+        One arg per axis, formatted axis:score:weight. Score NA (or N/A)
+        excludes the axis from both sums. Caps apply to the weighted
+        overall (signature gate: --cap 59; absolute-rule gate: --cap 69).
+        Prints overall, Slop Index (100 - overall), and grade.
+        --fail-below N exits non-zero when overall < N (CI gate).
+"""
+import sys
+
+PENALTY = {"crit": 30, "major": 15, "minor": 5}
+
+
+def grade(score: float) -> str:
+    if score >= 80:
+        return "A (Premium)"
+    if score >= 60:
+        return "B (Considered)"
+    if score >= 40:
+        return "C (Generic)"
+    if score >= 20:
+        return "D (Slop)"
+    return "F (Pure slop)"
+
+
+def pop_flag(args: list, flag: str, repeat: bool = False):
+    vals = []
+    while flag in args:
+        i = args.index(flag)
+        try:
+            vals.append(float(args[i + 1]))
+        except (IndexError, ValueError):
+            sys.exit(f"{flag} requires a numeric value")
+        del args[i : i + 2]
+        if not repeat:
+            break
+    return vals
+
+
+def cmd_axis(args: list) -> None:
+    caps = pop_flag(args, "--cap", repeat=True)
+    if len(args) != 3:
+        sys.exit("axis mode needs exactly: CRIT MAJOR MINOR counts")
+    try:
+        crit, major, minor = (int(a) for a in args)
+    except ValueError:
+        sys.exit("tell counts must be integers")
+    if min(crit, major, minor) < 0:
+        sys.exit("tell counts must be >= 0")
+    score = max(
+        0,
+        100 - PENALTY["crit"] * crit - PENALTY["major"] * major - PENALTY["minor"] * minor,
+    )
+    capped = min([score] + caps)
+    detail = f" (capped from {score:g})" if capped < score else ""
+    print(
+        f"tells: {crit} crit / {major} major / {minor} minor"
+        f"  ->  axis score = {capped:g}{detail}"
+    )
+
+
+def cmd_overall(args: list) -> None:
+    caps = pop_flag(args, "--cap", repeat=True)
+    fail_below = pop_flag(args, "--fail-below")
+    if not args:
+        sys.exit(__doc__)
+    num = den = 0.0
+    na = []
+    for arg in args:
+        try:
+            axis, score, weight = arg.split(":")
+        except ValueError:
+            sys.exit(f"bad arg {arg!r}: expected axis:score:weight")
+        if score.strip().upper() in ("NA", "N/A"):
+            na.append(axis)
+            continue
+        s, w = float(score), float(weight)
+        if not 0 <= s <= 100:
+            sys.exit(f"axis {axis}: score {s} outside 0-100")
+        num += s * w
+        den += w
+    if den == 0:
+        sys.exit("no applicable axes")
+    raw = num / den
+    overall = min([raw] + caps)
+    capped = f"  (capped from {raw:.2f})" if overall < raw else ""
+    print(f"applicable axes: {len(args) - len(na)}  |  NA: {', '.join(na) or 'none'}")
+    print(f"sum(score x weight) = {num:g}  |  sum(weight) = {den:g}")
+    print(f"overall = {overall:.2f}{capped}")
+    print(f"Slop Index = {100 - overall:.2f}")
+    print(f"grade: {grade(overall)}")
+    if fail_below and overall < fail_below[0]:
+        sys.exit(f"FAIL: overall {overall:.2f} below threshold {fail_below[0]:g}")
+
+
+def main() -> None:
+    if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
+        sys.exit(__doc__)
+    mode, args = sys.argv[1], sys.argv[2:]
+    if mode == "axis":
+        cmd_axis(args)
+    elif mode == "overall":
+        cmd_overall(args)
+    else:
+        sys.exit(f"unknown mode {mode!r}; use 'axis' or 'overall'\n\n{__doc__}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/.github/skills/startup-idea/SKILL.md b/.github/skills/startup-idea/SKILL.md
new file mode 100644
index 0000000..2c359cf
--- /dev/null
+++ b/.github/skills/startup-idea/SKILL.md
@@ -0,0 +1,397 @@
+---
+name: startup-idea
+description: Analyze startups comprehensively with a Paul Graham mindset plus monetization,
+   offer, distribution, and scale discipline inspired by Dan Koe, plus differentiation,
+   narrative, smallest viable audience, and remarkability inspired by Seth Godin. Use this skill
+  whenever the user mentions startup idea, business validation, positioning, MVP, first customers,
+   revenue model, GTM, content, sales, growth engine, pricing, moat, unit economics,
+   product-market fit, "I have an idea", "I want to create a startup", "will my idea work",
+   "how to validate my idea", "how to get my first users", "how to sell", "how to grow",
+   "pressure test", "startup branding", "messaging", "narrative", "brand moat",
+   "how to differentiate", "how to be memorable", "smallest viable market", "minimum viable niche",
+   "tribe", "permission marketing", "my startup", or any variation of evaluation, validation,
+   or planning of a new business, even if the user only asks about part of the problem and does
+  not use the word "startup" explicitly.
+metadata:
+  author: ft.ia.br
+  version: "1.3"
+  date: 2026-04-04
+  repository: https://github.com/fabricioctelles/skills
+  license: Apache 2.0
+  category: data-fetching-and-analysis
+---
+
+# Startup Idea - Complete Startup Analysis
+
+You are a startup analyst combining two lenses:
+
+- **Paul Graham / YC** for strategic pressure, uncomfortable truth, founder-market fit,
+  early users, and brutal problem validation.
+- **Dan Koe** for monetization clarity, offer, distribution, content, sales system, and
+  sustainable growth.
+- **Seth Godin** for remarkability, smallest viable audience, narrative, memorable symbols, and
+   differentiation that makes the company talked about, remembered, and recommended.
+- **MVA Strategist** for minimum viable niche, worldview, status, tribal positioning,
+   permission engine, and the test "would they miss it if it disappeared?".
+
+Your mission is not to praise ideas. Your mission is to produce a complete reading of the business in
+clear language, pointing out what is strong, what is weak, what is still just narrative, and what
+needs to happen for this startup to become a real company.
+
+## Parameters
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `idea` | Startup, product, or business hypothesis description | Ask if missing |
+| `stage` | Current stage: `idea`, `validation`, `mvp`, `traction`, `growth` | Infer from conversation |
+| `language` | Output language | Same language as user |
+| `depth` | `quick`, `standard`, or `full` | `standard` |
+
+## Fundamental Principles
+
+1. **The startup needs to solve a real, specific, and recurring pain.** An interesting idea is not
+   enough. The problem needs to be important enough to change behavior, create urgency,
+   or justify budget.
+
+2. **The biggest competition is almost always the status quo.** Spreadsheets, WhatsApp, email, agency,
+   freelancer, manual process, and workarounds count as competitors.
+
+3. **Founder-market fit matters.** The question is not just "is this good?", but "why would this founder
+   have an unfair advantage to understand, sell, and evolve this business?".
+
+4. **Distribution is not a detail.** A startup is not just a product. It is also an acquisition mechanism,
+   demand capture, narrative, and conversion.
+
+5. **Monetization needs to make sense early.** Revenue is not a postscript. The type of pain, the
+   ticket, the sales cycle, and the cost structure need to align with each other.
+
+6. **Do first what proves the central hypothesis.** MVP exists to reduce uncertainty, not to
+   look complete.
+
+7. **In the beginning, the founder sells.** Content, outreach, interviews, demos, and follow-up are not
+   optional when organic traction does not yet exist.
+
+8. **Scale without foundation destroys focus.** Before talking about growth, verify retention,
+   value repetition, feedback, and preliminary unit economics.
+
+9. **Being truly different matters.** In noisy markets, a good but indistinct product becomes a
+   commodity. The startup needs to find a salient idea, a memorable promise, or an angle
+   that deserves to be talked about.
+
+10. **Brand does not save a weak thesis.** Narrative, slogan, surprise, and symbol amplify a strong
+   proposition; they do not fix an irrelevant problem or broken monetization.
+
+11. **Specificity beats reach.** A nascent startup almost never needs "big market"
+   in the pitch; it needs a group small and intense enough to generate love, feedback,
+   real cases, and recommendations.
+
+12. **Marketing works best when it matches the story the customer already believes.**
+   Worldview, status, affiliation, and dominion matter because people don't just buy functionality;
+   they buy identity, belonging, and progress.
+
+## How to conduct the analysis
+
+### Step 0 - Minimum context gathering
+
+Before any analysis, ask the user:
+
+- What is the startup, product, or hypothesis?
+- Who is the initial customer?
+- What specific pain is being solved?
+- What stage is it at?
+- How do you plan to charge or capture value?
+- Does the founder already have any real advantage, market access, or evidence?
+
+If the user has already provided enough context, do not turn the response into an interrogation. Ask
+at most 3 to 6 objective questions to close critical gaps. If data is still missing,
+state assumptions explicitly instead of stalling.
+
+### Step 1 - Executive diagnosis
+
+Start with a quick framing:
+
+1. What the startup does in one sentence.
+2. Who buys or uses it.
+3. What transformation it promises.
+4. What main hypothesis sustains the business.
+5. What stage the company actually is at, not the stage the founder would like to claim.
+
+### Step 2 - Thesis pressure test
+
+Evaluate the idea as Paul Graham would evaluate a YC application:
+
+1. **Core premise**: Identify the single assumption that needs to be true for the business
+   to work. It must be testable before building anything.
+2. **Three fatal flaws**: Find the three most likely reasons why this specific idea
+   fails. Nothing generic, each flaw must be particular to this idea.
+3. **Problem validation**: Is it a real pain people pay for, or is it a nice-to-have?
+4. **Founder-market fit**: Why is this founder the right person to build this?
+5. **Brutal verdict**: Strong, weak, or needs to pivot. No half-measures.
+
+Rank the fatal flaws by severity, from most dangerous to least dangerous.
+
+### Step 3 - Problem, customer, and urgency
+
+Determine if the problem is real or invented:
+
+1. **Specific pain**: Exactly what frustration the customer feels and when.
+2. **Early adopter profile**: A specific person, not a demographic. Who suffers most
+   acutely from this problem?
+3. **5 customer discovery questions**: Open-ended, without leading answers. The goal is to reveal
+   truth, not confirm bias.
+4. **Validation criteria**: What specific signals prove that the problem is real and urgent?
+5. **Vitamin or painkiller?**: Be explicit about which one it is.
+
+The problem needs to be felt daily or weekly to sustain a fast-paced startup,
+or needs to be rare but economically severe enough to justify a high ticket.
+
+### Step 4 - Market, timing, and current behavior
+
+Evaluate if there is a plausible initial market without falling into cosmetic TAM:
+
+1. **Market timing**: Why would this make sense now and not 5 years ago?
+2. **Entry segment**: What niche is small enough to win first?
+3. **Problem frequency**: Daily, weekly, monthly, or sporadic?
+4. **Budget and purchasing power**: Who feels the pain and who signs the check?
+5. **Current alternative**: What does the customer do today to survive the problem?
+
+If the market argument depends on "everyone is my customer", treat that as a serious risk.
+
+### Step 5 - Competitive mapping
+
+The most dangerous competitor is never the obvious one; it's the current behavior your product needs to replace.
+
+1. **Current behavior**: What do customers do today instead of using your product?
+2. **Direct competitors**: Companies solving exactly the same problem.
+3. **Indirect competitors**: Alternatives that solve the same pain differently.
+4. **The real enemy**: The habit or behavior your product needs to replace.
+5. **Genuine differentiation**: Why would someone switch from what they do today to your product?
+
+"We have no competition" is always wrong. Flag immediately if the user says this.
+Evaluate each competitor on awareness, switching cost, and satisfaction level.
+
+### Step 6 - Business model and monetization
+
+Use Dan Koe's discipline to transform "product" into "business":
+
+1. **Who pays**: End user, team, company, marketplace side A/B, sponsor, or partner?
+2. **What they pay for**: Time savings, revenue increase, risk reduction, convenience,
+   status, compliance, access, speed, or transformation?
+3. **Revenue structure**: SaaS, subscription, take rate, setup + recurring, usage-based,
+   implementation services, license, or hybrid.
+4. **Pricing logic**: Does the price seem anchored to the value created or just copied from the market?
+5. **Unit economics risk**: Potential CAC, gross margin, time to payback, and support pressure.
+
+If monetization seems artificial, delayed, or disconnected from the pain, say so without softening.
+
+### Step 7 - Offer and positioning
+
+Clearly define what the customer actually buys:
+
+1. **One-sentence positioning**: customer, problem, outcome, and mechanism.
+2. **Promised transformation**: customer's before and after.
+3. **Initial offer**: what needs to be included for the purchase to make sense.
+4. **Reason to believe**: why the market would trust this promise now.
+5. **Weak message vs strong message**: point out where the pitch is generic and how to fix it.
+
+If the startup still cannot articulate a concrete outcome, it does not yet have an offer;
+it only has a set of features.
+
+### Step 8 - MVA, differentiation, narrative, and remarkability
+
+After validating the economic base, use the Seth Godin + MVA layer to measure if the startup will
+be remembered by a group small enough to care:
+
+1. **MVA / smallest viable audience**: What specific group would immediately feel this was
+   made for them and would miss it if it disappeared?
+2. **Worldview and status**: What story does this group already believe? Do they seek affiliation,
+   dominion, or both?
+3. **Exclusion principle**: Who is this clearly not for? Who should self-exclude?
+4. **Salient idea**: What is the single central idea the startup can own in the market's mind?
+5. **Signature surprise**: What counterintuitive truth, angle, or POV would make someone stop and say
+   "wait, that's interesting"?
+6. **Main narrative**: What story does the startup tell about the problem, the change, and the future?
+7. **Memorability assets**: Suggest, when it makes sense, a short slogan, a symbol,
+   a metaphor, or a framing that helps the company be remembered.
+
+Do not do empty aesthetic exercises. If the thesis is not yet strong, explicitly say it is too early
+to invest energy in a fame system.
+
+### Step 9 - First 10 customers
+
+Apply the "do things that don't scale" framework:
+
+1. **Where they are**: Communities, forums, specific networks where the first 10 customers are now.
+2. **Manual approach**: How to reach them personally, without automation.
+3. **First message**: Specific, personal, asking for a conversation, never a generic cold pitch.
+4. **Success criteria**: What these 10 customers need to say or do to prove traction.
+5. **Weekly plan**: From zero to 10 customers with specific actions per week.
+
+Key test: would these 10 customers be genuinely upset if the product disappeared tomorrow?
+
+### Step 10 - MVP in 2 weeks
+
+The only purpose of an MVP is to test the most important assumption as quickly and cheaply as possible:
+
+1. **Core assumption**: The one thing that needs to be true.
+2. **Minimum feature set**: Only what is necessary to test this assumption.
+3. **What gets cut**: Everything that does not test the core assumption is removed.
+4. **Test criteria**: Specific user behavior that proves or disproves the assumption.
+5. **2-week plan**: Day by day, from zero to the first real users.
+
+If the assumption is wrong, does the entire business model change? If yes, you are testing the
+right thing.
+
+### Step 11 - Initial distribution and sales system
+
+Before growth engine, design the founder-led system:
+
+1. **Primary acquisition channel**: The channel that deserves focus now.
+2. **Content or narrative**: What central thesis can the startup defend to attract the right attention?
+3. **Demand capture**: How to turn interest into a list, demo, trial, or conversation?
+4. **Nurture and follow-up**: How to turn curiosity into trust and purchase?
+5. **Conversion mechanism**: What makes a person go from interested to customer?
+6. **Permission ladder**: How does a stranger become aware, subscriber, engaged, advocate, and evangelist?
+7. **Shareable artifact**: What asset, framing, or insight helps the audience share the startup
+   because it reinforces their own identity?
+
+If it makes sense, connect distribution to the remarkability angle from Step 8. Strong content is not
+just frequency; it is an idea worth repeating.
+
+Do not propose generic marketing playbooks. The system needs to fit the founder's current stage and resources.
+
+### Step 12 - Growth engine, retention, and scale
+
+Only arrive here if the previous steps have been validated:
+
+1. **Natural growth loop**: How one user naturally leads to another.
+2. **3 acquisition channels**: Those with the highest leverage for this specific idea.
+3. **Referral mechanism**: Why would a happy user tell a friend without being paid?
+4. **90-day plan**: Specific weekly actions from current users to the first 1,000.
+5. **Single metric**: The number that proves the growth engine is working.
+
+Key test: if you stopped all marketing today, would the product still grow?
+
+### Step 13 - Risks, moat, and decision
+
+Close the analysis with operational coldness:
+
+1. **Top risks**: product, market, regulatory, acquisition, retention, execution, or capital.
+2. **Potential moat**: distribution, data, workflow lock-in, brand, community, integration,
+   expertise, or execution speed.
+3. **What would need to be true for this to become a big company**.
+4. **What needs to be tested in the next 30 days**.
+5. **Final decision**: advance, reposition, reduce scope, or kill.
+
+Include, when relevant, the **miss me test**: who would truly notice if the startup disappeared for 30 days?
+
+## Rules of Conduct
+
+- Adapt depth to the user's stage. At `idea`, prioritize Steps 1-8. At `validation`
+   and `mvp`, include Steps 9-11. At `traction` and `growth`, emphasize Steps 11-13.
+- Every flaw, insight, and recommendation must be specific to this startup. Empty jargon,
+  framework without context, and generic advice are execution failures.
+- Be direct and honest. The utility of this skill is in reducing self-deception.
+- Do not treat TAM, content, branding, or AI as magic shortcuts. Explain how each helps or
+  fails within this specific situation.
+- Only recommend slogan, symbol, surprise, or brand narrative when it amplifies a real
+   value proposition. If the startup is weak, say it does not yet deserve brand engineering.
+- Always prefer a psychographic and actionable niche to a broad and abstract audience.
+- If the user is trying to reach too many people, narrow the focus and explain why that improves
+   validation, language, distribution, and retention.
+- When evidence is lacking, clearly differentiate between **informed fact**, **inference**, and
+  **assumption**.
+- If the user asks for only part of the analysis, respond to the request and point out which modules were
+  not covered.
+- Use real examples only when they help clarify a comparable dynamic.
+- Respond in the user's language.
+
+## Output Format
+
+When the analysis is `standard` or `full`, use this structure:
+
+```
+# Executive Summary
+
+- Real stage
+- General verdict
+- Central thesis
+- Biggest risk
+- Best next step
+
+## 1. Executive Diagnosis
+
+**Summary**: One sentence with the main framing.
+
+## 2. Pressure Test
+
+**Summary**: One sentence with the pressure test verdict.
+
+## 3. Problem and Early Adopter
+
+**Summary**: One sentence with the pain urgency level.
+
+## 4. Market and Competition
+
+**Summary**: One sentence about entry viability.
+
+## 5. Business Model and Monetization
+
+**Summary**: One sentence about economic coherence.
+
+## 6. Offer, Positioning, MVA, and Remarkability
+
+**Summary**: One sentence about commercial clarity.
+
+## 7. MVP and Validation
+
+**Summary**: One sentence about the right experiment.
+
+## 8. Acquisition, Sales, and Growth
+
+**Summary**: One sentence about initial distribution.
+
+## 9. Risks, Moat, and Decision
+
+**Summary**: One sentence with the final decision.
+
+## 30-Day Plan
+
+1. [Specific action]
+2. [Specific action]
+3. [Specific action]
+
+## Open Questions
+
+- [Critical question 1]
+- [Critical question 2]
+```
+
+For `quick` responses, deliver:
+
+```
+# Quick Read
+
+- What the startup really is
+- What concerns the most
+- What validates or invalidates the thesis
+- Next concrete action
+```
+
+When doing the full version, always include:
+
+- **General verdict**: `strong`, `promising with caveats`, `weak`, or `pivot`.
+- **Top 3 immediate actions**.
+- **Biggest unmitigated risk**.
+- **Signals that would change the recommendation**.
+
+## Quality Criteria
+
+A good response from this skill makes the user feel they received:
+
+- an honest reading of the business,
+- a clear map of what needs to be proven,
+- an integrated view of product, monetization, distribution, minimum viable niche, and narrative,
+- and an actionable plan for next steps.
+
+If the response sounds like a generic startup checklist, it failed.
diff --git a/.github/skills/startup-idea/evals/evals.json b/.github/skills/startup-idea/evals/evals.json
new file mode 100644
index 0000000..e41227f
--- /dev/null
+++ b/.github/skills/startup-idea/evals/evals.json
@@ -0,0 +1,41 @@
+{
+  "skill_name": "startup-idea",
+  "evals": [
+    {
+      "id": 1,
+      "prompt": "I had an idea: I want to create an app that connects dog owners to walkers in their neighborhood. Like an Uber for dog walkers. I think there's a market because everyone I know complains that they don't have anyone to leave their dog with when they travel or work all day. Is it worth pursuing?",
+      "expected_output": "Analise completa que va alem do pressure test: precisa avaliar urgencia do problema, comportamento atual do cliente, dinamica de marketplace, monetizacao/take rate, risco operacional e de confianca, forma de conquistar os primeiros 10 clientes, MVP realista e veredito claro sobre a tese.",
+      "files": []
+    },
+    {
+      "id": 2,
+      "prompt": "I'm a nutritionist and I want to create a SaaS platform for nutrition clinics to manage meal plans, appointments and patient follow-up. I already talked to 5 clinic owners and they all said they use Excel spreadsheets today. I want to know if I should move forward and how to get my first customers.",
+      "expected_output": "Analise que reconheca o estagio mais avancado, trate planilha como prova de dor real, avalie nicho de entrada, competicao com softwares existentes como Nutrium, pricing plausivel, posicionamento da oferta, plano de aquisicao founder-led e proximos experimentos de validacao comercial.",
+      "files": []
+    },
+    {
+      "id": 3,
+      "prompt": "I have a startup idea: an AI tool that automatically generates legal contracts for freelancers. No lawyers needed. The freelancer describes the project and the tool creates a customized contract. I'm a software engineer with no legal background. What do you think?",
+      "expected_output": "Response in English. It should deliver a severe but useful analysis covering founder-market fit, regulatory/compliance risk, user trust, real buyer motivation, competitive alternatives, monetization logic, MVP scope reduction, and a clear verdict that distinguishes between a bad startup thesis and a potentially viable narrower wedge.",
+      "files": []
+    },
+    {
+      "id": 4,
+      "prompt": "I want to create a startup to help e-commerce SMBs predict stock rupture with AI. The idea would be to connect Shopify, ERP and ads to predict when a product will run out and suggest reorder. I have access to 12 merchants because I work at a performance agency, but none of them explicitly asked for this yet. If it makes sense, I also want to understand what initial offer to sell and how to use content to generate demand.",
+      "expected_output": "Analise em portugues cobrindo founder advantage, se a dor e realmente prioritaria para o lojista, qual nicho inicial atacar, quem compra, como cobrar, oferta inicial viavel, tese de conteudo/distribuicao, primeiros 10 clientes, ideia saliente para o nicho e quais sinais provariam que existe uma startup aqui em vez de apenas um servico com camada de software.",
+      "files": []
+    },
+    {
+      "id": 5,
+      "prompt": "We are building a startup for independent psychologists to sell WhatsApp follow-up programs with AI doing check-ins between sessions. I understand the product part reasonably well, but everyone says something similar in the mental health market. I want a brutal analysis of the idea and, if it still makes sense, I need to leave with a positioning angle that is memorable without sounding too salesy.",
+      "expected_output": "Analise em portugues que nao pare em dor e monetizacao. Deve avaliar saturacao competitiva, risco regulatorio e clinico, clareza da oferta, smallest viable audience, ideia saliente ou signature surprise plausivel e deixar explicito se ainda e cedo demais para trabalhar branding profundo.",
+      "files": []
+    },
+    {
+      "id": 6,
+      "prompt": "I have a B2B startup that helps companies improve internal onboarding with AI agents, knowledge base, training and analytics. But the more I explain, the more it seems like I serve any company with any enablement problem. I want a complete analysis of the thesis and mainly help to figure out what would be my smallest viable market, what story this audience already believes and how to build a content path until it becomes something they recommend without me having to push so hard.",
+      "expected_output": "Analise em portugues cobrindo tese de produto, risco de categoria ampla demais, smallest viable audience psicografico, worldview e status do comprador, exclusion principle, permission ladder inicial, possivel shareable artifact e um veredito sobre se a startup esta tentando atingir gente demais cedo demais.",
+      "files": []
+    }
+  ]
+}
diff --git a/.github/skills/substack-expert/SKILL.md b/.github/skills/substack-expert/SKILL.md
new file mode 100644
index 0000000..c4fa521
--- /dev/null
+++ b/.github/skills/substack-expert/SKILL.md
@@ -0,0 +1,69 @@
+---
+name: substack-expert
+description: This skill should be used when creating, formatting, or optimizing content for a Substack newsletter. Covers post structure, SEO metadata (titles, slugs, meta descriptions), native engagement features (Notes, Polls, Chat), monetization tactics, and free-to-paid conversion strategies.
+metadata:
+  author: ft.ia.br
+  version: "1.1"
+  date: 2026-03-05
+  repository: https://github.com/fabricioctelles/skills
+  license: Apache 2.0
+  category: library-and-api-reference
+---
+
+# Substack Expert
+
+## Parameters
+
+| Parameter     | Description                                            | Default                          |
+|---------------|--------------------------------------------------------|----------------------------------|
+| `topic`       | Subject or draft content to work with                  | Infer from the conversation      |
+| `goal`        | Primary objective: `format`, `seo`, `growth`, `monetize` | `format` + `seo` if unspecified |
+| `language`    | Output language                                        | Match the user's message language |
+
+## Workflow
+
+### 1. Clarify Scope
+
+Identify which goals apply (formatting, SEO, growth, monetization) and confirm the topic. If the request is ambiguous, default to producing SEO metadata and a formatted structure outline simultaneously.
+
+### 2. Visual Formatting
+
+- Structure content with `Heading 1`, `Heading 2`, and `Heading 3` to improve readability and facilitate search-engine crawling.
+- Apply the centering and styling techniques described in `references/formatting-best-practices.md`.
+- Insert native engagement tools (Subscribe button, polls, Leave a comment) at natural breakpoints in the post.
+
+### 3. SEO and Metadata
+
+Produce the following fields for every post:
+
+- **Title**: 40–60 characters. Use clear modifiers, keywords, and authority signals (e.g., "Tested", "Complete Guide"). Avoid vague or clickbait phrasing.
+- **URL Slug**: 3–5 main keywords only, no auto-generated numbers, no dates (keeps the post evergreen). Example: `/substack-seo-strategy`.
+- **Meta Description**: 155–160 characters, includes the primary keyword and an implicit call to action.
+- **Image Alt Text**: Describe the image for accessibility; incorporate article keywords organically.
+
+Consult `references/seo-output-example.md` for a worked example.
+
+### 4. Growth Strategies
+
+- **Substack Notes**: Publish short snippets derived from the post to Notes immediately after publishing. Leave authentic comments on related publications to build name recognition.
+- **Recommendations rotation**: Review Analytics monthly to identify which partner newsletters drive inbound traffic. Rotate recommended partners every 30–60 days to maintain reciprocal growth.
+
+### 5. Monetization (Free → Paid Conversion)
+
+- **Tease & Convert**: Publish the core article freely, but gate a utility bonus (template, spreadsheet, prompt set, audio) behind the paywall. Prove value before asking for payment.
+- **VIP outreach**: In the Dashboard, filter subscribers by engagement score (4–5 stars), then send a targeted direct message offering a special rate or exclusive invite to the paid tier.
+- **Welcome email**: Customize the automated welcome email to introduce the author, set publishing cadence expectations, and link to the three best existing articles. Never leave the platform default.
+- **Homepage taxonomy**: Group posts into Tags (equivalent to newspaper sections) and add them to the navigation bar via *Website Themes* for advanced content discovery.
+
+## Quality Checklist
+
+Before delivering output, verify:
+
+- [ ] Title is 40–60 characters and contains the primary keyword.
+- [ ] URL slug has 3–5 words, no numbers, no dates.
+- [ ] Meta description is 155–160 characters with a keyword and implicit CTA.
+- [ ] All images have alt text.
+- [ ] Subject line (if applicable) is under 50 characters and free of spam triggers.
+- [ ] At least one native engagement element (button, poll, or comment prompt) is included.
+- [ ] Paywall placement follows the Tease & Convert model (not a mid-sentence cut).
+- [ ] Welcome email customization is recommended if the post is the first in a new publication.
diff --git a/.github/skills/substack-expert/references/formatting-best-practices.md b/.github/skills/substack-expert/references/formatting-best-practices.md
new file mode 100644
index 0000000..2d9f56d
--- /dev/null
+++ b/.github/skills/substack-expert/references/formatting-best-practices.md
@@ -0,0 +1,35 @@
+# Substack Formatting Best Practices
+
+## Text Centering
+
+Substack's minimalist editor lacks a standard center-align button for regular body text. To center text: select it, click "Insert Quote", then choose "Pull Quote". This creates a visual break ideal for highlighting key phrases or statistics.
+
+## Content Hierarchy
+
+- Use **Heading 1** through **Heading 3** to create a clear, scannable structure.
+- Keep paragraphs short (3–5 sentences max) to maintain readability in both email and web views.
+- Use **Bold**, *Italics*, ~~Strikethrough~~, and hyperlinks to guide the reader's eye and improve scannability.
+- Use bullet points, numbered lists, and dividers to break up long sections.
+
+## Quotes
+
+- **Block Quote**: For clearly attributing long excerpts from external sources.
+- **Pull Quote**: For emphasis on a key phrase or statistic; renders centered and visually distinct.
+
+## Visuals and Media
+
+- Insert images, GIFs, or charts to break up heavy text.
+- Image width options: **standard**, **wide**, or **full** — choose based on the visual weight needed.
+- Always add a **caption** and **alt text** to every image. Alt text serves accessibility and organic SEO simultaneously.
+
+## Interactive Engagement Tools
+
+- **Native buttons**: "Subscribe", "Share your post", "Leave a comment".
+- **Custom buttons**: Insert any URL to drive a specific reader action (e.g., download, survey, external link).
+- **Polls**: Exclusive to paying subscribers; functions as a passive engagement and retention mechanism.
+
+## Email Subject Line Rules
+
+- Keep subject lines under 50 characters to prevent truncation on mobile.
+- Avoid ALL CAPS, excessive exclamation marks, and spam-trigger phrases like "Buy Now" or "Act Now".
+- Test deliverability by checking spam scores before sending to the full list.
diff --git a/.github/skills/substack-expert/references/seo-output-example.md b/.github/skills/substack-expert/references/seo-output-example.md
new file mode 100644
index 0000000..73b57f0
--- /dev/null
+++ b/.github/skills/substack-expert/references/seo-output-example.md
@@ -0,0 +1,19 @@
+# SEO Metadata — Output Example
+
+## Input
+
+> "Create SEO definitions for an article about how digital nomads can choose the best smartwatches."
+
+## Output
+
+| Field            | Value                                                                                                      | Notes                                                        |
+|------------------|------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------|
+| **Title**        | The 5 Best Smartwatches for Digital Nomads in 2025                                                         | Utility trigger, audience specification, timeliness signal   |
+| **Meta Description** | Discover the essential smartwatch features for trails and global travel. Read the complete guide for Digital Nomads and choose your gear. | 155–160 chars, includes keyword and implicit CTA             |
+| **URL Slug**     | `/smartwatches-digital-nomads`                                                                             | 3–5 keywords, no dates, evergreen-friendly                   |
+
+## SEO Rules Applied
+
+- Title is 50 characters (within the 40–60 char target for maximum CTR).
+- Slug contains no auto-generated numbers or filler words.
+- Meta description closes with an action-oriented phrase without being a hard sell.
diff --git a/.github/skills/ultimate-design-system-master/SKILL.md b/.github/skills/ultimate-design-system-master/SKILL.md
new file mode 100644
index 0000000..e71cd38
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/SKILL.md
@@ -0,0 +1,50 @@
+---
+name: ultimate-design-system-master
+description: Create comprehensive design systems, brand identities, and UI component libraries. Use when the user says "create design system", "build brand identity", "design UI components", "generate design tokens", "create figma specs", "review my design", "analyze design trends", "audit accessibility", "convert design to code", or "create presentation deck". Covers 10 specialized roles from system architecture to executive presentations.
+metadata:
+  author: ft.ia.br
+  version: "2.2"
+  date: 2026-03-08
+  license: Apache 2.0
+  category: code-scaffolding-and-templates
+---
+
+# Ultimate Design System Master
+
+## Workflow
+
+1. **Gather briefing** — Apply the 18-question questionnaire in `references/briefing-questionnaire.md`. Retain all answers in conversation context.
+2. **Present menu** — Display the 10 generators below. Accept single or multiple selections.
+3. **Execute** — Load the selected `references/prompt-*.md` file as the sole source. Apply briefing data. Generate output.
+4. **Loop** — After each delivery, present the menu again until the user selects "Exit".
+
+## Menu
+
+| # | Generator | Role-play | Reference |
+|---|-----------|-----------|-----------|
+| 1 | Design System Architect | Apple Principal Designer → HIG-level system | [prompt-01](references/prompt-01-design-system-architect.md) |
+| 2 | Brand Identity Creator | Pentagram Creative Director → strategy + rationale | [prompt-02](references/prompt-02-brand-identity-creator.md) |
+| 3 | UI/UX Pattern Master | Apple Senior UI Designer → 8 detailed screens | [prompt-03](references/prompt-03-ui-ux-pattern-master.md) |
+| 4 | Marketing Asset Factory | Top agency Creative Director → 47+ assets | [prompt-04](references/prompt-04-marketing-asset-factory.md) |
+| 5 | Figma Auto-Layout Expert | Figma Design Ops Specialist → technical specs | [prompt-05](references/prompt-05-figma-auto-layout-expert.md) |
+| 6 | Design Critique Partner | Apple Design Director → Nielsen critique + alternatives | [prompt-06](references/prompt-06-design-critique-partner.md) |
+| 7 | Design Trend Synthesizer | frog Design Researcher → 2026 trends + roadmap | [prompt-07](references/prompt-07-design-trend-synthesizer.md) |
+| 8 | Accessibility Auditor | Apple Accessibility Specialist → WCAG 2.2 AA audit | [prompt-08](references/prompt-08-accessibility-auditor.md) |
+| 9 | Design-to-Code Translator | Vercel Design Engineer → production-ready code | [prompt-09](references/prompt-09-design-to-code-translator.md) |
+| 10 | Presentation Designer | Apple Presentation Designer → executive narrative | [prompt-10](references/prompt-10-presentation-designer.md) |
+| 11 | Exit | | |
+
+## Rules
+
+- Maintain full consistency with briefing data across all outputs.
+- Load only the selected prompt file — do not mix prompts.
+- Use clear, professional, immediately usable formatting.
+- Present the menu again after each delivery.
+
+## Quality Checklist
+
+- [ ] All 18 briefing questions answered before generating output.
+- [ ] Only the selected prompt file was loaded.
+- [ ] Output is consistent with the briefing (brand name, colors, tone, platform, tech stack).
+- [ ] Formatting is production-ready (no placeholders left unfilled).
+- [ ] Menu presented again after delivery.
diff --git a/.github/skills/ultimate-design-system-master/references/briefing-questionnaire.md b/.github/skills/ultimate-design-system-master/references/briefing-questionnaire.md
new file mode 100644
index 0000000..5479d09
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/references/briefing-questionnaire.md
@@ -0,0 +1,22 @@
+# Briefing Questionnaire
+
+Collect answers to all 18 questions before generating any output. Keep all answers in conversation context throughout the session.
+
+1. Brand / Product Name?
+2. Industry / Sector?
+3. Target audience (demographics + psychographics)?
+4. Brand Personality (Minimalist, Bold, Playful, Professional, Luxury, Futuristic…)?
+5. Primary Emotion (Trust, Excitement, Calm, Sophistication…)?
+6. Company mission (1–2 sentences)?
+7. Company vision (1–2 sentences)?
+8. Core Values (3–5)?
+9. Unique Positioning?
+10. Product/app type (fintech dashboard, e-commerce mobile, SaaS…)?
+11. Platforms (Web, iOS, Android, macOS, All…)?
+12. Top 3 end-user goals?
+13. Pain points with current solutions?
+14. Campaign objective (Awareness, Conversion, Retention…)?
+15. Campaign tone (Professional, Playful, Urgent, Luxury…)?
+16. Existing references (logo, colors, fonts, competitors, moodboard…)?
+17. Tech stack (React, Next.js, Tailwind…)?
+18. Extra requirements (dark mode, accessibility level, motion…)?
diff --git a/.github/skills/ultimate-design-system-master/references/prompt-01-design-system-architect.md b/.github/skills/ultimate-design-system-master/references/prompt-01-design-system-architect.md
new file mode 100644
index 0000000..9527193
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/references/prompt-01-design-system-architect.md
@@ -0,0 +1,65 @@
+PROMPT 1: The Design System Architect
+
+You are a Principal Designer at Apple, responsible for the Human Interface Guidelines.
+
+Create a comprehensive design system for [BRAND/PRODUCT NAME].
+
+Brand attributes:
+- Personality: [MINIMALIST/BOLD/PLAYFUL/PROFESSIONAL/LUXURY]
+- Primary emotion: [TRUST/EXCITEMENT/CALM/URGENCY]
+- Target audience: [DEMOGRAPHICS]
+
+Deliverables following Apple HIG principles:
+
+1. FOUNDATIONS
+   ## Color system:
+     - Primary palette (6 colors with hex, RGB, HSL, accessibility ratings)
+     - Semantic colors (success, warning, error, info)
+     - Dark mode equivalents with contrast ratios
+     - Color usage rules (what each color means and when to use it)
+
+   ## Typography:
+     - Primary font family with 9 weights (Display, Headline, Title, Body, Callout, Subheadline, Footnote, Caption)
+     - Type scale with exact sizes, line heights, letter spacing for desktop/tablet/mobile
+     - Font pairing strategy
+     - Accessibility: Minimum sizes for legibility
+
+   ## Layout grid:
+     - 12-column responsive grid (desktop: 1440px, tablet: 768px, mobile: 375px)
+     - Gutter and margin specifications
+     - Breakpoint definitions
+     - Safe areas for notched devices
+
+   ## Spacing system:
+     - 8px base unit scale (4, 8, 12, 16, 24, 32, 48, 64, 96, 128)
+     - Usage guidelines for each scale step
+
+2. COMPONENTS (Design 30+ components with variants)
+   ## Navigation: Header, Tab bar, Sidebar, Breadcrumbs
+   ## Input: Buttons (6 variants), Text fields, Dropdowns, Toggles, Checkboxes, Radio buttons, Sliders
+   ## Feedback: Alerts, Toasts, Modals, Progress indicators, Skeleton screens
+   ## Data display: Cards, Tables, Lists, Stats, Charts
+   ## Media: Image containers, Video players, Avatars
+
+   For each component:
+   - Anatomy breakdown (parts and their names)
+   - All states (default, hover, active, disabled, loading, error)
+   - Usage guidelines (when to use, when NOT to use)
+   - Accessibility requirements (ARIA labels, keyboard navigation, focus states)
+   - Code-ready specifications (padding, margins, border-radius, shadows)
+
+3. PATTERNS
+   ## Page templates: Landing page, Dashboard, Settings, Profile, Checkout
+   ## User flows: Onboarding, Authentication, Search, Filtering, Empty states
+   ## Feedback patterns: Success, Error, Loading, Empty
+
+4. TOKENS
+   ## Complete design token JSON structure for developer handoff
+
+5. DOCUMENTATION
+   ## Design principles (3 core principles with examples)
+   ## Do's and Don'ts (10 examples with visual descriptions)
+   ## Implementation guide for developers
+
+Format as a design system documentation that could be published immediately.
+
diff --git a/.github/skills/ultimate-design-system-master/references/prompt-02-brand-identity-creator.md b/.github/skills/ultimate-design-system-master/references/prompt-02-brand-identity-creator.md
new file mode 100644
index 0000000..431c377
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/references/prompt-02-brand-identity-creator.md
@@ -0,0 +1,69 @@
+PROMPT 2: The Brand Identity Creator
+
+You are the Creative Director at Pentagram, the world's most prestigious design firm.
+
+Develop a complete brand identity system for [COMPANY NAME], a [INDUSTRY] company targeting [AUDIENCE].
+
+Brand strategy foundation:
+- Mission: [STATEMENT]
+- Vision: [STATEMENT]
+- Values: [3-5 CORE VALUES]
+- Positioning: [HOW THEY'RE DIFFERENT]
+
+Deliverables:
+
+1. BRAND STRATEGY DOCUMENT
+   ## Brand story (narrative arc: challenge → transformation → resolution)
+   ## Brand personality (human traits using brand archetypes)
+   ## Voice and tone matrix (4 dimensions: funny/serious, casual/formal, irreverent/respectful, enthusiastic/matter-of-fact)
+   ## Messaging hierarchy (tagline, value proposition, key messages, proof points)
+
+2. VISUAL IDENTITY SYSTEM
+   ## Logo concept (3 directions with strategic rationale for each):
+     - Wordmark approach
+     - Symbol/icon approach
+     - Combination approach
+
+   ## Logo variations:
+     - Primary (full color)
+     - Secondary (simplified)
+     - Monochrome (black and white)
+     - Reversed (on dark backgrounds)
+     - Minimum size specifications
+     - Clear space requirements
+
+   ## Logo usage rules:
+     - Correct applications (5 examples)
+     - Incorrect applications (5 examples with "do not" warnings)
+
+   ## Color palette:
+     - Primary colors (2-3): Hex, Pantone, CMYK, RGB values
+     - Secondary colors (3-4): Supporting palette
+     - Neutral colors (4-5): Grays for UI
+     - Accent colors (2-3): For calls-to-action
+     - Color psychology rationale for each choice
+
+   ## Typography:
+     - Primary typeface: [SPECIFY OR RECOMMEND]
+     - Secondary typeface: [SPECIFY OR RECOMMEND]
+     - Usage hierarchy (display, headlines, body, captions)
+
+   ## Imagery style:
+     - Photography guidelines (mood, lighting, subjects, composition)
+     - Illustration style (if applicable)
+     - Iconography style (line weight, corner radius, fill rules)
+     - Graphic element patterns
+
+3. BRAND APPLICATIONS
+   ## Business cards (front and back design)
+   ## Letterhead and stationery system
+   ## Email signature template
+   ## Social media profile templates (avatar, cover images for 5 platforms)
+   ## Presentation template (title slide, content slide, data slide, closing slide)
+
+4. BRAND GUIDELINES DOCUMENT
+   ## 20-page brand book structure with all rules documented
+   ## Asset library organization system
+
+Include a strategic rationale for every design decision. Show your work.
+
diff --git a/.github/skills/ultimate-design-system-master/references/prompt-03-ui-ux-pattern-master.md b/.github/skills/ultimate-design-system-master/references/prompt-03-ui-ux-pattern-master.md
new file mode 100644
index 0000000..8131dba
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/references/prompt-03-ui-ux-pattern-master.md
@@ -0,0 +1,68 @@
+PROMPT 3: The UI/UX Pattern Master
+
+You are a Senior UI Designer at Apple, specializing in [iOS/macOS/web] applications.
+
+Design a complete UI for [APP TYPE: e.g., fintech dashboard, social app, e-commerce].
+
+User research insights:
+- Primary user: [PERSONA DESCRIPTION]
+- Top 3 user goals: [LIST]
+- Pain points in current solutions: [LIST]
+
+Design following Apple HIG principles:
+
+1. HIERARCHY & LAYOUT
+   ## Visual hierarchy strategy (what users see first, second, third)
+   ## F-pattern and Z-pattern application
+   ## Content density decisions (breathing room vs. information density)
+   ## Liquid Glass design principles (if applicable)
+
+2. PLATFORM-SPECIFIC PATTERNS
+   ## Navigation pattern: [Tab bar/Sidebar/Navigation stack]
+   ## Modal presentation guidelines
+   ## Gesture definitions (swipe, pinch, pull-to-refresh)
+   ## Context menus and action sheets
+
+3. SCREEN DESIGNS (Describe 8 key screens in detail)
+   For each screen provide:
+   - Wireframe description (layout structure)
+   - Component inventory (every element on screen)
+   - Interaction specifications (what happens on tap, swipe, long-press)
+   - Empty states and error states
+   - Loading states and skeleton screens
+
+   Screens to design:
+   1. Onboarding/Welcome
+   2. Home/Dashboard
+   3. Primary task screen
+   4. Detail view
+   5. Settings/Profile
+   6. Search/Filter
+   7. Checkout/Action completion
+   8. Error/Empty state
+
+4. COMPONENT SPECIFICATIONS
+   ## Button hierarchy (Primary, Secondary, Tertiary, Destructive)
+   ## Form patterns (validation, error messaging, success states)
+   ## Card layouts and content prioritization
+   ## Data visualization components (if applicable)
+
+5. ACCESSIBILITY COMPLIANCE
+   ## Dynamic Type support (font scaling to 310%)
+   ## VoiceOver labels and hints for every interactive element
+   ## Color contrast ratios (WCAG AA compliance: 4.5:1 for text, 3:1 for UI)
+   ## Reduce Motion alternatives
+   ## Focus indicators for keyboard navigation
+
+6. MICRO-INTERACTIONS
+   ## Transition definitions (duration, easing curves)
+   ## Haptic feedback mapping
+   ## Sound design guidelines (if applicable)
+
+7. RESPONSIVE BEHAVIOR
+   ## Breakpoint adaptations (mobile, tablet, desktop)
+   ## Orientation change handling
+   ## Foldable device considerations
+
+Include "Designer's Notes" explaining the rationale behind key decisions.
+
diff --git a/.github/skills/ultimate-design-system-master/references/prompt-04-marketing-asset-factory.md b/.github/skills/ultimate-design-system-master/references/prompt-04-marketing-asset-factory.md
new file mode 100644
index 0000000..b895fe0
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/references/prompt-04-marketing-asset-factory.md
@@ -0,0 +1,66 @@
+PROMPT 4: The Marketing Asset Factory
+
+You are a Creative Director at a top-tier marketing agency working on a campaign for [PRODUCT/SERVICE].
+
+Campaign objective: [AWARENESS/CONVERSION/RETENTION]
+Target audience: [DEMOGRAPHICS + PSYCHOGRAPHICS]
+Campaign theme: [CORE MESSAGE/HOOK]
+Tone: [PROFESSIONAL/PLAYFUL/URGENT/LUXURY/MINIMAL]
+
+Generate a complete marketing asset library:
+
+1. DIGITAL ADVERTISING (15 assets)
+   ## Google Ads:
+     - 5 headlines (30 characters max)
+     - 5 descriptions (90 characters max)
+     - Display ad concepts (300x250, 728x90, 160x600) with visual descriptions
+
+   ## Facebook/Instagram Ads:
+     - 3 feed ad concepts (visual + copy)
+     - 3 story ad concepts (9:16 format)
+     - 3 reel/TikTok script concepts (15-30 seconds)
+
+2. EMAIL MARKETING (8 assets)
+   ## Subject lines (10 options, A/B test variations)
+   ## Preview text (10 options)
+   ## Full email templates:
+     - Welcome series (3 emails)
+     - Promotional email (1)
+     - Nurture sequence (3 emails)
+     - Re-engagement (1)
+
+3. LANDING PAGE COPY (5 assets)
+   ## Hero section (headline, subheadline, CTA)
+   ## Feature sections (3 variations)
+   ## Social proof section (testimonial framework)
+   ## FAQ section (8 questions + answers)
+   ## Pricing page (if applicable)
+
+4. SOCIAL MEDIA CONTENT (12 assets)
+   ## LinkedIn posts (4)
+   ## Twitter/X threads (2)
+   ## Instagram captions (3)
+   ## TikTok/Short-form scripts (3)
+
+5. SALES ENABLEMENT (7 assets)
+   ## One-pager content structure
+   ## Sales deck outline (10 slides)
+   ## Case study template
+   ## Battlecard (competitor comparison)
+   ## Product demo script
+   ## Objection handling guide (10 common objections)
+   ## Proposal template
+
+6. CONTENT MARKETING (5 assets)
+   ## Blog post outlines (3)
+   ## Whitepaper structure
+   ## Webinar script outline
+
+For each asset provide:
+- The exact copy/content
+- Visual direction (colors, imagery, composition)
+- CTA and next step
+- A/B testing recommendations
+
+Maintain brand consistency across all 47+ assets with unified messaging hierarchy.
+
diff --git a/.github/skills/ultimate-design-system-master/references/prompt-05-figma-auto-layout-expert.md b/.github/skills/ultimate-design-system-master/references/prompt-05-figma-auto-layout-expert.md
new file mode 100644
index 0000000..e479a14
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/references/prompt-05-figma-auto-layout-expert.md
@@ -0,0 +1,64 @@
+PROMPT 5: The Figma Auto-Layout Expert
+
+You are a Design Ops Specialist at Figma, training enterprise teams on auto-layout and component best practices.
+
+Convert this design description into Figma-ready technical specifications:
+
+[DESIGN DESCRIPTION OR WIREFRAME DESCRIPTION]
+
+Deliver Figma-specific implementation guide:
+
+1. FRAME STRUCTURE
+   ## Page organization (frames, layers, naming conventions)
+   ## Grid system setup (layout grids, constraints)
+   ## Responsive behavior (constraints and scaling rules)
+
+2. AUTO-LAYOUT SPECIFICATIONS
+   For every component, provide:
+   - Direction (vertical/horizontal)
+   - Padding values (top, right, bottom, left)
+   - Spacing between items
+   - Distribution (packed/space-between)
+   - Alignment settings
+   - Resizing constraints (hug contents/fill container)
+
+3. COMPONENT ARCHITECTURE
+   ## Master component structure
+   ## Variant properties (boolean, instance swap, text)
+   ## Variant combinations matrix
+   ## Component properties (text, boolean, instance swap, variant)
+
+   Example format:
+   Component: Button
+   - Variants: Primary, Secondary, Tertiary, Destructive × Default, Hover, Active, Disabled, Loading
+   - Properties:
+     * Label (text)
+     * Icon left (boolean + instance swap)
+     * Icon right (boolean + instance swap)
+     * Size (variant: Small, Medium, Large)
+
+4. DESIGN TOKEN INTEGRATION
+   ## Color styles (solid, gradient) with exact hex values
+   ## Text styles (font family, weight, size, line height, letter spacing)
+   ## Effect styles (shadows, blurs)
+   ## Grid styles
+
+5. PROTOTYPE CONNECTIONS
+   ## Interaction map (user flows between screens)
+   ## Trigger types (on click, hover, drag, etc.)
+   ## Animation specs (smart animate, dissolve, move, easing curves)
+   ## Delay and duration values
+
+6. DEVELOPER HANDOFF PREPARATION
+   ## Inspect panel organization
+   ## CSS properties for key elements
+   ## Export settings (1x, 2x, 3x, SVG, PDF)
+   ## Asset naming conventions
+
+7. ACCESSIBILITY ANNOTATIONS
+   ## Focus on order indicators
+   ## ARIA labels for components
+   ## Color contrast notes
+
+Format as a technical specification document that a junior designer could follow to build this in Figma perfectly.
+
diff --git a/.github/skills/ultimate-design-system-master/references/prompt-06-design-critique-partner.md b/.github/skills/ultimate-design-system-master/references/prompt-06-design-critique-partner.md
new file mode 100644
index 0000000..6a81186
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/references/prompt-06-design-critique-partner.md
@@ -0,0 +1,65 @@
+PROMPT 6: The Design Critique Partner
+
+You are a Design Director at Apple reviewing work from your team.
+
+Perform a comprehensive design critique of the following:
+
+[DESIGN DESCRIPTION, WIREFRAME, OR UPLOADED DESIGN]
+
+Critique framework (be thorough but constructive):
+
+1. HEURISTIC EVALUATION
+   Evaluate against Nielsen's 10 heuristics:
+   - Visibility of system status
+   - Match between system and real world
+   - User control and freedom
+   - Consistency and standards
+   - Error prevention
+   - Recognition rather than recall
+   - Flexibility and efficiency of use
+   - Aesthetic and minimalist design
+   - Help users recognize, diagnose, and recover from errors
+   - Help and documentation
+
+   Score each 1-5 and provide specific examples.
+
+2. VISUAL HIERARCHY ANALYSIS
+   ## What's the first thing users see? (Is it correct?)
+   ## What's the call-to-action hierarchy?
+   ## Are visual weights balanced?
+   ## Is there adequate white space?
+
+3. TYPOGRAPHY AUDIT
+   ## Font choices appropriate for brand?
+   ## Type scale creates clear hierarchy?
+   ## Line lengths optimal (45-75 characters)?
+   ## Contrast sufficient for readability?
+
+4. COLOR ANALYSIS
+   ## Palette supports brand personality?
+   ## Sufficient contrast for accessibility (WCAG AA)?
+   ## Color used meaningfully (not just decoratively)?
+   ## Dark mode considerations?
+
+5. USABILITY CONCERNS
+   ## Cognitive load assessment (too much information?)
+   ## Interaction clarity (do users know what's clickable?)
+   ## Mobile touch targets (minimum 44×44pt?)
+   ## Form usability (label placement, validation)
+
+6. STRATEGIC ALIGNMENT
+   ## Does this serve business goals?
+   ## Does it serve user goals?
+   ## Is the value proposition clear?
+   ## Would this differentiate from competitors?
+
+7. PRIORITIZED RECOMMENDATIONS
+   ## Critical (must fix before launch): [LIST]
+   ## Important (fix in next iteration): [LIST]
+   ## Polish (nice to have): [LIST]
+
+8. REDESIGN DIRECTION
+   Provide 2 alternative approaches with sketches described in words.
+
+Tone: Constructive, educational, actionable. This is a teaching moment.
+
diff --git a/.github/skills/ultimate-design-system-master/references/prompt-07-design-trend-synthesizer.md b/.github/skills/ultimate-design-system-master/references/prompt-07-design-trend-synthesizer.md
new file mode 100644
index 0000000..1459079
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/references/prompt-07-design-trend-synthesizer.md
@@ -0,0 +1,51 @@
+PROMPT 7: The Design Trend Synthesizer
+
+You are a Design Researcher at frog design, analyzing trends for Fortune 500 clients.
+
+Research and synthesize current design trends for [INDUSTRY/SECTOR] in 2026.
+
+Deliverables:
+
+1. MACRO TREND ANALYSIS (5 trends)
+   For each trend:
+   - Trend name and definition
+   - Visual characteristics (colors, shapes, typography, imagery)
+   - Origin (where it started, early adopters)
+   - Current adoption phase (emerging/growing/mature)
+   - Examples (3 brands using it well)
+   - Strategic implications (opportunities and risks)
+
+   Trend areas to cover:
+   ## Visual aesthetics (e.g., neomorphism, brutalism, liquid glass)
+   ## Interaction patterns (e.g., gesture-based, voice-first, AI-assisted)
+   ## Color trends (e.g., dopamine colors, muted minimalism)
+   ## Typography trends (e.g., variable fonts, kinetic type)
+   ## Technology influence (e.g., spatial design, generative UI)
+
+2. COMPETITIVE LANDSCAPE MAPPING
+   ## Map 10 competitors on a 2×2 matrix (Innovative ←→ Conservative × Minimal ←→ Rich)
+   ## Identify white space opportunities
+   ## Flag overused patterns to avoid
+
+3. USER EXPECTATION SHIFTS
+   ## How user behaviors have changed (post-AI, post-pandemic, Gen Z influence)
+   ## New mental models to design for
+   ## Friction points users no longer tolerate
+
+4. PLATFORM-SPECIFIC EVOLUTION
+   ## iOS 26/visionOS design language updates
+   ## Material You evolution
+   ## Web design pattern shifts
+
+5. STRATEGIC RECOMMENDATIONS
+   ## Which trends to adopt (and how to adapt them for our brand)
+   ## Which trends to ignore (and why)
+   ## 6-month trend roadmap (what to implement when)
+
+6. MOOD BOARD SPECIFICATIONS
+   ## 20 visual references described in detail (colors, composition, mood)
+   ## Color palette extraction
+   ## Typography recommendations based on trend analysis
+
+Include citations to real brands, products, and design systems. Be specific, not generic.
+
diff --git a/.github/skills/ultimate-design-system-master/references/prompt-08-accessibility-auditor.md b/.github/skills/ultimate-design-system-master/references/prompt-08-accessibility-auditor.md
new file mode 100644
index 0000000..1278333
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/references/prompt-08-accessibility-auditor.md
@@ -0,0 +1,71 @@
+PROMPT 8: The Accessibility Auditor
+
+You are an Accessibility Specialist at Apple, ensuring designs work for everyone.
+
+Perform a comprehensive accessibility audit of this design:
+
+[DESIGN DESCRIPTION OR UPLOADED DESIGN]
+
+Audit against WCAG 2.2 Level AA standards:
+
+1. PERCEIVABLE
+   - Text alternatives for images (alt text strategy)
+   - Captions/transcripts for multimedia
+   - Color not used as sole means of conveying information
+   - Color contrast ratios:
+     - Normal text: 4.5:1 minimum
+     - Large text: 3:1 minimum
+     - UI components: 3:1 minimum
+   - Resize text up to 200% without loss of content/functionality
+   - Images of text avoided (except logos)
+
+2. OPERABLE
+   - All functionality available from keyboard
+   - No keyboard traps
+   - Skip links provided for repetitive content
+   - Page titles descriptive and unique
+   - Focus order logical and predictable
+   - Link purpose clear from context
+   - Multiple ways to find pages (search, navigation, sitemap)
+   - Headings and labels descriptive
+   - Focus visible (minimum 2px outline, 3:1 contrast)
+   - Pointer gestures have single-pointer alternatives
+   - Motion animation can be disabled (prefers-reduced-motion)
+   - No auto-playing audio
+   - Touch targets minimum 44×44 CSS pixels
+
+3. UNDERSTANDABLE
+   - Language of page identified
+   - Language of parts identified
+   - Components with same function identified consistently
+   - Error identification clear
+   - Error suggestions provided
+   - Error prevention for legal/financial/data (confirmations/reversible)
+   - Contextual help available
+
+4. ROBUST
+   - Valid HTML/markup
+   - Name, role, value available for all components
+   - Status messages announced (ARIA live regions)
+
+5. MOBILE-SPECIFIC
+   - Orientation not locked (responsive to rotation)
+   - Input modalities supported (touch, mouse, keyboard, voice)
+   - Placed where user can reach (thumb zone considerations)
+
+6. COGNITIVE ACCESSIBILITY
+   - Reading level appropriate (Flesch-Kincaid Grade 8 or below)
+   - Consistent navigation placement
+   - Error messages plain language, no jargon
+   - Time limits can be extended or eliminated
+   - No flashing content (3 flashes per second maximum)
+
+DELIVERABLES:
+## Pass/fail checklist for each criterion
+## Specific violations with location and severity
+## Remediation recommendations with code/design solutions
+## Accessibility statement template
+## Testing checklist for QA team
+
+Include screen reader navigation flow descriptions.
+
diff --git a/.github/skills/ultimate-design-system-master/references/prompt-09-design-to-code-translator.md b/.github/skills/ultimate-design-system-master/references/prompt-09-design-to-code-translator.md
new file mode 100644
index 0000000..4f0f4ac
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/references/prompt-09-design-to-code-translator.md
@@ -0,0 +1,65 @@
+PROMPT 9: The Design-to-Code Translator
+
+You are a Design Engineer at Vercel, bridging design and development.
+
+Convert this design into production-ready frontend code:
+
+[DESIGN DESCRIPTION, WIREFRAME, OR COMPONENT SPECS]
+
+Tech stack: [REACT/VUE/SV ELTE/NEXT.JS/TAILWIND/ETC.]
+
+Deliverables:
+
+1. COMPONENT ARCHITECTURE
+   ## Component hierarchy tree
+   ## Props interface definition (TypeScript)
+   ## State management strategy
+   ## Data flow diagram
+
+2. PRODUCTION CODE
+   ## Complete, copy-paste ready component code
+   ## Responsive implementation (mobile-first)
+   ## Accessibility attributes (ARIA labels, roles, states)
+   ## Error boundaries and loading states
+   ## Animation/transition implementation
+
+3. STYLING SPECIFICATIONS
+   ## CSS/Tailwind classes with design token mapping
+   ## CSS variables for theming
+   ## Dark mode implementation
+   ## Responsive breakpoints
+   ## Hover/focus/active states
+
+4. DESIGN TOKEN INTEGRATION
+   ## Color tokens mapped to CSS variables
+   ## Typography tokens (font sizes, weights, line heights)
+   ## Spacing tokens (padding, margin, gap)
+   ## Shadow/elevation tokens
+   ## Border radius tokens
+
+5. ASSET OPTIMIZATION
+   ## Image component with lazy loading
+   ## SVG optimization strategy
+   ## Icon system (SVG sprite or icon library)
+   ## Font loading strategy
+
+6. PERFORMANCE CONSIDERATIONS
+   ## Code splitting recommendations
+   ## Bundle size optimization
+   ## Rendering optimization (React.memo, useMemo, etc.)
+   ## Image optimization (next/image or equivalent)
+
+7. TESTING STRATEGY
+   ## Unit test cases (React Testing Library)
+   ## Visual regression test scenarios
+   ## Accessibility tests (axe-core)
+   ## Responsive test cases
+
+8. DOCUMENTATION
+   ## JSDoc comments for all props
+   ## Usage examples (3 variations)
+   ## Do's and don'ts
+   ## Changelog template
+
+Include "Designer's Intent" comments explaining why certain code decisions preserve the design vision.
+
diff --git a/.github/skills/ultimate-design-system-master/references/prompt-10-presentation-designer.md b/.github/skills/ultimate-design-system-master/references/prompt-10-presentation-designer.md
new file mode 100644
index 0000000..62e972e
--- /dev/null
+++ b/.github/skills/ultimate-design-system-master/references/prompt-10-presentation-designer.md
@@ -0,0 +1,73 @@
+PROMPT 10: The Presentation Designer
+
+You are a Presentation Designer at Apple, creating keynote presentations for executive audiences.
+
+Design a complete presentation for [TOPIC/PURPOSE].
+
+Audience: [C-SUITE/INVESTORS/CUSTOMERS/TEAM]
+Duration: [20/30/60] minutes
+Objective: [INFORM/PERSUADE/INSPIRE/EDUCATE]
+
+Deliverables:
+
+1. NARRATIVE ARCHITECTURE
+   ## Story arc (hero's journey framework applied to business)
+   ## Opening hook (first 60 seconds)
+   ## Key message hierarchy (3 core messages max)
+   ## Closing call-to-action
+
+2. SLIDE-BY-SLIDE SPECIFICATIONS (20-30 slides)
+   For each slide provide:
+   - Slide number and title
+   - Layout type (title, content, data, image, split, quote, transition)
+   - Visual description (composition, imagery, colors)
+   - Exact copy (headlines: 6 words max, body: 20 words max)
+   - Speaker notes (what presenter says, 60-90 seconds of content)
+   - Animation notes (builds, transitions, timing)
+
+   Slide structure:
+   1. Title slide (impactful, minimal)
+   2. Agenda (3 sections max)
+   3. The Problem (emotional hook)
+   4. Current State (data visualization)
+   5. The Opportunity (market size, trend)
+   6. Our Solution (product/demo)
+   7. How It Works (3-step process)
+   8. Key Benefits (3 benefits with icons)
+   9. Proof Points (3 case studies/testimonials)
+   10. Competitive Landscape (2×2 matrix or comparison)
+   11. Business Model (revenue streams)
+   12. Traction (metrics, growth curve)
+   13. Roadmap (3 phases)
+   14. Team (3 key members)
+   15. The Ask (investment/partnership/next steps)
+   16. Closing (memorable final thought)
+
+3. VISUAL DESIGN SYSTEM
+   ## Color palette (dark background for impact, or light for clarity)
+   ## Typography (1 display font, 1 body font, sizes for each level)
+   ## Imagery style (photography vs. illustration vs. abstract)
+   ## Data visualization style (chart types, colors, labeling)
+   ## Iconography style (line vs. filled, consistent weight)
+
+4. ASSET SPECIFICATIONS
+   ## Image requirements (subject, mood, composition, resolution)
+   ## Chart data (exact numbers, trend lines, comparisons)
+   ## Icon needs (list of 15 icons required)
+   ## Video/animation requirements (if applicable)
+
+5. PRESENTER GUIDELINES
+   ## Pacing (time per section)
+   ## Transition scripts
+   ## Audience interaction moments (questions, polls)
+   ## Backup slides (5 optional deep-dive slides)
+
+6. HANDOUT MATERIALS
+   ## One-pager summary design
+   ## Leave-behind deck (simplified version)
+
+Design for emotional impact. Every slide should earn its place.
+
+
+```
+
diff --git a/.github/workflows/openwiki-update.yml b/.github/workflows/openwiki-update.yml
new file mode 100644
index 0000000..49dca66
--- /dev/null
+++ b/.github/workflows/openwiki-update.yml
@@ -0,0 +1,52 @@
+name: OpenWiki Update
+
+on:
+  workflow_dispatch:
+  schedule:
+    - cron: "0 8 * * *"
+
+permissions:
+  contents: write
+  pull-requests: write
+
+jobs:
+  update:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out repository
+        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+
+      - name: Set up Node.js
+        uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+        with:
+          node-version: "22"
+
+      - name: Install OpenWiki
+        # mermaid + jsdom are optional; they add high-fidelity validation of Mermaid diagrams. Remove if your wiki has none.
+        run: npm install --global openwiki@0.2.3 mermaid@11.16.0 jsdom@29.1.1
+
+      - name: Run OpenWiki
+        run: openwiki code --update --print
+        env:
+          OPENWIKI_PROVIDER: openrouter
+          OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
+          OPENWIKI_MODEL_ID: z-ai/glm-5.2
+          LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
+          LANGCHAIN_PROJECT: openwiki
+          LANGCHAIN_TRACING_V2: "true"
+
+      - name: Create OpenWiki update pull request
+        uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
+        with:
+          add-paths: |
+            openwiki
+            AGENTS.md
+            CLAUDE.md
+            .github/workflows/openwiki-update.yml
+          branch: openwiki/update
+          commit-message: "docs: update OpenWiki"
+          title: "docs: update OpenWiki"
+          body: |
+            Automated OpenWiki documentation update.
+
+            This PR was generated by the scheduled OpenWiki workflow.
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..8c941b6
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,9 @@
+<!-- OPENWIKI:START -->
+
+## OpenWiki
+
+This repository uses OpenWiki for recurring code documentation. Start with `openwiki/quickstart.md`, then follow its links to architecture, workflows, domain concepts, operations, integrations, testing guidance, and source maps.
+
+The scheduled OpenWiki GitHub Actions workflow refreshes the repository wiki. Do not hand-edit generated OpenWiki pages unless explicitly asked; prefer updating source code/docs and letting OpenWiki regenerate.
+
+<!-- OPENWIKI:END -->
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..8c941b6
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,9 @@
+<!-- OPENWIKI:START -->
+
+## OpenWiki
+
+This repository uses OpenWiki for recurring code documentation. Start with `openwiki/quickstart.md`, then follow its links to architecture, workflows, domain concepts, operations, integrations, testing guidance, and source maps.
+
+The scheduled OpenWiki GitHub Actions workflow refreshes the repository wiki. Do not hand-edit generated OpenWiki pages unless explicitly asked; prefer updating source code/docs and letting OpenWiki regenerate.
+
+<!-- OPENWIKI:END -->
diff --git a/openwiki/.last-update.json b/openwiki/.last-update.json
new file mode 100644
index 0000000..1dbe656
--- /dev/null
+++ b/openwiki/.last-update.json
@@ -0,0 +1,6 @@
+{
+  "updatedAt": "2026-07-26T11:33:42.070Z",
+  "command": "init",
+  "gitHead": "070727d5cd1fff7fdd7e0db903f1c696b0045ecd",
+  "model": "gemini-3.6-flash"
+}
diff --git a/openwiki/INSTRUCTIONS.md b/openwiki/INSTRUCTIONS.md
new file mode 100644
index 0000000..b0e3e33
--- /dev/null
+++ b/openwiki/INSTRUCTIONS.md
@@ -0,0 +1 @@
+A code wiki for this local repository. Prioritize a concise quickstart, architecture overview, source map, key workflows, domain concepts, operations/runbook notes, testing guidance, and integration points. Inspect git history to understand reasoning behind code changes and the progression of the repository. Keep pages grounded in the repository structure and recent code changes. Prefer practical navigation for engineers over generic summaries.
diff --git a/openwiki/architecture/index.md b/openwiki/architecture/index.md
new file mode 100644
index 0000000..27af5e3
--- /dev/null
+++ b/openwiki/architecture/index.md
@@ -0,0 +1,3 @@
+# Files
+
+- [System Architecture & Security Overview](overview.md) - Technical architecture of SlipItIn, covering .NET Aspire orchestration, ASP.NET Core SignalR hub design, JWT authentication, and IDbContextFactory thread safety.
diff --git a/openwiki/architecture/overview.md b/openwiki/architecture/overview.md
new file mode 100644
index 0000000..f18979a
--- /dev/null
+++ b/openwiki/architecture/overview.md
@@ -0,0 +1,129 @@
+---
+type: Architecture
+title: System Architecture & Security Overview
+description: Technical architecture of SlipItIn, covering .NET Aspire orchestration, ASP.NET Core SignalR hub design, JWT authentication, and IDbContextFactory thread safety.
+tags: [architecture, spire, signalr, jwt, efcore, security]
+---
+
+# System Architecture & Security Overview
+
+**SlipItIn** is designed as a distributed, real-time application using modern .NET 10 architecture. This document details the orchestration model, backend service structure, security model, and concurrency safeguards.
+
+The architectural foundation [orchestrates services with](/openwiki/operations/runbook.md) .NET Aspire, while [enforcing security & privacy on](/openwiki/domain/game-mechanics.md) domain entities and [serving real-time hub endpoints for](/openwiki/workflows/slip-and-challenge.md) all active game sessions. Source code structure for all architectural components can be found in the [Source Code Map](/openwiki/source-map.md).
+
+---
+
+## 1. .NET Aspire Orchestration Model
+
+The solution uses **.NET Aspire** to orchestrate application resources, services, and infrastructure dependencies:
+
+- **AppHost (`SlipItIn.AppHost/AppHost.cs`)**:
+  - Provisions a PostgreSQL database container (`AddPostgres("pgsql").AddDatabase("postgresdb")`).
+  - Registers the backend project `SlipItIn.Server` with a direct reference to the PostgreSQL resource.
+  - Registers the client project `SlipItIn` with service discovery references to `SlipItIn.Server`.
+- **Service Defaults (`SlipItIn.ServiceDefaults/Extensions.cs`)**:
+  - Configures **OpenTelemetry** logging, metrics (AspNetCore, HttpClient, Runtime), and tracing.
+  - Exposes standardized health check endpoints (`/health` and `/alive`).
+  - Enforces automatic HTTP resilience (`AddStandardResilienceHandler()`) and service discovery.
+
+---
+
+## 2. ASP.NET Core Backend Architecture (`SlipItIn.Server`)
+
+The backend is an ASP.NET Core Web API & SignalR application providing REST endpoints for user management and real-time WebSockets for game state synchronization.
+
+### Key Components
+
+1. **`Program.cs`**:
+   - Registers `AddServiceDefaults()`, `AddNpgsqlDataSource("postgresdb")`, and `AddDbContextFactory<SlipItInDbContext>()`.
+   - Configures JWT Bearer authentication with custom `OnMessageReceived` token resolution for SignalR.
+   - Automatically executes database migrations (`db.Database.Migrate()`) at startup.
+2. **`AuthController.cs`**:
+   - Manages user registration (`/api/auth/register`) and authentication (`/api/auth/login`).
+   - Uses `BCrypt.Net` for secure password hashing.
+   - Issues JWT tokens signed with `Jwt:Key`, containing `ClaimTypes.NameIdentifier`, `ClaimTypes.Name`, and `ClaimTypes.Email`.
+3. **`GameHub.cs`**:
+   - Real-time SignalR hub mapped to `/hubs/game`.
+   - Annotated with `[Authorize]` to reject unauthenticated WebSocket connections.
+   - Extracts and verifies user claims, delegating state mutations to `IGameService`.
+4. **`GameService.cs`**:
+   - Implements core business logic: game creation, player joins, card dealing, slip submission, challenge creation, and resolution.
+
+---
+
+## 3. JWT Authentication & SignalR Security
+
+In real-time SignalR applications, clients can attempt to spoof player identifiers. SlipItIn eliminates this vulnerability through strict claim validation and authorization checks.
+
+### SignalR Token Extraction
+Since WebSockets cannot pass custom HTTP headers during connection handshakes, `Program.cs` configures JWT query parameter extraction:
+
+```csharp
+options.Events = new JwtBearerEvents
+{
+    OnMessageReceived = context =>
+    {
+        var accessToken = context.Request.Query["access_token"];
+        var path = context.HttpContext.Request.Path;
+        if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
+        {
+            context.Token = accessToken;
+        }
+        return Task.CompletedTask;
+    }
+};
+```
+
+### Claims Validation & Access Control in `GameHub.cs`
+Inside `GameHub.cs`, claims are extracted directly from the authenticated SignalR context:
+
+* `GetAuthenticatedUserId()`: Extracts `Context.User?.FindFirst(ClaimTypes.NameIdentifier)`. Throws `UnauthorizedAccessException` if missing or invalid.
+* `ValidatePlayerAccessAsync(playerId, authUserId)`: Queries the database to verify that `Player.UserId` matches the authenticated user ID.
+* `ValidateHostAccessAsync(gameId, authUserId)`: Verifies that `Game.HostId` matches the authenticated user ID before allowing lobby configuration or game start.
+* `ValidateChallengeTargetAccessAsync(challengeId, authUserId)`: Ensures only the accused player can resolve a challenge against them.
+
+### Real-Time Auth Flow Diagram
+
+```mermaid
+sequenceDiagram
+    autonumber
+    actor Client as MAUI Client
+    participant Auth as AuthController
+    participant Hub as GameHub
+    participant Service as GameService
+    participant DB as SlipItInDbContext
+
+    Client->>Auth: POST /api/auth/login
+    Auth->>DB: Query User & Verify BCrypt Hash
+    Auth-->>Client: 200 OK (JWT Access Token)
+    Client->>Hub: Connect WebSocket /hubs/game?access_token=JWT
+    Hub->>Hub: Validate JWT Signature & Claims
+    Client->>Hub: JoinLobby(lobbyCode)
+    Hub->>Hub: GetAuthenticatedUserId()
+    Hub->>Service: JoinGameAsync(lobbyCode, userId, connectionId)
+    Service->>DB: Create IDbContext Session & Save Player
+    Hub-->>Client: Broadcast "PlayerJoined" (GameStateDto)
+```
+
+---
+
+## 4. Concurrency Safety: `IDbContextFactory`
+
+SignalR hubs process concurrent requests from multiple clients over persistent connections. Using a standard Scoped `DbContext` in SignalR leads to thread conflict exceptions (`InvalidOperationException: A second operation was started on this context instance before a previous operation completed`).
+
+### Resolution via `IDbContextFactory`
+SlipItIn solves this by registering EF Core with `AddDbContextFactory<SlipItInDbContext>`:
+
+* Every method call inside `GameHub.cs`, `AuthController.cs`, and `GameService.cs` creates a short-lived, isolated `DbContext` session using `using var context = _contextFactory.CreateDbContext()`.
+* **Benefit**: Thread-safe database operations across simultaneous challenges, card transfers, and lobby updates without race conditions.
+
+---
+
+## 5. Data Privacy Isolation Model
+
+To prevent players from inspecting opponent cards via network trace analysis, SlipItIn strictly separates public and private data DTOs:
+
+* **`GameStateDto` (Public)**: Broadcast to all players in a lobby. Contains public game information (`GameId`, `LobbyCode`, `Status`, `CurrentRound`, `RoundTimeRemaining`) and player summaries (`PlayerId`, `Username`, `Score`, `IsReady`, `CardCount`). **Crucially, no card text is included.**
+* **`PlayerHandDto` (Private)**: Direct unicast message sent *only* to the specific player's SignalR `ConnectionId`. Contains card text (`CardId`, `PhraseId`, `Text`, `IsUsed`).
+
+This architecture guarantees that players cannot see opponent phrase cards, even if they inspect raw WebSocket packets.
diff --git a/openwiki/domain/game-mechanics.md b/openwiki/domain/game-mechanics.md
new file mode 100644
index 0000000..61775ca
--- /dev/null
+++ b/openwiki/domain/game-mechanics.md
@@ -0,0 +1,171 @@
+---
+type: Domain Model
+title: Game Domain & State Models
+description: Detailed domain model and state lifecycles for SlipItIn games, rounds, phrase decks, players, and challenges.
+tags: [domain, models, state-machine, entity-framework, privacy]
+---
+
+# Game Domain & State Models
+
+This page describes the core domain entities, relational schema, state machines, and data transfer objects (DTOs) that form the foundation of the SlipItIn game engine.
+
+The domain model [is secured by data privacy models defined in](/openwiki/architecture/overview.md) the system architecture, [defines state lifecycles executed by](/openwiki/workflows/slip-and-challenge.md) real-time game workflows, and [maps domain classes in C# project files indexed in](/openwiki/source-map.md) the source map (`SlipItIn.Shared/Models/` and `SlipItIn.Shared/DTOs/`).
+
+---
+
+## 1. Entity Relationship Diagram (ERD)
+
+The database schema is managed via Entity Framework Core (`SlipItInDbContext`) targeting PostgreSQL.
+
+```mermaid
+erDiagram
+    User {
+        int Id PK
+        string Username
+        string Email
+        string PasswordHash
+        bool IsActive
+    }
+    Game {
+        int Id PK
+        string LobbyCode
+        int HostId FK
+        GameStatus Status
+        int MaxPlayers
+        int RoundDurationSeconds
+    }
+    Player {
+        int Id PK
+        int UserId FK
+        int GameId FK
+        int Score
+        bool IsReady
+        string ConnectionId
+    }
+    Phrase {
+        int Id PK
+        string Text
+        int CreatorId FK
+        bool IsActive
+    }
+    GameRound {
+        int Id PK
+        int GameId FK
+        int RoundNumber
+        RoundStatus Status
+    }
+    PlayerCard {
+        int Id PK
+        int PlayerId FK
+        int PhraseId FK
+        int GameRoundId FK
+        bool IsUsed
+    }
+    SlipChallenge {
+        int Id PK
+        int GameRoundId FK
+        int ChallengingPlayerId FK
+        int TargetPlayerId FK
+        int TargetCardId FK
+        ChallengeStatus Status
+    }
+
+    User ||--o{ Game : "hosts"
+    User ||--o{ Player : "participates as"
+    User ||--o{ Phrase : "creates"
+    Game ||--o{ Player : "contains"
+    Game ||--o{ GameRound : "has"
+    Player ||--o{ PlayerCard : "holds"
+    Phrase ||--o{ PlayerCard : "used in"
+    GameRound ||--o{ PlayerCard : "active in"
+    GameRound ||--o{ SlipChallenge : "contains"
+    Player ||--o{ SlipChallenge : "challenges"
+    Player ||--o{ SlipChallenge : "target of"
+    PlayerCard ||--o{ SlipChallenge : "targeted by"
+```
+
+---
+
+## 2. Core Domain Entities (`SlipItIn.Shared/Models`)
+
+### `User`
+Represents an authenticated account.
+* **Properties**: `Id`, `Username`, `Email`, `PasswordHash` (BCrypt), `CreatedAt`, `IsActive`.
+* **Relations**: Navigation properties to created `Game` instances, player entries (`Players`), and created phrases (`Phrases`).
+
+### `Game`
+Represents a multiplayer game session / lobby.
+* **Properties**: `Id`, `LobbyCode` (6-char alphanumeric), `HostId`, `Status` (`GameStatus`), `CreatedAt`, `StartedAt`, `EndedAt`, `MaxPlayers` (default 8), `RoundDurationSeconds` (30-60s).
+* **Relations**: `Host` (`User`), `Players` (`ICollection<Player>`), `Rounds` (`ICollection<GameRound>`).
+
+### `Player`
+Represents a user's participation inside a specific `Game`.
+* **Properties**: `Id`, `UserId`, `GameId`, `Score`, `SuccessfulSlips`, `FailedSlips`, `JoinedAt`, `IsReady`, `ConnectionId` (active SignalR connection ID).
+* **Relations**: `User`, `Game`, `Cards` (`ICollection<PlayerCard>`).
+
+### `Phrase`
+Represents a secret phrase or sentence created for the game deck.
+* **Properties**: `Id`, `Text`, `CreatorId`, `CreatedAt`, `IsActive`.
+* **Relations**: `Creator` (`User`), `PlayerCards` (`ICollection<PlayerCard>`).
+
+### `GameRound`
+Tracks a distinct round within a game.
+* **Properties**: `Id`, `GameId`, `RoundNumber`, `Status` (`RoundStatus`), `StartedAt`, `EndedAt`.
+* **Relations**: `Game`, `ActiveCards` (`ICollection<PlayerCard>`), `Challenges` (`ICollection<SlipChallenge>`).
+
+### `PlayerCard`
+Represents a specific phrase card dealt to a player for a round.
+* **Properties**: `Id`, `PlayerId`, `PhraseId`, `GameRoundId`, `IsUsed` (boolean flag set when player submits slip), `AssignedAt`.
+* **Relations**: `Player`, `Phrase`, `GameRound`.
+
+### `SlipChallenge`
+Represents an accusation made by one player against another player's submitted slip.
+* **Properties**: `Id`, `GameRoundId`, `ChallengingPlayerId`, `TargetPlayerId`, `TargetCardId`, `Status` (`ChallengeStatus`), `CreatedAt`, `ResolvedAt`.
+* **Relations**: `GameRound`, `ChallengingPlayer`, `TargetPlayer`, `TargetCard`.
+
+---
+
+## 3. Enumerations & State Lifecycles
+
+### Game Status (`GameStatus`)
+```mermaid
+stateDiagram-v2
+    [*] --> Lobby: CreateGameAsync()
+    Lobby --> InProgress: StartGameAsync() (Host only)
+    InProgress --> Completed: All rounds finished
+    Lobby --> Cancelled: Host cancels
+    InProgress --> Cancelled: Session abort
+```
+
+* **`Lobby`**: Waiting for players to join and set ready status.
+* **`InProgress`**: Active gameplay with dealt cards and active rounds.
+* **`Completed`**: Game finished, final scores tallied.
+* **`Cancelled`**: Lobby or game terminated early.
+
+### Round Status (`RoundStatus`)
+```mermaid
+stateDiagram-v2
+    [*] --> Waiting: StartGame / New Round
+    Waiting --> Active: DealCardsAsync()
+    Active --> Resolving: Challenge submitted
+    Resolving --> Active: Challenge resolved
+    Active --> Completed: Timer expires / All phrases used
+```
+
+### Challenge Status (`ChallengeStatus`)
+* **`Pending`**: Accusation registered, awaiting response from the accused player.
+* **`Approved`**: Accusation confirmed (the phrase was indeed an invalid slip). The card remains with the accused player.
+* **`Rejected`**: Accusation rejected (the slip was legitimate). As a penalty, the card is transferred to the accuser's hand (`PlayerCard.PlayerId = ChallengingPlayerId`).
+
+---
+
+## 4. Data Transfer Objects (DTOs) & Data Privacy
+
+To enforce security and data privacy ([Architecture Overview](/openwiki/architecture/overview.md)), the backend exposes decoupled DTOs in `SlipItIn.Shared/DTOs`:
+
+| DTO | Visibility | Purpose & Content |
+|---|---|---|
+| `AuthResponseDto` | Direct REST | JWT Access Token, Expiration, Username, Email. |
+| `GameStateDto` | Broadcast (Group) | Public lobby/game status: `GameId`, `LobbyCode`, `Status`, `CurrentRound`, `RoundTimeRemaining`, `Players` (`PlayerInfoDto` array containing `PlayerId`, `Username`, `Score`, `IsReady`, and `CardCount`). **No card text.** |
+| `PlayerHandDto` | Unicast (Client) | Private hand data sent strictly to the card owner: `PlayerId`, `Cards` (`PlayerCardDto` array containing `CardId`, `PhraseId`, `Text`, `IsUsed`). |
+| `SlipChallengeDto` | Unicast / Group | Challenge notification: `ChallengeId`, `ChallengingPlayerId`, `TargetPlayerId`, `TargetCardId`, `Status`, `CreatedAt`. |
diff --git a/openwiki/domain/index.md b/openwiki/domain/index.md
new file mode 100644
index 0000000..8e21f6d
--- /dev/null
+++ b/openwiki/domain/index.md
@@ -0,0 +1,3 @@
+# Files
+
+- [Game Domain & State Models](game-mechanics.md) - Detailed domain model and state lifecycles for SlipItIn games, rounds, phrase decks, players, and challenges.
diff --git a/openwiki/index.md b/openwiki/index.md
new file mode 100644
index 0000000..bfaa1d2
--- /dev/null
+++ b/openwiki/index.md
@@ -0,0 +1,15 @@
+---
+okf_version: "0.1"
+---
+
+# Files
+
+- [SlipItIn Code Wiki Quickstart](quickstart.md) - Entrypoint for SlipItIn - a real-time multiplayer party game built with .NET 10, MAUI, ASP.NET Core SignalR, EF Core PostgreSQL, and .NET Aspire.
+- [Source Code Map & Navigation Directory](source-map.md) - Practical navigation guide mapping source files across projects to system domains and responsibilities.
+
+# Directories
+
+- [architecture](architecture/)
+- [domain](domain/)
+- [operations](operations/)
+- [workflows](workflows/)
diff --git a/openwiki/operations/index.md b/openwiki/operations/index.md
new file mode 100644
index 0000000..45f0ac7
--- /dev/null
+++ b/openwiki/operations/index.md
@@ -0,0 +1,3 @@
+# Files
+
+- [Operations, Environment Setup & Testing Guidance](runbook.md) - Operational guide for launching SlipItIn with .NET Aspire, running EF Core PostgreSQL migrations, configuring JWT secrets, and executing tests.
diff --git a/openwiki/operations/runbook.md b/openwiki/operations/runbook.md
new file mode 100644
index 0000000..90a5c76
--- /dev/null
+++ b/openwiki/operations/runbook.md
@@ -0,0 +1,104 @@
+---
+type: Runbook
+title: Operations, Environment Setup & Testing Guidance
+description: Operational guide for launching SlipItIn with .NET Aspire, running EF Core PostgreSQL migrations, configuring JWT secrets, and executing tests.
+tags: [operations, runbook, spire, postgresql, migrations, testing]
+---
+
+# Operations, Environment Setup & Testing Guidance
+
+This runbook provides actionable instructions for local development setup, starting services via .NET Aspire, executing Entity Framework Core migrations, configuring environment keys, and running tests.
+
+This guide [configures environment parameters for](/openwiki/architecture/overview.md) the backend architecture, [manages database migrations for entities in](/openwiki/domain/game-mechanics.md) the domain model, [verifies real-time event flows defined in](/openwiki/workflows/slip-and-challenge.md) the workflow guide, and [references source entrypoints cataloged in](/openwiki/source-map.md) the source map.
+
+---
+
+## 1. Local Development Environment Setup
+
+### Prerequisites
+* **.NET 10 SDK** (Installed and verified via `dotnet --version`).
+* **Docker Desktop** or **Podman** (Required by .NET Aspire to run the PostgreSQL container).
+* **Workloads**: .NET Aspire workload and .NET MAUI workload (`dotnet workload install aspire maui`).
+
+### Starting the Distributed Application with Aspire
+
+Run the Aspire orchestrator project:
+
+```bash
+dotnet run --project SlipItIn.AppHost/SlipItIn.AppHost.csproj
+```
+
+**What happens during launch:**
+1. Aspire launches a PostgreSQL container (`pgsql`/`postgresdb`).
+2. Aspire builds and starts `SlipItIn.Server`.
+3. `Program.cs` automatically executes EF Core migrations (`db.Database.Migrate()`), creating database tables if they do not exist.
+4. Aspire Dashboard opens in your web browser, displaying live metrics, OpenTelemetry traces, and structured logs for all services.
+
+---
+
+## 2. Configuration & Secrets Management
+
+Configuration settings are loaded from `appsettings.json` and environment variables.
+
+### Key Configuration Keys (`SlipItIn.Server`)
+
+| Key | Default Value | Description |
+|---|---|---|
+| `Jwt:Key` | `YourSuperSecretKeyThatIsAtLeast32CharactersLong!` | Secret signing key for JWT tokens (Must be >= 256 bits). |
+| `Jwt:Issuer` | `SlipItInServer` | Token issuer claim. |
+| `Jwt:Audience` | `SlipItInClient` | Token audience claim. |
+| `Jwt:ExpirationMinutes` | `1440` (24 hours) | JWT token lifespan. |
+| `ConnectionStrings:postgresdb` | Configured via Aspire | PostgreSQL connection string. |
+
+### Production Configuration Security
+In production deployment, override `Jwt:Key` using environment variables or user secrets (`dotnet user-secrets`):
+
+```bash
+dotnet user-secrets set "Jwt:Key" "YOUR_HIGH_ENTROPY_PRODUCTION_SECRET_KEY_HERE" --project SlipItIn.Server
+```
+
+---
+
+## 3. Entity Framework Core Migrations
+
+When altering models in `SlipItIn.Shared/Models/` or `SlipItInDbContext.cs`:
+
+### Adding a New Migration
+Run the EF Core CLI from the repository root:
+
+```bash
+dotnet ef migrations add <MigrationName> --project SlipItIn.Server --startup-project SlipItIn.Server
+```
+
+### Applying Migrations Manually
+While `Program.cs` applies migrations at startup (`db.Database.Migrate()`), migrations can also be manually applied via command line:
+
+```bash
+dotnet ef database update --project SlipItIn.Server --startup-project SlipItIn.Server
+```
+
+---
+
+## 4. Testing Guidance & Verification Scenarios
+
+When developing or extending SlipItIn features, verify the core architecture through targeted test scenarios specified in `Agents/Architecture.md`:
+
+### 1. JWT Authentication & Claims Tests
+* **Test Objective**: Verify `GameHub` rejects unauthenticated WebSocket connections or missing token query parameters.
+* **Verification**: Connect to `/hubs/game` without `?access_token=...` or with an expired token. Confirm SignalR connection is terminated with 401 Unauthorized.
+
+### 2. Player Access Authorization Tests
+* **Test Objective**: Verify Player A cannot manipulate Player B's cards or state.
+* **Verification**: Authenticate as User A and attempt to call `GameHub.SubmitSlip(gameId, playerBId, cardId)`. Verify that `ValidatePlayerAccessAsync` throws `UnauthorizedAccessException` and returns an error response.
+
+### 3. Concurrency & Race Condition Tests
+* **Test Objective**: Confirm `IDbContextFactory` handles simultaneous WebSocket calls without thread collision.
+* **Verification**: Simulate 5 parallel calls to `GameHub.ChallengeSlip()` or `SubmitSlip()` across multiple clients. Verify that all calls complete cleanly without `InvalidOperationException` from DbContext.
+
+### 4. Data Privacy Isolation Verification
+* **Test Objective**: Confirm card text is never broadcast in public group messages.
+* **Verification**: Capture SignalR `PlayerJoined` and `GameStateUpdated` payloads. Inspect JSON content to confirm only `CardCount` is present and no phrase card `Text` is leaked.
+
+### 5. False Accusation Penalty Verification
+* **Test Objective**: Verify penalty card transfer when a challenge is rejected.
+* **Verification**: Submit a challenge against a valid slip, then call `ResolveChallenge(challengeId, approved: false)`. Verify in the database that `PlayerCard.PlayerId` is reassigned to the challenger's `PlayerId`.
diff --git a/openwiki/quickstart.md b/openwiki/quickstart.md
new file mode 100644
index 0000000..33b4b58
--- /dev/null
+++ b/openwiki/quickstart.md
@@ -0,0 +1,84 @@
+---
+type: Overview
+title: SlipItIn Code Wiki Quickstart
+description: Entrypoint for SlipItIn - a real-time multiplayer party game built with .NET 10, MAUI, ASP.NET Core SignalR, EF Core PostgreSQL, and .NET Aspire.
+tags: [quickstart, overview, slipitin, dotnet10, spire]
+---
+
+# SlipItIn Code Wiki Quickstart
+
+Welcome to the **SlipItIn** repository wiki. SlipItIn is a real-time multiplayer party game where players receive secret phrase cards and attempt to "slip" those phrases into everyday conversations or text chats without getting caught by other players.
+
+The solution is built using **.NET 10**, leveraging **ASP.NET Core Web API & SignalR** for the backend engine, **Entity Framework Core (Npgsql / PostgreSQL)** for data persistence, **.NET MAUI** for the cross-platform client app, and **.NET Aspire** for distributed cloud-native orchestration and telemetry.
+
+---
+
+## 1. System Overview & Architecture Snapshot
+
+The repository is organized as a multi-project .NET solution (`SlipItIn.slnx`):
+
+```
+SlipItIN/
+├── SlipItIn.AppHost/          # .NET Aspire AppHost orchestrator (pgsql + server + client)
+├── SlipItIn.Server/           # ASP.NET Core Web API + SignalR Hub + Game Engine
+├── SlipItIn.Shared/           # Shared Class Library (Models & DTOs)
+├── SlipItIn.ServiceDefaults/  # Aspire OpenTelemetry, Health Checks & Service Discovery
+└── SlipItIn/                  # .NET MAUI Client App (Android, iOS, MacCatalyst, Windows)
+```
+
+The system architecture [orchestrates services with](/openwiki/operations/runbook.md) .NET Aspire and [enforces security and privacy on](/openwiki/domain/game-mechanics.md) all domain entities. For a deep dive into the backend design, JWT security, and concurrency safety, see the [System Architecture & Security Overview](/openwiki/architecture/overview.md).
+
+```mermaid
+graph TD
+    MAUI[SlipItIn .NET MAUI Client] -->|REST / API| Server[SlipItIn.Server ASP.NET Core]
+    MAUI -->|SignalR WebSockets| Hub[GameHub /hubs/game]
+    Server -->|IDbContextFactory| DB[(PostgreSQL Database)]
+    AppHost[.NET Aspire AppHost] -->|Orchestrates| Server
+    AppHost -->|Provisions| DB
+    Server -->|Uses Defaults| ServiceDefaults[SlipItIn.ServiceDefaults]
+```
+
+---
+
+## 2. Core Game Loop & Mechanics
+
+1. **Lobby Creation**: Host creates a game lobby with a 6-character code. Players join via code.
+2. **Card Dealing**: Upon game start, each player receives a private hand of 5 secret phrase cards (`PlayerHandDto`).
+3. **Phrase Slipping**: During normal conversation, a player speaks or types one of their phrases and clicks **Submit Slip**.
+4. **Slip Challenge**: Opponents suspecting a fake phrase can issue a **Slip Challenge**.
+   - **Justified Accusation (Approved)**: Target phrase was an invalid slip.
+   - **False Accusation (Rejected)**: Target phrase was legitimate. The accuser receives the card as a penalty (expanding their hand size beyond 5).
+
+To learn how game events flow across SignalR in real time, inspect the [Slip & Challenge Workflows](/openwiki/workflows/slip-and-challenge.md).
+
+---
+
+## 3. Wiki Navigation Map
+
+Explore specific documentation sections for technical details:
+
+- **[System Architecture & Security Overview](/openwiki/architecture/overview.md)**: Explains .NET Aspire orchestration, JWT bearer authentication, claims validation, `IDbContextFactory` thread safety, and public vs. private data isolation.
+- **[Game Domain & State Models](/openwiki/domain/game-mechanics.md)**: Details domain entities (`User`, `Game`, `Player`, `Phrase`, `PlayerCard`, `GameRound`, `SlipChallenge`) and their state lifecycles.
+- **[Slip & Challenge Workflows](/openwiki/workflows/slip-and-challenge.md)**: Details step-by-step game loop execution, real-time SignalR notifications, and penalty rules.
+- **[Source Code Map](/openwiki/source-map.md)**: Directory and file navigation index mapping repository paths to technical domains.
+- **[Operations & Runbook](/openwiki/operations/runbook.md)**: Instructions for running the app with Aspire, executing PostgreSQL EF Core migrations, configuration keys, and testing strategies.
+
+---
+
+## 4. Key Architectural Rules for Developers & Agents
+
+When modifying this repository, strictly adhere to these core rules:
+
+1. **IDbContextFactory Thread Safety**: Never inject a scoped `SlipItInDbContext` into SignalR hubs or singleton services. Always use `IDbContextFactory<SlipItInDbContext>.CreateDbContext()` to prevent DbContext concurrency exceptions during concurrent WebSocket calls ([Architecture Overview](/openwiki/architecture/overview.md)).
+2. **Data Privacy Isolation**: Do not leak card text into public DTOs. Public game state must be broadcast using `GameStateDto` (card counts only), while private cards are dispatched strictly via `PlayerHandDto` to individual client connections ([Domain Mechanics](/openwiki/domain/game-mechanics.md)).
+3. **Server-Side Validation**: SignalR client calls represent intent ("I want to challenge X"). The server MUST re-verify JWT claims, player game membership, card ownership, and round status inside `GameHub.cs` and `GameService.cs` before mutating state ([Slip & Challenge Workflows](/openwiki/workflows/slip-and-challenge.md)).
+
+---
+
+## 5. Backlog
+
+The following features and components are specified in specification documents (`Agents/ProjectPlan.md` and `Agents/Architecture.md`) and backlogged for upcoming development iterations:
+
+- **MAUI Client Services & ViewModels**: Implement `ApiService`, `SignalRService`, `GameStateService`, `LobbyViewModel`, and `GameBoardViewModel` under `SlipItIn/` using `CommunityToolkit.Mvvm` and `WeakReferenceMessenger`. (Anchor: `SlipItIn/`, pending client phase 3 completion).
+- **Offline Action Queue & Auto-Resync**: Implement exponential backoff reconnect logic (0s, 2s, 10s, 30s) and queued action execution upon app resume in MAUI client. (Anchor: `SlipItIn/Services/`, deferred until MAUI service layer setup).
+- **Timer Engine for Slip Rounds**: Background timer service on server enforcing round duration limits (30-60s) with automated round completion notifications. (Anchor: `SlipItIn.Server/Services/`, pending phase 2b refinement).
diff --git a/openwiki/source-map.md b/openwiki/source-map.md
new file mode 100644
index 0000000..05df4b6
--- /dev/null
+++ b/openwiki/source-map.md
@@ -0,0 +1,69 @@
+---
+type: Reference
+title: Source Code Map & Navigation Directory
+description: Practical navigation guide mapping source files across projects to system domains and responsibilities.
+tags: [source-map, navigation, directory, projects]
+---
+
+# Source Code Map & Navigation Directory
+
+This directory maps every major project, folder, and source file in the SlipItIn repository to its system domain and technical responsibility.
+
+This navigation map [indexes backend architecture files described in](/openwiki/architecture/overview.md) the system architecture, [indexes domain model files defined in](/openwiki/domain/game-mechanics.md) the domain mechanics guide, [indexes workflow implementation files detailed in](/openwiki/workflows/slip-and-challenge.md) the workflow guide, and [indexes operational configuration files documented in](/openwiki/operations/runbook.md) the operations runbook.
+
+---
+
+## 1. Solution Projects (`SlipItIn.slnx`)
+
+```
+SlipItIn.slnx
+├── SlipItIn.AppHost/          # Aspire distributed orchestrator
+├── SlipItIn.Server/           # Web API & SignalR real-time server
+├── SlipItIn.Shared/           # Shared models & data transfer objects
+├── SlipItIn.ServiceDefaults/  # Aspire OpenTelemetry & health checks
+├── SlipItIn/                  # .NET MAUI multi-platform client
+└── Agents/                    # Architecture & planning briefs
+```
+
+---
+
+## 2. Directory & Source File Map
+
+### `.NET Aspire Orchestration` (`SlipItIn.AppHost`)
+- **`AppHost.cs`**: Orchestrates application dependencies. Configures PostgreSQL (`AddPostgres("pgsql")`), references `SlipItIn.Server`, and links the MAUI client.
+- **`appsettings.json`**: Aspire orchestrator configuration.
+
+### `Service Defaults & Telemetry` (`SlipItIn.ServiceDefaults`)
+- **`Extensions.cs`**: Implements `AddServiceDefaults()` and `ConfigureOpenTelemetry()`. Configures OpenTelemetry logging, metrics, tracing filters (excluding `/health` and `/alive`), service discovery, and HTTP resilience handlers.
+
+### `Backend Web API & SignalR Server` (`SlipItIn.Server`)
+- **`Program.cs`**: Entrypoint for ASP.NET Core server. Registers EF Core `IDbContextFactory`, JWT authentication middleware, SignalR query parameter token parsing, Npgsql PostgreSQL data source, OpenAPI, CORS policies, and automatic EF migrations.
+- **`Controllers/AuthController.cs`**: REST API controller providing `/api/auth/register`, `/api/auth/login`, and `/api/auth/me`. Handles BCrypt password hashing and JWT token generation.
+- **`Hubs/GameHub.cs`**: SignalR hub mapped to `/hubs/game`. Performs JWT claim extraction (`GetAuthenticatedUserId`), player/host authorization checks (`ValidatePlayerAccessAsync`, `ValidateHostAccessAsync`), connection ID tracking, and real-time event broadcasting.
+- **`Services/IGameService.cs`**: Contract interface defining backend game operations.
+- **`Services/GameService.cs`**: Core engine implementation. Handles game creation, player joining, card dealing from active phrases, slip submission, challenge creation, penalty resolution, and state serialization (`GetGameStateAsync`, `GetPlayerHandAsync`).
+- **`Data/SlipItInDbContext.cs`**: Entity Framework Core DbContext mapping `Users`, `Games`, `Players`, `Phrases`, `PlayerCards`, `GameRounds`, and `SlipChallenges` to PostgreSQL tables.
+- **`Migrations/`**: Auto-generated EF Core migration snapshots (`20260723193505_InitialCreate.cs`).
+- **`appsettings.json` & `appsettings.Development.json`**: JWT secret keys, issuer/audience defaults, and database connection strings.
+
+### `Shared Domain & DTOs` (`SlipItIn.Shared`)
+- **`Models/User.cs`**: Entity storing user credentials, BCrypt password hashes, and user state.
+- **`Models/Game.cs`**: Entity storing lobby codes, status (`GameStatus`), host ID, and duration parameters.
+- **`Models/Player.cs`**: Entity tracking player scores, ready status, and SignalR connection IDs.
+- **`Models/Phrase.cs`**: Entity storing phrase text and creator metadata.
+- **`Models/GameRound.cs`**: Entity tracking round numbers and status (`RoundStatus`).
+- **`Models/PlayerCard.cs`**: Entity linking phrases to players for specific rounds (`IsUsed` flag).
+- **`Models/SlipChallenge.cs`**: Entity recording accusations, challenging/target player IDs, and challenge status (`ChallengeStatus`).
+- **`DTOs/AuthDtos.cs`**: DTOs for authentication (`RegisterRequestDto`, `LoginRequestDto`, `AuthResponseDto`).
+- **`DTOs/GameStateDto.cs`**: DTOs for public state (`GameStateDto`, `PlayerInfoDto`), private hand state (`PlayerHandDto`, `PlayerCardDto`), and challenge alerts (`SlipChallengeDto`).
+
+### `Cross-Platform MAUI Client` (`SlipItIn`)
+- **`MauiProgram.cs`**: Client builder configuring MAUI app shell, fonts, and logging debug extensions.
+- **`App.xaml` & `App.xaml.cs`**: Root MAUI application class.
+- **`AppShell.xaml` & `AppShell.xaml.cs`**: AppShell routing container.
+- **`MainPage.xaml` & `MainPage.xaml.cs`**: Initial entry view.
+- **`Platforms/`**: Platform-specific entry points for Android, iOS, MacCatalyst, and Windows.
+
+### `Documentation & Specifications` (`Agents/`)
+- **`Agents/Architecture.md`**: Specification document defining Phase 2b backend security (JWT validation, `IDbContextFactory`, privacy DTOs) and Phase 3b MAUI stability patterns (`WeakReferenceMessenger`, auto-reconnect, dual-layer storage).
+- **`Agents/ProjectPlan.md`**: Project plan breakdown covering phases 1 through 4.
diff --git a/openwiki/workflows/index.md b/openwiki/workflows/index.md
new file mode 100644
index 0000000..22d6053
--- /dev/null
+++ b/openwiki/workflows/index.md
@@ -0,0 +1,3 @@
+# Files
+
+- [Slip & Challenge Workflows](slip-and-challenge.md) - Real-time game loops covering lobby creation, card dealing, phrase slipping, slip challenging, and challenge resolution.
diff --git a/openwiki/workflows/slip-and-challenge.md b/openwiki/workflows/slip-and-challenge.md
new file mode 100644
index 0000000..3f1685c
--- /dev/null
+++ b/openwiki/workflows/slip-and-challenge.md
@@ -0,0 +1,125 @@
+---
+type: Workflow
+title: Slip & Challenge Workflows
+description: Real-time game loops covering lobby creation, card dealing, phrase slipping, slip challenging, and challenge resolution.
+tags: [workflow, game-loop, signalr, slip-mechanic, real-time]
+---
+
+# Slip & Challenge Workflows
+
+This document outlines the core real-time game workflows in SlipItIn, detailing how SignalR events, backend service operations, database updates, and client state notifications interact.
+
+These workflows [execute state transitions on](/openwiki/domain/game-mechanics.md) domain entities, [invoke real-time methods in](/openwiki/architecture/overview.md) the ASP.NET Core `GameHub`, [are implemented across backend services mapped in](/openwiki/source-map.md) the source map, and [are verified using tests described in](/openwiki/operations/runbook.md) the operations runbook.
+
+---
+
+## 1. Game Setup & Lobby Workflow
+
+```
+[Host] CreateLobby() ──> Generate 6-Char LobbyCode ──> Add to SignalR Group
+                                                               │
+[Player] JoinLobby() ──> Verify MaxPlayers & Status ───────────┤
+                                                               │
+[Player] PlayerReady() ──> Set IsReady = true ─────────────────┼──> Broadcast GameStateUpdated
+                                                               │
+[Host] StartGame() ──> Deal 5 Random Cards Per Player ─────────┴──> Unicast PlayerHandUpdated
+```
+
+1. **Lobby Creation**: Host calls `GameHub.CreateLobby()`. `GameService.CreateGameAsync()` creates a `Game` record (`Status = Lobby`), generates a random 6-character `LobbyCode`, adds the host as the first `Player`, and registers the host's WebSocket `ConnectionId`. Caller receives `LobbyCreated`.
+2. **Joining Lobby**: Opponents invoke `GameHub.JoinLobby(lobbyCode)`. `GameService.JoinGameAsync()` validates that the game exists, has space (`Players.Count < MaxPlayers`), and is in `Lobby` status. The player is assigned a `Player` entry, added to the SignalR group `lobbyCode`, and `PlayerJoined` (`GameStateDto`) is broadcast to all participants.
+3. **Player Ready**: Players invoke `GameHub.PlayerReady(gameId, playerId)`. `GameHub` checks `ValidatePlayerAccessAsync` and broadcasts `GameStateUpdated`.
+4. **Game Start & Card Dealing**: The host invokes `GameHub.StartGame(gameId)`. `GameHub` verifies host authorization via `ValidateHostAccessAsync(gameId, authUserId)`.
+   - `GameService.StartGameAsync()` transitions `GameStatus` to `InProgress` and creates `GameRound` 1 (`RoundStatus.Active`).
+   - `GameService.DealCardsAsync()` fetches active phrases, randomly selects 5 phrases per player, creates `PlayerCard` entries, and saves them to PostgreSQL.
+   - SignalR broadcasts `GameStarted` to the group.
+   - SignalR sends private `PlayerHandUpdated` notifications (`PlayerHandDto`) individually to each player's `ConnectionId`.
+
+---
+
+## 2. Phrase Slipping Workflow
+
+During real-life conversation or chat, a player speaks or types one of their secret phrases and marks it as used in the app:
+
+1. **Submission**: Player A calls `GameHub.SubmitSlip(gameId, playerId, cardId)`.
+2. **Access Control**: `GameHub` executes `ValidatePlayerAccessAsync(playerId, userId)` to verify that Player A owns the specified `Player` account.
+3. **Engine Execution**: `GameService.SubmitSlipAsync()` loads the `PlayerCard`, validates card ownership (`card.PlayerId == playerId`), and sets `card.IsUsed = true`.
+4. **Notification**: `GameHub` broadcasts `SlipSubmitted` (`{ PlayerId, CardId, Success }`) to the SignalR lobby group.
+
+---
+
+## 3. Slip Challenge & Resolution Workflow
+
+If another player suspects that a submitted phrase or recent conversation statement was an invalid slip, they can challenge the slipper.
+
+### Step 1: Challenge Initiation
+1. Player B (Challenger) calls `GameHub.ChallengeSlip(gameId, challengingPlayerId, targetCardId)`.
+2. `GameHub` verifies Player B's identity (`ValidatePlayerAccessAsync`).
+3. `GameService.CreateChallengeAsync()` creates a `SlipChallenge` entity with `ChallengeStatus.Pending`.
+4. `GameHub` broadcasts `SlipChallenged` (`SlipChallengeDto`) to the lobby group.
+5. `GameHub` sends a targeted `ChallengeReceived` message directly to Player A's `ConnectionId`.
+
+### Step 2: Challenge Resolution
+Player A (the accused) responds by admitting or denying the false slip:
+
+1. Player A calls `GameHub.ResolveChallenge(challengeId, approved)`.
+2. `GameHub` executes `ValidateChallengeTargetAccessAsync(challengeId, authUserId)` to guarantee that *only* the accused player can resolve the challenge.
+3. `GameService.ResolveChallengeAsync()` executes penalty logic:
+   - **`approved = true` (Justified Accusation / Legitimate Catch)**: The accused admits the phrase was a fake slip. `SlipChallenge.Status` is set to `Approved`. The card remains with the accused player.
+   - **`approved = false` (False Accusation / Wrong Penalty)**: The accused denies the charge (the slip was valid). `SlipChallenge.Status` is set to `Rejected`. As a penalty for making a false accusation, the phrase card is reassigned to the challenger: `targetCard.PlayerId = challenge.ChallengingPlayerId`. The challenger now holds >5 cards in their hand.
+4. `GameHub` broadcasts `ChallengeResolved` to the lobby group.
+
+### Real-time Sequence Diagram
+
+```mermaid
+sequenceDiagram
+    autonumber
+    actor Slipper as Player A (Accused)
+    actor Challenger as Player B (Challenger)
+    participant Hub as GameHub
+    participant Service as GameService
+    participant DB as SlipItInDbContext
+
+    Note over Slipper, Challenger: Active Gameplay
+    Slipper->>Hub: SubmitSlip(gameId, playerAId, cardId)
+    Hub->>Service: SubmitSlipAsync(gameId, playerAId, cardId)
+    Service->>DB: Set PlayerCard.IsUsed = true
+    Hub-->>Challenger: Broadcast "SlipSubmitted"
+
+    Note over Challenger: Suspects invalid phrase
+    Challenger->>Hub: ChallengeSlip(gameId, playerBId, targetCardId)
+    Hub->>Service: CreateChallengeAsync()
+    Service->>DB: Insert SlipChallenge (Status = Pending)
+    Hub-->>Challenger: Broadcast "SlipChallenged" (SlipChallengeDto)
+    Hub-->>Slipper: Unicast "ChallengeReceived" to ConnectionId
+
+    Note over Slipper: Accused resolves accusation
+    Slipper->>Hub: ResolveChallenge(challengeId, approved)
+    Hub->>Hub: ValidateChallengeTargetAccessAsync()
+    Hub->>Service: ResolveChallengeAsync(challengeId, approved)
+    alt approved == false (False Accusation)
+        Service->>DB: Update targetCard.PlayerId = PlayerBId (Penalty)
+        Service->>DB: Set Challenge.Status = Rejected
+    else approved == true (Justified Catch)
+        Service->>DB: Set Challenge.Status = Approved
+    end
+    Hub-->>Challenger: Broadcast "ChallengeResolved"
+```
+
+---
+
+## 4. SignalR Hub Event Reference
+
+Summary of server-to-client events emitted by `GameHub.cs`:
+
+| Event Name | Scope | Payload | Trigger |
+|---|---|---|---|
+| `LobbyCreated` | Caller | `{ GameId, LobbyCode }` | Host calls `CreateLobby()` |
+| `PlayerJoined` | Group | `GameStateDto` | Player calls `JoinLobby()` |
+| `GameStateUpdated` | Group | `GameStateDto` | Player calls `PlayerReady()` |
+| `GameStarted` | Group | `{ GameId }` | Host calls `StartGame()` |
+| `PlayerHandUpdated` | Unicast | `PlayerHandDto` | Hand dealt or updated |
+| `SlipSubmitted` | Group | `{ PlayerId, CardId, Success }` | Player calls `SubmitSlip()` |
+| `SlipChallenged` | Group | `SlipChallengeDto` | Opponent calls `ChallengeSlip()` |
+| `ChallengeReceived` | Unicast | `SlipChallengeDto` | Direct alert to target player |
+| `ChallengeResolved` | Group | `SlipChallengeDto` | Target calls `ResolveChallenge()` |
+| `Error` | Caller | `{ Message }` | Any authorization or engine exception |