@orkastery/cli 0.4.2 → 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 -3
- package/adapters/openclaw/dist/hitl-ingress.js +37 -3
- package/adapters/openclaw/dist/index.js +92 -39
- package/adapters/openclaw/openclaw.plugin.json +10 -3
- package/adapters/openclaw/package.json +2 -2
- package/adapters/openclaw/src/hitl-ingress.ts +32 -3
- package/adapters/openclaw/src/index.ts +108 -41
- 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 +88 -9
- package/dist/adapters/codex-events.js +3 -1
- 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 +117 -31
- 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/fix.js +1 -1
- package/dist/forja.js +435 -0
- package/dist/gates.js +10 -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 +605 -41
- 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 +36 -10
- 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/ratelimit.js +3 -2
- package/dist/retry.js +230 -9
- package/dist/roadmap-reservas.js +262 -20
- package/dist/roadmap-status.js +213 -0
- package/dist/runtime-ambiente.js +12 -1
- package/dist/session-watcher-claude.js +26 -0
- package/dist/sessoes.js +49 -15
- package/dist/thread-close.js +3 -0
- package/dist/verify.js +5 -4
- package/dist/worktree.js +77 -0
- 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
|
|
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.JANELA_CURTA_MAX_MS = exports.NOME_ADAPTER = void 0;
|
|
46
46
|
exports.ambienteDoPerfil = ambienteDoPerfil;
|
|
47
|
+
exports.semContextoDeDespacho = semContextoDeDespacho;
|
|
47
48
|
exports.disponivel = disponivel;
|
|
48
49
|
exports.versao = versao;
|
|
49
50
|
exports.montarComando = montarComando;
|
|
@@ -62,6 +63,8 @@ exports.limparAnsi = limparAnsi;
|
|
|
62
63
|
exports.logs = logs;
|
|
63
64
|
exports.logsDaSessao = logsDaSessao;
|
|
64
65
|
exports.parar = parar;
|
|
66
|
+
exports.pararAchada = pararAchada;
|
|
67
|
+
exports.acharSessaoNasContas = acharSessaoNasContas;
|
|
65
68
|
exports.ocupacaoDeContexto = ocupacaoDeContexto;
|
|
66
69
|
const util_1 = require("../util");
|
|
67
70
|
const runtime_ambiente_1 = require("../runtime-ambiente");
|
|
@@ -82,6 +85,14 @@ function ambienteDoPerfil(perfil) {
|
|
|
82
85
|
return (0, runtime_profiles_1.ambienteComPerfil)((0, runtime_ambiente_1.ambienteDeAssinatura)(), perfil);
|
|
83
86
|
}
|
|
84
87
|
exports.NOME_ADAPTER = 'claude-bg';
|
|
88
|
+
/** RM-037 (defeitosdeco D-4): o ambiente do `claude --bg` sem canal nem chave do contexto do despacho. */
|
|
89
|
+
function semContextoDeDespacho(base, contexto) {
|
|
90
|
+
const env = { ...base };
|
|
91
|
+
delete env.ORK_CANAL;
|
|
92
|
+
for (const chave of Object.keys(contexto ?? {}))
|
|
93
|
+
delete env[chave];
|
|
94
|
+
return env;
|
|
95
|
+
}
|
|
85
96
|
/** Consultas do perfil interactive; gates e respostas humanas nunca recebem grant. */
|
|
86
97
|
const CONSULTAS_MCP = [
|
|
87
98
|
'mcp__orkastery__ork_thread_status',
|
|
@@ -91,8 +102,9 @@ const CONSULTAS_MCP = [
|
|
|
91
102
|
'mcp__orkastery__ork_artifact_read',
|
|
92
103
|
'mcp__orkastery__ork_claims_list',
|
|
93
104
|
];
|
|
105
|
+
// RM-037 (defeito 1): a decisao autonoma pelo MCP tem o mesmo grant nos dois runtimes.
|
|
94
106
|
const MUTACOES_WORKTREE = [
|
|
95
|
-
'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',
|
|
96
108
|
'mcp__orkastery__ork_git_commit', 'mcp__orkastery__ork_verify', 'mcp__orkastery__ork_ship',
|
|
97
109
|
];
|
|
98
110
|
/** Regras nativas de arquivo, nao sandbox de processos. Edit tambem cobre Write. */
|
|
@@ -129,6 +141,13 @@ function montarComando(pedido) {
|
|
|
129
141
|
args.push('--model', pedido.model);
|
|
130
142
|
if (pedido.effort)
|
|
131
143
|
args.push('--effort', pedido.effort);
|
|
144
|
+
// RM-037 (defeitosdeco D-4): o contexto do despacho (identidade, thread, canal) chega POR SESSAO.
|
|
145
|
+
// O `claude --bg` entrega a sessao a um processo reserva do daemon da conta, e o daemon guarda o
|
|
146
|
+
// ambiente de quem o iniciou: pelo ambiente, a sessao nascia com a identidade de outro despacho.
|
|
147
|
+
// Medido em 28/09/2026 (2.1.284): o `env` de `--settings` chega ao Bash da sessao e vence o herdado.
|
|
148
|
+
const doDespacho = pedido.ambienteExtra ?? {};
|
|
149
|
+
if (Object.keys(doDespacho).length)
|
|
150
|
+
args.push('--settings', JSON.stringify({ env: doDespacho }));
|
|
132
151
|
if (pedido.contextoRuntime) {
|
|
133
152
|
if (pedido.contextoRuntime.host !== 'claude-code')
|
|
134
153
|
throw Error('runtime.context.invalid: host Claude esperado');
|
|
@@ -136,8 +155,10 @@ function montarComando(pedido) {
|
|
|
136
155
|
// Somente o servidor gerado: nao mesclar MCP de outros escopos nesta sessao filha.
|
|
137
156
|
const permissoes = contexto.permissoesFilho === 'worktree'
|
|
138
157
|
? permissoesDaWorktree(pedido.cwd, contexto.permiteEditarProduto && !plano) : null;
|
|
139
|
-
// PLAN não implementa: nem commit, nem claim, nem verify, nem ship, em nenhum perfil.
|
|
140
|
-
|
|
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];
|
|
141
162
|
const deny = [...(plano ? ESCRITA_DE_ARQUIVO : []), ...(permissoes?.deny ?? [])];
|
|
142
163
|
args.push('--plugin-dir', contexto.instalacao, '--strict-mcp-config', '--mcp-config', JSON.stringify({ mcpServers: { orkastery: contexto.servidor } }), '--allowedTools', allow.join(','));
|
|
143
164
|
if (deny.length)
|
|
@@ -255,8 +276,10 @@ function acharSessao(chave, sessoes) {
|
|
|
255
276
|
function despachar(pedido) {
|
|
256
277
|
const comando = montarComando(pedido);
|
|
257
278
|
let ambiente;
|
|
279
|
+
// D-4: nada do contexto do despacho vai ao ambiente do processo `claude`, para um daemon novo nunca
|
|
280
|
+
// nascer com ele; a identidade herdada ja sai em `ambienteDeAssinatura`, e o canal sai aqui.
|
|
258
281
|
try {
|
|
259
|
-
ambiente =
|
|
282
|
+
ambiente = semContextoDeDespacho(ambienteDoPerfil(pedido.perfil), pedido.ambienteExtra);
|
|
260
283
|
}
|
|
261
284
|
catch (e) {
|
|
262
285
|
return { ok: false, comando, sessionId: null, verificada: false, stdout: '', stderr: '', erro: e.message };
|
|
@@ -497,6 +520,13 @@ const FRASES_DE_AUTH_AUSENTE = [
|
|
|
497
520
|
* so vale como auth ausente quando o mesmo texto nao traz frase de cota: a cota, mais especifica, vence.
|
|
498
521
|
*/
|
|
499
522
|
const FRASES_AMPLAS_DE_AUTH = [/\bcodex login\b/i];
|
|
523
|
+
/**
|
|
524
|
+
* RM-037 (defeitosdeco D-6): modelo inexistente ou sem acesso na conta. Medido na transcricao da
|
|
525
|
+
* sessao af32834f (28/09/2026, `fable-5-1` num perfil de conta sem esse modelo): `error: "model_not_found"` e o texto
|
|
526
|
+
* "There's an issue with the selected model (fable-5-1). It may not exist or you may not have access
|
|
527
|
+
* to it". Frase de sobrecarga ("model is overloaded") nao entra: e falha transitoria da infra.
|
|
528
|
+
*/
|
|
529
|
+
const FRASES_DE_MODELO_INACESSIVEL = [/\bmodel_not_found\b/i, /issue with the selected model/i];
|
|
500
530
|
/**
|
|
501
531
|
* I-33 (D16): CRITERIO UNICO entre ESGOTAMENTO da conta e RATE LIMIT comum. Decisao do dono em
|
|
502
532
|
* 19/09/2026 (opcao a do A3 do CHECK aa279e17): esgotamento de cota, credito ou limite do plano
|
|
@@ -593,6 +623,9 @@ function naturezaDoLimite(saida, agoraMs = Date.now()) {
|
|
|
593
623
|
}
|
|
594
624
|
function parseFalhaDeConta(saida, agoraMs = Date.now()) {
|
|
595
625
|
const texto = saida ?? '';
|
|
626
|
+
const modelo = FRASES_DE_MODELO_INACESSIVEL.find((r) => r.test(texto));
|
|
627
|
+
if (modelo)
|
|
628
|
+
return { motivo: 'runtime.model-unavailable', resetEm: null, fonte: 'sem-horario', trecho: trechoDa(texto, modelo) };
|
|
596
629
|
const auth = FRASES_DE_AUTH_AUSENTE.find((r) => r.test(texto));
|
|
597
630
|
if (auth)
|
|
598
631
|
return { motivo: 'runtime.auth-missing', resetEm: null, fonte: 'sem-horario', trecho: trechoDa(texto, auth) };
|
|
@@ -645,9 +678,15 @@ function conferirAuth(perfil) {
|
|
|
645
678
|
}
|
|
646
679
|
return { ok: true, detalhe: `claude auth status: loggedIn (${metodo})` };
|
|
647
680
|
}
|
|
648
|
-
/**
|
|
649
|
-
|
|
650
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Comando de anexar a sessao (precisa de TTY, entao o CLI imprime em vez de executar). RM-037
|
|
683
|
+
* (defeitosdeco D-2): sessao de um perfil so e anexada com o `CLAUDE_CONFIG_DIR` daquela conta.
|
|
684
|
+
*/
|
|
685
|
+
function comandoAttach(sessionId, configDir) {
|
|
686
|
+
if (!configDir)
|
|
687
|
+
return `claude attach ${sessionId}`;
|
|
688
|
+
const dir = /^[A-Za-z0-9_./-]+$/.test(configDir) ? configDir : `'${configDir.replace(/'/g, `'\\''`)}'`;
|
|
689
|
+
return `CLAUDE_CONFIG_DIR=${dir} claude attach ${sessionId}`;
|
|
651
690
|
}
|
|
652
691
|
/**
|
|
653
692
|
* Transforma o dump de tela que o `claude logs` devolve em texto legivel.
|
|
@@ -736,10 +775,50 @@ function logsDaSessao(id, linhas = 60, ambiente = (0, runtime_ambiente_1.ambient
|
|
|
736
775
|
/** Para uma sessao em background (o `claude stop` tambem trabalha com o id curto). */
|
|
737
776
|
function parar(chave, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
|
|
738
777
|
const sessao = acharSessao(chave, listarSessoes(undefined, true, ambiente));
|
|
739
|
-
|
|
740
|
-
|
|
778
|
+
return pararAchada(sessao ?? { sessionId: chave }, ambiente);
|
|
779
|
+
}
|
|
780
|
+
/** Para a sessao ja achada numa conta, com o ambiente dessa conta, sem listar de novo. */
|
|
781
|
+
function pararAchada(sessao, ambiente = (0, runtime_ambiente_1.ambienteDeAssinatura)()) {
|
|
782
|
+
const r = exec('claude', ['stop', sessao.id ?? sessao.sessionId.slice(0, 8)], undefined, 60000, ambiente);
|
|
741
783
|
return { ok: r.ok, texto: limparAnsi(r.stdout + r.stderr).trim() };
|
|
742
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* RM-037 (defeitosdeco D-2): o `claude agents` so lista a sessao para a conta que a despachou, entao
|
|
787
|
+
* uma chave e procurada no ambiente do processo e em cada perfil claude-bg informado, sem repetir
|
|
788
|
+
* diretorio efetivo. Consulta que falha nao vira "nao encontrada": vai para `falhas`.
|
|
789
|
+
*/
|
|
790
|
+
function acharSessaoNasContas(chave, perfis) {
|
|
791
|
+
const achadas = [], falhas = [], vistos = new Set();
|
|
792
|
+
// GO-FIX (R5a): cada consulta pode custar dezenas de segundos sob carga. UUID completo e unico em
|
|
793
|
+
// qualquer conta, entao a busca para na primeira que acha; prefixo ou nome consulta todas, para
|
|
794
|
+
// recusar a chave ambigua.
|
|
795
|
+
const unica = UUID_CLAUDE.test(chave);
|
|
796
|
+
for (const perfil of [null, ...perfis.filter(p => p.runtime === 'claude-bg')]) {
|
|
797
|
+
let ambiente, configDir;
|
|
798
|
+
try {
|
|
799
|
+
ambiente = ambienteDoPerfil(perfil);
|
|
800
|
+
configDir = path.resolve((0, runtime_profiles_1.diretorioEfetivo)('claude-bg', perfil, ambiente));
|
|
801
|
+
}
|
|
802
|
+
catch (e) {
|
|
803
|
+
falhas.push(`${perfil?.id ?? 'processo'}: ${e.message}`);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (vistos.has(configDir))
|
|
807
|
+
continue;
|
|
808
|
+
vistos.add(configDir);
|
|
809
|
+
const consulta = consultarSessoes(undefined, true, ambiente);
|
|
810
|
+
if (!consulta.ok) {
|
|
811
|
+
falhas.push(`${perfil?.id ?? 'processo'}: ${consulta.detalhe}`);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const sessao = acharSessao(chave, consulta.sessoes);
|
|
815
|
+
if (sessao)
|
|
816
|
+
achadas.push({ sessao, perfil, configDir, ambiente });
|
|
817
|
+
if (sessao && unica)
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
return { achadas, falhas };
|
|
821
|
+
}
|
|
743
822
|
/**
|
|
744
823
|
* Ocupacao da janela de contexto de uma sessao, entre 0 e 1.
|
|
745
824
|
*
|
|
@@ -12,7 +12,9 @@ const falha = () => ({ classificacao: 'gate_blocked', motivo: 'runtime.unavailab
|
|
|
12
12
|
const sucesso = () => ({ classificacao: 'fase_concluida', motivo: null });
|
|
13
13
|
const humano = () => ({ classificacao: 'human.pending', motivo: 'human.pending' });
|
|
14
14
|
/** I-33 (D12): motivos da conta so saem de erro estruturado do runtime (`falhaDeContaDoErroCodex`). */
|
|
15
|
-
const MOTIVOS_DA_CONTA = ['runtime.quota-exhausted', 'runtime.auth-missing'
|
|
15
|
+
const MOTIVOS_DA_CONTA = ['runtime.quota-exhausted', 'runtime.auth-missing',
|
|
16
|
+
// GO-FIX (R3, RM-037): o modelo inacessivel tambem so vale do erro estruturado, nunca do texto do agente.
|
|
17
|
+
'runtime.model-unavailable'];
|
|
16
18
|
function motivo(v) {
|
|
17
19
|
if (v === undefined || v === null)
|
|
18
20
|
return null;
|
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;
|