@orkastery/cli 0.5.1 → 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 (110) hide show
  1. package/adapters/claude-code/agents/ork-master.md +2 -0
  2. package/adapters/claude-code/commands/master.md +8 -0
  3. package/adapters/claude-code/commands/ork.md +9 -0
  4. package/adapters/codex/skills/ork/SKILL.md +18 -3
  5. package/adapters/hermes/README.md +1 -0
  6. package/adapters/hermes/bin/ork-network-status.sh +10 -0
  7. package/adapters/hermes/hermes.plugin.json +1 -0
  8. package/adapters/hermes/hitl-ingress/__init__.py +2 -2
  9. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +6 -5
  10. package/adapters/openclaw/README.md +24 -4
  11. package/adapters/openclaw/dist/hitl-ingress.js +2 -2
  12. package/adapters/openclaw/dist/index.js +46 -11
  13. package/adapters/openclaw/openclaw.plugin.json +5 -1
  14. package/adapters/openclaw/package.json +2 -2
  15. package/adapters/openclaw/src/hitl-ingress.ts +2 -2
  16. package/adapters/openclaw/src/index.ts +49 -11
  17. package/assets/orkmind_bridge.py +117 -9
  18. package/dist/adapters/claude-bg.js +39 -5
  19. package/dist/adapters/codex.js +4 -3
  20. package/dist/agents-md.js +1 -0
  21. package/dist/auditrun.js +5 -1
  22. package/dist/board.js +16 -14
  23. package/dist/branch-de-estado.js +66 -10
  24. package/dist/busca-semantica.js +52 -12
  25. package/dist/canarios-rm057.js +149 -0
  26. package/dist/canarios.js +2 -0
  27. package/dist/ci.js +8 -1
  28. package/dist/claims.js +39 -1
  29. package/dist/company-brain-context.js +158 -22
  30. package/dist/docs.js +48 -9
  31. package/dist/doctor-pulse-cron.js +194 -0
  32. package/dist/doctor-rede.js +149 -0
  33. package/dist/doctor.js +89 -9
  34. package/dist/entrega-pr.js +20 -8
  35. package/dist/esgotamentos.js +181 -0
  36. package/dist/experiencia.js +12 -2
  37. package/dist/fabrica-estado.js +14 -6
  38. package/dist/fabrica-publicar.js +6 -0
  39. package/dist/fechamento.js +20 -66
  40. package/dist/forja.js +31 -7
  41. package/dist/gates.js +5 -1
  42. package/dist/hitl-contract.js +59 -4
  43. package/dist/hitl-curto.js +11 -6
  44. package/dist/hitl-gates.js +5 -0
  45. package/dist/hitl-lote.js +5 -2
  46. package/dist/hitl-resumo.js +9 -0
  47. package/dist/hitl-tempo-parado.js +201 -0
  48. package/dist/hitl-texto-livre.js +5 -5
  49. package/dist/horario.js +27 -0
  50. package/dist/hosts.js +52 -10
  51. package/dist/impedimento.js +205 -0
  52. package/dist/index.js +391 -74
  53. package/dist/indice-vetorial.js +87 -10
  54. package/dist/indice.js +3 -1
  55. package/dist/init.js +17 -0
  56. package/dist/integracoes-locais.js +2 -0
  57. package/dist/intelligence-graph-cli.js +91 -8
  58. package/dist/intelligence-graph-contexto.js +215 -0
  59. package/dist/intelligence-graph-parsers.js +6 -4
  60. package/dist/leases.js +664 -87
  61. package/dist/ledger-stats.js +15 -1
  62. package/dist/ledger.js +3 -0
  63. package/dist/licoes.js +1 -0
  64. package/dist/maestro-runtime.js +4 -1
  65. package/dist/manifest.js +20 -1
  66. package/dist/master-nota.js +14 -5
  67. package/dist/master.js +129 -12
  68. package/dist/mcp-artifacts.js +34 -3
  69. package/dist/mcp-git.js +20 -3
  70. package/dist/mcp-grafo-worker.js +5 -3
  71. package/dist/mcp-grafo.js +50 -4
  72. package/dist/mcp-server.js +14 -1
  73. package/dist/memoria.js +48 -6
  74. package/dist/network-roadmap.js +191 -16
  75. package/dist/ocupacao.js +11 -3
  76. package/dist/orkmind.js +90 -5
  77. package/dist/orquestracao.js +12 -10
  78. package/dist/parado-no-condutor.js +188 -32
  79. package/dist/phase.js +68 -15
  80. package/dist/policies.js +52 -1
  81. package/dist/portfolio.js +4 -1
  82. package/dist/projeto-alvo.js +5 -1
  83. package/dist/prompts.js +36 -1
  84. package/dist/prova-ativacao.js +75 -6
  85. package/dist/pulse-delivery.js +34 -2
  86. package/dist/pulse.js +49 -3
  87. package/dist/ratelimit.js +20 -4
  88. package/dist/recall.js +3 -1
  89. package/dist/rede-adesao.js +399 -0
  90. package/dist/rede-forja.js +289 -0
  91. package/dist/rede-projetos.js +239 -0
  92. package/dist/rede-status.js +336 -0
  93. package/dist/rede.js +980 -0
  94. package/dist/retry.js +69 -2
  95. package/dist/roadmap-status.js +23 -3
  96. package/dist/runtime-profiles.js +22 -1
  97. package/dist/saida-segura.js +58 -0
  98. package/dist/session-watcher-claude.js +113 -23
  99. package/dist/sessoes-contas.js +187 -0
  100. package/dist/sessoes-inventario.js +6 -18
  101. package/dist/sessoes.js +83 -2
  102. package/dist/ship.js +85 -19
  103. package/dist/thread.js +192 -13
  104. package/dist/worktree.js +5 -2
  105. package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
  106. package/package.json +7 -3
  107. package/references/definition-of-done.md +1 -1
  108. package/skills/README.md +1 -1
  109. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  110. package/skills/phases/master-metrics/SKILL.md +8 -3
