terminal-smart-cli 0.97.30 → 0.97.41

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/lib/meta.js CHANGED
@@ -11,8 +11,11 @@ const path = require('path');
11
11
  const cp = require('child_process');
12
12
  const crypto = require('crypto');
13
13
  const compactor = require('./compactor');
14
+ const fileLock = require('./file-lock');
14
15
  const { api, ApiError } = require('./api');
15
16
  const keyring = require('./keyring');
17
+ const toolRunner = require('./tools');
18
+ const intelligence = require('./intelligence-core');
16
19
  // Teto de IA estourado (free/plano) no gateway → 402 (ou 429 com type cost_cap). Os helpers de
17
20
  // IA da missão DEVEM lançar (não retornar vazio em silêncio), senão a missão desperdiça rodadas
18
21
  // fingindo trabalhar. O run() pausa (estado salvo = resumível) e mostra CTA de upgrade.
@@ -22,6 +25,15 @@ function _capGuard(res, j) {
22
25
  }
23
26
  const agent = require('./agent');
24
27
 
28
+ // Uma chamada de Meta pode ir direto ao gateway (sem passar pelo loop de
29
+ // agent.js). Preserve aqui o mesmo contrato: quando escolhemos um modelo
30
+ // específico para planejar, revisar ou recuperar uma edição, o gateway não
31
+ // pode trocar para outro sem avisar. IDs com prefixo de provedor normalizam.
32
+ function modelWasSubstituted(requested, returned) {
33
+ if (!requested || !returned) return false;
34
+ return intelligence.normalizeModelId(requested) !== intelligence.normalizeModelId(returned);
35
+ }
36
+
25
37
  const FILE = '.ts-meta.json';
26
38
  const MAX_ATTEMPTS_PER_ITEM = 2;
27
39
  const MAX_ESCALATIONS = 3; // quantas vezes chamamos o modelo CARO "pensador"
