@orkastery/cli 0.4.3 → 0.5.1

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 (146) 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/README.md +11 -2
  4. package/adapters/claude-code/agents/ork-check.md +1 -1
  5. package/adapters/claude-code/agents/ork-go.md +2 -2
  6. package/adapters/claude-code/agents/ork-goal.md +1 -1
  7. package/adapters/claude-code/agents/ork-master.md +1 -1
  8. package/adapters/claude-code/agents/ork-plan.md +1 -1
  9. package/adapters/claude-code/agents/ork-ship.md +1 -1
  10. package/adapters/claude-code/commands/master.md +11 -8
  11. package/adapters/claude-code/commands/onboarding.md +3 -2
  12. package/adapters/claude-code/commands/ork.md +8 -0
  13. package/adapters/claude-code/commands/ship.md +2 -2
  14. package/adapters/codex/skills/ork/SKILL.md +12 -1
  15. package/adapters/hermes/README.md +3 -0
  16. package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
  17. package/adapters/hermes/bin/ork-brain.sh +3 -0
  18. package/adapters/hermes/bin/ork-maestro.sh +4 -1
  19. package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
  20. package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
  21. package/adapters/hermes/hermes.plugin.json +2 -0
  22. package/adapters/hermes/hitl-ingress/__init__.py +36 -3
  23. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
  24. package/adapters/openclaw/README.md +45 -2
  25. package/adapters/openclaw/dist/hitl-ingress.js +37 -3
  26. package/adapters/openclaw/dist/index.js +93 -21
  27. package/adapters/openclaw/openclaw.plugin.json +10 -1
  28. package/adapters/openclaw/package.json +2 -2
  29. package/adapters/openclaw/src/hitl-ingress.ts +32 -3
  30. package/adapters/openclaw/src/index.ts +109 -21
  31. package/assets/docs/padroes/documentacao-de-produto.md +2 -2
  32. package/assets/docs/padroes/roadmap-de-produto.md +1 -1
  33. package/assets/docs/produto/_modelo-feature.md +2 -0
  34. package/assets/orkmind_bridge.py +214 -1
  35. package/dist/adapters/claude-bg.js +24 -7
  36. package/dist/auditrun.js +1 -1
  37. package/dist/board.js +74 -0
  38. package/dist/branch-de-estado.js +5 -4
  39. package/dist/busca-semantica.js +161 -0
  40. package/dist/ci.js +141 -10
  41. package/dist/claim-lint.js +3 -1
  42. package/dist/company-brain-cli.js +27 -2
  43. package/dist/company-brain-context.js +132 -0
  44. package/dist/company-brain-dossie.js +376 -0
  45. package/dist/company-brain-mcp.js +4 -1
  46. package/dist/conducao.js +71 -18
  47. package/dist/decisao-autonoma.js +42 -0
  48. package/dist/docs.js +135 -25
  49. package/dist/doctor.js +242 -32
  50. package/dist/entrega-pr.js +116 -5
  51. package/dist/experiencia-instalacao.js +256 -0
  52. package/dist/experiencia.js +72 -0
  53. package/dist/fabrica-estado.js +63 -12
  54. package/dist/fechamento.js +105 -0
  55. package/dist/fix.js +3 -3
  56. package/dist/forja.js +435 -0
  57. package/dist/gates.js +9 -1
  58. package/dist/hitl-classificacao.js +11 -1
  59. package/dist/hitl-contract.js +50 -7
  60. package/dist/hitl-curto.js +158 -0
  61. package/dist/hitl-gates.js +67 -1
  62. package/dist/hitl-lote.js +30 -15
  63. package/dist/hitl-presentation.js +10 -7
  64. package/dist/hitl-resumo.js +57 -13
  65. package/dist/hitl-texto-livre.js +150 -0
  66. package/dist/hosts.js +92 -3
  67. package/dist/index.js +637 -45
  68. package/dist/indice-vetorial.js +317 -0
  69. package/dist/init.js +24 -5
  70. package/dist/integracoes-locais.js +3 -1
  71. package/dist/intelligence-benchmark-contract.js +570 -0
  72. package/dist/intelligence-graph-cli.js +584 -0
  73. package/dist/intelligence-graph-contract.js +636 -0
  74. package/dist/intelligence-graph-extract-md.js +487 -0
  75. package/dist/intelligence-graph-extract-ts.js +980 -0
  76. package/dist/intelligence-graph-extract.js +546 -0
  77. package/dist/intelligence-graph-index.js +693 -0
  78. package/dist/intelligence-graph-parsers.js +327 -0
  79. package/dist/intelligence-graph-query.js +344 -0
  80. package/dist/intelligence-graph-repo.js +197 -0
  81. package/dist/leases.js +100 -0
  82. package/dist/ledger.js +23 -0
  83. package/dist/licoes.js +5 -4
  84. package/dist/liveness.js +2 -1
  85. package/dist/maestro-cli.js +10 -1
  86. package/dist/maestro-contract.js +5 -1
  87. package/dist/maestro-discovery.js +16 -2
  88. package/dist/maestro-runtime.js +3 -0
  89. package/dist/maestro-snapshot.js +6 -2
  90. package/dist/manifest.js +166 -5
  91. package/dist/master-batch.js +3 -2
  92. package/dist/master-digest.js +2 -2
  93. package/dist/master-nota.js +203 -0
  94. package/dist/master.js +4 -0
  95. package/dist/mcp-artifacts.js +2 -0
  96. package/dist/mcp-experiencia.js +164 -0
  97. package/dist/mcp-git.js +64 -12
  98. package/dist/mcp-grafo-worker.js +91 -0
  99. package/dist/mcp-grafo.js +233 -0
  100. package/dist/mcp-install.js +5 -2
  101. package/dist/mcp-maestro.js +3 -2
  102. package/dist/mcp-server.js +93 -5
  103. package/dist/mcp-ship.js +2 -0
  104. package/dist/mcp-verify.js +3 -1
  105. package/dist/memoria.js +147 -1
  106. package/dist/network-roadmap.js +907 -0
  107. package/dist/ocupacao.js +43 -3
  108. package/dist/onboarding.js +122 -11
  109. package/dist/orkmind.js +165 -9
  110. package/dist/orquestracao.js +2 -1
  111. package/dist/parado-no-condutor.js +933 -0
  112. package/dist/phase.js +166 -13
  113. package/dist/policies.js +29 -2
  114. package/dist/projeto-alvo.js +551 -0
  115. package/dist/prompts.js +1 -1
  116. package/dist/prova-ativacao.js +345 -0
  117. package/dist/pulse-consentimento.js +4 -2
  118. package/dist/pulse-delivery.js +32 -6
  119. package/dist/pulse-resposta.js +674 -49
  120. package/dist/pulse.js +74 -3
  121. package/dist/retry.js +51 -8
  122. package/dist/roadmap-reservas.js +262 -20
  123. package/dist/roadmap-status.js +348 -0
  124. package/dist/runtime-context.js +5 -1
  125. package/dist/sessoes-inventario.js +7 -1
  126. package/dist/sessoes.js +4 -1
  127. package/dist/ship.js +25 -1
  128. package/dist/thread-close.js +3 -0
  129. package/dist/thread.js +21 -2
  130. package/dist/util.js +71 -4
  131. package/dist/verify.js +6 -5
  132. package/dist/worktree.js +29 -6
  133. package/eval/casos/onboarding.json +1 -1
  134. package/eval/casos/orchestration-experience-pt-br.json +48 -0
  135. package/eval/casos/orchestration-experience.json +48 -0
  136. package/package.json +1 -1
  137. package/schemas/code-artifact-graph.v1.schema.json +581 -0
  138. package/schemas/graph-benchmark.v1.schema.json +2240 -0
  139. package/schemas/maestro-snapshot.schema.json +23 -0
  140. package/skills/README.md +7 -5
  141. package/skills/core/onboarding/SKILL.md +10 -2
  142. package/skills/core/orchestration-experience/SKILL.md +113 -0
  143. package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
  144. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  145. package/skills/phases/go-implementation/SKILL.md +2 -0
  146. package/skills/phases/ship-release/SKILL.md +6 -3
