@orkastery/cli 0.6.4 → 0.6.5

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/dist/manifest.js CHANGED
@@ -257,6 +257,197 @@ function lerCheck(v, erros, avisos) {
257
257
  }
258
258
  /** Nome de lane: o que vira `test:<lane>` no ledger. */
259
259
  const NOME_DE_LANE = /^[a-z0-9][a-z0-9-]{0,31}$/;
260
+ /** RM-081 F1: o relatorio e sempre um caminho POSIX relativo, nunca uma busca ou um caminho da maquina. */
261
+ function caminhoJUnit(chave, v, erros) {
262
+ if (typeof v !== 'string' || !v.trim()) {
263
+ erros.push(`${chave} precisa ser um caminho relativo, em texto`);
264
+ return undefined;
265
+ }
266
+ const bruto = v.trim();
267
+ const normal = path.posix.normalize(bruto);
268
+ if (bruto.includes('\\') || bruto.includes('\0') || path.posix.isAbsolute(bruto) || path.win32.isAbsolute(bruto)
269
+ || normal === '.' || normal === '..' || normal.startsWith('../')) {
270
+ erros.push(`${chave}: ${exibir(v)} precisa ser um caminho relativo dentro da worktree`);
271
+ return undefined;
272
+ }
273
+ return normal;
274
+ }
275
+ /** RM-081 F3: o runner recebe a lista somente no marcador fechado, uma vez. */
276
+ function filtroDoComando(chave, v, erros) {
277
+ if (v === undefined || v === null)
278
+ return undefined;
279
+ if (typeof v !== 'string' || !v.trim()) {
280
+ erros.push(`${chave} precisa ser um comando em texto com o marcador {testes}`);
281
+ return undefined;
282
+ }
283
+ const filtro = v.trim();
284
+ const marcadores = filtro.match(/\{testes\}/g)?.length ?? 0;
285
+ if (marcadores !== 1) {
286
+ erros.push(`${chave} precisa ter o marcador {testes} exatamente uma vez`);
287
+ return undefined;
288
+ }
289
+ return filtro;
290
+ }
291
+ /** RM-081 F5: caminho ou glob POSIX relativo, sem sair da worktree nem alcançar estado do nucleo. */
292
+ function caminhoDaSelecao(chave, v, erros) {
293
+ if (typeof v !== 'string' || !v.trim()) {
294
+ erros.push(`${chave} precisa ser um caminho ou glob relativo, em texto`);
295
+ return undefined;
296
+ }
297
+ const bruto = v.trim().replace(/\/+$/, '');
298
+ const normal = path.posix.normalize(bruto);
299
+ if (!bruto || bruto.includes('\\') || bruto.includes('\0') || path.posix.isAbsolute(bruto)
300
+ || path.win32.isAbsolute(bruto) || normal === '.' || normal === '..' || normal.startsWith('../')
301
+ || /^(\.orkastery|\.git)(\/|$)/.test(normal)) {
302
+ erros.push(`${chave}: ${exibir(v)} precisa ficar dentro da worktree, fora de .orkastery e .git`);
303
+ return undefined;
304
+ }
305
+ return normal;
306
+ }
307
+ /** RM-081 F5: o manifesto declara o mecanismo e a superficie que o runner sabe relacionar. */
308
+ function selecaoDoComando(chave, v, erros) {
309
+ if (v === undefined || v === null)
310
+ return undefined;
311
+ if (typeof v !== 'object' || Array.isArray(v)) {
312
+ erros.push(`${chave} precisa ser um mapa com comando, mapeia e configuracao opcional`);
313
+ return undefined;
314
+ }
315
+ for (const nome of Object.keys(v)) {
316
+ if (!['comando', 'mapeia', 'configuracao'].includes(nome)) {
317
+ erros.push(`${chave}.${nome} desconhecida; use somente comando, mapeia e configuracao`);
318
+ }
319
+ }
320
+ const comando = typeof v.comando === 'string' ? v.comando.trim() : '';
321
+ const bases = comando.match(/\{base\}/g)?.length ?? 0;
322
+ const arquivos = comando.match(/\{arquivos\}/g)?.length ?? 0;
323
+ if (!comando || bases + arquivos !== 1) {
324
+ erros.push(`${chave}.comando precisa ter exatamente um marcador {base} ou {arquivos}`);
325
+ }
326
+ const lista = (nome) => {
327
+ const valor = v[nome];
328
+ if (valor === undefined || valor === null)
329
+ return [];
330
+ const itens = Array.isArray(valor) ? valor : [valor];
331
+ return itens.flatMap((item, indice) => {
332
+ const lido = caminhoDaSelecao(`${chave}.${nome}[${indice}]`, item, erros);
333
+ return lido ? [lido] : [];
334
+ });
335
+ };
336
+ const mapeia = lista('mapeia');
337
+ const configuracao = lista('configuracao');
338
+ return comando && bases + arquivos === 1
339
+ ? { comando, mapeia, ...(configuracao.length ? { configuracao } : {}) }
340
+ : undefined;
341
+ }
342
+ /** RM-081 F2: uma unica estrategia, fechada e validada antes de qualquer teste rodar. */
343
+ function relogioDoComando(chave, v, erros) {
344
+ if (v === undefined || v === null)
345
+ return undefined;
346
+ if (typeof v !== 'object' || Array.isArray(v)) {
347
+ erros.push(`${chave} precisa ser um mapa com exatamente uma de nome, marcador ou tag`);
348
+ return undefined;
349
+ }
350
+ const conhecidas = ['nome', 'marcador', 'tag'];
351
+ for (const nome of Object.keys(v)) {
352
+ if (!conhecidas.includes(nome))
353
+ erros.push(`${chave}.${nome} desconhecida; use somente nome, marcador ou tag`);
354
+ }
355
+ const presentes = conhecidas.filter((nome) => v[nome] !== undefined && v[nome] !== null);
356
+ if (presentes.length !== 1) {
357
+ erros.push(`${chave} precisa declarar exatamente uma de nome, marcador ou tag`);
358
+ return undefined;
359
+ }
360
+ const estrategia = presentes[0];
361
+ if (estrategia === 'nome') {
362
+ const valores = Array.isArray(v.nome) ? v.nome : [v.nome];
363
+ const padroes = valores.flatMap((item) => typeof item === 'string' && item.trim() ? [item.trim()] : []);
364
+ if (padroes.length !== valores.length || padroes.length === 0 || padroes.length > 20) {
365
+ erros.push(`${chave}.nome precisa ter de 1 a 20 expressoes regulares em texto`);
366
+ return undefined;
367
+ }
368
+ for (const padrao of padroes) {
369
+ try {
370
+ void new RegExp(padrao);
371
+ }
372
+ catch {
373
+ erros.push(`${chave}.nome contem expressao regular invalida: ${exibir(padrao)}`);
374
+ return undefined;
375
+ }
376
+ }
377
+ return { nome: padroes };
378
+ }
379
+ const valor = v[estrategia];
380
+ if (typeof valor !== 'string' || !valor.trim()) {
381
+ erros.push(`${chave}.${estrategia} precisa ser texto nao vazio`);
382
+ return undefined;
383
+ }
384
+ return estrategia === 'marcador' ? { marcador: valor.trim() } : { tag: valor.trim() };
385
+ }
386
+ /** RM-081 F4: recursos da lane sao nomes locais a rodada, unicos e de uma linha. */
387
+ function recursosDoComando(chave, v, erros) {
388
+ if (v === undefined)
389
+ return undefined;
390
+ if (!Array.isArray(v) || v.length === 0) {
391
+ erros.push(`${chave} precisa ser uma lista nao vazia de textos`);
392
+ return undefined;
393
+ }
394
+ const lidos = [];
395
+ let valido = true;
396
+ for (const recurso of v) {
397
+ if (typeof recurso !== 'string' || !recurso.trim() || recurso.trim().length > 80 || /[\r\n]/.test(recurso)) {
398
+ erros.push(`${chave}: ${exibir(recurso)} precisa ser texto de uma linha, de 1 a 80 caracteres`);
399
+ valido = false;
400
+ }
401
+ else if (lidos.includes(recurso.trim())) {
402
+ erros.push(`${chave}: ${exibir(recurso.trim())} esta repetido`);
403
+ valido = false;
404
+ }
405
+ else
406
+ lidos.push(recurso.trim());
407
+ }
408
+ return valido ? lidos : undefined;
409
+ }
410
+ /** Texto continua sendo o contrato legado; o objeto exige comando e aceita os opt-ins da superficie. */
411
+ function lerComando(chave, v, erros, aceitaSelecao = false, aceitaRecursos = false) {
412
+ if (v === undefined || v === null)
413
+ return {};
414
+ if (typeof v === 'string')
415
+ return { comando: v };
416
+ if (typeof v !== 'object' || Array.isArray(v)) {
417
+ erros.push(`${chave} precisa ser o comando em texto ou um mapa com comando (recebido ${exibir(v)})`);
418
+ return {};
419
+ }
420
+ const conhecidas = ['comando', 'junit', 'filtro', 'relogio', ...(aceitaSelecao ? ['selecao'] : []),
421
+ ...(aceitaRecursos ? ['recursos'] : [])];
422
+ for (const nome of Object.keys(v)) {
423
+ if (!conhecidas.includes(nome)) {
424
+ erros.push(`${chave}.${nome} desconhecida; use somente ${conhecidas.join(', ')}`);
425
+ }
426
+ }
427
+ const comando = typeof v.comando === 'string' && v.comando.trim() ? v.comando.trim() : undefined;
428
+ if (!comando)
429
+ erros.push(`${chave}.comando precisa ser texto nao vazio`);
430
+ const junit = v.junit === undefined ? undefined : caminhoJUnit(`${chave}.junit`, v.junit, erros);
431
+ const dispensaJUnit = aceitaRecursos && v.recursos !== undefined && v.recursos !== null;
432
+ if (v.junit === undefined) {
433
+ const dependencias = ['filtro', 'relogio']
434
+ .filter((dependencia) => v[dependencia] !== undefined && v[dependencia] !== null);
435
+ for (const dependencia of dependencias) {
436
+ if (v[dependencia] !== undefined && v[dependencia] !== null) {
437
+ erros.push(`${chave}.junit precisa ser declarado porque ${chave}.${dependencia} depende do relatorio JUnit`);
438
+ }
439
+ }
440
+ if (!dependencias.length && !dispensaJUnit) {
441
+ erros.push(`${chave}.junit precisa ser declarado no objeto de comando`);
442
+ }
443
+ }
444
+ const filtro = filtroDoComando(`${chave}.filtro`, v.filtro, erros);
445
+ const relogio = relogioDoComando(`${chave}.relogio`, v.relogio, erros);
446
+ const selecao = aceitaSelecao ? selecaoDoComando(`${chave}.selecao`, v.selecao, erros) : undefined;
447
+ const recursos = aceitaRecursos ? recursosDoComando(`${chave}.recursos`, v.recursos, erros) : undefined;
448
+ return { ...(comando ? { comando } : {}), ...(junit ? { junit } : {}), ...(filtro ? { filtro } : {}),
449
+ ...(relogio ? { relogio } : {}), ...(selecao ? { selecao } : {}), ...(recursos ? { recursos } : {}) };
450
+ }
260
451
  /**
261
452
  * RM-072 (D3): o bloco `check.agile`. Cada chave liga uma peca do Agile; tipo errado e erro do manifesto, porque uma
262
453
  * peca mal escrita que sumisse em silencio mudaria o que o CHECK roda. `produto` so aceita caminho relativo dentro da
@@ -302,16 +493,42 @@ function lerAgile(v, erros, avisos) {
302
493
  }
303
494
  else {
304
495
  const lanes = {};
305
- for (const [lane, comando] of Object.entries(v.test)) {
496
+ const junit = {};
497
+ const filtro = {};
498
+ const relogio = {};
499
+ const selecao = {};
500
+ const recursos = {};
501
+ for (const [lane, valor] of Object.entries(v.test)) {
306
502
  if (!NOME_DE_LANE.test(lane))
307
503
  erros.push(`check.agile.test.${lane}: nome de lane aceita [a-z0-9-], ate 32 caracteres`);
308
- else if (typeof comando !== 'string' || !comando.trim())
309
- erros.push(`check.agile.test.${lane} precisa ser o comando, em texto`);
310
- else
311
- lanes[lane] = comando.trim();
504
+ else {
505
+ const lido = lerComando(`check.agile.test.${lane}`, valor, erros, true, true);
506
+ if (lido.comando)
507
+ lanes[lane] = lido.comando.trim();
508
+ if (lido.junit)
509
+ junit[lane] = lido.junit;
510
+ if (lido.filtro)
511
+ filtro[lane] = lido.filtro;
512
+ if (lido.relogio)
513
+ relogio[lane] = lido.relogio;
514
+ if (lido.selecao)
515
+ selecao[lane] = lido.selecao;
516
+ if (lido.recursos)
517
+ recursos[lane] = lido.recursos;
518
+ }
312
519
  }
313
520
  if (Object.keys(lanes).length)
314
521
  agile.test = lanes;
522
+ if (Object.keys(junit).length)
523
+ agile.junit = junit;
524
+ if (Object.keys(filtro).length)
525
+ agile.filtro = filtro;
526
+ if (Object.keys(relogio).length)
527
+ agile.relogio = relogio;
528
+ if (Object.keys(selecao).length)
529
+ agile.selecao = selecao;
530
+ if (Object.keys(recursos).length)
531
+ agile.recursos = recursos;
315
532
  }
316
533
  }
317
534
  if (v.apos_a_varredura !== undefined && v.apos_a_varredura !== null) {
@@ -666,6 +883,14 @@ function carregarManifesto(dirInicial = diretorioDoProjeto()) {
666
883
  '(esperado nome de branch do git, sem "-" no começo e sem "..")');
667
884
  }
668
885
  const verify = mapa(dados.verify);
886
+ const comandosVerify = Object.fromEntries(['build', 'test', 'typecheck', 'security_scan']
887
+ .map((nome) => [nome, lerComando(`verify.${nome}`, verify[nome], erros)]));
888
+ const junitVerify = Object.fromEntries(Object.entries(comandosVerify)
889
+ .flatMap(([nome, lido]) => lido.junit ? [[nome, lido.junit]] : []));
890
+ const filtroVerify = Object.fromEntries(Object.entries(comandosVerify)
891
+ .flatMap(([nome, lido]) => lido.filtro ? [[nome, lido.filtro]] : []));
892
+ const relogioVerify = Object.fromEntries(Object.entries(comandosVerify)
893
+ .flatMap(([nome, lido]) => lido.relogio ? [[nome, lido.relogio]] : []));
669
894
  const ci = mapa(dados.ci);
670
895
  const concurrency = mapa(dados.concurrency);
671
896
  const handoff = mapa(dados.handoff);
@@ -879,15 +1104,14 @@ function carregarManifesto(dirInicial = diretorioDoProjeto()) {
879
1104
  ...dependenciasDaWorktree(worktree.dependencias, erros),
880
1105
  },
881
1106
  verify: {
882
- build: verify.build === null || verify.build === undefined ? undefined : texto(verify.build, ''),
883
- test: verify.test === null || verify.test === undefined ? undefined : texto(verify.test, ''),
1107
+ build: comandosVerify.build.comando,
1108
+ test: comandosVerify.test.comando,
884
1109
  ...prazosDoVerify(verify, erros),
885
- typecheck: verify.typecheck === null || verify.typecheck === undefined
886
- ? undefined
887
- : texto(verify.typecheck, ''),
888
- security_scan: verify.security_scan === null || verify.security_scan === undefined
889
- ? undefined
890
- : texto(verify.security_scan, ''),
1110
+ typecheck: comandosVerify.typecheck.comando,
1111
+ security_scan: comandosVerify.security_scan.comando,
1112
+ ...(Object.keys(junitVerify).length ? { junit: junitVerify } : {}),
1113
+ ...(Object.keys(filtroVerify).length ? { filtro: filtroVerify } : {}),
1114
+ ...(Object.keys(relogioVerify).length ? { relogio: relogioVerify } : {}),
891
1115
  // I-54 (RM-037, D10): o preparo unico da rodada de verify, quando declarado.
892
1116
  ...(typeof verify.preparo === 'string' && verify.preparo.trim() ? { preparo: verify.preparo.trim() } : {}),
893
1117
  },
package/dist/retry.js CHANGED
@@ -117,6 +117,8 @@ const postura_local_1 = require("./postura-local");
117
117
  const locale_1 = require("./locale");
118
118
  const locale_retry_1 = require("./locale-retry");
119
119
  const retomada_checkpoint_1 = require("./retomada-checkpoint");
120
+ const codex_task_complete_1 = require("./codex-task-complete");
121
+ const session_events_1 = require("./session-events");
120
122
  /** RM-061 (F2b, D3): `porque` e `correcao` de cada motivo tem a fonte no catalogo pt-BR; o `--json` nao muda. */
