@orkastery/cli 0.4.3 → 0.5.0

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 (121) hide show
  1. package/adapters/README.md +11 -0
  2. package/adapters/claude-code/.claude-plugin/plugin.json +3 -1
  3. package/adapters/claude-code/agents/ork-check.md +1 -1
  4. package/adapters/claude-code/agents/ork-go.md +2 -2
  5. package/adapters/claude-code/agents/ork-goal.md +1 -1
  6. package/adapters/claude-code/agents/ork-master.md +1 -1
  7. package/adapters/claude-code/agents/ork-plan.md +1 -1
  8. package/adapters/claude-code/agents/ork-ship.md +1 -1
  9. package/adapters/claude-code/commands/master.md +11 -8
  10. package/adapters/claude-code/commands/ork.md +8 -0
  11. package/adapters/claude-code/commands/ship.md +2 -2
  12. package/adapters/codex/skills/ork/SKILL.md +12 -1
  13. package/adapters/hermes/README.md +3 -0
  14. package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
  15. package/adapters/hermes/bin/ork-brain.sh +3 -0
  16. package/adapters/hermes/bin/ork-maestro.sh +4 -1
  17. package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
  18. package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
  19. package/adapters/hermes/hermes.plugin.json +2 -0
  20. package/adapters/hermes/hitl-ingress/__init__.py +36 -3
  21. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
  22. package/adapters/openclaw/README.md +34 -2
  23. package/adapters/openclaw/dist/hitl-ingress.js +37 -3
  24. package/adapters/openclaw/dist/index.js +92 -20
  25. package/adapters/openclaw/openclaw.plugin.json +10 -1
  26. package/adapters/openclaw/package.json +2 -2
  27. package/adapters/openclaw/src/hitl-ingress.ts +32 -3
  28. package/adapters/openclaw/src/index.ts +108 -20
  29. package/assets/docs/padroes/documentacao-de-produto.md +2 -2
  30. package/assets/docs/padroes/roadmap-de-produto.md +1 -1
  31. package/assets/docs/produto/_modelo-feature.md +2 -0
  32. package/assets/orkmind_bridge.py +214 -1
  33. package/dist/adapters/claude-bg.js +6 -3
  34. package/dist/board.js +74 -0
  35. package/dist/branch-de-estado.js +5 -4
  36. package/dist/busca-semantica.js +161 -0
  37. package/dist/ci.js +141 -10
  38. package/dist/company-brain-cli.js +27 -2
  39. package/dist/company-brain-context.js +132 -0
  40. package/dist/company-brain-dossie.js +376 -0
  41. package/dist/company-brain-mcp.js +4 -1
  42. package/dist/conducao.js +71 -18
  43. package/dist/decisao-autonoma.js +42 -0
  44. package/dist/docs.js +64 -22
  45. package/dist/doctor.js +33 -0
  46. package/dist/entrega-pr.js +109 -5
  47. package/dist/experiencia-instalacao.js +256 -0
  48. package/dist/experiencia.js +72 -0
  49. package/dist/fabrica-estado.js +43 -10
  50. package/dist/fechamento.js +105 -0
  51. package/dist/forja.js +435 -0
  52. package/dist/gates.js +9 -1
  53. package/dist/hitl-classificacao.js +11 -1
  54. package/dist/hitl-contract.js +50 -7
  55. package/dist/hitl-curto.js +158 -0
  56. package/dist/hitl-gates.js +67 -1
  57. package/dist/hitl-lote.js +30 -15
  58. package/dist/hitl-presentation.js +10 -7
  59. package/dist/hitl-resumo.js +33 -12
  60. package/dist/hitl-texto-livre.js +150 -0
  61. package/dist/hosts.js +86 -1
  62. package/dist/index.js +602 -38
  63. package/dist/indice-vetorial.js +317 -0
  64. package/dist/init.js +13 -0
  65. package/dist/integracoes-locais.js +1 -1
  66. package/dist/intelligence-benchmark-contract.js +570 -0
  67. package/dist/intelligence-graph-cli.js +430 -0
  68. package/dist/intelligence-graph-contract.js +577 -0
  69. package/dist/intelligence-graph-extract-md.js +469 -0
  70. package/dist/intelligence-graph-extract-ts.js +810 -0
  71. package/dist/intelligence-graph-extract.js +369 -0
  72. package/dist/intelligence-graph-index.js +512 -0
  73. package/dist/intelligence-graph-parsers.js +327 -0
  74. package/dist/intelligence-graph-query.js +344 -0
  75. package/dist/intelligence-graph-repo.js +187 -0
  76. package/dist/leases.js +84 -0
  77. package/dist/ledger.js +14 -0
  78. package/dist/maestro-cli.js +10 -1
  79. package/dist/maestro-contract.js +5 -1
  80. package/dist/maestro-discovery.js +16 -2
  81. package/dist/maestro-snapshot.js +6 -2
  82. package/dist/manifest.js +142 -5
  83. package/dist/master-batch.js +3 -2
  84. package/dist/master-digest.js +2 -2
  85. package/dist/master-nota.js +203 -0
  86. package/dist/master.js +4 -0
  87. package/dist/mcp-experiencia.js +164 -0
  88. package/dist/mcp-git.js +4 -0
  89. package/dist/mcp-install.js +3 -1
  90. package/dist/mcp-maestro.js +3 -2
  91. package/dist/mcp-server.js +84 -5
  92. package/dist/memoria.js +147 -1
  93. package/dist/network-roadmap.js +907 -0
  94. package/dist/ocupacao.js +38 -2
  95. package/dist/onboarding.js +113 -7
  96. package/dist/orkmind.js +165 -9
  97. package/dist/phase.js +166 -13
  98. package/dist/policies.js +2 -0
  99. package/dist/projeto-alvo.js +551 -0
  100. package/dist/prompts.js +1 -1
  101. package/dist/pulse-consentimento.js +4 -2
  102. package/dist/pulse-delivery.js +5 -0
  103. package/dist/pulse-resposta.js +674 -49
  104. package/dist/pulse.js +21 -0
  105. package/dist/retry.js +51 -8
  106. package/dist/roadmap-reservas.js +262 -20
  107. package/dist/roadmap-status.js +213 -0
  108. package/dist/thread-close.js +3 -0
  109. package/dist/verify.js +4 -3
  110. package/eval/casos/orchestration-experience-pt-br.json +48 -0
  111. package/eval/casos/orchestration-experience.json +48 -0
  112. package/package.json +1 -1
  113. package/schemas/code-artifact-graph.v1.schema.json +581 -0
  114. package/schemas/graph-benchmark.v1.schema.json +2240 -0
  115. package/schemas/maestro-snapshot.schema.json +23 -0
  116. package/skills/README.md +7 -5
  117. package/skills/core/onboarding/SKILL.md +7 -0
  118. package/skills/core/orchestration-experience/SKILL.md +113 -0
  119. package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
  120. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  121. package/skills/phases/go-implementation/SKILL.md +2 -0