@@ -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
@@ -71,6 +71,7 @@ const runtime_ambiente_1 = require("../runtime-ambiente");
71
71
  const path = __importStar(require("node:path"));
72
72
  const runtime_context_1 = require("../runtime-context");
73
73
  const runtime_profiles_1 = require("../runtime-profiles");
74
+ const mcp_grafo_1 = require("../mcp-grafo");
74
75
  function exec(cmd, args, cwd, timeoutMs, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
75
76
  return (0, util_1.exec)(cmd, args, cwd, timeoutMs, ambiente);
76
77
  }
@@ -102,12 +103,19 @@ const CONSULTAS_MCP = [
102
103
  'mcp__orkastery__ork_artifact_read',
103
104
  'mcp__orkastery__ork_claims_list',
104
105
  ];
106
+ // RM-037 (defeito 1): a decisao autonoma pelo MCP tem o mesmo grant nos dois runtimes.
105
107
  const MUTACOES_WORKTREE = [
106
- 'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_claim_add',
108
+ 'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_claim_add', 'mcp__orkastery__ork_decision_record',
107
109
  'mcp__orkastery__ork_git_commit', 'mcp__orkastery__ork_verify', 'mcp__orkastery__ork_ship',
108
110
  ];
111
+ /**
112
+ * RM-031 KG5 (D7): as consultas do grafo, de leitura, so com a flag do manifesto da raiz; sem ela, nada muda.
113
+ * Montada na chamada: o `mcp-grafo` chega a este modulo pelo `mcp-git`, e no ciclo de imports o valor pode
114
+ * ainda nao existir quando este modulo carrega.
115
+ */
116
+ const consultasDoGrafoMcp = () => mcp_grafo_1.TOOLS_DO_GRAFO.map((nome) => `mcp__orkastery__${nome}`);
109
117
  /** Regras nativas de arquivo, nao sandbox de processos. Edit tambem cobre Write. */
