@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
@@ -189,6 +189,20 @@ const PROJETO_DA_REDE: ProjetoDaTool = {
189
189
  recusa: 'informe o NOME de um projeto de `ork projetos` (ex.: orkastery) ou github:dono/repo, nunca caminho nem URL',
190
190
  };
191
191
 
192
+ /**
193
+ * RM-057 (fatia 2): a regra do HITL de conducao, a mesma frase dos adaptadores do Claude Code e do
194
+ * Codex (fatia 1). No OpenClaw o modelo so le as descricoes das tools: ela vai nas que recebem o
195
+ * pedido do dono (ork_modo_do_pedido), apresentam o panorama (ork_maestro) e despacham fase. Ensaio
196
+ * de 03/10: no perfil `coding` o modelo so ve as tools do `toolMetadata` (ork_network_roadmap e
197
+ * ork_network_status), entao a regra vai tambem nelas, sem ampliar o que o perfil expoe.
198
+ */
199
+ const REGRA_HITL_DE_CONDUCAO =
200
+ 'HITL de condução é seleção (RM-057): de 3 a 5 alternativas, exatamente uma com o selo "Recomendação". ' +
201
+ 'Nunca peça ao dono um "confirmo" em texto livre nem que ele cole texto; o pedido que ele colou com autorização ' +
202
+ 'explícita vale como instrução dele, dentro dessa autorização. Dúvida dentro da autorização vira `ork decisao registrar` ' +
203
+ 'e a thread segue. Texto só quando a fábrica não consegue seguir sozinha e o dono precisa rodar um comando no terminal: ' +
204
+ 'mostre o comando exato.';
205
+
192
206
  /** Catálogo em paridade nome a nome com o manifesto; aprovação genérica legada aposentada. */