@@ -1,4 +1,4 @@
1
- """Transporte de fabrica: OrkMind instalado, stdin JSON, sem provider ou embedder."""
1
+ """Transporte de fabrica: OrkMind instalado, stdin JSON; embedder so na operacao embed (I-38)."""
2
2
  from __future__ import annotations
3
3
  import asyncio
4
4
  import contextlib
@@ -6,7 +6,9 @@ import hashlib
6
6
  import io
7
7
  import json
8
8
  import logging
9
+ import math
9
10
  import os
11
+ import re
10
12
  import sys
11
13
 
12
14
 
@@ -188,6 +190,205 @@ def query_contract(request):
188
190
  return {k: list(v) for k, v in tags.items()}, request['collection'], limit
189
191
 
190
192
 
193
+ # I-38 (D1, D4, D8): a chave de embedding chega so ao ambiente desta operacao, com este nome.
194
+ EMBED_KEY_ENV = 'ORKMIND_EMBEDDING_API_KEY'
195
+ EMBED_TIMEOUT_S = 10.0
196
+ EMBED_MAX_TEXTS = 32
197
+ EMBED_MAX_CHARS = 24000
198
+ LOCAL_MAX_TOKENS = 8192
199
+ MODEL_NAME = re.compile(r'[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*')
200
+ # Mesmos padroes da policy segredo_em_prompt, mais URL com credencial: recusados antes da rede.
201
+ SECRET_PATTERNS = [re.compile(p) for p in (
202
+ r'sk-ant-[A-Za-z0-9_-]{16,}', r'sk-or-v1-[A-Za-z0-9]{32,}', r'\bsk-[A-Za-z0-9]{32,}\b',
203
+ r'\bAKIA[0-9A-Z]{16}\b', r'\bgh[pousr]_[A-Za-z0-9]{20,}\b', r'-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----',
204
+ r'(?i)\b(?:api[_-]?key|secret|token|senha|password)\s*[:=]\s*[\'"][^\'"\s]{16,}[\'"]',
205
+ r'\b[a-z][a-z0-9+.-]*://[^\s/:@]+:[^\s/@]+@')]
206
+
207
+
208
+ def embed_contract(request):
209
+ valid = isinstance(request, dict) and set(request) == {'op', 'papel', 'alvo', 'modelo', 'dim', 'textos'}
210
+ textos = request.get('textos') if valid else None
211
+ dim = request.get('dim') if valid else None
212
+ if (not valid or request['papel'] not in ('consulta', 'documento')
213
+ or request['alvo'] not in ('primario', 'fallback')
214
+ or not isinstance(request['modelo'], str) or MODEL_NAME.fullmatch(request['modelo']) is None
215
+ or type(dim) is not int or not 32 <= dim <= 4096
216
+ or not isinstance(textos, list) or not 1 <= len(textos) <= EMBED_MAX_TEXTS
217
+ or any(not isinstance(t, str) or not t.strip() or len(t) > EMBED_MAX_CHARS for t in textos)):
218
+ raise QueryError('memory.embed.invalid')
219
+
220
+
221
+ def model_prefix(modelo, papel):
222
+ """Instrucao de uso do proprio modelo; o conteudo da entrada vai inteiro depois dela."""
223
+ nome = modelo.lower().split('/')[-1]
224
+ if nome.startswith('qwen3-embedding'):
225
+ return ('Instruct: Given a search query, retrieve relevant memory entries that answer the query\nQuery:'
226
+ if papel == 'consulta' else '')
227
+ if re.search(r'(^|[-_])e5([-_]|$)', nome):
228
+ return 'query: ' if papel == 'consulta' else 'passage: '
229
+ return ''
230
+
231
+
232
+ async def embed_primary(modelo, dim, textos):
233
+ if not os.environ.get(EMBED_KEY_ENV, '').strip():
234
+ raise QueryError('embeddings.chave-ausente')
235
+ import httpx
236
+ from orkmind.embeddings.provider import OpenRouterEmbeddingProvider
237
+ provider = OpenRouterEmbeddingProvider(api_key_env=EMBED_KEY_ENV, model=modelo, embedding_dim=dim,
238
+ timeout_s=EMBED_TIMEOUT_S, request_dimensions=True)
239
+ try:
240
+ return await provider.embed_batch(textos)
241
+ except Exception as error:
242
+ # Nunca repassar texto do provider: ele pode ecoar a entrada ou o cabecalho.
243
+ cause = error.__cause__ or error
244
+ raise QueryError('embeddings.timeout' if isinstance(cause, httpx.TimeoutException)
245
+ else 'embeddings.provider-indisponivel') from None
246
+
247
+
248
+ def local_model_dir(modelo):
249
+ """Pasta do modelo no cache local, sem rede. None quando nao foi baixado."""
250
+ os.environ['HF_HUB_OFFLINE'] = '1'
251
+ os.environ['TRANSFORMERS_OFFLINE'] = '1'
252
+ try:
253
+ from huggingface_hub import snapshot_download
254
+ except ImportError:
255
+ raise QueryError('embeddings.dependencia-ausente') from None
256
+ try:
257
+ return snapshot_download(modelo, local_files_only=True)
258
+ except Exception:
259
+ return None
260
+
261
+
262
+ def local_pooling(pasta):
263
+ from pathlib import Path
264
+ modules = Path(pasta, 'modules.json')
265
+ if modules.exists() and any('Dense' in m.get('type', '') for m in json.loads(modules.read_text())):
266
+ raise QueryError('embeddings.dimensao-divergente')
267
+ config = Path(pasta, '1_Pooling', 'config.json')
268
+ modos = json.loads(config.read_text()) if config.exists() else {}
269
+ return ('lasttoken' if modos.get('pooling_mode_lasttoken') else
270
+ 'cls' if modos.get('pooling_mode_cls_token') else 'mean')
271
+
272
+
273
+ class LocalEmbeddingProvider:
274
+ """Fallback local (D4): mesma interface do EmbeddingProvider da biblioteca, offline, em CPU."""
275
+
276
+ def __init__(self, modelo):
277
+ pasta = local_model_dir(modelo)
278
+ if pasta is None:
279
+ raise QueryError('embeddings.local-ausente')
280
+ try:
281
+ import torch
282
+ from transformers import AutoModel, AutoTokenizer
283
+ except ImportError:
284
+ raise QueryError('embeddings.dependencia-ausente') from None
285
+ torch.set_num_threads(min(4, os.cpu_count() or 1))
286
+ self._torch = torch
287
+ self._pooling = local_pooling(pasta)
288
+ self._tokenizer = AutoTokenizer.from_pretrained(
289
+ pasta, local_files_only=True, padding_side='left' if self._pooling == 'lasttoken' else 'right')
290
+ self._model = AutoModel.from_pretrained(pasta, local_files_only=True)
291
+ self._model.eval()
292
+ self._dim = int(self._model.config.hidden_size)
293
+ self._max = min(int(getattr(self._tokenizer, 'model_max_length', LOCAL_MAX_TOKENS) or LOCAL_MAX_TOKENS),
294
+ LOCAL_MAX_TOKENS)
295
+ # Texto acima do contexto do modelo local e embedado pelo comeco e DECLARADO (nunca em silencio).
296
+ self.truncados = []
297
+
298
+ @property
299
+ def dim(self):
300
+ return self._dim
301
+
302
+ async def embed(self, text):
303
+ return (await self.embed_batch([text]))[0]
304
+
305
+ async def embed_batch(self, texts):
306
+ torch = self._torch
307
+ self.truncados = [i for i, ids in enumerate(self._tokenizer(texts, truncation=False)['input_ids'])
308
+ if len(ids) > self._max]
309
+ lote = self._tokenizer(texts, padding=True, truncation=True, max_length=self._max, return_tensors='pt')
310
+ with torch.inference_mode():
311
+ saida = self._model(**lote).last_hidden_state
312
+ if self._pooling == 'lasttoken':
313
+ vetores = saida[:, -1]
314
+ elif self._pooling == 'cls':
315
+ vetores = saida[:, 0]
316
+ else:
317
+ mascara = lote['attention_mask'].unsqueeze(-1).to(saida.dtype)
318
+ vetores = (saida * mascara).sum(1) / mascara.sum(1).clamp(min=1e-9)
319
+ return torch.nn.functional.normalize(vetores, p=2, dim=1).tolist()
320
+
321
+
322
+ async def embed(request):
323
+ """Operacao embed: unica que instancia embedder; nao toca a base nem recebe a DSN."""
324
+ embed_contract(request)
325
+ textos = request['textos']
326
+ if any(p.search(t) for p in SECRET_PATTERNS for t in textos):
327
+ raise QueryError('embeddings.conteudo-recusado')
328
+ prefixo = model_prefix(request['modelo'], request['papel'])
329
+ entrada = [prefixo + t for t in textos]
330
+ truncados = []
331
+ if request['alvo'] == 'primario':
332
+ vetores = await embed_primary(request['modelo'], request['dim'], entrada)
333
+ else:
334
+ local = LocalEmbeddingProvider(request['modelo'])
335
+ vetores = await local.embed_batch(entrada)
336
+ truncados = list(local.truncados)
337
+ # A biblioteca nao confere a dimensao devolvida (D8): a ponte confere vetor a vetor.
338
+ if (not isinstance(vetores, list) or len(vetores) != len(textos)
339
+ or any(not isinstance(v, list) or len(v) != request['dim']
340
+ or not all(isinstance(x, float) and math.isfinite(x) for x in v) for v in vetores)):
341
+ raise QueryError('embeddings.dimensao-divergente')
342
+ return {'alvo': request['alvo'], 'modelo': request['modelo'], 'dim': request['dim'], 'truncados': truncados,
343
+ 'vetores': [[round(x, 7) for x in v] for v in vetores]}
344
+
345
+
346
+ def health_contract(request):
347
+ if not isinstance(request, dict) or set(request) != {'op'}:
348
+ raise QueryError('memory.health.invalid')
349
+
350
+
351
+ async def health(store):
352
+ """Sonda barata (I-38 T4): contagens, versao da biblioteca e dependencias do fallback, sem rede nem torch."""
353
+ import importlib.metadata
354
+ import importlib.util
355
+ # Versao e dependencias sao informacao, nao saude: falha nelas nunca derruba o regime.
356
+ try:
357
+ versao = importlib.metadata.version('orkmind')
358
+ except Exception:
359
+ versao = None
360
+ def presente(modulo):
361
+ try:
362
+ return importlib.util.find_spec(modulo) is not None
363
+ except Exception:
364
+ return False
365
+ dependencias = all(presente(m) for m in ('torch', 'transformers', 'huggingface_hub'))
366
+ return {'contagens': {c: await store.count(c) for c in await store.list_collections()},
367
+ 'orkmind': versao, 'fallback': {'dependencias': dependencias}}
368
+
369
+
370
+ ORK_COLLECTIONS = ('decision', 'handoff', 'rule', 'learning', 'roadmap')
371
+
372
+
373
+ def fts_contract(request):
374
+ valid = isinstance(request, dict) and set(request) == {'op', 'tenant', 'texto'}
375
+ def text(value, maximum):
376
+ return isinstance(value, str) and 0 < len(value.strip()) and len(value) <= maximum and re.search(r'[\x00-\x1f\x7f]', value) is None
377
+ if not valid or not text(request['tenant'], 128) or not text(request['texto'], 2000):
378
+ raise QueryError('memory.fts.invalid')
379
+
380
+
381
+ async def fts(request, store):
382
+ """FTS da biblioteca (I-38 D9) com fronteira de tenant obrigatoria: devolve so ids, na ordem do ranking."""
383
+ # 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', [])]}
390
+
391
+
191
392
  async def execute(request, store):