110
- function permissoesDaWorktree(cwd, permiteEditarProduto) {
118
+ function permissoesDaWorktree(cwd, permiteEditarProduto, consultas) {
111
119
  if (!path.isAbsolute(cwd) || cwd === '/' || path.normalize(cwd) !== cwd || !/^[A-Za-z0-9_./-]+$/.test(cwd))
112
120
  throw Error('runtime.context.permissions: worktree contem caracteres ambiguos para regras nativas; use caminho sem glob, espacos ou delimitadores');
113
121
  const absoluto = '/' + cwd; // Claude exige // para caminho absoluto.
@@ -116,7 +124,7 @@ function permissoesDaWorktree(cwd, permiteEditarProduto) {
116
124
  for (const prefixo of [`${absoluto}/${nome}`, `${absoluto}/**/${nome}`])
117
125
  deny.push(`Edit(${prefixo})`, `Edit(${prefixo}/**)`);
118
126
  }
119
- return { allow: [...CONSULTAS_MCP, 'mcp__orkastery__ork_git_status', ...MUTACOES_WORKTREE, ...(permiteEditarProduto ? [`Edit(${absoluto}/**)`] : [])], deny };
127
+ return { allow: [...consultas, 'mcp__orkastery__ork_git_status', ...MUTACOES_WORKTREE, ...(permiteEditarProduto ? [`Edit(${absoluto}/**)`] : [])], deny };
120
128
  }
121
129
  /** O runtime esta disponivel nesta maquina? */
122
130
  function disponivel() {
@@ -151,11 +159,14 @@ function montarComando(pedido) {
151
159
  if (pedido.contextoRuntime.host !== 'claude-code')
152
160
  throw Error('runtime.context.invalid: host Claude esperado');
153
161
  const contexto = (0, runtime_context_1.validarContextoRuntime)(pedido.contextoRuntime, pedido.cwd);
162
+ const consultas = [...CONSULTAS_MCP, ...(contexto.grafoMcp ? consultasDoGrafoMcp() : [])];
154
163
  // Somente o servidor gerado: nao mesclar MCP de outros escopos nesta sessao filha.
155
164
  const permissoes = contexto.permissoesFilho === 'worktree'
156
- ? 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];
165
+ ? permissoesDaWorktree(pedido.cwd, contexto.permiteEditarProduto && !plano, consultas) : null;
166
+ // PLAN não implementa: nem commit, nem claim, nem verify, nem ship, em nenhum perfil. Decidir não é
167
+ // implementar, e o PLAN é onde as decisões nascem (RM-037, achado S7 do CHECK): a decisão autônoma fica.
168
+ const allow = plano ? [...consultas, 'mcp__orkastery__ork_artifact_write', 'mcp__orkastery__ork_decision_record']
169
+ : permissoes?.allow ?? consultas;
159
170
  const deny = [...(plano ? ESCRITA_DE_ARQUIVO : []), ...(permissoes?.deny ?? [])];
160
171
  args.push('--plugin-dir', contexto.instalacao, '--strict-mcp-config', '--mcp-config', JSON.stringify({ mcpServers: { orkastery: contexto.servidor } }), '--allowedTools', allow.join(','));
161
172
  if (deny.length)
@@ -186,12 +197,18 @@ function extrairSessionId(saida) {
186
197
  function listarSessoes(cwd, todas = false, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
187
198
  return consultarSessoes(cwd, todas, ambiente).sessoes;
188
199
  }
189
- /** Falha de consulta não pode ser confundida com uma máquina sem sessões. */
200
+ /**
201
+ * Falha de consulta não pode ser confundida com uma máquina sem sessões. O binário ausente segue
202
+ * falha aqui (o observador e o pulse não concluem nada dele); só o inventário o trata como fonte
203
+ * ausente, com a correção.
204
+ */
190
205
  function consultarSessoes(cwd, todas = false, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
191
206
  const args = ['agents', '--json'];
192
207
  if (todas)
193
208
  args.push('--all');
194
209
  const r = exec('claude', args, undefined, 60000, ambiente);
210
+ if (r.error === 'ENOENT')
211
+ return { ok: false, ausente: true, sessoes: [], detalhe: 'binário `claude` fora do PATH' };
195
212
  if (!r.ok)
196
213
  return { ok: false, sessoes: [], detalhe: `claude agents falhou (código ${r.code})` };
197
214
  let dados;
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}`);
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
+ }