193
207
  interface FerramentaOrk {
194
208
  name: string;
@@ -198,12 +212,14 @@ interface FerramentaOrk {
198
212
  entrada?: (params: Record<string, unknown>) => string;
199
213
  /** O `projeto` desta tool, quando nao e o da RM-052 (so nome). */
200
214
  projeto?: ProjetoDaTool;
215
+ /** RM-054 (fatia 3): a tool e da pessoa, nao de um projeto (`ork network status`); sem `projeto`. */
216
+ semProjeto?: true;
201
217
  }
202
218
 
203
219
  const FERRAMENTAS: FerramentaOrk[] = [
204
220
  {
205
221
  name: 'ork_maestro',
206
- description: 'Ao receber a frase exata orkastery maestro: sem projeto nomeado, chame ork_network_roadmap sem projeto (o panorama da rede, com fontes, frescor e lacunas) e apresente-o como vem; com projeto nomeado, consulte este panorama somente leitura com projeto. Não cria thread. Apresente fontes, lacunas e HITL com recomendação e opções claras; horários para o dono vêm dos campos *Local (fuso do dono), nunca do ISO. O panorama NÃO lê o roadmap, as reservas nem as outras máquinas (veja notConsulted): zero threads nunca é roadmap vazio; para o roadmap use ork_network_roadmap. Ação posterior exige operação autorizada e readback.',
222
+ description: 'Ao receber a frase exata orkastery maestro: sem projeto nomeado, chame ork_network_roadmap sem projeto (o panorama da rede, com fontes, frescor e lacunas) e apresente-o como vem; com projeto nomeado, consulte este panorama somente leitura com projeto. Não cria thread. Apresente fontes, lacunas e HITL com recomendação e opções claras; horários para o dono vêm dos campos *Local (fuso do dono), nunca do ISO. O panorama NÃO lê o roadmap, as reservas nem as outras máquinas (veja notConsulted): zero threads nunca é roadmap vazio; para o roadmap use ork_network_roadmap. Ação posterior exige operação autorizada e readback. ' + REGRA_HITL_DE_CONDUCAO,
207
223
  parameters: { type: 'object', additionalProperties: false, properties: {
208
224
  thread: { type: 'string' }, section: { type: 'string', enum: ['portfolio','demands','threads','sessions','blockers','leases','retries','hitl','ship','master','nextActions'] },
209
225
  offset: { type: 'integer', minimum: 0, maximum: 100000 },
@@ -220,7 +236,7 @@ const FERRAMENTAS: FerramentaOrk[] = [
220
236
  },
221
237
  {
222
238
  name: 'ork_onboarding',
223
- description: 'Consulta a pauta do núcleo e registra respostas públicas. Credenciais somente como nomes de variáveis; valores ficam em ~/.hermes/.env. Sync publica opcionalmente e relata degradação.',
239
+ description: 'Consulta a pauta do núcleo e registra respostas públicas. Credenciais somente como nomes de variáveis; valores ficam no ambiente do processo ou no cofre do host (no Hermes, ~/.hermes/.env). Sync publica opcionalmente e relata degradação.',
224
240
  parameters: {
225
241
  type: 'object', additionalProperties: false, required: ['acao'],
226
242
  properties: {
@@ -245,7 +261,7 @@ const FERRAMENTAS: FerramentaOrk[] = [
245
261
  {
246
262
  name: 'ork_modo_do_pedido',
247
263
  description:
248
- 'Le a #TAG de conducao do pedido do builder (#Classic, #Maestro, #Auto, #Fast) e devolve o modo. #TAG aposentada (#Look, #Ork) recebe recusa tipada modo.aposentado, com saida != 0, em vez de virar o default em silencio. Sem tag, devolve o conduction.default_mode do manifesto. Use SEMPRE este tool em vez de reconhecer a tag no host.',
264
+ 'Le a #TAG de conducao do pedido do builder (#Classic, #Maestro, #Auto, #Fast) e devolve o modo. #TAG aposentada (#Look, #Ork) recebe recusa tipada modo.aposentado, com saida != 0, em vez de virar o default em silencio. Sem tag, devolve o conduction.default_mode do manifesto. Use SEMPRE este tool em vez de reconhecer a tag no host. ' + REGRA_HITL_DE_CONDUCAO,
249
265
  parameters: schema({
250
266
  pedido: { type: 'string', description: 'O texto inteiro do pedido do builder' },
251
267
  }),
@@ -281,12 +297,18 @@ const FERRAMENTAS: FerramentaOrk[] = [
281
297
  {
282
298
  name: 'ork_phase_run',
283
299
  description:
284
- 'Despacha uma fase (GOAL PLAN GO CHECK SHIP MASTER) pelo runtime adapter, gravando o prompt exato com sha256 no ledger.',
285
- parameters: schema({
286
- thread: { type: 'string' },
287
- fase: { type: 'string', enum: ['GOAL', 'PLAN', 'GO', 'CHECK', 'SHIP', 'MASTER'] },
288
- prompt: { type: 'string', description: 'O pedido do builder para esta fase' },
289
- }),
300
+ 'Despacha uma fase (GOAL PLAN GO CHECK SHIP MASTER) pelo runtime adapter, gravando o prompt exato com sha256 no ledger. ' +
301
+ REGRA_HITL_DE_CONDUCAO,
302
+ parameters: {
303
+ type: 'object', additionalProperties: false, required: ['thread', 'fase', 'prompt'],
304
+ properties: {
305
+ thread: { type: 'string' },
306
+ fase: { type: 'string', enum: ['GOAL', 'PLAN', 'GO', 'CHECK', 'SHIP', 'MASTER'] },
307
+ prompt: { type: 'string', description: 'O pedido do builder para esta fase' },
308
+ // RM-056: a conta do despacho; o nucleo recusa perfil inexistente, de outro runtime, esgotado ou sem login.
309
+ perfil: { type: 'string', description: 'id do perfil de conta (ork accounts list); omita para o rodizio do nucleo' },
310
+ },
311
+ },
290
312
  argv: (p) => [
291
313
  'phase',
292
314
  'run',
@@ -294,6 +316,7 @@ const FERRAMENTAS: FerramentaOrk[] = [
294
316
  texto(p, 'fase'),
295
317
  '--prompt',
296
318
  texto(p, 'prompt'),
319
+ ...(p.perfil === undefined ? [] : ['--perfil', texto(p, 'perfil')]),
297
320
  ],
298
321
  },
299
322
  {
@@ -458,12 +481,21 @@ const FERRAMENTAS: FerramentaOrk[] = [
458
481
  {
459
482
  name: 'ork_network_roadmap',
460
483
  description:
461
- 'Roadmap da rede, somente leitura: para cada projeto, o status report do roadmap (RM-048) com as threads de TODAS as máquinas, as reservas, as threads por máquina com a idade da batida, a fonte e a hora de cada parte e as lacunas. É a fonte para qualquer pergunta sobre o roadmap ou o status report: transporte o texto como vem, sem reescrever nem resumir. Passe projeto com o nome que o dono pediu (ex.: orkastery) ou github:dono/repo; sem projeto, vêm todos os projetos conhecidos, e esse é o panorama da frase orkastery maestro sem projeto. Lacuna, "não lido" e "Não consultado" são fontes que ficaram sem leitura: nunca conclua "roadmap vazio" nem "nenhuma máquina publicou" a partir delas.',
484
+ 'Roadmap da rede, somente leitura: para cada projeto, o status report do roadmap (RM-048) com as threads de TODAS as máquinas, as reservas, as threads por máquina com a idade da batida, a fonte e a hora de cada parte e as lacunas. É a fonte para qualquer pergunta sobre o roadmap ou o status report: transporte o texto como vem, sem reescrever nem resumir. Passe projeto com o nome que o dono pediu (ex.: orkastery) ou github:dono/repo; sem projeto, vêm todos os projetos conhecidos, e esse é o panorama da frase orkastery maestro sem projeto. Lacuna, "não lido" e "Não consultado" são fontes que ficaram sem leitura: nunca conclua "roadmap vazio" nem "nenhuma máquina publicou" a partir delas. ' + REGRA_HITL_DE_CONDUCAO,
462
485
  parameters: schema({}),
463
486
  projeto: PROJETO_DA_REDE,
464
487
  // RM-054 (fatia 2): uma chamada de CLI; o `--projeto` vai no inicio e o nucleo o devolve ao `network`.
465
488
  argv: () => ['network', 'roadmap'],
466
489
  },
490
+ {
491
+ name: 'ork_network_status',
492
+ description:
493
+ 'Status da Orkastery Network, somente leitura: as máquinas da pessoa (a casa privada na forja), com a batida de cada uma, a forja e o login, os runtimes e hosts com versão e os projetos que cada uma declara, mais a fonte e as lacunas. É a fonte para "quais máquinas eu tenho" e "qual máquina está parada": transporte o texto como vem. Lacuna e "Não consultado" são o que não foi lido: nunca conclua "nenhuma máquina" nem "rede vazia" a partir delas. Não lê roadmap nem threads: para isso, ork_network_roadmap. ' + REGRA_HITL_DE_CONDUCAO,
494
+ parameters: schema({}),
495
+ semProjeto: true,
496
+ // RM-054 (fatia 3): uma chamada de CLI; a rede e da pessoa e nao recebe `--projeto`.
497
+ argv: () => ['network', 'status'],
498
+ },
467
499
  {
468
500
  name: 'ork_master_batch',
469
501
  description:
@@ -475,6 +507,7 @@ const FERRAMENTAS: FerramentaOrk[] = [
475
507
 
476
508
  /** RM-052: toda tool aceita `projeto` opcional, sem mudar o que ela ja exigia. */
477
509
  function comProjeto(f: FerramentaOrk): Record<string, unknown> {
510
+ if (f.semProjeto) return f.parameters;
478
511
  const propriedades = (f.parameters.properties ?? {}) as Record<string, unknown>;
479
512
  const p = f.projeto;
480
513
  const projeto = p ? { type: 'string', pattern: p.padrao.source, description: p.descricao } : PARAMETRO_PROJETO;
@@ -484,6 +517,11 @@ function comProjeto(f: FerramentaOrk): Record<string, unknown> {
484
517
  /** `--projeto <nome>` vai no inicio do argv; o resto dos parametros segue para a tool como antes. */
485
518
  function argvComProjeto(f: FerramentaOrk, params: Record<string, unknown>): { argv: string[]; resto: Record<string, unknown> } {
486
519
  const { projeto, ...resto } = params;
520
+ if (f.semProjeto) {
521
+ // A tool sem `projeto` recusa o parametro em vez de ignora-lo: o modelo saberia que a rede veio filtrada.
522
+ if (projeto !== undefined) throw new Error('projeto.invalido');
523
+ return { argv: f.argv(resto), resto };
524
+ }
487
525
  if (projeto !== undefined && (typeof projeto !== 'string' || !(f.projeto ?? PROJETO_DA_RM052).padrao.test(projeto))) {
488
526
  throw new Error('projeto.invalido');
489
527
  }
@@ -494,7 +532,7 @@ const plugin = defineToolPlugin({
494
532
  id: 'orkastery',
495
533
  name: 'Orkastery',
496
534
  description:
497
- 'Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI. Toda tool aceita projeto (nome do projeto pedido); o cwd do gateway nunca escolhe o projeto. O status do roadmap vem de ork_network_roadmap.',
535
+ 'Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI. Toda tool de projeto aceita projeto (nome do projeto pedido); o cwd do gateway nunca escolhe o projeto. O status do roadmap vem de ork_network_roadmap; as maquinas da pessoa, de ork_network_status (sem projeto).',
498
536
  tools: (tool) =>
499
537
  FERRAMENTAS.map((f) =>
500
538
  tool({
@@ -506,7 +544,7 @@ const plugin = defineToolPlugin({
506
544
  try { chamada = argvComProjeto(f, params); }
507
545
  catch (e) {
508
546
  if ((e as Error).message === 'projeto.invalido') {
509
- return `[ork recusou] projeto.invalido: ${(f.projeto ?? PROJETO_DA_RM052).recusa}`;
547
+ return `[ork recusou] projeto.invalido: ${f.semProjeto ? 'esta tool é da rede da pessoa e não recebe projeto; chame sem projeto' : (f.projeto ?? PROJETO_DA_RM052).recusa}`;
510
548
  }
511
549
  return '[ork recusou] resposta humana não confirmada; confira origem e correlação do pedido';
512
550
  }
@@ -10,6 +10,7 @@ import math
10
10
  import os
11
11
  import re
12
12
  import sys
13
+ from datetime import datetime, timezone
13
14
 
14
15
 
15
16
  class LegacyProvenanceCollision(ValueError):
@@ -370,6 +371,102 @@ async def health(store):
370
371
  ORK_COLLECTIONS = ('decision', 'handoff', 'rule', 'learning', 'roadmap')
371
372
 
372
373
 
374
+ def no_universo(entry, tenant, agora=None):
375
+ """RM-038: o predicado unico do universo da busca (indice, vetor, FTS e status).
376
+
377
+ O search_by_tags governado ja tira expiradas e injection_risk; o search_by_text nao tira
378
+ injection_risk. O mesmo predicado vale nos dois caminhos, com o tenant conferido como item
379
+ de lista: texto nunca casa por substring. `agora` e o instante de ANTES da leitura: o que a
380
+ biblioteca devolveu como ativo continua ativo aqui, sem corrida de expiracao.
381
+ """
382
+ tags = getattr(entry, 'tags', None)
383
+ projeto = tags.get('project') if isinstance(tags, dict) else None
384
+ expira = getattr(entry, 'expires_at', None)
385
+ if expira is not None and expira.tzinfo is None:
386
+ expira = expira.replace(tzinfo=timezone.utc)
387
+ return (getattr(entry, 'collection', None) in ORK_COLLECTIONS
388
+ and isinstance(projeto, list) and tenant in projeto
389
+ and not getattr(entry, 'injection_risk', False)
390
+ and (expira is None or expira > (agora or datetime.now(timezone.utc))))
391
+
392
+
393
+ async def ate_o_fim(contar, ler):
394
+ """RM-038: le com a janela da contagem mais um; janela cheia conta e le de novo uma vez.
395
+
396
+ Cheia so acontece com escrita concorrente entre a contagem e a leitura. Uma escrita isolada
397
+ se resolve na segunda leitura; cheia de novo, a resposta e falhar alto, nunca cortar.
398
+ """
399
+ for _ in range(2):
400
+ limite = await contar() + 1
401
+ lidas = await ler(limite)
402
+ if len(lidas) < limite:
403
+ return lidas
404
+ raise QueryError('memory.query.window-saturated')
405
+
406
+
407
+ def universo_contract(request):
408
+ valid = isinstance(request, dict) and set(request) == {'op', 'tenant'}
409
+ tenant = request.get('tenant') if valid else None
410
+ if (not valid or request['op'] != 'universo' or not isinstance(tenant, str) or not tenant.strip()
411
+ or len(tenant) > 128 or re.search(r'[\x00-\x1f\x7f]', tenant) is not None):
412
+ raise QueryError('memory.universo.invalid')
413
+
414
+
415
+ # Contagem SO de numeros: nenhuma coluna de conteudo sai da base por aqui. O tenant e o mesmo
416
+ # predicado da leitura governada por tag (`tags @> {"project": [tenant]}`), que usa o indice de tags.
417
+ FORA_DA_BUSCA_SQL = """
418
+ SELECT
419
+ count(*) FILTER (WHERE collection = ANY(%s) AND injection_risk
420
+ AND (expires_at IS NULL OR expires_at > now())) AS injecao,
421
+ count(*) FILTER (WHERE collection = ANY(%s) AND expires_at IS NOT NULL AND expires_at <= now()) AS expiradas,
422
+ count(*) FILTER (WHERE NOT (collection = ANY(%s))) AS outras_colecoes
423
+ FROM memories
424
+ WHERE tags @> jsonb_build_object('project', jsonb_build_array(%s::text))
425
+ """
426
+
427
+
428
+ async def contar_fora_da_busca(store, tenant):
429
+ """RM-038: quantas entradas do tenant ficam fora do universo e por que.
430
+
431
+ A API governada nao devolve o que filtra; a contagem le a tabela pela conexao do adapter
432
+ pgvector (o mesmo acesso do lock de add e handoff), so com count(*). Em outro backend a
433
+ contagem nao e medida e volta None.
434
+ """
435
+ if getattr(getattr(store, 'capabilities', None), 'backend', None) != 'pgvector':
436
+ return None
437
+ conn = await store.inner._get_conn()
438
+ colecoes = list(ORK_COLLECTIONS)
439
+ cur = await conn.execute(FORA_DA_BUSCA_SQL, (colecoes, colecoes, colecoes, tenant))
440
+ linha = await cur.fetchone()
441
+ return {'injecao': int(linha['injecao']), 'expiradas': int(linha['expiradas']),
442
+ 'outrasColecoes': int(linha['outras_colecoes'])}
443
+
444
+
445
+ async def universo(request, store):
446
+ """RM-038: o universo da busca do tenant, lido ate o fim, uma leitura governada por colecao.
447
+
448
+ O tenant e filtro na origem: texto de outro tenant nunca sai da base por aqui. A janela e a
449
+ colecao inteira mais um (ate_o_fim). Entrada fora do predicado e violacao. O vetor gravado na
450
+ base nao viaja: o ork nao o usa.
451
+ """
452
+ tenant = request['tenant']
453
+ agora = datetime.now(timezone.utc)
454
+ entradas = []
455
+ for colecao in ORK_COLLECTIONS:
456
+ async def contar(colecao=colecao):
457
+ return await store.count(colecao)
458
+ async def ler(limite, colecao=colecao):
459
+ return await store.search_by_tags({'project': [tenant]}, collection=colecao, limit=limite)
460
+ lidas = await ate_o_fim(contar, ler)
461
+ if any(e.collection != colecao or not no_universo(e, tenant, agora) for e in lidas):
462
+ raise QueryError('memory.query.scope-violation')
463
+ entradas.extend(lidas)
464
+ if len({e.id for e in entradas}) != len(entradas):
465
+ raise QueryError('memory.query.scope-violation')
466
+ return {'entradas': [e.model_dump(mode='json', exclude={'embedding'}) for e in entradas],
467
+ 'foraDaBusca': await contar_fora_da_busca(store, tenant)}
468
+
469
+
373
470
  def fts_contract(request):
374
471
  valid = isinstance(request, dict) and set(request) == {'op', 'tenant', 'texto'}
375
472
  def text(value, maximum):
@@ -379,14 +476,16 @@ def fts_contract(request):
379
476
 
380
477
 
381
478
  async def fts(request, store):
382
- """FTS da biblioteca (I-38 D9) com fronteira de tenant obrigatoria: devolve so ids, na ordem do ranking."""
479
+ """FTS da biblioteca (I-38 D9) restrito ao universo da busca (RM-038): so ids, na ordem do ranking."""
383
480
  # A janela cobre a base inteira: ausencia so conta quando nada ficou de fora do corte.
384
- limit = await store.count() + 1
385
- entries = await store.search_by_text(request['texto'], limit=limit)
386
- if len(entries) >= limit:
387
- raise QueryError('memory.query.window-saturated')
388
- return {'ids': [e.id for e in entries if e.collection in ORK_COLLECTIONS
389
- and request['tenant'] in (e.tags or {}).get('project', [])]}
481
+ agora = datetime.now(timezone.utc)
482
+ async def contar():
483
+ return await store.count()
484
+ async def ler(limite):
485
+ return await store.search_by_text(request['texto'], limit=limite)
486
+ entries = await ate_o_fim(contar, ler)
487
+ # O search_by_text alcanca injection_risk; o universo nao. Mesmo predicado do indice.
488
+ return {'ids': [e.id for e in entries if no_universo(e, request['tenant'], agora)]}
390
489
 
391
490
 
392
491
  async def execute(request, store):
@@ -403,6 +502,9 @@ async def execute(request, store):
403
502
  if op == 'fts':
404
503
  fts_contract(request)
405
504
  return await fts(request, store)
505
+ if op == 'universo':
506
+ universo_contract(request)
507
+ return await universo(request, store)
406
508
  if op == 'query':
407
509
  tags, collection, limit = query_contract(request)
408
510
  # GovernedStore conserva validade, anti-injection e visibilidade existentes.
@@ -417,8 +519,12 @@ async def execute(request, store):
417
519
  return [e.model_dump(mode='json') for e in entries]
418
520
  if op == 'export':
419
521
  collection = request['collection']
420
- count = await store.count(collection)
421
- entries = await store.search_by_tags({}, collection=collection, limit=count + 1)
522
+ async def contar():
523
+ return await store.count(collection)
524
+ async def ler(limite):
525
+ return await store.search_by_tags({}, collection=collection, limit=limite)
526
+ # RM-038: le ate o fim; janela cheia de novo e falha alta, nunca corte em silencio.
527
+ entries = await ate_o_fim(contar, ler)
422
528
  return [e.model_dump(mode='json') for e in entries]
423
529
  if op == 'get':
424
530
  entry = await store.retrieve(request['id'])
@@ -538,6 +644,8 @@ async def main(request):
538
644
  health_contract(request)
539
645
  if isinstance(request, dict) and request.get('op') == 'fts':
540
646
  fts_contract(request)
647
+ if isinstance(request, dict) and request.get('op') == 'universo':
648
+ universo_contract(request)
541
649
  native_schema_fields()
542
650
  from orkmind.core.config import OrkMindConfig
543
651
  from orkmind.store.factory import create_store
@@ -66,11 +66,13 @@ exports.parar = parar;
66
66
  exports.pararAchada = pararAchada;
67
67
  exports.acharSessaoNasContas = acharSessaoNasContas;
68
68
  exports.ocupacaoDeContexto = ocupacaoDeContexto;
69
+ const horario_1 = require("../horario");
69
70
  const util_1 = require("../util");
70
71
  const runtime_ambiente_1 = require("../runtime-ambiente");
71
72
  const path = __importStar(require("node:path"));
72
73
  const runtime_context_1 = require("../runtime-context");
73
74
  const runtime_profiles_1 = require("../runtime-profiles");
75
+ const mcp_grafo_1 = require("../mcp-grafo");
74
76
  function exec(cmd, args, cwd, timeoutMs, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
75
77
  return (0, util_1.exec)(cmd, args, cwd, timeoutMs, ambiente);
76
78
  }
@@ -107,8 +109,14 @@ const MUTACOES_WORKTREE = [
107
109
  'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_claim_add', 'mcp__orkastery__ork_decision_record',
108
110
  'mcp__orkastery__ork_git_commit', 'mcp__orkastery__ork_verify', 'mcp__orkastery__ork_ship',
109
111
  ];
112
+ /**
113
+ * RM-031 KG5 (D7): as consultas do grafo, de leitura, so com a flag do manifesto da raiz; sem ela, nada muda.
114
+ * Montada na chamada: o `mcp-grafo` chega a este modulo pelo `mcp-git`, e no ciclo de imports o valor pode
115
+ * ainda nao existir quando este modulo carrega.
116
+ */
117
+ const consultasDoGrafoMcp = () => mcp_grafo_1.TOOLS_DO_GRAFO.map((nome) => `mcp__orkastery__${nome}`);
110
118
  /** Regras nativas de arquivo, nao sandbox de processos. Edit tambem cobre Write. */
111
- function permissoesDaWorktree(cwd, permiteEditarProduto) {
119
+ function permissoesDaWorktree(cwd, permiteEditarProduto, consultas) {
112
120
  if (!path.isAbsolute(cwd) || cwd === '/' || path.normalize(cwd) !== cwd || !/^[A-Za-z0-9_./-]+$/.test(cwd))
113
121
  throw Error('runtime.context.permissions: worktree contem caracteres ambiguos para regras nativas; use caminho sem glob, espacos ou delimitadores');
114
122
  const absoluto = '/' + cwd; // Claude exige // para caminho absoluto.
@@ -117,7 +125,7 @@ function permissoesDaWorktree(cwd, permiteEditarProduto) {
117
125
  for (const prefixo of [`${absoluto}/${nome}`, `${absoluto}/**/${nome}`])
118
126
  deny.push(`Edit(${prefixo})`, `Edit(${prefixo}/**)`);
119
127
  }
120
- return { allow: [...CONSULTAS_MCP, 'mcp__orkastery__ork_git_status', ...MUTACOES_WORKTREE, ...(permiteEditarProduto ? [`Edit(${absoluto}/**)`] : [])], deny };
128
+ return { allow: [...consultas, 'mcp__orkastery__ork_git_status', ...MUTACOES_WORKTREE, ...(permiteEditarProduto ? [`Edit(${absoluto}/**)`] : [])], deny };
121
129
  }
122
130
  /** O runtime esta disponivel nesta maquina? */
123
131
  function disponivel() {
@@ -152,13 +160,14 @@ function montarComando(pedido) {
152
160
  if (pedido.contextoRuntime.host !== 'claude-code')
153
161
  throw Error('runtime.context.invalid: host Claude esperado');
154
162
  const contexto = (0, runtime_context_1.validarContextoRuntime)(pedido.contextoRuntime, pedido.cwd);
163
+ const consultas = [...CONSULTAS_MCP, ...(contexto.grafoMcp ? consultasDoGrafoMcp() : [])];
155
164
  // Somente o servidor gerado: nao mesclar MCP de outros escopos nesta sessao filha.
156
165
  const permissoes = contexto.permissoesFilho === 'worktree'
157
- ? permissoesDaWorktree(pedido.cwd, contexto.permiteEditarProduto && !plano) : null;
166
+ ? permissoesDaWorktree(pedido.cwd, contexto.permiteEditarProduto && !plano, consultas) : null;
158
167
  // PLAN não implementa: nem commit, nem claim, nem verify, nem ship, em nenhum perfil. Decidir não é
159
168
  // implementar, e o PLAN é onde as decisões nascem (RM-037, achado S7 do CHECK): a decisão autônoma fica.
160
- const allow = plano ? [...CONSULTAS_MCP, 'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_decision_record']
161
- : permissoes?.allow ?? [...CONSULTAS_MCP];
169
+ const allow = plano ? [...consultas, 'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_decision_record']
170
+ : permissoes?.allow ?? consultas;
162
171
  const deny = [...(plano ? ESCRITA_DE_ARQUIVO : []), ...(permissoes?.deny ?? [])];
163
172
  args.push('--plugin-dir', contexto.instalacao, '--strict-mcp-config', '--mcp-config', JSON.stringify({ mcpServers: { orkastery: contexto.servidor } }), '--allowedTools', allow.join(','));
164
173
  if (deny.length)
@@ -189,12 +198,18 @@ function extrairSessionId(saida) {
189
198
  function listarSessoes(cwd, todas = false, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
190
199
  return consultarSessoes(cwd, todas, ambiente).sessoes;
191
200
  }
192
- /** Falha de consulta não pode ser confundida com uma máquina sem sessões. */
201
+ /**
202
+ * Falha de consulta não pode ser confundida com uma máquina sem sessões. O binário ausente segue
203
+ * falha aqui (o observador e o pulse não concluem nada dele); só o inventário o trata como fonte
204
+ * ausente, com a correção.
205
+ */
193
206
  function consultarSessoes(cwd, todas = false, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
194
207
  const args = ['agents', '--json'];
195
208
  if (todas)
196
209
  args.push('--all');
197
210
  const r = exec('claude', args, undefined, 60000, ambiente);
211
+ if (r.error === 'ENOENT')
212
+ return { ok: false, ausente: true, sessoes: [], detalhe: 'binário `claude` fora do PATH' };
198
213
  if (!r.ok)
199
214
  return { ok: false, sessoes: [], detalhe: `claude agents falhou (código ${r.code})` };
200
215
  let dados;
@@ -413,12 +428,25 @@ function casarDuracao(texto, agoraMs) {
413
428
  }
414
429
  const MESES = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
415
430
  /**
416
- * N5: data e hora de relogio local nas formas reais. Codex: `or try again at Sep 22nd, 2026 3:05 PM`
431
+ * RM-037 (fatia 5): o fuso IANA entre parenteses logo depois da hora (`resets 4:40am (<Area>/<Cidade>)`),
432
+ * quando o Intl o aceita. O nome vem do texto do runtime; nenhum nome literal de fuso mora aqui (I-35).
433
+ */
434
+ function fusoDito(texto, depois) {
435
+ const m = /^\s*\(([A-Za-z][A-Za-z0-9_+-]*(?:\/[A-Za-z0-9_+-]+)*)\)/.exec(texto.slice(depois));
436
+ return m ? (0, horario_1.normalizarFuso)(m[1]) : undefined;
437
+ }
438
+ /** A data do dia seguinte no calendario (mes de 1 a 12), sem passar por fuso nenhum. */
439
+ function diaSeguinte(ano, mes, dia) {
440
+ const d = new Date(Date.UTC(ano, mes - 1, dia + 1));
441
+ return [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()];
442
+ }
443
+ /**
444
+ * N5: data e hora de relogio nas formas reais. Codex: `or try again at Sep 22nd, 2026 3:05 PM`
417
445
  * (`%b %-d` com sufixo ordinal, `, %Y %-I:%M %p`). Claude Code, quando o reset passa de 24 h:
418
446
  * `resets Sep 22, 3pm (<nome IANA do fuso>)` e, em outro ano, `resets Sep 22, 2027, 3:05pm`. Sem ano,
419
- * vale o ano corrente, ou o seguinte quando a data ja ficou mais de um dia para tras. O fuso entre
420
- * parenteses nao entra na regex nem no calculo (a hora e lida no relogio desta maquina), e por isso
421
- * fica como placeholder: o nome literal de fuso mora so em `core/src/horario.ts` (I-35).
447
+ * vale o ano corrente, ou o seguinte quando a data ja ficou mais de um dia para tras. RM-037 (fatia 5):
448
+ * com o fuso entre parenteses, a hora e a desse fuso (`fusoDito`); sem ele, ou com um nome que o Intl
449
+ * recusa, a hora e lida no relogio desta maquina, como antes. `agoraMs` e a ancora: o instante da mensagem.
422
450
  */
423
451
  function casarData(texto, agoraMs) {
424
452
  const m = texto.match(/(?:try again at|reset[a-z]*(?:\s+at)?)\s+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s*(?:(\d{4}),?\s*)?(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*([ap]\.?m\.?)?(?![a-z0-9])/i);
@@ -435,6 +463,16 @@ function casarData(texto, agoraMs) {
435
463
  hora = 0;
436
464
  if (dia < 1 || dia > 31 || hora > 23 || minuto > 59)
437
465
  return null;
466
+ const fuso = fusoDito(texto, (m.index ?? 0) + m[0].length);
467
+ if (fuso) {
468
+ const ano = m[3] !== undefined ? Number(m[3]) : Number((0, horario_1.partesLocais)(agoraMs, fuso).ano);
469
+ let alvo = (0, horario_1.instanteNoFuso)(ano, mes + 1, dia, hora, minuto, fuso);
470
+ if (!Number.isFinite(alvo))
471
+ return null;
472
+ if (m[3] === undefined && alvo < agoraMs - 24 * 3600 * 1000)
473
+ alvo = (0, horario_1.instanteNoFuso)(ano + 1, mes + 1, dia, hora, minuto, fuso);
474
+ return Number.isFinite(alvo) ? { resetEm: new Date(alvo).toISOString(), trecho: m[0].trim() } : null;
475
+ }
438
476
  const base = new Date(agoraMs);
439
477
  const ano = m[3] !== undefined ? Number(m[3]) : base.getFullYear();
440
478
  const alvo = new Date(ano, mes, dia, hora, minuto, 0, 0);
@@ -458,7 +496,17 @@ function casarRelogio(texto, agoraMs) {
458
496
  hora = 0;
459
497
  if (hora > 23 || minuto > 59)
460
498
  return null;
461
- // Proxima ocorrencia daquela hora de relogio a partir de agora, no fuso da maquina.
499
+ // RM-037 (fatia 5): a proxima ocorrencia daquela hora depois da ancora, no fuso dito pelo runtime.
500
+ const fuso = fusoDito(texto, (m.index ?? 0) + m[0].length);
501
+ if (fuso) {
502
+ const p = (0, horario_1.partesLocais)(agoraMs, fuso);
503
+ let alvo = (0, horario_1.instanteNoFuso)(Number(p.ano), Number(p.mes), Number(p.dia), hora, minuto, fuso);
504
+ if (alvo <= agoraMs)
505
+ alvo = (0, horario_1.instanteNoFuso)(...diaSeguinte(Number(p.ano), Number(p.mes), Number(p.dia)), hora, minuto, fuso);
506
+ if (Number.isFinite(alvo))
507
+ return { resetEm: new Date(alvo).toISOString(), trecho: m[0].trim() };
508
+ }
509
+ // Sem fuso dito: a proxima ocorrencia daquela hora de relogio a partir da ancora, no fuso da maquina.
462
510
  const base = new Date(agoraMs);
463
511
  const alvo = new Date(base.getFullYear(), base.getMonth(), base.getDate(), hora, minuto, 0, 0);
464
512
  if (alvo.getTime() <= agoraMs)
@@ -174,9 +174,10 @@ function consultarRollouts(todas = false, casa = casaDoCodex()) {
174
174
  continue;
175
175
  try {
176
176
  const fd = fs.openSync(arquivo, 'r');
177
- let primeira, cauda;
177
+ let primeira, cauda, atividadeEm;
178
178
  try {
179
- const tamanho = fs.fstatSync(fd).size;
179
+ const st = fs.fstatSync(fd), tamanho = st.size;
180
+ atividadeEm = st.mtimeMs;
180
181
  const cab = Buffer.alloc(Math.min(tamanho, 65536));
181
182
  fs.readSync(fd, cab, 0, cab.length, 0);
182
183
  primeira = cab.toString('utf8').split('\n')[0];
@@ -207,7 +208,7 @@ function consultarRollouts(todas = false, casa = casaDoCodex()) {
207
208
  catch { /* primeira/última linha do recorte pode estar parcial */ }
208
209
  }
209
210
  if (todas || state !== 'completed')
210
- sessoes.push({ sessionId: p.id, cwd: p.cwd, state, kind: 'codex-rollout' });
211
+ sessoes.push({ sessionId: p.id, cwd: p.cwd, state, kind: 'codex-rollout', atividadeEm });
211
212
  }
212
213
  catch {
213
214
  falhar(arquivo, 'leitura ou metadados inválidos ou duplicados');
package/dist/agents-md.js CHANGED
@@ -60,6 +60,7 @@ Projeto: **${seguro(manifesto.project.name)}** (${seguro(manifesto.project.abbre
60
60
  - Edite produto somente na worktree vinculada; estado em \`.orkastery/\` pertence ao núcleo.
61
61
  - PLAN não implementa. GO usa commits atômicos. CHECK não corrige. SHIP acontece por \`ork ship\`.
62
62
  - Toda alegação exige claim e comando reproduzível; self-report não é evidência.
63
+ - MASTER por omissão só da sua thread: \`ork master <thread> --aceitar-omissao\`; sem a thread, fecha também as entregas de outras frentes.
63
64
  ${comandos.length ? `\nVerificações do manifesto:\n\n${comandos.join('\n')}\n` : ''}
64
65
  Este bloco é mantido por \`ork init\`. Edite livremente o restante do arquivo.
65
66
  ${exports.FIM_AGENTS_ORK}`;
package/dist/auditrun.js CHANGED
@@ -798,7 +798,7 @@ function textoDaRodada(raiz, id) {
798
798
  L.push('');
799
799
  if (r.veredito) {
800
800
  L.push(` Claims do auditor: ${r.veredito.ok ? 'SUSTENTADAS' : 'REPROVADAS'} ` +
801
- `(motivos: ${r.veredito.motivos.join(', ') || 'nenhum'}) no HEAD ${r.veredito.commit.slice(0, 8)}`);
801
+ `(motivos: ${r.veredito.motivos.join(', ') || 'nenhum'}) no HEAD ${(0, util_1.shaCurto)(r.veredito.commit)}`);
802
802
  }
803
803
  else {
804
804
  L.push(` Claims do auditor: NAO verificadas. Rode: ork audit verify ${r.id}`);
@@ -893,13 +893,14 @@ function abrirThreadDoAchado(carregado, achadoId, opcoes = {}) {
893
893
  };
894
894
  }
895
895
  const modo = opcoes.modo ?? manifesto.conduction.default_mode;
896
- const { thread, gravada } = (0, thread_1.novaThread)(carregado, {
896
+ const { thread, gravada, worktreePor, worktreeFalharia } = (0, thread_1.novaThread)(carregado, {
897
897
  nome: opcoes.nome ?? achado.titulo,
898
898
  modo,
899
899
  slug: opcoes.slug,
900
900
  assunto: opcoes.assunto,
901
901
  worktree: opcoes.worktree ?? null,
902
902
  criarWorktree: opcoes.criarWorktree,
903
+ origemDaWorktree: opcoes.origemDaWorktree,
903
904
  dryRun: opcoes.dryRun,
904
905
  variante: opcoes.variante ?? null,
905
906
  });
@@ -909,6 +910,8 @@ function abrirThreadDoAchado(carregado, achadoId, opcoes = {}) {
909
910
  achado,
910
911
  thread,
911
912
  pedido,
913
+ worktreePor,
914
+ worktreeFalharia,
912
915
  motivo: null,
913
916
  detalhe: 'simulacao (--dry-run): nenhuma thread e nenhum carimbo no board foram gravados',
914
917
  correcao: '',
@@ -968,6 +971,7 @@ function abrirThreadDoAchado(carregado, achadoId, opcoes = {}) {
968
971
  achado,
969
972
  thread,
970
973
  pedido,
974
+ worktreePor,
971
975
  motivo: achado.claim.estado === 'nao-verificavel' ? 'claims.unverifiable' : null,
972
976
  detalhe: achado.claim.estado === 'nao-verificavel'
973
977
  ? `AVISO: a claim ${achado.claim.id} do achado nao tem comando de verificacao (motivo tipado claims.unverifiable, que avisa e nao bloqueia)`
package/dist/board.js CHANGED
@@ -60,7 +60,7 @@ exports.textoDoBoard = textoDoBoard;
60
60
  const fs = __importStar(require("node:fs"));
61
61
  const path = __importStar(require("node:path"));
62
62
  const adapter = __importStar(require("./adapters/claude-bg"));
63
- const hitl_1 = require("./hitl");
63
+ const sessoes_contas_1 = require("./sessoes-contas");
64
64
  const leases_1 = require("./leases");
65
65
  const ledger_1 = require("./ledger");
66
66
  const manifest_1 = require("./manifest");
@@ -102,28 +102,30 @@ function threadsDeTodosOsPerfis(carregado, todos = true) {
102
102
  const saida = [];
103
103
  for (const perfil of perfis) {
104
104
  const fila = (0, leases_1.lerFila)(perfil.raiz);
105
+ // RM-036 (D7): uma listagem por perfil. Por thread, a leitura do legado das worktrees se repetiria.
106
+ // I-36: a conducao (`exec:`) tem secao propria; o escalonador segue contando so os leases de escrita.
107
+ const ativos = (0, leases_1.listarLeases)(perfil.raiz).filter((l) => !(0, leases_1.expirado)(l) && (0, leases_1.tipoDoLease)(l.nome) !== 'exec');
105
108
  for (const id of (0, thread_1.listarIds)(perfil.raiz)) {
106
109
  const thread = (0, thread_1.lerThread)(perfil.raiz, id);
107
110
  saida.push({
108
111
  perfil: perfil.nome,
109
112
  raiz: perfil.raiz,
110
113
  thread,
111
- // I-36: a conducao (`exec:`) tem secao propria; o escalonador segue contando so os leases de escrita.
112
- leases: (0, leases_1.leasesDaThread)(perfil.raiz, id).filter((l) => (0, leases_1.tipoDoLease)(l.nome) !== 'exec').map((l) => l.nome),
114
+ leases: [...new Set(ativos.filter((l) => l.thread === id).map((l) => l.nome))],
113
115
  naFila: fila.filter((p) => p.thread === id),
114
116
  });
115
117
  }
116
118
  }
117
119
  return saida;
118
120
  }
119
- /** Estados de sessao do runtime, ou null quando ele nao pode ser consultado. */
120
- function estadosDeSessao() {
121
+ /**
122
+ * Estados de sessao do runtime, ou null quando ele nao pode ser consultado. RM-056 (D7): de todas
123
+ * as contas (a do processo e cada perfil do store), com o fantasma fora da vaga e o codex vivo.
124
+ */
125
+ function estadosDeSessao(raiz = null, staleMin) {
121
126
  if (!adapter.disponivel())
122
127
  return null;
123
- return new Map(adapter
124
- .listarSessoes()
125
- .filter((s) => !!s.sessionId)
126
- .map((s) => [s.sessionId, (0, hitl_1.estadoBruto)(s)]));
128
+ return (0, sessoes_contas_1.estadosNasContas)(raiz, { staleMin });
127
129
  }
128
130
  /**
129
131
  * RM-037 (rm037defeito, defeito 3): o portao de vaga do `ork phase run`. Com `max_parallel_threads: 5`,
@@ -210,7 +212,7 @@ function planejar(carregado, opcoes = {}) {
210
212
  const max = Math.max(1, manifesto.concurrency.max_parallel_threads);
211
213
  const staleMin = manifesto.concurrency.stale_after_min;
212
214
  const quando = opcoes.agora ?? (0, util_1.agora)();
213
- const estados = opcoes.estados === undefined ? estadosDeSessao() : opcoes.estados;
215
+ const estados = opcoes.estados === undefined ? estadosDeSessao(raiz, staleMin) : opcoes.estados;
214
216
  const fila = (0, leases_1.lerFila)(raiz);
215
217
  // I-36: a conducao (`exec:`) nao entra na conta de vagas: a ocupacao da sessao ja sai do ledger
216
218
  // e do runtime (`avaliarOcupacao`), e conta-la de novo mudaria o escalonador.
@@ -231,7 +233,7 @@ function planejar(carregado, opcoes = {}) {
231
233
  if (t.status === 'fechada' || (0, sessoes_adopt_1.ehRegistroDeAdocao)(t))
232
234
  continue;
233
235
  const oc = ocupacoes.get(t.id);
234
- const meus = ativos.filter((l) => l.thread === t.id).map((l) => l.nome);
236
+ const meus = [...new Set(ativos.filter((l) => l.thread === t.id).map((l) => l.nome))];
235
237
  const ocupa = oc.ocupaVaga || (meus.length > 0 && oc.classe === 'ociosa');
236
238
  if (!ocupa)
237
239
  continue;
@@ -254,7 +256,7 @@ function planejar(carregado, opcoes = {}) {
254
256
  for (const t of threads) {
255
257
  if (emAndamento.has(t.id))
256
258
  continue;
257
- const meus = ativos.filter((l) => l.thread === t.id).map((l) => l.nome);
259
+ const meus = [...new Set(ativos.filter((l) => l.thread === t.id).map((l) => l.nome))];
258
260
  if (t.status === 'fechada') {
259
261
  vagas.push({
260
262
  thread: t.id,
@@ -286,7 +288,7 @@ function planejar(carregado, opcoes = {}) {
286
288
  situacao: 'pausada',
287
289
  motivo: 'human.pending',
288
290
  detalhe: oc.detalhe,
289
- correcao: `ork gate request ${t.id}`,
291
+ correcao: oc.correcao ?? `ork gate request ${t.id}`,
290
292
  leases: meus,
291
293
  });
292
294
  continue;
@@ -385,7 +387,7 @@ function devolverVagas(carregado, opcoes = {}) {
385
387
  const { raiz, manifesto } = carregado;
386
388
  const staleMin = manifesto.concurrency.stale_after_min;
387
389
  const quando = opcoes.agora ?? (0, util_1.agora)();
388
- const estados = opcoes.estados === undefined ? estadosDeSessao() : opcoes.estados;
390
+ const estados = opcoes.estados === undefined ? estadosDeSessao(raiz, staleMin) : opcoes.estados;
389
391
  const quem = opcoes.por ?? 'escalonador (ork board reap)';
390
392
  const devolvidas = [];
391
393
  for (const id of (0, thread_1.listarIds)(raiz)) {