@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.
Files changed (131) hide show
  1. package/adapters/claude-code/README.md +11 -2
  2. package/adapters/claude-code/agents/ork-master.md +2 -0
  3. package/adapters/claude-code/commands/master.md +8 -0
  4. package/adapters/claude-code/commands/onboarding.md +3 -2
  5. package/adapters/claude-code/commands/ork.md +9 -0
  6. package/adapters/codex/skills/ork/SKILL.md +18 -3
  7. package/adapters/hermes/README.md +1 -0
  8. package/adapters/hermes/bin/ork-network-status.sh +10 -0
  9. package/adapters/hermes/hermes.plugin.json +1 -0
  10. package/adapters/hermes/hitl-ingress/__init__.py +2 -2
  11. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +6 -5
  12. package/adapters/openclaw/README.md +35 -4
  13. package/adapters/openclaw/dist/hitl-ingress.js +2 -2
  14. package/adapters/openclaw/dist/index.js +47 -12
  15. package/adapters/openclaw/openclaw.plugin.json +5 -1
  16. package/adapters/openclaw/package.json +2 -2
  17. package/adapters/openclaw/src/hitl-ingress.ts +2 -2
  18. package/adapters/openclaw/src/index.ts +50 -12
  19. package/assets/orkmind_bridge.py +117 -9
  20. package/dist/adapters/claude-bg.js +59 -11
  21. package/dist/adapters/codex.js +4 -3
  22. package/dist/agents-md.js +1 -0
  23. package/dist/auditrun.js +6 -2
  24. package/dist/board.js +16 -14
  25. package/dist/branch-de-estado.js +66 -10
  26. package/dist/busca-semantica.js +52 -12
  27. package/dist/canarios-rm057.js +149 -0
  28. package/dist/canarios.js +2 -0
  29. package/dist/ci.js +8 -1
  30. package/dist/claim-lint.js +3 -1
  31. package/dist/claims.js +39 -1
  32. package/dist/company-brain-context.js +158 -22
  33. package/dist/docs.js +110 -3
  34. package/dist/doctor-pulse-cron.js +194 -0
  35. package/dist/doctor-rede.js +149 -0
  36. package/dist/doctor.js +294 -37
  37. package/dist/entrega-pr.js +27 -8
  38. package/dist/esgotamentos.js +181 -0
  39. package/dist/experiencia.js +12 -2
  40. package/dist/fabrica-estado.js +35 -9
  41. package/dist/fabrica-publicar.js +6 -0
  42. package/dist/fechamento.js +20 -66
  43. package/dist/fix.js +3 -3
  44. package/dist/forja.js +31 -7
  45. package/dist/gates.js +5 -1
  46. package/dist/hitl-contract.js +59 -4
  47. package/dist/hitl-curto.js +11 -6
  48. package/dist/hitl-gates.js +5 -0
  49. package/dist/hitl-lote.js +5 -2
  50. package/dist/hitl-resumo.js +33 -1
  51. package/dist/hitl-tempo-parado.js +201 -0
  52. package/dist/hitl-texto-livre.js +5 -5
  53. package/dist/horario.js +27 -0
  54. package/dist/hosts.js +59 -13
  55. package/dist/impedimento.js +205 -0
  56. package/dist/index.js +433 -88
  57. package/dist/indice-vetorial.js +87 -10
  58. package/dist/indice.js +3 -1
  59. package/dist/init.js +28 -5
  60. package/dist/integracoes-locais.js +4 -0
  61. package/dist/intelligence-graph-cli.js +267 -30
  62. package/dist/intelligence-graph-contexto.js +215 -0
  63. package/dist/intelligence-graph-contract.js +71 -12
  64. package/dist/intelligence-graph-extract-md.js +41 -23
  65. package/dist/intelligence-graph-extract-ts.js +181 -11
  66. package/dist/intelligence-graph-extract.js +186 -9
  67. package/dist/intelligence-graph-index.js +205 -24
  68. package/dist/intelligence-graph-parsers.js +6 -4
  69. package/dist/intelligence-graph-repo.js +10 -0
  70. package/dist/leases.js +680 -87
  71. package/dist/ledger-stats.js +15 -1
  72. package/dist/ledger.js +12 -0
  73. package/dist/licoes.js +6 -4
  74. package/dist/liveness.js +2 -1
  75. package/dist/maestro-runtime.js +7 -1
  76. package/dist/manifest.js +44 -1
  77. package/dist/master-nota.js +14 -5
  78. package/dist/master.js +129 -12
  79. package/dist/mcp-artifacts.js +36 -3
  80. package/dist/mcp-git.js +80 -15
  81. package/dist/mcp-grafo-worker.js +93 -0
  82. package/dist/mcp-grafo.js +279 -0
  83. package/dist/mcp-install.js +2 -1
  84. package/dist/mcp-server.js +23 -1
  85. package/dist/mcp-ship.js +2 -0
  86. package/dist/mcp-verify.js +3 -1
  87. package/dist/memoria.js +48 -6
  88. package/dist/network-roadmap.js +195 -20
  89. package/dist/ocupacao.js +17 -5
  90. package/dist/onboarding.js +9 -4
  91. package/dist/orkmind.js +90 -5
  92. package/dist/orquestracao.js +14 -11
  93. package/dist/parado-no-condutor.js +1089 -0
  94. package/dist/phase.js +68 -15
  95. package/dist/policies.js +79 -3
  96. package/dist/portfolio.js +4 -1
  97. package/dist/projeto-alvo.js +5 -1
  98. package/dist/prompts.js +36 -1
  99. package/dist/prova-ativacao.js +414 -0
  100. package/dist/pulse-delivery.js +60 -7
  101. package/dist/pulse.js +99 -3
  102. package/dist/ratelimit.js +20 -4
  103. package/dist/recall.js +3 -1
  104. package/dist/rede-adesao.js +399 -0
  105. package/dist/rede-forja.js +289 -0
  106. package/dist/rede-projetos.js +239 -0
  107. package/dist/rede-status.js +336 -0
  108. package/dist/rede.js +980 -0
  109. package/dist/retry.js +69 -2
  110. package/dist/roadmap-status.js +162 -7
  111. package/dist/runtime-context.js +5 -1
  112. package/dist/runtime-profiles.js +22 -1
  113. package/dist/saida-segura.js +58 -0
  114. package/dist/session-watcher-claude.js +113 -23
  115. package/dist/sessoes-contas.js +187 -0
  116. package/dist/sessoes-inventario.js +6 -12
  117. package/dist/sessoes.js +87 -3
  118. package/dist/ship.js +110 -20
  119. package/dist/thread.js +212 -14
  120. package/dist/util.js +71 -4
  121. package/dist/verify.js +2 -2
  122. package/dist/worktree.js +34 -8
  123. package/eval/casos/onboarding.json +1 -1
  124. package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
  125. package/package.json +7 -3
  126. package/references/definition-of-done.md +1 -1
  127. package/skills/README.md +1 -1
  128. package/skills/core/onboarding/SKILL.md +3 -2
  129. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  130. package/skills/phases/master-metrics/SKILL.md +8 -3
  131. package/skills/phases/ship-release/SKILL.md +6 -3
