@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.
- package/adapters/claude-code/agents/ork-check.md +1 -1
- package/adapters/claude-code/agents/ork-go.md +1 -1
- package/adapters/claude-code/agents/ork-goal.md +1 -1
- package/adapters/claude-code/agents/ork-master.md +1 -1
- package/adapters/claude-code/agents/ork-plan.md +1 -1
- package/adapters/claude-code/agents/ork-ship.md +1 -1
- package/adapters/claude-code/commands/ork.md +1 -1
- package/adapters/claude-code/mods/ork-torre/hooks/frases.ts +2 -0
- package/adapters/codex/skills/ork/SKILL.md +1 -1
- package/adapters/openclaw/openclaw.plugin.json +1 -1
- package/adapters/openclaw/package.json +1 -1
- package/assets/adapters-en/claude-code/agents/ork-check.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-go.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-goal.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-master.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-plan.md +1 -1
- package/assets/adapters-en/claude-code/agents/ork-ship.md +1 -1
- package/assets/adapters-en/claude-code/commands/ork.md +1 -1
- package/assets/adapters-en/codex/skills/ork/SKILL.md +1 -1
- package/assets/orkmind_fixture.py +4 -1
- package/assets/skills-en/core/orkastery-bootstrap/SKILL.md +1 -1
- package/assets/skills-en/phases/check-quality/SKILL.md +1 -1
- package/assets/skills-en/phases/go-implementation/SKILL.md +1 -1
- package/dist/adapters/claude-bg.js +9 -7
- package/dist/adapters/grok-events.js +227 -0
- package/dist/adapters/grok-mcp-servidor.js +622 -0
- package/dist/adapters/grok-mcp.js +178 -0
- package/dist/adapters/grok.js +686 -0
- package/dist/agendador-cpu.js +92 -0
- package/dist/branch-de-estado.js +7 -2
- package/dist/changelog-check.js +217 -0
- package/dist/check-agil.js +289 -25
- package/dist/check-paralelo.js +73 -18
- package/dist/ci.js +3 -2
- package/dist/claim-lint.js +29 -0
- package/dist/claims.js +4 -0
- package/dist/conducao.js +5 -0
- package/dist/docs.js +16 -16
- package/dist/doctor-forja.js +1 -3
- package/dist/doctor.js +1 -1
- package/dist/entrega-pr.js +10 -10
- package/dist/fabrica-estado.js +20 -11
- package/dist/fila-pesada.js +121 -11
- package/dist/hitl-classificacao.js +1 -0
- package/dist/horario.js +4 -1
- package/dist/index.js +63 -35
- package/dist/integracoes-locais.js +2 -0
- package/dist/intelligence-graph-cli.js +27 -9
- package/dist/locale-accounts.js +11 -3
- package/dist/locale-ajuda.js +10 -6
- package/dist/locale-claims.js +16 -2
- package/dist/locale-forja.js +2 -4
- package/dist/locale-grafo.js +56 -1
- package/dist/locale-maestro.js +38 -0
- package/dist/locale-network.js +8 -6
- package/dist/locale-onboarding.js +14 -0
- package/dist/locale-operacao-restante.js +2 -2
- package/dist/locale-preflight.js +80 -0
- package/dist/locale-projetos.js +6 -0
- package/dist/locale-prompts.js +8 -8
- package/dist/locale-pulse.js +4 -0
- package/dist/locale-retry.js +56 -2
- package/dist/locale-security-scan.js +35 -1
- package/dist/locale-ship.js +10 -2
- package/dist/locale-verify.js +15 -0
- package/dist/locale.js +24 -7
- package/dist/maestro-cli.js +12 -17
- package/dist/manifest.js +6 -1
- package/dist/mcp-install.js +24 -13
- package/dist/mcp-server.js +62 -33
- package/dist/mcp-verify-async.js +353 -0
- package/dist/mcp-verify-worker.js +17 -0
- package/dist/mcp-verify.js +10 -1
- package/dist/merge-da-entrega.js +85 -0
- package/dist/network-roadmap.js +18 -14
- package/dist/noctum-aceite.js +154 -14
- package/dist/noctum-cli.js +69 -12
- package/dist/noctum-entrega.js +55 -7
- package/dist/noctum-envelope.js +1 -1
- package/dist/noctum-execucao.js +5 -1
- package/dist/noctum-fechamento.js +18 -7
- package/dist/noctum-fila.js +6 -1
- package/dist/noctum-guarda.js +81 -4
- package/dist/noctum-maquinas.js +5 -1
- package/dist/noctum-modo.js +35 -7
- package/dist/noctum-reconciliacao.js +4 -1
- package/dist/noctum-rede.js +102 -0
- package/dist/noctum-relatorio.js +44 -10
- package/dist/noctum-retomada-rede.js +71 -0
- package/dist/noctum-vigia.js +49 -9
- package/dist/parado-no-condutor.js +14 -15
- package/dist/pedido-conducao.js +51 -0
- package/dist/phase.js +43 -15
- package/dist/postura-local.js +7 -3
- package/dist/preflight.js +27 -27
- package/dist/prompts.js +4 -2
- package/dist/pulse.js +4 -2
- package/dist/ratelimit.js +1 -0
- package/dist/rede.js +24 -12
- package/dist/retry.js +115 -10
- package/dist/runtime-context.js +165 -45
- package/dist/runtime-escada.js +7 -1
- package/dist/runtime-profiles.js +27 -17
- package/dist/runtimes.js +42 -5
- package/dist/security-scan/agil.js +18 -7
- package/dist/security-scan/cli.js +2 -2
- package/dist/security-scan/executor.js +8 -0
- package/dist/security-scan/report.js +1 -1
- package/dist/session-watcher-grok.js +425 -0
- package/dist/session-watcher.js +52 -11
- package/dist/sessoes-contas.js +33 -5
- package/dist/setup.js +11 -11
- package/dist/varredura-acoes.js +8 -15
- package/dist/verify.js +191 -43
- package/package.json +2 -2
- package/skills/core/orkastery-bootstrap/SKILL.md +1 -1
- package/skills/phases/check-quality/SKILL.md +1 -1
- 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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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}-*`]);
|
package/dist/doctor-forja.js
CHANGED
|
@@ -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
|
|
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((
|
|
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
|
}
|
package/dist/entrega-pr.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
package/dist/fabrica-estado.js
CHANGED
|
@@ -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
|
|
137
|
-
*
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
}
|
package/dist/fila-pesada.js
CHANGED
|
@@ -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(
|
|
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
|
-
/**
|
|
92
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
|
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
|
-
|
|
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 '';
|