@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.
- package/adapters/README.md +11 -0
- package/adapters/claude-code/.claude-plugin/plugin.json +3 -1
- package/adapters/claude-code/agents/ork-check.md +1 -1
- package/adapters/claude-code/agents/ork-go.md +2 -2
- package/adapters/claude-code/agents/ork-goal.md +1 -1
- package/adapters/claude-code/agents/ork-master.md +1 -1
- package/adapters/claude-code/agents/ork-plan.md +1 -1
- package/adapters/claude-code/agents/ork-ship.md +1 -1
- package/adapters/claude-code/commands/master.md +11 -8
- package/adapters/claude-code/commands/ork.md +8 -0
- package/adapters/claude-code/commands/ship.md +2 -2
- package/adapters/codex/skills/ork/SKILL.md +12 -1
- package/adapters/hermes/README.md +3 -0
- package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
- package/adapters/hermes/bin/ork-brain.sh +3 -0
- package/adapters/hermes/bin/ork-maestro.sh +4 -1
- package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
- package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
- package/adapters/hermes/hermes.plugin.json +2 -0
- package/adapters/hermes/hitl-ingress/__init__.py +36 -3
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
- package/adapters/openclaw/README.md +34 -2
- package/adapters/openclaw/dist/hitl-ingress.js +37 -3
- package/adapters/openclaw/dist/index.js +92 -20
- package/adapters/openclaw/openclaw.plugin.json +10 -1
- package/adapters/openclaw/package.json +2 -2
- package/adapters/openclaw/src/hitl-ingress.ts +32 -3
- package/adapters/openclaw/src/index.ts +108 -20
- package/assets/docs/padroes/documentacao-de-produto.md +2 -2
- package/assets/docs/padroes/roadmap-de-produto.md +1 -1
- package/assets/docs/produto/_modelo-feature.md +2 -0
- package/assets/orkmind_bridge.py +214 -1
- package/dist/adapters/claude-bg.js +6 -3
- package/dist/board.js +74 -0
- package/dist/branch-de-estado.js +5 -4
- package/dist/busca-semantica.js +161 -0
- package/dist/ci.js +141 -10
- package/dist/company-brain-cli.js +27 -2
- package/dist/company-brain-context.js +132 -0
- package/dist/company-brain-dossie.js +376 -0
- package/dist/company-brain-mcp.js +4 -1
- package/dist/conducao.js +71 -18
- package/dist/decisao-autonoma.js +42 -0
- package/dist/docs.js +64 -22
- package/dist/doctor.js +33 -0
- package/dist/entrega-pr.js +109 -5
- package/dist/experiencia-instalacao.js +256 -0
- package/dist/experiencia.js +72 -0
- package/dist/fabrica-estado.js +43 -10
- package/dist/fechamento.js +105 -0
- package/dist/forja.js +435 -0
- package/dist/gates.js +9 -1
- package/dist/hitl-classificacao.js +11 -1
- package/dist/hitl-contract.js +50 -7
- package/dist/hitl-curto.js +158 -0
- package/dist/hitl-gates.js +67 -1
- package/dist/hitl-lote.js +30 -15
- package/dist/hitl-presentation.js +10 -7
- package/dist/hitl-resumo.js +33 -12
- package/dist/hitl-texto-livre.js +150 -0
- package/dist/hosts.js +86 -1
- package/dist/index.js +602 -38
- package/dist/indice-vetorial.js +317 -0
- package/dist/init.js +13 -0
- package/dist/integracoes-locais.js +1 -1
- package/dist/intelligence-benchmark-contract.js +570 -0
- package/dist/intelligence-graph-cli.js +430 -0
- package/dist/intelligence-graph-contract.js +577 -0
- package/dist/intelligence-graph-extract-md.js +469 -0
- package/dist/intelligence-graph-extract-ts.js +810 -0
- package/dist/intelligence-graph-extract.js +369 -0
- package/dist/intelligence-graph-index.js +512 -0
- package/dist/intelligence-graph-parsers.js +327 -0
- package/dist/intelligence-graph-query.js +344 -0
- package/dist/intelligence-graph-repo.js +187 -0
- package/dist/leases.js +84 -0
- package/dist/ledger.js +14 -0
- package/dist/maestro-cli.js +10 -1
- package/dist/maestro-contract.js +5 -1
- package/dist/maestro-discovery.js +16 -2
- package/dist/maestro-snapshot.js +6 -2
- package/dist/manifest.js +142 -5
- package/dist/master-batch.js +3 -2
- package/dist/master-digest.js +2 -2
- package/dist/master-nota.js +203 -0
- package/dist/master.js +4 -0
- package/dist/mcp-experiencia.js +164 -0
- package/dist/mcp-git.js +4 -0
- package/dist/mcp-install.js +3 -1
- package/dist/mcp-maestro.js +3 -2
- package/dist/mcp-server.js +84 -5
- package/dist/memoria.js +147 -1
- package/dist/network-roadmap.js +907 -0
- package/dist/ocupacao.js +38 -2
- package/dist/onboarding.js +113 -7
- package/dist/orkmind.js +165 -9
- package/dist/phase.js +166 -13
- package/dist/policies.js +2 -0
- package/dist/projeto-alvo.js +551 -0
- package/dist/prompts.js +1 -1
- package/dist/pulse-consentimento.js +4 -2
- package/dist/pulse-delivery.js +5 -0
- package/dist/pulse-resposta.js +674 -49
- package/dist/pulse.js +21 -0
- package/dist/retry.js +51 -8
- package/dist/roadmap-reservas.js +262 -20
- package/dist/roadmap-status.js +213 -0
- package/dist/thread-close.js +3 -0
- package/dist/verify.js +4 -3
- package/eval/casos/orchestration-experience-pt-br.json +48 -0
- package/eval/casos/orchestration-experience.json +48 -0
- package/package.json +1 -1
- package/schemas/code-artifact-graph.v1.schema.json +581 -0
- package/schemas/graph-benchmark.v1.schema.json +2240 -0
- package/schemas/maestro-snapshot.schema.json +23 -0
- package/skills/README.md +7 -5
- package/skills/core/onboarding/SKILL.md +7 -0
- package/skills/core/orchestration-experience/SKILL.md +113 -0
- package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- package/skills/phases/go-implementation/SKILL.md +2 -0
package/assets/orkmind_bridge.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Transporte de fabrica: OrkMind instalado, stdin JSON
|
|
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
|
-
|
|
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
|
*
|
package/dist/branch-de-estado.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|