@orkastery/cli 0.6.1 → 0.6.2

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.
Files changed (69) hide show
  1. package/adapters/claude-code/agents/ork-check.md +1 -1
  2. package/adapters/claude-code/agents/ork-go.md +1 -1
  3. package/adapters/claude-code/agents/ork-goal.md +1 -1
  4. package/adapters/claude-code/agents/ork-master.md +1 -1
  5. package/adapters/claude-code/agents/ork-plan.md +1 -1
  6. package/adapters/claude-code/agents/ork-ship.md +1 -1
  7. package/adapters/claude-code/commands/ork.md +1 -1
  8. package/adapters/claude-code/mods/ork-torre/hooks/frases.ts +2 -0
  9. package/adapters/codex/skills/ork/SKILL.md +1 -1
  10. package/adapters/openclaw/openclaw.plugin.json +1 -1
  11. package/adapters/openclaw/package.json +1 -1
  12. package/assets/adapters-en/claude-code/agents/ork-check.md +1 -1
  13. package/assets/adapters-en/claude-code/agents/ork-go.md +1 -1
  14. package/assets/adapters-en/claude-code/agents/ork-goal.md +1 -1
  15. package/assets/adapters-en/claude-code/agents/ork-master.md +1 -1
  16. package/assets/adapters-en/claude-code/agents/ork-plan.md +1 -1
  17. package/assets/adapters-en/claude-code/agents/ork-ship.md +1 -1
  18. package/assets/adapters-en/claude-code/commands/ork.md +1 -1
  19. package/assets/adapters-en/codex/skills/ork/SKILL.md +1 -1
  20. package/assets/orkmind_fixture.py +4 -1
  21. package/assets/skills-en/core/orkastery-bootstrap/SKILL.md +1 -1
  22. package/assets/skills-en/phases/check-quality/SKILL.md +1 -1
  23. package/assets/skills-en/phases/go-implementation/SKILL.md +1 -1
  24. package/dist/adapters/claude-bg.js +3 -1
  25. package/dist/adapters/grok-mcp-servidor.js +22 -12
  26. package/dist/adapters/grok.js +1 -1
  27. package/dist/changelog-check.js +217 -0
  28. package/dist/check-agil.js +241 -5
  29. package/dist/ci.js +3 -2
  30. package/dist/claim-lint.js +29 -0
  31. package/dist/claims.js +4 -0
  32. package/dist/docs.js +7 -0
  33. package/dist/fila-pesada.js +121 -11
  34. package/dist/hitl-classificacao.js +1 -0
  35. package/dist/index.js +43 -25
  36. package/dist/intelligence-graph-cli.js +27 -9
  37. package/dist/locale-claims.js +16 -2
  38. package/dist/locale-grafo.js +56 -1
  39. package/dist/locale-maestro.js +38 -0
  40. package/dist/locale-network.js +8 -6
  41. package/dist/locale-onboarding.js +14 -0
  42. package/dist/locale-preflight.js +80 -0
  43. package/dist/locale-projetos.js +6 -0
  44. package/dist/locale-prompts.js +8 -8
  45. package/dist/locale-pulse.js +4 -0
  46. package/dist/locale-retry.js +16 -0
  47. package/dist/locale-security-scan.js +35 -1
  48. package/dist/locale-verify.js +15 -0
  49. package/dist/locale.js +24 -7
  50. package/dist/maestro-cli.js +12 -17
  51. package/dist/mcp-install.js +2 -1
  52. package/dist/mcp-server.js +20 -17
  53. package/dist/mcp-verify-async.js +353 -0
  54. package/dist/mcp-verify-worker.js +17 -0
  55. package/dist/mcp-verify.js +10 -1
  56. package/dist/pedido-conducao.js +51 -0
  57. package/dist/preflight.js +25 -26
  58. package/dist/prompts.js +4 -2
  59. package/dist/pulse.js +4 -2
  60. package/dist/rede.js +24 -12
  61. package/dist/retry.js +12 -0
  62. package/dist/security-scan/cli.js +2 -2
  63. package/dist/security-scan/report.js +1 -1
  64. package/dist/session-watcher.js +33 -7
  65. package/dist/verify.js +166 -34
  66. package/package.json +1 -1
  67. package/skills/core/orkastery-bootstrap/SKILL.md +1 -1
  68. package/skills/phases/check-quality/SKILL.md +1 -1
  69. package/skills/phases/go-implementation/SKILL.md +1 -1
@@ -38,6 +38,7 @@ exports.paralelismoDaVarreduraNoGrupo = paralelismoDaVarreduraNoGrupo;
38
38
  exports.passosDoComando = passosDoComando;
39
39
  exports.elosDoComando = elosDoComando;
40
40
  exports.tirarPassosDoPreparo = tirarPassosDoPreparo;
41
+ exports.tirarPassosDoPreparoAposLeituras = tirarPassosDoPreparoAposLeituras;
41
42
  exports.hashDoProduto = hashDoProduto;
42
43
  exports.prepararNoAgile = prepararNoAgile;
43
44
  exports.executorDaClaimNoAgile = executorDaClaimNoAgile;
@@ -47,6 +48,9 @@ exports.porQueDasLanes = porQueDasLanes;
47
48
  exports.comandosNoAgile = comandosNoAgile;
48
49
  exports.pecasDoRelatorio = pecasDoRelatorio;
49
50
  exports.listasDasLanes = listasDasLanes;
