@orkastery/cli 0.6.0 → 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 (118) 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 +9 -7
  25. package/dist/adapters/grok-events.js +227 -0
  26. package/dist/adapters/grok-mcp-servidor.js +622 -0
  27. package/dist/adapters/grok-mcp.js +178 -0
  28. package/dist/adapters/grok.js +686 -0
  29. package/dist/agendador-cpu.js +92 -0
  30. package/dist/branch-de-estado.js +7 -2
  31. package/dist/changelog-check.js +217 -0
  32. package/dist/check-agil.js +289 -25
  33. package/dist/check-paralelo.js +73 -18
  34. package/dist/ci.js +3 -2
  35. package/dist/claim-lint.js +29 -0
  36. package/dist/claims.js +4 -0
  37. package/dist/conducao.js +5 -0
  38. package/dist/docs.js +16 -16
  39. package/dist/doctor-forja.js +1 -3
  40. package/dist/doctor.js +1 -1
  41. package/dist/entrega-pr.js +10 -10
  42. package/dist/fabrica-estado.js +20 -11
  43. package/dist/fila-pesada.js +121 -11
  44. package/dist/hitl-classificacao.js +1 -0
  45. package/dist/horario.js +4 -1
  46. package/dist/index.js +63 -35
  47. package/dist/integracoes-locais.js +2 -0
  48. package/dist/intelligence-graph-cli.js +27 -9
  49. package/dist/locale-accounts.js +11 -3
  50. package/dist/locale-ajuda.js +10 -6
  51. package/dist/locale-claims.js +16 -2
  52. package/dist/locale-forja.js +2 -4
  53. package/dist/locale-grafo.js +56 -1
  54. package/dist/locale-maestro.js +38 -0
  55. package/dist/locale-network.js +8 -6
  56. package/dist/locale-onboarding.js +14 -0
  57. package/dist/locale-operacao-restante.js +2 -2
  58. package/dist/locale-preflight.js +80 -0
  59. package/dist/locale-projetos.js +6 -0
  60. package/dist/locale-prompts.js +8 -8
  61. package/dist/locale-pulse.js +4 -0
  62. package/dist/locale-retry.js +56 -2
  63. package/dist/locale-security-scan.js +35 -1
  64. package/dist/locale-ship.js +10 -2
  65. package/dist/locale-verify.js +15 -0
  66. package/dist/locale.js +24 -7
  67. package/dist/maestro-cli.js +12 -17
  68. package/dist/manifest.js +6 -1
  69. package/dist/mcp-install.js +24 -13
  70. package/dist/mcp-server.js +62 -33
  71. package/dist/mcp-verify-async.js +353 -0
  72. package/dist/mcp-verify-worker.js +17 -0
  73. package/dist/mcp-verify.js +10 -1
  74. package/dist/merge-da-entrega.js +85 -0
  75. package/dist/network-roadmap.js +18 -14
  76. package/dist/noctum-aceite.js +154 -14
  77. package/dist/noctum-cli.js +69 -12
  78. package/dist/noctum-entrega.js +55 -7
  79. package/dist/noctum-envelope.js +1 -1
  80. package/dist/noctum-execucao.js +5 -1
  81. package/dist/noctum-fechamento.js +18 -7
  82. package/dist/noctum-fila.js +6 -1
  83. package/dist/noctum-guarda.js +81 -4
  84. package/dist/noctum-maquinas.js +5 -1
  85. package/dist/noctum-modo.js +35 -7
  86. package/dist/noctum-reconciliacao.js +4 -1
  87. package/dist/noctum-rede.js +102 -0
  88. package/dist/noctum-relatorio.js +44 -10
  89. package/dist/noctum-retomada-rede.js +71 -0
  90. package/dist/noctum-vigia.js +49 -9
  91. package/dist/parado-no-condutor.js +14 -15
  92. package/dist/pedido-conducao.js +51 -0
  93. package/dist/phase.js +43 -15
  94. package/dist/postura-local.js +7 -3
  95. package/dist/preflight.js +27 -27
  96. package/dist/prompts.js +4 -2
  97. package/dist/pulse.js +4 -2
  98. package/dist/ratelimit.js +1 -0
  99. package/dist/rede.js +24 -12
  100. package/dist/retry.js +115 -10
  101. package/dist/runtime-context.js +165 -45
  102. package/dist/runtime-escada.js +7 -1
  103. package/dist/runtime-profiles.js +27 -17
  104. package/dist/runtimes.js +42 -5
  105. package/dist/security-scan/agil.js +18 -7
  106. package/dist/security-scan/cli.js +2 -2
  107. package/dist/security-scan/executor.js +8 -0
  108. package/dist/security-scan/report.js +1 -1
  109. package/dist/session-watcher-grok.js +425 -0
  110. package/dist/session-watcher.js +52 -11
  111. package/dist/sessoes-contas.js +33 -5
  112. package/dist/setup.js +11 -11
  113. package/dist/varredura-acoes.js +8 -15
  114. package/dist/verify.js +191 -43
  115. package/package.json +2 -2
  116. package/skills/core/orkastery-bootstrap/SKILL.md +1 -1
  117. package/skills/phases/check-quality/SKILL.md +1 -1
  118. package/skills/phases/go-implementation/SKILL.md +1 -1