@@ -38,6 +38,7 @@ exports.normalizarNativo = normalizarNativo;
38
38
  exports.stopCorrelacionado = stopCorrelacionado;
39
39
  exports.bloqueioPendente = bloqueioPendente;
40
40
  exports.classificarSessaoClaude = classificarSessaoClaude;
41
+ exports.ultimoErroDeApi = ultimoErroDeApi;
41
42
  exports.falhaDeContaDaTranscricao = falhaDeContaDaTranscricao;
42
43
  exports.comFalhaDeConta = comFalhaDeConta;
43
44
  exports.processoNativo = processoNativo;
@@ -268,22 +269,27 @@ function textoDoErroDeApi(e) {
268
269
  const codigo = typeof e.error === 'string' ? e.error : '';
269
270
  return (0, hitl_1.redigirSegredos)([codigo, textos.filter(Boolean).join(' ')].filter(Boolean).join(': ')).slice(0, 4096);
270
271
  }
272
+ const UUID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
273
+ /** A transcricao da sessao no diretorio do perfil (ou no implicito, sem perfil); `null` com sessao que nao e uuid. */
274
+ function arquivoDaTranscricao(perfil, cwd, sessionId) {
275
+ if (!UUID.test(sessionId))
276
+ return null;
277
+ return path.join((0, runtime_profiles_1.diretorioEfetivo)('claude-bg', perfil), 'projects', path.resolve(cwd).replace(/[^a-zA-Z0-9-]/g, '-'), `${sessionId}.jsonl`);
278
+ }
271
279
  /**
272
- * I-33 (D11b): falha de conta registrada pelo proprio Claude Code na transcricao da sessao, no
273
- * diretorio do perfil (ou no implicito, sem perfil). So entram linhas marcadas como erro de API
274
- * (`isApiErrorMessage` ou campo `error`): texto do agente nunca vira motivo. Leitura limitada
275
- * da cauda, sem seguir link; ausencia de transcricao e ausencia de evidencia. Vale o ULTIMO erro de API
276
- * (N3), o que encerrou a sessao. O codigo `rate_limit`
277
- * do Claude Code cobre tanto o limite do plano quanto o 429 transitorio ("Request rejected (429)",
278
- * "not your usage limit"): quem separa e o criterio unico da D16 (`naturezaDoLimite`), pela frase.
280
+ * I-33 (D11b): o erro de API que o proprio Claude Code gravou na transcricao da sessao. So entram linhas marcadas
281
+ * como erro de API (`isApiErrorMessage` ou campo `error`): texto do agente nunca vira motivo. Leitura limitada da
282
+ * cauda, sem seguir link; ausencia de transcricao e ausencia de evidencia. Vale o ULTIMO erro de API (N3), o que
283
+ * encerrou a sessao. RM-037 (fatia 5): com ele vem o instante da linha, que e a ancora da hora dita.
279
284
  */
