@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
@@ -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,6 +66,7 @@ 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"));
@@ -427,12 +428,25 @@ function casarDuracao(texto, agoraMs) {
427
428
  }
428
429
  const MESES = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
429
430
  /**
430
- * 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`
431
445
  * (`%b %-d` com sufixo ordinal, `, %Y %-I:%M %p`). Claude Code, quando o reset passa de 24 h:
432
446
  * `resets Sep 22, 3pm (<nome IANA do fuso>)` e, em outro ano, `resets Sep 22, 2027, 3:05pm`. Sem ano,
433
- * vale o ano corrente, ou o seguinte quando a data ja ficou mais de um dia para tras. O fuso entre
434
- * parenteses nao entra na regex nem no calculo (a hora e lida no relogio desta maquina), e por isso
435
- * 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.
436
450
  */
437
451
  function casarData(texto, agoraMs) {
438
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);
@@ -449,6 +463,16 @@ function casarData(texto, agoraMs) {
449
463
  hora = 0;
450
464
  if (dia < 1 || dia > 31 || hora > 23 || minuto > 59)
451
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
+ }
452
476
  const base = new Date(agoraMs);
453
477
  const ano = m[3] !== undefined ? Number(m[3]) : base.getFullYear();
454
478
  const alvo = new Date(ano, mes, dia, hora, minuto, 0, 0);
