@orkastery/cli 0.6.3 → 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.
@@ -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.3",
5
+ "version": "0.6.5",
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.3",
3
+ "version": "0.6.5",
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,7 +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_CPUS_DOCKER = exports.ENV_SINAL_DA_RESERVA = exports.ENV_CPUS_DA_LANE_APOS_RESERVA = 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
+ exports.registrarContainerDaLane = registrarContainerDaLane;
5
+ exports.workersDaReserva = workersDaReserva;
4
6
  exports.cargaDoProcLoadavg = cargaDoProcLoadavg;
7
+ exports.mediana = mediana;
8
+ exports.resumirMedicaoCpuDoGrupo = resumirMedicaoCpuDoGrupo;
5
9
  exports.medirCpuDoGrupo = medirCpuDoGrupo;
6
10
  exports.planejarCpuDoGrupo = planejarCpuDoGrupo;
7
11
  exports.paredeClassicaSimulada = paredeClassicaSimulada;
@@ -10,19 +14,43 @@ exports.paredeAgileSimulada = paredeAgileSimulada;
10
14
  * RM-074: o plano de CPU do grupo do Agile Check.
11
15
  *
12
16
  * A lane hermetica deixa duas CPUs para a varredura e para a integracao. Cada conteiner recebe uma cota dessa fatia,
13
- * em vez de enxergar a maquina inteira. Antes de lancar os comandos, o auxiliar do verify mede `/proc/loadavg` e uma
14
- * 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.
15
21
  */
16
22
  const node_fs_1 = require("node:fs");
17
23
  const steal_1 = require("./steal");
18
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';
27
+ exports.ENV_CPUS_DA_LANE_APOS_VARREDURA = 'ORK_CHECK_CPUS_DA_LANE_APOS_VARREDURA';
19
28
  exports.ENV_CPUS_DA_LANE_APOS_RESERVA = 'ORK_CHECK_CPUS_DA_LANE_APOS_RESERVA';
20
29
  exports.ENV_SINAL_DA_RESERVA = 'ORK_CHECK_RESERVA_DEVOLVIDA';
21
30
  exports.ENV_CPUS_DOCKER = 'ORK_CHECK_CPUS_DOCKER';
31
+ /** Registro privado da rodada: quem sobe um container na lane anota o nome para a sonda de CPU. */
32
+ exports.ENV_CONTAINERS_DA_LANE = 'ORK_CHECK_CONTAINERS_DA_LANE';
22
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. */
23
36
  exports.LIMIAR_STEAL_DO_AGENDADOR_PCT = 10;
24
37
  exports.JANELA_DA_SONDA_MS = 250;
25
- const cpusValidas = (cpus) => Math.max(1, Math.floor(Number.isFinite(cpus) ? cpus : 1));
38
+ exports.AMOSTRAS_DA_SONDA = 5;
39
+ /** Registra o container antes do `docker run`; falha de evidencia nunca impede a ferramenta de rodar. */
40
+ function registrarContainerDaLane(nome, ambiente = process.env) {
41
+ const arquivo = ambiente[exports.ENV_CONTAINERS_DA_LANE];
42
+ if (!arquivo || !nome)
43
+ return;
44
+ try {
45
+ (0, node_fs_1.appendFileSync)(arquivo, JSON.stringify({ nome, em: new Date().toISOString() }) + '\n', { mode: 0o600 });
46
+ }
47
+ catch { /* a medicao fica incompleta e o agendador conserva a reserva */ }
48
+ }
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;
26
54
  /** A carga de um minuto de `/proc/loadavg`; entrada invalida ou ausencia nao inventam saturacao. */
27
55
  function cargaDoProcLoadavg(conteudo) {
28
56
  if (!conteudo)
@@ -38,44 +66,93 @@ function lerProc(arquivo) {
38
66
  return null;
39
67
  }
40
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
+ }
41
83
  /**
42
- * Mede a maquina dentro do proprio grupo, antes de qualquer comando: a carga nas duas pontas e o steal da janela.
43
- * 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.
44
86
  */
