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 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
- > A esteira de agentes autônoma (task PR) é um produto à parte e **não** está
58
- > neste pacote.
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
- // Namespace: este pacote é o companion (compressão + economia no Claude Code).
109
- // A esteira de agentes (Caminho 2, sobre o opencode) é um produto à parte e
110
- // não vai neste pacote reservamos o nome pra não confundir com o engine.
111
- console.error(
112
- "novyr: a esteira de agentes (pipeline / Caminho 2, opencode) não está " +
113
- "incluída neste pacote.\nEste pacote é o companion: compressão + economia " +
114
- "de tokens no Claude Code (`novyr setup`, `novyr line`, `novyr stats`)."
115
- );
116
- process.exit(1);
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 };
@@ -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.1.2",
4
- "description": "Novyr — corte 60-90% dos tokens do seu Claude Code. Traz o nvr (engine de compressão) e mostra a economia em tempo real na statusline.",
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",