192
393
  from orkmind.core.models import MemoryEntry
193
394
  from orkmind.core.semantic_layer import SemanticLayer
@@ -196,6 +397,12 @@ async def execute(request, store):
196
397
  op = request['op']
197
398
  if op == 'stats':
198
399
  return {c: await store.count(c) for c in await store.list_collections()}
400
+ if op == 'health':
401
+ health_contract(request)
402
+ return await health(store)
403
+ if op == 'fts':
404
+ fts_contract(request)
405
+ return await fts(request, store)
199
406
  if op == 'query':
200
407
  tags, collection, limit = query_contract(request)
201
408
  # GovernedStore conserva validade, anti-injection e visibilidade existentes.
@@ -323,8 +530,14 @@ async def main(request):
323
530
  return {'schema':'orkmind.company-brain-api/v1','state':'conflict','error':'brain.api.invalid'}
324
531
  from orkmind.cli.company_brain import service_request
325
532
  return service_request(request['request'])
533
+ if isinstance(request, dict) and request.get('op') == 'embed':
534
+ return await embed(request)
326
535
  if isinstance(request, dict) and request.get('op') == 'query':
327
536
  query_contract(request)
537
+ if isinstance(request, dict) and request.get('op') == 'health':
538
+ health_contract(request)
539
+ if isinstance(request, dict) and request.get('op') == 'fts':
540
+ fts_contract(request)
328
541
  native_schema_fields()