280
- function falhaDeContaDaTranscricao(perfil, cwd, sessionId, agoraMs = Date.now()) {
281
- if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(sessionId))
285
+ function ultimoErroDeApi(perfil, cwd, sessionId) {
286
+ const arquivo = arquivoDaTranscricao(perfil, cwd, sessionId);
287
+ if (!arquivo)
282
288
  return null;
283
- const arquivo = path.join((0, runtime_profiles_1.diretorioEfetivo)('claude-bg', perfil), 'projects', path.resolve(cwd).replace(/[^a-zA-Z0-9-]/g, '-'), `${sessionId}.jsonl`);
284
289
  let fd;
290
+ // O_NONBLOCK: um FIFO no lugar da transcricao nao trava o observador no open (sugestao da rodada 1 do CHECK).
285
291
  try {
286
- fd = fs.openSync(arquivo, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
292
+ fd = fs.openSync(arquivo, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
287
293
  }
288
294
  catch {
289
295
  return null;
@@ -300,7 +306,7 @@ function falhaDeContaDaTranscricao(perfil, cwd, sessionId, agoraMs = Date.now())
300
306
  finally {
301
307
  fs.closeSync(fd);
302
308
  }
303
- const erros = [];
309
+ let ultimo = null;
304
310
  for (const linha of cauda.split('\n')) {
305
311
  let e;
306
312
  try {
@@ -311,14 +317,82 @@ function falhaDeContaDaTranscricao(perfil, cwd, sessionId, agoraMs = Date.now())
311
317
  }
312
318
  if (!e || typeof e !== 'object' || (e.isApiErrorMessage !== true && e.error === undefined))
313
319
  continue;
314
- erros.push(textoDoErroDeApi(e));
320
+ const ms = typeof e.timestamp === 'string' ? Date.parse(e.timestamp) : NaN;
321
+ ultimo = { texto: textoDoErroDeApi(e), em: Number.isFinite(ms) ? new Date(ms).toISOString() : null,
322
+ uuid: typeof e.uuid === 'string' && UUID.test(e.uuid) ? e.uuid.toLowerCase() : null };
315
323
  }
316
- if (!erros.length)
317
- return null;
318
- // I-33 (N3): o criterio roda sobre o erro que encerrou a sessao (o ultimo erro de API), nunca sobre os
319
- // anteriores juntos: um 429 transitorio do meio nao esconde o limite que matou a sessao, e um
320
- // esgotamento ja superado nao tira o perfil do rodizio por causa de um 429 transitorio no fim.
321
- return (0, claude_bg_1.parseFalhaDeConta)(erros[erros.length - 1], agoraMs);
324
+ return ultimo;
325
+ }
326
+ /**
327
+ * I-33 (D11b): falha de conta registrada pelo Claude Code na transcricao da sessao. O codigo `rate_limit` do
328
+ * Claude Code cobre tanto o limite do plano quanto o 429 transitorio ("Request rejected (429)", "not your usage
329
+ * limit"): quem separa e o criterio unico da D16 (`naturezaDoLimite`), pela frase. I-33 (N3): o criterio roda
330
+ * sobre o erro que encerrou a sessao, nunca sobre os anteriores juntos. RM-037 (fatia 5): a hora dita e ancorada
331
+ * no instante da mensagem; sem instante na linha, no relogio de quem le, como antes.
332
+ */
333
+ function falhaDeContaDaTranscricao(perfil, cwd, sessionId, agoraMs = Date.now()) {
334
+ return falhaDoErro(ultimoErroDeApi(perfil, cwd, sessionId), agoraMs);
335
+ }
336
+ function falhaDoErro(erro, agoraMs) {
337
+ return erro ? (0, claude_bg_1.parseFalhaDeConta)(erro.texto, erro.em ? Date.parse(erro.em) : agoraMs) : null;
338
+ }
339
+ /**
340
+ * RM-037 (fatia 5): a cota vista com a sessao viva. Em 02/10/2026 as sessoes de uma conta pararam as 06:00Z no
341
+ * limite de gasto ("your session limit resets 4:40am (<fuso>)"), sem Stop e com o processo vivo por mais uma
342
+ * hora; o perfil so saiu do rodizio as 07:15Z, quando o processo morreu, e o redespacho das 06:14Z caiu na mesma
343
+ * conta. Aqui, a cada volta com a transcricao maior que na anterior, o ultimo erro de API depois do despacho passa
344
+ * pelo criterio unico da D16; esgotamento com prazo futuro marca o perfil ate a hora dita (ou a janela padrao
345
+ * contada da mensagem) e grava `runtime_quota_detected` uma vez por erro. A fase nao fecha e a conducao nao muda:
346
+ * a sessao continua viva, e o fecho segue o caminho de sempre, sem marcar de novo o mesmo erro.
347
+ */
348
+ function cotaAoVivo(ctx, perfil, cwd, cursor, erroDaTranscricao, gravar) {
349
+ const { sessao, dir } = ctx;
350
+ const arquivo = arquivoDaTranscricao(perfil, cwd, sessao.sessionId);
351
+ let tamanho;
352
+ try {
353
+ const st = arquivo ? fs.lstatSync(arquivo) : null;
354
+ // Link, FIFO ou diretorio no lugar da transcricao nao e transcricao: nada a ler.
355
+ tamanho = st?.isFile() ? st.size : NaN;
356
+ }
357
+ catch {
358
+ return;
359
+ }
360
+ if (!Number.isFinite(tamanho) || cursor.transcricaoBytes === tamanho)
361
+ return;
362
+ cursor.transcricaoBytes = tamanho;
363
+ const erro = erroDaTranscricao();
364
+ const erroMs = erro?.em ? Date.parse(erro.em) : NaN;
365
+ // Linha sem instante, ou de antes deste despacho (sessao retomada), nao prova que a cota e deste turno.
366
+ if (!erro || !Number.isFinite(erroMs) || erroMs < Date.parse(sessao.despachadaEm))
367
+ return;
368
+ const falha = falhaDoErro(erro, ctx.agoraMs);
369
+ if (falha?.motivo !== 'runtime.quota-exhausted' || !ctx.carregado)
370
+ return;
371
+ const esgotadoAte = (0, ratelimit_1.prazoDaCotaVista)(ctx.carregado.manifesto, falha, erroMs, ctx.agoraMs);
372
+ if (!esgotadoAte)
373
+ return;
374
+ // A chave do erro: o uuid da linha ou, sem ele, o instante e o trecho (dado de maquina, nunca texto).
375
+ const sensorEventId = (0, node_crypto_1.createHash)('sha256')
376
+ .update(JSON.stringify([sessao.sessionId, sessao.despachadaEm, erro.uuid ?? [erro.em, falha.trecho]])).digest('hex');
377
+ (0, session_events_1.comLockDeSessao)(dir, () => {
378
+ if ((0, ledger_1.lerLedger)(dir).some(e => e.tipo === ledger_1.TIPOS_DE_EVENTO.cotaVistaNaTranscricao && e.sensorEventId === sensorEventId))
379
+ return;
380
+ if (perfil) {
381
+ try {
382
+ (0, ratelimit_1.marcarContaDaFalha)(ctx.carregado, perfil, falha, ctx.agoraMs, esgotadoAte);
383
+ }
384
+ catch {
385
+ // Store ocupado ou invalido: a proxima volta le a transcricao de novo e tenta outra vez.
386
+ delete cursor.transcricaoBytes;
387
+ return;
388
+ }
389
+ }
390
+ gravar(ledger_1.TIPOS_DE_EVENTO.cotaVistaNaTranscricao, { fase: sessao.fase, sessionId: sessao.sessionId, despachoEm: sessao.despachadaEm,
391
+ runtime: 'claude-bg', sensorEventId, motivo: falha.motivo, erroEm: erro.em, resetEm: falha.resetEm, fonteDoPrazo: falha.fonte,
392
+ // O trecho ja vem redigido e numa linha; sem controle, como no store de perfis.
393
+ esgotadoAte, trecho: falha.trecho.replace(/\p{Cc}/gu, ' '), perfil: perfil ? { id: perfil.id, runtime: perfil.runtime } : null,
394
+ perfilMarcado: !!perfil, origem: 'sessions.watch' });
395
+ });
322
396
  }
323
397
  /**
324
398
  * I-33 (D11): precedencia entre a prova da I-34 e a falha de conta. Com Stop correlacionado vale
@@ -360,8 +434,10 @@ function estadoDoArtefato(raiz, threadId, arquivo) {
360
434
  const file = path.join((0, thread_1.dirThread)(raiz, threadId), arquivo);
361
435
  const vazio = { sha256: null, mtimeMs: null, texto: null };
362
436
  let fd;
437
+ // RM-037 (fatia 5, sugestao da rodada 2 do CHECK): O_NONBLOCK, como na transcricao, para um FIFO no lugar do
438
+ // artefato nao travar o observador no open; o `isFile` abaixo o recusa.
363
439
  try {
364
- fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
440
+ fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
365
441
  }
366
442
  catch (erro) {
367
443
  return erro.code === 'ENOENT' ? vazio : { ...vazio, invalido: true };
@@ -600,9 +676,13 @@ function observarClaudeSobLock(ctx) {
600
676
  const registro = consulta.ok ? consulta.registros.find(s => s.sessionId === sessao.sessionId && s.cwd === fonte.cwd) ?? null : null;
601
677
  const artefato = saidaDoArtefato(raiz, thread.id, sessao, fonte, eventos);
602
678
  let prova;
679
+ // RM-037 (fatia 5): o ultimo erro de API da transcricao, lido no maximo uma vez por volta, serve a cota ao vivo
680
+ // e ao fecho.
681
+ let erroLido;
682
+ const erroDaTranscricao = () => erroLido !== undefined ? erroLido : (erroLido = ultimoErroDeApi(perfil, fonte.cwd, sessao.sessionId));
603
683
  const { c, falha } = comFalhaDeConta(classificarSessaoClaude({ sessao, eventos, consulta, registro, cursor, agoraMs: ctx.agoraMs,
604
684
  limiteMs: ctx.limiteMs, pausaAoFim: (0, thread_1.pausaNaThread)(thread, sessao.fase),
605
- prova: () => prova ??= provaDoOrk(raiz, thread.id, sessao, eventos, fonte), processo: processoNativo }), () => falhaDeContaDaTranscricao(perfil, fonte.cwd, sessao.sessionId, ctx.agoraMs));
685
+ prova: () => prova ??= provaDoOrk(raiz, thread.id, sessao, eventos, fonte), processo: processoNativo }), () => falhaDoErro(erroDaTranscricao(), ctx.agoraMs));
606
686
  (0, codex_runner_1.gravarAtomico)(arquivoCursor, cursor);
607
687
  // GO-FIX 3 (achado 1): cursor gravado numa thread que sumiu durante a consulta é desfeito aqui;
608
688
  // as pastas criadas pela observação, pelo `session-watcher`.
@@ -610,6 +690,12 @@ function observarClaudeSobLock(ctx) {
610
690
  fs.rmSync(arquivoCursor, { force: true });
611
691
  throw new Error(exports.THREAD_REMOVIDA);
612
692
  }
693
+ if (!c.terminal) {
694
+ const lidos = cursor.transcricaoBytes;
695
+ cotaAoVivo(ctx, perfil, fonte.cwd, cursor, erroDaTranscricao, gravar);
696
+ if (cursor.transcricaoBytes !== lidos)
697
+ (0, codex_runner_1.gravarAtomico)(arquivoCursor, cursor);
698
+ }
613
699
  if (!c.terminal && c.inconclusivo)
614
700
  return (0, session_events_1.comLockDeSessao)(dir, () => {
615
701
  const diagnosticoId = `${resultadoId}:inconclusivo`;
@@ -649,8 +735,12 @@ function observarClaudeSobLock(ctx) {
649
735
  observadoEm, gate: 'phase.dispatch', origem: 'sessions.watch' };
650
736
  if (!atuais.some(e => e.tipo === c.classificacao && e.sensorResultId === resultadoId))
651
737
  gravar(c.classificacao, dados);
652
- // I-33 (D12): o perfil sai do rodizio na primeira gravacao do resultado, onde a evidencia esta.
653
- if (falha && perfil && ctx.carregado && !atuais.some(e => e.tipo === 'phase_result' && e.sensorResultId === resultadoId)) {
738
+ // I-33 (D12): o perfil sai do rodizio na primeira gravacao do resultado, onde a evidencia esta. RM-037 (fatia 5):
739
+ // o erro que a cota ao vivo ja marcou nao marca de novo; depois do prazo dito, marcar de novo tiraria do
740
+ // rodizio uma conta que ja voltou.
741
+ const jaVista = !!erroLido?.em && atuais.some(e => e.tipo === ledger_1.TIPOS_DE_EVENTO.cotaVistaNaTranscricao &&
742
+ e.sessionId === sessao.sessionId && e.despachoEm === sessao.despachadaEm && e.erroEm === erroLido.em);
743
+ if (falha && perfil && ctx.carregado && !jaVista && !atuais.some(e => e.tipo === 'phase_result' && e.sensorResultId === resultadoId)) {
654
744
  try {
655
745
  (0, ratelimit_1.marcarContaDaFalha)(ctx.carregado, perfil, falha, ctx.agoraMs);
656
746
  }
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.pidVivo = pidVivo;
37
+ exports.ehFantasma = ehFantasma;
38
+ exports.contasDeSessoes = contasDeSessoes;
39
+ exports.consultarContas = consultarContas;
40
+ exports.estadosNasContas = estadosNasContas;
41
+ exports.sessoesVivasPorPerfil = sessoesVivasPorPerfil;
42
+ /**
43
+ * RM-056 (D4, D5): as sessoes desta maquina em TODAS as contas que o `ork` conhece.
44
+ *
45
+ * O `claude agents` e o `~/.codex/sessions` so respondem pela conta do ambiente de quem consulta:
46
+ * com perfis (`ork accounts`), uma sessao despachada pelo perfil X so aparece consultando com o
47
+ * `CLAUDE_CONFIG_DIR`/`CODEX_HOME` de X. Aqui a consulta passa pela conta do processo e por cada
48
+ * perfil do store, sem repetir diretorio, e cada sessao volta com o id do perfil dela (nunca o
49
+ * diretorio nem nada de dentro dele).
50
+ *
51
+ * Fantasma (D5): sessao claude-bg com estado nao terminal sem pid, ou com pid sem processo. O
52
+ * `claude agents` guarda o registro, mas nao ha quem trabalhe: ela nao ocupa vaga, e
53
+ * `ork sessions limpar-fantasmas` solta o vinculo do `ork` sem tocar no runtime.
54
+ */
55
+ const fs = __importStar(require("node:fs"));
56
+ const path = __importStar(require("node:path"));
57
+ const claude_bg_1 = require("./adapters/claude-bg");
58
+ const codex_1 = require("./adapters/codex");
59
+ const runtime_profiles_1 = require("./runtime-profiles");
60
+ const TERMINAIS = ['done', 'completed', 'exited', 'stopped', 'failed'];
61
+ const estadoDe = (s) => String(s.state ?? s.status ?? '').trim().toLowerCase();
62
+ /** O processo existe? `EPERM` e processo de outro usuario: existe. */
63
+ function pidVivo(pid) {
64
+ if (typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0)
65
+ return false;
66
+ try {
67
+ process.kill(pid, 0);
68
+ return true;
69
+ }
70
+ catch (e) {
71
+ return e.code === 'EPERM';
72
+ }
73
+ }
74
+ /** D5: fantasma e so claude-bg (o codex nao expoe pid na fonte), em estado nao terminal. */
75
+ function ehFantasma(runtime, s, vivo = pidVivo) {
76
+ return runtime === 'claude-bg' && !TERMINAIS.includes(estadoDe(s)) && !vivo(s.pid);
77
+ }
78
+ const real = (dir) => { try {
79
+ return fs.realpathSync(dir);
80
+ }
81
+ catch {
82
+ return path.resolve(dir);
83
+ } };
84
+ /**
85
+ * D4: as contas a consultar. Os perfis do store vem primeiro (inclusive desativado, que ainda pode
86
+ * ter sessao viva), para a sessao levar o id do perfil quando a conta do processo e a mesma pasta.
87
+ */
88
+ function contasDeSessoes(raiz, env = process.env) {
89
+ const contas = [], fontes = [];
90
+ const vistos = new Set();
91
+ const incluir = (runtime, perfil, dir) => {
92
+ const chave = `${runtime}:${real(dir)}`;
93
+ if (vistos.has(chave))
94
+ return;
95
+ vistos.add(chave);
96
+ contas.push({ runtime, perfil, dir });
97
+ };
98
+ if (raiz) {
99
+ try {
100
+ for (const p of (0, runtime_profiles_1.lerPerfis)(raiz).perfis) {
101
+ const d = (0, runtime_profiles_1.perfilDeDespacho)(p);
102
+ incluir(d.runtime, d, (d.configDir ?? d.codexHome));
103
+ }
104
+ }
105
+ catch (e) {
106
+ fontes.push({ origem: 'perfis de conta (ork accounts)', ok: false, detalhe: e.message });
107
+ }
108
+ }
109
+ for (const runtime of runtime_profiles_1.RUNTIMES_COM_PERFIL)
110
+ incluir(runtime, null, (0, runtime_profiles_1.diretorioImplicito)(runtime, env));
111
+ return { contas, fontes };
112
+ }
113
+ const rotulo = (c) => c.perfil ? `perfil ${c.perfil.id}` : 'conta do processo';
114
+ /**
115
+ * D4: as sessoes de todas as contas. Consulta que falha vira fonte `ok: false`, nunca "zero
116
+ * sessoes". `staleMin` e a janela de atividade que torna viva uma sessao codex (D3).
117
+ */
118
+ function consultarContas(raiz, opcoes = {}) {
119
+ const base = opcoes.contas ? { contas: opcoes.contas, fontes: [] } : contasDeSessoes(raiz);
120
+ const fontes = [...base.fontes];
121
+ const sessoes = [];
122
+ const agoraMs = opcoes.agoraMs ?? Date.now();
123
+ const janelaMs = (opcoes.staleMin ?? 30) * 60_000;
124
+ for (const conta of base.contas) {
125
+ const perfil = conta.perfil?.id ?? null;
126
+ if (conta.runtime === 'claude-bg') {
127
+ let ambiente;
128
+ try {
129
+ ambiente = (0, claude_bg_1.ambienteDoPerfil)(conta.perfil);
130
+ }
131
+ catch (e) {
132
+ fontes.push({ origem: `claude agents (${rotulo(conta)})`, ok: false, detalhe: e.message });
133
+ continue;
134
+ }
135
+ const r = (0, claude_bg_1.consultarSessoes)(undefined, opcoes.todas, ambiente);
136
+ const origem = `claude agents --json${opcoes.todas ? ' --all' : ''} (${rotulo(conta)})`;
137
+ // Fatia 2 do ensaio da 0.5.0 (P1): só o ENOENT antes da consulta prova a fonte ausente; qualquer
138
+ // outra falha do `claude` continua deixando o inventário incompleto.
139
+ fontes.push(r.ausente
140
+ ? { origem, ok: true, ausente: true, detalhe: `${r.detalhe}: nenhuma sessão claude-bg a listar`,
141
+ correcao: 'para despachar e listar pelo claude-bg, instale o Claude Code e garanta `claude` no PATH' }
142
+ : { origem, ok: r.ok, detalhe: r.detalhe });
143
+ for (const s of r.sessoes) {
144
+ const fantasma = ehFantasma('claude-bg', s);
145
+ sessoes.push({ ...s, runtime: 'claude-bg', perfil, fantasma, viva: !fantasma && !TERMINAIS.includes(estadoDe(s)) });
146
+ }
147
+ }
148
+ else {
149
+ const r = (0, codex_1.consultarRollouts)(opcoes.todas, conta.dir);
150
+ // A conta do processo segue com o caminho de antes; a do perfil vai pelo id, nunca pela pasta.
151
+ fontes.push(...(conta.perfil ? r.resultadosFontes.map(f => ({ ...f,
152
+ origem: `codex (${rotulo(conta)}): ${path.relative(conta.dir, f.origem)}` })) : r.resultadosFontes));
153
+ for (const s of r.sessoes) {
154
+ const recente = typeof s.atividadeEm === 'number' && agoraMs - s.atividadeEm <= janelaMs;
155
+ sessoes.push({ ...s, runtime: 'codex', perfil, fantasma: false, viva: estadoDe(s) !== 'completed' && recente });
156
+ }
157
+ }
158
+ }
159
+ return { sessoes, fontes, contas: base.contas };
160
+ }
161
+ /**
162
+ * D7: o mapa sessionId -> estado que o escalonador e o monitor usam, agora de todas as contas.
163
+ * Fantasma vira `fantasma` (nem `working` nem `blocked`: nao ocupa vaga nem pede o dono); codex
164
+ * vivo vira `working`. `null` ("runtime nao consultado") quando nenhuma conta claude-bg respondeu e
165
+ * nenhuma sessao codex apareceu: um mapa vazio ali diria "nada rodando" sem ter olhado.
166
+ */
167
+ function estadosNasContas(raiz, opcoes = {}) {
168
+ const r = consultarContas(raiz, opcoes);
169
+ const claude = r.fontes.filter(f => f.origem.startsWith('claude agents'));
170
+ if (claude.length > 0 && !claude.some(f => f.ok) && !r.sessoes.some(s => s.runtime === 'codex'))
171
+ return null;
172
+ return new Map(r.sessoes.map(s => [s.sessionId,
173
+ s.fantasma ? 'fantasma' : s.runtime === 'codex' ? (s.viva ? 'working' : estadoDe(s)) : estadoDe(s)]));
174
+ }
175
+ /**
176
+ * RM-056 (D3): sessoes vivas de cada perfil do runtime nesta maquina, a carga do rodizio por
177
+ * carga. So as contas dos perfis sao consultadas (a do processo nao recebe despacho com perfis).
178
+ */
179
+ function sessoesVivasPorPerfil(raiz, runtime, staleMin, agoraMs) {
180
+ const contas = contasDeSessoes(raiz).contas.filter(c => c.runtime === runtime && c.perfil);
181
+ const carga = new Map(contas.map(c => [c.perfil.id, 0]));
182
+ for (const s of consultarContas(raiz, { contas, staleMin, agoraMs }).sessoes) {
183
+ if (s.viva && s.perfil)
184
+ carga.set(s.perfil, (carga.get(s.perfil) ?? 0) + 1);
185
+ }
186
+ return carga;
187
+ }
@@ -38,9 +38,8 @@ exports.inventariarSessoes = inventariarSessoes;
38
38
  const fs = __importStar(require("node:fs"));
39
39
  const path = __importStar(require("node:path"));
40
40
  const os = __importStar(require("node:os"));
41
- const claude_bg_1 = require("./adapters/claude-bg");
42
- const codex_1 = require("./adapters/codex");
43
41
  const estado_thread_1 = require("./estado-thread");
42
+ const sessoes_contas_1 = require("./sessoes-contas");
44
43
  const thread_1 = require("./thread");
45
44
  /** Projeto canônico mais próximo de cada cwd declarado, sem varrer o HOME inteiro. */
46
45
  function raizesDeRegistros(cwds) {
@@ -71,16 +70,10 @@ function raizesDeRegistros(cwds) {
71
70
  }
72
71
  function inventariarSessoes(raiz, opcoes = {}) {
73
72
  const canonica = (0, estado_thread_1.raizDoEstado)(raiz);
74
- const claude = (0, claude_bg_1.consultarSessoes)(undefined, opcoes.todas);
75
- const codex = (0, codex_1.consultarRollouts)(opcoes.todas);
76
- const fontes = [
77
- { origem: `claude agents --json${opcoes.todas ? ' --all' : ''}`, ok: claude.ok, detalhe: claude.detalhe },
78
- ...codex.resultadosFontes,
79
- ];
80
- const todas = [
81
- ...claude.sessoes.map(s => ({ ...s, runtime: 'claude-bg', vinculos: [] })),
82
- ...codex.sessoes.map(s => ({ ...s, runtime: 'codex', vinculos: [] })),
83
- ];
73
+ // RM-056 (D4): a conta do processo e cada perfil do store, sem repetir diretorio.
74
+ const contas = (0, sessoes_contas_1.consultarContas)(canonica, { todas: opcoes.todas });
75
+ const fontes = contas.fontes;
76
+ const todas = contas.sessoes.map(s => ({ ...s, vinculos: [] }));
84
77
  // O cruzamento usa o universo global mesmo quando a apresentação é local.
85
78
  try {
86
79
  const projetos = raizesDeRegistros([canonica, ...todas.map(s => s.cwd)]);
@@ -155,5 +148,6 @@ function inventariarSessoes(raiz, opcoes = {}) {
155
148
  escopo: { usuario: os.userInfo().username, global: !!opcoes.global, historico: !!opcoes.todas, raiz: canonica },
156
149
  fontes, sessoes, total: sessoes.length,
157
150
  semThread: sessoes.filter(s => s.vinculos.length === 0).length, ambiguas,
151
+ fantasmas: sessoes.filter(s => s.fantasma).length,
158
152
  };
159
153
  }
package/dist/sessoes.js CHANGED
@@ -44,21 +44,34 @@ exports.tabelaDeSessoes = tabelaDeSessoes;
44
44
  exports.logsDaSessao = logsDaSessao;
45
45
  exports.pararSessao = pararSessao;
46
46
  exports.comandoDeAttach = comandoDeAttach;
47
+ exports.limparFantasmas = limparFantasmas;
48
+ exports.sessoesSemFim = sessoesSemFim;
49
+ exports.textoDaLimpeza = textoDaLimpeza;
47
50
  const adapter = __importStar(require("./adapters/claude-bg"));
48
51
  const sessoes_inventario_1 = require("./sessoes-inventario");
49
52
  const runtime_profiles_1 = require("./runtime-profiles");
50
53
  const util_1 = require("./util");
54
+ const estado_thread_1 = require("./estado-thread");
55
+ const ledger_1 = require("./ledger");
56
+ const thread_1 = require("./thread");
51
57
  function textoDoInventario(r) {
52
58
  const linhas = [
53
59
  `Inventário ${r.escopo.global ? 'global' : 'do projeto'} da conta ${r.escopo.usuario}; histórico: ${r.escopo.historico ? 'incluído' : 'omitido'}.`,
54
- `Fontes: ${r.fontes.map(f => `${f.origem} (${f.ok ? 'ok' : 'FALHA'})`).join('; ')}`,
55
- `Total: ${r.total}; sem thread: ${r.semThread}; ambíguas: ${r.ambiguas}; consulta: ${r.ok ? 'válida' : 'INCOMPLETA'}.`,
60
+ `Fontes: ${r.fontes.map(f => `${f.origem} (${f.ausente ? 'ausente' : f.ok ? 'ok' : 'FALHA'})`).join('; ')}`,
61
+ `Total: ${r.total}; sem thread: ${r.semThread}; ambíguas: ${r.ambiguas}; fantasmas: ${r.fantasmas}; consulta: ${r.ok ? 'válida' : 'INCOMPLETA'}.`,
56
62
  ];
63
+ // RM-056 (D4): a coluna PERFIL e o id do perfil (`processo` sem perfil), nunca o diretorio da conta.
57
64
  if (r.sessoes.length)
58
- linhas.push((0, util_1.tabela)(['ID', 'RUNTIME', 'ESTADO', 'THREAD/FASE'], r.sessoes.map(s => [s.sessionId, s.runtime, s.state ?? s.status ?? 'unknown',
65
+ linhas.push((0, util_1.tabela)(['ID', 'RUNTIME', 'PERFIL', 'ESTADO', 'THREAD/FASE'], r.sessoes.map(s => [s.sessionId, s.runtime, s.perfil ?? 'processo',
66
+ `${s.state ?? s.status ?? 'unknown'}${s.fantasma ? ' (fantasma)' : ''}`,
59
67
  s.vinculos.map(v => `${v.thread}/${v.fase}`).join(', ') || '(fora do ork)'])));
68
+ if (r.fantasmas > 0)
69
+ linhas.push('Fantasma: registro sem processo vivo, não ocupa vaga; `ork sessions limpar-fantasmas` solta o vínculo do ork sem tocar no runtime.');
60
70
  for (const f of r.fontes.filter(f => !f.ok))
61
71
  linhas.push(`Falha: ${f.detalhe}`);
72
+ // Fatia 2 do ensaio da 0.5.0 (P1): a fonte ausente nao e falha, mas diz o que falta e como ter.
73
+ for (const f of r.fontes.filter(f => f.ausente))
74
+ linhas.push(`Ausente: ${f.detalhe}${f.correcao ? `; correção: ${f.correcao}` : ''}`);
62
75
  return linhas.join('\n');
63
76
  }
64
77
  function tabelaDeSessoes(raiz, todas = false) {
@@ -117,3 +130,74 @@ function comandoDeAttach(chave, raiz) {
117
130
  const { sessao, perfil, configDir } = l.achada;
118
131
  return { texto: adapter.comandoAttach(sessao.sessionId, perfil ? configDir : null), codigo: 0 };
119
132
  }
133
+ const ENCERRAM_A_SESSAO = ['phase_result', 'sessao_morta', 'session_superseded'];
134
+ /**
135
+ * RM-056 (D6): solta o vinculo do `ork` com cada sessao fantasma (sem pid e sem processo) deste
136
+ * projeto, gravando `sessao_morta` no ledger da thread: e o evento que libera a conducao
137
+ * (`fimDaSessao`) e a vaga. Nunca chama o runtime: o registro no `claude agents` fica onde esta.
138
+ */
139
+ function limparFantasmas(raiz, opcoes = {}) {
140
+ const canonica = (0, estado_thread_1.raizDoEstado)(raiz);
141
+ const inv = (0, sessoes_inventario_1.inventariarSessoes)(canonica, { global: true });
142
+ const itens = [];
143
+ for (const s of inv.sessoes.filter(x => x.fantasma)) {
144
+ // Fechamento (RM-056, ao fechar): so os vinculos da thread que fecha; o resto fica como estava.
145
+ const vinculos = opcoes.thread ? s.vinculos.filter(v => v.thread === opcoes.thread && v.raiz === canonica) : s.vinculos;
146
+ if (opcoes.thread && vinculos.length === 0)
147
+ continue;
148
+ if (vinculos.length === 0) {
149
+ itens.push({ sessionId: s.sessionId, perfil: s.perfil, thread: null, fase: null, acao: 'sem-thread' });
150
+ continue;
151
+ }
152
+ for (const v of vinculos) {
153
+ const base = { sessionId: s.sessionId, perfil: s.perfil, thread: v.thread, fase: v.fase };
154
+ if (v.raiz !== canonica) {
155
+ itens.push({ ...base, acao: 'outro-projeto' });
156
+ continue;
157
+ }
158
+ const dir = (0, thread_1.dirThread)(canonica, v.thread);
159
+ if ((0, ledger_1.lerLedger)(dir).some(e => ENCERRAM_A_SESSAO.includes(e.tipo) && e.sessionId === s.sessionId)) {
160
+ itens.push({ ...base, acao: 'ja-solto' });
161
+ continue;
162
+ }
163
+ if (!opcoes.dryRun)
164
+ (0, ledger_1.registrar)(dir, v.thread, 'sessao_morta', {
165
+ fase: v.fase, sessionId: s.sessionId, runtime: s.runtime, ...(s.perfil ? { perfilId: s.perfil } : {}),
166
+ origem: opcoes.origem ?? 'sessions.limpar-fantasmas', estadoNoRuntime: s.state ?? s.status ?? null,
167
+ evidencia: 'claude agents lista a sessao sem pid ou com pid sem processo (fantasma, RM-056 D5)',
168
+ razao: 'solta o vinculo do ork (conducao e vaga) sem tocar no runtime: claude stop e claude rm nao acham o job',
169
+ });
170
+ itens.push({ ...base, acao: 'solto' });
171
+ }
172
+ }
173
+ return { ok: inv.ok, dryRun: opcoes.dryRun === true, itens, falhas: inv.fontes.filter(f => !f.ok).map(f => `${f.origem}: ${f.detalhe}`) };
174
+ }
175
+ /**
176
+ * RM-056 (ao fechar): as sessoes registradas na thread que o ledger dela ainda nao encerra. E o filtro
177
+ * barato do fechamento: so com alguma delas o `liberarAoFechar` consulta o inventario (o `claude agents`
178
+ * de cada conta). Sessao sem fim pode estar viva; quem decide se e fantasma e o inventario.
179
+ */
180
+ function sessoesSemFim(raiz, threadId) {
181
+ const canonica = (0, estado_thread_1.raizDoEstado)(raiz);
182
+ const t = (0, thread_1.lerThread)(canonica, threadId);
183
+ const eventos = (0, ledger_1.lerLedger)((0, thread_1.dirThread)(canonica, threadId));
184
+ const encerradas = new Set(eventos.filter(e => ENCERRAM_A_SESSAO.includes(e.tipo)).map(e => String(e.sessionId ?? '')));
185
+ const ids = (Array.isArray(t.sessoes) ? t.sessoes : []).map(s => s?.sessionId).filter((id) => typeof id === 'string');
186
+ return [...new Set(ids)].filter(id => !encerradas.has(id));
187
+ }
188
+ function textoDaLimpeza(r) {
189
+ if (r.itens.length === 0)
190
+ return `Nenhuma sessão fantasma${r.ok ? '' : ' nas contas que responderam'}.` +
191
+ r.falhas.map(f => `\nFalha: ${f}`).join('');
192
+ const verbo = {
193
+ solto: r.dryRun ? 'soltaria (sessao_morta no ledger)' : 'solto: sessao_morta no ledger',
194
+ 'ja-solto': 'já encerrada no ledger', 'sem-thread': 'fora do ork: nada a soltar',
195
+ 'outro-projeto': 'vínculo de outro projeto: rode lá',
196
+ };
197
+ return [
198
+ `Sessões fantasma (sem pid e sem processo)${r.dryRun ? ', ensaio: nada foi gravado' : ''}. O runtime não foi tocado.`,
199
+ (0, util_1.tabela)(['ID', 'PERFIL', 'THREAD/FASE', 'AÇÃO'], r.itens.map(i => [i.sessionId, i.perfil ?? 'processo',
200
+ i.thread ? `${i.thread}/${i.fase}` : '-', verbo[i.acao]])),
201
+ ...r.falhas.map(f => `Falha: ${f}`),
202
+ ].join('\n');
203
+ }