@neetru/cli 2.20.3 → 2.22.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 +41 -0
- package/dist/commands/code.js +381 -27
- 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 +14 -4
- package/dist/lib/code-agent-files.js +88 -9
- 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 +9 -0
- package/dist/lib/code-agent-protocol.js.map +1 -1
- package/dist/lib/code-agent-session.js +8 -1
- 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 +72 -15
- package/dist/lib/code-agent-tools.js.map +1 -1
- package/dist/lib/code-agent-tui.d.ts +16 -0
- package/dist/lib/code-agent-tui.js +75 -5
- 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 +16 -0
- package/dist/lib/code-agent-ui.js +76 -27
- 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/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/package.json +1 -1
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,128 @@ 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
|
+
// 2026-07-13 (achado em uso real, transcript colado pelo usuario): um modelo
|
|
210
|
+
// preso repetindo o MESMO diff quebrado ("corrupt patch") gastava o
|
|
211
|
+
// orcamento de turnos INTEIRO so em correcoes de patch — cada tentativa
|
|
212
|
+
// invalida consome 1 turno igual a qualquer outro, entao 3-4 tentativas
|
|
213
|
+
// seguidas ja fecham a sessao inteira sem sobrar turno pra nada. Depois de
|
|
214
|
+
// N tentativas seguidas, para de pedir "corrija o diff" (que nao esta
|
|
215
|
+
// funcionando) e forca o modelo a mudar de estrategia: explicar em texto em
|
|
216
|
+
// vez de insistir no mesmo formato.
|
|
217
|
+
const MAX_CONSECUTIVE_PATCH_REPAIRS = 3;
|
|
218
|
+
// Limite de turnos "duro demais" era a outra metade da mesma reclamacao:
|
|
219
|
+
// ao esgotar `maxTurns` a sessao INTEIRA falhava com um erro opaco, sem
|
|
220
|
+
// chance do usuario (que esta ali, na sessao interativa) simplesmente pedir
|
|
221
|
+
// mais turnos pra terminar algo que estava proximo. So oferece a extensao
|
|
222
|
+
// quando ha um humano de verdade pra perguntar — subagentes (`silent`) e
|
|
223
|
+
// modo nao-interativo sem TTY (`defaultConfirm` ja retorna false) continuam
|
|
224
|
+
// falhando direto, sem regressao de comportamento.
|
|
225
|
+
const TURN_LIMIT_EXTENSION = 6;
|
|
226
|
+
// Teto de extensoes por tarefa — sem isso, um `confirm` que sempre aprova
|
|
227
|
+
// (script/automacao, ou um humano otimista demais) mantem um modelo preso
|
|
228
|
+
// num loop que nunca conclui rodando PARA SEMPRE, um turno estendido atras
|
|
229
|
+
// do outro (achado rodando os testes: um mock de tool-loop infinito com
|
|
230
|
+
// confirm sempre-true travou o worker de teste ate estourar memoria).
|
|
231
|
+
// Depois do teto, falha direto — nao pergunta mais, mesmo que aprovassem.
|
|
232
|
+
const MAX_TURN_LIMIT_EXTENSIONS = 2;
|
|
233
|
+
function classifyObjectiveText(content) {
|
|
234
|
+
const trimmed = content.trimStart();
|
|
235
|
+
if (/^NEETRU_DONE\b/i.test(trimmed))
|
|
236
|
+
return 'done';
|
|
237
|
+
if (/^NEETRU_BLOCKED\b/i.test(trimmed))
|
|
238
|
+
return 'blocked';
|
|
239
|
+
return 'partial';
|
|
240
|
+
}
|
|
241
|
+
function stripObjectiveMarker(content) {
|
|
242
|
+
return content
|
|
243
|
+
.replace(/^\s*NEETRU_DONE\b[:\s-]*/i, '')
|
|
244
|
+
.replace(/^\s*NEETRU_BLOCKED\b[:\s-]*/i, '')
|
|
245
|
+
.trim();
|
|
246
|
+
}
|
|
247
|
+
function buildObjectiveContinuationPrompt(turn, maxTurns) {
|
|
248
|
+
return [
|
|
249
|
+
`Modo objetivo: sua resposta anterior parece um avanco parcial, nao uma conclusao marcada.`,
|
|
250
|
+
`Continue trabalhando no mesmo objetivo sem pedir nova instrucao ao usuario.`,
|
|
251
|
+
`Se houver proxima acao util, use NEETRU_TOOL ou responda com NEETRU_PATCH.`,
|
|
252
|
+
`Se estiver realmente concluido, responda com NEETRU_DONE seguido do resumo final.`,
|
|
253
|
+
`Se estiver bloqueado por informacao/credencial/decisao externa, responda com NEETRU_BLOCKED seguido do motivo.`,
|
|
254
|
+
`Turno atual: ${turn}/${maxTurns}.`,
|
|
255
|
+
].join('\n');
|
|
256
|
+
}
|
|
257
|
+
function applyInteractiveCodeMode(settings, mode) {
|
|
258
|
+
settings.apply = true;
|
|
259
|
+
settings.yes = mode === 'accept' || mode === 'auto';
|
|
260
|
+
settings.objectiveMode = mode === 'auto';
|
|
261
|
+
}
|
|
262
|
+
function currentInteractiveCodeMode(settings) {
|
|
263
|
+
if (settings.apply && settings.yes && settings.objectiveMode)
|
|
264
|
+
return 'auto';
|
|
265
|
+
if (settings.apply && settings.yes)
|
|
266
|
+
return 'accept';
|
|
267
|
+
return 'manual';
|
|
268
|
+
}
|
|
269
|
+
function cycleInteractiveCodeMode(settings) {
|
|
270
|
+
if (!settings.apply) {
|
|
271
|
+
applyInteractiveCodeMode(settings, 'manual');
|
|
272
|
+
return 'manual';
|
|
273
|
+
}
|
|
274
|
+
const current = currentInteractiveCodeMode(settings);
|
|
275
|
+
const next = current === 'manual' ? 'accept' : current === 'accept' ? 'auto' : 'manual';
|
|
276
|
+
applyInteractiveCodeMode(settings, next);
|
|
277
|
+
return next;
|
|
278
|
+
}
|
|
279
|
+
function formatInteractiveCodeMode(settings) {
|
|
280
|
+
if (!settings.apply)
|
|
281
|
+
return 'plano';
|
|
282
|
+
if (settings.yes && settings.objectiveMode)
|
|
283
|
+
return 'auto';
|
|
284
|
+
if (settings.yes)
|
|
285
|
+
return 'aceita mudancas';
|
|
286
|
+
if (settings.objectiveMode)
|
|
287
|
+
return 'manual + objetivo';
|
|
288
|
+
return 'manual';
|
|
289
|
+
}
|
|
290
|
+
function formatCodeStatusLine(settings) {
|
|
291
|
+
return `/help comandos · Shift+Tab modo: ${formatInteractiveCodeMode(settings)} · /exit sai · Ctrl+C cancela`;
|
|
292
|
+
}
|
|
293
|
+
function refreshStatusLine(ctx, settings) {
|
|
294
|
+
ctx.setStatusLine?.(formatCodeStatusLine(settings));
|
|
295
|
+
}
|
|
296
|
+
function buildObjectiveTurnPrompt(task, settings) {
|
|
297
|
+
const blocks = [task];
|
|
298
|
+
if (!settings.apply) {
|
|
299
|
+
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.');
|
|
300
|
+
}
|
|
301
|
+
if (settings.objectiveMode) {
|
|
302
|
+
blocks.push('Modo objetivo esta ligado nesta tarefa. Continue usando tools/patches enquanto houver proxima acao util. ' +
|
|
303
|
+
'Finalize texto apenas com NEETRU_DONE quando concluir ou NEETRU_BLOCKED quando houver bloqueio real.');
|
|
304
|
+
}
|
|
305
|
+
return blocks.length === 1 ? task : truncateForGateway(blocks.join('\n\n'));
|
|
306
|
+
}
|
|
192
307
|
/**
|
|
193
308
|
* `history` e mutado e reaproveitado entre chamadas: e o que da ao modo
|
|
194
309
|
* interativo memoria de conversa entre turnos do REPL (como Claude Code/Codex),
|
|
@@ -219,11 +334,19 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
219
334
|
};
|
|
220
335
|
const messages = history;
|
|
221
336
|
const isFirstTurn = messages.length === 0;
|
|
222
|
-
if (isFirstTurn)
|
|
223
|
-
messages.push({
|
|
337
|
+
if (isFirstTurn) {
|
|
338
|
+
messages.push({
|
|
339
|
+
role: 'system',
|
|
340
|
+
content: buildSystemPrompt({
|
|
341
|
+
skills: loadCodeSkills(settings.cwd),
|
|
342
|
+
objectiveMode: settings.objectiveMode,
|
|
343
|
+
planMode: !settings.apply,
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
224
347
|
messages.push({
|
|
225
348
|
role: 'user',
|
|
226
|
-
content: isFirstTurn ? buildInitialPrompt(task, settings) : task,
|
|
349
|
+
content: isFirstTurn ? buildInitialPrompt(task, settings) : buildObjectiveTurnPrompt(task, settings),
|
|
227
350
|
});
|
|
228
351
|
// Um unico spinner vivo pra tarefa inteira (nao uma linha por evento) —
|
|
229
352
|
// achado de uso real: turno/tool-call/tool-result como linhas permanentes
|
|
@@ -232,8 +355,31 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
232
355
|
// aparecendo como linha permanente via printEvent, pausando o spinner
|
|
233
356
|
// pra nao interlear as duas saidas.
|
|
234
357
|
let spinner = makeSpinner();
|
|
358
|
+
let patchRepairStreak = 0;
|
|
359
|
+
let turnLimitExtensionsUsed = 0;
|
|
235
360
|
try {
|
|
236
|
-
for (let turn = 1
|
|
361
|
+
for (let turn = 1;; turn++) {
|
|
362
|
+
if (turn > settings.maxTurns) {
|
|
363
|
+
// Rede de seguranca virou pergunta em vez de falha direta (achado em
|
|
364
|
+
// uso real): so faz sentido quando ha um humano de verdade podendo
|
|
365
|
+
// responder — `confirm` ja retorna false sem TTY/em subagentes
|
|
366
|
+
// (`silent`), preservando o comportamento antigo nesses casos. Teto
|
|
367
|
+
// de MAX_TURN_LIMIT_EXTENSIONS: mesmo aprovado, nao pergunta pra
|
|
368
|
+
// sempre — um modelo genuinamente preso (nunca conclui, nunca
|
|
369
|
+
// bloqueia) nao pode virar loop infinito so porque alguem/algo
|
|
370
|
+
// aprovou a extensao uma vez.
|
|
371
|
+
spinner.stop();
|
|
372
|
+
const canOfferExtension = !silent && turnLimitExtensionsUsed < MAX_TURN_LIMIT_EXTENSIONS;
|
|
373
|
+
const shouldExtend = canOfferExtension && (await confirm(`\nLimite de ${settings.maxTurns} turno(s) atingido sem concluir. Continuar mais ${TURN_LIMIT_EXTENSION} turno(s)? [y/N] `));
|
|
374
|
+
if (!shouldExtend) {
|
|
375
|
+
goto('failed');
|
|
376
|
+
throw new Error(`Limite de turnos atingido (${settings.maxTurns}) sem resposta final.`);
|
|
377
|
+
}
|
|
378
|
+
turnLimitExtensionsUsed++;
|
|
379
|
+
settings.maxTurns += TURN_LIMIT_EXTENSION;
|
|
380
|
+
emit('ok', 'Limite de turnos estendido', `+${TURN_LIMIT_EXTENSION} (agora ${settings.maxTurns} no total)`);
|
|
381
|
+
spinner = makeSpinner();
|
|
382
|
+
}
|
|
237
383
|
// Cancelamento cooperativo: verificado na fronteira de cada turno. A
|
|
238
384
|
// chamada de rede em voo tem timeout proprio (code-agent-gateway.ts), que
|
|
239
385
|
// este modulo nao controla sem editar aquele arquivo — o corte efetivo
|
|
@@ -260,10 +406,12 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
260
406
|
// "Limite de turnos atingido"). No ultimo turno permitido, forca o
|
|
261
407
|
// modelo a responder AGORA em vez de so descobrir isso ao falhar.
|
|
262
408
|
if (turn === settings.maxTurns) {
|
|
409
|
+
const finalTurnInstruction = settings.objectiveMode
|
|
410
|
+
? '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.'
|
|
411
|
+
: '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
412
|
messages.push({
|
|
264
413
|
role: 'user',
|
|
265
|
-
content:
|
|
266
|
-
'texto usando o que voce ja sabe — nao peca mais NEETRU_TOOL nem proponha patch.',
|
|
414
|
+
content: finalTurnInstruction,
|
|
267
415
|
});
|
|
268
416
|
}
|
|
269
417
|
const baseDetail = `turno ${turn}/${settings.maxTurns}`;
|
|
@@ -274,6 +422,7 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
274
422
|
messages.push({ role: 'assistant', content });
|
|
275
423
|
const tool = parseToolRequest(content);
|
|
276
424
|
if (tool) {
|
|
425
|
+
patchRepairStreak = 0;
|
|
277
426
|
goto('tool_requested');
|
|
278
427
|
spinner.update(`${baseDetail} · ${tool.tool}`);
|
|
279
428
|
goto('tool_running');
|
|
@@ -287,6 +436,43 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
287
436
|
}
|
|
288
437
|
const patch = extractPatch(content);
|
|
289
438
|
if (patch) {
|
|
439
|
+
// Codex review (2026-07-11): antes, um patch invalido (hunk header
|
|
440
|
+
// sem numeros de linha, etc.) so era detectado DEPOIS do usuario
|
|
441
|
+
// confirmar "Aplicar este patch? [y/N]" — gastava a interacao numa
|
|
442
|
+
// resposta fadada a falhar. Valida ANTES de pedir confirmacao; se
|
|
443
|
+
// invalido, devolve o erro ao modelo pra corrigir o diff (consome um
|
|
444
|
+
// turno, como uma tool call) em vez de expor o usuario a isso. So
|
|
445
|
+
// roda quando `settings.apply` (modo plano nunca aplica de verdade).
|
|
446
|
+
if (settings.apply) {
|
|
447
|
+
const checkError = checkPatchApplies(settings.cwd, patch);
|
|
448
|
+
if (checkError) {
|
|
449
|
+
patchRepairStreak++;
|
|
450
|
+
// 3+ tentativas seguidas de patch invalido: pedir "corrija o
|
|
451
|
+
// diff" de novo claramente nao esta funcionando (achado em uso
|
|
452
|
+
// real — o mesmo erro se repetia turno apos turno ate esgotar o
|
|
453
|
+
// limite). Muda de estrategia em vez de insistir no mesmo pedido.
|
|
454
|
+
if (patchRepairStreak >= MAX_CONSECUTIVE_PATCH_REPAIRS) {
|
|
455
|
+
spinner.update(`${baseDetail} · patch invalido varias vezes, mudando de estrategia`);
|
|
456
|
+
emit('warn', 'Patch invalido varias vezes seguidas — pedindo explicacao em texto', `${patchRepairStreak} tentativas · ultimo erro: ${checkError.slice(0, 200)}`);
|
|
457
|
+
messages.push({
|
|
458
|
+
role: 'user',
|
|
459
|
+
content: truncateForGateway(`Ja foram ${patchRepairStreak} tentativas seguidas de patch invalido (ultimo erro: ${checkError.slice(0, 200)}). ` +
|
|
460
|
+
'Pare de tentar gerar um diff neste turno. Descreva em texto claro, para o usuario, exatamente o ' +
|
|
461
|
+
'que precisa mudar (arquivo, trecho, motivo) — sem NEETRU_PATCH desta vez.'),
|
|
462
|
+
});
|
|
463
|
+
patchRepairStreak = 0;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
spinner.update(`${baseDetail} · patch invalido, pedindo correcao`);
|
|
467
|
+
emit('warn', 'Patch nao aplicavel, pedindo correcao ao modelo', checkError.slice(0, 300));
|
|
468
|
+
messages.push({
|
|
469
|
+
role: 'user',
|
|
470
|
+
content: truncateForGateway(`O patch proposto NAO pode ser aplicado (git apply --check falhou):\n\n${checkError}\n\n` +
|
|
471
|
+
'Corrija o diff (confira os hunk headers "@@ -a,b +c,d @@") e responda de novo com NEETRU_PATCH.'),
|
|
472
|
+
});
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
290
476
|
spinner.stop();
|
|
291
477
|
goto('patch_ready');
|
|
292
478
|
if (!silent) {
|
|
@@ -315,6 +501,34 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
315
501
|
goto('applied');
|
|
316
502
|
return { kind: 'applied', patch };
|
|
317
503
|
}
|
|
504
|
+
if (settings.objectiveMode) {
|
|
505
|
+
const objectiveState = classifyObjectiveText(content);
|
|
506
|
+
const cleanContent = objectiveState === 'partial' ? content : stripObjectiveMarker(content);
|
|
507
|
+
if (objectiveState === 'partial' && turn < settings.maxTurns) {
|
|
508
|
+
patchRepairStreak = 0;
|
|
509
|
+
spinner.stop();
|
|
510
|
+
if (!silent) {
|
|
511
|
+
emit('ok', 'Avanco parcial', 'modo objetivo continuara trabalhando');
|
|
512
|
+
}
|
|
513
|
+
messages.push({
|
|
514
|
+
role: 'user',
|
|
515
|
+
content: truncateForGateway(buildObjectiveContinuationPrompt(turn + 1, settings.maxTurns)),
|
|
516
|
+
});
|
|
517
|
+
goto('idle');
|
|
518
|
+
spinner = makeSpinner();
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
spinner.stop();
|
|
522
|
+
if (!silent) {
|
|
523
|
+
console.log();
|
|
524
|
+
if (objectiveState === 'partial') {
|
|
525
|
+
emit('warn', 'Limite do modo objetivo atingido', `${settings.maxTurns} turno(s)`);
|
|
526
|
+
}
|
|
527
|
+
printAssistantText(cleanContent || content);
|
|
528
|
+
}
|
|
529
|
+
goto('idle');
|
|
530
|
+
return { kind: 'text', text: cleanContent || content };
|
|
531
|
+
}
|
|
318
532
|
spinner.stop();
|
|
319
533
|
if (!silent) {
|
|
320
534
|
console.log();
|
|
@@ -323,9 +537,6 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
323
537
|
goto('idle');
|
|
324
538
|
return { kind: 'text', text: content };
|
|
325
539
|
}
|
|
326
|
-
spinner.stop();
|
|
327
|
-
goto('failed');
|
|
328
|
-
throw new Error(`Limite de turnos atingido (${settings.maxTurns}) sem resposta final.`);
|
|
329
540
|
}
|
|
330
541
|
catch (error) {
|
|
331
542
|
spinner.stop();
|
|
@@ -481,6 +692,53 @@ async function runAgentsCommand(line, settings, confirm) {
|
|
|
481
692
|
printEvent('warn', 'Subcomando desconhecido', 'use: start | list | stop | result | apply');
|
|
482
693
|
}
|
|
483
694
|
}
|
|
695
|
+
function runSkillsCommand(line, settings) {
|
|
696
|
+
const args = line.trim().slice('/skills'.length).trim();
|
|
697
|
+
const [subRaw, ...rest] = args.split(/\s+/).filter(Boolean);
|
|
698
|
+
const sub = (subRaw ?? 'list').toLowerCase();
|
|
699
|
+
if (sub === 'init') {
|
|
700
|
+
const results = installAllBuiltinSkills(settings.cwd);
|
|
701
|
+
printBox('skills init', results.map((r) => `${r.name}${r.created ? '' : ' (ja existia)'} — ${r.path}`));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (sub === 'show') {
|
|
705
|
+
const rawName = rest.join(' ');
|
|
706
|
+
if (!rawName) {
|
|
707
|
+
printEvent('warn', 'Uso', '/skills show <nome>');
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
let name;
|
|
711
|
+
try {
|
|
712
|
+
name = normalizeSkillName(rawName);
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
printEvent('warn', 'Nome de skill invalido', error instanceof Error ? error.message : String(error));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const skill = loadCodeSkills(settings.cwd).find((item) => item.name === name);
|
|
719
|
+
if (!skill) {
|
|
720
|
+
printEvent('warn', 'Skill nao encontrada', name);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
printBox(`skill ${skill.name}`, [
|
|
724
|
+
`descricao: ${skill.description}`,
|
|
725
|
+
`fonte: ${skill.relativePath}`,
|
|
726
|
+
'',
|
|
727
|
+
...skill.body.split(/\r?\n/).slice(0, 80),
|
|
728
|
+
...(skill.body.split(/\r?\n/).length > 80 ? ['[TRUNCADO: use read_file para ver o SKILL.md completo]'] : []),
|
|
729
|
+
]);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (sub !== 'list') {
|
|
733
|
+
printEvent('warn', 'Subcomando desconhecido', 'use: /skills | /skills init | /skills show <nome>');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const skills = loadCodeSkills(settings.cwd);
|
|
737
|
+
printBox('skills', [
|
|
738
|
+
`raiz local: ${CODE_SKILLS_ROOT}`,
|
|
739
|
+
...skills.map((skill) => `${skill.name}${skill.builtin ? ' (builtin)' : ''} — ${skill.description} — ${skill.relativePath}`),
|
|
740
|
+
]);
|
|
741
|
+
}
|
|
484
742
|
async function runInteractive(settings) {
|
|
485
743
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
486
744
|
throw new Error('Modo interativo exige terminal TTY. Use: neetru code "tarefa".');
|
|
@@ -490,6 +748,33 @@ async function runInteractive(settings) {
|
|
|
490
748
|
const { resolveSessionEmail } = await import('../lib/auth.js');
|
|
491
749
|
const sessionEmail = await resolveSessionEmail().catch(() => null);
|
|
492
750
|
let history = [];
|
|
751
|
+
let pendingPlanTask = null;
|
|
752
|
+
// Limite de mensagens preservadas ao reconstruir o system prompt (troca de
|
|
753
|
+
// modo). Nao e so `/approve`: TODO toggle de modo (Shift+Tab, /goal,
|
|
754
|
+
// /ultracode, /plan, /auto, /apply, /yes, /no) chama isto — sem avisar
|
|
755
|
+
// quando corta, uma sessao que troca de modo varias vezes perde contexto
|
|
756
|
+
// silenciosamente a cada troca (achado revisando o codigo, 2026-07-13).
|
|
757
|
+
const RESET_SYSTEM_PROMPT_KEEP_LAST = 12;
|
|
758
|
+
const resetSystemPromptForCurrentMode = () => {
|
|
759
|
+
if (history.length === 0)
|
|
760
|
+
return;
|
|
761
|
+
const nonSystem = history.filter((message) => message.role !== 'system');
|
|
762
|
+
const carried = nonSystem.slice(-RESET_SYSTEM_PROMPT_KEEP_LAST);
|
|
763
|
+
if (nonSystem.length > carried.length) {
|
|
764
|
+
printEvent('warn', 'Contexto reduzido ao trocar de modo', `mantidas as ultimas ${carried.length} mensagem(ns) de ${nonSystem.length}`);
|
|
765
|
+
}
|
|
766
|
+
history = [
|
|
767
|
+
{
|
|
768
|
+
role: 'system',
|
|
769
|
+
content: buildSystemPrompt({
|
|
770
|
+
skills: loadCodeSkills(settings.cwd),
|
|
771
|
+
objectiveMode: settings.objectiveMode,
|
|
772
|
+
planMode: !settings.apply,
|
|
773
|
+
}),
|
|
774
|
+
},
|
|
775
|
+
...carried,
|
|
776
|
+
];
|
|
777
|
+
};
|
|
493
778
|
// Notificação de conclusão de subagente: UMA linha permanente quando termina
|
|
494
779
|
// (sucesso/falha/parado). Enquanto rodam, subagentes são silenciosos — o
|
|
495
780
|
// LineBuffer do Ink commita esta única linha acima do prompt/spinner sem
|
|
@@ -508,7 +793,13 @@ async function runInteractive(settings) {
|
|
|
508
793
|
try {
|
|
509
794
|
await runInkSession({
|
|
510
795
|
initialPromptLabel: renderPromptLabel(settings),
|
|
511
|
-
statusLine:
|
|
796
|
+
statusLine: formatCodeStatusLine(settings),
|
|
797
|
+
onShiftTab: () => {
|
|
798
|
+
cycleInteractiveCodeMode(settings);
|
|
799
|
+
resetSystemPromptForCurrentMode();
|
|
800
|
+
printEvent('ok', 'Modo atualizado', formatInteractiveCodeMode(settings));
|
|
801
|
+
return formatCodeStatusLine(settings);
|
|
802
|
+
},
|
|
512
803
|
onStart: () => {
|
|
513
804
|
log.banner(sessionEmail);
|
|
514
805
|
renderHeader(settings, 'interactive');
|
|
@@ -526,10 +817,16 @@ async function runInteractive(settings) {
|
|
|
526
817
|
});
|
|
527
818
|
return 'continue';
|
|
528
819
|
}
|
|
820
|
+
if (lower === '/skills' || lower.startsWith('/skills ')) {
|
|
821
|
+
runSkillsCommand(line, settings);
|
|
822
|
+
return 'continue';
|
|
823
|
+
}
|
|
529
824
|
if (lower === '/clear') {
|
|
530
825
|
history = [];
|
|
826
|
+
pendingPlanTask = null;
|
|
531
827
|
renderHeader(settings, 'interactive');
|
|
532
828
|
printEvent('ok', 'Conversa limpa');
|
|
829
|
+
refreshStatusLine(ctx, settings);
|
|
533
830
|
return 'continue';
|
|
534
831
|
}
|
|
535
832
|
if (lower === '/files') {
|
|
@@ -655,42 +952,91 @@ async function runInteractive(settings) {
|
|
|
655
952
|
printEvent('ok', 'Effort atualizado', next);
|
|
656
953
|
return 'continue';
|
|
657
954
|
}
|
|
955
|
+
if (lower === '/goal' || lower === '/objective' || lower === '/objetivo') {
|
|
956
|
+
settings.objectiveMode = !settings.objectiveMode;
|
|
957
|
+
resetSystemPromptForCurrentMode();
|
|
958
|
+
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');
|
|
959
|
+
refreshStatusLine(ctx, settings);
|
|
960
|
+
return 'continue';
|
|
961
|
+
}
|
|
658
962
|
if (lower === '/ultracode') {
|
|
659
963
|
settings.ultracode = !settings.ultracode;
|
|
660
964
|
if (settings.ultracode) {
|
|
661
965
|
settings.maxTurns = ULTRACODE_MAX_TURNS;
|
|
662
966
|
settings.maxTokens = ULTRACODE_MAX_TOKENS;
|
|
663
967
|
settings.effort = ULTRACODE_EFFORT;
|
|
968
|
+
settings.objectiveMode = true;
|
|
664
969
|
printEvent('ok', 'Ultracode ligado', `${ULTRACODE_MAX_TURNS} turnos · ${ULTRACODE_MAX_TOKENS} tokens · effort ${ULTRACODE_EFFORT}`);
|
|
665
970
|
}
|
|
666
971
|
else {
|
|
667
972
|
settings.maxTurns = DEFAULT_CODE_MAX_TURNS;
|
|
668
973
|
settings.maxTokens = DEFAULT_CODE_MAX_TOKENS;
|
|
669
974
|
settings.effort = resolveCodeEffort(undefined, config.get('gatewayEffort'));
|
|
975
|
+
settings.objectiveMode = false;
|
|
670
976
|
printEvent('ok', 'Ultracode desligado', `voltou pro default (${DEFAULT_CODE_MAX_TURNS} turnos)`);
|
|
671
977
|
}
|
|
978
|
+
resetSystemPromptForCurrentMode();
|
|
979
|
+
refreshStatusLine(ctx, settings);
|
|
672
980
|
return 'continue';
|
|
673
981
|
}
|
|
674
982
|
if (lower === '/plan') {
|
|
675
983
|
settings.apply = false;
|
|
676
|
-
|
|
984
|
+
settings.yes = false;
|
|
985
|
+
settings.objectiveMode = false;
|
|
986
|
+
pendingPlanTask = null;
|
|
987
|
+
resetSystemPromptForCurrentMode();
|
|
988
|
+
printEvent('ok', 'Modo plano', 'somente leitura/pesquisa; escrita e execucao bloqueadas');
|
|
989
|
+
refreshStatusLine(ctx, settings);
|
|
990
|
+
return 'continue';
|
|
991
|
+
}
|
|
992
|
+
if (lower === '/approve' || lower === '/aprovar' || lower === '/approved') {
|
|
993
|
+
applyInteractiveCodeMode(settings, 'auto');
|
|
994
|
+
refreshStatusLine(ctx, settings);
|
|
995
|
+
printEvent('ok', 'Plano aprovado', 'entrando em auto mode');
|
|
996
|
+
if (!pendingPlanTask) {
|
|
997
|
+
return 'continue';
|
|
998
|
+
}
|
|
999
|
+
const approvedTask = pendingPlanTask;
|
|
1000
|
+
pendingPlanTask = null;
|
|
1001
|
+
resetSystemPromptForCurrentMode();
|
|
1002
|
+
try {
|
|
1003
|
+
await runAgentTask(`Plano aprovado pelo usuario. Execute agora em auto mode a tarefa planejada: ${approvedTask}`, settings, async (question) => {
|
|
1004
|
+
const answer = await ctx.askLine(question);
|
|
1005
|
+
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
1006
|
+
}, history);
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
printEvent('error', 'Falha', error instanceof Error ? error.message : String(error));
|
|
1010
|
+
}
|
|
1011
|
+
autoSaveCodeSession(settings, history);
|
|
1012
|
+
return 'continue';
|
|
1013
|
+
}
|
|
1014
|
+
if (lower === '/auto') {
|
|
1015
|
+
applyInteractiveCodeMode(settings, 'auto');
|
|
1016
|
+
resetSystemPromptForCurrentMode();
|
|
1017
|
+
printEvent('ok', 'Auto mode ativo', 'sem confirmacao para patches, shell_exec, git_push e gh_pr_create');
|
|
1018
|
+
refreshStatusLine(ctx, settings);
|
|
677
1019
|
return 'continue';
|
|
678
1020
|
}
|
|
679
1021
|
if (lower === '/apply') {
|
|
680
|
-
settings
|
|
681
|
-
|
|
1022
|
+
applyInteractiveCodeMode(settings, 'manual');
|
|
1023
|
+
resetSystemPromptForCurrentMode();
|
|
682
1024
|
printEvent('ok', 'Modo aplicar', 'pedira confirmacao antes do git apply');
|
|
1025
|
+
refreshStatusLine(ctx, settings);
|
|
683
1026
|
return 'continue';
|
|
684
1027
|
}
|
|
685
1028
|
if (lower === '/yes') {
|
|
686
|
-
settings
|
|
687
|
-
|
|
1029
|
+
applyInteractiveCodeMode(settings, 'accept');
|
|
1030
|
+
resetSystemPromptForCurrentMode();
|
|
688
1031
|
printEvent('warn', 'Auto-aplicar ativo', 'patches validos serao aplicados sem pergunta');
|
|
1032
|
+
refreshStatusLine(ctx, settings);
|
|
689
1033
|
return 'continue';
|
|
690
1034
|
}
|
|
691
1035
|
if (lower === '/no') {
|
|
692
|
-
settings
|
|
1036
|
+
applyInteractiveCodeMode(settings, 'manual');
|
|
1037
|
+
resetSystemPromptForCurrentMode();
|
|
693
1038
|
printEvent('ok', 'Confirmacao manual ativa');
|
|
1039
|
+
refreshStatusLine(ctx, settings);
|
|
694
1040
|
return 'continue';
|
|
695
1041
|
}
|
|
696
1042
|
if (lower === '/help' || lower === '/ajuda') {
|
|
@@ -698,10 +1044,18 @@ async function runInteractive(settings) {
|
|
|
698
1044
|
return 'continue';
|
|
699
1045
|
}
|
|
700
1046
|
try {
|
|
701
|
-
|
|
1047
|
+
const wasPlanMode = !settings.apply;
|
|
1048
|
+
const outcome = await runAgentTask(line, settings, async (question) => {
|
|
702
1049
|
const answer = await ctx.askLine(question);
|
|
703
1050
|
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
704
1051
|
}, history);
|
|
1052
|
+
if (wasPlanMode && (outcome.kind === 'text' || outcome.kind === 'patch')) {
|
|
1053
|
+
pendingPlanTask = line;
|
|
1054
|
+
printEvent('ok', 'Plano aguardando aprovacao', 'use /approve para entrar em auto mode e executar');
|
|
1055
|
+
}
|
|
1056
|
+
else if (!wasPlanMode) {
|
|
1057
|
+
pendingPlanTask = null;
|
|
1058
|
+
}
|
|
705
1059
|
}
|
|
706
1060
|
catch (error) {
|
|
707
1061
|
printEvent('error', 'Falha', error instanceof Error ? error.message : String(error));
|
|
@@ -735,7 +1089,7 @@ function runDoctor(settings) {
|
|
|
735
1089
|
console.log(` Core API: ${chalk.dim(apiUrl)}`);
|
|
736
1090
|
console.log(` AI Gateway: ${chalk.dim(AI_GATEWAY_CHAT_PATH)}`);
|
|
737
1091
|
console.log(` git: ${git.status === 0 ? chalk.green('ok') : chalk.red('falhou')}`);
|
|
738
|
-
console.log(` modo patch: ${settings
|
|
1092
|
+
console.log(` modo patch: ${formatInteractiveCodeMode(settings)}`);
|
|
739
1093
|
console.log();
|
|
740
1094
|
if (!token || git.status !== 0)
|
|
741
1095
|
setSafeExitCode(1);
|