@@ -472,7 +496,17 @@ function casarRelogio(texto, agoraMs) {
472
496
  hora = 0;
473
497
  if (hora > 23 || minuto > 59)
474
498
  return null;
475
- // 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.
476
510
  const base = new Date(agoraMs);
477
511
  const alvo = new Date(base.getFullYear(), base.getMonth(), base.getDate(), hora, minuto, 0, 0);
478
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
@@ -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)) {
@@ -33,9 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.refRemota = void 0;
36
+ exports.refRemota = exports.remotoValido = exports.REMOTO_DO_GIT = void 0;
37
37
  exports.git = git;
38
38
  exports.exigirGit = exigirGit;
39
+ exports.remotoRedigido = remotoRedigido;
40
+ exports.exigirRemoto = exigirRemoto;
39
41
  exports.pontaLocal = pontaLocal;
40
42
  exports.buscarBranch = buscarBranch;
41
43
  exports.jsonsDaPonta = jsonsDaPonta;
@@ -57,6 +59,7 @@ const node_child_process_1 = require("node:child_process");
57
59
  const fs = __importStar(require("node:fs"));
58
60
  const os = __importStar(require("node:os"));
59
61
  const path = __importStar(require("node:path"));
62
+ const redacao_url_1 = require("./redacao-url");
60
63
  function git(raiz, args, extra = {}) {
61
64
  const r = (0, node_child_process_1.spawnSync)('git', args, { cwd: raiz, encoding: 'utf8', input: extra.input, timeout: extra.timeoutMs ?? 60000,
62
65
  env: { ...process.env, ...extra.env } });
@@ -68,6 +71,42 @@ function exigirGit(raiz, args, prefixo, extra = {}) {
68
71
  throw new Error(`${prefixo}.git: git ${args[0]} falhou: ${r.stderr.trim().split('\n').pop() ?? ''}`);
69
72
  return r.stdout;
70
73
  }
74
+ /**
75
+ * Nome de remoto do git, como `origin`, `upstream` ou `meu-remoto.2`. O valor vem do manifesto
76
+ * versionado (`fabrica.remoto`) ou da linha de comando (`--remoto`): quem clona um repositorio roda
77
+ * o `orkastery.yaml` de outra pessoa. Um valor que comece com `-` viraria opcao do git, e uma URL
78
+ * escolheria o transporte (`ext::` roda comando): so este formato chega ao git (RM-047).
79
+ */
80
+ exports.REMOTO_DO_GIT = /^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/;
81
+ /**
82
+ * `true` quando o valor e nome de remoto que pode ir ao git como argumento. Suspeitas da revisao de
83
+ * 03/10: o git aceita `/` no nome (`time/origem`), e a 0.5.2 tambem; cada parte comeca por letra ou
84
+ * digito, entao a barra nao abre caminho absoluto, `.`/`..`, opcao nem transporte.
85
+ */
86
+ const remotoValido = (remoto) => typeof remoto === 'string' && remoto.length <= 64 && exports.REMOTO_DO_GIT.test(remoto) && !remoto.includes('..') && !remoto.endsWith('.');
87
+ exports.remotoValido = remotoValido;
88
+ /**
89
+ * O valor recusado, para a mensagem: sem credencial de URL, sem caractere de controle e curto.
90
+ * A mensagem vai ao terminal, ao log da publicacao em segundo plano e ao ledger.
91
+ */
92
+ function remotoRedigido(remoto) {
93
+ if (typeof remoto !== 'string')
94
+ return `(${remoto === null ? 'null' : typeof remoto})`;
95
+ const semCredencial = (0, redacao_url_1.redigirCredenciaisUrl)(remoto);
96
+ const curto = semCredencial.length > 40 ? `${semCredencial.slice(0, 40)}...` : semCredencial;
97
+ return JSON.stringify(curto);
98
+ }
99
+ /**
100
+ * Recusa tipada (`<prefixo>.remoto-invalido`) antes de qualquer chamada ao git com o remoto.
101
+ * `prefixo` e o de quem usa: `fabrica` na fabrica, `roadmap` nas reservas.
102
+ */
103
+ function exigirRemoto(remoto, prefixo) {
104
+ if ((0, exports.remotoValido)(remoto))
105
+ return remoto;
106
+ throw new Error(`${prefixo}.remoto-invalido: o remoto ${remotoRedigido(remoto)} não é nome de remoto do git ` +
107
+ '(letras, dígitos, ".", "_", "-" e "/" entre partes, cada parte começando por letra ou dígito, sem URL); nada foi passado ao git. ' +
108
+ 'Corrija fabrica.remoto no orkastery.yaml ou --remoto (padrão: origin).');
109
+ }
71
110
  const refRemota = (remoto, branch) => `refs/remotes/${remoto}/${branch}`;
72
111
  exports.refRemota = refRemota;
73
112
  /** A ultima copia lida da branch nesta maquina, sem rede. `null` quando nunca foi lida. */
@@ -80,16 +119,32 @@ function pontaLocal(raiz, remoto, branch) {
80
119
  * copia local com `atualizado: false`.
81
120
  */
82
121
  function buscarBranch(raiz, remoto, branch, prefixo, timeoutMs, env) {
83
- const r = git(raiz, ['fetch', '--quiet', '--no-tags', remoto, `+refs/heads/${branch}:${(0, exports.refRemota)(remoto, branch)}`], { timeoutMs, env });
84
- if (r.ok)
85
- return { ponta: exigirGit(raiz, ['rev-parse', '--verify', (0, exports.refRemota)(remoto, branch)], prefixo).trim(), atualizado: true };
86
- if (/couldn't find remote ref|could not find remote ref/i.test(r.stderr)) {
87
- // A branch ainda nao nasceu: a primeira gravacao a cria. Copia local antiga nao vale mais.
88
- git(raiz, ['update-ref', '-d', (0, exports.refRemota)(remoto, branch)]);
89
- return { ponta: null, atualizado: true };
122
+ exigirRemoto(remoto, prefixo);
123
+ for (let tentativa = 1;; tentativa++) {
124
+ // `--`: daqui para a frente, so repositorio e refspec, nunca opcao (RM-047).
125
+ const r = git(raiz, ['fetch', '--quiet', '--no-tags', '--', remoto, `+refs/heads/${branch}:${(0, exports.refRemota)(remoto, branch)}`], { timeoutMs, env });
126
+ if (r.ok)
127
+ return { ponta: exigirGit(raiz, ['rev-parse', '--verify', (0, exports.refRemota)(remoto, branch)], prefixo).trim(), atualizado: true };
128
+ if (/couldn't find remote ref|could not find remote ref/i.test(r.stderr)) {
129
+ // A branch ainda nao nasceu: a primeira gravacao a cria. Copia local antiga nao vale mais.
130
+ git(raiz, ['update-ref', '-d', (0, exports.refRemota)(remoto, branch)]);
131
+ return { ponta: null, atualizado: true };
132
+ }
133
+ if (tentativa < TENTATIVAS_NA_DISPUTA_DE_REF && DISPUTA_DE_REF.test(r.stderr)) {
134
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50 * tentativa);
135
+ continue;
136
+ }
137
+ return { ponta: pontaLocal(raiz, remoto, branch), atualizado: false };
90
138
  }
91
- return { ponta: pontaLocal(raiz, remoto, branch), atualizado: false };
92
139
  }
140
+ /**
141
+ * Suspeitas da revisao de 03/10: dois `fetch` simultaneos na mesma copia (o `ork network status` e a
142
+ * publicacao no cache da casa, a leitura e a gravacao da fabrica no clone) disputam o lock da ref
143
+ * remota, e o perdedor sai com "incorrect old value provided" (ou "cannot lock ref") sem que a rede
144
+ * tenha falhado. A ref ja foi atualizada pelo vencedor: buscar de novo da certo.
145
+ */
146
+ const DISPUTA_DE_REF = /incorrect old value provided|cannot lock ref|unable to create '[^']*\.lock'|is at [0-9a-f]+ but expected/i;
147
+ const TENTATIVAS_NA_DISPUTA_DE_REF = 5;
93
148
  /** Os `.json` de um diretorio da ponta, ja lidos. Filtro por caminho: diretorio vazio some da arvore. */
94
149
  function jsonsDaPonta(raiz, ponta, dir, prefixo) {
95
150
  if (!ponta)
@@ -103,6 +158,7 @@ function jsonsDaPonta(raiz, ponta, dir, prefixo) {
103
158
  * indice temporario, blobs, `write-tree` e `commit-tree`. Push sem forca; recusa volta `false`.
104
159
  */
105
160
  function gravarNaBranch(raiz, remoto, branch, ponta, mudancas, mensagem, prefixo, rede = {}) {
161
+ exigirRemoto(remoto, prefixo);
106
162
  const indice = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'ork-branch-estado-')), 'index');
107
163
  const env = { GIT_INDEX_FILE: indice };
108
164
  try {
@@ -122,7 +178,7 @@ function gravarNaBranch(raiz, remoto, branch, ponta, mudancas, mensagem, prefixo
122
178
  const arvore = exigirGit(raiz, ['write-tree'], prefixo, { env }).trim();
123
179
  const commit = exigirGit(raiz, ['commit-tree', arvore, ...(ponta ? ['-p', ponta] : []), '-m', mensagem], prefixo).trim();
124
180
  // `rede`: prazo e ambiente so do que vai a rede (RM-037, achado A3: o fechamento nao espera 60 s).
125
- const push = git(raiz, ['push', '--quiet', remoto, `${commit}:refs/heads/${branch}`], rede);
181
+ const push = git(raiz, ['push', '--quiet', '--', remoto, `${commit}:refs/heads/${branch}`], rede);
126
182
  if (push.ok) {
127
183
  git(raiz, ['update-ref', (0, exports.refRemota)(remoto, branch), commit]);
128
184
  return commit;
@@ -6,10 +6,12 @@
6
6
  * deterministicos. Esta busca e superficie separada e todo resultado sai com
7
7
  * `deterministico: false`.
8
8
  *
9
- * O universo e o export governado do tenant (filtro de injecao e visibilidade da biblioteca):
9
+ * O universo e o universo da busca do tenant (RM-038: `universoDaBusca`, o mesmo do indice e do
10
+ * status, com a governanca da biblioteca aplicada na ponte):
10
11
  * - vetor: cosseno da consulta contra o indice local do MESMO modelo e dimensao, so para
11
12
  * entradas com sha256 coerente; nunca compara vetores de espacos diferentes;
12
- * - FTS: `search_by_text` da biblioteca, filtrado por tenant na ponte e intersectado aqui;
13
+ * - FTS: `search_by_text` da biblioteca, restrito ao universo na ponte e intersectado aqui (id
14
+ * fora do universo e descartado e declarado, nunca em silencio);
13
15
  * - fusao: RRF com k = 60.
14
16
  * A cadeia do vetor e primario, fallback local, nenhum; sem vetor, cai para FTS com motivo tipado.
15
17
  */
@@ -18,6 +20,7 @@ exports.CANDIDATOS_DO_VETOR = exports.LIMITE_MAXIMO_DA_BUSCA = exports.LIMITE_PA
18
20
  exports.cosseno = cosseno;
19
21
  exports.fundirRrf = fundirRrf;
20
22
  exports.buscarPorSignificado = buscarPorSignificado;
23
+ exports.textoDaBuscaPorSignificado = textoDaBuscaPorSignificado;
21
24
  const indice_vetorial_1 = require("./indice-vetorial");
22
25
  exports.MODOS_DE_BUSCA = ['hibrido', 'vetor', 'fts'];
23
26
  /** D9: constante da fusao RRF. */
@@ -44,7 +47,7 @@ function fundirRrf(listas, k = exports.RRF_K) {
44
47
  }
45
48
  const resumo = (conteudo) => conteudo.split('\n')[0].slice(0, 100);
46
49
  /** Lado vetorial: o primeiro caminho da cadeia que tem indice, responde e fala o mesmo espaco. */
47
- function ladoVetorial(o) {
50
+ function ladoVetorial(o, entradas) {
48
51
  const detalhes = [];
49
52
  let motivo = null;
50
53
  const falhar = (alvo, m, d) => {
@@ -103,7 +106,7 @@ function ladoVetorial(o) {
103
106
  }
104
107
  if (resposta.truncados?.length)
105
108
  detalhes.push(`${alvo}: consulta acima do contexto do modelo, embedada pelo comeco`);
106
- const ranking = [...(0, indice_vetorial_1.vetoresCoerentes)(lido.indice, o.universo)]
109
+ const ranking = [...(0, indice_vetorial_1.vetoresCoerentes)(lido.indice, entradas)]
107
110
  .map(([id, v]) => ({ id, sim: cosseno(vetor, v) }))
108
111
  .sort((a, b) => b.sim - a.sim || a.id.localeCompare(b.id));
109
112
  return { alvo, modelo: espaco.modelo, ranking, motivo, detalhes };
@@ -112,14 +115,21 @@ function ladoVetorial(o) {
112
115
  }
113
116
  /** `ork memory search --texto`: ranking hibrido, nunca deterministico, sempre dentro do tenant. */
114
117
  function buscarPorSignificado(o) {
115
- const universo = o.universo.filter(e => (e.tags.project ?? []).includes(o.tenant));
116
- const porId = new Map(universo.map(e => [e.id, e]));
118
+ // RM-038 (D6): entrada alheia no universo e violacao antes de embedar a consulta, nunca descarte.
119
+ if (o.universo.tenant !== o.tenant)
120
+ throw new Error('memory.query.scope-violation');
121
+ (0, indice_vetorial_1.conferirUniverso)(o.universo.entradas, o.tenant, o.universo.lidoEm);
122
+ const doUniverso = new Set(o.universo.entradas.map(e => e.id));
123
+ // As entradas buscadas: o universo inteiro, ou so a colecao pedida.
124
+ const buscadas = o.colecao ? o.universo.entradas.filter(e => e.collection === o.colecao) : o.universo.entradas;
125
+ const porId = new Map(buscadas.map(e => [e.id, e]));
117
126
  const saida = { texto: o.texto, modo: o.modo, origem: 'nenhum', modeloUsado: null,
118
- deterministico: false, motivo: null, detalhe: '', resultados: [], listas: { vetor: [], fts: [] } };
127
+ deterministico: false, motivo: null, detalhe: '', resultados: [], listas: { vetor: [], fts: [] },
128
+ ftsForaDoUniverso: 0, coberturaDoIndice: null };
119
129
  const detalhes = [];
120
130
  let similaridade = new Map();
121
131
  if (o.modo !== 'fts') {
122
- const v = ladoVetorial({ ...o, universo });
132
+ const v = ladoVetorial(o, buscadas);
123
133
  detalhes.push(...v.detalhes);
124
134
  saida.motivo = v.motivo;
125
135
  if (v.alvo) {
@@ -128,22 +138,33 @@ function buscarPorSignificado(o) {
128
138
  const candidatos = v.ranking.slice(0, exports.CANDIDATOS_DO_VETOR);
129
139
  saida.listas.vetor = candidatos.map(c => c.id);
130
140
  similaridade = new Map(candidatos.map(c => [c.id, c.sim]));
141
+ // RM-038 (D7): o vetor so ranqueia o que o indice cobre; menos que o universo e dito aqui.
142
+ saida.coberturaDoIndice = { coerentes: v.ranking.length, universo: buscadas.length };
143
+ const aviso = (0, indice_vetorial_1.avisoDeCobertura)(v.ranking.length, buscadas, v.alvo, o.colecao ? ` na colecao ${o.colecao}` : '');
144
+ if (aviso)
145
+ detalhes.push(`vetor: ${aviso}`);
131
146
  }
132
147
  }
133
148
  if (o.modo !== 'vetor') {
149
+ let ftsFalhou = false;
134
150
  try {
135
151
  if (!o.buscarTexto)
136
152
  throw new Error('memory.transport.fts');
137
- // A ponte ja filtra o tenant; a intersecao com o universo governado vale de novo aqui.
138
- saida.listas.fts = [...new Set(o.buscarTexto(o.tenant, o.texto))].filter(id => porId.has(id));
153
+ // A ponte ja restringe ao universo; a intersecao vale de novo aqui e a quantidade que sobra e declarada (D5).
154
+ const ids = [...new Set(o.buscarTexto(o.tenant, o.texto))];
155
+ saida.ftsForaDoUniverso = ids.filter(id => !doUniverso.has(id)).length;
156
+ if (saida.ftsForaDoUniverso)
157
+ detalhes.push(`fts: ${saida.ftsForaDoUniverso} id(s) fora do universo da busca descartado(s)`);
158
+ saida.listas.fts = ids.filter(id => porId.has(id));
139
159
  }
140
160
  catch (erro) {
141
- const codigo = /^[a-z]+(?:\.[a-z-]+)+/.exec(erro instanceof Error ? erro.message : '')?.[0] ?? 'memory.transport.fts';
161
+ ftsFalhou = true;
162
+ const codigo = (0, indice_vetorial_1.codigoDaFalha)(erro, 'memory.transport.fts');
142
163
  detalhes.push(`fts: ${codigo}`);
143
164
  if (o.modo === 'fts' || saida.origem === 'nenhum')
144
165
  saida.motivo = saida.motivo ?? codigo;
145
166
  }
146
- if (saida.origem === 'nenhum' && !detalhes.some(d => d.startsWith('fts:')))
167
+ if (saida.origem === 'nenhum' && !ftsFalhou)
147
168
  saida.origem = 'fts';
148
169
  }
149
170
  const scores = fundirRrf([saida.listas.vetor, saida.listas.fts]);
@@ -159,3 +180,22 @@ function buscarPorSignificado(o) {
159
180
  saida.detalhe = detalhes.join('; ');
160
181
  return saida;
161
182
  }
183
+ /**
184
+ * Texto de `ork memory search --texto`. Revisao de 03/10 (RM-038): o detalhe sai tambem sem motivo; o
185
+ * id do FTS fora do universo e o aviso de cobertura vivem nele, e o texto os perdia na busca que deu certo.
186
+ */
187
+ function textoDaBuscaPorSignificado(r) {
188
+ const linhas = [`Busca por significado (NAO deterministica; modo ${r.modo}, origem ${r.origem}${r.modeloUsado ? ` ${r.modeloUsado}` : ''})`];
189
+ if (r.motivo)
190
+ linhas.push(` motivo: ${r.motivo}${r.detalhe ? `; ${r.detalhe}` : ''}`);
191
+ else if (r.detalhe)
192
+ linhas.push(` detalhe: ${r.detalhe}`);
193
+ linhas.push('');
194
+ r.resultados.forEach((e, i) => {
195
+ const sim = e.similaridade === null ? '' : ` similaridade ${e.similaridade}`;
196
+ linhas.push(` ${i + 1}. [${e.collection}] ${e.id} score ${e.score} ${e.fontes.join('+')}${sim}`);
197
+ linhas.push(` ${e.resumo}`);
198
+ });
199
+ linhas.push('', ` ${r.resultados.length} resultado(s); busca por tag continua em ork memory search --tags`);
200
+ return linhas.join('\n');
201
+ }