@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/adapters/openclaw/openclaw.plugin.json +1 -1
- package/adapters/openclaw/package.json +1 -1
- package/dist/adapters/codex-controller-sensor.js +21 -2
- package/dist/adapters/codex-events.js +15 -6
- package/dist/adapters/grok-events.js +43 -2
- package/dist/adapters/grok.js +28 -5
- package/dist/agendador-cpu.js +82 -28
- package/dist/check-agil.js +85 -9
- package/dist/check-paralelo.js +77 -15
- package/dist/check-selecao.js +149 -0
- package/dist/codex-task-complete.js +95 -0
- package/dist/junit.js +546 -0
- package/dist/locale-verify.js +37 -0
- package/dist/manifest.js +237 -13
- package/dist/retry.js +35 -0
- package/dist/session-watcher-claude.js +38 -22
- package/dist/session-watcher-grok.js +10 -0
- package/dist/session-watcher.js +47 -18
- package/dist/ship.js +9 -3
- package/dist/verify.js +215 -52
- package/package.json +1 -1
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
|
-
|
|
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
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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:
|
|
883
|
-
test:
|
|
1107
|
+
build: comandosVerify.build.comando,
|
|
1108
|
+
test: comandosVerify.test.comando,
|
|
884
1109
|
...prazosDoVerify(verify, erros),
|
|
885
|
-
typecheck:
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
|
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,
|
|
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',
|