@orkastery/cli 0.6.4 → 0.6.6

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.
@@ -2,7 +2,7 @@
2
2
  "id": "orkastery",
3
3
  "name": "Orkastery",
4
4
  "description": "Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI. Toda tool de projeto aceita projeto (nome do projeto pedido); o cwd do gateway nunca escolhe o projeto. O status do roadmap vem de ork_network_roadmap; as maquinas da pessoa, de ork_network_status (sem projeto).",
5
- "version": "0.6.4",
5
+ "version": "0.6.6",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "properties": {},
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orkastery/openclaw-plugin",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Plugin Orkastery para OpenClaw: 32 tools ork_* que traduzem intencao em chamada de CLI do nucleo `ork`. Zero regra de negocio no host.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CONTRATO_FIXACAO = exports.LIMITE_METADADO_BYTES = void 0;
37
+ exports.lerFonteControllerFixada = lerFonteControllerFixada;
38
+ exports.consumirRolloutControllerFixado = consumirRolloutControllerFixado;
37
39
  exports.registrarFonteController = registrarFonteController;
38
40
  exports.lerSnapshotController = lerSnapshotController;
39
41
  /** D1/D2: leitor validado da fonte do controller. Sem RPC, resposta, sinal ou resume. */
@@ -222,7 +224,7 @@ function conferirSessionMeta(fd, tamanho, sessionId, cwd) {
222
224
  * Não há atomicidade contra troca entre lstat e open: a garantia é a reconferência do
223
225
  * descritor aberto e do vínculo com a fonte fixada, com recusa quando a prova não se sustenta.
224
226
  */
225
- function conferirFonteDeRollout(rollout, uid, sessionId, cwd, fixado) {
227
+ function comRolloutConferido(rollout, uid, sessionId, cwd, fixado, consumir) {
226
228
  if (typeof rollout !== 'string' || !rollout || !path.isAbsolute(rollout))
227
229
  recusar('rollout do controller não confirmado');
228
230
  const file = rollout;
@@ -252,12 +254,15 @@ function conferirFonteDeRollout(rollout, uid, sessionId, cwd, fixado) {
252
254
  if (fixado && (st.ino !== fixado.ino || st.dev !== fixado.dev))
253
255
  recusar('rollout do controller rotacionado sem vínculo provado com a fonte fixada');
254
256
  conferirSessionMeta(fd, st.size, sessionId, cwd);
255
- return { file, ino: st.ino, dev: st.dev };
257
+ return { fonte: { file, ino: st.ino, dev: st.dev }, valor: consumir(fd, st.size) };
256
258
  }
257
259
  finally {
258
260
  fs.closeSync(fd);
259
261
  }
260
262
  }
263
+ function conferirFonteDeRollout(rollout, uid, sessionId, cwd, fixado) {
264
+ return comRolloutConferido(rollout, uid, sessionId, cwd, fixado, () => undefined).fonte;
265
+ }
261
266
  /**
262
267
  * Fixacao privada da fonte: escrita uma unica vez, antes de qualquer efeito de observacao,
263
268
  * e reconferida em cada poll e em cada restart do observador. Sem redescoberta de processo.
@@ -303,6 +308,20 @@ function lerFonteFixada(file, esperado, uid) {
303
308
  cwd: esperado.cwd, sessionId: esperado.sessionId, rollout: f.rollout, rolloutIno: f.rolloutIno,
304
309
  rolloutDev: f.rolloutDev, criadoEm: f.criadoEm, processoController: controlador, processoRuntime: runtime, uid };
305
310
  }
311
+ /** Leitura pura da fixação do watcher: não descobre fonte, não grava pin e reconfere o rollout. */
312
+ function lerFonteControllerFixada(file, esperado) {
313
+ const uid = esperado.uid ?? uidAtual();
314
+ const fonte = lerFonteFixada(file, esperado, uid);
315
+ if (!fonte)
316
+ return null;
317
+ conferirDiretorioDaFonte(fonte.dir, esperado.dirSessoes, uid);
318
+ conferirFonteDeRollout(fonte.rollout, uid, fonte.sessionId, fonte.cwd, { file: fonte.rollout, ino: fonte.rolloutIno, dev: fonte.rolloutDev });
319
+ return fonte;
320
+ }
321
+ /** Consome bytes do descritor autenticado do rollout já fixado, sem reabrir pathname não conferido. */
322
+ function consumirRolloutControllerFixado(fonte, consumir) {
323
+ return comRolloutConferido(fonte.rollout, fonte.uid, fonte.sessionId, fonte.cwd, { file: fonte.rollout, ino: fonte.rolloutIno, dev: fonte.rolloutDev }, consumir).valor;
324
+ }
306
325
  /**
307
326
  * Espera humana so suspende o prazo com o tuple nativo completo: thread, turno, `requestId`
308
327
  * inclusive o numero `0`, item e hash iguais aos da pergunta projetada.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ParserCodex = exports.LIMITE_RESPOSTA_CODEX = exports.LIMITE_LINHA_CODEX = void 0;
4
4
  exports.falhaDeContaDoErroCodex = falhaDeContaDoErroCodex;
5
5
  exports.classificarMensagem = classificarMensagem;
6
+ exports.mensagemFinalPedeHumano = mensagemFinalPedeHumano;
6
7
  /** D4/D5: stream e rollout são observações de turno, nunca aprovação de CHECK. */
7
8
  const gates_1 = require("../gates");
8
9
  const claude_bg_1 = require("./claude-bg");
@@ -59,13 +60,21 @@ function classificarMensagem(texto) {
59
60
  const estado = /(?:^|\n)\s*(?:motivo|gate_blocked|bloquead[oa](?:\s+por)?|blocked(?:\s+by)?)\s*[:=]?\s*`?([a-z]+[._][a-z_.-]+)`?/i.exec(final);
60
61
  if (estado)
61
62
  return motivo(estado[1]) ?? falha();
62
- const pergunta = final.replace(/`[^`]*`/g, '').trim();
63
+ return mensagemFinalPedeHumano(final) ? humano() : sucesso();
64
+ }
65
+ /**
66
+ * RM-075: a mensagem final pede uma resposta do humano? A regra é compartilhada pelos
67
+ * rollouts Codex e pela tela final do claude-bg, para `human.pending` não nascer apenas
68
+ * do estado nativo `blocked`.
69
+ */
70
+ function mensagemFinalPedeHumano(texto) {
71
+ if (typeof texto !== 'string')
72
+ return false;
73
+ const pergunta = texto.replace(/`[^`]*`/g, '').trim();
63
74
  if (/(?:preciso|aguardo|aguardando|waiting for|need)\s+(?:da?\s+)?(?:sua|your|confirmação|resposta|autorização|approval|confirmation)/i.test(pergunta))
64
- return humano();
65
- if (/\?\s*$/.test(pergunta) &&
66
- /(?:\b(?:você|voce|you|confirma|confirme|deseja|prefere|autoriza|posso|can you|could you|should I)\b|(?:qual|which|what)\s+(?:é\s+o\s+|is\s+the\s+)?(?:diretório|diretorio|caminho|arquivo|opção|opcao|directory|path|file|option))/i.test(pergunta))
67
- return humano();
68
- return sucesso();
75
+ return true;
76
+ return /\?\s*$/.test(pergunta) &&
77
+ /(?:\b(?:você|voce|you|confirma|confirme|deseja|prefere|autoriza|posso|can you|could you|should I)\b|(?:qual|which|what)\s+(?:é\s+o\s+|is\s+the\s+)?(?:diretório|diretorio|caminho|arquivo|opção|opcao|directory|path|file|option))/i.test(pergunta);
69
78
  }
70
79
  function tokens(v, fonte, escopo) {
71
80
  if (!v || typeof v !== 'object')
@@ -64,6 +64,19 @@ exports.LIMITE_LINHA_GROK = 1024 * 1024;
64
64
  exports.LIMITE_ERRO_GROK = 300;
65
65
  const STOP = /^[a-z][a-z0-9_]{0,39}$/;
66
66
  const ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
67
+ /** A saída estrutural do Bash que o runtime usa quando `dontAsk` nega antes de executar. */
68
+ function negacaoDaPolitica(e) {
69
+ const saida = e.rawOutput;
70
+ if (saida && typeof saida === 'object' && !Array.isArray(saida)) {
71
+ const o = saida;
72
+ if (o.type === 'PermissionDenied' || o.code === 'permission_denied' || o.outcome === 'blocked-by-permissions')
73
+ return true;
74
+ if (typeof o.exit_code === 'number')
75
+ return false;
76
+ return typeof o.output_for_prompt === 'string' && /Denied by permission policy|tool permission denied for/i.test(o.output_for_prompt);
77
+ }
78
+ return typeof saida === 'string' && /Denied by permission policy|tool permission denied for/i.test(saida);
79
+ }
67
80
  /**
68
81
  * D7: um texto do runtime pronto para o ledger, sem corte: os segredos que o núcleo já conhece, a chave da xAI
69
82
  * (`xai-...`) e sem controle. GO-FIX 1 (A4): quem corta, corta depois de redigir, para nenhum segredo perder o prefixo.
@@ -106,7 +119,13 @@ class ParserGrok {
106
119
  if (estado && estado.sessaoEsperada !== sessaoEsperada)
107
120
  throw new Error('estado do parser do grok de outra sessão');
108
121
  this.estado = estado ? structuredClone(estado)
109
- : { sessaoEsperada, fim: null, erro: null, invalido: false, sessaoDivergente: false, eventos: 0 };
122
+ : { sessaoEsperada, fim: null, erro: null, invalido: false, sessaoDivergente: false, eventos: 0,
123
+ comandosTerminal: {}, negacaoTerminal: null };
124
+ // Cursor gravado antes da F5a continua legível.
125
+ if (!this.estado.comandosTerminal || typeof this.estado.comandosTerminal !== 'object')
126
+ this.estado.comandosTerminal = {};
127
+ if (this.estado.negacaoTerminal === undefined)
128
+ this.estado.negacaoTerminal = null;
110
129
  }
111
130
  /** Lê as linhas completas do bloco; o resto fica pendente para o próximo. */
112
131
  push(chunk) {
@@ -158,12 +177,34 @@ class ParserGrok {
158
177
  }
159
178
  this.eventosValidos++;
160
179
  this.estado.eventos++;
180
+ if (e.type === 'tool_call') {
181
+ this.estado.negacaoTerminal = null;
182
+ const entrada = e.rawInput;
183
+ if (typeof e.toolCallId === 'string' && ID.test(e.toolCallId) && (e.toolName === 'bash' || e.toolName === 'run_terminal_cmd') &&
184
+ entrada && typeof entrada === 'object' && !Array.isArray(entrada) && typeof entrada.command === 'string') {
185
+ const comando = redigirErroGrok(entrada.command);
186
+ if (comando)
187
+ this.estado.comandosTerminal[e.toolCallId] = comando;
188
+ }
189
+ return;
190
+ }
191
+ if (e.type === 'tool_call_update') {
192
+ const id = typeof e.toolCallId === 'string' && ID.test(e.toolCallId) ? e.toolCallId : null;
193
+ const comando = id ? this.estado.comandosTerminal[id] : undefined;
194
+ this.estado.negacaoTerminal = comando && negacaoDaPolitica(e) ? { comando } : null;
195
+ if (id && e.status === 'completed')
196
+ delete this.estado.comandosTerminal[id];
197
+ return;
198
+ }
161
199
  if (e.type === 'error') {
200
+ this.estado.negacaoTerminal = null;
162
201
  this.estado.erro = { mensagem: redigirErroGrok(e.message) || 'erro do grok sem mensagem' };
163
202
  return;
164
203
  }
165
- if (e.type !== 'end')
204
+ if (e.type !== 'end') {
205
+ this.estado.negacaoTerminal = null;
166
206
  return;
207
+ }
167
208
  const sessionId = typeof e.sessionId === 'string' && ID.test(e.sessionId) ? e.sessionId : null;
168
209
  if (sessionId !== this.estado.sessaoEsperada)
169
210
  this.estado.sessaoDivergente = true;
@@ -33,7 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.IGNORADOS_DA_CONFIG_DO_FILHO = exports.MARCA_DA_CONFIG_DO_FILHO = exports.ESPERA_PADRAO_MS = exports.ESFORCO_DO_GROK = exports.PERFIL_DE_SANDBOX_PADRAO = exports.PERFIL_DE_SANDBOX_DO_GROK = exports.AMBIENTE_SEM_MCP_DE_OUTROS_CLIENTES = exports.ENVS_REMOVIDAS_DO_DESPACHO = exports.FONTE_VERIFICACAO = exports.NOME_ADAPTER = void 0;
36
+ exports.IGNORADOS_DA_CONFIG_DO_FILHO = exports.MARCA_DA_CONFIG_DO_FILHO = exports.ESPERA_PADRAO_MS = exports.ESFORCO_DO_GROK = exports.COMANDOS_TERMINAL_DO_GROK = exports.PERFIL_DE_SANDBOX_PADRAO = exports.PERFIL_DE_SANDBOX_DO_GROK = exports.AMBIENTE_SEM_MCP_DE_OUTROS_CLIENTES = exports.ENVS_REMOVIDAS_DO_DESPACHO = exports.FONTE_VERIFICACAO = exports.NOME_ADAPTER = void 0;
37
+ exports.regrasDosComandosDoGrok = regrasDosComandosDoGrok;
37
38
  exports.esforcoDoGrok = esforcoDoGrok;
38
39
  exports.ferramentasDoGrok = ferramentasDoGrok;
39
40
  exports.ferramentasServidasAoGrok = ferramentasServidasAoGrok;
@@ -112,6 +113,24 @@ exports.PERFIL_DE_SANDBOX_DO_GROK = Object.freeze({
112
113
  'danger-full-access': 'off',
113
114
  });
114
115
  exports.PERFIL_DE_SANDBOX_PADRAO = 'workspace';
116
+ /**
117
+ * RM-067 (F5a, D3): a lista fechada dos comandos de terminal do ciclo da fase. O motivo mora com o comando para uma
118
+ * inclusão futura nunca virar só mais um curinga sem justificativa. As regras do MCP continuam em `ferramentasDoGrok`.
119
+ */
120
+ exports.COMANDOS_TERMINAL_DO_GROK = Object.freeze([
121
+ { comando: 'git', motivo: 'inspecionar e registrar o estado da worktree pelos fluxos autorizados da fase' },
122
+ { comando: 'node', motivo: 'executar o CLI compilado e os scripts de prova do projeto' },
123
+ { comando: 'npm', motivo: 'instalar, compilar e testar o pacote pelo manifesto' },
124
+ { comando: 'ork', motivo: 'consultar e operar o ciclo da fase quando o transporte nativo estiver disponível' },
125
+ { comando: 'grep', motivo: 'localizar texto e produzir evidência reproduzível' },
126
+ { comando: 'find', motivo: 'descobrir arquivos dentro do sandbox da worktree' },
127
+ { comando: 'ls', motivo: 'inspecionar diretórios dentro do sandbox da worktree' },
128
+ { comando: 'cat', motivo: 'ler arquivos de texto dentro do sandbox da worktree' },
129
+ ]);
130
+ /** A forma oficial do guia 22: a regra exata cobre o comando nu, e a regra com espaço cobre seus argumentos. */
131
+ function regrasDosComandosDoGrok() {
132
+ return exports.COMANDOS_TERMINAL_DO_GROK.flatMap(({ comando }) => [`Bash(${comando})`, `Bash(${comando} *)`]);
133
+ }
115
134
  /**
116
135
  * RM-067 (F4, E6): o esforço do Orkastery nos níveis canônicos do `grok` (`none`, `minimal`, `low`, `medium`, `high`,
117
136
  * `xhigh`, `max`, no guia headless). O `eco` vira `minimal`, para a escada de esforço (`eco` antes de `low`) seguir com
@@ -245,7 +264,10 @@ function comandoDoDespacho(pedido, ferramentas) {
245
264
  args.push('-m', pedido.model);
246
265
  if (pedido.effort?.trim())
247
266
  args.push('--effort', esforcoDoGrok(pedido.effort));
248
- args.push('--sandbox', perfilDeSandbox(pedido.sandbox, pedido.colaboracao), '--no-auto-update');
267
+ // RM-067 (F5a, D3): no headless, o que não casar com uma regra é negado e devolvido ao modelo, sem abrir diálogo.
268
+ args.push('--permission-mode', 'dontAsk', '--sandbox', perfilDeSandbox(pedido.sandbox, pedido.colaboracao), '--no-auto-update');
269
+ for (const regra of regrasDosComandosDoGrok())
270
+ args.push('--allow', regra);
249
271
  // RM-067 (F4b, G2): com o MCP do ork, uma regra por ferramenta liberada, no fim, para o despacho sem contexto ficar igual
250
272
  // e a retomada (que guarda o que vem depois do `-s`) levar as mesmas regras.
251
273
  for (const ferramenta of ferramentas)
@@ -253,8 +275,9 @@ function comandoDoDespacho(pedido, ferramentas) {
253
275
  return ['grok', ...args];
254
276
  }
255
277
  /**
256
- * D2: a linha de comando exata do despacho (usada também pelo `--dry-run`). O prompt fica no argumento 2, que o ledger
257
- * mascara. Nunca `--yolo`, `--always-approve` nem `--permission-mode`.
278
+ * D2 e F5a: a linha de comando exata do despacho (usada também pelo `--dry-run`). O prompt fica no argumento 2, que o
279
+ * ledger mascara. Todo despacho headless usa `--permission-mode dontAsk` com as regras fechadas de D3; nunca `--yolo`,
280
+ * `--always-approve` nem `--permission-mode bypassPermissions`.
258
281
  *
259
282
  * RM-067 (F4, E8): `--trust` confia a pasta do `--cwd`, a worktree da thread, que é o checkout do próprio projeto. O guia
260
283
  * de regras de projeto do `grok` diz que o `AGENTS.md` só carrega na partida com a pasta confiada (`--trust` ou a
@@ -263,7 +286,7 @@ function comandoDoDespacho(pedido, ferramentas) {
263
286
  * `grok` lê; não aprova ferramenta nenhuma.
264
287
  *
265
288
  * RM-067 (F4b, G2): num projeto preparado, o comando leva no fim um `--allow MCPTool(orkastery__<ferramenta>)` por
266
- * ferramenta do MCP do `ork` que o caso libera (`ferramentasDoGrok`), e nenhuma outra regra.
289
+ * ferramenta do MCP do `ork` que o caso libera (`ferramentasDoGrok`), além das regras fechadas de terminal da F5a.
267
290
  */
268
291
  function montarComando(pedido) {
269
292
  return comandoDoDespacho(pedido, ferramentasDoDespacho(pedido, contextoDoDespacho(pedido)));
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.JANELA_DA_SONDA_MS = exports.LIMIAR_STEAL_DO_AGENDADOR_PCT = exports.RESERVA_DO_GRUPO = exports.ENV_CONTAINERS_DA_LANE = exports.ENV_CPUS_DOCKER = exports.ENV_SINAL_DA_RESERVA = exports.ENV_CPUS_DA_LANE_APOS_RESERVA = exports.ENV_CPUS_DA_LANE_APOS_VARREDURA = exports.ENV_CPUS_DA_LANE = void 0;
3
+ exports.AMOSTRAS_DA_SONDA = exports.JANELA_DA_SONDA_MS = exports.LIMIAR_STEAL_DO_AGENDADOR_PCT = exports.MINIMO_ESTRUTURAL_DO_GRUPO = exports.RESERVA_DO_GRUPO = exports.ENV_CONTAINERS_DA_LANE = exports.ENV_CPUS_DOCKER = exports.ENV_SINAL_DA_RESERVA = exports.ENV_CPUS_DA_LANE_APOS_RESERVA = exports.ENV_CPUS_DA_LANE_APOS_VARREDURA = exports.ENV_WORKERS = exports.ENV_CPUS_DA_LANE = void 0;
4
4
  exports.registrarContainerDaLane = registrarContainerDaLane;
5
+ exports.workersDaReserva = workersDaReserva;
5
6
  exports.cargaDoProcLoadavg = cargaDoProcLoadavg;
7
+ exports.mediana = mediana;
8
+ exports.resumirMedicaoCpuDoGrupo = resumirMedicaoCpuDoGrupo;
6
9
  exports.medirCpuDoGrupo = medirCpuDoGrupo;
7
10
  exports.planejarCpuDoGrupo = planejarCpuDoGrupo;
8
11
  exports.paredeClassicaSimulada = paredeClassicaSimulada;
@@ -11,12 +14,16 @@ exports.paredeAgileSimulada = paredeAgileSimulada;
11
14
  * RM-074: o plano de CPU do grupo do Agile Check.
12
15
  *
13
16
  * A lane hermetica deixa duas CPUs para a varredura e para a integracao. Cada conteiner recebe uma cota dessa fatia,
14
- * em vez de enxergar a maquina inteira. Antes de lancar os comandos, o auxiliar do verify mede `/proc/loadavg` e uma
15
- * janela de `/proc/stat`; sob carga ou steal, as pecas voltam a ordem do Classic e usam uma CPU a menos que a maquina.
17
+ * em vez de enxergar a maquina inteira. Antes de lancar os comandos, o auxiliar do verify mede `/proc/loadavg` e
18
+ * varias janelas curtas de `/proc/stat`. A mediana ignora um pico isolado. Steal acima do limiar reduz a capacidade do
19
+ * plano em degraus; a ordem do Classic, com a paralelizacao do Classic, so volta quando nao cabem as tres CPUs
20
+ * estruturais do grupo paralelo ou quando a carga ocupa quase a maquina inteira.
16
21
  */
17
22
  const node_fs_1 = require("node:fs");
18
23
  const steal_1 = require("./steal");
19
24
  exports.ENV_CPUS_DA_LANE = 'ORK_CHECK_CPUS_DA_LANE';
25
+ /** RM-081 F4: contrato publico com a quantidade inteira de workers disponivel ao runner. */
26
+ exports.ENV_WORKERS = 'ORK_WORKERS';
20
27
  exports.ENV_CPUS_DA_LANE_APOS_VARREDURA = 'ORK_CHECK_CPUS_DA_LANE_APOS_VARREDURA';
21
28
  exports.ENV_CPUS_DA_LANE_APOS_RESERVA = 'ORK_CHECK_CPUS_DA_LANE_APOS_RESERVA';
22
29
  exports.ENV_SINAL_DA_RESERVA = 'ORK_CHECK_RESERVA_DEVOLVIDA';
@@ -24,8 +31,11 @@ exports.ENV_CPUS_DOCKER = 'ORK_CHECK_CPUS_DOCKER';
24
31
  /** Registro privado da rodada: quem sobe um container na lane anota o nome para a sonda de CPU. */
25
32
  exports.ENV_CONTAINERS_DA_LANE = 'ORK_CHECK_CONTAINERS_DA_LANE';
26
33
  exports.RESERVA_DO_GRUPO = 2;
34
+ exports.MINIMO_ESTRUTURAL_DO_GRUPO = exports.RESERVA_DO_GRUPO + 1;
35
+ /** Abaixo dele o steal nao desconta capacidade (como na M7); acima, a M8 desconta a fracao medida, sem corte tudo ou nada. */
27
36
  exports.LIMIAR_STEAL_DO_AGENDADOR_PCT = 10;
28
37
  exports.JANELA_DA_SONDA_MS = 250;
38
+ exports.AMOSTRAS_DA_SONDA = 5;
29
39
  /** Registra o container antes do `docker run`; falha de evidencia nunca impede a ferramenta de rodar. */
30
40
  function registrarContainerDaLane(nome, ambiente = process.env) {
31
41
  const arquivo = ambiente[exports.ENV_CONTAINERS_DA_LANE];
@@ -36,7 +46,11 @@ function registrarContainerDaLane(nome, ambiente = process.env) {
36
46
  }
37
47
  catch { /* a medicao fica incompleta e o agendador conserva a reserva */ }
38
48
  }
39
- const cpusValidas = (cpus) => Math.max(1, Math.floor(Number.isFinite(cpus) ? cpus : 1));
49
+ /** Reserva publicada para runners: entrada invalida, fracionaria ou zero nunca produz menos de um worker. */
50
+ function workersDaReserva(cpus) {
51
+ return Math.max(1, Math.floor(Number.isFinite(cpus) ? cpus : 1));
52
+ }
53
+ const cpusValidas = workersDaReserva;
40
54
  /** A carga de um minuto de `/proc/loadavg`; entrada invalida ou ausencia nao inventam saturacao. */
41
55
  function cargaDoProcLoadavg(conteudo) {
42
56
  if (!conteudo)
@@ -52,49 +66,89 @@ function lerProc(arquivo) {
52
66
  return null;
53
67
  }
54
68
  }
69
+ /** Mediana das amostras validas; quantidade par usa o ponto medio. */
70
+ function mediana(amostras) {
71
+ const validas = amostras.filter((x) => Number.isFinite(x) && x >= 0).sort((a, b) => a - b);
72
+ if (validas.length === 0)
73
+ return null;
74
+ const meio = Math.floor(validas.length / 2);
75
+ return validas.length % 2 ? validas[meio] : (validas[meio - 1] + validas[meio]) / 2;
76
+ }
77
+ /** Fecha as amostras da sonda nas duas medianas que alimentam o plano. */
78
+ function resumirMedicaoCpuDoGrupo(amostrasCarga1, amostrasStealPct) {
79
+ const cargas = amostrasCarga1.filter((x) => Number.isFinite(x) && x >= 0);
80
+ const steals = amostrasStealPct.filter((x) => Number.isFinite(x) && x >= 0);
81
+ return { carga1: mediana(cargas), stealPct: mediana(steals), amostrasCarga1: cargas, amostrasStealPct: steals };
82
+ }
55
83
  /**
56
- * Mede a maquina dentro do proprio grupo, antes de qualquer comando: a carga nas duas pontas e o steal da janela.
57
- * A leitura maior da carga impede uma subida durante a sonda de desaparecer na media de um minuto.
84
+ * Mede a maquina dentro do proprio grupo, antes de qualquer comando. A janela total continua curta, mas e dividida em
85
+ * amostras de carga e steal; a mediana deixa um unico pico registrado sem entregar a decisao a ele.
58
86
  */
59
87
  async function medirCpuDoGrupo(janelaMs = exports.JANELA_DA_SONDA_MS) {
60
- const statInicial = (0, steal_1.amostraDoProcStat)(lerProc('/proc/stat'));
61
- const cargaInicial = cargaDoProcLoadavg(lerProc('/proc/loadavg'));
62
- await new Promise((resolve) => setTimeout(resolve, janelaMs));
63
- const cargaFinal = cargaDoProcLoadavg(lerProc('/proc/loadavg'));
64
- const cargas = [cargaInicial, cargaFinal].filter((x) => x !== null);
65
- return {
66
- carga1: cargas.length ? Math.max(...cargas) : null,
67
- stealPct: (0, steal_1.stealDaJanela)(statInicial, (0, steal_1.amostraDoProcStat)(lerProc('/proc/stat'))),
68
- };
88
+ const cargas = [];
89
+ const steals = [];
90
+ let statAnterior = (0, steal_1.amostraDoProcStat)(lerProc('/proc/stat'));
91
+ const intervaloMs = janelaMs / exports.AMOSTRAS_DA_SONDA;
92
+ for (let i = 0; i < exports.AMOSTRAS_DA_SONDA; i++) {
93
+ await new Promise((resolve) => setTimeout(resolve, intervaloMs));
94
+ const carga = cargaDoProcLoadavg(lerProc('/proc/loadavg'));
95
+ if (carga !== null)
96
+ cargas.push(carga);
97
+ const statAtual = (0, steal_1.amostraDoProcStat)(lerProc('/proc/stat'));
98
+ const steal = (0, steal_1.stealDaJanela)(statAnterior, statAtual);
99
+ if (steal !== null)
100
+ steals.push(steal);
101
+ statAnterior = statAtual;
102
+ }
103
+ return resumirMedicaoCpuDoGrupo(cargas, steals);
69
104
  }
70
105
  /** O plano unico que alimenta concorrencia das lanes, varredura e `docker run --cpus`. */
71
106
  function planejarCpuDoGrupo(cpusInformadas, medicao) {
72
107
  const cpus = cpusValidas(cpusInformadas);
108
+ const steal = Math.min(100, Math.max(0, medicao.stealPct ?? 0));
109
+ // So o steal desconta capacidade. A carga de um minuto do /proc/loadavg inclui o build do proprio verify e a rodada
110
+ // anterior; descontada, tiraria CPUs de maquina quieta. A carga segue a regra anterior: ordem do Classic perto da
111
+ // maquina inteira ocupada.
112
+ const descontoDeSteal = steal >= exports.LIMIAR_STEAL_DO_AGENDADOR_PCT ? steal / 100 : 0;
113
+ const capacidadeEfetiva = Math.max(1, Math.min(cpus, Math.round(cpus * (1 - descontoDeSteal))));
73
114
  const classic = Math.max(1, cpus - 1);
74
115
  const motivos = [];
75
- if (cpus < 3)
116
+ if (cpus < exports.MINIMO_ESTRUTURAL_DO_GRUPO)
76
117
  motivos.push(`${cpus} CPU(s), sem duas CPUs livres para o grupo paralelo`);
118
+ else if (capacidadeEfetiva < exports.MINIMO_ESTRUTURAL_DO_GRUPO) {
119
+ motivos.push(`capacidade efetiva ${capacidadeEfetiva}/${cpus} CPU(s), abaixo do minimo estrutural ${exports.MINIMO_ESTRUTURAL_DO_GRUPO}`);
120
+ }
77
121
  if (medicao.carga1 !== null && medicao.carga1 >= classic)
78
122
  motivos.push(`carga ${medicao.carga1} em /proc/loadavg (limiar ${classic})`);
79
- if (medicao.stealPct !== null && medicao.stealPct >= exports.LIMIAR_STEAL_DO_AGENDADOR_PCT) {
80
- motivos.push(`steal ${medicao.stealPct}% em /proc/stat (limiar ${exports.LIMIAR_STEAL_DO_AGENDADOR_PCT}%)`);
81
- }
82
123
  const saturado = motivos.length > 0;
83
- const fatia = saturado ? classic : Math.min(exports.RESERVA_DO_GRUPO, cpus - 1);
124
+ const estado = saturado ? 'fallback' : capacidadeEfetiva < cpus ? 'degradado' : 'agile';
125
+ const fatia = saturado ? classic : Math.min(exports.RESERVA_DO_GRUPO, capacidadeEfetiva - 1);
84
126
  const ferramentas = Math.max(1, Math.floor(fatia));
85
127
  const medida = medicao.cpusIntegracao;
86
- const cpusIntegracao = saturado ? ferramentas : Math.max(1, Math.min(ferramentas, Number.isFinite(medida) && medida > 0 ? Math.ceil(medida) : ferramentas));
87
- const medidaCompativel = medicao.fonteCpusIntegracao === 'medida' && medida !== null && medida !== undefined;
88
- const integracaoEmParalelo = !saturado && medidaCompativel && cpus - fatia - cpusIntegracao >= 1;
128
+ const medidaCompativel = medicao.fonteCpusIntegracao === 'medida' && Number.isFinite(medida) && medida >= 0;
129
+ const cpusIntegracao = saturado ? ferramentas : medidaCompativel
130
+ ? Math.max(1, Math.min(capacidadeEfetiva - 1, Math.ceil(medida)))
131
+ : ferramentas;
132
+ const integracaoEmParalelo = !saturado && medidaCompativel && capacidadeEfetiva - fatia - cpusIntegracao >= 1;
133
+ const amostras = (xs, resumo) => `[${(xs?.length ? xs : resumo === null ? [] : [resumo]).join(', ')}]`;
134
+ const evidencia = `carga em /proc/loadavg: amostras ${amostras(medicao.amostrasCarga1, medicao.carga1)}, mediana ` +
135
+ `${medicao.carga1 ?? 'indisponivel'}; steal em /proc/stat: amostras ${amostras(medicao.amostrasStealPct, medicao.stealPct)}, ` +
136
+ `mediana ${medicao.stealPct === null ? 'indisponivel' : `${medicao.stealPct}%`}`;
137
+ const motivo = saturado
138
+ ? `${motivos.join('; ')}; ${evidencia}`
139
+ : `capacidade ${capacidadeEfetiva}/${cpus} CPU(s)${estado === 'degradado' ? ' depois do desconto do steal' : ' inteira'}; ${evidencia}; ` +
140
+ `${fatia} CPU(s) reservadas a varredura; ` +
141
+ (integracaoEmParalelo ? `integracao medida cabe em paralelo com ${cpusIntegracao} CPU(s)` : 'integracao permanece encadeada');
89
142
  return {
90
143
  cpus,
91
144
  ordem: saturado ? 'classic' : 'paralela',
145
+ estado,
146
+ capacidadeEfetiva,
92
147
  saturado,
93
- motivo: motivos.join('; ') || `carga e steal abaixo dos limiares; ${fatia} CPU(s) reservadas a varredura; ` +
94
- (integracaoEmParalelo ? `integracao medida cabe em paralelo com ${cpusIntegracao} CPU(s)` : 'integracao permanece encadeada'),
95
- cpusHermetica: saturado ? classic : Math.max(1, cpus - fatia - (integracaoEmParalelo ? cpusIntegracao : 0)),
96
- cpusHermeticaAposVarredura: saturado ? classic : Math.max(1, cpus - cpusIntegracao),
97
- cpusHermeticaAposReserva: saturado ? classic : cpus,
148
+ motivo,
149
+ cpusHermetica: saturado ? classic : Math.max(1, capacidadeEfetiva - fatia - (integracaoEmParalelo ? cpusIntegracao : 0)),
150
+ cpusHermeticaAposVarredura: saturado ? classic : Math.max(1, capacidadeEfetiva - cpusIntegracao),
151
+ cpusHermeticaAposReserva: saturado ? classic : capacidadeEfetiva,
98
152
  cpusIntegracao,
99
153
  fonteCpusIntegracao: saturado ? 'classic' : medidaCompativel
100
154
  ? 'medida' : 'conservadora',
@@ -388,6 +388,15 @@ function caiuDeVerdade(r) {
388
388
  return !r.ok && r.executado !== false && (r.causa === undefined || r.causa === 'exit') && Number.isInteger(r.code)
389
389
  && r.code >= 1 && r.code <= MAIOR_CODIGO_DA_LANE && !(r.relogioSobSteal?.length);
390
390
  }
391
+ /** O agregado so afirma contagens quando todas as lanes trouxeram JUnit valido. */
392
+ function junitDasLanes(lanes) {
393
+ if (!lanes.length || lanes.some((l) => l.resultado.junit?.estado !== 'lido'))
394
+ return undefined;
395
+ const relatorios = lanes.map((l) => l.resultado.junit);
396
+ const somar = (campo) => relatorios.reduce((total, r) => total + (r[campo] ?? 0), 0);
397
+ return { caminho: relatorios.map((r) => r.caminho).join(';'), estado: 'lido', testes: somar('testes'),
398
+ falhas: somar('falhas'), erros: somar('erros'), pulados: somar('pulados'), duracaoMs: somar('duracaoMs') };
399
+ }
391
400
  /**
392
401
  * O `test` das lanes (D4): ok so com todas ok; a evidencia de quem caiu; a parede e a da lane mais longa. A falha de
393
402
  * verdade de uma lane manda, como num comando so do Classic: o estouro ou o relogio sob steal de outra lane nao a esconde.
@@ -414,9 +423,22 @@ function testDasLanes(lanes) {
414
423
  const causas = lanes.map((l) => l.resultado.causa).filter((c) => !!c);
415
424
  const causa = deVerdade.length ? deVerdade[0].resultado.causa ?? 'exit' : PIOR_CAUSA.find((c) => causas.includes(c));
416
425
  const soRelogio = caidas.length > 0 && caidas.every((l) => (l.resultado.relogioSobSteal?.length ?? 0) > 0);
417
- const testes = juntar((r) => r.testeQueCaiu).slice(0, 10);
426
+ const nomesExtraidos = juntar((r) => r.testeQueCaiu);
427
+ const testes = lanes.some((l) => l.resultado.junit?.estado === 'lido') ? nomesExtraidos : nomesExtraidos.slice(0, 10);
418
428
  const falhas = juntar((r) => r.falhasDeTeste);
429
+ const parciais = lanes.filter((l) => !!l.resultado.parcial);
430
+ const filtrados = juntar((r) => r.parcial?.testes);
431
+ const junit = junitDasLanes(lanes);
419
432
  const relogio = soRelogio ? juntar((r) => r.relogioSobSteal) : [];
433
+ const retestes = lanes.map((l) => l.resultado.retesteDeRelogio).filter((r) => !!r);
434
+ const estadoDoReteste = ['reprovou', 'sem-veredito', 'sem-filtro', 'passou']
435
+ .find((estado) => retestes.some((r) => r.estado === estado));
436
+ const reteste = estadoDoReteste ? {
437
+ testes: [...new Set(retestes.flatMap((r) => r.testes))],
438
+ estado: estadoDoReteste,
439
+ ...(retestes.find((r) => r.estado === estadoDoReteste)?.code !== undefined
440
+ ? { code: retestes.find((r) => r.estado === estadoDoReteste).code } : {}),
441
+ } : undefined;
420
442
  const trechos = caidas.map((l) => l.resultado.trecho).filter((t) => !!t);
421
443
  return {
422
444
  nome: 'test',
@@ -430,12 +452,22 @@ function testDasLanes(lanes) {
430
452
  executado: !tempoComFalha && lanes.every((l) => l.resultado.executado !== false),
431
453
  ...(testes.length ? { testeQueCaiu: testes } : {}),
432
454
  ...(falhas.length ? { falhasDeTeste: falhas } : {}),
455
+ ...(junit ? { junit } : {}),
456
+ ...(parciais.length ? { parcial: { testes: [...new Set(filtrados)] } } : {}),
433
457
  ...(trechos.length ? { trecho: trechos.join('\n').slice(0, 1024) } : {}),
434
458
  ...(lanes.some((l) => l.resultado.stealPct !== undefined) ? { stealPct: lanes.find((l) => l.resultado.stealPct !== undefined).resultado.stealPct } : {}),
435
459
  ...(relogio.length ? { relogioSobSteal: relogio } : {}),
460
+ ...(reteste ? { retesteDeRelogio: reteste } : {}),
436
461
  lanes: lanes.map((l) => ({ lane: l.lane, comando: l.resultado.comando, ok: l.resultado.ok, code: l.resultado.code,
437
462
  ...(l.resultado.causa ? { causa: l.resultado.causa } : {}), duracaoMs: l.resultado.duracaoMs ?? 0,
438
- ...(l.resultado.testeQueCaiu?.length ? { testeQueCaiu: l.resultado.testeQueCaiu } : {}) })),
463
+ ...(l.resultado.testeQueCaiu?.length ? { testeQueCaiu: l.resultado.testeQueCaiu } : {}),
464
+ ...(l.resultado.junit ? { junit: l.resultado.junit } : {}),
465
+ ...(l.resultado.parcial ? { parcial: l.resultado.parcial } : {}),
466
+ ...(l.resultado.comandoExecutado ? { comandoExecutado: l.resultado.comandoExecutado } : {}),
467
+ ...(l.resultado.selecao ? { selecao: l.resultado.selecao } : {}),
468
+ ...(l.resultado.relogioSobSteal?.length ? { relogioSobSteal: l.resultado.relogioSobSteal } : {}),
469
+ ...(l.resultado.retesteDeRelogio ? { retesteDeRelogio: l.resultado.retesteDeRelogio } : {}),
470
+ ...(l.resultado.stealPct !== undefined ? { stealPct: l.resultado.stealPct } : {}) })),
439
471
  };
440
472
  }
441
473
  /**
@@ -466,7 +498,7 @@ function porQueDasLanes(c) {
466
498
  * Os comandos do manifesto no Agile (D4): `build` reaproveitado do preparo com a guarda valendo; o resto num grupo
467
499
  * paralelo, com as lanes no lugar do `verify.test`, so com a guarda valendo; sem ela, tudo em serie (o Classic).
468
500
  */
469
- function comandosNoAgile(manifesto, cwd, lista, executor, grupo, guarda, preparo) {
501
+ function comandosNoAgile(manifesto, cwd, lista, executor, grupo, guarda, preparo, selecao = { comandos: {}, lanes: {} }, passagemParcial, selecaoPorMudanca = {}) {
470
502
  const pecas = [];
471
503
  const passosDoPreparo = manifesto.verify.preparo ? passosDoComando(manifesto.verify.preparo) : [];
472
504
  const lanesDeclaradas = Object.entries(manifesto.check?.agile?.test ?? {});
@@ -480,7 +512,12 @@ function comandosNoAgile(manifesto, cwd, lista, executor, grupo, guarda, preparo
480
512
  pecas.push({ peca: 'grupo', estado: 'fallback', motivo, fallback: 'comandos em serie' });
481
513
  if (lanesDeclaradas.length > 0)
482
514
  pecas.push({ peca: 'lanes', estado: 'fallback', motivo, fallback: 'verify.test' });
483
- return { comandos: lista.map((c) => executor(c.nome, c.comando, cwd)), pecas, paralelo: false, lanes: [] };
515
+ const executar = (c) => {
516
+ const parcial = c.filtro && selecao.comandos[c.nome]
517
+ ? passagemParcial?.(c.nome, c.comando, cwd, c.filtro, selecao.comandos[c.nome]) : null;
518
+ return parcial && !parcial.ok ? parcial : executor(c.nome, c.comando, cwd);
519
+ };
520
+ return { comandos: lista.map(executar), pecas, paralelo: false, lanes: [] };
484
521
  }
485
522
  // Com a guarda: o build primeiro (do preparo, quando e so passos dele), e o resto junto.
486
523
  const build = lista.find((c) => c.nome === 'build');
@@ -499,17 +536,55 @@ function comandosNoAgile(manifesto, cwd, lista, executor, grupo, guarda, preparo
499
536
  for (const c of resto) {
500
537
  if (c.nome === 'test' && lanesDeclaradas.length > 0) {
501
538
  for (const [lane, comando] of lanesDeclaradas) {
539
+ const junit = manifesto.check?.agile?.junit?.[lane];
540
+ const filtro = manifesto.check?.agile?.filtro?.[lane];
541
+ const recursos = manifesto.check?.agile?.recursos?.[lane];
542
+ const testesFiltrados = selecao.lanes[lane];
543
+ const porMudanca = selecaoPorMudanca[lane];
544
+ const efetivo = porMudanca?.comando ?? comando;
545
+ const dadosDaSelecao = porMudanca ? { comandoIntegral: comando, selecaoPorMudanca: porMudanca } : {};
502
546
  if (depoisDaVarredura.has(lane))
503
- encadeados.push({ nome: `test:${lane}`, comando, apos: ['security_scan'], papelCpu: 'integracao' });
547
+ encadeados.push({ nome: `test:${lane}`, comando: efetivo, ...dadosDaSelecao,
548
+ ...(junit ? { junit } : {}), ...(filtro ? { filtro } : {}), ...(testesFiltrados ? { testesFiltrados } : {}),
549
+ ...(recursos?.length ? { recursos } : {}),
550
+ apos: ['security_scan'], papelCpu: 'integracao' });
504
551
  else
505
- itens.push({ nome: `test:${lane}`, comando, papelCpu: lane === 'hermetica' ? 'hermetica' : 'outro' });
552
+ itens.push({ nome: `test:${lane}`, comando: efetivo, ...dadosDaSelecao, ...(junit ? { junit } : {}),
553
+ ...(filtro ? { filtro } : {}), ...(testesFiltrados ? { testesFiltrados } : {}),
554
+ ...(recursos?.length ? { recursos } : {}),
555
+ papelCpu: lane === 'hermetica' ? 'hermetica' : 'outro' });
506
556
  }
507
557
  }
508
558
  else
509
- itens.push({ nome: c.nome, comando: c.comando, papelCpu: c.nome === 'security_scan' ? 'varredura' : 'outro' });
559
+ itens.push({ nome: c.nome, comando: c.comando, ...(c.junit ? { junit: c.junit } : {}),
560
+ ...(c.filtro ? { filtro: c.filtro } : {}), ...(selecao.comandos[c.nome] ? { testesFiltrados: selecao.comandos[c.nome] } : {}),
561
+ papelCpu: c.nome === 'security_scan' ? 'varredura' : 'outro' });
510
562
  }
511
563
  itens.push(...encadeados);
512
- const resultados = itens.length > 1 ? grupo(itens, cwd) : itens.map((i) => executor(i.nome, i.comando, cwd));
564
+ const parciaisVermelhas = new Map();
565
+ for (const item of itens) {
566
+ if (!item.filtro || !item.testesFiltrados?.length || !passagemParcial)
567
+ continue;
568
+ const parcial = passagemParcial(item.nome, item.comando, cwd, item.filtro, item.testesFiltrados);
569
+ if (parcial && !parcial.ok)
570
+ parciaisVermelhas.set(item.nome, parcial);
571
+ }
572
+ const inteiros = itens.filter((i) => !parciaisVermelhas.has(i.nome));
573
+ const resultadosInteiros = inteiros.length > 1 ? grupo(inteiros, cwd) : inteiros.map((i) => executor(i.nome, i.comando, cwd));
574
+ const porItemInteiro = new Map(inteiros.map((item, i) => [item.nome, resultadosInteiros[i]]));
575
+ const resultados = itens.map((item) => {
576
+ const parcial = parciaisVermelhas.get(item.nome);
577
+ if (parcial)
578
+ return parcial;
579
+ const resultado = porItemInteiro.get(item.nome);
580
+ if (!item.selecaoPorMudanca || !item.comandoIntegral)
581
+ return resultado;
582
+ const { comando: _comando, ...registro } = item.selecaoPorMudanca;
583
+ return item.selecaoPorMudanca.modo === 'selecionada'
584
+ ? { ...resultado, comando: item.comandoIntegral, comandoExecutado: item.comando,
585
+ parcial: { testes: [] }, selecao: registro }
586
+ : { ...resultado, comando: item.comandoIntegral, selecao: registro };
587
+ });
513
588
  pecas.push(...(grupo.pecas ?? []));
514
589
  const lanes = [];
515
590
  itens.forEach((item, i) => {
@@ -590,7 +665,8 @@ exports.RelatorioDoCheck = RelatorioDoCheck;
590
665
  */
591
666
  function pecasDoRelatorio(linhas) {
592
667
  const vistas = new Set();
593
- return linhas.filter((l) => l.tipo === 'peca' && typeof l.peca === 'string' && (l.estado === 'agile' || l.estado === 'fallback'))
668
+ return linhas.filter((l) => l.tipo === 'peca' && typeof l.peca === 'string'
669
+ && (l.estado === 'agile' || l.estado === 'degradado' || l.estado === 'fallback'))
594
670
  .map((l) => ({ peca: l.peca, estado: l.estado,
595
671
  ...(typeof l.motivo === 'string' ? { motivo: l.motivo } : {}), ...(typeof l.fallback === 'string' ? { fallback: l.fallback } : {}) }))
596
672
  .filter((p) => { const chave = JSON.stringify(p); if (vistas.has(chave))