@orkastery/cli 0.5.0 → 0.5.3
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 +11 -2
- package/adapters/claude-code/agents/ork-master.md +2 -0
- package/adapters/claude-code/commands/master.md +8 -0
- package/adapters/claude-code/commands/onboarding.md +3 -2
- package/adapters/claude-code/commands/ork.md +9 -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 +6 -5
- package/adapters/openclaw/README.md +35 -4
- package/adapters/openclaw/dist/hitl-ingress.js +2 -2
- package/adapters/openclaw/dist/index.js +47 -12
- 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 +50 -12
- package/assets/orkmind_bridge.py +117 -9
- package/dist/adapters/claude-bg.js +59 -11
- package/dist/adapters/codex.js +4 -3
- package/dist/agents-md.js +1 -0
- package/dist/auditrun.js +6 -2
- package/dist/board.js +16 -14
- package/dist/branch-de-estado.js +66 -10
- package/dist/busca-semantica.js +52 -12
- package/dist/canarios-rm057.js +149 -0
- package/dist/canarios.js +2 -0
- package/dist/ci.js +8 -1
- package/dist/claim-lint.js +3 -1
- package/dist/claims.js +39 -1
- package/dist/company-brain-context.js +158 -22
- package/dist/docs.js +110 -3
- package/dist/doctor-pulse-cron.js +194 -0
- package/dist/doctor-rede.js +149 -0
- package/dist/doctor.js +294 -37
- package/dist/entrega-pr.js +27 -8
- package/dist/esgotamentos.js +181 -0
- package/dist/experiencia.js +12 -2
- package/dist/fabrica-estado.js +35 -9
- package/dist/fabrica-publicar.js +6 -0
- package/dist/fechamento.js +20 -66
- package/dist/fix.js +3 -3
- package/dist/forja.js +31 -7
- package/dist/gates.js +5 -1
- package/dist/hitl-contract.js +59 -4
- package/dist/hitl-curto.js +11 -6
- package/dist/hitl-gates.js +5 -0
- package/dist/hitl-lote.js +5 -2
- package/dist/hitl-resumo.js +33 -1
- package/dist/hitl-tempo-parado.js +201 -0
- package/dist/hitl-texto-livre.js +5 -5
- package/dist/horario.js +27 -0
- package/dist/hosts.js +59 -13
- package/dist/impedimento.js +205 -0
- package/dist/index.js +433 -88
- package/dist/indice-vetorial.js +87 -10
- package/dist/indice.js +3 -1
- package/dist/init.js +28 -5
- package/dist/integracoes-locais.js +4 -0
- package/dist/intelligence-graph-cli.js +267 -30
- package/dist/intelligence-graph-contexto.js +215 -0
- package/dist/intelligence-graph-contract.js +71 -12
- package/dist/intelligence-graph-extract-md.js +41 -23
- package/dist/intelligence-graph-extract-ts.js +181 -11
- package/dist/intelligence-graph-extract.js +186 -9
- package/dist/intelligence-graph-index.js +205 -24
- package/dist/intelligence-graph-parsers.js +6 -4
- package/dist/intelligence-graph-repo.js +10 -0
- package/dist/leases.js +680 -87
- package/dist/ledger-stats.js +15 -1
- package/dist/ledger.js +12 -0
- package/dist/licoes.js +6 -4
- package/dist/liveness.js +2 -1
- package/dist/maestro-runtime.js +7 -1
- package/dist/manifest.js +44 -1
- package/dist/master-nota.js +14 -5
- package/dist/master.js +129 -12
- package/dist/mcp-artifacts.js +36 -3
- package/dist/mcp-git.js +80 -15
- package/dist/mcp-grafo-worker.js +93 -0
- package/dist/mcp-grafo.js +279 -0
- package/dist/mcp-install.js +2 -1
- package/dist/mcp-server.js +23 -1
- package/dist/mcp-ship.js +2 -0
- package/dist/mcp-verify.js +3 -1
- package/dist/memoria.js +48 -6
- package/dist/network-roadmap.js +195 -20
- package/dist/ocupacao.js +17 -5
- package/dist/onboarding.js +9 -4
- package/dist/orkmind.js +90 -5
- package/dist/orquestracao.js +14 -11
- package/dist/parado-no-condutor.js +1089 -0
- package/dist/phase.js +68 -15
- package/dist/policies.js +79 -3
- package/dist/portfolio.js +4 -1
- package/dist/projeto-alvo.js +5 -1
- package/dist/prompts.js +36 -1
- package/dist/prova-ativacao.js +414 -0
- package/dist/pulse-delivery.js +60 -7
- package/dist/pulse.js +99 -3
- package/dist/ratelimit.js +20 -4
- package/dist/recall.js +3 -1
- 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 +336 -0
- package/dist/rede.js +980 -0
- package/dist/retry.js +69 -2
- package/dist/roadmap-status.js +162 -7
- package/dist/runtime-context.js +5 -1
- package/dist/runtime-profiles.js +22 -1
- package/dist/saida-segura.js +58 -0
- package/dist/session-watcher-claude.js +113 -23
- package/dist/sessoes-contas.js +187 -0
- package/dist/sessoes-inventario.js +6 -12
- package/dist/sessoes.js +87 -3
- package/dist/ship.js +110 -20
- package/dist/thread.js +212 -14
- package/dist/util.js +71 -4
- package/dist/verify.js +2 -2
- package/dist/worktree.js +34 -8
- package/eval/casos/onboarding.json +1 -1
- package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
- package/package.json +7 -3
- package/references/definition-of-done.md +1 -1
- package/skills/README.md +1 -1
- package/skills/core/onboarding/SKILL.md +3 -2
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- package/skills/phases/master-metrics/SKILL.md +8 -3
- package/skills/phases/ship-release/SKILL.md +6 -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;
|
|
@@ -78,8 +85,11 @@ exports.adquirirRegiao = adquirirRegiao;
|
|
|
78
85
|
exports.tabelaDeLeases = tabelaDeLeases;
|
|
79
86
|
exports.soltarDaThread = soltarDaThread;
|
|
80
87
|
exports.soltarThreadFechada = soltarThreadFechada;
|
|
88
|
+
exports.podarRegioesDeThreadsFechadas = podarRegioesDeThreadsFechadas;
|
|
81
89
|
const fs = __importStar(require("node:fs"));
|
|
82
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");
|
|
83
93
|
const manifest_1 = require("./manifest");
|
|
84
94
|
const estado_thread_1 = require("./estado-thread");
|
|
85
95
|
const util_1 = require("./util");
|
|
@@ -102,17 +112,21 @@ exports.FAMILIAS_DE_LEASE = {
|
|
|
102
112
|
};
|
|
103
113
|
/** I-36 (D1): prefixo da familia de execucao, `exec:<thread>`. */
|
|
104
114
|
exports.PREFIXO_EXEC = 'exec';
|
|
105
|
-
/**
|
|
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
|
+
*/
|
|
106
121
|
function dirLeases(raiz) {
|
|
107
|
-
return path.join((0, manifest_1.dirEstado)(raiz), 'leases');
|
|
122
|
+
return path.join((0, manifest_1.dirEstado)((0, estado_thread_1.raizDoEstado)(raiz)), 'leases');
|
|
108
123
|
}
|
|
109
124
|
/**
|
|
110
|
-
* I-36: o lease de execucao mora no estado CANONICO do projeto
|
|
111
|
-
*
|
|
112
|
-
* 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.
|
|
113
127
|
*/
|
|
114
128
|
function dirLeasesDeExecucao(raiz) {
|
|
115
|
-
return
|
|
129
|
+
return dirLeases(raiz);
|
|
116
130
|
}
|
|
117
131
|
/**
|
|
118
132
|
* Nome canonico de um lease de familia tipada: `path:core/src/**`, `service:5173`.
|
|
@@ -148,8 +162,191 @@ function alvoDoLease(nome) {
|
|
|
148
162
|
* inalterado, o que preserva os leases gravados pelo bloco B1.
|
|
149
163
|
*/
|
|
150
164
|
function caminhoLease(raiz, nome) {
|
|
151
|
-
|
|
152
|
-
|
|
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;
|
|
153
350
|
}
|
|
154
351
|
/**
|
|
155
352
|
* I-36 (D3): regrava um lease que a propria operacao segura (renovacao do prazo e conversao do
|
|
@@ -162,11 +359,15 @@ function regravarLease(raiz, lease) {
|
|
|
162
359
|
fs.writeFileSync(temporario, JSON.stringify(lease, null, 2) + '\n', { encoding: 'utf8', mode: 0o644 });
|
|
163
360
|
fs.renameSync(temporario, caminho);
|
|
164
361
|
}
|
|
165
|
-
/**
|
|
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
|
+
*/
|
|
166
366
|
function lerLease(raiz, nome) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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) {
|
|
170
371
|
try {
|
|
171
372
|
return JSON.parse(fs.readFileSync(caminho, 'utf8'));
|
|
172
373
|
}
|
|
@@ -174,6 +375,196 @@ function lerLease(raiz, nome) {
|
|
|
174
375
|
return null;
|
|
175
376
|
}
|
|
176
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
|
+
}
|
|
177
568
|
/** O lease venceu? */
|
|
178
569
|
function expirado(lease, referencia = new Date()) {
|
|
179
570
|
const fim = Date.parse(lease.expiraEm);
|
|
@@ -276,27 +667,37 @@ function adquirir(raiz, nome, opcoes) {
|
|
|
276
667
|
expiraEm: new Date(Date.now() + ttl).toISOString(),
|
|
277
668
|
};
|
|
278
669
|
const corpo = JSON.stringify(lease, null, 2) + '\n';
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
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 };
|
|
284
675
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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 };
|
|
299
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' };
|
|
300
701
|
}
|
|
301
702
|
const atual = lerLease(raiz, nome);
|
|
302
703
|
return { ok: false, lease: null, ocupadoPor: atual, tomadoDeVencido: false };
|
|
@@ -304,62 +705,125 @@ function adquirir(raiz, nome, opcoes) {
|
|
|
304
705
|
/**
|
|
305
706
|
* Libera o lease. Sem `forcar`, so a thread que o segura pode liberar: liberar o lease
|
|
306
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.
|
|
307
711
|
*/
|
|
308
712
|
function liberar(raiz, nome, thread, forcar = false) {
|
|
309
|
-
const
|
|
310
|
-
|
|
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) {
|
|
311
723
|
sairDaFila(raiz, nome, thread);
|
|
312
724
|
return { ok: true, detalhe: `lease ${nome} ja estava livre` };
|
|
313
725
|
}
|
|
314
|
-
|
|
726
|
+
const soltas = copias.filter((c) => forcar || c.lease?.thread === thread);
|
|
727
|
+
if (soltas.length === 0) {
|
|
315
728
|
return {
|
|
316
729
|
ok: false,
|
|
317
|
-
detalhe: `lease ${nome} pertence a thread ${
|
|
730
|
+
detalhe: `lease ${nome} pertence a thread ${textoSeguro(copias[0].lease?.thread ?? '(ilegivel)')}; use --forcar para tomar`,
|
|
318
731
|
};
|
|
319
732
|
}
|
|
320
|
-
|
|
321
|
-
|
|
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));
|
|
322
753
|
const proximo = proximoDaFila(raiz, nome);
|
|
323
754
|
const seguinte = proximo ? `; proximo da fila: thread ${proximo.thread}` : '';
|
|
324
|
-
|
|
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}` };
|
|
325
758
|
}
|
|
326
|
-
/**
|
|
327
|
-
function
|
|
328
|
-
const
|
|
329
|
-
const ler = (dir,
|
|
330
|
-
|
|
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 {
|
|
331
768
|
return;
|
|
332
|
-
|
|
769
|
+
}
|
|
770
|
+
for (const arquivo of arquivos) {
|
|
333
771
|
if (!arquivo.endsWith('.json') || arquivo === exports.NOME_ARQUIVO_FILA)
|
|
334
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
|
+
}
|
|
335
788
|
try {
|
|
336
|
-
const lease = JSON.parse(fs.readFileSync(
|
|
789
|
+
const lease = JSON.parse(fs.readFileSync(caminho, 'utf8'));
|
|
337
790
|
if (!lease || typeof lease.nome !== 'string')
|
|
338
791
|
continue;
|
|
339
|
-
|
|
340
|
-
continue;
|
|
341
|
-
leases.push(lease);
|
|
792
|
+
saida.push({ lease, caminho, legado: null });
|
|
342
793
|
}
|
|
343
794
|
catch {
|
|
344
795
|
/* arquivo corrompido conta como lease ausente */
|
|
345
796
|
}
|
|
346
797
|
}
|
|
347
798
|
};
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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);
|
|
355
810
|
}
|
|
356
811
|
/** Leases ativos (nao vencidos) que colidem com o nome pedido. */
|
|
357
812
|
function leasesColidentes(raiz, nome, thread) {
|
|
358
813
|
return listarLeases(raiz).filter((l) => !expirado(l) && l.thread !== thread && leasesColidem(nome, l.nome));
|
|
359
814
|
}
|
|
360
|
-
/**
|
|
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
|
+
*/
|
|
361
819
|
function leasesDaThread(raiz, thread) {
|
|
362
|
-
|
|
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
|
+
});
|
|
363
827
|
}
|
|
364
828
|
// ---------------------------------------------------------------------------
|
|
365
829
|
// Fila por colisao.
|
|
@@ -370,23 +834,50 @@ exports.NOME_ARQUIVO_FILA = 'fila.json';
|
|
|
370
834
|
function caminhoFila(raiz) {
|
|
371
835
|
return path.join(dirLeases(raiz), exports.NOME_ARQUIVO_FILA);
|
|
372
836
|
}
|
|
373
|
-
/** Le
|
|
374
|
-
function
|
|
375
|
-
const caminho = caminhoFila(raiz);
|
|
376
|
-
if (!fs.existsSync(caminho))
|
|
377
|
-
return [];
|
|
837
|
+
/** Le um arquivo de fila: so os pedidos bem formados. Ausente ou corrompido conta como fila vazia. */
|
|
838
|
+
function lerArquivoDeFila(caminho) {
|
|
378
839
|
try {
|
|
379
840
|
const dados = JSON.parse(fs.readFileSync(caminho, 'utf8'));
|
|
380
|
-
|
|
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');
|
|
381
846
|
}
|
|
382
847
|
catch {
|
|
383
|
-
return
|
|
848
|
+
return null;
|
|
384
849
|
}
|
|
385
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
|
+
*/
|
|
386
867
|
function gravarFila(raiz, fila) {
|
|
387
868
|
const caminho = caminhoFila(raiz);
|
|
388
869
|
fs.mkdirSync(path.dirname(caminho), { recursive: true });
|
|
389
|
-
|
|
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
|
+
}
|
|
390
881
|
}
|
|
391
882
|
/** Coloca o pedido na fila (idempotente por thread e nome). Devolve a posicao, 1 = proxima. */
|
|
392
883
|
function enfileirar(raiz, nome, dados) {
|
|
@@ -443,9 +934,20 @@ function adquirirRegiao(raiz, nome, opcoes) {
|
|
|
443
934
|
podarThreadsFechadas(raiz, nome, opcoes.thread);
|
|
444
935
|
}
|
|
445
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
|
+
}
|
|
446
946
|
const colidentes = leasesColidentes(raiz, nome, opcoes.thread);
|
|
447
947
|
if (colidentes.length > 0) {
|
|
448
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);
|
|
449
951
|
const posicao = enfileirar(raiz, nome, {
|
|
450
952
|
thread: opcoes.thread,
|
|
451
953
|
motivo: opcoes.motivo,
|
|
@@ -462,8 +964,9 @@ function adquirirRegiao(raiz, nome, opcoes) {
|
|
|
462
964
|
motivo: 'lease.busy',
|
|
463
965
|
detalhe: `a regiao "${nome}" colide com "${dono.nome}", que esta com a thread ${dono.thread} ` +
|
|
464
966
|
`desde ${dono.adquiridoEm}`,
|
|
465
|
-
correcao:
|
|
466
|
-
`
|
|
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`,
|
|
467
970
|
};
|
|
468
971
|
}
|
|
469
972
|
// Fila vazia para a regiao? Entao adquire. Se ha fila e a vez nao e desta thread,
|
|
@@ -505,9 +1008,13 @@ function adquirirRegiao(raiz, nome, opcoes) {
|
|
|
505
1008
|
esperando: true,
|
|
506
1009
|
posicaoNaFila: posicao,
|
|
507
1010
|
colidiuCom: dono,
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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`,
|
|
511
1018
|
};
|
|
512
1019
|
}
|
|
513
1020
|
sairDaFila(raiz, nome, opcoes.thread);
|
|
@@ -526,39 +1033,104 @@ function adquirirRegiao(raiz, nome, opcoes) {
|
|
|
526
1033
|
}
|
|
527
1034
|
/** Texto de `ork lease list`: leases ativos, familias e a fila por colisao. */
|
|
528
1035
|
function tabelaDeLeases(raiz) {
|
|
529
|
-
const
|
|
1036
|
+
const emDisco = leasesEmDisco(raiz);
|
|
530
1037
|
const fila = lerFila(raiz);
|
|
531
1038
|
const linhas = ['Leases do projeto', ''];
|
|
532
|
-
|
|
1039
|
+
const principal = (0, estado_thread_1.raizDoEstado)(raiz);
|
|
1040
|
+
if (emDisco.length === 0) {
|
|
533
1041
|
linhas.push(' Nenhum lease tomado. O merge na base esta liberado.');
|
|
534
1042
|
}
|
|
535
1043
|
else {
|
|
536
|
-
for (const l of
|
|
1044
|
+
for (const { lease: l, legado } of emDisco) {
|
|
537
1045
|
const situacao = expirado(l) ? 'VENCIDO (tomavel)' : 'ativo';
|
|
538
|
-
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}`);
|
|
539
1047
|
linhas.push(` desde ${(0, horario_1.formatarDataHora)(l.adquiridoEm)} ate ${(0, horario_1.formatarDataHora)(l.expiraEm)} (pid ${l.pid})`);
|
|
540
|
-
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
|
+
}
|
|
541
1055
|
// I-36: o lease de execucao diz quem conduz, com a mesma linha das outras superficies.
|
|
542
1056
|
const conducao = (0, conducao_texto_1.conducaoDoLease)(l);
|
|
543
1057
|
if (conducao)
|
|
544
1058
|
linhas.push(` ${(0, conducao_texto_1.linhaDeConducao)(conducao)}`);
|
|
545
1059
|
}
|
|
546
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
|
+
}
|
|
547
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
|
+
}
|
|
548
1120
|
const merge = fila.filter((p) => p.tipo === 'main-tree');
|
|
549
1121
|
const regiao = fila.filter((p) => p.tipo !== 'main-tree');
|
|
550
1122
|
linhas.push(`Fila de merge (lease ${exports.LEASE_MAIN_TREE}): ${merge.length === 0 ? 'vazia' : `${merge.length} na espera`}`);
|
|
551
1123
|
merge.forEach((p, i) => {
|
|
552
|
-
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)})`);
|
|
553
1125
|
});
|
|
554
1126
|
linhas.push(`Fila por colisao de regiao: ${regiao.length === 0 ? 'vazia' : `${regiao.length} na espera`}`);
|
|
555
1127
|
regiao.forEach((p, i) => {
|
|
556
|
-
linhas.push(` ${i + 1}. thread ${p.thread} quer ${p.nome}, colide com ${p.colidiuCom} ` +
|
|
557
|
-
`(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)}`);
|
|
558
1130
|
});
|
|
559
|
-
if (
|
|
1131
|
+
if (emDisco.length > 0 || fila.length > 0)
|
|
560
1132
|
linhas.push((0, horario_1.legendaDoFuso)());
|
|
561
|
-
return linhas.join('\n');
|
|
1133
|
+
return linhas.map(textoSeguro).join('\n');
|
|
562
1134
|
}
|
|
563
1135
|
// ---------------------------------------------------------------------------
|
|
564
1136
|
// RM-037 (rm037noite, defeito 2): thread fechada nao segura regiao nem fila.
|
|
@@ -580,19 +1152,25 @@ function threadFechada(raiz, thread) {
|
|
|
580
1152
|
* conducao tem ciclo proprio e ja trata thread fechada como sem conducao.
|
|
581
1153
|
*/
|
|
582
1154
|
function soltarDaThread(raiz, thread) {
|
|
583
|
-
const leases =
|
|
584
|
-
|
|
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)) {
|
|
585
1158
|
if (lease.thread !== thread || tipoDoLease(lease.nome) === 'exec')
|
|
586
1159
|
continue;
|
|
587
1160
|
// Achado A2 do CHECK 1: rele logo antes de apagar. Outra poda pode ter soltado este lease e uma
|
|
588
1161
|
// thread viva pode ter adquirido o mesmo nome no meio; apagar pelo caminho da listagem levaria o
|
|
589
1162
|
// lease dela. So sai o arquivo que ainda e o da thread fechada, com o mesmo carimbo.
|
|
590
|
-
const atual =
|
|
1163
|
+
const atual = legado ? (arquivoDeVerdade(caminho) ? lerLeaseLegado(caminho) : null) : lerArquivoDeLease(caminho);
|
|
591
1164
|
if (!atual || atual.thread !== thread || atual.adquiridoEm !== lease.adquiridoEm)
|
|
592
1165
|
continue;
|
|
593
1166
|
try {
|
|
594
|
-
|
|
595
|
-
|
|
1167
|
+
if (legado) {
|
|
1168
|
+
if (!identidadeLegada || !ignorarLegado(raiz, caminho, identidadeLegada))
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
else
|
|
1172
|
+
fs.unlinkSync(caminho);
|
|
1173
|
+
leases.add(lease.nome);
|
|
596
1174
|
}
|
|
597
1175
|
catch { /* outra limpeza chegou antes */ }
|
|
598
1176
|
}
|
|
@@ -600,7 +1178,7 @@ function soltarDaThread(raiz, thread) {
|
|
|
600
1178
|
const dela = fila.filter((p) => p.thread === thread);
|
|
601
1179
|
if (dela.length > 0)
|
|
602
1180
|
gravarFila(raiz, fila.filter((p) => p.thread !== thread));
|
|
603
|
-
return { leases, fila: dela.map((p) => p.nome) };
|
|
1181
|
+
return { leases: [...leases], fila: dela.map((p) => p.nome) };
|
|
604
1182
|
}
|
|
605
1183
|
/**
|
|
606
1184
|
* Solta o que a thread fechada deixou e registra no ledger dela: `lease_released` por lease e
|
|
@@ -634,3 +1212,18 @@ function podarThreadsFechadas(raiz, nome, quem) {
|
|
|
634
1212
|
soltarThreadFechada(raiz, thread, 'poda', quem);
|
|
635
1213
|
}
|
|
636
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* RM-037 (fatia 3, defeito 4): a mesma poda, para as conferencias previas do MCP. O `ork_git_commit`
|
|
1217
|
+
* conferia `lerLease` antes do `adquirirRegiao` e recusava com `lease.busy` o lease que a ork-companybrai3
|
|
1218
|
+
* (fechada) deixou, e soltar o lease de outra thread exigia o dono. Lease e fila de thread fechada sao
|
|
1219
|
+
* orfaos: saem aqui, com registro no ledger dela. Lease de thread aberta, vencido ou nao, e o da propria
|
|
1220
|
+
* thread seguem como estao, e a conferencia de quem chama decide. De melhor esforco, como no `adquirirRegiao`.
|
|
1221
|
+
*/
|
|
1222
|
+
function podarRegioesDeThreadsFechadas(raiz, nomes, quem) {
|
|
1223
|
+
for (const nome of nomes) {
|
|
1224
|
+
try {
|
|
1225
|
+
podarThreadsFechadas(raiz, nome, quem);
|
|
1226
|
+
}
|
|
1227
|
+
catch { /* a conferencia de quem chama decide */ }
|
|
1228
|
+
}
|
|
1229
|
+
}
|