121
123
  const TEXTOS_DA_POLITICA = locale_retry_1.RETRY_PT_BR.politica;
122
124
  /**
@@ -385,6 +387,31 @@ function blocoPausa(thread, fase) {
385
387
  function estadoTerminalDeRetry(estado) {
386
388
  return ['completed', 'failed', 'stopped', 'cancelled', 'interrupted', 'terminated'].includes(estado.trim().toLowerCase());
387
389
  }
390
+ /** Prova estreita: somente o rollout fixado da sessão Codex que originou o gate da fase. */
391
+ function faseJaTerminouPorTaskComplete(raiz, thread, fase, evento) {
392
+ const candidata = [...thread.sessoes].reverse().find(s => s.runtime === 'codex' && s.fase === fase &&
393
+ typeof s.despachadaEm === 'string' && (typeof evento?.sessionId !== 'string' || s.sessionId === evento.sessionId) &&
394
+ (typeof evento?.despachoEm !== 'string' || s.despachadaEm === evento.despachoEm));
395
+ if (!candidata)
396
+ return false;
397
+ try {
398
+ const prova = (0, codex_task_complete_1.taskCompleteFixadoDaSessao)(raiz, thread, (0, session_events_1.exigirSessaoDespachada)(candidata, candidata.sessionId), (0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, thread.id)));
399
+ // task_complete encerra o turno, mas pode carregar uma falha estruturada de runtime.
400
+ // Só o resultado que concluiu a fase impede nova tentativa; cota/auth/modelo mantêm
401
+ // a política tipada de recuperação já existente.
402
+ return prova?.terminal.classificacao === 'fase_concluida';
403
+ }
404
+ catch {
405
+ return false;
406
+ }
407
+ }
408
+ function planoProtegidoPorTaskComplete(thread, fase, limite, effort) {
409
+ return { thread: thread.id, modo: thread.modo, fase, motivo: null,
410
+ detalhe: 'rollout autenticado da sessão despachada já contém task_complete', politica: null,
411
+ acao: 'sem-retry', tentativas: 0, limite, effort, effortAnterior: effort, automatica: false,
412
+ bloqueio: 'gate.sem-reprovacao',
413
+ razao: 'rollout autenticado já contém task_complete; a fase terminou e não será redespachada' };
414
+ }
388
415
  /**
389
416
  * Calcula o plano de retry SEM executar nada.
390
417
  *
@@ -417,6 +444,8 @@ function planejarRetry(carregado, threadId, opcoes = {}) {
417
444
  };
418
445
  }
419
446
  const fase = opcoes.fase ?? (evento ? faseDoEvento(evento, thread) : thread.faseAtual);
447
+ if (faseJaTerminouPorTaskComplete(raiz, thread, fase, evento))
448
+ return planoProtegidoPorTaskComplete(thread, fase, limite, effortBase);
420
449
  const politica = politicaDoMotivo(motivo);
421
450
  const tentativas = tentativasFeitas(raiz, threadId, fase, motivo);
422
451
  const detalhe = evento && typeof evento.detalhe === 'string' ? evento.detalhe : '';
@@ -1429,6 +1458,12 @@ function executarRetry(carregado, threadId, opcoes = {}) {
1429
1458
  }
1430
1459
  }
1431
1460
  const thread = (0, thread_1.lerThread)(raiz, threadId);
1461
+ // D4: reconfirma imediatamente antes do primeiro efeito do retry; o plano anterior pode ter
1462
+ // sido calculado enquanto o runtime ainda anexava o terminal ao rollout.
1463
+ if (plano.motivo !== null && plano.fase &&
1464
+ faseJaTerminouPorTaskComplete(raiz, thread, plano.fase, ultimaReprovacao(raiz, threadId))) {
1465
+ plano = planoProtegidoPorTaskComplete(thread, plano.fase, plano.limite, plano.effortAnterior);
1466
+ }
1432
1467
  const dir = (0, thread_1.dirThread)(raiz, threadId);
1433
1468
  const vazio = {
1434
1469
  plano,
@@ -70,6 +70,7 @@ const fs = __importStar(require("node:fs"));
70
70
  const path = __importStar(require("node:path"));
71
71
  const node_crypto_1 = require("node:crypto");
72
72
  const claude_bg_1 = require("./adapters/claude-bg");
73
+ const codex_events_1 = require("./adapters/codex-events");
73
74
  const ratelimit_1 = require("./ratelimit");
74
75
  const runtime_profiles_1 = require("./runtime-profiles");
75
76
  const codex_runner_1 = require("./adapters/codex-runner");
@@ -181,27 +182,19 @@ function classificarSessaoClaude(e) {
181
182
  const bloqueio = (fonte, motivo = 'runtime.unavailable', conclusaoNativa = false) => ({ terminal: true, classificacao: 'gate_blocked', motivo, fonte, conclusaoNativa, stop });
182
183
  // D12: o `done` classifica o texto final do agente; a fase só conclui com a prova do `ork`.
183
184
  const concluir = (sinal, conclusaoNativa) => {
185
+ // RM-075: indisponibilidade da tela não fabrica espera humana. Só uma pergunta
186
+ // efetivamente lida pode produzir human.pending.
187
+ const perguntaReal = e.perguntaReal?.() ?? false;
188
+ if (perguntaReal)
189
+ return { ...bloqueio(`${sinal}; a mensagem final pergunta ao humano`, 'human.pending', conclusaoNativa),
190
+ perguntaReal, dependeDoStop: true, diagnostico: 'a mensagem final pede uma resposta humana' };
184
191
  const prova = e.prova();
185
192
  const c = !prova.ok
186
193
  ? { ...bloqueio(`${sinal}; sem prova do ork: ${prova.fonte}`, prova.motivo ?? 'human.pending', conclusaoNativa),
187
194
  diagnostico: `sem prova do ork: ${prova.fonte}` }
188
195
  : e.pausaAoFim ? bloqueio(`${sinal}; ${prova.fonte}; pausa prevista ao fim do bloco`, 'human.pending', conclusaoNativa)
189
196
  : { terminal: true, classificacao: 'fase_concluida', motivo: null, fonte: `${sinal}; ${prova.fonte}`, conclusaoNativa, stop };
190
- return { ...c, dependeDoStop: true };
191
- };
192
- // RM-037 (defeitosdeco D-1): turno encerrado pelo Stop e sessão viva em `blocked` é o Claude Code
193
- // dizendo que o texto final pediu resposta. Nunca conclui direto, e a pausa humana abre sempre
194
- // (`human.pending`): GO-FIX (R6 do CHECK), sem a prova o motivo também é o humano, porque um
195
- // `artifact.missing` automático abriria correção sobre uma sessão viva que espera resposta.
196
- const esperaHumana = () => {
197
- const sinal = 'Stop correlacionado e sessão viva à espera humana (blocked)';
198
- const prova = e.prova();
199
- const c = !prova.ok
200
- ? { ...bloqueio(`${sinal}; sem prova do ork: ${prova.fonte}; o humano decide`, 'human.pending'),
201
- diagnostico: `a sessão espera resposta humana (blocked) e a fase ainda não tem a prova do ork: ${prova.fonte}` }
202
- : { ...bloqueio(`${sinal}; ${prova.fonte}; o humano decide`, 'human.pending'),
203
- diagnostico: 'a fase tem a prova do ork e a sessão espera resposta humana (blocked): o humano decide' };
204
- return { ...c, dependeDoStop: true };
197
+ return { ...c, perguntaReal, dependeDoStop: true };
205
198
  };
206
199
  const venceu = (desde) => agora - desde >= e.limiteMs;
207
200
  // Sem prova para concluir nem para bloquear: `runtime.unavailable` reexecutaria o mesmo prompt
@@ -276,7 +269,7 @@ function classificarSessaoClaude(e) {
276
269
  cursor.bloqueadoDesde ??= agora;
277
270
  if (agora - cursor.bloqueadoDesde < exports.INTERVALO_WATCH_CLAUDE_MS)
278
271
  return espera('turno encerrado pelo Stop com a sessão à espera humana (blocked); aguardando a segunda observação');
279
- return esperaHumana();
272
+ return concluir('Stop correlacionado e sessão blocked sem pergunta de permissão pendente', false);
280
273
  }
281
274
  delete cursor.bloqueadoDesde;
282
275
  // RM-037 (defeito 4 de 19 e 20/10/2026): turno encerrado pelo Stop e a sessão ociosa no prompt (`status: idle`). O
@@ -570,18 +563,18 @@ function commitConferido(cwd, sha, headNoDespacho) {
570
563
  */
