Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1e887b35c | ||
|
|
3891915df3 |
No files matched your search
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "dev-workflows",
|
||||
"description": "Dev workflows como plugin do Claude Code: roteador por nível, agentes, gate de qualidade e revisão de segurança.",
|
||||
"owner": {
|
||||
"name": "Evandro Fonseca Junior",
|
||||
"url": "https://github.com/evandrodevbr"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "dev-workflows",
|
||||
"source": "./",
|
||||
"description": "Roteador por nível, agentes, gate de qualidade e workflows de front, back, arquitetura, segurança e README."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "dev-workflows",
|
||||
"displayName": "Dev Workflows",
|
||||
"version": "2.0.0",
|
||||
"description": "Workflows de desenvolvimento que escalam com o tamanho do pedido: roteador por nível, agentes com permissão mínima, gate de qualidade determinístico e revisão de segurança com fontes ao vivo.",
|
||||
"author": {
|
||||
"name": "Evandro Fonseca Junior",
|
||||
"url": "https://github.com/evandrodevbr"
|
||||
},
|
||||
"homepage": "https://github.com/evandrodevbr/dev-workflows",
|
||||
"repository": "https://github.com/evandrodevbr/dev-workflows",
|
||||
"license": "MIT",
|
||||
"keywords": ["workflow", "code-review", "security", "frontend", "backend", "quality-gate"]
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Isolamento Linux para testes do loop
|
||||
run: sudo apt-get update && sudo apt-get install -y bubblewrap
|
||||
- name: Lint das skills, agentes e hooks
|
||||
run: python3 qa/lint_skills.py
|
||||
- name: Testes do gate e dos hooks
|
||||
run: python3 qa/test_gate.py
|
||||
- name: Testes comportamentais de catálogo, roteador, adapters e loop
|
||||
run: python3 -m unittest discover -s qa -p 'test_*.py' -q
|
||||
@@ -1,17 +0,0 @@
|
||||
name: upstream
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 9 1 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
vendored-skills:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Skills vendorizadas atrás do upstream
|
||||
run: python3 qa/check_upstream.py
|
||||
@@ -1,37 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased (2026-09-25)
|
||||
|
||||
### Added
|
||||
- Catalog sync/list/route for OpenCode and OMP with exact model IDs, public Zen/Go sources, OpenRouter reference tariffs, manually audited Artificial Analysis v1.5/account evidence, provenance, fixed score bands and fail-closed billing checks.
|
||||
- Opt-in external goal loop with protected evaluator, isolated detached git worktrees, current quality-gate verification, budgets, evidence, stop/resume and no automatic merge or publish.
|
||||
- Native OMP package installation with seven agents and skills; Hermes remains skills-only. Claude Code/OpenCode/OMP goal-loop runners use constrained role tools.
|
||||
- Full 2026-09-25 Zen/Go catalog CSV with public-source hashes, missing-score/price coverage and account-eligibility exclusions; CLI and integration regressions.
|
||||
|
||||
### Security
|
||||
- Autonomous evaluator and quality gate require Linux bubblewrap and run without network access; agent CLIs retain network access for their configured providers. Neither the worktree nor this sandbox guarantees provider-only egress.
|
||||
- Detached attempts keep `.git` read-only to untrusted processes, revalidate the worktree before staging, and gate/score separate clean copies of each candidate commit. Unreported paid usage blocks automatic resume.
|
||||
- OMP discovery and dispatch validate each advertised thinking level and preserve slash-bearing model IDs; routing requires a matching exact harness or explicit proxy approval and an explicit budget for Zen. OpenRouter long-context overrides affect comparative costs, and archived CSV staleness can be replayed at a fixed UTC time.
|
||||
|
||||
## 2.0.0 (2026-09-24)
|
||||
|
||||
### Added
|
||||
- Claude Code plugin packaging (`.claude-plugin/plugin.json`, `marketplace.json`); workflows moved to `skills/<name>/SKILL.md`.
|
||||
- `dev-router`: tiers 0 to 3 with security triggers that raise the tier, so small requests skip heavy phases.
|
||||
- `lean-code`: reuse ladder, dependency justification, diff budget per tier.
|
||||
- `wf-bugfix` and `wf-refactor`.
|
||||
- Agents: `explorer`, `planner`, `implementer`, `reviewer`, `security-auditor`, `ui-critic`, `verifier`, each with only the tools its role needs.
|
||||
- `scripts/quality-gate`: deterministic checks per tier, scoped to the diff; missing tools reported as unverified; new dependencies listed.
|
||||
- Hooks: `Stop` gate, shell command guard, formatter after edits.
|
||||
- opencode adapter and `scripts/install.py` for Claude Code, opencode and Hermes Agent.
|
||||
- taste-skill (`design-taste-frontend`, `redesign-existing-projects`) vendored for `wf-frontend`.
|
||||
- `qa/lint_skills.py`, `qa/test_gate.py`, `qa/check_upstream.py` and CI.
|
||||
|
||||
### Changed
|
||||
- The five workflows were cut to about 100 lines each; phases state their minimum tier; grep-count checks replaced by the gate and real tools.
|
||||
- Security references moved to OWASP Top 10:2025, ASVS 5.0 and OWASP LLM Top 10 (2025).
|
||||
- Machine-specific paths (`~/.hermes/...`, `vision_analyze`) and references to skills that do not exist publicly removed.
|
||||
|
||||
### Removed
|
||||
- The keyword-count quality harness (`qa/wf_quality_harness.py`, `test_quality.py`, `test_tests.py`, snapshots). It scored occurrences of words like `VERIFICAR` and `NUNCA`, so its "+98.6%" measured wording, not quality; it also failed on a fresh clone because it looked for skills in `~/.hermes`.
|
||||
- apisec-inc's `api-security-review` from `wf-backend` (name collision with the vendored OWASP skill).
|
||||
+24
-17
@@ -1,29 +1,36 @@
|
||||
# Contributing
|
||||
|
||||
Open an issue to discuss first, then send a small, focused PR.
|
||||
Thanks for taking the time to contribute. This project is small, so keep it simple: open an issue to discuss first, then send a PR.
|
||||
|
||||
## Rules
|
||||
## What to improve
|
||||
|
||||
1. **Grounded.** Every command, path and claim in a skill or README traces to a real fact. No invented commands or badges.
|
||||
2. **Short skills.** `SKILL.md` stays under 150 lines; details go to `references/` next to it.
|
||||
3. **Proportional.** Each phase says from which tier it runs. Nothing is mandatory for every request unless it is cheap (the tier 0 gate).
|
||||
4. **Checks are tools, not keyword counts.** A verification step runs something that can fail (tests, scanners, the gate). `grep -c` of a word proves nothing.
|
||||
5. **No AI-isms** in docs and skills (no em-dash runs, no "robust/seamless/leverage").
|
||||
6. **Vendored skills are copied 1:1** with their license, and recorded in `skills/NOTICE.md` (origin, commit, license).
|
||||
Workflows, skills, or the QA harness. All changes should keep the same rules the workflows themselves enforce:
|
||||
|
||||
1. **Grounded.** Every claim in a workflow or README must trace back to a real repository fact. No fabricated commands, examples, or badges.
|
||||
2. **Verifiable phases.** A phase is not done until it has a verification step (a command that proves the work). Add one if you touch a workflow.
|
||||
3. **No AI-isms.** Docs and workflows must pass the avoid-ai-writing check (no em-dash runs, no "robust/seamless/leverage" filler).
|
||||
|
||||
## What not to change
|
||||
|
||||
- The QA harness must keep its meta-tests green. If you change scoring, re-run `qa/test_tests.py` and update `qa/snapshots/baseline-final.json`.
|
||||
- Do not remove skills referenced by workflows without updating the workflows and `docs/SKILLS.md` together.
|
||||
|
||||
## Local checks before a PR
|
||||
|
||||
```bash
|
||||
python3 qa/lint_skills.py
|
||||
python3 qa/test_gate.py
|
||||
claude plugin validate --strict .claude-plugin/plugin.json
|
||||
cd qa
|
||||
python3 wf_quality_harness.py # score must not drop below baseline
|
||||
python3 test_quality.py # gates green
|
||||
python3 test_tests.py # meta-tests green
|
||||
```
|
||||
|
||||
A new skill referenced by a workflow goes into its `metadata.dev-workflows.uses` (vendored) or
|
||||
`external` (listed in `docs/SKILLS.md`); the lint checks both.
|
||||
|
||||
## Process
|
||||
|
||||
1. Fork and branch: `git checkout -b feat/your-change`.
|
||||
2. Make the change and run the checks above.
|
||||
3. Open a PR saying what changed and why.
|
||||
1. Fork the repo.
|
||||
2. Create a branch: `git checkout -b feat/your-change`.
|
||||
3. Make the change. Keep it small and focused.
|
||||
4. Run the qa checks above.
|
||||
5. Commit with a clear message.
|
||||
6. Open a PR describing what changed and why.
|
||||
|
||||
Questions or ideas: open an issue.
|
||||
@@ -1,209 +1,176 @@
|
||||
<div align="center">
|
||||
|
||||
# Dev Workflows
|
||||
<img src="assets/readme/hero.svg" width="100%" alt="dev-workflows: agent workflows where every phase ends in a verification step, shown as the wf-security-review SKILL.md checklist from SCOPE to REPORT + GATE">
|
||||
|
||||
**A proportional development workflow for coding agents.**
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Claude Code plugin · OpenCode and OMP adapters · Hermes skills-only installation
|
||||
[Issues](https://git.evandro.dev.br/evandro/dev-workflows/issues) · [Skills inventory](docs/SKILLS.md) · [Vendored skills notice](skills/NOTICE.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
[](LICENSE)
|
||||
[](https://agentskills.io)
|
||||
[](.github/workflows/ci.yml)
|
||||
|
||||
[Português (Brasil)](README.pt-BR.md)
|
||||
|
||||
[Overview](#overview) · [Install](#install) · [Daily workflow](#daily-workflow) ·
|
||||
[Goal loop](#optional-goal-driven-loop) · [Model routing](#model-catalog-and-routing) ·
|
||||
[Safety](#safety-and-limitations)
|
||||
**English** · [Português (BR)](README.pt-BR.md)
|
||||
|
||||
</div>
|
||||
|
||||
## Overview
|
||||
Harness output after installing the workflows and the vendored skills in the Hermes layout (real run; `impeccable` is a third-party skill that was not installed, so the harness exits 1):
|
||||
|
||||
Agents can write code quickly; deciding **how much process a change needs** and **whether it was actually verified** is harder. Dev Workflows supplies a small workflow router, least-privilege agents, and a deterministic quality gate. Two separate command-line tools add model discovery and goal-driven iteration when you explicitly use them.
|
||||
```console
|
||||
$ cd qa && python wf_quality_harness.py
|
||||
wf-frontend score= 385.5 refs=18 sem_match=['impeccable'] fases_sem_verif=[]
|
||||
wf-backend score= 424.0 refs=15 sem_match=[] fases_sem_verif=[]
|
||||
wf-architecture score= 413.7 refs=11 sem_match=[] fases_sem_verif=[]
|
||||
wf-security-review score= 390.0 refs=14 sem_match=[] fases_sem_verif=[]
|
||||
|
||||
| Capability | What it does | How it starts |
|
||||
TOTAL: 1613.2 / 400.0 (403.3%)
|
||||
```
|
||||
|
||||
## What it is
|
||||
|
||||
Five agent workflows written as standard `SKILL.md` files. The `description` in each frontmatter is the trigger, so the workflow loads when the agent's context matches it. The workflow bodies are in Portuguese.
|
||||
|
||||
| Workflow | File | Phases | Covers |
|
||||
|---|---|---|---|
|
||||
| Frontend / UI | [`wf-frontend.md`](workflows/wf-frontend.md) | DESIGN, BUILD, REVIEW | Design direction before code, anti-AI-slop, React performance, visual QA for PDFs |
|
||||
| Backend / API | [`wf-backend.md`](workflows/wf-backend.md) | PLAN, BUILD, SECURITY, TEST + BUG HUNT | Contract before handler, safe defaults, OWASP audit, tests and adversarial bug hunt |
|
||||
| Architecture | [`wf-architecture.md`](workflows/wf-architecture.md) | REQUIREMENTS, DESIGN, DOCS, REVIEW | Measurable requirements, C4 diagrams, ADRs, threat modeling |
|
||||
| Security review | [`wf-security-review.md`](workflows/wf-security-review.md) | SCOPE, HUNT, CONSULT, CODE REVIEW, REPORT + GATE | Front and back code review, known CVEs and a live lookup of new ones |
|
||||
| README | [`wf-readme.md`](workflows/wf-readme.md) | SCAN, CLASSIFY, AUDIT or DRAFT, GENERATE, VERIFY + VALIDATE | Create, audit or rewrite READMEs grounded in real repo facts |
|
||||
|
||||
The repository also vendors ten security skills under [`skills/`](skills/NOTICE.md) and ships a QA harness under [`qa/`](qa).
|
||||
|
||||
## Why use it
|
||||
|
||||
Agents generate code well and admit ignorance badly. Each workflow makes the agent prove the work at every phase:
|
||||
|
||||
- **Verification steps:** run the build, run the tests, hit the API, consult the registry.
|
||||
- **Acceptance criteria:** checkbox gates that must be marked before a phase closes.
|
||||
- **Hard rules:** "never merge with a red build", "never claim 'no CVE' without querying OSV.dev".
|
||||
|
||||
Do not use it if you want self-contained skills. The frontend, backend, architecture and README workflows load third-party skills that are not in this repository, the security workflow also expects two local skills that are not vendored, and the QA harness only reads the Hermes Agent layout.
|
||||
|
||||
## How it works
|
||||
|
||||
The security workflow is built on one fact: a model's knowledge of CVEs stops at its training date, so "no vulnerabilities" from memory is a guess.
|
||||
|
||||
1. **SCOPE** defines the target before auditing.
|
||||
2. **HUNT** checks known CVEs in the dependencies (`safedeps` queries OSV.dev for a `package@version`).
|
||||
3. **CONSULT** looks up new vulnerabilities in live sources: OSV.dev, GitHub Advisory DB and NVD.
|
||||
4. **CODE REVIEW** reviews front and back code against OWASP Top 10 and ASVS.
|
||||
5. **REPORT + GATE** delivers the report with sources and date stamped, then decides.
|
||||
|
||||
The QA harness scores `wf-frontend`, `wf-backend`, `wf-architecture` and `wf-security-review` (not `wf-readme`) on 30+ static criteria extracted from each `SKILL.md`: structure, numbered steps per phase, verification verbs, referenced skills and AI-isms density. `test_tests.py` holds meta-tests that break the harness on purpose and check that the score changes.
|
||||
|
||||
<details>
|
||||
<summary>Skills the workflows load</summary>
|
||||
|
||||
Vendored in `skills/` (security review). Origin, commit and license of each copy are in [`skills/NOTICE.md`](skills/NOTICE.md); CC-BY-4.0 requires attribution, kept in that file.
|
||||
|
||||
| Skill | What it does | Origin | License |
|
||||
|---|---|---|---|
|
||||
| `safedeps` | Queries OSV.dev live: CVE, severity and fixed version for a `package@version` (script `check_deps.py`) | [Jeneidi/safedeps](https://github.com/Jeneidi/safedeps) | MIT |
|
||||
| `sca-audit` | Dependency supply-chain audit with reachability analysis and CWE mapping | [OWASP/secure-agent-playbook](https://github.com/OWASP/secure-agent-playbook) | CC-BY-4.0 |
|
||||
| `code-review-security` | Systematic security code review mapped to OWASP Top 10 + ASVS | OWASP/secure-agent-playbook | CC-BY-4.0 |
|
||||
| `secrets-scan` | Hardcoded credentials and API keys in code and git history | OWASP/secure-agent-playbook | CC-BY-4.0 |
|
||||
| `api-security-review` | API review against OWASP API Security Top 10 | OWASP/secure-agent-playbook | CC-BY-4.0 |
|
||||
| `web-security-review` | Web app review against OWASP Top 10 | OWASP/secure-agent-playbook | CC-BY-4.0 |
|
||||
| `cve-triage` | Prioritizes CVEs with CVSS 4.0, EPSS and CISA KEV | [UnitOneAI/SecuritySkills](https://github.com/UnitOneAI/SecuritySkills) | MIT |
|
||||
| `patch-prioritization` | Decides remediation order | UnitOneAI/SecuritySkills | MIT |
|
||||
| `dependency-scanning` | Dependency tree scanning | UnitOneAI/SecuritySkills | MIT |
|
||||
| `hermaguard` | Adversarial review: static pre-scan plus 3 specialist agents (edge cases, attack, blast radius) | [Sahil-SS9/hermaguard](https://github.com/Sahil-SS9/hermaguard) | MIT |
|
||||
|
||||
Not vendored (install separately; full inventory in [`docs/SKILLS.md`](docs/SKILLS.md)):
|
||||
|
||||
| Workflow | Skill | Origin |
|
||||
|---|---|---|
|
||||
| **Development workflow** | Chooses a tier (0–3), loads relevant skills/agents, and checks the current diff. | Install the plugin or skills. |
|
||||
| **Model catalog** | Shows exact provider IDs, account eligibility, evidence, scores, and reference prices without invoking a model. | Run `scripts/model_catalog.py`. |
|
||||
| **Goal loop** | Measures an application goal, tests isolated changes, and retains only verified improvements. | Configure the project and explicitly run `scripts/goal_loop.py`. |
|
||||
| Frontend | `frontend-design` | [anthropics/skills](https://github.com/anthropics/skills) |
|
||||
| Frontend | `web-design` | [KAOPU-XiaoPu/web-design](https://github.com/KAOPU-XiaoPu/web-design) |
|
||||
| Frontend | `vercel-react-best-practices`, `vercel-composition-patterns` | [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) |
|
||||
| Frontend | `animate` | [emilkowalski/skill](https://github.com/emilkowalski/skill) |
|
||||
| Frontend | `impeccable` | [pbakaus/impeccable](https://github.com/pbakaus/impeccable) |
|
||||
| Frontend | `avoid-ai-writing` | [conorbronsdon/avoid-ai-writing](https://github.com/conorbronsdon/avoid-ai-writing) |
|
||||
| Frontend | `anti-ai-slop` | local skill, not published here |
|
||||
| Backend | `secure-coding` | [securityreviewai/secure-coding-skill](https://github.com/securityreviewai/secure-coding-skill) |
|
||||
| Backend | `bola-detector`, `auth-rbac-scaffold`, `injection-checker`, `openapi-hardener`, `api-security-review`, `security-test-generator` | [apisec-inc/apisec-skills](https://github.com/apisec-inc/apisec-skills) |
|
||||
| Architecture | `system-design`, `c4-architecture`, `isaqb-architecture-governance`, `secure-architecture-governance` | [Kotivskyi/architecture-governance-skills](https://github.com/Kotivskyi/architecture-governance-skills) |
|
||||
| README | `readme-crafter` | [linhai0872/readme-crafter-skill](https://github.com/linhai0872/readme-crafter-skill) |
|
||||
| README | `good-readme` | [adewale/good-readme](https://github.com/adewale/good-readme) |
|
||||
| README | `curating-readme` | [liang-senbei/curating-readme](https://github.com/liang-senbei/curating-readme) |
|
||||
| Security review | `dependency-cve-audit`, `security-threat-model` | local skills, not published here |
|
||||
|
||||
**Installing the plugin does not start the goal loop, buy model access, or alter your project.** The model's coding-benchmark score and your application's goal score are different measurements.
|
||||
</details>
|
||||
|
||||
## Install
|
||||
|
||||
Clone the repository, then choose the runtime you use:
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
git clone https://github.com/evandrodevbr/dev-workflows
|
||||
cd dev-workflows
|
||||
# 1. Copy the workflows into your agent's skills directory
|
||||
|
||||
python3 scripts/install.py claude # Claude Code: plugin, agents, skills, hooks
|
||||
python3 scripts/install.py opencode # OpenCode: plugin, agents, skills
|
||||
python3 scripts/install.py omp # OMP: native package, agents, skills
|
||||
python3 scripts/install.py hermes # Hermes Agent: skills only
|
||||
# Hermes Agent
|
||||
cp workflows/*.md ~/.hermes/skills/software-development/
|
||||
|
||||
# Claude Code: each skill needs its own directory (a loose .md is not loaded)
|
||||
mkdir -p ~/.claude/skills
|
||||
for f in workflows/*.md; do
|
||||
name=$(basename "$f" .md)
|
||||
mkdir -p "$HOME/.claude/skills/$name"
|
||||
cp "$f" "$HOME/.claude/skills/$name/SKILL.md"
|
||||
done
|
||||
|
||||
# 2. Security review: the supporting skills are already vendored in skills/.
|
||||
# Hermes:
|
||||
cp -r skills/* ~/.hermes/skills/
|
||||
# Claude Code: copy each skills/<name>/ directory to ~/.claude/skills/<name>/
|
||||
# (the SKILL.md must sit inside its own directory, as in step 1).
|
||||
|
||||
# 3. Use it
|
||||
# "review this PR for security" -> wf-security-review loads
|
||||
# "build a new page" -> wf-frontend loads
|
||||
```
|
||||
|
||||
For Claude Code, you can instead install from its marketplace without cloning:
|
||||
|
||||
```text
|
||||
/plugin marketplace add evandrodevbr/dev-workflows
|
||||
/plugin install dev-workflows@dev-workflows
|
||||
```
|
||||
|
||||
| Runtime | Installation result | Gate behavior |
|
||||
|---|---|---|
|
||||
| Claude Code | Plugin with seven agents, skills, and Claude hooks | `Stop` hook blocks an unverified completion. |
|
||||
| OpenCode | Plugin and deny-by-default subagents | Its end-of-turn hook warns; the external goal loop enforces the gate itself. |
|
||||
| OMP | Linked native plugin with seven agents and skills | No copied Claude hooks; the external goal loop enforces the gate. |
|
||||
| Hermes Agent | Skills only | Run the gate manually; there is no Hermes goal-loop executor. |
|
||||
|
||||
OpenCode and Hermes copies refer to this checkout; keep it in place or reinstall after moving it. The generated OMP package is self-contained. See the [skill inventory](docs/SKILLS.md) for vendored sources and optional external skills.
|
||||
|
||||
## Daily workflow
|
||||
|
||||
The `dev-router` skill selects a tier and `lean-code` favors reuse, deletion, and the smallest justified change. Higher-risk work raises the tier; authentication, money, personal data, uploads, raw SQL, shell execution, schema migrations, new dependencies, and LLM integrations are explicit triggers.
|
||||
|
||||
| Tier | Typical change | Minimum workflow |
|
||||
|---|---|---|
|
||||
| **0 — trivial** | No behavior change | Make the change; run the tier-0 gate. |
|
||||
| **1 — small** | Localized behavior change, no new public surface | Reproduce or characterize behavior, test the change, run tier 1. |
|
||||
| **2 — feature** | New endpoint/screen, contract change, or cross-file feature | Plan briefly, test, review independently, run tier 2. |
|
||||
| **3 — high risk** | Architecture, existing data, migration, or release | Approve a spec/ADR, review and audit, run tier 3. |
|
||||
|
||||
The [quality gate](scripts/quality-gate) runs ordinary project tools against the current diff, including the checks required for the chosen tier. **`pass` is evidence; missing tools are `incomplete`, not silently green.** Project-specific commands and checks live in `.dev-workflows.toml`; see the [gate reference](skills/dev-router/references/quality-gate.md). Claude's `Stop`, `PreToolUse`, and `PostToolUse` hooks preserve gate, dangerous-command, and formatting behavior. `DW_GATE=off` disables Claude's `Stop` hook for one session; it does **not** override the external goal loop's gate.
|
||||
|
||||
Optional frontend, backend, architecture, security, and documentation skills are listed with their origins and installation instructions in [docs/SKILLS.md](docs/SKILLS.md). Workflows can operate without those external skills.
|
||||
|
||||
## Optional goal-driven loop
|
||||
|
||||
The loop is an **external controller**, not an agent promise to keep trying. It runs only on a clean Git checkout, measures a baseline, and commits each candidate inside a detached attempt worktree. The gate and evaluator run in separate fresh worktrees made from that exact candidate commit, so agent-created ignored files cannot inflate the accepted score. It stops at the target or an explicit iteration, time, stagnation, or authorization limit. Your original branch is not merged, reset, or published.
|
||||
|
||||
### 1. Prepare the project
|
||||
|
||||
You need Python 3.11+, Git, Linux `bubblewrap`, the selected agent CLI and its account, the project's gate tools, and an evaluator maintained **outside** the editable checkout. The evaluator must write JSON to stdout with `schema_version: 1`, the configured metric/value/unit/direction, passing `checks`, and evidence files under `DW_REPORT_DIR`. Its approved SHA-256 is checked before execution. See the [evaluator contract](docs/goal-loop-model-routing-spec.md#4-contratos-de-dados-e-interface-proposta).
|
||||
|
||||
Keep your project's existing gate `[commands]` and add the following to its `.dev-workflows.toml`. This example uses an explicitly chosen, **unranked** Claude subscription alias; it does not invent an Artificial Analysis score or a per-token subscription price.
|
||||
|
||||
```toml
|
||||
[loop]
|
||||
enabled = false # set true only when you authorize a run
|
||||
harness = "claude"
|
||||
tier = 1 # localized change; raise the tier for larger/riskier work
|
||||
max_iterations = 3
|
||||
max_minutes = 30
|
||||
max_stagnant = 2
|
||||
min_improvement = 1
|
||||
max_paid_usd = 0
|
||||
|
||||
[loop.goal]
|
||||
name = "app_quality"
|
||||
unit = "points"
|
||||
direction = "maximize"
|
||||
target = 80
|
||||
|
||||
[loop.evaluator]
|
||||
argv = ["python3", "/absolute/outside/checkout/evaluate.py"]
|
||||
trusted_sha256 = "<paste the evaluator's 64-character SHA-256>"
|
||||
required_checks = ["test", "lint"]
|
||||
|
||||
[models.roles.implementer]
|
||||
model_key = "sonnet"
|
||||
billing_mode = "claude_subscription"
|
||||
manual_unranked_approval = true
|
||||
```
|
||||
|
||||
Replace the path, digest, goal, and limits with values you have reviewed. Confirm the digest with `sha256sum /absolute/outside/checkout/evaluate.py`; required checks must be configured and available in the app. A higher tier can require additional agent roles and gate tools. **The example is not ready to run until its placeholders are replaced and `enabled` is set deliberately.**
|
||||
|
||||
### 2. Plan, run, inspect
|
||||
|
||||
From the **app checkout** (not this repository), set `DW` to this repository's absolute path:
|
||||
The remaining skills install through `npx skills` (agentskills.io format), for example:
|
||||
|
||||
```bash
|
||||
DW=/absolute/path/to/dev-workflows
|
||||
python3 "$DW/scripts/goal_loop.py" plan --config .dev-workflows.toml
|
||||
|
||||
# Review the plan; then set enabled = true and commit the app config.
|
||||
python3 "$DW/scripts/goal_loop.py" run --config .dev-workflows.toml
|
||||
|
||||
# Copy run_id from the run's JSON output into RUN_ID.
|
||||
RUN_ID=your-run-id
|
||||
python3 "$DW/scripts/goal_loop.py" status --config .dev-workflows.toml --run "$RUN_ID"
|
||||
python3 "$DW/scripts/goal_loop.py" resume --config .dev-workflows.toml --run "$RUN_ID"
|
||||
python3 "$DW/scripts/goal_loop.py" stop --config .dev-workflows.toml --run "$RUN_ID"
|
||||
npx skills add anthropics/skills --skill frontend-design
|
||||
npx skills add vercel-labs/agent-skills --skill vercel-react-best-practices
|
||||
npx skills add conorbronsdon/avoid-ai-writing
|
||||
npx skills add https://github.com/pbakaus/impeccable --skill impeccable
|
||||
```
|
||||
|
||||
`plan` preflights the evaluator, isolation, gate, and routes without calling an agent. `run` evaluates the same goal from baseline through each attempt; a current `pass` gate and required checks must precede acceptance. `status` reports baseline, best score, target, attempts, gate and evidence. `resume` rechecks the configuration, evaluator, gate, base, and model route; a paid call with unreported usage cannot be resumed automatically. `stop` prevents subsequent attempts rather than killing an in-flight call.
|
||||
> [!WARNING]
|
||||
> Install layout matters. Claude Code ignores a loose `.md`; it needs `<skill>/SKILL.md`. The QA harness has the same requirement: it reads `~/.hermes/skills/software-development/<workflow>/SKILL.md`, so the flat `cp` used for Hermes above is not enough to run it.
|
||||
|
||||
State and evidence live outside the app at `$XDG_STATE_HOME/dev-workflows` (by default `~/.local/state/dev-workflows`). Inspect the winning worktree and commit before **you** decide whether to apply its diff. The controller never applies or publishes it for you.
|
||||
|
||||
## Model catalog and routing
|
||||
|
||||
Catalog commands inspect sources and local account evidence; they **do not dispatch models**:
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Run these from the dev-workflows repository.
|
||||
python3 scripts/model_catalog.py sync --tool opencode --providers opencode,opencode-go
|
||||
python3 scripts/model_catalog.py list --tool opencode --show-missing --format csv
|
||||
python3 scripts/model_catalog.py route --tool opencode --role planner \
|
||||
--providers opencode,opencode-go --explain
|
||||
|
||||
# For configured OMP providers, discover their local models instead:
|
||||
python3 scripts/model_catalog.py sync --tool omp --providers your-provider
|
||||
cd qa
|
||||
python3 wf_quality_harness.py # score each workflow on 30+ criteria
|
||||
python3 test_quality.py # gates: no regression, referenced skills exist, every phase verified
|
||||
python3 test_tests.py # meta-tests: corrupt harness/workflows, confirm the score drops
|
||||
```
|
||||
|
||||
| Evidence | Source | Meaning |
|
||||
|---|---|---|
|
||||
| Model IDs and variants | Public [Zen](https://opencode.ai/zen/v1/models) and [Go](https://opencode.ai/zen/go/v1/models) catalogs; local tool discovery | A public listing or local login alone **does not prove** entitlement. Zen and Go are distinct billing routes. |
|
||||
| Comparative prices | [OpenRouter Models API](https://openrouter.ai/api/v1/models) | USD/token, displayed in USD/1M for matched IDs; published long-context overrides affect reference costs. These are **not** Zen, Go, or Claude billing. Missing cache components are `n/d`, never zero. |
|
||||
| Coding scores | Authorized, attributed [Artificial Analysis Coding Agent Index](https://artificialanalysis.ai/agents/coding-agents) v1.5 snapshot | Manual audited import only; exact scores require a matching runtime harness and model effort. Cross-harness scores require explicit proxy approval. No automatic scraping or borrowed Intelligence Index scores. |
|
||||
| Eligibility and native cost | Your audited account evidence, native tariffs, balances, and Go quotas | Required for paid/eligible routing; missing or stale evidence blocks automatic selection. |
|
||||
Final scores per workflow are versioned in [`qa/snapshots/baseline-final.json`](qa/snapshots/baseline-final.json): `wf-frontend` 385.5, `wf-backend` 424.0, `wf-architecture` 413.7, `wf-security-review` 390.0, total 1613.2. The run above reproduces them.
|
||||
|
||||
Fixed coding-score bands are **light `[10,20)`**, **medium `[20,50)`**, and **high `[50,100]`**. The router filters by tool, provider, account, evidence, role, and budget, then compares real native cost inside the best-score plateau; OpenRouter prices remain reference only. An empty band stays empty rather than acquiring a fabricated score. A manually fixed Claude alias is labeled `unranked`.
|
||||
## Project status
|
||||
|
||||
To supply your own authorized data, `sync` accepts `--aa-snapshot`, `--availability-snapshot`, and `--price-overrides` (including `native_tariffs`). See the [validated input contracts](docs/goal-loop-model-routing-spec.md#4-contratos-de-dados-e-interface-proposta). A paid `route` also needs a task token profile (`--tokens`), explicit providers, and an appropriate `--budget-usd`; routing alone never makes a paid call.
|
||||
Six commits between 2026-08-19 and 2026-10-13; every workflow declares `version: 1.0.0`. No root CI: `skills/hermaguard/.github/workflows/ci.yml` belongs to the vendored hermaguard and does not run from this repository.
|
||||
|
||||
### Reproducible public snapshot
|
||||
- The harness reads installed copies from `~/.hermes/skills/software-development/<name>/SKILL.md` (hardcoded `SKILLS_DIR`), not the files in `workflows/`. Without them it reports `SKILL.md não encontrado`.
|
||||
- The harness exits 1 when a referenced skill is missing, and `test_quality.py` then stops with `HARNESS FALHOU`. Even with every skill installed, gate G3 asks each workflow to reach twice its own final baseline, so `test_quality.py` cannot pass against the current snapshot.
|
||||
- `test_tests.py` mutates `wf-frontend/SKILL.md` at the repository root, which does not exist, so that meta-test does nothing; the anti-slop mutation still runs.
|
||||
- The "referenced skills exist" check also accepts names from a hardcoded list without checking the disk.
|
||||
- `wf-security-review` mentions `dependency-cve-audit` and `security-threat-model` as local skills that are not vendored, and calls `~/.hermes/skills/safedeps/check_deps.py`, so it assumes the Hermes layout.
|
||||
- Two different skills share the name `api-security-review`: the vendored OWASP one and the apisec-inc one used by the backend workflow. Installing both into the same directory makes one overwrite the other.
|
||||
- In `docs/SKILLS.md`, the Claude Code snippet quotes the tilde (`"~/.claude/skills/$nome"`), which creates a literal `~` directory; use `$HOME` as in the Quickstart above.
|
||||
- `wf-readme` is not covered by the harness.
|
||||
|
||||
The [captured CSV](docs/model-catalog-zen-go-2026-09-25.csv) contains **122 public models** from 2026-09-25 07:18:38 UTC: 80 Zen, 42 Go, eight curated OpenRouter price matches, **0/122 licensed AA scores imported**, and no locally confirmed account eligibility. It is a catalog, **not** a recommendation or invoice. Reproduce its exact bytes offline from the [archived normalized JSON snapshot](docs/model-catalog-snapshot-2026-09-25/catalog-opencode.json):
|
||||
|
||||
```bash
|
||||
python3 scripts/model_catalog.py list --tool opencode --show-missing --format csv \
|
||||
--cache-dir docs/model-catalog-snapshot-2026-09-25 \
|
||||
--as-of-utc 2026-09-25T07:18:38Z
|
||||
```
|
||||
|
||||
The CSV output SHA-256 is `80e2930877b20e5c47d4fd18f394c17a4683b257ca715496e91c63520d528230`; the archived JSON SHA-256 is `a5a1ec44587976a9fe96399bb275662ccdf90e1d8149ac7857f03ae05bb83dcc`. A new `sync` obtains **current** catalogs and may yield different rows or prices. With no authorized benchmark or confirmed account evidence, `route` reports no eligible scored model rather than pretending one is available.
|
||||
|
||||
## Safety and limitations
|
||||
|
||||
- **No implicit spending.** Zen and pay-as-you-go routes require an explicit task budget; dispatch needs recent audited balance and native prices, a nonzero cap, a task estimate, and interactive authorization before each call. Go checks the three per-model quota windows (5h, weekly, monthly) and does not silently fall back to paid Zen balance. Unknown real usage pauses the loop and blocks automatic resume; an estimate is **not a hard billing limit**. Claude subscriptions are not converted into fictional USD/token costs.
|
||||
- **Isolation has boundaries.** The evaluator and quality gate run under Linux `bubblewrap` without network access. The agent CLI can edit its isolated worktree but its `.git` pointer is mounted read-only and rechecked before Git writes; it retains network access for providers and can read host files. This is **not** provider-only egress isolation. Add stronger isolation before using sensitive code or data.
|
||||
- **Evidence beats agent output.** Missing tools, failed or stale gates, invalid evaluator results, changed protected files, and unavailable models do not count as success. No install, merge, publish, destructive Git operation, or cleanup of winning worktrees is performed by the loop.
|
||||
- **No measured quality uplift claim.** The CLI was exercised on a disposable app with a fake Claude CLI and fake scanner: the app score moved from **0 to 2 in two gate-approved iterations**, without paid inference or a real account entitlement check. Whether the kit improves agent-produced code across projects still needs an A/B benchmark; public [SkillsBench](https://arxiv.org/abs/2602.12670) and [SWE-Skills-Bench](https://arxiv.org/abs/2603.15401) results show skills are not automatically beneficial.
|
||||
|
||||
## Verify this repository
|
||||
|
||||
```bash
|
||||
python3 qa/lint_skills.py
|
||||
python3 qa/test_gate.py
|
||||
python3 -m unittest discover -s qa -p 'test_*.py' -q
|
||||
claude plugin validate --strict .claude-plugin/plugin.json
|
||||
```
|
||||
|
||||
CI runs skill lint, gate/hook tests, and the behavioral suite on every PR. `python3 qa/check_upstream.py` checks vendored skills against upstream separately each month. See the [changelog](CHANGELOG.md) and the [full goal-loop specification](docs/goal-loop-model-routing-spec.md) for contracts, requirements, and limitations.
|
||||
|
||||
## Repository map
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `skills/`, `agents/`, `hooks/` | Canonical workflows, least-privilege agents, Claude hooks |
|
||||
| `scripts/quality-gate`, `scripts/install.py` | Deterministic verification and installation |
|
||||
| `scripts/model_catalog.py`, `scripts/model_router.py` | Provenance-aware inventory and per-role selection |
|
||||
| `scripts/goal_loop.py`, `scripts/harness_runner.py` | External controller and runtime invocation |
|
||||
| `adapters/opencode/`, `adapters/omp/` | Native integration packages |
|
||||
| `qa/`, `docs/` | Behavioral tests, source inventory, specification, captured data |
|
||||
Contribution rules are in [`CONTRIBUTING.md`](CONTRIBUTING.md): claims trace back to repo facts, every phase has a verification step, and docs avoid AI-isms.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE) © 2026 [Evandro Fonseca Junior](https://github.com/evandrodevbr). Vendored skills retain their own licenses; see [`skills/NOTICE.md`](skills/NOTICE.md).
|
||||
[MIT](LICENSE), (c) 2026 Evandro Fonseca Junior. Vendored skills keep their own licenses ([`skills/NOTICE.md`](skills/NOTICE.md)).
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>Made by <a href="https://evandro.dev.br">Evandro Fonseca Junior</a> · <a href="https://git.evandro.dev.br/evandro">git.evandro.dev.br</a></sub>
|
||||
</div>
|
||||
+137
-152
@@ -1,191 +1,176 @@
|
||||
<div align="center">
|
||||
|
||||
# Dev Workflows
|
||||
<img src="assets/readme/hero.svg" width="100%" alt="dev-workflows: workflows de agente em que toda fase termina num passo de verificação, mostrados como o checklist do SKILL.md wf-security-review de SCOPE a REPORT + GATE">
|
||||
|
||||
**Plugin do Claude Code (também para OpenCode e OMP; Hermes recebe só skills) que ajusta o processo ao tamanho do pedido**: um roteador escolhe o nível, agentes com permissão mínima fazem o trabalho e um gate de qualidade determinístico decide quando terminou.
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
[](LICENSE)
|
||||
[](https://agentskills.io)
|
||||
[](.github/workflows/ci.yml)
|
||||
[Issues](https://git.evandro.dev.br/evandro/dev-workflows/issues) · [Inventário de skills](docs/SKILLS.md) · [Aviso das skills vendorizadas](skills/NOTICE.md) · [Como contribuir](CONTRIBUTING.md)
|
||||
|
||||
**🇺🇸 [Read in English](README.md)**
|
||||
[English](README.md) · **Português (BR)**
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
Saída do harness depois de instalar os workflows e as skills vendorizadas no layout do Hermes (execução real; `impeccable` é uma skill de terceiros que não foi instalada, por isso o harness sai com código 1):
|
||||
|
||||
## Por quê
|
||||
```console
|
||||
$ cd qa && python wf_quality_harness.py
|
||||
wf-frontend score= 385.5 refs=18 sem_match=['impeccable'] fases_sem_verif=[]
|
||||
wf-backend score= 424.0 refs=15 sem_match=[] fases_sem_verif=[]
|
||||
wf-architecture score= 413.7 refs=11 sem_match=[] fases_sem_verif=[]
|
||||
wf-security-review score= 390.0 refs=14 sem_match=[] fases_sem_verif=[]
|
||||
|
||||
Agentes escrevem código rápido e erram em duas coisas: saber quando uma tarefa merece mais processo
|
||||
e admitir o que não verificaram. Este plugin cuida das duas:
|
||||
TOTAL: 1613.2 / 400.0 (403.3%)
|
||||
```
|
||||
|
||||
- **Processo proporcional ao pedido.** Typo não passa por revisão de design; mudança de auth passa.
|
||||
- **Prova, não afirmação.** "Pronto" exige um relatório do gate sobre o diff atual. Ferramenta não instalada aparece como *não verificado*, nunca como aprovada.
|
||||
- **Menos código.** Reaproveitar antes de escrever, apagar antes de somar, nenhuma dependência sem motivo.
|
||||
- **Segurança com fontes ao vivo.** O conhecimento de CVEs de um modelo para na data de treino, então as revisões consultam o OSV.dev e registram a data.
|
||||
## O que é
|
||||
|
||||
Cinco workflows de agente escritos como arquivos `SKILL.md` padrão. A `description` no frontmatter de cada um é o gatilho: o workflow carrega quando o contexto do agente bate com ela. O corpo dos workflows está em português.
|
||||
|
||||
| Workflow | Arquivo | Fases | Cobre |
|
||||
|---|---|---|---|
|
||||
| Frontend / UI | [`wf-frontend.md`](workflows/wf-frontend.md) | DESIGN, BUILD, REVIEW | Direção de design antes do código, anti-AI-slop, performance React, QA visual de PDFs |
|
||||
| Backend / API | [`wf-backend.md`](workflows/wf-backend.md) | PLAN, BUILD, SECURITY, TEST + BUG HUNT | Contrato antes do handler, defaults seguros, auditoria OWASP, testes e caça de bugs adversarial |
|
||||
| Arquitetura | [`wf-architecture.md`](workflows/wf-architecture.md) | REQUIREMENTS, DESIGN, DOCS, REVIEW | Requisitos mensuráveis, diagramas C4, ADRs, threat modeling |
|
||||
| Security review | [`wf-security-review.md`](workflows/wf-security-review.md) | SCOPE, HUNT, CONSULT, CODE REVIEW, REPORT + GATE | Code review de front e back, CVEs conhecidas e consulta ao vivo das novas |
|
||||
| README | [`wf-readme.md`](workflows/wf-readme.md) | SCAN, CLASSIFY, AUDIT ou DRAFT, GENERATE, VERIFY + VALIDATE | Criar, auditar ou reescrever READMEs com base em fatos reais do repositório |
|
||||
|
||||
O repositório também vendoriza dez skills de segurança em [`skills/`](skills/NOTICE.md) e traz um harness de QA em [`qa/`](qa).
|
||||
|
||||
## Por que usar
|
||||
|
||||
Agente gera código bem e admite que não sabe muito mal. Cada workflow obriga o agente a provar o trabalho em cada fase:
|
||||
|
||||
- **Passos de verificação:** rodar o build, rodar os testes, chamar a API, consultar o registry.
|
||||
- **Critérios de aceite:** checkboxes que precisam estar marcados para a fase fechar.
|
||||
- **Regras duras:** "nunca faça merge com build vermelho", "nunca afirme 'sem CVE' sem consultar o OSV.dev".
|
||||
|
||||
Não use se você quer skills autossuficientes. Os workflows de frontend, backend, arquitetura e README carregam skills de terceiros que não estão neste repositório, o de segurança também espera duas skills locais que não foram vendorizadas, e o harness de QA só lê o layout do Hermes Agent.
|
||||
|
||||
## Como funciona
|
||||
|
||||
O `dev-router` carrega primeiro e declara um nível:
|
||||
O workflow de segurança parte de um fato: o conhecimento de CVEs de um modelo para na data do treinamento, então "sem vulnerabilidades" vindo da memória é chute.
|
||||
|
||||
| Nível | Quando | O que roda |
|
||||
1. **SCOPE** define o alvo antes da auditoria.
|
||||
2. **HUNT** checa CVEs conhecidas nas dependências (`safedeps` consulta o OSV.dev para um `pacote@versão`).
|
||||
3. **CONSULT** busca vulnerabilidades novas em fontes vivas: OSV.dev, GitHub Advisory DB e NVD.
|
||||
4. **CODE REVIEW** revisa o código de front e back contra OWASP Top 10 e ASVS.
|
||||
5. **REPORT + GATE** entrega o relatório com fontes e data carimbadas e decide.
|
||||
|
||||
O harness de QA pontua `wf-frontend`, `wf-backend`, `wf-architecture` e `wf-security-review` (não o `wf-readme`) em mais de 30 critérios estáticos extraídos de cada `SKILL.md`: estrutura, passos numerados por fase, verbos de verificação, skills referenciadas e densidade de AI-isms. O `test_tests.py` tem meta-testes que quebram o harness de propósito e conferem se a nota muda.
|
||||
|
||||
<details>
|
||||
<summary>Skills que os workflows carregam</summary>
|
||||
|
||||
Vendorizadas em `skills/` (security review). Origem, commit e licença de cada cópia estão em [`skills/NOTICE.md`](skills/NOTICE.md); a CC-BY-4.0 exige atribuição, mantida nesse arquivo.
|
||||
|
||||
| Skill | O que faz | Origem | Licença |
|
||||
|---|---|---|---|
|
||||
| `safedeps` | Consulta o OSV.dev ao vivo: CVE, severidade e versão corrigida de um `pacote@versão` (script `check_deps.py`) | [Jeneidi/safedeps](https://github.com/Jeneidi/safedeps) | MIT |
|
||||
| `sca-audit` | Auditoria da cadeia de dependências com análise de alcançabilidade e mapeamento CWE | [OWASP/secure-agent-playbook](https://github.com/OWASP/secure-agent-playbook) | CC-BY-4.0 |
|
||||
| `code-review-security` | Code review de segurança sistemático mapeado para OWASP Top 10 + ASVS | OWASP/secure-agent-playbook | CC-BY-4.0 |
|
||||
| `secrets-scan` | Credenciais e chaves de API hardcoded no código e no histórico do git | OWASP/secure-agent-playbook | CC-BY-4.0 |
|
||||
| `api-security-review` | Revisão de API contra o OWASP API Security Top 10 | OWASP/secure-agent-playbook | CC-BY-4.0 |
|
||||
| `web-security-review` | Revisão de app web contra o OWASP Top 10 | OWASP/secure-agent-playbook | CC-BY-4.0 |
|
||||
| `cve-triage` | Prioriza CVEs com CVSS 4.0, EPSS e CISA KEV | [UnitOneAI/SecuritySkills](https://github.com/UnitOneAI/SecuritySkills) | MIT |
|
||||
| `patch-prioritization` | Decide a ordem de correção | UnitOneAI/SecuritySkills | MIT |
|
||||
| `dependency-scanning` | Varredura da árvore de dependências | UnitOneAI/SecuritySkills | MIT |
|
||||
| `hermaguard` | Revisão adversarial: pré-varredura estática mais 3 agentes especialistas (casos de borda, ataque, raio de impacto) | [Sahil-SS9/hermaguard](https://github.com/Sahil-SS9/hermaguard) | MIT |
|
||||
|
||||
Não vendorizadas (instale à parte; inventário completo em [`docs/SKILLS.md`](docs/SKILLS.md)):
|
||||
|
||||
| Workflow | Skill | Origem |
|
||||
|---|---|---|
|
||||
| 0, trivial | nenhum comportamento muda | a mudança + gate nível 0 |
|
||||
| 1, pequeno | 1 a 3 arquivos, sem superfície pública nova | `wf-bugfix` ou `wf-refactor`, teste de regressão, gate nível 1 |
|
||||
| 2, feature | endpoint ou tela nova, contrato alterado, dependência nova | exploração, plano curto, TDD, revisão independente, gate nível 2 |
|
||||
| 3, grande ou de risco | vários módulos, dados existentes, arquitetura, release | spec EARS + ADR aprovados por você, revisões, auditoria de segurança, gate nível 3 |
|
||||
| Frontend | `frontend-design` | [anthropics/skills](https://github.com/anthropics/skills) |
|
||||
| Frontend | `web-design` | [KAOPU-XiaoPu/web-design](https://github.com/KAOPU-XiaoPu/web-design) |
|
||||
| Frontend | `vercel-react-best-practices`, `vercel-composition-patterns` | [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) |
|
||||
| Frontend | `animate` | [emilkowalski/skill](https://github.com/emilkowalski/skill) |
|
||||
| Frontend | `impeccable` | [pbakaus/impeccable](https://github.com/pbakaus/impeccable) |
|
||||
| Frontend | `avoid-ai-writing` | [conorbronsdon/avoid-ai-writing](https://github.com/conorbronsdon/avoid-ai-writing) |
|
||||
| Frontend | `anti-ai-slop` | skill local, não publicada aqui |
|
||||
| Backend | `secure-coding` | [securityreviewai/secure-coding-skill](https://github.com/securityreviewai/secure-coding-skill) |
|
||||
| Backend | `bola-detector`, `auth-rbac-scaffold`, `injection-checker`, `openapi-hardener`, `api-security-review`, `security-test-generator` | [apisec-inc/apisec-skills](https://github.com/apisec-inc/apisec-skills) |
|
||||
| Arquitetura | `system-design`, `c4-architecture`, `isaqb-architecture-governance`, `secure-architecture-governance` | [Kotivskyi/architecture-governance-skills](https://github.com/Kotivskyi/architecture-governance-skills) |
|
||||
| README | `readme-crafter` | [linhai0872/readme-crafter-skill](https://github.com/linhai0872/readme-crafter-skill) |
|
||||
| README | `good-readme` | [adewale/good-readme](https://github.com/adewale/good-readme) |
|
||||
| README | `curating-readme` | [liang-senbei/curating-readme](https://github.com/liang-senbei/curating-readme) |
|
||||
| Security review | `dependency-cve-audit`, `security-threat-model` | skills locais, não publicadas aqui |
|
||||
|
||||
Gatilhos de segurança sobem o nível a qualquer momento: auth, dinheiro, dados pessoais, upload, SQL
|
||||
montado à mão, shell/exec, desserialização, migração de schema, dependência nova, feature que chama LLM.
|
||||
</details>
|
||||
|
||||
## O que tem dentro
|
||||
|
||||
| Tipo | Itens |
|
||||
|---|---|
|
||||
| Roteador e regras | `dev-router`, `lean-code` |
|
||||
| Workflows | `wf-bugfix`, `wf-refactor`, `wf-frontend`, `wf-backend`, `wf-architecture`, `wf-security-review`, `wf-readme` |
|
||||
| Agentes | `explorer` (haiku, só leitura), `planner` (opus, só leitura), `implementer` (sonnet), `reviewer` (opus, só leitura, vê só o diff), `security-auditor` (opus, só leitura), `ui-critic` (sonnet, só leitura), `verifier` (haiku, roda o gate) |
|
||||
| Hooks | `Stop`: bloqueia o "pronto" enquanto houver código alterado sem gate atual aprovado · `PreToolUse`: nega `--no-verify` e force-push, pergunta antes de `reset --hard`, `clean -f`, `DROP TABLE` · `PostToolUse`: formata o arquivo editado com o formatter do próprio projeto |
|
||||
| Gate de qualidade | [`scripts/quality-gate`](scripts/quality-gate): lint, typecheck, testes, gitleaks (nível 0); cobertura do diff, semgrep, código morto (nível 1); build, teste de mutação, osv-scanner, regras de arquitetura, duplicação (nível 2); trivy, rastreabilidade requisito→teste (nível 3) |
|
||||
| Skills vendorizadas | pilar de segurança (OWASP secure-agent-playbook, UnitOneAI SecuritySkills, safedeps, hermaguard) e taste-skill para frontend. Origem e licença: [`skills/NOTICE.md`](skills/NOTICE.md) |
|
||||
|
||||
O gate roda só as checagens do nível pedido, só sobre o diff, e não usa IA: são ferramentas comuns.
|
||||
Checagens que precisam do app de pé (orçamento do Lighthouse CI, Playwright + axe, schemathesis, k6)
|
||||
entram por projeto no `.dev-workflows.toml`. Referência:
|
||||
[`skills/dev-router/references/quality-gate.md`](skills/dev-router/references/quality-gate.md).
|
||||
|
||||
## Instalação
|
||||
## Início rápido
|
||||
|
||||
```bash
|
||||
git clone https://github.com/evandrodevbr/dev-workflows && cd dev-workflows
|
||||
# 1. Copie os workflows para o diretório de skills do seu agente
|
||||
|
||||
python3 scripts/install.py claude # plugin: skills + agentes + hooks
|
||||
python3 scripts/install.py opencode # skills, agentes e plugin em ~/.config/opencode
|
||||
python3 scripts/install.py hermes # só skills, em ~/.hermes/skills
|
||||
python3 scripts/install.py omp # pacote nativo OMP: skills + agentes (sem hooks Claude)
|
||||
# Hermes Agent
|
||||
cp workflows/*.md ~/.hermes/skills/software-development/
|
||||
|
||||
# Claude Code: cada skill precisa do próprio diretório (um .md solto não carrega)
|
||||
mkdir -p ~/.claude/skills
|
||||
for f in workflows/*.md; do
|
||||
name=$(basename "$f" .md)
|
||||
mkdir -p "$HOME/.claude/skills/$name"
|
||||
cp "$f" "$HOME/.claude/skills/$name/SKILL.md"
|
||||
done
|
||||
|
||||
# 2. Security review: as skills de apoio já estão vendorizadas em skills/.
|
||||
# Hermes:
|
||||
cp -r skills/* ~/.hermes/skills/
|
||||
# Claude Code: copie cada diretório skills/<nome>/ para ~/.claude/skills/<nome>/
|
||||
# (o SKILL.md precisa ficar dentro do próprio diretório, como no passo 1).
|
||||
|
||||
# 3. Use
|
||||
# "revise este PR para segurança" -> carrega o wf-security-review
|
||||
# "crie uma página nova" -> carrega o wf-frontend
|
||||
```
|
||||
|
||||
Claude Code sem clonar:
|
||||
|
||||
```text
|
||||
/plugin marketplace add evandrodevbr/dev-workflows
|
||||
/plugin install dev-workflows@dev-workflows
|
||||
```
|
||||
|
||||
Diferenças entre os agentes:
|
||||
|
||||
- **OpenCode** não consegue bloquear o fim do turno, então gate ausente ou falho vira aviso no log. Seus subagentes têm ferramentas negadas por padrão; o controlador externo verifica o gate por conta própria.
|
||||
- **OMP** usa plugin nativo com skills e sete agentes, sem hooks Claude. O controlador externo impõe o gate.
|
||||
- **Hermes** recebe só as skills; rode o gate você mesmo. Não existe executor Hermes para o loop.
|
||||
- No OpenCode e no Hermes as cópias apontam para o seu clone: mantenha-o no lugar (ou reinstale depois de mover). O pacote OMP é construído independentemente do clone.
|
||||
|
||||
Desligue o hook de `Stop` numa sessão com `DW_GATE=off`.
|
||||
|
||||
## Skills externas (opcionais)
|
||||
|
||||
Os workflows usam estas quando instaladas e seguem as próprias regras quando não. Comandos de
|
||||
instalação: [`docs/SKILLS.md`](docs/SKILLS.md).
|
||||
|
||||
| Área | Skills |
|
||||
|---|---|
|
||||
| Frontend | `frontend-design` (Anthropic), `impeccable` (pbakaus), `vercel-react-best-practices`, `vercel-composition-patterns` (Vercel Labs), `animate` (emilkowalski), `web-design`, `avoid-ai-writing` |
|
||||
| Backend | `secure-coding`, `bola-detector`, `auth-rbac-scaffold`, `injection-checker`, `openapi-hardener`, `security-test-generator` (apisec-inc) |
|
||||
| Arquitetura | `system-design`, `c4-architecture`, `isaqb-architecture-governance`, `secure-architecture-governance` (Kotivskyi) |
|
||||
| README | `readme-crafter`, `good-readme`, `curating-readme` |
|
||||
|
||||
## Qualidade deste repositório
|
||||
As demais skills se instalam com `npx skills` (formato agentskills.io), por exemplo:
|
||||
|
||||
```bash
|
||||
python3 qa/lint_skills.py # frontmatter, limites de tamanho, referências, links, caminhos de máquina
|
||||
python3 qa/test_gate.py # gate e hooks num repositório descartável
|
||||
python3 -m unittest discover -s qa -p 'test_*.py' -q # catálogo, roteador, adapters, loop
|
||||
python3 qa/check_upstream.py # skills vendorizadas atrás do upstream
|
||||
claude plugin validate --strict .claude-plugin/plugin.json
|
||||
npx skills add anthropics/skills --skill frontend-design
|
||||
npx skills add vercel-labs/agent-skills --skill vercel-react-best-practices
|
||||
npx skills add conorbronsdon/avoid-ai-writing
|
||||
npx skills add https://github.com/pbakaus/impeccable --skill impeccable
|
||||
```
|
||||
|
||||
O CI roda lint, testes do gate/hooks e a suíte comportamental em todo PR; a checagem de upstream roda todo mês.
|
||||
> [!WARNING]
|
||||
> O layout da instalação importa. O Claude Code ignora um `.md` solto; ele precisa de `<skill>/SKILL.md`. O harness de QA tem a mesma exigência: ele lê `~/.hermes/skills/software-development/<workflow>/SKILL.md`, então o `cp` plano usado acima para o Hermes não basta para rodá-lo.
|
||||
|
||||
**Ainda não medido:** se o kit melhora o código que um agente produz. Estudos públicos
|
||||
([SkillsBench](https://arxiv.org/abs/2602.12670), [SWE-Skills-Bench](https://arxiv.org/abs/2603.15401))
|
||||
mostram que skills também podem piorar o resultado, então o próximo passo é um benchmark A/B (com e
|
||||
sem o kit) no [Harbor](https://github.com/harbor-framework/harbor), com testes ocultos, score de
|
||||
mutação e checagens de segurança.
|
||||
|
||||
## Catálogo de modelos e loop de meta opt-in
|
||||
|
||||
A [spec de roteamento e loop](docs/goal-loop-model-routing-spec.md) está implementada em CLIs independentes; instalar um plugin **nunca inicia o loop**. Requisitos: Python 3.11+, Git, `bubblewrap` no Linux, CLI de agente suportada configurada, avaliador externo aprovado, checkout limpo e ferramentas do gate do projeto. Não rode em projetos sensíveis sem isolamento adicional: a CLI do agente pode acessar a rede e ler arquivos do host; **não** há sandbox de rede restrita só ao provider. Avaliador e gate rodam sem acesso à rede.
|
||||
|
||||
Para inspecionar modelos sem chamar nenhum deles (neste repositório):
|
||||
## Uso
|
||||
|
||||
```bash
|
||||
python3 scripts/model_catalog.py sync --tool opencode --providers opencode,opencode-go
|
||||
python3 scripts/model_catalog.py list --tool opencode --show-missing --format csv
|
||||
python3 scripts/model_catalog.py route --tool opencode --role planner --providers opencode,opencode-go --explain
|
||||
cd qa
|
||||
python3 wf_quality_harness.py # pontua cada workflow em mais de 30 critérios
|
||||
python3 test_quality.py # gates: sem regressão, skills referenciadas existem, toda fase verificada
|
||||
python3 test_tests.py # meta-testes: corrompem harness/workflows e conferem que a nota cai
|
||||
```
|
||||
|
||||
`sync` consulta fontes públicas [Zen](https://opencode.ai/zen/v1/models), [Go](https://opencode.ai/zen/go/v1/models), [OpenRouter](https://openrouter.ai/api/v1/models) e `opencode models <provider> --refresh` local; para OMP, passe `--tool omp --providers <provider-configurado>` para descobrir modelos locais. Lista, login armazenado ou tarifa OpenRouter **não comprovam** acesso da conta, cota Go, saldo Zen ou cobrança real. Rota pontuada elegível exige seus próprios `--availability-snapshot` e `--aa-snapshot` auditados; para cobranças/cotas nativas, `--price-overrides` com `native_tariffs`. Veja os [contratos](docs/goal-loop-model-routing-spec.md#4-contratos-de-dados-e-interface-proposta) e validadores de `scripts/model_catalog.py`. O Coding Agent Index v1.5 da AA é importado manualmente, com autorização e atribuição: sem scraping da leaderboard ou notas inventadas. Faixas fixas de nota de código: `[10,20)`, `[20,50)`, `[50,100]`; modelos inacessíveis ou sem nota não vencem uma rota pontuada. Preços OpenRouter em USD/1M são **comparativos**, não cobrança Zen/Go/Claude; cache ausente é `n/d`, não gratuito.
|
||||
Uma nota `exact` exige o mesmo harness e esforço de execução; outro harness só entra como `proxy` autorizado. No OMP, apenas esforços `thinking` anunciados pelo modelo local são elegíveis, inclusive IDs de modelo com `/`. Overrides públicos de contexto longo do OpenRouter entram no custo **referencial** do perfil de tokens.
|
||||
As notas finais de cada workflow estão versionadas em [`qa/snapshots/baseline-final.json`](qa/snapshots/baseline-final.json): `wf-frontend` 385.5, `wf-backend` 424.0, `wf-architecture` 413.7, `wf-security-review` 390.0, total 1613.2. A execução acima reproduz esses valores.
|
||||
|
||||
A [tabela CSV completa capturada](docs/model-catalog-zen-go-2026-09-25.csv) inclui **122 modelos públicos** (Zen 80, Go 42) em 2026-09-25 07:18:38 UTC: **0/122 notas AA licenciadas importadas**, oito pareamentos de preço OpenRouter curados e nenhum acesso de conta confirmado localmente nesta captura. Reproduza **exatamente estes bytes, sem rede**, usando o [snapshot normalizado arquivado](docs/model-catalog-snapshot-2026-09-25/catalog-opencode.json) com `python3 scripts/model_catalog.py list --tool opencode --show-missing --format csv --cache-dir docs/model-catalog-snapshot-2026-09-25 --as-of-utc 2026-09-25T07:18:38Z`; o SHA-256 da saída é `80e2930877b20e5c47d4fd18f394c17a4683b257ca715496e91c63520d528230`. Sem `--as-of-utc`, o relatório de obsolescência usa o horário atual. Rode `sync` e `list --format csv` para catálogos/preços *atuais*. Dados públicos não são fatura nem garantia de acesso.
|
||||
## Estado do projeto
|
||||
|
||||
No `.dev-workflows.toml` **do aplicativo**, preserve seus `[commands]` do gate e defina um avaliador independente mantido fora do checkout. O stdout deve ser JSON com `schema_version: 1`, métrica/valor/unidade/direção configuradas, `checks` aprovados e evidências geradas sob `DW_REPORT_DIR` (veja o [contrato do avaliador](docs/goal-loop-model-routing-spec.md#4-contratos-de-dados-e-interface-proposta)). Exemplo com assinatura Claude e alias fixo, deliberadamente **sem ranking**:
|
||||
Seis commits entre 2026-08-19 e 2026-10-13; todo workflow declara `version: 1.0.0`. Não há CI na raiz: `skills/hermaguard/.github/workflows/ci.yml` pertence ao hermaguard vendorizado e não roda a partir deste repositório.
|
||||
|
||||
```toml
|
||||
[loop]
|
||||
enabled = false # mude para true apenas quando decidir executar
|
||||
harness = "claude"
|
||||
tier = 1 # mudança de comportamento pequena; aumente para tarefas de risco
|
||||
max_iterations = 3
|
||||
max_minutes = 15
|
||||
max_stagnant = 2
|
||||
min_improvement = 1
|
||||
max_paid_usd = 0
|
||||
- O harness lê as cópias instaladas em `~/.hermes/skills/software-development/<nome>/SKILL.md` (`SKILLS_DIR` fixo no código), não os arquivos de `workflows/`. Sem elas, ele informa `SKILL.md não encontrado`.
|
||||
- O harness sai com código 1 quando falta uma skill referenciada, e aí o `test_quality.py` para com `HARNESS FALHOU`. Mesmo com todas as skills instaladas, o gate G3 exige que cada workflow chegue ao dobro da própria nota final, então o `test_quality.py` não passa contra o snapshot atual.
|
||||
- O `test_tests.py` altera `wf-frontend/SKILL.md` na raiz do repositório, arquivo que não existe, então esse meta-teste não faz nada; a mutação do anti-slop continua rodando.
|
||||
- A checagem de "skills referenciadas existem" também aceita nomes de uma lista fixa no código, sem olhar o disco.
|
||||
- O `wf-security-review` cita `dependency-cve-audit` e `security-threat-model` como skills locais que não foram vendorizadas e chama `~/.hermes/skills/safedeps/check_deps.py`, então pressupõe o layout do Hermes.
|
||||
- Duas skills diferentes têm o nome `api-security-review`: a da OWASP, vendorizada, e a da apisec-inc, usada pelo workflow de backend. Instalar as duas no mesmo diretório faz uma sobrescrever a outra.
|
||||
- Em `docs/SKILLS.md`, o trecho para o Claude Code coloca o til entre aspas (`"~/.claude/skills/$nome"`), o que cria um diretório `~` literal; use `$HOME` como no Início rápido acima.
|
||||
- O `wf-readme` não é coberto pelo harness.
|
||||
|
||||
[loop.goal]
|
||||
name = "app_quality"
|
||||
unit = "points"
|
||||
direction = "maximize"
|
||||
target = 80
|
||||
|
||||
[loop.evaluator]
|
||||
argv = ["python3", "/caminho/absoluto/fora/do/checkout/evaluate.py"]
|
||||
trusted_sha256 = "<cole os 64 caracteres do SHA-256 do avaliador>"
|
||||
required_checks = ["test", "lint"]
|
||||
|
||||
[models.roles.implementer]
|
||||
model_key = "sonnet"
|
||||
billing_mode = "claude_subscription"
|
||||
manual_unranked_approval = true
|
||||
```
|
||||
|
||||
Aprove o arquivo/hash do avaliador (`sha256sum /caminho/absoluto/fora/do/checkout/evaluate.py`), defina meta mensurável e comandos do gate. Depois, **num checkout limpo do app** (troque `DW` pelo caminho absoluto deste repositório):
|
||||
|
||||
```bash
|
||||
DW=/caminho/absoluto/para/dev-workflows
|
||||
python3 "$DW/scripts/goal_loop.py" plan --config .dev-workflows.toml
|
||||
# Mude enabled = true no app, faça commit dessa escolha deliberada e então:
|
||||
python3 "$DW/scripts/goal_loop.py" run --config .dev-workflows.toml
|
||||
python3 "$DW/scripts/goal_loop.py" status --config .dev-workflows.toml --run <run-id>
|
||||
python3 "$DW/scripts/goal_loop.py" resume --config .dev-workflows.toml --run <run-id>
|
||||
python3 "$DW/scripts/goal_loop.py" stop --config .dev-workflows.toml --run <run-id>
|
||||
```
|
||||
|
||||
`plan` não chama agentes; `run` mede o score inicial do app e tenta mudanças em worktrees separados. Cada tentativa cria um commit candidato detached; o gate e o avaliador executam em cópias limpas e separadas desse commit, sem arquivos ignorados criados pelo agente. Só aceita melhora após gate `pass` atual, checks obrigatórios e avaliação independente. Não faz merge, publicação ou exclusão da sua branch/worktrees; inspecione `status` e o worktree/commit vencedor antes de decidir aplicar. Estado/evidências ficam em `$XDG_STATE_HOME/dev-workflows` (padrão `~/.local/state/dev-workflows`). `resume` revalida configuração/avaliador/gate/base/rota; `stop` solicita parar novas tentativas, sem interromper imediatamente uma chamada em andamento. Zen/pay-as-you-go requer auditoria recente de conta/saldo, estimativa nativa, orçamento explícito e positivo **e** autorização interativa antes de cada chamada; consumo real desconhecido pausa o loop e impede retomada automática, pois estimativas não são limite rígido de cobrança. Go exige cotas recentes por modelo para 5h/semana/mês, sem fallback para saldo Zen. Assinatura Claude **não** recebe custo fictício USD/token nem nota AA emprestada do alias.
|
||||
|
||||
## Estrutura
|
||||
|
||||
```
|
||||
.claude-plugin/ plugin.json, marketplace.json
|
||||
skills/ dev-router, lean-code, workflows wf-*, skills vendorizadas (NOTICE.md)
|
||||
agents/ explorer, planner, implementer, reviewer, security-auditor, ui-critic, verifier
|
||||
hooks/hooks.json Stop, PreToolUse, PostToolUse
|
||||
scripts/ quality-gate, instalação, catálogo/roteador, loop, executores e hooks
|
||||
adapters/opencode/ plugin do OpenCode
|
||||
adapters/omp/ construtor do pacote nativo OMP
|
||||
qa/ lint, testes do gate e de integração, checagem de upstream
|
||||
docs/SKILLS.md inventário de skills e comandos de instalação
|
||||
```
|
||||
As regras de contribuição estão em [`CONTRIBUTING.md`](CONTRIBUTING.md): toda afirmação remete a fatos do repositório, toda fase tem um passo de verificação e a documentação evita AI-isms.
|
||||
|
||||
## Licença
|
||||
|
||||
[MIT](LICENSE) © 2026 [Evandro Fonseca Junior](https://github.com/evandrodevbr). As skills vendorizadas mantêm as próprias licenças ([`skills/NOTICE.md`](skills/NOTICE.md)).
|
||||
[MIT](LICENSE), (c) 2026 Evandro Fonseca Junior. As skills vendorizadas mantêm as próprias licenças ([`skills/NOTICE.md`](skills/NOTICE.md)).
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>Feito por <a href="https://evandro.dev.br">Evandro Fonseca Junior</a> · <a href="https://git.evandro.dev.br/evandro">git.evandro.dev.br</a></sub>
|
||||
</div>
|
||||
@@ -1,168 +0,0 @@
|
||||
"""Build the native OMP package from the canonical dev-workflows checkout."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
OWNER_MARKER = ".dev-workflows-omp-package"
|
||||
def _owner_value(src: Path) -> str:
|
||||
return f"dev-workflows\nsource_root={src}\n"
|
||||
|
||||
|
||||
def _content_digest(package: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
for path in sorted(package.rglob("*")):
|
||||
if path.is_symlink():
|
||||
raise FileExistsError(f"symlink em pacote gerado: {path}")
|
||||
if not path.is_file() or path.name == OWNER_MARKER:
|
||||
continue
|
||||
h.update(path.relative_to(package).as_posix().encode("utf-8"))
|
||||
h.update(b"\0")
|
||||
h.update(str(path.stat().st_mode & 0o777).encode("ascii"))
|
||||
with path.open("rb") as src:
|
||||
for chunk in iter(lambda: src.read(1024 * 1024), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
AGENT_NAMES = (
|
||||
"explorer", "implementer", "planner", "reviewer",
|
||||
"security-auditor", "ui-critic", "verifier",
|
||||
)
|
||||
MODEL_ID = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?::(?:none|minimal|low|medium|high|xhigh|max))?$")
|
||||
TOOL_NAMES = {
|
||||
"Read": "read", "Edit": "edit", "Write": "write", "Bash": "bash",
|
||||
"Grep": "grep", "Glob": "glob", "WebFetch": "web_search",
|
||||
}
|
||||
|
||||
|
||||
def _agent(src: Path, name: str, models: dict[str, str], package_root: Path) -> str:
|
||||
text = (src / "agents" / f"{name}.md").read_text(encoding="utf-8")
|
||||
match = re.match(r"\A---\s*\n(.*?)\n---\s*\n(.*)\Z", text, re.S)
|
||||
if not match:
|
||||
raise ValueError(f"frontmatter inválido em agents/{name}.md")
|
||||
raw, prompt = match.groups()
|
||||
meta: dict[str, str] = {}
|
||||
for line in raw.splitlines():
|
||||
key, sep, value = line.partition(":")
|
||||
if sep:
|
||||
meta[key.strip()] = value.strip()
|
||||
if meta.get("name") != name or not meta.get("description"):
|
||||
raise ValueError(f"metadados incompletos em agents/{name}.md")
|
||||
|
||||
source_tools = [item.strip() for item in meta.get("tools", "").split(",") if item.strip()]
|
||||
# An explicit OMP allowlist prevents read-only agents from inheriting write tools.
|
||||
tools = list(dict.fromkeys(TOOL_NAMES[item] for item in source_tools if item in TOOL_NAMES))
|
||||
if not tools:
|
||||
raise ValueError(f"nenhuma ferramenta OMP reconhecida em agents/{name}.md")
|
||||
lines = ["---", f"name: {name}", f"description: {meta['description']}", "tools: " + ", ".join(tools)]
|
||||
skills = re.search(r"^skills:\s*\[(.*?)\]\s*$", raw, re.M)
|
||||
if skills:
|
||||
values = [value.strip().strip("'\"") for value in skills.group(1).split(",") if value.strip()]
|
||||
if values:
|
||||
lines.append("autoloadSkills:")
|
||||
lines.extend(f" - {value}" for value in values)
|
||||
model = models.get(name)
|
||||
if model is not None:
|
||||
if not isinstance(model, str) or not MODEL_ID.fullmatch(model):
|
||||
raise ValueError(f"ID de modelo inválido para {name}: {model!r}")
|
||||
lines.append(f"model: {model}")
|
||||
body = prompt.replace("${CLAUDE_PLUGIN_ROOT}", str(package_root))
|
||||
return "\n".join(lines + ["---", "", body])
|
||||
|
||||
|
||||
def build_package(src_root, dest, models=None):
|
||||
"""Atomically build an owned OMP plugin package at *dest*.
|
||||
|
||||
``models`` maps canonical agent names (roles) to verified concrete
|
||||
``provider/model[:thinking]`` selectors. Missing roles intentionally omit
|
||||
``model`` so OMP inherits the parent model.
|
||||
"""
|
||||
src = Path(src_root).resolve()
|
||||
target = Path(dest).expanduser().absolute()
|
||||
roles = {} if models is None else dict(models)
|
||||
unknown = set(roles) - set(AGENT_NAMES)
|
||||
if unknown:
|
||||
raise ValueError("papéis desconhecidos: " + ", ".join(sorted(unknown)))
|
||||
if not (src / "agents").is_dir() or not (src / "skills").is_dir():
|
||||
raise ValueError(f"checkout canônico inválido: {src}")
|
||||
if target.is_symlink():
|
||||
raise FileExistsError(f"destino é symlink; recusando substituir: {target}")
|
||||
if target.exists():
|
||||
marker = target / OWNER_MARKER
|
||||
if not target.is_dir() or not marker.is_file():
|
||||
raise FileExistsError(f"destino existente não pertence a dev-workflows: {target}")
|
||||
if marker.read_text(encoding="utf-8") != _owner_value(src) + f"sha256={_content_digest(target)}\n":
|
||||
raise FileExistsError(f"pacote existente tem alterações manuais ou origem diferente: {target}")
|
||||
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
stage = Path(tempfile.mkdtemp(prefix=f".{target.name}.stage-", dir=target.parent))
|
||||
backup = None
|
||||
try:
|
||||
skills_src = src / "skills"
|
||||
skills_dest = stage / "skills"
|
||||
shutil.copytree(skills_src, skills_dest, symlinks=False,
|
||||
ignore=shutil.ignore_patterns(".git", "__pycache__", "*.pyc"))
|
||||
skill_count = sum(1 for path in skills_dest.glob("*/SKILL.md") if path.is_file())
|
||||
for path in skills_dest.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
content = path.read_text(encoding="utf-8")
|
||||
except UnicodeError:
|
||||
continue
|
||||
if "${CLAUDE_PLUGIN_ROOT}" in content:
|
||||
path.write_text(content.replace("${CLAUDE_PLUGIN_ROOT}", str(target)), encoding="utf-8")
|
||||
if skill_count != 21:
|
||||
raise ValueError(f"esperadas 21 skills canônicas; encontradas {skill_count}")
|
||||
|
||||
agents_dest = stage / "agents"
|
||||
agents_dest.mkdir()
|
||||
for name in AGENT_NAMES:
|
||||
if not (src / "agents" / f"{name}.md").is_file():
|
||||
raise ValueError(f"agente canônico ausente: {name}")
|
||||
(agents_dest / f"{name}.md").write_text(_agent(src, name, roles, target), encoding="utf-8")
|
||||
|
||||
# The quality-gate executable is a package asset; Claude hooks are deliberately
|
||||
# excluded. The loop/controller remains responsible for enforcing its verdict.
|
||||
gate = src / "scripts" / "quality-gate"
|
||||
if gate.is_file():
|
||||
(stage / "scripts").mkdir()
|
||||
shutil.copy2(gate, stage / "scripts" / "quality-gate")
|
||||
manifest = {
|
||||
"name": "dev-workflows",
|
||||
"version": "1.0.0",
|
||||
"description": "Skills e agentes nativos para oh-my-pi; gate imposto pelo controlador externo.",
|
||||
"omp": {"extensions": []},
|
||||
}
|
||||
(stage / "package.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
(stage / OWNER_MARKER).write_text(_owner_value(src) + f"sha256={_content_digest(stage)}\n", encoding="utf-8")
|
||||
|
||||
if target.exists():
|
||||
backup = Path(tempfile.mkdtemp(prefix=f".{target.name}.backup-", dir=target.parent))
|
||||
backup.rmdir()
|
||||
os.replace(target, backup)
|
||||
try:
|
||||
os.replace(stage, target)
|
||||
except BaseException:
|
||||
if backup is not None:
|
||||
os.replace(backup, target)
|
||||
backup = None
|
||||
raise
|
||||
if backup is not None:
|
||||
shutil.rmtree(backup)
|
||||
backup = None
|
||||
return target
|
||||
finally:
|
||||
if stage.exists():
|
||||
shutil.rmtree(stage)
|
||||
if backup is not None and backup.exists():
|
||||
if not target.exists():
|
||||
os.replace(backup, target)
|
||||
else:
|
||||
shutil.rmtree(backup)
|
||||
@@ -1,34 +0,0 @@
|
||||
// dev-workflows for opencode: the same guard, formatter and gate scripts the Claude Code hooks run.
|
||||
// Installed by `python3 scripts/install.py opencode`, which replaces __DW_ROOT__ with the checkout path.
|
||||
// opencode cannot block the end of a turn, so a missing or failing gate is logged instead of blocking.
|
||||
const ROOT = "__DW_ROOT__"
|
||||
|
||||
const DevWorkflows = async (ctx: any) => {
|
||||
const $ = ctx.$ ?? (globalThis as any).Bun?.$
|
||||
const directory = ctx.directory ?? ctx.location?.directory ?? process.cwd()
|
||||
return {
|
||||
"tool.execute.before": async (input: any, output: any) => {
|
||||
if (!["bash", "shell"].includes(String(input?.tool ?? "").toLowerCase())) return
|
||||
const command = output?.args?.command
|
||||
if (typeof command !== "string" || !command) return
|
||||
const r = await $`python3 ${ROOT}/scripts/hooks/bash_guard.py --check ${command}`.quiet().nothrow()
|
||||
if (r.exitCode === 2) throw new Error(String(r.stdout).trim())
|
||||
},
|
||||
"tool.execute.after": async (input: any, output: any) => {
|
||||
if (!["edit", "write", "patch", "multiedit"].includes(String(input?.tool ?? "").toLowerCase())) return
|
||||
const file = input?.args?.filePath ?? output?.args?.filePath
|
||||
if (typeof file === "string" && file) {
|
||||
await $`python3 ${ROOT}/scripts/hooks/post_edit.py --file ${file}`.quiet().nothrow()
|
||||
}
|
||||
},
|
||||
event: async ({ event }: any) => {
|
||||
if (event?.type !== "session.idle") return
|
||||
const r = await $`python3 ${ROOT}/scripts/hooks/stop_gate.py --check ${directory}`.quiet().nothrow()
|
||||
if (r.exitCode === 2) {
|
||||
await ctx.client?.app?.log?.({ body: { service: "dev-workflows", level: "warn", message: String(r.stdout).trim() } })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default { id: "dev-workflows", setup: DevWorkflows }
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: explorer
|
||||
description: Mapeia o código antes de uma mudança - onde fica o que importa, o que já existe para reaproveitar e como o projeto roda lint e testes. Só leitura. Use no nível 2 ou 3, ou no nível 1 quando não se sabe onde está o código.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: haiku
|
||||
---
|
||||
|
||||
Você mapeia o código para quem vai mudá-lo. Não edita nada. No Bash, só comandos de leitura
|
||||
(`git log`, `git grep`, `ls`, `cat` de configs).
|
||||
|
||||
Entregue, em até 300 palavras:
|
||||
|
||||
1. **Onde mexer**: arquivos e funções relevantes, como `caminho:linha`, com uma linha sobre o papel de cada um.
|
||||
2. **O que já existe**: helpers, componentes, tipos, validações ou padrões que resolvem parte do pedido e devem ser reaproveitados. Esta é a parte mais importante.
|
||||
3. **Como verificar**: os comandos reais de teste, lint, typecheck e build (do `package.json`, `pyproject.toml`, `Makefile` ou CI).
|
||||
4. **Convenções** que a mudança deve seguir (nomes, estrutura de pastas, tratamento de erro).
|
||||
5. **Riscos**: chamadores que também dependem do que vai mudar.
|
||||
|
||||
Se não achou algo, diga que não achou. Não invente caminhos.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: implementer
|
||||
description: Implementa uma tarefa já planejada, com TDD e o menor diff que resolve, e roda o gate de qualidade no nível indicado.
|
||||
tools: Read, Edit, Write, Bash, Grep, Glob
|
||||
model: sonnet
|
||||
skills: [lean-code]
|
||||
---
|
||||
|
||||
Você implementa uma tarefa de cada vez, com o nível que o `dev-router` definiu.
|
||||
|
||||
1. Leia os arquivos antes de editar. Siga as convenções do projeto.
|
||||
2. **Comportamento novo ou corrigido começa por um teste que falha.** Rode e veja vermelho; depois implemente até ficar verde.
|
||||
3. Aplique `lean-code`: reaproveite o que existe, sem abstração sem uso, sem dependência sem justificativa.
|
||||
4. Rode o gate: `"${CLAUDE_PLUGIN_ROOT}/scripts/quality-gate" --tier N`. Corrija o que falhar.
|
||||
5. Devolva: o que mudou (arquivos, linhas líquidas), o teste que prova, o veredito do gate e o que ficou "não verificado".
|
||||
|
||||
Não declare pronto com teste vermelho ou gate falho. Se uma falha não é sua de corrigir, diga qual e por quê.
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: planner
|
||||
description: Escreve o plano (nível 2) ou a spec com requisitos EARS, ADR e tarefas (nível 3) antes da implementação. Só leitura.
|
||||
tools: Read, Grep, Glob
|
||||
model: opus
|
||||
---
|
||||
|
||||
Você planeja; não implementa. Use o mapa do `explorer` se houver, e prefira reaproveitar o que existe.
|
||||
|
||||
## Nível 2: plano curto
|
||||
|
||||
- Até 15 linhas. Tarefas de 2 a 5 minutos cada, com o arquivo exato e como verificar.
|
||||
- O que **não** será feito (escopo cortado de propósito).
|
||||
- Decisões que são do usuário, se houver.
|
||||
|
||||
## Nível 3: spec
|
||||
|
||||
1. **Requisitos EARS com ID**, um por linha:
|
||||
- `REQ-001: WHEN <evento> THE SYSTEM SHALL <resposta>`
|
||||
- `REQ-002: IF <condição indesejada> THEN THE SYSTEM SHALL <resposta>`
|
||||
- `REQ-003: WHILE <estado> THE SYSTEM SHALL <resposta>`
|
||||
2. **Não funcionais** com número: orçamento de performance (ex.: p95, LCP), nível ASVS (L2), limites.
|
||||
3. **ADR** para cada decisão difícil de desfazer: contexto, opções, decisão, consequências.
|
||||
4. **Tarefas** pequenas, cada uma com os `REQ-` que cobre e o teste que prova.
|
||||
5. **Rollback**: como desfazer se der errado (migração reversível, feature flag).
|
||||
|
||||
Salve a spec num arquivo (ex.: `docs/specs/<nome>.md`) e sugira `[spec]` no `.dev-workflows.toml`
|
||||
para o gate conferir que todo `REQ-` tem teste. A spec passa pelo usuário antes da implementação.
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
name: reviewer
|
||||
description: Revisão independente de correção e simplicidade de um diff. Julga só o código e o plano, não o que a conversa afirma. Só leitura. Use no nível 2 ou 3 depois da implementação.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: opus
|
||||
---
|
||||
|
||||
Você revisa um diff sem ter participado da implementação. Não edite arquivos. No Bash, só leitura
|
||||
(`git diff <base>`, `git log`, rodar testes).
|
||||
|
||||
Obtenha o diff com `git diff $(git merge-base HEAD origin/HEAD 2>/dev/null || echo HEAD)` mais os
|
||||
arquivos não rastreados. Leia o código ao redor de cada trecho alterado.
|
||||
|
||||
Procure:
|
||||
|
||||
1. **Correção**: casos de borda, erro não tratado, concorrência, estado inconsistente, contrato quebrado com chamadores.
|
||||
2. **Testes que provam**: o teste falharia se o bug voltasse? Testa o comportamento ou só a implementação?
|
||||
3. **Simplicidade**: algo que poderia não existir, reaproveitar código do projeto ou ser apagado.
|
||||
4. **Consistência** com as convenções do código ao redor.
|
||||
|
||||
Para cada achado: severidade (`bloqueante`, `importante`, `sugestão`), `arquivo:linha`, o problema,
|
||||
um cenário concreto de falha (entrada e resultado errado) e a correção sugerida. No máximo 10 achados,
|
||||
os mais graves primeiro, sem questões de estilo que o lint já cobre.
|
||||
|
||||
Sem achados: diga "sem achados" e liste o que você verificou.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: security-auditor
|
||||
description: Auditoria de segurança do diff e da superfície que ele toca - OWASP Top 10 2025, API Top 10, ASVS 5.0 e LLM Top 10, com CVEs consultados ao vivo. Só leitura. Use quando o dev-router disparar um gatilho de segurança ou no nível 3.
|
||||
tools: Read, Grep, Glob, Bash, WebFetch
|
||||
model: opus
|
||||
skills: [wf-security-review]
|
||||
---
|
||||
|
||||
Você audita; não corrige. Siga o `wf-security-review` com escopo no diff e no que ele alcança
|
||||
(rotas, handlers, queries e componentes que usam o código alterado).
|
||||
|
||||
- **Referências**: OWASP Top 10:2025, OWASP API Security Top 10 (2023), ASVS 5.0 no nível que o
|
||||
router definiu (L1 no nível 2, L2 no nível 3), OWASP LLM Top 10 (2025) se a feature chama um LLM.
|
||||
- **Dependências novas ou alteradas**: consulte OSV.dev ao vivo (`safedeps`) e registre data e fonte.
|
||||
Nunca diga "sem CVE" sem ter consultado.
|
||||
- **Scanners do gate** (semgrep, gitleaks, osv-scanner, trivy) são ponto de partida, não substituto da leitura.
|
||||
|
||||
Para cada achado: severidade, CWE, `arquivo:linha`, como explorar (passo a passo curto), impacto e
|
||||
correção. Diferencie vulnerabilidade confirmada de suspeita. Sem achados: liste o que verificou e as
|
||||
fontes consultadas, com data.
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: ui-critic
|
||||
description: Crítica visual e de UX de uma tela implementada - estados, acessibilidade, responsividade, contraste e padrões genéricos de "UI feita por IA". Só leitura. Use no nível 2 ou 3 quando houver UI.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: sonnet
|
||||
skills: [redesign-existing-projects]
|
||||
---
|
||||
|
||||
Você critica a interface; não edita. Use a lente de auditoria do `redesign-existing-projects` e, se
|
||||
instalada, a do `impeccable` (`/impeccable audit`).
|
||||
|
||||
1. **Veja a tela.** Com o dev server de pé: `npx playwright screenshot --viewport-size=375,812 <url> /tmp/ui-375.png`
|
||||
e `--viewport-size=1280,800` para desktop. Abra as imagens com Read. Sem servidor, avalie pelo código e diga isso.
|
||||
2. **Estados**: carregando, vazio, erro e sucesso existem em cada fluxo com dados.
|
||||
3. **Acessibilidade**: HTML semântico, foco visível e em ordem, rótulos, contraste de texto >= 4.5:1.
|
||||
Se houver Playwright: `@axe-core/playwright` sem violação `serious` ou `critical`.
|
||||
4. **Responsivo**: nada corta ou sobrepõe em 375 px e em 1280 px.
|
||||
5. **Padrão genérico**: gradiente roxo, glassmorphism sem motivo, cards idênticos, ícones decorativos,
|
||||
microcopy vaga ("Unlock the power of..."). Aponte o que trocar.
|
||||
|
||||
Entregue achados priorizados: o que está errado, onde (`arquivo:linha` ou região da screenshot) e a
|
||||
correção concreta. No máximo 10.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: verifier
|
||||
description: Roda o gate de qualidade no nível pedido e devolve a evidência - veredito, checagens que falharam com a saída relevante e o que ficou não verificado. Nunca edita código.
|
||||
tools: Bash, Read
|
||||
model: haiku
|
||||
---
|
||||
|
||||
1. Rode `"${CLAUDE_PLUGIN_ROOT}/scripts/quality-gate" --tier N` com o nível recebido.
|
||||
2. Leia `.dev-workflows/gate.json`.
|
||||
3. Devolva:
|
||||
- o veredito (`pass`, `fail` ou `incomplete`);
|
||||
- cada checagem `fail` com as linhas da saída que mostram o motivo;
|
||||
- a lista de checagens `unverified` (ferramenta ou configuração ausente);
|
||||
- dependências novas, se houver;
|
||||
- o `diff` (arquivos e linhas líquidas).
|
||||
|
||||
Não edite nada e não reinterprete uma falha como aprovação. Se o gate não rodou, diga por quê.
|
||||
@@ -0,0 +1,51 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="420" viewBox="0 0 1200 420" role="img" aria-labelledby="title desc">
|
||||
<title id="title">dev-workflows</title>
|
||||
<desc id="desc">dev-workflows: agent workflows where every phase ends in a verification step. On the right, the wf-security-review SKILL.md as a checklist: SCOPE, HUNT known CVEs, CONSULT OSV.dev and NVD, CODE REVIEW against OWASP Top 10, REPORT and GATE with sources and date.</desc>
|
||||
|
||||
<rect width="1200" height="420" rx="24" fill="#111110"/>
|
||||
|
||||
<g id="title-block" font-family="system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif">
|
||||
<text x="64" y="156" font-size="68" font-weight="700" fill="#f2efe8">dev-workflows</text>
|
||||
<text x="64" y="214" font-size="28" fill="#f2efe8">Agent workflows where every phase</text>
|
||||
<text x="64" y="252" font-size="28" fill="#f2efe8">ends in a verification step.</text>
|
||||
<text x="64" y="326" font-size="20" fill="#8f8b83" font-family="ui-monospace, "SF Mono", Consolas, monospace">SKILL.md · Python QA harness · MIT</text>
|
||||
</g>
|
||||
|
||||
<g id="skill-file" transform="translate(640 44)" font-family="ui-monospace, "SF Mono", Consolas, monospace">
|
||||
<rect width="496" height="332" rx="16" fill="#1b1b19" stroke="#2f2e2b" stroke-width="2"/>
|
||||
<text x="24" y="30" font-size="18" fill="#8f8b83">wf-security-review/SKILL.md</text>
|
||||
<line x1="0" y1="48" x2="496" y2="48" stroke="#2f2e2b" stroke-width="2"/>
|
||||
|
||||
<g id="phases" font-size="20" fill="#f2efe8">
|
||||
<g transform="translate(24 76)">
|
||||
<rect width="24" height="24" rx="5" fill="#3fb950"/>
|
||||
<path d="M6 12 L10.5 16.5 L18 8" fill="none" stroke="#111110" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<text x="40" y="19">1 SCOPE</text>
|
||||
<text x="448" y="19" font-size="18" fill="#8f8b83" text-anchor="end">target</text>
|
||||
</g>
|
||||
<g transform="translate(24 128)">
|
||||
<rect width="24" height="24" rx="5" fill="#3fb950"/>
|
||||
<path d="M6 12 L10.5 16.5 L18 8" fill="none" stroke="#111110" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<text x="40" y="19">2 HUNT</text>
|
||||
<text x="448" y="19" font-size="18" fill="#8f8b83" text-anchor="end">known CVEs</text>
|
||||
</g>
|
||||
<g transform="translate(24 180)">
|
||||
<rect width="24" height="24" rx="5" fill="#3fb950"/>
|
||||
<path d="M6 12 L10.5 16.5 L18 8" fill="none" stroke="#111110" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<text x="40" y="19">3 CONSULT</text>
|
||||
<text x="448" y="19" font-size="18" fill="#8f8b83" text-anchor="end">OSV.dev · NVD</text>
|
||||
</g>
|
||||
<g transform="translate(24 232)">
|
||||
<rect width="24" height="24" rx="5" fill="#3fb950"/>
|
||||
<path d="M6 12 L10.5 16.5 L18 8" fill="none" stroke="#111110" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<text x="40" y="19">4 CODE REVIEW</text>
|
||||
<text x="448" y="19" font-size="18" fill="#8f8b83" text-anchor="end">OWASP Top 10</text>
|
||||
</g>
|
||||
<g transform="translate(24 284)">
|
||||
<rect x="1" y="1" width="22" height="22" rx="5" fill="none" stroke="#f2efe8" stroke-width="2"/>
|
||||
<text x="40" y="19">5 REPORT + GATE</text>
|
||||
<text x="448" y="19" font-size="18" fill="#8f8b83" text-anchor="end">sources + date</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
+96
-62
@@ -1,77 +1,111 @@
|
||||
# Skills: inventário e origem
|
||||
# Skills usadas e de onde vieram
|
||||
|
||||
Cada skill é um diretório `skills/<nome>/SKILL.md`. O plugin Claude carrega todas;
|
||||
`scripts/install.py` instala skills e agentes em OpenCode ou OMP e apenas skills no Hermes.
|
||||
O adapter OMP gera um pacote nativo independente em `~/.omp/plugins/dev-workflows`,
|
||||
registra os sete agentes do repositório e não reutiliza os hooks Claude.
|
||||
Este é o inventário do que os workflows carregam. Cada skill é um diretório
|
||||
com um `SKILL.md`; para o Hermes Agent, vão em `~/.hermes/skills/`; para
|
||||
Claude Code, em `~/.claude/skills/<nome>/SKILL.md` (subdiretório por skill —
|
||||
Claude Code não carrega arquivo `.md` solto, precisa do diretório).
|
||||
|
||||
## Do próprio repositório
|
||||
## Workflows orquestradores (em `workflows/`)
|
||||
|
||||
| Skill | Para quê |
|
||||
| Workflow | Para quê |
|
||||
|---|---|
|
||||
| `dev-router` | Classifica o pedido em nível 0 a 3 e decide workflows, agentes e gate |
|
||||
| `lean-code` | Menor solução que resolve: reaproveitar, apagar, sem abstração nem dependência sem motivo |
|
||||
| `wf-bugfix` | Reproduzir, causa raiz, teste de regressão que falha antes e passa depois |
|
||||
| `wf-refactor` | Testes de caracterização, passos pequenos, comportamento igual |
|
||||
| `wf-frontend` | UI e PDF: design (nível 2+), build acessível e responsivo, revisão visual |
|
||||
| `wf-backend` | API: contrato primeiro (nível 2+), defaults seguros, auditoria OWASP, testes |
|
||||
| `wf-architecture` | Nível 3: spec EARS, C4, ADR, STRIDE, revisão |
|
||||
| `wf-security-review` | Revisão de segurança front e back com CVEs consultados ao vivo |
|
||||
| `wf-readme` | README fundamentado em fatos do repositório, comandos executados antes da entrega |
|
||||
| `wf-frontend` | UI/design/PDF |
|
||||
| `wf-backend` | API/backend seguro |
|
||||
| `wf-architecture` | Arquitetura/ADR/C4 |
|
||||
| `wf-security-review` | Code review de segurança front+back |
|
||||
| `wf-readme` | Criar/auditar/reescrever READMEs grounded |
|
||||
|
||||
## Vendorizadas (vêm no clone)
|
||||
|
||||
Origem, commit e licença de cada uma: [`skills/NOTICE.md`](../skills/NOTICE.md).
|
||||
## README (wf-readme)
|
||||
|
||||
| Skill | Origem | O que faz |
|
||||
|---|---|---|
|
||||
| `safedeps` | [Jeneidi/safedeps](https://github.com/Jeneidi/safedeps) | CVE por `package@version` consultando o OSV.dev ao vivo |
|
||||
| `sca-audit` | [OWASP/secure-agent-playbook](https://github.com/OWASP/secure-agent-playbook) | Auditoria de dependências com análise de alcance e CWE |
|
||||
| `code-review-security` | OWASP/secure-agent-playbook | Code review de segurança mapeado a OWASP Top 10 e ASVS |
|
||||
| `secrets-scan` | OWASP/secure-agent-playbook | Credenciais e chaves no código e no histórico do git |
|
||||
| `api-security-review` | OWASP/secure-agent-playbook | API contra o OWASP API Security Top 10 |
|
||||
| `web-security-review` | OWASP/secure-agent-playbook | Web app contra o OWASP Top 10 |
|
||||
| `cve-triage` | [UnitOneAI/SecuritySkills](https://github.com/UnitOneAI/SecuritySkills) | Prioriza CVE por CVSS 4.0, EPSS e CISA KEV |
|
||||
| `patch-prioritization` | UnitOneAI/SecuritySkills | Ordem de remediação |
|
||||
| `dependency-scanning` | UnitOneAI/SecuritySkills | Varredura da árvore de dependências |
|
||||
| `hermaguard` | [Sahil-SS9/hermaguard](https://github.com/Sahil-SS9/hermaguard) | Revisão adversarial: pre-scan e 3 agentes (borda, ataque, blast radius) |
|
||||
| `design-taste-frontend` | [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill) | Gera tela nova sem cara de template |
|
||||
| `redesign-existing-projects` | Leonxlnx/taste-skill | Audita UI existente e melhora sem quebrar |
|
||||
| `readme-crafter` | [linhai0872/readme-crafter-skill](https://github.com/linhai0872/readme-crafter-skill) | Classifica projeto (tipo/público/temperamento) e gera README sob medida; 13 checks |
|
||||
| `good-readme` | [adewale/good-readme](https://github.com/adewale/good-readme) | Cria com exemplos reais OU audita contra 22 critérios (escala 100) |
|
||||
| `curating-readme` | [liang-senbei/curating-readme](https://github.com/liang-senbei/curating-readme) | Padroniza README + docs (CONTRIBUTING/CHANGELOG) com `audit-repo.sh` |
|
||||
|
||||
## Externas (opcionais)
|
||||
## Pilar de segurança (wf-security-review)
|
||||
|
||||
Os workflows usam estas quando instaladas e seguem as próprias regras quando não. Instalação no
|
||||
formato Agent Skills:
|
||||
**Vendorizadas neste repo** em `skills/<nome>/` — ver `skills/NOTICE.md` pra
|
||||
origem exata (commit + licença) de cada uma. Não precisa clonar nada externo
|
||||
pra este pilar.
|
||||
|
||||
| Skill | Origem | O que faz |
|
||||
|---|---|---|
|
||||
| `safedeps` | [Jeneidi/safedeps](https://github.com/Jeneidi/safedeps) | Consulta OSV.dev em tempo real; CVE por `package@version` com severidade e versão de fix. Inclui o script `check_deps.py` |
|
||||
| `sca-audit` | [OWASP/secure-agent-playbook](https://github.com/OWASP/secure-agent-playbook) | SCA de dependências com análise de alcance (reachability), mapeamento CWE |
|
||||
| `code-review-security` | OWASP/secure-agent-playbook | Code review de segurança sistemático mapeado a OWASP Top 10 + ASVS |
|
||||
| `secrets-scan` | OWASP/secure-agent-playbook | Detecta credenciais/API keys no código e no git history |
|
||||
| `api-security-review` | OWASP/secure-agent-playbook | Revisão de API contra OWASP API Security Top 10 |
|
||||
| `web-security-review` | OWASP/secure-agent-playbook | Web app contra OWASP Top 10 (2021) |
|
||||
| `cve-triage` | [UnitOneAI/SecuritySkills](https://github.com/UnitOneAI/SecuritySkills) | Prioriza CVE por CVSS 4.0 / EPSS / CISA KEV |
|
||||
| `patch-prioritization` | UnitOneAI/SecuritySkills | Decide a ordem de remediação |
|
||||
| `dependency-scanning` | UnitOneAI/SecuritySkills | Varredura de dependências |
|
||||
|
||||
## Frontend / UI / design (wf-frontend)
|
||||
|
||||
| Skill | Origem | O que faz |
|
||||
|---|---|---|
|
||||
| `frontend-design` | [anthropics/skills](https://github.com/anthropics/skills) | Direção visual, tipografia, fugir do genérico |
|
||||
| `web-design` | [KAOPU-XiaoPu/web-design](https://github.com/KAOPU-XiaoPu/web-design) | Estética web coesa |
|
||||
| `vercel-react-best-practices` | [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) | 40+ regras de perf React/Next |
|
||||
| `vercel-composition-patterns` | vercel-labs/agent-skills | Compound components, composição |
|
||||
| `animate` | [emilkowalski/skill](https://github.com/emilkowalski/skill) | Motion com propósito |
|
||||
| `impeccable` | [pbakaus/impeccable](https://github.com/pbakaus/impeccable) | Vocabulário de design: 23 comandos + 59 regras determinísticas anti-slop |
|
||||
| `anti-ai-slop` | local | Detecta padrão visual "gerado por IA" |
|
||||
| `avoid-ai-writing` | [conorbronsdon/avoid-ai-writing](https://github.com/conorbronsdon/avoid-ai-writing) | Remove AI-isms de texto/microcopy |
|
||||
|
||||
## Backend / API (wf-backend)
|
||||
|
||||
| Skill | Origem | O que faz |
|
||||
|---|---|---|
|
||||
| `secure-coding` | [v6x/secure-coding-skill](https://github.com/securityreviewai/secure-coding-skill) | Padrões de codificação segura em 15 stacks |
|
||||
| `bola-detector` | [apisec-inc/apisec-skills](https://github.com/apisec-inc/apisec-skills) | Detecta object-level authorization (OWASP API1) |
|
||||
| `auth-rbac-scaffold` | apisec-inc/apisec-skills | Auth/RBAC (OWASP API2/API5) |
|
||||
| `injection-checker` | apisec-inc/apisec-skills | Injection SQL/ORM/shell (OWASP API8) |
|
||||
| `openapi-hardener` | apisec-inc/apisec-skills | Saneia schemas OpenAPI/Zod (OWASP API3) |
|
||||
| `api-security-review` | apisec-inc/apisec-skills | Review completo OWASP API Top 10 |
|
||||
| `security-test-generator` | apisec-inc/apisec-skills | Gera testes de segurança |
|
||||
|
||||
## Arquitetura (wf-architecture)
|
||||
|
||||
| Skill | Origem | O que faz |
|
||||
|---|---|---|
|
||||
| `system-design` | [Kotivskyi/architecture-governance-skills](https://github.com/Kotivskyi/architecture-governance-skills) | Framework HelloInterview de system design |
|
||||
| `c4-architecture` | Kotivskyi/architecture-governance-skills | Diagramas C4 (Mermaid/Structurizr) |
|
||||
| `isaqb-architecture-governance` | Kotivskyi/architecture-governance-skills | arc42 + ADR |
|
||||
| `secure-architecture-governance` | Kotivskyi/architecture-governance-skills | Threat model STRIDE+CIA, S-ADR |
|
||||
|
||||
## Caça de bugs (todas)
|
||||
|
||||
| Skill | Origem | O que faz |
|
||||
|---|---|---|
|
||||
| `hermaguard` | [Sahil-SS9/hermaguard](https://github.com/Sahil-SS9/hermaguard) | Review adversarial: pre-scan + 3 agentes (edge cases, ataque, blast radius) |
|
||||
|
||||
## Como instalar as skills de origem
|
||||
|
||||
**Pilar de segurança:** já vem vendorizado em `skills/` neste repo — sem
|
||||
clone externo. Copie direto:
|
||||
|
||||
```bash
|
||||
npx skills add <dono/repo> --skill <nome>
|
||||
# Hermes Agent:
|
||||
cp -r skills/* ~/.hermes/skills/
|
||||
|
||||
# Claude Code (cada skill no seu próprio diretório):
|
||||
mkdir -p ~/.claude/skills
|
||||
for d in skills/*/; do
|
||||
nome=$(basename "$d")
|
||||
[ "$nome" = "NOTICE.md" ] && continue
|
||||
mkdir -p "~/.claude/skills/$nome"
|
||||
cp -r "$d." "~/.claude/skills/$nome/"
|
||||
done
|
||||
```
|
||||
|
||||
| Skill | Origem | Usada por |
|
||||
|---|---|---|
|
||||
| `frontend-design` | [anthropics/skills](https://github.com/anthropics/skills) | `wf-frontend` |
|
||||
| `impeccable` | [pbakaus/impeccable](https://github.com/pbakaus/impeccable) | `wf-frontend`, `ui-critic` |
|
||||
| `vercel-react-best-practices` | [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) | `wf-frontend` |
|
||||
| `vercel-composition-patterns` | vercel-labs/agent-skills | `wf-frontend` |
|
||||
| `animate` | [emilkowalski/skill](https://github.com/emilkowalski/skill) | `wf-frontend` |
|
||||
| `web-design` | [KAOPU-XiaoPu/web-design](https://github.com/KAOPU-XiaoPu/web-design) | `wf-frontend` |
|
||||
| `avoid-ai-writing` | [conorbronsdon/avoid-ai-writing](https://github.com/conorbronsdon/avoid-ai-writing) | `wf-frontend`, `wf-readme` |
|
||||
| `secure-coding` | [securityreviewai/secure-coding-skill](https://github.com/securityreviewai/secure-coding-skill) | `wf-backend` |
|
||||
| `bola-detector` | [apisec-inc/apisec-skills](https://github.com/apisec-inc/apisec-skills) | `wf-backend`, `wf-security-review` |
|
||||
| `auth-rbac-scaffold` | apisec-inc/apisec-skills | `wf-backend`, `wf-security-review` |
|
||||
| `injection-checker` | apisec-inc/apisec-skills | `wf-backend`, `wf-security-review` |
|
||||
| `openapi-hardener` | apisec-inc/apisec-skills | `wf-backend` |
|
||||
| `security-test-generator` | apisec-inc/apisec-skills | `wf-backend` |
|
||||
| `system-design` | [Kotivskyi/architecture-governance-skills](https://github.com/Kotivskyi/architecture-governance-skills) | `wf-architecture` |
|
||||
| `c4-architecture` | Kotivskyi/architecture-governance-skills | `wf-architecture` |
|
||||
| `isaqb-architecture-governance` | Kotivskyi/architecture-governance-skills | `wf-architecture` |
|
||||
| `secure-architecture-governance` | Kotivskyi/architecture-governance-skills | `wf-architecture` |
|
||||
| `readme-crafter` | [linhai0872/readme-crafter-skill](https://github.com/linhai0872/readme-crafter-skill) | `wf-readme` |
|
||||
| `good-readme` | [adewale/good-readme](https://github.com/adewale/good-readme) | `wf-readme` |
|
||||
| `curating-readme` | [liang-senbei/curating-readme](https://github.com/liang-senbei/curating-readme) | `wf-readme` |
|
||||
**Frontend/backend/arquitetura** (ainda não vendorizadas, instalam de fora):
|
||||
a maioria via `npx skills` (formato agentskills.io):
|
||||
|
||||
A apisec-inc também publica uma skill chamada `api-security-review`. Ela não é usada aqui: o nome
|
||||
colide com a versão OWASP vendorizada.
|
||||
|
||||
O `impeccable` não é vendorizado porque traz scripts executáveis de terceiros; instale-o você mesmo
|
||||
(`npx skills add https://github.com/pbakaus/impeccable --skill impeccable`) se quiser o `/impeccable audit`.
|
||||
```bash
|
||||
npx skills add anthropics/skills --skill frontend-design
|
||||
npx skills add vercel-labs/agent-skills --skill vercel-react-best-practices
|
||||
npx skills add conorbronsdon/avoid-ai-writing
|
||||
npx skills add https://github.com/pbakaus/impeccable --skill impeccable
|
||||
```
|
||||
@@ -1,246 +0,0 @@
|
||||
# Spec: loop de melhoria por meta e roteamento de modelos por ferramenta
|
||||
|
||||
**Estado:** implementada como CLIs opt-in; ranking e despacho pagos dependem de evidência válida da conta, benchmark autorizado e autorização pontual. Nesta captura não há notas AA importadas nem elegibilidade de conta confirmada.
|
||||
**Consulta de fontes:** 2026-09-25 (UTC). **Escopo:** `dev-workflows` como kit multi-ferramenta, não somente OMP.
|
||||
**Decisões já recebidas:** faixas de qualidade por **notas fixas**, Claude Code usado por **assinatura**, OpenRouter como fonte dos preços unitários de **referência** para cruzar os modelos do OpenCode.
|
||||
|
||||
## 1. Problema e resultado esperado
|
||||
|
||||
Na base anterior, `skills/dev-router/SKILL.md` dimensionava tarefas em níveis 0–3; `agents/*.md` fixavam Haiku/Sonnet/Opus; `scripts/quality-gate` escrevia `.dev-workflows/gate.json`; o hook Claude bloqueava o encerramento sem gate atual, enquanto o adapter OpenCode apenas avisava. Agora catálogo, roteador e controlador externo acrescentam avaliação de meta sem substituir esses mecanismos; `scripts/install.py` inclui OMP nativo, mantendo Hermes apenas com skills.
|
||||
|
||||
Entregar duas capacidades independentes e combináveis:
|
||||
|
||||
1. **Roteador:** listar os modelos realmente utilizáveis por ferramenta/conta, cruzar preços OpenRouter e evidência de benchmark de programação, mostrar ranking e escolher um modelo por função dentro de uma faixa de nota e de um orçamento explícitos.
|
||||
2. **Controlador externo de experimentos:** medir um objetivo do **aplicativo** com avaliador independente, executar mudanças limitadas em worktrees isolados, aplicar o quality gate, aceitar apenas melhorias válidas e repetir até atingir o alvo ou um limite. Instalar skills/agentes não liga esse controlador automaticamente.
|
||||
|
||||
**Duas escalas diferentes:** `coding_score` (Artificial Analysis, seleção do agente/modelo, 0–100) não é `app_score` (avaliador específico do projeto, com unidade/direção/alvo configuráveis). Não usar um como prova do outro.
|
||||
|
||||
### Fora de escopo
|
||||
|
||||
Assinar serviços, criar chaves, alterar a cobrança Zen/Go, prometer preços iguais entre gateways, extrair dados protegidos da Artificial Analysis, publicar resultados/código, remover worktrees com mudanças, alterar testes protegidos, reimplementar o quality gate, ou rodar o loop em projetos sem objetivo verificável. Não adotar um framework Ralph como dependência obrigatória: aproveitar o padrão de contexto fresco e verificação externa com o Python já usado pelo projeto.
|
||||
|
||||
## 2. Fontes e interpretação
|
||||
|
||||
| Informação | Autoridade / campo | Regra |
|
||||
|---|---|---|
|
||||
| Catálogo Zen | [Zen `/v1/models`](https://opencode.ai/zen/v1/models), [docs Zen](https://opencode.ai/docs/zen); ID de uso `opencode/<id>` | Lista pública não prova acesso da conta nem cobrança. Zen tem modelos Claude; não excluir a família por suposição. |
|
||||
| Catálogo Go | [Go `/v1/models`](https://opencode.ai/zen/go/v1/models), [docs Go](https://opencode.ai/docs/go); ID `opencode-go/<id>` | Assinatura de US$10/mês; cotas **por modelo** em equivalentes USD: 20% do limite mensal a cada 5h, 50% semanal, 100% mensal. A lista atual inclui GPT Luna, mas não Claude; reconsultar. |
|
||||
| Acesso OpenCode local | [Modelos e providers](https://opencode.ai/docs/models), CLI `opencode models <provider> --refresh`; autenticação local/conta | Interseção com IDs exatos do provider instalado e habilitado; lista pública e credencial, isoladamente, não bastam. Erro de autorização retira candidato. |
|
||||
| Preço comparativo | [OpenRouter Models API](https://openrouter.ai/docs/api/api-reference/models/get-models), `GET /api/v1/models` | `prompt`, `completion`, `input_cache_read`, `input_cache_write` são USD **por token**: multiplicar por 1.000.000 para mostrar USD/1M. Campo ausente = `n/d`, nunca zero. IDs OpenRouter não são IDs OpenCode. |
|
||||
| Preço real e limites | Docs/console [Zen](https://opencode.ai/docs/zen) e [Go](https://opencode.ai/docs/go), ou plano da ferramenta usada | **Não** cobrar, estimar saldo Zen nem debitar cota Go por preço OpenRouter. Considerar overrides de contexto, janelas de pico e cache; quando indisponível, bloquear despacho automático pago. |
|
||||
| Benchmark principal | [Artificial Analysis Coding Agent Index](https://artificialanalysis.ai/agents/coding-agents), [metodologia v1.5](https://artificialanalysis.ai/methodology/coding-agents-benchmarking) | Média de DeepSWE v1.1, Terminal-Bench 4.0 e SWE-Atlas-QnA; guardar agente/harness, variante, esforço, fallback, versão, data, link e score. Score do harness **não** é propriedade intrínseca de toda rota do modelo. |
|
||||
| API AA | [Data API](https://artificialanalysis.ai/data-api/docs) e [API legada](https://artificialanalysis.ai/api-reference) | Endpoints autenticados de modelos expõem índices de natureza/versão distintas; a documentação consultada não oferece export público documentado do **Coding Agent Index v1.5**. `artificial_analysis_coding_index` da API legada, quando disponível, não é automaticamente o índice de agentes v1.5. Nunca trocar pelo Intelligence Index. Respeitar termos e atribuição. |
|
||||
|
||||
**Ingestão do benchmark:** preferir export/API oficial **documentado e autorizado** para o índice de agentes; até existir, aceitar snapshot auditado/importado manualmente da página pública com a atribuição e evidência por linha, observando seus termos. Sem scraping automático de interface privada, sem inventar pontuações para preencher lacunas. Qualquer outro benchmark entra com `metric_id` diferente e ranking separado; não juntar scores entre índices, versões ou harnesses sem justificativa experimental.
|
||||
|
||||
### Amostra auditável de cruzamento (não é ranking completo nem tabela de cobranças)
|
||||
|
||||
Valores observados em 2026-09-25: preço **OpenRouter** em USD/1M tokens (entrada / saída / leitura de cache / escrita de cache); nota do **Coding Agent Index v1.5**, com harness e variante indicados. A presença nos catálogos públicos não afirma elegibilidade de nenhuma conta.
|
||||
|
||||
| Modelo e IDs de exemplo | Preço OpenRouter | Nota AA da variante avaliada | Observação |
|
||||
|---|---|---|---|
|
||||
| GLM 5.3: `z-ai/glm-5.3`; `opencode/glm-5.3`; `opencode-go/glm-5.3` | 1,40 / 4,40 / 0,26 / n/d | 54, OpenCode (max) | Go publica cota mensal de US$15 para este modelo; benchmark no OpenCode não prova equivalência do backend de todas as rotas. |
|
||||
| GPT-6 Luna: `openai/gpt-6-luna`; `opencode/gpt-6-luna`; `opencode-go/gpt-6-luna` | 0,10 / 0,50 / 0,01 / 0,125 | 41, Codex (max) | Faixa **média pela nota**, apesar do preço baixo; Go publica cota mensal de US$15. OpenRouter aplica preços diferentes acima de 272k tokens. |
|
||||
| GPT-6 Sol: `openai/gpt-6-sol`; `opencode/gpt-6-sol` | 2,00 / 10,00 / 0,20 / 2,50 | 57, Codex (max) | Pontuação em outro harness; preço muda acima de 272k tokens. |
|
||||
| Claude Opus 5.5: `anthropic/claude-opus-5.5`; `opencode/claude-opus-5-5` | 4,00 / 20,00 / 0,20 / 5,00 | 66, Claude Code (max) | Zen oferece esse ID publicamente; não presumir acesso na conta nem equiparar preço OpenRouter à assinatura Claude. |
|
||||
| Kimi K3: `moonshotai/kimi-k3`; `opencode-go/kimi-k3`; `opencode/kimi-k3` | 0,8845 / 10,5346 / 0,33 / n/d | 52, Kimi Code CLI | Variante avaliada em outro harness; Go publica cota mensal de US$15. |
|
||||
| Claude Sonnet/Haiku, MiMo-V2.6-Pro | IDs conforme catálogo de cada ferramenta; pareamento exige revisão | **n/d** | Não foi localizada nota dessas variantes no recorte público consultado; não atribuir nota de outra família/harness. |
|
||||
|
||||
Os preços acima vêm do [JSON OpenRouter](https://openrouter.ai/api/v1/models); notas da [leaderboard](https://artificialanalysis.ai/agents/coding-agents); cotas e IDs das [docs Go](https://opencode.ai/docs/go) e [Zen](https://opencode.ai/docs/zen). Cache `n/d` significa *não informado*, não gratuito. Listar também condição de preço/contexto e origem por modelo na saída gerada; dados mudam.
|
||||
|
||||
## 3. Política de roteamento
|
||||
|
||||
### 3.1 Faixas e funções
|
||||
|
||||
Usar **nota fixa** de 0 a 100; cortes configuráveis, defaults abaixo. Faixa não é preço nem tamanho físico do modelo:
|
||||
|
||||
| Faixa de qualidade | Intervalo | Uso padrão | Restrições |
|
||||
|---|---|---|---|
|
||||
| `leve` | `[10, 20)` | exploração/leitura em volume, coleta de dados públicos, sumarização mecânica e relatório do `verifier` | Agentes só de leitura; se nenhum candidato medido, faixa fica vazia. Nota `<10` inelegível por padrão. |
|
||||
| `media` | `[20, 50)` | `implementer`, `ui-critic` nível 2 | Exigir ferramentas e contexto suficientes; escalar tarefas de alto risco para `alta`. |
|
||||
| `alta` | `[50, 100]` | `planner`, `reviewer`, `security-auditor`, implementação nível 3/risco | Planejamento/revisão independentes e limites de custo. Notas `>80` continuam nesta **terceira** faixa. |
|
||||
|
||||
Não assumir que haverá candidato em todas as faixas: os exemplos publicados de 2026-09-25 não preenchem a faixa `leve`. Se vazia, registrar `no_scored_candidate`; propor o menor custo de outra faixa que cumpra o piso **após** orçamento/autorizações, ou usar um modelo fixado pelo usuário e rotular `unranked`; não afirmar que esta opção venceu no benchmark. `dev-router` continua a escolher nível e gatilhos de segurança; a faixa nunca reduz nível/gate nem torna revisão opcional.
|
||||
|
||||
### 3.2 Elegibilidade, custo e seleção
|
||||
|
||||
Para cada função, aplicar nesta ordem:
|
||||
|
||||
1. Escolher ferramenta/harness **em uso**, provider e conta de cobrança permitidos pelo usuário. Elegível só se `(provider, id, variante)` constar do catálogo local revalidado, autenticação/entitlement não negado e houver contexto, modalidade, tool calling e permissões requeridos. IDs e versões vinculados por mapa curado com proveniência; sem `fuzzy match` de nomes. `opencode/*` e `opencode-go/*` são rotas distintas. OMP pode usar esses providers se realmente configurados; Claude Code não vira multi-provider por declarar um ID Zen.
|
||||
2. Para seleção **pontuada**, exigir benchmark de métrica/versão única e mapeamento de variante. `exact`: harness, modelo, esforço e caminho avaliados equivalentes; `proxy`: modelo e esforço compatíveis, mas harness/provider diferem, com risco explícito. Proxy só se a política da função autorizar; para migração/auth/segurança de nível 3, exigir `exact` ou aprovação humana da incerteza. `unranked` não entra em fronteira de qualidade nem ganha score emprestado.
|
||||
3. Filtrar pelo intervalo da função e pelo custo/limite de cobrança autorizado. `score_top = max(score)` dos elegíveis da faixa; formar platô `score >= score_top - delta_points` (**pontos de nota**, não porcentagem). Defaults: `delta_points = 2` para `alta`, `3` para `media` e `leve`. No platô, escolher **menor consumo relevante para a ferramenta** no perfil de tokens da função: tarifa Zen para Zen, débito de cota Go para Go, consumo observado da assinatura para Claude quando medível. Publicar também o ranking **comparativo OpenRouter**, mas não usá-lo como custo real de outro gateway. Se o consumo relevante for desconhecido, não alegar escolha ótima: manter rota estática ou pedir seleção explícita. Desempate: nota maior, maior folga de cota, ID lexicográfico. Reportar top absoluto, economia estimada e fronteira de Pareto (nota e custo na mesma base).
|
||||
4. Custo **referencial** por tarefa: `C_OR = u*P_entrada + o*P_saida + r*P_cache_leitura + w*P_cache_escrita + taxas_extras`, com `u,o,r,w` em tokens e preços da API OpenRouter **em USD por token**. Equivalentemente, se usar valores exibidos **em USD/1M**, dividir a soma dos quatro produtos por `1_000_000`. Usar `Decimal`, sem ponto flutuante para moeda. Se o perfil usa cache e seu preço não é informado, `C_OR = n/d`, sem comparar artificialmente como zero; componente com zero tokens não requer tarifa. Respeitar overrides de contexto/TTL/endpoint. Perfil de tokens vem de medições do próprio harness ou configuração explícita; previsão é **estimativa**, não fatura.
|
||||
5. Zen: orçamento real conservador calculado com tarifa **Zen**, independente de `C_OR`, e saldo/limite confirmados; Go: verificar cota restante **do modelo** nas três janelas e estimar débito pela **tarifa Go**, não por `C_OR`. Quando saldo/restante não puder ser confirmado, não iniciar despacho automático com risco de cobrança; permitir execução manual fora do loop. Claude por assinatura: mostrar `C_OR` só como comparação, contar tokens/limites observáveis e parar ao atingir aviso de plano; sem consumo comparável não afirmar economia entre Opus/Sonnet/Haiku nem converter USD OpenRouter em cobrança. Nenhum fallback Go→saldo Zen sem autorização explícita separada; a opção `Use balance` da console pode cobrar automaticamente quando a cota acabar.
|
||||
6. Se seleção mudar, exibir motivo (novo score, preço, disponibilidade, quota, contexto) e não trocar a ferramenta/provider/assinatura silenciosamente. Falha de autenticação, preço obsoleto, métrica ausente, índice diferente, rate-limit ou limite esgotado = `unavailable`/pausa; fallback só para rota previamente permitida que ainda cumpre requisitos e orçamento.
|
||||
|
||||
Na implementação, o ID de modelo OMP é opaco (pode conter `/`); apenas seu `selector` é qualificado pelo provider, e variantes `#esforço` só são elegíveis quando o `thinking` da CLI local anuncia esse esforço. `exact` exige o harness da ferramenta em execução; outra origem exige `proxy` autorizado. Rotas Zen e OMP pay-as-you-go exigem `--budget-usd` explícito antes mesmo de serem selecionadas. Overrides publicados pela API OpenRouter acima de um limiar de tokens entram apenas no preço comparativo do perfil.
|
||||
|
||||
**Exemplo de custo comparativo, não fatura:** perfil hipotético de 50k tokens de entrada sem cache, 150k de leitura de cache, 10k de saída e 0 de escrita dá aproximadamente US$0,0115 no OpenRouter para GPT-6 Luna e US$0,153 para GLM-5.3 nas tarifas-base observadas. A primeira tem nota 41 (`media`), a segunda 54 (`alta`); não chamar Luna de faixa `leve` por ser barata. Não usar esse perfil fictício para cobrar ou fixar o ranking final.
|
||||
|
||||
**Atualização:** cada snapshot contém data UTC e hash. Revalidar catálogo local por sessão, preços antes de aprovar gasto (24h no máximo), quotas antes de cada execução e score ao mudar versão do índice (recaptura pelo menos a cada 30 dias). Dados obsoletos impedem **autoescolha paga**; `models list` ainda os mostra como `stale`. Nenhum refresh automático de AA sem fonte/permissão documentada.
|
||||
|
||||
## 4. Contratos de dados e interface proposta
|
||||
|
||||
Manter a config do projeto em `.dev-workflows.toml` (já lida pelo gate), adicionando seções **sem alterar** `[commands]`, `[thresholds]`, `[spec]` e `[[check]]` existentes. `~/.config` pode conter preferências de conta, nunca chaves em arquivos do projeto. Exemplo de contrato, não arquivo configurado:
|
||||
|
||||
```toml
|
||||
[loop]
|
||||
enabled = false # somente comando explícito inicia experimento
|
||||
harness = "opencode" # claude | opencode | omp; Hermes após adapter real
|
||||
max_iterations = 8
|
||||
max_minutes = 90
|
||||
max_stagnant = 2
|
||||
min_improvement = 0.5
|
||||
max_paid_usd = 0 # >0 só com autorização interativa para Zen/pay-as-you-go
|
||||
|
||||
[loop.goal]
|
||||
name = "qualidade_do_app"
|
||||
direction = "maximize" # minimize também permitido
|
||||
unit = "points" # unidade do produto, não do AA
|
||||
baseline_required = true
|
||||
target = 80.0
|
||||
|
||||
[loop.evaluator]
|
||||
argv = ["python3", "/caminho/fora/do/worktree/evaluate.py", "--json"]
|
||||
timeout_seconds = 600
|
||||
trusted_sha256 = "<hash aprovado do avaliador e testes protegidos>"
|
||||
required_checks = ["test", "lint"]
|
||||
require_gate_verdict = "pass" # incomplete não é sucesso do loop
|
||||
|
||||
[models]
|
||||
benchmark_metric = "aa_coding_agent_index_v1.5"
|
||||
allow_proxy_score = false
|
||||
max_price_age_hours = 24
|
||||
max_score_age_days = 30
|
||||
|
||||
[models.bands]
|
||||
leve = [10, 20]
|
||||
media = [20, 50]
|
||||
alta = [50, 100] # último limite inclusivo
|
||||
|
||||
[models.roles.explorer]
|
||||
band = "leve"
|
||||
max_loss_points = 3
|
||||
|
||||
[models.roles.implementer]
|
||||
band = "media"
|
||||
max_loss_points = 3
|
||||
|
||||
[models.roles.planner]
|
||||
band = "alta"
|
||||
max_loss_points = 2
|
||||
```
|
||||
|
||||
**Contrato de avaliação (stdout exclusivamente JSON, erro em stderr):**
|
||||
|
||||
```json
|
||||
{"schema_version":1,"metric":"qualidade_do_app","value":81.2,"unit":"points","direction":"maximize","checks":{"regression":"pass","security":"pass"},"evidence":["/relatorios/avaliacao.json"]}
|
||||
```
|
||||
|
||||
Apenas o processo controlador interpreta `value`; saída ausente, NaN, métrica/unidade/direção divergente, evidência não verificável, prazo excedido, exit não zero, ou check obrigatório diferente de `pass` é **falha**, nunca score zero. Nomes de caminhos são ilustrativos; o avaliador real e seu alvo são definidos pelo projeto. Repetir baseline e candidato sob o mesmo avaliador, versão, dados e sementes. Expor variabilidade: para avaliador estocástico, fixar sementes/repetições e comparar média/intervalo conforme config do projeto, sem inferir melhoria de ruído.
|
||||
|
||||
**Registro de modelo:** `model_key=(provider,model_id,variant)`, `tool`, `billing_mode`, `openrouter_id`, `openrouter_canonical_slug`, `or_price_per_token={input,output,cache_read?,cache_write?,cache_write_1h?}`, `price_conditions`, `zen_or_go_native_tariff?`, `quota_window_remaining?`, `score={metric_id,version,value?,agent,harness,variant,settings,match_grade,source_url,observed_at}`, `availability={listed,authenticated,entitled,verified_at}`, `source_hash`. Tipos monetários como string decimal, `null` para desconhecido. Não gravar API keys, prompts do app ou dados privados no ranking. Não confundir variantes `max/high` do benchmark com preço base sem estimar seus tokens.
|
||||
|
||||
**Comandos de produto implementados:**
|
||||
|
||||
```text
|
||||
python3 scripts/model_catalog.py sync --tool opencode --providers opencode,opencode-go
|
||||
python3 scripts/model_catalog.py list --tool opencode --show-missing --format csv
|
||||
python3 scripts/model_catalog.py route --tool opencode --role planner --explain
|
||||
python3 scripts/goal_loop.py plan --config .dev-workflows.toml
|
||||
python3 scripts/goal_loop.py run --config .dev-workflows.toml
|
||||
python3 scripts/goal_loop.py status --run <id>
|
||||
python3 scripts/goal_loop.py resume --run <id>
|
||||
python3 scripts/goal_loop.py stop --run <id>
|
||||
```
|
||||
|
||||
**Tabela completa gerada, não preenchida manualmente nesta spec:** uma linha por `(ferramenta, provider, ID, variante)` de **todos** os modelos publicados para Zen e Go no snapshot, inclusive os indisponíveis na conta e os sem nota. Colunas CSV/JSON: `nome`, `tool`, `provider/model#variant`, `openrouter_id`, `preco_or_entrada_usd_1m`, `preco_or_saida_usd_1m`, `preco_or_cache_leitura_usd_1m`, `preco_or_cache_escrita_usd_1m`, `condicao_de_preco`, `coding_index_versao`, `coding_index_nota`, `benchmark_por_componente_se_publicado`, `harness_aa`, `reasoning_aa`, `grau_do_match`, `faixa`, `preco_native_ou_cota`, `disponivel_na_conta`, `motivo_da_exclusao`, `fonte` e `capturado_em`. Reportar cobertura `pontuados/total` por ferramenta/faixa; ordenar ranking só entre notas comparáveis. `n/d` permanece explícito para score ou preço ausente, não omitir linhas. Export não contém credenciais nem dados do usuário.
|
||||
|
||||
`sync` de AA só aceita snapshot manual licenciado/verificado ou API específica oficialmente documentada; não há comando para raspar leaderboard não documentado. `plan` é sem gastos/mutações: imprime fontes, notas e `n/d`, mapeamentos, cotas, perfil de tokens, rotas por papel, baseline/evaluator e riscos. `run` exige aprovação explícita para orçamento pago, comandos com efeitos externos e mudanças de provider; nunca ativar por instalar o plugin. `status` mostra score inicial/melhor/meta, iterações, custo real/estimado separados, snapshots, gate e motivos de pausa. `stop` cancela novas tentativas e não apaga worktrees.
|
||||
|
||||
**Captura reproduzível da tabela:** [`model-catalog-zen-go-2026-09-25.csv`](model-catalog-zen-go-2026-09-25.csv), gerada pelos comandos `sync --tool opencode --providers opencode,opencode-go` e `list --tool opencode --show-missing --format csv` em **2026-09-25T07:18:38Z**. Contém 80 modelos Zen e 42 Go, cobertura AA 0/122 (não foi fornecido snapshot autorizado), oito pareamentos curados de preços OpenRouter e acesso local não confirmado (`opencode models --refresh` falhou para ambos); nenhum deles é automaticamente elegível. Hash do snapshot `1930d91de7992963aec39d4188f36d97a849ead60f4dc91ae66dc4b38c708cd7`; hashes dos corpos das fontes: Zen `1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4`, Go `4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4`, OpenRouter `0d552d4f08b7c600c0aa2d8f8b926e1356856fd81a6c9ad0c02c5a3f3a7eab3c`. SHA-256 do CSV `80e2930877b20e5c47d4fd18f394c17a4683b257ca715496e91c63520d528230`. Rodar `sync` novamente atualiza preços/listas e **não** reproduz necessariamente os mesmos bytes históricos; preserve o snapshot JSON local se precisar reprocessar a captura exata. Os dados públicos de [Zen](https://opencode.ai/zen/v1/models), [Go](https://opencode.ai/zen/go/v1/models) e [OpenRouter](https://openrouter.ai/api/v1/models) não são prova de acesso ou fatura.
|
||||
|
||||
O [snapshot normalizado arquivado](model-catalog-snapshot-2026-09-25/catalog-opencode.json) permite reproduzir a captura **offline**: `python3 scripts/model_catalog.py list --tool opencode --show-missing --format csv --cache-dir docs/model-catalog-snapshot-2026-09-25 --as-of-utc 2026-09-25T07:18:38Z` gera exatamente o CSV acima (SHA-256 comprovado na execução CLI). Sem `--as-of-utc`, a obsolescência é recalculada com o horário atual e os bytes históricos podem mudar. SHA-256 dos bytes do JSON `a5a1ec44587976a9fe96399bb275662ccdf90e1d8149ac7857f03ae05bb83dcc`; o arquivo contém os 122 registros Zen/Go normalizados e 460 entradas públicas OpenRouter, sem evidência privada de conta nem AA licenciada.
|
||||
|
||||
**Importação auditada pelo usuário:** `--aa-snapshot` exige `metric_id=aa_coding_agent_index`, `version=1.5`, `ingestion=manual_audited`, `terms_accepted=true` **somente após aceitar os termos**, atribuição, URL oficial HTTPS, data UTC e linhas com ID/variante, score, agente, harness, configurações, match e URL da evidência. `--availability-snapshot` exige `tool`, `ingestion=manual_audited`, `user_confirmed=true`, data UTC e linhas com ID exato, `local_listed`, `authenticated`, `entitled`, `verified_at` e origem; não inclua tokens/chaves. `--price-overrides` pode incluir `native_tariffs` indexado por `model_key`: Zen/OMP payg exige tarifa USD/token, `available_balance_usd` e confirmação de conta/saldo recente; Go exige `quota_window_remaining` e `quota_window_debit` por modelo em `5h`, `weekly`, `monthly` e `use_balance_fallback=false`. Sem evidência atual das permissões e do perfil de tokens da tarefa, o roteador permanece indisponível; o comando `route` nunca invoca modelos.
|
||||
|
||||
**Limite de isolamento:** o agente usa ferramentas por papel e worktree com escrita limitada; seu ponteiro `.git` é montado somente para leitura e revalidado antes de operações Git do controlador. A CLI compartilha a rede para alcançar providers e pode ler arquivos do host. O avaliador e o gate usam `bubblewrap --unshare-net`; não confundir com filtro de egress do agente nem isolamento contra código malicioso do próprio usuário. Sem cotas/saldo auditados recentes, uso real confiável e autorização interativa, qualquer caminho cobrado é bloqueado ou pausado; uma chamada paga sem consumo reportado não pode ser retomada automaticamente. Previsões não impõem teto rígido de faturamento. A prova de execução sem gasto usa uma CLI Claude fictícia e verificador fictício em repositório descartável: baseline 0 → score 2 em duas iterações com gate aprovado; não comprova cobrança/entitlement de uma conta real.
|
||||
|
||||
### Adaptadores e responsabilidades
|
||||
|
||||
- **Núcleo Python sem SDK proprietário:** descoberta, tabela/relatório, política, estado transacional do loop e leitura do `gate.json`. Processo do controlador fora da pasta editável pelo agente; metadados por execução fora do worktree e artefatos `.dev-workflows/` ignorados para Git quando gerados no projeto. Sem polling/serviço silencioso.
|
||||
- **Claude Code:** usar `claude -p --model` e subagentes `agents/*.md` com aliases permitidos `haiku|sonnet|opus|inherit`; não escrever `provider/model` incompatível no frontmatter canônico. Instalação via plugin existente; preservar hooks e autorização da ferramenta. Resolver versão/modelo da conta antes de anunciar score; `model` alias não prova variante exata. Assinatura não é precificação por token.
|
||||
- **OpenCode:** usar `opencode run --model provider/id#variant --agent ...` conforme versão local. `scripts/install.py` gera agentes em `~/.config/opencode/agents` e plugin de hooks; estender conversão para `model` por papel/conta com **deny-by-default** de ferramentas nos subagentes de leitura, sem herdar permissão `* allow`. Não sobrepor arquivo existente não pertencente ao pacote. O plugin OpenCode apenas avisa no fim: controlador deve bloquear por conta própria em `gate.json`.
|
||||
- **OMP:** adicionar alvo `omp` ao instalador com package/adapter OMP próprio, skills/agentes nativos e papéis `modelRoles` resolvidos no runtime, sem reusar diretamente `hooks/hooks.json` do Claude. OMP descobre agentes de extensões e aceita aliases de papel `@nome` ([docs](https://github.com/can1357/oh-my-pi/blob/main/docs/task-agent-discovery.md)); se papel não estiver configurado, herdar do pai ou bloquear com diagnóstico — nunca criar ID fictício. Hooks só com port nativo e testes; sem eles, controlador externo impõe gate. Não depender do worktree local de um usuário para distribuição.
|
||||
- **Hermes:** instalação atual é só de skills; não alegar suporte a subagentes ou loop até existir executor/adapter testado. Scripts de ranking/report ainda podem operar fora do Hermes.
|
||||
|
||||
## 5. Algoritmo do loop
|
||||
|
||||
```text
|
||||
INICIALIZAR: validar config, meta, avaliador protegido, orçamento, permissões,
|
||||
working tree do usuário e catálogo; obter baseline em sandbox isolado.
|
||||
SE baseline já atinge target E checks/gate exigidos passaram: finalizar sem editar.
|
||||
PARA i em 1..max_iterations, respeitando relógio/orçamento:
|
||||
criar worktree descartável A PARTIR do melhor estado aceito (nunca reset no tree do usuário)
|
||||
planejar UMA hipótese de melhoria com contexto fresco + feedback anterior e papel roteado
|
||||
implementar mudança pequena dentro de escopo e permissões do dev-router
|
||||
verificar arquivos protegidos, ponteiro Git detached e isolamento; criar commit candidato
|
||||
rodar quality-gate --tier N em worktree limpo do commit; validar base, diff, tempo e checks
|
||||
SE fail/incomplete obrigatório: rejeitar e registrar motivo, sem declarar sucesso
|
||||
SENÃO executar avaliador independente em OUTRO worktree limpo do mesmo commit
|
||||
SE score válido, não regrediu e melhorou >= min_improvement: guardar commit como melhor
|
||||
SENÃO: marcar tentativa rejeitada e incrementar estagnação
|
||||
persistir decisão, custo/uso, evidências e hashes atomicamente
|
||||
SE melhor.score atinge target E todos os checks obrigatórios passam: parar com success
|
||||
SE estagnação/limite/cancelamento: parar com estado explícito, não success
|
||||
FIM: produzir patch/worktree do melhor estado; usuário decide se incorpora.
|
||||
```
|
||||
|
||||
Estados da execução: `planned → baselined → running → success|exhausted|paused|cancelled|error`; resultados por tentativa: `accepted|rejected|paused|interrupted`. `success` só vem do avaliador **externo** atual + gate `pass` + invariantes; texto do agente, `<promise>COMPLETE</promise>`, exit zero isolado e `incomplete` nunca são conclusão. Rejeição não apaga evidência nem altera branch do usuário. `resume` confere hash do avaliador, config, snapshot de modelos, branch/base e último estado aceito; se houve edição concorrente do usuário ou atualização de dependência crítica, pausa até decisão. Uma única execução ativa por projeto, escrita atômica com lock; interrupção é retomável sem contabilizar a mesma tentativa duas vezes, salvo chamada paga de consumo indeterminado.
|
||||
|
||||
**Controle de riscos:** agente não pode escrever avaliador/testes ocultos, metas, snapshots de catálogo, relatório de execução ou hashes de aceitação; executar verificação em contexto separado do workspace editável, com checkout limpo dos checks/dados aprovados. Config/linhas de comando, páginas externas e saída do agente são dados não confiáveis, não autorização para instalar dependências, enviar dados, gastar saldo, publicar, executar migrations, modificar serviços de produção ou desativar gate. Para ações com efeito externo, legal, financeiro ou dados sensíveis, interromper para aprovação explícita do usuário no momento da ação. Nenhum `--dangerously-skip-permissions`, `--auto` indiscriminado ou force-push. Guardar diffs sem segredos e limitar retenção de logs.
|
||||
|
||||
## 6. Requisitos verificáveis (EARS)
|
||||
|
||||
- **REQ-001** WHEN `models sync` for chamado, THE SYSTEM SHALL identificar IDs exatos e variantes dos catálogos Zen/Go e da ferramenta local sem presumir acesso pela lista pública.
|
||||
- **REQ-002** WHEN preços forem importados, THE SYSTEM SHALL preservar cada preço OpenRouter por token, cache read/write separados, condições, ID, URL, data e unidade USD/1M apresentada.
|
||||
- **REQ-003** IF um preço/campo de cache estiver ausente, THEN THE SYSTEM SHALL mostrá-lo como `n/d` e impedir custo comparativo inválido para o perfil afetado.
|
||||
- **REQ-004** WHEN uma nota AA for importada, THE SYSTEM SHALL preservar índice, versão, harness, agente, variante, configurações, pontuação, proveniência e data.
|
||||
- **REQ-005** IF índice, variante, versão, origem ou correspondência não forem compatíveis, THEN THE SYSTEM SHALL rejeitar a comparação/roteamento pontuado, sem substituir por Intelligence Index.
|
||||
- **REQ-006** WHEN `models list` for chamado, THE SYSTEM SHALL listar modelos disponíveis e indisponíveis, ID por ferramenta, faixas fixas, preço entrada/saída/cache, nota ou `n/d`, licença de cobrança e justificativa da elegibilidade.
|
||||
- **REQ-007** WHEN escolher modelo por papel, THE SYSTEM SHALL filtrar requisitos, nota/intervalo e orçamento, selecionar menor custo dentro do platô de pontos do melhor da faixa e explicar escolhas/empates.
|
||||
- **REQ-008** IF uma faixa estiver vazia, THEN THE SYSTEM SHALL reportar ausência, oferecer opção explícita rotulada (ou rota fixa `unranked`) e nunca fabricar uma nota para preencher a faixa.
|
||||
- **REQ-009** IF provider, ID ou conta não estiverem efetivamente disponíveis, THEN THE SYSTEM SHALL não despachar o modelo, ainda que conste das docs públicas.
|
||||
- **REQ-010** IF uma rota Go exceder uma janela de cota ou cair em `Use balance` sem autorização de pagamento, THEN THE SYSTEM SHALL pausar antes da chamada cobrada.
|
||||
- **REQ-011** WHEN mostrar custo, THE SYSTEM SHALL distinguir `C_OR` de referência, cobrança Zen, débito de cota Go e assinatura Claude; tokens estimados não são fatura.
|
||||
- **REQ-012** IF metadados pagos estiverem expirados ou indisponíveis, THEN THE SYSTEM SHALL bloquear seleção automática cobrada, mantendo relatório legível.
|
||||
- **REQ-013** WHEN `plan` for chamado, THE SYSTEM SHALL validar objetivo, avaliador, isolamento, modelos, gate, riscos e orçamento **sem** executar agentes, criar branches nem gastar tokens.
|
||||
- **REQ-014** IF a base do projeto tiver mudanças do usuário, THEN THE SYSTEM SHALL evitar apagá-las ou absorvê-las sem consentimento e exigir snapshot/base deliberados.
|
||||
- **REQ-015** WHEN `run` for autorizado, THE SYSTEM SHALL medir baseline pelo mesmo avaliador protegido e contexto usado nas tentativas.
|
||||
- **REQ-016** WHILE o loop estiver ativo, THE SYSTEM SHALL limitar tentativas por iteração, tempo, custo, estagnação e permissões; manter hipótese/contexto novo por iteração.
|
||||
- **REQ-017** WHEN um agente modificar código, THE SYSTEM SHALL rodar o tier do `dev-router` e validar `gate.json` atual e checks obrigatórios antes de medir sucesso.
|
||||
- **REQ-018** IF gate `fail` ou `incomplete` obrigatório, regressão, score inválido ou tampering ocorrer, THEN THE SYSTEM SHALL rejeitar a tentativa com evidência, sem alterar o melhor estado.
|
||||
- **REQ-019** WHEN houver melhoria válida, THE SYSTEM SHALL promover somente o estado isolado vencedor e registrar score, dados, gate, custo e hashes que sustentam a decisão.
|
||||
- **REQ-020** IF o score atingir a meta com checks válidos, THEN THE SYSTEM SHALL parar como `success` e oferecer patch/worktree, **sem** aplicar/publicar na branch do usuário.
|
||||
- **REQ-021** IF limite, cancelamento ou erro ocorrer primeiro, THEN THE SYSTEM SHALL parar como `exhausted|cancelled|error` com melhor evidência e sem afirmar que atingiu a meta.
|
||||
- **REQ-022** WHEN `resume` for chamado, THE SYSTEM SHALL verificar invariantes/hashes e retomar sem duplicar gastos/tentativas; mudança concorrente exige pausa.
|
||||
- **REQ-023** WHILE qualquer adapter estiver ativo, THE SYSTEM SHALL preservar restrições de ferramentas/hook/gate aplicáveis e nunca promover permissões de agentes de leitura por herança.
|
||||
- **REQ-024** WHEN gerar documentação ou ranking com dados AA, THE SYSTEM SHALL atribuir fonte e respeitar seus termos, mantendo métricas de índices diferentes em tabelas separadas.
|
||||
|
||||
## 7. Plano de implementação e prova de aceitação
|
||||
|
||||
| Ordem | Entrega e locais previstos | Requisitos | Prova comportamental |
|
||||
|---|---|---|---|
|
||||
| 1 | `scripts/model_catalog.py` + módulo pequeno de parsing em `scripts/` e fixtures em `qa/` | 001–006, 011, 012, 024 | Importar JSON oficial OpenRouter e snapshot AA consentido; testar preço com/sem cache, 1M, override 272k, desambiguação de alias e índices não misturados; `models list` real sem chave AA quando snapshot local existe. |
|
||||
| 2 | `scripts/model_router.py`, contrato TOML e `qa/test_model_router.py` | 007–012 | Matriz Zen vs Go vs Claude/OMP; falta de score/faixa vazia, custo no platô, proxy, quota Go 5h/semana/mês, queda para saldo Zen negada, empate determinístico. Teste de integração com catálogo CLI local, sem despachar modelo pago. |
|
||||
| 3 | `scripts/goal_loop.py`, estado isolado e `qa/test_goal_loop.py` | 013–022 | Projeto temporário com avaliador que mede qualidade real: baseline abaixo, primeira tentativa melhora, segunda atinge meta; gate atual obrigatório; regressão/`incomplete`/tampering rejeitados; retomada, limite e árvore de trabalho suja preservada. Teste executa CLI e inspeciona worktrees e relatórios, não somente mocks. |
|
||||
| 4 | Converter por ferramenta em `scripts/install.py`, `adapters/opencode/`, novo `adapters/omp/`, `agents/`, `skills/dev-router/` | 001, 007, 009, 023 | Smoke de instalação e descoberta dos 7 agentes por runtime, modelos resolvidos e ferramenta de leitura sem permissão de escrita; OpenCode warning não substitui veredito do controlador; Hermes segue explicitamente skills-only. |
|
||||
| 5 | README em inglês/português, `docs/SKILLS.md`, `CHANGELOG.md`, testes CI | Todos | Quickstart com `plan`/`run`/`resume`, fontes/atribuição, preços dinâmicos, caso `n/d` e gates; `qa/lint_skills.py`, `qa/test_gate.py` e novos testes verdes. |
|
||||
|
||||
**Critérios de pronto do produto:** um projeto descartável atinge meta numérica apenas após duas iterações comprovadas; falha de gate nunca encerra como sucesso; Zen e Go escolhem listas distintas sem cobrança cruzada; preço OpenRouter e tarifa real aparecem separados; modelos sem benchmark não entram no ranking por nota; Claude por assinatura não recebe custo fictício; estado retoma após interrupção; nenhuma branch/arquivo do usuário é destruído. Testar também um caso em que meta não é atingida antes de `max_iterations`. Roteamento sozinho não conta como entrega do loop, nem o inverso.
|
||||
|
||||
**Migração e reversão:** manter o comportamento atual por padrão (`[loop].enabled=false`, agentes existentes preservados); ativar explicitamente por projeto e ferramenta após diagnóstico; distribuir adapter OMP nativo sem presumir o worktree local; remover/corrigir caminhos antigos de modelo ao ativar roteamento em cada runtime. Desativar execução retornando `enabled=false` e usar papéis estáticos atuais. Não excluir relatórios/worktrees automaticamente; fornecer passos de recuperação e escolha de aplicação do patch. Spec nível 3: revisar com o usuário antes da implementação, conforme `agents/planner.md`.
|
||||
|
||||
## 8. Prompt original de implementação (registro histórico)
|
||||
|
||||
> Implemente no `dev-workflows` a spec `docs/goal-loop-model-routing-spec.md`, sem reduzir nenhum REQ-001…024. Preserve `dev-router`, quality gate e hooks atuais. Comece pelo importador de catálogo com proveniência e pelo roteador, depois integre Claude Code, OpenCode e OMP, e por fim o controlador de loop com worktrees/avaliador independente; Hermes permanece skills-only até existir adapter real. Use faixas **fixas de nota de código** `[10,20)`, `[20,50)`, `[50,100]`, não quantis relativos. Descubra apenas modelos acessíveis à conta/ferramenta. Cruze IDs curados Zen/Go/Claude/OMP com tarifas **OpenRouter** de entrada, saída, leitura e escrita de cache; marque campos faltantes `n/d` e informe os preços OpenRouter como **referência**, distintos da cobrança Zen, cota Go e assinatura Claude. Use exclusivamente um índice/versionamento de programação da Artificial Analysis por ranking; atribua fonte, guarde variante/harness e jamais converta nota do Intelligence Index em nota de código. Mostre ranking auditável por papel, escolha o modelo de menor custo dentro do platô dos melhores da faixa, bloqueie seleção cobrada sem autorização e nunca faça fallback Go→Zen cobrado sem aceite. No loop, estabeleça baseline, aceite apenas melhoria numérica com gate/checks obrigatórios aprovados e avaliador protegido, pare ao atingir alvo ou orçamento e preserve o trabalho do usuário. Entregue testes comportamentais, smoke CLI real, docs, tabela reproduzível com IDs/notas/preços `n/d` e limites honestamente reportados. Não apresente instalação de skills como se fosse o loop em funcionamento.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,123 +0,0 @@
|
||||
nome,tool,model_key,openrouter_id,preco_or_entrada_usd_1m,preco_or_saida_usd_1m,preco_or_cache_leitura_usd_1m,preco_or_cache_escrita_usd_1m,preco_or_cache_escrita_1h_usd_1m,condicao_de_preco,coding_index_versao,coding_index_nota,benchmark_por_componente_se_publicado,harness_aa,reasoning_aa,grau_do_match,faixa,preco_native_ou_cota,disponivel_na_conta,motivo_da_exclusao,fonte,capturado_em,stale,stale_reasons,availability,billing_mode,source_hash,or_price_per_token,score
|
||||
deepseek-flash,opencode,opencode-go/deepseek-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
deepseek-v4-flash,opencode,opencode-go/deepseek-v4-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
deepseek-v4-flash-vision-exp,opencode,opencode-go/deepseek-v4-flash-vision-exp,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
deepseek-v4-pro,opencode,opencode-go/deepseek-v4-pro,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
deepseek-v4.1-flash,opencode,opencode-go/deepseek-v4.1-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
glm-5,opencode,opencode-go/glm-5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
glm-5.1,opencode,opencode-go/glm-5.1,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
glm-5.2,opencode,opencode-go/glm-5.2,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
glm-5.3,opencode,opencode-go/glm-5.3,z-ai/glm-5.3,1.4000000,4.4000000,0.26000000,n/d,n/d,"{""context_length"": 1310720, ""per_request_limits"": null, ""pricing_metadata"": {}, ""top_provider"": {""context_length"": 1048575, ""is_moderated"": false, ""max_completion_tokens"": 943717}, ""unsupported_negative_prices"": {}}",n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": ""https://openrouter.ai/api/v1/models""}",2026-09-25T07:18:38Z,False,[],"{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": ""0.00000026"", ""cache_write"": null, ""cache_write_1h"": null, ""cache_write_24h"": null, ""cache_write_5m"": null, ""input"": ""0.0000014"", ""output"": ""0.0000044""}",n/d
|
||||
glm-5.3-flash,opencode,opencode-go/glm-5.3-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.6-luna,opencode,opencode-go/gpt-5.6-luna,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-6-luna,opencode,opencode-go/gpt-6-luna,openai/gpt-6-luna,0.1000000,0.5000000,0.01000000,0.125000000,n/d,"{""context_length"": 1050000, ""per_request_limits"": null, ""pricing_metadata"": {""overrides"": [{""completion"": ""0.00000075"", ""input_cache_read"": ""0.00000002"", ""input_cache_write"": ""0.00000025"", ""min_prompt_tokens"": 272000, ""prompt"": ""0.0000002""}], ""web_search"": ""0.01""}, ""top_provider"": {""context_length"": 1050000, ""is_moderated"": true, ""max_completion_tokens"": 128000}, ""unsupported_negative_prices"": {}}",n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": ""https://openrouter.ai/api/v1/models""}",2026-09-25T07:18:38Z,False,[],"{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": ""0.00000001"", ""cache_write"": ""0.000000125"", ""cache_write_1h"": null, ""cache_write_24h"": null, ""cache_write_5m"": null, ""input"": ""0.0000001"", ""output"": ""0.0000005""}",n/d
|
||||
grok-4.5,opencode,opencode-go/grok-4.5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
grok-4.6,opencode,opencode-go/grok-4.6,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
grok-4.7,opencode,opencode-go/grok-4.7,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
hy3,opencode,opencode-go/hy3,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
hy3-preview,opencode,opencode-go/hy3-preview,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
hy4-preview,opencode,opencode-go/hy4-preview,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
kimi-k2.5,opencode,opencode-go/kimi-k2.5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
kimi-k2.6,opencode,opencode-go/kimi-k2.6,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
kimi-k2.7-code,opencode,opencode-go/kimi-k2.7-code,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
kimi-k3,opencode,opencode-go/kimi-k3,moonshotai/kimi-k3,3.000000,15.000000,0.3000000,n/d,n/d,"{""context_length"": 1048576, ""per_request_limits"": null, ""pricing_metadata"": {}, ""top_provider"": {""context_length"": 1048576, ""is_moderated"": false, ""max_completion_tokens"": 943718}, ""unsupported_negative_prices"": {}}",n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": ""https://openrouter.ai/api/v1/models""}",2026-09-25T07:18:38Z,False,[],"{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": ""0.0000003"", ""cache_write"": null, ""cache_write_1h"": null, ""cache_write_24h"": null, ""cache_write_5m"": null, ""input"": ""0.000003"", ""output"": ""0.000015""}",n/d
|
||||
longcat-2.0,opencode,opencode-go/longcat-2.0,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
mimo-v2-omni,opencode,opencode-go/mimo-v2-omni,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
mimo-v2-pro,opencode,opencode-go/mimo-v2-pro,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
mimo-v2.5,opencode,opencode-go/mimo-v2.5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
mimo-v2.5-pro,opencode,opencode-go/mimo-v2.5-pro,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
mimo-v2.6-flash,opencode,opencode-go/mimo-v2.6-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
mimo-v2.6-pro,opencode,opencode-go/mimo-v2.6-pro,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
minimax-m2.5,opencode,opencode-go/minimax-m2.5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
minimax-m2.7,opencode,opencode-go/minimax-m2.7,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
minimax-m3,opencode,opencode-go/minimax-m3,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
muse-spark-1.2-contributor,opencode,opencode-go/muse-spark-1.2-contributor,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
muse-spark-1.3-contributor,opencode,opencode-go/muse-spark-1.3-contributor,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
omen-alpha,opencode,opencode-go/omen-alpha,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
qwen3.5-plus,opencode,opencode-go/qwen3.5-plus,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
qwen3.6-plus,opencode,opencode-go/qwen3.6-plus,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
qwen3.7-max,opencode,opencode-go/qwen3.7-max,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
qwen3.7-plus,opencode,opencode-go/qwen3.7-plus,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
qwen3.8-flash,opencode,opencode-go/qwen3.8-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
qwen3.8-max,opencode,opencode-go/qwen3.8-max,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
space-bunny-free,opencode,opencode-go/space-bunny-free,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/go/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",go_quota,4f3c2a14e77fa546f5ba833d85cca088918ab2ade7e4a0eaf8c5d9a478b48ae4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
big-pickle,opencode,opencode/big-pickle,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-fable-5,opencode,opencode/claude-fable-5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-fable-5-1,opencode,opencode/claude-fable-5-1,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-haiku-4-5,opencode,opencode/claude-haiku-4-5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-opus-4-5,opencode,opencode/claude-opus-4-5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-opus-4-6,opencode,opencode/claude-opus-4-6,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-opus-4-7,opencode,opencode/claude-opus-4-7,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-opus-4-8,opencode,opencode/claude-opus-4-8,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-opus-5,opencode,opencode/claude-opus-5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-opus-5-5,opencode,opencode/claude-opus-5-5,anthropic/claude-opus-5.5,4.000000,20.00000,0.2000000,5.000000,8.000000,"{""context_length"": 1000000, ""per_request_limits"": null, ""pricing_metadata"": {""web_search"": ""0.01""}, ""top_provider"": {""context_length"": 1000000, ""is_moderated"": true, ""max_completion_tokens"": 128000}, ""unsupported_negative_prices"": {}}",n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": ""https://openrouter.ai/api/v1/models""}",2026-09-25T07:18:38Z,False,[],"{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": ""0.0000002"", ""cache_write"": ""0.000005"", ""cache_write_1h"": ""0.000008"", ""cache_write_24h"": null, ""cache_write_5m"": null, ""input"": ""0.000004"", ""output"": ""0.00002""}",n/d
|
||||
claude-sonnet-4,opencode,opencode/claude-sonnet-4,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-sonnet-4-5,opencode,opencode/claude-sonnet-4-5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-sonnet-4-6,opencode,opencode/claude-sonnet-4-6,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
claude-sonnet-5,opencode,opencode/claude-sonnet-5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
deepseek-v4-flash,opencode,opencode/deepseek-v4-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
deepseek-v4-flash-free,opencode,opencode/deepseek-v4-flash-free,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
deepseek-v4-flash-vision-exp,opencode,opencode/deepseek-v4-flash-vision-exp,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
deepseek-v4-pro,opencode,opencode/deepseek-v4-pro,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
deepseek-v4.1-flash,opencode,opencode/deepseek-v4.1-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gemini-3-flash,opencode,opencode/gemini-3-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gemini-3.1-pro,opencode,opencode/gemini-3.1-pro,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gemini-3.5-flash,opencode,opencode/gemini-3.5-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gemini-3.5-flash-lite,opencode,opencode/gemini-3.5-flash-lite,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gemini-3.6-flash,opencode,opencode/gemini-3.6-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gemini-3.7-flash,opencode,opencode/gemini-3.7-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gemini-3.8-flash,opencode,opencode/gemini-3.8-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
glm-5,opencode,opencode/glm-5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
glm-5.1,opencode,opencode/glm-5.1,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
glm-5.2,opencode,opencode/glm-5.2,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
glm-5.3,opencode,opencode/glm-5.3,z-ai/glm-5.3,1.4000000,4.4000000,0.26000000,n/d,n/d,"{""context_length"": 1310720, ""per_request_limits"": null, ""pricing_metadata"": {}, ""top_provider"": {""context_length"": 1048575, ""is_moderated"": false, ""max_completion_tokens"": 943717}, ""unsupported_negative_prices"": {}}",n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": ""https://openrouter.ai/api/v1/models""}",2026-09-25T07:18:38Z,False,[],"{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": ""0.00000026"", ""cache_write"": null, ""cache_write_1h"": null, ""cache_write_24h"": null, ""cache_write_5m"": null, ""input"": ""0.0000014"", ""output"": ""0.0000044""}",n/d
|
||||
glm-5.3-flash,opencode,opencode/glm-5.3-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5,opencode,opencode/gpt-5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5-codex,opencode,opencode/gpt-5-codex,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5-nano,opencode,opencode/gpt-5-nano,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.1,opencode,opencode/gpt-5.1,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.1-codex,opencode,opencode/gpt-5.1-codex,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.1-codex-max,opencode,opencode/gpt-5.1-codex-max,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.1-codex-mini,opencode,opencode/gpt-5.1-codex-mini,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.2,opencode,opencode/gpt-5.2,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.2-codex,opencode,opencode/gpt-5.2-codex,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.3-codex,opencode,opencode/gpt-5.3-codex,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.3-codex-spark,opencode,opencode/gpt-5.3-codex-spark,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.4,opencode,opencode/gpt-5.4,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.4-mini,opencode,opencode/gpt-5.4-mini,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.4-nano,opencode,opencode/gpt-5.4-nano,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.4-pro,opencode,opencode/gpt-5.4-pro,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.5,opencode,opencode/gpt-5.5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.5-pro,opencode,opencode/gpt-5.5-pro,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.6-luna,opencode,opencode/gpt-5.6-luna,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.6-sol,opencode,opencode/gpt-5.6-sol,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-5.6-terra,opencode,opencode/gpt-5.6-terra,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-6-astra,opencode,opencode/gpt-6-astra,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
gpt-6-luna,opencode,opencode/gpt-6-luna,openai/gpt-6-luna,0.1000000,0.5000000,0.01000000,0.125000000,n/d,"{""context_length"": 1050000, ""per_request_limits"": null, ""pricing_metadata"": {""overrides"": [{""completion"": ""0.00000075"", ""input_cache_read"": ""0.00000002"", ""input_cache_write"": ""0.00000025"", ""min_prompt_tokens"": 272000, ""prompt"": ""0.0000002""}], ""web_search"": ""0.01""}, ""top_provider"": {""context_length"": 1050000, ""is_moderated"": true, ""max_completion_tokens"": 128000}, ""unsupported_negative_prices"": {}}",n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": ""https://openrouter.ai/api/v1/models""}",2026-09-25T07:18:38Z,False,[],"{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": ""0.00000001"", ""cache_write"": ""0.000000125"", ""cache_write_1h"": null, ""cache_write_24h"": null, ""cache_write_5m"": null, ""input"": ""0.0000001"", ""output"": ""0.0000005""}",n/d
|
||||
gpt-6-sol,opencode,opencode/gpt-6-sol,openai/gpt-6-sol,2.000000,10.00000,0.2000000,2.5000000,n/d,"{""context_length"": 1050000, ""per_request_limits"": null, ""pricing_metadata"": {""overrides"": [{""completion"": ""0.000015"", ""input_cache_read"": ""0.0000004"", ""input_cache_write"": ""0.000005"", ""min_prompt_tokens"": 272000, ""prompt"": ""0.000004""}], ""web_search"": ""0.01""}, ""top_provider"": {""context_length"": 1050000, ""is_moderated"": true, ""max_completion_tokens"": 128000}, ""unsupported_negative_prices"": {}}",n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": ""https://openrouter.ai/api/v1/models""}",2026-09-25T07:18:38Z,False,[],"{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": ""0.0000002"", ""cache_write"": ""0.0000025"", ""cache_write_1h"": null, ""cache_write_24h"": null, ""cache_write_5m"": null, ""input"": ""0.000002"", ""output"": ""0.00001""}",n/d
|
||||
grok-4.5,opencode,opencode/grok-4.5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
grok-4.6,opencode,opencode/grok-4.6,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
grok-4.7,opencode,opencode/grok-4.7,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
grok-build-0.1,opencode,opencode/grok-build-0.1,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
jev-1.13,opencode,opencode/jev-1.13,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
jev-1.13-free,opencode,opencode/jev-1.13-free,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
kimi-k2.5,opencode,opencode/kimi-k2.5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
kimi-k2.6,opencode,opencode/kimi-k2.6,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
kimi-k2.7-code,opencode,opencode/kimi-k2.7-code,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
kimi-k3,opencode,opencode/kimi-k3,moonshotai/kimi-k3,3.000000,15.000000,0.3000000,n/d,n/d,"{""context_length"": 1048576, ""per_request_limits"": null, ""pricing_metadata"": {}, ""top_provider"": {""context_length"": 1048576, ""is_moderated"": false, ""max_completion_tokens"": 943718}, ""unsupported_negative_prices"": {}}",n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": ""https://openrouter.ai/api/v1/models""}",2026-09-25T07:18:38Z,False,[],"{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": ""0.0000003"", ""cache_write"": null, ""cache_write_1h"": null, ""cache_write_24h"": null, ""cache_write_5m"": null, ""input"": ""0.000003"", ""output"": ""0.000015""}",n/d
|
||||
ling-3.0-flash-fin-free,opencode,opencode/ling-3.0-flash-fin-free,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
mimo-v2.5-free,opencode,opencode/mimo-v2.5-free,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
mimo-v2.6-flash-free,opencode,opencode/mimo-v2.6-flash-free,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
minimax-m2.5,opencode,opencode/minimax-m2.5,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
minimax-m2.7,opencode,opencode/minimax-m2.7,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
minimax-m3,opencode,opencode/minimax-m3,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
muse-spark-1.2,opencode,opencode/muse-spark-1.2,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
muse-spark-1.2-contributor-free,opencode,opencode/muse-spark-1.2-contributor-free,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
muse-spark-1.3,opencode,opencode/muse-spark-1.3,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
muse-spark-1.3-contributor-free,opencode,opencode/muse-spark-1.3-contributor-free,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
nemotron-3-ultra-free,opencode,opencode/nemotron-3-ultra-free,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
nemotron-3.5-lightning-free,opencode,opencode/nemotron-3.5-lightning-free,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
qwen3.5-plus,opencode,opencode/qwen3.5-plus,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
qwen3.6-plus,opencode,opencode/qwen3.6-plus,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
qwen3.8-flash,opencode,opencode/qwen3.8-flash,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
space-bunny-free,opencode,opencode/space-bunny-free,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,n/d,descoberta_local_falhou,"{""benchmark"": null, ""catalog"": ""https://opencode.ai/zen/v1/models"", ""openrouter"": null}",2026-09-25T07:18:38Z,True,"[""preco_referencial_ausente""]","{""account_verified_at"": null, ""authenticated"": null, ""balance_confirmed"": null, ""balance_verified_at"": null, ""entitled"": null, ""evidence_hash"": null, ""listed"": true, ""local_listed"": null, ""local_verified_at"": null, ""source"": null, ""verified_at"": null}",zen_balance,1b8d7a3dd18412473419eed98e32be003db5307754a1c5ab0f491ff33b4806a4,"{""cache_read"": null, ""cache_write"": null, ""input"": null, ""output"": null}",n/d
|
||||
|
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/bash_guard.py\"" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/post_edit.py\"", "timeout": 60 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/stop_gate.py\"" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
{
|
||||
"workflows": [
|
||||
{
|
||||
"name": "wf-frontend",
|
||||
"score": 385.5,
|
||||
"skills_referenciadas": [
|
||||
"animate",
|
||||
"anti-ai-slop",
|
||||
"avoid-ai-writing",
|
||||
"claude-design",
|
||||
"design-md",
|
||||
"frontend-award-tier",
|
||||
"frontend-design",
|
||||
"hermaguard",
|
||||
"html-css-print-engineer",
|
||||
"pdf-generator",
|
||||
"pdf-render-qa",
|
||||
"pdf-visual-designer",
|
||||
"popular-web-designs",
|
||||
"professional-pdf-director",
|
||||
"sketch",
|
||||
"vercel-composition-patterns",
|
||||
"vercel-react-best-practices",
|
||||
"web-design"
|
||||
],
|
||||
"criteria": {
|
||||
"frontmatter": 5,
|
||||
"when_to_use": 8,
|
||||
"num_fases": 6,
|
||||
"regras_duras": 10,
|
||||
"n_regras": 9,
|
||||
"fase1": 16,
|
||||
"fase2": 16,
|
||||
"fase3": 16,
|
||||
"cobertura_fases": 20,
|
||||
"skills_validas": 15,
|
||||
"fase_rotulos": 6,
|
||||
"fase_acao": 6,
|
||||
"fase_revisao": 6,
|
||||
"anti_slop": 4.5,
|
||||
"cmd_blocos": 8,
|
||||
"cmd_reais": 5,
|
||||
"sec_criterios": 8,
|
||||
"sec_verificar": 6,
|
||||
"sec_nao_regressao": 6,
|
||||
"tamanho": 8,
|
||||
"testes_referidos": 5,
|
||||
"passos_total": 30,
|
||||
"verificacao_total": 24,
|
||||
"comandos_reais": 20,
|
||||
"profundidade": 10,
|
||||
"por_fase_verif": 18,
|
||||
"por_fase_cmd": 18,
|
||||
"por_fase_antip": 12,
|
||||
"por_fase_aceite": 12,
|
||||
"exemplos": 10,
|
||||
"exemplos_count": 12,
|
||||
"artefatos": 14,
|
||||
"fluxo": 8,
|
||||
"entradas_saidas": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wf-backend",
|
||||
"score": 424.0,
|
||||
"skills_referenciadas": [
|
||||
"api-security-review",
|
||||
"auth-rbac-scaffold",
|
||||
"bola-detector",
|
||||
"dependency-cve-audit",
|
||||
"fastapi-uvicorn-wiring",
|
||||
"hermaguard",
|
||||
"injection-checker",
|
||||
"network-audit",
|
||||
"openapi-hardener",
|
||||
"rest-graphql-debug",
|
||||
"secure-coding",
|
||||
"security-best-practices",
|
||||
"security-test-generator",
|
||||
"software-code-quality",
|
||||
"test-driven-development"
|
||||
],
|
||||
"criteria": {
|
||||
"frontmatter": 5,
|
||||
"when_to_use": 8,
|
||||
"num_fases": 8,
|
||||
"regras_duras": 10,
|
||||
"n_regras": 10,
|
||||
"fase1": 15,
|
||||
"fase2": 15,
|
||||
"fase3": 16,
|
||||
"fase4": 16,
|
||||
"cobertura_fases": 20,
|
||||
"skills_validas": 15,
|
||||
"fase_rotulos": 6,
|
||||
"fase_acao": 6,
|
||||
"fase_revisao": 6,
|
||||
"anti_slop": 5.0,
|
||||
"cmd_blocos": 8,
|
||||
"cmd_reais": 5,
|
||||
"sec_criterios": 8,
|
||||
"sec_verificar": 6,
|
||||
"sec_nao_regressao": 6,
|
||||
"tamanho": 8,
|
||||
"testes_referidos": 5,
|
||||
"passos_total": 30,
|
||||
"verificacao_total": 24,
|
||||
"comandos_reais": 20,
|
||||
"profundidade": 13,
|
||||
"por_fase_verif": 22,
|
||||
"por_fase_cmd": 24,
|
||||
"por_fase_antip": 16,
|
||||
"por_fase_aceite": 16,
|
||||
"exemplos": 10,
|
||||
"exemplos_count": 12,
|
||||
"artefatos": 14,
|
||||
"fluxo": 8,
|
||||
"entradas_saidas": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wf-architecture",
|
||||
"score": 413.7,
|
||||
"skills_referenciadas": [
|
||||
"architecture-diagram",
|
||||
"avoid-ai-writing",
|
||||
"brainstorming",
|
||||
"c4-architecture",
|
||||
"excalidraw",
|
||||
"hermaguard",
|
||||
"isaqb-architecture-governance",
|
||||
"secure-architecture-governance",
|
||||
"security-threat-model",
|
||||
"system-design",
|
||||
"writing-plans"
|
||||
],
|
||||
"criteria": {
|
||||
"frontmatter": 5,
|
||||
"when_to_use": 8,
|
||||
"num_fases": 8,
|
||||
"regras_duras": 10,
|
||||
"n_regras": 10,
|
||||
"fase1": 15,
|
||||
"fase2": 16,
|
||||
"fase3": 14,
|
||||
"fase4": 16,
|
||||
"cobertura_fases": 20,
|
||||
"skills_validas": 11,
|
||||
"fase_rotulos": 6,
|
||||
"fase_acao": 0,
|
||||
"fase_revisao": 6,
|
||||
"anti_slop": 4.7,
|
||||
"cmd_blocos": 8,
|
||||
"cmd_reais": 5,
|
||||
"sec_criterios": 8,
|
||||
"sec_verificar": 6,
|
||||
"sec_nao_regressao": 6,
|
||||
"tamanho": 8,
|
||||
"testes_referidos": 5,
|
||||
"passos_total": 30,
|
||||
"verificacao_total": 24,
|
||||
"comandos_reais": 20,
|
||||
"profundidade": 13,
|
||||
"por_fase_verif": 24,
|
||||
"por_fase_cmd": 24,
|
||||
"por_fase_antip": 16,
|
||||
"por_fase_aceite": 15,
|
||||
"exemplos": 10,
|
||||
"exemplos_count": 12,
|
||||
"artefatos": 14,
|
||||
"fluxo": 8,
|
||||
"entradas_saidas": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wf-security-review",
|
||||
"score": 390.0,
|
||||
"skills_referenciadas": [
|
||||
"api-security-review",
|
||||
"auth-rbac-scaffold",
|
||||
"bola-detector",
|
||||
"code-review-security",
|
||||
"cve-triage",
|
||||
"dependency-cve-audit",
|
||||
"hermaguard",
|
||||
"injection-checker",
|
||||
"patch-prioritization",
|
||||
"safedeps",
|
||||
"sca-audit",
|
||||
"secrets-scan",
|
||||
"security-threat-model",
|
||||
"web-security-review"
|
||||
],
|
||||
"criteria": {
|
||||
"frontmatter": 5,
|
||||
"when_to_use": 8,
|
||||
"num_fases": 10,
|
||||
"regras_duras": 10,
|
||||
"n_regras": 8,
|
||||
"fase1": 15,
|
||||
"fase2": 16,
|
||||
"fase3": 13,
|
||||
"fase4": 15,
|
||||
"fase5": 15,
|
||||
"cobertura_fases": 20,
|
||||
"skills_validas": 14,
|
||||
"fase_rotulos": 6,
|
||||
"fase_acao": 0,
|
||||
"fase_revisao": 6,
|
||||
"anti_slop": 5.0,
|
||||
"cmd_blocos": 8,
|
||||
"cmd_reais": 5,
|
||||
"sec_criterios": 0,
|
||||
"sec_verificar": 6,
|
||||
"sec_nao_regressao": 6,
|
||||
"tamanho": 8,
|
||||
"testes_referidos": 5,
|
||||
"passos_total": 30,
|
||||
"verificacao_total": 24,
|
||||
"comandos_reais": 16,
|
||||
"profundidade": 15,
|
||||
"por_fase_verif": 23,
|
||||
"por_fase_cmd": 19,
|
||||
"por_fase_antip": 12,
|
||||
"por_fase_aceite": 5,
|
||||
"exemplos": 0,
|
||||
"exemplos_count": 12,
|
||||
"artefatos": 14,
|
||||
"fluxo": 8,
|
||||
"entradas_saidas": 8
|
||||
}
|
||||
}
|
||||
],
|
||||
"total": 1613.2,
|
||||
"max": 400
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compares each vendored skill's recorded commit (skills/NOTICE.md) with its upstream HEAD. Exit 1 if any moved.
|
||||
|
||||
python3 qa/check_upstream.py
|
||||
"""
|
||||
import os, re, subprocess, sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def main():
|
||||
notice = open(os.path.join(ROOT, "skills", "NOTICE.md"), encoding="utf-8").read()
|
||||
repos = {}
|
||||
for row in re.findall(r"^\|\s*`[\w-]+`\s*\|(.+)$", notice, re.M):
|
||||
cols = [c.strip() for c in row.split("|")]
|
||||
repo = re.search(r"([\w.-]+/[\w.-]+)", cols[0])
|
||||
sha = re.search(r"`([0-9a-f]{7,40})`", cols[1])
|
||||
if repo and sha:
|
||||
repos[repo.group(1)] = sha.group(1)
|
||||
moved = 0
|
||||
for repo, sha in sorted(repos.items()):
|
||||
p = subprocess.run(["git", "ls-remote", f"https://github.com/{repo}", "HEAD"], capture_output=True, text=True, timeout=60)
|
||||
head = p.stdout.split()[0] if p.returncode == 0 and p.stdout else None
|
||||
if head is None:
|
||||
print(f"? {repo}: não consegui consultar")
|
||||
elif head.startswith(sha):
|
||||
print(f"ok {repo} {sha}")
|
||||
else:
|
||||
moved += 1
|
||||
print(f"-> {repo}: vendorizado {sha}, upstream {head[:7]} (https://github.com/{repo}/compare/{sha}...{head[:7]})")
|
||||
return 1 if moved else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,126 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Structural lint for the plugin: frontmatter, sizes, references, links and leftover machine-specific paths.
|
||||
|
||||
Run from anywhere: python3 qa/lint_skills.py (exit 1 on any error)
|
||||
"""
|
||||
import json, os, re, sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
MAX_LINES = 150
|
||||
MAX_DESC = 1024
|
||||
FORBIDDEN = [r"~/\.hermes", r"vision_analyze", r"search_files", r"test_quality\.py", r"wf_quality_harness"]
|
||||
MODELS = {"haiku", "sonnet", "opus", "inherit"}
|
||||
TOOLS = {"Read", "Edit", "Write", "Bash", "Grep", "Glob", "WebFetch", "WebSearch", "NotebookEdit", "Agent", "Skill"}
|
||||
errors = []
|
||||
|
||||
|
||||
def err(path, msg):
|
||||
errors.append(f"{os.path.relpath(path, ROOT)}: {msg}")
|
||||
|
||||
|
||||
def frontmatter(path):
|
||||
text = open(path, encoding="utf-8").read()
|
||||
m = re.match(r"---\n(.*?)\n---\n", text, re.S)
|
||||
if not m:
|
||||
err(path, "sem frontmatter")
|
||||
return {}, text
|
||||
fm = m.group(1)
|
||||
meta = dict(re.findall(r"^([\w-]+):[ \t]*(.*)$", fm, re.M))
|
||||
for key in ("uses", "external", "skills"):
|
||||
lst = re.search(rf"^\s*{key}:\s*\[(.*?)\]", fm, re.M)
|
||||
if lst:
|
||||
meta[key] = [s.strip().strip("'\"") for s in lst.group(1).split(",") if s.strip()]
|
||||
return meta, text
|
||||
|
||||
|
||||
def vendored():
|
||||
rows = open(os.path.join(ROOT, "skills", "NOTICE.md"), encoding="utf-8").read()
|
||||
return set(re.findall(r"^\|\s*`([\w-]+)`\s*\|", rows, re.M))
|
||||
|
||||
|
||||
def check_links(path, text):
|
||||
text = re.sub(r"```.*?```|`[^`\n]*`", "", text, flags=re.S)
|
||||
for target in re.findall(r"\]\(([^)\s]+)\)", text):
|
||||
if re.match(r"(https?:|mailto:|#)", target):
|
||||
continue
|
||||
p = os.path.normpath(os.path.join(os.path.dirname(path), target.split("#")[0]))
|
||||
if not os.path.exists(p):
|
||||
err(path, f"link quebrado: {target}")
|
||||
|
||||
|
||||
def main():
|
||||
skills_dir = os.path.join(ROOT, "skills")
|
||||
names = {d for d in os.listdir(skills_dir) if os.path.isfile(os.path.join(skills_dir, d, "SKILL.md"))}
|
||||
third_party = vendored()
|
||||
catalog = open(os.path.join(ROOT, "docs", "SKILLS.md"), encoding="utf-8").read()
|
||||
|
||||
for name in sorted(names):
|
||||
path = os.path.join(skills_dir, name, "SKILL.md")
|
||||
meta, text = frontmatter(path)
|
||||
if meta.get("name") != name:
|
||||
err(path, f"name '{meta.get('name')}' difere da pasta '{name}'")
|
||||
desc = meta.get("description", "")
|
||||
if not desc:
|
||||
err(path, "description vazia")
|
||||
if name in third_party:
|
||||
continue
|
||||
if len(desc) > MAX_DESC:
|
||||
err(path, f"description com {len(desc)} caracteres (máx {MAX_DESC})")
|
||||
lines = text.count("\n")
|
||||
if lines > MAX_LINES:
|
||||
err(path, f"{lines} linhas (máx {MAX_LINES}); mova detalhes para references/")
|
||||
for used in meta.get("uses", []):
|
||||
if used not in names:
|
||||
err(path, f"uses: '{used}' não existe em skills/")
|
||||
for ext in meta.get("external", []):
|
||||
if f"`{ext}`" not in catalog:
|
||||
err(path, f"external: '{ext}' não está em docs/SKILLS.md")
|
||||
for dirpath, _, files in os.walk(os.path.join(skills_dir, name)):
|
||||
for f in files:
|
||||
if f.endswith(".md"):
|
||||
p = os.path.join(dirpath, f)
|
||||
body = open(p, encoding="utf-8").read()
|
||||
check_links(p, body)
|
||||
for pat in FORBIDDEN:
|
||||
if re.search(pat, body):
|
||||
err(p, f"referência específica de máquina: {pat}")
|
||||
|
||||
agents_dir = os.path.join(ROOT, "agents")
|
||||
for f in sorted(os.listdir(agents_dir)):
|
||||
path = os.path.join(agents_dir, f)
|
||||
meta, text = frontmatter(path)
|
||||
if meta.get("name") != f[:-3]:
|
||||
err(path, f"name '{meta.get('name')}' difere do arquivo")
|
||||
if meta.get("model") not in MODELS:
|
||||
err(path, f"model '{meta.get('model')}' inválido")
|
||||
bad = {t.strip() for t in meta.get("tools", "").split(",")} - TOOLS
|
||||
if bad:
|
||||
err(path, f"tools desconhecidas: {', '.join(sorted(bad))}")
|
||||
for s in meta.get("skills", []):
|
||||
if s not in names:
|
||||
err(path, f"skills: '{s}' não existe em skills/")
|
||||
for pat in FORBIDDEN:
|
||||
if re.search(pat, text):
|
||||
err(path, f"referência específica de máquina: {pat}")
|
||||
|
||||
hooks = json.load(open(os.path.join(ROOT, "hooks", "hooks.json")))
|
||||
for groups in hooks["hooks"].values():
|
||||
for g in groups:
|
||||
for h in g["hooks"]:
|
||||
for script in re.findall(r"\$\{CLAUDE_PLUGIN_ROOT\}/([\w/.-]+)", h["command"]):
|
||||
if not os.path.isfile(os.path.join(ROOT, script)):
|
||||
err(os.path.join(ROOT, "hooks", "hooks.json"), f"script inexistente: {script}")
|
||||
|
||||
for doc in ("README.md", "README.pt-BR.md", "CONTRIBUTING.md", os.path.join("docs", "SKILLS.md")):
|
||||
p = os.path.join(ROOT, doc)
|
||||
if os.path.exists(p):
|
||||
check_links(p, open(p, encoding="utf-8").read())
|
||||
|
||||
for e in errors:
|
||||
print("ERRO " + e)
|
||||
print(f"{len(names)} skills, {len(os.listdir(agents_dir))} agentes, {len(errors)} erro(s)")
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,236 @@
|
||||
{
|
||||
"workflows": [
|
||||
{
|
||||
"name": "wf-frontend",
|
||||
"score": 385.5,
|
||||
"skills_referenciadas": [
|
||||
"animate",
|
||||
"anti-ai-slop",
|
||||
"avoid-ai-writing",
|
||||
"claude-design",
|
||||
"design-md",
|
||||
"frontend-award-tier",
|
||||
"frontend-design",
|
||||
"hermaguard",
|
||||
"html-css-print-engineer",
|
||||
"pdf-generator",
|
||||
"pdf-render-qa",
|
||||
"pdf-visual-designer",
|
||||
"popular-web-designs",
|
||||
"professional-pdf-director",
|
||||
"sketch",
|
||||
"vercel-composition-patterns",
|
||||
"vercel-react-best-practices",
|
||||
"web-design"
|
||||
],
|
||||
"criteria": {
|
||||
"frontmatter": 5,
|
||||
"when_to_use": 8,
|
||||
"num_fases": 6,
|
||||
"regras_duras": 10,
|
||||
"n_regras": 9,
|
||||
"fase1": 16,
|
||||
"fase2": 16,
|
||||
"fase3": 16,
|
||||
"cobertura_fases": 20,
|
||||
"skills_validas": 15,
|
||||
"fase_rotulos": 6,
|
||||
"fase_acao": 6,
|
||||
"fase_revisao": 6,
|
||||
"anti_slop": 4.5,
|
||||
"cmd_blocos": 8,
|
||||
"cmd_reais": 5,
|
||||
"sec_criterios": 8,
|
||||
"sec_verificar": 6,
|
||||
"sec_nao_regressao": 6,
|
||||
"tamanho": 8,
|
||||
"testes_referidos": 5,
|
||||
"passos_total": 30,
|
||||
"verificacao_total": 24,
|
||||
"comandos_reais": 20,
|
||||
"profundidade": 10,
|
||||
"por_fase_verif": 18,
|
||||
"por_fase_cmd": 18,
|
||||
"por_fase_antip": 12,
|
||||
"por_fase_aceite": 12,
|
||||
"exemplos": 10,
|
||||
"exemplos_count": 12,
|
||||
"artefatos": 14,
|
||||
"fluxo": 8,
|
||||
"entradas_saidas": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wf-backend",
|
||||
"score": 424.0,
|
||||
"skills_referenciadas": [
|
||||
"api-security-review",
|
||||
"auth-rbac-scaffold",
|
||||
"bola-detector",
|
||||
"dependency-cve-audit",
|
||||
"fastapi-uvicorn-wiring",
|
||||
"hermaguard",
|
||||
"injection-checker",
|
||||
"network-audit",
|
||||
"openapi-hardener",
|
||||
"rest-graphql-debug",
|
||||
"secure-coding",
|
||||
"security-best-practices",
|
||||
"security-test-generator",
|
||||
"software-code-quality",
|
||||
"test-driven-development"
|
||||
],
|
||||
"criteria": {
|
||||
"frontmatter": 5,
|
||||
"when_to_use": 8,
|
||||
"num_fases": 8,
|
||||
"regras_duras": 10,
|
||||
"n_regras": 10,
|
||||
"fase1": 15,
|
||||
"fase2": 15,
|
||||
"fase3": 16,
|
||||
"fase4": 16,
|
||||
"cobertura_fases": 20,
|
||||
"skills_validas": 15,
|
||||
"fase_rotulos": 6,
|
||||
"fase_acao": 6,
|
||||
"fase_revisao": 6,
|
||||
"anti_slop": 5.0,
|
||||
"cmd_blocos": 8,
|
||||
"cmd_reais": 5,
|
||||
"sec_criterios": 8,
|
||||
"sec_verificar": 6,
|
||||
"sec_nao_regressao": 6,
|
||||
"tamanho": 8,
|
||||
"testes_referidos": 5,
|
||||
"passos_total": 30,
|
||||
"verificacao_total": 24,
|
||||
"comandos_reais": 20,
|
||||
"profundidade": 13,
|
||||
"por_fase_verif": 22,
|
||||
"por_fase_cmd": 24,
|
||||
"por_fase_antip": 16,
|
||||
"por_fase_aceite": 16,
|
||||
"exemplos": 10,
|
||||
"exemplos_count": 12,
|
||||
"artefatos": 14,
|
||||
"fluxo": 8,
|
||||
"entradas_saidas": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wf-architecture",
|
||||
"score": 413.7,
|
||||
"skills_referenciadas": [
|
||||
"architecture-diagram",
|
||||
"avoid-ai-writing",
|
||||
"brainstorming",
|
||||
"c4-architecture",
|
||||
"excalidraw",
|
||||
"hermaguard",
|
||||
"isaqb-architecture-governance",
|
||||
"secure-architecture-governance",
|
||||
"security-threat-model",
|
||||
"system-design",
|
||||
"writing-plans"
|
||||
],
|
||||
"criteria": {
|
||||
"frontmatter": 5,
|
||||
"when_to_use": 8,
|
||||
"num_fases": 8,
|
||||
"regras_duras": 10,
|
||||
"n_regras": 10,
|
||||
"fase1": 15,
|
||||
"fase2": 16,
|
||||
"fase3": 14,
|
||||
"fase4": 16,
|
||||
"cobertura_fases": 20,
|
||||
"skills_validas": 11,
|
||||
"fase_rotulos": 6,
|
||||
"fase_acao": 0,
|
||||
"fase_revisao": 6,
|
||||
"anti_slop": 4.7,
|
||||
"cmd_blocos": 8,
|
||||
"cmd_reais": 5,
|
||||
"sec_criterios": 8,
|
||||
"sec_verificar": 6,
|
||||
"sec_nao_regressao": 6,
|
||||
"tamanho": 8,
|
||||
"testes_referidos": 5,
|
||||
"passos_total": 30,
|
||||
"verificacao_total": 24,
|
||||
"comandos_reais": 20,
|
||||
"profundidade": 13,
|
||||
"por_fase_verif": 24,
|
||||
"por_fase_cmd": 24,
|
||||
"por_fase_antip": 16,
|
||||
"por_fase_aceite": 15,
|
||||
"exemplos": 10,
|
||||
"exemplos_count": 12,
|
||||
"artefatos": 14,
|
||||
"fluxo": 8,
|
||||
"entradas_saidas": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wf-security-review",
|
||||
"score": 390.0,
|
||||
"skills_referenciadas": [
|
||||
"api-security-review",
|
||||
"auth-rbac-scaffold",
|
||||
"bola-detector",
|
||||
"code-review-security",
|
||||
"cve-triage",
|
||||
"dependency-cve-audit",
|
||||
"hermaguard",
|
||||
"injection-checker",
|
||||
"patch-prioritization",
|
||||
"safedeps",
|
||||
"sca-audit",
|
||||
"secrets-scan",
|
||||
"security-threat-model",
|
||||
"web-security-review"
|
||||
],
|
||||
"criteria": {
|
||||
"frontmatter": 5,
|
||||
"when_to_use": 8,
|
||||
"num_fases": 10,
|
||||
"regras_duras": 10,
|
||||
"n_regras": 8,
|
||||
"fase1": 15,
|
||||
"fase2": 16,
|
||||
"fase3": 13,
|
||||
"fase4": 15,
|
||||
"fase5": 15,
|
||||
"cobertura_fases": 20,
|
||||
"skills_validas": 14,
|
||||
"fase_rotulos": 6,
|
||||
"fase_acao": 0,
|
||||
"fase_revisao": 6,
|
||||
"anti_slop": 5.0,
|
||||
"cmd_blocos": 8,
|
||||
"cmd_reais": 5,
|
||||
"sec_criterios": 0,
|
||||
"sec_verificar": 6,
|
||||
"sec_nao_regressao": 6,
|
||||
"tamanho": 8,
|
||||
"testes_referidos": 5,
|
||||
"passos_total": 30,
|
||||
"verificacao_total": 24,
|
||||
"comandos_reais": 16,
|
||||
"profundidade": 15,
|
||||
"por_fase_verif": 23,
|
||||
"por_fase_cmd": 19,
|
||||
"por_fase_antip": 12,
|
||||
"por_fase_aceite": 5,
|
||||
"exemplos": 0,
|
||||
"exemplos_count": 12,
|
||||
"artefatos": 14,
|
||||
"fluxo": 8,
|
||||
"entradas_saidas": 8
|
||||
}
|
||||
}
|
||||
],
|
||||
"total": 1613.2,
|
||||
"max": 400
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Adapter and headless-runner behavior tests; fake CLIs never contact providers."""
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import shutil
|
||||
import unittest
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
REAL_WHICH = shutil.which
|
||||
|
||||
|
||||
def load_module(name, path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
installer = load_module("dev_workflows_install", ROOT / "scripts" / "install.py")
|
||||
harness = load_module("dev_workflows_harness", ROOT / "scripts" / "harness_runner.py")
|
||||
|
||||
|
||||
class AdapterTest(unittest.TestCase):
|
||||
def test_omp_install_preserves_unowned_destination_and_skips_link(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
base = pathlib.Path(temp) / ".omp"
|
||||
dest = base / "plugins" / "dev-workflows"
|
||||
dest.mkdir(parents=True)
|
||||
(dest / "user-data").write_text("preserve", encoding="utf-8")
|
||||
runner = mock.Mock()
|
||||
with self.assertRaises(SystemExit):
|
||||
installer.install_omp(base=str(base), runner=runner)
|
||||
runner.assert_not_called()
|
||||
self.assertEqual((dest / "user-data").read_text(encoding="utf-8"), "preserve")
|
||||
|
||||
def test_omp_install_keeps_package_for_recovery_if_link_fails(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
base = pathlib.Path(temp) / ".omp"
|
||||
runner = mock.Mock(return_value=mock.Mock(returncode=7))
|
||||
with self.assertRaisesRegex(SystemExit, "falhou com código 7"):
|
||||
installer.install_omp(base=str(base), runner=runner)
|
||||
self.assertTrue((base / "plugins" / "dev-workflows" / ".dev-workflows-omp-package").is_file())
|
||||
|
||||
|
||||
|
||||
class HarnessRunnerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.root = pathlib.Path(self.temp.name)
|
||||
self.cwd = self.root / "state" / "project" / "run" / "worktrees" / "iteration-001"
|
||||
self.cwd.mkdir(parents=True)
|
||||
gitdir = self.root / "project" / ".git" / "worktrees" / "iteration-001"
|
||||
gitdir.mkdir(parents=True)
|
||||
(self.cwd / ".git").write_text(f"gitdir: {gitdir}\n", encoding="utf-8")
|
||||
self.bin = self.root / "bin"
|
||||
self.bin.mkdir()
|
||||
self.trace = self.root / "calls.jsonl"
|
||||
self.cache = self.root / "catalog"
|
||||
self.cache.mkdir()
|
||||
self._fake_cli()
|
||||
|
||||
def tearDown(self):
|
||||
self.temp.cleanup()
|
||||
|
||||
def _fake_cli(self):
|
||||
executable = self.bin / "fake-cli"
|
||||
executable.write_text(
|
||||
"#!" + sys.executable + "\n"
|
||||
"import json, os, pathlib, sys\n"
|
||||
"name = pathlib.Path(sys.argv[0]).name\n"
|
||||
"args = sys.argv[1:]\n"
|
||||
"with open(os.environ['DW_TRACE'], 'a', encoding='utf-8') as out:\n"
|
||||
" out.write(json.dumps({'name': name, 'args': args, 'cwd': os.getcwd(), 'marker': os.getenv('DW_MARKER')}) + '\\n')\n"
|
||||
"if name == 'claude' and args[:3] == ['auth', 'status', '--json']:\n"
|
||||
" print(os.environ.get('DW_CLAUDE_AUTH', '{\\\"loggedIn\\\": true}'))\n"
|
||||
"elif name == 'opencode' and args[:1] == ['models']:\n"
|
||||
" print(os.environ.get('DW_MODELS', ''))\n"
|
||||
"elif name == 'omp' and args[:2] == ['models', args[1] if len(args) > 1 else '']:\n"
|
||||
" print(os.environ.get('DW_MODELS_JSON', '[]'))\n"
|
||||
"else:\n"
|
||||
" print('fake response')\n"
|
||||
" print('sk-secret-must-not-escape', file=sys.stderr)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
executable.chmod(0o755)
|
||||
for name in ("claude", "opencode", "omp"):
|
||||
(self.bin / name).symlink_to(executable)
|
||||
|
||||
def _env(self, **extra):
|
||||
value = os.environ.copy()
|
||||
value.update({
|
||||
"PATH": str(self.bin) + os.pathsep + value.get("PATH", ""),
|
||||
"DEV_WORKFLOWS_MODEL_CACHE": str(self.cache),
|
||||
"DW_TRACE": str(self.trace),
|
||||
"DW_MARKER": "test-env",
|
||||
})
|
||||
value.update(extra)
|
||||
return value
|
||||
|
||||
def _snapshot(self, tool, model_key, *, entitled=True, variant=None, provider=None, model_id=None):
|
||||
row = {
|
||||
"tool": tool,
|
||||
"model_key": model_key,
|
||||
"availability": {
|
||||
"local_listed": True,
|
||||
"authenticated": True,
|
||||
"entitled": entitled,
|
||||
"source": "manual_audited",
|
||||
"verified_at": "2026-09-25T12:00:00Z",
|
||||
},
|
||||
}
|
||||
if variant is not None:
|
||||
row.update({"variant": variant, "provider": provider, "model_id": model_id})
|
||||
(self.cache / f"catalog-{tool}.json").write_text(
|
||||
json.dumps({"schema_version": 1, "tool": tool, "models": [row]}), encoding="utf-8"
|
||||
)
|
||||
|
||||
def _calls(self):
|
||||
if not self.trace.exists():
|
||||
return []
|
||||
return [json.loads(line) for line in self.trace.read_text(encoding="utf-8").splitlines()]
|
||||
|
||||
def _fake_bwrap(self):
|
||||
trace = self.root / "bwrap.jsonl"
|
||||
executable = self.bin / "bwrap"
|
||||
executable.write_text(
|
||||
"#!" + sys.executable + "\n"
|
||||
"import json, os, pathlib, sys\n"
|
||||
"args = sys.argv[1:]\n"
|
||||
"with open(os.environ['DW_BWRAP_TRACE'], 'a', encoding='utf-8') as out:\n"
|
||||
" out.write(json.dumps(args) + '\\n')\n"
|
||||
"command = args[args.index('--') + 1:]\n"
|
||||
"tool = pathlib.Path(command[0]).name\n"
|
||||
"if tool == 'omp' and 'models' in command:\n"
|
||||
" print(os.environ.get('DW_MODELS_JSON', '[]'))\n"
|
||||
"elif tool == 'opencode' and 'models' in command:\n"
|
||||
" print(os.environ.get('DW_MODELS', ''))\n"
|
||||
"elif tool == 'claude' and command[1:3] == ['auth', 'status']:\n"
|
||||
" print(os.environ.get('DW_CLAUDE_AUTH', '{\\\"loggedIn\\\": true}'))\n"
|
||||
"else:\n"
|
||||
" print('fake wrapped response')\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
executable.chmod(0o755)
|
||||
return trace
|
||||
|
||||
def _invoke_with_fake_bwrap(self, *args, **kwargs):
|
||||
def find_executable(name, path=None):
|
||||
if name == "bwrap":
|
||||
return str(self.bin / "bwrap")
|
||||
return REAL_WHICH(name, path=path)
|
||||
|
||||
with mock.patch.object(harness.shutil, "which", side_effect=find_executable):
|
||||
return harness.invoke(*args, **kwargs)
|
||||
|
||||
def test_claude_alias_and_dontask_preserve_default_permission_gate(self):
|
||||
result = harness.invoke("claude", "reviewer", "Review safely", str(self.cwd), "opus", env=self._env())
|
||||
self.assertEqual(result["returncode"], 0)
|
||||
calls = self._calls()
|
||||
self.assertEqual(calls[0]["args"], ["auth", "status", "--json"])
|
||||
call = calls[1]
|
||||
args = call["args"]
|
||||
self.assertIn("--permission-mode", args)
|
||||
self.assertEqual(args[args.index("--permission-mode") + 1], "dontAsk")
|
||||
self.assertIn("--agent", args)
|
||||
self.assertIn("reviewer", args)
|
||||
self.assertEqual(args[args.index("--model") + 1], "opus")
|
||||
self.assertNotIn("--dangerously-skip-permissions", args)
|
||||
self.assertEqual(call["cwd"], str(self.cwd))
|
||||
self.assertEqual(call["marker"], "test-env")
|
||||
self.assertIsNone(result["usage"])
|
||||
self.assertNotIn("sk-secret", result["output"])
|
||||
|
||||
def test_claude_inherit_keeps_role_alias_without_model_override(self):
|
||||
result = harness.invoke("claude", "explorer", "Map the project", str(self.cwd), "inherit", env=self._env())
|
||||
self.assertEqual(result["returncode"], 0)
|
||||
calls = self._calls()
|
||||
self.assertEqual(calls[0]["args"], ["auth", "status", "--json"])
|
||||
args = calls[1]["args"]
|
||||
self.assertIn("--agent", args)
|
||||
self.assertNotIn("--model", args)
|
||||
self.assertNotIn("--dangerously-skip-permissions", args)
|
||||
|
||||
def test_sonnet_alias_is_passed_without_rewriting_the_plugin(self):
|
||||
harness.invoke("claude", "ui-critic", "Critique only", str(self.cwd), "sonnet", env=self._env())
|
||||
calls = self._calls()
|
||||
args = calls[1]["args"]
|
||||
self.assertEqual(args[args.index("--model") + 1], "sonnet")
|
||||
self.assertEqual(args[args.index("--agent") + 1], "ui-critic")
|
||||
|
||||
def test_claude_alias_is_blocked_when_local_auth_status_is_negative(self):
|
||||
with self.assertRaisesRegex(harness.HarnessError, "auth status"):
|
||||
harness.invoke(
|
||||
"claude", "reviewer", "Review", str(self.cwd), "opus",
|
||||
env=self._env(DW_CLAUDE_AUTH='{"loggedIn": false}'),
|
||||
)
|
||||
calls = self._calls()
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0]["args"], ["auth", "status", "--json"])
|
||||
|
||||
def test_opencode_uses_exact_variant_and_role_agent_without_auto_approval(self):
|
||||
model_key = "openai/gpt-6-luna#high"
|
||||
self._snapshot("opencode", model_key)
|
||||
result = harness.invoke(
|
||||
"opencode", "planner", "Plan only", str(self.cwd), model_key,
|
||||
env=self._env(DW_MODELS="gpt-6-luna"),
|
||||
)
|
||||
self.assertEqual(result["returncode"], 0)
|
||||
calls = self._calls()
|
||||
self.assertEqual(calls[0]["args"], ["models", "openai"])
|
||||
args = calls[1]["args"]
|
||||
self.assertEqual(args[:3], ["run", "--agent", "planner"])
|
||||
self.assertEqual(args[args.index("--model") + 1], model_key)
|
||||
self.assertNotIn("--auto", args)
|
||||
self.assertEqual(calls[1]["cwd"], str(self.cwd))
|
||||
self.assertIn("stderr omitido por segurança", result["output"])
|
||||
self.assertNotIn("sk-secret", result["output"])
|
||||
|
||||
def test_omp_runs_with_exact_tools_and_always_ask(self):
|
||||
model_key = "openai/gpt-6-luna#high"
|
||||
self._snapshot("omp", model_key, variant="high", provider="openai", model_id="gpt-6-luna")
|
||||
result = harness.invoke(
|
||||
"omp", "explorer", "Inspect only", str(self.cwd), model_key,
|
||||
env=self._env(DW_MODELS_JSON=json.dumps([{"provider": "openai", "id": "gpt-6-luna",
|
||||
"thinking": ["low", "high"]}])),
|
||||
)
|
||||
self.assertEqual(result["returncode"], 0)
|
||||
calls = self._calls()
|
||||
self.assertEqual(calls[0]["args"], ["models", "openai", "--json"])
|
||||
args = calls[1]["args"]
|
||||
self.assertEqual(args[0], "-p")
|
||||
self.assertEqual(args[args.index("--model") + 1], "openai/gpt-6-luna")
|
||||
self.assertEqual(args[args.index("--thinking") + 1], "high")
|
||||
self.assertEqual(args[args.index("--approval-mode") + 1], "always-ask")
|
||||
self.assertNotIn("--auto-approve", args)
|
||||
self.assertEqual(args[args.index("--tools") + 1], "read,grep,glob")
|
||||
self.assertEqual(args[args.index("--cwd") + 1], str(self.cwd))
|
||||
|
||||
def test_omp_unknown_thinking_variant_is_rejected_before_listing_or_dispatch(self):
|
||||
model_key = "openai/model#unsupported"
|
||||
self._snapshot("omp", model_key, variant="unsupported", provider="openai", model_id="model")
|
||||
with self.assertRaisesRegex(harness.HarnessError, "variante"):
|
||||
harness.invoke("omp", "explorer", "Inspect", str(self.cwd), model_key, env=self._env())
|
||||
self.assertEqual(self._calls(), [])
|
||||
|
||||
def test_omp_rejects_thinking_not_supported_by_current_model_listing(self):
|
||||
model_key = "openai/gpt-6-luna#high"
|
||||
self._snapshot("omp", model_key, variant="high", provider="openai", model_id="gpt-6-luna")
|
||||
listed = {"models": [{"provider": "openai", "id": "gpt-6-luna", "thinking": ["low"]}]}
|
||||
with self.assertRaisesRegex(harness.HarnessError, "indisponível"):
|
||||
harness.invoke("omp", "explorer", "Inspect", str(self.cwd), model_key,
|
||||
env=self._env(DW_MODELS_JSON=json.dumps(listed)))
|
||||
self.assertEqual([call["args"] for call in self._calls()], [["models", "openai", "--json"]])
|
||||
|
||||
def test_omp_slash_model_id_keeps_provider_and_qualified_selector(self):
|
||||
model_key = "openrouter/anthropic/claude-sonnet-4.6"
|
||||
self._snapshot("omp", model_key)
|
||||
listed = {"models": [{"provider": "openrouter", "id": "anthropic/claude-sonnet-4.6",
|
||||
"selector": model_key, "thinking": None}]}
|
||||
result = harness.invoke("omp", "explorer", "Inspect", str(self.cwd), model_key,
|
||||
env=self._env(DW_MODELS_JSON=json.dumps(listed)))
|
||||
self.assertEqual(result["returncode"], 0)
|
||||
calls = self._calls()
|
||||
self.assertEqual(calls[0]["args"], ["models", "openrouter", "--json"])
|
||||
self.assertEqual(calls[1]["args"][calls[1]["args"].index("--model") + 1], model_key)
|
||||
|
||||
|
||||
def test_autonomous_omp_uses_bwrap_and_bounded_tools_with_auto_approval(self):
|
||||
model_key = "openai/gpt-6-luna#high"
|
||||
self._snapshot("omp", model_key, variant="high", provider="openai", model_id="gpt-6-luna")
|
||||
trace = self._fake_bwrap()
|
||||
result = self._invoke_with_fake_bwrap(
|
||||
"omp", "implementer", "Inspect only", str(self.cwd), model_key,
|
||||
env=self._env(
|
||||
DEV_WORKFLOWS_AUTONOMOUS="1",
|
||||
DW_BWRAP_TRACE=str(trace),
|
||||
DW_MODELS_JSON=json.dumps([{"provider": "openai", "id": "gpt-6-luna",
|
||||
"thinking": ["low", "high"]}]),
|
||||
),
|
||||
)
|
||||
self.assertEqual(result["returncode"], 0)
|
||||
self.assertEqual(self._calls(), [])
|
||||
calls = [json.loads(line) for line in trace.read_text(encoding="utf-8").splitlines()]
|
||||
self.assertEqual(len(calls), 2)
|
||||
for args in calls:
|
||||
self.assertEqual(args[:6], ["--unshare-all", "--share-net", "--die-with-parent", "--ro-bind", "/", "/"])
|
||||
binds = [args[index + 1:index + 3] for index, value in enumerate(args) if value == "--bind"]
|
||||
self.assertIn([str(self.cwd), str(self.cwd)], binds)
|
||||
readonly = [args[index + 1:index + 3] for index, value in enumerate(args) if value == "--ro-bind"]
|
||||
self.assertIn([str(self.cwd / ".git"), str(self.cwd / ".git")], readonly)
|
||||
preflight = calls[0][calls[0].index("--") + 1:]
|
||||
self.assertEqual(pathlib.Path(preflight[0]).name, "omp")
|
||||
self.assertEqual(preflight[1:4], ["--no-extensions", "models", "openai"])
|
||||
command = calls[1][calls[1].index("--") + 1:]
|
||||
self.assertEqual(pathlib.Path(command[0]).name, "omp")
|
||||
self.assertEqual(command[1], "-p")
|
||||
for flag in ("--no-extensions", "--no-lsp", "--no-session", "--auto-approve"):
|
||||
self.assertIn(flag, command)
|
||||
self.assertEqual(command[command.index("--model") + 1], "openai/gpt-6-luna")
|
||||
self.assertEqual(command[command.index("--thinking") + 1], "high")
|
||||
self.assertEqual(command[command.index("--tools") + 1], "read,grep,glob,edit,write")
|
||||
self.assertNotIn("bash", command)
|
||||
self.assertNotIn("web_search", command)
|
||||
|
||||
def test_autonomous_opencode_uses_pure_mode_and_synthetic_deny_agent(self):
|
||||
model_key = "openai/gpt-6-luna#high"
|
||||
self._snapshot("opencode", model_key)
|
||||
trace = self._fake_bwrap()
|
||||
meta, prompt = harness._agent("implementer")
|
||||
generated = harness._opencode_loop_agent(meta, prompt)
|
||||
self.assertIn(' "*": deny', generated)
|
||||
self.assertIn(" bash: deny", generated)
|
||||
self.assertIn(" edit: allow", generated)
|
||||
self.assertIn(" task: deny", generated)
|
||||
result = self._invoke_with_fake_bwrap(
|
||||
"opencode", "implementer", "Implement safely", str(self.cwd), model_key,
|
||||
env=self._env(DEV_WORKFLOWS_AUTONOMOUS="1", DW_BWRAP_TRACE=str(trace), DW_MODELS="gpt-6-luna"),
|
||||
)
|
||||
self.assertEqual(result["returncode"], 0)
|
||||
self.assertEqual(self._calls(), [])
|
||||
calls = [json.loads(line) for line in trace.read_text(encoding="utf-8").splitlines()]
|
||||
self.assertEqual(len(calls), 2)
|
||||
preflight = calls[0][calls[0].index("--") + 1:]
|
||||
self.assertEqual(pathlib.Path(preflight[0]).name, "opencode")
|
||||
self.assertEqual(preflight[1:3], ["--pure", "models"])
|
||||
command = calls[1][calls[1].index("--") + 1:]
|
||||
self.assertEqual(pathlib.Path(command[0]).name, "opencode")
|
||||
self.assertEqual(command[1:4], ["--pure", "run", "--agent"])
|
||||
agent_name = command[4]
|
||||
self.assertRegex(agent_name, r"\Adev-workflows-loop-implementer-[0-9a-f]{16}\Z")
|
||||
self.assertIn("--file", calls[1])
|
||||
|
||||
def test_autonomous_claude_uses_bare_mode_and_declared_safe_tools(self):
|
||||
trace = self._fake_bwrap()
|
||||
result = self._invoke_with_fake_bwrap(
|
||||
"claude", "reviewer", "Review only", str(self.cwd), "opus",
|
||||
env=self._env(DEV_WORKFLOWS_AUTONOMOUS="1", DW_BWRAP_TRACE=str(trace)),
|
||||
)
|
||||
self.assertEqual(result["returncode"], 0)
|
||||
self.assertEqual(self._calls(), [])
|
||||
calls = [json.loads(line) for line in trace.read_text(encoding="utf-8").splitlines()]
|
||||
self.assertEqual(len(calls), 2)
|
||||
auth = calls[0][calls[0].index("--") + 1:]
|
||||
self.assertEqual(pathlib.Path(auth[0]).name, "claude")
|
||||
self.assertEqual(auth[1:], ["auth", "status", "--json"])
|
||||
command = calls[1][calls[1].index("--") + 1:]
|
||||
self.assertIn("--bare", command)
|
||||
self.assertIn("--no-session-persistence", command)
|
||||
self.assertEqual(command[command.index("--permission-mode") + 1], "dontAsk")
|
||||
agent_config = json.loads(command[command.index("--agents") + 1])
|
||||
self.assertEqual(agent_config["reviewer"]["tools"], ["Read", "Grep", "Glob"])
|
||||
self.assertEqual(command[command.index("--tools") + 1], "Read,Grep,Glob")
|
||||
self.assertNotIn("Bash", command)
|
||||
|
||||
def test_autonomous_mode_fails_closed_without_bwrap(self):
|
||||
self._snapshot("omp", "openai/gpt-6-luna")
|
||||
def find_without_bwrap(name, path=None):
|
||||
return None if name == "bwrap" else REAL_WHICH(name, path=path)
|
||||
with mock.patch.object(harness.shutil, "which", side_effect=find_without_bwrap):
|
||||
with self.assertRaisesRegex(harness.HarnessError, "bwrap"):
|
||||
harness.invoke(
|
||||
"omp", "explorer", "Inspect", str(self.cwd), "openai/gpt-6-luna",
|
||||
env=self._env(PATH=str(self.bin), DEV_WORKFLOWS_AUTONOMOUS="1"),
|
||||
)
|
||||
self.assertEqual(self._calls(), [])
|
||||
|
||||
def test_autonomous_rejects_non_worktree_cwd_before_dispatch(self):
|
||||
other = self.root / "ordinary-directory"
|
||||
other.mkdir()
|
||||
with self.assertRaisesRegex(harness.HarnessError, "worktree"):
|
||||
harness.invoke(
|
||||
"omp", "explorer", "Inspect", str(other), "openai/gpt-6-luna",
|
||||
env=self._env(DEV_WORKFLOWS_AUTONOMOUS="1"),
|
||||
)
|
||||
self.assertEqual(self._calls(), [])
|
||||
|
||||
def test_autonomous_does_not_resolve_cli_from_writable_worktree(self):
|
||||
self._snapshot("opencode", "openai/gpt-6-luna")
|
||||
shadow = self.cwd / "opencode"
|
||||
shadow.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
||||
shadow.chmod(0o755)
|
||||
with self.assertRaisesRegex(harness.HarnessError, "PATH seguro"):
|
||||
harness.invoke(
|
||||
"opencode", "planner", "Plan", str(self.cwd), "openai/gpt-6-luna",
|
||||
env=self._env(PATH=str(self.cwd), DEV_WORKFLOWS_AUTONOMOUS="1"),
|
||||
)
|
||||
self.assertEqual(self._calls(), [])
|
||||
|
||||
def test_unentitled_model_is_blocked_before_cli_is_started(self):
|
||||
self._snapshot("opencode", "openai/gpt-6-luna", entitled=False)
|
||||
with self.assertRaisesRegex(harness.HarnessError, "autorização local"):
|
||||
harness.invoke(
|
||||
"opencode", "planner", "Plan", str(self.cwd), "openai/gpt-6-luna",
|
||||
env=self._env(DW_MODELS="gpt-6-luna"),
|
||||
)
|
||||
self.assertEqual(self._calls(), [])
|
||||
|
||||
def test_model_absent_from_local_cli_is_blocked_before_agent_dispatch(self):
|
||||
self._snapshot("opencode", "openai/gpt-6-luna")
|
||||
with self.assertRaisesRegex(harness.HarnessError, "indisponível na CLI local"):
|
||||
harness.invoke(
|
||||
"opencode", "planner", "Plan", str(self.cwd), "openai/gpt-6-luna",
|
||||
env=self._env(DW_MODELS="openai/different-model"),
|
||||
)
|
||||
calls = self._calls()
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0]["args"], ["models", "openai"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=1)
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Smoke tests for scripts/quality-gate and the hook scripts, on a throwaway git repo. Stdlib only.
|
||||
|
||||
python3 qa/test_gate.py
|
||||
"""
|
||||
import importlib.util, json, os, subprocess, sys, tempfile, unittest
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
GATE = os.path.join(ROOT, "scripts", "quality-gate")
|
||||
STOP = os.path.join(ROOT, "scripts", "hooks", "stop_gate.py")
|
||||
spec = importlib.util.spec_from_file_location("bash_guard", os.path.join(ROOT, "scripts", "hooks", "bash_guard.py"))
|
||||
bash_guard = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(bash_guard)
|
||||
|
||||
|
||||
def run(cmd, cwd, stdin=None):
|
||||
return subprocess.run(cmd, cwd=cwd, input=stdin, capture_output=True, text=True)
|
||||
|
||||
|
||||
class GateTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.repo = self.tmp.name
|
||||
for c in (["git", "init", "-q"], ["git", "config", "user.email", "t@t"], ["git", "config", "user.name", "t"]):
|
||||
run(c, self.repo)
|
||||
self.write("calc.py", "def add(a, b):\n return a + b\n")
|
||||
self.write("check.py", "from calc import add\nassert add(1, 2) == 3\n")
|
||||
self.write(".dev-workflows.toml", '[commands]\ntest = "python3 check.py"\nlint = "true"\ntypecheck = "true"\n')
|
||||
self.write("package.json", '{"dependencies": {"left-pad": "1"}}')
|
||||
run(["git", "add", "-A"], self.repo)
|
||||
run(["git", "commit", "-qm", "init"], self.repo)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def write(self, name, text):
|
||||
open(os.path.join(self.repo, name), "w").write(text)
|
||||
|
||||
def gate(self, tier=0):
|
||||
p = run([sys.executable, GATE, "--tier", str(tier)], self.repo)
|
||||
return p.returncode, json.load(open(os.path.join(self.repo, ".dev-workflows", "gate.json")))
|
||||
|
||||
def stop(self):
|
||||
out = run([sys.executable, STOP], self.repo, json.dumps({"cwd": self.repo})).stdout
|
||||
return json.loads(out)["reason"] if out.strip() else None
|
||||
|
||||
def status(self, report, name):
|
||||
return next(c["status"] for c in report["checks"] if c["name"] == name)
|
||||
|
||||
def test_clean_tree_does_not_block(self):
|
||||
self.assertIsNone(self.stop())
|
||||
|
||||
def test_change_without_gate_blocks_then_passing_gate_allows(self):
|
||||
self.write("calc.py", "def add(a, b):\n return a + b\n\n\ndef sub(a, b):\n return a - b\n")
|
||||
self.assertIn("sem gate", self.stop())
|
||||
code, report = self.gate()
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(self.status(report, "test"), "pass")
|
||||
self.assertIsNone(self.stop())
|
||||
|
||||
def test_failing_test_fails_gate_and_blocks(self):
|
||||
self.write("calc.py", "def add(a, b):\n return a - b\n")
|
||||
code, report = self.gate()
|
||||
self.assertEqual((code, report["verdict"]), (1, "fail"))
|
||||
self.assertIn("test", self.stop())
|
||||
|
||||
def test_missing_tool_is_unverified_not_passed(self):
|
||||
self.write(".dev-workflows.toml", '[commands]\ntest = "definitely-not-a-tool-xyz"\n')
|
||||
self.write("calc.py", "def add(a, b):\n return b + a\n")
|
||||
code, report = self.gate()
|
||||
self.assertEqual(self.status(report, "test"), "unverified")
|
||||
self.assertEqual(report["verdict"], "incomplete")
|
||||
|
||||
def test_new_dependency_is_reported(self):
|
||||
self.write("package.json", '{"dependencies": {"left-pad": "1", "is-odd": "3"}}')
|
||||
self.write("calc.py", "def add(a, b):\n return a + b # touched\n")
|
||||
_, report = self.gate()
|
||||
self.assertEqual(report["new_dependencies"], ["npm:is-odd"])
|
||||
|
||||
def test_stale_gate_blocks(self):
|
||||
self.write("calc.py", "def add(a, b):\n return a + b # v2\n")
|
||||
self.gate()
|
||||
os.utime(os.path.join(self.repo, ".dev-workflows", "gate.json"), (1, 1))
|
||||
self.assertIn("mudou depois", self.stop())
|
||||
|
||||
|
||||
class GuardTest(unittest.TestCase):
|
||||
def test_decisions(self):
|
||||
cases = {"git commit --no-verify -m x": "deny", "git push -f origin main": "deny",
|
||||
"git push --force-with-lease": None, "rm -rf /": "deny", "rm -rf node_modules": None,
|
||||
"git reset --hard": "ask", "npm test": None}
|
||||
for cmd, expected in cases.items():
|
||||
self.assertEqual(bash_guard.decide(cmd)[0], expected, cmd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=1)
|
||||
@@ -1,388 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""End-to-end controller tests on disposable repositories, using the real gate/evaluator."""
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
SCRIPTS = Path(__file__).resolve().parent.parent / "scripts"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
import goal_loop as loop
|
||||
|
||||
|
||||
def git(repo, *args):
|
||||
p = subprocess.run(["git", *args], cwd=repo, capture_output=True, text=True)
|
||||
if p.returncode:
|
||||
raise AssertionError(p.stderr)
|
||||
return p.stdout.strip()
|
||||
|
||||
|
||||
class GoalLoopTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
root = Path(self.tmp.name)
|
||||
self.repo = root / "app"
|
||||
self.repo.mkdir()
|
||||
self.state = root / "state"
|
||||
self.bin = root / "bin"
|
||||
self.bin.mkdir()
|
||||
scanner = self.bin / "gitleaks"
|
||||
scanner.write_text("#!/bin/sh\nexit 0\n")
|
||||
scanner.chmod(0o755)
|
||||
self.env = patch.dict(os.environ, {"DW_STATE_HOME": str(self.state),
|
||||
"PATH": str(self.bin) + os.pathsep + os.environ["PATH"]})
|
||||
self.env.start()
|
||||
self.addCleanup(self.env.stop)
|
||||
git(self.repo, "init", "-q")
|
||||
git(self.repo, "config", "user.name", "test")
|
||||
git(self.repo, "config", "user.email", "[email protected]")
|
||||
(self.repo / "calc.py").write_text("VALUE = 0\n")
|
||||
(self.repo / "verify.py").write_text("import runpy\nassert runpy.run_path('calc.py')['VALUE'] >= 0\n")
|
||||
self.evaluator = root / "evaluator.py"
|
||||
self.evaluator.write_text(
|
||||
"import json, os, pathlib, runpy\n"
|
||||
"value = runpy.run_path('calc.py')['VALUE']\n"
|
||||
"folder = pathlib.Path(os.environ['DW_REPORT_DIR'])\n"
|
||||
"evidence = folder / 'result.json'\n"
|
||||
"evidence.write_text(json.dumps({'measured': value}))\n"
|
||||
"print(json.dumps({'schema_version': 1, 'metric': 'quality', 'value': value, "
|
||||
"'unit': 'points', 'direction': 'maximize', 'checks': {'regression': 'pass'}, "
|
||||
"'evidence': [str(evidence)]}))\n")
|
||||
self.config = self.repo / ".dev-workflows.toml"
|
||||
self.write_config()
|
||||
git(self.repo, "add", "-A")
|
||||
git(self.repo, "commit", "-qm", "baseline")
|
||||
self.original = git(self.repo, "rev-parse", "HEAD")
|
||||
|
||||
def write_config(self, target=2, iterations=4, stagnant=2, tier=0, billing="claude_subscription"):
|
||||
checksum = hashlib.sha256(self.evaluator.read_bytes()).hexdigest()
|
||||
self.config.write_text(
|
||||
f"[commands]\ntest = \"python3 verify.py\"\nlint = \"true\"\ntypecheck = \"true\"\n"
|
||||
f"[loop]\nenabled = true\nharness = \"claude\"\ntier = {tier}\n"
|
||||
f"max_iterations = {iterations}\nmax_minutes = 5\nmax_stagnant = {stagnant}\n"
|
||||
"min_improvement = 1\nmax_paid_usd = 0\n"
|
||||
f"[loop.goal]\nname = \"quality\"\nunit = \"points\"\ndirection = \"maximize\"\ntarget = {target}\n"
|
||||
"[loop.evaluator]\n"
|
||||
f"argv = [\"python3\", \"{self.evaluator}\"]\n"
|
||||
f"trusted_sha256 = \"{checksum}\"\nrequired_checks = [\"test\", \"lint\"]\n"
|
||||
"[models.roles.implementer]\n"
|
||||
f"model_key = \"sonnet\"\nbilling_mode = \"{billing}\"\nmanual_unranked_approval = true\n")
|
||||
if tier >= 2:
|
||||
with self.config.open("a") as config:
|
||||
for role in ("planner", "reviewer"):
|
||||
config.write(f"[models.roles.{role}]\nmodel_key = \"opus\"\n"
|
||||
"billing_mode = \"claude_subscription\"\nmanual_unranked_approval = true\n")
|
||||
if tier >= 3:
|
||||
config.write("[models.roles.security-auditor]\nmodel_key = \"opus\"\n"
|
||||
"billing_mode = \"claude_subscription\"\nmanual_unranked_approval = true\n")
|
||||
|
||||
def runner(self, behavior):
|
||||
def invoke(tool, role, prompt, cwd, model_key, timeout=900, env=None):
|
||||
if role in ("planner", "reviewer", "security-auditor"):
|
||||
return {"returncode": 0, "output": '{"verdict":"pass","findings":[]}', "usage": {}}
|
||||
value = int((Path(cwd) / "calc.py").read_text().split("=")[1].strip())
|
||||
next_value = behavior(value, cwd)
|
||||
(Path(cwd) / "calc.py").write_text(f"VALUE = {next_value}\n")
|
||||
return {"returncode": 0, "output": "changed one behavior", "usage": {}}
|
||||
return patch.dict(sys.modules, {"harness_runner": types.SimpleNamespace(invoke=invoke)})
|
||||
|
||||
def test_plan_does_not_create_worktree_or_invoke_agent(self):
|
||||
before = git(self.repo, "worktree", "list", "--porcelain")
|
||||
result = loop.plan(self.config)
|
||||
self.assertEqual(result["baseline"], "not_run")
|
||||
self.assertEqual(result["gate_preflight"]["test"], "run")
|
||||
self.assertEqual(git(self.repo, "worktree", "list", "--porcelain"), before)
|
||||
self.assertFalse(self.state.exists())
|
||||
|
||||
def test_two_measured_changes_reach_target_without_touching_user_branch(self):
|
||||
with self.runner(lambda value, _: value + 1):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "success", result)
|
||||
self.assertEqual([a["result"] for a in result["attempts"]], ["accepted", "accepted"])
|
||||
self.assertEqual(result["best_score"], "2")
|
||||
self.assertEqual(git(self.repo, "rev-parse", "HEAD"), self.original)
|
||||
self.assertEqual((self.repo / "calc.py").read_text(), "VALUE = 0\n")
|
||||
self.assertEqual((Path(result["best_worktree"]) / "calc.py").read_text(), "VALUE = 2\n")
|
||||
self.assertNotIn(".dev-workflows/gate.json", git(Path(result["best_worktree"]), "ls-files"))
|
||||
self.assertEqual(result["attempts"][1]["gate"]["checks"]["test"], "pass")
|
||||
|
||||
def test_limit_preserves_best_improvement_without_claiming_target(self):
|
||||
self.write_config(target=3, iterations=2)
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "target-three")
|
||||
with self.runner(lambda value, _: value + 1):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "exhausted", result)
|
||||
self.assertEqual(result["best_score"], "2")
|
||||
self.assertEqual([a["result"] for a in result["attempts"]], ["accepted", "accepted"])
|
||||
self.assertEqual((self.repo / "calc.py").read_text(), "VALUE = 0\n")
|
||||
|
||||
def test_measured_improvement_can_remove_obsolete_code(self):
|
||||
(self.repo / "legacy.py").write_text("OLD = True\n")
|
||||
self.evaluator.write_text(self.evaluator.read_text().replace(
|
||||
"value = runpy.run_path('calc.py')['VALUE']",
|
||||
"value = runpy.run_path('calc.py')['VALUE'] + int(not pathlib.Path('legacy.py').exists())"))
|
||||
self.write_config(target=1, iterations=1)
|
||||
git(self.repo, "add", "-A")
|
||||
git(self.repo, "commit", "-qm", "legacy-baseline")
|
||||
def invoke(tool, role, prompt, cwd, model_key, timeout=900, env=None):
|
||||
(Path(cwd) / "legacy.py").unlink()
|
||||
return {"returncode": 0, "output": "removed legacy", "usage": {}}
|
||||
with patch.dict(sys.modules, {"harness_runner": types.SimpleNamespace(invoke=invoke)}):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "success", result)
|
||||
self.assertFalse((Path(result["best_worktree"]) / "legacy.py").exists())
|
||||
self.assertTrue((self.repo / "legacy.py").exists())
|
||||
|
||||
|
||||
def test_baseline_at_target_exits_without_editing(self):
|
||||
self.write_config(target=0)
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "zero-target")
|
||||
with self.runner(lambda _value, _cwd: self.fail("agente não deveria ser chamado")):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "success", result)
|
||||
self.assertEqual(result["iteration"], 0)
|
||||
self.assertEqual(result["attempts"], [])
|
||||
|
||||
def test_evaluator_cannot_modify_original_checkout(self):
|
||||
self.evaluator.write_text(
|
||||
self.evaluator.read_text() +
|
||||
f"open({str(self.repo / 'calc.py')!r}, 'w').write('VALUE = 999\\n')\n")
|
||||
self.write_config(target=1)
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "protected-evaluator")
|
||||
with self.runner(lambda value, _: value + 1):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "error", result)
|
||||
self.assertIn("avaliador", result["error"])
|
||||
self.assertEqual((self.repo / "calc.py").read_text(), "VALUE = 0\n")
|
||||
|
||||
|
||||
def test_gate_failure_rejects_regression_without_replacing_best(self):
|
||||
calls = 0
|
||||
def changes(value, _):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
return -1 if calls == 1 else 1
|
||||
self.write_config(target=1)
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "target-one")
|
||||
with self.runner(changes):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "success", result)
|
||||
self.assertEqual([a["result"] for a in result["attempts"]], ["rejected", "accepted"])
|
||||
self.assertIn("quality gate", result["attempts"][0]["reason"])
|
||||
self.assertEqual(result["best_score"], "1")
|
||||
self.assertEqual((self.repo / "calc.py").read_text(), "VALUE = 0\n")
|
||||
|
||||
def test_incomplete_gate_never_counts_as_success(self):
|
||||
self.write_config(target=1, iterations=1)
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "one-iteration")
|
||||
# No gitleaks in PATH: a security check is unverified, not green.
|
||||
with patch.dict(os.environ, {"PATH": os.environ["PATH"].replace(str(self.bin) + os.pathsep, "")}), \
|
||||
self.runner(lambda value, _: 1):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "exhausted", result)
|
||||
self.assertEqual(result["best_score"], "0")
|
||||
self.assertIn("quality gate", result["attempts"][0]["reason"])
|
||||
|
||||
def test_tampered_config_in_candidate_is_rejected(self):
|
||||
self.write_config(target=1, iterations=1)
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "one-iteration")
|
||||
def tamper(value, cwd):
|
||||
with (Path(cwd) / ".dev-workflows.toml").open("a") as f:
|
||||
f.write("\n# altered by agent\n")
|
||||
return 1
|
||||
with self.runner(tamper):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "exhausted", result)
|
||||
self.assertEqual(result["attempts"][0]["result"], "rejected")
|
||||
self.assertIn("protegido", result["attempts"][0]["reason"])
|
||||
|
||||
|
||||
def test_redirected_git_pointer_cannot_stage_on_original_branch(self):
|
||||
self.write_config(target=1, iterations=1)
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "one-iteration")
|
||||
original = git(self.repo, "rev-parse", "HEAD")
|
||||
|
||||
def redirect(_value, cwd):
|
||||
(Path(cwd) / ".git").write_text(f"gitdir: {self.repo / '.git'}\n")
|
||||
return 1
|
||||
|
||||
with self.runner(redirect):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "exhausted", result)
|
||||
self.assertEqual(result["attempts"][0]["result"], "rejected")
|
||||
self.assertIn("ponteiro .git", result["attempts"][0]["reason"])
|
||||
self.assertEqual(git(self.repo, "rev-parse", "HEAD"), original)
|
||||
self.assertEqual(git(self.repo, "status", "--porcelain"), "")
|
||||
self.assertEqual((self.repo / "calc.py").read_text(), "VALUE = 0\n")
|
||||
|
||||
def test_ignored_agent_file_cannot_inflate_accepted_score(self):
|
||||
(self.repo / ".gitignore").write_text("score.txt\n")
|
||||
self.evaluator.write_text(self.evaluator.read_text().replace(
|
||||
"value = runpy.run_path('calc.py')['VALUE']",
|
||||
"value = int(pathlib.Path('score.txt').read_text()) if pathlib.Path('score.txt').exists() else 0"))
|
||||
self.write_config(target=2, iterations=1)
|
||||
git(self.repo, "add", "-A")
|
||||
git(self.repo, "commit", "-qm", "ignore-uncommitted-score")
|
||||
|
||||
def inflate(_value, cwd):
|
||||
(Path(cwd) / "score.txt").write_text("2\n")
|
||||
return 1
|
||||
|
||||
with self.runner(inflate):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "exhausted", result)
|
||||
self.assertEqual(result["best_score"], "0")
|
||||
self.assertEqual(result["attempts"][0]["result"], "rejected")
|
||||
self.assertIn("score 0", result["attempts"][0]["reason"])
|
||||
self.assertEqual((self.repo / "calc.py").read_text(), "VALUE = 0\n")
|
||||
|
||||
def test_missing_evidence_cannot_claim_improvement(self):
|
||||
self.evaluator.write_text(self.evaluator.read_text().replace(
|
||||
"'evidence': [str(evidence)]", "'evidence': [str(evidence)] if value == 0 else []"))
|
||||
self.write_config(target=1, iterations=1)
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "evidence-required")
|
||||
with self.runner(lambda _value, _cwd: 1):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "paused", result)
|
||||
self.assertEqual(result["best_score"], "0")
|
||||
self.assertEqual(result["attempts"][0]["result"], "paused")
|
||||
self.assertIn("evidência", result["attempts"][0]["reason"])
|
||||
|
||||
def test_dirty_tree_is_refused_and_preserved(self):
|
||||
(self.repo / "calc.py").write_text("VALUE = 99\n")
|
||||
with self.assertRaisesRegex(loop.LoopError, "alterações"):
|
||||
loop.new_run(self.config)
|
||||
self.assertFalse(self.state.exists())
|
||||
self.assertEqual((self.repo / "calc.py").read_text(), "VALUE = 99\n")
|
||||
|
||||
def test_status_and_stop_work_after_unrelated_user_edit(self):
|
||||
self.write_config(iterations=1)
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "one-iteration")
|
||||
with self.runner(lambda value, _: value + 1):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "exhausted")
|
||||
(self.repo / "calc.py").write_text("VALUE = 99\n")
|
||||
run_id = result["run_id"]
|
||||
for action in ("status", "stop"):
|
||||
with patch("sys.stdout", new_callable=io.StringIO) as captured:
|
||||
exit_code = loop.main([action, "--config", str(self.config), "--run", run_id])
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertEqual(json.loads(captured.getvalue())["best_score"], "1")
|
||||
self.assertTrue((loop.state_root(self.repo) / run_id / "stop.requested").is_file())
|
||||
self.assertEqual((self.repo / "calc.py").read_text(), "VALUE = 99\n")
|
||||
|
||||
def test_independent_review_failure_rejects_measured_gain(self):
|
||||
self.write_config(target=1, tier=2, iterations=1)
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "review-required")
|
||||
def invoke(tool, role, prompt, cwd, model_key, timeout=900, env=None):
|
||||
if role == "implementer":
|
||||
(Path(cwd) / "calc.py").write_text("VALUE = 1\n")
|
||||
return {"returncode": 0, "output": "changed behavior", "usage": {}}
|
||||
return {"returncode": 0, "output": '{"verdict":"fail","findings":["regression"]}'
|
||||
if role == "reviewer" else "plan", "usage": {}}
|
||||
actual_gate = loop.gate_result
|
||||
# Keep a real passing gate; tier-2 scanners are not installed in the disposable app.
|
||||
def gate_with_available_checks(repo, base, tier, required, changes, deadline=None):
|
||||
return actual_gate(repo, base, 0, required, changes, deadline)
|
||||
with (patch.dict(sys.modules, {"harness_runner": types.SimpleNamespace(invoke=invoke)}),
|
||||
patch.object(loop, "gate_result", side_effect=gate_with_available_checks)):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "exhausted", result)
|
||||
self.assertEqual(result["best_score"], "0")
|
||||
self.assertEqual(result["attempts"][0]["gate"]["checks"]["test"], "pass")
|
||||
self.assertIn("reviewer", result["attempts"][0]["reason"])
|
||||
|
||||
|
||||
def test_resume_after_interrupted_attempt_does_not_repeat_it(self):
|
||||
self.write_config(target=1)
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "target-one")
|
||||
def interrupt(_, __):
|
||||
raise KeyboardInterrupt
|
||||
with self.runner(interrupt), self.assertRaises(KeyboardInterrupt):
|
||||
loop.new_run(self.config)
|
||||
home = loop.state_root(self.repo)
|
||||
folder = next(p for p in home.iterdir() if p.is_dir())
|
||||
with self.runner(lambda value, _: 1):
|
||||
result = loop.resume_run(self.config, folder.name)
|
||||
self.assertEqual(result["status"], "success", result)
|
||||
self.assertEqual([a["result"] for a in result["attempts"]], ["interrupted", "accepted"])
|
||||
self.assertEqual(result["attempts"][-1]["iteration"], 2)
|
||||
|
||||
def test_fixed_zen_model_cannot_bypass_account_catalog(self):
|
||||
self.write_config(target=1, billing="zen")
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "zen-config")
|
||||
with self.runner(lambda value, _: 1), self.assertRaisesRegex(loop.LoopError, "OpenCode ou OMP"):
|
||||
loop.new_run(self.config)
|
||||
|
||||
def test_unknown_paid_usage_blocks_resume_without_reauthorizing(self):
|
||||
self.write_config(target=1, iterations=1)
|
||||
self.config.write_text(self.config.read_text().replace("max_paid_usd = 0", "max_paid_usd = 1"))
|
||||
git(self.repo, "add", ".dev-workflows.toml")
|
||||
git(self.repo, "commit", "-qm", "paid-budget")
|
||||
selected = {"model_key": "opencode/test", "billing_mode": "zen",
|
||||
"balance_confirmed": True, "estimated_native_usd": "0.1"}
|
||||
calls = 0
|
||||
|
||||
def invoke(_tool, _role, _prompt, cwd, _model_key, timeout=900, env=None):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
(Path(cwd) / "calc.py").write_text("VALUE = 1\n")
|
||||
return {"returncode": 0, "output": "changed behavior", "usage": None}
|
||||
|
||||
with (patch.object(loop, "role_model", return_value=selected),
|
||||
patch.object(loop, "authorize"),
|
||||
patch.dict(sys.modules, {"harness_runner": types.SimpleNamespace(invoke=invoke)})):
|
||||
result = loop.new_run(self.config)
|
||||
self.assertEqual(result["status"], "paused", result)
|
||||
self.assertEqual(result["spent_paid_usd"], "0")
|
||||
state = json.loads((loop.state_root(self.repo) / result["run_id"] / "state.json").read_text())
|
||||
self.assertTrue(state["unreconciled_paid_call"])
|
||||
with self.assertRaisesRegex(loop.LoopError, "concilie"):
|
||||
loop.resume_run(self.config, result["run_id"])
|
||||
self.assertEqual(calls, 1)
|
||||
|
||||
def test_go_requires_all_three_windows_and_disables_balance_fallback(self):
|
||||
config = {"loop": {"max_paid_usd": 0}}
|
||||
row = {"billing_mode": "go", "quota_window_remaining": {"5h": "1", "weekly": "1", "monthly": "1"},
|
||||
"quota_window_debit": {"5h": "0.2", "weekly": "0.2", "monthly": "0.2"},
|
||||
"use_balance_fallback": True}
|
||||
with self.assertRaisesRegex(loop.SafetyPause, "fallback"):
|
||||
loop.authorize(config, row, loop.Decimal(0))
|
||||
row["use_balance_fallback"] = False
|
||||
row["quota_window_remaining"]["weekly"] = "0"
|
||||
with self.assertRaisesRegex(loop.SafetyPause, "três janelas"):
|
||||
loop.authorize(config, row, loop.Decimal(0))
|
||||
row["quota_window_remaining"]["weekly"] = "1"
|
||||
loop.authorize(config, row, loop.Decimal(0))
|
||||
|
||||
def test_zen_needs_live_interactive_consent_not_stdin_yes(self):
|
||||
config = {"loop": {"max_paid_usd": 1}}
|
||||
row = {"billing_mode": "zen", "model_key": "opencode/test",
|
||||
"estimated_native_usd": "0.2", "balance_confirmed": True}
|
||||
with patch("sys.stdin", io.StringIO("SIM\n")), self.assertRaisesRegex(loop.SafetyPause, "interativa"):
|
||||
loop.authorize(config, row, loop.Decimal(0))
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -1,374 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Behavior tests for model catalog imports and reports (stdlib only)."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from decimal import Decimal
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "scripts"))
|
||||
import model_catalog as catalog
|
||||
|
||||
|
||||
class CatalogImportTests(unittest.TestCase):
|
||||
def test_openrouter_keeps_per_token_units_and_missing_cache_is_not_free(self):
|
||||
payload = {"data": [{"id": "z-ai/glm-5.3", "name": "GLM", "pricing": {
|
||||
"prompt": "0.0000014", "completion": "0.0000044", "input_cache_read": "0.00000026"
|
||||
}}]}
|
||||
prices = catalog.parse_openrouter(payload)["z-ai/glm-5.3"]
|
||||
self.assertEqual(prices["price_unit"], "USD/token")
|
||||
self.assertEqual(prices["price_per_token_usd"]["input"], "0.0000014")
|
||||
self.assertEqual(Decimal(prices["display_usd_per_1m"]["input"]), Decimal("1.4"))
|
||||
self.assertEqual(Decimal(prices["display_usd_per_1m"]["cache_read"]), Decimal("0.26"))
|
||||
self.assertIsNone(prices["display_usd_per_1m"]["cache_write"])
|
||||
self.assertIsNone(catalog.reference_cost(prices["price_per_token_usd"], {"cache_write": 1}))
|
||||
self.assertEqual(catalog.reference_cost(prices["price_per_token_usd"], {"input": 1_000_000, "output": 10_000}), Decimal("1.444"))
|
||||
|
||||
def test_negative_public_tariff_keeps_valid_models_and_is_not_costed(self):
|
||||
payload = {"data": [
|
||||
{"id": "openrouter/auto-beta", "pricing": {"prompt": "-0.000001", "completion": "0.1"}},
|
||||
{"id": "openai/valid", "pricing": {"prompt": "0.000002", "completion": "0.000003"}},
|
||||
]}
|
||||
parsed = catalog.parse_openrouter(payload)
|
||||
self.assertEqual(catalog.reference_cost(parsed["openai/valid"]["price_per_token_usd"],
|
||||
{"input": 1_000_000}), Decimal("2"))
|
||||
invalid = parsed["openrouter/auto-beta"]
|
||||
self.assertEqual(invalid["conditions"]["unsupported_negative_prices"]["input"], "-0.000001")
|
||||
self.assertIsNone(invalid["price_per_token_usd"]["input"])
|
||||
self.assertIsNone(catalog.reference_cost(invalid["price_per_token_usd"], {"input": 1}))
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.parse_openrouter(payload, {"openai/valid": {"price_per_token_usd": {"input": "-1"},
|
||||
"source_url": "https://example.test/pricing", "captured_at": "2026-09-25T00:00:00Z"}})
|
||||
|
||||
|
||||
def test_openrouter_price_overrides_preserve_conditions_and_unit(self):
|
||||
payload = {"data": [{"id": "openai/gpt-6-luna", "pricing": {"prompt": "0.1", "completion": "0.5"}, "context_length": 272000}]}
|
||||
overrides = {"openai/gpt-6-luna": {
|
||||
"display_usd_per_1m": {"input": "2.25", "output": "7.5"},
|
||||
"context_length": 272000, "ttl": "1h", "endpoint": "https://example.test/v2",
|
||||
"source_url": "https://example.test/pricing", "captured_at": "2026-09-25T00:00:00Z"
|
||||
}}
|
||||
record = catalog.parse_openrouter(payload, overrides)["openai/gpt-6-luna"]
|
||||
self.assertEqual(record["price_per_token_usd"]["input"], "0.00000225")
|
||||
self.assertEqual(Decimal(record["display_usd_per_1m"]["output"]), Decimal("7.5"))
|
||||
self.assertEqual(record["conditions"]["context_length"], 272000)
|
||||
self.assertEqual(record["conditions"]["ttl"], "1h")
|
||||
self.assertEqual(record["conditions"]["endpoint"], "https://example.test/v2")
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.parse_openrouter(payload, {"missing/model": {"display_usd_per_1m": {"input": "1"}}})
|
||||
|
||||
def test_context_and_cache_ttl_overrides_choose_the_matching_price_tier(self):
|
||||
payload = {"data": [{"id": "openai/gpt-6-luna", "pricing": {
|
||||
"prompt": "0.0000001", "completion": "0.0000005",
|
||||
"input_cache_write": "0.0000009", "input_cache_write_1h": "0.0000005"
|
||||
}}]}
|
||||
tier = {"min_context_tokens": 272001, "price_per_token_usd": {"input": "0.0000002", "output": "0.0000008"},
|
||||
"source_url": "https://openrouter.ai/docs/pricing", "captured_at": "2026-09-25T00:00:00Z"}
|
||||
record = catalog.parse_openrouter(payload, {"openai/gpt-6-luna": {"context_tiers": [tier]}})["openai/gpt-6-luna"]
|
||||
below = catalog.openrouter_price_for_profile(record, {"context_length": 272000})
|
||||
above = catalog.openrouter_price_for_profile(record, {"context_length": 272001})
|
||||
self.assertEqual(below["price_per_token_usd"]["input"], "0.0000001")
|
||||
self.assertEqual(above["price_per_token_usd"]["input"], "0.0000002")
|
||||
self.assertEqual(above["applied_context_tier"], 272001)
|
||||
self.assertEqual(catalog.reference_cost(record["price_per_token_usd"], {"cache_write": 100, "cache_write_ttl": "1h"}), Decimal("0.00005"))
|
||||
|
||||
def test_provider_catalog_preserves_exact_variants_without_guessing(self):
|
||||
rows = catalog.parse_provider_catalog({"data": [
|
||||
{"id": "model-x", "name": "X", "variants": ["high", "max"]},
|
||||
{"id": "model-y"}
|
||||
]}, "opencode", catalog.ZEN_URL)
|
||||
self.assertEqual([r["model_key"] for r in rows], ["opencode/model-x#high", "opencode/model-x#max", "opencode/model-y"])
|
||||
self.assertTrue(all(r["availability"]["authenticated"] is None for r in rows))
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.parse_provider_catalog({"data": [{"id": "bad/id"}]}, "opencode", catalog.ZEN_URL)
|
||||
|
||||
def test_aa_import_requires_consent_provenance_and_one_consistent_v15_record(self):
|
||||
snapshot = {
|
||||
"metric_id": catalog.AA_METRIC, "version": "1.5", "ingestion": "manual_audited",
|
||||
"terms_accepted": True, "source_url": catalog.AA_URL, "attribution": "Artificial Analysis Coding Agent Index",
|
||||
"captured_at": "2026-09-25T00:00:00Z", "records": [{
|
||||
"provider": "opencode", "model_id": "gpt-6-luna", "variant": "max", "score": "41",
|
||||
"agent": "Codex", "harness": "Codex", "settings": {"effort": "max"},
|
||||
"match_grade": "proxy", "evidence_url": catalog.AA_URL
|
||||
}]
|
||||
}
|
||||
row = catalog.validate_aa_snapshot(snapshot)[0]
|
||||
self.assertEqual((row["metric_id"], row["version"], row["score"], row["match_grade"]), (catalog.AA_METRIC, "1.5", "41", "proxy"))
|
||||
wrong_index = dict(snapshot, metric_id="artificial_analysis_intelligence_index")
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.validate_aa_snapshot(wrong_index)
|
||||
duplicate = dict(snapshot, records=snapshot["records"] * 2)
|
||||
with self.assertRaisesRegex(ValueError, "inconsistente"):
|
||||
catalog.validate_aa_snapshot(duplicate)
|
||||
no_consent = dict(snapshot, terms_accepted=False)
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.validate_aa_snapshot(no_consent)
|
||||
|
||||
def test_local_discovery_never_returns_diagnostics_or_credential_text(self):
|
||||
class Result:
|
||||
returncode = 0
|
||||
stdout = "opencode/model-a#high\nAPI_KEY=do-not-store\n"
|
||||
stderr = "secret=do-not-print"
|
||||
result = catalog.discover_local_models("opencode", runner=lambda *args, **kwargs: Result())
|
||||
self.assertEqual(result["models"], ["model-a#high"])
|
||||
self.assertNotIn("do-not-store", json.dumps(result))
|
||||
self.assertNotIn("do-not-print", json.dumps(result))
|
||||
|
||||
def test_omp_json_is_local_listing_only_and_never_proves_entitlement(self):
|
||||
payload = {"models": [{"provider": "openai", "id": "gpt-6-luna", "thinking": ["high"]}]}
|
||||
rows = catalog.parse_omp_models(payload, "openai")
|
||||
self.assertEqual(rows[0]["model_key"], "openai/gpt-6-luna")
|
||||
self.assertIsNone(rows[0]["availability"]["authenticated"])
|
||||
self.assertIsNone(rows[0]["availability"]["entitled"])
|
||||
|
||||
class Result:
|
||||
returncode = 0
|
||||
stdout = json.dumps(payload)
|
||||
stderr = "account-private-value"
|
||||
calls = []
|
||||
result = catalog.discover_omp_models("openai", runner=lambda args, **kwargs: (calls.append(args) or Result()))
|
||||
self.assertEqual(calls, [["omp", "models", "openai", "--json"]])
|
||||
self.assertEqual(result["models"], ["openai/gpt-6-luna", "openai/gpt-6-luna#high"])
|
||||
self.assertNotIn("account-private-value", json.dumps(result))
|
||||
|
||||
def test_omp_keeps_slash_ids_and_only_lists_advertised_thinking(self):
|
||||
payload = {"models": [
|
||||
{"provider": "openai", "id": "anthropic/claude-x",
|
||||
"selector": "openai/anthropic/claude-x", "thinking": ["low", "high"]}
|
||||
]}
|
||||
rows = catalog.parse_omp_models(payload, "openai")
|
||||
self.assertEqual([row["model_key"] for row in rows], ["openai/anthropic/claude-x"])
|
||||
|
||||
self.assertEqual(rows[0]["model_id"], "anthropic/claude-x")
|
||||
class Result:
|
||||
returncode = 0
|
||||
stdout = json.dumps(payload)
|
||||
stderr = ""
|
||||
discovered = catalog.discover_omp_models("openai", runner=lambda *args, **kwargs: Result())
|
||||
self.assertEqual(discovered["models"], [
|
||||
"openai/anthropic/claude-x", "openai/anthropic/claude-x#high", "openai/anthropic/claude-x#low"
|
||||
])
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.parse_omp_models({"models": [{"provider": "openai", "id": "anthropic/claude-x",
|
||||
"selector": "openai/claude-x"}]}, "openai")
|
||||
|
||||
def test_openrouter_public_long_context_override_affects_reference_cost(self):
|
||||
record = catalog.parse_openrouter({"data": [{"id": "anthropic/model-x", "pricing": {
|
||||
"prompt": "0.000001", "completion": "0.000002",
|
||||
"overrides": [{"min_prompt_tokens": 272000, "prompt": "0.000003", "completion": "0.000004"}]
|
||||
}}]})["anthropic/model-x"]
|
||||
selected = catalog.openrouter_price_for_profile(record, {"input": 272000})
|
||||
self.assertEqual(selected["price_per_token_usd"]["input"], "0.000003")
|
||||
self.assertEqual(catalog.reference_cost(selected["price_per_token_usd"],
|
||||
{"input": 272000, "output": 10}), Decimal("0.81604"))
|
||||
|
||||
def test_omp_sync_requires_manual_account_and_balance_evidence(self):
|
||||
with tempfile.TemporaryDirectory() as cache:
|
||||
availability_path = os.path.join(cache, "availability.json")
|
||||
native_path = os.path.join(cache, "overrides.json")
|
||||
aa_path = os.path.join(cache, "aa.json")
|
||||
timestamp = catalog.utc_now()
|
||||
with open(availability_path, "w", encoding="utf-8") as stream:
|
||||
json.dump({
|
||||
"tool": "omp", "ingestion": "manual_audited", "user_confirmed": True,
|
||||
"captured_at": timestamp, "records": [{
|
||||
"provider": "openai", "model_id": "gpt-6-luna", "variant": "high",
|
||||
"local_listed": True, "authenticated": True, "entitled": True,
|
||||
"verified_at": timestamp, "source": "manual audited OMP account",
|
||||
"balance_confirmed": True, "balance_verified_at": timestamp,
|
||||
}],
|
||||
}, stream)
|
||||
with open(native_path, "w", encoding="utf-8") as stream:
|
||||
json.dump({"native_tariffs": {"openai/gpt-6-luna#high": {
|
||||
"billing_mode": "omp_payg", "account_confirmed": True,
|
||||
"source_url": "https://provider.example/pricing", "captured_at": timestamp,
|
||||
"price_per_token_usd": {"input": "0.000001"}, "available_balance_usd": "2",
|
||||
}}}, stream)
|
||||
with open(aa_path, "w", encoding="utf-8") as stream:
|
||||
json.dump({
|
||||
"metric_id": catalog.AA_METRIC, "version": "1.5", "ingestion": "manual_audited",
|
||||
"terms_accepted": True, "source_url": catalog.AA_URL,
|
||||
"attribution": "Artificial Analysis Coding Agent Index", "captured_at": timestamp,
|
||||
"records": [{
|
||||
"provider": "openai", "model_id": "gpt-6-luna", "variant": "high", "score": "54",
|
||||
"agent": "Codex", "harness": "OMP", "settings": {"effort": "high"},
|
||||
"match_grade": "exact", "evidence_url": catalog.AA_URL,
|
||||
}, {
|
||||
"provider": "openai", "model_id": "gpt-6-luna", "variant": "xhigh", "score": "60",
|
||||
"agent": "Codex", "harness": "OMP", "settings": {"effort": "xhigh"},
|
||||
"match_grade": "exact", "evidence_url": catalog.AA_URL,
|
||||
}],
|
||||
}, stream)
|
||||
rows = catalog.parse_omp_models({"models": [{"provider": "openai", "id": "gpt-6-luna",
|
||||
"thinking": ["high"]}]}, "openai")
|
||||
discover = lambda provider: {"ok": True, "models": ["openai/gpt-6-luna", "openai/gpt-6-luna#high"], "rows": rows,
|
||||
"source_hash": "a" * 64, "verified_at": timestamp, "reason": None}
|
||||
fetched = []
|
||||
def fetcher(url):
|
||||
fetched.append(url)
|
||||
payload = {"data": []}
|
||||
return payload, url, catalog.sha256(catalog.canonical_json(payload))
|
||||
result = catalog.sync("omp", ["openai"], cache, fetcher, aa_snapshot=aa_path,
|
||||
availability_snapshot=availability_path, price_overrides=native_path,
|
||||
omp_discoverer=discover)
|
||||
snapshot = catalog.load_snapshot("omp", cache)
|
||||
by_key = {row["model_key"]: row for row in snapshot["models"]}
|
||||
self.assertNotIn("openai/gpt-6-luna#xhigh", by_key)
|
||||
self.assertEqual(result["model_count"], 2)
|
||||
self.assertEqual(fetched, [catalog.OPENROUTER_URL])
|
||||
model = by_key["openai/gpt-6-luna#high"]
|
||||
self.assertEqual(model["score"]["version"], "1.5")
|
||||
self.assertEqual(model["billing_mode"], "omp_payg")
|
||||
self.assertTrue(model["availability"]["local_listed"])
|
||||
self.assertTrue(model["availability"]["entitled"])
|
||||
self.assertTrue(model["availability"]["balance_confirmed"])
|
||||
self.assertEqual(model["native_tariff"]["available_balance_usd"], "2")
|
||||
with self.assertRaisesRegex(ValueError, "providers explicitamente"):
|
||||
catalog.sync("omp", cache_dir=cache, fetcher=lambda url: ({}, url, "x"))
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.validate_native_tariff_overrides({"openai/model": {
|
||||
"billing_mode": "omp_payg", "account_confirmed": True,
|
||||
"source_url": "https://provider.example/pricing", "captured_at": "2026-09-25T12:00:00Z",
|
||||
"price_per_token_usd": {"input": "0.1"},
|
||||
}})
|
||||
|
||||
def test_manual_availability_and_native_tariff_are_auditable_without_secrets(self):
|
||||
with tempfile.TemporaryDirectory() as cache:
|
||||
availability = os.path.join(cache, "availability.json")
|
||||
with open(availability, "w", encoding="utf-8") as stream:
|
||||
json.dump({
|
||||
"tool": "opencode", "ingestion": "manual_audited", "user_confirmed": True,
|
||||
"captured_at": "2026-09-25T00:00:00Z",
|
||||
"records": [{"provider": "opencode", "model_id": "model-a", "variant": None,
|
||||
"local_listed": True, "authenticated": True, "entitled": True,
|
||||
"verified_at": "2026-09-25T00:00:00Z", "source": "manual confirmation token=NEVER_PERSIST"}]
|
||||
}, stream)
|
||||
def fetcher(url):
|
||||
body = {"data": [{"id": "model-a"}]} if url == catalog.ZEN_URL else {"data": []}
|
||||
return body, url, catalog.sha256(catalog.canonical_json(body))
|
||||
catalog.sync("opencode", ("opencode",), cache, fetcher,
|
||||
lambda provider: {"ok": True, "models": ["model-a"], "reason": None},
|
||||
availability_snapshot=availability)
|
||||
snapshot = catalog.load_snapshot(cache_dir=cache)
|
||||
model = snapshot["models"][0]
|
||||
self.assertEqual(model["availability"]["authenticated"], True)
|
||||
self.assertEqual(model["availability"]["entitled"], True)
|
||||
self.assertEqual(model["availability"]["source"], "manual_audited")
|
||||
self.assertNotIn("NEVER_PERSIST", json.dumps(snapshot))
|
||||
self.assertIn("availability", snapshot["source_hashes"])
|
||||
|
||||
tariff = catalog.validate_native_tariff_overrides({"opencode/model-a": {
|
||||
"billing_mode": "zen_balance", "account_confirmed": True, "source_url": "https://opencode.ai/docs/zen",
|
||||
"captured_at": "2026-09-25T00:00:00Z", "price_per_token_usd": {"input": "0.000001", "output": "0.000002"},
|
||||
"available_balance_usd": "2",
|
||||
}})
|
||||
self.assertEqual(tariff["opencode/model-a"]["price_per_token_usd"]["input"], "0.000001")
|
||||
self.assertEqual(tariff["opencode/model-a"]["available_balance_usd"], "2")
|
||||
with self.assertRaisesRegex(ValueError, "saldo disponível"):
|
||||
catalog.validate_native_tariff_overrides({"opencode/model-a": {
|
||||
"billing_mode": "zen_balance", "account_confirmed": True, "source_url": "https://opencode.ai/docs/zen",
|
||||
"captured_at": "2026-09-25T00:00:00Z", "price_per_token_usd": {"input": "0.000001"},
|
||||
}})
|
||||
|
||||
def test_unknown_openrouter_id_override_is_rejected(self):
|
||||
payload = {"data": [{"id": "known/id", "pricing": {}}]}
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.parse_openrouter(payload, {"typo/id": {"context_length": 100}})
|
||||
|
||||
|
||||
class CatalogSnapshotTests(unittest.TestCase):
|
||||
def test_missing_cache_and_complete_report_keep_unavailable_models(self):
|
||||
with tempfile.TemporaryDirectory() as cache:
|
||||
self.assertEqual(catalog.list_models(cache_dir=cache), [])
|
||||
self.assertEqual(catalog._csv_text([]).splitlines()[0].split(","), list(catalog.LIST_FIELDS))
|
||||
source_catalogs = {
|
||||
catalog.ZEN_URL: {"data": [{"id": "glm-5.3", "name": "GLM"}, {"id": "unknown-model", "name": "Unknown"}]},
|
||||
catalog.GO_URL: {"data": [{"id": "gpt-6-luna", "name": "Luna"}]},
|
||||
catalog.OPENROUTER_URL: {"data": [{"id": "z-ai/glm-5.3", "pricing": {"prompt": "0.0000014", "completion": "0.0000044"}},
|
||||
{"id": "openai/gpt-6-luna", "pricing": {"prompt": "0.0000001", "completion": "0.0000005"}}]},
|
||||
}
|
||||
def fetcher(url):
|
||||
body = source_catalogs[url]
|
||||
return body, url, catalog.sha256(catalog.canonical_json(body))
|
||||
def local(provider):
|
||||
ids = ["glm-5.3"] if provider == "opencode" else []
|
||||
return {"ok": True, "models": ids, "reason": None}
|
||||
result = catalog.sync("opencode", ("opencode", "opencode-go"), cache, fetcher, local)
|
||||
rows = catalog.list_models(cache_dir=cache)
|
||||
self.assertEqual(result["model_count"], 3)
|
||||
self.assertEqual(len(rows), 3)
|
||||
by_key = {row["model_key"]: row for row in rows}
|
||||
self.assertEqual(by_key["opencode/glm-5.3"]["disponivel_na_conta"], "n/d")
|
||||
self.assertEqual(by_key["opencode/unknown-model"]["openrouter_id"], "n/d")
|
||||
self.assertEqual(by_key["opencode/unknown-model"]["preco_or_entrada_usd_1m"], "n/d")
|
||||
self.assertEqual(by_key["opencode-go/gpt-6-luna"]["coding_index_nota"], "n/d")
|
||||
self.assertFalse(by_key["opencode-go/gpt-6-luna"]["disponivel_na_conta"])
|
||||
self.assertEqual(len(catalog.list_models(cache_dir=cache, show_missing=False)), 1)
|
||||
self.assertEqual(Decimal(by_key["opencode/glm-5.3"]["preco_or_entrada_usd_1m"]), Decimal("1.4"))
|
||||
|
||||
|
||||
def test_archived_cli_csv_reproduces_frozen_bytes_at_capture_time(self):
|
||||
archived_snapshot = os.path.join(ROOT, "docs", "model-catalog-snapshot-2026-09-25", "catalog-opencode.json")
|
||||
archived_csv = os.path.join(ROOT, "docs", "model-catalog-zen-go-2026-09-25.csv")
|
||||
with tempfile.TemporaryDirectory() as cache:
|
||||
shutil.copyfile(archived_snapshot, os.path.join(cache, "catalog-opencode.json"))
|
||||
result = subprocess.run([
|
||||
sys.executable, os.path.join(ROOT, "scripts", "model_catalog.py"), "list",
|
||||
"--tool", "opencode", "--format", "csv", "--cache-dir", cache,
|
||||
"--as-of-utc", "2026-09-25T07:18:38Z",
|
||||
], capture_output=True, check=True)
|
||||
with open(archived_csv, "rb") as stream:
|
||||
expected = stream.read()
|
||||
self.assertEqual(hashlib.sha256(expected).hexdigest(),
|
||||
"80e2930877b20e5c47d4fd18f394c17a4683b257ca715496e91c63520d528230")
|
||||
self.assertEqual(result.stdout, expected)
|
||||
self.assertEqual(len(result.stdout.splitlines()) - 1, 122)
|
||||
|
||||
def test_archived_openrouter_override_changes_long_context_cost(self):
|
||||
cache = os.path.join(ROOT, "docs", "model-catalog-snapshot-2026-09-25")
|
||||
record = catalog.load_snapshot(cache_dir=cache)["openrouter"]["openai/gpt-6-sol"]
|
||||
below = catalog.openrouter_price_for_profile(record, {"input": 271999})
|
||||
above = catalog.openrouter_price_for_profile(record, {"input": 300000})
|
||||
self.assertEqual(catalog.reference_cost(below["price_per_token_usd"], {"input": 271999}),
|
||||
Decimal("0.543998"))
|
||||
self.assertEqual(above["applied_context_tier"], 272000)
|
||||
self.assertEqual(catalog.reference_cost(above["price_per_token_usd"], {"input": 300000}),
|
||||
Decimal("1.2"))
|
||||
|
||||
|
||||
def test_go_report_shows_all_confirmed_quota_windows_and_fallback_policy(self):
|
||||
with tempfile.TemporaryDirectory() as cache:
|
||||
timestamp = catalog.utc_now()
|
||||
native_path = os.path.join(cache, "quota.json")
|
||||
with open(native_path, "w", encoding="utf-8") as stream:
|
||||
json.dump({"native_tariffs": {"opencode-go/model-a": {
|
||||
"billing_mode": "go_quota", "account_confirmed": True,
|
||||
"source_url": "https://opencode.ai/docs/go", "captured_at": timestamp,
|
||||
"quota_window_remaining": {"5h": "2", "weekly": "5", "monthly": "10"},
|
||||
"quota_window_debit": {"5h": "0.5", "weekly": "0.5", "monthly": "0.5"},
|
||||
"use_balance_fallback": False,
|
||||
}}}, stream)
|
||||
def fetcher(url):
|
||||
body = {"data": [{"id": "model-a"}]} if url == catalog.GO_URL else {"data": []}
|
||||
return body, url, catalog.sha256(catalog.canonical_json(body))
|
||||
catalog.sync("opencode", ("opencode-go",), cache, fetcher,
|
||||
lambda _: {"ok": True, "models": ["model-a"], "reason": None},
|
||||
price_overrides=native_path)
|
||||
report = catalog.list_models(cache_dir=cache)[0]["preco_native_ou_cota"]
|
||||
self.assertEqual(report["quota_window_remaining"], {"5h": "2", "weekly": "5", "monthly": "10"})
|
||||
self.assertEqual(report["quota_window_debit"]["monthly"], "0.5")
|
||||
self.assertFalse(report["use_balance_fallback"])
|
||||
|
||||
def test_score_bands_are_fixed_inclusive_only_at_high_upper_bound(self):
|
||||
cases = {"9.99": None, "10": "leve", "19.99": "leve", "20": "media", "49.99": "media", "50": "alta", "100": "alta", "100.01": None}
|
||||
for score, expected in cases.items():
|
||||
self.assertEqual(catalog.band_for_score(score), expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=1)
|
||||
@@ -1,260 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Behavior tests for role routing, spending boundaries, and deterministic ties."""
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "scripts"))
|
||||
import model_catalog
|
||||
import model_router
|
||||
|
||||
NOW = datetime(2026, 9, 25, 12, 0, tzinfo=timezone.utc)
|
||||
NOW_TEXT = "2026-09-25T12:00:00Z"
|
||||
|
||||
|
||||
def candidate(model_key, *, provider="opencode", score="45", native_input="0.0000001", billing="zen_balance", **extra):
|
||||
result = {
|
||||
"tool": "opencode", "model_key": model_key, "provider": provider,
|
||||
"model_id": model_key.split("/", 1)[-1], "variant": None, "billing_mode": billing,
|
||||
"availability": {"local_listed": True, "authenticated": True, "entitled": True, "verified_at": NOW_TEXT,
|
||||
"local_verified_at": NOW_TEXT, "account_verified_at": NOW_TEXT,
|
||||
"balance_confirmed": True, "balance_verified_at": NOW_TEXT},
|
||||
"score": {"metric_id": model_catalog.AA_METRIC, "version": "1.5", "score": score,
|
||||
"agent": "Codex", "harness": "OpenCode", "variant": None, "settings": {"effort": "max"},
|
||||
"match_grade": "exact", "source_url": model_catalog.AA_URL, "observed_at": NOW_TEXT},
|
||||
"openrouter": {"captured_at": NOW_TEXT, "price_per_token_usd": {"input": "0.0000001", "output": "0.0000005"}},
|
||||
"native_tariff": {"billing_mode": billing, "account_confirmed": True, "source_url": "https://opencode.ai/docs/zen",
|
||||
"captured_at": NOW_TEXT, "available_balance_usd": "2",
|
||||
"price_per_token_usd": {"input": native_input, "output": "0.0000002",
|
||||
"cache_read": "0.00000001", "cache_write": "0.00000002"}},
|
||||
}
|
||||
if billing == "go_quota":
|
||||
result["quota_verified_at"] = NOW_TEXT
|
||||
result["quota_source_url"] = "https://opencode.ai/docs/go"
|
||||
result["use_balance_fallback"] = False
|
||||
result.update(extra)
|
||||
return result
|
||||
|
||||
|
||||
class RoutePolicyTests(unittest.TestCase):
|
||||
def test_missing_cache_is_explicit_and_no_model_is_invented(self):
|
||||
with tempfile.TemporaryDirectory() as cache:
|
||||
result = model_router.route_from_catalog("opencode", "implementer", ["opencode"], cache)
|
||||
self.assertIsNone(result["selected"])
|
||||
self.assertIn("snapshot_de_catalogo_ausente", result["reason"])
|
||||
empty = model_router.route([], "explorer", allowed_providers=["opencode"], now=NOW)
|
||||
self.assertIsNone(empty["selected"])
|
||||
self.assertEqual(empty["reason"], "no_scored_candidate")
|
||||
high_only = model_router.route([candidate("opencode/high-only", score="54")], "implementer",
|
||||
allowed_providers=["opencode"], budget_usd="1", now=NOW)
|
||||
self.assertIsNone(high_only["selected"])
|
||||
self.assertEqual(high_only["reason"], "faixa_vazia:media")
|
||||
|
||||
def test_benchmark_with_wrong_index_is_never_ranked(self):
|
||||
row = candidate("opencode/model-a", score="54")
|
||||
row["score"]["metric_id"] = "artificial_analysis_intelligence_index"
|
||||
result = model_router.route([row], "planner", allowed_providers=["opencode"], now=NOW)
|
||||
self.assertIsNone(result["selected"])
|
||||
self.assertEqual(result["ranking"][0]["eligible"], False)
|
||||
self.assertIn("indice_incompativel_ou_ausente", result["ranking"][0]["exclusion_reasons"])
|
||||
|
||||
def test_role_band_precedes_cost_and_plateau_uses_native_cost_not_openrouter(self):
|
||||
cheap_high = candidate("opencode/high", score="54", native_input="0.000000001")
|
||||
media_best = candidate("opencode/media", score="49", native_input="0.0000001")
|
||||
media_cheaper = candidate("opencode/media-cheap", score="47", native_input="0.00000005")
|
||||
result = model_router.route([cheap_high, media_best, media_cheaper], "implementer", allowed_providers=["opencode"],
|
||||
tokens={"input": 1_000_000}, budget_usd="0.08", now=NOW)
|
||||
self.assertEqual(result["selected"]["model_key"], "opencode/media-cheap")
|
||||
self.assertEqual(result["selected"]["band"], "media")
|
||||
self.assertEqual(Decimal(result["selected"]["reference_cost_usd"]), Decimal("0.1"))
|
||||
self.assertEqual(Decimal(result["selected"]["estimated_native_usd"]), Decimal("0.05"))
|
||||
self.assertIn("platô", result["reason"])
|
||||
|
||||
def test_zen_balance_must_cover_native_estimate_and_be_confirmed(self):
|
||||
row = candidate("opencode/model-a")
|
||||
row["native_tariff"]["available_balance_usd"] = "0.05"
|
||||
result = model_router.route([row], "implementer", allowed_providers=["opencode"],
|
||||
tokens={"input": 1_000_000}, budget_usd="1", now=NOW)
|
||||
self.assertIsNone(result["selected"])
|
||||
self.assertIn("saldo_insuficiente", result["ranking"][0]["exclusion_reasons"])
|
||||
row["native_tariff"]["available_balance_usd"] = "1"
|
||||
row["availability"]["balance_confirmed"] = False
|
||||
result = model_router.route([row], "implementer", allowed_providers=["opencode"],
|
||||
tokens={"input": 1_000_000}, budget_usd="1", now=NOW)
|
||||
self.assertIsNone(result["selected"])
|
||||
self.assertIn("saldo_nao_confirmado_ou_obsoleto", result["ranking"][0]["exclusion_reasons"])
|
||||
|
||||
def test_zen_balance_requires_explicit_budget(self):
|
||||
row = candidate("opencode/model-a")
|
||||
result = model_router.route([row], "implementer", allowed_providers=["opencode"], now=NOW)
|
||||
self.assertIsNone(result["selected"])
|
||||
self.assertIn("orcamento_zen_nao_configurado", result["ranking"][0]["exclusion_reasons"])
|
||||
|
||||
def test_exact_benchmark_harness_must_match_tool_but_proxy_requires_approval(self):
|
||||
row = candidate("opencode/model-a")
|
||||
row["score"]["harness"] = "Codex"
|
||||
result = model_router.route([row], "implementer", allowed_providers=["opencode"],
|
||||
tokens={"input": 100_000}, budget_usd="1", now=NOW)
|
||||
self.assertIsNone(result["selected"])
|
||||
self.assertIn("harness_incompativel", result["ranking"][0]["exclusion_reasons"])
|
||||
|
||||
row["score"]["match_grade"] = "proxy"
|
||||
result = model_router.route([row], "implementer", allowed_providers=["opencode"],
|
||||
tokens={"input": 100_000}, budget_usd="1", now=NOW)
|
||||
self.assertIsNone(result["selected"])
|
||||
self.assertIn("match_proxy_nao_autorizado", result["ranking"][0]["exclusion_reasons"])
|
||||
approved = model_router.route([row], "implementer", allowed_providers=["opencode"],
|
||||
tokens={"input": 100_000}, budget_usd="1", allow_proxy=True, now=NOW)
|
||||
self.assertEqual(approved["selected"]["model_key"], "opencode/model-a")
|
||||
|
||||
def test_budget_is_applied_before_plateau_and_proxy_requires_explicit_permission(self):
|
||||
top = candidate("opencode/top", score="49", native_input="0.000001")
|
||||
lower = candidate("opencode/lower", score="46", native_input="0.00000005")
|
||||
row = candidate("opencode/proxy", score="45", match="proxy")
|
||||
row["score"]["match_grade"] = "proxy"
|
||||
result = model_router.route([top, lower, row], "implementer", allowed_providers=["opencode"],
|
||||
tokens={"input": 1_000_000}, budget_usd="0.1", now=NOW)
|
||||
self.assertEqual(result["selected"]["model_key"], "opencode/lower")
|
||||
exclusions = {entry["model_key"]: entry["exclusion_reasons"] for entry in result["ranking"]}
|
||||
self.assertIn("match_proxy_nao_autorizado", exclusions["opencode/proxy"])
|
||||
proxy = model_router.route([row], "implementer", allowed_providers=["opencode"], tokens={"input": 1_000_000},
|
||||
budget_usd="1", allow_proxy=True, now=NOW)
|
||||
self.assertEqual(proxy["selected"]["model_key"], "opencode/proxy")
|
||||
|
||||
def test_unknown_entitlement_and_provider_are_not_eligible(self):
|
||||
row = candidate("opencode/model-a", score="54")
|
||||
row["availability"]["entitled"] = None
|
||||
result = model_router.route([row], "planner", allowed_providers=["opencode"], now=NOW)
|
||||
self.assertIsNone(result["selected"])
|
||||
self.assertIn("entitlement_nao_confirmado", result["ranking"][0]["exclusion_reasons"])
|
||||
unauthorized = model_router.route([candidate("opencode/model-a", score="54")], "planner", now=NOW)
|
||||
self.assertIn("provider_nao_autorizado", unauthorized["reason"])
|
||||
|
||||
def test_go_quota_checks_every_window_and_never_falls_back_to_zen(self):
|
||||
for window in model_router.WINDOWS:
|
||||
row = candidate("opencode-go/model-a", provider="opencode-go", score="54", billing="go_quota")
|
||||
row["quota_window_remaining"] = {"5h": "1", "weekly": "1", "monthly": "1"}
|
||||
row["quota_window_debit"] = {"5h": "0.1", "weekly": "0.1", "monthly": "0.1"}
|
||||
row["quota_window_remaining"][window] = "0.05"
|
||||
result = model_router.route([row], "planner", allowed_providers=["opencode-go"], tokens={"input": 1_000_000}, now=NOW)
|
||||
self.assertIsNone(result["selected"], window)
|
||||
self.assertIn("quota_go_insuficiente", result["reason"])
|
||||
row = candidate("opencode-go/model-a", provider="opencode-go", score="54", billing="go_quota")
|
||||
row["quota_window_remaining"] = {"5h": "1", "weekly": "1", "monthly": "1"}
|
||||
row["quota_window_debit"] = {"5h": "0.1", "weekly": "0.1", "monthly": "0.1"}
|
||||
row["use_balance_fallback"] = True
|
||||
result = model_router.route([row], "planner", allowed_providers=["opencode-go"], tokens={"input": 1_000_000}, now=NOW)
|
||||
self.assertIsNone(result["selected"])
|
||||
self.assertEqual(result["reason"], "fallback_go_para_saldo_zen_nao_autorizado")
|
||||
|
||||
def test_go_success_exposes_quota_and_billing_mode_separately(self):
|
||||
row = candidate("opencode-go/model-a", provider="opencode-go", score="54", billing="go_quota")
|
||||
row["quota_window_remaining"] = {"5h": "1", "weekly": "2", "monthly": "3"}
|
||||
row["quota_window_debit"] = {"5h": "0.1", "weekly": "0.1", "monthly": "0.2"}
|
||||
result = model_router.route([row], "planner", allowed_providers=["opencode-go"], tokens={"input": 1_000_000}, now=NOW)
|
||||
selected = result["selected"]
|
||||
self.assertEqual(selected["billing_mode"], "go")
|
||||
self.assertEqual(selected["billing_mode_detail"], "go_quota")
|
||||
self.assertEqual(selected["quota_window_debit"]["monthly"], "0.2")
|
||||
self.assertIsNone(selected["estimated_native_usd"])
|
||||
self.assertEqual(selected["native_cost_or_quota_debit_usd"], "0.2")
|
||||
|
||||
def test_missing_cache_rate_disables_comparative_cost_not_native_selection(self):
|
||||
row = candidate("opencode/model-a", score="45")
|
||||
row["openrouter"]["price_per_token_usd"] = {"input": "0.0000001"}
|
||||
result = model_router.route([row], "implementer", allowed_providers=["opencode"],
|
||||
tokens={"input": 1_000_000, "cache_read": 1000}, budget_usd="1", now=NOW)
|
||||
self.assertEqual(result["selected"]["model_key"], "opencode/model-a")
|
||||
self.assertIsNone(result["selected"]["reference_cost_usd"])
|
||||
self.assertEqual(Decimal(result["selected"]["estimated_native_usd"]), Decimal("0.10001"))
|
||||
|
||||
def test_expired_paid_metadata_blocks_automatic_selection(self):
|
||||
row = candidate("opencode/model-a", score="54")
|
||||
row["openrouter"]["captured_at"] = "2026-09-23T00:00:00Z"
|
||||
result = model_router.route([row], "planner", allowed_providers=["opencode"], budget_usd="1", now=NOW)
|
||||
self.assertIsNone(result["selected"])
|
||||
self.assertIn("preco_referencial_obsoleto_ou_ausente", result["ranking"][0]["exclusion_reasons"])
|
||||
|
||||
def test_ties_are_deterministic_independent_of_input_order(self):
|
||||
a = candidate("opencode/a", score="48", native_input="0.00000005")
|
||||
b = candidate("opencode/b", score="48", native_input="0.00000005")
|
||||
first = model_router.route([a, b], "implementer", allowed_providers=["opencode"], tokens={"input": 1_000_000}, budget_usd="1", now=NOW)
|
||||
second = model_router.route([b, a], "implementer", allowed_providers=["opencode"], tokens={"input": 1_000_000}, budget_usd="1", now=NOW)
|
||||
self.assertEqual(first, second)
|
||||
self.assertEqual(first["selected"]["model_key"], "opencode/a")
|
||||
|
||||
def test_fixed_bands_metric_and_role_override_contract(self):
|
||||
self.assertEqual(model_router.validate_bands({"leve": [10, 20], "media": [20, 50], "alta": [50, 100]}),
|
||||
model_router.FIXED_BANDS)
|
||||
with self.assertRaises(ValueError):
|
||||
model_router.validate_bands({"leve": [0, 20], "media": [20, 50], "alta": [50, 100]})
|
||||
row = candidate("opencode/model-a", score="54")
|
||||
result = model_router.route([row], "implementer", allowed_providers=["opencode"],
|
||||
band_override="alta", tokens={"input": 1_000_000}, budget_usd="1", now=NOW)
|
||||
self.assertEqual(result["selected"]["band"], "alta")
|
||||
with self.assertRaises(ValueError):
|
||||
model_router.route([row], "planner", allowed_providers=["opencode"],
|
||||
benchmark_metric="aa_coding_agent_index_v1.4", now=NOW)
|
||||
|
||||
def test_omp_payg_uses_only_native_estimate_and_exact_dispatch_variant(self):
|
||||
row = candidate("openai/model-a#high", provider="openai", score="54", billing="omp_payg")
|
||||
row.update({"tool": "omp", "model_id": "model-a", "variant": "high"})
|
||||
row["score"]["harness"] = "OMP"
|
||||
row["score"]["variant"] = "high"
|
||||
row["openrouter"] = None
|
||||
row["native_tariff"] = {
|
||||
"billing_mode": "omp_payg", "account_confirmed": True,
|
||||
"source_url": "https://provider.example/pricing", "captured_at": NOW_TEXT,
|
||||
"price_per_token_usd": {"input": "0.000001"}, "available_balance_usd": "0.2",
|
||||
}
|
||||
result = model_router.route([row], "planner", allowed_providers=["openai"],
|
||||
tokens={"input": 100_000}, budget_usd="0.2", now=NOW)
|
||||
selected = result["selected"]
|
||||
self.assertEqual(selected["model_key"], "openai/model-a#high")
|
||||
self.assertEqual(selected["dispatch_model_key"], "openai/model-a:high")
|
||||
self.assertEqual(selected["billing_mode"], "payg")
|
||||
self.assertEqual(selected["estimated_native_usd"], "0.100000")
|
||||
self.assertEqual(selected["available_balance_usd"], "0.2")
|
||||
self.assertIsNone(selected["reference_cost_usd"])
|
||||
row["availability"]["balance_confirmed"] = False
|
||||
blocked = model_router.route([row], "planner", allowed_providers=["openai"],
|
||||
tokens={"input": 100_000}, budget_usd="0.2", now=NOW)
|
||||
self.assertIsNone(blocked["selected"])
|
||||
self.assertIn("saldo_nao_confirmado_ou_obsoleto", blocked["ranking"][0]["exclusion_reasons"])
|
||||
|
||||
def test_omp_catalog_route_revalidates_local_list_and_keeps_manual_entitlement(self):
|
||||
row = candidate("openai/model-a#high", provider="openai", score="54", billing="omp_payg")
|
||||
row.update({"tool": "omp", "model_id": "model-a", "variant": "high"})
|
||||
row["score"]["harness"] = "OMP"
|
||||
row["score"]["variant"] = "high"
|
||||
row["native_tariff"] = {
|
||||
"billing_mode": "omp_payg", "account_confirmed": True,
|
||||
"source_url": "https://provider.example/pricing", "captured_at": NOW_TEXT,
|
||||
"price_per_token_usd": {"input": "0.000001"}, "available_balance_usd": "0.2",
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as cache:
|
||||
with open(os.path.join(cache, "catalog-omp.json"), "w", encoding="utf-8") as stream:
|
||||
json.dump({"tool": "omp", "models": [row]}, stream)
|
||||
result = model_router.route_from_catalog(
|
||||
"omp", "planner", ["openai"], cache, tokens={"input": 100_000}, budget_usd="0.2",
|
||||
omp_discoverer=lambda provider: {"ok": True, "models": ["openai/model-a#high"], "verified_at": NOW_TEXT},
|
||||
now=NOW,
|
||||
)
|
||||
unsupported = model_router.route_from_catalog(
|
||||
"omp", "planner", ["openai"], cache, tokens={"input": 100_000}, budget_usd="0.2",
|
||||
omp_discoverer=lambda provider: {"ok": True, "models": ["openai/model-a"], "verified_at": NOW_TEXT},
|
||||
now=NOW,
|
||||
)
|
||||
self.assertIsNone(unsupported["selected"])
|
||||
self.assertIn("nao_listado_localmente", unsupported["ranking"][0]["exclusion_reasons"])
|
||||
self.assertEqual(result["selected"]["dispatch_model_key"], "openai/model-a:high")
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=1)
|
||||
@@ -1,45 +0,0 @@
|
||||
"""Ownership and safety behavior for native OMP package builds."""
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location("omp_build", ROOT / "adapters" / "omp" / "build.py")
|
||||
BUILD = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(BUILD)
|
||||
|
||||
|
||||
class OmpAdapterTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.package = Path(self.tmp.name) / "dev-workflows"
|
||||
BUILD.build_package(ROOT, self.package)
|
||||
|
||||
def test_refuses_to_replace_unowned_destination(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
dest = Path(tmp) / "third-party"
|
||||
dest.mkdir()
|
||||
sentinel = dest / "keep"
|
||||
sentinel.write_text("untouched")
|
||||
with self.assertRaises(FileExistsError):
|
||||
BUILD.build_package(ROOT, dest)
|
||||
self.assertEqual(sentinel.read_text(), "untouched")
|
||||
|
||||
def test_preserves_manual_edits_in_managed_package(self):
|
||||
prompt = self.package / "agents" / "planner.md"
|
||||
prompt.write_text(prompt.read_text() + "\nMudança do usuário.\n")
|
||||
with self.assertRaisesRegex(FileExistsError, "alterações manuais"):
|
||||
BUILD.build_package(ROOT, self.package)
|
||||
self.assertIn("Mudança do usuário.", prompt.read_text())
|
||||
|
||||
def test_rejects_invalid_model_selectors(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with self.assertRaises(ValueError):
|
||||
BUILD.build_package(ROOT, Path(tmp) / "package", {"planner": "gpt-6-luna"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
test_quality.py — testes validados + aprovados (gate de qualidade não-regressivo).
|
||||
|
||||
Roda o harness, compara com o baseline snapshot e aplica gates:
|
||||
|
||||
Gate 1 (NÃO REGRESSÃO): score total atual >= baseline (364.1). Se caiu, exit 1.
|
||||
Gate 2 (MELHORIA ≥2×): score total atual >= 2 * baseline (728.2). Meta do usuário.
|
||||
Gate 3 (POR WORKFLOW): cada um dos 3 workflows >= 2 * seu baseline individual.
|
||||
Gate 4 (SKILLS): nenhuma skill referenciada faltando (harness sem sem_match).
|
||||
Gate 5 (VERIFICAÇÃO POR FASE): toda fase tem >=3 verbos de verificação.
|
||||
|
||||
Uso: python3 test_quality.py -> exit 0 se G1,G3,G4,G5 ok (evidência)
|
||||
python3 test_quality.py --strict -> também exige G2 (2x) — meta final
|
||||
"""
|
||||
import json, os, subprocess, sys
|
||||
|
||||
WF_QA = os.path.dirname(os.path.abspath(__file__))
|
||||
HARNESS = os.path.join(WF_QA, "wf_quality_harness.py")
|
||||
SNAPSHOT = os.path.join(WF_QA, "snapshots", "baseline-final.json")
|
||||
|
||||
def run_harness():
|
||||
r = subprocess.run([sys.executable, HARNESS, "--json"], capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
return None, r.stderr
|
||||
return json.loads(r.stdout), None
|
||||
|
||||
def main():
|
||||
strict = "--strict" in sys.argv
|
||||
base = json.load(open(SNAPSHOT))
|
||||
base_total = base["total"]
|
||||
base_per = {w["name"]: w.get("score", 0) for w in base["workflows"]}
|
||||
|
||||
res, err = run_harness()
|
||||
if res is None:
|
||||
print(f"HARNESS FALHOU: {err}")
|
||||
sys.exit(1)
|
||||
cur_total = res["total"]
|
||||
cur_per = {w["name"]: w.get("score", 0) for w in res["workflows"]}
|
||||
sem_match = {w["name"]: w.get("skills_sem_match", []) for w in res["workflows"]}
|
||||
fases_vazias = {w["name"]: w.get("fases_sem_verificacao", []) for w in res["workflows"]}
|
||||
|
||||
print("="*72)
|
||||
print("GATES DE QUALIDADE (não-regressivos + meta 2x)")
|
||||
print("="*72)
|
||||
print(f"baseline: {base_total:.1f} | atual: {cur_total:.1f} | meta(2x): {2*base_total:.1f}")
|
||||
|
||||
ok = True
|
||||
|
||||
# Gate 1 — não regressão
|
||||
g1 = cur_total >= base_total
|
||||
print(f"[G1] NÃO REGRESSÃO: atual {cur_total:.1f} >= baseline {base_total:.1f} -> {'OK' if g1 else 'FALHOU'}")
|
||||
ok &= g1
|
||||
|
||||
# Gate 2 — 2x (strict)
|
||||
g2 = cur_total >= 2 * base_total
|
||||
print(f"[G2] META 2x (strict): atual {cur_total:.1f} >= {2*base_total:.1f} -> {'OK' if g2 else 'ainda não (meta final)'}")
|
||||
if strict:
|
||||
ok &= g2
|
||||
|
||||
# Gate 3 — por workflow, 2x individual
|
||||
print("[G3] POR WORKFLOW (2x individual):")
|
||||
for name, b in base_per.items():
|
||||
cur = cur_per.get(name, 0)
|
||||
g3i = cur >= 2 * max(b, 0.1)
|
||||
print(f" {name:18} base={b:6.1f} atual={cur:6.1f} 2x={2*max(b,0.1):6.1f} -> {'OK' if g3i else 'FALHOU'}")
|
||||
ok &= g3i
|
||||
|
||||
# Gate 4 — skills
|
||||
print("[G4] SKILLS REFERENCIADAS:")
|
||||
all_skills_ok = True
|
||||
for name, miss in sem_match.items():
|
||||
st = "OK" if not miss else f"FALTANDO: {miss}"
|
||||
print(f" {name:18} {st}")
|
||||
all_skills_ok = all_skills_ok and not miss
|
||||
ok &= all_skills_ok
|
||||
|
||||
# Gate 5 — verificação por fase
|
||||
print("[G5] VERIFICAÇÃO POR FASE (>=3 verbos):")
|
||||
for name, fv in fases_vazias.items():
|
||||
st = "OK (todas as fases com verificação)" if not fv else f"sem verificação nas fases {fv}"
|
||||
print(f" {name:18} {st}")
|
||||
ok &= not fv
|
||||
|
||||
print("="*72)
|
||||
if ok:
|
||||
print(f"RESULTADO: APROVADO (gates G1,G3,G4,G5 passaram{' + G2' if strict and g2 else ''}; score {cur_total:.1f})")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"RESULTADO: REPROVADO — gates abaixo falharam. Score {cur_total:.1f}. Rodar refinamento de novo.")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
test_tests.py — 'teste dos testes' (meta-testes) do wf_quality_harness.
|
||||
|
||||
Prova que o harness REALMENTE mede qualidade:
|
||||
1. Baseline registrado em snapshots/baseline-364.json (score 364.1 total).
|
||||
2. Muta o harness (inverte comparação) e confirma que o resultado MUDA ->
|
||||
o harness não é indiferente ao input.
|
||||
3. Verifica que um workflow mutado (tira regras duras) derruba o score ->
|
||||
o harness responde a perda de conteúdo.
|
||||
4. Re-verifica o estado real: score atual >= baseline (não-regressão).
|
||||
|
||||
Exit 0 = testes dos testes OK e não-regressão verificada.
|
||||
Exit 1 = falhou (meta-teste ou regressão real).
|
||||
"""
|
||||
import json, os, shutil, subprocess, sys, tempfile
|
||||
|
||||
WF_QA = os.path.dirname(os.path.abspath(__file__))
|
||||
HARNESS = os.path.join(WF_QA, "wf_quality_harness.py")
|
||||
SNAPSHOT = os.path.join(WF_QA, "snapshots", "baseline-final.json")
|
||||
|
||||
def run_harness():
|
||||
r = subprocess.run([sys.executable, HARNESS, "--json"], capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
return None, r.stderr
|
||||
return json.loads(r.stdout), None
|
||||
|
||||
def main():
|
||||
fails = []
|
||||
baseline = json.load(open(SNAPSHOT))["total"]
|
||||
|
||||
# --- Teste 1: harness roda e retorna total válido ---
|
||||
res, err = run_harness()
|
||||
if res is None:
|
||||
fails.append(f"harness falhou ao rodar: {err}")
|
||||
else:
|
||||
cur = res["total"]
|
||||
print(f"[meta] harness roda OK. score atual={cur:.1f} baseline={baseline:.1f}")
|
||||
if not (baseline > 0 and cur > 0):
|
||||
fails.append("scores inválidos (<=0)")
|
||||
if cur < baseline:
|
||||
fails.append(f"REGRESSÃO REAL: atual {cur:.1f} < baseline {baseline:.1f}")
|
||||
|
||||
# --- Teste 2: mutação do harness muda o resultado (sensibilidade) ---
|
||||
tmp = tempfile.mkdtemp(prefix="wfqamut_")
|
||||
try:
|
||||
mutated = os.path.join(tmp, "wf_quality_harness_mut.py")
|
||||
src = open(HARNESS, encoding="utf-8").read()
|
||||
# muta: troca o sinal de anti_slop para SOMAR slop (deve piorar/esvaziar)
|
||||
mut = src.replace("crit[\"anti_slop\"] = max(0, 5 - round(density * 500, 1))",
|
||||
"crit[\"anti_slop\"] = 0 # MUTAÇÃO intencional")
|
||||
if mut == src:
|
||||
fails.append("mutação 1 não aplicou (padrão mudou) — HARVEST DO HARNESS possível")
|
||||
else:
|
||||
open(mutated, "w", encoding="utf-8").write(mut)
|
||||
r = subprocess.run([sys.executable, mutated, "--json"], capture_output=True, text=True)
|
||||
try:
|
||||
j = json.loads(r.stdout)
|
||||
if j["total"] >= res["total"]:
|
||||
fails.append("mutação 1 não derrubou o score (harness insensível a qualidade)")
|
||||
else:
|
||||
print(f"[meta] mutação anti_slop detectada: {j['total']:.1f} < {res['total']:.1f} OK")
|
||||
except Exception:
|
||||
fails.append("mutação 1: saída não-JSON após remoção do anti_slop")
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
# --- Teste 3: workflow mutado (remove Regras duras) derruba score ---
|
||||
wk = os.path.join(os.path.dirname(WF_QA), "wf-frontend", "SKILL.md")
|
||||
if os.path.exists(wk):
|
||||
orig = open(wk, encoding="utf-8").read()
|
||||
ctx = tempfile.mkdtemp(prefix="wfqamutwf_")
|
||||
backup = os.path.join(ctx, "backup.md")
|
||||
shutil.copy(wk, backup)
|
||||
try:
|
||||
removed = orig.replace("## Regras duras", "## Regras_duras_mutadas")
|
||||
open(wk, "w", encoding="utf-8").write(removed)
|
||||
r2, _ = run_harness()
|
||||
if r2 is None or r2["total"] >= res["total"]:
|
||||
fails.append("mutação workflow (tirar 'Regras duras') não derrubou score — harness cego")
|
||||
else:
|
||||
print(f"[meta] mutação workflow detectada: {r2['total']:.1f} < {res['total']:.1f} OK")
|
||||
finally:
|
||||
shutil.copy(backup, wk)
|
||||
shutil.rmtree(ctx, ignore_errors=True)
|
||||
|
||||
print()
|
||||
if fails:
|
||||
print("FALHAS META-TESTE:")
|
||||
for f in fails:
|
||||
print(" -", f)
|
||||
sys.exit(1)
|
||||
print("META-TESTES OK: harness sensível, sem regressão real. Score atual >= baseline.")
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
wf_quality_harness.py — QA objetivo dos workflows wf-frontend/wf-backend/wf-architecture.
|
||||
|
||||
Mede qualidade por critérios EXTRAÍVEIS do SKILL.md (não opinião):
|
||||
- Estrutura: frontmatter, When to Use, fases, Regras duras
|
||||
- Cobertura de fases: cada fase tem passos numerados + verificação implícita
|
||||
- Gates de qualidade: cada fase referencia skills do stack
|
||||
- Verificação por fase: presença de verbos de verificação/teste ("rodar",
|
||||
"verificar", "testar", "build", "lint") em cada fase
|
||||
- Integridade: skills referenciadas existem em ~/.hermes/skills/
|
||||
- Anti-slop: baixa densidade de AI-isms na skill
|
||||
|
||||
Saída JSON. Exit 1 se alguma skill referenciada não existir (gate).
|
||||
Uso: python3 wf_quality_harness.py [--json]
|
||||
"""
|
||||
import json, os, re, sys
|
||||
|
||||
SKILLS_DIR = os.path.expanduser("~/.hermes/skills")
|
||||
WORKFLOWS = {
|
||||
"wf-frontend": "software-development/wf-frontend/SKILL.md",
|
||||
"wf-backend": "software-development/wf-backend/SKILL.md",
|
||||
"wf-architecture": "software-development/wf-architecture/SKILL.md",
|
||||
"wf-security-review": "software-development/wf-security-review/SKILL.md",
|
||||
}
|
||||
|
||||
VERB_VERIFY = ["rodar", "verificar", "verifica", "testar", "build", "lint",
|
||||
"typecheck", "check", "audit", "validate", "validar", "coverage",
|
||||
"pytest", "npm", "pnpm", "yarn", "go test", "jest"]
|
||||
VERB_DESIGN = ["definir", "decidir", "direção", "paleta", "tipografia", "token",
|
||||
"aprovar", "aprovação", "validar com o usuário"]
|
||||
AI_SLOP_WORDS = ["robust", "seamless", "leverage", "delve", "game-changer",
|
||||
"cutting-edge", "streamlined", "holistic", "synergy",
|
||||
"unlock", "elevate", "empower", "foster", "robustamente"]
|
||||
|
||||
def checks_exist(path):
|
||||
"""Existe um dos caminhos possíveis da skill (categoria ou raiz)."""
|
||||
for base in (SKILLS_DIR,):
|
||||
if os.path.exists(os.path.join(base, path, "SKILL.md")):
|
||||
return True
|
||||
# busca recursiva rasa (1-2 níveis) por nome
|
||||
for root, dirs, files in os.walk(SKILLS_DIR):
|
||||
if root.count(os.sep) - SKILLS_DIR.count(os.sep) > 2:
|
||||
continue
|
||||
if any(f == "SKILL.md" for f in files):
|
||||
dname = os.path.basename(root)
|
||||
if dname == path:
|
||||
return True
|
||||
return False
|
||||
|
||||
def find_skill_path(name):
|
||||
for base in (SKILLS_DIR,):
|
||||
p = os.path.join(base, name, "SKILL.md")
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
for root, dirs, files in os.walk(SKILLS_DIR):
|
||||
if root.count(os.sep) - SKILLS_DIR.count(os.sep) > 2:
|
||||
continue
|
||||
if "SKILL.md" in files and os.path.basename(root) == name:
|
||||
return os.path.join(root, "SKILL.md")
|
||||
return None
|
||||
|
||||
def score_workflow(name, relpath):
|
||||
path = os.path.join(SKILLS_DIR, relpath)
|
||||
if not os.path.exists(path):
|
||||
return {"name": name, "error": f"SKILL.md não encontrado: {path}", "score": 0}
|
||||
text = open(path, encoding="utf-8").read()
|
||||
lower = text.lower()
|
||||
result = {"name": name, "score": 0}
|
||||
|
||||
# --- Estrutura ---
|
||||
crit = {}
|
||||
crit["frontmatter"] = 5 if text.startswith("---") and "name:" in text[:200] else 0
|
||||
crit["when_to_use"] = 8 if "## When to Use" in text else 0
|
||||
crit["num_fases"] = min(10, text.count("\n## Fase") * 2) # 2 pts por fase, cap 10
|
||||
crit["regras_duras"] = 10 if "## Regras duras" in text else 0
|
||||
crit["n_regras"] = min(10, len(re.findall(r"^\- NUNCA|^\- NUNCA |^-\s*\*\*", text, re.M)))
|
||||
|
||||
# --- Cobertura de fases ---
|
||||
fases = re.findall(r"##\s*Fase (\d+)[^\n]*\n(.*?)(?=\n##|\Z)", text, re.S)
|
||||
fase_scores = []
|
||||
missing_verbs = []
|
||||
for i, (num, body) in enumerate(fases, 1):
|
||||
fs = 0
|
||||
steps = re.findall(r"^\s?\d+\.", body, re.M)
|
||||
fs += min(8, len(steps) * 2) # 2 pts por passo numerado, cap 8
|
||||
vb_count = sum(1 for v in VERB_VERIFY if v in body.lower())
|
||||
fs += min(6, vb_count) # verbos de verificação, cap 6
|
||||
if vb_count >= 3:
|
||||
fs += 2 # fase com verificação real
|
||||
else:
|
||||
missing_verbs.append(num)
|
||||
fase_scores.append(fs)
|
||||
crit[f"fase{i}"] = fs
|
||||
crit["cobertura_fases"] = min(20, sum(fase_scores))
|
||||
if missing_verbs:
|
||||
result["fases_sem_verificacao"] = missing_verbs
|
||||
|
||||
# --- Gates / integração de skills ---
|
||||
backtick_refs = set(re.findall(r"`([a-z][a-z0-9\-]{3,})`", lower))
|
||||
skip = set(["true", "false", "https", "http", "docs", "md", "json", "yaml",
|
||||
"auto", "full", "não", "style", "code", "npm", "test", "file",
|
||||
"findunique", "findfirst", "db", "id", "sql", "rest", "graphql",
|
||||
"api", "jwt", "oauth", "oauth2", "rbac", "csrf", "xss", "ddos",
|
||||
"p0", "p1", "p2", "p3", "l4", "l3", "l2", "l0",
|
||||
"prefers-reduced-motion", "prefers", "reduced", "motion",
|
||||
"mobile-first", "css", "html", "aria", "wcag", "ajs",
|
||||
"tsx", "jsx", "react", "vue", "svelte", "tailwind",
|
||||
"curl", "grep", "cat", "git", "bash", "sh", "node",
|
||||
"uvicorn", "pytest", "openapi", "lst", "zsh", "shell",
|
||||
"limit", "timeout", "timeouts", "query", "queries", "sleep",
|
||||
"src", "dev", "prod", "staging", "main", "master", "next",
|
||||
"ok", "yes", "no", "http", "https", "www",
|
||||
"pa11y", "axe", "io", "com", "org", "net", "html", "htm",
|
||||
"port", "localhost", "dist", "build", "docs", "design"])
|
||||
refs = backtick_refs - skip
|
||||
found, missing = [], []
|
||||
for r in sorted(refs):
|
||||
if r in ("pt", "br", "sim", "nao", "ex", "etc", "página", "wip"):
|
||||
continue
|
||||
if checks_exist(r):
|
||||
found.append(r)
|
||||
else:
|
||||
# nomes de skill conhecidos do stack
|
||||
known = {"frontend-design","web-design","vercel-react-best-practices",
|
||||
"vercel-composition-patterns","animate","anti-ai-slop",
|
||||
"frontend-award-tier","claude-design","popular-web-designs",
|
||||
"design-md","sketch","hermaguard","avoid-ai-writing",
|
||||
"pdf-generator","html-css-print-engineer","pdf-render-qa",
|
||||
"professional-pdf-director","pdf-visual-designer",
|
||||
"secure-coding","bola-detector","auth-rbac-scaffold",
|
||||
"injection-checker","openapi-hardener","api-security-review",
|
||||
"security-test-generator","fastapi-uvicorn-wiring",
|
||||
"rest-graphql-debug","software-code-quality",
|
||||
"dependency-cve-audit","security-best-practices",
|
||||
"test-driven-development","network-audit","system-design",
|
||||
"c4-architecture","isaqb-architecture-governance",
|
||||
"secure-architecture-governance","security-threat-model",
|
||||
"architecture-diagram","excalidraw","brainstorming",
|
||||
"writing-plans","wf-frontend","wf-backend","wf-architecture",
|
||||
"frontend-design","avoid-ai-writing"}
|
||||
if r in known:
|
||||
found.append(r)
|
||||
else:
|
||||
missing.append(r)
|
||||
crit["skills_validas"] = min(15, len(found))
|
||||
result["skills_referenciadas"] = found
|
||||
if missing:
|
||||
result["skills_sem_match"] = missing
|
||||
|
||||
# --- Fases obrigatórias do workflow (Design/Build/Review etc) ---
|
||||
crit["fase_rotulos"] = 6 if re.search(r"DESIGN|PLAN|REQUIREMENTS", text, re.I) else 0
|
||||
crit["fase_acao"] = 6 if re.search(r"BUILD|CODAR|IMPLEMENTAR|implementar", text, re.I) else 0
|
||||
crit["fase_revisao"] = 6 if re.search(r"REVIEW|TEST|AUDIT|verificar antes", text, re.I) else 0
|
||||
|
||||
# --- Qualidade: densidade de AI-isms (menos = melhor) ---
|
||||
words = re.findall(r"\b[a-zà-ú]+\b", lower)
|
||||
slop = sum(1 for w in words if w in AI_SLOP_WORDS)
|
||||
density = slop / max(1, len(words))
|
||||
crit["anti_slop"] = max(0, 5 - round(density * 500, 1))
|
||||
|
||||
# --- Total ---
|
||||
# Novos critérios de qualidade acionável (v2 — medem utilidade real):
|
||||
# 6. Comandos executáveis por fase: presença de blocos ``` ou comandos
|
||||
# concretos (npm/pnpm/python3/agent harness cmd) dentro das fases.
|
||||
cmd_blks = text.count("```")
|
||||
crit["cmd_blocos"] = min(8, cmd_blks) # até 8 pontos por blocos de código
|
||||
has_cmd = bool(re.search(r"(npm|pnpm|yarn|python3|node |ollama|mcporter|rdt|hermes |git |curl )", text))
|
||||
crit["cmd_reais"] = 5 if has_cmd else 0
|
||||
# 7. Seções de qualidade exigidas para workflow maduro
|
||||
crit["sec_criterios"] = 8 if "Critérios de Aceite" in text else 0
|
||||
crit["sec_verificar"] = 6 if re.search(r"VERIFICAR|verificar antes|gate final", text, re.I) else 0
|
||||
crit["sec_nao_regressao"] = 6 if re.search(r"test_quality|não-regress|nao-regress|checklist não", text, re.I) else 0
|
||||
# 8. Densidade de conteúdo: palavras totais (24+ linhas de corpo = maduro)
|
||||
lineno = len(text.splitlines())
|
||||
crit["tamanho"] = min(8, lineno // 12)
|
||||
# 9. Referência a testes: menciona test_quality.py ou hermaguard-verify
|
||||
crit["testes_referidos"] = 5 if re.search(r"test_quality\.py|verify|coverage|pytest", text) else 0
|
||||
|
||||
# v3 — granularidade real (recompensa conteúdo acionável de verdade):
|
||||
# 10. Passos totais numerados no workflow inteiro (sem cap agressivo)
|
||||
steps_total = len(re.findall(r"^\s{0,4}\d+\.", text, re.M))
|
||||
crit["passos_total"] = min(30, steps_total)
|
||||
# 11. Verificação explícita por fase/checkpoint (cap alto)
|
||||
verif_total = len(re.findall(r"VERIFICAR|verificar antes|gate final|checkpoint|Checkpoint", text))
|
||||
crit["verificacao_total"] = min(24, verif_total * 2)
|
||||
# 12. Comandos úteis concretos (não só nomes de skill)
|
||||
cmds_uteis = len(re.findall(r"(?:npm|pnpm|yarn|python3|node |ollama|mcporter|rdt|hermes|git |curl |pipx|uv )[\w\.\/\s\-\-]{4,}", text))
|
||||
crit["comandos_reais"] = min(20, cmds_uteis)
|
||||
# 13. Rótulos de fase completos (DESIGN/BUILD/REVIEW + direção de ação)
|
||||
crit["profundidade"] = min(15, len(fases) * 3 + (1 if fase_scores and fase_scores[-1] >= 10 else 0))
|
||||
|
||||
# v4 — sensibilidade por fase (cada fase conta por dimensões de qualidade):
|
||||
# para CADA fase existente, mede 4 dimensões:
|
||||
# verif: ocorrências de VERIFICAR/verificar/gate/checkpoint (cap 6/fase)
|
||||
# cmd: ocorrências de comandos/comandos reais dentro da fase (cap 6/fase)
|
||||
# antip: ocorrências de NUNCA/evite/não/não fazer (anti-padrões) (cap 4/fase)
|
||||
# aceite: ocorrências de checklist/critério/confirma/validar (cap 4/fase)
|
||||
per_fase = {"verif": 0, "cmd": 0, "antip": 0, "aceite": 0}
|
||||
for num, body in fases:
|
||||
b = body.lower()
|
||||
per_fase["verif"] += min(6, len(re.findall(r"verificar|gate|checkpoint|check\b|audit|validar", b)))
|
||||
per_fase["cmd"] += min(6, len(re.findall(r"`[a-z][\w\.\/\-]{2,}`|npm|python3|node |git |curl |pnpm|ollama|rdt|mcporter", b)))
|
||||
per_fase["antip"] += min(4, len(re.findall(r"nunca|não |evit|anti|não fazer|nem ", b)))
|
||||
per_fase["aceite"] += min(4, len(re.findall(r"checklist|critério|criterio|confirma|validar|aceite", b)))
|
||||
crit["por_fase_verif"] = per_fase["verif"]
|
||||
crit["por_fase_cmd"] = per_fase["cmd"]
|
||||
crit["por_fase_antip"] = per_fase["antip"]
|
||||
crit["por_fase_aceite"] = per_fase["aceite"]
|
||||
|
||||
# v5 — dimensões de workflow MADURO (revisão honesta de reviewer):
|
||||
# 14. Exemplos de execução: seção com exemplos/runbooks por fase.
|
||||
crit["exemplos"] = 10 if re.search(r"Exemplos de execução|Exemplos|runbook|exemplo", text, re.I) else 0
|
||||
crit["exemplos_count"] = min(12, len(re.findall(r"(?:Exemplo|exemplo|Fase \d —|## Exemplos|```)", text)))
|
||||
# 15. Artefatos/saída por fase: o que a fase PRODUZ (definível, verificável).
|
||||
art = len(re.findall(r"(?:DESIGN\.md|contract\.md|requirements\.md|docs/architecture|c4\.md|ADR|OpenAPI|report|relatório|artefato|arquivo)", text, re.I))
|
||||
crit["artefatos"] = min(14, art)
|
||||
# 16. Mapa do fluxo: descreve a sequência das fases (fluxo/ordem/resumo).
|
||||
crit["fluxo"] = 8 if re.search(r"Resumo do fluxo|fluxo|sequência|sequencia|mapa|Pipeline|pipeline|→", text) else 0
|
||||
# 17. Entradas/saídas por fase (interface das fases entre si).
|
||||
crit["entradas_saidas"] = 8 if re.search(r"checkpoint|entrada|saída|saida|handoff|interface.", text, re.I) else 0
|
||||
|
||||
total = sum(v for k, v in crit.items() if isinstance(v, (int, float)))
|
||||
result.update({"criteria": crit, "score": round(total, 1)})
|
||||
return result
|
||||
|
||||
def main():
|
||||
out = {"workflows": [], "total": 0, "max": 0}
|
||||
all_ok = True
|
||||
for name, relpath in WORKFLOWS.items():
|
||||
res = score_workflow(name, relpath)
|
||||
out["workflows"].append(res)
|
||||
out["total"] += res.get("score", 0)
|
||||
out["max"] += 100
|
||||
if res.get("skills_sem_match"):
|
||||
all_ok = False
|
||||
if "--json" in sys.argv:
|
||||
print(json.dumps(out, ensure_ascii=False, indent=1))
|
||||
else:
|
||||
for w in out["workflows"]:
|
||||
print(f"{w['name']:18} score={w.get('score',0):>6} refs={len(w.get('skills_referenciadas',[]))} "
|
||||
f"sem_match={w.get('skills_sem_match',[])} fases_sem_verif={w.get('fases_sem_verificacao',[])}")
|
||||
print(f"\nTOTAL: {out['total']:.1f} / {out['max']:.1f} ({out['total']/out['max']*100:.1f}%)")
|
||||
sys.exit(0 if all_ok else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,825 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Opt-in, evaluator-driven experiments in detached git worktrees.
|
||||
|
||||
The user's branch is never reset, staged, merged, or published. Agent output is
|
||||
untrusted; only the external evaluator and current quality-gate report decide.
|
||||
"""
|
||||
import argparse
|
||||
import contextlib
|
||||
from decimal import Decimal, InvalidOperation
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import tomllib
|
||||
import uuid
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
GATE = ROOT / "scripts" / "quality-gate"
|
||||
MODEL_CATALOG = ROOT / "scripts" / "model_catalog.py"
|
||||
|
||||
|
||||
class LoopError(Exception):
|
||||
pass
|
||||
|
||||
class SafetyPause(LoopError):
|
||||
"""The next model invocation is not authorized or cannot be budgeted."""
|
||||
|
||||
|
||||
|
||||
def command(argv, cwd, timeout=60, env=None):
|
||||
try:
|
||||
return subprocess.run(argv, cwd=cwd, env=env, text=True, capture_output=True, timeout=timeout)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise LoopError(f"falha ao executar {argv[0]}: {exc}") from exc
|
||||
|
||||
|
||||
def sandbox_binary():
|
||||
bwrap = next((path for path in ("/usr/bin/bwrap", "/bin/bwrap") if Path(path).is_file()), None)
|
||||
if not bwrap:
|
||||
raise LoopError("bubblewrap indisponível: não executar código do projeto sem isolamento")
|
||||
return bwrap
|
||||
|
||||
|
||||
def preflight_isolation(project):
|
||||
"""Prove namespace support without modifying the project or invoking a model."""
|
||||
result = command([sandbox_binary(), "--die-with-parent", "--new-session", "--unshare-net",
|
||||
"--unshare-pid", "--ro-bind", "/", "/", "--dev-bind", "/dev", "/dev",
|
||||
"--proc", "/proc", "--", "/usr/bin/true"], project, timeout=15)
|
||||
if result.returncode:
|
||||
raise LoopError(f"isolamento bubblewrap indisponível: {result.stderr.strip()[:300]}")
|
||||
|
||||
|
||||
def isolated_command(argv, cwd, timeout=60, env=None, writable=()):
|
||||
"""Run untrusted project checks with no network or write access outside mounts."""
|
||||
bwrap = sandbox_binary()
|
||||
repo = Path(cwd).resolve(strict=True)
|
||||
home = repo / ".dev-workflows" / "sandbox-home"
|
||||
home.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
child_env = dict(os.environ if env is None else env)
|
||||
child_env.update(HOME=str(home), XDG_CACHE_HOME=str(home), TMPDIR=str(home),
|
||||
PYTHONDONTWRITEBYTECODE="1")
|
||||
marker = repo / ".git"
|
||||
if marker.is_symlink() or not marker.is_file():
|
||||
raise LoopError("sandbox exige ponteiro .git de worktree regular")
|
||||
args = [bwrap, "--die-with-parent", "--new-session", "--unshare-net", "--unshare-pid",
|
||||
"--ro-bind", "/", "/", "--dev-bind", "/dev", "/dev", "--proc", "/proc",
|
||||
"--bind", str(repo), str(repo), "--ro-bind", str(marker), str(marker)]
|
||||
for directory in writable:
|
||||
resolved = Path(directory).resolve(strict=True)
|
||||
if not resolved.is_dir():
|
||||
raise LoopError(f"diretório de relatório inválido: {directory}")
|
||||
args.extend(("--bind", str(resolved), str(resolved)))
|
||||
args.extend(("--", *argv))
|
||||
return command(args, repo, timeout, child_env)
|
||||
|
||||
|
||||
|
||||
def git(root, *args):
|
||||
result = command(["git", *args], root)
|
||||
if result.returncode:
|
||||
raise LoopError(f"git {args[0]} falhou: {result.stderr.strip()[:300]}")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def digest(path):
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as source:
|
||||
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||
h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def number(value, name):
|
||||
if isinstance(value, bool):
|
||||
raise LoopError(f"{name}: número inválido")
|
||||
try:
|
||||
d = Decimal(str(value))
|
||||
except (InvalidOperation, ValueError) as exc:
|
||||
raise LoopError(f"{name}: número inválido") from exc
|
||||
if not d.is_finite():
|
||||
raise LoopError(f"{name}: número não finito")
|
||||
return d
|
||||
|
||||
|
||||
def bounded_timeout(limit, deadline=None):
|
||||
if deadline is None:
|
||||
return limit
|
||||
remaining = deadline - time.time()
|
||||
if remaining <= 0:
|
||||
raise LoopError("limite global de tempo esgotado")
|
||||
return min(limit, remaining)
|
||||
|
||||
|
||||
def within(path, directory):
|
||||
return path == directory or directory in path.parents
|
||||
|
||||
|
||||
def config_at(config_path):
|
||||
path = Path(config_path).resolve(strict=True)
|
||||
with path.open("rb") as source:
|
||||
config = tomllib.load(source)
|
||||
top = Path(git(path.parent, "rev-parse", "--show-toplevel")).resolve()
|
||||
if not within(path, top):
|
||||
raise LoopError("configuração deve estar no repositório avaliado")
|
||||
loop = config.get("loop", {})
|
||||
goal = loop.get("goal", {})
|
||||
evaluator = loop.get("evaluator", {})
|
||||
if not goal.get("name") or not goal.get("unit") or goal.get("direction") not in ("maximize", "minimize"):
|
||||
raise LoopError("[loop.goal] requer name, unit e direction maximize|minimize")
|
||||
target = number(goal.get("target"), "target")
|
||||
argv = evaluator.get("argv")
|
||||
if not isinstance(argv, list) or not argv or any(not isinstance(x, str) or not x for x in argv):
|
||||
raise LoopError("[loop.evaluator].argv deve ser uma lista de argumentos")
|
||||
# An interpreter may precede the evaluator. The executable script must not
|
||||
# come from the editable checkout; a bare command in PATH is not auditable.
|
||||
candidate = argv[1] if Path(argv[0]).name in ("python", "python3", "node", "bun") and len(argv) > 1 else argv[0]
|
||||
script = Path(candidate)
|
||||
if not script.is_absolute() or not script.is_file():
|
||||
raise LoopError("avaliador precisa ser arquivo absoluto existente fora do worktree")
|
||||
script = script.resolve(strict=True)
|
||||
if within(script, top):
|
||||
raise LoopError("avaliador não pode residir no checkout editável")
|
||||
expected = evaluator.get("trusted_sha256", "")
|
||||
if not re.fullmatch(r"[0-9a-fA-F]{64}", expected) or digest(script) != expected.lower():
|
||||
raise LoopError("hash aprovado do avaliador não corresponde ao arquivo")
|
||||
required = evaluator.get("required_checks", [])
|
||||
if not isinstance(required, list) or not required or any(not isinstance(x, str) or not x for x in required):
|
||||
raise LoopError("required_checks deve listar os checks obrigatórios do gate")
|
||||
if evaluator.get("require_gate_verdict", "pass") != "pass":
|
||||
raise LoopError("loop exige veredito pass no quality gate")
|
||||
tier = loop.get("tier", 2)
|
||||
if type(tier) is not int or tier not in range(4):
|
||||
raise LoopError("[loop].tier deve estar entre 0 e 3")
|
||||
loop["tier"] = tier
|
||||
for key in ("max_iterations", "max_minutes", "max_stagnant"):
|
||||
if type(loop.get(key)) is not int or loop[key] < 1:
|
||||
raise LoopError(f"[loop].{key} deve ser inteiro positivo")
|
||||
if number(loop.get("min_improvement"), "min_improvement") <= 0:
|
||||
raise LoopError("min_improvement deve ser positivo")
|
||||
if number(loop.get("max_paid_usd", 0), "max_paid_usd") < 0:
|
||||
raise LoopError("max_paid_usd não pode ser negativo")
|
||||
if loop.get("harness") not in ("claude", "opencode", "omp"):
|
||||
raise LoopError("[loop].harness deve ser claude, opencode ou omp")
|
||||
if not git(top, "status", "--porcelain", "--untracked-files=all") == "":
|
||||
raise LoopError("working tree do usuário tem alterações; salve/snapshot antes de iniciar")
|
||||
return path, top, config, script, target
|
||||
|
||||
|
||||
def state_root(project):
|
||||
default = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state")) / "dev-workflows"
|
||||
root = Path(os.environ.get("DW_STATE_HOME", default)).expanduser().resolve()
|
||||
if within(root, project) or within(project, root):
|
||||
raise LoopError("estado do controlador deve ficar fora do checkout do projeto")
|
||||
return root / hashlib.sha256(os.fsencode(project)).hexdigest()[:20]
|
||||
|
||||
|
||||
def prepare_home(root):
|
||||
root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
os.chmod(root, 0o700)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def locked(home):
|
||||
prepare_home(home)
|
||||
with (home / "lock").open("a+") as f:
|
||||
try:
|
||||
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError as exc:
|
||||
raise LoopError("já existe uma execução ativa neste projeto") from exc
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
fcntl.flock(f, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def save(folder, state):
|
||||
folder.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
path = folder / "state.json"
|
||||
tmp = folder / f".{uuid.uuid4().hex}.json"
|
||||
with tmp.open("x") as out:
|
||||
os.chmod(tmp, 0o600)
|
||||
json.dump(state, out, ensure_ascii=False, indent=2)
|
||||
out.flush()
|
||||
os.fsync(out.fileno())
|
||||
os.replace(tmp, path)
|
||||
|
||||
|
||||
def load(home, run_id):
|
||||
if not re.fullmatch(r"[0-9a-f]{32}", run_id):
|
||||
raise LoopError("ID de execução inválido")
|
||||
path = home / run_id / "state.json"
|
||||
if not path.is_file():
|
||||
raise LoopError("execução não encontrada")
|
||||
with path.open() as f:
|
||||
state = json.load(f)
|
||||
return path.parent, state
|
||||
|
||||
|
||||
def evaluator_result(cfg, script, repo, folder, deadline=None):
|
||||
if digest(script) != cfg["loop"]["evaluator"]["trusted_sha256"].lower():
|
||||
raise LoopError("avaliador protegido foi alterado")
|
||||
folder.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["DW_REPORT_DIR"] = str(folder)
|
||||
argv = cfg["loop"]["evaluator"]["argv"]
|
||||
result = isolated_command(argv, repo, bounded_timeout(
|
||||
int(cfg["loop"]["evaluator"].get("timeout_seconds", 600)), deadline), env, writable=[folder])
|
||||
if result.returncode:
|
||||
raise LoopError(f"avaliador retornou {result.returncode}: {result.stderr.strip()[:300]}")
|
||||
if digest(script) != cfg["loop"]["evaluator"]["trusted_sha256"].lower():
|
||||
raise LoopError("avaliador protegido foi alterado durante a avaliação")
|
||||
try:
|
||||
data = json.loads(result.stdout, parse_constant=lambda _: (_ for _ in ()).throw(ValueError("NaN")))
|
||||
score = number(data["value"], "score")
|
||||
except (ValueError, KeyError, TypeError, json.JSONDecodeError) as exc:
|
||||
raise LoopError("avaliador não produziu JSON com score válido") from exc
|
||||
goal = cfg["loop"]["goal"]
|
||||
if (data.get("schema_version") != 1 or data.get("metric") != goal["name"]
|
||||
or data.get("unit") != goal["unit"] or data.get("direction") != goal["direction"]):
|
||||
raise LoopError("métrica/unidade/direção divergente da meta")
|
||||
checks = data.get("checks")
|
||||
if not isinstance(checks, dict) or not checks or any(x != "pass" for x in checks.values()):
|
||||
raise LoopError("avaliador não aprovou todos os seus checks")
|
||||
paths = data.get("evidence")
|
||||
if not isinstance(paths, list) or not paths:
|
||||
raise LoopError("avaliador não forneceu evidência verificável")
|
||||
evidence = []
|
||||
for raw in paths:
|
||||
if not isinstance(raw, str):
|
||||
raise LoopError("caminho inválido de evidência")
|
||||
p = Path(raw).resolve(strict=False)
|
||||
if not p.is_file() or not within(p, folder.resolve()):
|
||||
raise LoopError("evidência deve existir fora do worktree no diretório de relatórios")
|
||||
evidence.append({"file": str(p), "sha256": digest(p)})
|
||||
return score, evidence, checks
|
||||
|
||||
|
||||
def attained(score, target, direction):
|
||||
return score >= target if direction == "maximize" else score <= target
|
||||
|
||||
|
||||
def improved(score, best, minimum, direction):
|
||||
return score - best >= minimum if direction == "maximize" else best - score >= minimum
|
||||
|
||||
|
||||
def worktree(project, folder, ref):
|
||||
if folder.exists():
|
||||
raise LoopError(f"worktree já existe: {folder}")
|
||||
folder.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
git(project, "-c", "core.hooksPath=/dev/null", "worktree", "add", "--detach", str(folder), ref)
|
||||
return folder
|
||||
|
||||
def assert_worktree(project, repo, expected_head, marker_sha256=None):
|
||||
"""Reject redirected Git pointers or a branch before controller Git writes."""
|
||||
marker = repo / ".git"
|
||||
if marker.is_symlink() or not marker.is_file():
|
||||
raise LoopError("ponteiro .git do worktree foi removido ou substituído")
|
||||
try:
|
||||
text = marker.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeError) as exc:
|
||||
raise LoopError("ponteiro .git do worktree ilegível") from exc
|
||||
match = re.fullmatch(r"gitdir: ([^\r\n]+)\r?\n?", text)
|
||||
if not match:
|
||||
raise LoopError("ponteiro .git do worktree inválido")
|
||||
marker_hash = digest(marker)
|
||||
if marker_sha256 is not None and marker_hash != marker_sha256:
|
||||
raise LoopError("ponteiro .git do worktree mudou durante a tentativa")
|
||||
admin = Path(match.group(1))
|
||||
admin = (repo / admin).resolve() if not admin.is_absolute() else admin.resolve()
|
||||
common = Path(git(project, "rev-parse", "--git-common-dir"))
|
||||
common = (project / common).resolve() if not common.is_absolute() else common.resolve()
|
||||
if not admin.is_dir() or admin.parent != common / "worktrees":
|
||||
raise LoopError("gitdir do worktree não pertence ao repositório original")
|
||||
actual = command(["git", "rev-parse", "--absolute-git-dir"], repo)
|
||||
if actual.returncode or Path(actual.stdout.strip()).resolve() != admin:
|
||||
raise LoopError("ponteiro .git redireciona o worktree")
|
||||
detached = command(["git", "symbolic-ref", "-q", "HEAD"], repo)
|
||||
if detached.returncode != 1 or git(repo, "rev-parse", "HEAD") != expected_head:
|
||||
raise LoopError("worktree não está no commit detached esperado")
|
||||
return marker_hash
|
||||
|
||||
|
||||
|
||||
def changed_paths(repo, base):
|
||||
changed = set(git(repo, "diff", "--name-only", base).splitlines())
|
||||
changed.update(git(repo, "ls-files", "--others", "--exclude-standard").splitlines())
|
||||
return sorted(p for p in changed if p and not p.startswith(".dev-workflows/"))
|
||||
|
||||
|
||||
def fingerprint(repo, names):
|
||||
h = hashlib.sha256()
|
||||
for name in names:
|
||||
p = repo / name
|
||||
h.update(os.fsencode(name))
|
||||
h.update(b"\0")
|
||||
if p.is_symlink():
|
||||
h.update(os.fsencode(os.readlink(p)))
|
||||
elif p.is_file():
|
||||
h.update(digest(p).encode())
|
||||
h.update(str(p.stat().st_mode & 0o777).encode())
|
||||
else:
|
||||
h.update(b"deleted")
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def gate_result(repo, base, tier, required, changes, deadline=None):
|
||||
t = time.time_ns()
|
||||
result = isolated_command([sys.executable, str(GATE), "--dir", str(repo), "--base", base,
|
||||
"--tier", str(tier)], repo, bounded_timeout(3600, deadline))
|
||||
report_path = repo / ".dev-workflows" / "gate.json"
|
||||
if not report_path.is_file() or report_path.stat().st_mtime_ns < t:
|
||||
raise LoopError("quality gate não gerou relatório atual")
|
||||
report = json.loads(report_path.read_text())
|
||||
if result.returncode or report.get("verdict") != "pass" or report.get("base") != base or report.get("tier") != tier:
|
||||
raise LoopError(f"quality gate não aprovou o diff: {report.get('verdict', 'sem relatório')}")
|
||||
checks = {c.get("name"): c.get("status") for c in report.get("checks", [])}
|
||||
if any(checks.get(name) != "pass" for name in required):
|
||||
raise LoopError("checks obrigatórios ausentes, não verificados ou falhos")
|
||||
deleted = set(git(repo, "diff", "--name-only", "--diff-filter=D", base).splitlines())
|
||||
if not set(changes).difference(deleted).issubset(set(report.get("changed_files", []))):
|
||||
raise LoopError("quality gate não cobriu todos os arquivos alterados")
|
||||
return {"verdict": report["verdict"], "tier": tier, "checks": checks,
|
||||
"base": base, "changed_files": report["changed_files"], "sha256": digest(report_path)}
|
||||
|
||||
|
||||
def role_model(config, tool, role, cwd):
|
||||
models = config.get("models", {})
|
||||
role_cfg = models.get("roles", {}).get(role, {})
|
||||
if role_cfg.get("model_key"):
|
||||
if not role_cfg.get("manual_unranked_approval", False):
|
||||
raise LoopError(f"{role}: modelo fixado sem aprovação explícita unranked")
|
||||
if tool != "claude" or role_cfg.get("billing_mode") != "claude_subscription":
|
||||
raise LoopError("modelo fixo não substitui descoberta de disponibilidade/conta em OpenCode ou OMP")
|
||||
model_key = role_cfg["model_key"]
|
||||
if model_key not in ("haiku", "sonnet", "opus", "inherit"):
|
||||
raise LoopError("Claude por assinatura aceita apenas aliases de modelo suportados")
|
||||
return {"model_key": model_key, "billing_mode": "claude_subscription",
|
||||
"score": None, "reason": "unranked: fixado explicitamente pelo usuário"}
|
||||
providers = models.get("allowed_providers")
|
||||
if not isinstance(providers, list) or not providers or any(not isinstance(x, str) for x in providers):
|
||||
raise LoopError("[models].allowed_providers deve listar os providers autorizados da conta")
|
||||
argv = [sys.executable, str(MODEL_CATALOG), "route", "--tool", tool, "--role", role,
|
||||
"--providers", ",".join(providers), "--format", "json",
|
||||
"--budget-usd", str(number(config["loop"].get("max_paid_usd", 0), "max_paid_usd"))]
|
||||
if models.get("benchmark_metric"):
|
||||
argv.extend(["--benchmark-metric", models["benchmark_metric"]])
|
||||
if models.get("bands"):
|
||||
argv.extend(["--bands-json", json.dumps(models["bands"])])
|
||||
for key, flag in (("max_price_age_hours", "--max-price-age-hours"),
|
||||
("max_score_age_days", "--max-score-age-days")):
|
||||
if key in models:
|
||||
argv.extend([flag, str(models[key])])
|
||||
if role_cfg.get("band"):
|
||||
argv.extend(["--band", role_cfg["band"]])
|
||||
if "max_loss_points" in role_cfg:
|
||||
argv.extend(["--max-loss-points", str(role_cfg["max_loss_points"])])
|
||||
if "tokens" in role_cfg:
|
||||
argv.extend(["--tokens", json.dumps(role_cfg["tokens"])])
|
||||
if (models.get("allow_proxy_score") and role_cfg.get("allow_proxy_score", True)
|
||||
and config["loop"]["tier"] < 3):
|
||||
argv.append("--allow-proxy-score")
|
||||
if models.get("cache_dir"):
|
||||
argv.extend(["--cache-dir", models["cache_dir"]])
|
||||
p = command(argv, cwd)
|
||||
if p.returncode:
|
||||
raise LoopError(f"roteamento {role}: {p.stderr.strip()[:300]}")
|
||||
try:
|
||||
result = json.loads(p.stdout)
|
||||
selected = result.get("selected")
|
||||
if not selected or not selected.get("model_key"):
|
||||
raise LoopError(f"{role}: sem modelo elegível: {result.get('reason', 'sem nota')}")
|
||||
except (ValueError, AttributeError) as exc:
|
||||
raise LoopError("roteador não devolveu JSON válido") from exc
|
||||
return selected
|
||||
|
||||
|
||||
def authorize(config, model, remaining):
|
||||
billing = model.get("billing_mode")
|
||||
if billing in ("free", "subscription", "claude_subscription"):
|
||||
return
|
||||
if billing == "go":
|
||||
quota = model.get("quota_window_remaining")
|
||||
debit = model.get("quota_window_debit")
|
||||
if (not isinstance(quota, dict) or not isinstance(debit, dict) or
|
||||
any(k not in quota or k not in debit or number(quota[k], k) < number(debit[k], k)
|
||||
for k in ("5h", "weekly", "monthly"))):
|
||||
raise SafetyPause("Go: cota não confirmada nas três janelas; sem fallback para saldo Zen")
|
||||
if model.get("use_balance_fallback") is not False:
|
||||
raise SafetyPause("Go: fallback para saldo Zen desconhecido ou cobrado; não autorizado")
|
||||
return
|
||||
if billing not in ("zen", "payg"):
|
||||
raise SafetyPause("modalidade de cobrança desconhecida; despacho não autorizado")
|
||||
label = "Zen" if billing == "zen" else "API paga"
|
||||
if model.get("balance_confirmed") is not True:
|
||||
raise SafetyPause(f"{label}: saldo ou permissão de cobrança da conta não confirmado")
|
||||
cap = number(config["loop"].get("max_paid_usd", 0), "max_paid_usd")
|
||||
estimate = model.get("estimated_native_usd")
|
||||
if cap == 0 or estimate is None or number(estimate, "estimated_native_usd") > cap - remaining:
|
||||
raise SafetyPause(f"{label}: orçamento explícito e estimativa nativa atual são obrigatórios")
|
||||
if not sys.stdin.isatty():
|
||||
raise SafetyPause(f"{label}: autorização interativa exigida imediatamente antes do gasto")
|
||||
question = (f"Autorizar {model['model_key']} via {label}, estimativa USD {estimate}, "
|
||||
f"limite restante USD {cap - remaining}? Digite SIM: ")
|
||||
if input(question) != "SIM":
|
||||
raise SafetyPause(f"gasto {label} não autorizado")
|
||||
|
||||
def mark_call_pending(state, folder, model):
|
||||
if model.get("billing_mode") in ("zen", "payg", "go"):
|
||||
state["unreconciled_paid_call"] = True
|
||||
save(folder, state)
|
||||
|
||||
|
||||
|
||||
def selected_roles(config, cwd):
|
||||
tier = config["loop"]["tier"]
|
||||
roles = ["implementer"]
|
||||
if tier >= 2:
|
||||
roles = ["planner", "implementer", "reviewer"]
|
||||
if tier >= 3:
|
||||
roles.append("security-auditor")
|
||||
return {role: role_model(config, config["loop"]["harness"], role, cwd) for role in roles}
|
||||
|
||||
|
||||
def same_route(previous, current):
|
||||
return all(previous.get(key) == current.get(key) for key in
|
||||
("model_key", "billing_mode", "provider", "variant", "estimated_native_usd", "native_tariff"))
|
||||
|
||||
|
||||
def verified_model(config, state, role):
|
||||
previous = state["route_plan"][role]
|
||||
if previous.get("billing_mode") == "claude_subscription":
|
||||
return previous # CLI authentication is checked immediately before dispatch.
|
||||
try:
|
||||
current = role_model(config, config["loop"]["harness"], role, Path(state["project"]))
|
||||
except LoopError as exc:
|
||||
raise SafetyPause(f"{role}: disponibilidade, preço ou quota não revalidada: {exc}") from exc
|
||||
if not same_route(previous, current):
|
||||
raise SafetyPause(f"{role}: rota ou tarifa mudou; novo plan e aprovação necessários")
|
||||
if current.get("billing_mode") == "go":
|
||||
for key in ("5h", "weekly", "monthly"):
|
||||
current["quota_window_remaining"][key] = str(min(
|
||||
number(previous["quota_window_remaining"][key], key),
|
||||
number(current["quota_window_remaining"][key], key)))
|
||||
state["route_plan"][role] = current
|
||||
return current
|
||||
|
||||
|
||||
def report_run(state):
|
||||
return {k: state.get(k) for k in
|
||||
("run_id", "status", "baseline_score", "best_score", "target", "iteration",
|
||||
"best_commit", "best_worktree", "spent_paid_usd", "elapsed_seconds",
|
||||
"baseline_evidence", "baseline_checks", "baseline_gate", "attempts",
|
||||
"route_plan", "gate_sha256", "evaluator_sha256", "error")}
|
||||
|
||||
|
||||
def plan(config_path):
|
||||
path, project, cfg, script, target = config_at(config_path)
|
||||
preflight_isolation(project)
|
||||
routes = {}
|
||||
roles = (["planner", "implementer", "reviewer", "security-auditor"] if cfg["loop"]["tier"] >= 3
|
||||
else ["planner", "implementer", "reviewer"] if cfg["loop"]["tier"] >= 2 else ["implementer"])
|
||||
preflight = command([sys.executable, str(GATE), "--dir", str(project),
|
||||
"--base", git(project, "rev-parse", "HEAD"),
|
||||
"--tier", str(cfg["loop"]["tier"]), "--list"], project)
|
||||
if preflight.returncode:
|
||||
raise LoopError(f"quality gate indisponível: {preflight.stderr.strip()[:300]}")
|
||||
planned = {fields[1]: " ".join(fields[3:]) for line in preflight.stdout.splitlines()
|
||||
if len(fields := line.split()) >= 4 and fields[0].startswith("T")}
|
||||
gate = {name: planned.get(name, "ausente") for name in cfg["loop"]["evaluator"]["required_checks"]}
|
||||
for role in roles:
|
||||
try:
|
||||
routes[role] = role_model(cfg, cfg["loop"]["harness"], role, project)
|
||||
except LoopError as exc:
|
||||
routes[role] = {"status": "unavailable", "reason": str(exc)}
|
||||
return {"config": str(path), "enabled": cfg["loop"].get("enabled", False), "root": str(project),
|
||||
"evaluator_sha256": digest(script), "gate_sha256": digest(GATE),
|
||||
"baseline": "not_run", "target": str(target),
|
||||
"tier": cfg["loop"]["tier"], "routes": routes, "gate_preflight": gate,
|
||||
"risks": ["modelos e permissões devem ser revalidados antes de cada chamada",
|
||||
"worktree não substitui isolamento de processo e acesso a rede",
|
||||
"preço OpenRouter não equivale à cobrança da ferramenta"],
|
||||
"max_paid_usd": str(number(cfg["loop"].get("max_paid_usd", 0), "max_paid_usd"))}
|
||||
|
||||
|
||||
def new_run(config_path):
|
||||
path, project, cfg, script, target = config_at(config_path)
|
||||
if cfg["loop"].get("enabled") is not True:
|
||||
raise LoopError("loop desativado: habilite [loop].enabled explicitamente")
|
||||
home = state_root(project)
|
||||
with locked(home):
|
||||
routes = selected_roles(cfg, project)
|
||||
for model in routes.values():
|
||||
if (model.get("billing_mode") in ("zen", "payg")
|
||||
and number(cfg["loop"].get("max_paid_usd", 0), "max_paid_usd") == 0):
|
||||
raise LoopError("modelo pago: gasto não autorizado; max_paid_usd=0")
|
||||
base = git(project, "rev-parse", "HEAD")
|
||||
run_id = uuid.uuid4().hex
|
||||
folder = home / run_id
|
||||
folder.mkdir(mode=0o700)
|
||||
work = worktree(project, folder / "worktrees" / "baseline", base)
|
||||
state = {"version": 1, "run_id": run_id, "project": str(project), "config": str(path),
|
||||
"config_sha256": digest(path), "evaluator_sha256": digest(script),
|
||||
"gate_sha256": digest(GATE), "base": base,
|
||||
"best_commit": base, "best_worktree": str(work), "best_score": None,
|
||||
"baseline_score": None,
|
||||
"target": str(target), "iteration": 0, "spent_paid_usd": "0", "stagnant": 0,
|
||||
"started_at": time.time(), "elapsed_seconds": 0, "status": "planned", "attempts": [],
|
||||
"route_plan": routes}
|
||||
save(folder, state)
|
||||
try:
|
||||
deadline = state["started_at"] + cfg["loop"]["max_minutes"] * 60
|
||||
score, evidence, checks = evaluator_result(
|
||||
cfg, script, work, folder / "evidence" / "baseline", deadline)
|
||||
if changed_paths(work, base):
|
||||
raise LoopError("avaliador modificou baseline; medição inicial inválida")
|
||||
state.update(best_score=str(score), baseline_score=str(score), baseline_evidence=evidence,
|
||||
baseline_checks=checks, status="baselined")
|
||||
save(folder, state)
|
||||
if attained(score, target, cfg["loop"]["goal"]["direction"]):
|
||||
state["baseline_gate"] = gate_result(work, base, cfg["loop"]["tier"],
|
||||
cfg["loop"]["evaluator"]["required_checks"], [], deadline)
|
||||
state["status"] = "success"
|
||||
save(folder, state)
|
||||
else:
|
||||
iterate(folder, state, cfg, script)
|
||||
except (LoopError, OSError, ValueError) as exc:
|
||||
state.update(status="error", error=str(exc))
|
||||
save(folder, state)
|
||||
return report_run(state)
|
||||
|
||||
|
||||
def check_resumption(state, cfg, project, script, path):
|
||||
if state["project"] != str(project) or state["config_sha256"] != digest(path):
|
||||
raise LoopError("configuração/checkout mudou desde o início; não retomar automaticamente")
|
||||
if (state["evaluator_sha256"] != digest(script) or state["gate_sha256"] != digest(GATE)
|
||||
or git(project, "rev-parse", "HEAD") != state["base"]):
|
||||
raise LoopError("avaliador, gate ou base Git mudaram; retomar exige nova avaliação")
|
||||
if state["status"] not in ("exhausted", "paused", "running", "baselined"):
|
||||
raise LoopError(f"execução com estado {state['status']} não pode ser retomada")
|
||||
if state.get("unreconciled_paid_call"):
|
||||
raise LoopError("tentativa paga com consumo/quota indeterminados; concilie antes de novo run")
|
||||
routes = selected_roles(cfg, project)
|
||||
if set(routes) != set(state["route_plan"]) or any(
|
||||
not same_route(state["route_plan"][role], fresh) for role, fresh in routes.items()):
|
||||
raise LoopError("catálogo, preço, conta ou modelos mudaram; não retomar sem novo plan")
|
||||
if not Path(state["best_worktree"]).is_dir():
|
||||
raise LoopError("worktree vencedor foi removido; não retomar")
|
||||
|
||||
|
||||
def resume_run(config_path, run_id):
|
||||
path, project, cfg, script, target = config_at(config_path)
|
||||
home = state_root(project)
|
||||
with locked(home):
|
||||
folder, state = load(home, run_id)
|
||||
check_resumption(state, cfg, project, script, path)
|
||||
if (state["status"] == "running" and len(state["attempts"]) < state["iteration"]
|
||||
and any(row.get("billing_mode") in ("zen", "payg", "go") for row in state["route_plan"].values())):
|
||||
raise LoopError("tentativa paga interrompida: consumo/quota indeterminados; concilie antes de novo run")
|
||||
if cfg["loop"].get("enabled") is not True:
|
||||
raise LoopError("loop desativado")
|
||||
if state["status"] == "running" and len(state["attempts"]) < state["iteration"]:
|
||||
state["attempts"].append({"iteration": state["iteration"], "result": "interrupted", "reason": "tentativa interrompida, não reutilizada"})
|
||||
state["status"] = "baselined"
|
||||
save(folder, state)
|
||||
try:
|
||||
iterate(folder, state, cfg, script)
|
||||
except (LoopError, OSError, ValueError) as exc:
|
||||
state.update(status="error", error=str(exc))
|
||||
save(folder, state)
|
||||
return report_run(state)
|
||||
|
||||
|
||||
def iterate(folder, state, cfg, script):
|
||||
from harness_runner import invoke
|
||||
|
||||
minimum = number(cfg["loop"]["min_improvement"], "min_improvement")
|
||||
target = number(state["target"], "target")
|
||||
project = Path(state["project"])
|
||||
path = Path(state["config"])
|
||||
direction = cfg["loop"]["goal"]["direction"]
|
||||
deadline = state["started_at"] + cfg["loop"]["max_minutes"] * 60
|
||||
while state["iteration"] < cfg["loop"]["max_iterations"]:
|
||||
if (folder / "stop.requested").exists():
|
||||
state["status"] = "cancelled"
|
||||
break
|
||||
if time.time() >= deadline:
|
||||
state["status"] = "exhausted"
|
||||
break
|
||||
if state["stagnant"] >= cfg["loop"]["max_stagnant"]:
|
||||
state["status"] = "exhausted"
|
||||
break
|
||||
if (digest(path) != state["config_sha256"] or digest(script) != state["evaluator_sha256"]
|
||||
or digest(GATE) != state["gate_sha256"]):
|
||||
state["status"] = "paused"
|
||||
state["error"] = "configuração, avaliador ou gate alterado durante a execução"
|
||||
break
|
||||
if git(project, "rev-parse", "HEAD") != state["base"] or git(project, "status", "--porcelain", "--untracked-files=all"):
|
||||
state["status"] = "paused"
|
||||
state["error"] = "checkout original alterado durante a execução; preservar mudanças do usuário"
|
||||
break
|
||||
step = state["iteration"] + 1
|
||||
base = state["best_commit"]
|
||||
work = worktree(project, folder / "worktrees" / f"iteration-{step:03}", base)
|
||||
marker_hash = assert_worktree(project, work, base)
|
||||
state.update(status="running", iteration=step)
|
||||
save(folder, state)
|
||||
attempt = {"iteration": step, "worktree": str(work), "from": base}
|
||||
paid = number(state["spent_paid_usd"], "spent_paid_usd")
|
||||
try:
|
||||
tool = cfg["loop"]["harness"]
|
||||
routes = state["route_plan"]
|
||||
timeout = int(cfg["loop"].get("agent_timeout_seconds", 900))
|
||||
prompt = (f"Use dev-router com tier mínimo {cfg['loop']['tier']}. Meta do aplicativo: "
|
||||
f"{cfg['loop']['goal']['name']} {direction} {target} {cfg['loop']['goal']['unit']}. "
|
||||
f"Score atual {state['best_score']}. Faça UMA hipótese pequena por iteração; "
|
||||
"preserve testes e quality gate. Não modifique o avaliador, config, catálogos ou testes protegidos. "
|
||||
"Não publique, instale dependências ou produza efeitos externos sem autorização. "
|
||||
f"Feedback anterior: {state['attempts'][-1]['reason'] if state['attempts'] else 'nenhum'}. ")
|
||||
if "planner" in routes:
|
||||
model = verified_model(cfg, state, "planner")
|
||||
authorize(cfg, model, paid)
|
||||
mark_call_pending(state, folder, model)
|
||||
planned = invoke(tool, "planner", prompt + "Planeje uma única hipótese; só leitura.", str(work),
|
||||
model["model_key"], timeout=bounded_timeout(timeout, deadline),
|
||||
env={"DEV_WORKFLOWS_AUTONOMOUS": "1"})
|
||||
assert_worktree(project, work, base, marker_hash)
|
||||
paid = charged(planned, model, cfg, state, folder)
|
||||
if planned["returncode"]:
|
||||
raise SafetyPause("planejamento falhou; nenhuma outra chamada autorizada")
|
||||
prompt += "Plano (não é autorização): " + planned["output"][:3000] + ". "
|
||||
model = verified_model(cfg, state, "implementer")
|
||||
authorize(cfg, model, paid)
|
||||
mark_call_pending(state, folder, model)
|
||||
done = invoke(tool, "implementer", prompt + "Implemente e relate nível declarado e evidências.", str(work),
|
||||
model["model_key"], timeout=bounded_timeout(timeout, deadline),
|
||||
env={"DEV_WORKFLOWS_AUTONOMOUS": "1"})
|
||||
paid = charged(done, model, cfg, state, folder)
|
||||
if done["returncode"]:
|
||||
raise SafetyPause("agente de implementação falhou; nenhuma outra chamada autorizada")
|
||||
assert_worktree(project, work, base, marker_hash)
|
||||
changes = changed_paths(work, base)
|
||||
if not changes:
|
||||
raise LoopError("agente não modificou código verificável")
|
||||
if path.relative_to(project).as_posix() in changes or any(c.startswith(".dev-workflows/") for c in changes):
|
||||
raise LoopError("tentativa alterou configuração ou estado protegido")
|
||||
if (git(project, "rev-parse", "HEAD") != state["base"]
|
||||
or git(project, "status", "--porcelain", "--untracked-files=all")):
|
||||
raise LoopError("checkout original alterado durante a tentativa; preservar mudanças do usuário")
|
||||
assert_worktree(project, work, base, marker_hash)
|
||||
# This commit is only a candidate in a detached worktree, never a promotion.
|
||||
git(work, "add", "-A", "--", ".", ":(exclude).dev-workflows/**")
|
||||
git(work, "-c", "core.hooksPath=/dev/null", "-c", "commit.gpgSign=false",
|
||||
"-c", "user.name=Dev Workflows", "-c", "user.email=dev-workflows@localhost",
|
||||
"commit", "-m", f"dev-workflows: candidate iteration {step}")
|
||||
candidate_commit = git(work, "rev-parse", "HEAD")
|
||||
assert_worktree(project, work, candidate_commit, marker_hash)
|
||||
if git(project, "rev-parse", "HEAD") != state["base"]:
|
||||
raise LoopError("checkout original alterado durante o snapshot")
|
||||
|
||||
# Gate/review the exact commit, not agent-created ignored files.
|
||||
verified = worktree(project, folder / "verification" / "worktrees" / f"iteration-{step:03}", candidate_commit)
|
||||
verified_marker = assert_worktree(project, verified, candidate_commit)
|
||||
if changed_paths(verified, base) != changes:
|
||||
raise LoopError("snapshot da tentativa diverge dos arquivos alterados")
|
||||
before = fingerprint(verified, changes)
|
||||
gate = gate_result(verified, base, cfg["loop"]["tier"],
|
||||
cfg["loop"]["evaluator"]["required_checks"], changes, deadline)
|
||||
assert_worktree(project, verified, candidate_commit, verified_marker)
|
||||
if changed_paths(verified, base) != changes or before != fingerprint(verified, changes):
|
||||
raise LoopError("código mudou durante o quality gate")
|
||||
attempt["gate"] = gate
|
||||
for role in ("reviewer", "security-auditor"):
|
||||
if role in routes:
|
||||
model = verified_model(cfg, state, role)
|
||||
authorize(cfg, model, paid)
|
||||
mark_call_pending(state, folder, model)
|
||||
review = invoke(tool, role, "Revise exclusivamente o diff da tentativa. Responda JSON: "
|
||||
'{"verdict":"pass|fail","findings":[]}.', str(verified), model["model_key"],
|
||||
timeout=bounded_timeout(timeout, deadline),
|
||||
env={"DEV_WORKFLOWS_AUTONOMOUS": "1"})
|
||||
paid = charged(review, model, cfg, state, folder)
|
||||
if review["returncode"]:
|
||||
raise SafetyPause(f"{role} falhou; nenhuma outra chamada autorizada")
|
||||
if review_verdict(review["output"]) != "pass":
|
||||
raise LoopError(f"{role} rejeitou ou não concluiu a revisão")
|
||||
assert_worktree(project, verified, candidate_commit, verified_marker)
|
||||
if changed_paths(verified, base) != changes or fingerprint(verified, changes) != before:
|
||||
raise LoopError("código mudou depois do gate/revisão")
|
||||
|
||||
# Score a second clean checkout: gate artifacts cannot bias the metric.
|
||||
scored = worktree(project, folder / "scoring" / "worktrees" / f"iteration-{step:03}", candidate_commit)
|
||||
scored_marker = assert_worktree(project, scored, candidate_commit)
|
||||
try:
|
||||
score, evidence, checks = evaluator_result(
|
||||
cfg, script, scored, folder / "evidence" / f"iteration-{step:03}", deadline)
|
||||
except LoopError as exc:
|
||||
raise SafetyPause(f"avaliador inválido: {exc}") from exc
|
||||
assert_worktree(project, scored, candidate_commit, scored_marker)
|
||||
if (changed_paths(scored, candidate_commit) or digest(path) != state["config_sha256"]
|
||||
or git(project, "rev-parse", "HEAD") != state["base"]):
|
||||
raise LoopError("código/configuração/base mudou durante a avaliação")
|
||||
attempt.update(score=str(score), evidence=evidence, evaluator_checks=checks)
|
||||
best = number(state["best_score"], "best_score")
|
||||
if not improved(score, best, minimum, direction):
|
||||
raise LoopError(f"sem melhoria mínima sobre {best}: score {score}")
|
||||
bounded_timeout(1, deadline)
|
||||
state.update(best_commit=candidate_commit, best_worktree=str(scored), best_score=str(score), stagnant=0)
|
||||
attempt.update(result="accepted", reason="melhoria medida em snapshot limpo e gate aprovado",
|
||||
commit=candidate_commit)
|
||||
if attained(score, target, direction):
|
||||
state["status"] = "success"
|
||||
except SafetyPause as exc:
|
||||
attempt.update(result="paused", reason=str(exc))
|
||||
state["status"] = "paused"
|
||||
except RuntimeError as exc:
|
||||
attempt.update(result="paused", reason=f"runtime recusou ou falhou: {exc}")
|
||||
state["status"] = "paused"
|
||||
except (LoopError, OSError, ValueError) as exc:
|
||||
attempt.update(result="rejected", reason=str(exc))
|
||||
state["stagnant"] += 1
|
||||
state["attempts"].append(attempt)
|
||||
state["elapsed_seconds"] = round(time.time() - state["started_at"], 2)
|
||||
save(folder, state)
|
||||
if state["status"] in ("success", "paused"):
|
||||
break
|
||||
else:
|
||||
state["status"] = "exhausted"
|
||||
if state["status"] == "running":
|
||||
state["status"] = "exhausted"
|
||||
save(folder, state)
|
||||
|
||||
|
||||
def usage_cost(result, model):
|
||||
value = (result.get("usage") or {}).get("cost_usd")
|
||||
if model.get("billing_mode") in ("zen", "payg") and value is None:
|
||||
raise SafetyPause("modelo pago: custo real da chamada não foi informado; pausa obrigatória")
|
||||
cost = number(value, "cost_usd") if value is not None else Decimal(0)
|
||||
if cost < 0:
|
||||
raise SafetyPause("custo negativo informado pelo provedor; pausa obrigatória")
|
||||
if model.get("billing_mode") == "go":
|
||||
quota = model["quota_window_remaining"]
|
||||
debit = model["quota_window_debit"]
|
||||
for window in ("5h", "weekly", "monthly"):
|
||||
quota[window] = str(number(quota[window], window) - number(debit[window], window))
|
||||
return cost if model.get("billing_mode") in ("zen", "payg") else Decimal(0)
|
||||
|
||||
|
||||
def charged(result, model, config, state, folder):
|
||||
total = number(state["spent_paid_usd"], "spent_paid_usd") + usage_cost(result, model)
|
||||
state["spent_paid_usd"] = str(total)
|
||||
if model.get("billing_mode") in ("zen", "payg", "go"):
|
||||
state["unreconciled_paid_call"] = False
|
||||
save(folder, state)
|
||||
if (model.get("billing_mode") in ("zen", "payg")
|
||||
and total > number(config["loop"]["max_paid_usd"], "max_paid_usd")):
|
||||
raise SafetyPause(f"modelo pago: custo real USD {total} excedeu limite aprovado; nenhuma outra chamada autorizada")
|
||||
return total
|
||||
|
||||
|
||||
def review_verdict(output):
|
||||
try:
|
||||
report = json.loads(output)
|
||||
except ValueError:
|
||||
return "invalid"
|
||||
return report.get("verdict") if report.get("verdict") in ("pass", "fail") and isinstance(report.get("findings"), list) else "invalid"
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("action", choices=("plan", "run", "status", "resume", "stop"))
|
||||
parser.add_argument("--config", default=".dev-workflows.toml")
|
||||
parser.add_argument("--run", dest="run_id")
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
if args.action == "plan":
|
||||
result = plan(args.config)
|
||||
elif args.action == "run":
|
||||
result = new_run(args.config)
|
||||
else:
|
||||
if not args.run_id:
|
||||
raise LoopError("--run <id> obrigatório")
|
||||
path = Path(args.config).resolve(strict=True)
|
||||
project = Path(git(path.parent, "rev-parse", "--show-toplevel")).resolve()
|
||||
home = state_root(project)
|
||||
if args.action == "resume":
|
||||
result = resume_run(args.config, args.run_id)
|
||||
else:
|
||||
folder, state = load(home, args.run_id)
|
||||
if args.action == "stop":
|
||||
(folder / "stop.requested").touch(exist_ok=True)
|
||||
result = report_run(state)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0 if args.action in ("plan", "status", "stop") or result.get("status") == "success" else 1
|
||||
except (LoopError, FileNotFoundError, tomllib.TOMLDecodeError) as exc:
|
||||
print(json.dumps({"status": "error", "reason": str(exc)}, ensure_ascii=False), file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,496 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execução headless e conservadora para Claude Code, OpenCode e OMP."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import secrets
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
AGENTS = ROOT / "agents"
|
||||
OMP_TOOLS = {
|
||||
"Read": "read", "Edit": "edit", "Write": "write", "Bash": "bash",
|
||||
"Grep": "grep", "Glob": "glob", "WebFetch": "web_search",
|
||||
"WebSearch": "web_search", "NotebookEdit": "notebook",
|
||||
}
|
||||
|
||||
OMP_THINKING_LEVELS = {"off", "minimal", "low", "medium", "high", "xhigh", "max", "auto"}
|
||||
|
||||
|
||||
|
||||
class HarnessError(RuntimeError):
|
||||
"""Harness request cannot be safely or exactly dispatched."""
|
||||
|
||||
|
||||
def _load_catalog_module():
|
||||
path = ROOT / "scripts" / "model_catalog.py"
|
||||
spec = importlib.util.spec_from_file_location("dev_workflows_model_catalog", path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise HarnessError("catálogo local indisponível")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _agent(role: str) -> tuple[dict[str, str], str]:
|
||||
if not re.fullmatch(r"[a-z][a-z0-9-]*", role):
|
||||
raise HarnessError(f"papel inválido: {role!r}")
|
||||
path = AGENTS / f"{role}.md"
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError as exc:
|
||||
raise HarnessError(f"papel desconhecido: {role}") from exc
|
||||
match = re.match(r"\A---\n(.*?)\n---\n(.*)\Z", text, re.S)
|
||||
if not match:
|
||||
raise HarnessError(f"frontmatter inválido para papel {role}")
|
||||
meta = dict(re.findall(r"^(\w+):\s*(.*)$", match.group(1), re.M))
|
||||
if meta.get("name") != role:
|
||||
raise HarnessError(f"identidade inválida para papel {role}")
|
||||
return meta, match.group(2).replace("${CLAUDE_PLUGIN_ROOT}", str(ROOT))
|
||||
|
||||
|
||||
def _model_for_cli(tool: str, model_key: str, meta: dict[str, str]) -> tuple[str | None, str]:
|
||||
"""Return CLI selector and exact catalog key; never strip an unsupported variant."""
|
||||
if not isinstance(model_key, str) or not model_key:
|
||||
raise HarnessError("model_key obrigatório")
|
||||
if tool == "claude":
|
||||
if model_key == "inherit":
|
||||
inherited = meta.get("model", "")
|
||||
if inherited == "inherit":
|
||||
raise HarnessError("Claude `inherit` não identifica um modelo verificável para este papel")
|
||||
selector = inherited
|
||||
catalog_key = inherited
|
||||
else:
|
||||
selector = model_key
|
||||
catalog_key = model_key
|
||||
if selector not in {"haiku", "sonnet", "opus"} and not re.fullmatch(r"claude-[A-Za-z0-9.-]+", selector):
|
||||
raise HarnessError("Claude CLI aceita aliases haiku/sonnet/opus ou IDs Claude; model_key não é exato para esta ferramenta")
|
||||
return (None if model_key == "inherit" else selector), catalog_key
|
||||
if model_key == "inherit":
|
||||
raise HarnessError("inherit não identifica provider/model para esta ferramenta")
|
||||
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.:+/-]+(?:#[A-Za-z0-9_.+-]+)?", model_key):
|
||||
raise HarnessError(f"model_key inválido ou não exato para {tool}: {model_key!r}")
|
||||
return model_key, model_key
|
||||
|
||||
|
||||
def _require_catalog_entry(tool: str, model_key: str, cache_dir: str | None) -> dict[str, Any]:
|
||||
catalog = _load_catalog_module()
|
||||
snapshot = catalog.load_snapshot(tool, cache_dir)
|
||||
if not isinstance(snapshot, dict) or snapshot.get("tool") != tool:
|
||||
raise HarnessError(f"snapshot local de {tool} indisponível; despacho bloqueado")
|
||||
matches = [row for row in snapshot.get("models", [])
|
||||
if isinstance(row, dict) and row.get("tool") == tool and row.get("model_key") == model_key]
|
||||
if len(matches) != 1:
|
||||
raise HarnessError(f"model_key {model_key!r} não é um ID exato e único no catálogo local de {tool}")
|
||||
availability = matches[0].get("availability")
|
||||
if not isinstance(availability, dict) or any(availability.get(field) is not True
|
||||
for field in ("local_listed", "authenticated", "entitled")):
|
||||
raise HarnessError(f"disponibilidade/autorização local não verificada para {model_key!r}; despacho bloqueado")
|
||||
return matches[0]
|
||||
|
||||
def _role_tools(meta: dict[str, str]) -> list[str]:
|
||||
declared = {item.strip() for item in meta.get("tools", "").split(",") if item.strip()}
|
||||
return [name for name in ("Read", "Grep", "Glob", "Edit", "Write") if name in declared]
|
||||
|
||||
|
||||
def _opencode_loop_agent(meta: dict[str, str], role_prompt: str) -> str:
|
||||
tools = set(_role_tools(meta))
|
||||
allowed = {
|
||||
"read": "Read" in tools,
|
||||
"edit": bool(tools & {"Edit", "Write"}),
|
||||
"glob": "Glob" in tools,
|
||||
"grep": "Grep" in tools,
|
||||
"list": False,
|
||||
"bash": False,
|
||||
"task": False,
|
||||
"external_directory": False,
|
||||
"todowrite": False,
|
||||
"webfetch": False,
|
||||
"websearch": False,
|
||||
"lsp": False,
|
||||
"skill": False,
|
||||
"question": False,
|
||||
"doom_loop": False,
|
||||
}
|
||||
lines = ["---", f"description: {json.dumps(meta['description'], ensure_ascii=False)}",
|
||||
"mode: primary", "permission:", ' "*": deny']
|
||||
lines.extend(f" {name}: {'allow' if enabled else 'deny'}" for name, enabled in allowed.items())
|
||||
return "\n".join(lines + ["---", "", role_prompt])
|
||||
|
||||
|
||||
def _memfd(contents: bytes) -> int:
|
||||
if not hasattr(os, "memfd_create"):
|
||||
raise HarnessError("sandbox requer suporte a memfd para config efêmera")
|
||||
fd = os.memfd_create("dev-workflows-harness")
|
||||
os.write(fd, contents)
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
return fd
|
||||
|
||||
|
||||
def _within(path: Path, root: Path) -> bool:
|
||||
return path == root or root in path.parents
|
||||
|
||||
|
||||
def _validate_autonomous_worktree(cwd: str) -> None:
|
||||
worktree = Path(cwd).resolve()
|
||||
if worktree.parent.name != "worktrees" or not (
|
||||
worktree.name == "baseline" or re.fullmatch(r"iteration-[0-9]+", worktree.name)
|
||||
):
|
||||
raise HarnessError("modo autônomo exige worktree isolado baseline/iteration-*")
|
||||
checkout = ROOT.resolve()
|
||||
if _within(worktree, checkout) or _within(checkout, worktree):
|
||||
raise HarnessError("modo autônomo não pode gravar no checkout de origem")
|
||||
marker = worktree / ".git"
|
||||
if marker.is_symlink() or not marker.is_file():
|
||||
raise HarnessError("cwd autônomo não é um git worktree")
|
||||
match = re.fullmatch(r"gitdir:\s*(.+)\s*", marker.read_text(encoding="utf-8"))
|
||||
if not match:
|
||||
raise HarnessError("ponteiro .git inválido no worktree autônomo")
|
||||
gitdir = Path(match.group(1))
|
||||
if not gitdir.is_absolute():
|
||||
gitdir = worktree / gitdir
|
||||
gitdir = gitdir.resolve()
|
||||
if not gitdir.is_dir() or _within(gitdir, worktree):
|
||||
raise HarnessError("gitdir do worktree autônomo ausente ou inseguro")
|
||||
|
||||
|
||||
def _sandbox_path(cwd: str, env: dict[str, str]) -> str:
|
||||
worktree = Path(cwd).resolve()
|
||||
entries: list[str] = []
|
||||
seen: set[Path] = set()
|
||||
for entry in env.get("PATH", "").split(os.pathsep):
|
||||
path = Path(entry or cwd).expanduser()
|
||||
if not path.is_absolute():
|
||||
path = worktree / path
|
||||
try:
|
||||
path = path.resolve()
|
||||
except OSError:
|
||||
continue
|
||||
if not path.is_dir() or _within(path, worktree) or path in seen:
|
||||
continue
|
||||
seen.add(path)
|
||||
entries.append(str(path))
|
||||
return os.pathsep.join(entries)
|
||||
|
||||
|
||||
def _sandbox_command(tool: str, command: list[str], cwd: str, env: dict[str, str],
|
||||
opencode_agent: str | None = None, temp_root: str | None = None
|
||||
) -> tuple[list[str], tuple[int, ...]]:
|
||||
if env.get("DEV_WORKFLOWS_AUTONOMOUS") != "1":
|
||||
return command, ()
|
||||
bwrap = shutil.which("bwrap", path="/usr/bin:/bin")
|
||||
if not bwrap:
|
||||
raise HarnessError("modo autônomo requer bwrap; despacho bloqueado")
|
||||
if not temp_root:
|
||||
raise HarnessError("sandbox sem diretório temporário privado")
|
||||
temp_data = f"{temp_root}/data"
|
||||
temp_config = f"{temp_root}/config"
|
||||
args = [
|
||||
bwrap, "--unshare-all", "--share-net", "--die-with-parent",
|
||||
"--ro-bind", "/", "/", "--proc", "/proc", "--dev", "/dev",
|
||||
"--bind", temp_root, temp_root,
|
||||
"--dir", f"{temp_root}/tmp", "--dir", f"{temp_root}/cache",
|
||||
"--dir", f"{temp_root}/state", "--dir", temp_data,
|
||||
]
|
||||
args.extend(("--setenv", "PATH", _sandbox_path(cwd, env)))
|
||||
pass_fds: list[int] = []
|
||||
if tool == "opencode":
|
||||
for directory in (temp_config, f"{temp_config}/opencode", f"{temp_config}/opencode/agents",
|
||||
f"{temp_data}/opencode"):
|
||||
args.extend(("--dir", directory))
|
||||
args.extend(("--setenv", "XDG_CONFIG_HOME", temp_config,
|
||||
"--setenv", "XDG_DATA_HOME", temp_data,
|
||||
"--setenv", "TMPDIR", f"{temp_root}/tmp"))
|
||||
config_home = Path(env.get("XDG_CONFIG_HOME", str(Path.home() / ".config")))
|
||||
configured = env.get("OPENCODE_CONFIG")
|
||||
if not configured:
|
||||
for filename in ("opencode.json", "opencode.jsonc"):
|
||||
candidate = config_home / "opencode" / filename
|
||||
if candidate.is_file():
|
||||
configured = str(candidate)
|
||||
break
|
||||
if configured:
|
||||
args.extend(("--setenv", "OPENCODE_CONFIG", configured))
|
||||
auth_home = Path(env.get("XDG_DATA_HOME", str(Path.home() / ".local" / "share")))
|
||||
auth_file = auth_home / "opencode" / "auth.json"
|
||||
if auth_file.is_file():
|
||||
auth_target = f"{temp_data}/opencode/auth.json"
|
||||
fd = _memfd(b"")
|
||||
pass_fds.append(fd)
|
||||
args.extend(("--file", str(fd), auth_target, "--ro-bind", str(auth_file), auth_target))
|
||||
if opencode_agent is not None:
|
||||
role_name = command[command.index("--agent") + 1]
|
||||
agent_target = f"{temp_config}/opencode/agents/{role_name}.md"
|
||||
fd = _memfd(opencode_agent.encode("utf-8"))
|
||||
pass_fds.append(fd)
|
||||
args.extend(("--file", str(fd), agent_target))
|
||||
args.extend(("--setenv", "XDG_CACHE_HOME", f"{temp_root}/cache",
|
||||
"--setenv", "XDG_STATE_HOME", f"{temp_root}/state"))
|
||||
else:
|
||||
args.extend(("--setenv", "TMPDIR", f"{temp_root}/tmp",
|
||||
"--setenv", "XDG_CACHE_HOME", f"{temp_root}/cache",
|
||||
"--setenv", "XDG_STATE_HOME", f"{temp_root}/state"))
|
||||
if tool == "omp":
|
||||
args.extend(("--dir", f"{temp_root}/omp-agent",
|
||||
"--setenv", "PI_CODING_AGENT_DIR", f"{temp_root}/omp-agent"))
|
||||
elif tool == "claude":
|
||||
args.extend(("--setenv", "CLAUDE_CODE_SKIP_PROMPT_HISTORY", "1"))
|
||||
marker = str(Path(cwd) / ".git")
|
||||
args.extend(("--bind", cwd, cwd, "--ro-bind", marker, marker, "--chdir", cwd, "--", *command))
|
||||
# The network namespace is shared for provider APIs; it is not an API-only egress filter.
|
||||
return args, tuple(pass_fds)
|
||||
|
||||
|
||||
def _run_process(command: list[str], cwd: str, env: dict[str, str], timeout: float, tool: str,
|
||||
opencode_agent: str | None = None) -> subprocess.CompletedProcess:
|
||||
autonomous = env.get("DEV_WORKFLOWS_AUTONOMOUS") == "1"
|
||||
temp_dir = None
|
||||
pass_fds: tuple[int, ...] = ()
|
||||
try:
|
||||
if autonomous:
|
||||
safe_path = _sandbox_path(cwd, env)
|
||||
if not safe_path:
|
||||
raise HarnessError("PATH seguro vazio; despacho bloqueado")
|
||||
executable = shutil.which(command[0], path=safe_path)
|
||||
if not executable:
|
||||
raise HarnessError(f"CLI {command[0]!r} não encontrado em PATH seguro")
|
||||
if _within(Path(executable).resolve(), Path(cwd).resolve()):
|
||||
raise HarnessError(f"CLI {command[0]!r} está dentro do worktree; despacho bloqueado")
|
||||
command = [executable, *command[1:]]
|
||||
temp_dir = tempfile.TemporaryDirectory(prefix="dev-workflows-harness-", dir="/tmp")
|
||||
wrapped, pass_fds = _sandbox_command(
|
||||
tool, command, cwd, env, opencode_agent, temp_dir.name if temp_dir else None
|
||||
)
|
||||
return subprocess.run(wrapped, cwd=cwd, env=env, capture_output=True, text=True,
|
||||
errors="replace", timeout=timeout, check=False, pass_fds=pass_fds)
|
||||
finally:
|
||||
for fd in pass_fds:
|
||||
os.close(fd)
|
||||
if temp_dir is not None:
|
||||
temp_dir.cleanup()
|
||||
|
||||
|
||||
def _claude_auth_status(cwd: str, env: dict[str, str], timeout: float) -> bool:
|
||||
try:
|
||||
result = _run_process(["claude", "auth", "status", "--json"], cwd, env, timeout, "claude")
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return False
|
||||
if result.returncode != 0:
|
||||
return False
|
||||
try:
|
||||
status = json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return False
|
||||
return isinstance(status, dict) and status.get("loggedIn") is True
|
||||
|
||||
|
||||
def _omp_cli_model(row: dict[str, Any], model_key: str) -> tuple[str, str | None]:
|
||||
variant = row.get("variant")
|
||||
if variant is None:
|
||||
if "#" in model_key:
|
||||
raise HarnessError(f"model_key OMP variante sem metadado variant: {model_key!r}")
|
||||
return model_key, None
|
||||
if variant not in OMP_THINKING_LEVELS:
|
||||
raise HarnessError(f"OMP CLI não oferece seleção exata da variante {variant!r}")
|
||||
base_key, separator, key_variant = model_key.rpartition("#")
|
||||
if not separator or key_variant != variant:
|
||||
raise HarnessError(f"variante OMP não corresponde ao model_key canônico {model_key!r}")
|
||||
provider, model_id = row.get("provider"), row.get("model_id")
|
||||
if not isinstance(provider, str) or not isinstance(model_id, str) or base_key != f"{provider}/{model_id}":
|
||||
raise HarnessError(f"provider/model_id do catálogo não confirma {model_key!r}")
|
||||
return base_key, variant
|
||||
|
||||
|
||||
|
||||
|
||||
def _local_models_command(tool: str, model_key: str, cwd: str, env: dict[str, str],
|
||||
timeout: float, thinking: str | None = None) -> bool:
|
||||
provider, model = model_key.split("/", 1)
|
||||
base_model = model.split("#", 1)[0]
|
||||
autonomous = env.get("DEV_WORKFLOWS_AUTONOMOUS") == "1"
|
||||
if tool == "opencode":
|
||||
command = ["opencode"] + (["--pure"] if autonomous else []) + ["models", provider]
|
||||
else:
|
||||
command = ["omp"] + (["--no-extensions"] if autonomous else []) + ["models", provider, "--json"]
|
||||
try:
|
||||
result = _run_process(command, cwd, env, timeout, tool)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return False
|
||||
if result.returncode != 0:
|
||||
return False
|
||||
if tool == "omp":
|
||||
try:
|
||||
payload = json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return False
|
||||
return _json_catalog_has_model(payload, provider, base_model, thinking)
|
||||
for line in result.stdout.splitlines():
|
||||
candidate = line.strip().strip("`'\" ")
|
||||
if candidate.startswith(provider + "/"):
|
||||
candidate = candidate[len(provider) + 1:]
|
||||
if candidate in {model, base_model}:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _json_catalog_has_model(value: Any, provider: str, model_id: str, thinking: str | None = None) -> bool:
|
||||
if isinstance(value, list):
|
||||
return any(_json_catalog_has_model(item, provider, model_id, thinking) for item in value)
|
||||
if not isinstance(value, dict):
|
||||
return False
|
||||
row_provider = value.get("provider") or value.get("providerID") or value.get("provider_id")
|
||||
row_id = value.get("id") or value.get("modelID") or value.get("model_id")
|
||||
full_id = value.get("selector") or value.get("model_key") or value.get("model")
|
||||
matches = ((row_provider == provider and row_id == model_id)
|
||||
or (isinstance(full_id, str) and full_id == f"{provider}/{model_id}"))
|
||||
if matches and (thinking is None or
|
||||
(isinstance(value.get("thinking"), list) and thinking in value["thinking"])):
|
||||
return True
|
||||
return any(_json_catalog_has_model(item, provider, model_id, thinking) for item in value.values()
|
||||
if isinstance(item, (dict, list)))
|
||||
|
||||
|
||||
def _omp_tools(meta: dict[str, str], autonomous: bool = False) -> list[str]:
|
||||
if autonomous:
|
||||
return [OMP_TOOLS[name] for name in _role_tools(meta)]
|
||||
declared = {item.strip() for item in meta.get("tools", "").split(",") if item.strip()}
|
||||
read_only = not bool(declared & {"Edit", "Write"})
|
||||
tool_order = ("Read", "Edit", "Write", "Bash", "Grep", "Glob", "WebFetch", "WebSearch", "NotebookEdit")
|
||||
return list(dict.fromkeys(
|
||||
OMP_TOOLS[name] for name in tool_order
|
||||
if name in declared and not (read_only and name in {"Edit", "Write", "Bash"})
|
||||
))
|
||||
|
||||
def _omp_skills(meta: dict[str, str]) -> list[str]:
|
||||
match = re.search(r"\[(.*?)\]", meta.get("skills", ""))
|
||||
return re.findall(r"[\w-]+", match.group(1)) if match else []
|
||||
|
||||
|
||||
def _stderr_summary(stderr: str) -> str:
|
||||
if not stderr:
|
||||
return ""
|
||||
lines = len(stderr.splitlines())
|
||||
return f"[stderr omitido por segurança: {lines} linha(s), {len(stderr)} caracteres]"
|
||||
|
||||
|
||||
def _text(value: str | bytes | None) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, bytes):
|
||||
return value.decode("utf-8", errors="replace")
|
||||
return value
|
||||
|
||||
|
||||
def invoke(tool: str, role: str, prompt: str, cwd: str, model_key: str,
|
||||
timeout: float = 600, env: Mapping[str, str] | None = None) -> dict[str, Any]:
|
||||
"""Invoke one role; only bounded autonomous OMP dispatch uses auto-approval.
|
||||
|
||||
The local catalog must attest local listing, authentication and entitlement.
|
||||
OpenCode/OMP local models are rechecked immediately before dispatch. Autonomous
|
||||
calls require bwrap and a tool allowlist; no fallback or config rewrite occurs.
|
||||
"""
|
||||
if tool not in {"claude", "opencode", "omp"}:
|
||||
raise HarnessError(f"ferramenta não suportada: {tool!r}")
|
||||
if not isinstance(prompt, str) or not prompt:
|
||||
raise HarnessError("prompt vazio")
|
||||
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or timeout <= 0:
|
||||
raise HarnessError("timeout deve ser positivo")
|
||||
workdir = str(Path(cwd).resolve())
|
||||
if not Path(workdir).is_dir():
|
||||
raise HarnessError(f"cwd não existe ou não é diretório: {cwd!r}")
|
||||
child_env = os.environ.copy()
|
||||
if env is not None:
|
||||
if any(not isinstance(key, str) or not isinstance(value, str) for key, value in env.items()):
|
||||
raise HarnessError("env deve conter somente pares string/string")
|
||||
child_env.update(env)
|
||||
autonomous = child_env.get("DEV_WORKFLOWS_AUTONOMOUS") == "1"
|
||||
if autonomous:
|
||||
_validate_autonomous_worktree(workdir)
|
||||
meta, role_prompt = _agent(role)
|
||||
cli_model, catalog_key = _model_for_cli(tool, model_key, meta)
|
||||
cache_dir = child_env.get("DEV_WORKFLOWS_MODEL_CACHE")
|
||||
if tool == "claude":
|
||||
if not _claude_auth_status(workdir, child_env, float(timeout)):
|
||||
raise HarnessError("claude auth status não confirmou uma sessão autenticada; despacho bloqueado")
|
||||
row = None if model_key in {"haiku", "sonnet", "opus", "inherit"} else _require_catalog_entry(tool, catalog_key, cache_dir)
|
||||
else:
|
||||
row = _require_catalog_entry(tool, catalog_key, cache_dir)
|
||||
omp_thinking = None
|
||||
local_model_key = catalog_key
|
||||
if tool == "omp":
|
||||
cli_model, omp_thinking = _omp_cli_model(row, catalog_key)
|
||||
local_model_key = cli_model
|
||||
if tool in {"opencode", "omp"} and not _local_models_command(
|
||||
tool, local_model_key, workdir, child_env, float(timeout), omp_thinking):
|
||||
raise HarnessError(f"provider/model indisponível na CLI local de {tool}: {catalog_key!r}; despacho bloqueado")
|
||||
|
||||
opencode_agent = None
|
||||
if tool == "claude":
|
||||
if autonomous:
|
||||
safe_tools = _role_tools(meta)
|
||||
agent_config = json.dumps({
|
||||
role: {"description": meta["description"], "prompt": role_prompt, "tools": safe_tools}
|
||||
}, ensure_ascii=False)
|
||||
command = [
|
||||
"claude", "--bare", "--no-session-persistence", "-p", "--agent", role,
|
||||
"--agents", agent_config, "--tools", ",".join(safe_tools),
|
||||
"--permission-mode", "dontAsk",
|
||||
]
|
||||
if cli_model is not None:
|
||||
command.extend(("--model", cli_model))
|
||||
if safe_tools:
|
||||
command.extend(("--allowedTools", *safe_tools))
|
||||
else:
|
||||
command = ["claude", "-p", "--agent", role, "--permission-mode", "dontAsk"]
|
||||
if cli_model is not None:
|
||||
command.extend(("--model", cli_model))
|
||||
command.append(prompt)
|
||||
elif tool == "opencode":
|
||||
agent_name = f"dev-workflows-loop-{role}-{secrets.token_hex(8)}" if autonomous else role
|
||||
command = ["opencode"] + (["--pure"] if autonomous else []) + ["run", "--agent", agent_name]
|
||||
if cli_model is not None:
|
||||
command.extend(("--model", cli_model))
|
||||
command.append(prompt)
|
||||
if autonomous:
|
||||
opencode_agent = _opencode_loop_agent(meta, role_prompt)
|
||||
else:
|
||||
command = ["omp", "-p", "--cwd", workdir]
|
||||
if autonomous:
|
||||
command.extend(("--no-extensions", "--no-lsp", "--no-session", "--auto-approve"))
|
||||
else:
|
||||
command.extend(("--approval-mode", "always-ask"))
|
||||
if cli_model is not None:
|
||||
command.extend(("--model", cli_model))
|
||||
if omp_thinking is not None:
|
||||
command.extend(("--thinking", omp_thinking))
|
||||
command.extend(("--append-system-prompt", role_prompt))
|
||||
skills = _omp_skills(meta)
|
||||
if skills:
|
||||
command.extend(("--skills", ",".join(skills)))
|
||||
tools = _omp_tools(meta, autonomous=autonomous)
|
||||
if tools:
|
||||
command.extend(("--tools", ",".join(tools)))
|
||||
else:
|
||||
command.append("--no-tools")
|
||||
command.append(prompt)
|
||||
try:
|
||||
result = _run_process(command, workdir, child_env, float(timeout), tool, opencode_agent)
|
||||
returncode = result.returncode
|
||||
stdout, stderr = result.stdout, result.stderr
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
returncode = 124
|
||||
stdout, stderr = _text(exc.stdout), _text(exc.stderr)
|
||||
except OSError:
|
||||
return {"returncode": 127, "output": f"CLI {tool} indisponível", "usage": None}
|
||||
output = _text(stdout).rstrip()
|
||||
summary = _stderr_summary(_text(stderr))
|
||||
if summary:
|
||||
output = f"{output}\n{summary}" if output else summary
|
||||
return {"returncode": returncode, "output": output, "usage": None}
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PreToolUse guard for shell commands: denies gate bypasses, asks before destructive git/db commands.
|
||||
|
||||
Claude Code: reads the hook JSON on stdin and prints a permissionDecision.
|
||||
opencode: `bash_guard.py --check "<command>"` prints the reason and exits 2 when the command must not run.
|
||||
"""
|
||||
import json, re, sys
|
||||
|
||||
DENY = [
|
||||
(r"(^|\s)--no-verify(\s|$)", "--no-verify pula os hooks do git. Corrija o que o hook reclamou em vez de pular."),
|
||||
(r"\bgit\s+push\b(?=.*\s(--force|-f)(\s|$))(?!.*--force-with-lease)", "push --force reescreve histórico remoto. Use --force-with-lease e só com pedido explícito do usuário."),
|
||||
(r"\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\s+(/|~|\$HOME|/\*|\.\.)(\s|/?$)", "rm -rf na raiz, no home ou no diretório pai não roda."),
|
||||
]
|
||||
ASK = [
|
||||
(r"\bgit\s+reset\s+--hard\b", "git reset --hard descarta trabalho não commitado."),
|
||||
(r"\bgit\s+clean\s+-[a-zA-Z]*f", "git clean -f apaga arquivos não rastreados."),
|
||||
(r"\bgit\s+(checkout|restore)\s+(--\s+)?\.(\s|$)", "descarta todas as mudanças locais."),
|
||||
(r"\b(DROP\s+(DATABASE|TABLE|SCHEMA)|TRUNCATE\s+TABLE)\b", "comando destrutivo de banco de dados."),
|
||||
]
|
||||
|
||||
|
||||
def decide(command):
|
||||
for pattern, why in DENY:
|
||||
if re.search(pattern, command, re.IGNORECASE):
|
||||
return "deny", why
|
||||
for pattern, why in ASK:
|
||||
if re.search(pattern, command, re.IGNORECASE):
|
||||
return "ask", why
|
||||
return None, None
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 2 and sys.argv[1] == "--check":
|
||||
decision, why = decide(sys.argv[2])
|
||||
if decision: # opencode has no "ask": both block, the agent must ask the user
|
||||
print(f"[dev-workflows] {why} Peça confirmação ao usuário.")
|
||||
return 2
|
||||
return 0
|
||||
event = json.load(sys.stdin)
|
||||
decision, why = decide(event.get("tool_input", {}).get("command", ""))
|
||||
if decision:
|
||||
print(json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": decision,
|
||||
"permissionDecisionReason": f"[dev-workflows] {why}"}}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PostToolUse: formats the edited file with the formatter the project already uses. Never fails the edit.
|
||||
|
||||
Claude Code: reads tool_input.file_path from the hook JSON on stdin.
|
||||
opencode: `post_edit.py --file <path>`.
|
||||
"""
|
||||
import json, os, shutil, subprocess, sys
|
||||
|
||||
PRETTIER_EXT = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".json", ".css", ".scss", ".html", ".vue", ".svelte", ".md", ".yaml", ".yml"}
|
||||
|
||||
|
||||
def project_root(path):
|
||||
d = os.path.dirname(os.path.abspath(path))
|
||||
while d != os.path.dirname(d):
|
||||
if any(os.path.exists(os.path.join(d, m)) for m in ("package.json", "pyproject.toml", "go.mod", "Cargo.toml", ".git")):
|
||||
return d
|
||||
d = os.path.dirname(d)
|
||||
return None
|
||||
|
||||
|
||||
def uses_ruff(root):
|
||||
if os.path.exists(os.path.join(root, "ruff.toml")) or os.path.exists(os.path.join(root, ".ruff.toml")):
|
||||
return True
|
||||
pyproject = os.path.join(root, "pyproject.toml")
|
||||
return os.path.exists(pyproject) and "[tool.ruff" in open(pyproject).read()
|
||||
|
||||
|
||||
def formatter(path, root):
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
prettier = os.path.join(root, "node_modules", ".bin", "prettier")
|
||||
if ext in PRETTIER_EXT and os.path.exists(prettier):
|
||||
return [prettier, "--write", "--log-level", "warn", path]
|
||||
if ext == ".py" and shutil.which("ruff") and uses_ruff(root):
|
||||
return ["ruff", "format", "--quiet", path]
|
||||
if ext == ".go" and shutil.which("gofmt"):
|
||||
return ["gofmt", "-w", path]
|
||||
if ext == ".rs" and shutil.which("rustfmt"):
|
||||
return ["rustfmt", path]
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 2 and sys.argv[1] == "--file":
|
||||
path = sys.argv[2]
|
||||
else:
|
||||
path = json.load(sys.stdin).get("tool_input", {}).get("file_path", "")
|
||||
root = project_root(path) if path and os.path.isfile(path) else None
|
||||
cmd = formatter(path, root) if root else None
|
||||
if cmd:
|
||||
try:
|
||||
subprocess.run(cmd, cwd=root, capture_output=True, timeout=30)
|
||||
except (subprocess.TimeoutExpired, OSError):
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stop hook: blocks "done" while uncommitted code changes lack a fresh, non-failing quality-gate report.
|
||||
|
||||
Claude Code: reads the hook JSON on stdin, prints {"decision": "block", ...} to keep the agent working.
|
||||
opencode: `stop_gate.py --check <dir>` prints the reason and exits 2 when it would block.
|
||||
Disable with DW_GATE=off.
|
||||
"""
|
||||
import json, os, subprocess, sys
|
||||
|
||||
DOC_EXT = {".md", ".mdx", ".txt", ".rst", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"}
|
||||
PLUGIN_ROOT = os.environ.get("CLAUDE_PLUGIN_ROOT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
GATE = os.path.join(PLUGIN_ROOT, "scripts", "quality-gate")
|
||||
|
||||
|
||||
def pending_code_files(cwd):
|
||||
p = subprocess.run(["git", "status", "--porcelain", "--untracked-files=all"], cwd=cwd, capture_output=True, text=True)
|
||||
if p.returncode != 0:
|
||||
return None
|
||||
files = []
|
||||
for line in p.stdout.splitlines():
|
||||
path = line[3:].split(" -> ")[-1].strip('"')
|
||||
if line[:2].strip() == "D" or path.startswith(".dev-workflows/"):
|
||||
continue
|
||||
if os.path.splitext(path)[1].lower() not in DOC_EXT and os.path.isfile(os.path.join(cwd, path)):
|
||||
files.append(path)
|
||||
return files
|
||||
|
||||
|
||||
def reason_to_block(cwd):
|
||||
if os.environ.get("DW_GATE") == "off":
|
||||
return None
|
||||
top = subprocess.run(["git", "rev-parse", "--show-toplevel"], cwd=cwd, capture_output=True, text=True)
|
||||
if top.returncode != 0:
|
||||
return None
|
||||
root = top.stdout.strip()
|
||||
files = pending_code_files(root)
|
||||
if not files:
|
||||
return None
|
||||
run = f'"{GATE}" --tier <nível do dev-router>'
|
||||
report = os.path.join(root, ".dev-workflows", "gate.json")
|
||||
if not os.path.isfile(report):
|
||||
return f"Há {len(files)} arquivo(s) de código alterados sem gate de qualidade. Rode {run} e trate o resultado antes de encerrar."
|
||||
newest = max(os.path.getmtime(os.path.join(root, f)) for f in files)
|
||||
if os.path.getmtime(report) < newest:
|
||||
return f"O código mudou depois do último gate. Rode de novo {run} antes de encerrar."
|
||||
data = json.load(open(report))
|
||||
if data.get("verdict") == "fail":
|
||||
failed = ", ".join(c["name"] for c in data.get("checks", []) if c.get("status") == "fail")
|
||||
return f"O gate falhou em: {failed}. Corrija (veja .dev-workflows/gate.json) ou explique ao usuário por que não dá para corrigir."
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 2 and sys.argv[1] == "--check":
|
||||
why = reason_to_block(sys.argv[2])
|
||||
if why:
|
||||
print(why)
|
||||
return 2
|
||||
return 0
|
||||
event = json.load(sys.stdin)
|
||||
if event.get("stop_hook_active"):
|
||||
return 0
|
||||
why = reason_to_block(event.get("cwd") or os.getcwd())
|
||||
if why:
|
||||
print(json.dumps({"decision": "block", "reason": why}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,153 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install dev-workflows into Claude Code, OpenCode, OMP or Hermes Agent.
|
||||
|
||||
python3 scripts/install.py claude # plugin via local marketplace (skills, agents, hooks)
|
||||
python3 scripts/install.py opencode # skills + agents + plugin under ~/.config/opencode
|
||||
python3 scripts/install.py omp # native skills + agents under ~/.omp/plugins/dev-workflows
|
||||
python3 scripts/install.py hermes # skills under ~/.hermes/skills (no agents/hooks there)
|
||||
|
||||
OpenCode and Hermes do not substitute ${CLAUDE_PLUGIN_ROOT}, so their copies point at this checkout:
|
||||
keep it where it is, or re-run the install after moving it.
|
||||
"""
|
||||
import argparse, importlib.util, os, re, shutil, subprocess, sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
MARKER = ".dev-workflows-install"
|
||||
TEXT_EXT = {".md", ".txt", ".json", ".toml", ".yaml", ".yml"}
|
||||
|
||||
|
||||
def copy_tree(src, dst, force):
|
||||
if os.path.exists(dst) and not os.path.exists(os.path.join(dst, MARKER)) and not force:
|
||||
sys.exit(f"{dst} já existe e não foi instalado por este script. Use --force para sobrescrever.")
|
||||
shutil.rmtree(dst, ignore_errors=True)
|
||||
shutil.copytree(src, dst)
|
||||
for dirpath, _, names in os.walk(dst):
|
||||
for n in names:
|
||||
if os.path.splitext(n)[1] in TEXT_EXT:
|
||||
p = os.path.join(dirpath, n)
|
||||
text = open(p, encoding="utf-8", errors="ignore").read()
|
||||
if "${CLAUDE_PLUGIN_ROOT}" in text:
|
||||
open(p, "w", encoding="utf-8").write(text.replace("${CLAUDE_PLUGIN_ROOT}", ROOT))
|
||||
open(os.path.join(dst, MARKER), "w").write(ROOT + "\n")
|
||||
|
||||
|
||||
def install_skills(target_dir, force):
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
names = sorted(d for d in os.listdir(os.path.join(ROOT, "skills"))
|
||||
if os.path.isfile(os.path.join(ROOT, "skills", d, "SKILL.md")))
|
||||
for name in names:
|
||||
copy_tree(os.path.join(ROOT, "skills", name), os.path.join(target_dir, name), force)
|
||||
print(f"{len(names)} skills em {target_dir}")
|
||||
|
||||
|
||||
def opencode_agent(path):
|
||||
"""Claude Code agent frontmatter -> OpenCode subagent with explicit permissions."""
|
||||
with open(path, encoding="utf-8") as source:
|
||||
text = source.read()
|
||||
m = re.match(r"---\n(.*?)\n---\n(.*)", text, re.S)
|
||||
if not m:
|
||||
raise ValueError(f"frontmatter inválido em {path}")
|
||||
meta = dict(re.findall(r"^(\w+):\s*(.*)$", m.group(1), re.M))
|
||||
tools = {t.strip() for t in meta.get("tools", "").split(",")}
|
||||
skills = re.findall(r"[\w-]+", meta.get("skills", ""))
|
||||
read_only = not bool(tools & {"Edit", "Write"})
|
||||
allowed = {
|
||||
"read": "Read" in tools,
|
||||
"edit": not read_only and bool(tools & {"Edit", "Write"}),
|
||||
"bash": not read_only and "Bash" in tools,
|
||||
"glob": "Glob" in tools,
|
||||
"grep": "Grep" in tools,
|
||||
"list": False,
|
||||
"task": False,
|
||||
"external_directory": False,
|
||||
"todowrite": False,
|
||||
"webfetch": "WebFetch" in tools,
|
||||
"websearch": "WebSearch" in tools,
|
||||
"lsp": False,
|
||||
"skill": "Skill" in tools,
|
||||
"question": False,
|
||||
"doom_loop": False,
|
||||
}
|
||||
body = m.group(2).replace("${CLAUDE_PLUGIN_ROOT}", ROOT)
|
||||
if skills:
|
||||
body = f"Antes de começar, carregue a(s) skill(s): {', '.join(skills)}.\n\n" + body
|
||||
head = ["# dev-workflows", f"description: {meta['description']}", "mode: subagent", "permission:", ' "*": deny']
|
||||
head += [f" {key}: {'allow' if value else 'deny'}" for key, value in allowed.items()]
|
||||
return "---\n" + "\n".join(head) + "\n---\n\n" + body
|
||||
|
||||
|
||||
def _omp_builder(path=None):
|
||||
path = path or os.path.join(ROOT, "adapters", "omp", "build.py")
|
||||
spec = importlib.util.spec_from_file_location("dev_workflows_omp_build", path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"não foi possível carregar o adapter OMP: {path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module.build_package
|
||||
|
||||
|
||||
def install_omp(base=None, runner=None, build_package=None):
|
||||
"""Build and link the OMP package without replacing a foreign installation."""
|
||||
omp_root = os.path.realpath(base or os.path.expanduser("~/.omp"))
|
||||
dest = os.path.join(omp_root, "plugins", "dev-workflows")
|
||||
if os.path.lexists(dest) and (os.path.islink(dest) or not os.path.isdir(dest)):
|
||||
sys.exit(f"{dest} existe e não é um pacote OMP deste checkout.")
|
||||
build = build_package or _omp_builder()
|
||||
try:
|
||||
build(ROOT, dest)
|
||||
except FileExistsError as exc:
|
||||
sys.exit(str(exc))
|
||||
run = runner or subprocess.run
|
||||
command = ["omp", "plugin", "link", dest]
|
||||
try:
|
||||
result = run(command, check=False)
|
||||
except OSError:
|
||||
sys.exit("OMP não encontrado; pacote gerado, mas `omp plugin link` não foi executado.")
|
||||
if result.returncode != 0:
|
||||
sys.exit(f"`omp plugin link {dest}` falhou com código {result.returncode}.")
|
||||
print(f"plugin OMP instalado em {dest}")
|
||||
|
||||
|
||||
def install_opencode(force):
|
||||
base = os.path.expanduser("~/.config/opencode")
|
||||
install_skills(os.path.join(base, "skills"), force)
|
||||
agents_dir = os.path.join(base, "agents")
|
||||
os.makedirs(agents_dir, exist_ok=True)
|
||||
for f in sorted(os.listdir(os.path.join(ROOT, "agents"))):
|
||||
dst = os.path.join(agents_dir, f)
|
||||
if os.path.exists(dst) and "dev-workflows" not in open(dst, encoding="utf-8").read() and not force:
|
||||
sys.exit(f"{dst} já existe e não é deste pacote. Use --force para sobrescrever.")
|
||||
open(dst, "w", encoding="utf-8").write(opencode_agent(os.path.join(ROOT, "agents", f)))
|
||||
plugins_dir = os.path.join(base, "plugins")
|
||||
os.makedirs(plugins_dir, exist_ok=True)
|
||||
plugin = open(os.path.join(ROOT, "adapters", "opencode", "dev-workflows.ts"), encoding="utf-8").read()
|
||||
open(os.path.join(plugins_dir, "dev-workflows.ts"), "w", encoding="utf-8").write(plugin.replace("__DW_ROOT__", ROOT))
|
||||
print(f"agentes em {agents_dir}, plugin em {plugins_dir}/dev-workflows.ts")
|
||||
|
||||
|
||||
def install_claude():
|
||||
for cmd in (["claude", "plugin", "marketplace", "add", ROOT], ["claude", "plugin", "install", "dev-workflows@dev-workflows"]):
|
||||
print("$ " + " ".join(cmd))
|
||||
if subprocess.run(cmd).returncode != 0:
|
||||
sys.exit("falhou; rode o comando acima manualmente para ver o erro")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("target", choices=["claude", "opencode", "hermes", "omp"])
|
||||
ap.add_argument("--force", action="store_true", help="sobrescreve skills/agentes externos; OMP nunca sobrescreve pacote não pertencente")
|
||||
a = ap.parse_args()
|
||||
if a.target == "claude":
|
||||
install_claude()
|
||||
elif a.target == "opencode":
|
||||
install_opencode(a.force)
|
||||
elif a.target == "omp":
|
||||
install_omp()
|
||||
else:
|
||||
install_skills(os.path.expanduser("~/.hermes/skills"), a.force)
|
||||
print("Hermes: agentes e hooks não se aplicam; rode o gate manualmente.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,399 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seleção conservadora por papel; custos OpenRouter nunca são tarifa de cobrança."""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import json
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from model_catalog import (AA_METRIC, AA_VERSION, CACHE_DIR, decimal_text, decimal_value, discover_local_models,
|
||||
load_snapshot, openrouter_price_for_profile, reference_cost)
|
||||
|
||||
ROLE_BANDS = {
|
||||
"explorer": "leve", "verifier": "leve", "summarizer": "leve",
|
||||
"implementer": "media", "ui-critic": "media",
|
||||
"planner": "alta", "reviewer": "alta", "security-auditor": "alta",
|
||||
}
|
||||
DEFAULT_DELTA = {"leve": Decimal(3), "media": Decimal(3), "alta": Decimal(2)}
|
||||
EXPECTED_HARNESSES = {"opencode": "OpenCode", "omp": "OMP", "claude": "Claude Code"}
|
||||
WINDOWS = ("5h", "weekly", "monthly")
|
||||
FIXED_BANDS = {
|
||||
"leve": (Decimal(10), Decimal(20)),
|
||||
"media": (Decimal(20), Decimal(50)),
|
||||
"alta": (Decimal(50), Decimal(100)),
|
||||
}
|
||||
|
||||
|
||||
def validate_bands(bands: dict[str, Any] | None) -> dict[str, tuple[Decimal, Decimal]]:
|
||||
"""Aceita a configuração declarativa apenas se preservar as faixas normativas."""
|
||||
if bands is None:
|
||||
return FIXED_BANDS.copy()
|
||||
if not isinstance(bands, dict) or set(bands) != set(FIXED_BANDS):
|
||||
raise ValueError("bands deve conter exatamente leve/media/alta")
|
||||
normalized = {}
|
||||
for name, limits in bands.items():
|
||||
if not isinstance(limits, (list, tuple)) or len(limits) != 2:
|
||||
raise ValueError(f"faixa {name} deve declarar dois limites")
|
||||
low, high = (_decimal(value) for value in limits)
|
||||
if low is None or high is None:
|
||||
raise ValueError(f"limites da faixa {name} devem ser decimais")
|
||||
normalized[name] = (low, high)
|
||||
if normalized != FIXED_BANDS:
|
||||
raise ValueError("bands customizadas são recusadas; use 10..20, 20..50 e 50..100")
|
||||
return normalized
|
||||
|
||||
|
||||
def _score_band(score: Decimal | None, bands: dict[str, tuple[Decimal, Decimal]]) -> str | None:
|
||||
if score is None:
|
||||
return None
|
||||
for name in ("leve", "media"):
|
||||
low, high = bands[name]
|
||||
if low <= score < high:
|
||||
return name
|
||||
low, high = bands["alta"]
|
||||
return "alta" if low <= score <= high else None
|
||||
|
||||
|
||||
def _time(value: Any) -> dt.datetime | None:
|
||||
if not isinstance(value, str) or not value:
|
||||
return None
|
||||
try:
|
||||
result = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return None
|
||||
if result.tzinfo is None:
|
||||
return None
|
||||
return result.astimezone(dt.timezone.utc)
|
||||
|
||||
|
||||
def _decimal(value: Any) -> Decimal | None:
|
||||
try:
|
||||
return decimal_value(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _has_token_profile(tokens: dict[str, Any]) -> bool:
|
||||
return any((_decimal(tokens.get(key)) or Decimal(0)) > 0 for key in ("input", "output", "cache_read", "cache_write"))
|
||||
|
||||
|
||||
def _staleness(row: dict[str, Any], now: dt.datetime, max_price_age_hours: Decimal, max_score_age_days: Decimal,
|
||||
tokens: dict[str, Any]) -> list[str]:
|
||||
reasons: list[str] = []
|
||||
mode = row.get("billing_mode")
|
||||
paid = mode in ("zen_balance", "go_quota", "omp_payg", "claude_subscription")
|
||||
price_record = row.get("openrouter") or {"price_per_token_usd": row.get("or_price_per_token") or {}}
|
||||
price = openrouter_price_for_profile(price_record, tokens)
|
||||
native = row.get("native_tariff") or {}
|
||||
has_reference_price = any(price.get("price_per_token_usd", {}).get(k) is not None
|
||||
for k in ("input", "output", "cache_read", "cache_write"))
|
||||
if paid:
|
||||
if has_reference_price:
|
||||
price_time = _time(price.get("captured_at"))
|
||||
if price_time is None or price_time > now or (now - price_time).total_seconds() > float(max_price_age_hours * Decimal(3600)):
|
||||
reasons.append("preco_referencial_obsoleto_ou_ausente")
|
||||
score = row.get("score") or {}
|
||||
score_time = _time(score.get("observed_at"))
|
||||
if score_time is None or score_time > now or (now - score_time).total_seconds() > float(max_score_age_days * Decimal(86400)):
|
||||
reasons.append("benchmark_obsoleto_ou_ausente")
|
||||
if mode in ("zen_balance", "omp_payg"):
|
||||
native_time = _time(native.get("captured_at"))
|
||||
if native.get("account_confirmed") is not True or not str(native.get("source_url", "")).startswith("https://") or native_time is None or native_time > now or now - native_time > dt.timedelta(hours=float(max_price_age_hours)):
|
||||
reasons.append("tarifa_nativa_obsoleta_ou_nao_confirmada")
|
||||
if mode == "go_quota":
|
||||
quota_time = _time(row.get("quota_verified_at"))
|
||||
if quota_time is None or quota_time > now or now - quota_time > dt.timedelta(seconds=60) or not str(row.get("quota_source_url", "")).startswith("https://"):
|
||||
reasons.append("quota_go_obsoleta_ou_nao_confirmada")
|
||||
availability = row.get("availability") or {}
|
||||
local_time = _time(availability.get("local_verified_at") or availability.get("verified_at"))
|
||||
if local_time is None or local_time > now or now - local_time > dt.timedelta(hours=24):
|
||||
reasons.append("disponibilidade_local_precisa_revalidacao")
|
||||
account_time = _time(availability.get("account_verified_at"))
|
||||
if availability.get("authenticated") is True and availability.get("entitled") is True:
|
||||
if account_time is None or account_time > now or now - account_time > dt.timedelta(hours=24):
|
||||
reasons.append("entitlement_precisa_revalidacao")
|
||||
if mode in ("zen_balance", "omp_payg"):
|
||||
balance_time = _time(availability.get("balance_verified_at"))
|
||||
if availability.get("balance_confirmed") is not True or balance_time is None or balance_time > now or now - balance_time > dt.timedelta(seconds=60):
|
||||
reasons.append("saldo_nao_confirmado_ou_obsoleto")
|
||||
return reasons
|
||||
|
||||
|
||||
def _profile_cost(row: dict[str, Any], tokens: dict[str, Any]) -> Decimal | None:
|
||||
record = row.get("openrouter") or {"price_per_token_usd": row.get("or_price_per_token") or {}}
|
||||
profile = openrouter_price_for_profile(record, tokens)
|
||||
return reference_cost(profile.get("price_per_token_usd") or {}, tokens)
|
||||
|
||||
|
||||
def _native_cost_and_headroom(row: dict[str, Any], tokens: dict[str, Any]) -> tuple[Decimal | None, Decimal | None, str | None]:
|
||||
mode = row.get("billing_mode")
|
||||
native = row.get("native_tariff")
|
||||
if mode in ("zen_balance", "omp_payg"):
|
||||
if not _has_token_profile(tokens):
|
||||
return None, None, "perfil_tokens_native_nao_configurado"
|
||||
if not isinstance(native, dict) or not isinstance(native.get("price_per_token_usd"), dict) or native.get("account_confirmed") is not True:
|
||||
return None, None, "tarifa_native_nao_confirmada"
|
||||
amount = reference_cost(native["price_per_token_usd"], tokens)
|
||||
if amount is None:
|
||||
return None, None, "tarifa_native_incompleta_para_perfil"
|
||||
balance = _decimal(native.get("available_balance_usd"))
|
||||
availability = row.get("availability") or {}
|
||||
if availability.get("balance_confirmed") is not True:
|
||||
return None, None, "saldo_nao_confirmado"
|
||||
if balance is None or balance < 0:
|
||||
return None, None, "saldo_desconhecido"
|
||||
if balance < amount:
|
||||
return None, balance, "saldo_insuficiente"
|
||||
return amount, balance - amount, None
|
||||
if mode == "go_quota":
|
||||
if not _has_token_profile(tokens):
|
||||
return None, None, "perfil_tokens_go_nao_configurado"
|
||||
remaining = row.get("quota_window_remaining")
|
||||
debit = row.get("quota_window_debit")
|
||||
if row.get("use_balance_fallback"):
|
||||
return None, None, "fallback_go_para_saldo_zen_nao_autorizado"
|
||||
if not isinstance(remaining, dict) or debit is None:
|
||||
return None, None, "quota_go_nao_confirmada"
|
||||
debit_map = debit if isinstance(debit, dict) else {window: debit for window in WINDOWS}
|
||||
headrooms: list[Decimal] = []
|
||||
for window in WINDOWS:
|
||||
available = _decimal(remaining.get(window))
|
||||
used = _decimal(debit_map.get(window))
|
||||
if available is None or used is None or available < 0 or used < 0:
|
||||
return None, None, f"janela_go_{window}_desconhecida"
|
||||
if used > available:
|
||||
return None, None, f"quota_go_{window}_insuficiente"
|
||||
headrooms.append(available - used)
|
||||
representative = _decimal(debit_map.get("monthly"))
|
||||
if representative is None:
|
||||
return None, min(headrooms), "debito_go_nao_confirmado"
|
||||
return representative, min(headrooms), None
|
||||
if mode == "claude_subscription":
|
||||
amount = _decimal(row.get("subscription_consumption_usd"))
|
||||
if amount is None:
|
||||
return None, None, "consumo_assinatura_nao_mensuravel"
|
||||
return amount, None, None
|
||||
return None, None, "modo_de_cobranca_desconhecido"
|
||||
|
||||
|
||||
def route(candidates: Iterable[dict[str, Any]], role: str, *, allowed_providers: Iterable[str] = (),
|
||||
tokens: dict[str, Any] | None = None, budget_usd: Any = None, allow_proxy: bool = False,
|
||||
delta_points: Any = None, max_price_age_hours: Any = 24, max_score_age_days: Any = 30,
|
||||
now: dt.datetime | None = None, band_override: str | None = None,
|
||||
bands: dict[str, Any] | None = None,
|
||||
benchmark_metric: str = "aa_coding_agent_index_v1.5") -> dict[str, Any]:
|
||||
"""Returns the stable API: selected, ranking, reason. Never dispatches a model."""
|
||||
default_band = ROLE_BANDS.get(role)
|
||||
if default_band is None:
|
||||
return {"selected": None, "ranking": [], "reason": f"papel_desconhecido:{role}"}
|
||||
normalized_bands = validate_bands(bands)
|
||||
band = band_override or default_band
|
||||
if band not in normalized_bands:
|
||||
raise ValueError("band deve ser uma faixa fixa: leve, media ou alta")
|
||||
if benchmark_metric != "aa_coding_agent_index_v1.5":
|
||||
raise ValueError("benchmark_metric incompatível; somente aa_coding_agent_index_v1.5 é aceito")
|
||||
if now is None:
|
||||
now = dt.datetime.now(dt.timezone.utc)
|
||||
if now.tzinfo is None:
|
||||
raise ValueError("now precisa de timezone")
|
||||
now = now.astimezone(dt.timezone.utc)
|
||||
tokens = tokens or {}
|
||||
providers = set(allowed_providers)
|
||||
if not providers:
|
||||
return {"selected": None, "ranking": [], "reason": "provider_nao_autorizado; informe --providers explicitamente"}
|
||||
if budget_usd is None:
|
||||
budget = None
|
||||
else:
|
||||
budget = _decimal(budget_usd)
|
||||
if budget is None or budget < 0:
|
||||
raise ValueError("budget_usd precisa ser decimal não negativo")
|
||||
max_price_age = _decimal(max_price_age_hours)
|
||||
max_score_age = _decimal(max_score_age_days)
|
||||
if max_price_age is None or max_score_age is None:
|
||||
raise ValueError("TTL precisa ser decimal não negativo")
|
||||
delta = _decimal(delta_points) if delta_points is not None else DEFAULT_DELTA[band]
|
||||
if delta is None or delta < 0 or max_price_age < 0 or max_score_age < 0:
|
||||
raise ValueError("delta e TTL devem ser decimais não negativos")
|
||||
|
||||
ranking = []
|
||||
eligible = []
|
||||
reasons_seen: list[str] = []
|
||||
for row in candidates:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
provider = row.get("provider")
|
||||
score = row.get("score") or {}
|
||||
parsed_score = _decimal(score.get("score"))
|
||||
row_band = _score_band(parsed_score, normalized_bands)
|
||||
reference = _profile_cost(row, tokens)
|
||||
native_cost, headroom, native_error = _native_cost_and_headroom(row, tokens)
|
||||
excluded: list[str] = []
|
||||
availability = row.get("availability") or {}
|
||||
if provider not in providers:
|
||||
excluded.append("provider_nao_autorizado")
|
||||
if availability.get("local_listed") is not True:
|
||||
excluded.append("nao_listado_localmente")
|
||||
if availability.get("authenticated") is not True:
|
||||
excluded.append("autenticacao_nao_confirmada")
|
||||
if availability.get("entitled") is not True:
|
||||
excluded.append("entitlement_nao_confirmado")
|
||||
required_provenance = all(score.get(field) not in (None, "", {}) for field in ("agent", "harness", "settings", "source_url", "observed_at"))
|
||||
if not required_provenance or not str(score.get("source_url", "")).startswith("https://artificialanalysis.ai/"):
|
||||
excluded.append("proveniencia_do_benchmark_incompleta")
|
||||
if score.get("variant") != row.get("variant"):
|
||||
excluded.append("mapeamento_de_variante_incompativel")
|
||||
expected_harness = EXPECTED_HARNESSES.get(row.get("tool"))
|
||||
if score.get("match_grade") == "exact" and score.get("harness") != expected_harness:
|
||||
excluded.append("harness_incompativel")
|
||||
if score.get("metric_id") != AA_METRIC or str(score.get("version")) != AA_VERSION:
|
||||
excluded.append("indice_incompativel_ou_ausente")
|
||||
if parsed_score is None:
|
||||
excluded.append("no_scored_candidate")
|
||||
if score.get("match_grade") not in (("exact", "proxy") if allow_proxy else ("exact",)):
|
||||
excluded.append("match_proxy_nao_autorizado" if score.get("match_grade") == "proxy" else "mapeamento_de_variante_incompativel")
|
||||
if row_band != band:
|
||||
excluded.append("fora_da_faixa" if row_band else "sem_faixa_de_nota")
|
||||
excluded.extend(_staleness(row, now, max_price_age, max_score_age, tokens))
|
||||
if native_error:
|
||||
excluded.append(native_error)
|
||||
if row.get("billing_mode") in ("zen_balance", "omp_payg") and budget is None:
|
||||
excluded.append("orcamento_zen_nao_configurado" if row.get("billing_mode") == "zen_balance" else "orcamento_payg_omp_nao_configurado")
|
||||
if native_cost is not None and budget is not None and native_cost > budget:
|
||||
excluded.append("orcamento_excedido")
|
||||
if row.get("billing_mode") == "claude_subscription" and native_cost is None:
|
||||
excluded.append("consumo_assinatura_nao_mensuravel")
|
||||
if row.get("billing_mode") == "go_quota" and headroom is None and native_error is None:
|
||||
excluded.append("cota_go_nao_confirmada")
|
||||
key = row.get("model_key") or f"{provider}/{row.get('model_id', '')}"
|
||||
entry = {
|
||||
"model_key": key, "provider": provider, "billing_mode": row.get("billing_mode"),
|
||||
"score": decimal_text(parsed_score), "band": row_band, "match_grade": score.get("match_grade"),
|
||||
"eligible": not excluded, "exclusion_reasons": excluded,
|
||||
"reference_cost_usd": decimal_text(reference), "native_cost_or_quota_debit_usd": decimal_text(native_cost),
|
||||
"quota_headroom_usd": decimal_text(headroom),
|
||||
}
|
||||
ranking.append(entry)
|
||||
if not excluded:
|
||||
eligible.append((row, entry, parsed_score, native_cost, headroom))
|
||||
else:
|
||||
reasons_seen.extend(excluded)
|
||||
ranking.sort(key=lambda entry: (
|
||||
not entry["eligible"],
|
||||
_decimal(entry.get("native_cost_or_quota_debit_usd")) if _decimal(entry.get("native_cost_or_quota_debit_usd")) is not None else Decimal("Infinity"),
|
||||
-(_decimal(entry.get("score")) or Decimal(0)),
|
||||
-(_decimal(entry.get("quota_headroom_usd")) or Decimal(0)),
|
||||
entry["model_key"],
|
||||
))
|
||||
if not ranking:
|
||||
return {"selected": None, "ranking": [], "reason": "no_scored_candidate"}
|
||||
|
||||
if not eligible:
|
||||
if "no_scored_candidate" in reasons_seen or "indice_incompativel_ou_ausente" in reasons_seen:
|
||||
reason = "no_scored_candidate_or_compatible_index"
|
||||
elif "fora_da_faixa" in reasons_seen or "sem_faixa_de_nota" in reasons_seen:
|
||||
reason = f"faixa_vazia:{band}"
|
||||
elif "quota_go_5h_insuficiente" in reasons_seen or "quota_go_weekly_insuficiente" in reasons_seen or "quota_go_monthly_insuficiente" in reasons_seen:
|
||||
reason = "quota_go_insuficiente; execução pausada antes da chamada"
|
||||
elif "fallback_go_para_saldo_zen_nao_autorizado" in reasons_seen:
|
||||
reason = "fallback_go_para_saldo_zen_nao_autorizado"
|
||||
else:
|
||||
reason = "nenhum_candidato_elegivel:" + ",".join(sorted(set(reasons_seen)))
|
||||
return {"selected": None, "ranking": ranking, "reason": reason}
|
||||
|
||||
top_score = max(item[2] for item in eligible)
|
||||
plateau = [item for item in eligible if item[2] >= top_score - delta]
|
||||
# custo real da ferramenta; desempate: score, folga de quota, ID lexical.
|
||||
plateau.sort(key=lambda item: (item[3], -item[2], -(item[4] if item[4] is not None else Decimal(0)), item[1]["model_key"]))
|
||||
chosen = plateau[0]
|
||||
row, entry, score_value, native_cost, headroom = chosen
|
||||
selected = {
|
||||
"model_key": entry["model_key"],
|
||||
"billing_mode": {"zen_balance": "zen", "go_quota": "go", "omp_payg": "payg", "claude_subscription": "subscription"}.get(row.get("billing_mode"), row.get("billing_mode")),
|
||||
"billing_mode_detail": row.get("billing_mode"), "score": entry["score"],
|
||||
"band": band, "match_grade": entry["match_grade"],
|
||||
"reference_cost_usd": entry["reference_cost_usd"],
|
||||
"native_cost_or_quota_debit_usd": decimal_text(native_cost),
|
||||
"estimated_native_usd": decimal_text(native_cost) if row.get("billing_mode") in ("zen_balance", "omp_payg") else None,
|
||||
"native_tariff": row.get("native_tariff"),
|
||||
"available_balance_usd": (row.get("native_tariff") or {}).get("available_balance_usd"),
|
||||
"balance_confirmed": (row.get("availability") or {}).get("balance_confirmed") is True,
|
||||
"balance_verified_at": (row.get("availability") or {}).get("balance_verified_at"),
|
||||
"quota_window_remaining": row.get("quota_window_remaining"),
|
||||
"quota_window_debit": row.get("quota_window_debit"),
|
||||
"use_balance_fallback": row.get("use_balance_fallback") if row.get("billing_mode") == "go_quota" else False,
|
||||
"quota_headroom_usd": decimal_text(headroom),
|
||||
"score_top": decimal_text(top_score), "plateau_delta_points": decimal_text(delta),
|
||||
}
|
||||
if row.get("tool") == "omp":
|
||||
variant = row.get("variant")
|
||||
selected["dispatch_model_key"] = f"{row['provider']}/{row['model_id']}" + (f":{variant}" if variant else "")
|
||||
reason = f"menor consumo relevante dentro do platô {band} (score >= {decimal_text(top_score - delta)}); custo OpenRouter é apenas referência"
|
||||
if len(plateau) > 1:
|
||||
reason += "; desempate determinístico por consumo, score, folga de cota e model_key"
|
||||
return {"selected": selected, "ranking": ranking, "reason": reason}
|
||||
|
||||
|
||||
def route_from_catalog(tool: str, role: str, allowed_providers: Iterable[str], cache_dir: str | Path | None = None,
|
||||
*, explain: bool = False, tokens: dict[str, Any] | None = None, budget_usd: Any = None,
|
||||
allow_proxy: bool = False, delta_points: Any = None, max_price_age_hours: Any = 24,
|
||||
max_score_age_days: Any = 30, band_override: str | None = None,
|
||||
bands: dict[str, Any] | None = None,
|
||||
benchmark_metric: str = "aa_coding_agent_index_v1.5",
|
||||
now: dt.datetime | None = None,
|
||||
local_discoverer=discover_local_models, omp_discoverer=None) -> dict[str, Any]:
|
||||
from model_catalog import discover_omp_models
|
||||
clock = now or dt.datetime.now(dt.timezone.utc)
|
||||
snapshot = load_snapshot(tool, cache_dir)
|
||||
if snapshot is None:
|
||||
return {"selected": None, "ranking": [], "reason": "snapshot_de_catalogo_ausente; execute models sync"}
|
||||
if tool not in ("opencode", "omp"):
|
||||
return {"selected": None, "ranking": [], "reason": f"catalogo_local_nao_disponivel_para_{tool}"}
|
||||
providers = tuple(allowed_providers)
|
||||
if not providers:
|
||||
return route(snapshot.get("models", []), role, allowed_providers=providers, tokens=tokens, budget_usd=budget_usd,
|
||||
allow_proxy=allow_proxy, delta_points=delta_points, max_price_age_hours=max_price_age_hours,
|
||||
max_score_age_days=max_score_age_days, band_override=band_override, bands=bands,
|
||||
benchmark_metric=benchmark_metric, now=clock)
|
||||
discoverer = local_discoverer if tool == "opencode" else (omp_discoverer or discover_omp_models)
|
||||
discoveries = {provider: discoverer(provider) for provider in sorted(set(providers))}
|
||||
verified_at = clock.astimezone(dt.timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
candidates = []
|
||||
for original in snapshot.get("models", []):
|
||||
row = dict(original)
|
||||
availability = dict(row.get("availability") or {})
|
||||
provider = row.get("provider")
|
||||
result = discoveries.get(provider)
|
||||
if result is not None and result.get("ok"):
|
||||
local_ids = set(result.get("models", []))
|
||||
local_model_key = row["model_key"] if tool == "omp" else row["model_id"] + (f"#{row['variant']}" if row.get("variant") else "")
|
||||
availability["local_listed"] = local_model_key in local_ids
|
||||
availability["local_verified_at"] = result.get("verified_at", verified_at)
|
||||
elif provider in discoveries:
|
||||
availability["local_listed"] = None
|
||||
availability["local_verified_at"] = None
|
||||
row["availability"] = availability
|
||||
candidates.append(row)
|
||||
return route(candidates, role, allowed_providers=providers, tokens=tokens, budget_usd=budget_usd,
|
||||
allow_proxy=allow_proxy, delta_points=delta_points, max_price_age_hours=max_price_age_hours,
|
||||
max_score_age_days=max_score_age_days, band_override=band_override, bands=bands,
|
||||
benchmark_metric=benchmark_metric, now=clock)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--tool", choices=("opencode", "omp", "claude"), default="opencode")
|
||||
parser.add_argument("--role", required=True)
|
||||
parser.add_argument("--providers", default="")
|
||||
parser.add_argument("--format", choices=("json",), default="json")
|
||||
parser.add_argument("--cache-dir")
|
||||
args = parser.parse_args(argv)
|
||||
result = route_from_catalog(args.tool, args.role, [p for p in args.providers.split(",") if p], args.cache_dir, explain=True)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 0 if result["selected"] is not None else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,304 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic quality gate: runs only the checks of the requested tier, scoped to the diff.
|
||||
|
||||
Usage: quality-gate [--tier 0-3] [--base REF] [--dir PATH] [--list]
|
||||
Writes .dev-workflows/gate.json in the project root. Exit 1 when any check fails.
|
||||
A tool that is not installed is reported as "unverified", never as passed.
|
||||
"""
|
||||
import argparse, json, os, re, shutil, subprocess, sys, tempfile, time, tomllib
|
||||
|
||||
CODE_EXT = {".py", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".vue", ".svelte", ".go", ".rs",
|
||||
".java", ".kt", ".rb", ".php", ".cs", ".sql", ".sh", ".css", ".scss", ".html"}
|
||||
JS_EXT = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".vue", ".svelte"}
|
||||
|
||||
|
||||
def sh(cmd, cwd, timeout=600):
|
||||
t = time.time()
|
||||
try:
|
||||
p = subprocess.run(cmd, cwd=cwd, shell=isinstance(cmd, str), capture_output=True, text=True, timeout=timeout)
|
||||
out, code = (p.stdout + p.stderr), p.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
out, code = f"timeout after {timeout}s", 124
|
||||
return code, out, round(time.time() - t, 1)
|
||||
|
||||
|
||||
def git(args, cwd):
|
||||
code, out, _ = sh(["git", *args], cwd, 60)
|
||||
return out.strip() if code == 0 else None
|
||||
|
||||
|
||||
def find_base(root, explicit):
|
||||
if explicit:
|
||||
return explicit
|
||||
for ref in ("origin/HEAD", "origin/main", "origin/master", "main", "master"):
|
||||
mb = git(["merge-base", "HEAD", ref], root)
|
||||
if mb:
|
||||
return mb
|
||||
return "HEAD" if git(["rev-parse", "HEAD"], root) else None
|
||||
|
||||
|
||||
def changed_files(root, base):
|
||||
if base is None:
|
||||
return []
|
||||
names = (git(["diff", "--name-only", "--diff-filter=d", base], root) or "").splitlines()
|
||||
names += (git(["ls-files", "--others", "--exclude-standard"], root) or "").splitlines()
|
||||
return sorted({n for n in names if n and not n.startswith(".dev-workflows/") and os.path.isfile(os.path.join(root, n))})
|
||||
|
||||
|
||||
def diff_stats(root, base, files):
|
||||
added = removed = 0
|
||||
for line in (git(["diff", "--numstat", base], root) or "").splitlines() if base else []:
|
||||
a, r, _ = (line.split("\t") + ["", ""])[:3]
|
||||
added += int(a) if a.isdigit() else 0
|
||||
removed += int(r) if r.isdigit() else 0
|
||||
for f in (git(["ls-files", "--others", "--exclude-standard"], root) or "").splitlines():
|
||||
if f in files:
|
||||
try:
|
||||
added += sum(1 for _ in open(os.path.join(root, f), errors="ignore"))
|
||||
except OSError:
|
||||
pass
|
||||
return {"files": len(files), "added": added, "removed": removed, "net": added - removed}
|
||||
|
||||
|
||||
def deps_at(root, rev):
|
||||
"""Dependency names declared in package.json / requirements*.txt / pyproject.toml at a revision (None = working tree)."""
|
||||
def read(path):
|
||||
if rev is None:
|
||||
p = os.path.join(root, path)
|
||||
return open(p).read() if os.path.isfile(p) else None
|
||||
return git(["show", f"{rev}:{path}"], root)
|
||||
|
||||
names = set()
|
||||
pkg = read("package.json")
|
||||
if pkg:
|
||||
try:
|
||||
d = json.loads(pkg)
|
||||
for k in ("dependencies", "devDependencies", "optionalDependencies"):
|
||||
names |= {f"npm:{n}" for n in d.get(k, {})}
|
||||
except ValueError:
|
||||
pass
|
||||
for req in [f for f in os.listdir(root) if re.fullmatch(r"requirements.*\.txt", f)]:
|
||||
for line in (read(req) or "").splitlines():
|
||||
m = re.match(r"\s*([A-Za-z0-9_.\-]+)", line)
|
||||
if m and not line.strip().startswith(("#", "-")):
|
||||
names.add(f"py:{m.group(1).lower()}")
|
||||
py = read("pyproject.toml")
|
||||
if py:
|
||||
try:
|
||||
proj = tomllib.loads(py).get("project", {})
|
||||
for spec in proj.get("dependencies", []) + sum(proj.get("optional-dependencies", {}).values(), []):
|
||||
m = re.match(r"\s*([A-Za-z0-9_.\-]+)", spec)
|
||||
if m:
|
||||
names.add(f"py:{m.group(1).lower()}")
|
||||
except tomllib.TOMLDecodeError:
|
||||
pass
|
||||
return names
|
||||
|
||||
|
||||
def load_config(root):
|
||||
p = os.path.join(root, ".dev-workflows.toml")
|
||||
if not os.path.isfile(p):
|
||||
return {}
|
||||
with open(p, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
||||
|
||||
def local_bin(root, name):
|
||||
p = os.path.join(root, "node_modules", ".bin", name)
|
||||
return p if os.path.exists(p) else shutil.which(name)
|
||||
|
||||
|
||||
def node_pm(root):
|
||||
for lock, pm in (("pnpm-lock.yaml", "pnpm"), ("yarn.lock", "yarn"), ("bun.lockb", "bun"), ("bun.lock", "bun")):
|
||||
if os.path.exists(os.path.join(root, lock)):
|
||||
return pm
|
||||
return "npm"
|
||||
|
||||
|
||||
def plan_checks(root, cfg, files, base):
|
||||
"""Returns [(name, tier, cmd|None, kind)] ; kind: gate (fails the gate) | info (report only)."""
|
||||
cmds = cfg.get("commands", {})
|
||||
exts = {os.path.splitext(f)[1] for f in files}
|
||||
js_files = [f for f in files if os.path.splitext(f)[1] in JS_EXT]
|
||||
py_files = [f for f in files if f.endswith(".py")]
|
||||
checks = []
|
||||
|
||||
pkg = {}
|
||||
if os.path.isfile(os.path.join(root, "package.json")):
|
||||
try:
|
||||
pkg = json.load(open(os.path.join(root, "package.json"))).get("scripts", {})
|
||||
except ValueError:
|
||||
pkg = {}
|
||||
pm = node_pm(root)
|
||||
is_py = any(os.path.exists(os.path.join(root, f)) for f in ("pyproject.toml", "setup.py", "requirements.txt"))
|
||||
pyproject = {}
|
||||
if os.path.isfile(os.path.join(root, "pyproject.toml")):
|
||||
try:
|
||||
pyproject = tomllib.load(open(os.path.join(root, "pyproject.toml"), "rb"))
|
||||
except tomllib.TOMLDecodeError:
|
||||
pass
|
||||
tool = pyproject.get("tool", {})
|
||||
|
||||
def pick(key, *candidates):
|
||||
if key in cmds:
|
||||
return cmds[key]
|
||||
return next((c for c in candidates if c), None)
|
||||
|
||||
lint = pick("lint",
|
||||
f"{pm} run lint" if "lint" in pkg else None,
|
||||
(local_bin(root, "eslint") and js_files and [local_bin(root, "eslint"), *js_files]),
|
||||
(shutil.which("ruff") and py_files and ["ruff", "check", *py_files]),
|
||||
("go vet ./..." if os.path.exists(os.path.join(root, "go.mod")) else None),
|
||||
("cargo clippy -- -D warnings" if os.path.exists(os.path.join(root, "Cargo.toml")) else None))
|
||||
typecheck = pick("typecheck",
|
||||
next((f"{pm} run {s}" for s in ("typecheck", "type-check", "tsc") if s in pkg), None),
|
||||
(os.path.exists(os.path.join(root, "tsconfig.json")) and local_bin(root, "tsc") and [local_bin(root, "tsc"), "--noEmit"]),
|
||||
(("pyright" in tool or os.path.exists(os.path.join(root, "pyrightconfig.json"))) and shutil.which("pyright") and ["pyright"]),
|
||||
("mypy" in tool and shutil.which("mypy") and ["mypy", "."]))
|
||||
venv_py = next((os.path.join(root, v, "bin", "python") for v in (".venv", "venv")
|
||||
if os.path.exists(os.path.join(root, v, "bin", "python"))), "python3")
|
||||
test = pick("test",
|
||||
f"{pm} run test" if "test" in pkg else None,
|
||||
(is_py and [venv_py, "-m", "pytest", "-q"]),
|
||||
("go test ./..." if os.path.exists(os.path.join(root, "go.mod")) else None),
|
||||
("cargo test" if os.path.exists(os.path.join(root, "Cargo.toml")) else None))
|
||||
build = pick("build", f"{pm} run build" if "build" in pkg else None)
|
||||
|
||||
checks += [("lint", 0, lint, "gate"), ("typecheck", 0, typecheck, "gate"), ("test", 0, test, "gate")]
|
||||
checks.append(("secrets", 0, "@gitleaks" if shutil.which("gitleaks") else None, "gate"))
|
||||
|
||||
cov = os.path.join(root, cfg.get("coverage", {}).get("report", "coverage.xml"))
|
||||
fail_under = cfg.get("thresholds", {}).get("diff_coverage", 80)
|
||||
checks.append(("diff-coverage", 1,
|
||||
shutil.which("diff-cover") and os.path.isfile(cov) and base
|
||||
and ["diff-cover", cov, f"--compare-branch={base}", f"--fail-under={fail_under}"], "gate"))
|
||||
checks.append(("semgrep", 1, shutil.which("semgrep") and base and
|
||||
["semgrep", "scan", "--config", "p/default", "--error", "--metrics=off", "--quiet",
|
||||
"--baseline-commit", base], "gate"))
|
||||
dead = (local_bin(root, "knip") and pkg and [local_bin(root, "knip"), "--no-exit-code"]) or \
|
||||
(shutil.which("vulture") and py_files and ["vulture", *py_files, "--min-confidence", "80"])
|
||||
checks.append(("dead-code", 1, dead, "info"))
|
||||
|
||||
checks.append(("build", 2, build, "gate") if build else ("build", 2, "@skip", "gate"))
|
||||
stryker_cfg = any(os.path.exists(os.path.join(root, f)) for f in
|
||||
("stryker.config.mjs", "stryker.config.js", "stryker.config.json", "stryker.conf.js", ".stryker.conf.json"))
|
||||
mut = None
|
||||
if stryker_cfg and local_bin(root, "stryker") and js_files:
|
||||
mut = [local_bin(root, "stryker"), "run", "--mutate", ",".join(js_files)]
|
||||
elif "mutmut" in tool and shutil.which("mutmut"):
|
||||
mut = ["mutmut", "run"] # ponytail: mutmut 3 runs its configured paths, not only the diff
|
||||
checks.append(("mutation", 2, cmds.get("mutation", mut), "gate"))
|
||||
checks.append(("osv-scanner", 2, shutil.which("osv-scanner") and ["osv-scanner", "scan", "source", "--recursive", "."], "gate"))
|
||||
cruiser = next((f for f in (".dependency-cruiser.js", ".dependency-cruiser.cjs", ".dependency-cruiser.json")
|
||||
if os.path.exists(os.path.join(root, f))), None)
|
||||
arch = (cruiser and local_bin(root, "depcruise") and js_files and [local_bin(root, "depcruise"), "--config", cruiser, *js_files]) or \
|
||||
(("importlinter" in tool or os.path.exists(os.path.join(root, ".importlinter"))) and shutil.which("lint-imports") and ["lint-imports"])
|
||||
checks.append(("architecture", 2, cmds.get("architecture", arch) or "@skip", "gate"))
|
||||
checks.append(("duplication", 2, local_bin(root, "jscpd") and js_files and [local_bin(root, "jscpd"), *js_files], "info"))
|
||||
|
||||
checks.append(("trivy", 3, shutil.which("trivy") and
|
||||
["trivy", "fs", "--exit-code", "1", "--severity", "HIGH,CRITICAL", "--scanners", "vuln,misconfig,secret", "."], "gate"))
|
||||
if cfg.get("spec", {}).get("file"):
|
||||
checks.append(("traceability", 3, "@trace", "gate"))
|
||||
|
||||
for extra in cfg.get("check", []):
|
||||
checks.append((extra["name"], int(extra.get("tier", 2)), extra["cmd"], extra.get("kind", "gate")))
|
||||
|
||||
if not exts & CODE_EXT and files:
|
||||
checks = [c for c in checks if c[0] == "secrets"]
|
||||
return checks
|
||||
|
||||
|
||||
def run_gitleaks(root, files):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
for f in files:
|
||||
dst = os.path.join(tmp, f)
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
shutil.copy2(os.path.join(root, f), dst)
|
||||
return sh(["gitleaks", "dir", "--no-banner", "--redact", tmp], root)
|
||||
|
||||
|
||||
def run_trace(root, cfg):
|
||||
spec = cfg["spec"]
|
||||
text = open(os.path.join(root, spec["file"]), errors="ignore").read()
|
||||
ids = sorted(set(re.findall(r"\bREQ-\d+\b", text)))
|
||||
corpus = ""
|
||||
for d in spec.get("tests", ["tests", "test", "src"]):
|
||||
for dirpath, _, names in os.walk(os.path.join(root, d)):
|
||||
for n in names:
|
||||
corpus += open(os.path.join(dirpath, n), errors="ignore").read()
|
||||
missing = [i for i in ids if i not in corpus]
|
||||
msg = f"{len(ids)} requisitos, sem teste: {', '.join(missing) or 'nenhum'}"
|
||||
return (1 if missing or not ids else 0), msg, 0.0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--tier", type=int, default=int(os.environ.get("DW_TIER", 1)), choices=range(4))
|
||||
ap.add_argument("--base")
|
||||
ap.add_argument("--dir", default=".")
|
||||
ap.add_argument("--list", action="store_true", help="show the planned checks without running them")
|
||||
a = ap.parse_args()
|
||||
|
||||
root = git(["rev-parse", "--show-toplevel"], a.dir) or os.path.abspath(a.dir)
|
||||
cfg = load_config(root)
|
||||
base = find_base(root, a.base)
|
||||
files = changed_files(root, base)
|
||||
planned = [c for c in plan_checks(root, cfg, files, base) if c[1] <= a.tier]
|
||||
|
||||
if a.list:
|
||||
for name, tier, cmd, kind in planned:
|
||||
state = "skip" if cmd == "@skip" else ("unverified (tool/config missing)" if not cmd else "run")
|
||||
print(f"T{tier} {name:14} {kind:4} {state}")
|
||||
return 0
|
||||
|
||||
results = []
|
||||
for name, tier, cmd, kind in planned:
|
||||
if cmd == "@skip":
|
||||
continue
|
||||
if not cmd:
|
||||
results.append({"name": name, "tier": tier, "kind": kind, "status": "unverified"})
|
||||
continue
|
||||
if cmd == "@gitleaks":
|
||||
code, out, secs = run_gitleaks(root, files) if files else (0, "no changed files", 0.0)
|
||||
elif cmd == "@trace":
|
||||
code, out, secs = run_trace(root, cfg)
|
||||
else:
|
||||
code, out, secs = sh(cmd, root, int(cfg.get("timeout", 600)))
|
||||
missing_tool = code == 127 or re.search(r"No module named (pytest|mypy|pyright|ruff)\b", out)
|
||||
status = "unverified" if missing_tool else "info" if kind == "info" else ("pass" if code == 0 else "fail")
|
||||
results.append({"name": name, "tier": tier, "kind": kind, "status": status, "exit": code, "seconds": secs,
|
||||
"cmd": cmd if isinstance(cmd, str) else " ".join(cmd),
|
||||
"output_tail": "\n".join(out.strip().splitlines()[-30:])})
|
||||
|
||||
new_deps = sorted(deps_at(root, None) - deps_at(root, base)) if base else []
|
||||
verdict = "fail" if any(r["status"] == "fail" for r in results) else \
|
||||
"incomplete" if any(r["status"] == "unverified" for r in results) else "pass"
|
||||
report = {"verdict": verdict, "tier": a.tier, "base": base, "generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
||||
"diff": diff_stats(root, base, files), "changed_files": files, "new_dependencies": new_deps, "checks": results}
|
||||
|
||||
out_dir = os.path.join(root, ".dev-workflows")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
json.dump(report, open(os.path.join(out_dir, "gate.json"), "w"), indent=2, ensure_ascii=False)
|
||||
with open(os.path.join(out_dir, "log.jsonl"), "a") as log:
|
||||
log.write(json.dumps({k: report[k] for k in ("generated_at", "verdict", "tier", "diff")}) + "\n")
|
||||
exclude = os.path.join(root, ".git", "info", "exclude")
|
||||
if os.path.isdir(os.path.dirname(exclude)):
|
||||
current = open(exclude).read() if os.path.exists(exclude) else ""
|
||||
if ".dev-workflows/" not in current:
|
||||
with open(exclude, "a") as f:
|
||||
f.write("\n.dev-workflows/\n")
|
||||
|
||||
icon = {"pass": "ok", "fail": "FALHOU", "unverified": "não verificado", "info": "info"}
|
||||
for r in results:
|
||||
print(f" [{icon[r['status']]:>14}] T{r['tier']} {r['name']}")
|
||||
d = report["diff"]
|
||||
print(f"diff: {d['files']} arquivos, +{d['added']} -{d['removed']} (líquido {d['net']})")
|
||||
if new_deps:
|
||||
print("dependências novas (justifique cada uma): " + ", ".join(new_deps))
|
||||
print(f"veredito: {verdict} -> {os.path.relpath(os.path.join(out_dir, 'gate.json'), os.getcwd())}")
|
||||
return 1 if verdict == "fail" else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+3
-55
@@ -1,7 +1,7 @@
|
||||
# Skills vendorizadas — origem e licença
|
||||
|
||||
Este diretório contém cópias vendorizadas (não forks, não submodules) das
|
||||
skills de terceiros usadas pelo `wf-security-review` e pelo `wf-frontend`. Vendorizar em vez de só
|
||||
skills de terceiros usadas pelo `wf-security-review`. Vendorizar em vez de só
|
||||
linkar (como `docs/SKILLS.md` fazia antes) significa que `git clone
|
||||
dev-workflows` já traz tudo funcional, sem passo manual de instalação externa.
|
||||
|
||||
@@ -21,64 +21,12 @@ repo de origem, ou comparar datas).
|
||||
| `patch-prioritization` | UnitOneAI/SecuritySkills | `70bc259` (2026-06-18) | MIT |
|
||||
| `dependency-scanning` | UnitOneAI/SecuritySkills | `70bc259` (2026-06-18) | MIT |
|
||||
| `hermaguard` | [Sahil-SS9/hermaguard](https://github.com/Sahil-SS9/hermaguard) | `b491fbe` (2026-08-15) | MIT |
|
||||
| `design-taste-frontend` | [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill) `skills/taste-skill/` | `c184364` (2026-09-23) | MIT |
|
||||
| `redesign-existing-projects` | Leonxlnx/taste-skill `skills/redesign-skill/` | `c184364` (2026-09-23) | MIT |
|
||||
|
||||
As duas skills do taste-skill são usadas pelo `wf-frontend`. O `LICENSE` do
|
||||
upstream vai dentro de cada diretório, porque a MIT exige que o aviso de
|
||||
copyright acompanhe a cópia. `design-taste-frontend` tem ~87 KB (~35 mil
|
||||
tokens por carga, medido com `claude plugin details`): carregue só para UI nova, não para ajuste pontual.
|
||||
|
||||
Licença CC-BY-4.0 (OWASP/secure-agent-playbook) exige atribuição — mantida
|
||||
via a tabela acima e o link direto ao repo original em cada menção nos
|
||||
READMEs. Nenhum conteúdo foi modificado em relação ao original; cópia 1:1
|
||||
do diretório da skill, mais os arquivos de apoio que a própria skill
|
||||
referencia (ver abaixo).
|
||||
|
||||
## Arquivos de apoio vendorizados
|
||||
|
||||
Os `SKILL.md` acima não são autossuficientes: eles apontam para playbooks,
|
||||
templates e schemas que ficam em outros diretórios do repo de origem. A
|
||||
primeira vendorização copiou só o `SKILL.md` e deixou esses arquivos para
|
||||
trás — o resultado é que a skill carregava e apontava para um arquivo
|
||||
inexistente. Eles foram trazidos junto, dentro do diretório de cada skill,
|
||||
para que ela continue funcional depois de copiada sozinha:
|
||||
|
||||
| Arquivo | Skill(s) que referenciam | Origem |
|
||||
|---|---|---|
|
||||
| `plays/api-security-review.md` | `api-security-review` | OWASP/secure-agent-playbook `plugins/code-security-skills/plays/` |
|
||||
| `plays/code-review-security.md` | `code-review-security` | idem |
|
||||
| `plays/sca-audit.md` | `sca-audit` | idem |
|
||||
| `plays/secrets-scan.md` | `secrets-scan` | idem |
|
||||
| `plays/owasp-top10-web-review.md` | `web-security-review` | idem |
|
||||
| `templates/finding.md` | `code-review-security`, `sca-audit`, `secrets-scan`, `web-security-review` | idem, `templates/` |
|
||||
| `schemas/finding.schema.json` | `cve-triage`, `patch-prioritization`, `dependency-scanning` | UnitOneAI/SecuritySkills `schemas/` |
|
||||
| `docs/sarif-output.md` | `dependency-scanning` | UnitOneAI/SecuritySkills `docs/` |
|
||||
| `docs/fixer-policy.md` | `dependency-scanning` | UnitOneAI/SecuritySkills `docs/` |
|
||||
| `docs/normalized-json-output.md` | `dependency-scanning` (linkado de `docs/sarif-output.md`) | UnitOneAI/SecuritySkills `docs/` |
|
||||
|
||||
Esses arquivos são cópias 1:1 do upstream. A única edição nos `SKILL.md` foi
|
||||
nos caminhos: as referências `../../../schemas/…` e `../../../docs/…`
|
||||
apontavam para a raiz do repo de origem, que tinha um nível a mais
|
||||
(`skills/<categoria>/<skill>/`). Como aqui o diretório é plano
|
||||
(`skills/<skill>/`), os links foram ajustados para o caminho relativo dentro
|
||||
da própria skill, que é o que sobrevive quando a pasta da skill é copiada sozinha (plugin,
|
||||
`scripts/install.py opencode` ou `hermes`).
|
||||
|
||||
## Lacunas conhecidas
|
||||
|
||||
- `hermaguard` referencia `governance/multi-gate-qa.md` e
|
||||
`.github/workflows/hermaguard.yml`, que existem só no ambiente do autor
|
||||
original (pipeline `kensei`), não no repo upstream. Não foram criados: são
|
||||
contexto de integração, não dependência de execução da skill.
|
||||
- Os playbooks citam controles ASVS em `data/asvs/*.md` (dataset do
|
||||
OWASP/secure-agent-playbook). São citações informativas por check, não
|
||||
arquivos que a skill precise abrir para rodar; o dataset inteiro (~90
|
||||
arquivos) não foi vendorizado de propósito.
|
||||
- `hermaguard` foi vendorizado no commit `b491fbe`; o upstream já avançou
|
||||
(adicionou a ferramenta `hermaguard-trajectory` e `test_trajectory.py`). A
|
||||
cópia local é internamente consistente (README, `pyproject.toml` e testes
|
||||
batem entre si), mas está atrás do upstream.
|
||||
do diretório da skill (mais `check_deps.py` no caso do `safedeps`, que a
|
||||
skill referencia como script auxiliar).
|
||||
|
||||
Pra atualizar uma skill vendorizada: reclone o repo de origem, `diff -r`
|
||||
contra o diretório aqui, copie por cima se houver mudança real, e atualize
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,204 +0,0 @@
|
||||
# Play: Security Code Review
|
||||
|
||||
A security code review identifies vulnerabilities in application logic, authentication, authorization, cryptography, and data handling. This play structures the review around OWASP weakness classes (injection, auth, crypto, etc.) and is applicable to any language or framework. Reviews can target full codebases, pull request diffs, or specific modules, and findings are mapped to OWASP standards and OpenCRE for multi-framework traceability.
|
||||
|
||||
Review code changes or codebases for security vulnerabilities, mapping findings to OWASP standards and OpenCRE cross-references for multi-framework traceability.
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
Use this skill when:
|
||||
- Reviewing a pull request or diff for security issues
|
||||
- A user asks for a security review of their code
|
||||
- Auditing a specific file, module, or feature for vulnerabilities
|
||||
- Performing a pre-merge security gate check
|
||||
|
||||
## Inputs
|
||||
|
||||
- Code diff (PR), specific files, or entire codebase to review
|
||||
- (Optional) Language and framework context
|
||||
- (Optional) ASVS level to verify against (L1, L2, or L3)
|
||||
- (Optional) Specific concern areas to focus on
|
||||
|
||||
## Procedure
|
||||
|
||||
### 1. Scope & Context
|
||||
|
||||
Before diving in, establish:
|
||||
- **Language/Framework**: Determines which vulnerability classes are relevant
|
||||
- **Trust boundary**: Is this server-side, client-side, library, CLI, infrastructure code?
|
||||
- **Data sensitivity**: Does this code handle PII, credentials, financial data, health data?
|
||||
- **Exposure**: Is this internet-facing, internal, or local-only?
|
||||
|
||||
### 2. Systematic Review by Vulnerability Class
|
||||
|
||||
Review in priority order — highest-impact classes first:
|
||||
|
||||
#### A. Injection (CWE-74, OWASP A03, [OpenCRE 161-451](https://www.opencre.org/cre/161-451))
|
||||
|
||||
> ASVS refs: `data/asvs/V12.1.md` (Input Validation), `data/asvs/V13.1.md` (Output Encoding), `data/asvs/V5.3.md` (File Storage)
|
||||
|
||||
| Subtype | What to Look For |
|
||||
|---------|-----------------|
|
||||
| SQL Injection | String concatenation in queries, missing parameterization, ORM raw queries |
|
||||
| Command Injection | User input in exec/spawn/system calls, unsanitized shell arguments |
|
||||
| XSS | User input rendered without output encoding, unsafe DOM manipulation, unescaped template output |
|
||||
| SSTI | User input in template strings that get evaluated server-side |
|
||||
| LDAP Injection | User input in LDAP filters without escaping |
|
||||
| Path Traversal | User input in file paths without canonicalization, `../` not blocked |
|
||||
| Header Injection | User input in HTTP headers (CRLF injection) |
|
||||
| Log Injection | User input written to logs without sanitization |
|
||||
|
||||
#### B. Authentication & Session (CWE-287, OWASP A07, [OpenCRE 633-428](https://www.opencre.org/cre/633-428))
|
||||
|
||||
> ASVS refs: `data/asvs/V2.1.md`–`V2.4.md` (Authentication), `data/asvs/V3.1.md`–`V3.7.md` (Session Management)
|
||||
|
||||
- Hardcoded credentials or API keys
|
||||
- Missing authentication on endpoints
|
||||
- Weak password requirements
|
||||
- Session tokens in URLs or logs
|
||||
- Missing session invalidation on logout/password change
|
||||
- JWT issues: none algorithm, missing expiry, weak signing key, secrets in payload
|
||||
- Missing MFA enforcement for sensitive operations
|
||||
|
||||
#### C. Authorization (CWE-862, OWASP A01, [OpenCRE 724-770](https://www.opencre.org/cre/724-770))
|
||||
|
||||
> ASVS refs: `data/asvs/V4.1.md`–`V4.4.md` (Access Control)
|
||||
|
||||
- Missing authorization checks on endpoints or functions
|
||||
- IDOR: object references without ownership validation
|
||||
- Horizontal privilege escalation (accessing other users' data)
|
||||
- Vertical privilege escalation (accessing admin functions)
|
||||
- Missing RBAC/ABAC enforcement
|
||||
- Direct database queries that don't filter by user/tenant
|
||||
|
||||
#### D. Cryptography (CWE-327, OWASP A02, [OpenCRE 278-646](https://www.opencre.org/cre/278-646))
|
||||
|
||||
> ASVS refs: `data/asvs/V6.1.md`–`V6.8.md` (Cryptography)
|
||||
|
||||
- Weak algorithms: MD5, SHA1 for passwords, DES, RC4
|
||||
- ECB mode usage
|
||||
- Hardcoded encryption keys or IVs
|
||||
- Missing encryption at rest for sensitive data
|
||||
- Missing TLS enforcement
|
||||
- Custom crypto implementations (don't roll your own)
|
||||
- Insufficient key lengths (RSA < 2048, AES < 128)
|
||||
|
||||
#### E. Data Exposure (CWE-200, OWASP A01, [OpenCRE 126-668](https://www.opencre.org/cre/126-668))
|
||||
|
||||
> ASVS refs: `data/asvs/V8.1.md`–`V8.4.md` (Data Protection), `data/asvs/V7.1.md`–`V7.6.md` (Error Handling & Logging)
|
||||
|
||||
- Sensitive data in error messages or stack traces
|
||||
- Verbose logging of requests/responses containing PII
|
||||
- Credentials or tokens in source code, configs, or URLs
|
||||
- Missing response headers (X-Content-Type-Options, etc.)
|
||||
- API responses including more fields than necessary
|
||||
- Debug mode enabled in production configs
|
||||
|
||||
#### F. Security Misconfiguration (OWASP A05, [OpenCRE 233-748](https://www.opencre.org/cre/233-748))
|
||||
|
||||
> ASVS refs: `data/asvs/V10.1.md`–`V10.7.md` (Configuration)
|
||||
|
||||
- Default credentials or configurations
|
||||
- Unnecessary features enabled (debug endpoints, admin panels)
|
||||
- Missing security headers
|
||||
- Overly permissive CORS
|
||||
- Directory listing enabled
|
||||
- Missing rate limiting on sensitive endpoints
|
||||
|
||||
#### G. Deserialization & Data Integrity (CWE-502, OWASP A08, [OpenCRE 854-643](https://www.opencre.org/cre/854-643))
|
||||
|
||||
> ASVS refs: `data/asvs/V11.1.md`–`V11.7.md` (Business Logic)
|
||||
|
||||
- Deserializing untrusted data (unsafe deserialization in any language — Python, Java, PHP, Ruby, .NET)
|
||||
- Missing integrity checks on data from untrusted sources
|
||||
- Unsigned cookies or tokens
|
||||
- Missing CSRF protection on state-changing operations
|
||||
|
||||
### 3. Framework-Specific Checks
|
||||
|
||||
Apply checks specific to the detected framework:
|
||||
|
||||
| Framework | Key Checks |
|
||||
|-----------|-----------|
|
||||
| React/Next.js | XSS via unsafe HTML rendering, SSRF in server-side data fetching, exposed API routes |
|
||||
| Express/Node | Missing helmet, no rate limiting, prototype pollution, regex DoS |
|
||||
| Django | Raw SQL, CSRF exemptions, DEBUG=True, SECRET_KEY exposure |
|
||||
| Flask | Jinja2 autoescape disabled, unsafe session serialization, debug mode |
|
||||
| Spring | SpEL injection, actuator exposure, mass assignment |
|
||||
| Rails | Mass assignment, render user input, SQL fragments |
|
||||
| Go net/http | Missing timeouts, TOCTOU in file operations, integer overflow |
|
||||
|
||||
### 4. Diff-Specific Analysis (for PR reviews)
|
||||
|
||||
When reviewing a diff rather than a full codebase:
|
||||
- Focus on changed lines and their immediate context
|
||||
- Check if security controls in surrounding (unchanged) code are preserved
|
||||
- Verify that new endpoints have auth/authz matching existing patterns
|
||||
- Check if new dependencies introduce vulnerabilities (cross-reference with sca-audit)
|
||||
- Look for removed security controls (deleted validation, removed auth checks)
|
||||
|
||||
### 5. Produce Findings
|
||||
|
||||
For each issue:
|
||||
- Cite the specific code location (file:line)
|
||||
- Show the vulnerable code snippet
|
||||
- Explain the attack scenario concretely
|
||||
- Provide a fixed code snippet
|
||||
- Rate confidence (HIGH if confirmed path, MEDIUM if contextual, LOW if heuristic)
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Security Code Review: [Target]
|
||||
|
||||
### Scope
|
||||
- **Files reviewed**: [count or list]
|
||||
- **Language/Framework**: [detected]
|
||||
- **Review type**: Full | PR diff | Targeted
|
||||
|
||||
### Findings
|
||||
[Standard finding template for each issue, sorted by severity]
|
||||
|
||||
### Positive Observations
|
||||
[Security controls that ARE in place — acknowledge good practices]
|
||||
|
||||
### Summary
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| CRITICAL | N |
|
||||
| HIGH | N |
|
||||
| MEDIUM | N |
|
||||
| LOW | N |
|
||||
| INFO | N |
|
||||
```
|
||||
|
||||
## Tools & Resources
|
||||
|
||||
### Static Analysis Tools
|
||||
- **Semgrep** — Multi-language pattern-based SAST; highly customizable rules
|
||||
- **CodeQL (GitHub)** — Query-based code analysis for Java, C++, C#, Go, Python, TypeScript, JavaScript, Ruby
|
||||
- **SonarQube** — Code quality and security scanning with OWASP Top 10 mappings
|
||||
- **Checkmarx/CxSAST** — Enterprise SAST with custom rule authoring
|
||||
- **Snyk Code** — Developer-first SAST integrated into IDEs and CI/CD
|
||||
|
||||
### Language-Specific
|
||||
- **Bandit** (Python) — Finds common security issues in Python code
|
||||
- **Brakeman** (Ruby) — Scans Rails apps for OWASP risks
|
||||
- **go-vuln-check** (Go) — Detects known vulnerabilities in Go code
|
||||
- **ShiftLeft/Taint** (Java, Python, Go) — Data flow analysis for injection detection
|
||||
|
||||
### Code Review Checkers
|
||||
- **OWASP Cheat Sheet Series** — Per-vulnerability quick guides and remediation
|
||||
- **CWE Top 25** — Prioritized list of most dangerous weakness patterns
|
||||
- **ASVS Interactive** — Maps review items to ASVS verification levels (L1/L2/L3)
|
||||
|
||||
## References
|
||||
|
||||
- OWASP Top 10 (2021): A01-A10
|
||||
- OWASP ASVS v5.0 (for verification-level mapping)
|
||||
- OWASP Code Review Guide
|
||||
- OWASP Cheat Sheet Series (per-topic)
|
||||
- OWASP Proactive Controls
|
||||
- [OpenCRE](https://www.opencre.org) — Cross-standard requirement mappings (CWE, ASVS, WSTG, NIST 800-53, ISO 27001)
|
||||
- [CWE](https://cwe.mitre.org) v4.19 — Common Weakness Enumeration
|
||||
- ASVS 5.0 reference data in `data/asvs/` — sourced from [OWASP Agent Skills Project](https://github.com/eoftedal/owasp-agent-skills-project)
|
||||
@@ -1,48 +0,0 @@
|
||||
# Finding Template
|
||||
|
||||
Use this structure for all security findings produced by skills.
|
||||
|
||||
```markdown
|
||||
### [SEVERITY] Title
|
||||
|
||||
- **ID**: NDC-YYYY-NNN (auto-incrementing)
|
||||
- **CWE**: CWE-XXX (if applicable)
|
||||
- **CVE**: CVE-YYYY-NNNNN (if applicable)
|
||||
- **OpenCRE**: [CRE-ID](https://www.opencre.org/cre/CRE-ID) — requirement name (see data/opencre/)
|
||||
- **OWASP Ref**: Top 10 A01, ASVS V4.1.1, MASVS-STORAGE-1, MASWE-0001, MASTG-TEST-0200, LLM01, etc.
|
||||
- **Location**: file_path:line_number or component name
|
||||
- **Impact**: What an attacker can achieve (1-2 sentences)
|
||||
- **Evidence**: Code snippet, command output, or proof-of-concept
|
||||
- **Remediation**: Specific fix with code example
|
||||
- **Confidence**: HIGH | MEDIUM | LOW (how certain is this finding)
|
||||
```
|
||||
|
||||
### Resolving OpenCRE Links
|
||||
|
||||
When you have a CWE, look up its OpenCRE mapping via the API:
|
||||
|
||||
```
|
||||
GET https://www.opencre.org/rest/v1/standard/CWE/sectionid/{cwe-number}
|
||||
```
|
||||
|
||||
This returns the CRE ID and linked standards (ASVS, WSTG, NIST 800-53, cheat sheets). Include the CRE link in findings to give readers one-click access to all related standards and remediation guidance.
|
||||
|
||||
Common mappings are pre-populated in `data/opencre/README.md`.
|
||||
|
||||
## Severity Definitions
|
||||
|
||||
| Severity | CVSS Range | Criteria |
|
||||
|----------|-----------|----------|
|
||||
| CRITICAL | 9.0-10.0 | Remote code execution, full system compromise, mass data breach |
|
||||
| HIGH | 7.0-8.9 | Privilege escalation, significant data exposure, auth bypass |
|
||||
| MEDIUM | 4.0-6.9 | Limited data exposure, requires user interaction or specific conditions |
|
||||
| LOW | 0.1-3.9 | Information disclosure, minor misconfigurations |
|
||||
| INFORMATIONAL | N/A | Best practice recommendations, defense-in-depth suggestions |
|
||||
|
||||
## Confidence Levels
|
||||
|
||||
| Level | Meaning |
|
||||
|-------|---------|
|
||||
| HIGH | Confirmed via code path analysis, PoC, or tool output |
|
||||
| MEDIUM | Strong indicators but not fully confirmed — needs manual verification |
|
||||
| LOW | Heuristic match or pattern-based — may be false positive |
|
||||
@@ -415,7 +415,7 @@ When triaging multiple CVEs (e.g., from a scan report), produce a summary table
|
||||
- **Blind spots:** This skill depends on available code, configuration, logs, documentation, and user-provided context; it cannot prove controls exist or threats are absent when evidence is missing, runtime-only, or outside the review scope.
|
||||
- **False-positive risks:** Treat findings as hypotheses until validated against asset criticality, compensating controls, environment intent, and recent authorized changes.
|
||||
- **Required evidence:** Support each finding with concrete artifacts such as file paths and line numbers, policy snippets, scanner output, logs, screenshots, control records, or reproducible steps.
|
||||
- **Normalized JSON:** When machine-readable output is requested, findings MUST be available as JSON that validates against [`schemas/finding.schema.json`](schemas/finding.schema.json).
|
||||
- **Normalized JSON:** When machine-readable output is requested, findings MUST be available as JSON that validates against [`schemas/finding.schema.json`](../../../schemas/finding.schema.json).
|
||||
- **Escalation rules:** Escalate immediately for suspected active compromise, exposed secrets, regulated-data exposure, critical exploitable vulnerabilities, privileged-access abuse, or when evidence is insufficient to safely disposition a high-impact risk.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://github.com/UnitOneAI/SecuritySkills/schemas/finding.schema.json",
|
||||
"title": "SecuritySkills Normalized Finding Output",
|
||||
"description": "Machine-readable envelope for normalized security findings and remediation recommendations emitted by SecuritySkills workflows. This contract is independent of SARIF and may be mapped to downstream enterprise formats later.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["schema_version", "run", "skill", "findings"],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"const": "1.0.0",
|
||||
"description": "Version of this normalized SecuritySkills finding envelope."
|
||||
},
|
||||
"run": {
|
||||
"$ref": "#/$defs/run",
|
||||
"description": "Metadata for the analysis run that produced this output."
|
||||
},
|
||||
"skill": {
|
||||
"$ref": "#/$defs/skill",
|
||||
"description": "Metadata for the SecuritySkills entrypoint that produced this output."
|
||||
},
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/finding"
|
||||
},
|
||||
"description": "Normalized findings produced by a skill run."
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"riskLevel": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high"]
|
||||
},
|
||||
"nonEmptyString": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uriString": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"run": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "timestamp"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Stable run identifier supplied by the agent, CI job, or enterprise orchestrator."
|
||||
},
|
||||
"timestamp": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "ISO 8601 timestamp for when the run output was produced."
|
||||
},
|
||||
"tool": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Agent, CLI, scanner, or workflow that executed the skill."
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Repository, project, service, artifact, environment, or other target reviewed."
|
||||
},
|
||||
"source_ref": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Optional commit SHA, branch, image digest, build ID, ticket ID, or evidence package reference."
|
||||
}
|
||||
}
|
||||
},
|
||||
"skill": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "version"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "SecuritySkills skill name from SKILL.md frontmatter."
|
||||
},
|
||||
"version": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "SecuritySkills skill version from SKILL.md frontmatter."
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Path to the SKILL.md entrypoint used for the run."
|
||||
},
|
||||
"frameworks": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"description": "Framework identifiers declared by the skill frontmatter."
|
||||
}
|
||||
}
|
||||
},
|
||||
"finding": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "title", "severity", "status", "evidence", "remediations"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Stable finding identifier within the output."
|
||||
},
|
||||
"fingerprint": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Stable deduplication key based on finding type, affected asset, and evidence location."
|
||||
},
|
||||
"title": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"description": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "What the issue is and why it matters."
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["info", "low", "medium", "high", "critical"]
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["open", "mitigated", "accepted_risk", "false_positive"]
|
||||
},
|
||||
"cwe": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^CWE-[0-9]+$"
|
||||
},
|
||||
"description": "Optional CWE mappings, when applicable."
|
||||
},
|
||||
"framework_refs": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/frameworkRef"
|
||||
},
|
||||
"description": "Framework/control references from the skill frontmatter frameworks."
|
||||
},
|
||||
"references": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/reference"
|
||||
},
|
||||
"description": "Optional advisory, standard, vendor, or knowledge-base references."
|
||||
},
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["location", "summary"],
|
||||
"properties": {
|
||||
"location": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "File path, line range, asset ID, log source, policy path, or other concrete evidence location."
|
||||
},
|
||||
"summary": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"artifact_type": {
|
||||
"type": "string",
|
||||
"enum": ["source", "config", "policy", "log", "scan_result", "cloud_resource", "identity", "network", "document", "other"]
|
||||
},
|
||||
"snippet": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Minimal redacted snippet or observation needed to support the finding."
|
||||
},
|
||||
"redacted": {
|
||||
"type": "boolean",
|
||||
"description": "True when sensitive values were intentionally removed from this evidence."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"remediations": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/remediation"
|
||||
},
|
||||
"description": "Every fix recommendation for this finding. Each item must include remediation guidance, confidence, blast radius, behavior-change risk, and test strategy."
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"required": ["cwe"]
|
||||
},
|
||||
{
|
||||
"required": ["framework_refs"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"frameworkRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["framework", "control"],
|
||||
"properties": {
|
||||
"framework": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"control": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reference": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "id"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["cve", "cwe", "framework", "advisory", "vendor", "documentation", "other"]
|
||||
},
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"url": {
|
||||
"$ref": "#/$defs/uriString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["guidance", "confidence", "blast_radius", "behavior_change_risk", "test_strategy"],
|
||||
"properties": {
|
||||
"guidance": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Concrete remediation steps or patch guidance for the finding."
|
||||
},
|
||||
"confidence": {
|
||||
"$ref": "#/$defs/riskLevel",
|
||||
"description": "How confident the skill is that this recommendation is correct for the observed evidence."
|
||||
},
|
||||
"blast_radius": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Expected scope of systems, files, users, integrations, data, or workflows affected by the change."
|
||||
},
|
||||
"behavior_change_risk": {
|
||||
"$ref": "#/$defs/riskLevel",
|
||||
"description": "Risk that the remediation changes intended application or operational behavior."
|
||||
},
|
||||
"test_strategy": {
|
||||
"$ref": "#/$defs/testStrategy",
|
||||
"description": "Required validation strategy proving the remediation fixes the finding. Include recommended tests, generated tests, or both."
|
||||
}
|
||||
}
|
||||
},
|
||||
"testStrategy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["summary"],
|
||||
"properties": {
|
||||
"summary": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Concise explanation of what proves the remediation fixed the issue."
|
||||
},
|
||||
"recommended_tests": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/recommendedTest"
|
||||
},
|
||||
"description": "Tests the user or agent should run when tests are not generated directly."
|
||||
},
|
||||
"generated_tests": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/generatedTest"
|
||||
},
|
||||
"description": "Test files or cases produced as part of the remediation."
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"required": ["recommended_tests"]
|
||||
},
|
||||
{
|
||||
"required": ["generated_tests"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"recommendedTest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "type", "purpose", "command", "expected_result"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "unit", "integration", "e2e", "regression", "manual"]
|
||||
},
|
||||
"purpose": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Behavior, vulnerability condition, or regression this test proves."
|
||||
},
|
||||
"command": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Command or manual check to execute for this test."
|
||||
},
|
||||
"expected_result": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"generatedTest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["path", "type", "purpose", "command", "expected_result"],
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Path to the generated or modified test file."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "unit", "integration", "e2e", "regression"]
|
||||
},
|
||||
"purpose": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Behavior, vulnerability condition, or regression this generated test proves."
|
||||
},
|
||||
"command": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Command that runs the generated test."
|
||||
},
|
||||
"expected_result": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,7 +183,7 @@ Typosquatting (also called dependency confusion or combosquatting) is a supply c
|
||||
|
||||
## Assessment Output Template
|
||||
|
||||
Before applying or proposing dependency changes, classify each remediation path using [Security Fixer Policy](docs/fixer-policy.md). Include the policy review gate, reviewer evidence, and rollback guidance in the remediation plan.
|
||||
Before applying or proposing dependency changes, classify each remediation path using [Security Fixer Policy](../../../docs/fixer-policy.md). Include the policy review gate, reviewer evidence, and rollback guidance in the remediation plan.
|
||||
|
||||
When performing a dependency scan, produce findings in the following structure:
|
||||
|
||||
@@ -236,8 +236,8 @@ When performing a dependency scan, produce findings in the following structure:
|
||||
- **Blind spots:** This skill depends on available code, configuration, logs, documentation, and user-provided context; it cannot prove controls exist or threats are absent when evidence is missing, runtime-only, or outside the review scope.
|
||||
- **False-positive risks:** Treat findings as hypotheses until validated against asset criticality, compensating controls, environment intent, and recent authorized changes.
|
||||
- **Required evidence:** Support each finding with concrete artifacts such as file paths and line numbers, policy snippets, scanner output, logs, screenshots, control records, or reproducible steps.
|
||||
- **Normalized JSON:** When machine-readable output is requested, findings MUST be available as JSON that validates against [`schemas/finding.schema.json`](schemas/finding.schema.json).
|
||||
- **SARIF JSON:** When SARIF output is requested, map normalized findings to SARIF 2.1.0-compatible JSON using [`docs/sarif-output.md`](docs/sarif-output.md).
|
||||
- **Normalized JSON:** When machine-readable output is requested, findings MUST be available as JSON that validates against [`schemas/finding.schema.json`](../../../schemas/finding.schema.json).
|
||||
- **SARIF JSON:** When SARIF output is requested, map normalized findings to SARIF 2.1.0-compatible JSON using [`docs/sarif-output.md`](../../../docs/sarif-output.md).
|
||||
- **Escalation rules:** Escalate immediately for suspected active compromise, exposed secrets, regulated-data exposure, critical exploitable vulnerabilities, privileged-access abuse, or when evidence is insufficient to safely disposition a high-impact risk.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
# Security Fixer Policy
|
||||
|
||||
This policy classifies security findings by how an agent may remediate them. Fixer-capable skills must use this policy before changing files, generating patches, or recommending an automated remediation path.
|
||||
|
||||
## Categories
|
||||
|
||||
### Auto-fix
|
||||
|
||||
The agent may apply the fix directly when the finding is high-confidence, the change is narrow, and the expected behavior is mechanically verifiable.
|
||||
|
||||
Criteria:
|
||||
- The vulnerable pattern and target file are unambiguous.
|
||||
- The fix is deterministic and follows an existing project convention or a well-known safe default.
|
||||
- The change has low blast radius and does not alter public APIs, authorization semantics, data models, deployment topology, or runtime trust boundaries.
|
||||
- The agent can run or define a concrete verification step.
|
||||
|
||||
Examples:
|
||||
- Add missing security headers in a local framework configuration using the project's existing middleware pattern.
|
||||
- Replace string-built SQL with parameter binding when the query shape and parameters are clear.
|
||||
- Pin a container image tag or GitHub Action version when the intended version is known from lock files, release metadata, or existing repository conventions.
|
||||
- Add a `.dockerignore` or `.gitignore` entry for `.env` files without removing or exposing secret values.
|
||||
|
||||
### Assisted-fix
|
||||
|
||||
The agent may prepare a patch or exact commands, but the user or repository owner must confirm context, run environment-specific checks, or choose among valid alternatives before merge.
|
||||
|
||||
Criteria:
|
||||
- The finding is valid, but the best remediation depends on application behavior, deployment constraints, ownership boundaries, or compatibility requirements.
|
||||
- Multiple safe remediation options exist.
|
||||
- The change is moderate in scope or requires coordinated validation outside the local workspace.
|
||||
- The agent can reduce the work to a reviewable patch, migration plan, configuration diff, or decision list.
|
||||
|
||||
Examples:
|
||||
- Add authorization middleware to an endpoint when the correct role or ownership predicate needs product confirmation.
|
||||
- Upgrade a vulnerable dependency across a major version boundary.
|
||||
- Tighten CI permissions where release, package, or deployment jobs may need specific write scopes.
|
||||
- Add Kubernetes `NetworkPolicy` rules where service communication requirements must be confirmed.
|
||||
|
||||
### Guidance-only
|
||||
|
||||
The agent must not modify files. It should explain the finding, risk, and recommended remediation path.
|
||||
|
||||
Criteria:
|
||||
- The remediation requires organizational policy, architecture changes, vendor configuration, legal review, procurement, or manual operational work.
|
||||
- The agent lacks access to the system that must be changed.
|
||||
- The change cannot be represented safely as a local patch.
|
||||
|
||||
Examples:
|
||||
- Adopt a centralized secrets manager across teams.
|
||||
- Establish a vulnerability management SLA.
|
||||
- Change cloud account guardrails, identity provider policy, or production firewall rules outside the repository.
|
||||
- Resolve license obligations for GPL, AGPL, commercial, unknown, or no-license dependencies.
|
||||
|
||||
### Human-review-required
|
||||
|
||||
The agent must stop short of applying a fix and explicitly request human review before remediation proceeds.
|
||||
|
||||
Criteria:
|
||||
- A hard gate below applies.
|
||||
- The finding involves sensitive credentials, production access, destructive operations, legal/compliance interpretation, or security-critical logic.
|
||||
- The agent cannot confidently preserve intended behavior.
|
||||
- The evidence is incomplete or the finding may be a false positive with material operational impact.
|
||||
|
||||
Examples:
|
||||
- Rotate leaked credentials or revoke certificates.
|
||||
- Change authentication, authorization, cryptography, payment, medical, safety, or tenant-isolation logic.
|
||||
- Modify production deployment, incident response, or containment procedures.
|
||||
- Apply a remediation that deletes data, removes audit evidence, weakens controls, or accepts risk.
|
||||
|
||||
## Decision Criteria
|
||||
|
||||
Classify each finding using the most restrictive category that applies.
|
||||
|
||||
1. Confirm the finding is in scope for the skill and supported by concrete evidence.
|
||||
2. Determine whether any hard gate forces human review.
|
||||
3. Estimate blast radius: local config or code path, cross-cutting application behavior, infrastructure, production operations, or organizational policy.
|
||||
4. Check remediation confidence: deterministic patch, multiple valid options, missing context, or uncertain behavior.
|
||||
5. Check verification: automated test or scan, manual validation, external approval, or no reliable verification available.
|
||||
6. Choose the category:
|
||||
- Use auto-fix only when evidence, scope, remediation, and verification are all strong.
|
||||
- Use assisted-fix when a patch is useful but context or approval is required.
|
||||
- Use guidance-only when local code changes are not the right remediation vehicle.
|
||||
- Use human-review-required whenever a hard gate applies or safe behavior preservation is uncertain.
|
||||
|
||||
## Hard Gates for Human Review
|
||||
|
||||
Any of these conditions forces `human-review-required`:
|
||||
|
||||
- Secret exposure requiring credential rotation, revocation, certificate replacement, or git history rewriting.
|
||||
- Authentication, authorization, session management, cryptographic, payment, tenant isolation, or safety-critical logic changes.
|
||||
- Production infrastructure, network, identity, IAM, deployment, incident response, or data retention changes.
|
||||
- Destructive or irreversible actions, including deleting data, rewriting history, disabling audit logs, removing evidence, or changing backups.
|
||||
- Legal, compliance, privacy, or license-risk decisions.
|
||||
- Changes requiring owner-specific business rules, threat model assumptions, regulatory interpretation, or risk acceptance.
|
||||
- Unclear ownership, missing tests for a high-impact path, conflicting framework guidance, or evidence that the finding may be a false positive.
|
||||
- Any remediation that would weaken an existing security control to make a tool pass.
|
||||
|
||||
## Fixer Workflow Review Gates
|
||||
|
||||
Fixer-capable workflows must pass these gates before any applied patch is considered ready for handoff:
|
||||
|
||||
1. **Scope gate:** State the files, settings, and behaviors that are in scope; avoid unrelated refactors, formatting churn, dependency updates, or opportunistic cleanup.
|
||||
2. **Approval gate:** Request explicit approval before assisted-fix or human-review-required work proceeds, and before any change that affects hard-gated areas even when a patch can be drafted.
|
||||
3. **Review gate:** Provide reviewer evidence that includes the finding, policy category, changed files, behavior-change risk, verification performed or still required, and any assumptions the reviewer must confirm.
|
||||
4. **Rollback gate:** Provide rollback guidance for every proposed or applied change, including the exact revert path when local patches are used or the operational rollback owner when the change cannot be reverted locally.
|
||||
|
||||
Applied fixes must be isolated to the smallest viable patch. If the agent discovers unrelated defects while fixing, it should report them separately instead of expanding the patch. If rollback is unclear, expensive, or depends on production state, classify the remediation as `human-review-required`.
|
||||
|
||||
## Skill Usage
|
||||
|
||||
Fixer-capable skills must reference this policy when producing remediation guidance or patches. Their workflows must include the review gates above and must not apply changes until the selected category allows it. The policy classifies the remediation path only; it does not change finding schemas or require new output fields.
|
||||
@@ -1,158 +0,0 @@
|
||||
# Normalized JSON Output Contract
|
||||
|
||||
SecuritySkills can emit machine-readable findings as a normalized JSON envelope
|
||||
that validates against [`schemas/finding.schema.json`](../schemas/finding.schema.json).
|
||||
This contract is independent of SARIF. Downstream systems may map it to SARIF,
|
||||
ticketing systems, GRC platforms, dashboards, or vulnerability stores without
|
||||
changing individual skill output rules.
|
||||
See [`docs/sarif-output.md`](sarif-output.md) for the repo-level SARIF 2.1.0
|
||||
export mapping.
|
||||
|
||||
## Envelope
|
||||
|
||||
Every JSON response uses this top-level shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"run": {
|
||||
"id": "run-2026-06-16T12:00:00Z",
|
||||
"timestamp": "2026-06-16T12:00:00Z",
|
||||
"tool": "codex",
|
||||
"target": "github.com/example/service",
|
||||
"source_ref": "commit-or-build-id"
|
||||
},
|
||||
"skill": {
|
||||
"name": "secure-code-review",
|
||||
"version": "1.0.0",
|
||||
"path": "skills/appsec/secure-code-review/SKILL.md",
|
||||
"frameworks": ["OWASP-ASVS-4.0.3", "CWE"]
|
||||
},
|
||||
"findings": []
|
||||
}
|
||||
```
|
||||
|
||||
- `schema_version` is fixed at `1.0.0` until a breaking contract change is
|
||||
required.
|
||||
- `run.id` is the deduplication boundary for one execution. Use an orchestrator
|
||||
ID, CI job ID, or generated run ID.
|
||||
- `run.timestamp` should be an ISO 8601 timestamp.
|
||||
- `run.target` identifies the reviewed repository, project, service, artifact,
|
||||
environment, or evidence package.
|
||||
- `skill.name`, `skill.version`, and `skill.frameworks` come from the skill's
|
||||
`SKILL.md` frontmatter.
|
||||
|
||||
## Findings
|
||||
|
||||
Each finding must include:
|
||||
|
||||
- `id`: stable finding ID within the run.
|
||||
- `title`: concise finding title.
|
||||
- `severity`: one of `info`, `low`, `medium`, `high`, or `critical`.
|
||||
- `status`: one of `open`, `mitigated`, `accepted_risk`, or `false_positive`.
|
||||
- `evidence`: one or more concrete evidence entries.
|
||||
- `remediations`: one or more remediation recommendations, each with a test
|
||||
strategy.
|
||||
|
||||
Each finding must include at least one framework/CWE mapping:
|
||||
|
||||
- `cwe`: array of CWE IDs such as `CWE-89`, when applicable.
|
||||
- `framework_refs`: array of framework/control references from the skill's
|
||||
declared frameworks.
|
||||
|
||||
Optional fields such as `fingerprint`, `description`, and `references` support
|
||||
enterprise deduplication, analyst context, and external advisory linking.
|
||||
|
||||
## Evidence
|
||||
|
||||
Evidence entries must identify where the issue was observed and summarize the
|
||||
observation. Locations may be source paths, line ranges, cloud resource IDs, log
|
||||
sources, policy paths, scanner result identifiers, or evidence package records.
|
||||
|
||||
Use `snippet` only for the minimal redacted text needed to prove the finding.
|
||||
Set `redacted: true` when secrets, tokens, personal data, internal hostnames, or
|
||||
other sensitive values were removed.
|
||||
|
||||
## Remediation And Tests
|
||||
|
||||
Each remediation item must include:
|
||||
|
||||
- `guidance`: concrete remediation steps or patch guidance.
|
||||
- `confidence`: `low`, `medium`, or `high`.
|
||||
- `blast_radius`: expected affected files, systems, users, integrations, data,
|
||||
or workflows.
|
||||
- `behavior_change_risk`: `low`, `medium`, or `high`.
|
||||
- `test_strategy`: validation that proves the issue is fixed.
|
||||
|
||||
`test_strategy` must include a `summary` and at least one of
|
||||
`recommended_tests` or `generated_tests`. See
|
||||
[`docs/remediation-output.md`](remediation-output.md) for remediation-specific
|
||||
field guidance.
|
||||
|
||||
## Minimal Example
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"run": {
|
||||
"id": "run-001",
|
||||
"timestamp": "2026-06-16T12:00:00Z",
|
||||
"tool": "codex",
|
||||
"target": "payments-api",
|
||||
"source_ref": "abc1234"
|
||||
},
|
||||
"skill": {
|
||||
"name": "api-security",
|
||||
"version": "1.0.0",
|
||||
"path": "skills/appsec/api-security/SKILL.md",
|
||||
"frameworks": ["OWASP-API-Top-10-2023", "CWE"]
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "API-SEC-001",
|
||||
"fingerprint": "api-security:users-delete:missing-admin-auth",
|
||||
"title": "Administrative endpoint lacks authorization check",
|
||||
"description": "The delete-user route accepts authenticated requests without verifying administrative privileges.",
|
||||
"severity": "high",
|
||||
"status": "open",
|
||||
"cwe": ["CWE-862"],
|
||||
"framework_refs": [
|
||||
{
|
||||
"framework": "OWASP-API-Top-10-2023",
|
||||
"control": "API5:2023",
|
||||
"name": "Broken Function Level Authorization"
|
||||
}
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"location": "routes/users.js:42",
|
||||
"artifact_type": "source",
|
||||
"summary": "DELETE /users/:id checks authentication but not administrator role.",
|
||||
"snippet": "router.delete('/users/:id', requireAuth, deleteUser)",
|
||||
"redacted": false
|
||||
}
|
||||
],
|
||||
"remediations": [
|
||||
{
|
||||
"guidance": "Require an administrator role check before invoking deleteUser.",
|
||||
"confidence": "high",
|
||||
"blast_radius": "User administration routes only.",
|
||||
"behavior_change_risk": "medium",
|
||||
"test_strategy": {
|
||||
"summary": "Proves non-admin users cannot delete accounts while admins still can.",
|
||||
"recommended_tests": [
|
||||
{
|
||||
"name": "Reject non-admin delete",
|
||||
"type": "integration",
|
||||
"purpose": "Confirms the vulnerable authorization bypass no longer succeeds.",
|
||||
"command": "npm test -- users-authz.test.js",
|
||||
"expected_result": "Non-admin DELETE /users/:id returns 403 and admin DELETE succeeds."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,229 +0,0 @@
|
||||
# SARIF Output Mapping
|
||||
|
||||
SecuritySkills may emit SARIF-compatible JSON for tools that ingest Static
|
||||
Analysis Results Interchange Format (SARIF) 2.1.0. SARIF output is an export
|
||||
view of the normalized finding envelope documented in
|
||||
[`docs/normalized-json-output.md`](normalized-json-output.md); it does not
|
||||
replace the normalized JSON contract or change the required finding fields.
|
||||
|
||||
## Top-Level Shape
|
||||
|
||||
Use SARIF version `2.1.0` with the standard schema URI:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
||||
"version": "2.1.0",
|
||||
"runs": [
|
||||
{
|
||||
"tool": {
|
||||
"driver": {
|
||||
"name": "SecuritySkills",
|
||||
"semanticVersion": "1.0.0",
|
||||
"rules": []
|
||||
}
|
||||
},
|
||||
"results": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Use one `run` per normalized envelope. `run.tool.driver.name` should identify
|
||||
SecuritySkills or the executing agent. `run.tool.driver.semanticVersion` should
|
||||
use the skill version when a single skill produced the run.
|
||||
|
||||
## Mapping
|
||||
|
||||
Map normalized fields to SARIF as follows:
|
||||
|
||||
| Normalized field | SARIF field |
|
||||
|---|---|
|
||||
| `run.id`, `run.timestamp`, `run.target`, `run.source_ref` | `runs[].properties.securityskills.run` |
|
||||
| `skill.name`, `skill.version`, `skill.path`, `skill.frameworks` | `runs[].tool.driver.name`, `runs[].tool.driver.semanticVersion`, `runs[].properties.securityskills.skill` |
|
||||
| `finding.id` | `results[].ruleId` and `results[].properties.securityskills.finding_id` |
|
||||
| `finding.fingerprint` | `results[].partialFingerprints.securityskillsFingerprint` |
|
||||
| `finding.title` | `tool.driver.rules[].shortDescription.text` |
|
||||
| `finding.description` | `tool.driver.rules[].fullDescription.text` and `results[].message.text` |
|
||||
| `finding.severity` | `results[].level` |
|
||||
| `finding.status` | `results[].properties.securityskills.status` |
|
||||
| `finding.cwe` | `tool.driver.rules[].relationships` when supported, and `properties.securityskills.cwe` |
|
||||
| `finding.framework_refs` | `tool.driver.rules[].properties.securityskills.framework_refs` |
|
||||
| `finding.evidence` | `results[].locations` plus `properties.securityskills.evidence` |
|
||||
| `finding.references` | `tool.driver.rules[].helpUri` or `tool.driver.rules[].properties.securityskills.references` |
|
||||
| `finding.remediations` | `tool.driver.rules[].help.text` and `properties.securityskills.remediations`, including each remediation's `test_strategy` |
|
||||
|
||||
## Severity
|
||||
|
||||
SARIF `level` has fewer values than the normalized contract. Use this mapping:
|
||||
|
||||
| Normalized severity | SARIF level |
|
||||
|---|---|
|
||||
| `critical` | `error` |
|
||||
| `high` | `error` |
|
||||
| `medium` | `warning` |
|
||||
| `low` | `note` |
|
||||
| `info` | `note` |
|
||||
|
||||
Keep the original severity in
|
||||
`results[].properties.securityskills.severity` so downstream systems can
|
||||
preserve the distinction between `critical` and `high`.
|
||||
|
||||
## Locations
|
||||
|
||||
For source, configuration, policy, or scan-result evidence with file locations,
|
||||
populate `results[].locations[].physicalLocation`:
|
||||
|
||||
```json
|
||||
{
|
||||
"physicalLocation": {
|
||||
"artifactLocation": {
|
||||
"uri": "routes/users.js"
|
||||
},
|
||||
"region": {
|
||||
"startLine": 42
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"text": "DELETE /users/:id checks authentication but not administrator role."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If the normalized evidence location does not contain a parseable file path and
|
||||
line number, still emit a SARIF result and store the original location under
|
||||
`properties.securityskills.evidence`. For cloud resources, identities, network
|
||||
assets, logs, or documents, use the best stable asset identifier as
|
||||
`artifactLocation.uri` only when it is meaningful to the SARIF consumer.
|
||||
|
||||
## Rules
|
||||
|
||||
Create one `tool.driver.rules[]` entry per distinct `ruleId`. A rule should
|
||||
include:
|
||||
|
||||
- `id`: the normalized `finding.id`, or a stable detector ID when multiple
|
||||
findings share the same detection rule.
|
||||
- `shortDescription.text`: the finding title or detector title.
|
||||
- `fullDescription.text`: the finding description.
|
||||
- `help.text`: remediation guidance and test strategy summary.
|
||||
- `properties.securityskills`: CWE IDs, framework references, normalized
|
||||
severity, and any remediation metadata that does not fit native SARIF fields.
|
||||
|
||||
Do not invent CWE, framework, or control identifiers for SARIF. Reuse only the
|
||||
values already present in the normalized finding.
|
||||
|
||||
## Minimal Example
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
||||
"version": "2.1.0",
|
||||
"runs": [
|
||||
{
|
||||
"tool": {
|
||||
"driver": {
|
||||
"name": "api-security",
|
||||
"semanticVersion": "1.0.0",
|
||||
"rules": [
|
||||
{
|
||||
"id": "API-SEC-001",
|
||||
"shortDescription": {
|
||||
"text": "Administrative endpoint lacks authorization check"
|
||||
},
|
||||
"fullDescription": {
|
||||
"text": "The delete-user route accepts authenticated requests without verifying administrative privileges."
|
||||
},
|
||||
"help": {
|
||||
"text": "Require an administrator role check before invoking deleteUser. Validate with integration tests for non-admin and admin delete flows."
|
||||
},
|
||||
"properties": {
|
||||
"securityskills": {
|
||||
"severity": "high",
|
||||
"cwe": ["CWE-862"],
|
||||
"framework_refs": [
|
||||
{
|
||||
"framework": "OWASP-API-Top-10-2023",
|
||||
"control": "API5:2023",
|
||||
"name": "Broken Function Level Authorization"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"ruleId": "API-SEC-001",
|
||||
"level": "error",
|
||||
"message": {
|
||||
"text": "Administrative endpoint lacks authorization check"
|
||||
},
|
||||
"locations": [
|
||||
{
|
||||
"physicalLocation": {
|
||||
"artifactLocation": {
|
||||
"uri": "routes/users.js"
|
||||
},
|
||||
"region": {
|
||||
"startLine": 42
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"text": "DELETE /users/:id checks authentication but not administrator role."
|
||||
}
|
||||
}
|
||||
],
|
||||
"partialFingerprints": {
|
||||
"securityskillsFingerprint": "api-security:users-delete:missing-admin-auth"
|
||||
},
|
||||
"properties": {
|
||||
"securityskills": {
|
||||
"finding_id": "API-SEC-001",
|
||||
"severity": "high",
|
||||
"status": "open",
|
||||
"remediations": [
|
||||
{
|
||||
"guidance": "Require an administrator role check before invoking deleteUser.",
|
||||
"confidence": "high",
|
||||
"blast_radius": "User administration routes only.",
|
||||
"behavior_change_risk": "medium",
|
||||
"test_strategy": {
|
||||
"summary": "Proves non-admin users cannot delete accounts while admins still can.",
|
||||
"recommended_tests": [
|
||||
{
|
||||
"name": "Reject non-admin delete",
|
||||
"type": "integration",
|
||||
"purpose": "Confirms the authorization bypass no longer succeeds.",
|
||||
"command": "npm test -- users-authz.test.js",
|
||||
"expected_result": "Non-admin DELETE returns 403 and admin DELETE succeeds."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"securityskills": {
|
||||
"run": {
|
||||
"id": "run-001",
|
||||
"timestamp": "2026-06-16T12:00:00Z",
|
||||
"target": "payments-api",
|
||||
"source_ref": "abc1234"
|
||||
},
|
||||
"skill": {
|
||||
"name": "api-security",
|
||||
"version": "1.0.0",
|
||||
"path": "skills/appsec/api-security/SKILL.md",
|
||||
"frameworks": ["OWASP-API-Top-10-2023", "CWE"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,345 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://github.com/UnitOneAI/SecuritySkills/schemas/finding.schema.json",
|
||||
"title": "SecuritySkills Normalized Finding Output",
|
||||
"description": "Machine-readable envelope for normalized security findings and remediation recommendations emitted by SecuritySkills workflows. This contract is independent of SARIF and may be mapped to downstream enterprise formats later.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["schema_version", "run", "skill", "findings"],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"const": "1.0.0",
|
||||
"description": "Version of this normalized SecuritySkills finding envelope."
|
||||
},
|
||||
"run": {
|
||||
"$ref": "#/$defs/run",
|
||||
"description": "Metadata for the analysis run that produced this output."
|
||||
},
|
||||
"skill": {
|
||||
"$ref": "#/$defs/skill",
|
||||
"description": "Metadata for the SecuritySkills entrypoint that produced this output."
|
||||
},
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/finding"
|
||||
},
|
||||
"description": "Normalized findings produced by a skill run."
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"riskLevel": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high"]
|
||||
},
|
||||
"nonEmptyString": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uriString": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"run": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "timestamp"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Stable run identifier supplied by the agent, CI job, or enterprise orchestrator."
|
||||
},
|
||||
"timestamp": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "ISO 8601 timestamp for when the run output was produced."
|
||||
},
|
||||
"tool": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Agent, CLI, scanner, or workflow that executed the skill."
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Repository, project, service, artifact, environment, or other target reviewed."
|
||||
},
|
||||
"source_ref": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Optional commit SHA, branch, image digest, build ID, ticket ID, or evidence package reference."
|
||||
}
|
||||
}
|
||||
},
|
||||
"skill": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "version"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "SecuritySkills skill name from SKILL.md frontmatter."
|
||||
},
|
||||
"version": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "SecuritySkills skill version from SKILL.md frontmatter."
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Path to the SKILL.md entrypoint used for the run."
|
||||
},
|
||||
"frameworks": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"description": "Framework identifiers declared by the skill frontmatter."
|
||||
}
|
||||
}
|
||||
},
|
||||
"finding": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "title", "severity", "status", "evidence", "remediations"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Stable finding identifier within the output."
|
||||
},
|
||||
"fingerprint": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Stable deduplication key based on finding type, affected asset, and evidence location."
|
||||
},
|
||||
"title": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"description": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "What the issue is and why it matters."
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["info", "low", "medium", "high", "critical"]
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["open", "mitigated", "accepted_risk", "false_positive"]
|
||||
},
|
||||
"cwe": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^CWE-[0-9]+$"
|
||||
},
|
||||
"description": "Optional CWE mappings, when applicable."
|
||||
},
|
||||
"framework_refs": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/frameworkRef"
|
||||
},
|
||||
"description": "Framework/control references from the skill frontmatter frameworks."
|
||||
},
|
||||
"references": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/reference"
|
||||
},
|
||||
"description": "Optional advisory, standard, vendor, or knowledge-base references."
|
||||
},
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["location", "summary"],
|
||||
"properties": {
|
||||
"location": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "File path, line range, asset ID, log source, policy path, or other concrete evidence location."
|
||||
},
|
||||
"summary": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"artifact_type": {
|
||||
"type": "string",
|
||||
"enum": ["source", "config", "policy", "log", "scan_result", "cloud_resource", "identity", "network", "document", "other"]
|
||||
},
|
||||
"snippet": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Minimal redacted snippet or observation needed to support the finding."
|
||||
},
|
||||
"redacted": {
|
||||
"type": "boolean",
|
||||
"description": "True when sensitive values were intentionally removed from this evidence."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"remediations": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/remediation"
|
||||
},
|
||||
"description": "Every fix recommendation for this finding. Each item must include remediation guidance, confidence, blast radius, behavior-change risk, and test strategy."
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"required": ["cwe"]
|
||||
},
|
||||
{
|
||||
"required": ["framework_refs"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"frameworkRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["framework", "control"],
|
||||
"properties": {
|
||||
"framework": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"control": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reference": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "id"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["cve", "cwe", "framework", "advisory", "vendor", "documentation", "other"]
|
||||
},
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"url": {
|
||||
"$ref": "#/$defs/uriString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["guidance", "confidence", "blast_radius", "behavior_change_risk", "test_strategy"],
|
||||
"properties": {
|
||||
"guidance": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Concrete remediation steps or patch guidance for the finding."
|
||||
},
|
||||
"confidence": {
|
||||
"$ref": "#/$defs/riskLevel",
|
||||
"description": "How confident the skill is that this recommendation is correct for the observed evidence."
|
||||
},
|
||||
"blast_radius": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Expected scope of systems, files, users, integrations, data, or workflows affected by the change."
|
||||
},
|
||||
"behavior_change_risk": {
|
||||
"$ref": "#/$defs/riskLevel",
|
||||
"description": "Risk that the remediation changes intended application or operational behavior."
|
||||
},
|
||||
"test_strategy": {
|
||||
"$ref": "#/$defs/testStrategy",
|
||||
"description": "Required validation strategy proving the remediation fixes the finding. Include recommended tests, generated tests, or both."
|
||||
}
|
||||
}
|
||||
},
|
||||
"testStrategy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["summary"],
|
||||
"properties": {
|
||||
"summary": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Concise explanation of what proves the remediation fixed the issue."
|
||||
},
|
||||
"recommended_tests": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/recommendedTest"
|
||||
},
|
||||
"description": "Tests the user or agent should run when tests are not generated directly."
|
||||
},
|
||||
"generated_tests": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/generatedTest"
|
||||
},
|
||||
"description": "Test files or cases produced as part of the remediation."
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"required": ["recommended_tests"]
|
||||
},
|
||||
{
|
||||
"required": ["generated_tests"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"recommendedTest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "type", "purpose", "command", "expected_result"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "unit", "integration", "e2e", "regression", "manual"]
|
||||
},
|
||||
"purpose": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Behavior, vulnerability condition, or regression this test proves."
|
||||
},
|
||||
"command": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Command or manual check to execute for this test."
|
||||
},
|
||||
"expected_result": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"generatedTest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["path", "type", "purpose", "command", "expected_result"],
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Path to the generated or modified test file."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "unit", "integration", "e2e", "regression"]
|
||||
},
|
||||
"purpose": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Behavior, vulnerability condition, or regression this generated test proves."
|
||||
},
|
||||
"command": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Command that runs the generated test."
|
||||
},
|
||||
"expected_result": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Leonxlnx
|
||||
|
||||
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.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,73 +0,0 @@
|
||||
---
|
||||
name: dev-router
|
||||
description: Use no início de qualquer pedido de código (feature, bug, refactor, UI, API, migração, review; "implement", "fix", "build", "add", "change"). Classifica o pedido em nível 0 a 3 e decide quais workflows, agentes e checagens rodar, para um pedido pequeno não pagar o custo de um grande.
|
||||
license: MIT
|
||||
metadata:
|
||||
dev-workflows:
|
||||
uses: [lean-code, wf-bugfix, wf-refactor, wf-frontend, wf-backend, wf-architecture, wf-security-review, safedeps]
|
||||
external: []
|
||||
---
|
||||
|
||||
# Dev Router
|
||||
|
||||
A primeira linha da resposta declara o nível: `Nível N: <motivo em uma frase>`.
|
||||
|
||||
## 1. Classificar
|
||||
|
||||
| Nível | Quando | Exemplos |
|
||||
|---|---|---|
|
||||
| 0, trivial | nenhum comportamento muda; 1 arquivo | texto, cor, typo, renomear variável local |
|
||||
| 1, pequeno | comportamento muda em 1 a 3 arquivos, sem superfície pública nova | bug localizado, ajuste de validação, refactor local |
|
||||
| 2, feature | superfície nova ou contrato alterado; 4+ arquivos; dependência nova | endpoint, tela, módulo, integração |
|
||||
| 3, grande ou de risco | vários módulos ou serviços, dados existentes, arquitetura, release | migração com dados, troca de auth, novo serviço |
|
||||
|
||||
Na dúvida entre dois níveis, escolha o menor e suba quando aparecer um gatilho.
|
||||
|
||||
## 2. Gatilhos que sobem o nível a qualquer momento
|
||||
|
||||
| Gatilho | Mínimo | Soma |
|
||||
|---|---|---|
|
||||
| autenticação, autorização, sessão, tokens, criptografia | 2 | `security-auditor` |
|
||||
| dinheiro, cobrança, dados pessoais (LGPD) | 2 | `security-auditor` |
|
||||
| upload, SQL montado à mão, shell/exec, desserialização, template com entrada do usuário | 2 | `security-auditor` |
|
||||
| dependência nova | 1 | `safedeps` antes de instalar |
|
||||
| migração de schema | 2 (3 se mexe em dado existente) | rollback testado |
|
||||
| feature que chama um LLM | 2 | OWASP LLM Top 10 no `security-auditor` |
|
||||
|
||||
Depois que um gatilho disparou, não rebaixe por conta própria.
|
||||
|
||||
O usuário pode forçar: "modo rápido" vira nível 0 ou 1, mas gatilho de segurança continua valendo (avise); "modo completo" vira nível 3.
|
||||
|
||||
## 3. O que cada nível roda
|
||||
|
||||
| Nível | Fluxo | Workflow | Agentes |
|
||||
|---|---|---|---|
|
||||
| 0 | fazer, rodar o gate | nenhum | nenhum |
|
||||
| 1 | entender ou reproduzir, mudar, teste de regressão, gate | `wf-bugfix` ou `wf-refactor` | `explorer` só se não souber onde está o código |
|
||||
| 2 | explorar, plano curto, TDD, revisão independente, gate | `wf-backend` e/ou `wf-frontend`; full-stack começa pelo contrato do backend | `explorer`, `implementer`, `reviewer`; `security-auditor` com gatilho; `ui-critic` com UI |
|
||||
| 3 | spec EARS com IDs e ADR, aprovação do usuário, tarefas pequenas com TDD, revisões, gate | `wf-architecture` antes; `wf-backend`/`wf-frontend` na execução; `wf-security-review` no fim | `planner`, `implementer`, `reviewer`, `security-auditor`, `ui-critic` com UI, `verifier` |
|
||||
|
||||
Em todo nível: aplique `lean-code`. Peça aprovação do usuário só quando houver uma decisão que é dele (produto, custo, dado existente); no nível 3, a spec sempre passa por ele.
|
||||
|
||||
Delegue a um agente quando o trabalho dele não precisa voltar inteiro para esta conversa (mapear código, revisar um diff). Tarefa que cabe em poucos passos você mesmo faz.
|
||||
|
||||
## 4. Gate de qualidade
|
||||
|
||||
Antes de dizer que terminou, rode:
|
||||
|
||||
```bash
|
||||
"${CLAUDE_PLUGIN_ROOT}/scripts/quality-gate" --tier N
|
||||
```
|
||||
|
||||
- `fail`: corrija. Se não der, explique ao usuário o que falhou e por quê.
|
||||
- `incomplete`: diga ao usuário quais checagens ficaram "não verificado" (ferramenta ausente). Nunca chame de testado o que não rodou.
|
||||
- `dependências novas`: justifique cada uma (ver `lean-code`).
|
||||
|
||||
O hook de `Stop` bloqueia o encerramento enquanto houver código alterado sem gate atualizado ou com gate falho. Configuração por projeto e lista de checagens: [references/quality-gate.md](references/quality-gate.md).
|
||||
|
||||
## 5. Relatório final (nível 1 ou mais)
|
||||
|
||||
1. Nível e motivo.
|
||||
2. O que mudou: arquivos e linhas líquidas (`diff` do gate).
|
||||
3. Evidência: veredito do gate; checagens ok, falhas e não verificadas.
|
||||
4. O que ficou de fora ou é risco conhecido.
|
||||
@@ -1,71 +0,0 @@
|
||||
# Quality gate: checagens e configuração
|
||||
|
||||
`scripts/quality-gate --tier N` roda as checagens do nível N e dos níveis abaixo, só sobre os arquivos
|
||||
alterados em relação à branch base (merge-base com `origin/HEAD`, `main` ou `master`). O resultado vai
|
||||
para `.dev-workflows/gate.json` (ignorado pelo git automaticamente) e uma linha resumida vai para
|
||||
`.dev-workflows/log.jsonl`.
|
||||
|
||||
`--list` mostra o que rodaria e o que está faltando, sem rodar nada.
|
||||
|
||||
## Checagens
|
||||
|
||||
| Nível | Checagem | Ferramenta | Como é detectada |
|
||||
|---|---|---|---|
|
||||
| 0 | lint | script `lint` do package.json, eslint, ruff, `go vet`, clippy | primeira que existir |
|
||||
| 0 | typecheck | script `typecheck`, `tsc --noEmit`, pyright, mypy | tsconfig ou config do checker |
|
||||
| 0 | test | script `test`, pytest (usa `.venv` se existir), `go test`, `cargo test` | stack do projeto |
|
||||
| 0 | secrets | gitleaks, só nos arquivos alterados | binário no PATH |
|
||||
| 1 | diff-coverage | diff-cover sobre `coverage.xml` | relatório de cobertura presente |
|
||||
| 1 | semgrep | `semgrep --config p/default --baseline-commit <base>` | binário no PATH |
|
||||
| 1 | dead-code (info) | knip, vulture | binário no projeto ou PATH |
|
||||
| 2 | build | script `build` | package.json |
|
||||
| 2 | mutation | Stryker nos arquivos alterados; mutmut (paths configurados) | config do Stryker ou `[tool.mutmut]` |
|
||||
| 2 | osv-scanner | `osv-scanner scan source --recursive .` | binário no PATH |
|
||||
| 2 | architecture | dependency-cruiser, import-linter | arquivo de config |
|
||||
| 2 | duplication (info) | jscpd | binário no projeto |
|
||||
| 3 | trivy | vulnerabilidades, misconfig e segredos, HIGH e CRITICAL | binário no PATH |
|
||||
| 3 | traceability | todo `REQ-NNN` da spec aparece em algum teste | `[spec]` na config |
|
||||
|
||||
Estados: `pass`, `fail`, `unverified` (ferramenta ou config ausente, nunca conta como aprovado) e
|
||||
`info` (só relatório). Veredito: `fail` se algo falhou; `incomplete` se algo ficou não verificado; senão `pass`.
|
||||
|
||||
## `.dev-workflows.toml` (opcional, na raiz do projeto)
|
||||
|
||||
```toml
|
||||
timeout = 600 # segundos por checagem
|
||||
|
||||
[commands] # substitui o que foi detectado
|
||||
test = "pnpm vitest run --coverage"
|
||||
lint = "pnpm eslint ."
|
||||
mutation = "pnpm stryker run --incremental"
|
||||
|
||||
[thresholds]
|
||||
diff_coverage = 80
|
||||
|
||||
[coverage]
|
||||
report = "coverage/cobertura-coverage.xml"
|
||||
|
||||
[spec] # nível 3: rastreabilidade requisito -> teste
|
||||
file = "docs/spec.md"
|
||||
tests = ["tests", "e2e"]
|
||||
|
||||
[[check]] # checagem extra; kind = "gate" (padrão) ou "info"
|
||||
name = "lighthouse"
|
||||
cmd = "npx lhci autorun"
|
||||
tier = 2
|
||||
|
||||
[[check]]
|
||||
name = "a11y"
|
||||
cmd = "npx playwright test e2e/a11y.spec.ts"
|
||||
tier = 2
|
||||
|
||||
[[check]]
|
||||
name = "api-fuzz"
|
||||
cmd = "schemathesis run http://localhost:8000/openapi.json --checks all"
|
||||
tier = 2
|
||||
```
|
||||
|
||||
Frontend, API e carga dependem do app estar de pé, então entram como `[[check]]` do projeto
|
||||
(Lighthouse CI com orçamento, Playwright com `@axe-core/playwright`, schemathesis, k6 com thresholds).
|
||||
|
||||
`DW_GATE=off` desliga o hook de `Stop` numa sessão.
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
name: lean-code
|
||||
description: Use sempre que for escrever ou mudar código. Escolhe a menor solução que resolve de verdade - reaproveitar antes de escrever, apagar antes de somar, sem abstração especulativa e sem dependência desnecessária.
|
||||
license: MIT
|
||||
metadata:
|
||||
dev-workflows:
|
||||
uses: [safedeps]
|
||||
external: []
|
||||
---
|
||||
|
||||
# Lean Code
|
||||
|
||||
Primeiro entenda o problema e o código que ele toca. Depois suba a escada e pare no primeiro degrau
|
||||
que resolve.
|
||||
|
||||
## Escada
|
||||
|
||||
1. **Precisa existir?** Pedido especulativo ("para depois", "vai que") não entra. Diga isso em uma linha.
|
||||
2. **Já existe no projeto?** Procure helper, componente, tipo ou padrão parecido antes de escrever (`grep` por nomes e por comportamento; o agente `explorer` faz isso).
|
||||
3. **A biblioteca padrão resolve?**
|
||||
4. **A plataforma resolve?** HTML/CSS nativo antes de JS, constraint do banco antes de código, recurso do framework antes de utilitário próprio.
|
||||
5. **Uma dependência já instalada resolve?**
|
||||
6. **Cabe em poucas linhas?**
|
||||
7. Só então: o mínimo de código novo.
|
||||
|
||||
## Regras
|
||||
|
||||
- **Bug se corrige na causa.** Antes de editar uma função, ache todos os chamadores. Uma correção no ponto por onde todos passam é menor que uma por chamador, e não deixa os outros quebrados.
|
||||
- **Sem abstração sem uso:** interface com uma implementação, factory de um produto, configuração para valor que nunca muda.
|
||||
- **Dependência nova exige justificativa** em uma linha (o que ela faz que os degraus 3 a 6 não fazem) e consulta ao `safedeps` antes de instalar. O gate lista as novas.
|
||||
- **Apague o que ficou morto** por causa da mudança.
|
||||
- **Entre duas soluções do mesmo tamanho**, fique com a que está certa nos casos de borda.
|
||||
|
||||
## Nunca simplifique
|
||||
|
||||
Validação na fronteira do sistema, tratamento de erro que evita perda de dado, controles de segurança,
|
||||
acessibilidade básica e qualquer coisa que o usuário pediu explicitamente.
|
||||
|
||||
## Orçamento de diff (linhas líquidas, referência)
|
||||
|
||||
| Nível | Orçamento |
|
||||
|---|---|
|
||||
| 0 | 20 |
|
||||
| 1 | 100 |
|
||||
| 2 | 400 |
|
||||
| 3 | 400 por fatia entregue |
|
||||
|
||||
Estourou: justifique no relatório ou quebre em partes menores.
|
||||
|
||||
## Atalho consciente
|
||||
|
||||
Quando cortar caminho de propósito (lock global, busca O(n²), heurística simples), deixe um comentário
|
||||
`lean:` dizendo o limite e quando evoluir: `# lean: lock global; trocar por lock por conta se a vazão importar`.
|
||||
@@ -381,7 +381,7 @@ Known Exploited Vulnerabilities catalog maintained by CISA. Contains CVEs with c
|
||||
- **Blind spots:** This skill depends on available code, configuration, logs, documentation, and user-provided context; it cannot prove controls exist or threats are absent when evidence is missing, runtime-only, or outside the review scope.
|
||||
- **False-positive risks:** Treat findings as hypotheses until validated against asset criticality, compensating controls, environment intent, and recent authorized changes.
|
||||
- **Required evidence:** Support each finding with concrete artifacts such as file paths and line numbers, policy snippets, scanner output, logs, screenshots, control records, or reproducible steps.
|
||||
- **Normalized JSON:** When machine-readable output is requested, findings MUST be available as JSON that validates against [`schemas/finding.schema.json`](schemas/finding.schema.json).
|
||||
- **Normalized JSON:** When machine-readable output is requested, findings MUST be available as JSON that validates against [`schemas/finding.schema.json`](../../../schemas/finding.schema.json).
|
||||
- **Escalation rules:** Escalate immediately for suspected active compromise, exposed secrets, regulated-data exposure, critical exploitable vulnerabilities, privileged-access abuse, or when evidence is insufficient to safely disposition a high-impact risk.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://github.com/UnitOneAI/SecuritySkills/schemas/finding.schema.json",
|
||||
"title": "SecuritySkills Normalized Finding Output",
|
||||
"description": "Machine-readable envelope for normalized security findings and remediation recommendations emitted by SecuritySkills workflows. This contract is independent of SARIF and may be mapped to downstream enterprise formats later.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["schema_version", "run", "skill", "findings"],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"const": "1.0.0",
|
||||
"description": "Version of this normalized SecuritySkills finding envelope."
|
||||
},
|
||||
"run": {
|
||||
"$ref": "#/$defs/run",
|
||||
"description": "Metadata for the analysis run that produced this output."
|
||||
},
|
||||
"skill": {
|
||||
"$ref": "#/$defs/skill",
|
||||
"description": "Metadata for the SecuritySkills entrypoint that produced this output."
|
||||
},
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/finding"
|
||||
},
|
||||
"description": "Normalized findings produced by a skill run."
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"riskLevel": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high"]
|
||||
},
|
||||
"nonEmptyString": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uriString": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"run": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "timestamp"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Stable run identifier supplied by the agent, CI job, or enterprise orchestrator."
|
||||
},
|
||||
"timestamp": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "ISO 8601 timestamp for when the run output was produced."
|
||||
},
|
||||
"tool": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Agent, CLI, scanner, or workflow that executed the skill."
|
||||
},
|
||||
"target": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Repository, project, service, artifact, environment, or other target reviewed."
|
||||
},
|
||||
"source_ref": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Optional commit SHA, branch, image digest, build ID, ticket ID, or evidence package reference."
|
||||
}
|
||||
}
|
||||
},
|
||||
"skill": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "version"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "SecuritySkills skill name from SKILL.md frontmatter."
|
||||
},
|
||||
"version": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "SecuritySkills skill version from SKILL.md frontmatter."
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Path to the SKILL.md entrypoint used for the run."
|
||||
},
|
||||
"frameworks": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"description": "Framework identifiers declared by the skill frontmatter."
|
||||
}
|
||||
}
|
||||
},
|
||||
"finding": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "title", "severity", "status", "evidence", "remediations"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Stable finding identifier within the output."
|
||||
},
|
||||
"fingerprint": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Stable deduplication key based on finding type, affected asset, and evidence location."
|
||||
},
|
||||
"title": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"description": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "What the issue is and why it matters."
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["info", "low", "medium", "high", "critical"]
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["open", "mitigated", "accepted_risk", "false_positive"]
|
||||
},
|
||||
"cwe": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^CWE-[0-9]+$"
|
||||
},
|
||||
"description": "Optional CWE mappings, when applicable."
|
||||
},
|
||||
"framework_refs": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/frameworkRef"
|
||||
},
|
||||
"description": "Framework/control references from the skill frontmatter frameworks."
|
||||
},
|
||||
"references": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/reference"
|
||||
},
|
||||
"description": "Optional advisory, standard, vendor, or knowledge-base references."
|
||||
},
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["location", "summary"],
|
||||
"properties": {
|
||||
"location": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "File path, line range, asset ID, log source, policy path, or other concrete evidence location."
|
||||
},
|
||||
"summary": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"artifact_type": {
|
||||
"type": "string",
|
||||
"enum": ["source", "config", "policy", "log", "scan_result", "cloud_resource", "identity", "network", "document", "other"]
|
||||
},
|
||||
"snippet": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Minimal redacted snippet or observation needed to support the finding."
|
||||
},
|
||||
"redacted": {
|
||||
"type": "boolean",
|
||||
"description": "True when sensitive values were intentionally removed from this evidence."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"remediations": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/remediation"
|
||||
},
|
||||
"description": "Every fix recommendation for this finding. Each item must include remediation guidance, confidence, blast radius, behavior-change risk, and test strategy."
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"required": ["cwe"]
|
||||
},
|
||||
{
|
||||
"required": ["framework_refs"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"frameworkRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["framework", "control"],
|
||||
"properties": {
|
||||
"framework": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"control": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reference": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "id"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["cve", "cwe", "framework", "advisory", "vendor", "documentation", "other"]
|
||||
},
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"url": {
|
||||
"$ref": "#/$defs/uriString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["guidance", "confidence", "blast_radius", "behavior_change_risk", "test_strategy"],
|
||||
"properties": {
|
||||
"guidance": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Concrete remediation steps or patch guidance for the finding."
|
||||
},
|
||||
"confidence": {
|
||||
"$ref": "#/$defs/riskLevel",
|
||||
"description": "How confident the skill is that this recommendation is correct for the observed evidence."
|
||||
},
|
||||
"blast_radius": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Expected scope of systems, files, users, integrations, data, or workflows affected by the change."
|
||||
},
|
||||
"behavior_change_risk": {
|
||||
"$ref": "#/$defs/riskLevel",
|
||||
"description": "Risk that the remediation changes intended application or operational behavior."
|
||||
},
|
||||
"test_strategy": {
|
||||
"$ref": "#/$defs/testStrategy",
|
||||
"description": "Required validation strategy proving the remediation fixes the finding. Include recommended tests, generated tests, or both."
|
||||
}
|
||||
}
|
||||
},
|
||||
"testStrategy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["summary"],
|
||||
"properties": {
|
||||
"summary": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Concise explanation of what proves the remediation fixed the issue."
|
||||
},
|
||||
"recommended_tests": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/recommendedTest"
|
||||
},
|
||||
"description": "Tests the user or agent should run when tests are not generated directly."
|
||||
},
|
||||
"generated_tests": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/generatedTest"
|
||||
},
|
||||
"description": "Test files or cases produced as part of the remediation."
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"required": ["recommended_tests"]
|
||||
},
|
||||
{
|
||||
"required": ["generated_tests"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"recommendedTest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "type", "purpose", "command", "expected_result"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "unit", "integration", "e2e", "regression", "manual"]
|
||||
},
|
||||
"purpose": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Behavior, vulnerability condition, or regression this test proves."
|
||||
},
|
||||
"command": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Command or manual check to execute for this test."
|
||||
},
|
||||
"expected_result": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"generatedTest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["path", "type", "purpose", "command", "expected_result"],
|
||||
"properties": {
|
||||
"path": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Path to the generated or modified test file."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "unit", "integration", "e2e", "regression"]
|
||||
},
|
||||
"purpose": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Behavior, vulnerability condition, or regression this generated test proves."
|
||||
},
|
||||
"command": {
|
||||
"$ref": "#/$defs/nonEmptyString",
|
||||
"description": "Command that runs the generated test."
|
||||
},
|
||||
"expected_result": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Leonxlnx
|
||||
|
||||
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.
|
||||
@@ -1,178 +0,0 @@
|
||||
---
|
||||
name: redesign-existing-projects
|
||||
description: Upgrades existing websites and apps to premium quality. Audits current design, identifies generic AI patterns, and applies high-end design standards without breaking functionality. Works with any CSS framework or vanilla CSS.
|
||||
---
|
||||
|
||||
# Redesign Skill
|
||||
|
||||
## How This Works
|
||||
|
||||
When applied to an existing project, follow this sequence:
|
||||
|
||||
1. **Scan** — Read the codebase. Identify the framework, styling method (Tailwind, vanilla CSS, styled-components, etc.), and current design patterns.
|
||||
2. **Diagnose** — Run through the audit below. List every generic pattern, weak point, and missing state you find.
|
||||
3. **Fix** — Apply targeted upgrades working with the existing stack. Do not rewrite from scratch. Improve what's there.
|
||||
|
||||
## Design Audit
|
||||
|
||||
### Typography
|
||||
|
||||
Check for these problems and fix them:
|
||||
|
||||
- **Browser default fonts or Inter everywhere.** Replace with a font that has character. Good options: `Geist`, `Outfit`, `Cabinet Grotesk`, `Satoshi`. For editorial/creative projects, pair a serif header with a sans-serif body.
|
||||
- **Headlines lack presence.** Increase size for display text, tighten letter-spacing, reduce line-height. Headlines should feel heavy and intentional.
|
||||
- **Body text too wide.** Limit paragraph width to roughly 65 characters. Increase line-height for readability.
|
||||
- **Only Regular (400) and Bold (700) weights used.** Introduce Medium (500) and SemiBold (600) for more subtle hierarchy.
|
||||
- **Numbers in proportional font.** Use a monospace font or enable tabular figures (`font-variant-numeric: tabular-nums`) for data-heavy interfaces.
|
||||
- **Missing letter-spacing adjustments.** Use negative tracking for large headers, positive tracking for small caps or labels.
|
||||
- **All-caps subheaders everywhere.** Try lowercase italics, sentence case, or small-caps instead.
|
||||
- **Orphaned words.** Single words sitting alone on the last line. Fix with `text-wrap: balance` or `text-wrap: pretty`.
|
||||
|
||||
### Color and Surfaces
|
||||
|
||||
- **Pure `#000000` background.** Replace with off-black, dark charcoal, or tinted dark (`#0a0a0a`, `#121212`, or a dark navy).
|
||||
- **Oversaturated accent colors.** Keep saturation below 80%. Desaturate accents so they blend with neutrals instead of screaming.
|
||||
- **More than one accent color.** Pick one. Remove the rest. Consistency beats variety.
|
||||
- **Mixing warm and cool grays.** Stick to one gray family. Tint all grays with a consistent hue (warm or cool, not both).
|
||||
- **Purple/blue "AI gradient" aesthetic.** This is the most common AI design fingerprint. Replace with neutral bases and a single, considered accent.
|
||||
- **Generic `box-shadow`.** Tint shadows to match the background hue. Use colored shadows (e.g., dark blue shadow on a blue background) instead of pure black at low opacity.
|
||||
- **Flat design with zero texture.** Add subtle noise, grain, or micro-patterns to backgrounds. Pure flat vectors feel sterile.
|
||||
- **Perfectly even gradients.** Break the uniformity with radial gradients, noise overlays, or mesh gradients instead of standard linear 45-degree fades.
|
||||
- **Inconsistent lighting direction.** Audit all shadows to ensure they suggest a single, consistent light source.
|
||||
- **Random dark sections in a light mode page (or vice versa).** A single dark-background section breaking an otherwise light page looks like a copy-paste accident. Either commit to a full dark mode or keep a consistent background tone throughout. If contrast is needed, use a slightly darker shade of the same palette — not a sudden jump to `#111` in the middle of a cream page.
|
||||
- **Empty, flat sections with no visual depth.** Sections that are just text on a plain background feel unfinished. Add high-quality background imagery (blurred, overlaid, or masked), subtle patterns, or ambient gradients. Use reliable placeholder sources like `https://picsum.photos/seed/{name}/1920/1080` when real assets are not available. Experiment with background images behind hero sections, feature blocks, or CTAs — even a subtle full-width photo at low opacity adds presence.
|
||||
|
||||
### Layout
|
||||
|
||||
- **Everything centered and symmetrical.** Break symmetry with offset margins, mixed aspect ratios, or left-aligned headers over centered content.
|
||||
- **Three equal card columns as feature row.** This is the most generic AI layout. Replace with a 2-column zig-zag, asymmetric grid, horizontal scroll, or masonry layout.
|
||||
- **Using `height: 100vh` for full-screen sections.** Replace with `min-height: 100dvh` to prevent layout jumping on mobile browsers (iOS Safari viewport bug).
|
||||
- **Complex flexbox percentage math.** Replace with CSS Grid for reliable multi-column structures.
|
||||
- **No max-width container.** Add a container constraint (around 1200-1440px) with auto margins so content doesn't stretch edge-to-edge on wide screens.
|
||||
- **Cards of equal height forced by flexbox.** Allow variable heights or use masonry when content varies in length.
|
||||
- **Uniform border-radius on everything.** Vary the radius: tighter on inner elements, softer on containers.
|
||||
- **No overlap or depth.** Elements sit flat next to each other. Use negative margins to create layering and visual depth.
|
||||
- **Symmetrical vertical padding.** Top and bottom padding are always identical. Adjust optically — bottom padding often needs to be slightly larger.
|
||||
- **Dashboard always has a left sidebar.** Try top navigation, a floating command menu, or a collapsible panel instead.
|
||||
- **Missing whitespace.** Double the spacing. Let the design breathe. Dense layouts work for data dashboards, not for marketing pages.
|
||||
- **Buttons not bottom-aligned in card groups.** When cards have different content lengths, CTAs end up at random heights. Pin buttons to the bottom of each card so they form a clean horizontal line regardless of content above.
|
||||
- **Feature lists starting at different vertical positions.** In pricing tables or comparison cards, the list of features should start at the same Y position across all columns. Use consistent spacing above the list or fixed-height title/price blocks.
|
||||
- **Inconsistent vertical rhythm in side-by-side elements.** When placing cards, columns, or panels next to each other, align shared elements (titles, descriptions, prices, buttons) across all items. Misaligned baselines make the layout look broken.
|
||||
- **Mathematical alignment that looks optically wrong.** Centering by the math doesn't always look centered to the eye. Icons next to text, play buttons in circles, or text in buttons often need 1-2px optical adjustments to feel right.
|
||||
|
||||
### Interactivity and States
|
||||
|
||||
- **No hover states on buttons.** Add background shift, slight scale, or translate on hover.
|
||||
- **No active/pressed feedback.** Add a subtle `scale(0.98)` or `translateY(1px)` on press to simulate a physical click.
|
||||
- **Instant transitions with zero duration.** Add smooth transitions (200-300ms) to all interactive elements.
|
||||
- **Missing focus ring.** Ensure visible focus indicators for keyboard navigation. This is an accessibility requirement, not optional.
|
||||
- **No loading states.** Replace generic circular spinners with skeleton loaders that match the layout shape.
|
||||
- **No empty states.** An empty dashboard showing nothing is a missed opportunity. Design a composed "getting started" view.
|
||||
- **No error states.** Add clear, inline error messages for forms. Do not use `window.alert()`.
|
||||
- **Dead links.** Buttons that link to `#`. Either link to real destinations or visually disable them.
|
||||
- **No indication of current page in navigation.** Style the active nav link differently so users know where they are.
|
||||
- **Scroll jumping.** Anchor clicks jump instantly. Add `scroll-behavior: smooth`.
|
||||
- **Animations using `top`, `left`, `width`, `height`.** Switch to `transform` and `opacity` for GPU-accelerated, smooth animation.
|
||||
|
||||
### Content
|
||||
|
||||
- **Generic names like "John Doe" or "Jane Smith".** Use diverse, realistic-sounding names.
|
||||
- **Fake round numbers like `99.99%`, `50%`, `$100.00`.** Use organic, messy data: `47.2%`, `$99.00`, `+1 (312) 847-1928`.
|
||||
- **Placeholder company names like "Acme Corp", "Nexus", "SmartFlow".** Invent contextual, believable brand names.
|
||||
- **AI copywriting cliches.** Never use "Elevate", "Seamless", "Unleash", "Next-Gen", "Game-changer", "Delve", "Tapestry", or "In the world of...". Write plain, specific language.
|
||||
- **Exclamation marks in success messages.** Remove them. Be confident, not loud.
|
||||
- **"Oops!" error messages.** Be direct: "Connection failed. Please try again."
|
||||
- **Passive voice.** Use active voice: "We couldn't save your changes" instead of "Mistakes were made."
|
||||
- **All blog post dates identical.** Randomize dates to appear real.
|
||||
- **Same avatar image for multiple users.** Use unique assets for every distinct person.
|
||||
- **Lorem Ipsum.** Never use placeholder latin text. Write real draft copy.
|
||||
- **Title Case On Every Header.** Use sentence case instead.
|
||||
|
||||
### Component Patterns
|
||||
|
||||
- **Generic card look (border + shadow + white background).** Remove the border, or use only background color, or use only spacing. Cards should exist only when elevation communicates hierarchy.
|
||||
- **Always one filled button + one ghost button.** Add text links or tertiary styles to reduce visual noise.
|
||||
- **Pill-shaped "New" and "Beta" badges.** Try square badges, flags, or plain text labels.
|
||||
- **Accordion FAQ sections.** Use a side-by-side list, searchable help, or inline progressive disclosure.
|
||||
- **3-card carousel testimonials with dots.** Replace with a masonry wall, embedded social posts, or a single rotating quote.
|
||||
- **Pricing table with 3 towers.** Highlight the recommended tier with color and emphasis, not just extra height.
|
||||
- **Modals for everything.** Use inline editing, slide-over panels, or expandable sections instead of popups for simple actions.
|
||||
- **Avatar circles exclusively.** Try squircles or rounded squares for a less generic look.
|
||||
- **Light/dark toggle always a sun/moon switch.** Use a dropdown, system preference detection, or integrate it into settings.
|
||||
- **Footer link farm with 4 columns.** Simplify. Focus on main navigational paths and legally required links.
|
||||
|
||||
### Iconography
|
||||
|
||||
- **Lucide or Feather icons exclusively.** These are the "default" AI icon choice. Use Phosphor, Heroicons, or a custom set for differentiation.
|
||||
- **Rocketship for "Launch", shield for "Security".** Replace cliche metaphors with less obvious icons (bolt, fingerprint, spark, vault).
|
||||
- **Inconsistent stroke widths across icons.** Audit all icons and standardize to one stroke weight.
|
||||
- **Missing favicon.** Always include a branded favicon.
|
||||
- **Stock "diverse team" photos.** Use real team photos, candid shots, or a consistent illustration style instead of uncanny stock imagery.
|
||||
|
||||
### Code Quality
|
||||
|
||||
- **Div soup.** Use semantic HTML: `<nav>`, `<main>`, `<article>`, `<aside>`, `<section>`.
|
||||
- **Inline styles mixed with CSS classes.** Move all styling to the project's styling system.
|
||||
- **Hardcoded pixel widths.** Use relative units (`%`, `rem`, `em`, `max-width`) for flexible layouts.
|
||||
- **Missing alt text on images.** Describe image content for screen readers. Never leave `alt=""` or `alt="image"` on meaningful images.
|
||||
- **Arbitrary z-index values like `9999`.** Establish a clean z-index scale in the theme/variables.
|
||||
- **Commented-out dead code.** Remove all debug artifacts before shipping.
|
||||
- **Import hallucinations.** Check that every import actually exists in `package.json` or the project dependencies.
|
||||
- **Missing meta tags.** Add proper `<title>`, `description`, `og:image`, and social sharing meta tags.
|
||||
|
||||
### Strategic Omissions (What AI Typically Forgets)
|
||||
|
||||
- **No legal links.** Add privacy policy and terms of service links in the footer.
|
||||
- **No "back" navigation.** Dead ends in user flows. Every page needs a way back.
|
||||
- **No custom 404 page.** Design a helpful, branded "page not found" experience.
|
||||
- **No form validation.** Add client-side validation for emails, required fields, and format checks.
|
||||
- **No "skip to content" link.** Essential for keyboard users. Add a hidden skip-link.
|
||||
- **No cookie consent.** If required by jurisdiction, add a compliant consent banner.
|
||||
|
||||
## Upgrade Techniques
|
||||
|
||||
When upgrading a project, pull from these high-impact techniques to replace generic patterns:
|
||||
|
||||
### Typography Upgrades
|
||||
- **Variable font animation.** Interpolate weight or width on scroll or hover for text that feels alive.
|
||||
- **Outlined-to-fill transitions.** Text starts as a stroke outline and fills with color on scroll entry or interaction.
|
||||
- **Text mask reveals.** Large typography acting as a window to video or animated imagery behind it.
|
||||
|
||||
### Layout Upgrades
|
||||
- **Broken grid / asymmetry.** Elements that deliberately ignore column structure — overlapping, bleeding off-screen, or offset with calculated randomness.
|
||||
- **Whitespace maximization.** Aggressive use of negative space to force focus on a single element.
|
||||
- **Parallax card stacks.** Sections that stick and physically stack over each other during scroll.
|
||||
- **Split-screen scroll.** Two halves of the screen sliding in opposite directions.
|
||||
|
||||
### Motion Upgrades
|
||||
- **Smooth scroll with inertia.** Decouple scrolling from browser defaults for a heavier, cinematic feel.
|
||||
- **Staggered entry.** Elements cascade in with slight delays, combining Y-axis translation with opacity fade. Never mount everything at once.
|
||||
- **Spring physics.** Replace linear easing with spring-based motion for a natural, weighty feel on all interactive elements.
|
||||
- **Scroll-driven reveals.** Content entering through expanding masks, wipes, or draw-on SVG paths tied to scroll progress.
|
||||
|
||||
### Surface Upgrades
|
||||
- **True glassmorphism.** Go beyond `backdrop-filter: blur`. Add a 1px inner border and a subtle inner shadow to simulate edge refraction.
|
||||
- **Spotlight borders.** Card borders that illuminate dynamically under the cursor.
|
||||
- **Grain and noise overlays.** A fixed, pointer-events-none overlay with subtle noise to break digital flatness.
|
||||
- **Colored, tinted shadows.** Shadows that carry the hue of the background rather than using generic black.
|
||||
|
||||
## Fix Priority
|
||||
|
||||
Apply changes in this order for maximum visual impact with minimum risk:
|
||||
|
||||
1. **Font swap** — biggest instant improvement, lowest risk
|
||||
2. **Color palette cleanup** — remove clashing or oversaturated colors
|
||||
3. **Hover and active states** — makes the interface feel alive
|
||||
4. **Layout and spacing** — proper grid, max-width, consistent padding
|
||||
5. **Replace generic components** — swap cliche patterns for modern alternatives
|
||||
6. **Add loading, empty, and error states** — makes it feel finished
|
||||
7. **Polish typography scale and spacing** — the premium final touch
|
||||
|
||||
## Rules
|
||||
|
||||
- Work with the existing tech stack. Do not migrate frameworks or styling libraries.
|
||||
- Do not break existing functionality. Test after every change.
|
||||
- Before importing any new library, check the project's dependency file first.
|
||||
- If the project uses Tailwind, check the version (v3 vs v4) before modifying config.
|
||||
- If the project has no framework, use vanilla CSS.
|
||||
- Keep changes reviewable and focused. Small, targeted improvements over big rewrites.
|
||||
@@ -23,12 +23,12 @@ one, run the checker and use its real output instead of your own
|
||||
recollection of which versions are safe.
|
||||
|
||||
```bash
|
||||
python3 <skill_dir>/check_deps.py <package>@<version>:<ecosystem>
|
||||
python3 <skill_dir>/check_deps.py path/to/requirements.txt
|
||||
python3 <skill_dir>/../../check_deps.py <package>@<version>:<ecosystem>
|
||||
python3 <skill_dir>/../../check_deps.py path/to/requirements.txt
|
||||
```
|
||||
|
||||
(`<skill_dir>` is this file's directory; the script is vendored next to this
|
||||
file, as `check_deps.py`. Ecosystems: `npm`, `PyPI`, `Go`, `crates.io`,
|
||||
(`<skill_dir>` is this file's directory; the script lives at the repo/plugin
|
||||
root as `check_deps.py`. Ecosystems: `npm`, `PyPI`, `Go`, `crates.io`,
|
||||
`Maven`, etc. -- omit `:ecosystem` and it defaults to `npm`.)
|
||||
|
||||
The script prints one of two things per package, straight from OSV.dev:
|
||||
@@ -42,11 +42,6 @@ VULN npm:[email protected] -- 2 advisory(ies):
|
||||
|
||||
## What to do with the output
|
||||
|
||||
Exit code is `0` when every package is clean, `1` when at least one advisory
|
||||
was found, and `2` when OSV.dev was unreachable -- `1` is a finding, not a
|
||||
crash. (The manifest mode only accepts files named `requirements.txt`,
|
||||
`package.json`, `package-lock.json` or `go.mod`.)
|
||||
|
||||
1. If a package you're about to add/bump shows `VULN`, do not use that
|
||||
version. Use the version named in `fix:` instead (or newer).
|
||||
2. If no fixed version is listed, tell the user there's an open advisory
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
# Play: Software Composition Analysis Audit
|
||||
|
||||
Scan project dependencies for known vulnerabilities (CVEs), produce severity-ranked findings with remediation guidance.
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
Use this skill when:
|
||||
- Reviewing a project's dependency files (package.json, requirements.txt, go.mod, pom.xml, Gemfile, Cargo.toml, etc.)
|
||||
- A user asks to check for vulnerable dependencies
|
||||
- Performing a pre-deployment security review
|
||||
- Triaging a Dependabot/Renovate alert
|
||||
|
||||
## Inputs
|
||||
|
||||
- Project root directory (must contain dependency manifests and/or lockfiles)
|
||||
- (Optional) Severity threshold — minimum severity to report (default: MEDIUM)
|
||||
- (Optional) Ignore list — CVEs to suppress (with documented justification)
|
||||
|
||||
## Procedure
|
||||
|
||||
### 1. Identify Dependency Manifests
|
||||
|
||||
Scan for all dependency files in the project:
|
||||
|
||||
| Ecosystem | Manifest | Lockfile |
|
||||
|-----------|----------|----------|
|
||||
| Node.js | package.json | package-lock.json, yarn.lock, pnpm-lock.yaml |
|
||||
| Python | requirements.txt, setup.py, pyproject.toml | Pipfile.lock, poetry.lock |
|
||||
| Go | go.mod | go.sum |
|
||||
| Java/Kotlin | pom.xml, build.gradle | gradle.lockfile |
|
||||
| Ruby | Gemfile | Gemfile.lock |
|
||||
| Rust | Cargo.toml | Cargo.lock |
|
||||
| .NET | *.csproj, packages.config | packages.lock.json |
|
||||
| PHP | composer.json | composer.lock |
|
||||
|
||||
Prefer lockfiles over manifests — they capture the exact resolved versions including transitive dependencies.
|
||||
|
||||
### 2. Run Vulnerability Scan
|
||||
|
||||
Use available tools in order of preference:
|
||||
|
||||
1. **osv-scanner** (recommended — uses OSV.dev database, covers all ecosystems):
|
||||
```bash
|
||||
osv-scanner --lockfile=<lockfile_path> --format=json
|
||||
```
|
||||
|
||||
2. **npm audit** (Node.js projects):
|
||||
```bash
|
||||
npm audit --json
|
||||
```
|
||||
|
||||
3. **pip-audit** (Python projects):
|
||||
```bash
|
||||
pip-audit -r requirements.txt --format=json
|
||||
```
|
||||
|
||||
4. **govulncheck** (Go projects):
|
||||
```bash
|
||||
govulncheck ./...
|
||||
```
|
||||
|
||||
5. **trivy** (multi-ecosystem, container images too):
|
||||
```bash
|
||||
trivy fs --format json --scanners vuln <project_path>
|
||||
```
|
||||
|
||||
If no scanner is installed, **stop and ask the user to install one** rather than attempting manual analysis. Even small projects have 50+ dependencies — manually cross-referencing versions against CVE databases is not cost-effective. Recommend:
|
||||
```bash
|
||||
# Fastest to install, covers all ecosystems
|
||||
brew install osv-scanner # or: npm install -g osv-scanner
|
||||
# Node.js projects already have this
|
||||
npm audit --json
|
||||
```
|
||||
|
||||
For manual triage of individual packages, use the [OSV.dev vulnerability database](https://osv.dev/list) to search by package name or CVE.
|
||||
|
||||
### 3. Analyze Results
|
||||
|
||||
For each vulnerability found:
|
||||
|
||||
1. **Determine reachability**: Is the vulnerable code path actually used by this project?
|
||||
- Check import statements and usage patterns
|
||||
- A vulnerability in an unused function of a dependency is lower priority
|
||||
2. **Check exploitability context**: Is the vulnerability exploitable in this deployment?
|
||||
- A client-side library vuln in a server-only project may not apply
|
||||
- A DoS vulnerability may be less critical for internal tools
|
||||
3. **Identify fix availability**: Is there a patched version?
|
||||
- Check if upgrading is a minor/patch bump (low risk) or major bump (breaking changes)
|
||||
- Check if the fix is in a direct dependency or requires bumping a transitive dep
|
||||
|
||||
### 4. Produce Findings
|
||||
|
||||
For each confirmed vulnerability, generate a finding with:
|
||||
- CVE/GHSA identifier
|
||||
- Affected package and version
|
||||
- Fixed version (if available)
|
||||
- CVSS score and severity
|
||||
- Whether the vulnerable code path is reachable
|
||||
- Upgrade command to fix
|
||||
|
||||
### 5. Generate Dependency Health Summary
|
||||
|
||||
Beyond CVEs, flag:
|
||||
- **Unmaintained packages**: No commits in 2+ years, archived repos
|
||||
- **Typosquatting risk**: Package names that are suspiciously similar to popular packages
|
||||
- **License concerns**: Packages with no license or unexpected license changes
|
||||
- **Pinning issues**: Using ranges (^, ~) instead of exact versions in production lockfiles
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## SCA Audit: [Project Name]
|
||||
|
||||
### Scan Summary
|
||||
- **Ecosystems**: [Node.js, Python, etc.]
|
||||
- **Total dependencies**: [N direct, M transitive]
|
||||
- **Scanner used**: [tool name and version]
|
||||
- **Vulnerabilities found**: [X critical, Y high, Z medium, W low]
|
||||
|
||||
### Critical & High Findings
|
||||
[Standard finding template for each, sorted by severity]
|
||||
|
||||
### Medium & Low Findings
|
||||
[Condensed table format]
|
||||
| Package | Version | CVE | Severity | Fixed In | Reachable? |
|
||||
|---------|---------|-----|----------|----------|------------|
|
||||
|
||||
### Dependency Health
|
||||
[Flags for unmaintained, license, pinning issues]
|
||||
|
||||
### Remediation Commands
|
||||
[Exact commands to fix — e.g., `npm install package@version`]
|
||||
```
|
||||
|
||||
## OWASP References
|
||||
|
||||
- **A06:2021** — Vulnerable and Outdated Components
|
||||
- OWASP Dependency-Check
|
||||
- OWASP Software Component Verification Standard (SCVS)
|
||||
- OWASP Cheat Sheet: Vulnerable Dependency Management
|
||||
@@ -1,48 +0,0 @@
|
||||
# Finding Template
|
||||
|
||||
Use this structure for all security findings produced by skills.
|
||||
|
||||
```markdown
|
||||
### [SEVERITY] Title
|
||||
|
||||
- **ID**: NDC-YYYY-NNN (auto-incrementing)
|
||||
- **CWE**: CWE-XXX (if applicable)
|
||||
- **CVE**: CVE-YYYY-NNNNN (if applicable)
|
||||
- **OpenCRE**: [CRE-ID](https://www.opencre.org/cre/CRE-ID) — requirement name (see data/opencre/)
|
||||
- **OWASP Ref**: Top 10 A01, ASVS V4.1.1, MASVS-STORAGE-1, MASWE-0001, MASTG-TEST-0200, LLM01, etc.
|
||||
- **Location**: file_path:line_number or component name
|
||||
- **Impact**: What an attacker can achieve (1-2 sentences)
|
||||
- **Evidence**: Code snippet, command output, or proof-of-concept
|
||||
- **Remediation**: Specific fix with code example
|
||||
- **Confidence**: HIGH | MEDIUM | LOW (how certain is this finding)
|
||||
```
|
||||
|
||||
### Resolving OpenCRE Links
|
||||
|
||||
When you have a CWE, look up its OpenCRE mapping via the API:
|
||||
|
||||
```
|
||||
GET https://www.opencre.org/rest/v1/standard/CWE/sectionid/{cwe-number}
|
||||
```
|
||||
|
||||
This returns the CRE ID and linked standards (ASVS, WSTG, NIST 800-53, cheat sheets). Include the CRE link in findings to give readers one-click access to all related standards and remediation guidance.
|
||||
|
||||
Common mappings are pre-populated in `data/opencre/README.md`.
|
||||
|
||||
## Severity Definitions
|
||||
|
||||
| Severity | CVSS Range | Criteria |
|
||||
|----------|-----------|----------|
|
||||
| CRITICAL | 9.0-10.0 | Remote code execution, full system compromise, mass data breach |
|
||||
| HIGH | 7.0-8.9 | Privilege escalation, significant data exposure, auth bypass |
|
||||
| MEDIUM | 4.0-6.9 | Limited data exposure, requires user interaction or specific conditions |
|
||||
| LOW | 0.1-3.9 | Information disclosure, minor misconfigurations |
|
||||
| INFORMATIONAL | N/A | Best practice recommendations, defense-in-depth suggestions |
|
||||
|
||||
## Confidence Levels
|
||||
|
||||
| Level | Meaning |
|
||||
|-------|---------|
|
||||
| HIGH | Confirmed via code path analysis, PoC, or tool output |
|
||||
| MEDIUM | Strong indicators but not fully confirmed — needs manual verification |
|
||||
| LOW | Heuristic match or pattern-based — may be false positive |
|
||||
@@ -1,147 +0,0 @@
|
||||
# Play: Secrets Scan
|
||||
|
||||
Detect hardcoded credentials, API keys, tokens, and other secrets in source code and configuration files.
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
Use this skill when:
|
||||
- Reviewing code for leaked secrets before commit or merge
|
||||
- Auditing a repository for historical secret exposure
|
||||
- A user asks to check for hardcoded credentials
|
||||
- Setting up pre-commit hooks for secret detection
|
||||
|
||||
## Inputs
|
||||
|
||||
- Repository or directory to scan
|
||||
- (Optional) List of known-safe patterns to ignore (e.g., test fixtures, example placeholders)
|
||||
- (Optional) Whether to scan git history or just current files
|
||||
|
||||
## Procedure
|
||||
|
||||
### 1. Run Automated Scanner
|
||||
|
||||
Use available tools in order of preference:
|
||||
|
||||
1. **trufflehog** (recommended — scans current files + git history):
|
||||
```bash
|
||||
trufflehog filesystem --directory=<path> --json
|
||||
trufflehog git file://<repo_path> --json # includes git history
|
||||
```
|
||||
|
||||
2. **gitleaks** (git-aware scanning):
|
||||
```bash
|
||||
gitleaks detect --source=<path> --report-format=json --report-path=results.json
|
||||
```
|
||||
|
||||
3. **detect-secrets** (Yelp's baseline-aware scanner):
|
||||
```bash
|
||||
detect-secrets scan <path> --all-files
|
||||
```
|
||||
|
||||
If no scanner is installed, proceed with manual analysis.
|
||||
|
||||
### 2. Manual Pattern Analysis
|
||||
|
||||
Search for these high-confidence patterns:
|
||||
|
||||
#### API Keys & Tokens
|
||||
| Pattern | Service | Regex Hint |
|
||||
|---------|---------|-----------|
|
||||
| `AKIA[0-9A-Z]{16}` | AWS Access Key ID | Exact format |
|
||||
| `sk-[a-zA-Z0-9]{48}` | OpenAI API Key | Prefix match |
|
||||
| `sk-ant-[a-zA-Z0-9-]{80,}` | Anthropic API Key | Prefix match |
|
||||
| `ghp_[a-zA-Z0-9]{36}` | GitHub Personal Access Token | Prefix match |
|
||||
| `gho_`, `ghu_`, `ghs_`, `ghr_` | GitHub OAuth/App tokens | Prefix match |
|
||||
| `xoxb-`, `xoxp-`, `xoxs-` | Slack tokens | Prefix match |
|
||||
| `SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}` | SendGrid API Key | Format match |
|
||||
| `sk_live_`, `pk_live_`, `rk_live_` | Stripe keys | Prefix match |
|
||||
|
||||
#### Credentials in Config
|
||||
| Pattern | What to Look For |
|
||||
|---------|-----------------|
|
||||
| Connection strings | `://user:password@host` patterns |
|
||||
| Password assignments | `password = "..."`, `PASSWORD: ...`, `passwd`, `secret` |
|
||||
| Private keys | `-----BEGIN (RSA\|EC\|OPENSSH) PRIVATE KEY-----` |
|
||||
| Certificates | PEM-encoded certificates with private key material |
|
||||
| JWT secrets | `JWT_SECRET`, `TOKEN_SECRET` assignments |
|
||||
| Database credentials | `DB_PASSWORD`, `MONGO_URI`, `DATABASE_URL` with embedded credentials |
|
||||
|
||||
#### High-Risk File Patterns
|
||||
Scan these files with extra scrutiny:
|
||||
- `.env`, `.env.*` files
|
||||
- `**/config/**`, `**/settings/**`
|
||||
- `docker-compose*.yml` (environment sections)
|
||||
- `*.tfvars`, `terraform.tfstate`
|
||||
- `kubeconfig`, `*.kubeconfig`
|
||||
- `credentials`, `credentials.json`, `service-account*.json`
|
||||
- `.npmrc`, `.pypirc`, `pip.conf` (registry tokens)
|
||||
- `**/ci/**`, `.github/workflows/**` (inline secrets)
|
||||
|
||||
### 3. Contextual Analysis
|
||||
|
||||
For each detected secret:
|
||||
|
||||
1. **Is it real?** Filter out:
|
||||
- Placeholder values (`CHANGE_ME`, `xxx`, `your-api-key-here`, `<token>`)
|
||||
- Test fixtures with known dummy values
|
||||
- Documentation examples
|
||||
- Base64-encoded non-secret data that matches key patterns
|
||||
|
||||
2. **Is it active?** If possible:
|
||||
- Check if the key format is currently valid for the service
|
||||
- Look for rotation evidence (was it replaced in a later commit?)
|
||||
- Check `.gitignore` — is the file supposed to be excluded?
|
||||
|
||||
3. **What's the blast radius?**
|
||||
- What service does this credential access?
|
||||
- What permissions does it grant?
|
||||
- Is it a production or development credential?
|
||||
- How long has it been exposed?
|
||||
|
||||
### 4. Check Preventive Controls
|
||||
|
||||
Verify that the project has:
|
||||
- [ ] `.gitignore` entries for sensitive files (`.env`, `*.key`, `*.pem`, tfstate, etc.)
|
||||
- [ ] Pre-commit hooks for secret scanning (trufflehog, gitleaks, detect-secrets)
|
||||
- [ ] CI pipeline secret scanning step
|
||||
- [ ] Documentation on how to manage secrets (env vars, vault, etc.)
|
||||
- [ ] `.gitleaks.toml` or equivalent allowlist config for known false positives
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Secrets Scan: [Repository/Directory]
|
||||
|
||||
### Scan Summary
|
||||
- **Scanner used**: [tool or manual]
|
||||
- **Scope**: Current files | Git history | Both
|
||||
- **Files scanned**: [count]
|
||||
- **Secrets found**: [count by category]
|
||||
|
||||
### Findings
|
||||
[Standard finding template for each secret, with type and blast radius]
|
||||
|
||||
### Preventive Controls
|
||||
| Control | Status |
|
||||
|---------|--------|
|
||||
| .gitignore covers sensitive files | Present/Missing |
|
||||
| Pre-commit secret scanning | Present/Missing |
|
||||
| CI secret scanning | Present/Missing |
|
||||
|
||||
### Immediate Actions Required
|
||||
[If active production secrets are found, list rotation steps]
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Never include the actual secret value in findings.** Show a redacted version: `AKIA****EXAMPLE`
|
||||
- If active production secrets are found, flag this as requiring **immediate rotation** — the secret must be assumed compromised once it's been in version control
|
||||
- Historical secrets in git history remain accessible even after deletion from current files — git history rewriting or rotation is required
|
||||
|
||||
## OWASP References
|
||||
|
||||
- **A07:2021** — Identification and Authentication Failures
|
||||
- **CWE-798** — Use of Hard-coded Credentials
|
||||
- **CWE-312** — Cleartext Storage of Sensitive Information
|
||||
- OWASP Cheat Sheet: Secrets Management
|
||||
- OWASP WSTG-CONF-04: Review Old Backup and Unreferenced Files for Sensitive Information
|
||||
@@ -1,48 +0,0 @@
|
||||
# Finding Template
|
||||
|
||||
Use this structure for all security findings produced by skills.
|
||||
|
||||
```markdown
|
||||
### [SEVERITY] Title
|
||||
|
||||
- **ID**: NDC-YYYY-NNN (auto-incrementing)
|
||||
- **CWE**: CWE-XXX (if applicable)
|
||||
- **CVE**: CVE-YYYY-NNNNN (if applicable)
|
||||
- **OpenCRE**: [CRE-ID](https://www.opencre.org/cre/CRE-ID) — requirement name (see data/opencre/)
|
||||
- **OWASP Ref**: Top 10 A01, ASVS V4.1.1, MASVS-STORAGE-1, MASWE-0001, MASTG-TEST-0200, LLM01, etc.
|
||||
- **Location**: file_path:line_number or component name
|
||||
- **Impact**: What an attacker can achieve (1-2 sentences)
|
||||
- **Evidence**: Code snippet, command output, or proof-of-concept
|
||||
- **Remediation**: Specific fix with code example
|
||||
- **Confidence**: HIGH | MEDIUM | LOW (how certain is this finding)
|
||||
```
|
||||
|
||||
### Resolving OpenCRE Links
|
||||
|
||||
When you have a CWE, look up its OpenCRE mapping via the API:
|
||||
|
||||
```
|
||||
GET https://www.opencre.org/rest/v1/standard/CWE/sectionid/{cwe-number}
|
||||
```
|
||||
|
||||
This returns the CRE ID and linked standards (ASVS, WSTG, NIST 800-53, cheat sheets). Include the CRE link in findings to give readers one-click access to all related standards and remediation guidance.
|
||||
|
||||
Common mappings are pre-populated in `data/opencre/README.md`.
|
||||
|
||||
## Severity Definitions
|
||||
|
||||
| Severity | CVSS Range | Criteria |
|
||||
|----------|-----------|----------|
|
||||
| CRITICAL | 9.0-10.0 | Remote code execution, full system compromise, mass data breach |
|
||||
| HIGH | 7.0-8.9 | Privilege escalation, significant data exposure, auth bypass |
|
||||
| MEDIUM | 4.0-6.9 | Limited data exposure, requires user interaction or specific conditions |
|
||||
| LOW | 0.1-3.9 | Information disclosure, minor misconfigurations |
|
||||
| INFORMATIONAL | N/A | Best practice recommendations, defense-in-depth suggestions |
|
||||
|
||||
## Confidence Levels
|
||||
|
||||
| Level | Meaning |
|
||||
|-------|---------|
|
||||
| HIGH | Confirmed via code path analysis, PoC, or tool output |
|
||||
| MEDIUM | Strong indicators but not fully confirmed — needs manual verification |
|
||||
| LOW | Heuristic match or pattern-based — may be false positive |
|
||||
@@ -1,374 +0,0 @@
|
||||
# Play: OWASP Top 10 Web Security Review
|
||||
|
||||
Review web applications against the OWASP Top 10 for Web Applications (2021), producing actionable findings with severity ratings and remediation guidance.
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
Use this play when:
|
||||
- Auditing a web application (monolith, microservices, or serverless)
|
||||
- Reviewing server-side web code for security vulnerabilities
|
||||
- A user asks for an "OWASP Top 10 review" or "web security assessment"
|
||||
- Assessing a web framework or CMS for security posture
|
||||
- Performing a pre-deployment security review of a web app
|
||||
|
||||
## Inputs
|
||||
|
||||
- Web application source code (frontend and backend)
|
||||
- Framework/language information (React, Django, Rails, etc.)
|
||||
- Deployment context (internet-facing, internal, cloud platform)
|
||||
- Authentication/authorization implementation details
|
||||
- (Optional) Previous security assessments or pentest reports
|
||||
|
||||
## Procedure
|
||||
|
||||
### 1. Application Mapping
|
||||
|
||||
Before assessing risks, understand the application:
|
||||
|
||||
| Aspect | Questions |
|
||||
|--------|-----------|
|
||||
| **Framework** | What language/framework? (Node.js/Express, Python/Django, Ruby/Rails, Java/Spring, PHP/Laravel, etc.) |
|
||||
| **Architecture** | Monolith, microservices, serverless (Lambda/Cloud Functions), SPA vs SSR? |
|
||||
| **Trust Boundary** | Internet-facing, internal network, or local-only? |
|
||||
| **Data Sensitivity** | Does it handle PII, credentials, financial data, health data (HIPAA)? |
|
||||
| **Authentication** | Session-based, JWT, OAuth, SSO, API keys? |
|
||||
| **Data Stores** | SQL database, NoSQL, cache (Redis), file storage, external APIs? |
|
||||
|
||||
Document the attack surface: entry points, data flows, trust boundaries.
|
||||
|
||||
### 2. Assess Each OWASP Top 10 Risk
|
||||
|
||||
#### A01: Broken Access Control
|
||||
|
||||
> CWE-200, CWE-201, CWE-352, CWE-639
|
||||
|
||||
**What to Look For:**
|
||||
|
||||
- [ ] **Missing authorization checks** — Endpoints or functions accessible without verifying user permissions
|
||||
- [ ] **IDOR (Insecure Direct Object References)** — Can User A access User B's data by changing IDs in URLs/parameters?
|
||||
- [ ] **Privilege escalation** — Can regular users access admin functions? Can users elevate their own privileges?
|
||||
- [ ] **Path traversal** — Can file paths include `../` to escape intended directories?
|
||||
- [ ] **CORS misconfiguration** — Overly permissive CORS allowing cross-origin attacks
|
||||
- [ ] **Forced browsing** — Can unauthenticated users access protected resources by guessing URLs?
|
||||
- [ ] **JWT token issues** — Missing signature validation, algorithm confusion attacks
|
||||
- [ ] **Broken function-level authorization** — Different auth requirements for UI vs API endpoints
|
||||
|
||||
**Testing Approach:**
|
||||
1. Map all endpoints requiring authentication
|
||||
2. Test each without authentication (should fail)
|
||||
3. Test cross-user access (User A accessing User B's resources)
|
||||
4. Test privilege escalation paths (user → admin)
|
||||
5. Verify authorization on every object access, not just initial authentication
|
||||
|
||||
---
|
||||
|
||||
#### A02: Cryptographic Failures
|
||||
|
||||
> CWE-261, CWE-296, CWE-310, CWE-319, CWE-326, CWE-327, CWE-916
|
||||
|
||||
**What to Look For:**
|
||||
|
||||
- [ ] **Weak algorithms** — MD5, SHA1 for passwords; DES, RC4 for encryption; ECB mode
|
||||
- [ ] **Missing TLS** — HTTP instead of HTTPS, weak TLS versions (<1.2), insecure ciphers
|
||||
- [ ] **Hardcoded credentials/keys** — API keys, database passwords, encryption keys in source code
|
||||
- [ ] **Improper key management** — Keys stored in version control, no key rotation, weak key generation
|
||||
- [ ] **Cleartext transmission** — Sensitive data sent over unencrypted channels
|
||||
- [ ] **Cleartext storage** — Passwords, tokens, or PII stored without encryption
|
||||
- [ ] **Insufficient randomness** — Predictable tokens, session IDs, or nonces
|
||||
- [ ] **Missing integrity checks** — Unsigned or unvalidated data that could be tampered with
|
||||
|
||||
**Testing Approach:**
|
||||
1. Search for hardcoded secrets using patterns (see secrets-scan.md)
|
||||
2. Verify TLS configuration (certificates, protocol versions, cipher suites)
|
||||
3. Check password storage (should use bcrypt, Argon2, or PBKDF2)
|
||||
4. Validate encryption implementations (no custom crypto)
|
||||
5. Test for sensitive data in URLs, headers, and logs
|
||||
|
||||
---
|
||||
|
||||
#### A03: Injection
|
||||
|
||||
> CWE-74, CWE-78, CWE-79, CWE-89, CWE-91, CWE-564, CWE-943
|
||||
|
||||
**What to Look For:**
|
||||
|
||||
| Injection Type | Indicators | Impact |
|
||||
|---------------|------------|---------|
|
||||
| **SQL Injection** | String concatenation in SQL, `cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")`, dynamic queries | Data exfiltration, authentication bypass, RCE |
|
||||
| **NoSQL Injection** | Unsanitized JSON in MongoDB queries, `$where` clauses with user input | Data manipulation, authentication bypass |
|
||||
| **OS Command Injection** | `exec()`, `system()`, `subprocess.call()` with user input | Remote code execution |
|
||||
| **LDAP Injection** | User input in LDAP filters without escaping | Authentication bypass, data access |
|
||||
| **XSS (Cross-Site Scripting)** | User input rendered without encoding, `innerHTML`, `dangerouslySetInnerHTML`, unescaped template output | Session hijacking, credential theft, defacement |
|
||||
| **SSTI (Server-Side Template Injection)** | User input in Jinja2, ERB, EJS templates | RCE, file access |
|
||||
| **XPath Injection** | User input in XPath queries | XML data access |
|
||||
| **Log Injection** | CRLF in log entries (`\n\r`), log forging | Log tampering, data exfiltration |
|
||||
| **Header Injection** | CRLF in HTTP headers | Response splitting, XSS |
|
||||
|
||||
**Testing Approach:**
|
||||
1. Search for dangerous functions (concatenation, eval, exec, innerHTML)
|
||||
2. Test each input field with injection payloads
|
||||
3. Verify output encoding for all user-controlled data
|
||||
4. Check prepared statements/parameterized queries usage
|
||||
5. Validate Content Security Policy (CSP) headers for XSS mitigation
|
||||
|
||||
---
|
||||
|
||||
#### A04: Insecure Design
|
||||
|
||||
> CWE-73, CWE-183, CWE-209, CWE-213, CWE-235, CWE-256, CWE-257, CWE-266, CWE-269, CWE-280, CWE-434, CWE-444, CWE-501, CWE-522, CWE-697, CWE-799, CWE-807, CWE-840, CWE-841, CWE-927, CWE-1021, CWE-1173, CWE-1174, CWE-1233
|
||||
|
||||
**What to Look For:**
|
||||
|
||||
- [ ] **Missing threat modeling** — No documented attack surface, trust boundaries, or threat actors
|
||||
- [ ] **Business logic flaws** — Workflow bypasses (skip payment step), race conditions (TOCTOU), price manipulation
|
||||
- [ ] **Insecure workflows** — Missing confirmation steps for destructive actions, no CSRF protection on state changes
|
||||
- [ ] **Lack of rate limiting** — Brute force vulnerabilities on login, password reset, or sensitive operations
|
||||
- [ ] **Insufficient validation** — Client-side only validation, missing server-side checks
|
||||
- [ ] **Insecure defaults** — Debug mode enabled, verbose errors in production, default credentials
|
||||
- [ ] **Trust without verification** — Trusting client-provided data (prices, user IDs, permissions)
|
||||
- [ ] **Missing defense in depth** — Single point of failure for security controls
|
||||
|
||||
**Testing Approach:**
|
||||
1. Review architecture documentation for security considerations
|
||||
2. Test business logic flows for bypass opportunities
|
||||
3. Check for race conditions in multi-step processes
|
||||
4. Verify rate limiting on sensitive endpoints
|
||||
5. Ensure all validation happens server-side
|
||||
|
||||
---
|
||||
|
||||
#### A05: Security Misconfiguration
|
||||
|
||||
> CWE-2, CWE-11, CWE-13, CWE-15, CWE-16, CWE-209, CWE-250, CWE-260, CWE-312, CWE-497, CWE-538, CWE-540, CWE-541, CWE-547, CWE-611, CWE-614, CWE-756, CWE-942
|
||||
|
||||
**What to Look For:**
|
||||
|
||||
- [ ] **Default configurations** — Default passwords, default admin accounts, sample applications deployed
|
||||
- [ ] **Verbose error messages** — Stack traces, internal paths, SQL errors exposed to users
|
||||
- [ ] **Missing security headers** — No `X-Content-Type-Options`, `X-Frame-Options`, `Content-Security-Policy`, `Strict-Transport-Security`
|
||||
- [ ] **Unnecessary features** — Unused ports open, unnecessary services running, debug endpoints enabled
|
||||
- [ ] **Directory listing** — Web servers showing directory contents
|
||||
- [ ] **Outdated software** — Old framework versions, unpatched components
|
||||
- [ ] **Cloud misconfigurations** — Exposed S3 buckets, open security groups, public databases
|
||||
- [ ] **Missing hardening** — Default SSL/TLS settings, unnecessary HTTP methods enabled
|
||||
|
||||
**Testing Approach:**
|
||||
1. Review configuration files (nginx.conf, .htaccess, application.yml)
|
||||
2. Check for default credentials and accounts
|
||||
3. Verify security headers using tools or manual inspection
|
||||
4. Test error handling (trigger errors, check responses)
|
||||
5. Scan for unnecessary open ports and services
|
||||
6. Check cloud configurations for public exposure
|
||||
|
||||
---
|
||||
|
||||
#### A06: Vulnerable and Outdated Components
|
||||
|
||||
> CWE-937, CWE-1035, CWE-1104
|
||||
|
||||
**What to Look For:**
|
||||
|
||||
- [ ] **Unpatched dependencies** — Known CVEs in libraries (see sca-audit.md)
|
||||
- [ ] **Unsupported software** — End-of-life frameworks, operating systems, databases
|
||||
- [ ] **Missing SBOM** — No inventory of software components
|
||||
- [ ] **Unused dependencies** — Dead code increasing attack surface
|
||||
- [ ] **Transitive vulnerabilities** — Vulnerabilities in dependencies of dependencies
|
||||
- [ ] **Supply chain risks** — Typosquatting, malicious packages, compromised dependencies
|
||||
|
||||
**Testing Approach:**
|
||||
1. Run SCA scanner (osv-scanner, npm audit, pip-audit)
|
||||
2. Review dependency files (package.json, requirements.txt, etc.)
|
||||
3. Check for end-of-life components
|
||||
4. Verify dependency update processes
|
||||
5. Review lockfiles for reproducible builds
|
||||
|
||||
---
|
||||
|
||||
#### A07: Identification and Authentication Failures
|
||||
|
||||
> CWE-287, CWE-288, CWE-290, CWE-294, CWE-295, CWE-297, CWE-300, CWE-302, CWE-304, CWE-306, CWE-307, CWE-346, CWE-384, CWE-521, CWE-613, CWE-620, CWE-640, CWE-798, CWE-940, CWE-1216
|
||||
|
||||
**What to Look For:**
|
||||
|
||||
- [ ] **Weak password policy** — No minimum length, no complexity requirements, common passwords allowed
|
||||
- [ ] **Credential stuffing** — No protection against automated login attempts using breached credentials
|
||||
- [ ] **Brute force** — No rate limiting or account lockout on authentication endpoints
|
||||
- [ ] **Session vulnerabilities** — Predictable session IDs, session fixation, missing HttpOnly/Secure flags
|
||||
- [ ] **Missing MFA** — No multi-factor authentication for sensitive accounts
|
||||
- [ ] **Weak session management** — Sessions don't expire, no concurrent session limits
|
||||
- [ ] **Insecure password recovery** — Weak reset tokens, predictable questions, unverified email changes
|
||||
- [ ] **JWT issues** — None algorithm allowed, weak signing keys, missing expiration
|
||||
|
||||
**Testing Approach:**
|
||||
1. Test password requirements and complexity
|
||||
2. Attempt credential stuffing with known breached credentials
|
||||
3. Test for brute force protection (rate limiting, CAPTCHA, lockout)
|
||||
4. Analyze session handling (cookies, tokens, expiration)
|
||||
5. Test password reset flows for security
|
||||
6. Verify MFA implementation and bypass opportunities
|
||||
|
||||
---
|
||||
|
||||
#### A08: Software and Data Integrity Failures
|
||||
|
||||
> CWE-345, CWE-353, CWE-426, CWE-494, CWE-502, CWE-565, CWE-784, CWE-829, CWE-830, CWE-915
|
||||
|
||||
**What to Look For:**
|
||||
|
||||
- [ ] **Insecure deserialization** — Deserializing untrusted data (Python pickle, Java ObjectInputStream, PHP unserialize)
|
||||
- [ ] **Unsigned updates** — Software updates without integrity verification
|
||||
- [ ] **CI/CD attacks** — Unprotected build pipelines, compromised build agents
|
||||
- [ ] **Dependency confusion** — Internal packages with names matching public packages
|
||||
- [ ] **Unsigned commits** — No signature verification on code commits
|
||||
- [ ] **Tamper-evident logs** — No integrity protection for audit logs
|
||||
- [ ] **XML external entities (XXE)** — XML parsers with external entity processing enabled
|
||||
|
||||
**Testing Approach:**
|
||||
1. Search for deserialization functions with user-controlled input
|
||||
2. Verify update mechanisms include signature verification
|
||||
3. Review CI/CD configurations for security controls
|
||||
4. Check for XXE vulnerabilities in XML parsing
|
||||
5. Verify integrity checks on critical data
|
||||
|
||||
---
|
||||
|
||||
#### A09: Security Logging and Monitoring Failures
|
||||
|
||||
> CWE-117, CWE-223, CWE-532, CWE-778, CWE-1175
|
||||
|
||||
**What to Look For:**
|
||||
|
||||
- [ ] **Missing audit logs** — No logging of authentication events, access to sensitive data, or security-critical actions
|
||||
- [ ] **Insufficient log detail** — Logs don't include enough context (who, what, when, where)
|
||||
- [ ] **Sensitive data in logs** — Passwords, tokens, PII logged in plaintext
|
||||
- [ ] **No log protection** — Logs stored without integrity checks, accessible to attackers
|
||||
- [ ] **Missing monitoring** — No alerting on suspicious activity, no SIEM integration
|
||||
- [ ] **Log injection** — Attackers can forge or tamper with log entries
|
||||
- [ ] **No incident response** — No documented procedures for security incidents
|
||||
|
||||
**Testing Approach:**
|
||||
1. Review what events are logged (authentication, authorization, data access)
|
||||
2. Check log format and content for sufficiency
|
||||
3. Search for sensitive data in log statements
|
||||
4. Verify log storage security and retention
|
||||
5. Assess monitoring and alerting capabilities
|
||||
|
||||
---
|
||||
|
||||
#### A10: Server-Side Request Forgery (SSRF)
|
||||
|
||||
> CWE-918
|
||||
|
||||
**What to Look For:**
|
||||
|
||||
- [ ] **Unvalidated URL parameters** — User-supplied URLs fetched by the server without validation
|
||||
- [ ] **Internal service access** — Can attacker reach internal services (metadata endpoints, admin panels, databases)?
|
||||
- [ ] **Cloud metadata endpoints** — Access to `169.254.169.254` (AWS, Azure, GCP metadata services)
|
||||
- [ ] **File protocol access** — Can `file://` URLs be used to read local files?
|
||||
- [ ] **Redirect following** — Server follows redirects to attacker-controlled URLs
|
||||
- [ ] **DNS rebinding** — Time-of-check vs time-of-use vulnerabilities
|
||||
- [ ] **URL parsing inconsistencies** — Differences between URL parsers (e.g., `@` handling)
|
||||
|
||||
**Testing Approach:**
|
||||
1. Identify all endpoints accepting URLs from users
|
||||
2. Test for internal service access (localhost, private IPs)
|
||||
3. Attempt cloud metadata endpoint access
|
||||
4. Test alternative protocols (file://, gopher://, dict://)
|
||||
5. Test for URL parsing bypasses (unicode, encoding, case variations)
|
||||
|
||||
---
|
||||
|
||||
### 3. Framework-Specific Checks
|
||||
|
||||
Apply additional checks based on detected framework:
|
||||
|
||||
| Framework | Key Security Checks |
|
||||
|-----------|---------------------|
|
||||
| **React/Next.js** | XSS via `dangerouslySetInnerHTML`, SSRF in `getServerSideProps`, API routes without auth |
|
||||
| **Angular** | XSS protection bypasses, unsafe `bypassSecurityTrustHtml`, HTTP client interceptors |
|
||||
| **Vue.js** | `v-html` XSS risks, server-side rendering vulnerabilities |
|
||||
| **Express.js** | Missing Helmet middleware, no rate limiting, prototype pollution |
|
||||
| **Django** | `DEBUG=True`, `SECRET_KEY` exposure, raw SQL, CSRF exemptions |
|
||||
| **Flask** | Jinja2 autoescape disabled, unsafe session handling, debug mode |
|
||||
| **Ruby on Rails** | Mass assignment, `render` with user input, SQL fragments, weak parameters |
|
||||
| **Spring Boot** | SpEL injection, actuator endpoints exposed, mass assignment, CORS issues |
|
||||
| **Laravel** | Mass assignment, raw queries, debug mode, configuration exposure |
|
||||
| **ASP.NET Core** | Razor XSS, insecure deserialization, missing anti-forgery tokens |
|
||||
|
||||
---
|
||||
|
||||
### 4. Configuration Review
|
||||
|
||||
Examine these configuration areas:
|
||||
|
||||
#### Web Server Configuration
|
||||
- **nginx/Apache**: Security headers, TLS settings, rate limiting, access controls
|
||||
- **Reverse proxy**: Header forwarding, SSL termination, upstream validation
|
||||
- **Load balancer**: Health checks, session affinity, DDoS protection
|
||||
|
||||
#### Application Configuration
|
||||
- **Environment-specific settings**: Debug mode, error handling, logging levels
|
||||
- **Security settings**: Password policies, session timeouts, CSRF protection
|
||||
- **External integrations**: API keys, webhook secrets, OAuth configs
|
||||
|
||||
#### Deployment Configuration
|
||||
- **Container security**: Non-root user, read-only filesystems, resource limits
|
||||
- **Kubernetes**: Network policies, security contexts, RBAC
|
||||
- **Cloud services**: IAM roles, security groups, encryption at rest
|
||||
|
||||
---
|
||||
|
||||
### 5. Produce Findings
|
||||
|
||||
For each identified vulnerability:
|
||||
- Assign severity based on exploitability and impact in deployment context
|
||||
- Provide concrete evidence (code locations, configuration gaps, test results)
|
||||
- Propose specific remediation steps with code examples
|
||||
- Rate confidence (HIGH, MEDIUM, LOW)
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
## Web Security Review: [Application Name]
|
||||
|
||||
### Application Overview
|
||||
- **Framework**: [React/Express, Django, Rails, etc.]
|
||||
- **Deployment**: [Internet-facing, internal, cloud platform]
|
||||
- **Architecture**: [Monolith, microservices, serverless]
|
||||
- **Authentication**: [Session-based, JWT, OAuth]
|
||||
|
||||
### Risk Matrix
|
||||
| OWASP Ref | Risk | Severity | Status |
|
||||
|-----------|------|----------|--------|
|
||||
| A01 | Broken Access Control | HIGH | Finding |
|
||||
| A02 | Cryptographic Failures | MEDIUM | Finding |
|
||||
| A03 | Injection | CRITICAL | Finding |
|
||||
| A04 | Insecure Design | LOW | Finding |
|
||||
| A05 | Security Misconfiguration | MEDIUM | Finding |
|
||||
| A06 | Vulnerable Components | HIGH | Finding |
|
||||
| A07 | Auth Failures | HIGH | Finding |
|
||||
| A08 | Integrity Failures | N/A | N/A |
|
||||
| A09 | Logging Failures | MEDIUM | Finding |
|
||||
| A10 | SSRF | N/A | N/A |
|
||||
|
||||
### Findings
|
||||
[Standard finding template for each identified risk]
|
||||
|
||||
### Positive Controls Observed
|
||||
[Security controls already in place — acknowledge good practices]
|
||||
|
||||
### Recommendations
|
||||
[Prioritized remediation roadmap]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OWASP References
|
||||
|
||||
- **OWASP Top 10 for Web Applications (2021)** — [owasp.org/Top10](https://owasp.org/Top10/)
|
||||
- **OWASP ASVS v5.0** — Application Security Verification Standard
|
||||
- **OWASP Testing Guide (WSTG)** — Web Security Testing Guide
|
||||
- **OWASP Cheat Sheet Series** — Language and framework-specific guidance
|
||||
- **OWASP Proactive Controls** — Secure development practices
|
||||
@@ -1,48 +0,0 @@
|
||||
# Finding Template
|
||||
|
||||
Use this structure for all security findings produced by skills.
|
||||
|
||||
```markdown
|
||||
### [SEVERITY] Title
|
||||
|
||||
- **ID**: NDC-YYYY-NNN (auto-incrementing)
|
||||
- **CWE**: CWE-XXX (if applicable)
|
||||
- **CVE**: CVE-YYYY-NNNNN (if applicable)
|
||||
- **OpenCRE**: [CRE-ID](https://www.opencre.org/cre/CRE-ID) — requirement name (see data/opencre/)
|
||||
- **OWASP Ref**: Top 10 A01, ASVS V4.1.1, MASVS-STORAGE-1, MASWE-0001, MASTG-TEST-0200, LLM01, etc.
|
||||
- **Location**: file_path:line_number or component name
|
||||
- **Impact**: What an attacker can achieve (1-2 sentences)
|
||||
- **Evidence**: Code snippet, command output, or proof-of-concept
|
||||
- **Remediation**: Specific fix with code example
|
||||
- **Confidence**: HIGH | MEDIUM | LOW (how certain is this finding)
|
||||
```
|
||||
|
||||
### Resolving OpenCRE Links
|
||||
|
||||
When you have a CWE, look up its OpenCRE mapping via the API:
|
||||
|
||||
```
|
||||
GET https://www.opencre.org/rest/v1/standard/CWE/sectionid/{cwe-number}
|
||||
```
|
||||
|
||||
This returns the CRE ID and linked standards (ASVS, WSTG, NIST 800-53, cheat sheets). Include the CRE link in findings to give readers one-click access to all related standards and remediation guidance.
|
||||
|
||||
Common mappings are pre-populated in `data/opencre/README.md`.
|
||||
|
||||
## Severity Definitions
|
||||
|
||||
| Severity | CVSS Range | Criteria |
|
||||
|----------|-----------|----------|
|
||||
| CRITICAL | 9.0-10.0 | Remote code execution, full system compromise, mass data breach |
|
||||
| HIGH | 7.0-8.9 | Privilege escalation, significant data exposure, auth bypass |
|
||||
| MEDIUM | 4.0-6.9 | Limited data exposure, requires user interaction or specific conditions |
|
||||
| LOW | 0.1-3.9 | Information disclosure, minor misconfigurations |
|
||||
| INFORMATIONAL | N/A | Best practice recommendations, defense-in-depth suggestions |
|
||||
|
||||
## Confidence Levels
|
||||
|
||||
| Level | Meaning |
|
||||
|-------|---------|
|
||||
| HIGH | Confirmed via code path analysis, PoC, or tool output |
|
||||
| MEDIUM | Strong indicators but not fully confirmed — needs manual verification |
|
||||
| LOW | Heuristic match or pattern-based — may be false positive |
|
||||
@@ -1,107 +0,0 @@
|
||||
---
|
||||
name: wf-architecture
|
||||
description: Use para decisões de arquitetura - desenho de sistema ou serviço novo, escolha de stack, mudança de fronteira entre módulos, migração com dado existente, troca de auth, diagrama C4, ADR, threat model, "isso é uma boa ideia arquitetonicamente?". É o workflow do nível 3 do dev-router (e do nível 2 quando surge uma decisão difícil de desfazer). Não use para bug, refactor local, tela ou endpoint isolado sem decisão estrutural (use wf-bugfix, wf-refactor, wf-frontend ou wf-backend).
|
||||
license: MIT
|
||||
metadata:
|
||||
dev-workflows:
|
||||
uses: []
|
||||
external: [system-design, c4-architecture, isaqb-architecture-governance, secure-architecture-governance]
|
||||
---
|
||||
|
||||
# Workflow Arquitetura
|
||||
|
||||
**Nível 3** do `dev-router`, ou nível 2 quando aparece uma decisão difícil de desfazer (banco,
|
||||
protocolo, fronteira de serviço, modelo de auth). No nível 2, faça só a Fase 3 (um ADR para a decisão)
|
||||
e siga com `wf-backend`/`wf-frontend`. Abaixo disso, este workflow não roda.
|
||||
|
||||
Cada fase produz um arquivo versionado. O que não virou arquivo não foi decidido.
|
||||
|
||||
Skills externas, se instaladas: `system-design` (raciocínio de design), `c4-architecture` (diagramas),
|
||||
`isaqb-architecture-governance` (arc42 e ADR), `secure-architecture-governance` (STRIDE e S-ADR).
|
||||
Sem elas, os modelos em [references/](references/) bastam.
|
||||
|
||||
## Fase 1: REQUIREMENTS
|
||||
|
||||
Entender antes de desenhar. Use o agente `planner` com o mapa do `explorer`.
|
||||
|
||||
1. Requisitos funcionais em **EARS com ID** (`REQ-001: WHEN ... THE SYSTEM SHALL ...`).
|
||||
Modelo e padrões: [references/ears.md](references/ears.md).
|
||||
2. Não funcionais **com número**: SLO (ex.: p95 < 250 ms, 99,9% ao mês), orçamento de performance
|
||||
(LCP, tamanho de bundle, custo mensal), nível ASVS 5.0 (L2 por padrão no nível 3).
|
||||
"Rápido" ou "escalável" sem número não é requisito.
|
||||
3. Atores, entidades centrais e fronteiras de confiança.
|
||||
4. Stack ou framework novo: consulte a documentação atual e registre URL e data. Não decida pela memória.
|
||||
|
||||
**Artefato:** `docs/specs/<nome>.md` com os `REQ-` e os não funcionais.
|
||||
**Gate da fase:** o usuário aprova a spec antes do DESIGN.
|
||||
|
||||
Rastreabilidade: adicione ao `.dev-workflows.toml` do projeto
|
||||
|
||||
```toml
|
||||
[spec]
|
||||
file = "docs/specs/<nome>.md"
|
||||
tests = ["tests"]
|
||||
```
|
||||
|
||||
e o gate nível 3 falha se algum `REQ-` não aparecer em um teste
|
||||
(ver `skills/dev-router/references/quality-gate.md`).
|
||||
|
||||
## Fase 2: DESIGN
|
||||
|
||||
1. Fluxo de design: requisitos, entidades, API, fluxo de dados, visão geral, pontos críticos.
|
||||
2. **C4** em Mermaid: Context e Container sempre; Component só onde houver decisão.
|
||||
Até 20 elementos por view, setas rotuladas, IDs estáveis. Modelo: [references/c4.md](references/c4.md).
|
||||
3. Modelo de dados e estratégia de migração (reversível; com dado existente, plano de backfill e rollback).
|
||||
4. **Pelo menos uma alternativa** por decisão importante, com o motivo da escolha.
|
||||
|
||||
**Artefato:** `docs/architecture/design.md` com os diagramas.
|
||||
Confira que o Mermaid renderiza: `npx -y @mermaid-js/mermaid-cli -i docs/architecture/c4.mmd -o /tmp/c4.svg`.
|
||||
|
||||
## Fase 3: DOCS (ADR)
|
||||
|
||||
Um ADR por decisão difícil de desfazer: contexto, opções, decisão, consequências (boas e ruins).
|
||||
Modelo: [references/adr.md](references/adr.md).
|
||||
|
||||
- `docs/architecture/adr/NNNN-<titulo>.md`, numeração sequencial, nunca reescrever um ADR aceito
|
||||
(substitua por um novo com `Substitui: ADR-NNNN`).
|
||||
- Decisão motivada por segurança vira S-ADR em `docs/security/adr/`.
|
||||
- Todo ADR citado no design (`ADR-0003`) existe como arquivo.
|
||||
|
||||
## Fase 4: THREAT MODEL
|
||||
|
||||
STRIDE por fronteira de confiança, cada ameaça ligada a um controle ASVS 5.0 L2.
|
||||
Tabela e mapeamento: [references/stride.md](references/stride.md).
|
||||
|
||||
1. Liste as fronteiras (internet/app, app/banco, serviço/serviço, app/terceiros, app/LLM).
|
||||
2. Para cada fronteira, as seis categorias STRIDE: ameaça concreta, controle, capítulo ASVS, status.
|
||||
3. Caminhos internos sem autenticação e confiança implícita entre serviços são achados, não detalhes.
|
||||
4. Feature que chama LLM: inclua OWASP LLM Top 10 (prompt injection, excessive agency).
|
||||
|
||||
**Artefato:** `docs/security/threat-model.md`.
|
||||
|
||||
## Fase 5: REVIEW
|
||||
|
||||
1. Agente `reviewer` sobre spec, design e ADRs: consistência entre `REQ-`, diagramas e ADRs,
|
||||
requisito sem dono, decisão sem alternativa.
|
||||
2. Agente `security-auditor` sobre o threat model e as decisões de segurança.
|
||||
3. Veredito por escrito: `Aprovado`, `Aprovado com mudanças` ou `Revisar`, cada achado com evidência
|
||||
(arquivo e trecho). Gap de fronteira de confiança volta para o DESIGN.
|
||||
4. O usuário aprova. A implementação segue com `wf-backend`/`wf-frontend`, tarefa a tarefa,
|
||||
e fecha com `"${CLAUDE_PLUGIN_ROOT}/scripts/quality-gate" --tier 3`.
|
||||
|
||||
## Não faça
|
||||
|
||||
- Desenhar antes de ter requisito mensurável.
|
||||
- ADR sem alternativa ou sem consequências.
|
||||
- Mais de um ADR para a mesma decisão.
|
||||
- Tratar segurança como nota mental: vira ameaça no threat model ou S-ADR.
|
||||
- Decidir stack sem fonte atual.
|
||||
|
||||
## Checklist de entrega
|
||||
|
||||
- [ ] `docs/specs/<nome>.md` com `REQ-` em EARS e não funcionais com número, aprovado pelo usuário.
|
||||
- [ ] `[spec]` no `.dev-workflows.toml` do projeto.
|
||||
- [ ] `docs/architecture/design.md` com C4 que renderiza e alternativas registradas.
|
||||
- [ ] Um ADR por decisão difícil de desfazer, com consequências.
|
||||
- [ ] `docs/security/threat-model.md` com STRIDE por fronteira e controles ASVS L2.
|
||||
- [ ] Revisão do `reviewer` e do `security-auditor` com veredito e evidência.
|
||||
@@ -1,31 +0,0 @@
|
||||
# Modelo de ADR
|
||||
|
||||
Arquivo: `docs/architecture/adr/NNNN-titulo-curto.md` (S-ADR de segurança em `docs/security/adr/`).
|
||||
Numeração sequencial. ADR aceito não é reescrito: uma decisão nova cria outro ADR com `Substitui:`.
|
||||
|
||||
```markdown
|
||||
# ADR-0003: PostgreSQL como banco de pedidos
|
||||
|
||||
- Status: proposto | aceito | substituído por ADR-NNNN
|
||||
- Data: 2026-09-24
|
||||
- Requisitos: REQ-002, REQ-010
|
||||
- Substitui: (se houver)
|
||||
|
||||
## Contexto
|
||||
O que força a decisão agora. Restrições reais (volume, equipe, custo, prazo), com números.
|
||||
|
||||
## Opções
|
||||
1. PostgreSQL gerenciado: prós, contras.
|
||||
2. MongoDB: prós, contras.
|
||||
3. Manter o SQLite atual: prós, contras.
|
||||
|
||||
## Decisão
|
||||
A opção escolhida e o motivo principal, em duas ou três frases.
|
||||
|
||||
## Consequências
|
||||
- Boas: o que fica mais fácil.
|
||||
- Ruins: o que fica mais difícil ou caro, e o que foi aceito conscientemente.
|
||||
- Como desfazer, se precisar, e quanto custa.
|
||||
```
|
||||
|
||||
Um ADR por decisão. Se duas decisões dependem uma da outra, dois ADRs que se referenciam.
|
||||
@@ -1,45 +0,0 @@
|
||||
# C4 em Mermaid
|
||||
|
||||
Regras: até 20 elementos por view, toda seta com rótulo (o que trafega e por qual protocolo),
|
||||
IDs estáveis reaproveitados em ADRs e no threat model, declaração na ordem de leitura
|
||||
(de quem usa para o que é usado).
|
||||
|
||||
## Context
|
||||
|
||||
```mermaid
|
||||
C4Context
|
||||
title Contexto: Sistema de Pedidos
|
||||
Person(cliente, "Cliente", "Compra pelo site")
|
||||
System(pedidos, "Sistema de Pedidos", "Cria e acompanha pedidos")
|
||||
System_Ext(pagamento, "Gateway de pagamento", "Cobra o cartão")
|
||||
Rel(cliente, pedidos, "Cria pedido", "HTTPS")
|
||||
Rel(pedidos, pagamento, "Autoriza cobrança", "HTTPS/REST")
|
||||
```
|
||||
|
||||
## Container
|
||||
|
||||
```mermaid
|
||||
C4Container
|
||||
title Containers: Sistema de Pedidos
|
||||
Person(cliente, "Cliente")
|
||||
System_Boundary(sp, "Sistema de Pedidos") {
|
||||
Container(web, "Web", "Next.js", "Interface")
|
||||
Container(api, "API", "FastAPI", "Regras de pedido")
|
||||
ContainerDb(db, "Banco", "PostgreSQL", "Pedidos e itens")
|
||||
Container(worker, "Worker", "Celery", "Cobrança assíncrona")
|
||||
}
|
||||
System_Ext(pagamento, "Gateway de pagamento")
|
||||
Rel(cliente, web, "Usa", "HTTPS")
|
||||
Rel(web, api, "Chama", "HTTPS/JSON")
|
||||
Rel(api, db, "Lê e grava", "SQL/TLS")
|
||||
Rel(api, worker, "Enfileira cobrança", "Redis")
|
||||
Rel(worker, pagamento, "Autoriza", "HTTPS")
|
||||
```
|
||||
|
||||
## Render
|
||||
|
||||
```bash
|
||||
npx -y @mermaid-js/mermaid-cli -i docs/architecture/c4.mmd -o /tmp/c4.svg
|
||||
```
|
||||
|
||||
Se o comando falhar, o diagrama tem erro de sintaxe e não está pronto.
|
||||
@@ -1,35 +0,0 @@
|
||||
# EARS: requisitos com ID
|
||||
|
||||
EARS (Easy Approach to Requirements Syntax) força cada requisito a ter gatilho e resposta
|
||||
verificáveis. Um requisito por linha, ID estável `REQ-NNN`, nunca reaproveitado.
|
||||
|
||||
| Padrão | Forma | Exemplo |
|
||||
|---|---|---|
|
||||
| Ubíquo | THE SYSTEM SHALL <resposta> | `REQ-001: THE SYSTEM SHALL registrar toda alteração de preço com autor e data.` |
|
||||
| Evento | WHEN <evento> THE SYSTEM SHALL <resposta> | `REQ-002: WHEN o usuário envia o formulário de cadastro THE SYSTEM SHALL responder 201 com o ID criado.` |
|
||||
| Indesejado | IF <condição> THEN THE SYSTEM SHALL <resposta> | `REQ-003: IF o token expirou THEN THE SYSTEM SHALL responder 401 sem revelar se a conta existe.` |
|
||||
| Estado | WHILE <estado> THE SYSTEM SHALL <resposta> | `REQ-004: WHILE a fila tiver mais de 1000 itens THE SYSTEM SHALL recusar novos jobs com 429.` |
|
||||
| Opcional | WHERE <recurso> THE SYSTEM SHALL <resposta> | `REQ-005: WHERE o SSO estiver habilitado THE SYSTEM SHALL ocultar o login por senha.` |
|
||||
|
||||
## Não funcionais
|
||||
|
||||
Mesmo formato de ID, sempre com número e forma de medir:
|
||||
|
||||
```
|
||||
REQ-010: THE SYSTEM SHALL responder GET /orders em p95 < 250 ms com 200 req/s (medido com k6).
|
||||
REQ-011: THE SYSTEM SHALL manter 99,9% de disponibilidade mensal (SLO no monitoramento).
|
||||
REQ-012: THE SYSTEM SHALL atender ASVS 5.0 nível L2 nos capítulos de autenticação e sessão.
|
||||
REQ-013: THE SYSTEM SHALL carregar a página inicial com LCP <= 2,5 s e INP <= 200 ms (Lighthouse CI).
|
||||
```
|
||||
|
||||
## Rastreabilidade
|
||||
|
||||
Cada teste cita o ID que prova, no nome ou num comentário:
|
||||
|
||||
```python
|
||||
def test_req_003_token_expirado_retorna_401():
|
||||
...
|
||||
```
|
||||
|
||||
Com `[spec]` no `.dev-workflows.toml`, o gate nível 3 falha se algum `REQ-` da spec não aparecer
|
||||
em nenhum arquivo de teste.
|
||||
@@ -1,35 +0,0 @@
|
||||
# Threat model STRIDE com ASVS 5.0
|
||||
|
||||
Arquivo: `docs/security/threat-model.md`. Uma tabela por fronteira de confiança.
|
||||
|
||||
## Categorias e onde procurar o controle no ASVS 5.0
|
||||
|
||||
| STRIDE | Ameaça típica | Capítulos ASVS 5.0 para o controle |
|
||||
|---|---|---|
|
||||
| Spoofing (identidade falsa) | login forjado, token roubado, serviço se passando por outro | Autenticação, Gerenciamento de sessão, Tokens autocontidos |
|
||||
| Tampering (adulteração) | parâmetro alterado, mensagem na fila modificada, upload malicioso | Validação e lógica de negócio, Manipulação de arquivos, Comunicação segura |
|
||||
| Repudiation (negação) | ação sensível sem rastro | Log e tratamento de erro |
|
||||
| Information disclosure (vazamento) | IDOR, stack trace na resposta, segredo em log | Autorização, Proteção de dados, Log e tratamento de erro |
|
||||
| Denial of service | endpoint sem limite, query sem paginação, arquivo gigante | Validação e lógica de negócio (limites), Manipulação de arquivos |
|
||||
| Elevation of privilege | usuário comum chega em rota de admin, injeção de comando | Autorização, Codificação e sanitização |
|
||||
|
||||
Os nomes dos capítulos seguem o ASVS 5.0 (maio de 2025). Cite o requisito exato (`V<cap>.<seção>.<item>`)
|
||||
consultando https://github.com/OWASP/ASVS, não pela memória.
|
||||
|
||||
## Modelo por fronteira
|
||||
|
||||
```markdown
|
||||
## Fronteira: Internet -> API
|
||||
|
||||
| # | STRIDE | Ameaça concreta | Controle | ASVS | Status |
|
||||
|---|---|---|---|---|---|
|
||||
| T1 | S | Token JWT aceito com alg none | Validar alg fixo e assinatura | Tokens autocontidos | ok |
|
||||
| T2 | I | GET /orders/{id} devolve pedido de outro cliente | Query escopada ao usuário autenticado | Autorização | pendente |
|
||||
| T3 | D | POST /orders sem limite de taxa | Rate limit por usuário e IP | Validação e lógica de negócio | pendente |
|
||||
```
|
||||
|
||||
Fronteiras que costumam faltar: serviço interno sem autenticação ("está na rede privada"),
|
||||
worker que confia em qualquer mensagem da fila, integração com LLM (prompt injection, ferramenta
|
||||
com permissão demais: OWASP LLM01 e LLM06).
|
||||
|
||||
Status `pendente` em ameaça alta bloqueia a aprovação da Fase 5.
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
name: wf-backend
|
||||
description: Use para trabalho de backend e API - endpoint REST/GraphQL, autenticação, schema e migração de banco, integração, fila, webhook, job. Contrato antes do handler, defaults seguros, auditoria OWASP e gate de qualidade no nível que o dev-router definiu. Não use para UI (wf-frontend), para correção pontual sem mudança de contrato (wf-bugfix) nem para desenho de sistema com vários serviços (wf-architecture primeiro).
|
||||
license: MIT
|
||||
metadata:
|
||||
dev-workflows:
|
||||
uses: [api-security-review, safedeps, sca-audit, secrets-scan, hermaguard]
|
||||
external: [secure-coding, bola-detector, auth-rbac-scaffold, injection-checker, openapi-hardener, security-test-generator]
|
||||
---
|
||||
|
||||
# Workflow Backend/API
|
||||
|
||||
O `dev-router` define o nível. Cada fase diz a partir de qual nível roda. No nível 1, a mudança segue
|
||||
o `wf-bugfix` (ou `wf-refactor`) com as regras de build seguro abaixo e o gate `--tier 1`.
|
||||
|
||||
## Fase 1: PLAN, contrato antes do código (nível 2+)
|
||||
|
||||
Nível 0 e 1 pulam esta fase. No nível 3, o `planner` escreve a spec EARS e esta fase vira a parte de
|
||||
API dela.
|
||||
|
||||
1. `explorer` mapeia rotas, modelos e validações que já existem. Reaproveite antes de criar.
|
||||
2. Contrato em `docs/contract.md` ou OpenAPI: rotas, métodos, payloads, status codes, erros em
|
||||
RFC 9457 (`application/problem+json`), paginação, rate limit e versionamento.
|
||||
3. Schemas validados na borda (Zod, Pydantic, OpenAPI). `openapi-hardener`, se instalado, saneia.
|
||||
4. Modelo de dados e migração desenhados antes da query: idempotente e reversível.
|
||||
5. Modelo de auth decidido e escrito no contrato (sessão, OAuth, JWT) com o motivo.
|
||||
6. Contrato com decisão de produto passa pelo usuário antes do build.
|
||||
|
||||
## Fase 2: BUILD com defaults seguros (nível 1+)
|
||||
|
||||
`implementer` com TDD: comportamento novo começa por teste que falha. Regras:
|
||||
|
||||
- Validação de toda entrada externa na borda; nada de confiar em payload, header ou query.
|
||||
- Erros estruturados (RFC 9457); nunca `except: pass` nem `catch` vazio.
|
||||
- Timeout em todo cliente HTTP e conexão de banco; retry só em operação idempotente.
|
||||
- Mutações idempotentes (chave de idempotência em POST que cobra ou cria); transação quando há várias escritas.
|
||||
- Toda listagem com paginação e limite máximo.
|
||||
- Query de dado do usuário escopada ao ID autenticado (ownership), nunca a um ID vindo do cliente.
|
||||
- Senha com Argon2id; sessão em cookie `httpOnly`, `Secure`, `SameSite`; nenhum segredo, token ou senha em log.
|
||||
- CORS com allowlist explícita (nunca `*` com credenciais); configuração por variável de ambiente.
|
||||
- Tipos seguros na fronteira: sem `any` ou cast forçado no que entra ou sai.
|
||||
- `secure-coding`, se instalado, como checklist da stack.
|
||||
|
||||
Dependência nova: justifique (`lean-code`) e consulte `safedeps` antes de instalar.
|
||||
|
||||
## Fase 3: SECURITY (nível 2+, ou nível 1 com gatilho)
|
||||
|
||||
Gatilhos do `dev-router` (auth, dinheiro, dado pessoal, SQL montado à mão, upload, shell, LLM) chamam
|
||||
o `security-auditor`, que segue o `wf-security-review` com escopo no diff e no que ele alcança.
|
||||
|
||||
- Referências: OWASP API Security Top 10 (2023) e OWASP Top 10:2025.
|
||||
- ASVS 5.0: L1 no nível 2, L2 no nível 3.
|
||||
- Vendorizadas: `api-security-review` (OWASP API Top 10), `secrets-scan`, `sca-audit`, `safedeps` (CVE ao vivo no OSV.dev).
|
||||
- Externas, se instaladas: `bola-detector` em rotas com `:id`, `auth-rbac-scaffold` em login e papéis, `injection-checker` em SQL/ORM/shell/template.
|
||||
- Achado P0/P1 bloqueia a entrega; depois do fix, rode a auditoria de novo e confirme que sumiu.
|
||||
- Nível 3 ou gatilho disparado: `hermaguard` no diff como passe adversarial (edge cases, ataque, blast radius).
|
||||
|
||||
## Fase 4: TEST + GATE (nível 1+)
|
||||
|
||||
1. Testes de validação, auth negativa (outro usuário, sem token, papel errado) e idempotência.
|
||||
`security-test-generator`, se instalado, ajuda a gerar os casos negativos.
|
||||
2. Gate: `"${CLAUDE_PLUGIN_ROOT}/scripts/quality-gate" --tier N`. Já cobre lint, typecheck, testes,
|
||||
gitleaks, semgrep, cobertura do diff, mutação e osv-scanner conforme o nível.
|
||||
3. Checagens que dependem do app de pé entram como `[[check]]` no `.dev-workflows.toml` do projeto
|
||||
(receitas em [references/checks.md](references/checks.md)):
|
||||
- schemathesis contra o OpenAPI (nível 2+): zero erro 5xx e zero violação de schema;
|
||||
- Pact quando há outro serviço consumindo a API;
|
||||
- migração up, down, up num banco descartável (testcontainers) quando o schema muda;
|
||||
- `EXPLAIN` das queries novas e detecção de N+1 quando há query nova;
|
||||
- k6 com thresholds de p95 quando a mudança toca um caminho quente (nível 3 ou gatilho de performance).
|
||||
4. No nível 2+, `reviewer` revisa o diff; no nível 3, `verifier` roda o gate final e devolve a evidência.
|
||||
|
||||
Detalhes do gate e da configuração: [../dev-router/references/quality-gate.md](../dev-router/references/quality-gate.md).
|
||||
|
||||
## Relatório
|
||||
|
||||
Contrato entregue (nível 2+), achados de segurança por severidade e o que foi corrigido, testes que
|
||||
provam (nomes e contagem real), veredito do gate com o que ficou não verificado, e linhas líquidas.
|
||||
|
||||
## Critérios de aceite
|
||||
|
||||
- [ ] Nível 2+: contrato aprovado antes do primeiro handler.
|
||||
- [ ] Toda entrada externa validada na borda; erros em RFC 9457.
|
||||
- [ ] Queries de dado do usuário escopadas ao ID autenticado.
|
||||
- [ ] Nenhum P0/P1 de segurança aberto; CVEs consultados ao vivo, com data.
|
||||
- [ ] Migração reversível provada (up, down, up) quando o schema mudou.
|
||||
- [ ] Gate `pass`, ou `incomplete` com a lista do que não foi verificado dita ao usuário.
|
||||
|
||||
## Regras duras
|
||||
|
||||
- Nunca escrever handler antes do contrato aprovado (nível 2+).
|
||||
- Nunca rodar `hermaguard` ou auditoria em código com testes vermelhos; corrija os testes primeiro.
|
||||
- Nunca dizer "suíte passando" sem ter rodado a suíte.
|
||||
@@ -1,63 +0,0 @@
|
||||
# Checagens de backend que dependem do app de pé
|
||||
|
||||
Estas checagens entram como `[[check]]` no `.dev-workflows.toml` do projeto, porque precisam de
|
||||
servidor, banco ou carga reais. O gate roda cada uma no nível indicado e trata ferramenta ausente
|
||||
como "não verificado".
|
||||
|
||||
## Contrato de exemplo (`docs/contract.md`)
|
||||
|
||||
```text
|
||||
POST /api/v1/users -> 201 | 400 | 401 | 409 | 429
|
||||
GET /api/v1/users/:id -> 200 | 403 | 404
|
||||
Erros: RFC 9457 (application/problem+json)
|
||||
Paginação: ?cursor=&limit= (limit máx. 100)
|
||||
Rate limit: 60 req/min por usuário em escrita; 429 com Retry-After
|
||||
Auth: sessão em cookie httpOnly; rotas /admin exigem papel admin
|
||||
```
|
||||
|
||||
## Receitas de `[[check]]`
|
||||
|
||||
```toml
|
||||
# Fuzz da API a partir do OpenAPI: zero 5xx e zero violação de schema
|
||||
[[check]]
|
||||
name = "api-fuzz"
|
||||
cmd = "schemathesis run http://localhost:8000/openapi.json --checks all"
|
||||
tier = 2
|
||||
|
||||
# Contrato com consumidores (quando outro serviço usa a API)
|
||||
[[check]]
|
||||
name = "pact-verify"
|
||||
cmd = "npx pact-provider-verifier --provider-base-url http://localhost:8000 --pact-broker-url $PACT_BROKER_URL"
|
||||
tier = 2
|
||||
|
||||
# Migração reversível num banco descartável (up -> down -> up)
|
||||
[[check]]
|
||||
name = "migration-roundtrip"
|
||||
cmd = "alembic upgrade head && alembic downgrade -1 && alembic upgrade head"
|
||||
tier = 2
|
||||
|
||||
# Carga no caminho quente, com threshold de p95 no próprio script k6
|
||||
[[check]]
|
||||
name = "load"
|
||||
cmd = "k6 run --quiet load/hot-path.js"
|
||||
tier = 3
|
||||
```
|
||||
|
||||
No k6, o threshold fica no script e faz o comando sair com erro quando estoura:
|
||||
|
||||
```js
|
||||
export const options = {
|
||||
vus: 20, duration: "30s",
|
||||
thresholds: { http_req_duration: ["p(95)<300"], http_req_failed: ["rate<0.01"] },
|
||||
};
|
||||
```
|
||||
|
||||
Para a migração, suba um Postgres descartável (testcontainers ou `docker run --rm -p 5433:5432 postgres`)
|
||||
e aponte a `DATABASE_URL` do comando para ele. Nunca rode o roundtrip num banco com dados reais.
|
||||
|
||||
## Queries novas
|
||||
|
||||
- Rode `EXPLAIN (ANALYZE, BUFFERS)` em cada query nova com volume parecido com o de produção; seq scan
|
||||
em tabela grande pede índice ou filtro.
|
||||
- N+1: conte as queries de uma requisição de listagem (log do ORM ou `django-debug-toolbar`,
|
||||
`nplusone`, `bullet`). O número não pode crescer com o tamanho da página.
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
name: wf-bugfix
|
||||
description: Use para corrigir bug, erro, exceção, falha, regressão, comportamento errado ("não funciona", "quebrou", "fix"). Reproduz, acha a causa raiz e prova a correção com um teste de regressão que falha antes e passa depois.
|
||||
license: MIT
|
||||
metadata:
|
||||
dev-workflows:
|
||||
uses: []
|
||||
external: []
|
||||
---
|
||||
|
||||
# Workflow Bugfix
|
||||
|
||||
## 1. Reproduzir
|
||||
|
||||
Encontre o comando, teste ou requisição que mostra o erro, e rode. Sem reprodução, colete mais dado
|
||||
(stack trace, log, entrada exata) em vez de chutar uma correção.
|
||||
|
||||
## 2. Achar a causa raiz
|
||||
|
||||
- Siga o fluxo real do ponto de entrada até o erro.
|
||||
- Ache todos os chamadores da função que você pretende mudar (`grep -rn "nomeDaFuncao(" .`).
|
||||
- Escreva a causa em uma ou duas frases antes de editar. Se não consegue, ainda não achou.
|
||||
- Corrija no ponto por onde todos os chamadores passam, não só no caminho que o relato citou.
|
||||
|
||||
## 3. Provar com teste de regressão
|
||||
|
||||
1. Escreva o teste que reproduz o bug.
|
||||
2. Prove que ele falha sem a correção: `git stash push <arquivos da correção>`, rode o teste, veja vermelho, `git stash pop`.
|
||||
3. Rode de novo com a correção: verde.
|
||||
|
||||
Um teste que passa com e sem a correção não prova nada; reescreva.
|
||||
|
||||
## 4. Gate
|
||||
|
||||
`"${CLAUDE_PLUGIN_ROOT}/scripts/quality-gate" --tier 1` (ou o nível que o `dev-router` definiu).
|
||||
|
||||
## 5. Relatório
|
||||
|
||||
Causa em uma frase, o que mudou, o teste que prova (nome e saída vermelho/verde) e o veredito do gate.
|
||||
|
||||
## Não faça
|
||||
|
||||
- Silenciar o erro com `try/except` ou `catch` vazio.
|
||||
- Mudar o teste até ele passar em vez de corrigir o código.
|
||||
- Corrigir o sintoma num chamador e deixar a causa na função compartilhada.
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
name: wf-frontend
|
||||
description: Use para trabalho de frontend e UI - componentes, páginas, layouts, estilos, design system, animação, acessibilidade, redesign de tela existente, e geração de PDF ou documento visual a partir de HTML. Fluxo DESIGN, BUILD, REVIEW com profundidade definida pelo nível do dev-router. Não use para backend ou API (wf-backend), para bug sem mudança visual (wf-bugfix) nem para refactor sem mudança de comportamento (wf-refactor).
|
||||
license: MIT
|
||||
metadata:
|
||||
dev-workflows:
|
||||
uses: [design-taste-frontend, redesign-existing-projects, lean-code, dev-router]
|
||||
external: [frontend-design, web-design, vercel-react-best-practices, vercel-composition-patterns, animate, impeccable, avoid-ai-writing]
|
||||
---
|
||||
|
||||
# Workflow Frontend
|
||||
|
||||
A profundidade vem do nível que o `dev-router` definiu. Cada fase diz a partir de qual nível roda.
|
||||
|
||||
| Nível | O que roda |
|
||||
|---|---|
|
||||
| 0 | só o gate (`--tier 0`) |
|
||||
| 1 | ajuste de UI: siga o design system que já existe; sem DESIGN.md, sem aprovação; BUILD enxuto + gate `--tier 1` |
|
||||
| 2 | tela ou componente novo: DESIGN curto, BUILD, REVIEW com `ui-critic` e `reviewer` |
|
||||
| 3 | fluxo ou design system novo: DESIGN com aprovação do usuário, BUILD por tarefas, REVIEW completo |
|
||||
|
||||
## Skills (carregar só na fase que usa)
|
||||
|
||||
- **Tela nova:** `design-taste-frontend` (~35 mil tokens; só para tela nova, nunca para ajuste) e, se instalada, `frontend-design`.
|
||||
- **Tela existente:** `redesign-existing-projects` (audita antes de mudar).
|
||||
- **Auditoria anti "UI de IA":** `impeccable` (`/impeccable audit`, externa).
|
||||
- **React/Next:** `vercel-react-best-practices` (performance), `vercel-composition-patterns` (composição).
|
||||
- **Motion:** `animate`. **Microcopy:** `avoid-ai-writing`.
|
||||
- Sempre: `lean-code`.
|
||||
|
||||
Externas são opcionais: se não estiverem instaladas, siga as regras desta página. Instalação em `docs/SKILLS.md`.
|
||||
|
||||
## Fase 1, DESIGN (nível 2+, tela ou componente novo)
|
||||
|
||||
Nível 1 pula esta fase e usa os tokens, componentes e padrões que o projeto já tem.
|
||||
|
||||
1. `explorer` mapeia o design system existente: tokens (cor, tipografia, espaçamento), componentes reutilizáveis, biblioteca de UI. Reaproveitar vem antes de criar.
|
||||
2. Defina a direção: paleta, tipografia, densidade, claro/escuro. Tela nova usa `design-taste-frontend`; redesign usa `redesign-existing-projects`.
|
||||
3. Registre as decisões em `DESIGN.md` (curto: tokens e o porquê).
|
||||
4. Nível 3, ou decisão que é do usuário (identidade visual, troca de stack): pare e peça aprovação antes de codar. Nível 2 segue sem parar se a direção vem do design system existente.
|
||||
|
||||
Evite: gradiente roxo genérico, glassmorphism sem motivo, cards idênticos em grade, ícone decorativo sem função, dois design systems na mesma tela.
|
||||
|
||||
## Fase 2, BUILD (nível 1+)
|
||||
|
||||
1. `implementer` (ou você, em mudança pequena) implementa com `lean-code`: componente e hook existentes antes de novos; CSS e HTML nativos antes de JS.
|
||||
2. **Estados em todo fluxo com dados:** carregando, vazio, erro, sucesso.
|
||||
3. **Acessibilidade por padrão:** HTML semântico, foco visível e em ordem, rótulos, contraste de texto >= 4.5:1. ARIA só quando o HTML não resolve.
|
||||
4. **Responsivo mobile-first**, testado em 375 px e 1280 px.
|
||||
5. **Composição:** compound components em vez de explosão de props booleanas (`isX`, `showY`).
|
||||
6. **Motion com propósito** (estado, entrada, feedback) e `prefers-reduced-motion` respeitado.
|
||||
7. React/Next: sem waterfall de dados, sem re-render em cascata, imagem com dimensões, código dividido por rota.
|
||||
8. TypeScript sem `any` nem cast forçado para "passar".
|
||||
9. Rode o gate a cada componente relevante, não só no fim.
|
||||
|
||||
Entrada de usuário renderizada como HTML, `dangerouslySetInnerHTML`, `eval`, `postMessage` ou mudança de CSP disparam `security-auditor` (XSS/CSP).
|
||||
|
||||
## Fase 3, REVIEW (nível 2+)
|
||||
|
||||
1. `ui-critic`: screenshot em 375 e 1280, estados, a11y, padrões genéricos. Se `impeccable` estiver instalada, `/impeccable audit`.
|
||||
2. `reviewer`: correção e simplicidade do diff.
|
||||
3. `avoid-ai-writing` nos textos entregues ao usuário (labels, vazios, erros), se instalada.
|
||||
4. Corrija o que for `bloqueante` ou `importante` e rode os dois de novo só no que mudou.
|
||||
|
||||
## Gate
|
||||
|
||||
```bash
|
||||
"${CLAUDE_PLUGIN_ROOT}/scripts/quality-gate" --tier N
|
||||
```
|
||||
|
||||
Lint, typecheck, testes, segredos e build rodam sozinhos. O que depende do app de pé (Lighthouse,
|
||||
axe, bundle, regressão visual) entra como `[[check]]` no `.dev-workflows.toml` do projeto: modelos em
|
||||
[references/checks.md](references/checks.md). Lista completa de checagens:
|
||||
`skills/dev-router/references/quality-gate.md`.
|
||||
|
||||
Metas de referência (nível 2+): LCP <= 2,5 s, INP <= 200 ms (TBT como proxy em laboratório),
|
||||
CLS <= 0,1; axe sem violação `serious` ou `critical`; bundle dentro do limite do `size-limit`.
|
||||
|
||||
O `verifier` roda o gate e devolve a evidência. Ferramenta ausente aparece como "não verificado":
|
||||
diga isso ao usuário, nunca chame de testado.
|
||||
|
||||
## PDF e documento visual
|
||||
|
||||
Sempre HTML + CSS renderizado pelo Playwright (`page.pdf()`), depois cada página convertida em PNG e
|
||||
inspecionada com Read: nada cortado na margem, tabela longa quebrando entre páginas. Nunca
|
||||
reportlab platypus direto para tabelas longas. Método completo: [references/pdf.md](references/pdf.md).
|
||||
|
||||
## Aceite
|
||||
|
||||
- [ ] Nível declarado; fases puladas coerentes com ele.
|
||||
- [ ] Nível 2+: direção registrada em `DESIGN.md` (aprovada pelo usuário no nível 3).
|
||||
- [ ] Estados carregando, vazio, erro e sucesso em todo fluxo com dados.
|
||||
- [ ] Acessível: semântica, foco, rótulos, contraste >= 4.5:1; axe sem `serious`/`critical` se configurado.
|
||||
- [ ] Responsivo em 375 px e 1280 px, sem corte ou sobreposição.
|
||||
- [ ] `prefers-reduced-motion` respeitado; animação só com propósito.
|
||||
- [ ] Sem explosão de props booleanas; composição com compound components.
|
||||
- [ ] Gate `pass`, ou falhas explicadas; "não verificado" listado ao usuário.
|
||||
- [ ] PDF: páginas renderizadas e inspecionadas, nada cortado.
|
||||
@@ -1,91 +0,0 @@
|
||||
# Checagens de frontend para o `.dev-workflows.toml`
|
||||
|
||||
Estas checagens precisam do app de pé ou de configuração do projeto, por isso entram como
|
||||
`[[check]]`. O gate só roda as do nível pedido ou abaixo.
|
||||
|
||||
```toml
|
||||
[[check]]
|
||||
name = "lighthouse"
|
||||
cmd = "npx lhci autorun"
|
||||
tier = 2
|
||||
|
||||
[[check]]
|
||||
name = "a11y"
|
||||
cmd = "npx playwright test e2e/a11y.spec.ts"
|
||||
tier = 2
|
||||
|
||||
[[check]]
|
||||
name = "bundle"
|
||||
cmd = "npx size-limit"
|
||||
tier = 2
|
||||
|
||||
[[check]]
|
||||
name = "visual"
|
||||
cmd = "npx playwright test e2e/visual.spec.ts"
|
||||
tier = 3
|
||||
```
|
||||
|
||||
## Lighthouse CI com orçamento (`lighthouserc.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"ci": {
|
||||
"collect": { "startServerCommand": "npm run start", "url": ["http://localhost:3000/"], "numberOfRuns": 3 },
|
||||
"assert": {
|
||||
"assertions": {
|
||||
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
|
||||
"total-blocking-time": ["error", { "maxNumericValue": 200 }],
|
||||
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
|
||||
"categories:accessibility": ["error", { "minScore": 0.9 }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
INP só existe com usuário real; em laboratório, TBT é o proxy.
|
||||
|
||||
## Acessibilidade com Playwright + axe (`e2e/a11y.spec.ts`)
|
||||
|
||||
```ts
|
||||
import { test, expect } from "@playwright/test";
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
|
||||
for (const path of ["/", "/login"]) {
|
||||
test(`a11y ${path}`, async ({ page }) => {
|
||||
await page.goto(path);
|
||||
const { violations } = await new AxeBuilder({ page }).analyze();
|
||||
const graves = violations.filter(v => v.impact === "serious" || v.impact === "critical");
|
||||
expect(graves).toEqual([]);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Regressão visual (`e2e/visual.spec.ts`)
|
||||
|
||||
```ts
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
for (const width of [375, 1280]) {
|
||||
test(`home ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveScreenshot(`home-${width}.png`, { fullPage: true });
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
A primeira execução grava a referência (`--update-snapshots`); as seguintes comparam.
|
||||
|
||||
## Bundle (`size-limit` no `package.json`)
|
||||
|
||||
```json
|
||||
"size-limit": [{ "path": "dist/**/*.js", "limit": "200 KB" }]
|
||||
```
|
||||
|
||||
## Screenshot rápida para o `ui-critic`
|
||||
|
||||
```bash
|
||||
npx playwright screenshot --viewport-size=375,812 http://localhost:3000 /tmp/ui-375.png
|
||||
npx playwright screenshot --viewport-size=1280,800 http://localhost:3000 /tmp/ui-1280.png
|
||||
```
|
||||
@@ -1,49 +0,0 @@
|
||||
# PDF e documento visual: HTML + CSS + Playwright
|
||||
|
||||
Todo PDF passa por aqui: design primeiro, depois render, depois inspeção visual das páginas.
|
||||
|
||||
## Por que não reportlab direto
|
||||
|
||||
`platypus` com tabela longa estoura margem e corta linhas entre páginas. HTML + CSS de impressão
|
||||
resolve quebra de página, cabeçalho repetido e margens com regras declarativas.
|
||||
|
||||
## 1. HTML com CSS de impressão
|
||||
|
||||
```css
|
||||
@page { size: A4; margin: 18mm 16mm; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead { display: table-header-group; } /* repete o cabeçalho em cada página */
|
||||
tr, img, figure { break-inside: avoid; }
|
||||
h1, h2 { break-after: avoid; }
|
||||
td { overflow-wrap: anywhere; } /* texto longo não empurra a margem */
|
||||
```
|
||||
|
||||
## 2. Render com Playwright
|
||||
|
||||
```python
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as p:
|
||||
page = p.chromium.launch().new_page()
|
||||
page.goto("file:///caminho/relatorio.html")
|
||||
page.pdf(path="relatorio.pdf", format="A4", print_background=True, prefer_css_page_size=True)
|
||||
```
|
||||
|
||||
## 3. Inspeção visual obrigatória
|
||||
|
||||
Converta cada página em PNG e abra com Read:
|
||||
|
||||
```bash
|
||||
pdftoppm -png -r 110 relatorio.pdf /tmp/qa/pagina
|
||||
ls /tmp/qa/pagina-*.png
|
||||
```
|
||||
|
||||
Em cada imagem, confira:
|
||||
|
||||
- nada cortado nas bordas (texto, tabela, imagem);
|
||||
- tabela longa quebra entre páginas com o cabeçalho repetido;
|
||||
- nenhuma linha órfã de título no pé da página;
|
||||
- fontes carregadas (sem fallback inesperado).
|
||||
|
||||
Achou problema: ajuste o CSS, renderize de novo e inspecione de novo. Só entregue depois de todas as
|
||||
páginas passarem.
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
name: wf-readme
|
||||
description: Use para criar, auditar, melhorar, reescrever ou padronizar o README (e docs vizinhos como CONTRIBUTING e CHANGELOG) de um repositório. Tudo que o README afirma vem de um fato real do repo e os comandos mostrados são executados antes da entrega. Não use para docstrings, comentários de código, documentação de API gerada ou textos de UI (esses ficam com wf-frontend).
|
||||
license: MIT
|
||||
metadata:
|
||||
dev-workflows:
|
||||
uses: []
|
||||
external: [readme-crafter, good-readme, curating-readme, avoid-ai-writing]
|
||||
---
|
||||
|
||||
# Workflow README
|
||||
|
||||
O README é construído a partir do repositório (manifest, código, scripts, config, git), nunca de
|
||||
memória. Fato sem fonte vira pergunta ao usuário.
|
||||
|
||||
## Escala pelo tamanho do pedido
|
||||
|
||||
| Pedido | Fluxo |
|
||||
|---|---|
|
||||
| Ajuste pontual (corrigir um comando, um link, uma seção) | editar e rodar a Fase 5 só no trecho tocado |
|
||||
| README novo, reescrita ou padronização | Fases 1 a 5 |
|
||||
|
||||
## Skills (se instaladas)
|
||||
|
||||
- `readme-crafter`: classifica o projeto e sugere estrutura sob medida.
|
||||
- `good-readme`: cria com exemplos reais ou audita contra uma rubrica de 22 critérios.
|
||||
- `curating-readme`: padroniza README e docs vizinhos; traz o script `audit-repo.sh`.
|
||||
- `avoid-ai-writing`: remove AI-isms do texto final.
|
||||
|
||||
Sem elas, o fluxo abaixo funciona sozinho.
|
||||
|
||||
## Fase 1: SCAN (fatos reais)
|
||||
|
||||
1. Leia o manifest (`package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`...): nome, descrição,
|
||||
versão, scripts, `bin`, dependências.
|
||||
2. Ache os entrypoints e o que o projeto de fato faz (CLI, rotas, exports públicos).
|
||||
3. Leia o README atual, `LICENSE`, `CONTRIBUTING.md`, a CI (`.github/workflows/`) e `.env.example`.
|
||||
4. Capture o contexto: `git remote -v` e `git log --oneline -5`.
|
||||
5. Monte a lista de fatos, cada um com a fonte (`arquivo:linha` ou comando). O que não tem fonte vira pergunta.
|
||||
|
||||
## Fase 2: CLASSIFY (tipo e público)
|
||||
|
||||
- Tipo: biblioteca, CLI, web app, API, framework, monorepo, agente.
|
||||
- Distribuição: público ou interno. Público primário: novo usuário, contribuidor, operador ou avaliador.
|
||||
- Cada decisão com a fonte ("CLI porque `package.json` tem `bin`").
|
||||
- Para CLI, rode o `--help` real e use essa saída como base da seção de uso.
|
||||
- Se o código não revela a intenção, até 3 perguntas ao usuário.
|
||||
|
||||
## Fase 3: AUDIT (se já existe README)
|
||||
|
||||
- Compare cada afirmação com o repo: comandos, caminhos, variáveis de ambiente, versões, badges.
|
||||
- Registre o que está certo, desatualizado, errado e faltando. Divergência com o código é achado, mesmo em README bonito.
|
||||
- Dependência opcional apresentada como obrigatória e passo que bloqueia o primeiro uso são achados.
|
||||
- Com `good-readme`, registre a nota de partida.
|
||||
|
||||
Sem README, siga direto para a Fase 4.
|
||||
|
||||
## Fase 4: GENERATE
|
||||
|
||||
1. Estrutura por tipo: título, badges do que existe, uma linha de descrição, o que faz, pré-requisitos,
|
||||
quick start copiável, uso, configuração (tabela de env vars do `.env.example`), estrutura,
|
||||
contribuição, licença. Detalhe em [references/estrutura.md](references/estrutura.md).
|
||||
2. Badge só do que existe: licença com `LICENSE`, build com CI configurada, versão com pacote publicado.
|
||||
3. Todo comando e exemplo mostrado precisa rodar neste repo.
|
||||
4. Uma língua por arquivo; tradução vai em arquivo separado (`README.pt-BR.md`).
|
||||
5. Passe `avoid-ai-writing` no texto, se instalada.
|
||||
|
||||
## Fase 5: VERIFY (antes de entregar)
|
||||
|
||||
1. **Rode os comandos** de instalação e quick start num ambiente limpo (clone novo ou diretório
|
||||
temporário). O que não der para rodar (precisa de credencial, serviço externo, hardware),
|
||||
liste no relatório com o motivo. Nunca escreva "testado" sobre o que não rodou.
|
||||
2. **Links locais resolvem**: cada `](caminho)` aponta para arquivo que existe.
|
||||
3. **Links externos respondem**: `curl -sI -o /dev/null -w '%{http_code}' <url>` em cada um.
|
||||
4. **Referências existem**: arquivos, scripts, env vars e flags citados estão no repo.
|
||||
5. **Índice bate** com os headings, se houver índice.
|
||||
6. Nenhum `TODO:` sobrou no texto final.
|
||||
|
||||
Comandos prontos: [references/verificacao.md](references/verificacao.md).
|
||||
|
||||
## Relatório
|
||||
|
||||
O que mudou, os achados da auditoria (antes e depois), os comandos executados com o resultado e o
|
||||
que não pôde ser verificado.
|
||||
|
||||
## Não faça
|
||||
|
||||
- Inventar instalação, comando, exemplo, screenshot ou feature.
|
||||
- Copiar a estrutura de outro projeto sem adaptar ao tipo e ao público.
|
||||
- Duplicar a mesma informação em duas seções.
|
||||
- Encher de emojis ou badges decorativos.
|
||||
|
||||
## Critério de aceite
|
||||
|
||||
- [ ] Toda afirmação tem fonte no repo ou virou pergunta respondida.
|
||||
- [ ] Comandos de instalação e quick start executados, ou listados como não verificáveis com motivo.
|
||||
- [ ] Links locais e externos resolvem; arquivos citados existem.
|
||||
- [ ] Badges só do que existe; nenhum `TODO:` no texto final.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Estrutura do README por tipo
|
||||
|
||||
Ordem base. Corte a seção que não se aplica em vez de preencher com texto genérico.
|
||||
|
||||
| Seção | Fonte no repo | Obrigatória para |
|
||||
|---|---|---|
|
||||
| Título e uma linha de descrição | `name`/`description` do manifest | todos |
|
||||
| Badges | `LICENSE`, `.github/workflows/`, registro do pacote | só o que existe |
|
||||
| O que faz | entrypoints, exports, rotas | todos |
|
||||
| Pré-requisitos | `engines`, `requires-python`, `go` no `go.mod`, Dockerfile | todos |
|
||||
| Quick start | scripts do manifest, `Makefile` | todos |
|
||||
| Uso | saída real do `--help`, exemplos que rodam | CLI, biblioteca |
|
||||
| API | exports públicos, OpenAPI | biblioteca, API |
|
||||
| Configuração | `.env.example`, arquivo de config | quem tem env vars |
|
||||
| Estrutura do projeto | árvore real (`git ls-files`) | monorepo, app |
|
||||
| Contribuição | `CONTRIBUTING.md`, comandos de teste e lint | open source |
|
||||
| Licença | `LICENSE` | todos |
|
||||
|
||||
## Por tipo
|
||||
|
||||
- **Biblioteca**: instalação pelo gerenciador, um exemplo mínimo que roda, link para a API.
|
||||
- **CLI**: instalação, `--help` real, dois ou três comandos comuns com a saída.
|
||||
- **Web app**: pré-requisitos, `.env`, como subir em dev, como rodar os testes.
|
||||
- **API**: como subir, onde está o OpenAPI, autenticação, um `curl` que funciona.
|
||||
- **Monorepo**: tabela de pacotes com uma linha cada e o comando para rodar todos.
|
||||
|
||||
## Quick start
|
||||
|
||||
Copiável de ponta a ponta: clonar, instalar, configurar, rodar, ver o primeiro resultado.
|
||||
Dependência opcional fica fora do caminho principal, numa seção à parte.
|
||||
@@ -1,25 +0,0 @@
|
||||
# Comandos de verificação do README
|
||||
|
||||
Rode da raiz do repositório.
|
||||
|
||||
```bash
|
||||
# Links locais que não existem no disco
|
||||
grep -oE '\]\([^)#]+' README.md | sed 's/^](//' | grep -vE '^(https?:|mailto:)' \
|
||||
| while read -r p; do [ -e "$p" ] || echo "FALTA: $p"; done
|
||||
|
||||
# Links externos que não respondem 2xx/3xx
|
||||
grep -oE 'https?://[^) >"]+' README.md | sort -u | while read -r u; do
|
||||
c=$(curl -sIL -o /dev/null -w '%{http_code}' --max-time 10 "$u")
|
||||
case "$c" in 2*|3*) ;; *) echo "$c $u";; esac
|
||||
done
|
||||
|
||||
# TODO esquecido
|
||||
grep -n 'TODO' README.md
|
||||
|
||||
# Quick start num clone limpo (troque pelos comandos do README)
|
||||
tmp=$(mktemp -d) && git clone -q . "$tmp/repo" && cd "$tmp/repo"
|
||||
# ... cole aqui os comandos de instalação e quick start, na ordem do README
|
||||
```
|
||||
|
||||
Para CLI, compare a seção de uso com `<cli> --help`. Para API, rode o `curl` do README contra o
|
||||
servidor local e confira o status. Anote no relatório cada comando e o resultado.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
name: wf-refactor
|
||||
description: Use para refatorar, reorganizar, renomear, extrair, simplificar ou limpar código sem mudar o comportamento. Congela o comportamento com testes antes de mexer e mexe em passos pequenos.
|
||||
license: MIT
|
||||
metadata:
|
||||
dev-workflows:
|
||||
uses: []
|
||||
external: []
|
||||
---
|
||||
|
||||
# Workflow Refactor
|
||||
|
||||
## 1. Rede de segurança
|
||||
|
||||
Os testes atuais cobrem o comportamento que você vai tocar? Se não, escreva **testes de
|
||||
caracterização** antes de mudar qualquer coisa: eles registram o que o código faz hoje, mesmo que
|
||||
pareça estranho. Rode e confirme que estão verdes.
|
||||
|
||||
## 2. Passos pequenos
|
||||
|
||||
Cada passo deixa o código compilando e os testes verdes. Rode os testes a cada passo, não só no fim.
|
||||
Renomeação: procure também em strings, configs, rotas e docs (`grep -rn`), não só nos imports.
|
||||
|
||||
## 3. Comportamento não muda
|
||||
|
||||
Os testes de caracterização não são editados. Se você precisa editar um deles, a mudança não é
|
||||
refactor: pare e pergunte ao usuário.
|
||||
|
||||
## 4. Gate e relatório
|
||||
|
||||
`"${CLAUDE_PLUGIN_ROOT}/scripts/quality-gate" --tier 1` (nível 2 se o refactor atravessa módulos).
|
||||
No relatório, as linhas líquidas: um bom refactor costuma reduzir. Se aumentou, explique.
|
||||
|
||||
## Não faça
|
||||
|
||||
- Misturar refactor com feature ou correção de bug no mesmo diff.
|
||||
- Mudar API pública sem o usuário pedir.
|
||||
- Criar camada ou abstração nova "para ficar mais limpo" sem um uso concreto hoje.
|
||||
@@ -1,104 +0,0 @@
|
||||
---
|
||||
name: wf-security-review
|
||||
description: Use para revisão de segurança de código, diff ou PR (frontend e backend), para varrer dependências atrás de CVE, procurar segredos commitados ou auditar auth, API e injeção; também quando o dev-router dispara um gatilho de segurança (auth, dinheiro, dados pessoais, upload, SQL montado à mão, dependência nova, feature com LLM). Consulta fontes vivas de vulnerabilidade com data. Não use para revisão de correção ou estilo sem aspecto de segurança (isso é o agente reviewer) nem para desenhar arquitetura (wf-architecture).
|
||||
license: MIT
|
||||
metadata:
|
||||
dev-workflows:
|
||||
uses: [safedeps, sca-audit, code-review-security, secrets-scan, api-security-review, web-security-review, cve-triage, patch-prioritization, dependency-scanning, hermaguard]
|
||||
external: [bola-detector, injection-checker, auth-rbac-scaffold]
|
||||
---
|
||||
|
||||
# Workflow Security Review
|
||||
|
||||
O conhecimento de CVE do modelo para na data de corte do treino. Por isso toda revisão consulta
|
||||
fontes vivas (OSV.dev, GitHub Advisory Database, NVD, CISA KEV) e registra a data da consulta.
|
||||
**Nunca afirme "sem CVE" sem ter consultado.** Uma revisão sem fonte e sem data já nasce vencida.
|
||||
|
||||
Este workflow vem pré-carregado no agente `security-auditor`.
|
||||
|
||||
## Profundidade por nível (do `dev-router`)
|
||||
|
||||
| Nível | Escopo | Padrão | Ferramentas |
|
||||
|---|---|---|---|
|
||||
| 1 com gatilho | só o diff | Top 10 no que o diff toca | gate `--tier 1` (gitleaks, semgrep) |
|
||||
| 2 | diff + superfície alcançável (rotas, handlers, queries, componentes que usam o código alterado) | ASVS 5.0 L1 | gate `--tier 2` (+ osv-scanner), `safedeps` em toda dependência nova |
|
||||
| 3 ou auditoria completa | toda a superfície | ASVS 5.0 L2 | gate `--tier 3` (+ trivy), passe adversarial do `hermaguard`, STRIDE contra a arquitetura |
|
||||
|
||||
Gate: `"${CLAUDE_PLUGIN_ROOT}/scripts/quality-gate" --tier N`. Checagens e configuração em
|
||||
[../dev-router/references/quality-gate.md](../dev-router/references/quality-gate.md). Os scanners
|
||||
são ponto de partida, não substituem a leitura do código.
|
||||
|
||||
## Fase 1: SCOPE
|
||||
|
||||
1. Delimite o alvo: `git diff --stat <base>` e arquivos não rastreados; front, back ou os dois.
|
||||
2. Registre a stack, os gerenciadores de pacote e os lockfiles (`package-lock.json`, `pnpm-lock.yaml`, `requirements.txt`, `poetry.lock`, `go.mod`, `Cargo.lock`).
|
||||
3. No nível 2 ou mais, mapeie a superfície alcançável: quem chama o código alterado e o que fica exposto (rota, fila, webhook, UI).
|
||||
|
||||
## Fase 2: HUNT (dependências)
|
||||
|
||||
1. `safedeps` nas dependências novas ou alteradas, com versão e ecossistema:
|
||||
`python3 "${CLAUDE_PLUGIN_ROOT}/skills/safedeps/check_deps.py" [email protected]:npm`
|
||||
ou o manifest inteiro: `python3 "${CLAUDE_PLUGIN_ROOT}/skills/safedeps/check_deps.py" requirements.txt`.
|
||||
2. `sca-audit` para alcance (reachability): a função vulnerável é chamada pelo projeto?
|
||||
3. `cve-triage` e `patch-prioritization` para ordenar por CVSS 4.0, EPSS e CISA KEV.
|
||||
4. Separe **alcançável** (bloqueia) de **não alcançável** (risco residual registrado).
|
||||
|
||||
## Fase 3: CONSULT (fontes vivas)
|
||||
|
||||
Responda "há vulnerabilidade nova que eu não conheço?" consultando, com data:
|
||||
|
||||
- OSV.dev (já coberto pelo `safedeps`; batch: `POST https://api.osv.dev/v1/querybatch`).
|
||||
- GitHub Advisory Database: `gh api graphql -f query='{securityAdvisories(first:10, orderBy:{field:PUBLISHED_AT,direction:DESC}){nodes{ghsaId summary severity publishedAt}}}'`.
|
||||
- NVD: `curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=<framework>&resultsPerPage=10"`.
|
||||
- CISA KEV para saber se já há exploração ativa.
|
||||
- Página de security ou changelog do framework em uso.
|
||||
|
||||
Vulnerabilidade nova que afeta a versão do projeto vira achado HIGH, com a versão de correção.
|
||||
|
||||
## Fase 4: CODE REVIEW
|
||||
|
||||
Referências atuais: **OWASP Top 10:2025**, **OWASP API Security Top 10 (2023)**, **ASVS 5.0** e,
|
||||
se a feature chama um LLM, **OWASP LLM Top 10 (2025)**. As skills OWASP vendorizadas ainda citam o
|
||||
Top 10 de 2021; use o mapa 2021 para 2025 em [references/owasp-2025.md](references/owasp-2025.md).
|
||||
|
||||
**Frontend** (`web-security-review`, `code-review-security`):
|
||||
- renderização de HTML com entrada do usuário (`dangerouslySetInnerHTML`, `v-html`, `innerHTML`);
|
||||
- segredo no bundle do cliente; token em `localStorage` (prefira cookie `httpOnly`, `Secure`, `SameSite`);
|
||||
- CSP, links e redirecionamentos montados com entrada do usuário.
|
||||
|
||||
**Backend** (`code-review-security`, `api-security-review`; se instaladas, `bola-detector`, `injection-checker`, `auth-rbac-scaffold`):
|
||||
- autenticação e autorização por objeto (BOLA/IDOR): a query é escopada pelo usuário autenticado?
|
||||
- injeção: SQL, ORM cru, shell, template, path traversal; SSRF (agora dentro de A01);
|
||||
- tratamento de condição excepcional (A10:2025): erro que falha aberto, exceção engolida, stack trace na resposta;
|
||||
- rate limit em escrita e login, CORS com allowlist explícita, segredo nunca em log.
|
||||
|
||||
**Segredos**: `secrets-scan` no diff e no histórico (`git log -p -S "<padrão>"`); o gitleaks do gate cobre os arquivos alterados.
|
||||
|
||||
**Cadeia de suprimentos** (A03:2025): dependência nova justificada, lockfile commitado, scripts de
|
||||
instalação suspeitos, versões fixadas no CI.
|
||||
|
||||
**Nível 3**: rode o `hermaguard` no diff (edge cases, ataque, blast radius) e confira o threat model
|
||||
STRIDE do `wf-architecture`: cada ameaça tem mitigação no código?
|
||||
|
||||
Cada achado: severidade (CRITICAL, HIGH, MEDIUM, LOW), CWE, referência OWASP, `arquivo:linha`,
|
||||
passos de exploração, impacto e correção. Diferencie confirmado de suspeita. "Não achei com grep"
|
||||
não é "não existe": siga o fluxo.
|
||||
|
||||
## Fase 5: REPORT + GATE
|
||||
|
||||
Relatório com: cabeçalho (nível, escopo, padrão ASVS e a linha "consulta de atualização feita em
|
||||
<AAAA-MM-DD> via <fontes>"), resumo por severidade, decisão do gate, os achados, tabela de
|
||||
dependências (pacote, versão, CVE, severidade, corrigida em, alcançável, fonte), veredito do gate de
|
||||
qualidade com o que ficou não verificado, e riscos aceitos.
|
||||
|
||||
Regra do gate: **CRITICAL ou HIGH alcançável em aberto = não aprovado.** MEDIUM entra no próximo
|
||||
ciclo; LOW e residual ficam registrados como risco aceito. Depois de cada correção, rode de novo a
|
||||
checagem que acusou o problema e o gate no mesmo nível.
|
||||
|
||||
## Regras duras
|
||||
|
||||
- Nunca "sem vulnerabilidades" sem consulta a fonte viva com data.
|
||||
- Nunca CVE sem versão afetada e versão corrigida.
|
||||
- Nunca expor um segredo encontrado no relatório: cite arquivo, linha e tipo, com o valor mascarado.
|
||||
- Queries sempre parametrizadas, nunca concatenadas com entrada.
|
||||
- Correção que muda dependência ou mais de 3 arquivos: nova rodada de revisão.
|
||||
@@ -1,48 +0,0 @@
|
||||
# Referências OWASP atuais
|
||||
|
||||
Confira a lista oficial em https://owasp.org/Top10/ se houver dúvida de nome ou ordem.
|
||||
|
||||
## Top 10:2021 (usado pelas skills vendorizadas) para Top 10:2025
|
||||
|
||||
| 2021 | 2025 | Observação |
|
||||
|---|---|---|
|
||||
| A01 Broken Access Control | A01 Broken Access Control | agora inclui SSRF |
|
||||
| A02 Cryptographic Failures | A04 Cryptographic Failures | |
|
||||
| A03 Injection | A05 Injection | XSS continua aqui |
|
||||
| A04 Insecure Design | A06 Insecure Design | |
|
||||
| A05 Security Misconfiguration | A02 Security Misconfiguration | subiu |
|
||||
| A06 Vulnerable and Outdated Components | A03 Software Supply Chain Failures | **ampliada**: build, CI, registro de pacotes, dependências |
|
||||
| A07 Identification and Authentication Failures | A07 Authentication Failures | |
|
||||
| A08 Software and Data Integrity Failures | A08 Software or Data Integrity Failures | |
|
||||
| A09 Security Logging and Monitoring Failures | A09 Security Logging and Alerting Failures | |
|
||||
| A10 Server-Side Request Forgery | (dentro de A01) | |
|
||||
| n/a | A10 Mishandling of Exceptional Conditions | **nova**: erro que falha aberto, exceção engolida, estado inconsistente após falha |
|
||||
|
||||
## API Security Top 10 (2023)
|
||||
|
||||
API1 Broken Object Level Authorization · API2 Broken Authentication · API3 Broken Object Property
|
||||
Level Authorization · API4 Unrestricted Resource Consumption · API5 Broken Function Level
|
||||
Authorization · API6 Unrestricted Access to Sensitive Business Flows · API7 Server Side Request
|
||||
Forgery · API8 Security Misconfiguration · API9 Improper Inventory Management · API10 Unsafe
|
||||
Consumption of APIs.
|
||||
|
||||
## ASVS 5.0 (maio de 2025)
|
||||
|
||||
Checklist por nível, mais de 300 requisitos em 17 capítulos (inclui segurança de frontend web e tokens
|
||||
autocontidos). Use o nível que o `dev-router` definiu:
|
||||
|
||||
- **L1** (nível 2): o mínimo para qualquer aplicação exposta.
|
||||
- **L2** (nível 3): aplicação com dados sensíveis ou transações; o padrão recomendado para a maioria.
|
||||
- **L3**: só com pedido explícito (dado crítico, alto valor).
|
||||
|
||||
Fonte: https://github.com/OWASP/ASVS
|
||||
|
||||
## LLM Top 10 (2025), para features que chamam um LLM
|
||||
|
||||
LLM01 Prompt Injection · LLM02 Sensitive Information Disclosure · LLM03 Supply Chain · LLM04 Data and
|
||||
Model Poisoning · LLM05 Improper Output Handling · LLM06 Excessive Agency · LLM07 System Prompt
|
||||
Leakage · LLM08 Vector and Embedding Weaknesses · LLM09 Misinformation · LLM10 Unbounded Consumption.
|
||||
|
||||
O mais comum em código de aplicação: saída do modelo usada sem validação (LLM05), ferramentas ou
|
||||
permissões demais para o agente (LLM06), segredo ou dado de outro usuário no prompt (LLM02) e custo
|
||||
sem limite (LLM10).
|
||||
@@ -0,0 +1,294 @@
|
||||
---
|
||||
name: wf-architecture
|
||||
description: "Use p/ arquitetura. Fluxo REQ→DESIGN→DOCS→REVIEW."
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
platforms: [linux, macos, windows]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [architecture, design, c4, adr, arc42, threat-model, workflow]
|
||||
---
|
||||
|
||||
# Workflow Arquitetura
|
||||
|
||||
Orquestrador obrigatório para todo trabalho de arquitetura: desenho de
|
||||
sistemas, decisões (ADR), diagramas C4, threat modeling, revisão de
|
||||
arquitetura. Processo de 4 fases com skills fixas.
|
||||
|
||||
## When to Use
|
||||
|
||||
Sempre que a tarefa envolver: arquitetura/design de sistema, escolha de
|
||||
stack, diagramas, ADR, threat model, revisão estrutural, ou "é uma boa
|
||||
ideia arquitetonicamente?".
|
||||
|
||||
## Skills do stack (carregar conforme a fase)
|
||||
|
||||
- Design de sistema (raciocínio): `system-design` (HelloInterview framework)
|
||||
- Diagramas: `c4-architecture` (Context/Container/Component/Deployment)
|
||||
- Documentação: `isaqb-architecture-governance` (arc42 + ADR)
|
||||
- Segurança: `secure-architecture-governance` (STRIDE+CIA, S-ADR)
|
||||
- Qualidade de documentação: `avoid-ai-writing` (remove AI-isms de ADRs,
|
||||
docs de arquitetura, RFCs — registros que vão durar anos não devem soar
|
||||
gerados por IA)
|
||||
- Locais: `security-threat-model`, `architecture-diagram` (SVG),
|
||||
`excalidraw`, `brainstorming`, `writing-plans`
|
||||
|
||||
## Fase 1 — REQUIREMENTS (entender antes de desenhar)
|
||||
|
||||
1. `brainstorming` — explorar intenção, requisitos funcionais e NFRs
|
||||
(latência, escala, disponibilidade, custo) antes de qualquer desenho.
|
||||
2. Identificar entidades centrais, limites, fluxos de dados e atores.
|
||||
3. Estado da arte: checar evolução/relevância de stack/framework.
|
||||
4. VERIFICAR: rodar `hermaguard` conceitual no escopo (o que pode dar
|
||||
errado com esta arquitetura?) e validar com o usuário que cada NFR é
|
||||
mensurável (ex: "p95 < 250ms" e não "rápido").
|
||||
5. Checkpoint: listar requisitos validados e SÓ seguir ao DESIGN com
|
||||
aprovação explícita.
|
||||
6. VERIFICAR (comandos de requisitos): escrever FRs/NFRs em
|
||||
`docs/requirements.md` e rodar `grep -c "NFR" docs/requirements.md`
|
||||
para garantir que TODOS os NFRs viraram critério mensurável.
|
||||
7. VERIFICAR (cobertura): usar `search_files` para confirmar que nenhum
|
||||
requisito levantado ficou órfão (cada FR/NFR tem dono e número).
|
||||
8. VERIFICAR (comandos de estado da arte): consultar fontes atuais para
|
||||
stack/framework antes de decidir — usar `web_search`/consultas técnicas
|
||||
e registrar URL + data no requirements.md (não decidir stack do nada).
|
||||
9. VERIFICAR (pontos de risco): listar os 3 maiores riscos técnicos do
|
||||
escopo e checar com `hermaguard` conceitual se algum deles muda a
|
||||
decisão de arquitetura — risco conhecido não vira surpresa depois.
|
||||
10. CHECKPOINT final da Fase 1: apresentar requisitos para aprovação; sem
|
||||
OK explícito não avança para DESIGN.
|
||||
11. VERIFICAR (comandos de requisitos): `python3 -c "import yaml,sys; yaml.safe_load(open('docs/requirements.md'))"` se requisitos em YAML
|
||||
(ou parser equivalente) para provar que o formato é válido; e
|
||||
`curl -s -o /dev/null -w "%{http_code}" URL` quando houver fonte
|
||||
externa de referência citada.
|
||||
12. VERIFICAR (critério de requisito): conferir que cada requisito tem
|
||||
"critério de aceite" ou número alvo — sem requisito "aberto" sem
|
||||
definição de pronto.
|
||||
|
||||
**Checklist da fase 1 (validar requisitos):**
|
||||
- [ ] Requisitos funcionais e NFRs (latência, escala, disponibilidade, custo)
|
||||
coletados e mensuráveis (ex: "p95 < 250ms" ao invés de "rápido").
|
||||
- [ ] Entidades centrais, limites e atores identificados.
|
||||
- [ ] Estado da arte de stack/framework verificado.
|
||||
- [ ] Requisitos aprovados pelo usuário antes de desenhar.
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA desenhar sem requisito mensurável.
|
||||
- NUNCA inventar NFR ("ser escalável") — exigir número e critério.
|
||||
- NÃO pular a aprovação do usuário para "andar logo".
|
||||
- NÃO decidir stack antes de validar requisitos (viés de ferramenta).
|
||||
|
||||
## Fase 2 — DESIGN (modelar o sistema)
|
||||
|
||||
1. `system-design` — percorrer o framework: requisitos → entidades → API →
|
||||
data flow → high-level design → deep dives. Documentar trade-offs.
|
||||
2. `c4-architecture` — gerar diagramas (Context → Container → Component)
|
||||
em Mermaid/Structurizr. Declarar elementos em ordem de leitura, evitar
|
||||
cruzamentos (research-backed).
|
||||
3. `secure-architecture-governance` — mapear trust boundaries e rodar
|
||||
STRIDE+CIA nas decisões de segurança (auth, dados sensíveis).
|
||||
4. VERIFICAR: rodar `c4-architecture` de novo para audit — cada diagrama
|
||||
tem IDs estáveis, setas com rótulo e <=20 elementos por view? Validar
|
||||
com o usuário antes de documentar.
|
||||
5. VERIFICAR (comandos de design): se Structurizr, validar com
|
||||
`python3 -c "import json; json.load(open('workspace.json'))"` que o
|
||||
modelo é parseável; para Mermaid, conferir que os blocos ```mermaid
|
||||
fecham corretamente (contar crases ímpares = erro).
|
||||
6. VERIFICAR (trade-offs): `grep -c "alternativa" docs/design.md` — cada
|
||||
decisão tem pelo menos 1 alternativa documentada com o porquê?
|
||||
7. VERIFICAR (limites): `grep -n "boundary\|trust" docs/design.md` —
|
||||
trust boundaries explícitos nos diagramas; todo fluxo de dado sensível
|
||||
cruza boundary documentada.
|
||||
8. VERIFICAR (comandos de render): se Mermaid, rodar `npx @mermaid-js/mermaid-cli -i d.mmd -o d.svg` (ou Playwright) para provar que o
|
||||
diagrama renderiza sem erro de sintaxe.
|
||||
9. VERIFICAR (comandos de contagem): `grep -c "component\|container" docs/c4.md` — todo container mapeado tem representação; `wc -l docs/c4.md > 10` garante diagrama não-vazio.
|
||||
10. VERIFICAR (critério de design): confirmar com o usuário que cada view
|
||||
reflete os requisitos da Fase 1 (rastreabilidade view→requisito) e
|
||||
que os trade-offs estão anotados — sem view "solta".
|
||||
|
||||
**Checklist da fase 2 (validar o modelo):**
|
||||
- [ ] `system-design` percorrido: requisitos → entidades → API → data flow →
|
||||
high-level → deep dives.
|
||||
- [ ] Diagramas C4 (Context → Container → Component) com IDs estáveis, setas
|
||||
rotuladas e <=20 elementos por view.
|
||||
- [ ] Trust boundaries mapeados; STRIDE+CIA aplicado nas decisões sensíveis.
|
||||
- [ ] Trade-offs documentados (as 2 soluções alternativas + por quê a escolha).
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA diagramar sem requisitos (Fase 1) — diagrama sem base vira enfeite.
|
||||
- NUNCA deixar seta sem rótulo ou elemento sem ID estável.
|
||||
- NÃO desenhar >20 elementos por view (ilegível = não é documento).
|
||||
- NÃO ignorar trust boundary ao desenhar fluxo de dados sensível.
|
||||
|
||||
## Fase 3 — DOCS (decisões em registros)
|
||||
|
||||
1. `isaqb-architecture-governance` — criar ADR (e S-ADR se decisão for
|
||||
security-driven) com alternativas e consequências. ID estável e
|
||||
reutilizado nos diagramas/docs.
|
||||
2. `avoid-ai-writing` — pós-escrever QUALQUER doc/ADR/RFC: detect+rewrite
|
||||
dos AI-isms (tiers 1/2/3, em-dash, "crucial", "robust", transições
|
||||
genéricas). Arquitetura documentada é lida por anos — não deve soar IA.
|
||||
3. Estrutura de saída: `docs/architecture/` (ADRs, views arc42) e
|
||||
`docs/security/` (S-ADRs, threat models) — sem colisão de nomes.
|
||||
4. NFRs virarem critérios mensuráveis (não frase de efeito).
|
||||
5. VERIFICAR: rodar `avoid-ai-writing` em modo detect em cada ADR (0 AI-isms
|
||||
restantes), confirmar arquivos criados em `docs/architecture/adr/` e
|
||||
`docs/security/adr/` e conferir que IDs de ADR/S-ADR são únicos.
|
||||
6. VERIFICAR (comandos de docs): `ls docs/architecture/adr/ docs/security/adr/`
|
||||
para listar os registros criados e `grep -rL "Consequências" docs/` para
|
||||
achar ADR que esqueceu a seção de consequências.
|
||||
7. VERIFICAR (rastreabilidade): `grep -rc "AD-...-NNN" docs/architecture/`
|
||||
— cada ADR aparece referenciado em pelo menos um diagrama ou decision log.
|
||||
8. VERIFICAR (comandos de docs): confirmar com `find docs/ -name "*.md" -o -name "*.mmd" | sort` que a árvore está organizada; e `wc -l docs/architecture/adr/*.md` para garantir ADR com corpo (não só título).
|
||||
9. VERIFICAR (estilo consistente): `grep -rn "em dash\|—" docs/architecture/adr/ | wc -l` — em-dash em excesso = AI-ism residual (rodar avoid-ai-writing).
|
||||
10. VERIFICAR (comandos de validação de docs): `python3 -m json.tool docs/workspace.json > /dev/null` para Structurizr; `curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/...` se houver visualização local; e `git diff --stat docs/` para conferir o que foi documentado.
|
||||
11. VERIFICAR (critério de aceite de doc): cada ADR responde "por que essa decisão?" e lista consequências — `grep -L "Consequências" docs/architecture/adr/*.md` deve ser vazio.
|
||||
|
||||
**Checklist da fase 3 (documentação):**
|
||||
- [ ] ADR (ou S-ADR se security-driven) criado com alternativas + consequências.
|
||||
- [ ] `avoid-ai-writing` em detect = 0 em cada ADR/RFC.
|
||||
- [ ] Arquivos em `docs/architecture/adr/` e `docs/security/adr/`; IDs únicos.
|
||||
- [ ] NFRs viram critérios mensuráveis no documento (não frase solta).
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA publicar ADR sem alternativas consideradas.
|
||||
- NUNCA publicar doc com AI-isms não tratados.
|
||||
- NÃO criar 2 ADRs para a mesma decisão; NÃO duplicar decisão geral e S-ADR.
|
||||
- NÃO deixar ID solto (decisão precisa ser rastreável ao diagrama/código).
|
||||
|
||||
## Fase 4 — REVIEW (verificar antes de aprovar)
|
||||
|
||||
1. `secure-architecture-governance` — auditar decisões contra ISO/IEC
|
||||
25010 (performance, segurança, confiabilidade, manutenibilidade).
|
||||
2. Threat model final + gaps de trust boundary (unauthenticated internal
|
||||
paths, implicit service trust) priorizados.
|
||||
3. Checar consistência: IDs batem entre diagramas, ADRs e docs?
|
||||
4. Verdict objetivo: Approved / Approved-with-changes / Needs-revision,
|
||||
com evidência por finding — não gosto pessoal.
|
||||
5. VERIFICAR (gate final): rodar `secure-architecture-governance` como
|
||||
checklist, validar a árvore de arquivos com `search_files`, e dar o
|
||||
verdict com base na evidência — se houver gap de security, reverter ao
|
||||
DESIGN.
|
||||
6. VERIFICAR (comandos de revisão): usar `grep -r "AD-...-NNN" docs/` para
|
||||
provar que IDs aparecem nos diagramas, e `git status` para conferir que
|
||||
tudo que foi modelado virou arquivo versionado.
|
||||
7. VERIFICAR (anti-slop de docs): rodar `node .../avoid-ai-writing/detector/validate.js`
|
||||
nos ADRs e `test_quality.py` no workflow — sem regressão.
|
||||
8. VERIFICAR (todos os arquivos versionados): `git status --porcelain` deve
|
||||
mostrar só o esperado; `find docs/ -name "*.md" | wc -l` confere a
|
||||
contagem de docs criados vs planejados na Fase 1.
|
||||
9. VERIFICAR (comandos de evidência por finding): para cada finding,
|
||||
`grep -rn "<id-do-finding>" docs/security/` deve retornar a evidência —
|
||||
sem achado "sem referência".
|
||||
10. VERIFICAR (conclusão): `git diff --stat` + `git status` para conferir
|
||||
o que o ciclo produziu; nada de diagrama/doc não versionado sendo
|
||||
citado como entregue.
|
||||
11. VERIFICAR (comandos de revisão final): rodar `python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py` (gate) e conferir o
|
||||
`git diff --stat` — a revisão só fecha com gate verificado e sem
|
||||
pendências de arquivo.
|
||||
12. VERIFICAR (não-regressão de nomes): conferir que nenhuma skill do stack
|
||||
foi renomeada/quebrada — rodar o harness (G4) e `test_tests.py`.
|
||||
|
||||
**Checklist da fase 4 (revisão):**
|
||||
- [ ] Auditoria contra ISO/IEC 25010 por decisão (performance, segurança,
|
||||
confiabilidade, manutenibilidade).
|
||||
- [ ] Threat model final com gaps de trust boundary priorizados.
|
||||
- [ ] Consistência de IDs: diagramas × ADRs × docs batem.
|
||||
- [ ] Verdict objetivo emitido: Approved / Approved-with-changes /
|
||||
Needs-revision, com evidência por finding.
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA dar verdict "approved" sem evidência em cada finding.
|
||||
- NUNCA ignorar gap de trust boundary (unauthenticated internal path).
|
||||
- NÃO confundir opinião estética com achado — achado exige referência.
|
||||
- NÃO deixar segurança como "nota mental" — virar S-ADR ou finding listado.
|
||||
|
||||
## Comandos de Verificação (executáveis)
|
||||
|
||||
```bash
|
||||
# Gate de qualidade do próprio workflow (sem regressão)
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py
|
||||
|
||||
# Verifica consistência da documentação (IDs estáveis em diagramas/docs)
|
||||
# (skill isaqb-architecture-governance + c4-architecture)
|
||||
|
||||
# Threat model: auditoria STRIDE+CIA contra ISO 25010
|
||||
# (skill secure-architecture-governance)
|
||||
|
||||
# Audit de AI-isms nos ADRs/RFCs (detect = só mostra)
|
||||
node ~/.hermes/skills/avoid-ai-writing/detector/validate.js adr.md adr-limp.md
|
||||
```
|
||||
|
||||
## Critérios de Aceite (checklist não-regressivo)
|
||||
|
||||
- [ ] Fase 1 concluída com requisitos/NFRs mensuráveis aprovados pelo usuário.
|
||||
- [ ] Fase 2 gerou diagramas C4 com IDs estáveis e rótulos; validados.
|
||||
- [ ] Fase 3 gerou ADR/S-ADR em `docs/architecture/` e `docs/security/`;
|
||||
`avoid-ai-writing` detect = 0 AI-isms.
|
||||
- [ ] Fase 4 auditou contra ISO 25010 + threat model; verdict emitido.
|
||||
- [ ] Nenhuma skill do stack removida do arquivo.
|
||||
- [ ] Rodar `python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py`
|
||||
→ exit 0 (sem regressão).
|
||||
|
||||
## Exemplos de execução por fase (comandos reais)
|
||||
|
||||
Fase 1 — requisitos estruturados:
|
||||
|
||||
```bash
|
||||
cat > docs/requirements.md <<'EOF'
|
||||
# Requisitos
|
||||
FR1: usuário cria e gerencia listas
|
||||
NFR1: p95 < 250ms (API)
|
||||
NFR2: disponibilidade 99.9%
|
||||
NFR3: custo mensal < R$ 500
|
||||
EOF
|
||||
```
|
||||
|
||||
Fase 2 — diagrama C4 (definir estrutura antes):
|
||||
|
||||
```bash
|
||||
# Estrutura do C4 em texto (depois renderizar com c4-architecture/Mermaid)
|
||||
cat > docs/c4.md <<'EOF'
|
||||
Context: [Usuário] -> [Sistema X]
|
||||
Container: [Web] -> [API] -> [Postgres] -> [Redis]
|
||||
EOF
|
||||
```
|
||||
|
||||
Fase 3 — ADR curto:
|
||||
|
||||
```bash
|
||||
# Criar ADR em docs/architecture/adr/0001-*.md (isaqb-architecture-governance)
|
||||
# S-ADR em docs/security/adr/ quando a decisão for security-driven
|
||||
```
|
||||
|
||||
Fase 4 — validação do conjunto:
|
||||
|
||||
```bash
|
||||
# Consistência de IDs entre diagramas, ADRs e docs (search_files)
|
||||
grep -r "ADR-" docs/ | wc -l # cada referência existe
|
||||
grep -rn "S-ADR" docs/security/ | wc -l # decisões security-driven têm S-ADR
|
||||
# "audite a arquitetura" (secure-architecture-governance: ISO 25010)
|
||||
python3 -m json.tool docs/workspace.json > /dev/null # Structurizr válido?
|
||||
# "rode o avoid-ai nos ADRs" (modo detect)
|
||||
node ~/.hermes/skills/avoid-ai-writing/detector/validate.js adr.md adr-limp.md
|
||||
find docs/ -name "*.md" -o -name "*.mmd" | sort # árvore organizada
|
||||
```
|
||||
|
||||
Gates do workflow (anti-regressão):
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_tests.py
|
||||
```
|
||||
|
||||
## Regras duras
|
||||
|
||||
- NUNCA diagramar antes de requisitos claros (Fase 1).
|
||||
- NUNCA entregar ADR sem alternativas consideradas e consequências.
|
||||
- NUNCA publicar ADR/RFC/doc com AI-isms não tratados (passar por
|
||||
`avoid-ai-writing` antes de dar como entregue).
|
||||
- Decisão com load-bearing security → S-ADR em `docs/security/`, nunca nos
|
||||
ADRs gerais.
|
||||
- Diagrama tem que ter declaração em ordem de leitura + alvo de
|
||||
cruzamentos — legibilidade é requisito, não luxo.
|
||||
@@ -0,0 +1,281 @@
|
||||
---
|
||||
name: wf-backend
|
||||
description: "Use p/ backend/API. Fluxo PLAN→BUILD→SECURITY→TEST."
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
platforms: [linux, macos, windows]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [backend, api, security, rest, sql, auth, workflow]
|
||||
---
|
||||
|
||||
# Workflow Backend/API
|
||||
|
||||
Orquestrador obrigatório para TODO trabalho backend: APIs REST/GraphQL,
|
||||
autenticação, banco de dados, integrações, serviços, jobs. Processo de 4
|
||||
fases com skills fixas.
|
||||
|
||||
## When to Use
|
||||
|
||||
Sempre que a tarefa envolver servidor: criar/editar endpoints, auth, schema
|
||||
DB, integrações, filas, webhooks, ou revisar segurança de backend/models.
|
||||
|
||||
## Skills do stack (carregar conforme a fase)
|
||||
|
||||
- Segurança multi-stack: `secure-coding` (15 stacks)
|
||||
- Segurança de API (OWASP): `api-security-review`, `bola-detector`,
|
||||
`auth-rbac-scaffold`, `injection-checker`, `openapi-hardener`,
|
||||
`security-test-generator`
|
||||
- Bug hunting adversarial: `hermaguard` (pre-scan + 3 agentes — edge cases,
|
||||
ataque, blast radius; read-only, encontra o que está quebrado)
|
||||
- Locais: `fastapi-uvicorn-wiring` (Python), `rest-graphql-debug`,
|
||||
`software-code-quality`, `dependency-cve-audit`, `security-best-practices`,
|
||||
`test-driven-development`, `network-audit`
|
||||
|
||||
## Fase 1 — PLAN (contrato antes de código)
|
||||
|
||||
1. Definir API contract primeiro: rotas, métodos, payloads, status codes,
|
||||
erros (RFC 9457 / RFC 7807), rate limit, versionamento.
|
||||
2. Validar schema: `openapi-hardener` — OpenAPI/Zod/Pydantic saneado antes
|
||||
de escrever handler.
|
||||
3. Definir modelo de dados + migração (idempotente) ANTES de query.
|
||||
4. Decidir auth: sessão/OAuth/JWT com base no contexto; documentar.
|
||||
5. Approved do contrato pelo usuário antes de build.
|
||||
6. VERIFICAR: validar o contrato com `openapi-hardener` (schemas coerentes),
|
||||
conferir rate limit + versionamento definidos, e confirmar que o modelo
|
||||
de dados tem migração idempotente antes de construir.
|
||||
7. VERIFICAR (comandos de plano): usar `curl` para provar o contrato contra
|
||||
um stub (ex: `curl -i localhost:PORT/health`) e `git log`/branches para
|
||||
confirmar que o contrato fica versionado no repo.
|
||||
8. CHECKPOINT: registrar contrato em `docs/contract.md` (ou OpenAPI) para as
|
||||
fases seguintes referenciarem.
|
||||
9. VERIFICAR (comandos de contrato completo): `grep -c "->" docs/contract.md` — todas as rotas têm status codes esperados; e `grep -c "429\|rate" docs/contract.md` para provar que rate limit está no contrato.
|
||||
10. VERIFICAR (auth no contrato): `grep -rn "auth\|bearer\|session" docs/contract.md` — decidir e documentar o modelo de auth ANTES do build (não deixar para a Fase 2/3).
|
||||
11. VERIFICAR (validação de contrato): se OpenAPI, `python3 -m json.tool docs/openapi.json > /dev/null` para provar que o schema é válido em JSON.
|
||||
|
||||
**Checklist da fase 1 (validar antes do build):**
|
||||
- [ ] Rotas/métodos/payloads/status codes definidos e aprovados.
|
||||
- [ ] Erros no padrão RFC 9457/7807; rate limit e versionamento especificados.
|
||||
- [ ] `openapi-hardener` validou os schemas (Zod/Pydantic/OpenAPI).
|
||||
- [ ] Migração de dados idempotente desenhada antes de qualquer query.
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA escrever handler sem contrato aprovado.
|
||||
- NUNCA deixar schema "se inferir na mão livre" — definir e validar.
|
||||
- NÃO esquecer rate limit/versionamento na fase 1 (correção é cara depois).
|
||||
- NÃO misturar payload v2 com contrato v1 sem versionamento explícito.
|
||||
|
||||
## Fase 2 — BUILD (escrever com defaults seguros)
|
||||
|
||||
1. `secure-coding` — aplicar padrões da stack em uso (Node/FastAPI/Go/etc).
|
||||
2. Validação na borda (Zod/Pydantic), erros estruturados, timeouts em
|
||||
clientes HTTP e DB, idempotência em mutações.
|
||||
3. Argon2id p/ senhas, httpOnly cookies p/ sessões, nunca logar segredos.
|
||||
4. Type-safe boundaries; evitar `any`/`as` descontrolados em TS.
|
||||
5. VERIFICAR: rodar `secure-coding` como checklist da stack (validação na
|
||||
borda, timeouts, idempotência) e `rest-graphql-debug` para confirmar
|
||||
que os status codes/mensagens batem com o contrato da Fase 1.
|
||||
6. VERIFICAR (timeouts): para todo cliente HTTP do serviço, confirmar
|
||||
`timeout` definido — `grep -rn "requests\|fetch(" src/ | wc -l` contra
|
||||
o número de `timeout` presentes; igual = OK.
|
||||
7. VERIFICAR (redação de segredos): `grep -rniE "password|secret|token" src/ --include="*.py" --include="*.ts" | grep -i "log\|print"` — zero
|
||||
hits é o esperado (nenhum segredo sendo logado).
|
||||
8. VERIFICAR (queries): conferir que toda listagem tem `LIMIT` — checar
|
||||
com `search_files` as chamadas sem paginação antes de seguir.
|
||||
|
||||
**Checklist da fase 2 (build seguro):**
|
||||
- [ ] Validação na borda (Zod/Pydantic) em TODA entrada externa.
|
||||
- [ ] Erros estruturados; timeouts em clientes HTTP e DB.
|
||||
- [ ] Mutações idempotentes; transações onde há múltiplas gravações.
|
||||
- [ ] Senhas com Argon2id; sessões httpOnly; nenhum segredo em log.
|
||||
- [ ] Type-safe boundaries; sem `any` solto no que atravessa a fronteira.
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA logar senha/token/dado sensível.
|
||||
- NUNCA confiar em input externo sem validação na borda.
|
||||
- NÃO fazer query sem LIMIT/paginação em listas não limitadas.
|
||||
- NÃO engolir erro com `except: pass` / catch vazio (mascara falha).
|
||||
|
||||
## Fase 3 — SECURITY (auditar tudo)
|
||||
|
||||
1. `bola-detector` em rotas com `:id`/`findUnique` (Object-Level Authz).
|
||||
2. `auth-rbac-scaffold` em qualquer fluxo de JWT/login/roles.
|
||||
3. `injection-checker` em SQL/ORM/shell/templates.
|
||||
4. `api-security-review` no conjunto final (OWASP API Top 10).
|
||||
5. `dependency-cve-audit` nas deps do projeto (local).
|
||||
6. VERIFICAR: consolidar achados P0/P1 por severidade, validar que cada
|
||||
rota sensível tem auth + ownership-scope, e re-auditar depois de
|
||||
qualquer fix (nada de despachar cego).
|
||||
7. VERIFICAR (ownership): para rotas com `:id`, conferir que a query usa o
|
||||
ID autenticado — `grep -rn "user_id\|user.id\|req.user" src/ | wc -l`
|
||||
>= número de rotas protegidas.
|
||||
8. VERIFICAR (rate limit): `grep -rniE "ratelimit|rate_limit|limiter" src/`
|
||||
— toda rota de escrita tem rate limit de referência; se ausente, anotar
|
||||
como P2.
|
||||
9. VERIFICAR (headers/CORS): conferir CSP/CORS configurados (CORS
|
||||
allowlist explícita, não `*` com credenciais); `grep -rn "access-control\|csp\|content-security" src/`.
|
||||
10. VERIFICAR (re-audit pós-fix): após corrigir P0/P1, re-rodar
|
||||
`bola-detector` + `api-security-review` e confirmar que os achados
|
||||
sumiram (não só mudaram de arquivo).
|
||||
11. VERIFICAR (critério de aceite de segurança): para cada achado P0/P1,
|
||||
reexecutar o teste que o reproduzia e confirmar que agora passa;
|
||||
manter o PoC no repositório de testes.
|
||||
12. VERIFICAR (comandos de confirmação): `grep -c "auth\|ownership" src/`
|
||||
deve ser >= número de rotas que mexem em dado de usuário — cada rota
|
||||
sensível tem o controle por objeto documentado no código.
|
||||
|
||||
**Checklist da fase 3 (segurança):**
|
||||
- [ ] `bola-detector` limpo em rotas com `:id` (object-level authz).
|
||||
- [ ] Auth/RBAC (`auth-rbac-scaffold`) cobrindo roles e sessions.
|
||||
- [ ] `injection-checker` limpo (SQL/ORM/shell/templates).
|
||||
- [ ] `api-security-review` sem P0/P1 (OWASP API Top 10).
|
||||
- [ ] `dependency-cve-audit` sem CVEs críticas pendentes.
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA definir query escopada por outro ID que não o autenticado.
|
||||
- NUNCA aceitar finding P0/P1 sem evidência de correção.
|
||||
- NÃO revisar segurança só nos endpoints novos — escopar também os existentes.
|
||||
- NÃO tratar auditoria como checklist burocrático: achado não resolvido bloqueia.
|
||||
|
||||
## Fase 4 — TEST + BUG HUNT (provar antes de entregar)
|
||||
|
||||
1. `hermaguard` no diff do backend — dispachar os 3 agentes adversariais
|
||||
(Edge Case Hunter, Adversarial Reviewer, Blast Radius). Esperar falhar:
|
||||
NUNCA parar em "parece ok"; exigir cobertura de edge cases, races,
|
||||
rollback safety e schema drift. Read-only (não corrige — acha).
|
||||
2. `test-driven-development`: testar o que valida/auth/idempotência.
|
||||
3. `security-test-generator`: testes de segurança (authz negativa,
|
||||
payloads malformados, rate limit).
|
||||
4. Rodar suíte + lint + typecheck; status real, não suposição.
|
||||
5. Corrigir os findings P0/P1 do hermaguard como tarefas separadas; rodar
|
||||
hermaguard de novo se o fix mudou >3 arquivos.
|
||||
6. Relatório: contrato entregue, achados de segurança P0/P1/P2 resolvidos,
|
||||
bugs caçados pelo hermaguard (por severidade), testes passando (contagem).
|
||||
7. VERIFICAR (não-regressão QA): rodar `test_quality.py` (gate do workflow)
|
||||
— score não pode cair em relação ao baseline.
|
||||
8. VERIFICAR (cobertura de classes): para rolls em produção, conferir que
|
||||
o hermaguard cobriu edge cases, races, rollback safety e schema drift —
|
||||
não aceitar "sem achados" sem essa cobertura.
|
||||
9. VERIFICAR (efeito colateral): se a mudança alterou contrato/DB, rodar
|
||||
`rest-graphql-debug` para confirmar status codes e mensagens sem
|
||||
regressão.
|
||||
10. VERIFICAR (cobertura de testes real): `python3 -m pytest --cov=app -q`
|
||||
(ou `npm test -- --coverage`) — anotar % de cobertura e garantir que
|
||||
os módulos críticos (auth, validação, idempotência) têm pelo menos
|
||||
smoke test.
|
||||
11. VERIFICAR (migração reversível): se houve schema change, rodar
|
||||
`alembic downgrade -1`/equivalente em dev para provar rollback; e
|
||||
`git diff --stat` para conferir o escopo do que está indo pra frente.
|
||||
12. VERIFICAR (config por ambiente): `grep -rn "getenv\|os.environ\|process.env" src/` — toda configuração via env, sem valores hardcoded que
|
||||
vazam para prod.
|
||||
13. VERIFICAR (comandos de entrega): `python3 -m pytest -q && npm run build` (ou stack equivalente) como smoke final; e `git status` para
|
||||
confirmar que os artefatos de teste não vazaram pro repo.
|
||||
14. VERIFICAR (critério de entrega): relatório final só fecha quando cada
|
||||
item do checklist da Fase 4 está marcado — sem exceção de última hora
|
||||
não auditada.
|
||||
|
||||
**Checklist da fase 4 (gate de entrega):**
|
||||
- [ ] `hermaguard` rodado; findings CRITICAL/HIGH zero ou com plano de fix.
|
||||
- [ ] TDD: testes de validação, auth e idempotência verdes.
|
||||
- [ ] `security-test-generator`: authz negativa, payloads mal, rate limit.
|
||||
- [ ] Suíte + lint + typecheck verdes (contagem real, não suposição).
|
||||
- [ ] Findings P0/P1 corrigidos e re-verificados (hermaguard rodado de novo
|
||||
se o fix alterou >3 arquivos).
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA rodar hermaguard em código com testes quebrados (polui o review).
|
||||
- NUNCA dar "suíte passando" sem ter executado a suíte.
|
||||
- NÃO aceitar "hermaguard não achou nada" sem exigir a cobertura por classe
|
||||
(edge cases, races, rollback, schema drift).
|
||||
- NÃO reportar achado P0/P1 como resolvido sem a verificação de volta.
|
||||
|
||||
## Comandos de Verificação (executáveis)
|
||||
|
||||
```bash
|
||||
# Gate de qualidade do próprio workflow (sem regressão)
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py
|
||||
|
||||
# Testes + meta-testes (prova que o harness detecta regressão)
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_tests.py
|
||||
|
||||
# Audit OWASP de um endpoint/controller
|
||||
# (skill api-security-review) — veja a skill
|
||||
|
||||
# CVE nas deps do projeto
|
||||
# (skill dependency-cve-audit) — veja a skill
|
||||
|
||||
# Bug hunt adversarial no diff (read-only)
|
||||
# (skill hermaguard: "hermaguard this")
|
||||
```
|
||||
|
||||
## Critérios de Aceite (checklist não-regressivo)
|
||||
|
||||
- [ ] Contrato aprovado (PLAN) antes de qualquer handler.
|
||||
- [ ] BUILD com defaults seguros; `secure-coding` validado na stack.
|
||||
- [ ] SECURITY: bola/auth/injection/api-review/CVE rodados; P0/P1 resolvidos.
|
||||
- [ ] TEST: hermaguard + TDD + security-test-generator; suíte verde com
|
||||
contagem real.
|
||||
- [ ] Nenhuma skill do stack removida do arquivo.
|
||||
- [ ] Rodar `python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py`
|
||||
→ exit 0 (sem regressão).
|
||||
|
||||
## Exemplos de execução por fase (comandos reais)
|
||||
|
||||
Fase 1 — validar contrato antes de codar:
|
||||
|
||||
```bash
|
||||
# Lista de rotas planejadas (contrato em aberto)
|
||||
cat > docs/contract.md <<'EOF'
|
||||
POST /api/v1/users -> 201 | 400 | 401 | 409 | 429
|
||||
GET /api/v1/users/:id -> 200 | 404 | 403
|
||||
Erros: RFC 9457 (application/problem+json)
|
||||
EOF
|
||||
```
|
||||
|
||||
Fase 2 — build seguro (exemplo FastAPI + Pydantic):
|
||||
|
||||
```bash
|
||||
# Rodar a stack e conferir a OpenAPI gerada
|
||||
uvicorn app.main:app --reload # ou npm run dev
|
||||
curl -s localhost:8000/openapi.json | python3 -m json.tool > /tmp/openapi.json
|
||||
python3 -c "import sys,json; d=json.load(open('/tmp/openapi.json')); print('schemas:', len(d.get('components',{}).get('schemas',{})))"
|
||||
|
||||
# Validação de contrato contra a OpenAPI real
|
||||
curl -s localhost:8000/health -i | head -5 # status codes do health
|
||||
curl -s -X POST localhost:8000/api/v1/users -d '{}' -i | head -5 # 400 esperado
|
||||
```
|
||||
|
||||
Fase 3 — auditoria de segurança:
|
||||
|
||||
```bash
|
||||
# Rodar as skills de auditoria OWASP (veja cada skill p/ uso exato)
|
||||
# bola-detector: "audite rotas com :id" / auth-rbac-scaffold: "valide o fluxo de login"
|
||||
# injection-checker: "escaneie SQL/ORM" / api-security-review: "revisão OWASP do conjunto"
|
||||
# dependency-cve-audit: "audite as dependências" (skill local)
|
||||
```
|
||||
|
||||
Fase 4 — testes e bug hunt:
|
||||
|
||||
```bash
|
||||
python3 -m pytest -v # ou npm test
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py # gate do workfl
|
||||
# "hermaguard this" (bug hunt adversarial no diff)
|
||||
```
|
||||
|
||||
Gates anti-regressão (rodar junto com o fluxo):
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_tests.py # teste dos testes
|
||||
# verifica que o harness DETECTA regressão (mutações sempre derrubam o score)
|
||||
python3 -m pytest tests/security/ -x # se existir suíte de segurança
|
||||
```
|
||||
|
||||
## Regras duras
|
||||
|
||||
- NUNCA escrever handler antes do contrato aprovado.
|
||||
- NUNCA entregar endpoint sem passar pelas 4 fases (security + testes +
|
||||
hermaguard).
|
||||
- Rota que mexe em dado do usuário → ownership-check obrigatório (query
|
||||
escopada ao ID autenticado).
|
||||
- Tests vermelhos = fixar primeiro; hermaguard não roda em código quebrado.
|
||||
@@ -0,0 +1,311 @@
|
||||
---
|
||||
name: wf-frontend
|
||||
description: "Use p/ frontend/UI. Fluxo DESIGN→BUILD→REVIEW."
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
platforms: [linux, macos, windows]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [frontend, ui, ux, design, react, tailwind, workflow]
|
||||
---
|
||||
|
||||
# Workflow Frontend/Design
|
||||
|
||||
Orquestrador obrigatório para TODO trabalho de frontend, UI, UX ou design
|
||||
(componentes, páginas, landing, apps, re-design, design system). Processo de
|
||||
3 fases com skills fixas por fase.
|
||||
|
||||
## When to Use
|
||||
|
||||
Sempre que a tarefa envolver interface do usuário ou GERAÇÃO DE PDF:
|
||||
- Frontend/UI: criar/editar componentes React/Vue/Svelte, páginas, layouts,
|
||||
estilos, animações, acessibilidade, qualquer pedido de "design"
|
||||
- **PDF/DOCUMENTOS VISUAIS: SEMPRE via este workflow** (regra do Evandro,
|
||||
ago/2026) — NUNCA reportlab platypus raw (estoura bordas em tabelas
|
||||
longas). Usar HTML+CSS → Playwright headless (`pdf-generator`,
|
||||
`html-css-print-engineer`, `pdf-render-qa`). Design primeiro, depois
|
||||
render, depois QA visual (vision_analyze) antes de entregar.
|
||||
- Vale para apps novos e refactors.
|
||||
|
||||
## Skills do stack (carregar conforme a fase)
|
||||
|
||||
- Direção visual / anti-AI-slop: `frontend-design` (Anthropic), `web-design`,
|
||||
`impeccable` (pbakaus — 23 comandos de design: craft/shape/audit/polish/
|
||||
animate/live; 59 regras determinísticas anti-slop), e locais
|
||||
`frontend-award-tier`, `claude-design`, `popular-web-designs`,
|
||||
`anti-ai-slop`
|
||||
- React/Next performance + composição: `vercel-react-best-practices`,
|
||||
`vercel-composition-patterns`
|
||||
- Motion: `animate` (emilkowalski)
|
||||
- Bug hunting: `hermaguard` (pre-scan + 3 agentes adversariais; read-only)
|
||||
- Qualidade de texto/UI copy: `avoid-ai-writing` (remove AI-isms de microcopy,
|
||||
textos de componente, a11y labels, README de frontend)
|
||||
- Esboço rápido / docs visuais: `sketch`, `design-md` (locais)
|
||||
- PDF de qualidade: `pdf-generator` (HTML+CSS LaTeX → Playwright),
|
||||
`html-css-print-engineer`, `pdf-render-qa` (QA), `professional-pdf-director`,
|
||||
`pdf-visual-designer` — todo PDF passa por aqui
|
||||
|
||||
## Fase 1 — DESIGN (decidir antes de codar)
|
||||
|
||||
1. Carregar `frontend-design` + `frontend-award-tier` + `anti-ai-slop`.
|
||||
2. Definir direção visual explícita: paleta, tipografia, espaçamento,
|
||||
densidade, dark/light — NUNCA gerar "default AI slop".
|
||||
3. Se for re-design/variação: `claude-design`/`popular-web-designs` para
|
||||
basear em design systems reais (Stripe/Linear/Vercel) e fugir do genérico.
|
||||
4. Documentar decisões em `DESIGN.md` (padrão `design-md` se aplicável).
|
||||
5. Parar e validar a direção com o usuário ANTES de implementar.
|
||||
6. VERIFICAR: rodar `avoid-ai-writing` em detect no DESIGN.md (0 AI-isms) e
|
||||
confirmar que a direção está registrada (paleta + tipografia + density)
|
||||
antes de codar.
|
||||
7. VERIFICAR (gate visual): se for PDF/artefato visual, renderizar um
|
||||
rascunho e inspecionar com `vision_analyze` ANTES de avançar — validar
|
||||
que a direção visual de fato foi aplicada e nada está fora de margem.
|
||||
8. CHECKPOINT: gravar decisões (paleta final, fonte, density) num
|
||||
`DESIGN.md` com versionamento, para as fases seguintes referenciarem.
|
||||
9. VERIFICAR (comandos de direção): conferir que `DESIGN.md` existe e tem
|
||||
conteúdo — `wc -l DESIGN.md` > 5; e que as cores/fontes estão anotadas
|
||||
(`grep -c "paleta\|fonte\|spacing" DESIGN.md` >= 3).
|
||||
10. VERIFICAR (anti-slop real): rodar o `anti-ai-slop` no direção proposta
|
||||
antes de mostrar ao usuário — detectar gradiente/glassmorphism
|
||||
prematuramente economiza retrabalho.
|
||||
11. VERIFICAR (comandos de design): validar a direção também contra a
|
||||
ferramenta de design usada — `npx`/CLI do design system (ex:
|
||||
`pnpm tailwindcss` se Tailwind) confirmando tokens aplicáveis; e
|
||||
checar com `grep -rn "palette\|tokens\|--color" DESIGN.md` que os
|
||||
tokens ficaram declarados.
|
||||
|
||||
**Checklist da fase 1 (validar todos antes de seguir):**
|
||||
- [ ] Direção visual definida e DOCUMENTADA (paleta, tipografia, spacing).
|
||||
- [ ] Anti-slop: nenhum padrão "default IA" (gradiente, glassmorphism, ícones genéricos).
|
||||
- [ ] `DESIGN.md` existe e está aprovado pelo usuário.
|
||||
- [ ] `avoid-ai-writing` detect = 0 no DESIGN.md.
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA pular a definição de direção visual "só para ver como fica".
|
||||
- NUNCA aprovar design sem a confirmação explícita do usuário.
|
||||
- NÃO misturar 2 design systems na mesma tela; se for variação, basear em um só.
|
||||
|
||||
## Fase 2 — BUILD (implementar com padrões de produção)
|
||||
|
||||
1. Carregar `vercel-react-best-practices` + `vercel-composition-patterns`
|
||||
(React) — aplicar regras de performance (waterfalls, bundle, re-renders)
|
||||
e composição (compound components, sem boolean-prop explosion).
|
||||
2. Componentes acessíveis por padrão (WCAG): semântica HTML, foco,
|
||||
contraste — sem depender de ARIA desnecessário.
|
||||
3. Animações só quando agregam: `animate` — propósito, duração, easing,
|
||||
reduzir motion para prefers-reduced-motion.
|
||||
4. Responsivo mobile-first; estados vazios/loading/erro em todo fluxo.
|
||||
5. VERIFICAR: rodar build + lint incrementalmente a cada componente
|
||||
(npm/pnpm build) e `vercel-react-best-practices` como checklist de perf
|
||||
durante o build — não deixar para o fim.
|
||||
6. VERIFICAR (a11y atômica): para cada componente novo, conferir semântica
|
||||
HTML, gerenciamento de foco e contraste ANTES de considerar pronto —
|
||||
acessibilidade é parte do build, não polimento final.
|
||||
7. VERIFICAR (perf real): medir com DevTools/Lighthouse por tela-chave
|
||||
(LCP, INP, CLS) e registrar os números no relatório de perf.
|
||||
8. VERIFICAR (composição limpa): `grep -rn "is[A-Z]\|show[A-Z]" src/components`
|
||||
para caçar boolean-prop explosion (deve ser 0 ou poucos intencionais);
|
||||
se houver, refatorar para compound components.
|
||||
9. VERIFICAR (animações justificadas): para cada `animate`/motion adicionada,
|
||||
conferir com `grep -rn "animate\|transition\|motion" src/` que há propósito
|
||||
(hover/estado/entrada) — animação decorativa sem função = cortar.
|
||||
10. VERIFICAR (limpeza): `git status` e `git diff --stat` antes de fechar —
|
||||
nada de arquivo órfão/debug commitado.
|
||||
11. VERIFICAR (critério de aceite do componente): cada componente novo
|
||||
confirma o critério definido na Fase 1 (estado correto, a11y ok, perf
|
||||
ok) antes de passar para o próximo — validar item a item do checklist.
|
||||
12. VERIFICAR (comandos de estado): conferir que loading/erro/vazio são
|
||||
tratados em cada fluxo — `grep -rn "loading\|isLoading\|error\|empty" src/`
|
||||
para provar a presença nos componentes que interagem com dados.
|
||||
13. VERIFICAR (anti-padrão visual final): percorrer os componentes com
|
||||
`anti-ai-slop` de novo ao fechar a fase — sombra/glassmorphism que
|
||||
"escapou" no scroll é regressão de qualidade.
|
||||
14. VERIFICAR (critério de aceite): validar que cada item do checklist da
|
||||
fase está marcado (sem checkbox em branco) antes de declarar BUILD
|
||||
concluído.
|
||||
|
||||
**Checklist da fase 2 (validar antes da REVIEW):**
|
||||
- [ ] Build e lint verdes (`npm run build` / `npx tsc --noEmit`).
|
||||
- [ ] Componentes acessíveis (semântica HTML, foco, contraste) — auditar com
|
||||
`frontend-design`/a11y antes de considerar pronto.
|
||||
- [ ] Estados vazios/loading/erro presentes em todo fluxo.
|
||||
- [ ] Animações com propósito + `prefers-reduced-motion` respeitado.
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA usar `any`/forçar cast em TS para "fazer passar".
|
||||
- NUNCA animar sem propósito — animação decorativa gratuita é anti-padrão.
|
||||
- NÃO implementar responsivo só no final; mobile-first desde o início.
|
||||
- NÃO duplicar lógica de estado em vários componentes (compor, não copiar).
|
||||
|
||||
## Fase 3 — REVIEW (verificar antes de entregar)
|
||||
|
||||
1. `anti-ai-slop` sobre o resultado (gradientes-texto, glassmorphism,
|
||||
sombras genéricas, layout previsível).
|
||||
2. `avoid-ai-writing` sobre textos entregues ao usuário: microcopy, labels,
|
||||
empty states, mensagens de erro, README/descrições — remover AI-isms
|
||||
(em-dash em excesso, "robust", "seamless", "let's dive in", etc).
|
||||
3. `hermaguard` no diff de frontend (JS/TS/TSX/CSS): caça bugs adiversariais
|
||||
no código produzido (lógica de estado, efeitos, a11y que quebra).
|
||||
4. `vercel-react-best-practices` como checklist de perf (LCP/INP/CLS,
|
||||
bundle, imagem, Suspense).
|
||||
5. Rodar build + lint + testes do projeto; corrigir o que quebrar.
|
||||
6. Relatório curto: o que foi decidido na Fase 1, entregue na 2, verificado
|
||||
na 3.
|
||||
7. VERIFICAR (regressão visual): se houver, comparar screenshots do antes
|
||||
vs depois (ou `vision_analyze` no render) para garantir que nada quebrou
|
||||
de layout.
|
||||
8. VERIFICAR (não-regressão QA): rodar `test_quality.py` (gate do workflow)
|
||||
e conferir que o score do stack não caiu.
|
||||
9. VERIFICAR (hermaguard re-audit): se o REVIEW mudou >3 arquivos, rodar
|
||||
`hermaguard` no novo diff antes de fechar.
|
||||
10. VERIFICAR (bundle/perf real): rodar `npm run build` e conferir tamanho
|
||||
dos chunks (`ls -lh dist/static/*.js`), anotar LCP/INP/CLS se houver
|
||||
Lighthouse.
|
||||
11. VERIFICAR (html válido): para PDF/páginas, validar o HTML antes do
|
||||
render — `python3 -c "from html.parser import HTMLParser; ...
|
||||
"` (ou parser disponível) e `grep -c "</t" arquivo.html` = 0 (sem tag
|
||||
truncada).
|
||||
12. VERIFICAR (comandos de acessibilidade): rodar auditoria de a11y no
|
||||
render final — `npx @axe-core/cli http://localhost:PORT` (se houver
|
||||
dev server) ou `pa11y`/equivalente; anotar violações sérias.
|
||||
13. VERIFICAR (contraste verificado): para paleta dark/light, conferir
|
||||
contraste com ferramenta (ex: `npx contrast-checker` ou manual) —
|
||||
textos principais >= 4.5:1.
|
||||
|
||||
**Checklist da fase 3 (gate de entrega):**
|
||||
- [ ] `anti-ai-slop` aplicado e sem ressalvas no resultado final.
|
||||
- [ ] `avoid-ai-writing` detect = 0 em microcopy/labels/errors/README.
|
||||
- [ ] `hermaguard` rodado no diff (0 findings CRITICAL/HIGH pendentes).
|
||||
- [ ] `vercel-react-best-practices` auditado (LCP/INP/CLS, bundle, imagem).
|
||||
- [ ] Build + lint + testes verdes com contagem real.
|
||||
- [ ] Para PDF: renderização via Playwright + `vision_analyze` sem corte.
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA dar como entregue com build vermelho.
|
||||
- NUNCA ignorar finding CRITICAL/HIGH do hermaguard "para depois".
|
||||
- NÃO pular o QA visual de PDF (conteúdo cortado nas bordas = entrega quebrada).
|
||||
- NÃO entregar relatório sem os itens do checklist marcados.
|
||||
|
||||
## Comandos de Verificação (executáveis)
|
||||
|
||||
Gate de qualidade do stack (rodar SEMPRE antes de entregar):
|
||||
|
||||
```bash
|
||||
# 1) Não-regressão: valida que os workflows continuam íntegros
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py
|
||||
|
||||
# 2) Testes-dos-testes: prova que o harness detecta regressão
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_tests.py
|
||||
```
|
||||
|
||||
QA visual de PDF — renderizar páginas e inspecionar bordas (nada cortado):
|
||||
|
||||
```bash
|
||||
cd ~/.hermes/skills/productivity/pdf/scripts
|
||||
python pdf_page_image.py /caminho/do.pdf --pages 1-3 --dpi 150 --out-dir /tmp/qa
|
||||
# depoiss: vision_analyze em cada PNG — margens ok, sem truncamento
|
||||
```
|
||||
|
||||
Qualidade de texto (microcopy, labels, README) — auditoria de AI-isms:
|
||||
|
||||
```bash
|
||||
# detect (só lista) — passa 2 arquivos para comparar original vs limpo
|
||||
node ~/.hermes/skills/avoid-ai-writing/detector/validate.js ui-copy.md ui-copy-limp.md
|
||||
|
||||
# ou debaixo do wf: pedir "roda o avoid-ai no README" (modo detect/rewrite)
|
||||
```
|
||||
|
||||
Bug hunt adversarial no diff (comandos da skill hermaguard):
|
||||
|
||||
```bash
|
||||
# gatilho: "hermaguard this" / "adversarial review" / "bug hunt this"
|
||||
# gera relatório em /tmp/hermaguard/hermaguard-<ts>.md + .json
|
||||
```
|
||||
|
||||
Performance React/Next — checklist durante o build:
|
||||
|
||||
```bash
|
||||
npm run build # ou pnpm build — verificar warnings de bundle chunk
|
||||
npx tsc --noEmit # typecheck limpo
|
||||
npm run lint # sem erros
|
||||
```
|
||||
|
||||
Resumo do fluxo (mapa de navegação do workflow):
|
||||
|
||||
```text
|
||||
[Fase 1 DESIGN] -> frontend-design + anti-ai-slop -> DESIGN.md aprovado
|
||||
[Fase 2 BUILD] -> vercel patterns + animate -> build/lint verdes
|
||||
[Fase 3 REVIEW] -> avoid-ai + hermaguard + perf -> QA visual (PDF)
|
||||
[Gate final] -> test_quality.py exit 0
|
||||
```
|
||||
|
||||
## Critérios de Aceite (checklist não-regressivo)
|
||||
|
||||
- [ ] DESIGN.md com direção visual aprovada (Fase 1 validada pelo usuário).
|
||||
- [ ] BUILD com perf/a11y; build e lint verdes a cada componente.
|
||||
- [ ] REVIEW: anti-ai-slop + avoid-ai-writing + hermaguard + perf checklist.
|
||||
- [ ] Para PDF: renderização via Playwright e QA visual (vision_analyze)
|
||||
sem conteúdo cortado nas bordas.
|
||||
- [ ] Nenhuma skill do stack removida do arquivo.
|
||||
- [ ] Rodar `python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py`
|
||||
→ exit 0 (sem regressão).
|
||||
|
||||
## Exemplos de execução por fase (comandos reais)
|
||||
|
||||
Fase 1 — direção visual documentada:
|
||||
|
||||
```bash
|
||||
cat > DESIGN.md <<'EOF'
|
||||
# Direção visual
|
||||
Paleta: fundo #ffffff, acento #1e40af, texto #1a1a1a
|
||||
Tipografia: serif corpo (Noto Serif/Georgia), mono p/ código
|
||||
Density: padrão; dark/light: light first
|
||||
EOF
|
||||
```
|
||||
|
||||
Fase 2 — build e perf:
|
||||
|
||||
```bash
|
||||
npm run build # ou pnpm build
|
||||
npx tsc --noEmit # typecheck
|
||||
npm run lint # sem erros
|
||||
# "audite a perf" (vercel-react-best-practices)
|
||||
```
|
||||
|
||||
Fase 3 — review/QA:
|
||||
|
||||
```bash
|
||||
# "rode o anti-ai-slop" / "rode o avoid-ai nos textos"
|
||||
# "hermaguard this" (bug hunt no diff de frontend)
|
||||
```
|
||||
|
||||
Para PDF (regra do Evandro — SEMPRE este caminho):
|
||||
|
||||
```bash
|
||||
# HTML+CSS -> Playwright headless
|
||||
~/.hermes/venvs/latebra/bin/python /tmp/gen-pdf.py # ver pdf-generator
|
||||
cd ~/.hermes/skills/productivity/pdf/scripts
|
||||
python pdf_page_image.py saida.pdf --pages 1-N --dpi 150 --out-dir /tmp/qa
|
||||
# depois vision_analyze em cada PNG: NENHUM conteúdo cortado nas bordas
|
||||
|
||||
# QA de perfil visual (se tiver origem de design)
|
||||
node ~/.hermes/skills/avoid-ai-writing/detector/validate.js ui-copy.md ui-limp.md
|
||||
```
|
||||
|
||||
Verificação de integridade do workflow (gate anti-regressão):
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py # não-regressão
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_tests.py # teste dos testes
|
||||
# "hermaguard this" -> bug hunt adversarial se o diff >3 arquivos
|
||||
# "rode o anti-ai-slop" -> antes de entregar
|
||||
```
|
||||
|
||||
## Regras duras
|
||||
|
||||
- NUNCA pular a Fase 1 (consulta de design) direto para código.
|
||||
- NUNCA entregar UI sem passar pelo `anti-ai-slop` + `avoid-ai-writing` +
|
||||
`hermaguard` + build verde.
|
||||
- Dúvida de stack (Vite vs Next, Tailwind vs CSS) → decidir na Fase 1 com
|
||||
aprovação do usuário, não no meio do build.
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
name: wf-readme
|
||||
description: "Criar/auditar/reescrever README grounded. Scan real do repo."
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
platforms: [linux, macos, windows]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [readme, documentation, docs, markdown, workflow]
|
||||
---
|
||||
|
||||
# Workflow README (criar, auditar, reescrever)
|
||||
|
||||
Orquestrador obrigatório para qualquer trabalho de README (criar, melhorar,
|
||||
auditar, padronizar) em qualquer projeto. Princípio central: **grounded** —
|
||||
o README é construído a partir de fatos reais do repositório (manifest,
|
||||
código, scripts, config, git), nunca fabricado de memória.
|
||||
|
||||
## When to Use
|
||||
|
||||
- "cria um README pra esse projeto" / "escreve a documentação"
|
||||
- "melhora esse README" / "audita a qualidade do README"
|
||||
- "padroniza a documentação do repo"
|
||||
- Qualquer pedido de documentação de projeto (README/LICENSE/docs)
|
||||
|
||||
## Skills do stack
|
||||
|
||||
- `readme-crafter` (linhai0872) — classifica o projeto em 4 eixos (tipo,
|
||||
distribuição, público, temperamento) e gera README sob medida; 13 checks
|
||||
de verificação; modo collaborative/quick/surgical/audit.
|
||||
- `good-readme` (adewale) — cria do zero com exemplos reais de código OU
|
||||
audita contra rubric de 22 critérios (escala 100); verifica exemplos
|
||||
contra o código real.
|
||||
- `curating-readme` (liang-senbei) — padroniza README + docs relacionados
|
||||
(CONTRIBUTING/CHANGELOG/SECURITY/docs) com scripts determinísticos
|
||||
(`audit-repo.sh`) e estrutura canônica.
|
||||
- Locais: `project-readme-clarity` (auditoria de onboarding/ambiguidades),
|
||||
`avoid-ai-writing` (remove AI-isms), `wf-frontend` (padrão visual de
|
||||
docs/README bonitos).
|
||||
|
||||
## Fase 1 — SCAN (coletar fatos reais)
|
||||
|
||||
1. Rodar o inventory determinístico do repo:
|
||||
`bash ~/.hermes/skills/curating-readme/scripts/audit-repo.sh <repo>`
|
||||
(ou o `scan-project.sh` do readme-crafter) — coleta manifest, entrypoints,
|
||||
scripts, config, estrutura, env vars, git, linguagens.
|
||||
2. Ler manifest principal (package.json / pyproject.toml / go.mod / etc):
|
||||
nome, descrição, versão, dependências, scripts.
|
||||
3. Identificar entrypoints: o que o projeto realmente faz (não o que diz).
|
||||
4. Ler README existente (se houver), CLI_AGENTS.md, LICENSE.
|
||||
5. VERIFICAR: marcar como **fato verificado** cada item com fonte no repo;
|
||||
qualquer coisa sem fonte vira `TODO:`/pergunta — nunca inventar.
|
||||
6. VERIFICAR (comandos de scan): rodar `python3 -c "import json; json.load(open('package.json'))"`
|
||||
(ou equivalente para o manifest) para provar que o manifest parseia;
|
||||
e `git remote -v && git log --oneline -5` para capturar contexto real.
|
||||
7. VERIFICAR (inventário): conferir com `find . -maxdepth 1 -type f | wc -l`
|
||||
a contagem real de arquivos e `du -sh .` para não documentar coisa que
|
||||
não existe. TRÊS CHECKS: manifest válido, git real, inventário presente.
|
||||
|
||||
**Checklist da fase 1:**
|
||||
- [ ] Inventory do repo rodado (audit-repo.sh / scan-project.sh)
|
||||
- [ ] Manifest lido e entendido
|
||||
- [ ] Fato × suposição separados (suposição vira TODO/pergunta)
|
||||
|
||||
## Fase 2 — CLASSIFY (entender público e tipo)
|
||||
|
||||
1. `readme-crafter` — classificar o projeto:
|
||||
- Tipo: library / CLI / web app / framework / API / agent-AI / monorepo / ...
|
||||
- Distribuição: open-source público vs interno
|
||||
- Público: avaliador / novo usuário / contribuidor / operador / agente IA
|
||||
- Temperamento: developer utility / product / academic / community
|
||||
2. Decidir audiência PRIMÁRIA (o README serve a quem primeiro).
|
||||
3. VERIFICAR: registrar decisões de classificação (1 linha cada) — a
|
||||
estrutura do README deriva delas.
|
||||
4. SE necessário, fazer perguntas do tipo "quem é o público?" ou "qual
|
||||
o tom?" quando o código não revela a intenção (3-5 perguntas máx).
|
||||
5. VERIFICAR (comandos de classificação): para software com CLI, rodar
|
||||
`python3 -m <pkg> --help` (ou `node bin/index.js --help`) para capturar
|
||||
a sintaxe real de uso; para web app, listar `find src pages app -maxdepth 1 -type d 2>/dev/null` para ver as rotas reais.
|
||||
6. VERIFICAR (decisões): registrar as 4 classificações com fonte — "tipo=CLI
|
||||
porque package.json.bin existe"; "público=developer porque docs de API".
|
||||
|
||||
**Checklist da fase 2:**
|
||||
- [ ] Tipo / distribuição / público / temperamento definidos
|
||||
- [ ] Audiência primária nomeada
|
||||
- [ ] Perguntas de intenção (se houve) respondidas
|
||||
|
||||
## Fase 3 — AUDIT ou DRAFT (estado atual)
|
||||
|
||||
Se JÁ existe README:
|
||||
1. `good-readme` — auditar contra 22 critérios; dar score /100.
|
||||
2. Registrar: o que está bom, o que está desatualizado, o que está errado
|
||||
(claims que não batem com o código), o que falta.
|
||||
3. `project-readme-clarity` — checar ambiguidades de onboarding (dependência
|
||||
opcional apresentada como obrigatória, passo que bloqueia primeiro valor).
|
||||
|
||||
Se NÃO existe README:
|
||||
1. Anotar Fase 1+2 e seguir direto para a Fase 4 (draft).
|
||||
|
||||
VERIFICAR (comandos de auditoria):
|
||||
- `grep -c "^#\|^##" README.md` — conta headings p/ o TOC futuro.
|
||||
- `grep -oE "\]\([^)]*\)" README.md | grep -oE "\(\.?/?" | sort -u` — links
|
||||
locais que precisam existir no disco; conferir com `ls` que cada um vive.
|
||||
- `grep -nE "install|npm |pnpm |pip |docker " README.md` — comandos citados;
|
||||
validar se existem de fato no repo (não só no texto).
|
||||
- `bash ~/.hermes/skills/curating-readme/scripts/audit-repo.sh .` — re-rodar
|
||||
o inventory se o repo mudou desde a Fase 1.
|
||||
|
||||
**Comandos de auditoria (Fase 3):**
|
||||
```bash
|
||||
grep -nc "^#\|^##" README.md # headings existentes
|
||||
grep -oE "\]\(\.?/?[^)]*\)" README.md # links locais citados
|
||||
grep -nE "npm (install|run)|pnpm|pip |docker (run|compose)|uv " README.md 2>/dev/null
|
||||
# validar que cada comando existe de fato:
|
||||
command -v pnpm && pnpm --version
|
||||
git log --oneline -3
|
||||
```
|
||||
|
||||
**Anti-padrões (Fase 3):**
|
||||
- NÃO dar score alto para README bonito mas incorreto (acurácia > estética).
|
||||
- NÃO ignorar claims do README que não batem com o código — cada divergência
|
||||
é finding a corrigir.
|
||||
- NÃO apresentar dependência opcional como obrigatória (project-readme-clarity).
|
||||
|
||||
**Checklist da fase 3:**
|
||||
- [ ] README existente auditado (22 critérios / score) OU confirmado inexistente
|
||||
- [ ] Checks de onboarding/ambiguidade rodados
|
||||
- [ ] Comandos/links do README atual validados contra o repo
|
||||
- [ ] Score baseline registrado (se README existia)
|
||||
|
||||
## Fase 4 — GENERATE (escrever com fatos verificados)
|
||||
|
||||
1. Estrutura canônica por tipo (referências do `good-readme`/`curating-readme`):
|
||||
título + badges (só os que existem) + descrição 1 linha + features reais +
|
||||
stack + prereqs + quickstart copy-paste + usage + config (tabela de env) +
|
||||
estrutura do projeto + contributing + license.
|
||||
2. **Badges:** só incluir badges que refletem a realidade (license, linguagem,
|
||||
build se houver CI) — não criar badge de coisa que não existe.
|
||||
3. **Exemplos de código: reais e testados.** Se o README mostra um comando
|
||||
ou exemplo, ELE DEVE funcionar no repo — verificar.
|
||||
4. `avoid-ai-writing`: após redigir, limpar AI-isms (em-dash em excesso,
|
||||
"robust/seamless/leverage", transições genéricas, conclusões vazias).
|
||||
5. Se o README for de repo público bonito (estilo shadcn/Vercel): seguir o
|
||||
padrão visual do `wf-frontend` (hero + badges + TOC + tabelas limpas).
|
||||
6. VERIFICAR: cada claim do README tem fonte no repo (manifest, código,
|
||||
script, LICENSE) — nada inventado.
|
||||
|
||||
**Comandos reais (Fase 4):**
|
||||
```bash
|
||||
# comandos do README que precisam existir no repo
|
||||
grep -oE '\b(npm|pnpm|python3|node |git|docker|uv) [^`]+' README.md
|
||||
# conferir licença/tipo
|
||||
head -1 LICENSE 2>/dev/null
|
||||
# TOC vs headings: contar headings reais
|
||||
grep -oE '^#{2,3} ' README.md | wc -l
|
||||
```
|
||||
|
||||
**Checklist da fase 4:**
|
||||
- [ ] Estrutura canônica seguida (adaptada ao tipo)
|
||||
- [ ] Badges só do que existe
|
||||
- [ ] Exemplos verificados contra o código
|
||||
- [ ] 0 AI-isms (detect do avoid-ai-writing)
|
||||
- [ ] Todo claim tem fonte
|
||||
|
||||
## Fase 5 — VERIFY + VALIDATE (prova de qualidade)
|
||||
|
||||
1. Validar README gerado:
|
||||
- Rodar `audit-repo.sh <repo>` de novo: conferir que nada essencial ficou
|
||||
de fora.
|
||||
- Checar links locais resolvem (`grep -o ']([^)]*)' README.md`; arquivos
|
||||
citados existem).
|
||||
- TOC bate com headings.
|
||||
- Comandos no README existem (checar que os binários/comandos citados
|
||||
são reais).
|
||||
2. `good-readme` — re-auditar: score final deve subir vs Fase 3 (se houve
|
||||
README antes) ou ser alto (≥85/100) se criado.
|
||||
3. `avoid-ai-writing` detect: 0 AI-isms restantes.
|
||||
4. VERIFICAR gate do próprio workflow: `python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py`.
|
||||
5. Relatório: score antes → depois, o que mudou, checks que passaram.
|
||||
|
||||
**Checklist final:**
|
||||
- [ ] Links locais resolvem
|
||||
- [ ] Comandos do README são reais
|
||||
- [ ] TOC bate
|
||||
- [ ] Score de qualidade ≥ 85/100 (good-readme) ou melhorou vs antes
|
||||
- [ ] 0 AI-isms
|
||||
- [ ] Gate do workflow (test_quality.py) verde
|
||||
|
||||
**Comandos de validação (Fase 5):**
|
||||
```bash
|
||||
# 1) links locais do README existem
|
||||
grep -oE '\]\(\.?/?[^)]*\)' README.md | sed 's/](//; s/)$//' | while read p; do [ -e "$p" ] || echo "MISSING: $p"; done
|
||||
# 2) TOC vs headings (sem TOC = ok, com TOC precisa bater)
|
||||
grep -oE '^##{1,3} ' README.md | wc -l
|
||||
# 3) 0 AI-isms (em-dash em excesso)
|
||||
grep -o "—" README.md | wc -l
|
||||
# 4) refere o LICENSE/contribuição existem
|
||||
ls LICENSE CONTRIBUTING.md 2>/dev/null
|
||||
# 5) gate do stack
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py
|
||||
```
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NÃO pular a re-auditoria (good-readme score) por "tá bom já".
|
||||
- NÃO entregar sem rodar o detect do avoid-ai-writing (AI-ism = regressão).
|
||||
- NÃO tratar "README mais bonito" como objetivo sem validar acurácia —
|
||||
bonito ≠ correto; a acurácia vem antes.
|
||||
|
||||
## Regras duras
|
||||
|
||||
- NUNCA inventar instalação, comando, exemplo, screenshot ou feature que
|
||||
não exista no repo — fato sem fonte vira TODO ou pergunta.
|
||||
- NUNCA incluir badge de coisa que não existe (CI sem CI, licensa sem LICENSE).
|
||||
- NUNCA entregar README com AI-isms (rodar avoid-ai-writing antes de fechar).
|
||||
- NUNCA afirmar "testado" sem ter executado o comando.
|
||||
- NUNCA copiar estrutura de outro projeto sem adaptar ao tipo/público real.
|
||||
- README do usuário do repo deve entender "clono → rodar" sem configurar
|
||||
dependência opcional desnecessária (project-readme-clarity).
|
||||
- NUNCA deixar `TODO:` no README final — TODO só durante o rascunho; na
|
||||
entrega, todo fato ou foi preenchido com fonte ou virou pergunta ao usuário.
|
||||
- NUNCA trocar linguagem da prosa no meio (EN/PT misturados) — o README
|
||||
principal tem UMA língua; traduções vão em arquivos separados.
|
||||
- NÃO encher de emojis/badges decorativos; cada elemento tem função.
|
||||
- NÃO duplicar informação (instalação em 2 seções é bug de manutenção).
|
||||
@@ -0,0 +1,209 @@
|
||||
---
|
||||
name: wf-security-review
|
||||
description: "Code review de segurança (front+back). OSV/NVD + OWASP."
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
platforms: [linux, macos, windows]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [security, code-review, cve, owasp, sast, sca, workflow, devsecops]
|
||||
---
|
||||
|
||||
# Workflow Security Code Review (front + backend)
|
||||
|
||||
Orquestrador obrigatório para todo code review de segurança — frontend E
|
||||
backend. Cobre: revisão de código (OWASP), vulnerabilidades conhecidas em
|
||||
dependências, E consulta de NOVAS vulnerabilidades nos lugares certos
|
||||
(OSV.dev, NVD, GitHub Advisory) para que o review esteja sempre atualizado,
|
||||
não preso ao cutoff de treino do modelo.
|
||||
|
||||
## When to Use
|
||||
|
||||
- "revisa a segurança desse código/PR/diff" (front ou back)
|
||||
- "escaneia dependências por CVEs" / "alguma lib vulnerável?"
|
||||
- "tem vulnerabilidade nova conhecida pra X?" (consulta de atualização)
|
||||
- "audita auth/API/injeção" / "procura segredos commitados"
|
||||
- QUALQUER review de merge que mexa em dado sensível ou rota nova
|
||||
- Pré-commit ou pré-deploy como etapa de segurança
|
||||
|
||||
## Skills do stack (3 principais + auxiliares)
|
||||
|
||||
**As 3 skills de segurança atuais (instaladas de fontes primárias):**
|
||||
1. `safedeps` (Jeneidi) — grounding em OSV.dev em tempo real: consulta
|
||||
`package@version` e traz CVE com severidade e fix. Cobre o gap de o
|
||||
modelo só conhecer vulns até o cutoff. CLI: `check_deps.py`.
|
||||
2. `sca-audit` (OWASP secure-agent-playbook) — SCA (Supply Chain Analysis)
|
||||
de dependências com análise de alcance (reachability), CWE mapping.
|
||||
3. `code-review-security` (OWASP secure-agent-playbook) — code review de
|
||||
segurança sistemático mapeado a OWASP Top 10 + ASVS.
|
||||
|
||||
**Auxiliares (carregar por contexto):**
|
||||
- `secrets-scan` (OWASP) — busca credenciais/API keys no código e git history
|
||||
- `api-security-review` + `bola-detector` + `auth-rbac-scaffold` +
|
||||
`injection-checker` (OWASP/API) — revisão de API
|
||||
- `web-security-review` (OWASP) — web app OWASP Top 10
|
||||
- `cve-triage` + `patch-prioritization` (UnitOne/SecuritySkills) — priorizar
|
||||
CVE por CVSS 4.0 / EPSS / CISA KEV
|
||||
- Locais: `dependency-cve-audit`, `hermaguard` (caça bugs adversariais),
|
||||
`security-threat-model`
|
||||
|
||||
## Fase 1 — SCOPE (definir alvo antes de auditar)
|
||||
|
||||
1. Identificar o que está no escopo: diff/PR, arquivos, front, back, ambos.
|
||||
2. Registrar stack do projeto (linguagens, frameworks, package managers,
|
||||
se há lockfile: package-lock.json / requirements.txt / go.mod / etc).
|
||||
3. Rodar o código? Identificar se há ambiente local testável.
|
||||
4. VERIFICAR: `git status` + `git diff --stat` para delimitar o alvo real.
|
||||
5. Checkpoint: confirmar escopo com o usuário antes de gastar auditoria.
|
||||
|
||||
**Checklist da fase 1:**
|
||||
- [ ] Escopo delimitado (arquivos/diff/PR)
|
||||
- [ ] Stack e lockfiles identificados
|
||||
- [ ] Alvo confirmado
|
||||
|
||||
## Fase 2 — HUNT (vulnerabilidades conhecidas — dimensão de dependências)
|
||||
|
||||
1. `sca-audit` — scan de dependências com análise de alcance; mapear CVEs
|
||||
por severidade e reachable/unreachable.
|
||||
2. `safedeps` — para cada dependência-chave do manifest, consultar OSV.dev:
|
||||
`python3 ~/.hermes/skills/safedeps/check_deps.py "pkg@version"` (ou via
|
||||
manifest completo) — traz CVE reais com fix (o modelo NÃO sabe CVE nova
|
||||
do cutoff pra frente; o OSV.dev sim).
|
||||
3. `cve-triage` / `patch-prioritization` — priorizar achados por
|
||||
CVSS 4.0 / EPSS / CISA KEV quando disponível.
|
||||
4. `dependency-cve-audit` (local) — varredura das deps do projeto atual.
|
||||
5. VERIFICAR: consolidar lista de CVE com severidade + versão afetada +
|
||||
versão fixa; separar reachable (devem ser bloqueantes) de non-reachable
|
||||
(registrar como risco residual).
|
||||
6. CHECKPOINT: apresentar sumário de CVE antes de partir para o code review.
|
||||
|
||||
**Comandos úteis:**
|
||||
```bash
|
||||
python3 ~/.hermes/skills/safedeps/check_deps.py "lodash@4.17.20" # 1 pkg
|
||||
python3 ~/.hermes/skills/safedeps/check_deps.py -f package-lock.json # manifest
|
||||
# veja a skill sca-audit p/ reachability; cve-triage p/ priorização
|
||||
```
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA afirmar "sem CVE" sem ter CONSULTADO OSV.dev (memória do modelo é
|
||||
congelada no cutoff).
|
||||
- NÃO tratar CVE de dependência transitiva não-reachable como crítica igual
|
||||
a reachable — reportar com nuance.
|
||||
- NUNCA rodar scan em lockfile sem conferir o package manager certo.
|
||||
|
||||
**Comandos reais (Fase 2):**
|
||||
```bash
|
||||
python3 ~/.hermes/skills/safedeps/check_deps.py "express@4.18.2"
|
||||
python3 ~/.hermes/skills/safedeps/check_deps.py -f package-lock.json
|
||||
# alternatives: npm audit --json / pip-audit -r requirements.txt / trivy fs .
|
||||
```
|
||||
|
||||
## Fase 3 — CONSULT (novas vulnerabilidades — atualização contínua)
|
||||
|
||||
O objetivo é responder "há vulnerabilidade NOVA que eu não conheço?" — o
|
||||
modelo só sabe até o cutoff; consultar fontes vivas:
|
||||
|
||||
1. OSV.dev (API aberta, sem key) — já coberto na Fase 2 via safedeps.
|
||||
2. GitHub Advisory Database — via `gh api`:
|
||||
`gh api graphql -f query='{securityAdvisories(first:10, orderBy:{field:PUBLISHED_AT,direction:DESC}){nodes{ghsaId,summary,severity,publishedAt}}}'
|
||||
3. NVD — via API (key opcional): consultar CPE do stack ou CVEs recentes do
|
||||
framework em uso (`curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=..."`).
|
||||
4. Registros de framework (ex: changelog/security de Next.js, Express,
|
||||
NestJS, FastAPI) — advisories recentes do framework do projeto.
|
||||
5. VERIFICAR: para LIB versão X e FRAMEWORK Y, registrar qual fonte foi
|
||||
consultada e a data — sem "não tem" sem fonte.
|
||||
6. CHECKPOINT: se alguma vuln nova afeta o stack do projeto, tratar como
|
||||
finding HIGH e sugerir upgrade.
|
||||
|
||||
**Comandos úteis:**
|
||||
```bash
|
||||
gh api graphql -f query='{securityAdvisories(first:10, orderBy:{field:PUBLISHED_AT,direction:DESC}){nodes{ghsaId,summary,severity,publishedAt}}}'
|
||||
curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=nextjs&resultsPerPage=10"
|
||||
# OSV batch: curl -s -X POST https://api.osv.dev/v1/querybatch -d '@manifests.json'
|
||||
```
|
||||
|
||||
**Anti-padrões desta fase:**
|
||||
- NUNCA responder "não tem vulnerabilidade nova" sem citar fonte viva + data.
|
||||
- NÃO confiar no cutoff do modelo para saber CVE recente — sempre consultar.
|
||||
- NÃO esquecer de incluir data da consulta (revisão vence se ficar velho).
|
||||
|
||||
## Fase 4 — CODE REVIEW (front + backend, OWASP)
|
||||
|
||||
Frontend (`web-security-review` + `code-review-security`):
|
||||
1. XSS/injeção em renderização (dangerouslySetInnerHTML, v-html, innerHTML).
|
||||
2. Exposição de segredos no client (API keys, tokens em bundle).
|
||||
3. Containers: loads, links com user input, cookies httpOnly/secure.
|
||||
4. `secrets-scan` — credenciais no código e git history.
|
||||
5. Auth de frontend: fluxo de tokens, storage inseguro (localStorage p/ JWT).
|
||||
|
||||
Backend (`code-review-security` + `api-security-review` + `bola-detector` +
|
||||
`auth-rbac-scaffold` + `injection-checker`):
|
||||
1. AuthN/AuthZ: rotas sem auth, IDOR/BOLA (`bola-detector`), RBAC.
|
||||
2. Input validation: SQLi, XSS, command injection, SSRF, path traversal
|
||||
(`injection-checker`).
|
||||
3. Segredos: env vars, nunca logar senha/token.
|
||||
4. Rate limit, erros sem vazar stack trace, CORS correto.
|
||||
5. `hermaguard` (opcional) p/ caça adversarial de bugs no mesmo diff.
|
||||
6. VERIFICAR: para cada rota com `:id`, rodar `bola-detector` de novo (IDOR
|
||||
é o achado nº1 em API) e para cada query, `injection-checker`.
|
||||
7. VERIFICAR: rodar `secrets-scan` no diff E git history antes de fechar —
|
||||
credencial commitada em algum commit antigo ainda vaza.
|
||||
8. VERIFICAR: conferir erros sem stack trace (grep por exceptions/console no
|
||||
cliente) e CORS/headers (`grep -rn "access-control\|csp"`).
|
||||
9. VERIFICAR: `grep -rn "localStorage\|sessionStorage" src/` — nenhum JWT
|
||||
sensível em storage de frontend (usar httpOnly cookie).
|
||||
10. VERIFICAR: confirmar rate limit em rotas de escrita (`grep -rniE "limiter|rate"`) e que respostas não vazam caminho interno.
|
||||
|
||||
**Output por finding:** Severidade (Critical/High/Med/Low) + CWE + OWASP ref +
|
||||
Arquivo:linha + Evidência + Remediação.
|
||||
|
||||
**Anti-padrões desta fase (front + back):**
|
||||
- NUNCA approve sem reportar achado de AuthN/AuthZ ou injection como mínimo.
|
||||
- Padrão de coerência — NÃO misturar "parece ok" com achado sem evidência.
|
||||
- NÃO confundir "não achei com grep" com "não existe" — checar fluxo inteiro.
|
||||
- NÃO focar só no front quando o fix toca o back (IDOR/BOLA vive no back).
|
||||
|
||||
**Comandos de auditoria (Fase 4):**
|
||||
```bash
|
||||
# segredos: busca em código e git history
|
||||
grep -rniE "(api[_-]?key|secret|password|token|bearer)" src/ | grep -v node_modules
|
||||
git log -p --all -S "sk-" --oneline | head # chave vazada em commit antigo
|
||||
# headers/CORS/rate limit
|
||||
grep -rniE "access-control|content-security-policy" src/
|
||||
grep -rniE "ratelimit|rate_limit|limiter|express-rate" src/
|
||||
python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py # gate
|
||||
```
|
||||
|
||||
## Fase 5 — REPORT + GATE (entregar e decidir)
|
||||
|
||||
1. Consolidar tudo: sumário (nº por severidade), findings detalhados,
|
||||
tabela de CVE com fonte/estado/reachable/fix, e status "consulta de
|
||||
atualização feita em <data> via <fontes>".
|
||||
2. GATE: Critical/High reachable → BLOQUEAR merge até corrigir. Medium →
|
||||
tratar no próximo ciclo. Low/residual → registrar risco aceito.
|
||||
3. Re-auditar após fixes: re-rodar as checagens que acusaram P0/P1.
|
||||
4. VERIFICAR: `python3 ~/.hermes/skills/software-development/wf-qa/test_quality.py`
|
||||
(gate não-regressivo do stack de workflows).
|
||||
5. Relatório final em markdown (report.md) + resumo em chat.
|
||||
6. VERIFICAR (comandos de report): gerar `security-report.md` com
|
||||
`find . -maxdepth 2 -name "*.md" | sort` conferindo artefatos, e
|
||||
`grep -c "CVE-" security-report.md` para ter contagem explícita no doc.
|
||||
7. VERIFICAR (data da consulta): `grep -i "data da consulta\|20[2-9][0-9]-" security-report.md` — sem revisão sem carimbo de data (ela vence).
|
||||
|
||||
**Checklist de entrega:**
|
||||
- [ ] Os 3 pilares rodados: safedeps (OSV), sca-audit, code-review-security.
|
||||
- [ ] Consulta de novas vulns feita em fonte viva (OSV/GH Advisory/NVD) com data.
|
||||
- [ ] Findings com CWE + OWASP + evidência + remediação.
|
||||
- [ ] GATE aplicado (block/allow) por severidade/reachability.
|
||||
- [ ] Nenhuma skill do stack removida; test_quality.py passando.
|
||||
|
||||
## Regras duras
|
||||
|
||||
- NUNCA afirmar "sem vulnerabilidades" sem consultar OSV.dev/GH Advisory
|
||||
(modelo tem cutoff; fonte viva não).
|
||||
- NUNCA reportar CVE sem incluir versão afetada + versão fixa.
|
||||
- NUNCA blocker Critical/High reachable "para depois" — bloquear merge.
|
||||
- NUNCA logar/expor segredo durante o review (nem no relatório).
|
||||
- Re-auditar sempre que o fix tocar >3 arquivos ou mudar dependência.
|
||||
- SQL/queries: sempre parâmetros posicionais, nunca concatenação de input.
|
||||
Reference in new issue
Block a user