@neetru/cli 2.20.2 → 2.21.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 +31 -0
- package/dist/commands/code.js +324 -24
- package/dist/commands/code.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/code-agent-files.d.ts +13 -4
- package/dist/lib/code-agent-files.js +74 -8
- package/dist/lib/code-agent-files.js.map +1 -1
- package/dist/lib/code-agent-gateway.d.ts +2 -2
- package/dist/lib/code-agent-gateway.js +9 -4
- package/dist/lib/code-agent-gateway.js.map +1 -1
- package/dist/lib/code-agent-git-tools.d.ts +12 -16
- package/dist/lib/code-agent-git-tools.js +35 -26
- package/dist/lib/code-agent-git-tools.js.map +1 -1
- package/dist/lib/code-agent-protocol.d.ts +5 -0
- package/dist/lib/code-agent-protocol.js.map +1 -1
- package/dist/lib/code-agent-session.js +16 -3
- package/dist/lib/code-agent-session.js.map +1 -1
- package/dist/lib/code-agent-skills.d.ts +38 -0
- package/dist/lib/code-agent-skills.js +347 -0
- package/dist/lib/code-agent-skills.js.map +1 -0
- package/dist/lib/code-agent-tools.d.ts +7 -1
- package/dist/lib/code-agent-tools.js +57 -11
- package/dist/lib/code-agent-tools.js.map +1 -1
- package/dist/lib/code-agent-tui.d.ts +3 -0
- package/dist/lib/code-agent-tui.js +18 -4
- package/dist/lib/code-agent-tui.js.map +1 -1
- package/dist/lib/code-agent-types.d.ts +5 -0
- package/dist/lib/code-agent-ui.d.ts +9 -0
- package/dist/lib/code-agent-ui.js +39 -4
- package/dist/lib/code-agent-ui.js.map +1 -1
- package/dist/lib/code-agent-web-tools.d.ts +4 -0
- package/dist/lib/code-agent-web-tools.js +257 -0
- package/dist/lib/code-agent-web-tools.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
Convenção: [Keep a Changelog](https://keepachangelog.com/) + [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [2.21.0] — 2026-07-13 (`neetru code` — skills, modo objetivo/plano/auto, ferramentas web/GitHub + hardening pós-review)
|
|
6
|
+
|
|
7
|
+
> 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.
|
|
8
|
+
|
|
9
|
+
### Adicionado
|
|
10
|
+
|
|
11
|
+
- **Skills locais do `neetru code`** — skills vivem em `.neetru/code/skills/<nome>/SKILL.md` (frontmatter `name`/`description`). Nova ferramenta `NEETRU_TOOL {"tool":"create_skill","name":"...","description":"..."}` (pede confirmação, respeita `--yes`). Comando `/skills` (lista carregadas), `/skills init` (materializa as skills builtin em disco), `/skills show <nome>`. O system prompt carrega as skills disponíveis no início da sessão (`src/lib/code-agent-skills.ts`).
|
|
12
|
+
- **3 skills builtin** (sempre carregadas, mesmo sem `.neetru/code/skills/` local) — `skill-creator`, `design-excellence` (cores/contraste/tipografia/layout/estados de UI) e `structured-programming` (funções pequenas e coesas, sem abstração prematura, erro tratado só na fronteira). `/skills init` materializa as 3 de uma vez (`installAllBuiltinSkills`).
|
|
13
|
+
- **Validação de patch ANTES da confirmação** — `checkPatchApplies` (`code-agent-files.ts`) roda `git apply --check` assim que o modelo propõe um patch, antes de mostrar "Aplicar este patch? [y/N]". Se inválido, o erro volta pro modelo pedindo um diff corrigido.
|
|
14
|
+
- **Modo plano / modo objetivo / auto mode / `/approve`** — 4 modos reais agora (`somente plano`, `manual`, `aceita mudanças`, `auto`), com Shift+Tab ciclando entre eles. Modo plano bloqueia toda ferramenta que escreve/executa/publica (`PLAN_MODE_BLOCKED_TOOLS`); modo objetivo (`/goal`, `--goal`/`--objective`) continua trabalhando entre turnos até `NEETRU_DONE`/`NEETRU_BLOCKED`/limite, em vez de parar em toda resposta parcial; `/approve` aprova um plano e entra em auto mode executando a tarefa planejada; `/auto` entra direto em auto mode. Auto mode (`yes && objectiveMode`) é o único em que `git_push`/`gh_pr_create`/`shell_exec`/`create_skill` rodam sem confirmação — nos demais modos essas 4 ferramentas continuam confirmando sempre.
|
|
15
|
+
- **Ferramentas de pesquisa externa** — `web_search`/`web_fetch` (DuckDuckGo HTML + fetch genérico, com bloqueio de host privado/SSRF) e `github_search`/`github_repo` (API pública do GitHub) em `code-agent-web-tools.ts`.
|
|
16
|
+
|
|
17
|
+
### Corrigido
|
|
18
|
+
|
|
19
|
+
- **`thinking -> thinking` inválido no loop de auto-correção de patch** — auto-transição faltava em `VALID_TRANSITIONS`.
|
|
20
|
+
- **P1 (Codex-rescue, confirmado por leitura direta) — preview de confirmação não sanitizava texto do modelo** — `shell_exec`/`gh_pr_create`/`git_commit` interpolavam `command`/`title`/`body`/`message` crus no prompt `[y/N]`; um valor "repetido" de conteúdo de arquivo lido (prompt injection) podia usar ESC/CR pra reescrever visualmente o que aparece na tela, tornando a confirmação tecnicamente real mas enganosa. Nova `sanitizeForPreview()` (`code-agent-git-tools.ts`) remove C0/C1 antes de montar a preview — só a preview muda, o valor original continua sendo o que roda após aprovado.
|
|
21
|
+
- **P2 (Codex-rescue, confirmado por leitura direta) — `/agents stop` não cancelava a chamada de rede em voo** — o `AbortSignal` do subagente só era checado nas bordas do turno; `callGateway`/`streamGatewayChat` nunca recebiam esse signal, então parar um subagente cortava só o PRÓXIMO turno, não a requisição HTTP já em andamento. `streamGatewayChat` agora combina (`AbortSignal.any`) o timeout interno com o signal do chamador; `callGateway` encaminha pro `apiRequest`.
|
|
22
|
+
- **P2 (Codex-rescue, confirmado por leitura direta) — contagem regressiva do retry (GAP-M1-21) não aparecia na tela** — `callGatewayWithProgress` já para o spinner recebido antes de lançar (streaming + fallback sem stream falharam); o wrapper de retry atualizava esse MESMO spinner morto pra mostrar "tentando de novo em Ns". Agora cria um spinner novo pra cada rodada de retry.
|
|
23
|
+
- **Cabeçalho/`/status` não distinguiam "aceita mudanças" de "auto"** — `renderHeader`/`renderSessionStatus` ainda usavam a lógica binária antiga (`apply`×`yes`) e mostravam os dois modos como "auto-aplica", apesar de terem garantias de confirmação diferentes. Nova `formatPatchMode()` (`code-agent-ui.ts`) reflete os 4 estados reais.
|
|
24
|
+
- **Troca de modo truncava o histórico pra 12 mensagens em silêncio** — Shift+Tab e todo comando de modo (`/goal`, `/ultracode`, `/plan`, `/approve`, `/auto`, `/apply`, `/yes`, `/no`) reconstroem o system prompt preservando só as últimas 12 mensagens; agora avisa quando isso de fato corta contexto.
|
|
25
|
+
- Doc comments perdidos em `InkSessionContext` (`code-agent-tui.tsx`) restaurados.
|
|
26
|
+
|
|
27
|
+
## [2.20.3] — 2026-07-11 (`neetru code` — HOTFIX: erro real de patch mascarado por "Transicao de estado invalida")
|
|
28
|
+
|
|
29
|
+
> Reportado ao vivo: usuário aprovou um patch, `git apply` rejeitou (hunk header malformado no diff gerado pelo modelo), mas em vez do erro real apareceu `Falha Transicao de estado invalida: awaiting_confirm -> failed`.
|
|
30
|
+
|
|
31
|
+
### Corrigido
|
|
32
|
+
|
|
33
|
+
- **`awaiting_confirm`/`patch_ready` não permitiam transição para `failed`** (`code-agent-session.ts`) — quando `applyUnifiedPatch` lançava DEPOIS do usuário aprovar, o `catch` de `runAgentTask` tentava `goto('failed')` a partir de um estado que a máquina rejeitava, e essa exceção da própria transição substituía o erro real do git apply. Adicionado `'failed'` às duas transições, e o `goto('failed')` no catch agora está em try/catch próprio (defesa em profundidade — nunca mais mascara o erro original, mesmo que surja outro caso não previsto).
|
|
34
|
+
- **Prompt do modelo mais explícito sobre hunk headers válidos** (`code-agent-tools.ts`) — exemplo completo com `@@ -0,0 +1,3 @@` em vez de `...` — reduz a chance do modelo gerar diff malformado.
|
|
35
|
+
|
|
5
36
|
## [2.20.2] — 2026-07-11 (`neetru code` — HOTFIX: confirmação `[y/N]` não reagia a teclado)
|
|
6
37
|
|
|
7
38
|
> Reportado ao vivo: `shell_exec` (`git init`) mostrou o prompt de confirmação mas apertar `y` não fazia nada — sessão travada. Bug latente desde a reescrita do painel Ink (2.19.0), que afetava TODA confirmação aninhada (patch-apply sem `--yes`, `/pick`, `/resume`, `/review`, e agora git_commit/branch/push, gh_pr_create, shell_exec) — só ficou visível agora porque as novas tools de git/shell confirmam sempre.
|
package/dist/commands/code.js
CHANGED
|
@@ -8,16 +8,16 @@ 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
10
|
import { parseToolRequest, extractPatch } from '../lib/code-agent-protocol.js';
|
|
11
|
-
import { applyUnifiedPatch, getGitDiff, getProjectOverview, listFiles, readFileContext, renderColoredDiff, runCodeTool, truncateForGateway, } from '../lib/code-agent-files.js';
|
|
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
15
|
import { CliApiError } from '../lib/api-client.js';
|
|
16
16
|
import { buildSystemPrompt } from '../lib/code-agent-tools.js';
|
|
17
|
+
import { CODE_SKILLS_ROOT, installAllBuiltinSkills, loadCodeSkills, normalizeSkillName } from '../lib/code-agent-skills.js';
|
|
17
18
|
import { printBox, printEvent, printAssistantText, printPatchPreview, patchSummaryRows, renderHeader, renderInteractiveHelp, renderSessionStatus, renderPromptLabel, printFileSample, printFilePicker, printSessionList, startTurnSpinner, summarizeToolResult, } from '../lib/code-agent-ui.js';
|
|
18
19
|
import { runInkSession } from '../lib/code-agent-tui.js';
|
|
19
20
|
import { configureSubagentRunner, startSubagent, stopSubagent, stopAllSubagents, listSubagents, getSubagentResult, getApplicablePatch, onSubagentSettled, } from '../lib/code-agent-subagents.js';
|
|
20
|
-
const SYSTEM_PROMPT = buildSystemPrompt();
|
|
21
21
|
/** Spinner inerte pro modo silencioso (subagentes) — nunca escreve no terminal. */
|
|
22
22
|
const NOOP_TURN_SPINNER = { update: () => { }, stop: () => { } };
|
|
23
23
|
function asOptionalString(value) {
|
|
@@ -44,6 +44,7 @@ export function resolveSettings(opts) {
|
|
|
44
44
|
});
|
|
45
45
|
const provider = resolveCodeProvider(asOptionalString(opts.provider), config.get('gatewayProvider'), model.model);
|
|
46
46
|
const ultracode = opts.ultracode === true;
|
|
47
|
+
const objectiveMode = opts.goal === true || opts.objective === true || ultracode;
|
|
47
48
|
const effort = ultracode
|
|
48
49
|
? ULTRACODE_EFFORT
|
|
49
50
|
: resolveCodeEffort(asOptionalString(opts.effort), config.get('gatewayEffort'));
|
|
@@ -70,13 +71,16 @@ export function resolveSettings(opts) {
|
|
|
70
71
|
timeoutMs: 120_000,
|
|
71
72
|
effort,
|
|
72
73
|
ultracode,
|
|
74
|
+
objectiveMode,
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
async function runReviewCommand(settings, confirm, history) {
|
|
76
78
|
const previousApply = settings.apply;
|
|
77
79
|
const previousYes = settings.yes;
|
|
80
|
+
const previousObjectiveMode = settings.objectiveMode;
|
|
78
81
|
settings.apply = false;
|
|
79
82
|
settings.yes = false;
|
|
83
|
+
settings.objectiveMode = false;
|
|
80
84
|
try {
|
|
81
85
|
await runAgentTask([
|
|
82
86
|
'Revise as mudancas atuais do projeto.',
|
|
@@ -87,6 +91,7 @@ async function runReviewCommand(settings, confirm, history) {
|
|
|
87
91
|
finally {
|
|
88
92
|
settings.apply = previousApply;
|
|
89
93
|
settings.yes = previousYes;
|
|
94
|
+
settings.objectiveMode = previousObjectiveMode;
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
97
|
function buildInitialPrompt(task, settings) {
|
|
@@ -94,13 +99,17 @@ function buildInitialPrompt(task, settings) {
|
|
|
94
99
|
for (const file of settings.files) {
|
|
95
100
|
fileBlocks.push(readFileContext(settings.cwd, file));
|
|
96
101
|
}
|
|
102
|
+
const modeBlock = settings.apply
|
|
103
|
+
? ''
|
|
104
|
+
: 'Modo plano ativo: pesquise e leia o necessario, mas nao escreva arquivo, nao execute codigo e nao proponha NEETRU_PATCH. Entregue um plano para aprovacao.';
|
|
97
105
|
return truncateForGateway([
|
|
98
106
|
`Tarefa do usuario:\n${task}`,
|
|
99
107
|
`Cwd: ${settings.cwd}`,
|
|
108
|
+
modeBlock,
|
|
100
109
|
fileBlocks.length
|
|
101
110
|
? `Contexto inicial de arquivos:\n\n${fileBlocks.join('\n\n')}`
|
|
102
111
|
: 'Nenhum arquivo foi anexado inicialmente. Use NEETRU_TOOL para pedir contexto local.',
|
|
103
|
-
].join('\n\n'));
|
|
112
|
+
].filter(Boolean).join('\n\n'));
|
|
104
113
|
}
|
|
105
114
|
async function defaultConfirm(question) {
|
|
106
115
|
if (!process.stdin.isTTY)
|
|
@@ -128,11 +137,11 @@ async function defaultConfirm(question) {
|
|
|
128
137
|
* (nao progresso normal), entao para o spinner pra nao interlear com o
|
|
129
138
|
* `printEvent` e abre um novo pra continuar depois.
|
|
130
139
|
*/
|
|
131
|
-
async function callGatewayWithProgress(settings, messages, spinner, baseDetail, silent = false) {
|
|
140
|
+
async function callGatewayWithProgress(settings, messages, spinner, baseDetail, silent = false, signal) {
|
|
132
141
|
try {
|
|
133
142
|
const content = await streamGatewayChat(settings, messages, (_delta, total) => {
|
|
134
143
|
spinner.update(`${baseDetail} · ${total} char(s) recebido(s)`);
|
|
135
|
-
});
|
|
144
|
+
}, signal);
|
|
136
145
|
return { content, spinner };
|
|
137
146
|
}
|
|
138
147
|
catch (error) {
|
|
@@ -145,7 +154,7 @@ async function callGatewayWithProgress(settings, messages, spinner, baseDetail,
|
|
|
145
154
|
const fallbackSpinner = silent ? NOOP_TURN_SPINNER : startTurnSpinner('neetru code');
|
|
146
155
|
fallbackSpinner.update(`${baseDetail} · sem stream`);
|
|
147
156
|
try {
|
|
148
|
-
const content = await callGateway(settings, messages);
|
|
157
|
+
const content = await callGateway(settings, messages, signal);
|
|
149
158
|
return { content, spinner: fallbackSpinner };
|
|
150
159
|
}
|
|
151
160
|
catch (fallbackError) {
|
|
@@ -173,22 +182,104 @@ function delay(ms, signal) {
|
|
|
173
182
|
});
|
|
174
183
|
}
|
|
175
184
|
export async function callGatewayWithProgressRetrying(settings, messages, spinner, baseDetail, silent, signal) {
|
|
185
|
+
let activeSpinner = spinner;
|
|
176
186
|
for (let attempt = 0;; attempt++) {
|
|
177
187
|
try {
|
|
178
|
-
return await callGatewayWithProgress(settings, messages,
|
|
188
|
+
return await callGatewayWithProgress(settings, messages, activeSpinner, baseDetail, silent, signal);
|
|
179
189
|
}
|
|
180
190
|
catch (error) {
|
|
181
191
|
if (!isProviderPoolUnavailable(error) || attempt >= POOL_RETRY_DELAYS_MS.length || signal?.aborted) {
|
|
182
192
|
throw error;
|
|
183
193
|
}
|
|
184
194
|
const waitMs = POOL_RETRY_DELAYS_MS[attempt];
|
|
185
|
-
|
|
195
|
+
// `callGatewayWithProgress` ja parou o spinner recebido (streaming e o
|
|
196
|
+
// fallback sem stream falharam) antes de lancar — atualizar ELE aqui
|
|
197
|
+
// nao aparece na tela (achado do Codex review, 2026-07-11). Cria um
|
|
198
|
+
// spinner novo so pra mostrar a contagem regressiva do retry.
|
|
199
|
+
activeSpinner = silent ? NOOP_TURN_SPINNER : startTurnSpinner('neetru code');
|
|
200
|
+
activeSpinner.update(`${baseDetail} · pool de IA sem capacidade, tentando de novo em ${Math.round(waitMs / 1000)}s`);
|
|
186
201
|
await delay(waitMs, signal);
|
|
187
|
-
if (signal?.aborted)
|
|
202
|
+
if (signal?.aborted) {
|
|
203
|
+
activeSpinner.stop();
|
|
188
204
|
throw error;
|
|
205
|
+
}
|
|
189
206
|
}
|
|
190
207
|
}
|
|
191
208
|
}
|
|
209
|
+
function classifyObjectiveText(content) {
|
|
210
|
+
const trimmed = content.trimStart();
|
|
211
|
+
if (/^NEETRU_DONE\b/i.test(trimmed))
|
|
212
|
+
return 'done';
|
|
213
|
+
if (/^NEETRU_BLOCKED\b/i.test(trimmed))
|
|
214
|
+
return 'blocked';
|
|
215
|
+
return 'partial';
|
|
216
|
+
}
|
|
217
|
+
function stripObjectiveMarker(content) {
|
|
218
|
+
return content
|
|
219
|
+
.replace(/^\s*NEETRU_DONE\b[:\s-]*/i, '')
|
|
220
|
+
.replace(/^\s*NEETRU_BLOCKED\b[:\s-]*/i, '')
|
|
221
|
+
.trim();
|
|
222
|
+
}
|
|
223
|
+
function buildObjectiveContinuationPrompt(turn, maxTurns) {
|
|
224
|
+
return [
|
|
225
|
+
`Modo objetivo: sua resposta anterior parece um avanco parcial, nao uma conclusao marcada.`,
|
|
226
|
+
`Continue trabalhando no mesmo objetivo sem pedir nova instrucao ao usuario.`,
|
|
227
|
+
`Se houver proxima acao util, use NEETRU_TOOL ou responda com NEETRU_PATCH.`,
|
|
228
|
+
`Se estiver realmente concluido, responda com NEETRU_DONE seguido do resumo final.`,
|
|
229
|
+
`Se estiver bloqueado por informacao/credencial/decisao externa, responda com NEETRU_BLOCKED seguido do motivo.`,
|
|
230
|
+
`Turno atual: ${turn}/${maxTurns}.`,
|
|
231
|
+
].join('\n');
|
|
232
|
+
}
|
|
233
|
+
function applyInteractiveCodeMode(settings, mode) {
|
|
234
|
+
settings.apply = true;
|
|
235
|
+
settings.yes = mode === 'accept' || mode === 'auto';
|
|
236
|
+
settings.objectiveMode = mode === 'auto';
|
|
237
|
+
}
|
|
238
|
+
function currentInteractiveCodeMode(settings) {
|
|
239
|
+
if (settings.apply && settings.yes && settings.objectiveMode)
|
|
240
|
+
return 'auto';
|
|
241
|
+
if (settings.apply && settings.yes)
|
|
242
|
+
return 'accept';
|
|
243
|
+
return 'manual';
|
|
244
|
+
}
|
|
245
|
+
function cycleInteractiveCodeMode(settings) {
|
|
246
|
+
if (!settings.apply) {
|
|
247
|
+
applyInteractiveCodeMode(settings, 'manual');
|
|
248
|
+
return 'manual';
|
|
249
|
+
}
|
|
250
|
+
const current = currentInteractiveCodeMode(settings);
|
|
251
|
+
const next = current === 'manual' ? 'accept' : current === 'accept' ? 'auto' : 'manual';
|
|
252
|
+
applyInteractiveCodeMode(settings, next);
|
|
253
|
+
return next;
|
|
254
|
+
}
|
|
255
|
+
function formatInteractiveCodeMode(settings) {
|
|
256
|
+
if (!settings.apply)
|
|
257
|
+
return 'plano';
|
|
258
|
+
if (settings.yes && settings.objectiveMode)
|
|
259
|
+
return 'auto';
|
|
260
|
+
if (settings.yes)
|
|
261
|
+
return 'aceita mudancas';
|
|
262
|
+
if (settings.objectiveMode)
|
|
263
|
+
return 'manual + objetivo';
|
|
264
|
+
return 'manual';
|
|
265
|
+
}
|
|
266
|
+
function formatCodeStatusLine(settings) {
|
|
267
|
+
return `/help comandos · Shift+Tab modo: ${formatInteractiveCodeMode(settings)} · /exit sai · Ctrl+C cancela`;
|
|
268
|
+
}
|
|
269
|
+
function refreshStatusLine(ctx, settings) {
|
|
270
|
+
ctx.setStatusLine?.(formatCodeStatusLine(settings));
|
|
271
|
+
}
|
|
272
|
+
function buildObjectiveTurnPrompt(task, settings) {
|
|
273
|
+
const blocks = [task];
|
|
274
|
+
if (!settings.apply) {
|
|
275
|
+
blocks.push('Modo plano ativo: use apenas leitura/pesquisa, nao escreva arquivo, nao execute codigo, nao publique nada e nao proponha NEETRU_PATCH. Entregue um plano para aprovacao.');
|
|
276
|
+
}
|
|
277
|
+
if (settings.objectiveMode) {
|
|
278
|
+
blocks.push('Modo objetivo esta ligado nesta tarefa. Continue usando tools/patches enquanto houver proxima acao util. ' +
|
|
279
|
+
'Finalize texto apenas com NEETRU_DONE quando concluir ou NEETRU_BLOCKED quando houver bloqueio real.');
|
|
280
|
+
}
|
|
281
|
+
return blocks.length === 1 ? task : truncateForGateway(blocks.join('\n\n'));
|
|
282
|
+
}
|
|
192
283
|
/**
|
|
193
284
|
* `history` e mutado e reaproveitado entre chamadas: e o que da ao modo
|
|
194
285
|
* interativo memoria de conversa entre turnos do REPL (como Claude Code/Codex),
|
|
@@ -219,11 +310,19 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
219
310
|
};
|
|
220
311
|
const messages = history;
|
|
221
312
|
const isFirstTurn = messages.length === 0;
|
|
222
|
-
if (isFirstTurn)
|
|
223
|
-
messages.push({
|
|
313
|
+
if (isFirstTurn) {
|
|
314
|
+
messages.push({
|
|
315
|
+
role: 'system',
|
|
316
|
+
content: buildSystemPrompt({
|
|
317
|
+
skills: loadCodeSkills(settings.cwd),
|
|
318
|
+
objectiveMode: settings.objectiveMode,
|
|
319
|
+
planMode: !settings.apply,
|
|
320
|
+
}),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
224
323
|
messages.push({
|
|
225
324
|
role: 'user',
|
|
226
|
-
content: isFirstTurn ? buildInitialPrompt(task, settings) : task,
|
|
325
|
+
content: isFirstTurn ? buildInitialPrompt(task, settings) : buildObjectiveTurnPrompt(task, settings),
|
|
227
326
|
});
|
|
228
327
|
// Um unico spinner vivo pra tarefa inteira (nao uma linha por evento) —
|
|
229
328
|
// achado de uso real: turno/tool-call/tool-result como linhas permanentes
|
|
@@ -260,10 +359,12 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
260
359
|
// "Limite de turnos atingido"). No ultimo turno permitido, forca o
|
|
261
360
|
// modelo a responder AGORA em vez de so descobrir isso ao falhar.
|
|
262
361
|
if (turn === settings.maxTurns) {
|
|
362
|
+
const finalTurnInstruction = settings.objectiveMode
|
|
363
|
+
? 'Este e o ultimo turno disponivel. Responda AGORA com NEETRU_DONE se concluiu o maximo possivel, ou NEETRU_BLOCKED se ha bloqueio real. Nao peca mais NEETRU_TOOL nem proponha patch.'
|
|
364
|
+
: '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.';
|
|
263
365
|
messages.push({
|
|
264
366
|
role: 'user',
|
|
265
|
-
content:
|
|
266
|
-
'texto usando o que voce ja sabe — nao peca mais NEETRU_TOOL nem proponha patch.',
|
|
367
|
+
content: finalTurnInstruction,
|
|
267
368
|
});
|
|
268
369
|
}
|
|
269
370
|
const baseDetail = `turno ${turn}/${settings.maxTurns}`;
|
|
@@ -287,6 +388,26 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
287
388
|
}
|
|
288
389
|
const patch = extractPatch(content);
|
|
289
390
|
if (patch) {
|
|
391
|
+
// Codex review (2026-07-11): antes, um patch invalido (hunk header
|
|
392
|
+
// sem numeros de linha, etc.) so era detectado DEPOIS do usuario
|
|
393
|
+
// confirmar "Aplicar este patch? [y/N]" — gastava a interacao numa
|
|
394
|
+
// resposta fadada a falhar. Valida ANTES de pedir confirmacao; se
|
|
395
|
+
// invalido, devolve o erro ao modelo pra corrigir o diff (consome um
|
|
396
|
+
// turno, como uma tool call) em vez de expor o usuario a isso. So
|
|
397
|
+
// roda quando `settings.apply` (modo plano nunca aplica de verdade).
|
|
398
|
+
if (settings.apply) {
|
|
399
|
+
const checkError = checkPatchApplies(settings.cwd, patch);
|
|
400
|
+
if (checkError) {
|
|
401
|
+
spinner.update(`${baseDetail} · patch invalido, pedindo correcao`);
|
|
402
|
+
emit('warn', 'Patch nao aplicavel, pedindo correcao ao modelo', checkError.slice(0, 300));
|
|
403
|
+
messages.push({
|
|
404
|
+
role: 'user',
|
|
405
|
+
content: truncateForGateway(`O patch proposto NAO pode ser aplicado (git apply --check falhou):\n\n${checkError}\n\n` +
|
|
406
|
+
'Corrija o diff (confira os hunk headers "@@ -a,b +c,d @@") e responda de novo com NEETRU_PATCH.'),
|
|
407
|
+
});
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
290
411
|
spinner.stop();
|
|
291
412
|
goto('patch_ready');
|
|
292
413
|
if (!silent) {
|
|
@@ -315,6 +436,33 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
315
436
|
goto('applied');
|
|
316
437
|
return { kind: 'applied', patch };
|
|
317
438
|
}
|
|
439
|
+
if (settings.objectiveMode) {
|
|
440
|
+
const objectiveState = classifyObjectiveText(content);
|
|
441
|
+
const cleanContent = objectiveState === 'partial' ? content : stripObjectiveMarker(content);
|
|
442
|
+
if (objectiveState === 'partial' && turn < settings.maxTurns) {
|
|
443
|
+
spinner.stop();
|
|
444
|
+
if (!silent) {
|
|
445
|
+
emit('ok', 'Avanco parcial', 'modo objetivo continuara trabalhando');
|
|
446
|
+
}
|
|
447
|
+
messages.push({
|
|
448
|
+
role: 'user',
|
|
449
|
+
content: truncateForGateway(buildObjectiveContinuationPrompt(turn + 1, settings.maxTurns)),
|
|
450
|
+
});
|
|
451
|
+
goto('idle');
|
|
452
|
+
spinner = makeSpinner();
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
spinner.stop();
|
|
456
|
+
if (!silent) {
|
|
457
|
+
console.log();
|
|
458
|
+
if (objectiveState === 'partial') {
|
|
459
|
+
emit('warn', 'Limite do modo objetivo atingido', `${settings.maxTurns} turno(s)`);
|
|
460
|
+
}
|
|
461
|
+
printAssistantText(cleanContent || content);
|
|
462
|
+
}
|
|
463
|
+
goto('idle');
|
|
464
|
+
return { kind: 'text', text: cleanContent || content };
|
|
465
|
+
}
|
|
318
466
|
spinner.stop();
|
|
319
467
|
if (!silent) {
|
|
320
468
|
console.log();
|
|
@@ -329,8 +477,17 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
329
477
|
}
|
|
330
478
|
catch (error) {
|
|
331
479
|
spinner.stop();
|
|
480
|
+
// Defesa em profundidade (2026-07-11): mesmo com a tabela de transicoes
|
|
481
|
+
// cobrindo os casos conhecidos, uma falha na PROPRIA transicao de estado
|
|
482
|
+
// nunca pode mascarar o erro real do turno — por isso o goto('failed')
|
|
483
|
+
// vai num try/catch proprio, descartando qualquer erro que ELE gere.
|
|
332
484
|
if (machine.state !== 'failed' && machine.state !== 'aborted' && machine.state !== 'applied') {
|
|
333
|
-
|
|
485
|
+
try {
|
|
486
|
+
goto('failed');
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
// Intencional: erro da transicao nunca substitui `error` abaixo.
|
|
490
|
+
}
|
|
334
491
|
}
|
|
335
492
|
throw error;
|
|
336
493
|
}
|
|
@@ -472,6 +629,53 @@ async function runAgentsCommand(line, settings, confirm) {
|
|
|
472
629
|
printEvent('warn', 'Subcomando desconhecido', 'use: start | list | stop | result | apply');
|
|
473
630
|
}
|
|
474
631
|
}
|
|
632
|
+
function runSkillsCommand(line, settings) {
|
|
633
|
+
const args = line.trim().slice('/skills'.length).trim();
|
|
634
|
+
const [subRaw, ...rest] = args.split(/\s+/).filter(Boolean);
|
|
635
|
+
const sub = (subRaw ?? 'list').toLowerCase();
|
|
636
|
+
if (sub === 'init') {
|
|
637
|
+
const results = installAllBuiltinSkills(settings.cwd);
|
|
638
|
+
printBox('skills init', results.map((r) => `${r.name}${r.created ? '' : ' (ja existia)'} — ${r.path}`));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (sub === 'show') {
|
|
642
|
+
const rawName = rest.join(' ');
|
|
643
|
+
if (!rawName) {
|
|
644
|
+
printEvent('warn', 'Uso', '/skills show <nome>');
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
let name;
|
|
648
|
+
try {
|
|
649
|
+
name = normalizeSkillName(rawName);
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
printEvent('warn', 'Nome de skill invalido', error instanceof Error ? error.message : String(error));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const skill = loadCodeSkills(settings.cwd).find((item) => item.name === name);
|
|
656
|
+
if (!skill) {
|
|
657
|
+
printEvent('warn', 'Skill nao encontrada', name);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
printBox(`skill ${skill.name}`, [
|
|
661
|
+
`descricao: ${skill.description}`,
|
|
662
|
+
`fonte: ${skill.relativePath}`,
|
|
663
|
+
'',
|
|
664
|
+
...skill.body.split(/\r?\n/).slice(0, 80),
|
|
665
|
+
...(skill.body.split(/\r?\n/).length > 80 ? ['[TRUNCADO: use read_file para ver o SKILL.md completo]'] : []),
|
|
666
|
+
]);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (sub !== 'list') {
|
|
670
|
+
printEvent('warn', 'Subcomando desconhecido', 'use: /skills | /skills init | /skills show <nome>');
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const skills = loadCodeSkills(settings.cwd);
|
|
674
|
+
printBox('skills', [
|
|
675
|
+
`raiz local: ${CODE_SKILLS_ROOT}`,
|
|
676
|
+
...skills.map((skill) => `${skill.name}${skill.builtin ? ' (builtin)' : ''} — ${skill.description} — ${skill.relativePath}`),
|
|
677
|
+
]);
|
|
678
|
+
}
|
|
475
679
|
async function runInteractive(settings) {
|
|
476
680
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
477
681
|
throw new Error('Modo interativo exige terminal TTY. Use: neetru code "tarefa".');
|
|
@@ -481,6 +685,33 @@ async function runInteractive(settings) {
|
|
|
481
685
|
const { resolveSessionEmail } = await import('../lib/auth.js');
|
|
482
686
|
const sessionEmail = await resolveSessionEmail().catch(() => null);
|
|
483
687
|
let history = [];
|
|
688
|
+
let pendingPlanTask = null;
|
|
689
|
+
// Limite de mensagens preservadas ao reconstruir o system prompt (troca de
|
|
690
|
+
// modo). Nao e so `/approve`: TODO toggle de modo (Shift+Tab, /goal,
|
|
691
|
+
// /ultracode, /plan, /auto, /apply, /yes, /no) chama isto — sem avisar
|
|
692
|
+
// quando corta, uma sessao que troca de modo varias vezes perde contexto
|
|
693
|
+
// silenciosamente a cada troca (achado revisando o codigo, 2026-07-13).
|
|
694
|
+
const RESET_SYSTEM_PROMPT_KEEP_LAST = 12;
|
|
695
|
+
const resetSystemPromptForCurrentMode = () => {
|
|
696
|
+
if (history.length === 0)
|
|
697
|
+
return;
|
|
698
|
+
const nonSystem = history.filter((message) => message.role !== 'system');
|
|
699
|
+
const carried = nonSystem.slice(-RESET_SYSTEM_PROMPT_KEEP_LAST);
|
|
700
|
+
if (nonSystem.length > carried.length) {
|
|
701
|
+
printEvent('warn', 'Contexto reduzido ao trocar de modo', `mantidas as ultimas ${carried.length} mensagem(ns) de ${nonSystem.length}`);
|
|
702
|
+
}
|
|
703
|
+
history = [
|
|
704
|
+
{
|
|
705
|
+
role: 'system',
|
|
706
|
+
content: buildSystemPrompt({
|
|
707
|
+
skills: loadCodeSkills(settings.cwd),
|
|
708
|
+
objectiveMode: settings.objectiveMode,
|
|
709
|
+
planMode: !settings.apply,
|
|
710
|
+
}),
|
|
711
|
+
},
|
|
712
|
+
...carried,
|
|
713
|
+
];
|
|
714
|
+
};
|
|
484
715
|
// Notificação de conclusão de subagente: UMA linha permanente quando termina
|
|
485
716
|
// (sucesso/falha/parado). Enquanto rodam, subagentes são silenciosos — o
|
|
486
717
|
// LineBuffer do Ink commita esta única linha acima do prompt/spinner sem
|
|
@@ -499,7 +730,13 @@ async function runInteractive(settings) {
|
|
|
499
730
|
try {
|
|
500
731
|
await runInkSession({
|
|
501
732
|
initialPromptLabel: renderPromptLabel(settings),
|
|
502
|
-
statusLine:
|
|
733
|
+
statusLine: formatCodeStatusLine(settings),
|
|
734
|
+
onShiftTab: () => {
|
|
735
|
+
cycleInteractiveCodeMode(settings);
|
|
736
|
+
resetSystemPromptForCurrentMode();
|
|
737
|
+
printEvent('ok', 'Modo atualizado', formatInteractiveCodeMode(settings));
|
|
738
|
+
return formatCodeStatusLine(settings);
|
|
739
|
+
},
|
|
503
740
|
onStart: () => {
|
|
504
741
|
log.banner(sessionEmail);
|
|
505
742
|
renderHeader(settings, 'interactive');
|
|
@@ -517,10 +754,16 @@ async function runInteractive(settings) {
|
|
|
517
754
|
});
|
|
518
755
|
return 'continue';
|
|
519
756
|
}
|
|
757
|
+
if (lower === '/skills' || lower.startsWith('/skills ')) {
|
|
758
|
+
runSkillsCommand(line, settings);
|
|
759
|
+
return 'continue';
|
|
760
|
+
}
|
|
520
761
|
if (lower === '/clear') {
|
|
521
762
|
history = [];
|
|
763
|
+
pendingPlanTask = null;
|
|
522
764
|
renderHeader(settings, 'interactive');
|
|
523
765
|
printEvent('ok', 'Conversa limpa');
|
|
766
|
+
refreshStatusLine(ctx, settings);
|
|
524
767
|
return 'continue';
|
|
525
768
|
}
|
|
526
769
|
if (lower === '/files') {
|
|
@@ -646,42 +889,91 @@ async function runInteractive(settings) {
|
|
|
646
889
|
printEvent('ok', 'Effort atualizado', next);
|
|
647
890
|
return 'continue';
|
|
648
891
|
}
|
|
892
|
+
if (lower === '/goal' || lower === '/objective' || lower === '/objetivo') {
|
|
893
|
+
settings.objectiveMode = !settings.objectiveMode;
|
|
894
|
+
resetSystemPromptForCurrentMode();
|
|
895
|
+
printEvent(settings.objectiveMode ? 'ok' : 'warn', settings.objectiveMode ? 'Modo objetivo ligado' : 'Modo objetivo desligado', settings.objectiveMode ? 'continua ate NEETRU_DONE/NEETRU_BLOCKED ou limite' : 'uma resposta final encerra o turno');
|
|
896
|
+
refreshStatusLine(ctx, settings);
|
|
897
|
+
return 'continue';
|
|
898
|
+
}
|
|
649
899
|
if (lower === '/ultracode') {
|
|
650
900
|
settings.ultracode = !settings.ultracode;
|
|
651
901
|
if (settings.ultracode) {
|
|
652
902
|
settings.maxTurns = ULTRACODE_MAX_TURNS;
|
|
653
903
|
settings.maxTokens = ULTRACODE_MAX_TOKENS;
|
|
654
904
|
settings.effort = ULTRACODE_EFFORT;
|
|
905
|
+
settings.objectiveMode = true;
|
|
655
906
|
printEvent('ok', 'Ultracode ligado', `${ULTRACODE_MAX_TURNS} turnos · ${ULTRACODE_MAX_TOKENS} tokens · effort ${ULTRACODE_EFFORT}`);
|
|
656
907
|
}
|
|
657
908
|
else {
|
|
658
909
|
settings.maxTurns = DEFAULT_CODE_MAX_TURNS;
|
|
659
910
|
settings.maxTokens = DEFAULT_CODE_MAX_TOKENS;
|
|
660
911
|
settings.effort = resolveCodeEffort(undefined, config.get('gatewayEffort'));
|
|
912
|
+
settings.objectiveMode = false;
|
|
661
913
|
printEvent('ok', 'Ultracode desligado', `voltou pro default (${DEFAULT_CODE_MAX_TURNS} turnos)`);
|
|
662
914
|
}
|
|
915
|
+
resetSystemPromptForCurrentMode();
|
|
916
|
+
refreshStatusLine(ctx, settings);
|
|
663
917
|
return 'continue';
|
|
664
918
|
}
|
|
665
919
|
if (lower === '/plan') {
|
|
666
920
|
settings.apply = false;
|
|
667
|
-
|
|
921
|
+
settings.yes = false;
|
|
922
|
+
settings.objectiveMode = false;
|
|
923
|
+
pendingPlanTask = null;
|
|
924
|
+
resetSystemPromptForCurrentMode();
|
|
925
|
+
printEvent('ok', 'Modo plano', 'somente leitura/pesquisa; escrita e execucao bloqueadas');
|
|
926
|
+
refreshStatusLine(ctx, settings);
|
|
927
|
+
return 'continue';
|
|
928
|
+
}
|
|
929
|
+
if (lower === '/approve' || lower === '/aprovar' || lower === '/approved') {
|
|
930
|
+
applyInteractiveCodeMode(settings, 'auto');
|
|
931
|
+
refreshStatusLine(ctx, settings);
|
|
932
|
+
printEvent('ok', 'Plano aprovado', 'entrando em auto mode');
|
|
933
|
+
if (!pendingPlanTask) {
|
|
934
|
+
return 'continue';
|
|
935
|
+
}
|
|
936
|
+
const approvedTask = pendingPlanTask;
|
|
937
|
+
pendingPlanTask = null;
|
|
938
|
+
resetSystemPromptForCurrentMode();
|
|
939
|
+
try {
|
|
940
|
+
await runAgentTask(`Plano aprovado pelo usuario. Execute agora em auto mode a tarefa planejada: ${approvedTask}`, settings, async (question) => {
|
|
941
|
+
const answer = await ctx.askLine(question);
|
|
942
|
+
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
943
|
+
}, history);
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
printEvent('error', 'Falha', error instanceof Error ? error.message : String(error));
|
|
947
|
+
}
|
|
948
|
+
autoSaveCodeSession(settings, history);
|
|
949
|
+
return 'continue';
|
|
950
|
+
}
|
|
951
|
+
if (lower === '/auto') {
|
|
952
|
+
applyInteractiveCodeMode(settings, 'auto');
|
|
953
|
+
resetSystemPromptForCurrentMode();
|
|
954
|
+
printEvent('ok', 'Auto mode ativo', 'sem confirmacao para patches, shell_exec, git_push e gh_pr_create');
|
|
955
|
+
refreshStatusLine(ctx, settings);
|
|
668
956
|
return 'continue';
|
|
669
957
|
}
|
|
670
958
|
if (lower === '/apply') {
|
|
671
|
-
settings
|
|
672
|
-
|
|
959
|
+
applyInteractiveCodeMode(settings, 'manual');
|
|
960
|
+
resetSystemPromptForCurrentMode();
|
|
673
961
|
printEvent('ok', 'Modo aplicar', 'pedira confirmacao antes do git apply');
|
|
962
|
+
refreshStatusLine(ctx, settings);
|
|
674
963
|
return 'continue';
|
|
675
964
|
}
|
|
676
965
|
if (lower === '/yes') {
|
|
677
|
-
settings
|
|
678
|
-
|
|
966
|
+
applyInteractiveCodeMode(settings, 'accept');
|
|
967
|
+
resetSystemPromptForCurrentMode();
|
|
679
968
|
printEvent('warn', 'Auto-aplicar ativo', 'patches validos serao aplicados sem pergunta');
|
|
969
|
+
refreshStatusLine(ctx, settings);
|
|
680
970
|
return 'continue';
|
|
681
971
|
}
|
|
682
972
|
if (lower === '/no') {
|
|
683
|
-
settings
|
|
973
|
+
applyInteractiveCodeMode(settings, 'manual');
|
|
974
|
+
resetSystemPromptForCurrentMode();
|
|
684
975
|
printEvent('ok', 'Confirmacao manual ativa');
|
|
976
|
+
refreshStatusLine(ctx, settings);
|
|
685
977
|
return 'continue';
|
|
686
978
|
}
|
|
687
979
|
if (lower === '/help' || lower === '/ajuda') {
|
|
@@ -689,10 +981,18 @@ async function runInteractive(settings) {
|
|
|
689
981
|
return 'continue';
|
|
690
982
|
}
|
|
691
983
|
try {
|
|
692
|
-
|
|
984
|
+
const wasPlanMode = !settings.apply;
|
|
985
|
+
const outcome = await runAgentTask(line, settings, async (question) => {
|
|
693
986
|
const answer = await ctx.askLine(question);
|
|
694
987
|
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
695
988
|
}, history);
|
|
989
|
+
if (wasPlanMode && (outcome.kind === 'text' || outcome.kind === 'patch')) {
|
|
990
|
+
pendingPlanTask = line;
|
|
991
|
+
printEvent('ok', 'Plano aguardando aprovacao', 'use /approve para entrar em auto mode e executar');
|
|
992
|
+
}
|
|
993
|
+
else if (!wasPlanMode) {
|
|
994
|
+
pendingPlanTask = null;
|
|
995
|
+
}
|
|
696
996
|
}
|
|
697
997
|
catch (error) {
|
|
698
998
|
printEvent('error', 'Falha', error instanceof Error ? error.message : String(error));
|
|
@@ -726,7 +1026,7 @@ function runDoctor(settings) {
|
|
|
726
1026
|
console.log(` Core API: ${chalk.dim(apiUrl)}`);
|
|
727
1027
|
console.log(` AI Gateway: ${chalk.dim(AI_GATEWAY_CHAT_PATH)}`);
|
|
728
1028
|
console.log(` git: ${git.status === 0 ? chalk.green('ok') : chalk.red('falhou')}`);
|
|
729
|
-
console.log(` modo patch: ${settings
|
|
1029
|
+
console.log(` modo patch: ${formatInteractiveCodeMode(settings)}`);
|
|
730
1030
|
console.log();
|
|
731
1031
|
if (!token || git.status !== 0)
|
|
732
1032
|
setSafeExitCode(1);
|