571
564
  function provaDoOrk(raiz, threadId, sessao, eventos, fonte) {
572
565
  const intervalo = eventosDoIntervalo(eventos, sessao);
573
- const falta = (texto, extra = {}) => ({ ok: false, fonte: texto, motivo: sessao.fase === 'PLAN' ? 'artifact.missing' : 'human.pending', ...extra });
566
+ const falta = (texto, extra = {}) => ({ ok: false, fonte: texto, ...extra });
574
567
  const saida = saidaDoArtefato(raiz, threadId, sessao, fonte, eventos);
575
568
  if (saida) {
576
569
  if (!saida.produzido)
577
- return falta(`${saida.arquivo} não foi gravado no intervalo do despacho`);
570
+ return falta(`${saida.arquivo} não foi gravado no intervalo do despacho`, { motivo: 'artifact.missing' });
578
571
  if (sessao.fase !== 'CHECK')
579
572
  return { ok: true, fonte: `${saida.arquivo} gravado no intervalo do despacho` };
580
573
  const v = intervalo.filter(e => e.tipo === 'verify_run' && typeof e.commit === 'string').at(-1);
581
574
  const verify = v ? { eventId: String(v.eventId), commit: String(v.commit), ts: v.ts } : null;
582
575
  const veredito = saida.texto === null ? null : vereditoDoParecer(saida.texto);
583
576
  if (!veredito)
584
- return falta(`${saida.arquivo} gravado sem exatamente um veredito legível`, { veredito: null, verify });
577
+ return falta(`${saida.arquivo} gravado sem exatamente um veredito legível`, { motivo: 'artifact.missing', veredito: null, verify });
585
578
  return { ok: true, fonte: `${saida.arquivo} gravado com veredito ${veredito}`, veredito, verify };
586
579
  }
587
580
  if (sessao.fase === 'GO') {
@@ -589,9 +582,22 @@ function provaDoOrk(raiz, threadId, sessao, eventos, fonte) {
589
582
  const registrados = [...new Set(intervalo.filter(e => (e.tipo === 'mcp_git_committed' ||
590
583
  (e.tipo === 'commit' && e.sessionId === sessao.sessionId)) &&
591
584
  typeof e.commit === 'string' && SHA_COMMIT.test(e.commit)).map(e => String(e.commit)))].slice(-20);
592
- if (!registrados.length)
593
- return falta('nenhum commit registrado pelo núcleo no intervalo do despacho', { commits: [] });
594
585
  const cwd = fonte?.cwd, headNoDespacho = fonte?.head;
586
+ if (!registrados.length) {
587
+ if (!cwd || typeof headNoDespacho !== 'string')
588
+ return falta('HEAD da worktree no despacho não registrado; GO sem mudança não conferível', { commits: [] });
589
+ if ((0, procedencia_1.cwdLocalOuNulo)(raiz, threadId, cwd, 'ledger') === null)
590
+ return falta('cwd do despacho fora das worktrees registradas; GO sem mudança não conferível', { commits: [] });
591
+ if (headDaWorktree(cwd) !== headNoDespacho)
592
+ return falta('HEAD da worktree mudou sem commit registrado pelo núcleo', { commits: [], worktreeLimpa: false });
593
+ const status = (0, util_1.exec)('git', ['status', '--porcelain'], cwd, 30000);
594
+ if (!status.ok)
595
+ return falta('git status da worktree do despacho falhou', { commits: [], worktreeLimpa: false });
596
+ if (status.stdout.trim() !== '')
597
+ return falta('worktree com alterações não commitadas', { commits: [], worktreeLimpa: false });
598
+ return { ok: true, fonte: 'GO sem mudança: nenhum commit, HEAD igual ao despacho e worktree limpa',
599
+ commits: [], worktreeLimpa: true, resultado: 'go.sem-mudanca' };
600
+ }
595
601
  if (!cwd || typeof headNoDespacho !== 'string')
596
602
  return falta('HEAD da worktree no despacho não registrado; commit não conferível', { commits: [], recusados: registrados });
597
603
  // RM-047 (P2): o git da prova só roda na worktree registrada; o cwd vem do ledger.
@@ -733,9 +739,17 @@ function observarClaudeSobLock(ctx) {
733
739
  // e ao fecho.
734
740
  let erroLido;
735
741
  const erroDaTranscricao = () => erroLido !== undefined ? erroLido : (erroLido = ultimoErroDeApi(perfil, fonte.cwd, sessao.sessionId));
742
+ let perguntaLida;
743
+ const perguntaReal = () => {
744
+ if (perguntaLida !== undefined)
745
+ return perguntaLida;
746
+ const tela = (ctx.tela ?? (() => (0, claude_bg_1.logsDaSessao)(registro?.id ?? sessao.sessionId.slice(0, 8), 60, (0, claude_bg_1.ambienteDoPerfil)(perfil))))();
747
+ perguntaLida = tela.ok && (0, codex_events_1.mensagemFinalPedeHumano)(tela.texto);
748
+ return perguntaLida;
749
+ };
736
750
  const { c, falha } = comFalhaDeConta(classificarSessaoClaude({ sessao, eventos, consulta, registro, cursor, agoraMs: ctx.agoraMs,
737
751
  limiteMs: ctx.limiteMs, pausaAoFim: (0, thread_1.pausaNaThread)(thread, sessao.fase),
738
- prova: () => prova ??= provaDoOrk(raiz, thread.id, sessao, eventos, fonte), processo: processoNativo }), () => falhaDoErro(erroDaTranscricao(), ctx.agoraMs));
752
+ prova: () => prova ??= provaDoOrk(raiz, thread.id, sessao, eventos, fonte), processo: processoNativo, perguntaReal }), () => falhaDoErro(erroDaTranscricao(), ctx.agoraMs));
739
753
  (0, codex_runner_1.gravarAtomico)(arquivoCursor, cursor);
740
754
  // GO-FIX 3 (achado 1): cursor gravado numa thread que sumiu durante a consulta é desfeito aqui;
741
755
  // as pastas criadas pela observação, pelo `session-watcher`.
@@ -787,6 +801,8 @@ function observarClaudeSobLock(ctx) {
787
801
  evidencia: { fonte: 'claude agents --json --all', consultadoEm: consulta.consultadoEm, registro: evidenciaDoRegistro(registro) },
788
802
  ...(artefato ? { artefato: { arquivo: artefato.arquivo, sha256Base: artefato.sha256Base, sha256Atual: artefato.sha256 } } : {}),
789
803
  ...(prova ? { provaOrk: prova } : {}),
804
+ ...(prova?.resultado ? { resultado: prova.resultado } : {}),
805
+ ...(c.perguntaReal !== undefined ? { perguntaReal: c.perguntaReal } : {}),
790
806
  conclusaoNativa: c.conclusaoNativa, conclusaoNativaAusente: c.conclusaoNativa ? null : c.fonte,
791
807
  ...(perfil ? { perfil } : {}),
792
808
  ...(falha ? { falhaDeConta: { motivo: falha.motivo, resetEm: falha.resetEm, fonte: falha.fonte, trecho: falha.trecho } } : {}),
@@ -144,8 +144,14 @@ function classificarSessaoGrok(e) {
144
144
  const bloqueio = (motivo, fonte, extra = {}) => ({ terminal: true, classificacao: 'gate_blocked', motivo, fonte, conclusaoNativa: false, diagnostico: fonte, ...extra });
145
145
  const interrupcao = (codigo, sinal, fonte) => bloqueio('human.pending', fonte, { interrupcao: { codigo, sinal, stopReason: estado.fim?.stopReason ?? null },
146
146
  retomar: true, diagnostico: `${fonte}; ${RETOMADA}` });
147
+ const porNegacao = () => {
148
+ const comando = estado.negacaoTerminal.comando;
149
+ return bloqueio('policy.violation', `comando de terminal negado pela política fechada: ${comando}`, { comandoNegado: comando, diagnostico: `a sessão terminou imediatamente depois de negar o comando: ${comando}` });
150
+ };
147
151
  const pelaFim = (comRecibo) => {
148
152
  const fim = estado.fim;
153
+ if (estado.negacaoTerminal)
154
+ return porNegacao();
149
155
  if (fim.stopReason === grok_events_1.STOP_CANCELADO)
150
156
  return interrupcao(recibo?.exitCode ?? null, recibo?.signal ?? null, 'end com stopReason cancelled');
151
157
  if (fim.stopReason !== grok_events_1.STOP_FIM_NORMAL) {
@@ -180,6 +186,8 @@ function classificarSessaoGrok(e) {
180
186
  }
181
187
  if (recibo.erro)
182
188
  return bloqueio('runtime.unavailable', `o supervisor não iniciou o grok (${recibo.erro})`);
189
+ if (estado.negacaoTerminal && valido)
190
+ return porNegacao();
183
191
  if (recibo.exitCode === 130 || recibo.exitCode === 143 || recibo.signal === 'SIGINT' || recibo.signal === 'SIGTERM') {
184
192
  return interrupcao(recibo.exitCode, recibo.signal, `processo interrompido (${recibo.signal ?? `saida ${recibo.exitCode}`}) no recibo do supervisor`);
185
193
  }
@@ -406,6 +414,8 @@ function observarGrokSobLock(ctx) {
406
414
  ...(c.erroDoRuntime ? { erroDoRuntime: c.erroDoRuntime } : {}),
407
415
  ...(c.falhaDeConta ? { falhaDeConta: { motivo: c.falhaDeConta.motivo, resetEm: c.falhaDeConta.resetEm,
408
416
  fonte: c.falhaDeConta.fonte, trecho: c.falhaDeConta.trecho } } : {}),
417
+ ...(c.comandoNegado ? { comandoNegado: c.comandoNegado,
418
+ evidencia: `comando negado pela lista fechada do Grok: ${c.comandoNegado}` } : {}),
409
419
  // D18 da I-34: o diagnóstico vai ao pedido HITL e, como `detalhe` do `gate_blocked`, ao monitor e ao pulse.
410
420
  diagnostico: c.diagnostico, ...(detalhe ? { detalhe } : {}),
411
421
  observadoEm: new Date(agoraMs).toISOString(), gate: 'phase.dispatch', origem: 'sessions.watch',