329
542
  from orkmind.core.config import OrkMindConfig
330
543
  from orkmind.store.factory import create_store
@@ -102,8 +102,9 @@ const CONSULTAS_MCP = [
102
102
  'mcp__orkastery__ork_artifact_read',
103
103
  'mcp__orkastery__ork_claims_list',
104
104
  ];
105
+ // RM-037 (defeito 1): a decisao autonoma pelo MCP tem o mesmo grant nos dois runtimes.
105
106
  const MUTACOES_WORKTREE = [
106
- 'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_claim_add',
107
+ 'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_claim_add', 'mcp__orkastery__ork_decision_record',
107
108
  'mcp__orkastery__ork_git_commit', 'mcp__orkastery__ork_verify', 'mcp__orkastery__ork_ship',
108
109
  ];
109
110
  /** Regras nativas de arquivo, nao sandbox de processos. Edit tambem cobre Write. */
@@ -154,8 +155,10 @@ function montarComando(pedido) {
154
155
  // Somente o servidor gerado: nao mesclar MCP de outros escopos nesta sessao filha.
155
156
  const permissoes = contexto.permissoesFilho === 'worktree'
156
157
  ? permissoesDaWorktree(pedido.cwd, contexto.permiteEditarProduto && !plano) : null;
157
- // PLAN não implementa: nem commit, nem claim, nem verify, nem ship, em nenhum perfil.
158
- const allow = plano ? [...CONSULTAS_MCP, 'mcp__orkastery__ork_artifact_write'] : permissoes?.allow ?? [...CONSULTAS_MCP];
158
+ // PLAN não implementa: nem commit, nem claim, nem verify, nem ship, em nenhum perfil. Decidir não é
159
+ // 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];
159
162
  const deny = [...(plano ? ESCRITA_DE_ARQUIVO : []), ...(permissoes?.deny ?? [])];
160
163
  args.push('--plugin-dir', contexto.instalacao, '--strict-mcp-config', '--mcp-config', JSON.stringify({ mcpServers: { orkastery: contexto.servidor } }), '--allowedTools', allow.join(','));
161
164
  if (deny.length)
package/dist/board.js CHANGED
@@ -50,6 +50,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
50
50
  exports.perfisDetectados = perfisDetectados;
51
51
  exports.threadsDeTodosOsPerfis = threadsDeTodosOsPerfis;
52
52
  exports.estadosDeSessao = estadosDeSessao;
53
+ exports.vagaDoDespacho = vagaDoDespacho;
54
+ exports.esperarVaga = esperarVaga;
53
55
  exports.planejar = planejar;
54
56
  exports.devolverVagas = devolverVagas;
55
57
  exports.textoDoReap = textoDoReap;
@@ -123,6 +125,78 @@ function estadosDeSessao() {
123
125
  .filter((s) => !!s.sessionId)
124
126
  .map((s) => [s.sessionId, (0, hitl_1.estadoBruto)(s)]));
125
127
  }
