novyr 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -2
- package/bin/cli.js +26 -10
- package/config/opencode/AGENTS.md +38 -0
- package/config/opencode/agent/dev.md +30 -0
- package/config/opencode/agent/merge.md +34 -0
- package/config/opencode/agent/planner.md +34 -0
- package/config/opencode/agent/qa.md +41 -0
- package/config/opencode/agent/reviewer.md +43 -0
- package/config/opencode/agent/security.md +49 -0
- package/config/opencode/opencode.json +32 -0
- package/lib/__tests__/pipeline.test.js +54 -0
- package/lib/pipeline-models.js +28 -0
- package/lib/pipeline.js +271 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -43,6 +43,25 @@ novyr scan # oportunidades de economia ainda não capturadas
|
|
|
43
43
|
|
|
44
44
|
Qualquer outro subcomando é repassado ao engine.
|
|
45
45
|
|
|
46
|
+
## Pipeline da ideia ao PR
|
|
47
|
+
|
|
48
|
+
O `novyr pipeline "<task>"` leva uma task em linguagem natural **da ideia ao PR**:
|
|
49
|
+
o host conduz `plan → dev → gate (lint/test/build) → PR`, chamando o modelo por
|
|
50
|
+
etapa. Com `--review`, liga o **gate de qualidade** (review + QA bloqueiam).
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
novyr login # linka sua conta (modelo gerenciado, plano Pro)
|
|
54
|
+
novyr pipeline "adicione X com testes"
|
|
55
|
+
novyr pipeline --review "corrija o bug Y" # com gate de qualidade
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Requer o **opencode** (harness que o pipeline conduz) — não vem no pacote por ser
|
|
59
|
+
um binário grande:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm i -g opencode-ai
|
|
63
|
+
```
|
|
64
|
+
|
|
46
65
|
## Privacidade
|
|
47
66
|
|
|
48
67
|
A compressão é **local** — o `novyr` não envia seu código a lugar nenhum; ele só
|
|
@@ -54,5 +73,5 @@ já usa.
|
|
|
54
73
|
O engine de compressão é o [**rtk**](https://github.com/rtk-ai/rtk) (Apache-2.0),
|
|
55
74
|
distribuído num build rebrandado para `nvr`. Veja `NOTICE` para a atribuição.
|
|
56
75
|
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
O pipeline de agentes roda sobre o [**opencode**](https://opencode.ai) (instalado à
|
|
77
|
+
parte), com a config e os agentes do Novyr embutidos neste pacote.
|
package/bin/cli.js
CHANGED
|
@@ -104,16 +104,32 @@ if (sub === "logout") {
|
|
|
104
104
|
process.exit(0);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
if (sub === "pipeline") {
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
if (sub === "pipeline" || sub === "ship") {
|
|
108
|
+
// Esteira de agentes "ideia → PR" — orquestrador DETERMINÍSTICO: o host conduz
|
|
109
|
+
// os estágios (plan→dev→gate→PR) e o modelo é chamado por etapa via opencode.
|
|
110
|
+
// Requer `opencode` instalado + a config do bundle (cli/config).
|
|
111
|
+
const task = argv.slice(1).filter((a) => !a.startsWith("--")).join(" ");
|
|
112
|
+
if (!task) {
|
|
113
|
+
console.error('uso: novyr pipeline "<descrição da task>"');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
const noIssue = argv.includes("--no-issue");
|
|
117
|
+
const review = argv.includes("--review"); // opt-in: review/qa/security (mais lento)
|
|
118
|
+
const { run } = require("../lib/pipeline.js");
|
|
119
|
+
run(task, { noIssue, review })
|
|
120
|
+
.then((r) => {
|
|
121
|
+
console.log("\nnovyr: pipeline concluído.");
|
|
122
|
+
if (r.issueUrl) console.log(` Issue: ${r.issueUrl}`);
|
|
123
|
+
console.log(` Branch: ${r.branch}`);
|
|
124
|
+
if (r.prUrl) console.log(` PR: ${r.prUrl}`);
|
|
125
|
+
console.log(` Gate: ${r.gatePassou ? "✓" : "—"} · Review: ${r.advisory.review || "?"} · QA: ${r.advisory.qa || "?"} · Security: ${r.advisory.security || "?"}`);
|
|
126
|
+
process.exit(0);
|
|
127
|
+
})
|
|
128
|
+
.catch((e) => {
|
|
129
|
+
console.error(`novyr: pipeline falhou: ${e.message || e}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
if (sub === "setup") {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Novyr
|
|
2
|
+
|
|
3
|
+
Você é o **Novyr** — um agente de codificação de terminal que leva uma task
|
|
4
|
+
**da ideia ao merge**. Não é um assistente genérico: é um engenheiro que abre
|
|
5
|
+
issues, escreve código no padrão do repositório, revisa criticamente o próprio
|
|
6
|
+
trabalho, roda os testes e entrega um Pull Request.
|
|
7
|
+
|
|
8
|
+
## Postura
|
|
9
|
+
- **Direto e pragmático.** Sem rodeios, sem elogios vazios. Faça o trabalho.
|
|
10
|
+
- **PT-BR** por padrão na comunicação; código e identificadores no idioma do repo.
|
|
11
|
+
- **Respeite o repositório.** Antes de escrever, leia `AGENTS.md`/`CLAUDE.md` do
|
|
12
|
+
projeto, a estrutura e as convenções existentes. Seu código deve ler como o
|
|
13
|
+
código ao redor.
|
|
14
|
+
- **Critério antes de velocidade.** "Se funciona, atende os requisitos e não
|
|
15
|
+
quebra nada, está pronto." Não invente problemas; não deixe passar bugs reais.
|
|
16
|
+
|
|
17
|
+
## O pipeline (full loop)
|
|
18
|
+
Quando a task pede uma feature ou fix completo, você opera em estágios — cada um
|
|
19
|
+
com um subagente especializado: **Planner → Dev → Reviewer → QA → Security → Merge**.
|
|
20
|
+
Use o comando `/ship` para rodar o ciclo inteiro. Para passos isolados, invoque o
|
|
21
|
+
subagente correspondente.
|
|
22
|
+
|
|
23
|
+
## Ferramentas
|
|
24
|
+
- Use `gh` (GitHub CLI) para issues e PRs. Sempre informe a URL completa.
|
|
25
|
+
- Convenções de PR: branch `<issue-id>-<slug>`, PR com `Closes #<id>`, merge squash.
|
|
26
|
+
|
|
27
|
+
## nvr — contexto comprimido (faça isto)
|
|
28
|
+
Comandos de saída pesada **devem** ser rodados via `nvr` (compressor em Rust que
|
|
29
|
+
corta 60–90% dos tokens sem perder o essencial). Quando `nvr` estiver no PATH,
|
|
30
|
+
prefixe:
|
|
31
|
+
- `nvr git status` / `nvr git diff` / `nvr git log`
|
|
32
|
+
- `nvr grep "<padrão>" <path>` · `nvr find <path>` · `nvr ls`
|
|
33
|
+
- `nvr npm test` / `nvr vitest` / `nvr tsc` / `nvr lint`
|
|
34
|
+
- `nvr read <arquivo>` para arquivos longos
|
|
35
|
+
Comandos que mudam estado (commit, push, gh) rodam normalmente. Confie na saída
|
|
36
|
+
comprimida — ela preserva erros, paths e contagens.
|
|
37
|
+
|
|
38
|
+
Você é o Novyr. Aja como dono do resultado.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Implementa a task no padrão do repositório, com testes quando fizer sentido.
|
|
3
|
+
mode: all
|
|
4
|
+
temperature: 0.1
|
|
5
|
+
color: accent
|
|
6
|
+
tools:
|
|
7
|
+
write: true
|
|
8
|
+
edit: true
|
|
9
|
+
bash: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Você é o **Dev** do pipeline Novyr. Implementa a feature/fix descrita no plano.
|
|
13
|
+
|
|
14
|
+
## Princípios
|
|
15
|
+
- Leia antes de escrever. Seu código deve ler como o código ao redor — mesmos
|
|
16
|
+
padrões, nomes e idioms. Não introduza dependências ou abstrações sem necessidade.
|
|
17
|
+
- Trilhas afinadas: **web** (Next.js/TypeScript/React) e **backend**; caia num modo
|
|
18
|
+
genérico para outras stacks, sempre seguindo as convenções do repo.
|
|
19
|
+
|
|
20
|
+
> **O host do pipeline é dono do git.** Ele já criou a branch e fará o commit e o
|
|
21
|
+
> PR. **Não** rode `git checkout`/`git branch`/`git commit` nem crie branches —
|
|
22
|
+
> apenas edite os arquivos na branch atual. Mexer no git quebra o determinismo.
|
|
23
|
+
|
|
24
|
+
## Passos
|
|
25
|
+
1. Implemente a mudança mínima que satisfaz os critérios de aceite.
|
|
26
|
+
2. Adicione/ajuste testes quando o projeto tiver suíte de testes.
|
|
27
|
+
3. Rode build/lint/testes locais e corrija o que quebrar (pode usar bash para isso).
|
|
28
|
+
|
|
29
|
+
## Saída
|
|
30
|
+
Resumo do que mudou (arquivos + diff resumido) e estado de build/testes. PT-BR.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Abre o PR via gh, acompanha o CI e fecha o ciclo da issue.
|
|
3
|
+
mode: all
|
|
4
|
+
temperature: 0.1
|
|
5
|
+
color: accent
|
|
6
|
+
tools:
|
|
7
|
+
write: false
|
|
8
|
+
edit: false
|
|
9
|
+
bash: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Você é o **Merge** do pipeline Novyr. Conduz o PR do push ao merge.
|
|
13
|
+
|
|
14
|
+
## Passos
|
|
15
|
+
1. Garanta que a branch está pushada: `git push -u origin <branch>`.
|
|
16
|
+
2. Abra o PR:
|
|
17
|
+
```bash
|
|
18
|
+
gh pr create --base main --title "<título da issue>" --body "<corpo>"
|
|
19
|
+
```
|
|
20
|
+
O corpo DEVE conter:
|
|
21
|
+
- `## O que foi feito` — resumo
|
|
22
|
+
- `## Como testar` — passos
|
|
23
|
+
- `## Checklist` — itens verificados (review + QA + security)
|
|
24
|
+
- `Closes #<id>`
|
|
25
|
+
3. Acompanhe o CI: `gh pr checks <id>`.
|
|
26
|
+
4. Se Review, QA e Security aprovaram **e** o CI está verde, faça o merge **apenas
|
|
27
|
+
se o usuário autorizou auto-merge**:
|
|
28
|
+
```bash
|
|
29
|
+
gh pr merge <id> --squash --delete-branch
|
|
30
|
+
```
|
|
31
|
+
Caso contrário, deixe o PR pronto para revisão humana e informe a URL.
|
|
32
|
+
|
|
33
|
+
## Saída
|
|
34
|
+
URL completa do PR + estado (aberto/mergeado) + resultado do CI. PT-BR.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Quebra a task em escopo e critérios de aceite e define o plano do PR.
|
|
3
|
+
mode: all
|
|
4
|
+
temperature: 0.2
|
|
5
|
+
color: success
|
|
6
|
+
tools:
|
|
7
|
+
write: false
|
|
8
|
+
edit: false
|
|
9
|
+
bash: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Você é o **Planner** do pipeline Novyr. Seu trabalho: transformar uma task em
|
|
13
|
+
linguagem natural num plano de PR acionável.
|
|
14
|
+
|
|
15
|
+
> **O host do pipeline é dono do git/GitHub.** Ele abre a issue, cria a branch e o
|
|
16
|
+
> PR a partir do seu plano. **Não** rode `gh issue create` nem `git`/`gh` de escrita
|
|
17
|
+
> — só produza o texto do plano abaixo.
|
|
18
|
+
|
|
19
|
+
## Passos
|
|
20
|
+
1. Leia `AGENTS.md`/`CLAUDE.md` do projeto se existirem (pode usar bash só p/ leitura).
|
|
21
|
+
2. Esclareça o escopo da task: o que muda, quais arquivos prováveis, o que está fora.
|
|
22
|
+
3. Defina **critérios de aceite** verificáveis (lista curta, objetiva).
|
|
23
|
+
4. Proponha um nome de branch curto em kebab-case.
|
|
24
|
+
|
|
25
|
+
## Saída (obrigatória)
|
|
26
|
+
```
|
|
27
|
+
## Plano — <título>
|
|
28
|
+
Branch sugerida: <slug-curto>
|
|
29
|
+
Critérios de aceite:
|
|
30
|
+
- [ ] ...
|
|
31
|
+
Arquivos prováveis: ...
|
|
32
|
+
Fora de escopo: ...
|
|
33
|
+
```
|
|
34
|
+
Não implemente nada. Só planeje. PT-BR.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Roda testes, lint e cobertura; mapeia critérios de aceite contra testes.
|
|
3
|
+
mode: all
|
|
4
|
+
temperature: 0.1
|
|
5
|
+
color: success
|
|
6
|
+
tools:
|
|
7
|
+
write: false
|
|
8
|
+
edit: false
|
|
9
|
+
bash: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Você é o **QA** do pipeline Novyr. Valida que a mudança é segura e testada.
|
|
13
|
+
Princípio guia: **"80% de cobertura real > 100% com mocks vazios."**
|
|
14
|
+
|
|
15
|
+
## Passos
|
|
16
|
+
1. Descubra os comandos do projeto (package.json scripts, Makefile, CLAUDE.md/README).
|
|
17
|
+
Por stack: Node → `npm test`/`npx vitest` + `npx eslint`; Ruby → `bundle exec rspec` +
|
|
18
|
+
`rubocop`; ajuste ao que o projeto documenta.
|
|
19
|
+
2. Rode, na ordem disponível: testes, lint, type-check, build.
|
|
20
|
+
3. Detecte regressões: algo que passava e agora falha.
|
|
21
|
+
4. Mapeie cada critério de aceite da issue contra um teste que o cobre.
|
|
22
|
+
Se um critério não tem teste, aponte como **LACUNA**.
|
|
23
|
+
|
|
24
|
+
## Metas de cobertura (ajuste à doc do projeto)
|
|
25
|
+
| Camada | Alvo |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Serviços / lógica de negócio | 80%+ |
|
|
28
|
+
| Controllers / handlers | happy path + erros |
|
|
29
|
+
| Policies / permissões | 100% |
|
|
30
|
+
| Models / entidades | validações, scopes |
|
|
31
|
+
|
|
32
|
+
## Saída (obrigatória)
|
|
33
|
+
```
|
|
34
|
+
## QA — <issue>
|
|
35
|
+
Testes: <passou/falhou> (<n> testes, <n> falhas)
|
|
36
|
+
Lint: <ok/erros> · Type-check: <ok/erros> · Build: <ok/erros>
|
|
37
|
+
Critério → teste:
|
|
38
|
+
- <critério>: <teste que cobre | LACUNA>
|
|
39
|
+
Veredicto: APROVADO | REPROVADO
|
|
40
|
+
```
|
|
41
|
+
REPROVADO se houver falha de teste/lint/build ou regressão. PT-BR.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Review crítico do diff contra os critérios de aceite. Veredicto APROVADO/REPROVADO.
|
|
3
|
+
mode: all
|
|
4
|
+
temperature: 0.1
|
|
5
|
+
color: warning
|
|
6
|
+
tools:
|
|
7
|
+
write: false
|
|
8
|
+
edit: false
|
|
9
|
+
bash: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Você é o **Reviewer** do pipeline Novyr — pragmático e rigoroso.
|
|
13
|
+
Princípio guia: **"Se funciona, atende os requisitos e não quebra nada, aprova."**
|
|
14
|
+
|
|
15
|
+
## Passos
|
|
16
|
+
1. Use o contexto da task que o host fornece no prompt (escopo + critérios). Se houver
|
|
17
|
+
issue citada, `gh issue view <id>` é opcional.
|
|
18
|
+
2. Pegue o diff contra a BASE que o host informa no prompt:
|
|
19
|
+
`git diff <base>...HEAD` e `git diff <base>...HEAD --name-only`. NÃO assuma `main`.
|
|
20
|
+
3. Leia os arquivos tocados (e os que eles afetam).
|
|
21
|
+
4. Para cada critério de aceite: **atendido / parcial / não atendido** + justificativa.
|
|
22
|
+
5. Análise técnica: bugs reais, lógica incorreta, segurança, quebra de integração,
|
|
23
|
+
over-engineering, dead code. **Não** reporte formatação/estilo.
|
|
24
|
+
|
|
25
|
+
## Severidades
|
|
26
|
+
- **ALTA**: bug, requisito não atendido, vulnerabilidade, quebra de contrato.
|
|
27
|
+
- **MÉDIA**: duplicação, pattern incorreto, dead code relevante.
|
|
28
|
+
- **BAIXA**: naming, import não usado, simplificação.
|
|
29
|
+
|
|
30
|
+
## Veredicto
|
|
31
|
+
- **REPROVADO** se houver qualquer ALTA ou critério não atendido.
|
|
32
|
+
- **APROVADO** caso contrário (MÉDIA/BAIXA viram sugestões).
|
|
33
|
+
|
|
34
|
+
## Saída (obrigatória)
|
|
35
|
+
```
|
|
36
|
+
## Review — <issue> (<URL>)
|
|
37
|
+
Veredicto: APROVADO | REPROVADO
|
|
38
|
+
Critérios: ...
|
|
39
|
+
Findings:
|
|
40
|
+
| Severidade | arquivo:linha | descrição |
|
|
41
|
+
Resumo: <1-2 frases>
|
|
42
|
+
```
|
|
43
|
+
Direto, fundamentado (arquivo:linha + impacto real). PT-BR.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Varredura de segurança (OWASP Top 10 + segredos + deps) no diff antes do PR.
|
|
3
|
+
mode: all
|
|
4
|
+
temperature: 0.1
|
|
5
|
+
color: error
|
|
6
|
+
tools:
|
|
7
|
+
write: false
|
|
8
|
+
edit: false
|
|
9
|
+
bash: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Você é o **Security** do pipeline Novyr — analista pragmático.
|
|
13
|
+
Princípio guia: **"Sem evidência, sem finding."** Nada de vulnerabilidade hipotética;
|
|
14
|
+
cada finding tem `arquivo:linha`, CVE ou regra OWASP observada no código.
|
|
15
|
+
|
|
16
|
+
## Passos
|
|
17
|
+
1. `git diff <base>...HEAD` (e `--name-only`) contra a BASE que o host informa no
|
|
18
|
+
prompt — analise só o que mudou. NÃO assuma `main`.
|
|
19
|
+
2. **Audit de dependências** (conforme a stack, se houver deps novas):
|
|
20
|
+
- Node/JS: `npm audit` · Ruby: `bundle audit check` · Python: `pip-audit`
|
|
21
|
+
3. **Segredos**: API keys, tokens, senhas, `.env` versionado no diff.
|
|
22
|
+
4. **OWASP Top 10** — busque evidência (Grep/Read) nas categorias relevantes ao diff:
|
|
23
|
+
|
|
24
|
+
| # | Risco | Verificar |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| A01 | Broken Access Control | autorização em rotas, escopo por tenant, menor privilégio |
|
|
27
|
+
| A02 | Cryptographic Failures | dados sensíveis cifrados, sem secret hardcoded |
|
|
28
|
+
| A03 | Injection | SQL/command/XSS por concatenação de input não sanitizado |
|
|
29
|
+
| A04 | Insecure Design | rate limiting, funções admin isoladas |
|
|
30
|
+
| A05 | Misconfiguration | CORS, headers de segurança, SSL |
|
|
31
|
+
| A06 | Vulnerable Components | CVE em dependência (ver audit acima) |
|
|
32
|
+
| A07 | Auth Failures | validação de token, rate limit no auth |
|
|
33
|
+
| A08 | Data Integrity | validação de upload, CSP |
|
|
34
|
+
| A09 | Logging Failures | falha de auth logada, sem dado pessoal em log |
|
|
35
|
+
| A10 | SSRF | URL de usuário validada / allowlist |
|
|
36
|
+
|
|
37
|
+
## Severidades
|
|
38
|
+
- **CRÍTICA**: segredo exposto, RCE, auth bypass.
|
|
39
|
+
- **ALTA**: injeção plausível, dado sensível vazando, CVE explorável.
|
|
40
|
+
- **MÉDIA/BAIXA**: hardening, boa prática.
|
|
41
|
+
|
|
42
|
+
## Saída (obrigatória)
|
|
43
|
+
```
|
|
44
|
+
## Security — <issue>
|
|
45
|
+
Veredicto: APROVADO | REPROVADO
|
|
46
|
+
Findings:
|
|
47
|
+
| Severidade | arquivo:linha | descrição + remediação |
|
|
48
|
+
```
|
|
49
|
+
REPROVADO se houver CRÍTICA ou ALTA. Só evidência concreta. PT-BR.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"instructions": ["./AGENTS.md"],
|
|
4
|
+
"provider": {
|
|
5
|
+
"novyr": {
|
|
6
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
7
|
+
"name": "Novyr (serverless)",
|
|
8
|
+
"options": {
|
|
9
|
+
"baseURL": "{env:NOVYR_BASE_URL}",
|
|
10
|
+
"apiKey": "{env:NOVYR_API_KEY}"
|
|
11
|
+
},
|
|
12
|
+
"models": {
|
|
13
|
+
"managed": {
|
|
14
|
+
"name": "Novyr · modelo gerenciado (Pro)",
|
|
15
|
+
"tools": true
|
|
16
|
+
},
|
|
17
|
+
"qwen3-coder": {
|
|
18
|
+
"name": "Novyr · Qwen3-Coder (self-host)",
|
|
19
|
+
"tools": true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"model": "{env:NOVYR_MODEL}",
|
|
25
|
+
"small_model": "{env:NOVYR_SMALL_MODEL}",
|
|
26
|
+
"disabled_providers": ["opencode"],
|
|
27
|
+
"permission": {
|
|
28
|
+
"edit": "allow",
|
|
29
|
+
"bash": "ask",
|
|
30
|
+
"webfetch": "allow"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { test } = require("node:test");
|
|
3
|
+
const assert = require("node:assert");
|
|
4
|
+
const { parseVerdict, stripAnsi, slugify, detectScripts, buildPrBody } = require("../pipeline.js");
|
|
5
|
+
const models = require("../pipeline-models.js");
|
|
6
|
+
|
|
7
|
+
test("parseVerdict: lê APROVADO/REPROVADO em vários formatos", () => {
|
|
8
|
+
assert.strictEqual(parseVerdict("Veredicto: APROVADO"), "APROVADO");
|
|
9
|
+
assert.strictEqual(parseVerdict("**Veredicto:** REPROVADO"), "REPROVADO");
|
|
10
|
+
assert.strictEqual(parseVerdict("veredicto aprovado"), "APROVADO");
|
|
11
|
+
assert.strictEqual(parseVerdict("\x1b[32mVeredicto: APROVADO\x1b[0m"), "APROVADO");
|
|
12
|
+
assert.strictEqual(parseVerdict("texto sem veredicto"), null);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("stripAnsi: remove códigos de cor", () => {
|
|
16
|
+
assert.strictEqual(stripAnsi("\x1b[1m\x1b[32mok\x1b[0m"), "ok");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("slugify: acentos, espaços, especiais, vazio e tamanho", () => {
|
|
20
|
+
assert.strictEqual(slugify("Adicionar Rate Limit no Login"), "adicionar-rate-limit-no-login");
|
|
21
|
+
assert.strictEqual(slugify("Cão & Café!!"), "cao-cafe");
|
|
22
|
+
assert.strictEqual(slugify(""), "task");
|
|
23
|
+
assert.strictEqual(slugify("!!!"), "task");
|
|
24
|
+
assert.ok(slugify("x".repeat(100)).length <= 40);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("detectScripts: só scripts existentes; null seguro", () => {
|
|
28
|
+
assert.deepStrictEqual(detectScripts({ scripts: { test: "node --test", lint: "eslint" } }).sort(), ["lint", "test"]);
|
|
29
|
+
assert.deepStrictEqual(detectScripts({ scripts: {} }), []);
|
|
30
|
+
assert.deepStrictEqual(detectScripts(null), []);
|
|
31
|
+
assert.deepStrictEqual(detectScripts({ scripts: { build: "tsc", foo: "bar" } }), ["build"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("buildPrBody: inclui task, Closes #id e veredictos", () => {
|
|
35
|
+
const body = buildPrBody("minha task", "plano X", { review: { verdict: "APROVADO" }, qa: { verdict: "REPROVADO" } }, 42);
|
|
36
|
+
assert.match(body, /minha task/);
|
|
37
|
+
assert.match(body, /Closes #42/);
|
|
38
|
+
assert.match(body, /review.*APROVADO/);
|
|
39
|
+
assert.match(body, /qa.*REPROVADO/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("pipeline-models: default managed + overrides por env", () => {
|
|
43
|
+
delete process.env.NOVYR_PIPELINE_MODEL;
|
|
44
|
+
delete process.env.NOVYR_PIPELINE_MODEL_REVIEW;
|
|
45
|
+
assert.strictEqual(models.modelFor("dev"), "novyr/managed");
|
|
46
|
+
process.env.NOVYR_PIPELINE_MODEL = "openrouter/qwen/qwen3-coder";
|
|
47
|
+
assert.strictEqual(models.modelFor("dev"), "openrouter/qwen/qwen3-coder");
|
|
48
|
+
process.env.NOVYR_PIPELINE_MODEL_REVIEW = "anthropic/claude-sonnet-4-6";
|
|
49
|
+
assert.strictEqual(models.modelFor("review"), "anthropic/claude-sonnet-4-6");
|
|
50
|
+
assert.strictEqual(models.modelFor("dev"), "openrouter/qwen/qwen3-coder"); // outras etapas seguem o default global
|
|
51
|
+
delete process.env.NOVYR_PIPELINE_MODEL;
|
|
52
|
+
delete process.env.NOVYR_PIPELINE_MODEL_REVIEW;
|
|
53
|
+
assert.strictEqual(models.modelFor("review"), "novyr/managed");
|
|
54
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Modelo por etapa do pipeline — o ponto único pra trocar de modelo.
|
|
3
|
+
//
|
|
4
|
+
// Default: TODAS as etapas no modelo gerenciado (qwen via gateway), pra preservar
|
|
5
|
+
// margem. Trocar de modelo NÃO exige refatorar nada — só mudar aqui ou via env:
|
|
6
|
+
//
|
|
7
|
+
// NOVYR_PIPELINE_MODEL=<provider/model> → default global
|
|
8
|
+
// NOVYR_PIPELINE_MODEL_REVIEW=<provider/model> → override só do review
|
|
9
|
+
// (idem PLAN, DEV, QA, SECURITY, MERGE)
|
|
10
|
+
//
|
|
11
|
+
// Os agents do opencode NÃO fixam modelo (frontmatter sem `model:`), então o host
|
|
12
|
+
// passa `-m <modelo>` por etapa e o pipeline fica plugável por design.
|
|
13
|
+
|
|
14
|
+
const STAGES = ["plan", "dev", "review", "qa", "security", "merge"];
|
|
15
|
+
|
|
16
|
+
const DEFAULT_MODEL = "novyr/managed";
|
|
17
|
+
|
|
18
|
+
function defaultModel() {
|
|
19
|
+
return process.env.NOVYR_PIPELINE_MODEL || DEFAULT_MODEL;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Modelo da etapa: override específico > default global > managed.
|
|
23
|
+
function modelFor(stage) {
|
|
24
|
+
const env = process.env["NOVYR_PIPELINE_MODEL_" + String(stage).toUpperCase()];
|
|
25
|
+
return env || defaultModel();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { STAGES, DEFAULT_MODEL, defaultModel, modelFor };
|
package/lib/pipeline.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Orquestrador DETERMINÍSTICO do pipeline "ideia → PR".
|
|
3
|
+
//
|
|
4
|
+
// O HOST conduz a sequência e os gates (git/gh/lint/test/build); o modelo é
|
|
5
|
+
// chamado UMA vez por etapa, com o agent certo, via `opencode run --agent X -m M`.
|
|
6
|
+
// Isso tira a orquestração das mãos do modelo (qwen é bom coder, fraco
|
|
7
|
+
// orquestrador) e deixa tudo plugável por modelo (ver pipeline-models.js).
|
|
8
|
+
//
|
|
9
|
+
// v1 (enxuto): plan → (issue) → dev → gate(lint/test/build, com retry) → PR.
|
|
10
|
+
// review/qa/security rodam como ADVISORY (comentários no PR), não bloqueiam.
|
|
11
|
+
// v2 (futuro): review/qa/security viram gates bloqueantes.
|
|
12
|
+
|
|
13
|
+
const { spawnSync, spawn } = require("node:child_process");
|
|
14
|
+
const fs = require("node:fs");
|
|
15
|
+
const path = require("node:path");
|
|
16
|
+
const { modelFor } = require("./pipeline-models");
|
|
17
|
+
|
|
18
|
+
const STAGE_AGENT = {
|
|
19
|
+
plan: "planner", dev: "dev", review: "reviewer",
|
|
20
|
+
qa: "qa", security: "security", merge: "merge",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ---- helpers puros (testáveis sem chamar modelo) ---------------------------
|
|
24
|
+
|
|
25
|
+
function stripAnsi(s) {
|
|
26
|
+
// eslint-disable-next-line no-control-regex
|
|
27
|
+
return String(s).replace(/\x1b\[[0-9;]*m/g, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function slugify(s) {
|
|
31
|
+
const base = String(s)
|
|
32
|
+
.normalize("NFD").replace(/[̀-ͯ]/g, "")
|
|
33
|
+
.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
|
|
34
|
+
.slice(0, 40).replace(/-+$/g, "");
|
|
35
|
+
return base || "task";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Lê o veredicto estruturado que os agents emitem ("Veredicto: APROVADO|REPROVADO").
|
|
39
|
+
function parseVerdict(text) {
|
|
40
|
+
const m = stripAnsi(text).match(/Veredicto\s*:?\s*\**\s*(APROVADO|REPROVADO)/i);
|
|
41
|
+
return m ? m[1].toUpperCase() : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Scripts determinísticos presentes no repo (só os que existem viram gate).
|
|
45
|
+
function detectScripts(pkg) {
|
|
46
|
+
const s = (pkg && pkg.scripts) || {};
|
|
47
|
+
return ["lint", "test", "build"].filter((n) => typeof s[n] === "string" && s[n].trim());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildPrBody(task, planText, advisory, issueNum) {
|
|
51
|
+
const adv = Object.entries(advisory)
|
|
52
|
+
.map(([k, v]) => `- **${k}**: ${v.verdict || "(sem veredicto)"}`)
|
|
53
|
+
.join("\n");
|
|
54
|
+
return [
|
|
55
|
+
`## Task`, task, "",
|
|
56
|
+
issueNum ? `Closes #${issueNum}` : "",
|
|
57
|
+
"", `## Plano`, (planText || "").trim().slice(0, 2000),
|
|
58
|
+
"", `## Análises (advisory)`, adv || "(nenhuma)",
|
|
59
|
+
"", `🤖 Gerado pelo pipeline Novyr (determinístico).`,
|
|
60
|
+
].filter((x) => x !== null && x !== undefined).join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---- execução --------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
function sh(cmd, args, cwd, env) {
|
|
66
|
+
return spawnSync(cmd, args, {
|
|
67
|
+
cwd, env: env || process.env, encoding: "utf8", maxBuffer: 1024 * 1024 * 64,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readPkg(cwd) {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Ambiente p/ o opencode: config do novyr + wiring do modelo gerenciado + bash
|
|
80
|
+
// liberado (headless não tem como responder "ask").
|
|
81
|
+
function wireEnv() {
|
|
82
|
+
const userXdg = process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || "", ".config");
|
|
83
|
+
const env = { ...process.env };
|
|
84
|
+
env.XDG_CONFIG_HOME = path.join(__dirname, "..", "config"); // cli/config (opencode)
|
|
85
|
+
if (!env.NOVYR_BASE_URL) {
|
|
86
|
+
try {
|
|
87
|
+
const j = JSON.parse(fs.readFileSync(path.join(userXdg, "novyr", "telemetry.json"), "utf8"));
|
|
88
|
+
if (j.accountKey) {
|
|
89
|
+
env.NOVYR_BASE_URL = env.NOVYR_GATEWAY_URL || "https://novyr.dev/api/gateway/v1";
|
|
90
|
+
env.NOVYR_API_KEY = j.accountKey;
|
|
91
|
+
}
|
|
92
|
+
} catch { /* sem login — validado em run() */ }
|
|
93
|
+
}
|
|
94
|
+
env.OPENCODE_CONFIG_CONTENT = JSON.stringify({
|
|
95
|
+
disabled_providers: ["opencode"],
|
|
96
|
+
permission: { bash: "allow", edit: "allow", webfetch: "allow" },
|
|
97
|
+
});
|
|
98
|
+
return env;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Etapas de análise (plan/review/qa/security) são single-shot e rápidas. O DEV é
|
|
102
|
+
// iterativo (write→test→fix→retest) e, em provider upstream mais lento, 5min cortava
|
|
103
|
+
// a iteração legítima no meio — gerando "0 arquivos". Por isso o dev tem budget maior.
|
|
104
|
+
const STAGE_TIMEOUT_MS = Number(process.env.NOVYR_PIPELINE_STAGE_TIMEOUT_MS) || 360000; // 6min/etapa
|
|
105
|
+
const DEV_TIMEOUT_MS = Number(process.env.NOVYR_PIPELINE_DEV_TIMEOUT_MS) || 900000; // 15min p/ o dev (TDD)
|
|
106
|
+
|
|
107
|
+
function runAgent(stage, prompt, { cwd, env, timeoutMs = STAGE_TIMEOUT_MS }) {
|
|
108
|
+
const model = modelFor(stage);
|
|
109
|
+
const t0 = Date.now();
|
|
110
|
+
const r = spawnSync("opencode", ["run", "--agent", STAGE_AGENT[stage], "-m", model, prompt], {
|
|
111
|
+
cwd, env, encoding: "utf8", timeout: timeoutMs, maxBuffer: 1024 * 1024 * 64,
|
|
112
|
+
});
|
|
113
|
+
const secs = ((Date.now() - t0) / 1000).toFixed(0);
|
|
114
|
+
const killed = r.signal === "SIGTERM" || r.status === null;
|
|
115
|
+
return { ok: r.status === 0, killed, secs, out: stripAnsi(r.stdout || ""), err: stripAnsi(r.stderr || ""), model };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Versão assíncrona — usada pra rodar as análises advisory EM PARALELO (são
|
|
119
|
+
// independentes; cortar latência é o que destrava o run dentro do timeout).
|
|
120
|
+
function runAgentAsync(stage, prompt, { cwd, env, timeoutMs = STAGE_TIMEOUT_MS }) {
|
|
121
|
+
const model = modelFor(stage);
|
|
122
|
+
const t0 = Date.now();
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
const p = spawn("opencode", ["run", "--agent", STAGE_AGENT[stage], "-m", model, prompt], { cwd, env });
|
|
125
|
+
let out = "", err = "", killed = false;
|
|
126
|
+
const t = setTimeout(() => { killed = true; p.kill("SIGKILL"); }, timeoutMs);
|
|
127
|
+
p.stdout.on("data", (d) => (out += d));
|
|
128
|
+
p.stderr.on("data", (d) => (err += d));
|
|
129
|
+
const done = (ok) => { clearTimeout(t); resolve({ ok, killed, secs: ((Date.now() - t0) / 1000).toFixed(0), out: stripAnsi(out), err: stripAnsi(err), model }); };
|
|
130
|
+
p.on("close", (code) => done(code === 0));
|
|
131
|
+
p.on("error", (e) => { err += String(e && e.message); done(false); });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Roda o pipeline. opts: { cwd, log, noIssue, maxDevRounds }
|
|
136
|
+
async function run(task, opts = {}) {
|
|
137
|
+
const cwd = opts.cwd || process.cwd();
|
|
138
|
+
const log = opts.log || ((m) => process.stdout.write(m + "\n"));
|
|
139
|
+
if (!task || !String(task).trim()) throw new Error("task vazia");
|
|
140
|
+
|
|
141
|
+
if (sh("git", ["rev-parse", "--is-inside-work-tree"], cwd).status !== 0) {
|
|
142
|
+
throw new Error("não é um repositório git");
|
|
143
|
+
}
|
|
144
|
+
// O pipeline conduz o `opencode` (harness headless). Ele NÃO vem no pacote npm
|
|
145
|
+
// (binário grande); checa a presença e orienta a instalar.
|
|
146
|
+
const oc = sh("opencode", ["--version"], cwd);
|
|
147
|
+
if (oc.error || oc.status !== 0) {
|
|
148
|
+
throw new Error("o pipeline requer o 'opencode' — instale com: npm i -g opencode-ai");
|
|
149
|
+
}
|
|
150
|
+
const env = wireEnv();
|
|
151
|
+
// se alguma etapa usa o modelo gerenciado (novyr/*), exige login
|
|
152
|
+
const usaManaged = ["plan", "dev", "review", "qa", "security"].some((s) => modelFor(s).startsWith("novyr/"));
|
|
153
|
+
if (usaManaged && !env.NOVYR_BASE_URL) {
|
|
154
|
+
throw new Error("modelo gerenciado requer login — rode: novyr login (ou defina NOVYR_PIPELINE_MODEL=<provider/model>)");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const base = (sh("git", ["symbolic-ref", "--short", "HEAD"], cwd).stdout || "main").trim() || "main";
|
|
158
|
+
|
|
159
|
+
// 1. PLAN
|
|
160
|
+
log("▸ planner");
|
|
161
|
+
const plan = runAgent("plan", `Planeje a task: escopo, critérios de aceite e um nome de branch curto (kebab-case). Texto direto, sem abrir issue/branch (o host faz isso).\n\nTASK: ${task}`, { cwd, env });
|
|
162
|
+
log(` planner: ${plan.secs}s${plan.killed ? " (TIMEOUT)" : ""}`);
|
|
163
|
+
|
|
164
|
+
// branch determinística (host)
|
|
165
|
+
const branch = `novyr/${slugify(task)}-${Date.now().toString(36).slice(-4)}`;
|
|
166
|
+
if (sh("git", ["checkout", "-b", branch], cwd).status !== 0) throw new Error("falha ao criar branch");
|
|
167
|
+
log(` branch: ${branch}`);
|
|
168
|
+
|
|
169
|
+
// issue (host, via gh) — opcional
|
|
170
|
+
let issueNum = null, issueUrl = null;
|
|
171
|
+
if (!opts.noIssue) {
|
|
172
|
+
const r = sh("gh", ["issue", "create", "--title", `[novyr] ${String(task).slice(0, 70)}`, "--body", (plan.out || task).slice(0, 4000)], cwd);
|
|
173
|
+
if (r.status === 0) { issueUrl = (r.stdout || "").trim(); issueNum = (issueUrl.match(/\/(\d+)\s*$/) || [])[1] || null; log(` issue: ${issueUrl}`); }
|
|
174
|
+
else log(` (issue não criada: ${(r.stderr || "").trim().slice(0, 120)})`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2. DEV + gate determinístico (lint/test/build) com retry
|
|
178
|
+
const maxRounds = opts.maxDevRounds || 2;
|
|
179
|
+
let gatePassou = false;
|
|
180
|
+
let gateOutput = "";
|
|
181
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
182
|
+
log(`▸ dev (rodada ${round})`);
|
|
183
|
+
const devPrompt = round === 1
|
|
184
|
+
? `Implemente a task na branch atual, com testes quando fizer sentido.${issueNum ? ` Issue #${issueNum}.` : ""}\n\nTASK: ${task}\n\nPLANO:\n${(plan.out || "").slice(0, 3000)}`
|
|
185
|
+
: `Os gates falharam. Corrija mantendo o escopo. Saídas:\n${gateOutput}`;
|
|
186
|
+
const d = runAgent("dev", devPrompt, { cwd, env, timeoutMs: DEV_TIMEOUT_MS });
|
|
187
|
+
log(` dev: ${d.secs}s${d.killed ? " (TIMEOUT)" : ""}`);
|
|
188
|
+
|
|
189
|
+
const scripts = detectScripts(readPkg(cwd));
|
|
190
|
+
if (scripts.length === 0) { gatePassou = true; log(" (sem scripts lint/test/build — gate pulado)"); break; }
|
|
191
|
+
const fails = [];
|
|
192
|
+
for (const s of scripts) {
|
|
193
|
+
const r = sh("npm", ["run", s, "--silent"], cwd); // env original (não o do opencode)
|
|
194
|
+
if (r.status !== 0) fails.push(`[${s}]\n${((r.stdout || "") + (r.stderr || "")).slice(-1500)}`);
|
|
195
|
+
}
|
|
196
|
+
if (fails.length === 0) { gatePassou = true; log(` gate ✓ (${scripts.join(", ")})`); break; }
|
|
197
|
+
gateOutput = fails.join("\n\n");
|
|
198
|
+
log(` gate ✗ (${fails.length} falha[s]) — devolvendo ao dev`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// O dev roda com bash liberado e PODE ter criado a própria branch e commitado
|
|
202
|
+
// nela (o modelo não respeita "fique na branch atual" de forma confiável). Pra
|
|
203
|
+
// manter a garantia determinística, força a branch do host a apontar pro estado
|
|
204
|
+
// final — capturando commits que o modelo fez em branch própria — e só então
|
|
205
|
+
// commita o que sobrou no working tree. `-B` reescreve o ref pra HEAD atual sem
|
|
206
|
+
// tocar a árvore (já bate com o commit), então não há perda.
|
|
207
|
+
sh("git", ["checkout", "-B", branch], cwd);
|
|
208
|
+
sh("git", ["add", "-A"], cwd);
|
|
209
|
+
sh("git", ["commit", "-m", `feat: ${String(task).slice(0, 60)}${issueNum ? ` (#${issueNum})` : ""}`], cwd);
|
|
210
|
+
|
|
211
|
+
// 3. GATE DE QUALIDADE (opt-in via --review). review + qa BLOQUEIAM: REPROVADO
|
|
212
|
+
// devolve ao dev com o feedback, re-roda os scripts e re-commita (até maxQualityRounds).
|
|
213
|
+
// security é ADVISORY (comenta no PR mas não bloqueia — evita falso-positivo travar).
|
|
214
|
+
// Default OFF porque ~triplica o custo de tokens; com ele, o "PR confiável" tem peso.
|
|
215
|
+
const advisory = {};
|
|
216
|
+
let qualityPassed = true;
|
|
217
|
+
if (opts.review) {
|
|
218
|
+
const maxQ = opts.maxQualityRounds || 2;
|
|
219
|
+
for (let q = 1; q <= maxQ; q++) {
|
|
220
|
+
log(`▸ gate de qualidade (rodada ${q}): review + qa + security`);
|
|
221
|
+
const ctx = `BASE do diff: ${base} (use \`git diff ${base}...HEAD\`).${issueNum ? ` Issue #${issueNum}.` : ""}\nTASK: ${task}`;
|
|
222
|
+
// SEQUENCIAL (não paralelo): rodar 3 instâncias do opencode ao mesmo tempo no
|
|
223
|
+
// mesmo projeto faz elas brigarem pelo estado/sessão (~/.local/share/opencode),
|
|
224
|
+
// e todas saíam sem produzir output. Sequencial é mais lento mas confiável.
|
|
225
|
+
const rev = runAgent("review", `${ctx}\nFaça o review crítico e emita o veredicto no formato do agent reviewer.`, { cwd, env });
|
|
226
|
+
const qa = runAgent("qa", `${ctx}\nRode testes/lint/build e emita o veredicto no formato do agent qa.`, { cwd, env });
|
|
227
|
+
const sec = runAgent("security", `${ctx}\nFaça a varredura e emita o veredicto no formato do agent security.`, { cwd, env });
|
|
228
|
+
advisory.review = { text: rev.out, verdict: parseVerdict(rev.out) };
|
|
229
|
+
advisory.qa = { text: qa.out, verdict: parseVerdict(qa.out) };
|
|
230
|
+
advisory.security = { text: sec.out, verdict: parseVerdict(sec.out) };
|
|
231
|
+
log(` review:${advisory.review.verdict || "?"} · qa:${advisory.qa.verdict || "?"} · security:${advisory.security.verdict || "?"} (advisory)`);
|
|
232
|
+
|
|
233
|
+
const blockers = ["review", "qa"].filter((s) => advisory[s].verdict === "REPROVADO");
|
|
234
|
+
if (blockers.length === 0) { qualityPassed = true; log(" gate de qualidade ✓"); break; }
|
|
235
|
+
qualityPassed = false;
|
|
236
|
+
if (q === maxQ) { log(` gate de qualidade ✗ após ${maxQ} rodadas — PR sai marcado p/ revisão humana`); break; }
|
|
237
|
+
|
|
238
|
+
const feedback = blockers.map((s) => `### ${s}\n${(advisory[s].text || "").slice(0, 2500)}`).join("\n\n");
|
|
239
|
+
log(` ✗ REPROVADO (${blockers.join(", ")}) — devolvendo ao dev`);
|
|
240
|
+
runAgent("dev", `O gate de qualidade reprovou. Corrija mantendo o escopo e os critérios. Feedback:\n${feedback}\n\nTASK: ${task}`, { cwd, env, timeoutMs: DEV_TIMEOUT_MS });
|
|
241
|
+
for (const s of detectScripts(readPkg(cwd))) sh("npm", ["run", s, "--silent"], cwd);
|
|
242
|
+
sh("git", ["checkout", "-B", branch], cwd);
|
|
243
|
+
sh("git", ["add", "-A"], cwd);
|
|
244
|
+
sh("git", ["commit", "-m", `fix: ajustes do gate de qualidade (rodada ${q})`], cwd);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 4. PR (host, via gh) + comentários advisory — env ORIGINAL (gh autenticado;
|
|
249
|
+
// NÃO o env do opencode, que sobrescreve XDG_CONFIG_HOME e esconde a auth do gh).
|
|
250
|
+
let prUrl = null;
|
|
251
|
+
if (sh("git", ["push", "-u", "origin", branch], cwd).status === 0) {
|
|
252
|
+
// `--head` explícito: NÃO confiar na branch atual (o modelo pode ter movido o
|
|
253
|
+
// HEAD pra uma branch própria); o PR sempre sai da branch do host.
|
|
254
|
+
const pr = sh("gh", ["pr", "create", "--title", `[novyr] ${String(task).slice(0, 70)}`, "--body", buildPrBody(task, plan.out, advisory, issueNum), "--head", branch, "--base", base], cwd);
|
|
255
|
+
if (pr.status === 0) {
|
|
256
|
+
prUrl = (pr.stdout || "").trim();
|
|
257
|
+
for (const stage of Object.keys(advisory)) {
|
|
258
|
+
const t = (advisory[stage].text || "").trim();
|
|
259
|
+
if (t) sh("gh", ["pr", "comment", prUrl, "--body", `### ${stage}\n\n${t.slice(0, 5000)}`], cwd);
|
|
260
|
+
}
|
|
261
|
+
log(`▸ PR: ${prUrl}`);
|
|
262
|
+
} else log(` (PR não criado: ${(pr.stderr || "").trim().slice(0, 150)})`);
|
|
263
|
+
} else log(" (push falhou — sem remote?)");
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
branch, base, issueUrl, prUrl, gatePassou, qualityPassed,
|
|
267
|
+
advisory: Object.fromEntries(Object.entries(advisory).map(([k, v]) => [k, v.verdict])),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = { run, parseVerdict, stripAnsi, slugify, detectScripts, buildPrBody };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "novyr",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Novyr — corte 60-90% dos tokens do seu Claude Code
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Novyr — corte 60-90% dos tokens do seu Claude Code (engine nvr) + pipeline de agentes da ideia ao PR. Economia em tempo real na statusline.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://novyr.dev",
|
|
7
7
|
"bin": {
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
"lib/",
|
|
18
18
|
"scripts/postinstall.js",
|
|
19
19
|
"README.md",
|
|
20
|
-
"NOTICE"
|
|
20
|
+
"NOTICE",
|
|
21
|
+
"config/opencode/opencode.json",
|
|
22
|
+
"config/opencode/AGENTS.md",
|
|
23
|
+
"config/opencode/agent"
|
|
21
24
|
],
|
|
22
25
|
"optionalDependencies": {
|
|
23
26
|
"@novyr/nvr-linux-x64": "0.1.2",
|