@neetru/cli 2.21.0 → 2.24.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/CHANGELOG.md +66 -0
- package/README.md +17 -0
- package/dist/commands/code.js +239 -34
- package/dist/commands/code.js.map +1 -1
- package/dist/commands/dev-context.d.ts +28 -0
- package/dist/commands/dev-context.js +211 -0
- package/dist/commands/dev-context.js.map +1 -0
- package/dist/commands/dev-up.d.ts +2 -0
- package/dist/commands/dev-up.js +122 -18
- package/dist/commands/dev-up.js.map +1 -1
- package/dist/commands/dev.d.ts +3 -0
- package/dist/commands/dev.js +101 -25
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/env.d.ts +1 -1
- package/dist/commands/env.js +17 -10
- package/dist/commands/env.js.map +1 -1
- package/dist/index.js +58 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/code-agent-budget.d.ts +67 -0
- package/dist/lib/code-agent-budget.js +80 -0
- package/dist/lib/code-agent-budget.js.map +1 -0
- package/dist/lib/code-agent-files.d.ts +1 -0
- package/dist/lib/code-agent-files.js +30 -9
- package/dist/lib/code-agent-files.js.map +1 -1
- package/dist/lib/code-agent-protocol.d.ts +5 -0
- package/dist/lib/code-agent-protocol.js +10 -5
- package/dist/lib/code-agent-protocol.js.map +1 -1
- package/dist/lib/code-agent-session.d.ts +1 -1
- package/dist/lib/code-agent-session.js +16 -1
- package/dist/lib/code-agent-session.js.map +1 -1
- package/dist/lib/code-agent-tools.js +40 -10
- package/dist/lib/code-agent-tools.js.map +1 -1
- package/dist/lib/code-agent-tui.d.ts +34 -0
- package/dist/lib/code-agent-tui.js +120 -6
- package/dist/lib/code-agent-tui.js.map +1 -1
- package/dist/lib/code-agent-types.d.ts +19 -1
- package/dist/lib/code-agent-ui.d.ts +7 -0
- package/dist/lib/code-agent-ui.js +44 -30
- package/dist/lib/code-agent-ui.js.map +1 -1
- package/dist/lib/code-agent-web-tools.js +141 -35
- package/dist/lib/code-agent-web-tools.js.map +1 -1
- package/dist/lib/code-agent-write-tools.d.ts +5 -0
- package/dist/lib/code-agent-write-tools.js +141 -0
- package/dist/lib/code-agent-write-tools.js.map +1 -0
- package/dist/lib/dev-local-context.d.ts +29 -0
- package/dist/lib/dev-local-context.js +114 -0
- package/dist/lib/dev-local-context.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,72 @@
|
|
|
2
2
|
|
|
3
3
|
Convenção: [Keep a Changelog](https://keepachangelog.com/) + [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [2.24.0] — 2026-07-13 (contexto dev-local: `neetru dev use/status/sync`)
|
|
6
|
+
|
|
7
|
+
> Trabalho do Codex nesta sessão (mesmo repo, frente separada do `neetru code`).
|
|
8
|
+
|
|
9
|
+
### Adicionado
|
|
10
|
+
|
|
11
|
+
- **Contexto de projeto local** (`.neetru/dev.json`) com 2 modos: `core-product` (projeto ligado a produto/workspace do Core) e `prototype` (rascunho local ainda não cadastrado). Comandos novos: `neetru dev use --product <slug> [--sync]`, `neetru dev use --prototype <nome>`, `neetru dev status`, `neetru dev sync --write-db`.
|
|
12
|
+
- `dev up`/`dev exec` agora leem esse contexto automaticamente pra decidir `corePath`, emuladores padrão e mostrar em qual produto/protótipo a sessão está.
|
|
13
|
+
- `dev sync` busca workspace/tenant dev ativo e bancos dev-local do Core; `--write-db` mescla em `.neetru/db.json`.
|
|
14
|
+
|
|
15
|
+
### Docs
|
|
16
|
+
|
|
17
|
+
- README documenta os 4 comandos novos + o arquivo `.neetru/dev.json`.
|
|
18
|
+
|
|
19
|
+
49 testes novos passando, build limpo. Ver seção abaixo para o restante do que entrou nesta versão (Objective Runtime Controller + hardening de segurança do `neetru code`).
|
|
20
|
+
|
|
21
|
+
## [2.23.0] — 2026-07-13 (`neetru code` — multi-tool por turno, auto-extensao silenciosa em auto mode, HOTFIX render loop no autocomplete)
|
|
22
|
+
|
|
23
|
+
> Sessão com trabalho concorrente Claude+Codex sobre o mesmo turn-loop de `runAgentTask`. Codex: múltiplas `NEETRU_TOOL` por resposta (leitura/pesquisa em lote, mutação continua 1 por vez), auto mode estende o limite de turnos automaticamente sem perguntar `[y/N]` (respeitando o mesmo teto `MAX_TURN_LIMIT_EXTENSIONS`), e `NEETRU_DONE`/`NEETRU_BLOCKED` sem texto não vazam mais crus na UI. Eu: HOTFIX de um crash real reportado ao vivo no autocomplete (2.22.0, ver abaixo).
|
|
24
|
+
|
|
25
|
+
### Adicionado
|
|
26
|
+
|
|
27
|
+
- **Múltiplas ferramentas por resposta** — o modelo pode pedir até 5 `NEETRU_TOOL` na mesma resposta (`parseToolRequests`); leitura/pesquisa roda em lote, mas ferramentas que escrevem/executam/publicam (`write_file`, `git_*`, `shell_exec`, etc. — `SINGLE_ACTION_TOOLS`) continuam limitadas a uma por resposta, as demais voltam com erro claro pedindo pra tentar de novo no próximo turno.
|
|
28
|
+
- **Auto mode estende o limite de turnos sem perguntar** — dentro do mesmo teto `MAX_TURN_LIMIT_EXTENSIONS` (2) já existente; fora do auto mode continua perguntando `[y/N]` a um humano de verdade (ou falhando direto em subagente/não-interativo).
|
|
29
|
+
|
|
30
|
+
### Corrigido
|
|
31
|
+
|
|
32
|
+
- **HOTFIX (2.22.0 → 2.22.1, achado em uso real ao vivo): "Maximum update depth exceeded" ao digitar `/agents` ou `/skills`** — o dropdown do autocomplete usava `key={s.word}` no JSX, mas vários comandos compartilham a mesma 1ª palavra (`/agents start|list|stop|result|apply` todos têm `word="/agents"`; `/skills`/`/skills init`/`/skills show` idem). Key duplicada disparava o warning de dev do React; como `console.error` é redirecionado pro `LineBuffer` (que chama `setState`), o próprio warning disparava outro render, que warnava de novo — loop infinito, painel travava ao abrir. Corrigido pra `key={s.cmd}` (esse sim único em toda a lista) + teste de regressão garantindo que `cmd` nunca colide.
|
|
33
|
+
- `NEETRU_DONE`/`NEETRU_BLOCKED` sem nenhum texto depois do marcador não vazam mais crus pro usuário — caem em "Concluido."/"Bloqueado." como fallback.
|
|
34
|
+
- **Ainda não estava corrigido apesar da alegação**: a redação "NEETRU_DONE se concluiu o máximo possível" (última chamada, quando o limite de turnos se aproxima) misturava "fiz o que deu" com "objetivo realmente concluído" — o modelo podia marcar uma tarefa incompleta como terminada só por ter ficado sem turnos. `NEETRU_DONE` agora exige conclusão REAL; sem isso, cai em texto normal resumindo o progresso.
|
|
35
|
+
|
|
36
|
+
### Segurança (achados de revisão de segurança sobre os commits desta sessão)
|
|
37
|
+
|
|
38
|
+
- **HIGH — escrita através de symlink escapava do projeto**: `resolveInside` só revalidava o realpath quando o ALVO já existia; `write_file`/`append_file` para um arquivo NOVO dentro de uma pasta symlinkada apontando pra fora do cwd escapavam da checagem por completo (o arquivo-alvo em si nunca existe antes da escrita). Corrigido pra resolver o realpath do ancestral EXISTENTE mais próximo do alvo, não só do alvo.
|
|
39
|
+
- **HIGH — SSRF via redirecionamento em `web_fetch`**: validava só a URL inicial e seguia redirecionamento automaticamente (`redirect: 'follow'`); um domínio público podia redirecionar pra `169.254.169.254`/localhost/IP privado sem revalidar. Trocado pra `redirect: 'manual'` com revalidação a cada salto (máx. 5) + resolução de DNS também validada contra rede privada/link-local (best-effort — não é prova contra DNS rebinding sem um dispatcher fixando o IP).
|
|
40
|
+
- **Médio — `web_fetch` só checava o limite de tamanho DEPOIS de carregar o corpo inteiro**: `content-length` era validado, mas a resposta inteira já tinha sido lida antes da checagem de tamanho real disparar. Agora lê em chunks e aborta assim que passa do limite.
|
|
41
|
+
|
|
42
|
+
### Adicionado — Objective Runtime Controller (`code-agent-budget.ts`, novo módulo)
|
|
43
|
+
|
|
44
|
+
> Pedido direto do usuário após um transcript real de sessão travada: o modelo ficava repetindo "avanço parcial" em modo objetivo sem nunca chamar tool nem propor patch, e `settings.maxTurns` — um CONTADOR, não um sinal de progresso — não enxergava isso. Design pesquisado (arquitetura documentada do Claude Code: modos de permissão, loop context→ação→verificação, bloqueio-com-motivo em vez de `[y/N]`) e sintetizado via 3 propostas independentes + 3 rodadas de verificação adversarial antes de implementar — a 1ª síntese tinha 4 problemas reais (edge de estado faltando, promessa falsa de segurança do tsc + bug real de `pendingPlanTask`, colisão com o mecanismo de patch-repair já testado, import circular), todos corrigidos e reverificados antes do código ser escrito.
|
|
45
|
+
|
|
46
|
+
- **2 novas dimensões de orçamento, ortogonais a `maxTurns`**: `noProgress` (turnos consecutivos em modo objetivo sem tool call nem patch — o modelo só descreve avanço) e `repeatedErrors` (total CUMULATIVO de patches inválidos na sessão inteira — nunca reseta, ao contrário do `patchRepairStreak` local que já existia e continua intacto; pega o padrão que o mecanismo antigo não pega: erra 3x, muda de estratégia, reseta, erra 3x de novo, indefinidamente sem convergir).
|
|
47
|
+
- **Novo outcome `budget_exceeded`** em `AgentTaskOutcome` — nunca se estende (nem em auto mode, diferente do limite de turnos): as duas dimensões são sinal de que o modelo não está CONVERGINDO, não de que faltou tempo.
|
|
48
|
+
- **Checkpoint roda logo após `goto('thinking')`** no loop — só precisou de 1 edge nova na máquina de estados (`thinking -> budget_exceeded`) em vez de enumerar os 3 estados alcançáveis no topo do loop, porque todos já têm edge legal pra `thinking` hoje.
|
|
49
|
+
- `MAX_CONSECUTIVE_PATCH_REPAIRS`/`MAX_TOOLS_PER_TURN`/`SINGLE_ACTION_TOOLS` migraram de `commands/code.ts` pra `code-agent-budget.ts` (evita um import circular: o novo módulo não pode importar essas constantes DE VOLTA de `code.ts` já que `code.ts` importa `decideBudgetAction`/`BudgetTracker` de lá).
|
|
50
|
+
- Os dois pontos que comparavam `outcome.kind` com `if/else` (não pegos por `tsc` — nenhum é um `switch` exaustivo hoje) viraram `switch` com `const _exhaustive: never = outcome.kind` no `default`, fechando de vez um bug real e independente: em modo plano, um outcome `'aborted'` deixava `pendingPlanTask` num estado obsoleto (nem atualizado, nem limpo) — `/approve` executaria um plano errado depois. Corrigido preservando um plano anterior ainda válido quando o turno atual não produziu nem plano nem execução real.
|
|
51
|
+
|
|
52
|
+
1051 testes passando, tsc limpo (isolado das mudanças desta sessão — um arquivo novo e não relacionado de outra frente de trabalho em andamento no mesmo repo tem 2 erros de tipo pré-existentes, fora do escopo desta mudança).
|
|
53
|
+
|
|
54
|
+
> Feedback direto do uso real (transcript colado pelo usuário): um modelo preso reescrevendo o mesmo diff quebrado gastava o orçamento de turnos INTEIRO em correções de patch e a sessão inteira falhava com "Limite de turnos atingido" sem chance de continuar. Diagnóstico confirmado por revisão de código (Codex): o problema é arquitetural — o CLI depende só de `NEETRU_PATCH` (unified diff cru gerado pelo modelo) como único caminho de escrita.
|
|
55
|
+
|
|
56
|
+
### Adicionado
|
|
57
|
+
|
|
58
|
+
- **Autocomplete de slash commands** — digitar `/` na sessão interativa lista os comandos, narrowing conforme digita (substring, não só prefixo), setas ↑/↓ navegam, Tab ou Enter (quando a sugestão ainda não é o texto exato) completa em vez de submeter. Fonte única (`SLASH_COMMANDS` em `code-agent-ui.ts`) compartilhada com `/help`, pra nunca ficar desalinhada.
|
|
59
|
+
- **3 ferramentas deterministicas de escrita** (`code-agent-write-tools.ts`) — `write_file` (cria/sobrescreve um arquivo inteiro), `append_file` (adiciona ao final), `replace_in_file` (troca um trecho exato, com detecção de ocorrência ambígua e `"replace_all":true` explícito). Viram o caminho PRINCIPAL pra criar/editar UM arquivo por vez; `NEETRU_PATCH` vira fallback só pra diff multi-arquivo/multi-hunk num único turno. Mesmo tier de confirmação de `git_commit`/`git_branch` (local/reversível, `--yes` dispensa a pergunta); path sempre resolvido via `resolveInside` (contenção + symlink real path) e escrita em arquivo de segredo (.env/chave) bloqueada, espelhando o bloqueio de leitura. Bloqueadas em modo plano.
|
|
60
|
+
|
|
61
|
+
### Corrigido
|
|
62
|
+
|
|
63
|
+
- **Loop de patch inválido gastava o orçamento de turnos inteiro** — cada tentativa de correção consumia 1 turno igual a qualquer outro; um modelo preso no mesmo erro nunca sobrava turno pra nada. Depois de 3 tentativas SEGUIDAS de patch inválido, o loop para de pedir "corrija o diff" e força o modelo a mudar de estratégia (explicar em texto, ou — com as novas ferramentas de escrita — usar `write_file`/`replace_in_file` no arquivo específico).
|
|
64
|
+
- **"Limite de turnos atingido" matava a sessão sem chance de continuar** — esgotar `maxTurns` agora pergunta "Continuar mais N turnos?" antes de falhar, mas SÓ quando há um humano de verdade (`!silent`) — subagentes e sessões não-interativas (sem TTY) continuam falhando direto, sem regressão de comportamento nem risco de loop custando dinheiro sem supervisão.
|
|
65
|
+
- **Achado testando a própria correção acima: a extensão de turnos podia rodar PARA SEMPRE** — um `confirm` que sempre aprova (script/automação, ou um `confirm` de teste ingênuo) mantinha um modelo genuinamente preso (nunca conclui, nunca bloqueia) estendendo o limite indefinidamente — travou de verdade um worker de teste até estourar memória. Novo teto `MAX_TURN_LIMIT_EXTENSIONS` (2): depois disso, falha direto mesmo que a extensão continue sendo aprovada.
|
|
66
|
+
|
|
67
|
+
### Docs
|
|
68
|
+
|
|
69
|
+
- System prompt (`buildSystemPrompt`) atualizado pra orientar o modelo a preferir write_file/append_file/replace_in_file sobre NEETRU_PATCH pra mudança de um arquivo só, e a abandonar um diff que já falhou 2-3 vezes em vez de insistir.
|
|
70
|
+
|
|
5
71
|
## [2.21.0] — 2026-07-13 (`neetru code` — skills, modo objetivo/plano/auto, ferramentas web/GitHub + hardening pós-review)
|
|
6
72
|
|
|
7
73
|
> Sessão com trabalho concorrente Claude+Codex no mesmo repo. Codex implementou skills, patch pre-check, modo objetivo/plano/auto, `/approve`/`/auto` e as ferramentas `web_search`/`web_fetch`/`github_search`/`github_repo`. Um Codex-review dedicado (`codex-rescue`) auditou os 4 commits anteriores (2.20.1-2.20.3 + o `feat` de base); os achados P1/P2 confirmados por leitura direta do código + os achados adicionais encontrados nesta revisão foram corrigidos nesta mesma versão, antes da publicação.
|
package/README.md
CHANGED
|
@@ -111,8 +111,25 @@ neetru dev up --skip-core
|
|
|
111
111
|
neetru dev exec "npm run test:integration"
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
Contexto do projeto local:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Projeto ligado a um produto já existente no Core:
|
|
118
|
+
neetru dev use --product meu-produto --sync
|
|
119
|
+
|
|
120
|
+
# Protótipo local ainda não cadastrado no Core:
|
|
121
|
+
neetru dev use --prototype meu-rascunho
|
|
122
|
+
|
|
123
|
+
# Ver o contexto efetivo que dev up/dev exec vão usar:
|
|
124
|
+
neetru dev status
|
|
125
|
+
|
|
126
|
+
# Sincronizar bancos dev-local do Core para .neetru/db.json:
|
|
127
|
+
neetru dev sync --write-db
|
|
128
|
+
```
|
|
129
|
+
|
|
114
130
|
> `neetru dev` (sem subcomando) mantém o modo legado: banco Docker local + watch de schema.
|
|
115
131
|
> Flags: `--only firestore,auth,functions`, `--core-port 9003`, `--core-path <dir>`.
|
|
132
|
+
> O arquivo `.neetru/dev.json` guarda o contexto local: produto Core ou protótipo, Core path e emuladores padrão.
|
|
116
133
|
|
|
117
134
|
## Configuração
|
|
118
135
|
|
package/dist/commands/code.js
CHANGED
|
@@ -7,11 +7,12 @@ import { config } from '../lib/config.js';
|
|
|
7
7
|
import { getActiveTokenSync } from '../lib/token-resolver.js';
|
|
8
8
|
import { log } from '../utils/logger.js';
|
|
9
9
|
import { setSafeExitCode } from '../utils/safe-exit.js';
|
|
10
|
-
import {
|
|
10
|
+
import { parseToolRequests, extractPatch } from '../lib/code-agent-protocol.js';
|
|
11
11
|
import { applyUnifiedPatch, checkPatchApplies, getGitDiff, getProjectOverview, listFiles, readFileContext, renderColoredDiff, runCodeTool, truncateForGateway, } from '../lib/code-agent-files.js';
|
|
12
12
|
import { compactCodeHistory, compactHistoryToFit, MAX_HISTORY_TOTAL_CHARS, saveCodeSession, autoSaveCodeSession, listCodeSessions, loadCodeSession, logAppliedPatch, CodeAgentStateMachine, } from '../lib/code-agent-session.js';
|
|
13
13
|
import { DEFAULT_CODE_MAX_TOKENS, DEFAULT_CODE_MAX_TURNS, DEFAULT_CODE_TEMPERATURE, ULTRACODE_EFFORT, ULTRACODE_MAX_TOKENS, ULTRACODE_MAX_TURNS, parseBoundedNumber, resolveCodeEffort, resolveCodeModel, resolveCodeProvider, } from '../lib/code-agent-settings.js';
|
|
14
14
|
import { callGateway, streamGatewayChat, AI_GATEWAY_CHAT_PATH } from '../lib/code-agent-gateway.js';
|
|
15
|
+
import { createBudgetTracker, decideBudgetAction, MAX_CONSECUTIVE_PATCH_REPAIRS, MAX_TOOLS_PER_TURN, SINGLE_ACTION_TOOLS, } from '../lib/code-agent-budget.js';
|
|
15
16
|
import { CliApiError } from '../lib/api-client.js';
|
|
16
17
|
import { buildSystemPrompt } from '../lib/code-agent-tools.js';
|
|
17
18
|
import { CODE_SKILLS_ROOT, installAllBuiltinSkills, loadCodeSkills, normalizeSkillName } from '../lib/code-agent-skills.js';
|
|
@@ -206,6 +207,32 @@ export async function callGatewayWithProgressRetrying(settings, messages, spinne
|
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
209
|
}
|
|
210
|
+
// 2026-07-13 (achado em uso real, transcript colado pelo usuario): um modelo
|
|
211
|
+
// preso repetindo o MESMO diff quebrado ("corrupt patch") gastava o
|
|
212
|
+
// orcamento de turnos INTEIRO so em correcoes de patch — cada tentativa
|
|
213
|
+
// invalida consome 1 turno igual a qualquer outro, entao 3-4 tentativas
|
|
214
|
+
// seguidas ja fecham a sessao inteira sem sobrar turno pra nada. Depois de
|
|
215
|
+
// N tentativas seguidas, para de pedir "corrija o diff" (que nao esta
|
|
216
|
+
// funcionando) e forca o modelo a mudar de estrategia: explicar em texto em
|
|
217
|
+
// vez de insistir no mesmo formato.
|
|
218
|
+
// (MAX_CONSECUTIVE_PATCH_REPAIRS/MAX_TOOLS_PER_TURN/SINGLE_ACTION_TOOLS agora
|
|
219
|
+
// vivem em code-agent-budget.ts — evita um ciclo de import com o Objective
|
|
220
|
+
// Runtime Controller, que consome esse modulo; ver comentario la.)
|
|
221
|
+
// Limite de turnos "duro demais" era a outra metade da mesma reclamacao:
|
|
222
|
+
// ao esgotar `maxTurns` a sessao INTEIRA falhava com um erro opaco, sem
|
|
223
|
+
// chance do usuario (que esta ali, na sessao interativa) simplesmente pedir
|
|
224
|
+
// mais turnos pra terminar algo que estava proximo. So oferece a extensao
|
|
225
|
+
// quando ha um humano de verdade pra perguntar — subagentes (`silent`) e
|
|
226
|
+
// modo nao-interativo sem TTY (`defaultConfirm` ja retorna false) continuam
|
|
227
|
+
// falhando direto, sem regressao de comportamento.
|
|
228
|
+
const TURN_LIMIT_EXTENSION = 6;
|
|
229
|
+
// Teto de extensoes por tarefa — sem isso, um `confirm` que sempre aprova
|
|
230
|
+
// (script/automacao, ou um humano otimista demais) mantem um modelo preso
|
|
231
|
+
// num loop que nunca conclui rodando PARA SEMPRE, um turno estendido atras
|
|
232
|
+
// do outro (achado rodando os testes: um mock de tool-loop infinito com
|
|
233
|
+
// confirm sempre-true travou o worker de teste ate estourar memoria).
|
|
234
|
+
// Depois do teto, falha direto — nao pergunta mais, mesmo que aprovassem.
|
|
235
|
+
const MAX_TURN_LIMIT_EXTENSIONS = 2;
|
|
209
236
|
function classifyObjectiveText(content) {
|
|
210
237
|
const trimmed = content.trimStart();
|
|
211
238
|
if (/^NEETRU_DONE\b/i.test(trimmed))
|
|
@@ -220,6 +247,22 @@ function stripObjectiveMarker(content) {
|
|
|
220
247
|
.replace(/^\s*NEETRU_BLOCKED\b[:\s-]*/i, '')
|
|
221
248
|
.trim();
|
|
222
249
|
}
|
|
250
|
+
function objectiveDisplayText(state, content) {
|
|
251
|
+
if (state === 'done')
|
|
252
|
+
return stripObjectiveMarker(content) || 'Concluido.';
|
|
253
|
+
if (state === 'blocked')
|
|
254
|
+
return stripObjectiveMarker(content) || 'Bloqueado.';
|
|
255
|
+
return content;
|
|
256
|
+
}
|
|
257
|
+
function isAutoMode(settings) {
|
|
258
|
+
return settings.apply === true && settings.yes === true && settings.objectiveMode === true;
|
|
259
|
+
}
|
|
260
|
+
function canAutoExtendTurnLimit(settings, silent, usedExtensions) {
|
|
261
|
+
return !silent && isAutoMode(settings) && usedExtensions < MAX_TURN_LIMIT_EXTENSIONS;
|
|
262
|
+
}
|
|
263
|
+
function extendTurnLimit(settings) {
|
|
264
|
+
settings.maxTurns += TURN_LIMIT_EXTENSION;
|
|
265
|
+
}
|
|
223
266
|
function buildObjectiveContinuationPrompt(turn, maxTurns) {
|
|
224
267
|
return [
|
|
225
268
|
`Modo objetivo: sua resposta anterior parece um avanco parcial, nao uma conclusao marcada.`,
|
|
@@ -331,8 +374,33 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
331
374
|
// aparecendo como linha permanente via printEvent, pausando o spinner
|
|
332
375
|
// pra nao interlear as duas saidas.
|
|
333
376
|
let spinner = makeSpinner();
|
|
377
|
+
let patchRepairStreak = 0;
|
|
378
|
+
let turnLimitExtensionsUsed = 0;
|
|
379
|
+
const budgetTracker = createBudgetTracker();
|
|
334
380
|
try {
|
|
335
|
-
for (let turn = 1
|
|
381
|
+
for (let turn = 1;; turn++) {
|
|
382
|
+
if (turn > settings.maxTurns) {
|
|
383
|
+
// Rede de seguranca virou pergunta em vez de falha direta (achado em
|
|
384
|
+
// uso real): so faz sentido quando ha um humano de verdade podendo
|
|
385
|
+
// responder — `confirm` ja retorna false sem TTY/em subagentes
|
|
386
|
+
// (`silent`), preservando o comportamento antigo nesses casos. Teto
|
|
387
|
+
// de MAX_TURN_LIMIT_EXTENSIONS: mesmo aprovado, nao pergunta pra
|
|
388
|
+
// sempre — um modelo genuinamente preso (nunca conclui, nunca
|
|
389
|
+
// bloqueia) nao pode virar loop infinito so porque alguem/algo
|
|
390
|
+
// aprovou a extensao uma vez.
|
|
391
|
+
spinner.stop();
|
|
392
|
+
const autoExtend = canAutoExtendTurnLimit(settings, silent, turnLimitExtensionsUsed);
|
|
393
|
+
const canOfferExtension = !silent && !autoExtend && turnLimitExtensionsUsed < MAX_TURN_LIMIT_EXTENSIONS;
|
|
394
|
+
const shouldExtend = autoExtend || (canOfferExtension && (await confirm(`\nLimite de ${settings.maxTurns} turno(s) atingido sem concluir. Continuar mais ${TURN_LIMIT_EXTENSION} turno(s)? [y/N] `)));
|
|
395
|
+
if (!shouldExtend) {
|
|
396
|
+
goto('failed');
|
|
397
|
+
throw new Error(`Limite de turnos atingido (${settings.maxTurns}) sem resposta final.`);
|
|
398
|
+
}
|
|
399
|
+
turnLimitExtensionsUsed++;
|
|
400
|
+
extendTurnLimit(settings);
|
|
401
|
+
emit('ok', autoExtend ? 'Limite de turnos estendido automaticamente' : 'Limite de turnos estendido', `+${TURN_LIMIT_EXTENSION} (agora ${settings.maxTurns} no total)`);
|
|
402
|
+
spinner = makeSpinner();
|
|
403
|
+
}
|
|
336
404
|
// Cancelamento cooperativo: verificado na fronteira de cada turno. A
|
|
337
405
|
// chamada de rede em voo tem timeout proprio (code-agent-gateway.ts), que
|
|
338
406
|
// este modulo nao controla sem editar aquele arquivo — o corte efetivo
|
|
@@ -342,6 +410,24 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
342
410
|
return { kind: 'aborted' };
|
|
343
411
|
}
|
|
344
412
|
goto('thinking');
|
|
413
|
+
// Objective Runtime Controller (2026-07-13) — checkpoint de orcamento
|
|
414
|
+
// logo APOS goto('thinking') de proposito: os 3 estados alcancaveis no
|
|
415
|
+
// topo do loop (idle/thinking/tool_running, via os `continue` do loop)
|
|
416
|
+
// ja tem edge legal pra 'thinking' hoje, entao colocar o checkpoint
|
|
417
|
+
// AQUI exige so UMA edge nova na maquina de estados (thinking ->
|
|
418
|
+
// budget_exceeded), em vez de enumerar as 3. Nunca estende (nem em
|
|
419
|
+
// auto mode, diferente do limite de turnos acima): noProgress/
|
|
420
|
+
// repeatedErrors sao sinal de que o modelo nao esta CONVERGINDO, nao
|
|
421
|
+
// de que faltou tempo — estender so prolongaria a mesma falha.
|
|
422
|
+
const budgetDecision = decideBudgetAction(budgetTracker, options.budgetOverrides);
|
|
423
|
+
if (budgetDecision.exceeded) {
|
|
424
|
+
spinner.stop();
|
|
425
|
+
if (!silent) {
|
|
426
|
+
emit('warn', 'Orcamento do modo objetivo excedido', `${budgetDecision.dimension} — ${budgetDecision.reason}`);
|
|
427
|
+
}
|
|
428
|
+
goto('budget_exceeded');
|
|
429
|
+
return { kind: 'budget_exceeded', dimension: budgetDecision.dimension, reason: budgetDecision.reason };
|
|
430
|
+
}
|
|
345
431
|
// GAP-M1-15: auto-compact por TAMANHO antes de cada chamada — sem isso
|
|
346
432
|
// uma sessao com varias tool results grandes (cada uma ate
|
|
347
433
|
// MAX_GATEWAY_SAFE_CHARS) estoura o limite de payload TOTAL do
|
|
@@ -358,9 +444,16 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
358
444
|
// em producao: "oi" gastou os 8 turnos so chamando tools e falhou com
|
|
359
445
|
// "Limite de turnos atingido"). No ultimo turno permitido, forca o
|
|
360
446
|
// modelo a responder AGORA em vez de so descobrir isso ao falhar.
|
|
361
|
-
if (turn === settings.maxTurns) {
|
|
447
|
+
if (turn === settings.maxTurns && !canAutoExtendTurnLimit(settings, silent, turnLimitExtensionsUsed)) {
|
|
448
|
+
// 2026-07-13 (achado do Codex review): a redacao antiga ("NEETRU_DONE
|
|
449
|
+
// se concluiu o maximo possivel") misturava "fiz o que deu" com
|
|
450
|
+
// "objetivo realmente concluido" — o modelo podia marcar NEETRU_DONE
|
|
451
|
+
// so por estar sem turnos, reportando uma tarefa incompleta como
|
|
452
|
+
// terminada. NEETRU_DONE agora exige conclusao REAL; sem isso, texto
|
|
453
|
+
// normal resumindo o progresso (cai no classificador 'partial', que
|
|
454
|
+
// ja avisa "Limite do modo objetivo atingido" em vez de fingir feito).
|
|
362
455
|
const finalTurnInstruction = settings.objectiveMode
|
|
363
|
-
? 'Este e o ultimo turno disponivel.
|
|
456
|
+
? 'Este e o ultimo turno disponivel. Se o objetivo esta REALMENTE concluido, responda com NEETRU_DONE. Se ha um bloqueio real (falta de informacao/credencial/decisao externa), responda com NEETRU_BLOCKED. Caso contrario — se ainda falta trabalho mas o tempo acabou — responda em texto normal resumindo o progresso ate aqui, SEM usar NEETRU_DONE (nao esta concluido so porque o tempo acabou). Nao peca mais NEETRU_TOOL nem proponha patch.'
|
|
364
457
|
: 'Este e o ultimo turno disponivel. Responda AGORA com uma conclusao em texto usando o que voce ja sabe — nao peca mais NEETRU_TOOL nem proponha patch.';
|
|
365
458
|
messages.push({
|
|
366
459
|
role: 'user',
|
|
@@ -373,16 +466,40 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
373
466
|
spinner = gatewayResult.spinner;
|
|
374
467
|
const content = gatewayResult.content;
|
|
375
468
|
messages.push({ role: 'assistant', content });
|
|
376
|
-
const
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
469
|
+
const tools = parseToolRequests(content);
|
|
470
|
+
if (tools.length > 0) {
|
|
471
|
+
patchRepairStreak = 0;
|
|
472
|
+
budgetTracker.noProgressStreak = 0; // acao real (tool) — nao e "so texto"
|
|
473
|
+
const selectedTools = tools.slice(0, MAX_TOOLS_PER_TURN);
|
|
474
|
+
const results = [];
|
|
475
|
+
let singleActionSeen = false;
|
|
476
|
+
for (const [index, tool] of selectedTools.entries()) {
|
|
477
|
+
const isSingleAction = SINGLE_ACTION_TOOLS.has(tool.tool);
|
|
478
|
+
if (isSingleAction && singleActionSeen) {
|
|
479
|
+
results.push(`--- Resultado da tool ${index + 1}/${selectedTools.length}: ${tool.tool} ---\n` +
|
|
480
|
+
`ERRO: apenas uma ferramenta que escreve, executa codigo ou publica pode rodar por resposta. ` +
|
|
481
|
+
`Peca esta acao novamente no proximo turno se ainda for necessaria.`);
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (isSingleAction)
|
|
485
|
+
singleActionSeen = true;
|
|
486
|
+
if (index > 0)
|
|
487
|
+
goto('thinking');
|
|
488
|
+
goto('tool_requested');
|
|
489
|
+
spinner.update(`${baseDetail} · tool ${index + 1}/${selectedTools.length}: ${tool.tool}`);
|
|
490
|
+
goto('tool_running');
|
|
491
|
+
const result = await runCodeTool(settings.cwd, tool, { settings, confirm });
|
|
492
|
+
spinner.update(`${baseDetail} · tool ${index + 1}/${selectedTools.length}: ${summarizeToolResult(result)}`);
|
|
493
|
+
results.push(`--- Resultado da tool ${index + 1}/${selectedTools.length}: ${tool.tool} ---\n${result}`);
|
|
494
|
+
}
|
|
495
|
+
if (tools.length > selectedTools.length) {
|
|
496
|
+
results.push(`--- Ferramentas ignoradas ---\n` +
|
|
497
|
+
`A resposta pediu ${tools.length} ferramentas; o limite por turno e ${MAX_TOOLS_PER_TURN}. ` +
|
|
498
|
+
`Peca as restantes no proximo turno se ainda forem necessarias.`);
|
|
499
|
+
}
|
|
383
500
|
messages.push({
|
|
384
501
|
role: 'user',
|
|
385
|
-
content: truncateForGateway(`
|
|
502
|
+
content: truncateForGateway(`Resultados das tools:\n\n${results.join('\n\n')}`),
|
|
386
503
|
});
|
|
387
504
|
continue;
|
|
388
505
|
}
|
|
@@ -398,6 +515,29 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
398
515
|
if (settings.apply) {
|
|
399
516
|
const checkError = checkPatchApplies(settings.cwd, patch);
|
|
400
517
|
if (checkError) {
|
|
518
|
+
patchRepairStreak++;
|
|
519
|
+
// Cumulativo, NUNCA reseta (nem quando patchRepairStreak reseta
|
|
520
|
+
// 2 linhas abaixo) — pega o padrao que o streak local nao pega:
|
|
521
|
+
// erra 3x, muda de estrategia (streak reseta), volta a errar 3x,
|
|
522
|
+
// reseta de novo, indefinidamente sem convergir. Ver
|
|
523
|
+
// code-agent-budget.ts.
|
|
524
|
+
budgetTracker.repeatedErrorTotal++;
|
|
525
|
+
// 3+ tentativas seguidas de patch invalido: pedir "corrija o
|
|
526
|
+
// diff" de novo claramente nao esta funcionando (achado em uso
|
|
527
|
+
// real — o mesmo erro se repetia turno apos turno ate esgotar o
|
|
528
|
+
// limite). Muda de estrategia em vez de insistir no mesmo pedido.
|
|
529
|
+
if (patchRepairStreak >= MAX_CONSECUTIVE_PATCH_REPAIRS) {
|
|
530
|
+
spinner.update(`${baseDetail} · patch invalido varias vezes, mudando de estrategia`);
|
|
531
|
+
emit('warn', 'Patch invalido varias vezes seguidas — pedindo explicacao em texto', `${patchRepairStreak} tentativas · ultimo erro: ${checkError.slice(0, 200)}`);
|
|
532
|
+
messages.push({
|
|
533
|
+
role: 'user',
|
|
534
|
+
content: truncateForGateway(`Ja foram ${patchRepairStreak} tentativas seguidas de patch invalido (ultimo erro: ${checkError.slice(0, 200)}). ` +
|
|
535
|
+
'Pare de tentar gerar um diff neste turno. Descreva em texto claro, para o usuario, exatamente o ' +
|
|
536
|
+
'que precisa mudar (arquivo, trecho, motivo) — sem NEETRU_PATCH desta vez.'),
|
|
537
|
+
});
|
|
538
|
+
patchRepairStreak = 0;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
401
541
|
spinner.update(`${baseDetail} · patch invalido, pedindo correcao`);
|
|
402
542
|
emit('warn', 'Patch nao aplicavel, pedindo correcao ao modelo', checkError.slice(0, 300));
|
|
403
543
|
messages.push({
|
|
@@ -408,6 +548,7 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
408
548
|
continue;
|
|
409
549
|
}
|
|
410
550
|
}
|
|
551
|
+
budgetTracker.noProgressStreak = 0; // patch valido — acao real, nao "so texto"
|
|
411
552
|
spinner.stop();
|
|
412
553
|
goto('patch_ready');
|
|
413
554
|
if (!silent) {
|
|
@@ -438,8 +579,19 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
438
579
|
}
|
|
439
580
|
if (settings.objectiveMode) {
|
|
440
581
|
const objectiveState = classifyObjectiveText(content);
|
|
441
|
-
const
|
|
442
|
-
if (objectiveState === 'partial' &&
|
|
582
|
+
const displayContent = objectiveDisplayText(objectiveState, content);
|
|
583
|
+
if (objectiveState === 'partial' &&
|
|
584
|
+
(turn < settings.maxTurns || canAutoExtendTurnLimit(settings, silent, turnLimitExtensionsUsed))) {
|
|
585
|
+
if (turn >= settings.maxTurns) {
|
|
586
|
+
turnLimitExtensionsUsed++;
|
|
587
|
+
extendTurnLimit(settings);
|
|
588
|
+
emit('ok', 'Limite de turnos estendido automaticamente', `+${TURN_LIMIT_EXTENSION} (agora ${settings.maxTurns} no total)`);
|
|
589
|
+
}
|
|
590
|
+
patchRepairStreak = 0;
|
|
591
|
+
// Chegar aqui significa: sem tool call, sem patch (os dois ramos
|
|
592
|
+
// anteriores no loop ja fazem `continue`/`return` antes disto) —
|
|
593
|
+
// "sem progresso" e definicional pra este turno.
|
|
594
|
+
budgetTracker.noProgressStreak++;
|
|
443
595
|
spinner.stop();
|
|
444
596
|
if (!silent) {
|
|
445
597
|
emit('ok', 'Avanco parcial', 'modo objetivo continuara trabalhando');
|
|
@@ -458,10 +610,10 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
458
610
|
if (objectiveState === 'partial') {
|
|
459
611
|
emit('warn', 'Limite do modo objetivo atingido', `${settings.maxTurns} turno(s)`);
|
|
460
612
|
}
|
|
461
|
-
printAssistantText(
|
|
613
|
+
printAssistantText(displayContent);
|
|
462
614
|
}
|
|
463
615
|
goto('idle');
|
|
464
|
-
return { kind: 'text', text:
|
|
616
|
+
return { kind: 'text', text: displayContent };
|
|
465
617
|
}
|
|
466
618
|
spinner.stop();
|
|
467
619
|
if (!silent) {
|
|
@@ -471,9 +623,6 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
471
623
|
goto('idle');
|
|
472
624
|
return { kind: 'text', text: content };
|
|
473
625
|
}
|
|
474
|
-
spinner.stop();
|
|
475
|
-
goto('failed');
|
|
476
|
-
throw new Error(`Limite de turnos atingido (${settings.maxTurns}) sem resposta final.`);
|
|
477
626
|
}
|
|
478
627
|
catch (error) {
|
|
479
628
|
spinner.stop();
|
|
@@ -581,17 +730,44 @@ async function runAgentsCommand(line, settings, confirm) {
|
|
|
581
730
|
return;
|
|
582
731
|
}
|
|
583
732
|
const outcome = record.outcome;
|
|
584
|
-
if (outcome
|
|
585
|
-
printEvent('ok', 'Patch proposto', `#${idArg} — /agents apply ${idArg} pra aplicar`);
|
|
586
|
-
printPatchPreview(outcome.patch);
|
|
587
|
-
}
|
|
588
|
-
else if (outcome?.kind === 'text') {
|
|
589
|
-
printAssistantText(outcome.text ?? '(sem texto)');
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
733
|
+
if (!outcome) {
|
|
592
734
|
printEvent('warn', 'Sem resultado aplicavel', `#${idArg}`);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
// switch exaustivo (nao if/else) de proposito: um `outcome.kind` novo
|
|
738
|
+
// sem `case` aqui quebra `tsc --noEmit` de verdade (assign pro `never`
|
|
739
|
+
// no default), ao contrario do if/else que isto substitui — ver
|
|
740
|
+
// achado do Codex review 2026-07-13 (a promessa antiga de "o tsc pega"
|
|
741
|
+
// era falsa pra if/else). Todo `case` (inclusive `default`) termina em
|
|
742
|
+
// `return` — isto e o ULTIMO statement de `case 'result':` no switch
|
|
743
|
+
// externo (`switch (sub)` acima); sem `return` em todo ramo, cair do
|
|
744
|
+
// switch interno cairia direto no proximo `case` do switch externo.
|
|
745
|
+
switch (outcome.kind) {
|
|
746
|
+
case 'patch':
|
|
747
|
+
if (outcome.patch) {
|
|
748
|
+
printEvent('ok', 'Patch proposto', `#${idArg} — /agents apply ${idArg} pra aplicar`);
|
|
749
|
+
printPatchPreview(outcome.patch);
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
printEvent('warn', 'Sem resultado aplicavel', `#${idArg}`);
|
|
753
|
+
}
|
|
754
|
+
return;
|
|
755
|
+
case 'text':
|
|
756
|
+
printAssistantText(outcome.text ?? '(sem texto)');
|
|
757
|
+
return;
|
|
758
|
+
case 'budget_exceeded':
|
|
759
|
+
printEvent('warn', 'Subagente parou por orcamento excedido', `${outcome.dimension ?? '?'} — ${outcome.reason ?? `#${idArg}`}`);
|
|
760
|
+
return;
|
|
761
|
+
case 'applied':
|
|
762
|
+
case 'aborted':
|
|
763
|
+
printEvent('warn', 'Sem resultado aplicavel', `#${idArg}`);
|
|
764
|
+
return;
|
|
765
|
+
default: {
|
|
766
|
+
const _exhaustive = outcome.kind;
|
|
767
|
+
printEvent('warn', 'Sem resultado aplicavel', `#${idArg}`);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
593
770
|
}
|
|
594
|
-
return;
|
|
595
771
|
}
|
|
596
772
|
case 'apply': {
|
|
597
773
|
if (!idArg) {
|
|
@@ -986,12 +1162,41 @@ async function runInteractive(settings) {
|
|
|
986
1162
|
const answer = await ctx.askLine(question);
|
|
987
1163
|
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
988
1164
|
}, history);
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1165
|
+
// switch exaustivo (2026-07-13, Objective Runtime Controller): o
|
|
1166
|
+
// if/else antigo so cobria 'text'/'patch' explicitamente e
|
|
1167
|
+
// `!wasPlanMode`; um outcome 'aborted' EM modo plano nao caia em
|
|
1168
|
+
// nenhum ramo, deixando pendingPlanTask parado num valor antigo —
|
|
1169
|
+
// achado real do Codex review. `budget_exceeded` teria o mesmo gap.
|
|
1170
|
+
switch (outcome.kind) {
|
|
1171
|
+
case 'text':
|
|
1172
|
+
case 'patch':
|
|
1173
|
+
if (wasPlanMode) {
|
|
1174
|
+
pendingPlanTask = line;
|
|
1175
|
+
printEvent('ok', 'Plano aguardando aprovacao', 'use /approve para entrar em auto mode e executar');
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
pendingPlanTask = null;
|
|
1179
|
+
}
|
|
1180
|
+
break;
|
|
1181
|
+
case 'applied':
|
|
1182
|
+
case 'aborted':
|
|
1183
|
+
case 'budget_exceeded':
|
|
1184
|
+
// Nenhum destes produz um plano novo. So limpa pendingPlanTask
|
|
1185
|
+
// quando NAO estavamos em modo plano (a tarefa rodou de verdade).
|
|
1186
|
+
// Em modo plano, um turno que nao produziu plano (abortado,
|
|
1187
|
+
// orcamento excedido) NAO pode apagar um plano anterior ainda
|
|
1188
|
+
// valido esperando /approve — senao um Ctrl+C numa pergunta nao
|
|
1189
|
+
// relacionada destruiria um plano aprovavel (achado da 2a rodada
|
|
1190
|
+
// de revisao adversarial: a correcao ingenua deste mesmo bug
|
|
1191
|
+
// introduzia esta regressao especificamente pro caso 'aborted').
|
|
1192
|
+
if (!wasPlanMode)
|
|
1193
|
+
pendingPlanTask = null;
|
|
1194
|
+
break;
|
|
1195
|
+
default: {
|
|
1196
|
+
const _exhaustive = outcome.kind;
|
|
1197
|
+
if (!wasPlanMode)
|
|
1198
|
+
pendingPlanTask = null;
|
|
1199
|
+
}
|
|
995
1200
|
}
|
|
996
1201
|
}
|
|
997
1202
|
catch (error) {
|