45
87
  async function medirCpuDoGrupo(janelaMs = exports.JANELA_DA_SONDA_MS) {
46
- const statInicial = (0, steal_1.amostraDoProcStat)(lerProc('/proc/stat'));
47
- const cargaInicial = cargaDoProcLoadavg(lerProc('/proc/loadavg'));
48
- await new Promise((resolve) => setTimeout(resolve, janelaMs));
49
- const cargaFinal = cargaDoProcLoadavg(lerProc('/proc/loadavg'));
50
- const cargas = [cargaInicial, cargaFinal].filter((x) => x !== null);
51
- return {
52
- carga1: cargas.length ? Math.max(...cargas) : null,
53
- stealPct: (0, steal_1.stealDaJanela)(statInicial, (0, steal_1.amostraDoProcStat)(lerProc('/proc/stat'))),
54
- };
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);
55
104
  }
56
105
  /** O plano unico que alimenta concorrencia das lanes, varredura e `docker run --cpus`. */
57
106
  function planejarCpuDoGrupo(cpusInformadas, medicao) {
58
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))));
59
114
  const classic = Math.max(1, cpus - 1);
60
115
  const motivos = [];
61
- if (cpus < 3)
116
+ if (cpus < exports.MINIMO_ESTRUTURAL_DO_GRUPO)
62
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
+ }
63
121
  if (medicao.carga1 !== null && medicao.carga1 >= classic)
64
122
  motivos.push(`carga ${medicao.carga1} em /proc/loadavg (limiar ${classic})`);
65
- if (medicao.stealPct !== null && medicao.stealPct >= exports.LIMIAR_STEAL_DO_AGENDADOR_PCT) {
66
- motivos.push(`steal ${medicao.stealPct}% em /proc/stat (limiar ${exports.LIMIAR_STEAL_DO_AGENDADOR_PCT}%)`);
67
- }
68
123
  const saturado = motivos.length > 0;
69
- 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);
70
126
  const ferramentas = Math.max(1, Math.floor(fatia));
127
+ const medida = medicao.cpusIntegracao;
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');
71
142
  return {
72
143
  cpus,
73
144
  ordem: saturado ? 'classic' : 'paralela',
145
+ estado,
146
+ capacidadeEfetiva,
74
147
  saturado,
75
- motivo: motivos.join('; ') || `carga e steal abaixo dos limiares; ${fatia} CPU(s) reservadas ao grupo`,
76
- cpusHermetica: saturado ? classic : Math.max(1, cpus - fatia),
77
- cpusHermeticaAposReserva: saturado ? classic : cpus,
78
- cpusIntegracao: ferramentas,
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,
152
+ cpusIntegracao,
153
+ fonteCpusIntegracao: saturado ? 'classic' : medidaCompativel
154
+ ? 'medida' : 'conservadora',
155
+ integracaoEmParalelo,
79
156
  cpusDaFatia: fatia,
80
157
  ferramentasDaVarredura: ferramentas,
81
158
  cpusPorContainer: Math.max(0.01, Math.floor((fatia / ferramentas) * 100) / 100),
@@ -91,10 +168,23 @@ function paredeAgileSimulada(plano, trabalho) {
91
168
  return trabalho.hermetica / plano.cpusHermetica + trabalho.varredura / plano.cpusDaFatia
92
169
  + trabalho.integracao / plano.cpusIntegracao;
93
170
  }
94
- const paredeDaReserva = trabalho.varredura / plano.cpusDaFatia + trabalho.integracao / plano.cpusIntegracao;
95
- const feitoAntesDaDevolucao = Math.min(trabalho.hermetica, paredeDaReserva * plano.cpusHermetica);
96
- const paredeDaHermetica = feitoAntesDaDevolucao === trabalho.hermetica
97
- ? trabalho.hermetica / plano.cpusHermetica
98
- : paredeDaReserva + (trabalho.hermetica - feitoAntesDaDevolucao) / plano.cpusHermeticaAposReserva;
99
- return Math.max(paredeDaHermetica, paredeDaReserva);
171
+ const paredeVarredura = trabalho.varredura / plano.cpusDaFatia;
172
+ const paredeIntegracao = trabalho.integracao / plano.cpusIntegracao;
173
+ if (plano.integracaoEmParalelo) {
174
+ let instante = 0, restante = trabalho.hermetica, capacidade = plano.cpusHermetica;
175
+ for (const evento of [{ em: paredeVarredura, libera: plano.cpusDaFatia },
176
+ { em: paredeIntegracao, libera: plano.cpusIntegracao }].sort((a, b) => a.em - b.em)) {
177
+ restante = Math.max(0, restante - (evento.em - instante) * capacidade);
178
+ instante = evento.em;
179
+ capacidade += evento.libera;
180
+ }
181
+ return Math.max(paredeVarredura, paredeIntegracao, instante + restante / capacidade);
182
+ }
183
+ const feitoNaVarredura = Math.min(trabalho.hermetica, paredeVarredura * plano.cpusHermetica);
184
+ const restanteAposVarredura = trabalho.hermetica - feitoNaVarredura;
185
+ const feitoNaIntegracao = Math.min(restanteAposVarredura, paredeIntegracao * plano.cpusHermeticaAposVarredura);
186
+ const restanteFinal = restanteAposVarredura - feitoNaIntegracao;
187
+ const paredeDaHermetica = (feitoNaVarredura ? paredeVarredura : 0)
188
+ + (feitoNaIntegracao ? paredeIntegracao : 0) + restanteFinal / plano.cpusHermeticaAposReserva;
189
+ return Math.max(paredeDaHermetica, paredeVarredura + paredeIntegracao);
100
190
  }