@@ -51,6 +63,23 @@ function artifactForMetaItem(item, st, dir = '') {
51
63
  const criteria = Array.isArray(st && st.criteria) ? st.criteria : [];
52
64
  const namedCriterion = criteria.find(c => c && c.path && d.includes(path.basename(String(c.path)).toLowerCase()));
53
65
  if (namedCriterion) return String(namedCriterion.path);
66
+ // Electron não usa index.html como artefato principal para o estado local.
67
+ // Associar a persistência ao módulo real impede que a rodada seja marcada
68
+ // sem prova ou que procure o fallback genérico artifacts/iN.md.
69
+ const electronProject = dir && fs.existsSync(path.join(dir, 'package.json'))
70
+ && /electron/i.test(String(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')));
71
+ if (electronProject && /persist[êe]ncia|electron-store|salvar.*carregar|carregar.*salvar|armazenamento local/.test(d)) return 'src/store.js';
72
+ // Um checklist Electron precisa sempre apontar para o produto executável, nunca
73
+ // para o fallback artifacts/iN.md. Caso contrário o agente pode gastar uma
74
+ // rodada inteira escrevendo documentação sem mover o aplicativo adiante.
75
+ if (electronProject && /modelo de dados|filme.*s[ée]rie.*livro|estado.*nota|nota.*coment[áa]rio|tags.*progresso/.test(d)) return 'src/model.js';
76
+ if (electronProject && /transi[çc][ãa]o de estado|busca.*filtro|filtro.*busca/.test(d)) return 'src/model.js';
77
+ if (electronProject && /exporta[çc][ãa]o|importa[çc][ãa]o|backup/.test(d)) return 'src/store.js';
78
+ if (electronProject && /teste|verifica[çc][ãa]o automatizada|npm test/.test(d)) return 'test/core.test.js';
79
+ if (electronProject && /readme|instru[çc][õo]es/.test(d)) return 'README.md';
80
+ if (electronProject && /integra[çc][õo]es futuras|tmdb|google books/.test(d)) return 'INTEGRACOES-FUTURAS.md';
81
+ if (electronProject && /modelo de neg[óo]cio|freemium|privacidade|an[uú]ncio/.test(d)) return 'MODELO-DE-NEGOCIO.md';
82
+ if (electronProject && /interface|tela|biblioteca|descobrir|cole[çc][ãa]o demo|limpar demo/.test(d)) return 'src/index.html';
54
83
  const splitWebProject = dir && fs.existsSync(path.join(dir, 'index.html')) && fs.existsSync(path.join(dir, 'src', 'game.js'));
55
84
  // In an existing browser game, UI/HUD requests are implementation work.
56
85
  // The generic UI-spec fallback below is only for greenfield design projects.
@@ -84,6 +113,72 @@ function artifactHash(file) {
84
113
  catch (_) { return null; }
85
114
  }
86
115
 
116
+ // A retomada não deve reabrir uma investigação inteira apenas para descobrir
117
+ // onde fazer um ajuste já conhecido. Entregamos um recorte determinístico do
118
+ // artefato-alvo, com números de linha, para que o executor tenha uma âncora
119
+ // exata de edição. Isso é contexto local verificável, não uma conclusão da IA.
120
+ function recoveryTargetExcerpt(file, item, maxChars = 7000) {
121
+ if (!file || !fs.existsSync(file)) return '';
122
+ let source;
123
+ try { source = fs.readFileSync(file, 'utf8'); }
124
+ catch (_) { return ''; }
125
+ const fold = value => String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
126
+ const ignored = new Set(['implementar', 'criar', 'fazer', 'para', 'como', 'com', 'sem', 'uma', 'este', 'esta', 'item', 'dados']);
127
+ const terms = [...new Set(fold(item && item.desc).match(/[a-z0-9_-]{4,}/g) || [])]
128
+ .filter(term => !ignored.has(term));
129
+ if (!terms.length) return '';
130
+ const lines = source.split(/\r?\n/);
131
+ const ranked = lines.map((line, index) => ({
132
+ index,
133
+ score: terms.reduce((total, term) => total + (fold(line).includes(term) ? 1 : 0), 0),
134
+ })).filter(row => row.score > 0).sort((a, b) => b.score - a.score || a.index - b.index).slice(0, 5);
135
+ if (!ranked.length) return '';
136
+ const selected = new Set();
137
+ for (const row of ranked) {
138
+ for (let index = Math.max(0, row.index - 4); index <= Math.min(lines.length - 1, row.index + 4); index++) selected.add(index);
139
+ }
140
+ const excerpt = [...selected].sort((a, b) => a - b)
141
+ .map(index => `${String(index + 1).padStart(5, ' ')} | ${lines[index]}`).join('\n');
142
+ return excerpt.slice(0, maxChars);
143
+ }
144
+
145
+ // Recuperação de microedição: quando o próprio Meta já tem a prova local e a
146
+ // âncora exata, não delegamos de novo a escolha de ferramentas ao loop geral.
147
+ // O DeepSeek Pro propõe exclusivamente a troca literal; o harness a aplica com
148
+ // editar_arquivo, que exige âncora única e faz backup. Assim a IA não consegue
149
+ // gastar a janela tentando pesquisar o mesmo arquivo pela quarta vez.
150
+ async function anchoredRecoveryEdit({ token, dir, expectedArtifact, expectedOnDisk, recoveryExcerpt, previousItemFacts, lang }) {
151
+ const localized = lang === 'en';
152
+ const system = localized
153
+ ? 'You are the DeepSeek Pro code executor. Return ONLY valid JSON: {"buscar":"exact existing text from the excerpt","substituir":"replacement text","resumo":"short factual summary"}. Make one minimal edit that fulfills the requested checklist item. Do not explain, do not use Markdown, do not propose a plan.'
154
+ : 'Você é o executor de código DeepSeek Pro. Responda SOMENTE JSON válido: {"buscar":"trecho existente EXATO do recorte","substituir":"texto substituto","resumo":"resumo factual curto"}. Faça uma única edição mínima que cumpra o item do checklist. Não explique, não use Markdown, não proponha plano.';
155
+ const user = localized
156
+ ? `CHECKLIST ITEM: ${String(expectedArtifact || '')}\nPREVIOUS FACTS:\n${previousItemFacts}\n\nTARGET SOURCE EXCERPT:\n${recoveryExcerpt}`
157
+ : `ITEM DO CHECKLIST: ${String(expectedArtifact || '')}\nFATOS ANTERIORES:\n${previousItemFacts}\n\nTRECHO DO ARTEFATO-ALVO:\n${recoveryExcerpt}`;
158
+ const proposal = await _llmJson({ token, model: 'deepseek-v4-pro', system, user, maxTokens: 2200 });
159
+ const patch = proposal && proposal.json || {};
160
+ const buscar = String(patch.buscar || '');
161
+ const substituir = String(patch.substituir || '');
162
+ if (!buscar || !substituir || buscar === substituir) throw new Error('RECOVERY_PATCH_INVALID: o executor não devolveu uma substituição válida');
163
+ const target = path.resolve(expectedOnDisk || path.join(dir, expectedArtifact || ''));
164
+ const confined = path.resolve(dir);
165
+ if (target !== confined && !target.startsWith(confined + path.sep)) throw new Error('RECOVERY_PATCH_SCOPE: artefato fora do projeto');
166
+ const result = await toolRunner.execute('editar_arquivo', { caminho: target, buscar, substituir }, {
167
+ confineDir: dir, baseDir: dir, allowedTools: ['editar_arquivo'],
168
+ });
169
+ if (!result || result.erro || !result.ok) throw new Error('RECOVERY_PATCH_FAILED: ' + String(result && (result.erro || result.stderr) || 'edição não confirmada'));
170
+ return {
171
+ text: String(patch.resumo || (localized ? 'Anchored recovery edit persisted.' : 'Edição de recuperação por âncora persistida.')),
172
+ steps: 1,
173
+ credits: Number(proposal.credits || 0),
174
+ tokens: { inTok: 0, outTok: 0, cachedTok: 0 },
175
+ model: 'deepseek-v4-pro',
176
+ actions: [{ name: 'editar_arquivo', target, result }],
177
+ toolErrors: [],
178
+ completion: { ok: true, reason: 'anchored_recovery_edit' },
179
+ };
180
+ }
181
+
87
182
  function normalizeMetaArtifact(relativePath, raw) {
88
183
  let text = String(raw || '').trim().replace(/^```(?:json|gdscript|markdown|md)?\s*/i, '').replace(/\s*```\s*$/i, '').trim();
89
184
  if (/\.json$/i.test(relativePath)) {
@@ -912,48 +1007,79 @@ function save(st, dir) {
912
1007
  }
913
1008
  }
914
1009
 
915
- // Chamada JSON barata (flash-lite via CDC, chave sk-hub do usuário) + cobrança
1010
+ // Chamada JSON do planejador/verificador. Nunca fixa Flash: o papel que chama
1011
+ // escolhe o modelo conforme o contrato oficial e o plano do usuário.
916
1012
  let _key = null;
917
1013
  let _keyNuvem = null; // credencial do GATEWAY (visão) — separada da BYOK
918
- async function _llmJson({ token, system, user, maxTokens = 900 }) {
1014
+ async function _llmJson({ token, system, user, maxTokens = 900, model = 'gpt-5.6-luna' }) {
919
1015
  if (!_key) _key = await keyring.resolve(token, { feature: 'cli_agent' });
920
1016
  const ctrl = new AbortController();
921
- const timer = setTimeout(() => ctrl.abort(), 60000);
1017
+ let timer;
922
1018
  let res;
923
1019
  try {
924
- res = await fetch(String(_key.baseUrl).replace(/\/+$/, '') + '/chat/completions', {
1020
+ const request = fetch(String(_key.baseUrl).replace(/\/+$/, '') + '/chat/completions', {
925
1021
  method: 'POST', signal: ctrl.signal,
926
1022
  headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + _key.key },
927
- body: JSON.stringify({ model: keyring.modeloPara(_key, 'gemini-2.5-flash-lite'), stream: false, max_completion_tokens: maxTokens,
1023
+ body: JSON.stringify({ model: keyring.modeloPara(_key, model), stream: false, max_completion_tokens: maxTokens,
928
1024
  messages: [{ role: 'system', content: system }, { role: 'user', content: user }] }),
929
1025
  });
1026
+ // Alguns gateways não encerram o socket imediatamente após abort(). A
1027
+ // corrida garante que o Meta devolva uma falha tratável em até 60 s, em
1028
+ // vez de deixar a missão presa em "pensando" indefinidamente.
1029
+ const deadline = new Promise((_, reject) => {
1030
+ timer = setTimeout(() => { ctrl.abort(); reject(new Error('META_MODEL_TIMEOUT')); }, 60000);
1031
+ });
1032
+ res = await Promise.race([request, deadline]);
930
1033
  } finally { clearTimeout(timer); }
931
1034
  const j = await res.json().catch(() => null);
932
1035
  if (!res.ok) _capGuard(res, j);
1036
+ if (modelWasSubstituted(model, j && j.model)) {
1037
+ throw new ApiError(`Modelo exigido não foi respeitado: solicitado "${model}", recebido "${j.model}".`, {
1038
+ status: 409, code: 'model_substituted',
1039
+ });
1040
+ }
1041
+ const content = String(j?.choices?.[0]?.message?.content || '').trim();
1042
+ if (!content) {
1043
+ const providerMessage = String(j?.error?.message || j?.message || '').trim();
1044
+ throw new ApiError(`O modelo ${model || 'selecionado'} retornou um JSON vazio${providerMessage ? ': ' + providerMessage : '.'}`, {
1045
+ status: 502, code: 'empty_model_response',
1046
+ });
1047
+ }
933
1048
  const u = j?.usage || {};
934
1049
  let credits = (j?.ts_billing && j.ts_billing.charged) || 0;
935
1050
  if (u.prompt_tokens) {
936
- if (!_key.billingAuthoritative) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model: 'gemini-2.5-flash-lite', inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {} }
1051
+ if (!_key.billingAuthoritative) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model, inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {} }
937
1052
  }
938
- return { json: _tolerantJson(String(j?.choices?.[0]?.message?.content || '')), credits };
1053
+ return { json: _tolerantJson(content), credits };
939
1054
  }
940
1055
 
941
1056
  // Chamada de TEXTO com modelo arbitrário — usada pelo PENSADOR (escalonamento).
942
1057
  async function _llmText({ token, model, system, user, maxTokens = 1200, creditBudget = null }) {
943
1058
  if (!_key) _key = await keyring.resolve(token, { feature: 'cli_agent' });
944
1059
  const ctrl = new AbortController();
945
- const timer = setTimeout(() => ctrl.abort(), 120000);
1060
+ let timer;
946
1061
  let res;
947
1062
  try {
948
- res = await fetch(String(_key.baseUrl).replace(/\/+$/, '') + '/chat/completions', {
1063
+ const request = fetch(String(_key.baseUrl).replace(/\/+$/, '') + '/chat/completions', {
949
1064
  method: 'POST', signal: ctrl.signal,
950
1065
  headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + _key.key,
951
1066
  ...(Number.isFinite(Number(creditBudget)) && Number(creditBudget) > 0 ? { 'X-TS-Credit-Budget': String(Math.floor(Number(creditBudget))) } : {}) },
952
1067
  body: JSON.stringify({ model: keyring.modeloPara(_key, model), stream: false, max_completion_tokens: maxTokens, messages: [{ role: 'system', content: system }, { role: 'user', content: user }] }),
953
1068
  });
1069
+ // Idem ao fluxo do agente: o prazo deve vencer mesmo se o proxy mantiver
1070
+ // a conexão aberta depois do abort, permitindo pausar/retomar com estado.
1071
+ const deadline = new Promise((_, reject) => {
1072
+ timer = setTimeout(() => { ctrl.abort(); reject(new Error('META_MODEL_TIMEOUT')); }, 120000);
1073
+ });
1074
+ res = await Promise.race([request, deadline]);
954
1075
  } finally { clearTimeout(timer); }
955
1076
  const j = await res.json().catch(() => null);
956
1077
  if (!res.ok) _capGuard(res, j);
1078
+ if (modelWasSubstituted(model, j && j.model)) {
1079
+ throw new ApiError(`Modelo exigido não foi respeitado: solicitado "${model}", recebido "${j.model}".`, {
1080
+ status: 409, code: 'model_substituted',
1081
+ });
1082
+ }
957
1083
  const u = j?.usage || {};
958
1084
  let credits = (j?.ts_billing && j.ts_billing.charged) || 0;
959
1085
  if (u.prompt_tokens && !_key.billingAuthoritative) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model, inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {} }
@@ -1134,15 +1260,44 @@ const MARK_SYS = 'Você é o VERIFICADOR de uma missão. Recebe UM item do check
1134
1260
 
1135
1261
  const fmtChecklist = (itens) => itens.map(it => `[${it.passes ? 'x' : it.blocked ? '!' : ' '}] (${it.id}) ${it.desc}`).join('\n');
1136
1262
 
1137
- async function makeChecklist(goal, token) {
1138
- const r = await _llmJson({ token, system: CHECKLIST_SYS, user: 'OBJETIVO:\n' + goal, maxTokens: 1800 });
1139
- const itens = (Array.isArray(r.json?.itens) ? r.json.itens : []).slice(0, 15)
1140
- .map((it, i) => ({ id: String(it.id || 'i' + (i + 1)), desc: String(it.desc || '').slice(0, 300), passes: false, attempts: 0 }))
1141
- .filter(it => it.desc);
1142
- // Critérios propostos pelo planner → VALIDADOS pelo compilador (formato conhecido; malformados caem fora).
1143
- let criteria = [];
1144
- try { criteria = require('./verify').compileCriteria(r.json?.criterios || []).criteria.slice(0, 6); } catch (_) {}
1145
- return { itens: itens.length ? itens : [{ id: 'i1', desc: String(goal).slice(0, 300), passes: false, attempts: 0 }], criteria, credits: r.credits };
1263
+ async function makeChecklist(goal, token, plannerModel = 'gpt-5.6-luna') {
1264
+ // Plano vazio ou malformado nunca vira uma etapa com o objetivo inteiro: isso
1265
+ // autorizava uma execução cega. Repetimos Luna uma vez e só então usamos
1266
+ // DeepSeek Pro como fallback objetivo, limitado a uma tentativa.
1267
+ const attempts = [plannerModel, plannerModel];
1268
+ if (plannerModel !== 'deepseek-v4-pro') attempts.push('deepseek-v4-pro');
1269
+ let credits = 0, lastError = null;
1270
+ for (const model of attempts) {
1271
+ try {
1272
+ const r = await _llmJson({ token, model, system: CHECKLIST_SYS, user: 'OBJETIVO:\n' + goal, maxTokens: 1800 });
1273
+ credits += r.credits || 0;
1274
+ const itens = (Array.isArray(r.json?.itens) ? r.json.itens : []).slice(0, 15)
1275
+ .map((it, i) => ({ id: String(it.id || 'i' + (i + 1)), desc: String(it.desc || '').slice(0, 300), passes: false, attempts: 0 }))
1276
+ .filter(it => it.desc && it.desc.length >= 8);
1277
+ if (!itens.length) { lastError = new Error(`planner ${model} não definiu etapas executáveis`); continue; }
1278
+ // Critérios propostos pelo planner → VALIDADOS pelo compilador (formato conhecido; malformados caem fora).
1279
+ let criteria = [];
1280
+ try { criteria = require('./verify').compileCriteria(r.json?.criterios || []).criteria.slice(0, 6); } catch (_) {}
1281
+ return { itens, criteria, credits, plannerModel: model, fallback: model !== plannerModel };
1282
+ } catch (e) { lastError = e; }
1283
+ }
1284
+ const err = new ApiError('Não foi possível obter um plano válido; nenhuma etapa foi executada. Tente novamente em instantes.', {
1285
+ status: 502, code: 'planner_invalid',
1286
+ });
1287
+ err.credits = credits;
1288
+ err.cause = lastError;
1289
+ throw err;
1290
+ }
1291
+
1292
+ // Missões antigas podiam ter salvo aliases de UI como "smart" em `st.model`.
1293
+ // Esse estado não é uma escolha explícita do usuário e nunca pode substituir o
1294
+ // executor oficial de uma tarefa Meta.
1295
+ function officialExecutorModel(explicitModel, persistedModel) {
1296
+ if (explicitModel) return explicitModel;
1297
+ const normalized = String(persistedModel || '').trim().toLowerCase();
1298
+ return ['deepseek-v4-pro', 'deepseek/deepseek-v4-pro'].includes(normalized)
1299
+ ? 'deepseek-v4-pro'
1300
+ : 'deepseek-v4-pro';
1146
1301
  }
1147
1302
 
1148
1303
  /**
@@ -1189,6 +1344,17 @@ async function run(goal, opts = {}) {
1189
1344
  const onAlert = opts.onAlert || (() => {}); // avisos (escalonei / preciso de você / travei)
1190
1345
  const startMs = Date.now();
1191
1346
 
1347
+ // Uma missão Meta persiste o mesmo .ts-meta.json em várias etapas. Duas
1348
+ // execuções concorrentes podem sobrescrever checklist, créditos e provas;
1349
+ // portanto cada projeto aceita somente uma rodada ativa por vez.
1350
+ const runLock = fileLock.acquire(stateFile(dir), { staleMs: 30 * 60 * 1000 });
1351
+ if (!runLock) {
1352
+ const current = load(dir) || {};
1353
+ onAlert({ type: 'busy', text: 'Outra missão Meta já está em execução neste projeto. Aguarde ela terminar antes de retomar.' });
1354
+ return Object.assign({}, current, { status: 'busy', busy: true });
1355
+ }
1356
+ try {
1357
+
1192
1358
  let st = load(dir);
1193
1359
  if (!st || st.status === 'done') {
1194
1360
  if (!goal) return null; // nada pra retomar
@@ -1198,7 +1364,7 @@ async function run(goal, opts = {}) {
1198
1364
  // FASE DE VIABILIDADE: avalia o POSSÍVEL e escopa honesto antes de codar app ambicioso
1199
1365
  let feasBrief = null, feasCred = 0;
1200
1366
  const feasMode = opts.feasibility === undefined ? 'auto' : opts.feasibility;
1201
- const feasModel = thinker || 'grok-4.5';
1367
+ const feasModel = thinker || 'gpt-5.6-luna';
1202
1368
  if (feasMode === true || (feasMode === 'auto' && looksComplex(goal))) {
1203
1369
  onRound({ n: 0, item: (lang === 'en' ? `Checking feasibility with ${feasModel}…` : `Avaliando viabilidade com ${feasModel}…`), attempt: 1 });
1204
1370
  try { const fe = await feasibilityPhase(goal, feasModel, token); feasBrief = fe.brief; feasCred = fe.credits || 0; try { fs.writeFileSync(path.join(dir, 'VIABILIDADE.md'), feasBrief); } catch (_) {} onAlert({ type: 'design', text: `🔎 ts: viabilidade avaliada (${feasModel}, ${feasCred} créditos) — o que dá e o que não dá está fixado. Salvo em VIABILIDADE.md.` }); } catch (_e) { if (_e && _e.code === 'no_credits') throw _e; onAlert({ type: 'phase_error', text: `⚠ ts: viabilidade não concluída: ${String(_e && _e.message || _e).slice(0, 220)}` }); }
@@ -1216,12 +1382,12 @@ async function run(goal, opts = {}) {
1216
1382
  // FASE DE ARQUITETURA (3º pilar): app complexo ganha um CONTRATO antes do checklist
1217
1383
  let archBrief = null, archCred = 0;
1218
1384
  const archMode = opts.arch === undefined ? 'auto' : opts.arch;
1219
- const archModel = thinker || 'grok-4.5';
1385
+ const archModel = thinker || 'gpt-5.6-luna';
1220
1386
  if (archMode === true || (archMode === 'auto' && looksComplex(goal))) {
1221
1387
  onRound({ n: 0, item: (lang === 'en' ? `Designing the architecture with ${archModel}…` : `Projetando a arquitetura com ${archModel}…`), attempt: 1 });
1222
1388
  try { const a = await archPhase(goal, archModel, token, { dir }); archBrief = a.brief; archCred = a.credits || 0; try { fs.writeFileSync(path.join(dir, 'ARQUITETURA.md'), archBrief); } catch (_) {} onAlert({ type: 'design', text: `📐 ts: contrato de arquitetura criado pelo ${archModel} (${archCred} créditos) — componentes, arquivos e assinaturas fixados. Salvo em ARQUITETURA.md.` }); } catch (_e) { if (_e && _e.code === 'no_credits') throw _e; onAlert({ type: 'phase_error', text: `⚠ ts: arquitetura não concluída: ${String(_e && _e.message || _e).slice(0, 220)}` }); }
1223
1389
  }
1224
- const mk = await makeChecklist(goal + (designBrief ? '\n\n[Há um DESIGN SYSTEM definido — o checklist deve refletir a aplicação desse visual]' : '') + (archBrief ? '\n\n[Há um CONTRATO DE ARQUITETURA definido — os itens devem seguir os componentes/arquivos dele]' : ''), token);
1390
+ const mk = await makeChecklist(goal + (designBrief ? '\n\n[Há um DESIGN SYSTEM definido — o checklist deve refletir a aplicação desse visual]' : '') + (archBrief ? '\n\n[Há um CONTRATO DE ARQUITETURA definido — os itens devem seguir os componentes/arquivos dele]' : ''), token, thinker || 'gpt-5.6-luna');
1225
1391
  // CRITÉRIOS (TaskSpec): usuário (--criterios) manda; senão, com --provar, combina o que o
1226
1392
  // PLANNER propôs (mk.criteria, já validado) + os TESTES da stack — tudo recompilado.
1227
1393
  // Mesmo sem --provar, critérios determinísticos derivados da stack rodam por padrão;
@@ -1232,7 +1398,10 @@ async function run(goal, opts = {}) {
1232
1398
  else if (prove) _criteria = _V.compileCriteria([..._V.deriveFromStack(dir), ..._planned]).criteria.slice(0, 8);
1233
1399
  else _criteria = _V.compileCriteria(_V.deriveFromStack(dir)).criteria.slice(0, 8);
1234
1400
  st = { goal: String(goal).slice(0, 4000), mockup: opts.mockup || null, archBrief, feasBrief, checklist: mk.itens, rounds: [], creditsSpent: (mk.credits || 0) + designCred + archCred + feasCred,
1235
- budget, maxRounds, status: 'running', model: model || null, designBrief: designBrief || null, criteria: _criteria, plannedCriteria: _planned, created_at: new Date().toISOString() };
1401
+ // Missões locais pagas nunca devem cair no alias genérico `gateway`, que
1402
+ // pode apontar para Flash ou para um provedor sem ferramentas. DeepSeek
1403
+ // Pro é o executor oficial; uma escolha explícita do usuário ainda vence.
1404
+ budget, maxRounds, status: 'running', model: model || 'deepseek-v4-pro', designBrief: designBrief || null, criteria: _criteria, plannedCriteria: _planned, created_at: new Date().toISOString() };
1236
1405
  save(st, dir);
1237
1406
  } else {
1238
1407
  st.status = 'running';
@@ -1246,7 +1415,10 @@ async function run(goal, opts = {}) {
1246
1415
  if (opts.addBudget) st.budget += opts.addBudget;
1247
1416
  else if (st.creditsSpent >= st.budget) st.budget = st.creditsSpent + budget; // nova janela ao retomar
1248
1417
  }
1249
- const useModel = model || st.model || null; // executor fixo persiste entre retomadas
1418
+ const useModel = officialExecutorModel(model, st.model);
1419
+ // Corrige o checkpoint legado para que a próxima retomada também fique no
1420
+ // contrato Luna (planner) + DeepSeek Pro (executor), sem depender da UI.
1421
+ if (!model && st.model !== useModel) { st.model = useModel; save(st, dir); }
1250
1422
  const useThinker = thinker || st.thinker || null; // modelo caro "pensador" (escalonamento)
1251
1423
  if (thinker && !st.thinker) { st.thinker = thinker; save(st, dir); }
1252
1424
  onChecklist(st.checklist);
@@ -1426,7 +1598,7 @@ async function run(goal, opts = {}) {
1426
1598
  }
1427
1599
  if (!item.directArtifact && (item.attempts || 0) > 0 && expectedOnDisk && !fs.existsSync(expectedOnDisk)) {
1428
1600
  item.directArtifact = true;
1429
- item.directModel = item.directModel || 'glm-5.2';
1601
+ item.directModel = item.directModel || 'deepseek-v4-pro';
1430
1602
  item.attempts = Math.max(0, item.attempts - 1);
1431
1603
  }
1432
1604
  item.attempts = (item.attempts || 0) + 1;
@@ -1436,7 +1608,7 @@ async function run(goal, opts = {}) {
1436
1608
  // generated in validated batches from the start (classes/maps/quests/monsters).
1437
1609
  if (!item.directArtifact && directArtifactPlan(item)) {
1438
1610
  item.directArtifact = true;
1439
- item.directModel = 'glm-5.2';
1611
+ item.directModel = 'deepseek-v4-pro';
1440
1612
  }
1441
1613
 
1442
1614
  // Some providers can generate a large document but refuse to place that
@@ -1448,7 +1620,7 @@ async function run(goal, opts = {}) {
1448
1620
  save(st, dir);
1449
1621
  }
1450
1622
  if (item.directArtifact) {
1451
- const directModel = item.directModel || 'glm-5.2';
1623
+ const directModel = item.directModel || 'deepseek-v4-pro';
1452
1624
  const plan = directArtifactPlan(item);
1453
1625
  let totalCredits = 0, finalText = '';
1454
1626
  if (plan) {
@@ -1572,14 +1744,25 @@ async function run(goal, opts = {}) {
1572
1744
  ? `\nMINIMAL-CODE RULES (economy — mandatory, think like a pragmatic lazy senior):\n1) Does this really need to exist? If not, don't build it.\n2) Does it already exist in this project? If yes, REUSE it — never rewrite a second version.\n3) Does the language/platform/browser already do it natively? If yes, use the native thing (e.g. <input type="date"> instead of a datepicker lib; fetch instead of axios; CSS instead of a JS animation lib).\n4) Only then write code — the MINIMUM that solves the item. NO extra library without real need, no gratuitous abstractions/wrappers, no speculative "future-proofing".\n`
1573
1745
  : `\nREGRAS DE CÓDIGO MÍNIMO (economia — obrigatórias; pense como um sênior pragmático e "preguiçoso"):\n1) Isso precisa mesmo existir? Se não precisa, NÃO construa.\n2) Já existe neste projeto? Se sim, REUSE — nunca escreva uma segunda versão.\n3) A linguagem/plataforma/navegador já faz isso nativo? Se sim, use o nativo (ex: <input type="date"> em vez de lib de datepicker; fetch em vez de axios; CSS em vez de lib JS de animação).\n4) Só então escreva código — o MÍNIMO que resolve o item. SEM biblioteca extra sem necessidade real, sem abstração/wrapper gratuito, sem "preparar pro futuro" especulativo.\n`);
1574
1746
  const artifactBlock = lang === 'en'
1575
- ? `\nMANDATORY PROOF ARTIFACT FOR THIS ITEM: ${expectedArtifact}\nYou MUST call escrever_arquivo or editar_arquivo and persist the deliverable at exactly this relative path. A text-only answer does NOT complete the item. Create parent directories through the file tool as needed.\n`
1576
- : `\nARTEFATO DE PROVA OBRIGATORIO DESTE ITEM: ${expectedArtifact}\nVoce DEVE chamar escrever_arquivo ou editar_arquivo e persistir a entrega exatamente nesse caminho relativo. Resposta apenas em texto NAO conclui o item. Crie os diretorios-pai pela ferramenta de arquivo quando necessario.\n`;
1577
- const task = feasBlock + archBlock + designBlock + seedBlock + rulesBlock + minimalBlock + artifactBlock + (lang === 'en'
1747
+ ? `\nMANDATORY PROOF ARTIFACT FOR THIS ITEM: ${expectedArtifact}\nYou MUST call escrever_arquivo or editar_arquivo and persist the deliverable at exactly this relative path. A text-only answer does NOT complete the item. Create parent directories through the file tool as needed. For an existing HTML/CSS/JS file, prefer editar_arquivo with an exact anchor. Create a temporary patch script only as a last resort; validate it first (Python: python -m py_compile <file>) before executing it.\n`
1748
+ : `\nARTEFATO DE PROVA OBRIGATORIO DESTE ITEM: ${expectedArtifact}\nVoce DEVE chamar escrever_arquivo ou editar_arquivo e persistir a entrega exatamente nesse caminho relativo. Resposta apenas em texto NAO conclui o item. Crie os diretorios-pai pela ferramenta de arquivo quando necessario. Para HTML/CSS/JS existente, prefira editar_arquivo com ancora exata. Crie script temporario de patch somente em ultimo caso; antes de executa-lo, valide-o (Python: python -m py_compile <arquivo>). Use no maximo UM script temporario por item: se ele falhar, corrija o MESMO arquivo com editar_arquivo e valide de novo; nao crie outro script de patch.\n`;
1749
+ const previousItemFacts = item.lastAttempt && item.lastAttempt.result
1750
+ ? String(item.lastAttempt.result).slice(0, 1400)
1751
+ : '';
1752
+ const recoveryExcerpt = previousItemFacts && expectedOnDisk
1753
+ ? recoveryTargetExcerpt(expectedOnDisk, item)
1754
+ : '';
1755
+ const recoveryBlock = previousItemFacts
1756
+ ? (lang === 'en'
1757
+ ? `\nRECOVERY FACTS (already verified in the previous attempt; DO NOT rediscover them):\n${previousItemFacts}${recoveryExcerpt ? `\n\nTARGET SOURCE EXCERPT (deterministic local evidence; use these numbered lines as the exact edit anchor — do NOT search, reread or run commands):\n${recoveryExcerpt}` : ''}\n\nRECOVERY RULE: make the concrete edit in ${expectedArtifact} now. ${recoveryExcerpt ? 'Your first tool call MUST edit/write the target using this excerpt. Persist only the change; the harness runs objective proof after your edit.' : 'You may inspect ONLY that target once for an exact anchor. Your next tool call after that must edit/write the target, then run one objective proof.'}\n`
1758
+ : `\nFATOS DA TENTATIVA ANTERIOR (já verificados; NÃO os redescubra):\n${previousItemFacts}${recoveryExcerpt ? `\n\nTRECHO DO ARTEFATO-ALVO (prova local determinística; use as linhas numeradas como âncora exata da edição — NÃO pesquise, releia nem rode comandos):\n${recoveryExcerpt}` : ''}\n\nREGRA DE RECUPERAÇÃO: faça agora a edição concreta em ${expectedArtifact}. ${recoveryExcerpt ? 'Sua primeira chamada de ferramenta DEVE editar/escrever o alvo usando este trecho. Apenas persista a mudança; o harness roda a prova objetiva depois da edição.' : 'Você pode inspecionar SOMENTE esse alvo uma vez para obter a âncora exata. A próxima chamada de ferramenta deve editar/escrever o alvo e depois rodar uma prova objetiva.'}\n`)
1759
+ : '';
1760
+ const task = feasBlock + archBlock + designBlock + seedBlock + rulesBlock + minimalBlock + artifactBlock + recoveryBlock + (lang === 'en'
1578
1761
  ? `Bigger goal (context — do NOT do everything now):\n${st.goal.slice(0, 800)}\n\nCHECKLIST (state):\n${fmtChecklist(st.checklist)}\n${mapBlock}`
1579
1762
  : `Objetivo maior (contexto — NÃO faça tudo agora):\n${st.goal.slice(0, 800)}\n\nCHECKLIST (estado):\n${fmtChecklist(st.checklist)}\n${mapBlock}`)
1580
1763
  + (last ? `\n${lang === 'en' ? 'Previous round' : 'Rodada anterior'}: ${String(last.result || '').slice(0, 600)}\n` : '')
1581
- + (item.attempts > 1
1582
- ? (lang === 'en' ? `\nPREVIOUS ATTEMPT at this item FAILED use a DIFFERENT approach now.\n` : `\nA tentativa ANTERIOR deste item FALHOU use uma abordagem DIFERENTE agora.\n`)
1764
+ + ((item.attempts > 1 || item.lastAttempt)
1765
+ ? (lang === 'en' ? `\nPREVIOUS ATTEMPT at this item FAILED. RECOVERY MODE: inspect ONLY the mandatory artifact once (a focused range), then edit it directly. Do not use ler_arquivos/buscar_arquivos, do not reread the project, and do not create any patch script. Persist one concrete fix and run one objective proof.\n` : `\nA tentativa ANTERIOR deste item FALHOU. MODO DE RECUPERAÇÃO: inspecione APENAS o artefato obrigatório uma única vez (faixa focada) e depois edite-o diretamente. Não use ler_arquivos/buscar_arquivos, não releia o projeto e não crie script de patch. Persista um único ajuste concreto e rode uma prova objetiva.\n`)
1583
1766
  : '')
1584
1767
  + (item.id === 'visual_fix' && st.buildError
1585
1768
  ? (lang === 'en' ? `\nAn ART DIRECTOR reviewed REAL screenshots and REJECTED the layout. Fix ONLY the points below by editing the layout/theme/drawable XML. RULES: fix the exact issue, do NOT redesign or add decorative backgrounds/blobs/ellipses behind rows (that makes it uglier); for clipped horizontal rows use a real HorizontalScrollView/RecyclerView with android:clipToPadding="false" and equal start/end padding so first AND last items show fully; keep it clean and minimal. Do NOT run the build yourself (the system re-checks and re-screenshots automatically):\n${st.buildError}\n` : `\nUm DIRETOR DE ARTE revisou PRINTS REAIS e REPROVOU o layout. Corrija APENAS os pontos abaixo editando os XML de layout/tema/drawable. REGRAS: conserte exatamente o problema, NÃO redesenhe nem adicione fundos/blobs/elipses decorativos atrás das fileiras (fica mais feio); pra fileira horizontal cortada use um HorizontalScrollView/RecyclerView de verdade com android:clipToPadding="false" e padding start/end IGUAIS pra o 1º E o último item aparecerem inteiros; mantenha limpo e minimalista. NÃO rode o build você mesmo (o sistema recompila e reprinta sozinho):\n${st.buildError}\n`)
@@ -1607,7 +1790,7 @@ async function run(goal, opts = {}) {
1607
1790
  // spend three HTTP retries on it again for every checklist item. Keep the
1608
1791
  // cooldown mission-local and short so normal routing recovers naturally.
1609
1792
  if (!model && Number(st.autoExecutorDegradedUntil || 0) > Date.now()) {
1610
- roundModel = 'glm-5.2';
1793
+ roundModel = 'gpt-5.6-luna';
1611
1794
  }
1612
1795
  // A model that answered with promises but executed zero tools is avoided
1613
1796
  // for this item on the next window. This is a capability failure, not a
@@ -1617,7 +1800,7 @@ async function run(goal, opts = {}) {
1617
1800
  // Use only model ids currently exposed by the CDC. Haiku remains in the
1618
1801
  // intelligence catalog for pricing/history, but it is not a callable CDC
1619
1802
  // id and must never be selected as the mission recovery executor.
1620
- const candidates = ['glm-5.2', 'gpt-5.6-luna', 'deepseek-v4-pro'];
1803
+ const candidates = ['deepseek-v4-pro', 'gpt-5.6-luna'];
1621
1804
  roundModel = candidates.find(c => !avoid.some(a => a.includes(c) || c.includes(a))) || roundModel;
1622
1805
  }
1623
1806
  if (visualLadder && item.id === 'visual_fix' && (st.visualFixes || 0) >= VISUAL_ESCALATE_AT && (useThinker || eye)) {
@@ -1631,11 +1814,41 @@ async function run(goal, opts = {}) {
1631
1814
  // can have a minimum context larger than that, so use the remaining
1632
1815
  // window budget while preserving the mission-wide ceiling.
1633
1816
  const roundCreditBudget = Math.max(1, Math.min(5000, Math.floor(st.budget - st.creditsSpent)));
1634
- const roundAllowedTools = Array.isArray(opts.allowedTools) ? opts.allowedTools : toolsForMetaItem(item, st.goal);
1635
- out = await agent.run(task, { token, lang, yes, autoAll, model: roundModel, maxCredits: roundCreditBudget, allowedTools: roundAllowedTools, confineDir: dir, skipSessionStart: true, onStep: opts.onStep, onStepDone: opts.onStepDone, askApprove: opts.askApprove, onRemote: opts.onRemote, onThinking: opts.onThinking });
1817
+ let roundAllowedTools = Array.isArray(opts.allowedTools) ? opts.allowedTools : toolsForMetaItem(item, st.goal);
1818
+ // Com a âncora local fornecida, a única responsabilidade do agente
1819
+ // nesta janela é persistir a correção. A prova é executada pelo
1820
+ // harness depois da edição; deixar comandos/leitura disponíveis fazia
1821
+ // o modelo gastar esta janela pesquisando de novo em vez de corrigir.
1822
+ if (recoveryExcerpt && Array.isArray(roundAllowedTools)) {
1823
+ const recoveryEditTools = new Set(['editar_arquivo', 'escrever_arquivo']);
1824
+ roundAllowedTools = roundAllowedTools.filter(name => recoveryEditTools.has(name));
1825
+ }
1826
+ // Cada item é uma janela independente. Sem este teto, uma resposta de
1827
+ // provedor que mantém o socket aberto deixa a missão em "running" por
1828
+ // muitos minutos, sem checkpoint nem retorno visual. Ao expirar, o
1829
+ // agente devolve uma pausa honesta e `ts meta` retoma do item salvo.
1830
+ const roundMaxDurationMs = Math.max(60000, Number(opts.roundMaxDurationMs) || 180000);
1831
+ out = recoveryExcerpt
1832
+ ? await anchoredRecoveryEdit({ token, dir, expectedArtifact, expectedOnDisk, recoveryExcerpt, previousItemFacts, lang })
1833
+ : await agent.run(task, { token, lang, yes, autoAll, model: roundModel, plannerModel: st.thinker || 'gpt-5.6-luna', atomicMetaItem: true, recoveryMode: !!previousItemFacts, recoveryHasExcerpt: !!recoveryExcerpt, skipPlanner: !!recoveryExcerpt, maxCredits: roundCreditBudget, maxDurationMs: roundMaxDurationMs, allowedTools: roundAllowedTools, confineDir: dir, skipSessionStart: true, onStep: opts.onStep, onStepDone: opts.onStepDone, askApprove: opts.askApprove, onRemote: opts.onRemote, onThinking: opts.onThinking });
1636
1834
  }
1637
1835
  catch (e) {
1638
1836
  connErr = e;
1837
+ // A microrecuperação tem uma única responsabilidade e uma âncora
1838
+ // local. Se o provedor expirar ou devolver um patch inválido, isso não
1839
+ // pode escapar do Meta e deixar o usuário em "Pensando…" sem estado.
1840
+ // Persistimos a falha objetiva; a próxima rodada pode retomar sem
1841
+ // inventar que a edição ocorreu.
1842
+ if (recoveryExcerpt) {
1843
+ const evidence = String(e && (e.message || e.code) || 'falha desconhecida').replace(/\s+/g, ' ').slice(0, 500);
1844
+ out = {
1845
+ text: `Recuperação por âncora não foi concluída: ${evidence}. Nenhum artefato foi marcado como pronto.`,
1846
+ steps: 0, credits: 0, tokens: { inTok: 0, outTok: 0, cachedTok: 0 }, model: 'deepseek-v4-pro', actions: [],
1847
+ toolErrors: [{ tool: 'recuperacao_por_ancora', class: 'anchored_recovery_failed', retryable: /TIMEOUT|timeout|temporar|network/i.test(evidence), evidence }],
1848
+ completion: { ok: false, reason: 'anchored_recovery_failed' },
1849
+ };
1850
+ break;
1851
+ }
1639
1852
  // teto de IA estourado → PAUSA limpa e resumível com CTA de upgrade (nunca segue em silêncio)
1640
1853
  if (e && e.code === 'no_credits') {
1641
1854
  st.status = 'paused'; st.pause_reason = 'no_credits'; save(st, dir);
@@ -1649,7 +1862,7 @@ async function run(goal, opts = {}) {
1649
1862
  // retries, switch to another tool-capable model before pausing.
1650
1863
  // Change provider family first: if Flash is down, Pro from the same
1651
1864
  // family is likely affected too. GLM is the economical independent fallback.
1652
- const fallbacks = ['glm-5.2', 'deepseek-v4-pro', 'gpt-5.6-luna'];
1865
+ const fallbacks = ['deepseek-v4-pro', 'gpt-5.6-luna'];
1653
1866
  if (!model && att === 0 && !roundModel) {
1654
1867
  st.autoExecutorDegradedUntil = Date.now() + (10 * 60 * 1000);
1655
1868
  save(st, dir);
@@ -1678,6 +1891,25 @@ async function run(goal, opts = {}) {
1678
1891
  }
1679
1892
 
1680
1893
  const expectedNow = String(expectedArtifact || '').replace(/\\/g, '/').toLowerCase();
1894
+ // Uma mutação parcial não é entrega. O agente já produz um veredito
1895
+ // determinístico que considera loop, limite, ausência de ação e provas;
1896
+ // ignorá-lo aqui fazia o Meta marcar um item como concluído apenas porque
1897
+ // algum arquivo foi tocado antes de um comando/teste falhar.
1898
+ if (out.completion && out.completion.ok === false) {
1899
+ item.lastAttempt = {
1900
+ at: new Date().toISOString(),
1901
+ model: String(out.model || roundModel || 'automatico'),
1902
+ steps: Number(out.steps || 0),
1903
+ completion: out.completion.reason || 'unverified',
1904
+ guard: out.guard && out.guard.kind || '',
1905
+ toolErrors: Array.isArray(out.toolErrors) ? out.toolErrors.slice(-6) : [],
1906
+ result: String(out.text || '').slice(0, 1200),
1907
+ };
1908
+ st.status = 'paused'; st.pause_reason = 'execution_unverified';
1909
+ save(st, dir);
1910
+ onAlert({ type: 'verification', text: `A execução não foi comprovada (${out.completion.reason || 'sem veredito válido'}). O item ficou aberto; nenhuma alteração parcial foi tratada como entrega.` });
1911
+ return st;
1912
+ }
1681
1913
  const expectedAfterHash = expectedOnDisk ? artifactHash(expectedOnDisk) : null;
1682
1914
  const expectedChangedNow = !!expectedAfterHash && expectedAfterHash !== expectedBeforeHash;
1683
1915
  const expectedChangedSinceBaseline = !!expectedAfterHash && !!item.artifactBaselineHash
@@ -1691,19 +1923,40 @@ async function run(goal, opts = {}) {
1691
1923
  // several tool steps exploring the project and still fail to create the
1692
1924
  // promised deliverable; treat that exactly like a zero-tool promise.
1693
1925
  if (expectedNow && !wroteExpectedNow) {
1694
- item.attempts = Math.max(0, (item.attempts || 1) - 1);
1695
- item.avoidModels = [...new Set([...(item.avoidModels || []), String(out.model || roundModel || 'automatico')])];
1926
+ // CAPABILITY_DENIED é bloqueio do harness, não incapacidade do modelo.
1927
+ // ignorar o gate explícito de execução, por outro lado, é uma prova
1928
+ // objetiva de que este executor não avançou: a próxima rodada pode usar
1929
+ // o fallback do plano, sem mascarar a falha como "problema do harness".
1930
+ const ignoredExecutionGate = Array.isArray(out.toolErrors)
1931
+ && out.toolErrors.some(e => e && e.class === 'inspection_phase_ignored');
1932
+ const harnessBlocked = /CAPABILITY_DENIED/i.test(JSON.stringify(out || {}))
1933
+ || (!ignoredExecutionGate && /inspection_phase_ignored/i.test(JSON.stringify(out || {})));
1934
+ if (!harnessBlocked) item.avoidModels = [...new Set([...(item.avoidModels || []), String(out.model || roundModel || 'automatico')])];
1696
1935
  const expectedTarget = path.isAbsolute(expectedArtifact) ? expectedArtifact : path.resolve(dir, expectedArtifact);
1697
1936
  // Conteúdo direto é seguro para um artefato NOVO. Para código existente ele poderia
1698
1937
  // substituir o projeto inteiro por uma resposta parcial do item; nesse caso troque o
1699
1938
  // executor e obrigue uma edição real, preservando o arquivo atual.
1700
1939
  item.directArtifact = !fs.existsSync(expectedTarget);
1701
- const failedModel = String(out.model || roundModel || 'glm-5.2');
1702
- if (item.directArtifact) item.directModel = /glm/i.test(failedModel) ? 'glm-5.2' : failedModel;
1940
+ // Conteúdo direto é uma recuperação controlada: mantenha o executor
1941
+ // oficial, nunca transforme um alias/Flash/GLM em política automática.
1942
+ if (item.directArtifact) item.directModel = 'deepseek-v4-pro';
1943
+ // Diagnóstico resumido e persistente: o próximo operador (ou o próprio
1944
+ // harness) consegue saber se faltou ferramenta, prova, tempo ou resposta
1945
+ // do modelo sem depender do console que já fechou.
1946
+ item.lastAttempt = {
1947
+ at: new Date().toISOString(),
1948
+ model: String(out.model || roundModel || 'automatico'),
1949
+ steps: Number(out.steps || 0),
1950
+ completion: out.completion && out.completion.reason || '',
1951
+ guard: out.guard && out.guard.kind || '',
1952
+ toolErrors: Array.isArray(out.toolErrors) ? out.toolErrors.slice(-4) : [],
1953
+ result: String(out.text || '').slice(0, 1200),
1954
+ };
1703
1955
  st.status = 'paused'; st.pause_reason = 'model_no_action';
1704
- st.budget = st.creditsSpent;
1705
1956
  save(st, dir);
1706
- onAlert({ type: 'retry', text: `O modelo ${out.model || roundModel || 'automatico'} não persistiu o artefato esperado. Não marquei o item como concluído; a retomada usará outro executor sem substituir código existente.` });
1957
+ onAlert({ type: 'retry', text: harnessBlocked
1958
+ ? `O harness bloqueou uma operação antes de o modelo persistir o artefato. Não marquei o item como concluído nem culpei o executor; a retomada repetirá o papel oficial após a correção local.`
1959
+ : `O modelo ${out.model || roundModel || 'automatico'} não persistiu o artefato esperado. Não marquei o item como concluído; a retomada usará outro executor sem substituir código existente.` });
1707
1960
  return st;
1708
1961
  }
1709
1962
 
@@ -1774,7 +2027,7 @@ async function run(goal, opts = {}) {
1774
2027
  // narrativa de uma rodada não é evidência suficiente para concluir itens não executados.
1775
2028
  try {
1776
2029
  const evid = written.length ? '\n\nARQUIVOS REALMENTE GRAVADOS nesta rodada (evidência objetiva):\n' + written.map(_basename).join(', ') : '';
1777
- const mk = await _llmJson({ token, system: MARK_SYS,
2030
+ const mk = await _llmJson({ token, model: 'gpt-5.6-luna', system: MARK_SYS,
1778
2031
  user: `OBJETIVO:\n${st.goal.slice(0, 600)}\n\nCHECKLIST (contexto):\n${fmtChecklist(st.checklist)}\n\nITEM ALVO: (${item.id}) ${item.desc}\n\nRESULTADO DA RODADA:\n${String(out.text || '').slice(0, 2000)}${evid}` });
1779
2032
  st.creditsSpent += mk.credits || 0;
1780
2033
  if (mk.json && mk.json.passou === true && wroteExpectedArtifact) marks.push(item.id);
@@ -1788,6 +2041,9 @@ async function run(goal, opts = {}) {
1788
2041
  save(st, dir);
1789
2042
  onRoundDone({ marks, credits: out.credits || 0, checklist: st.checklist, spent: st.creditsSpent });
1790
2043
  }
2044
+ } finally {
2045
+ runLock.release();
2046
+ }
1791
2047
  }
1792
2048
 
1793
2049
  // Notificação no Telegram do dono (best-effort — sem canal, segue em silêncio)
@@ -1839,4 +2095,4 @@ function trailSummary(st, lang) {
1839
2095
  };
1840
2096
  }
1841
2097
 
1842
- module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, toolsForMetaItem, artifactForMetaItem, normalizeMetaArtifact, persistMetaArtifact, directArtifactPlan, canGenerateDirectArtifact, verificationLabel, _checkCriteria, trailSummary };
2098
+ module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, toolsForMetaItem, artifactForMetaItem, recoveryTargetExcerpt, anchoredRecoveryEdit, normalizeMetaArtifact, persistMetaArtifact, directArtifactPlan, canGenerateDirectArtifact, verificationLabel, _checkCriteria, trailSummary, officialExecutorModel, modelWasSubstituted };