51
+ exports.resultadosDasLanes = resultadosDasLanes;
52
+ exports.assinaturaDoAmbiente = assinaturaDoAmbiente;
53
+ exports.executorDaClaimPelasLanes = executorDaClaimPelasLanes;
50
54
  /**
51
55
  * RM-072 (D3 a D8 da ork-check5x): o motor do Agile Check no verify.
52
56
  *
@@ -54,12 +58,16 @@ exports.listasDasLanes = listasDasLanes;
54
58
  *
55
59
  * - o preparo do Agile (`check.agile.preparo`, como o `tsgo` deste projeto) e, se ele falhar ou nao existir, o
56
60
  * `verify.preparo`, com a peca `preparo` em fallback;
57
- * - as claims, uma por vez, contra o produto do preparo: a claim que comeca pelos passos do `verify.preparo` roda sem
61
+ * - quando ha lanes, elas rodam antes das claims; `node --test` dos mesmos arquivos, HEAD e ambiente usa o resultado
62
+ * nominal da lane, enquanto comando aberto ou evidencia divergente roda inteiro; se uma claim posterior mudar o
63
+ * produto, preparo e grupo rodam outra vez sobre o produto final;
64
+ * - as demais claims, uma por vez, contra o produto do preparo: a claim que comeca pelos passos do `verify.preparo` roda sem
58
65
  * eles quando a guarda vale (o preparo passou, a arvore e a mesma do fim do preparo e o hash dos caminhos de
59
66
  * `check.agile.produto` tambem); qualquer divergencia roda o comando inteiro, e o motivo vai ao ledger. Passo no meio
60
- * da cadeia fica: o que vem antes dele pode mudar a entrada do build (o mutante que edita o fonte e compila). Quando
67
+ * da cadeia so sai quando tudo antes dele e uma leitura `grep -q` fechada; qualquer outro elo pode mudar a entrada do
68
+ * build (como o mutante que edita o fonte e compila), entao a cadeia fica inteira. Quando
61
69
  * so o produto mudou (a claim anterior compilou de novo no meio da cadeia), o preparo roda de novo antes da proxima
62
- * claim que o reaproveitaria, ate `MAXIMO_DE_PREPAROS_REFEITOS` vezes, e uma vez antes do grupo, com a peca
70
+ * claim que o reaproveitaria, ate `MAXIMO_DE_PREPAROS_REFEITOS` vezes, e uma vez depois das claims se elas mudarem o produto, com a peca
63
71
  * `preparo-refeito`;
64
72
  * - o `build` que e so passos do preparo, com a guarda valendo, sai do preparo desta rodada sem rodar de novo;
65
73
  * - `typecheck`, as lanes de `test` (`check.agile.test`, ou o `verify.test`) e o `security_scan` num grupo paralelo, so
@@ -157,6 +165,78 @@ function tirarPassosDoPreparo(comando, passosDoPreparo) {
157
165
  }
158
166
  return { passos, resto };
159
167
  }
168
+ /** Palavras de um comando simples, sem operadores ou expansoes do shell. */
169
+ function palavrasDoComando(comando) {
170
+ const palavras = [];
171
+ let atual = '', aspas = null, escape = false;
172
+ const fechar = () => { if (atual !== '')
173
+ palavras.push(atual); atual = ''; };
174
+ for (const c of comando) {
175
+ if (escape) {
176
+ atual += c;
177
+ escape = false;
178
+ continue;
179
+ }
180
+ if (c === '\\' && aspas !== "'") {
181
+ escape = true;
182
+ continue;
183
+ }
184
+ if (aspas) {
185
+ if (c === aspas)
186
+ aspas = null;
187
+ else if (aspas === '"' && (c === '$' || c === '`'))
188
+ return null;
189
+ else
190
+ atual += c;
191
+ continue;
192
+ }
193
+ if (c === "'" || c === '"') {
194
+ aspas = c;
195
+ continue;
196
+ }
197
+ if (/\s/.test(c)) {
198
+ fechar();
199
+ continue;
200
+ }
201
+ if (';&|<>$`(){}'.includes(c))
202
+ return null;
203
+ atual += c;
204
+ }
205
+ if (escape || aspas)
206
+ return null;
207
+ fechar();
208
+ return palavras;
209
+ }
210
+ /** Uma leitura sem efeito colateral aceita antes do preparo unico. */
211
+ function comandoGrepQ(comando) {
212
+ const palavras = palavrasDoComando(comando);
213
+ if (!palavras || palavras[0] !== 'grep' || palavras[1] !== '-q')
214
+ return false;
215
+ const argumentos = palavras[2] === '--' ? palavras.slice(3) : palavras.slice(2);
216
+ return argumentos.length === 2 && argumentos[0] !== '' && argumentos[1] !== '' && !argumentos[1].startsWith('-');
217
+ }
218
+ /**
219
+ * Tira passos exatos do preparo que aparecem depois apenas de `grep -q`. A leitura nao altera a entrada do build;
220
+ * qualquer outro elo antes do preparo conserva a regra antiga e faz a cadeia rodar inteira.
221
+ */
222
+ function tirarPassosDoPreparoAposLeituras(comando, passosDoPreparo) {
223
+ const elos = elosDoComando(comando);
224
+ if (!elos)
225
+ return { passos: [], resto: comando.trim() };
226
+ const passos = [];
227
+ const resto = [];
228
+ let somenteLeiturasAntes = true;
229
+ for (const elo of elos) {
230
+ if (somenteLeiturasAntes && passosDoPreparo.includes(elo)) {
231
+ passos.push(elo);
232
+ continue;
233
+ }
234
+ resto.push(elo);
235
+ if (!comandoGrepQ(elo))
236
+ somenteLeiturasAntes = false;
237
+ }
238
+ return passos.length > 0 ? { passos, resto: resto.join(SEPARADOR) } : { passos: [], resto: comando.trim() };
239
+ }
160
240
  /**
161
241
  * O sha256 do conteudo dos caminhos do produto (arquivos em ordem, com o caminho relativo). Caminho ausente entra como
162
242
  * ausente, para o produto que some nao passar por igual; link simbolico entra pelo destino que ele aponta, sem segui-lo.
@@ -256,7 +336,8 @@ function doPreparo(nome, comando, preparo, passos) {
256
336
  function executorDaClaimNoAgile(manifesto, executor, vigente, refazer) {
257
337
  const passosDoPreparo = manifesto.verify.preparo ? passosDoComando(manifesto.verify.preparo) : [];
258
338
  return (nome, comando, cwd) => {
259
- const { passos, resto } = tirarPassosDoPreparo(comando, passosDoPreparo);
339
+ const daFrente = tirarPassosDoPreparo(comando, passosDoPreparo);
340
+ const { passos, resto } = daFrente.passos.length > 0 ? daFrente : tirarPassosDoPreparoAposLeituras(comando, passosDoPreparo);
260
341
  if (passos.length === 0)
261
342
  return executor(nome, comando, cwd);
262
343
  let motivo = vigente.guarda.conferir();
@@ -353,7 +434,8 @@ function testDasLanes(lanes) {
353
434
  ...(lanes.some((l) => l.resultado.stealPct !== undefined) ? { stealPct: lanes.find((l) => l.resultado.stealPct !== undefined).resultado.stealPct } : {}),
354
435
  ...(relogio.length ? { relogioSobSteal: relogio } : {}),
355
436
  lanes: lanes.map((l) => ({ lane: l.lane, comando: l.resultado.comando, ok: l.resultado.ok, code: l.resultado.code,
356
- ...(l.resultado.causa ? { causa: l.resultado.causa } : {}), duracaoMs: l.resultado.duracaoMs ?? 0 })),
437
+ ...(l.resultado.causa ? { causa: l.resultado.causa } : {}), duracaoMs: l.resultado.duracaoMs ?? 0,
438
+ ...(l.resultado.testeQueCaiu?.length ? { testeQueCaiu: l.resultado.testeQueCaiu } : {}) })),
357
439
  };
358
440
  }
359
441
  /**
@@ -526,3 +608,157 @@ function listasDasLanes(linhas) {
526
608
  return { lane: l.lane, arquivos, sha256: (0, node_crypto_1.createHash)('sha256').update(arquivos.join('\n')).digest('hex') };
527
609
  });
528
610
  }
611
+ /** Evidencia nominal produzida pelo reporter, sem confiar em campos abertos do JSONL. */
612
+ function resultadosDasLanes(linhas) {
613
+ return linhas.filter((l) => l.tipo === 'resultados' && typeof l.lane === 'string' && typeof l.head === 'string'
614
+ && typeof l.ambiente === 'string' && Array.isArray(l.testes))
615
+ .map((l) => ({ lane: l.lane, head: l.head, ambiente: l.ambiente,
616
+ testes: l.testes.flatMap((t) => {
617
+ if (!t || typeof t !== 'object' || Array.isArray(t))
618
+ return [];
619
+ const x = t;
620
+ if (typeof x.arquivo !== 'string' || typeof x.nome !== 'string' || !Number.isInteger(x.nesting)
621
+ || !['pass', 'fail', 'skip', 'todo'].includes(String(x.estado)))
622
+ return [];
623
+ return [{ arquivo: x.arquivo, nome: x.nome, nesting: x.nesting,
624
+ estado: x.estado }];
625
+ }) }));
626
+ }
627
+ /** Hash do ambiente funcional; transporte do CHECK, npm e bookkeeping do shell nao mudam o teste. */
628
+ function assinaturaDoAmbiente(env) {
629
+ const ignorada = (nome) => nome.startsWith('npm_') || nome.startsWith('NPM_')
630
+ || ['ORK_CHECK_RELATORIO', 'ORK_CHECK_CPUS_DA_LANE', 'ORK_CHECK_CPUS_DOCKER',
631
+ 'NODE_TEST_CONTEXT', 'NODE_COMPILE_CACHE', 'PWD', 'OLDPWD', 'SHLVL', '_', 'INIT_CWD'].includes(nome);
632
+ const pares = Object.entries(env).filter(([nome, valor]) => valor !== undefined && !ignorada(nome))
633
+ .sort(([a], [b]) => a.localeCompare(b));
634
+ return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(pares)).digest('hex');
635
+ }
636
+ /** Le somente a gramatica aceita para reaproveitar: `node --test [--test-name-pattern P] arquivo...`. */
637
+ function comandoNodeTest(comando, cwd) {
638
+ const palavras = palavrasDoComando(comando);
639
+ if (!palavras)
640
+ return null;
641
+ if (palavras[0] !== 'node' || palavras[1] !== '--test')
642
+ return null;
643
+ let padrao;
644
+ const arquivos = [];
645
+ for (let i = 2; i < palavras.length; i++) {
646
+ const p = palavras[i];
647
+ if (p === '--test-name-pattern') {
648
+ if (padrao !== undefined || !palavras[++i])
649
+ return null;
650
+ padrao = palavras[i];
651
+ continue;
652
+ }
653
+ if (p.startsWith('--test-name-pattern=')) {
654
+ if (padrao !== undefined)
655
+ return null;
656
+ padrao = p.slice('--test-name-pattern='.length);
657
+ continue;
658
+ }
659
+ if (p.startsWith('-') || /[*?\[]/.test(p))
660
+ return null;
661
+ const absoluto = path.resolve(cwd, p);
662
+ const relativo = path.relative(path.join(cwd, 'core', 'dist-test', 'test'), absoluto);
663
+ if (!relativo || relativo.startsWith('..') || path.isAbsolute(relativo) || relativo !== path.basename(relativo)
664
+ || !relativo.endsWith('.test.js'))
665
+ return null;
666
+ arquivos.push(path.basename(absoluto));
667
+ }
668
+ if (padrao !== undefined) {
669
+ try {
670
+ void new RegExp(padrao);
671
+ }
672
+ catch {
673
+ return null;
674
+ }
675
+ }
676
+ return arquivos.length > 0 ? { arquivos: [...new Set(arquivos)], ...(padrao !== undefined ? { padrao } : {}) } : null;
677
+ }
678
+ /** A forma fechada usada pelas claims para provar contagem, zero falhas e zero skips no TAP. */
679
+ function auditoriaTap(comando, cwd) {
680
+ const divisor = ' | ';
681
+ const i = comando.indexOf(divisor);
682
+ if (i < 0 || comando.indexOf(divisor, i + divisor.length) >= 0)
683
+ return null;
684
+ const node = comando.slice(0, i).trim();
685
+ const awk = comando.slice(i + divisor.length).trim();
686
+ const pedido = comandoNodeTest(node.replace(/^node --test-reporter=tap\b/, 'node --test'), cwd);
687
+ if (!pedido || pedido.arquivos.length !== 1 || pedido.padrao !== undefined)
688
+ return null;
689
+ const m = /^awk '\/\^ \*not ok\/{f\+\+} \/# SKIP\/{s\+\+} \/\^ \*ok \[0-9\]\+ - ([A-Z][A-Z0-9_-]*)\[0-9\]\+:\/{p\+\+} END\{exit !\(p>=([1-9][0-9]*) && f==0 && s==0\)\}'$/.exec(awk);
690
+ return m ? { pedido, prefixo: m[1], minimo: Number(m[2]) } : null;
691
+ }
692
+ function resultadoReaproveitado(nome, comando, pedido, usadas, esperado, nomes) {
693
+ const lanes = [...new Set(usadas.filter((e) => e.testes.some((t) => pedido.arquivos.includes(t.arquivo))).map((e) => e.lane))];
694
+ return { nome, comando, ok: true, code: 0, causa: 'exit', duracaoMs: 0, executado: true,
695
+ resumo: `reaproveitado das lanes ${lanes.join(', ')} desta rodada`,
696
+ lane: { reaproveitada: true, lanes, arquivos: pedido.arquivos,
697
+ nomes: nomes ?? (pedido.padrao === undefined ? pedido.arquivos.map((a) => `${a}:todos`) : pedido.arquivos.map((a) => `${a}:/${pedido.padrao}/`)),
698
+ head: esperado.head, ambiente: esperado.ambiente } };
699
+ }
700
+ /**
701
+ * Reaproveita uma claim somente quando a lane terminou todos os arquivos pedidos no mesmo HEAD e ambiente. Um arquivo
702
+ * inteiro verde e evidencia mais forte que `--test-name-pattern`: o padrao fica registrado em `nomes`.
703
+ */
704
+ function executorDaClaimPelasLanes(base, evidencias, esperado) {
705
+ const assinatura = assinaturaDoAmbiente(esperado.ambiente);
706
+ return (nome, comando, cwd) => {
707
+ const executar = esperado.executorEquivalente ?? base;
708
+ if (esperado.podeReaproveitar?.() === false)
709
+ return executar(nome, comando, cwd);
710
+ const elos = elosDoComando(comando);
711
+ if (!elos)
712
+ return base(nome, comando, cwd);
713
+ const planejados = [];
714
+ for (const elo of elos) {
715
+ if (comandoGrepQ(elo)) {
716
+ planejados.push({ tipo: 'grep', comando: elo });
717
+ continue;
718
+ }
719
+ const tap = auditoriaTap(elo, cwd);
720
+ const pedido = tap?.pedido ?? comandoNodeTest(elo, cwd);
721
+ if (!pedido)
722
+ return base(nome, comando, cwd);
723
+ const usadas = evidencias.filter((e) => e.head === esperado.head && e.ambiente === assinatura
724
+ && e.testes.some((t) => pedido.arquivos.includes(t.arquivo)));
725
+ const cobertos = pedido.arquivos.every((arquivo) => {
726
+ const vistos = usadas.flatMap((e) => e.testes.filter((t) => t.arquivo === arquivo));
727
+ if (vistos.length === 0 || vistos.some((t) => t.estado === 'fail'))
728
+ return false;
729
+ if (vistos.some((t) => t.estado === 'skip' || t.estado === 'todo'))
730
+ return false;
731
+ if (!tap)
732
+ return true;
733
+ const doArquivo = vistos.filter((t) => t.nesting === 0);
734
+ return doArquivo.filter((t) => t.estado === 'pass' && new RegExp(`^${tap.prefixo}[0-9]+:`).test(t.nome)).length >= tap.minimo;
735
+ });
736
+ if (!cobertos)
737
+ return executar(nome, comando, cwd);
738
+ const nomes = tap ? pedido.arquivos.map((a) => `${a}:${tap.prefixo}>=${tap.minimo};sem-falha;sem-skip`) : undefined;
739
+ planejados.push({ tipo: 'lane', resultado: resultadoReaproveitado(nome, elo, pedido, usadas, { head: esperado.head, ambiente: assinatura }, nomes) });
740
+ }
741
+ let duracaoMs = 0;
742
+ const resumos = [];
743
+ let ultimo = null;
744
+ const lanes = new Set(), arquivos = new Set(), nomes = new Set();
745
+ for (const item of planejados) {
746
+ const r = item.tipo === 'grep' ? base(nome, item.comando, cwd) : item.resultado;
747
+ duracaoMs += r.duracaoMs ?? 0;
748
+ resumos.push(r.resumo);
749
+ ultimo = r;
750
+ if (!r.ok)
751
+ return { ...r, comando, comandoExecutado: item.tipo === 'grep' ? item.comando : r.comando, duracaoMs };
752
+ for (const lane of r.lane?.lanes ?? [])
753
+ lanes.add(lane);
754
+ for (const arquivo of r.lane?.arquivos ?? [])
755
+ arquivos.add(arquivo);
756
+ for (const n of r.lane?.nomes ?? [])
757
+ nomes.add(n);
758
+ }
759
+ if (!ultimo || lanes.size === 0)
760
+ return base(nome, comando, cwd);
761
+ return { ...ultimo, nome, comando, duracaoMs, resumo: resumos.filter(Boolean).join('\n'),
762
+ lane: { reaproveitada: true, lanes: [...lanes], arquivos: [...arquivos], nomes: [...nomes], head: esperado.head, ambiente: assinatura } };
763
+ };
764
+ }
package/dist/ci.js CHANGED
@@ -389,10 +389,11 @@ function lintDoBundle(claims) {
389
389
  for (const claim of claims) {
390
390
  for (const achado of (0, claim_lint_1.analisarComandos)(claim.verificar)) {
391
391
  const linha = (0, claim_lint_1.linhaDoLintDeClaim)(claim.id, achado);
392
- if (achado.regra === 'suite-inteira' && claim.lint !== undefined)
392
+ const bloqueiaNova = achado.regra === 'suite-inteira' || (0, claim_lint_1.regraFatalDeClaim)(achado.regra);
393
+ if (bloqueiaNova && claim.lint !== undefined)
393
394
  recusas.push(linha);
394
395
  else
395
- avisos.push(achado.regra === 'suite-inteira' ? anteriorAoLint(linha) : linha);
396
+ avisos.push(bloqueiaNova ? anteriorAoLint(linha) : linha);
396
397
  }
397
398
  }
398
399
  return { recusas, avisos };
@@ -16,6 +16,8 @@
16
16
  * alheia e mexer em estado de quem nao pediu (D16). Nada de data de corte no codigo.
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.REGRAS_FATAIS_DE_CLAIM = void 0;
20
+ exports.regraFatalDeClaim = regraFatalDeClaim;
19
21
  exports.analisarComando = analisarComando;
20
22
  exports.analisarComandos = analisarComandos;
21
23
  exports.linhaDoLintDeClaim = linhaDoLintDeClaim;
@@ -29,7 +31,18 @@ const CORRECAO = {
29
31
  'suite-inteira': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['suite-inteira'],
30
32
  'sha-intermediario': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['sha-intermediario'],
31
33
  'contagem-de-commits': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['contagem-de-commits'],
34
+ 'binario-rg': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['binario-rg'],
35
+ 'ork-fora-do-path': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['ork-fora-do-path'],
36
+ 'estado-local': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['estado-local'],
37
+ 'aspas-escapadas': (0, locale_1.msg)('pt-BR').claims.correcoesDoLint['aspas-escapadas'],
32
38
  };
39
+ /** RM-037: estes achados tornam uma claim nova inexequivel no checkout do runner. */
40
+ exports.REGRAS_FATAIS_DE_CLAIM = new Set([
41
+ 'binario-rg', 'ork-fora-do-path', 'estado-local', 'aspas-escapadas',
42
+ ]);
43
+ function regraFatalDeClaim(regra) {
44
+ return exports.REGRAS_FATAIS_DE_CLAIM.has(regra);
45
+ }
33
46
  /**
34
47
  * (a) `npm [--prefix <dir>] [run] test` sem subcomando (`test:ci` nao casa) e sem arquivos
35
48
  * nomeados depois de `--` (o comando que nomeia `.test.ts`/`.js` depois de `--` e focado).
@@ -41,8 +54,24 @@ const SO_ANCESTRALIDADE = /merge-base\s+--is-ancestor\s+\S+\s+\S+/g;
41
54
  /** (b2) Contagem de commits comparada a um numero. */
42
55
  const CONTAGEM = /rev-list\s+(?:[^|;&]*\s)?--count\b|\bgit\s+log\b[^|;&]*\|\s*wc\s+-l\b/;
43
56
  const COMPARACAO = /-(?:eq|ne|gt|lt|ge|le)\s+['"]?\d|[^=!<>]==?\s*['"]?\d|grep\s+-(?:q?x|xq)\s+['"]?\d/;
57
+ /** Executavel em posicao de comando, no inicio ou depois de um operador do shell. */
58
+ function executavelDireto(comando, nome) {
59
+ return new RegExp(`(?:^|&&|\\|\\||[;|(])\\s*(?:command\\s+|env\\s+)?(${nome})(?=\\s|$)`).exec(comando);
60
+ }
44
61
  function analisarComando(comando) {
45
62
  const achados = [];
63
+ const rg = executavelDireto(comando, 'rg');
64
+ if (rg)
65
+ achados.push({ regra: 'binario-rg', trecho: rg[1], correcao: CORRECAO['binario-rg'] });
66
+ const ork = executavelDireto(comando, 'ork');
67
+ if (ork)
68
+ achados.push({ regra: 'ork-fora-do-path', trecho: ork[1], correcao: CORRECAO['ork-fora-do-path'] });
69
+ const estado = /(?:^|[\s"'=])((?:\.\/)?\.orkastery(?:[\\/][^\s"']*)?)/.exec(comando);
70
+ if (estado)
71
+ achados.push({ regra: 'estado-local', trecho: estado[1], correcao: CORRECAO['estado-local'] });
72
+ const aspas = /\\x(?:22|27)/i.exec(comando);
73
+ if (aspas)
74
+ achados.push({ regra: 'aspas-escapadas', trecho: aspas[0], correcao: CORRECAO['aspas-escapadas'] });
46
75
  const suite = SUITE_INTEIRA.exec(comando);
47
76
  if (suite)
48
77
  achados.push({ regra: 'suite-inteira', trecho: suite[0].trim(), correcao: CORRECAO['suite-inteira'] });
package/dist/claims.js CHANGED
@@ -154,6 +154,10 @@ function adicionarClaim(raiz, threadId, opcoes) {
154
154
  verificar: opcoes.verificar,
155
155
  origem: opcoes.origem,
156
156
  });
157
+ const recusas = (claim.lint ?? []).filter((a) => (0, claim_lint_1.regraFatalDeClaim)(a.regra));
158
+ if (recusas.length > 0) {
159
+ throw new locale_1.ErroDoCatalogo((m) => m.claims.lintRecusado(recusas.map((a) => `claims.lint.${a.regra}: ${(0, claim_lint_1.linhaDoLintDeClaim)(claim.id, a)}`).join('\n ')));
160
+ }
157
161
  const negativa = claim.negativa;
158
162
  const verificar = claim.verificar;
159
163
  gravarClaim(raiz, claim);
package/dist/docs.js CHANGED
@@ -82,6 +82,7 @@ const ledger_1 = require("./ledger");
82
82
  const branch_de_estado_1 = require("./branch-de-estado");
83
83
  const locale_1 = require("./locale");
84
84
  const merge_da_entrega_1 = require("./merge-da-entrega");
85
+ const changelog_check_1 = require("./changelog-check");
85
86
  exports.DIR_PRODUTO = 'docs/produto';
86
87
  exports.DIR_ROADMAP = 'docs/roadmap';
87
88
  exports.DIR_PADROES = 'docs/padroes';
@@ -265,6 +266,12 @@ function verificarDocs(raiz, opcoes = {}) {
265
266
  if (!opcoes.semGit && !base) {
266
267
  achados.push(aviso('.', undefined, 'docs.git.base', (m) => m.baseAusente(opcoes.baseBranch ?? 'main'), (m) => m.baseAusenteCorrecao));
267
268
  }
269
+ if (opcoes.pr && base) {
270
+ const changelog = (0, changelog_check_1.checarChangelog)(raiz, { base, head: 'HEAD', baseCarimbada: opcoes.baseCarimbada });
271
+ if (!changelog.ok) {
272
+ achados.push(erro(changelog_check_1.CHANGELOG, undefined, changelog.motivo ?? 'changelog.invalido', () => changelog.detalhe, () => changelog.correcao ?? 'corrija o CHANGELOG e repita a verificação'));
273
+ }
274
+ }
268
275
  const comandos = opcoes.ajudaDoCli ? comandosDaAjuda(opcoes.ajudaDoCli) : null;
269
276
  for (const d of docs) {
270
277
  if (!TIPOS.includes(d.tipo))
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ErroDaFilaPesada = exports.ENV_DONO_DA_FILA = exports.CONTRATO_FILA_PESADA = void 0;
37
+ exports.avaliarVidaDoOcupante = avaliarVidaDoOcupante;
37
38
  exports.kernelDisponivel = kernelDisponivel;
38
39
  exports.tentarTravaExclusiva = tentarTravaExclusiva;
39
40
  exports.dirDaFilaPesada = dirDaFilaPesada;
@@ -66,6 +67,7 @@ const fs = __importStar(require("node:fs"));
66
67
  const path = __importStar(require("node:path"));
67
68
  const node_crypto_1 = require("node:crypto");
68
69
  const node_child_process_1 = require("node:child_process");
70
+ const node_worker_threads_1 = require("node:worker_threads");
69
71
  const estado_thread_1 = require("./estado-thread");
70
72
  const horario_1 = require("./horario");
71
73
  exports.CONTRATO_FILA_PESADA = 'ork.fila-pesada/v1';
@@ -75,11 +77,27 @@ const FLOCK = '/usr/bin/flock';
75
77
  const DIR = 'fila-pesada';
76
78
  /** Entre uma tentativa e outra. A vez costuma durar minutos; meio segundo não pesa em ninguém. */
77
79
  const PAUSA_MS = 500;
80
+ /** A batida e complementar ao flock: atravessa a observacao de um sandbox com outro namespace de PID. */
81
+ const BATIDA_CADA_MS = 1_000;
82
+ const BATIDA_VALE_MS = 5_000;
83
+ function avaliarVidaDoOcupante(e) {
84
+ if (e.trava === 'ocupada')
85
+ return { vida: 'vivo', prova: 'trava-kernel' };
86
+ if (e.trava === 'livre')
87
+ return { vida: 'morto', prova: 'trava-livre' };
88
+ if (e.batidaValida)
89
+ return { vida: 'vivo', prova: 'batida' };
90
+ if (e.mesmoHost && e.mesmoEspacoPids && e.processo === 'vivo')
91
+ return { vida: 'vivo', prova: 'processo' };
92
+ if (e.mesmoHost && e.mesmoEspacoPids && e.processo === 'ausente')
93
+ return { vida: 'morto', prova: 'processo-ausente' };
94
+ return { vida: 'desconhecido', prova: 'indeterminada' };
95
+ }
78
96
  /** A recusa é resposta, não falha: o CLI a imprime como a da condução, e sai com 3. */
79
97
  class ErroDaFilaPesada extends Error {
80
98
  recusa;
81
99
  constructor(recusa) {
82
- super(`fila-pesada.ocupada: ${recusa.texto}`);
100
+ super(`${recusa.motivo}: ${recusa.texto}`);
83
101
  this.name = 'ErroDaFilaPesada';
84
102
  this.recusa = recusa;
85
103
  }
@@ -88,11 +106,17 @@ exports.ErroDaFilaPesada = ErroDaFilaPesada;
88
106
  function kernelDisponivel() {
89
107
  return process.platform === 'linux' && fs.existsSync(FLOCK);
90
108
  }
91
- /** O `flock` exclusivo sem espera sobre um descritor já aberto. O helper tranca a descrição de arquivo, que fica do processo. */
92
- function trancar(fd) {
109
+ /** Diferencia a contencao provada do erro de observacao; erro nunca vira trava livre. */
110
+ function observarTrava(fd) {
93
111
  const r = (0, node_child_process_1.spawnSync)(FLOCK, ['--exclusive', '--nonblock', '3'], { stdio: ['ignore', 'ignore', 'ignore', fd], timeout: 5000 });
94
- return r.status === 0 && !r.error;
112
+ if (!r.error && r.status === 0)
113
+ return 'adquirida';
114
+ if (!r.error && r.status === 1)
115
+ return 'ocupada';
116
+ return 'desconhecida';
95
117
  }
118
+ /** Tomar a trava exige prova positiva. Contencao e observacao inconclusiva falham fechadas. */
119
+ function trancar(fd) { return observarTrava(fd) === 'adquirida'; }
96
120
  function abrirTrava(arquivo, criar) {
97
121
  const fd = fs.openSync(arquivo, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW | (criar ? fs.constants.O_CREAT : 0), 0o600);
98
122
  const st = fs.fstatSync(fd);
@@ -210,11 +234,57 @@ function minhaVez(dir, senha) {
210
234
  }
211
235
  return true;
212
236
  }
237
+ function hashLocal(valor) { return (0, node_crypto_1.createHash)('sha256').update(valor).digest('hex').slice(0, 24); }
238
+ /** Identidades opacas: provam igualdade sem publicar hostname, boot id ou inode do namespace. */
239
+ function identidadeDaMaquina() {
240
+ let host = null, espacoPids = null;
241
+ try {
242
+ host = hashLocal(fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim());
243
+ }
244
+ catch { /* nao observavel */ }
245
+ try {
246
+ espacoPids = hashLocal(fs.readlinkSync('/proc/self/ns/pid'));
247
+ }
248
+ catch { /* nao observavel */ }
249
+ return { host, espacoPids };
250
+ }
213
251
  function gravarDono(fd, dono, posse) {
214
252
  const bytes = Buffer.from(JSON.stringify({ contrato: exports.CONTRATO_FILA_PESADA, ...dono, posse }) + '\n', 'utf8');
215
253
  fs.ftruncateSync(fd, 0);
216
254
  fs.writeSync(fd, bytes, 0, bytes.length, 0);
217
255
  }
256
+ function arquivoDaBatida(dir, slot, posse) {
257
+ return path.join(dir, `vaga-${slot}.${posse}.heartbeat.json`);
258
+ }
259
+ /** Worker nativo: a batida continua enquanto o verify sincrono bloqueia a thread JS, sem processo shell perdido. */
260
+ function iniciarBatida(dir, slot, posse) {
261
+ const arquivo = arquivoDaBatida(dir, slot, posse);
262
+ try {
263
+ const worker = new node_worker_threads_1.Worker(`
264
+ const fs = require('node:fs');
265
+ const { parentPort, workerData } = require('node:worker_threads');
266
+ const { randomUUID } = require('node:crypto');
267
+ let ativo = true;
268
+ parentPort.on('message', () => { ativo = false; });
269
+ function bater() {
270
+ if (!ativo) return;
271
+ const tmp = workerData.arquivo + '.' + randomUUID() + '.tmp';
272
+ try {
273
+ fs.writeFileSync(tmp, JSON.stringify({ contrato: 'ork.fila-pesada-heartbeat/v1', posse: workerData.posse,
274
+ expiraEm: new Date(Date.now() + workerData.validadeMs).toISOString() }) + '\\n', { flag: 'wx', mode: 0o600 });
275
+ fs.renameSync(tmp, workerData.arquivo);
276
+ } catch (_) { try { fs.unlinkSync(tmp); } catch (_) {} }
277
+ if (ativo) setTimeout(bater, workerData.intervaloMs);
278
+ }
279
+ bater();
280
+ `, { eval: true, workerData: { arquivo, posse, intervaloMs: BATIDA_CADA_MS, validadeMs: BATIDA_VALE_MS } });
281
+ worker.unref();
282
+ return { worker, arquivo };
283
+ }
284
+ catch {
285
+ return null;
286
+ }
287
+ }
218
288
  /** O artefato primeiro, depois a primeira vaga livre; sem vaga, solta o artefato. Nunca espera segurando nada. */
219
289
  function tentarVaga(dir, pedido) {
220
290
  const fdArtefato = tentarTravaExclusiva(path.join(dir, `artefato-${chaveDoArtefato(pedido.artefato)}.lock`));
@@ -224,17 +294,29 @@ function tentarVaga(dir, pedido) {
224
294
  const fdVaga = tentarTravaExclusiva(path.join(dir, `vaga-${slot}.lock`));
225
295
  if (fdVaga === null)
226
296
  continue;
227
- const dono = { slot, operacao: pedido.operacao, thread: pedido.thread, artefato: path.resolve(pedido.artefato),
228
- pid: process.pid, desde: new Date().toISOString() };
229
297
  const id = (0, node_crypto_1.randomUUID)();
298
+ const identidade = identidadeDaMaquina();
299
+ const dono = { slot, operacao: pedido.operacao, thread: pedido.thread, artefato: path.resolve(pedido.artefato),
300
+ pid: process.pid, desde: new Date().toISOString(), inicio: inicioDoProcesso(process.pid), ...identidade };
230
301
  gravarDono(fdVaga, dono, id);
231
302
  gravarDono(fdArtefato, dono, id);
232
- return { fdArtefato, fdVaga, slot, id };
303
+ const batida = iniciarBatida(dir, slot, id);
304
+ return { fdArtefato, fdVaga, slot, id, batida: batida?.worker ?? null, arquivoDaBatida: batida?.arquivo ?? null };
233
305
  }
234
306
  fs.closeSync(fdArtefato);
235
307
  return null;
236
308
  }
237
309
  function soltar(posse) {
310
+ try {
311
+ posse.batida?.postMessage('parar');
312
+ void posse.batida?.terminate();
313
+ }
314
+ catch { /* o processo tambem encerra o worker */ }
315
+ if (posse.arquivoDaBatida)
316
+ try {
317
+ fs.unlinkSync(posse.arquivoDaBatida);
318
+ }
319
+ catch { /* batida expira sozinha */ }
238
320
  for (const fd of [posse.fdVaga, posse.fdArtefato]) {
239
321
  try {
240
322
  fs.ftruncateSync(fd, 0);
@@ -264,7 +346,8 @@ function ocupantesDaFilaPesada(raiz) {
264
346
  continue;
265
347
  }
266
348
  try {
267
- if (trancar(fd))
349
+ const trava = observarTrava(fd);
350
+ if (trava === 'adquirida')
268
351
  continue;
269
352
  const slot = Number(/^vaga-(\d+)\.lock$/.exec(nome)[1]);
270
353
  let dono = {};
@@ -272,9 +355,31 @@ function ocupantesDaFilaPesada(raiz) {
272
355
  dono = JSON.parse(fs.readFileSync(path.join(dir, nome), 'utf8'));
273
356
  }
274
357
  catch { /* dono ainda gravando */ }
358
+ let batidaExpiraEm = null;
359
+ if (typeof dono.posse === 'string') {
360
+ try {
361
+ const batida = JSON.parse(fs.readFileSync(arquivoDaBatida(dir, slot, dono.posse), 'utf8'));
362
+ if (batida.posse === dono.posse && typeof batida.expiraEm === 'string')
363
+ batidaExpiraEm = batida.expiraEm;
364
+ }
365
+ catch { /* batida ausente, vencida ou ainda sendo publicada */ }
366
+ }
367
+ const local = identidadeDaMaquina();
368
+ const mesmoHost = typeof dono.host === 'string' && local.host === dono.host;
369
+ const mesmoEspacoPids = typeof dono.espacoPids === 'string' && local.espacoPids === dono.espacoPids;
370
+ let processo = 'desconhecido';
371
+ if (mesmoHost && mesmoEspacoPids && typeof dono.pid === 'number' && typeof dono.inicio === 'string') {
372
+ const atual = inicioDoProcesso(dono.pid);
373
+ processo = atual === dono.inicio ? 'vivo' : atual === null ? 'ausente' : 'ausente';
374
+ }
375
+ const estado = avaliarVidaDoOcupante({ trava: trava === 'ocupada' ? 'ocupada' : 'desconhecida',
376
+ batidaValida: batidaExpiraEm !== null && Date.parse(batidaExpiraEm) > Date.now(), mesmoHost, mesmoEspacoPids, processo });
275
377
  ocupantes.push({ slot, operacao: typeof dono.operacao === 'string' ? dono.operacao : 'desconhecida',
276
378
  thread: typeof dono.thread === 'string' ? dono.thread : null, artefato: typeof dono.artefato === 'string' ? dono.artefato : '',
277
- pid: typeof dono.pid === 'number' ? dono.pid : 0, desde: typeof dono.desde === 'string' ? dono.desde : '' });
379
+ pid: typeof dono.pid === 'number' ? dono.pid : 0, desde: typeof dono.desde === 'string' ? dono.desde : '',
380
+ inicio: typeof dono.inicio === 'string' ? dono.inicio : null, host: typeof dono.host === 'string' ? dono.host : null,
381
+ espacoPids: typeof dono.espacoPids === 'string' ? dono.espacoPids : null,
382
+ posse: typeof dono.posse === 'string' ? dono.posse : undefined, batidaExpiraEm, ...estado });
278
383
  }
279
384
  finally {
280
385
  fs.closeSync(fd);
@@ -283,19 +388,24 @@ function ocupantesDaFilaPesada(raiz) {
283
388
  return ocupantes;
284
389
  }
285
390
  function recusaDaFila(pedido, esperouMs, ocupantes) {
391
+ const indeterminado = ocupantes.some(o => o.vida === 'desconhecido');
392
+ const motivo = indeterminado ? 'concurrency.lease-indeterminado' : 'fila-pesada.ocupada';
286
393
  const espera = esperouMs < 60000 ? `${Math.round(esperouMs / 1000)} s` : `${Math.round(esperouMs / 60000)} min`;
287
394
  const linhas = [
288
395
  `Operação pesada não executada: ${pedido.operacao}${pedido.thread ? ` da thread ${pedido.thread}` : ''} esperou ${espera} ` +
289
396
  `e a fila de recursos pesados não liberou a vez (até ${pedido.slots} ao mesmo tempo, concurrency.max_parallel_heavy). ` +
290
397
  'Verify e build juntos nesta máquina dão regressão falsa.',
291
398
  ...(ocupantes.length ? ocupantes.map(o => ` Quem ocupa: ${o.operacao}${o.thread ? ` da thread ${o.thread}` : ''}, ` +
292
- `desde ${(0, horario_1.formatarDesde)(o.desde)}, processo ${o.pid}.`) : [' Quem ocupa: a vez é de outro pedido que chegou antes e ainda espera.']),
399
+ `desde ${(0, horario_1.formatarDesde)(o.desde)}, processo ${o.pid}${o.vida === 'desconhecido' ? '; vida indeterminada' : ''}.`) :
400
+ [' Quem ocupa: a vez é de outro pedido que chegou antes e ainda espera.']),
401
+ ...(indeterminado ? ['A vida do lease não pôde ser comprovada deste ambiente; “não sei” nunca é órfão. ' +
402
+ 'Espere com --esperar ou devolva à condução com concurrency.lease-indeterminado.'] : []),
293
403
  'O que você pode fazer:',
294
404
  ' 1. Esperar a vez: o dono da vaga segue rodando e, quando ele termina ou morre, a vaga volta sozinha.',
295
405
  ...(pedido.comandoDeEsperar ? [` ${pedido.comandoDeEsperar}`] : []),
296
406
  ' 2. Desligar a fila só nesta máquina, se ela aguenta: concurrency.max_parallel_heavy no orkastery.yaml (0 desliga).',
297
407
  ];
298
- return { contrato: 'ork.fila-pesada-recusa/v1', motivo: 'fila-pesada.ocupada', operacao: pedido.operacao, thread: pedido.thread,
408
+ return { contrato: 'ork.fila-pesada-recusa/v1', motivo, operacao: pedido.operacao, thread: pedido.thread,
299
409
  slots: pedido.slots, esperouMs, ocupantes, texto: linhas.join('\n') };
300
410
  }
301
411
  const pausa = new Int32Array(new SharedArrayBuffer(4));
@@ -71,6 +71,7 @@ exports.MOTIVOS_DO_ORQUESTRADOR = Object.freeze([
71
71
  'verify.failed', 'verify.regression', 'verify.timeout', 'verify.sem-veredito',
72
72
  'claims.failed', 'claims.unverifiable', 'ci.failed', 'hitl.formato',
73
73
  'lease.busy', 'vaga.stale', 'conducao.em-andamento', 'concurrency.limite',
74
+ 'concurrency.lease-indeterminado', 'conducao.acao-necessaria',
74
75
  ]);
75
76
  function quemDecide(motivo) {
76
77
  return exports.MOTIVOS_DO_ORQUESTRADOR.includes(String(motivo ?? '')) ? 'orquestrador' : 'dono';