package/dist/retry.js CHANGED
@@ -97,6 +97,7 @@ const modos_1 = require("./modos");
97
97
  const thread_1 = require("./thread");
98
98
  const tokens_1 = require("./tokens");
99
99
  const util_1 = require("./util");
100
+ const impedimento_1 = require("./impedimento");
100
101
  const worktree_1 = require("./worktree");
101
102
  const delegation_1 = require("./delegation");
102
103
  const gates_2 = require("./gates");
@@ -163,6 +164,14 @@ exports.POLITICA_DE_RETRY = {
163
164
  porque: 'repetir o despacho no MESMO runtime daria o mesmo resultado: o que falta nao e tentativa, e um validador que nao seja quem executou. Escolher o outro runtime e decisao de conducao, nao retry',
164
165
  correcao: 'despache o CHECK em outro runtime (`ork phase run <thread> CHECK --runtime <outro>`), ou tire a exigencia de validacao cruzada da thread se ela nao se justifica',
165
166
  },
167
+ // RM-056 (D1): o perfil pedido nao existe ou e de outro runtime. Quem corrige e quem pediu.
168
+ 'runtime.profile-invalid': {
169
+ motivo: 'runtime.profile-invalid',
170
+ acao: 'escalar-humano',
171
+ automatica: false,
172
+ porque: 'o perfil foi pedido pelo dono (--perfil) e nao existe no store ou e de outro runtime: repetir o pedido daria a mesma recusa, e trocar de perfil sozinho desobedeceria o pedido',
173
+ correcao: 'ork accounts list mostra os perfis de cada runtime; repita o despacho com um perfil do runtime certo ou sem --perfil',
174
+ },
166
175
  'cost.violation': {
167
176
  motivo: 'cost.violation',
168
177
  acao: 'sem-retry',
@@ -251,6 +260,22 @@ exports.POLITICA_DE_RETRY = {
251
260
  porque: 'o modelo pedido nao existe ou a conta nao tem acesso a ele: o MESMO prompt, com o mesmo sha256, segue no proximo perfil do mesmo runtime com o mesmo modelo e depois no fallback do bloco, nunca de novo no par que ja recusou',
252
261
  correcao: 'ork retry run <thread> troca o destino; sem destino, ork setup <modo> --bloco N --model <modelo acessivel> ou --fallback runtime:modelo',
253
262
  },
263
+ // RM-055: so o dono destrava, no terminal. O retry nao repete o despacho as cegas: confere antes se o
264
+ // impedimento saiu (a confianca do diretorio tem prova local) e, sem prova, deixa o runtime decidir.
265
+ 'runtime.workspace-untrusted': {
266
+ motivo: 'runtime.workspace-untrusted',
267
+ acao: 'reexecutar',
268
+ automatica: true,
269
+ porque: 'o runtime recusou o diretorio da worktree antes de rodar qualquer coisa: depois que o dono aceita a confianca no terminal, o MESMO prompt, com o mesmo sha256, volta a mesma fase; enquanto a confianca nao aparece no .claude.json da conta, nada e despachado',
270
+ correcao: 'o dono roda o comando da pausa (cd <worktree> && claude, aceita a confianca); ork retry run <thread> re-despacha o prompt ja gravado',
271
+ },
272
+ 'runtime.consent-pending': {
273
+ motivo: 'runtime.consent-pending',
274
+ acao: 'reexecutar',
275
+ automatica: true,
276
+ porque: 'o runtime espera o dono aceitar termos novos antes de rodar qualquer coisa: depois do aceite, o MESMO prompt, com o mesmo sha256, volta a mesma fase; se o runtime recusar de novo, a mesma pausa do dono volta',
277
+ correcao: 'o dono abre o CLI do runtime e aceita os termos; ork retry run <thread> re-despacha o prompt ja gravado',
278
+ },
254
279
  'tree.blocked': {
255
280
  motivo: 'tree.blocked',
256
281
  acao: 'sincronizar-worktree',
@@ -258,6 +283,13 @@ exports.POLITICA_DE_RETRY = {
258
283
  porque: 'a arvore de destino andou por baixo da thread: rebasar a worktree contra a base e o passo que desbloqueia, e reexecutar antes disso so repete o conflito',
259
284
  correcao: 'ork worktree sync <thread> e so entao repetir o passo',
260
285
  },
286
+ 'lease.resume-unavailable': {
287
+ motivo: 'lease.resume-unavailable',
288
+ acao: 'escalar-humano',
289
+ automatica: false,
290
+ porque: 'sem inode seguro, a remocao forcada exige avaliar a posse antes de retomar',
291
+ correcao: 'ork lease release <nome> --forcar; depois repita a aquisicao',
292
+ },
261
293
  'lease.busy': {
262
294
  motivo: 'lease.busy',
263
295
  acao: 'reexecutar',
@@ -677,8 +709,19 @@ tomadaPrevia) {
677
709
  if (!resultado.ok || !resultado.sessionId) {
678
710
  (0, ledger_1.registrar)((0, thread_1.dirThread)(raiz, thread.id), thread.id, 'phase_dispatch_failed', { fase, runtime, slug, cwd,
679
711
  promptPath: absoluto, promptSha256: sha, controlador: resultado.controlador, model, effort,
680
- erro: resultado.erro ?? 'runtime.unavailable: sem sessionId', origem: 'retry', ...(perfil ? { perfil } : {}) });
712
+ erro: resultado.erro ?? 'runtime.unavailable: sem sessionId', stderr: (resultado.stderr ?? '').slice(0, 500),
713
+ origem: 'retry', ...(perfil ? { perfil } : {}) });
681
714
  const sinal = resultado.rateLimit ?? null;
715
+ // RM-055: o redespacho recusado pelo que so o dono resolve reabre a pausa dele, com o mesmo comando.
716
+ const impedimento = !resultado.falhaDeConta && !sinal
717
+ ? (0, impedimento_1.registrarImpedimentoDoDono)((0, thread_1.dirThread)(raiz, thread.id), thread, { fase, slug, runtime, cwd, origem: 'retry',
718
+ promptPath: absoluto, promptSha256: sha, perfil, saida: `${resultado.stderr ?? ''}\n${resultado.erro ?? ''}` })
719
+ : null;
720
+ if (impedimento) {
721
+ return { ok: false, slug, sessionId: null, verificada: false, motivo: impedimento.motivo,
722
+ detalhe: `${impedimento.motivo}: ${impedimento.trecho}`, sinal: null, falhaDeConta: null, perfil: perfil ?? null,
723
+ comando: resultado.comando, controlador: resultado.controlador, dryRun: false };
724
+ }
682
725
  // I-33 (D5): a conta recusou o redespacho; o perfil sai do rodizio aqui, onde a evidencia esta.
683
726
  if (resultado.falhaDeConta) {
684
727
  try {
@@ -1121,6 +1164,17 @@ function jaMarcado(raiz, perfil, falha, agoraMs = Date.now()) {
1121
1164
  ? atual.estado === 'esgotado' && atual.esgotadoAte !== null && Date.parse(atual.esgotadoAte) > agoraMs
1122
1165
  : atual.estado === 'sem-auth';
1123
1166
  }
1167
+ /**
1168
+ * RM-037 (fatia 5, aviso da rodada 1 do CHECK): a cota que o observador viu ao vivo (`runtime_quota_detected`, da mesma
1169
+ * sessao e do mesmo despacho, com o mesmo prazo dito) ja tirou o perfil do rodizio ate a hora da mensagem. Marcar de
1170
+ * novo aqui, depois dessa hora, pela janela padrao (A10), tiraria do rodizio uma conta que ja voltou.
1171
+ */
1172
+ function cotaJaVistaAoVivo(eventos, gate, falha) {
1173
+ if (falha.motivo !== 'runtime.quota-exhausted' || typeof gate?.sessionId !== 'string')
1174
+ return false;
1175
+ return eventos.some((e) => e.tipo === ledger_1.TIPOS_DE_EVENTO.cotaVistaNaTranscricao && e.sessionId === gate.sessionId &&
1176
+ e.despachoEm === gate.despachoEm && (e.resetEm ?? null) === falha.resetEm);
1177
+ }
1124
1178
  function sinalDoGate(gate, motivo) {
1125
1179
  const f = gate?.falhaDeConta;
1126
1180
  return { motivo: motivo === 'runtime.auth-missing' ? 'runtime.auth-missing' : 'runtime.quota-exhausted',
@@ -1282,6 +1336,18 @@ function executarRetry(carregado, threadId, opcoes = {}) {
1282
1336
  registrarTentativa(false, detalhe);
1283
1337
  return { ...vazio, detalhe };
1284
1338
  }
1339
+ // RM-055: o impedimento do dono com prova local (confianca do diretorio) so volta ao runtime depois de
1340
+ // resolvido. Sem a prova, nada e despachado e a tentativa nao conta: a pausa do dono continua aberta.
1341
+ if (plano.motivo && (0, impedimento_1.ehImpedimentoDoDono)(plano.motivo) && typeof falho.cwd === 'string') {
1342
+ const perfilFalho = perfilDoEvento(falho);
1343
+ const resolvido = (0, impedimento_1.impedimentoResolvido)(plano.motivo, { cwd: falho.cwd,
1344
+ runtime: typeof falho.runtime === 'string' ? falho.runtime : '', configDir: perfilFalho?.configDir ?? null });
1345
+ if (resolvido === false) {
1346
+ const detalhe = `${plano.motivo}: o runtime ainda nao confia em ${falho.cwd}; nada foi despachado. ` +
1347
+ `Rode \`cd ${falho.cwd} && claude\`, aceite a confianca e repita \`ork retry run ${threadId}\``;
1348
+ return { ...vazio, detalhe };
1349
+ }
1350
+ }
1285
1351
  const redespacho = redespachar(carregado, thread, plano.fase, falho.promptPath, falho.promptSha256, {
1286
1352
  effort: plano.effort,
1287
1353
  runtime: typeof falho.runtime === 'string' ? falho.runtime : null,
@@ -1349,7 +1415,8 @@ function rotacionarConta(carregado, thread, plano, opcoes, vazio, registrarTenta
1349
1415
  const politica = (0, runtime_profiles_1.politicaDeRotacao)(manifesto);
1350
1416
  const falha = sinalDoGate(gate, motivo);
1351
1417
  // I-33 (D12): o observador ja marca o perfil no caminho terminal; remarcar empurraria o prazo a cada retry.
1352
- if (!opcoes.dryRun && perfil && !jaMarcado(raiz, perfil, falha)) {
1418
+ // RM-037 (fatia 5, aviso da rodada 1 do CHECK): a cota que ele viu ao vivo ja marcou ate a hora da mensagem.
1419
+ if (!opcoes.dryRun && perfil && !jaMarcado(raiz, perfil, falha) && !cotaJaVistaAoVivo(eventos, gate, falha)) {
1353
1420
  try {
1354
1421
  (0, ratelimit_1.marcarContaDaFalha)(carregado, perfil, falha);
1355
1422
  }
@@ -37,6 +37,7 @@ const horario_1 = require("./horario");
37
37
  const projeto_alvo_1 = require("./projeto-alvo");
38
38
  const fabrica_estado_1 = require("./fabrica-estado");
39
39
  const hitl_resumo_1 = require("./hitl-resumo");
40
+ const hitl_tempo_parado_1 = require("./hitl-tempo-parado");
40
41
  const manifest_1 = require("./manifest");
41
42
  const maquina_1 = require("./maquina");
42
43
  const parado_no_condutor_1 = require("./parado-no-condutor");
@@ -129,8 +130,13 @@ function fatosLocais(raiz, quando, fuso) {
129
130
  if (entregas !== undefined)
130
131
  return entregas;
131
132
  try {
132
- const retrato = (0, parado_no_condutor_1.lerRetratoDePrs)(raiz);
133
- entregas = (0, parado_no_condutor_1.entregasDoProjeto)((0, manifest_1.exigirManifesto)(raiz), { quando, lerPrs: () => (retrato ? { ok: true, retrato } : null) });
133
+ const carregado = (0, manifest_1.exigirManifesto)(raiz);
134
+ // RM-037 (fatia 5, A5): a forja sem leitura que o pulse ja disse vale sobre o retrato, que e de antes dela (a
135
+ // leitura boa apaga a marca), e a linha nao diz "PR nao lido".
136
+ const aviso = (0, parado_no_condutor_1.forjaSemLeituraDoRemoto)(raiz, carregado.manifesto.fabrica.remoto);
137
+ const retrato = aviso ? null : (0, parado_no_condutor_1.lerRetratoDePrs)(raiz);
138
+ entregas = (0, parado_no_condutor_1.entregasDoProjeto)(carregado, { quando, lerPrs: () => (retrato ? { ok: true, retrato }
139
+ : aviso ? { ok: false, lidoEm: aviso.ditoEm, erro: aviso.motivo, semLeitura: { remoto: aviso.remoto, host: aviso.host } } : null) });
134
140
  }
135
141
  catch {
136
142
  entregas = null;
@@ -221,8 +227,18 @@ function montarStatusDoRoadmap(raiz, opcoes = {}) {
221
227
  catch {
222
228
  fabrica = undefined;
223
229
  }
230
+ // RM-057 (fatia 3): leitura pura do ledger; sem ela o relatorio sai como antes.
231
+ let hitlDeConducao;
232
+ try {
233
+ const h = (0, hitl_tempo_parado_1.lerHitlDeConducaoAgora)(raiz, quando);
234
+ if (h.abertas.length || h.seteDias.pedidos || h.seteDias.respondidos)
235
+ hitlDeConducao = h;
236
+ }
237
+ catch {
238
+ hitlDeConducao = undefined;
239
+ }
224
240
  return montarStatusDeFatos((0, docs_1.carregarDocs)(raiz).docs, fatosLocais(raiz, quando), { quando, projeto: opcoes.projeto, consulta: opcoes.consulta,
225
- ...(fabrica ? { fabrica } : {}) });
241
+ ...(fabrica ? { fabrica } : {}), ...(hitlDeConducao ? { hitlDeConducao } : {}) });
226
242
  }
227
243
  /** O relatorio a partir das paginas do roadmap e dos fatos das threads, de onde quer que venham. */
228
244
  function montarStatusDeFatos(docs, fatos, opcoes) {
@@ -271,6 +287,7 @@ function montarStatusDeFatos(docs, fatos, opcoes) {
271
287
  emSeguida: itens.filter(i => i.conduzindo && !i.hitl).map(i => ({ item: i.id, ...i.conduzindo })),
272
288
  ...(opcoes.consulta ? { consulta: opcoes.consulta } : {}),
273
289
  ...(opcoes.fabrica ? { fabrica: opcoes.fabrica } : {}),
290
+ ...(opcoes.hitlDeConducao ? { hitlDeConducao: opcoes.hitlDeConducao } : {}),
274
291
  };
275
292
  }
276
293
  /** "orkastery" vira "Orkastery": o titulo diz o nome do projeto como gente escreve. */
@@ -341,6 +358,9 @@ function textoDoStatusDoRoadmap(s, fuso, opcoes = {}) {
341
358
  ...(s.precisaDeVoce.length ? cortar(s.precisaDeVoce, x => `• ${x.item}${naMaquina(x.espera.maquina)}: ${x.espera.pergunta}` +
342
359
  (x.espera.codigo ? ` Responda ${x.espera.codigo} ${x.espera.recomendada ?? 'a'} (ou outra letra).` : ' A pergunta chega no próximo resumo.'))
343
360
  : ['• Nada agora.']),
361
+ // RM-057 (fatia 3): ha quanto tempo cada pergunta de conducao parou a thread, e a mediana da semana.
362
+ ...(s.hitlDeConducao ? [`• HITL de condução: ${s.hitlDeConducao.abertas.length} aberta(s); ${(0, hitl_tempo_parado_1.textoDaMediana)(s.hitlDeConducao)}.`,
363
+ ...cortar(s.hitlDeConducao.abertas, a => ` ${(0, hitl_tempo_parado_1.linhaDaPerguntaParada)(a, { agora: s.consultadoEm, fuso })}.`)] : []),
344
364
  '', 'O que eu faço em seguida',
345
365
  ...(s.emSeguida.length ? cortar(s.emSeguida, x => linhaDoQueVem(x, s.consultadoEm, fuso))
346
366
  : ['• Nada em andamento; sigo o que você priorizar.']),
@@ -67,6 +67,7 @@ exports.perfisDoRuntime = perfisDoRuntime;
67
67
  exports.perfilDisponivel = perfilDisponivel;
68
68
  exports.politicaDeRotacao = politicaDeRotacao;
69
69
  exports.proximoPerfilDisponivel = proximoPerfilDisponivel;
70
+ exports.proximoPerfilPorCarga = proximoPerfilPorCarga;
70
71
  exports.prazoDoRuntime = prazoDoRuntime;
71
72
  exports.prazoDaFila = prazoDaFila;
72
73
  exports.menorEsgotadoAte = menorEsgotadoAte;
@@ -794,7 +795,8 @@ exports.POLITICA_PADRAO = { mesmoRuntimePorCota: true, mesmoRuntimePorAuth: true
794
795
  function politicaDeRotacao(manifesto) {
795
796
  const r = manifesto.runtime_profiles;
796
797
  return { mesmoRuntimePorCota: r?.rotate_same_runtime_on_quota !== false,
797
- mesmoRuntimePorAuth: r?.rotate_same_runtime_on_auth !== false };
798
+ mesmoRuntimePorAuth: r?.rotate_same_runtime_on_auth !== false,
799
+ ...(r?.distribuir === 'carga' ? { distribuir: 'carga' } : {}) };
798
800
  }
799
801
  /**
800
802
  * D14: o perfil da vez do runtime, na ordem do store. `desativado` e `provider-pago` nunca foram
@@ -819,6 +821,25 @@ function proximoPerfilDisponivel(store, runtime, politica, opcoes = {}) {
819
821
  }
820
822
  return null;
821
823
  }
824
+ /**
825
+ * RM-056 (D3): a escolha por carga. Respeita a politica da D14 (sem perfil da vez pela ordem, sem
826
+ * perfil nenhum: a mesma retencao do runtime), e entre os perfis disponiveis vence o de menor
827
+ * carga (sessoes vivas da conta nesta maquina); empate pelo `ultimoUso` mais antigo (nunca usado
828
+ * primeiro), depois a ordem do store. O `ultimoUso` e gravado no despacho, entao dois despachos
829
+ * seguidos com carga igual vao a perfis diferentes mesmo antes de o runtime listar a sessao.
830
+ */
831
+ function proximoPerfilPorCarga(store, runtime, politica, carga, opcoes = {}) {
832
+ const daVez = proximoPerfilDisponivel(store, runtime, politica, opcoes);
833
+ if (!daVez)
834
+ return null;
835
+ const agoraMs = opcoes.agoraMs ?? Date.now();
836
+ const candidatos = perfisDoRuntime(store, runtime)
837
+ .map((p, i) => ({ p, i }))
838
+ .filter(({ p }) => !(opcoes.excluir ?? []).includes(p.id) && perfilDisponivel(p, agoraMs));
839
+ const uso = (p) => p.ultimoUso === null ? -Infinity : Date.parse(p.ultimoUso);
840
+ candidatos.sort((x, y) => (carga.get(x.p.id) ?? 0) - (carga.get(y.p.id) ?? 0) || uso(x.p) - uso(y.p) || x.i - y.i);
841
+ return candidatos[0]?.p ?? daVez;
842
+ }
822
843
  /**
823
844
  * D14: quando o runtime volta a ter perfil da vez, pela mesma regra da escolha. Com a troca por
824
845
  * cota desligada, e o prazo do perfil que segura o runtime; ligada, o menor prazo entre os
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /**
3
+ * RM-053 (fatia 2): o saneador de saida comum ao nucleo.
4
+ *
5
+ * Texto que vem de fora (o retrato de outra maquina, a branch `ork/fabrica-estado` do remoto de um
6
+ * projeto, onde outras pessoas escrevem) chega a tela do dono. A rede ja limpava o que imprime desde
7
+ * a revisao 4; o `ork fabrica` imprimia o `por` e o `projeto` legados crus. Este modulo e a regra
8
+ * unica: quem imprime valor alheio passa por aqui.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.emUmaLinha = exports.INVISIVEL = exports.CF_VISIVEL = void 0;
12
+ exports.valoresEmUmaLinha = valoresEmUmaLinha;
13
+ exports.jsonSemInvisivel = jsonSemInvisivel;
14
+ /**
15
+ * Y4 do CHECK 7: as marcas de `Cf` que se veem, lista fechada (todo `Prepended_Concatenation_Mark=Yes`
16
+ * do Unicode 17: os sinais numericos arabes U+0600 a U+0605, o fim de aya U+06DD, a abreviacao siriaca
17
+ * U+070F, as marcas de libra e piastra U+0890 e U+0891, o sanah U+08E2 e as marcas de numero kaithi
18
+ * U+110BD e U+110CD). Elas tem glifo e nao movem o texto, ao contrario das bidi, de largura zero e das
19
+ * tags, que seguem invisiveis. O regex do JS nao aceita `\p{Prepended_Concatenation_Mark}`; por isso a
20
+ * lista literal, presa ao `INVISIVEL` e conferida ponto a ponto no teste.
21
+ */
22
+ exports.CF_VISIVEL = Object.freeze([
23
+ 0x0600, 0x0601, 0x0602, 0x0603, 0x0604, 0x0605, 0x06dd, 0x070f, 0x0890, 0x0891, 0x08e2, 0x110bd, 0x110cd,
24
+ ]);
25
+ /**
26
+ * V2 da revisao 4, W11 da revisao 5 e X3 da revisao 6: caractere que o terminal executa ou que
27
+ * ninguem ve, pelas classes do Unicode: controles (`Cc`: ESC, BEL, CSI, quebra de linha), formato
28
+ * (`Cf`: bidi, largura zero, tags), separadores de linha e todo `Default_Ignorable_Code_Point`
29
+ * (preenchimentos Hangul, seletores de variacao, os de musica e de estenografia), mais o braile
30
+ * vazio. Ficam de fora o ZWJ (U+200D) e os seletores U+FE0E e U+FE0F: eles montam emoji comuns, e as
31
+ * marcas visiveis de `Cf` (`CF_VISIVEL`, Y4 do CHECK 7).
32
+ */
33
+ exports.INVISIVEL = /(?![\u200d\ufe0e\ufe0f\u0600-\u0605\u06dd\u070f\u0890\u0891\u08e2\u{110bd}\u{110cd}])[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}\u2800]/u;
34
+ const INVISIVEIS = new RegExp(exports.INVISIVEL.source, 'gu');
35
+ /**
36
+ * W4 da revisao 5: o valor numa linha so, sem nada invisivel. A quebra de linha (e a tabulacao) vira
37
+ * espaco: um valor que vem de fora nunca abre uma linha propria na saida, como se fosse do `ork`.
38
+ */
39
+ const emUmaLinha = (texto) => texto.replace(INVISIVEIS, (c) => (/[\t\n\v\f\r\u0085\u2028\u2029]/.test(c) ? ' ' : ''));
40
+ exports.emUmaLinha = emUmaLinha;
41
+ /**
42
+ * Cada texto de uma estrutura (objeto, lista, aninhados) em uma linha, sem invisivel; numero,
43
+ * booleano e `null` ficam como estao (X5 da revisao 6: a ida e volta em JSON trocava `NaN` por `null`).
44
+ */
45
+ function valoresEmUmaLinha(valor) {
46
+ const limpar = (v) => typeof v === 'string' ? (0, exports.emUmaLinha)(v)
47
+ : Array.isArray(v) ? v.map(limpar)
48
+ : v && typeof v === 'object' ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, limpar(x)])) : v;
49
+ return limpar(valor);
50
+ }
51
+ /**
52
+ * O JSON para a tela: o mesmo valor, com os caracteres invisiveis escritos como `\uXXXX` (quem le o
53
+ * JSON recebe o mesmo texto; so o terminal deixa de executa-lo). A quebra de linha da indentacao fica.
54
+ */
55
+ function jsonSemInvisivel(valor) {
56
+ return JSON.stringify(valor, null, 2).replace(INVISIVEIS, (c) => c === '\n' ? c
57
+ : Array.from({ length: c.length }, (_, i) => `\\u${c.charCodeAt(i).toString(16).padStart(4, '0')}`).join(''));
58
+ }
@@ -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
  }