128
+ /**
129
+ * RM-037 (rm037defeito, defeito 3): o portao de vaga do `ork phase run`. Com `max_parallel_threads: 5`,
130
+ * um sexto despacho saiu as 09h03 de 29/09/2026 com cinco sessoes vivas: o `phase run` nao consultava
131
+ * vaga nenhuma, e o escalonador so via as sessoes `claude agents` de uma conta.
132
+ *
133
+ * Ocupa vaga a outra thread cuja conducao `exec:<thread>` e de uma sessao viva (a prova que vale para
134
+ * claude-bg de qualquer conta e para codex, liberada por `phase_result`, `sessao_morta` ou
135
+ * `session_superseded`), salvo a sessao parada ou escalada para o humano (`sessaoLivreDaVaga`; achados A1 e
136
+ * N1 do CHECK: a pausa prevista e o verify reprovado nao param a sessao, e ela segue contando). Ocupa tambem o
137
+ * despacho em curso de outra thread (conducao de processo do `phase.run` ou do `retry.run`), senao dois
138
+ * pedidos simultaneos passariam juntos. A conducao da propria thread tem portao proprio. Somente leitura.
139
+ */
140
+ function vagaDoDespacho(carregado, threadId, quando = (0, util_1.agora)(),
141
+ /** S-5 do CHECK 3: o `desde` da tomada deste despacho; despacho em curso de outra thread so conta se veio antes. */
142
+ desdeProprio) {
143
+ const { raiz, manifesto } = carregado;
144
+ const limite = Math.max(1, manifesto.concurrency.max_parallel_threads);
145
+ const staleMin = manifesto.concurrency.stale_after_min;
146
+ const ocupam = [];
147
+ for (const id of (0, thread_1.listarIds)(raiz)) {
148
+ if (id === threadId)
149
+ continue;
150
+ let atual = null;
151
+ try {
152
+ atual = (0, conducao_1.conducaoDaThread)(raiz, id);
153
+ }
154
+ catch {
155
+ continue;
156
+ }
157
+ if (!atual)
158
+ continue;
159
+ const despachando = atual.dono.tipo === 'processo' && (atual.operacao === 'phase.run' || atual.operacao === 'retry.run');
160
+ if (atual.dono.tipo !== 'sessao' && !despachando)
161
+ continue;
162
+ // Dois despachos disputando a ultima vaga nao se recusam um ao outro: quem tomou antes fica com ela.
163
+ if (despachando && desdeProprio && (atual.desde > desdeProprio || (atual.desde === desdeProprio && id > threadId)))
164
+ continue;
165
+ if (atual.dono.tipo === 'sessao') {
166
+ try {
167
+ if ((0, ocupacao_1.sessaoLivreDaVaga)((0, ledger_1.lerLedger)((0, thread_1.dirThread)(raiz, id)), atual.desde, quando, staleMin, atual.dono.sessionId))
168
+ continue;
169
+ }
170
+ catch { /* ledger ilegivel: a conducao viva continua contando */ }
171
+ }
172
+ ocupam.push({ thread: id, fase: atual.fase, desde: atual.desde,
173
+ sessao: atual.dono.tipo === 'sessao' ? atual.dono.sessionId : null,
174
+ runtime: atual.dono.tipo === 'sessao' ? atual.dono.runtime : null });
175
+ }
176
+ if (ocupam.length < limite)
177
+ return null;
178
+ ocupam.sort((a, b) => a.desde.localeCompare(b.desde));
179
+ return {
180
+ limite,
181
+ ocupam,
182
+ detalhe: `concurrency.limite: o projeto ja tem ${ocupam.length} sessao(oes) viva(s) em outras threads e o limite e ${limite} ` +
183
+ `(concurrency.max_parallel_threads): ${ocupam.map((o) => `${o.thread} ${o.fase ?? '-'} ` +
184
+ (o.sessao ? `${o.runtime} ${o.sessao.slice(0, 8)}` : 'despacho em curso')).join('; ')}`,
185
+ correcao: 'espere uma sessao terminar e repita com --esperar <min>; sessao que morreu sem evento sai com ' +
186
+ 'ork conducao status <thread>; ou suba concurrency.max_parallel_threads no orkastery.yaml',
187
+ };
188
+ }
189
+ /** `--esperar`: espera a vaga do projeto ate o prazo. Devolve true quando ha vaga. */
190
+ function esperarVaga(carregado, threadId, esperarMs) {
191
+ const prazo = Date.now() + esperarMs;
192
+ for (;;) {
193
+ if (!vagaDoDespacho(carregado, threadId))
194
+ return true;
195
+ if (Date.now() >= prazo)
196
+ return false;
197
+ (0, conducao_1.dormir)(Math.min(2000, prazo - Date.now()));
198
+ }
199
+ }
126
200
  /**
127
201
  * Escalonador por maquina: decide quem avanca agora e quem espera, com motivo tipado.
128
202
  *
@@ -79,8 +79,8 @@ function pontaLocal(raiz, remoto, branch) {
79
79
  * Traz a ponta da branch. `ponta: null` quando ela ainda nao existe no remoto; sem rede, a ultima
80
80
  * copia local com `atualizado: false`.
81
81
  */