@@ -47,10 +47,10 @@ const estado_thread_1 = require("./estado-thread");
47
47
  const catalogo_1 = require("./catalogo");
48
48
  const linha = (e) => JSON.stringify(e) + '\n';
49
49
  function esperar(teste) {
50
- const limite = Date.now() + 5000;
50
+ const limite = Date.now() + 15000;
51
51
  while (!teste()) {
52
52
  if (Date.now() >= limite)
53
- throw new Error('subprocesso do cenário não terminou em 5000 ms');
53
+ throw new Error('subprocesso do cenário não terminou no prazo de transporte');
54
54
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
55
55
  }
56
56
  }
@@ -71,7 +71,8 @@ exports.CENARIO_SENSORES = {
71
71
  const hook = path.join(catalogo, 'adapters/claude-code/hooks/ork-sensor.js');
72
72
  const cli = (0, catalogo_1.cliDoCatalogo)(catalogo);
73
73
  const start = Date.now();
74
- const h = (0, node_child_process_1.spawnSync)(process.execPath, [hook], { cwd: wt, encoding: 'utf8', timeout: 5000,
74
+ // O teto de transporte absorve a fila do Classic; a fixture continua exigindo a latência funcional abaixo de 5 s.
75
+ const h = (0, node_child_process_1.spawnSync)(process.execPath, [hook], { cwd: wt, encoding: 'utf8', timeout: 15000,
75
76
  input: JSON.stringify({ cwd: wt, session_id: 'fixture-claude', hook_event_name: 'PermissionRequest', tool_use_id: 'fixture-tool' }),
76
77
  env: { ...process.env, ORK_SENSOR_CLI: cli } });
77
78
  if (h.status !== 0 || h.stderr || h.stdout)
@@ -98,7 +99,7 @@ exports.CENARIO_SENSORES = {
98
99
  }
99
100
  // A identidade vem de um processo realmente executado e encerrado, sem recibo.
100
101
  const runner = require.resolve('./adapters/codex-runner');
101
- const morto = (0, node_child_process_1.spawnSync)(process.execPath, ['-e', `console.log(JSON.stringify(require(process.argv[1]).identidadeProcesso(process.pid)))`, runner], { cwd: wt, encoding: 'utf8', timeout: 5000 });
102
+ const morto = (0, node_child_process_1.spawnSync)(process.execPath, ['-e', `console.log(JSON.stringify(require(process.argv[1]).identidadeProcesso(process.pid)))`, runner], { cwd: wt, encoding: 'utf8', timeout: 15000 });
102
103
  if (morto.status !== 0)
103
104
  throw new Error('processo de morte do cenário indisponível');
104
105
  const filho = JSON.parse(morto.stdout);