package/dist/conducao.js CHANGED
@@ -113,6 +113,11 @@ exports.CANAIS_DE_CONDUCAO = {
113
113
  origem: 'sessao do Codex',
114
114
  declaracao: 'servidor MCP iniciado com --host codex',
115
115
  },
116
+ // RM-067 (F4b, G5): o servidor do grok so serve a sessao filha; o canal e o do host dele, como no codex.
117
+ grok: {
118
+ origem: 'sessao filha do Grok Build',
119
+ declaracao: 'servidor MCP iniciado com --host grok, so na sessao filha vinculada a thread',
120
+ },
116
121
  mcp: {
117
122
  origem: 'cliente MCP sem host homologado',
118
123
  declaracao: 'reservado: o servidor MCP atual exige host homologado e declara o canal dele',
package/dist/docs.js CHANGED
@@ -81,6 +81,8 @@ const thread_1 = require("./thread");
81
81
  const ledger_1 = require("./ledger");
82
82
  const branch_de_estado_1 = require("./branch-de-estado");
83
83
  const locale_1 = require("./locale");
84
+ const merge_da_entrega_1 = require("./merge-da-entrega");
85
+ const changelog_check_1 = require("./changelog-check");
84
86
  exports.DIR_PRODUTO = 'docs/produto';
85
87
  exports.DIR_ROADMAP = 'docs/roadmap';
86
88
  exports.DIR_PADROES = 'docs/padroes';
@@ -264,6 +266,12 @@ function verificarDocs(raiz, opcoes = {}) {
264
266
  if (!opcoes.semGit && !base) {
265
267
  achados.push(aviso('.', undefined, 'docs.git.base', (m) => m.baseAusente(opcoes.baseBranch ?? 'main'), (m) => m.baseAusenteCorrecao));
266
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
+ }
267
275
  const comandos = opcoes.ajudaDoCli ? comandosDaAjuda(opcoes.ajudaDoCli) : null;
268
276
  for (const d of docs) {
269
277
  if (!TIPOS.includes(d.tipo))
@@ -279,7 +287,7 @@ function verificarDocs(raiz, opcoes = {}) {
279
287
  verificarTabelasDeEstado(d, achados);
280
288
  }
281
289
  }
282
- verificarIndices(raiz, docs, achados, opcoes.pr === true);
290
+ verificarIndices(raiz, docs, achados);
283
291
  return { docs, achados };
284
292
  }
285
293
  function verificarChaves(d, achados) {
@@ -653,7 +661,7 @@ const INDICES_GERADOS = [
653
661
  * do mesmo jeito: sem a RM-051 e com a RM-031 de antes. Divergente reprova; vazio (o modelo recem-copiado
654
662
  * pelo `ork docs init`) so avisa; sem os marcadores, nao ha indice gerado para conferir.
655
663
  */
656
- function verificarIndices(raiz, docs, achados, pr = false) {
664
+ function verificarIndices(raiz, docs, achados) {
657
665
  for (const [arquivo, gerar] of INDICES_GERADOS) {
658
666
  const abs = path.join(raiz, arquivo);
659
667
  if (!fs.existsSync(abs))
@@ -668,7 +676,7 @@ function verificarIndices(raiz, docs, achados, pr = false) {
668
676
  }
669
677
  }
670
678
  else if (atual !== gerar(docs)) {
671
- achados.push((pr ? aviso : erro)(arquivo, undefined, 'docs.paridade.indice', (m) => m.indiceDiverge(pr), (m) => m.indiceDivergeCorrecao));
679
+ achados.push(erro(arquivo, undefined, 'docs.paridade.indice', (m) => m.indiceDiverge(false), (m) => m.indiceDivergeCorrecao));
672
680
  }
673
681
  }
674
682
  }
@@ -779,22 +787,14 @@ function mergeDaThread(raiz, thread, base) {
779
787
  return null;
780
788
  }
781
789
  /**
782
- * Os commits `ship(<thread>)` da base, do mais velho ao mais novo, so pelo git (o CI nao tem ledger).
790
+ * Os merges da entrega na base, do mais velho ao mais novo, so pelo git (o CI pode nao ter ledger).
783
791
  * RM-037 (fatia 3): um lugar so para o `sincronizarDocs` e o `verificarDocs` acharem o mesmo merge.
784
792
  */
785
793
  function mergesDaThreadNoGit(raiz, thread, base) {
786
- // Sem `-n 1`: o git corta antes de inverter, e `--reverse -n 1` devolveria o mais recente.
787
- // GO-FIX 2 (achado 4 do CHECK): so a linha de primeiro pai da base e o assunto que COMECA com
788
- // `ship(<thread>)`, como no `mergeDaEntrega`. Commit que cita o merge no corpo, ou o Revert dele, nao e merge.
789
- // RM-047 (fronteira de confiança): a base pode ser o `worktree.base_branch` cru do clone.
790
- if (!(0, branch_de_estado_1.branchValida)(base))
791
- return [];
792
- const log = git(raiz, ['log', base, '--first-parent', '--reverse', '--format=%H%x09%s', '--fixed-strings', `--grep=ship(${thread})`]);
793
- if (!log.ok)
794
- return [];
795
- return log.saida.split('\n').map((l) => l.split('\t'))
796
- .filter(([sha, assunto]) => !!sha && typeof assunto === 'string' && assunto.startsWith(`ship(${thread})`))
797
- .map(([sha]) => sha);
794
+ // O ref resolvido pode ser local, remoto abreviado ou remoto completo; a mensagem padrao do GitLab leva so a base.
795
+ const destino = base.replace(/^refs\/heads\//, '').replace(/^refs\/remotes\/[^/]+\//, '').replace(/^origin\//, '');
796
+ const branch = (0, merge_da_entrega_1.branchRegistradaDaEntrega)(raiz, thread);
797
+ return ((0, merge_da_entrega_1.mergesDasEntregasNaBase)(raiz, base, destino, [{ thread, branch }], true).get(thread) ?? []).map((m) => m.mergeSha);
798
798
  }
799
799
  function temBranch(raiz, thread) {
800
800
  const r = git(raiz, ['for-each-ref', '--format=%(refname:short)', `refs/heads/ork/${thread}-*`, `refs/remotes/origin/ork/${thread}-*`]);
@@ -263,7 +263,7 @@ const PROJETO_DO_GITLAB = zod_1.z.object({
263
263
  const REPOSITORIO_DO_GITHUB = zod_1.z.object({
264
264
  allow_merge_commit: zod_1.z.boolean().optional(), allow_squash_merge: zod_1.z.boolean().optional(), allow_rebase_merge: zod_1.z.boolean().optional(),
265
265
  });
266
- /** O metodo de merge e o resto do que decide se o merge `ship(<thread>)` chega a base. So avisa; o projeto nao lido reprova. */
266
+ /** O metodo de merge e o que decide se a entrega chega a base com segundo pai. So avisa; o projeto nao lido reprova. */
267
267
  function projetoDaForja(f, api, linha) {
268
268
  const m = (0, locale_1.msg)().forja.doctor;
269
269
  const caminho = f.tipo === 'gitlab' ? (0, pedidos_1.projetoNaApi)(f.repo) : `repos/${f.repo}`;
@@ -282,8 +282,6 @@ function projetoDaForja(f, api, linha) {
282
282
  const avisos = [];
283
283
  if (p.merge_method === 'ff')
284
284
  avisos.push(m.avisoFf);
285
- else if (!(p.merge_commit_template ?? '').trimStart().startsWith('%{title}'))
286
- avisos.push(m.avisoMensagemPadrao);
287
285
  if (p.squash_option === 'always' || p.squash_option === 'default_on')
288
286
  avisos.push(m.avisoSquash(p.squash_option));
289
287
  const detalhe = [m.projetoGitlab(p.merge_method, p.squash_option ?? '?', estado(p.merge_pipelines_enabled), estado(p.merge_trains_enabled)),
package/dist/doctor.js CHANGED
@@ -105,7 +105,7 @@ function checarSeparacaoDaConducao(raiz, inventario, env = process.env) {
105
105
  try {
106
106
  if (dirAtual) {
107
107
  const p = (0, runtime_profiles_1.lerPerfisComContas)(raiz).perfis.find((x) => x.runtime === runtime &&
108
- real(((0, runtime_profiles_1.perfilDeDespacho)(x).configDir ?? (0, runtime_profiles_1.perfilDeDespacho)(x).codexHome)) === real(dirAtual));
108
+ real((0, runtime_profiles_1.diretorioDoPerfil)((0, runtime_profiles_1.perfilDeDespacho)(x))) === real(dirAtual));
109
109
  perfil = p?.id ?? null;
110
110
  }
111
111
  }
@@ -37,19 +37,19 @@ const util_1 = require("./util");
37
37
  const executor_1 = require("./forja/executor");
38
38
  const registro_1 = require("./forja/registro");
39
39
  const locale_1 = require("./locale");
40
- /** O merge `ship(<thread>)` mais recente na base remota, com o head do PR (segundo pai). */
40
+ const merge_da_entrega_1 = require("./merge-da-entrega");
41
+ const worktree_1 = require("./worktree");
42
+ /** O merge da entrega mais recente na base remota, com o head do PR (segundo pai). */
41
43
  function mergeDaEntrega(raiz, thread, remoto, base) {
42
- const r = (0, util_1.exec)('git', ['log', `refs/remotes/${remoto}/${base}`, '--first-parent', '--format=%H%x09%P%x09%s', `--grep=^ship(${thread})`], raiz);
43
- if (!r.ok)
44
+ let branch;
45
+ try {
46
+ branch = (0, merge_da_entrega_1.branchRegistradaDaEntrega)(raiz, thread) ?? (0, worktree_1.branchDaWorktree)((0, thread_1.lerThread)(raiz, thread));
47
+ }
48
+ catch {
44
49
  return null;
45
- for (const linha of r.stdout.split('\n')) {
46
- const [sha, pais, assunto] = linha.split('\t');
47
- if (!sha || !assunto || !assunto.startsWith(`ship(${thread})`))
48
- continue;
49
- const segundo = (pais ?? '').split(' ')[1] ?? null;
50
- return { mergeSha: sha, headSha: segundo };
51
50
  }
52
- return null;
51
+ const merge = (0, merge_da_entrega_1.mergesDasEntregasNaBase)(raiz, `refs/remotes/${remoto}/${base}`, base, [{ thread, branch }]).get(thread)?.[0];
52
+ return merge ? { mergeSha: merge.mergeSha, headSha: merge.headSha } : null;
53
53
  }
54
54
  /**
55
55
  * A thread chega ao SHIP quando o ciclo dela tem SHIP, e so AVANCA: o segundo PR da mesma entrega (os
@@ -85,6 +85,8 @@ const versao_1 = require("./versao");
85
85
  const saida_segura_1 = require("./saida-segura");
86
86
  const vida_da_thread_1 = require("./vida-da-thread");
87
87
  const locale_1 = require("./locale");
88
+ const merge_da_entrega_1 = require("./merge-da-entrega");
89
+ const worktree_1 = require("./worktree");
88
90
  exports.CONTRATO_MAQUINA = 'ork.fabrica-maquina/v1';
89
91
  exports.BRANCH_DA_FABRICA = 'ork/fabrica-estado';
90
92
  /** O diretorio dos retratos na branch; a leitura sem clone (RM-054) le o mesmo. */
@@ -133,8 +135,8 @@ function arquivoDaMaquina(maquina) {
133
135
  return `${nome}.json`;
134
136
  }
135
137
  /**
136
- * As entregas que ja estao na base: `ship(<thread>): ...` e o assunto do merge que o fluxo de
137
- * entrega usa, o mesmo fato que o `ork docs sincronizar` le. Um `git log` so, para todas.
138
+ * As entregas que ja estao na base, pelo mesmo reconhecedor do registro do PR e da paridade de docs.
139
+ * Um `git log` so, para todas as threads deste checkout.
138
140
  */
139
141
  function entregasNaBase(raiz, base, remoto = 'origin') {
140
142
  // A base vem do manifesto: a ref vai sempre qualificada (`refs/...`), e um valor como
@@ -142,16 +144,23 @@ function entregasNaBase(raiz, base, remoto = 'origin') {
142
144
  // Na ordem: a copia remota da base, a branch local, e a base escrita como remota (`origin/main`).
143
145
  const candidatas = [`refs/remotes/${remoto}/${base}`, `refs/heads/${base}`, `refs/remotes/${base}`];
144
146
  const ref = candidatas.find((c) => (0, branch_de_estado_1.git)(raiz, ['rev-parse', '--verify', '--quiet', c]).ok) ?? candidatas[1];
145
- // Regex basica do git: o `(` e literal.
146
- const r = (0, branch_de_estado_1.git)(raiz, ['log', ref, '--format=%h%x09%s', '--grep=^ship(']);
147
147
  const entregas = new Map();
148
- if (!r.ok)
149
- return entregas;
150
- for (const linha of r.stdout.split('\n')) {
151
- const m = /^([0-9a-f]+)\tship\(([A-Za-z0-9._-]+)\)/.exec(linha);
152
- // O log vem do mais novo para o mais velho: a primeira entrega de cada thread e a que vale.
153
- if (m && !entregas.has(m[2]))
154
- entregas.set(m[2], m[1]);
148
+ const threads = [];
149
+ for (const id of (0, thread_1.listarIds)(raiz)) {
150
+ try {
151
+ threads.push((0, thread_1.lerThread)(raiz, id));
152
+ }
153
+ catch { /* thread ilegivel fica de fora */ }
154
+ }
155
+ const destino = base.startsWith(`${remoto}/`) ? base.slice(remoto.length + 1) : base;
156
+ const reconhecidas = (0, merge_da_entrega_1.mergesDasEntregasNaBase)(raiz, ref, destino, threads.map((t) => ({
157
+ thread: t.id,
158
+ branch: (0, merge_da_entrega_1.branchRegistradaDaEntrega)(raiz, t.id) ?? (0, worktree_1.branchDaWorktree)(t),
159
+ })));
160
+ for (const [thread, merges] of reconhecidas) {
161
+ const merge = merges[0];
162
+ if (merge)
163
+ entregas.set(thread, merge.mergeSha.slice(0, 7));
155
164
  }
156
165
  return entregas;
157
166
  }
@@ -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';
package/dist/horario.js CHANGED
@@ -215,8 +215,11 @@ function duracaoRelativa(minutos) {
215
215
  }
216
216
  function sigla(fuso, ms, locale = (0, locale_1.localeAtivo)()) {
217
217
  try {
218
- return new Intl.DateTimeFormat(locale === 'en' ? 'en-US' : 'pt-BR', { timeZone: fuso, timeZoneName: 'short' })
218
+ const valor = new Intl.DateTimeFormat(locale === 'en' ? 'en-US' : 'pt-BR', { timeZone: fuso, timeZoneName: 'short' })
219
219
  .formatToParts(new Date(ms)).find(p => p.type === 'timeZoneName')?.value ?? '';
220
+ // RM-074 (F4): o deslocamento zero sai `GMT+0` no Node 20.20.2 e no 22.23.2; um ICU antigo escreve so `GMT`. O texto
221
+ // fica o mesmo em qualquer ICU.
222
+ return valor === 'GMT' ? 'GMT+0' : valor;
220
223
  }
221
224
  catch {
222
225
  return '';