@orkastery/cli 0.5.1 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/claude-code/README.md +158 -0
- package/adapters/claude-code/agents/ork-master.md +2 -0
- package/adapters/claude-code/commands/master.md +8 -0
- package/adapters/claude-code/commands/ork.md +9 -0
- package/adapters/claude-code/mods/autoridade.json +56 -0
- package/adapters/claude-code/mods/ork-clima/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-clima/hooks/contas.ts +283 -0
- package/adapters/claude-code/mods/ork-clima/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-clima/hooks/previsao.ts +393 -0
- package/adapters/claude-code/mods/ork-clima/hooks/register.ts +205 -0
- package/adapters/claude-code/mods/ork-clima/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-clima/types/index.d.ts +80 -0
- package/adapters/claude-code/mods/ork-freio/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-freio/hooks/carga.ts +160 -0
- package/adapters/claude-code/mods/ork-freio/hooks/conferencia.ts +309 -0
- package/adapters/claude-code/mods/ork-freio/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-freio/hooks/lote.ts +270 -0
- package/adapters/claude-code/mods/ork-freio/hooks/register.ts +285 -0
- package/adapters/claude-code/mods/ork-freio/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-freio/types/index.d.ts +127 -0
- package/adapters/claude-code/mods/ork-mesa/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/cartoes.tsx +246 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/decidir.ts +110 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/fila.ts +253 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/obvia.ts +98 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/register.tsx +493 -0
- package/adapters/claude-code/mods/ork-mesa/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-mesa/types/index.d.ts +131 -0
- package/adapters/claude-code/mods/ork-pista/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-pista/hooks/acoes.ts +71 -0
- package/adapters/claude-code/mods/ork-pista/hooks/ci.ts +117 -0
- package/adapters/claude-code/mods/ork-pista/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-pista/hooks/pista.tsx +356 -0
- package/adapters/claude-code/mods/ork-pista/hooks/register.tsx +439 -0
- package/adapters/claude-code/mods/ork-pista/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-pista/types/index.d.ts +85 -0
- package/adapters/claude-code/mods/ork-ponte/.claude-plugin/plugin.json +13 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/faixa.tsx +57 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/guarda.ts +410 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/mostrado.ts +55 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/nucleo.ts +157 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/papel.ts +42 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/register.ts +331 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/vigia.ts +76 -0
- package/adapters/claude-code/mods/ork-ponte/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-ponte/types/index.d.ts +178 -0
- package/adapters/claude-code/mods/ork-portao/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-portao/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-portao/hooks/lease.ts +201 -0
- package/adapters/claude-code/mods/ork-portao/hooks/plano.ts +304 -0
- package/adapters/claude-code/mods/ork-portao/hooks/register.ts +339 -0
- package/adapters/claude-code/mods/ork-portao/hooks/verify.ts +152 -0
- package/adapters/claude-code/mods/ork-portao/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-portao/types/index.d.ts +60 -0
- package/adapters/claude-code/mods/ork-radar/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-radar/hooks/colisao.ts +420 -0
- package/adapters/claude-code/mods/ork-radar/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-radar/hooks/mapa.tsx +322 -0
- package/adapters/claude-code/mods/ork-radar/hooks/register.tsx +470 -0
- package/adapters/claude-code/mods/ork-radar/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-radar/types/index.d.ts +72 -0
- package/adapters/claude-code/mods/ork-sentinela/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/alertas.ts +85 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/despacho.ts +156 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/register.ts +511 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/vigia.ts +186 -0
- package/adapters/claude-code/mods/ork-sentinela/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-sentinela/types/index.d.ts +65 -0
- package/adapters/claude-code/mods/ork-torre/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-torre/hooks/agenda.ts +60 -0
- package/adapters/claude-code/mods/ork-torre/hooks/detalhe.tsx +236 -0
- package/adapters/claude-code/mods/ork-torre/hooks/frases.ts +42 -0
- package/adapters/claude-code/mods/ork-torre/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-torre/hooks/painel.tsx +344 -0
- package/adapters/claude-code/mods/ork-torre/hooks/rede.ts +97 -0
- package/adapters/claude-code/mods/ork-torre/hooks/register.tsx +437 -0
- package/adapters/claude-code/mods/ork-torre/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-torre/types/index.d.ts +108 -0
- package/adapters/codex/skills/ork/SKILL.md +18 -3
- package/adapters/hermes/README.md +1 -0
- package/adapters/hermes/bin/ork-network-status.sh +10 -0
- package/adapters/hermes/hermes.plugin.json +1 -0
- package/adapters/hermes/hitl-ingress/__init__.py +2 -2
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +7 -6
- package/adapters/openclaw/README.md +26 -5
- package/adapters/openclaw/dist/hitl-ingress.js +2 -2
- package/adapters/openclaw/dist/index.js +46 -11
- package/adapters/openclaw/openclaw.plugin.json +5 -1
- package/adapters/openclaw/package.json +2 -2
- package/adapters/openclaw/src/hitl-ingress.ts +2 -2
- package/adapters/openclaw/src/index.ts +49 -11
- package/assets/docs-en/markdownlint-cli2.jsonc +22 -0
- package/assets/docs-en/padroes/documentacao-de-produto.md +135 -0
- package/assets/docs-en/padroes/roadmap-de-produto.md +100 -0
- package/assets/docs-en/produto/README.md +14 -0
- package/assets/docs-en/produto/_modelo-feature.md +62 -0
- package/assets/docs-en/roadmap/README.md +14 -0
- package/assets/docs-en/roadmap/_modelo-item.md +75 -0
- package/assets/orkmind_bridge.py +117 -9
- package/dist/adapters/claude-bg.js +116 -18
- package/dist/adapters/codex.js +14 -4
- package/dist/agents-md.js +24 -14
- package/dist/auditrun.js +22 -3
- package/dist/board.js +20 -17
- package/dist/branch-de-estado.js +88 -10
- package/dist/busca-semantica.js +52 -12
- package/dist/canarios-rm057.js +149 -0
- package/dist/canarios.js +2 -0
- package/dist/catalogo.js +33 -0
- package/dist/ci.js +21 -8
- package/dist/claims-conferir.js +342 -0
- package/dist/claims.js +41 -1
- package/dist/company-brain-client.js +36 -1
- package/dist/company-brain-context.js +158 -22
- package/dist/company-brain-journal.js +2 -1
- package/dist/company-brain-migration.js +36 -1
- package/dist/conducao-sentinela.js +388 -0
- package/dist/conducao-texto.js +12 -18
- package/dist/conducao.js +122 -30
- package/dist/conhecimento.js +490 -0
- package/dist/docs.js +70 -17
- package/dist/doctor-pulse-cron.js +349 -0
- package/dist/doctor-rede.js +150 -0
- package/dist/doctor.js +195 -111
- package/dist/entrega-pr.js +42 -29
- package/dist/escopo-escrita.js +10 -1
- package/dist/esgotamentos.js +181 -0
- package/dist/estado-thread.js +23 -1
- package/dist/experiencia-instalacao.js +45 -25
- package/dist/experiencia.js +12 -2
- package/dist/fabrica-batida.js +310 -0
- package/dist/fabrica-estado.js +52 -14
- package/dist/fabrica-publicar.js +9 -1
- package/dist/fechamento.js +20 -66
- package/dist/fila-pesada.js +444 -0
- package/dist/fix.js +3 -2
- package/dist/forja.js +31 -7
- package/dist/gates.js +3 -24
- package/dist/handoff.js +10 -6
- package/dist/hitl-contract.js +59 -4
- package/dist/hitl-curto.js +11 -6
- package/dist/hitl-gates.js +5 -0
- package/dist/hitl-local-receipt.js +2 -0
- package/dist/hitl-lote.js +5 -2
- package/dist/hitl-resumo.js +38 -3
- package/dist/hitl-tempo-parado.js +201 -0
- package/dist/hitl-texto-livre.js +5 -5
- package/dist/hitl.js +29 -33
- package/dist/horario.js +41 -7
- package/dist/hosts.js +104 -11
- package/dist/impedimento.js +299 -0
- package/dist/index.js +961 -542
- package/dist/indice-vetorial.js +87 -10
- package/dist/indice.js +9 -1
- package/dist/init.js +56 -43
- package/dist/instabilidade.js +144 -0
- package/dist/integracoes-locais.js +4 -0
- package/dist/intelligence-graph-cli.js +170 -16
- package/dist/intelligence-graph-contexto.js +254 -0
- package/dist/intelligence-graph-contract.js +2 -1
- package/dist/intelligence-graph-extract-md.js +56 -4
- package/dist/intelligence-graph-extract-ts.js +71 -3
- package/dist/intelligence-graph-parsers.js +6 -4
- package/dist/intelligence-graph-query.js +66 -19
- package/dist/leases.js +664 -87
- package/dist/ledger-stats.js +15 -1
- package/dist/ledger.js +18 -1
- package/dist/licoes.js +1 -0
- package/dist/liveness.js +6 -3
- package/dist/locale-ajuda.js +761 -0
- package/dist/locale-gerados.js +160 -0
- package/dist/locale-master.js +99 -0
- package/dist/locale-onboarding.js +92 -0
- package/dist/locale-phase.js +151 -0
- package/dist/locale-pulse.js +211 -0
- package/dist/locale-retry.js +385 -0
- package/dist/locale-roadmap.js +102 -0
- package/dist/locale-ship.js +494 -0
- package/dist/locale-thread.js +277 -0
- package/dist/locale-verify.js +125 -0
- package/dist/locale.js +714 -0
- package/dist/maestro-runtime.js +4 -1
- package/dist/manifest.js +149 -4
- package/dist/maquina.js +23 -4
- package/dist/master-digest.js +6 -1
- package/dist/master-nota.js +18 -11
- package/dist/master.js +145 -31
- package/dist/mcp-artifacts.js +43 -3
- package/dist/mcp-git.js +42 -8
- package/dist/mcp-grafo-worker.js +5 -3
- package/dist/mcp-grafo.js +55 -6
- package/dist/mcp-server.js +22 -3
- package/dist/mcp-ship.js +3 -1
- package/dist/mcp-verify.js +6 -1
- package/dist/memoria.js +48 -6
- package/dist/modos.js +3 -6
- package/dist/network-roadmap.js +196 -18
- package/dist/noctum-arco.js +911 -0
- package/dist/noctum-capacidade.js +141 -0
- package/dist/noctum-envelope.js +810 -0
- package/dist/noctum-execucao.js +304 -0
- package/dist/noctum-fila.js +280 -0
- package/dist/ocupacao.js +40 -5
- package/dist/onboarding.js +40 -21
- package/dist/orkmind.js +99 -7
- package/dist/orquestracao.js +24 -21
- package/dist/parado-no-condutor.js +219 -38
- package/dist/phase.js +160 -52
- package/dist/policies.js +56 -5
- package/dist/portfolio.js +8 -3
- package/dist/postura-local.js +234 -0
- package/dist/preflight.js +11 -10
- package/dist/procedencia.js +225 -0
- package/dist/projeto-alvo.js +132 -5
- package/dist/prompts.js +40 -1
- package/dist/prova-ativacao.js +75 -6
- package/dist/pulse-delivery.js +47 -4
- package/dist/pulse.js +98 -19
- package/dist/ratelimit.js +24 -6
- package/dist/recall.js +3 -1
- package/dist/redacao-saida.js +63 -0
- package/dist/rede-adesao.js +399 -0
- package/dist/rede-forja.js +289 -0
- package/dist/rede-projetos.js +239 -0
- package/dist/rede-status.js +337 -0
- package/dist/rede.js +980 -0
- package/dist/retry.js +291 -140
- package/dist/roadmap-reservas.js +20 -3
- package/dist/roadmap-status.js +150 -34
- package/dist/runtime-context.js +181 -3
- package/dist/runtime-profiles.js +22 -1
- package/dist/runtimes.js +2 -1
- package/dist/saida-segura.js +58 -0
- package/dist/session-events.js +7 -3
- package/dist/session-watcher-claude.js +120 -24
- package/dist/session-watcher.js +185 -33
- package/dist/sessoes-contas.js +187 -0
- package/dist/sessoes-inventario.js +6 -18
- package/dist/sessoes.js +83 -2
- package/dist/ship-publicar.js +800 -0
- package/dist/ship.js +157 -88
- package/dist/steal.js +77 -0
- package/dist/thread-close.js +3 -2
- package/dist/thread.js +299 -49
- package/dist/util.js +8 -1
- package/dist/verify.js +141 -52
- package/dist/vida-da-thread.js +254 -0
- package/dist/worktree-local.js +165 -0
- package/dist/worktree.js +25 -2
- package/dist/yaml.js +27 -8
- package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
- package/package.json +7 -3
- package/references/definition-of-done.md +1 -1
- package/schemas/code-artifact-graph.v1.schema.json +2 -1
- package/skills/README.md +1 -1
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- package/skills/phases/master-metrics/SKILL.md +8 -3
package/dist/leases.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Leases de exclusao mutua entre threads (`.orkastery/leases/<nome>.json`).
|
|
3
|
+
* Leases de exclusao mutua entre threads (`.orkastery/leases/<nome>.json` da raiz do projeto).
|
|
4
4
|
*
|
|
5
5
|
* O lease `main-tree` serializa o SHIP: com N threads paralelas, so uma mergeia por vez.
|
|
6
6
|
* A aquisicao e atomica de verdade (`open` com flag `wx`, que falha se o arquivo existe),
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* `path:<glob>`, `board:<card>` e `service:<porta>`, e uma FILA por colisao. Duas threads
|
|
14
14
|
* que pedem a mesma regiao de path nao brigam: a segunda entra na fila em FIFO e recebe
|
|
15
15
|
* o motivo tipado `lease.busy` com a correcao acionavel.
|
|
16
|
+
*
|
|
17
|
+
* RM-036 (fatia dos leases): todas as familias, e a fila, moram no estado CANONICO do projeto
|
|
18
|
+
* (`raizDoEstado`), como o `exec:` da I-36. O `ork` acha a raiz subindo do cwd e a worktree tem
|
|
19
|
+
* o proprio manifesto: com a pasta do checkout de quem chamava, um `ork` da raiz e outro da
|
|
20
|
+
* worktree pegavam o mesmo lease ao mesmo tempo.
|
|
16
21
|
*/
|
|
17
22
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
23
|
if (k2 === undefined) k2 = k;
|
|
@@ -55,6 +60,8 @@ exports.nomeDeLease = nomeDeLease;
|
|
|
55
60
|
exports.tipoDoLease = tipoDoLease;
|
|
56
61
|
exports.alvoDoLease = alvoDoLease;
|
|
57
62
|
exports.caminhoLease = caminhoLease;
|
|
63
|
+
exports.argumentoDeLease = argumentoDeLease;
|
|
64
|
+
exports.dirsLegadosDeLeases = dirsLegadosDeLeases;
|
|
58
65
|
exports.regravarLease = regravarLease;
|
|
59
66
|
exports.lerLease = lerLease;
|
|
60
67
|
exports.expirado = expirado;
|
|
@@ -81,6 +88,8 @@ exports.soltarThreadFechada = soltarThreadFechada;
|
|
|
81
88
|
exports.podarRegioesDeThreadsFechadas = podarRegioesDeThreadsFechadas;
|
|
82
89
|
const fs = __importStar(require("node:fs"));
|
|
83
90
|
const path = __importStar(require("node:path"));
|
|
91
|
+
const node_crypto_1 = require("node:crypto");
|
|
92
|
+
const node_child_process_1 = require("node:child_process");
|
|
84
93
|
const manifest_1 = require("./manifest");
|
|
85
94
|
const estado_thread_1 = require("./estado-thread");
|
|
86
95
|
const util_1 = require("./util");
|
|
@@ -103,17 +112,21 @@ exports.FAMILIAS_DE_LEASE = {
|
|
|
103
112
|
};
|
|
104
113
|
/** I-36 (D1): prefixo da familia de execucao, `exec:<thread>`. */
|
|
105
114
|
exports.PREFIXO_EXEC = 'exec';
|
|
106
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Diretorio dos leases do projeto: o `.orkastery/leases` da raiz do estado, o mesmo das threads,
|
|
117
|
+
* chamado da raiz ou de qualquer worktree (RM-036, D1 do PLAN). Com o diretorio do checkout, cada
|
|
118
|
+
* um teria o seu e os dois segurariam o mesmo lease. Fora de repositorio git, `raizDoEstado`
|
|
119
|
+
* devolve a propria raiz.
|
|
120
|
+
*/
|
|
107
121
|
function dirLeases(raiz) {
|
|
108
|
-
return path.join((0, manifest_1.dirEstado)(raiz), 'leases');
|
|
122
|
+
return path.join((0, manifest_1.dirEstado)((0, estado_thread_1.raizDoEstado)(raiz)), 'leases');
|
|
109
123
|
}
|
|
110
124
|
/**
|
|
111
|
-
* I-36: o lease de execucao mora no estado CANONICO do projeto
|
|
112
|
-
*
|
|
113
|
-
* mesmo arquivo; com o diretorio do checkout, cada um teria o seu e os dois executariam juntos.
|
|
125
|
+
* I-36: o lease de execucao mora no estado CANONICO do projeto, nunca no checkout de quem chamou.
|
|
126
|
+
* Desde a RM-036 todas as familias moram la; o nome segue para quem ja o importava.
|
|
114
127
|
*/
|
|
115
128
|
function dirLeasesDeExecucao(raiz) {
|
|
116
|
-
return
|
|
129
|
+
return dirLeases(raiz);
|
|
117
130
|
}
|
|
118
131
|
/**
|
|
119
132
|
* Nome canonico de um lease de familia tipada: `path:core/src/**`, `service:5173`.
|
|
@@ -149,8 +162,191 @@ function alvoDoLease(nome) {
|
|
|
149
162
|
* inalterado, o que preserva os leases gravados pelo bloco B1.
|
|
150
163
|
*/
|
|
151
164
|
function caminhoLease(raiz, nome) {
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
return path.join(dirLeases(raiz), `${encodeURIComponent(nome)}.json`);
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// RM-036: o legado, o que a versao anterior gravou no `.orkastery/leases` de uma worktree.
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/** Diretorio de verdade: `lstat`, entao link simbolico nao conta. */
|
|
171
|
+
function diretorioDeVerdade(caminho) {
|
|
172
|
+
try {
|
|
173
|
+
return fs.lstatSync(caminho).isDirectory();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Arquivo regular de verdade, nunca pelo link simbolico. */
|
|
180
|
+
function arquivoDeVerdade(caminho) {
|
|
181
|
+
try {
|
|
182
|
+
return fs.lstatSync(caminho).isFile();
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** Texto que pode sair no terminal, no monitor ou numa correcao de comando. */
|
|
189
|
+
function textoSeguro(texto) {
|
|
190
|
+
return String(texto ?? '').replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, '');
|
|
191
|
+
}
|
|
192
|
+
/** Um argumento literal de shell, inclusive quando vem da fila canonica. */
|
|
193
|
+
function argumentoDeLease(texto) {
|
|
194
|
+
return `'${textoSeguro(texto).replace(/'/g, "'\\''")}'`;
|
|
195
|
+
}
|
|
196
|
+
/** O legado nao admite sintaxe de shell nem nomes arbitrariamente longos. */
|
|
197
|
+
function nomeLegadoSeguro(nome) {
|
|
198
|
+
return typeof nome === 'string' && /^[A-Za-z0-9][A-Za-z0-9._/*?:@+-]{0,199}$/.test(nome);
|
|
199
|
+
}
|
|
200
|
+
/** A primeira consulta abre uma unica janela, mesmo sem legado; nunca a renova. */
|
|
201
|
+
function janelaDoLegado(principal) {
|
|
202
|
+
const marcador = path.join(dirLeases(principal), '.legado');
|
|
203
|
+
try {
|
|
204
|
+
fs.mkdirSync(path.dirname(marcador), { recursive: true });
|
|
205
|
+
fs.closeSync(fs.openSync(marcador, 'wx'));
|
|
206
|
+
}
|
|
207
|
+
catch { /* EEXIST: outra chamada abriu a mesma janela; falha de leitura fecha. */ }
|
|
208
|
+
try {
|
|
209
|
+
const stat = fs.lstatSync(marcador);
|
|
210
|
+
return stat.isFile() && Date.now() - stat.mtimeMs < exports.TTL_PADRAO_MS;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* As pastas `.orkastery/leases` das worktrees registradas no git do projeto, onde a versao anterior
|
|
218
|
+
* gravava os leases pedidos de dentro delas. A lista vem do git (`.git/worktrees/<n>/gitdir`),
|
|
219
|
+
* nunca de argumento, e so entra diretorio de verdade, sem link simbolico em `.orkastery` nem em `leases`:
|
|
220
|
+
* a leitura do legado nunca e levada para fora; limpeza grava apenas marcas no canonico. O `.git` da worktree precisa apontar
|
|
221
|
+
* de volta ao registro. A janela de 30 min dispensa a varredura depois da troca de versao.
|
|
222
|
+
*/
|
|
223
|
+
function dirsLegadosDeLeases(raiz) {
|
|
224
|
+
const principal = (0, estado_thread_1.raizDoEstado)(raiz);
|
|
225
|
+
if (janelaDoLegado(principal) === false)
|
|
226
|
+
return [];
|
|
227
|
+
const registro = path.join(principal, '.git', 'worktrees');
|
|
228
|
+
let nomes;
|
|
229
|
+
try {
|
|
230
|
+
nomes = fs.readdirSync(registro);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
const real = (dir) => { try {
|
|
236
|
+
return fs.realpathSync(dir);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return path.resolve(dir);
|
|
240
|
+
} };
|
|
241
|
+
const vistos = new Set([real(dirLeases(principal))]);
|
|
242
|
+
const dirs = [];
|
|
243
|
+
for (const nome of nomes.sort()) {
|
|
244
|
+
const entrada = path.join(registro, nome);
|
|
245
|
+
if (!diretorioDeVerdade(entrada) || !arquivoDeVerdade(path.join(entrada, 'gitdir')))
|
|
246
|
+
continue;
|
|
247
|
+
let gitdir;
|
|
248
|
+
try {
|
|
249
|
+
gitdir = fs.readFileSync(path.join(entrada, 'gitdir'), 'utf8').trim();
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (!gitdir)
|
|
255
|
+
continue;
|
|
256
|
+
const gitDaWorktree = path.resolve(entrada, gitdir);
|
|
257
|
+
if (path.basename(gitDaWorktree) !== '.git' || !arquivoDeVerdade(gitDaWorktree))
|
|
258
|
+
continue;
|
|
259
|
+
try {
|
|
260
|
+
const volta = /^gitdir: (.+)\s*$/.exec(fs.readFileSync(gitDaWorktree, 'utf8').trim());
|
|
261
|
+
if (!volta || real(path.resolve(path.dirname(gitDaWorktree), volta[1].trim())) !== real(entrada))
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const estado = (0, manifest_1.dirEstado)(path.dirname(gitDaWorktree));
|
|
268
|
+
const dir = path.join(estado, 'leases');
|
|
269
|
+
if (!diretorioDeVerdade(estado) || !diretorioDeVerdade(dir) || vistos.has(real(dir)))
|
|
270
|
+
continue;
|
|
271
|
+
vistos.add(real(dir));
|
|
272
|
+
dirs.push(dir);
|
|
273
|
+
}
|
|
274
|
+
return dirs;
|
|
275
|
+
}
|
|
276
|
+
/** Legado e entrada nao confiavel: formato, nome do arquivo e prazo precisam concordar. */
|
|
277
|
+
function lerLeaseLegado(caminho) {
|
|
278
|
+
let fd;
|
|
279
|
+
try {
|
|
280
|
+
fd = fs.openSync(caminho, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
281
|
+
const stat = fs.fstatSync(fd);
|
|
282
|
+
if (!stat.isFile() || stat.size > 64 * 1024)
|
|
283
|
+
return null;
|
|
284
|
+
const lease = JSON.parse(fs.readFileSync(fd, 'utf8'));
|
|
285
|
+
if (!lease || !nomeLegadoSeguro(lease.nome) ||
|
|
286
|
+
path.basename(caminho) !== `${encodeURIComponent(lease.nome)}.json` || tipoDoLease(lease.nome) === 'exec' ||
|
|
287
|
+
typeof lease.thread !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(lease.thread) ||
|
|
288
|
+
typeof lease.motivo !== 'string' || !Number.isSafeInteger(lease.pid) || lease.pid <= 0)
|
|
289
|
+
return null;
|
|
290
|
+
const inicio = Date.parse(lease.adquiridoEm), fim = Date.parse(lease.expiraEm);
|
|
291
|
+
if (!Number.isFinite(inicio) || !Number.isFinite(fim) ||
|
|
292
|
+
new Date(inicio).toISOString() !== lease.adquiridoEm || new Date(fim).toISOString() !== lease.expiraEm ||
|
|
293
|
+
inicio > Date.now() || fim <= inicio || fim - inicio > exports.TTL_PADRAO_MS + 1_000)
|
|
294
|
+
return null;
|
|
295
|
+
// Nao transporta campos extras (como conducao) de um arquivo gravavel pela worktree.
|
|
296
|
+
return { nome: lease.nome, thread: lease.thread, motivo: '(legado)', pid: lease.pid,
|
|
297
|
+
adquiridoEm: lease.adquiridoEm, expiraEm: lease.expiraEm };
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
if (fd !== undefined)
|
|
304
|
+
fs.closeSync(fd);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Descarte da geracao dev/ino/ctime; inode reutilizado nao herda o descarte anterior. */
|
|
308
|
+
function marcaDeLegado(raiz, identidade) {
|
|
309
|
+
return path.join(dirLeases(raiz), `.legado-ignorado-${identidade.dev}-${identidade.ino}-${identidade.ctimeMs}`);
|
|
310
|
+
}
|
|
311
|
+
function legadoIgnorado(raiz, identidade) {
|
|
312
|
+
return arquivoDeVerdade(marcaDeLegado(raiz, identidade));
|
|
313
|
+
}
|
|
314
|
+
function ignorarLegado(raiz, caminho, identidade) {
|
|
315
|
+
try {
|
|
316
|
+
if (!diretorioDeVerdade(path.dirname(caminho)) || !diretorioDeVerdade(path.dirname(path.dirname(caminho))))
|
|
317
|
+
return false;
|
|
318
|
+
const atual = fs.lstatSync(caminho);
|
|
319
|
+
if (!atual.isFile() || atual.dev !== identidade.dev || atual.ino !== identidade.ino || atual.ctimeMs !== identidade.ctimeMs)
|
|
320
|
+
return false;
|
|
321
|
+
fs.writeFileSync(marcaDeLegado(raiz, identidade), '', { flag: 'wx' });
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** As copias legadas de um nome. O `exec:` nunca morou no legado (I-36). */
|
|
329
|
+
function copiasLegadas(raiz, nome) {
|
|
330
|
+
if (tipoDoLease(nome) === 'exec')
|
|
331
|
+
return [];
|
|
332
|
+
const copias = [];
|
|
333
|
+
for (const dir of dirsLegadosDeLeases(raiz)) {
|
|
334
|
+
const caminho = path.join(dir, `${encodeURIComponent(nome)}.json`);
|
|
335
|
+
if (!arquivoDeVerdade(caminho))
|
|
336
|
+
continue;
|
|
337
|
+
let identidadeLegada;
|
|
338
|
+
try {
|
|
339
|
+
identidadeLegada = fs.lstatSync(caminho);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (!identidadeLegada.isFile() || legadoIgnorado(raiz, identidadeLegada))
|
|
345
|
+
continue;
|
|
346
|
+
const lease = lerLeaseLegado(caminho);
|
|
347
|
+
copias.push({ caminho, lease: lease && lease.nome === nome ? lease : null, identidadeLegada });
|
|
348
|
+
}
|
|
349
|
+
return copias;
|
|
154
350
|
}
|
|
155
351
|
/**
|
|
156
352
|
* I-36 (D3): regrava um lease que a propria operacao segura (renovacao do prazo e conversao do
|
|
@@ -163,11 +359,15 @@ function regravarLease(raiz, lease) {
|
|
|
163
359
|
fs.writeFileSync(temporario, JSON.stringify(lease, null, 2) + '\n', { encoding: 'utf8', mode: 0o644 });
|
|
164
360
|
fs.renameSync(temporario, caminho);
|
|
165
361
|
}
|
|
166
|
-
/**
|
|
362
|
+
/**
|
|
363
|
+
* Le somente o arquivo canonico. Legado pode barrar uma aquisicao, nunca provar posse
|
|
364
|
+
* para ativacao de escrita nem substituir um arquivo canonico que sumiu durante uma liberacao.
|
|
365
|
+
*/
|
|
167
366
|
function lerLease(raiz, nome) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
367
|
+
return lerArquivoDeLease(caminhoLease(raiz, nome));
|
|
368
|
+
}
|
|
369
|
+
/** O arquivo de lease do estado canonico; ausente ou corrompido conta como lease ausente. */
|
|
370
|
+
function lerArquivoDeLease(caminho) {
|
|
171
371
|
try {
|
|
172
372
|
return JSON.parse(fs.readFileSync(caminho, 'utf8'));
|
|
173
373
|
}
|
|
@@ -175,6 +375,196 @@ function lerLease(raiz, nome) {
|
|
|
175
375
|
return null;
|
|
176
376
|
}
|
|
177
377
|
}
|
|
378
|
+
/** O criador exclusivo pode estar entre o open(wx) e a escrita do JSON. */
|
|
379
|
+
function escritaRecente(caminho) {
|
|
380
|
+
try {
|
|
381
|
+
return Date.now() - fs.lstatSync(caminho).mtimeMs < 5_000;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** Candidatos e temporarios pertencem ao mesmo namespace de PID do processo local. */
|
|
388
|
+
function lerCandidatoRetomada(dir, nome) {
|
|
389
|
+
const match = /^(\d+)-[a-f0-9-]+\.json(\.tmp)?$/.exec(nome);
|
|
390
|
+
const pid = Number(match?.[1]);
|
|
391
|
+
if (!match || !Number.isSafeInteger(pid) || pid <= 0)
|
|
392
|
+
throw Object.assign(Error('candidato de retomada invalido'), { code: 'EPERM' });
|
|
393
|
+
let fd;
|
|
394
|
+
try {
|
|
395
|
+
fd = fs.openSync(path.join(dir, nome), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
396
|
+
const stat = fs.fstatSync(fd);
|
|
397
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1024)
|
|
398
|
+
throw Object.assign(Error('candidato inseguro'), { code: 'EPERM' });
|
|
399
|
+
// SIGKILL pode deixar JSON parcial: o temporario nao participa da ordem de tickets.
|
|
400
|
+
const ticket = match[2] ? null : JSON.parse(fs.readFileSync(fd, 'utf8')).ticket;
|
|
401
|
+
if (ticket !== null && (!Number.isSafeInteger(ticket) || ticket < 0))
|
|
402
|
+
throw Object.assign(Error('ticket invalido'), { code: 'EPERM' });
|
|
403
|
+
return { nome, pid, ticket, stat };
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
if (e.code === 'ENOENT')
|
|
407
|
+
return null;
|
|
408
|
+
throw e;
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
if (fd !== undefined)
|
|
412
|
+
fs.closeSync(fd);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** Nomes nao se repetem. Ainda assim, nunca recolhe um inode substituido. */
|
|
416
|
+
function recolherCandidato(dir, c) {
|
|
417
|
+
const arquivo = path.join(dir, c.nome), atual = fs.lstatSync(arquivo, { throwIfNoEntry: false });
|
|
418
|
+
if (atual?.isFile() && atual.nlink === 1 && atual.dev === c.stat.dev && atual.ino === c.stat.ino) {
|
|
419
|
+
try {
|
|
420
|
+
fs.unlinkSync(arquivo);
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
if (e.code !== 'ENOENT')
|
|
424
|
+
throw e;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Bakery local e portatil, com prazo limitado a 30 min. Publica ticket zero e depois
|
|
430
|
+
* o ticket por rename atomico. Todo retomador participa, inclusive com flock.
|
|
431
|
+
* Candidato recolhido nao autoriza seu processo a continuar: a posse e revalidada
|
|
432
|
+
* na entrada da secao critica e imediatamente antes da conferencia final do lease.
|
|
433
|
+
*/
|
|
434
|
+
function comFilaDeRetomada(caminho, retomar) {
|
|
435
|
+
const dir = `${caminho}.retomadas`;
|
|
436
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
437
|
+
if (!fs.lstatSync(dir).isDirectory())
|
|
438
|
+
throw Object.assign(Error('fila insegura'), { code: 'EPERM' });
|
|
439
|
+
const nome = `${process.pid}-${(0, node_crypto_1.randomUUID)()}.json`, arquivo = path.join(dir, nome);
|
|
440
|
+
const prazo = Date.now() + exports.TTL_PADRAO_MS;
|
|
441
|
+
const publicar = (ticket) => {
|
|
442
|
+
const tmp = `${arquivo}.tmp`;
|
|
443
|
+
try {
|
|
444
|
+
fs.writeFileSync(tmp, JSON.stringify({ ticket }), { flag: 'wx' });
|
|
445
|
+
fs.renameSync(tmp, arquivo);
|
|
446
|
+
}
|
|
447
|
+
finally {
|
|
448
|
+
try {
|
|
449
|
+
fs.unlinkSync(tmp);
|
|
450
|
+
}
|
|
451
|
+
catch (e) {
|
|
452
|
+
if (e.code !== 'ENOENT')
|
|
453
|
+
throw e;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
let recolheuExpirado = false;
|
|
458
|
+
const candidatos = () => {
|
|
459
|
+
const resultado = [];
|
|
460
|
+
for (const entrada of fs.readdirSync(dir)) {
|
|
461
|
+
const c = lerCandidatoRetomada(dir, entrada);
|
|
462
|
+
if (!c)
|
|
463
|
+
continue;
|
|
464
|
+
if (Date.now() - c.stat.mtimeMs >= exports.TTL_PADRAO_MS) {
|
|
465
|
+
recolherCandidato(dir, c);
|
|
466
|
+
recolheuExpirado = true;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
process.kill(c.pid, 0);
|
|
471
|
+
}
|
|
472
|
+
catch (e) {
|
|
473
|
+
if (e.code === 'ESRCH') {
|
|
474
|
+
recolherCandidato(dir, c);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
// EPERM nao prova morte; o prazo limita a espera por PID inacessivel/reutilizado.
|
|
478
|
+
}
|
|
479
|
+
if (c.ticket !== null)
|
|
480
|
+
resultado.push({ nome: entrada, ticket: c.ticket });
|
|
481
|
+
}
|
|
482
|
+
return resultado;
|
|
483
|
+
};
|
|
484
|
+
try {
|
|
485
|
+
publicar(0);
|
|
486
|
+
const ticket = Math.max(0, ...candidatos().map((c) => c.ticket)) + 1;
|
|
487
|
+
if (recolheuExpirado)
|
|
488
|
+
return 'fila-expirada';
|
|
489
|
+
publicar(ticket);
|
|
490
|
+
const identidade = fs.lstatSync(arquivo);
|
|
491
|
+
const aindaCandidato = () => {
|
|
492
|
+
const atual = fs.lstatSync(arquivo, { throwIfNoEntry: false });
|
|
493
|
+
return Date.now() < prazo && !!atual?.isFile() && atual.nlink === 1 &&
|
|
494
|
+
atual.dev === identidade.dev && atual.ino === identidade.ino;
|
|
495
|
+
};
|
|
496
|
+
const outros = candidatos();
|
|
497
|
+
if (recolheuExpirado || !aindaCandidato())
|
|
498
|
+
return 'fila-expirada';
|
|
499
|
+
if (outros.some((c) => c.nome !== nome && (c.ticket === 0 || c.ticket < ticket ||
|
|
500
|
+
(c.ticket === ticket && c.nome < nome))))
|
|
501
|
+
return 'ocupado';
|
|
502
|
+
return retomar(aindaCandidato);
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
// Se ficar, outro retomador o recolhe por PID morto ou prazo; preservar resultado e erro original.
|
|
506
|
+
try {
|
|
507
|
+
fs.unlinkSync(arquivo);
|
|
508
|
+
}
|
|
509
|
+
catch { /* Melhor esforco: candidato remanescente sera recolhido. */ }
|
|
510
|
+
// rmdir e atomico e so remove uma pasta vazia; nunca remove candidato concorrente.
|
|
511
|
+
try {
|
|
512
|
+
fs.rmdirSync(dir);
|
|
513
|
+
}
|
|
514
|
+
catch { /* Melhor esforco: nao altera o resultado da retomada. */ }
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/** O fd e a fila exclusiva ficam presos ao inode antigo ate concluir o novo wx. */
|
|
518
|
+
function retomarArquivo(caminho, corpo) {
|
|
519
|
+
let fd;
|
|
520
|
+
try {
|
|
521
|
+
fd = fs.openSync(caminho, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
522
|
+
const stat = fs.fstatSync(fd);
|
|
523
|
+
if (stat.nlink === 0)
|
|
524
|
+
return 'ocupado';
|
|
525
|
+
if (!stat.isFile() || stat.nlink !== 1)
|
|
526
|
+
return 'indisponivel';
|
|
527
|
+
try {
|
|
528
|
+
const trava = (0, node_child_process_1.spawnSync)('/usr/bin/flock', ['--exclusive', '--nonblock', '3'], { stdio: ['ignore', 'pipe', 'pipe', fd], timeout: 2_000 });
|
|
529
|
+
if (!trava.error && trava.status === 1)
|
|
530
|
+
return 'ocupado';
|
|
531
|
+
}
|
|
532
|
+
catch { /* Transporte ausente ou bloqueado: a fila portatil ainda serializa. */ }
|
|
533
|
+
const descritor = fd;
|
|
534
|
+
return comFilaDeRetomada(caminho, (aindaCandidato) => {
|
|
535
|
+
let atual = null;
|
|
536
|
+
try {
|
|
537
|
+
atual = JSON.parse(fs.readFileSync(descritor, 'utf8'));
|
|
538
|
+
}
|
|
539
|
+
catch { /* corrompido */ }
|
|
540
|
+
if ((atual && !expirado(atual)) || (!atual && Date.now() - fs.fstatSync(descritor).mtimeMs < 5_000))
|
|
541
|
+
return 'ocupado';
|
|
542
|
+
const agoraNoPath = fs.lstatSync(caminho);
|
|
543
|
+
if (!agoraNoPath.isFile() || agoraNoPath.dev !== stat.dev || agoraNoPath.ino !== stat.ino)
|
|
544
|
+
return 'ocupado';
|
|
545
|
+
if (!aindaCandidato())
|
|
546
|
+
return 'fila-expirada';
|
|
547
|
+
// Reconfere imediatamente antes do unlink; a primeira leitura pode ter envelhecido.
|
|
548
|
+
const antesDeApagar = fs.lstatSync(caminho);
|
|
549
|
+
if (!antesDeApagar.isFile() || antesDeApagar.dev !== stat.dev || antesDeApagar.ino !== stat.ino)
|
|
550
|
+
return 'ocupado';
|
|
551
|
+
fs.unlinkSync(caminho);
|
|
552
|
+
fs.writeFileSync(caminho, corpo, { encoding: 'utf8', flag: 'wx' });
|
|
553
|
+
return 'retomado';
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
catch (e) {
|
|
557
|
+
if (['ENOENT', 'EEXIST'].includes(e.code ?? ''))
|
|
558
|
+
return 'ocupado';
|
|
559
|
+
if (['ELOOP', 'EACCES', 'EPERM', 'ENOSYS'].includes(e.code ?? ''))
|
|
560
|
+
return 'indisponivel';
|
|
561
|
+
throw e;
|
|
562
|
+
}
|
|
563
|
+
finally {
|
|
564
|
+
if (fd !== undefined)
|
|
565
|
+
fs.closeSync(fd);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
178
568
|
/** O lease venceu? */
|
|
179
569
|
function expirado(lease, referencia = new Date()) {
|
|
180
570
|
const fim = Date.parse(lease.expiraEm);
|
|
@@ -277,27 +667,37 @@ function adquirir(raiz, nome, opcoes) {
|
|
|
277
667
|
expiraEm: new Date(Date.now() + ttl).toISOString(),
|
|
278
668
|
};
|
|
279
669
|
const corpo = JSON.stringify(lease, null, 2) + '\n';
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
return { ok:
|
|
670
|
+
// So o legado valido e vivo barra, inclusive quando o MCP desliga a retomada do canonico.
|
|
671
|
+
let tomouLegado = false;
|
|
672
|
+
for (const copia of copiasLegadas(raiz, nome)) {
|
|
673
|
+
if (copia.lease && !expirado(copia.lease)) {
|
|
674
|
+
return { ok: false, lease: null, ocupadoPor: copia.lease, tomadoDeVencido: false };
|
|
285
675
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
676
|
+
// Vencido nao barra: adquirir nunca apaga arquivos da worktree legada.
|
|
677
|
+
if (copia.lease)
|
|
678
|
+
tomouLegado = true;
|
|
679
|
+
}
|
|
680
|
+
try {
|
|
681
|
+
// `wx` falha se o arquivo existe: e a atomicidade do lease, sem corrida.
|
|
682
|
+
fs.writeFileSync(caminho, corpo, { encoding: 'utf8', flag: 'wx' });
|
|
683
|
+
return { ok: true, lease, ocupadoPor: null, tomadoDeVencido: tomouLegado };
|
|
684
|
+
}
|
|
685
|
+
catch (e) {
|
|
686
|
+
if (e.code !== 'EEXIST')
|
|
687
|
+
throw e;
|
|
688
|
+
const atual = lerLease(raiz, nome);
|
|
689
|
+
if (opcoes.retomarVencido === false || (atual && !expirado(atual)) || (!atual && escritaRecente(caminho))) {
|
|
690
|
+
return { ok: false, lease: null, ocupadoPor: atual, tomadoDeVencido: false };
|
|
300
691
|
}
|
|
692
|
+
const retomada = retomarArquivo(caminho, corpo);
|
|
693
|
+
if (retomada === 'retomado')
|
|
694
|
+
return { ok: true, lease, ocupadoPor: null, tomadoDeVencido: true };
|
|
695
|
+
if (retomada === 'fila-expirada')
|
|
696
|
+
return { ok: false, lease: null, ocupadoPor: lerLease(raiz, nome),
|
|
697
|
+
tomadoDeVencido: false, falhaRetomada: 'lease.resume-unavailable', filaRetomadaExpirada: true };
|
|
698
|
+
if (retomada === 'indisponivel')
|
|
699
|
+
return { ok: false, lease: null, ocupadoPor: lerLease(raiz, nome),
|
|
700
|
+
tomadoDeVencido: false, falhaRetomada: 'lease.resume-unavailable' };
|
|
301
701
|
}
|
|
302
702
|
const atual = lerLease(raiz, nome);
|
|
303
703
|
return { ok: false, lease: null, ocupadoPor: atual, tomadoDeVencido: false };
|
|
@@ -305,62 +705,125 @@ function adquirir(raiz, nome, opcoes) {
|
|
|
305
705
|
/**
|
|
306
706
|
* Libera o lease. Sem `forcar`, so a thread que o segura pode liberar: liberar o lease
|
|
307
707
|
* dos outros e exatamente o bug que o lease existe para impedir.
|
|
708
|
+
*
|
|
709
|
+
* RM-036: release atua no canonico, quando presente. Sem canonico, apenas marca o inode legado
|
|
710
|
+
* como ignorado. Nunca apaga o legado nem limpa as duas origens no mesmo pedido.
|
|
308
711
|
*/
|
|
309
712
|
function liberar(raiz, nome, thread, forcar = false) {
|
|
310
|
-
const
|
|
311
|
-
|
|
713
|
+
const copias = [];
|
|
714
|
+
const canonico = caminhoLease(raiz, nome);
|
|
715
|
+
if (fs.lstatSync(canonico, { throwIfNoEntry: false })) {
|
|
716
|
+
copias.push({ caminho: canonico, lease: lerArquivoDeLease(canonico) });
|
|
717
|
+
}
|
|
718
|
+
if (copias.length === 0) {
|
|
719
|
+
for (const copia of copiasLegadas(raiz, nome))
|
|
720
|
+
copias.push(copia);
|
|
721
|
+
}
|
|
722
|
+
if (copias.length === 0) {
|
|
312
723
|
sairDaFila(raiz, nome, thread);
|
|
313
724
|
return { ok: true, detalhe: `lease ${nome} ja estava livre` };
|
|
314
725
|
}
|
|
315
|
-
|
|
726
|
+
const soltas = copias.filter((c) => forcar || c.lease?.thread === thread);
|
|
727
|
+
if (soltas.length === 0) {
|
|
316
728
|
return {
|
|
317
729
|
ok: false,
|
|
318
|
-
detalhe: `lease ${nome} pertence a thread ${
|
|
730
|
+
detalhe: `lease ${nome} pertence a thread ${textoSeguro(copias[0].lease?.thread ?? '(ilegivel)')}; use --forcar para tomar`,
|
|
319
731
|
};
|
|
320
732
|
}
|
|
321
|
-
|
|
322
|
-
|
|
733
|
+
const removidas = [];
|
|
734
|
+
for (const copia of soltas) {
|
|
735
|
+
try {
|
|
736
|
+
if (copia.identidadeLegada) {
|
|
737
|
+
if (!ignorarLegado(raiz, copia.caminho, copia.identidadeLegada))
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
else
|
|
741
|
+
fs.unlinkSync(copia.caminho);
|
|
742
|
+
removidas.push(copia);
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
continue; // Nada saiu: nao anunciar liberacao nem retirar o dono da fila.
|
|
746
|
+
}
|
|
747
|
+
if (copia.lease)
|
|
748
|
+
sairDaFila(raiz, nome, copia.lease.thread);
|
|
749
|
+
}
|
|
750
|
+
if (removidas.length === 0)
|
|
751
|
+
return { ok: false, detalhe: `lease ${nome}: nada removido ou ignorado; consulte ork lease list` };
|
|
752
|
+
const outra = copias.find((c) => !removidas.includes(c));
|
|
323
753
|
const proximo = proximoDaFila(raiz, nome);
|
|
324
754
|
const seguinte = proximo ? `; proximo da fila: thread ${proximo.thread}` : '';
|
|
325
|
-
|
|
755
|
+
const resta = outra ? `; segue a copia da thread ${textoSeguro(outra.lease?.thread ?? '(ilegivel)')}` : '';
|
|
756
|
+
const acao = removidas.every((c) => c.identidadeLegada) ? 'legado ignorado (arquivo preservado)' : 'liberado';
|
|
757
|
+
return { ok: true, detalhe: `lease ${nome} ${acao}${seguinte}${resta}` };
|
|
326
758
|
}
|
|
327
|
-
/**
|
|
328
|
-
function
|
|
329
|
-
const
|
|
330
|
-
const ler = (dir,
|
|
331
|
-
|
|
759
|
+
/** Os leases do estado canonico e os do legado das worktrees, canonico antes do legado no mesmo nome. */
|
|
760
|
+
function leasesEmDisco(raiz) {
|
|
761
|
+
const saida = [];
|
|
762
|
+
const ler = (dir, legado) => {
|
|
763
|
+
let arquivos;
|
|
764
|
+
try {
|
|
765
|
+
arquivos = fs.readdirSync(dir);
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
332
768
|
return;
|
|
333
|
-
|
|
769
|
+
}
|
|
770
|
+
for (const arquivo of arquivos) {
|
|
334
771
|
if (!arquivo.endsWith('.json') || arquivo === exports.NOME_ARQUIVO_FILA)
|
|
335
772
|
continue;
|
|
773
|
+
const caminho = path.join(dir, arquivo);
|
|
774
|
+
if (legado) {
|
|
775
|
+
let identidadeLegada;
|
|
776
|
+
try {
|
|
777
|
+
identidadeLegada = fs.lstatSync(caminho);
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
const lease = arquivoDeVerdade(caminho) ? lerLeaseLegado(caminho) : null;
|
|
783
|
+
// O `exec:` nunca morou no legado (I-36): arquivo de execucao ali nao e lease de versao nenhuma.
|
|
784
|
+
if (lease && identidadeLegada.isFile() && !legadoIgnorado(raiz, identidadeLegada) && tipoDoLease(lease.nome) !== 'exec')
|
|
785
|
+
saida.push({ lease, caminho, legado: dir, identidadeLegada });
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
336
788
|
try {
|
|
337
|
-
const lease = JSON.parse(fs.readFileSync(
|
|
789
|
+
const lease = JSON.parse(fs.readFileSync(caminho, 'utf8'));
|
|
338
790
|
if (!lease || typeof lease.nome !== 'string')
|
|
339
791
|
continue;
|
|
340
|
-
|
|
341
|
-
continue;
|
|
342
|
-
leases.push(lease);
|
|
792
|
+
saida.push({ lease, caminho, legado: null });
|
|
343
793
|
}
|
|
344
794
|
catch {
|
|
345
795
|
/* arquivo corrompido conta como lease ausente */
|
|
346
796
|
}
|
|
347
797
|
}
|
|
348
798
|
};
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
799
|
+
ler(dirLeases(raiz), false);
|
|
800
|
+
for (const dir of dirsLegadosDeLeases(raiz))
|
|
801
|
+
ler(dir, true);
|
|
802
|
+
return saida.sort((a, b) => a.lease.nome.localeCompare(b.lease.nome) || Number(a.legado !== null) - Number(b.legado !== null));
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Leases do estado canonico e legados validos consultados durante a janela da troca.
|
|
806
|
+
* Vencidos podem aparecer para diagnostico; so os vivos disputam regioes.
|
|
807
|
+
*/
|
|
808
|
+
function listarLeases(raiz) {
|
|
809
|
+
return leasesEmDisco(raiz).map((e) => e.lease);
|
|
356
810
|
}
|
|
357
811
|
/** Leases ativos (nao vencidos) que colidem com o nome pedido. */
|
|
358
812
|
function leasesColidentes(raiz, nome, thread) {
|
|
359
813
|
return listarLeases(raiz).filter((l) => !expirado(l) && l.thread !== thread && leasesColidem(nome, l.nome));
|
|
360
814
|
}
|
|
361
|
-
/**
|
|
815
|
+
/**
|
|
816
|
+
* Leases ativos segurados por uma thread, um por nome: a copia canonica antes da legada que a
|
|
817
|
+
* versao anterior deixou com o mesmo nome (RM-036).
|
|
818
|
+
*/
|
|
362
819
|
function leasesDaThread(raiz, thread) {
|
|
363
|
-
|
|
820
|
+
const nomes = new Set();
|
|
821
|
+
return listarLeases(raiz).filter((l) => {
|
|
822
|
+
if (l.thread !== thread || expirado(l) || nomes.has(l.nome))
|
|
823
|
+
return false;
|
|
824
|
+
nomes.add(l.nome);
|
|
825
|
+
return true;
|
|
826
|
+
});
|
|
364
827
|
}
|
|
365
828
|
// ---------------------------------------------------------------------------
|
|
366
829
|
// Fila por colisao.
|
|
@@ -371,23 +834,50 @@ exports.NOME_ARQUIVO_FILA = 'fila.json';
|
|
|
371
834
|
function caminhoFila(raiz) {
|
|
372
835
|
return path.join(dirLeases(raiz), exports.NOME_ARQUIVO_FILA);
|
|
373
836
|
}
|
|
374
|
-
/** Le
|
|
375
|
-
function
|
|
376
|
-
const caminho = caminhoFila(raiz);
|
|
377
|
-
if (!fs.existsSync(caminho))
|
|
378
|
-
return [];
|
|
837
|
+
/** Le um arquivo de fila: so os pedidos bem formados. Ausente ou corrompido conta como fila vazia. */
|
|
838
|
+
function lerArquivoDeFila(caminho) {
|
|
379
839
|
try {
|
|
380
840
|
const dados = JSON.parse(fs.readFileSync(caminho, 'utf8'));
|
|
381
|
-
|
|
841
|
+
if (!Array.isArray(dados))
|
|
842
|
+
return null;
|
|
843
|
+
return dados.filter((p) => !!p && typeof p === 'object' &&
|
|
844
|
+
typeof p.nome === 'string' && typeof p.thread === 'string' &&
|
|
845
|
+
typeof p.desdeEm === 'string');
|
|
382
846
|
}
|
|
383
847
|
catch {
|
|
384
|
-
return
|
|
848
|
+
return null;
|
|
385
849
|
}
|
|
386
850
|
}
|
|
851
|
+
/** A espera legada era local ao checkout: nao migra, refaz-se no proximo pedido. */
|
|
852
|
+
function lerFila(raiz) {
|
|
853
|
+
const todos = lerArquivoDeFila(caminhoFila(raiz)) ?? [];
|
|
854
|
+
const vistos = new Set();
|
|
855
|
+
return todos.sort((a, b) => a.desdeEm.localeCompare(b.desdeEm)).filter((p) => {
|
|
856
|
+
const chave = JSON.stringify([p.thread, p.nome]);
|
|
857
|
+
if (vistos.has(chave))
|
|
858
|
+
return false;
|
|
859
|
+
vistos.add(chave);
|
|
860
|
+
return true;
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Grava a fila canonica por arquivo temporario e rename (RM-036, D4): a raiz e as worktrees dividem
|
|
865
|
+
* esta fila, e a fila lida pela metade contava como vazia, sumindo com a espera de todos.
|
|
866
|
+
*/
|
|
387
867
|
function gravarFila(raiz, fila) {
|
|
388
868
|
const caminho = caminhoFila(raiz);
|
|
389
869
|
fs.mkdirSync(path.dirname(caminho), { recursive: true });
|
|
390
|
-
|
|
870
|
+
const temporario = `${caminho}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
871
|
+
try {
|
|
872
|
+
fs.writeFileSync(temporario, JSON.stringify(fila, null, 2) + '\n', 'utf8');
|
|
873
|
+
fs.renameSync(temporario, caminho);
|
|
874
|
+
}
|
|
875
|
+
finally {
|
|
876
|
+
try {
|
|
877
|
+
fs.unlinkSync(temporario);
|
|
878
|
+
}
|
|
879
|
+
catch { /* ja virou a fila */ }
|
|
880
|
+
}
|
|
391
881
|
}
|
|
392
882
|
/** Coloca o pedido na fila (idempotente por thread e nome). Devolve a posicao, 1 = proxima. */
|
|
393
883
|
function enfileirar(raiz, nome, dados) {
|
|
@@ -444,9 +934,20 @@ function adquirirRegiao(raiz, nome, opcoes) {
|
|
|
444
934
|
podarThreadsFechadas(raiz, nome, opcoes.thread);
|
|
445
935
|
}
|
|
446
936
|
catch { /* a regra de colisao abaixo decide */ }
|
|
937
|
+
// Recusar a segunda copia nao cria espera atras de si mesmo, nem atras de quem ja espera por ela.
|
|
938
|
+
const proprio = copiasLegadas(raiz, nome).find((c) => c.lease?.thread === opcoes.thread && !expirado(c.lease));
|
|
939
|
+
if (proprio?.lease) {
|
|
940
|
+
sairDaFila(raiz, nome, opcoes.thread);
|
|
941
|
+
return { ...base, ok: false, ocupadoPor: proprio.lease, esperando: false, posicaoNaFila: 0,
|
|
942
|
+
colidiuCom: proprio.lease, motivo: 'lease.busy',
|
|
943
|
+
detalhe: `o lease legado "${nome}" ja pertence a thread ${opcoes.thread}`,
|
|
944
|
+
correcao: 'aguarde o prazo do legado ou o fim da janela de 30 min; o arquivo legado e preservado' };
|
|
945
|
+
}
|
|
447
946
|
const colidentes = leasesColidentes(raiz, nome, opcoes.thread);
|
|
448
947
|
if (colidentes.length > 0) {
|
|
449
948
|
const dono = colidentes[0];
|
|
949
|
+
const legado = copiasLegadas(raiz, dono.nome).some((c) => c.lease?.thread === dono.thread &&
|
|
950
|
+
c.lease.adquiridoEm === dono.adquiridoEm);
|
|
450
951
|
const posicao = enfileirar(raiz, nome, {
|
|
451
952
|
thread: opcoes.thread,
|
|
452
953
|
motivo: opcoes.motivo,
|
|
@@ -463,8 +964,9 @@ function adquirirRegiao(raiz, nome, opcoes) {
|
|
|
463
964
|
motivo: 'lease.busy',
|
|
464
965
|
detalhe: `a regiao "${nome}" colide com "${dono.nome}", que esta com a thread ${dono.thread} ` +
|
|
465
966
|
`desde ${dono.adquiridoEm}`,
|
|
466
|
-
correcao:
|
|
467
|
-
`
|
|
967
|
+
correcao: legado ? 'aguarde o prazo do legado ou o fim da janela de 30 min; o arquivo legado e preservado' :
|
|
968
|
+
`espere a thread ${dono.thread} liberar (posicao ${posicao} na fila), ` +
|
|
969
|
+
`ou reduza o glob da sua regiao, ou libere com: ork lease release ${argumentoDeLease(dono.nome)} --forcar`,
|
|
468
970
|
};
|
|
469
971
|
}
|
|
470
972
|
// Fila vazia para a regiao? Entao adquire. Se ha fila e a vez nao e desta thread,
|
|
@@ -506,9 +1008,13 @@ function adquirirRegiao(raiz, nome, opcoes) {
|
|
|
506
1008
|
esperando: true,
|
|
507
1009
|
posicaoNaFila: posicao,
|
|
508
1010
|
colidiuCom: dono,
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
1011
|
+
falhaRetomada: r.falhaRetomada,
|
|
1012
|
+
filaRetomadaExpirada: r.filaRetomadaExpirada,
|
|
1013
|
+
motivo: r.falhaRetomada ?? 'lease.busy',
|
|
1014
|
+
detalhe: r.filaRetomadaExpirada ? `fila de retomada do lease \"${nome}\" excedeu 30 min; candidatos expirados recolhidos` : r.falhaRetomada ? `retomada indisponivel para o lease "${nome}"; use --forcar para liberar explicitamente` :
|
|
1015
|
+
`o lease "${nome}" esta com a thread ${dono?.thread ?? '(desconhecida)'}`,
|
|
1016
|
+
correcao: r.filaRetomadaExpirada ? 'consulte ork lease list; depois repita a aquisicao' : r.falhaRetomada ? `ork lease release ${argumentoDeLease(nome)} --forcar; depois repita a aquisicao` :
|
|
1017
|
+
`espere a vez (posicao ${posicao} na fila) ou: ork lease release ${argumentoDeLease(nome)} --forcar`,
|
|
512
1018
|
};
|
|
513
1019
|
}
|
|
514
1020
|
sairDaFila(raiz, nome, opcoes.thread);
|
|
@@ -527,39 +1033,104 @@ function adquirirRegiao(raiz, nome, opcoes) {
|
|
|
527
1033
|
}
|
|
528
1034
|
/** Texto de `ork lease list`: leases ativos, familias e a fila por colisao. */
|
|
529
1035
|
function tabelaDeLeases(raiz) {
|
|
530
|
-
const
|
|
1036
|
+
const emDisco = leasesEmDisco(raiz);
|
|
531
1037
|
const fila = lerFila(raiz);
|
|
532
1038
|
const linhas = ['Leases do projeto', ''];
|
|
533
|
-
|
|
1039
|
+
const principal = (0, estado_thread_1.raizDoEstado)(raiz);
|
|
1040
|
+
if (emDisco.length === 0) {
|
|
534
1041
|
linhas.push(' Nenhum lease tomado. O merge na base esta liberado.');
|
|
535
1042
|
}
|
|
536
1043
|
else {
|
|
537
|
-
for (const l of
|
|
1044
|
+
for (const { lease: l, legado } of emDisco) {
|
|
538
1045
|
const situacao = expirado(l) ? 'VENCIDO (tomavel)' : 'ativo';
|
|
539
|
-
linhas.push(` ${l.nome.padEnd(28)} [${tipoDoLease(l.nome)}] thread ${l.thread.padEnd(18)} ${situacao}`);
|
|
1046
|
+
linhas.push(` ${textoSeguro(l.nome).padEnd(28)} [${tipoDoLease(l.nome)}] thread ${textoSeguro(l.thread).padEnd(18)} ${situacao}`);
|
|
540
1047
|
linhas.push(` desde ${(0, horario_1.formatarDataHora)(l.adquiridoEm)} ate ${(0, horario_1.formatarDataHora)(l.expiraEm)} (pid ${l.pid})`);
|
|
541
|
-
linhas.push(` motivo: ${l.motivo}`);
|
|
1048
|
+
linhas.push(` motivo: ${textoSeguro(l.motivo)}`);
|
|
1049
|
+
// RM-036: a copia antiga e somente lida, ate vencer ou encerrar a janela.
|
|
1050
|
+
if (legado) {
|
|
1051
|
+
const relativo = path.relative(principal, legado);
|
|
1052
|
+
const onde = relativo && !relativo.startsWith('..') && !path.isAbsolute(relativo) ? relativo : legado;
|
|
1053
|
+
linhas.push(` legado: ${textoSeguro(onde)} (vale enquanto vivo, dentro da janela de 30 min da troca)`);
|
|
1054
|
+
}
|
|
542
1055
|
// I-36: o lease de execucao diz quem conduz, com a mesma linha das outras superficies.
|
|
543
1056
|
const conducao = (0, conducao_texto_1.conducaoDoLease)(l);
|
|
544
1057
|
if (conducao)
|
|
545
1058
|
linhas.push(` ${(0, conducao_texto_1.linhaDeConducao)(conducao)}`);
|
|
546
1059
|
}
|
|
547
1060
|
}
|
|
1061
|
+
// Diagnosticos nunca entram em listarLeases: arquivo invalido nao disputa regiao nem prova posse.
|
|
1062
|
+
for (const dir of dirsLegadosDeLeases(raiz)) {
|
|
1063
|
+
let arquivos;
|
|
1064
|
+
try {
|
|
1065
|
+
arquivos = fs.readdirSync(dir);
|
|
1066
|
+
}
|
|
1067
|
+
catch {
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
for (const arquivo of arquivos.sort()) {
|
|
1071
|
+
const caminho = path.join(dir, arquivo);
|
|
1072
|
+
if (!arquivo.endsWith('.json') || arquivo === exports.NOME_ARQUIVO_FILA ||
|
|
1073
|
+
!arquivoDeVerdade(caminho) || lerLeaseLegado(caminho))
|
|
1074
|
+
continue;
|
|
1075
|
+
try {
|
|
1076
|
+
if (legadoIgnorado(raiz, fs.lstatSync(caminho)))
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
catch {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
let nome;
|
|
1083
|
+
try {
|
|
1084
|
+
nome = decodeURIComponent(arquivo.slice(0, -5));
|
|
1085
|
+
}
|
|
1086
|
+
catch {
|
|
1087
|
+
nome = arquivo.slice(0, -5);
|
|
1088
|
+
}
|
|
1089
|
+
if (tipoDoLease(nome) === 'exec')
|
|
1090
|
+
continue;
|
|
1091
|
+
linhas.push(` legado: ${textoSeguro(arquivo)} INVALIDO ou ILEGIVEL (ignorado)`);
|
|
1092
|
+
linhas.push(' arquivo legado preservado; ignorado sem alterar o lease canonico');
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
548
1095
|
linhas.push('');
|
|
1096
|
+
// Consulta nao recolhe candidatos: mostra a fila, inclusive sobras de SIGKILL.
|
|
1097
|
+
const pasta = dirLeases(raiz);
|
|
1098
|
+
const retomadas = fs.existsSync(pasta) ? fs.readdirSync(pasta).filter((n) => n.endsWith('.json.retomadas')).sort() : [];
|
|
1099
|
+
linhas.push(`Fila de retomada: ${retomadas.length === 0 ? 'vazia' : `${retomadas.length} lease(s)`}`);
|
|
1100
|
+
for (const nome of retomadas) {
|
|
1101
|
+
const dir = path.join(pasta, nome);
|
|
1102
|
+
linhas.push(` ${textoSeguro(nome)}`);
|
|
1103
|
+
try {
|
|
1104
|
+
if (!fs.lstatSync(dir).isDirectory())
|
|
1105
|
+
throw Error('fila insegura');
|
|
1106
|
+
for (const entrada of fs.readdirSync(dir).sort()) {
|
|
1107
|
+
const c = lerCandidatoRetomada(dir, entrada);
|
|
1108
|
+
if (!c)
|
|
1109
|
+
continue;
|
|
1110
|
+
const idade = Math.max(0, Date.now() - c.stat.mtimeMs), vencido = idade >= exports.TTL_PADRAO_MS;
|
|
1111
|
+
linhas.push(` ${textoSeguro(c.nome)} pid ${c.pid} ${c.ticket === null ? 'temporario' : `ticket ${c.ticket}`} idade ${Math.floor(idade / 1000)}s`);
|
|
1112
|
+
if (vencido)
|
|
1113
|
+
linhas.push(' lease.resume-unavailable: prazo de 30 min excedido; repita a aquisicao para recolher e consulte ork lease list');
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
catch {
|
|
1117
|
+
linhas.push(' lease.resume-unavailable: fila ilegivel ou insegura; inspecione candidatos antes de repetir a aquisicao');
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
549
1120
|
const merge = fila.filter((p) => p.tipo === 'main-tree');
|
|
550
1121
|
const regiao = fila.filter((p) => p.tipo !== 'main-tree');
|
|
551
1122
|
linhas.push(`Fila de merge (lease ${exports.LEASE_MAIN_TREE}): ${merge.length === 0 ? 'vazia' : `${merge.length} na espera`}`);
|
|
552
1123
|
merge.forEach((p, i) => {
|
|
553
|
-
linhas.push(` ${i + 1}. thread ${p.thread} desde ${(0, horario_1.formatarDesde)(p.desdeEm)} (bloqueada por ${p.bloqueadaPor})`);
|
|
1124
|
+
linhas.push(` ${i + 1}. thread ${textoSeguro(p.thread)} desde ${(0, horario_1.formatarDesde)(p.desdeEm)} (bloqueada por ${textoSeguro(p.bloqueadaPor)})`);
|
|
554
1125
|
});
|
|
555
1126
|
linhas.push(`Fila por colisao de regiao: ${regiao.length === 0 ? 'vazia' : `${regiao.length} na espera`}`);
|
|
556
1127
|
regiao.forEach((p, i) => {
|
|
557
|
-
linhas.push(` ${i + 1}. thread ${p.thread} quer ${p.nome}, colide com ${p.colidiuCom} ` +
|
|
558
|
-
`(thread ${p.bloqueadaPor}) desde ${(0, horario_1.formatarDesde)(p.desdeEm)}`);
|
|
1128
|
+
linhas.push(` ${i + 1}. thread ${textoSeguro(p.thread)} quer ${textoSeguro(p.nome)}, colide com ${textoSeguro(p.colidiuCom)} ` +
|
|
1129
|
+
`(thread ${textoSeguro(p.bloqueadaPor)}) desde ${(0, horario_1.formatarDesde)(p.desdeEm)}`);
|
|
559
1130
|
});
|
|
560
|
-
if (
|
|
1131
|
+
if (emDisco.length > 0 || fila.length > 0)
|
|
561
1132
|
linhas.push((0, horario_1.legendaDoFuso)());
|
|
562
|
-
return linhas.join('\n');
|
|
1133
|
+
return linhas.map(textoSeguro).join('\n');
|
|
563
1134
|
}
|
|
564
1135
|
// ---------------------------------------------------------------------------
|
|
565
1136
|
// RM-037 (rm037noite, defeito 2): thread fechada nao segura regiao nem fila.
|
|
@@ -581,19 +1152,25 @@ function threadFechada(raiz, thread) {
|
|
|
581
1152
|
* conducao tem ciclo proprio e ja trata thread fechada como sem conducao.
|
|
582
1153
|
*/
|
|
583
1154
|
function soltarDaThread(raiz, thread) {
|
|
584
|
-
const leases =
|
|
585
|
-
|
|
1155
|
+
const leases = new Set();
|
|
1156
|
+
// RM-036: a copia canonica e as legadas das worktrees, cada uma pelo proprio arquivo.
|
|
1157
|
+
for (const { lease, caminho, legado, identidadeLegada } of leasesEmDisco(raiz)) {
|
|
586
1158
|
if (lease.thread !== thread || tipoDoLease(lease.nome) === 'exec')
|
|
587
1159
|
continue;
|
|
588
1160
|
// Achado A2 do CHECK 1: rele logo antes de apagar. Outra poda pode ter soltado este lease e uma
|
|
589
1161
|
// thread viva pode ter adquirido o mesmo nome no meio; apagar pelo caminho da listagem levaria o
|
|
590
1162
|
// lease dela. So sai o arquivo que ainda e o da thread fechada, com o mesmo carimbo.
|
|
591
|
-
const atual =
|
|
1163
|
+
const atual = legado ? (arquivoDeVerdade(caminho) ? lerLeaseLegado(caminho) : null) : lerArquivoDeLease(caminho);
|
|
592
1164
|
if (!atual || atual.thread !== thread || atual.adquiridoEm !== lease.adquiridoEm)
|
|
593
1165
|
continue;
|
|
594
1166
|
try {
|
|
595
|
-
|
|
596
|
-
|
|
1167
|
+
if (legado) {
|
|
1168
|
+
if (!identidadeLegada || !ignorarLegado(raiz, caminho, identidadeLegada))
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
else
|
|
1172
|
+
fs.unlinkSync(caminho);
|
|
1173
|
+
leases.add(lease.nome);
|
|
597
1174
|
}
|
|
598
1175
|
catch { /* outra limpeza chegou antes */ }
|
|
599
1176
|
}
|
|
@@ -601,7 +1178,7 @@ function soltarDaThread(raiz, thread) {
|
|
|
601
1178
|
const dela = fila.filter((p) => p.thread === thread);
|
|
602
1179
|
if (dela.length > 0)
|
|
603
1180
|
gravarFila(raiz, fila.filter((p) => p.thread !== thread));
|
|
604
|
-
return { leases, fila: dela.map((p) => p.nome) };
|
|
1181
|
+
return { leases: [...leases], fila: dela.map((p) => p.nome) };
|
|
605
1182
|
}
|
|
606
1183
|
/**
|
|
607
1184
|
* Solta o que a thread fechada deixou e registra no ledger dela: `lease_released` por lease e
|