82
- function buscarBranch(raiz, remoto, branch, prefixo, timeoutMs) {
83
- const r = git(raiz, ['fetch', '--quiet', '--no-tags', remoto, `+refs/heads/${branch}:${(0, exports.refRemota)(remoto, branch)}`], { timeoutMs });
82
+ 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
84
  if (r.ok)
85
85
  return { ponta: exigirGit(raiz, ['rev-parse', '--verify', (0, exports.refRemota)(remoto, branch)], prefixo).trim(), atualizado: true };
86
86
  if (/couldn't find remote ref|could not find remote ref/i.test(r.stderr)) {
@@ -102,7 +102,7 @@ function jsonsDaPonta(raiz, ponta, dir, prefixo) {
102
102
  * Grava uma versao nova da branch em cima de `ponta`, sem tocar arvore de trabalho nem indice:
103
103
  * indice temporario, blobs, `write-tree` e `commit-tree`. Push sem forca; recusa volta `false`.
104
104
  */
105
- function gravarNaBranch(raiz, remoto, branch, ponta, mudancas, mensagem, prefixo) {
105
+ function gravarNaBranch(raiz, remoto, branch, ponta, mudancas, mensagem, prefixo, rede = {}) {
106
106
  const indice = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'ork-branch-estado-')), 'index');
107
107
  const env = { GIT_INDEX_FILE: indice };
108
108
  try {
@@ -121,7 +121,8 @@ function gravarNaBranch(raiz, remoto, branch, ponta, mudancas, mensagem, prefixo
121
121
  }
122
122
  const arvore = exigirGit(raiz, ['write-tree'], prefixo, { env }).trim();
123
123
  const commit = exigirGit(raiz, ['commit-tree', arvore, ...(ponta ? ['-p', ponta] : []), '-m', mensagem], prefixo).trim();
124
- const push = git(raiz, ['push', '--quiet', remoto, `${commit}:refs/heads/${branch}`]);
124
+ // `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);
125
126
  if (push.ok) {
126
127
  git(raiz, ['update-ref', (0, exports.refRemota)(remoto, branch), commit]);
127
128
  return commit;
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ /**
3
+ * I-38 (T5, D7, D9): busca por significado na memoria do tenant.
4
+ *
5
+ * E ACRESCIMO, nunca troca (P2 do GOAL): o recall e a injecao de prompt continuam so por tag,
6
+ * deterministicos. Esta busca e superficie separada e todo resultado sai com
7
+ * `deterministico: false`.
8
+ *
9
+ * O universo e o export governado do tenant (filtro de injecao e visibilidade da biblioteca):
10
+ * - vetor: cosseno da consulta contra o indice local do MESMO modelo e dimensao, so para
11
+ * 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
+ * - fusao: RRF com k = 60.
14
+ * A cadeia do vetor e primario, fallback local, nenhum; sem vetor, cai para FTS com motivo tipado.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.CANDIDATOS_DO_VETOR = exports.LIMITE_MAXIMO_DA_BUSCA = exports.LIMITE_PADRAO_DA_BUSCA = exports.RRF_K = exports.MODOS_DE_BUSCA = void 0;
18
+ exports.cosseno = cosseno;
19
+ exports.fundirRrf = fundirRrf;
20
+ exports.buscarPorSignificado = buscarPorSignificado;
21
+ const indice_vetorial_1 = require("./indice-vetorial");
22
+ exports.MODOS_DE_BUSCA = ['hibrido', 'vetor', 'fts'];
23
+ /** D9: constante da fusao RRF. */
24
+ exports.RRF_K = 60;
25
+ exports.LIMITE_PADRAO_DA_BUSCA = 10;
26
+ exports.LIMITE_MAXIMO_DA_BUSCA = 100;
27
+ /** Candidatos do lado vetorial que entram na fusao. */
28
+ exports.CANDIDATOS_DO_VETOR = 50;
29
+ function cosseno(a, b) {
30
+ let ab = 0, aa = 0, bb = 0;
31
+ for (let i = 0; i < a.length; i++) {
32
+ ab += a[i] * b[i];
33
+ aa += a[i] * a[i];
34
+ bb += b[i] * b[i];
35
+ }
36
+ return aa === 0 || bb === 0 ? 0 : ab / Math.sqrt(aa * bb);
37
+ }
38
+ /** RRF: soma de 1 / (k + posicao) em cada lista em que o id aparece (posicao comeca em 1). */
39
+ function fundirRrf(listas, k = exports.RRF_K) {
40
+ const scores = new Map();
41
+ for (const lista of listas)
42
+ lista.forEach((id, i) => scores.set(id, (scores.get(id) ?? 0) + 1 / (k + i + 1)));
43
+ return scores;
44
+ }
45
+ const resumo = (conteudo) => conteudo.split('\n')[0].slice(0, 100);
46
+ /** Lado vetorial: o primeiro caminho da cadeia que tem indice, responde e fala o mesmo espaco. */
47
+ function ladoVetorial(o) {
48
+ const detalhes = [];
49
+ let motivo = null;
50
+ const falhar = (alvo, m, d) => {
51
+ if (!motivo)
52
+ motivo = m;
53
+ detalhes.push(`${alvo}: ${m} (${d})`);
54
+ };
55
+ if (o.config.provider === 'none') {
56
+ return { alvo: null, modelo: null, ranking: [], motivo: 'embeddings.nao-configurado', detalhes: ['memory.embedding.provider none'] };
57
+ }
58
+ const base = (0, indice_vetorial_1.impressaoDaBase)(o.dsn);
59
+ for (const alvo of ['primario', 'fallback']) {
60
+ if (alvo === 'primario' && !o.chavePresente) {
61
+ falhar(alvo, 'embeddings.chave-ausente', 'variavel declarada fora do ambiente');
62
+ continue;
63
+ }
64
+ if (alvo === 'fallback' && !o.config.fallback_model) {
65
+ falhar(alvo, 'embeddings.nao-configurado', 'fallback_model vazio');
66
+ continue;
67
+ }
68
+ const espaco = (0, indice_vetorial_1.espacoDoAlvo)(o.config, alvo, o.env);
69
+ if ('motivo' in espaco) {
70
+ falhar(alvo, espaco.motivo, espaco.detalhe);
71
+ continue;
72
+ }
73
+ if (alvo === 'fallback' && !o.fallbackUsavel) {
74
+ falhar(alvo, 'embeddings.dependencia-ausente', 'torch/transformers ausentes na ponte');
75
+ continue;
76
+ }
77
+ const lido = (0, indice_vetorial_1.lerIndice)((0, indice_vetorial_1.arquivoDoIndice)(o.raiz, o.tenant, espaco.modelo, espaco.dim), { tenant: o.tenant, base, modelo: espaco.modelo, dim: espaco.dim });
78
+ if (lido.outroEspaco) {
79
+ falhar(alvo, 'embeddings.espaco-vetorial-divergente', 'arquivo de indice de outro modelo, dimensao ou base');
80
+ continue;
81
+ }
82
+ if (!lido.existia) {
83
+ falhar(alvo, 'embeddings.indice-ausente', `rode ork memory index --modelo ${alvo}`);
84
+ continue;
85
+ }
86
+ if (!o.embeddar) {
87
+ falhar(alvo, 'embeddings.provider-indisponivel', 'transporte de embedding ausente');
88
+ continue;
89
+ }
90
+ let resposta;
91
+ try {
92
+ resposta = o.embeddar({ papel: 'consulta', alvo, modelo: espaco.modelo, dim: espaco.dim, textos: [o.texto] }, { timeoutMs: o.timeoutMs });
93
+ }
94
+ catch (erro) {
95
+ falhar(alvo, (0, indice_vetorial_1.codigoDeEmbedding)(erro), 'a consulta nao foi embedada');
96
+ continue;
97
+ }
98
+ const vetor = resposta?.vetores?.[0];
99
+ // Nunca comparar espacos diferentes: modelo e dimensao da consulta precisam ser os do indice.
100
+ if (resposta?.modelo !== lido.indice.modelo || resposta?.dim !== lido.indice.dim || !Array.isArray(vetor) || vetor.length !== lido.indice.dim) {
101
+ falhar(alvo, 'embeddings.espaco-vetorial-divergente', 'consulta e indice de modelos ou dimensoes diferentes');
102
+ continue;
103
+ }
104
+ if (resposta.truncados?.length)
105
+ detalhes.push(`${alvo}: consulta acima do contexto do modelo, embedada pelo comeco`);
106
+ const ranking = [...(0, indice_vetorial_1.vetoresCoerentes)(lido.indice, o.universo)]
107
+ .map(([id, v]) => ({ id, sim: cosseno(vetor, v) }))
108
+ .sort((a, b) => b.sim - a.sim || a.id.localeCompare(b.id));
109
+ return { alvo, modelo: espaco.modelo, ranking, motivo, detalhes };
110
+ }
111
+ return { alvo: null, modelo: null, ranking: [], motivo, detalhes };
112
+ }
113
+ /** `ork memory search --texto`: ranking hibrido, nunca deterministico, sempre dentro do tenant. */
114
+ 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]));
117
+ const saida = { texto: o.texto, modo: o.modo, origem: 'nenhum', modeloUsado: null,
118
+ deterministico: false, motivo: null, detalhe: '', resultados: [], listas: { vetor: [], fts: [] } };
119
+ const detalhes = [];
120
+ let similaridade = new Map();
121
+ if (o.modo !== 'fts') {
122
+ const v = ladoVetorial({ ...o, universo });
123
+ detalhes.push(...v.detalhes);
124
+ saida.motivo = v.motivo;
125
+ if (v.alvo) {
126
+ saida.origem = v.alvo;
127
+ saida.modeloUsado = v.modelo;
128
+ const candidatos = v.ranking.slice(0, exports.CANDIDATOS_DO_VETOR);
129
+ saida.listas.vetor = candidatos.map(c => c.id);
130
+ similaridade = new Map(candidatos.map(c => [c.id, c.sim]));
131
+ }
132
+ }
133
+ if (o.modo !== 'vetor') {
134
+ try {
135
+ if (!o.buscarTexto)
136
+ 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));
139
+ }
140
+ catch (erro) {
141
+ const codigo = /^[a-z]+(?:\.[a-z-]+)+/.exec(erro instanceof Error ? erro.message : '')?.[0] ?? 'memory.transport.fts';
142
+ detalhes.push(`fts: ${codigo}`);
143
+ if (o.modo === 'fts' || saida.origem === 'nenhum')
144
+ saida.motivo = saida.motivo ?? codigo;
145
+ }
146
+ if (saida.origem === 'nenhum' && !detalhes.some(d => d.startsWith('fts:')))
147
+ saida.origem = 'fts';
148
+ }
149
+ const scores = fundirRrf([saida.listas.vetor, saida.listas.fts]);
150
+ saida.resultados = [...scores]
151
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
152
+ .slice(0, o.limite)
153
+ .map(([id, score]) => ({
154
+ id, collection: porId.get(id).collection, score: Math.round(score * 1e6) / 1e6,
155
+ fontes: [...(saida.listas.vetor.includes(id) ? ['vetor'] : []), ...(saida.listas.fts.includes(id) ? ['fts'] : [])],
156
+ similaridade: similaridade.has(id) ? Math.round(similaridade.get(id) * 1e4) / 1e4 : null,
157
+ resumo: resumo(porId.get(id).content),
158
+ }));
159
+ saida.detalhe = detalhes.join('; ');
160
+ return saida;
161
+ }