@orkastery/cli 0.5.1 → 0.5.6
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/claude-code/README.md +158 -0
- package/adapters/claude-code/agents/ork-master.md +2 -0
- package/adapters/claude-code/commands/master.md +8 -0
- package/adapters/claude-code/commands/ork.md +9 -0
- package/adapters/claude-code/mods/autoridade.json +56 -0
- package/adapters/claude-code/mods/ork-clima/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-clima/hooks/contas.ts +283 -0
- package/adapters/claude-code/mods/ork-clima/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-clima/hooks/previsao.ts +393 -0
- package/adapters/claude-code/mods/ork-clima/hooks/register.ts +205 -0
- package/adapters/claude-code/mods/ork-clima/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-clima/types/index.d.ts +80 -0
- package/adapters/claude-code/mods/ork-freio/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-freio/hooks/carga.ts +160 -0
- package/adapters/claude-code/mods/ork-freio/hooks/conferencia.ts +309 -0
- package/adapters/claude-code/mods/ork-freio/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-freio/hooks/lote.ts +270 -0
- package/adapters/claude-code/mods/ork-freio/hooks/register.ts +285 -0
- package/adapters/claude-code/mods/ork-freio/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-freio/types/index.d.ts +127 -0
- package/adapters/claude-code/mods/ork-mesa/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/cartoes.tsx +246 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/decidir.ts +110 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/fila.ts +253 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/obvia.ts +98 -0
- package/adapters/claude-code/mods/ork-mesa/hooks/register.tsx +493 -0
- package/adapters/claude-code/mods/ork-mesa/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-mesa/types/index.d.ts +131 -0
- package/adapters/claude-code/mods/ork-pista/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-pista/hooks/acoes.ts +71 -0
- package/adapters/claude-code/mods/ork-pista/hooks/ci.ts +117 -0
- package/adapters/claude-code/mods/ork-pista/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-pista/hooks/pista.tsx +356 -0
- package/adapters/claude-code/mods/ork-pista/hooks/register.tsx +439 -0
- package/adapters/claude-code/mods/ork-pista/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-pista/types/index.d.ts +85 -0
- package/adapters/claude-code/mods/ork-ponte/.claude-plugin/plugin.json +13 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/faixa.tsx +57 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/guarda.ts +410 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/mostrado.ts +55 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/nucleo.ts +157 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/papel.ts +42 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/register.ts +331 -0
- package/adapters/claude-code/mods/ork-ponte/hooks/vigia.ts +76 -0
- package/adapters/claude-code/mods/ork-ponte/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-ponte/types/index.d.ts +178 -0
- package/adapters/claude-code/mods/ork-portao/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-portao/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-portao/hooks/lease.ts +201 -0
- package/adapters/claude-code/mods/ork-portao/hooks/plano.ts +304 -0
- package/adapters/claude-code/mods/ork-portao/hooks/register.ts +339 -0
- package/adapters/claude-code/mods/ork-portao/hooks/verify.ts +152 -0
- package/adapters/claude-code/mods/ork-portao/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-portao/types/index.d.ts +60 -0
- package/adapters/claude-code/mods/ork-radar/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-radar/hooks/colisao.ts +420 -0
- package/adapters/claude-code/mods/ork-radar/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-radar/hooks/mapa.tsx +322 -0
- package/adapters/claude-code/mods/ork-radar/hooks/register.tsx +470 -0
- package/adapters/claude-code/mods/ork-radar/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-radar/types/index.d.ts +72 -0
- package/adapters/claude-code/mods/ork-sentinela/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/alertas.ts +85 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/despacho.ts +156 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/register.ts +511 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/vigia.ts +186 -0
- package/adapters/claude-code/mods/ork-sentinela/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-sentinela/types/index.d.ts +65 -0
- package/adapters/claude-code/mods/ork-torre/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-torre/hooks/agenda.ts +60 -0
- package/adapters/claude-code/mods/ork-torre/hooks/detalhe.tsx +236 -0
- package/adapters/claude-code/mods/ork-torre/hooks/frases.ts +42 -0
- package/adapters/claude-code/mods/ork-torre/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-torre/hooks/painel.tsx +344 -0
- package/adapters/claude-code/mods/ork-torre/hooks/rede.ts +97 -0
- package/adapters/claude-code/mods/ork-torre/hooks/register.tsx +437 -0
- package/adapters/claude-code/mods/ork-torre/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-torre/types/index.d.ts +108 -0
- package/adapters/codex/skills/ork/SKILL.md +18 -3
- package/adapters/hermes/README.md +1 -0
- package/adapters/hermes/bin/ork-network-status.sh +10 -0
- package/adapters/hermes/hermes.plugin.json +1 -0
- package/adapters/hermes/hitl-ingress/__init__.py +2 -2
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +7 -6
- package/adapters/openclaw/README.md +26 -5
- package/adapters/openclaw/dist/hitl-ingress.js +2 -2
- package/adapters/openclaw/dist/index.js +46 -11
- package/adapters/openclaw/openclaw.plugin.json +5 -1
- package/adapters/openclaw/package.json +2 -2
- package/adapters/openclaw/src/hitl-ingress.ts +2 -2
- package/adapters/openclaw/src/index.ts +49 -11
- package/assets/docs-en/markdownlint-cli2.jsonc +22 -0
- package/assets/docs-en/padroes/documentacao-de-produto.md +135 -0
- package/assets/docs-en/padroes/roadmap-de-produto.md +100 -0
- package/assets/docs-en/produto/README.md +14 -0
- package/assets/docs-en/produto/_modelo-feature.md +62 -0
- package/assets/docs-en/roadmap/README.md +14 -0
- package/assets/docs-en/roadmap/_modelo-item.md +75 -0
- package/assets/orkmind_bridge.py +117 -9
- package/dist/adapters/claude-bg.js +116 -18
- package/dist/adapters/codex.js +14 -4
- package/dist/agents-md.js +24 -14
- package/dist/auditrun.js +22 -3
- package/dist/board.js +20 -17
- package/dist/branch-de-estado.js +88 -10
- package/dist/busca-semantica.js +52 -12
- package/dist/canarios-rm057.js +149 -0
- package/dist/canarios.js +2 -0
- package/dist/catalogo.js +33 -0
- package/dist/ci.js +21 -8
- package/dist/claims-conferir.js +342 -0
- package/dist/claims.js +41 -1
- package/dist/company-brain-client.js +36 -1
- package/dist/company-brain-context.js +158 -22
- package/dist/company-brain-journal.js +2 -1
- package/dist/company-brain-migration.js +36 -1
- package/dist/conducao-sentinela.js +388 -0
- package/dist/conducao-texto.js +12 -18
- package/dist/conducao.js +122 -30
- package/dist/conhecimento.js +490 -0
- package/dist/docs.js +70 -17
- package/dist/doctor-pulse-cron.js +349 -0
- package/dist/doctor-rede.js +150 -0
- package/dist/doctor.js +195 -111
- package/dist/entrega-pr.js +42 -29
- package/dist/escopo-escrita.js +10 -1
- package/dist/esgotamentos.js +181 -0
- package/dist/estado-thread.js +23 -1
- package/dist/experiencia-instalacao.js +45 -25
- package/dist/experiencia.js +12 -2
- package/dist/fabrica-batida.js +310 -0
- package/dist/fabrica-estado.js +52 -14
- package/dist/fabrica-publicar.js +9 -1
- package/dist/fechamento.js +20 -66
- package/dist/fila-pesada.js +444 -0
- package/dist/fix.js +3 -2
- package/dist/forja.js +31 -7
- package/dist/gates.js +3 -24
- package/dist/handoff.js +10 -6
- package/dist/hitl-contract.js +59 -4
- package/dist/hitl-curto.js +11 -6
- package/dist/hitl-gates.js +5 -0
- package/dist/hitl-local-receipt.js +2 -0
- package/dist/hitl-lote.js +5 -2
- package/dist/hitl-resumo.js +38 -3
- package/dist/hitl-tempo-parado.js +201 -0
- package/dist/hitl-texto-livre.js +5 -5
- package/dist/hitl.js +29 -33
- package/dist/horario.js +41 -7
- package/dist/hosts.js +104 -11
- package/dist/impedimento.js +299 -0
- package/dist/index.js +961 -542
- package/dist/indice-vetorial.js +87 -10
- package/dist/indice.js +9 -1
- package/dist/init.js +56 -43
- package/dist/instabilidade.js +144 -0
- package/dist/integracoes-locais.js +4 -0
- package/dist/intelligence-graph-cli.js +170 -16
- package/dist/intelligence-graph-contexto.js +254 -0
- package/dist/intelligence-graph-contract.js +2 -1
- package/dist/intelligence-graph-extract-md.js +56 -4
- package/dist/intelligence-graph-extract-ts.js +71 -3
- package/dist/intelligence-graph-parsers.js +6 -4
- package/dist/intelligence-graph-query.js +66 -19
- package/dist/leases.js +664 -87
- package/dist/ledger-stats.js +15 -1
- package/dist/ledger.js +18 -1
- package/dist/licoes.js +1 -0
- package/dist/liveness.js +6 -3
- package/dist/locale-ajuda.js +761 -0
- package/dist/locale-gerados.js +160 -0
- package/dist/locale-master.js +99 -0
- package/dist/locale-onboarding.js +92 -0
- package/dist/locale-phase.js +151 -0
- package/dist/locale-pulse.js +211 -0
- package/dist/locale-retry.js +385 -0
- package/dist/locale-roadmap.js +102 -0
- package/dist/locale-ship.js +494 -0
- package/dist/locale-thread.js +277 -0
- package/dist/locale-verify.js +125 -0
- package/dist/locale.js +714 -0
- package/dist/maestro-runtime.js +4 -1
- package/dist/manifest.js +149 -4
- package/dist/maquina.js +23 -4
- package/dist/master-digest.js +6 -1
- package/dist/master-nota.js +18 -11
- package/dist/master.js +145 -31
- package/dist/mcp-artifacts.js +43 -3
- package/dist/mcp-git.js +42 -8
- package/dist/mcp-grafo-worker.js +5 -3
- package/dist/mcp-grafo.js +55 -6
- package/dist/mcp-server.js +22 -3
- package/dist/mcp-ship.js +3 -1
- package/dist/mcp-verify.js +6 -1
- package/dist/memoria.js +48 -6
- package/dist/modos.js +3 -6
- package/dist/network-roadmap.js +196 -18
- package/dist/noctum-arco.js +911 -0
- package/dist/noctum-capacidade.js +141 -0
- package/dist/noctum-envelope.js +810 -0
- package/dist/noctum-execucao.js +304 -0
- package/dist/noctum-fila.js +280 -0
- package/dist/ocupacao.js +40 -5
- package/dist/onboarding.js +40 -21
- package/dist/orkmind.js +99 -7
- package/dist/orquestracao.js +24 -21
- package/dist/parado-no-condutor.js +219 -38
- package/dist/phase.js +160 -52
- package/dist/policies.js +56 -5
- package/dist/portfolio.js +8 -3
- package/dist/postura-local.js +234 -0
- package/dist/preflight.js +11 -10
- package/dist/procedencia.js +225 -0
- package/dist/projeto-alvo.js +132 -5
- package/dist/prompts.js +40 -1
- package/dist/prova-ativacao.js +75 -6
- package/dist/pulse-delivery.js +47 -4
- package/dist/pulse.js +98 -19
- package/dist/ratelimit.js +24 -6
- package/dist/recall.js +3 -1
- package/dist/redacao-saida.js +63 -0
- package/dist/rede-adesao.js +399 -0
- package/dist/rede-forja.js +289 -0
- package/dist/rede-projetos.js +239 -0
- package/dist/rede-status.js +337 -0
- package/dist/rede.js +980 -0
- package/dist/retry.js +291 -140
- package/dist/roadmap-reservas.js +20 -3
- package/dist/roadmap-status.js +150 -34
- package/dist/runtime-context.js +181 -3
- package/dist/runtime-profiles.js +22 -1
- package/dist/runtimes.js +2 -1
- package/dist/saida-segura.js +58 -0
- package/dist/session-events.js +7 -3
- package/dist/session-watcher-claude.js +120 -24
- package/dist/session-watcher.js +185 -33
- package/dist/sessoes-contas.js +187 -0
- package/dist/sessoes-inventario.js +6 -18
- package/dist/sessoes.js +83 -2
- package/dist/ship-publicar.js +800 -0
- package/dist/ship.js +157 -88
- package/dist/steal.js +77 -0
- package/dist/thread-close.js +3 -2
- package/dist/thread.js +299 -49
- package/dist/util.js +8 -1
- package/dist/verify.js +141 -52
- package/dist/vida-da-thread.js +254 -0
- package/dist/worktree-local.js +165 -0
- package/dist/worktree.js +25 -2
- package/dist/yaml.js +27 -8
- package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
- package/package.json +7 -3
- package/references/definition-of-done.md +1 -1
- package/schemas/code-artifact-graph.v1.schema.json +2 -1
- package/skills/README.md +1 -1
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- package/skills/phases/master-metrics/SKILL.md +8 -3
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// A regra de autoridade dos mods do Orkastery (RM-059, T0.5 e T0.6; D5 do plano dos mods): nenhum mod
|
|
2
|
+
// aprova gate, permissao, nota do MASTER, push ou tag pelo dono, nem fala como o dono. A lista mora em
|
|
3
|
+
// `adapters/claude-code/mods/autoridade.json`; este modulo so a interpreta, e e o mesmo casamento para a
|
|
4
|
+
// guarda estatica (`core/scripts/checar-mods.cjs`, por transpilacao) e para a guarda em tempo de execucao
|
|
5
|
+
// da ponte. Modulo puro: so dados entram e saem, nenhum `$` passa por aqui. E melhor esforco: argv montado
|
|
6
|
+
// em tempo de execucao escapa da varredura estatica, e um script em arquivo escapa das duas.
|
|
7
|
+
|
|
8
|
+
export const FORMATO_DA_AUTORIDADE = 'ork.mods-autoridade/v1'
|
|
9
|
+
export const NOME_DA_PONTE = 'ork-ponte'
|
|
10
|
+
export const MARCA_DE_MOD = 'orkastery-mod.json'
|
|
11
|
+
|
|
12
|
+
export type RegraDeArgv = {
|
|
13
|
+
id: string
|
|
14
|
+
porque: string
|
|
15
|
+
/** Comando do `ork`: o prefixo das palavras posicionais, como o `parseArgs` do CLI as separa. */
|
|
16
|
+
sequencia?: string[]
|
|
17
|
+
/** Opcao que precisa estar junto (`--score`, `--por`, `--autorizar-push`). */
|
|
18
|
+
opcao?: string
|
|
19
|
+
/** Programa na posicao de comando (`git`, `gh`). */
|
|
20
|
+
programa?: string
|
|
21
|
+
/** Subcomando do programa (`push`, `tag`); sem ele, o programa inteiro. */
|
|
22
|
+
subcomando?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type Autoridade = {
|
|
26
|
+
formato: string
|
|
27
|
+
regra: string
|
|
28
|
+
ganchosProibidos: Array<{ id: string; evento: string; componente?: string; porque: string }>
|
|
29
|
+
decisoesProibidas: Array<{ id: string; evento: string; decisao: string; porque: string }>
|
|
30
|
+
chamadasProibidas: Array<{ id: string; chamada: string; tipo?: string; porque: string }>
|
|
31
|
+
argvProibido: RegraDeArgv[]
|
|
32
|
+
ambienteProibido: Array<{ id: string; prefixo: string; porque: string }>
|
|
33
|
+
caminhosProibidos: Array<{ id: string; trecho: string; porque: string }>
|
|
34
|
+
reservadosAPonte: Array<{ id: string; evento: string; porque: string }>
|
|
35
|
+
permitidos: Array<{ id: string; sequencia: string[]; porque: string }>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** O que a guarda achou: a regra (id) e o porque, para a recusa dizer o motivo. */
|
|
39
|
+
export type Achado = { regra: string; porque: string }
|
|
40
|
+
|
|
41
|
+
const SECOES = ['ganchosProibidos', 'decisoesProibidas', 'chamadasProibidas', 'argvProibido', 'ambienteProibido',
|
|
42
|
+
'caminhosProibidos', 'reservadosAPonte', 'permitidos'] as const
|
|
43
|
+
|
|
44
|
+
function texto(v: unknown): v is string {
|
|
45
|
+
return typeof v === 'string' && v.trim() !== ''
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function listaDeTexto(v: unknown): v is string[] {
|
|
49
|
+
return Array.isArray(v) && v.length > 0 && v.every(texto)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Confere a forma da lista; qualquer desvio e erro, porque a guarda fecha sem lista valida. */
|
|
53
|
+
export function validarAutoridade(x: unknown): Autoridade {
|
|
54
|
+
const a = x as Record<string, unknown> | null
|
|
55
|
+
if (!a || typeof a !== 'object' || Array.isArray(a)) throw new Error('lista de autoridade nao e um objeto')
|
|
56
|
+
if (a.formato !== FORMATO_DA_AUTORIDADE) throw new Error(`formato da lista de autoridade: ${String(a.formato)}`)
|
|
57
|
+
if (!texto(a.regra)) throw new Error('lista de autoridade sem regra')
|
|
58
|
+
const ids = new Set<string>()
|
|
59
|
+
for (const secao of SECOES) {
|
|
60
|
+
const itens = a[secao]
|
|
61
|
+
if (!Array.isArray(itens)) throw new Error(`lista de autoridade sem a secao ${secao}`)
|
|
62
|
+
for (const bruto of itens) {
|
|
63
|
+
const item = bruto as Record<string, unknown>
|
|
64
|
+
if (!item || !texto(item.id) || !texto(item.porque)) throw new Error(`item sem id ou porque em ${secao}`)
|
|
65
|
+
if (ids.has(item.id as string)) throw new Error(`id repetido na lista de autoridade: ${String(item.id)}`)
|
|
66
|
+
ids.add(item.id as string)
|
|
67
|
+
const falta = (campo: string) => { throw new Error(`${secao}/${String(item.id)} sem ${campo}`) }
|
|
68
|
+
if ((secao === 'ganchosProibidos' || secao === 'reservadosAPonte' || secao === 'decisoesProibidas') && !texto(item.evento)) falta('evento')
|
|
69
|
+
if (secao === 'decisoesProibidas' && !texto(item.decisao)) falta('decisao')
|
|
70
|
+
if (secao === 'chamadasProibidas' && !texto(item.chamada)) falta('chamada')
|
|
71
|
+
if (secao === 'ambienteProibido' && !texto(item.prefixo)) falta('prefixo')
|
|
72
|
+
if (secao === 'caminhosProibidos' && !texto(item.trecho)) falta('trecho')
|
|
73
|
+
if (secao === 'permitidos' && !listaDeTexto(item.sequencia)) falta('sequencia')
|
|
74
|
+
if (secao === 'argvProibido') {
|
|
75
|
+
const porSequencia = listaDeTexto(item.sequencia)
|
|
76
|
+
const porPrograma = texto(item.programa)
|
|
77
|
+
if (porSequencia === porPrograma) throw new Error(`argvProibido/${String(item.id)} precisa de sequencia ou programa, um so`)
|
|
78
|
+
if (item.opcao !== undefined && !(texto(item.opcao) && (item.opcao as string).startsWith('--'))) falta('opcao com --')
|
|
79
|
+
if (item.subcomando !== undefined && !texto(item.subcomando)) falta('subcomando')
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return a as unknown as Autoridade
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Le e confere o texto do `autoridade.json`. */
|
|
87
|
+
export function lerAutoridade(conteudo: string): Autoridade {
|
|
88
|
+
return validarAutoridade(JSON.parse(conteudo))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Mod do Orkastery: nome `ork-*` ou pasta com `orkastery-mod.json` (os conhecidos vem do `plugin.register`). */
|
|
92
|
+
export function ehModDoOrkastery(nome: unknown, conhecidos?: { has: (n: string) => boolean }): boolean {
|
|
93
|
+
return typeof nome === 'string' && (nome.startsWith('ork-') || (conhecidos?.has(nome) ?? false))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function nomeBase(token: string): string {
|
|
97
|
+
const t = token.replace(/\/+$/, '')
|
|
98
|
+
return t.slice(t.lastIndexOf('/') + 1)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** As palavras posicionais e as opcoes, exatamente como o `parseArgs` do CLI do ork as separa. */
|
|
102
|
+
export function separarArgs(resto: readonly string[]): { posicionais: string[]; opcoes: Set<string> } {
|
|
103
|
+
const posicionais: string[] = []
|
|
104
|
+
const opcoes = new Set<string>()
|
|
105
|
+
for (let i = 0; i < resto.length; i++) {
|
|
106
|
+
const a = resto[i] as string
|
|
107
|
+
if (a.startsWith('--')) {
|
|
108
|
+
const igual = a.indexOf('=')
|
|
109
|
+
if (igual > 0) { opcoes.add(a.slice(2, igual)); continue }
|
|
110
|
+
opcoes.add(a.slice(2))
|
|
111
|
+
const proximo = resto[i + 1]
|
|
112
|
+
if (proximo !== undefined && !proximo.startsWith('--')) i++
|
|
113
|
+
} else posicionais.push(a)
|
|
114
|
+
}
|
|
115
|
+
return { posicionais, opcoes }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function comecaCom(lista: readonly string[], prefixo: readonly string[]): boolean {
|
|
119
|
+
return prefixo.length > 0 && prefixo.every((p, i) => lista[i] === p)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const WRAPPERS = new Set(['env', 'nohup', 'timeout', 'nice', 'ionice', 'setsid', 'stdbuf', 'sudo', 'doas', 'command',
|
|
123
|
+
'exec', 'time', 'xargs', 'chrt', 'taskset', 'unbuffer', 'then', 'do', 'else', 'if', 'while', 'until', '!'])
|
|
124
|
+
const OPCOES_COM_VALOR: Record<string, readonly string[]> = {
|
|
125
|
+
env: ['-u', '--unset', '-C', '--chdir'], sudo: ['-u', '-g', '-C', '-D', '-h', '-p', '-r', '-t', '-U'],
|
|
126
|
+
nice: ['-n', '--adjustment'], ionice: ['-c', '-n', '-p'], timeout: ['-k', '--kill-after', '-s', '--signal'],
|
|
127
|
+
xargs: ['-a', '-d', '-E', '-I', '-L', '-n', '-P', '-s'], chrt: ['-p'], taskset: ['-p'], doas: ['-u', '-C'],
|
|
128
|
+
}
|
|
129
|
+
const SHELLS = new Set(['sh', 'bash', 'dash', 'zsh', 'ksh', 'mksh', 'ash', 'fish', 'busybox'])
|
|
130
|
+
const INTERPRETADORES: Record<string, readonly string[]> = {
|
|
131
|
+
node: ['-e', '--eval', '-p', '--print'], nodejs: ['-e', '--eval', '-p', '--print'], deno: ['eval'],
|
|
132
|
+
bun: ['-e', '--eval'], python: ['-c'], python3: ['-c'], perl: ['-e', '-E'], ruby: ['-e'], php: ['-r'],
|
|
133
|
+
}
|
|
134
|
+
const OPCOES_GLOBAIS_DO_GIT = ['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--super-prefix', '--config-env']
|
|
135
|
+
const ENTRADA_DO_ORK = /(^|\/)(ork|ork\.c?js|ork\.mjs|index\.c?js|cli\.c?js)$/
|
|
136
|
+
const MAX_PROFUNDIDADE = 4
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Tira o que so embrulha o comando (`env A=1`, `nohup`, `timeout 10`, `sudo -u x`) e devolve o comando de fato.
|
|
140
|
+
* `env` sem comando imprime o ambiente inteiro: `despejo` avisa isso.
|
|
141
|
+
*/
|
|
142
|
+
function semEmbrulho(tokens: readonly string[]): { tokens: string[]; scriptDoEnv?: string; despejo?: boolean } {
|
|
143
|
+
let t = [...tokens]
|
|
144
|
+
let ultimo = ''
|
|
145
|
+
for (let voltas = 0; voltas < 8 && t.length > 0; voltas++) {
|
|
146
|
+
while (t.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(t[0] as string)) t = t.slice(1)
|
|
147
|
+
const base = nomeBase(t[0] ?? '')
|
|
148
|
+
if (!WRAPPERS.has(base)) break
|
|
149
|
+
ultimo = base
|
|
150
|
+
t = t.slice(1)
|
|
151
|
+
const comValor = OPCOES_COM_VALOR[base] ?? []
|
|
152
|
+
while (t.length > 0) {
|
|
153
|
+
const a = t[0] as string
|
|
154
|
+
if (base === 'env' && (a === '-S' || a === '--split-string')) return { tokens: [], scriptDoEnv: t[1] ?? '' }
|
|
155
|
+
if (comValor.includes(a)) { t = t.slice(2); continue }
|
|
156
|
+
if (a.startsWith('-') || /^[A-Za-z_][A-Za-z0-9_]*=/.test(a)) { t = t.slice(1); continue }
|
|
157
|
+
if (base === 'timeout' && /^\d+(\.\d+)?[smhd]?$/.test(a)) { t = t.slice(1); continue }
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { tokens: t, ...(t.length === 0 && ultimo === 'env' ? { despejo: true } : {}) }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* O comando expoe o ambiente inteiro do processo (onde mora `ORK_HITL_*` quando o host o exporta):
|
|
166
|
+
* `printenv` sem nome, `export -p`, `declare -x`, `set` sem argumento, `compgen -e` ou `/proc/<pid>/environ`.
|
|
167
|
+
*/
|
|
168
|
+
function despejaOAmbiente(tokens: readonly string[]): boolean {
|
|
169
|
+
const base = nomeBase(tokens[0] ?? '')
|
|
170
|
+
const args = tokens.slice(1)
|
|
171
|
+
if (base === 'printenv') return args.every(t => t.startsWith('-'))
|
|
172
|
+
if (base === 'export') return args.length === 0 || args.includes('-p')
|
|
173
|
+
if (base === 'declare' || base === 'typeset') return args.some(t => /^-[A-Za-z]*[xp]/.test(t))
|
|
174
|
+
if (base === 'set') return args.length === 0
|
|
175
|
+
if (base === 'compgen') return args.includes('-e')
|
|
176
|
+
return tokens.some(t => /^\/proc\/[^/]+\/environ$/.test(t.replace(/\/{2,}/g, '/')))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Uma variavel da lista citada no texto (`echo $ORK_HITL_TOKEN`, `printenv ORK_HITL_TOKEN`). */
|
|
180
|
+
function citaAmbienteProibido(a: Autoridade, texto: string): Achado | null {
|
|
181
|
+
const r = a.ambienteProibido.find(p => texto.includes(p.prefixo))
|
|
182
|
+
return r ? { regra: r.id, porque: r.porque } : null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Um texto que cita caminho ou variavel da lista, seja ou nao linha de comando (a varredura estatica usa). */
|
|
186
|
+
export function avaliarCitacao(a: Autoridade, texto: unknown): Achado | null {
|
|
187
|
+
return typeof texto === 'string' ? avaliarCaminho(a, texto) ?? citaAmbienteProibido(a, texto) : null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function despejo(a: Autoridade): Achado | null {
|
|
191
|
+
const r = a.ambienteProibido[0]
|
|
192
|
+
return r ? { regra: r.id, porque: `${r.porque} (o comando imprime o ambiente inteiro)` } : null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** O resto do argv quando ele chama o CLI do ork (por `ork`, `node .../index.js` ou `npx ork`); `null` se nao chama. */
|
|
196
|
+
function restoDoOrk(tokens: readonly string[]): string[] | null {
|
|
197
|
+
const base = nomeBase(tokens[0] ?? '')
|
|
198
|
+
if (base === 'ork') return tokens.slice(1)
|
|
199
|
+
if (base === 'node' || base === 'nodejs') {
|
|
200
|
+
let i = 1
|
|
201
|
+
while (i < tokens.length && (tokens[i] as string).startsWith('-')) i += ['-r', '--require', '--import', '--loader'].includes(tokens[i] as string) ? 2 : 1
|
|
202
|
+
const script = tokens[i]
|
|
203
|
+
if (script !== undefined && (ENTRADA_DO_ORK.test(script) || script.includes('@orkastery/'))) return tokens.slice(i + 1)
|
|
204
|
+
}
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function avaliarOrk(a: Autoridade, resto: readonly string[]): Achado | null {
|
|
209
|
+
const { posicionais, opcoes } = separarArgs(resto)
|
|
210
|
+
if (a.permitidos.some(p => comecaCom(posicionais, p.sequencia))) return null
|
|
211
|
+
for (const r of a.argvProibido) {
|
|
212
|
+
if (!r.sequencia || !comecaCom(posicionais, r.sequencia)) continue
|
|
213
|
+
if (r.opcao !== undefined && !opcoes.has(r.opcao.replace(/^--/, ''))) continue
|
|
214
|
+
return { regra: r.id, porque: r.porque }
|
|
215
|
+
}
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function avaliarPrograma(a: Autoridade, tokens: readonly string[]): Achado | null {
|
|
220
|
+
const base = nomeBase(tokens[0] ?? '')
|
|
221
|
+
for (const r of a.argvProibido) {
|
|
222
|
+
if (!r.programa || r.programa !== base) continue
|
|
223
|
+
if (r.subcomando === undefined) return { regra: r.id, porque: r.porque }
|
|
224
|
+
let i = 1
|
|
225
|
+
while (i < tokens.length) {
|
|
226
|
+
const t = tokens[i] as string
|
|
227
|
+
if (base === 'git' && OPCOES_GLOBAIS_DO_GIT.includes(t)) { i += 2; continue }
|
|
228
|
+
if (t.startsWith('-')) { i++; continue }
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
if (tokens[i] === r.subcomando) return { regra: r.id, porque: r.porque }
|
|
232
|
+
}
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Um caminho lido ou citado que esta na lista (`.hermes/.env`), com as barras normalizadas. */
|
|
237
|
+
export function avaliarCaminho(a: Autoridade, caminho: unknown): Achado | null {
|
|
238
|
+
if (typeof caminho !== 'string') return null
|
|
239
|
+
// Grafia normalizada e sem diferenca de maiusculas: num sistema que as ignora, `.HERMES/.env` e o mesmo arquivo.
|
|
240
|
+
const normal = caminho.replace(/\\/g, '/').replace(/\/(\.\/)+/g, '/').replace(/\/{2,}/g, '/').toLowerCase()
|
|
241
|
+
const r = a.caminhosProibidos.find(c => normal.includes(c.trecho.toLowerCase()))
|
|
242
|
+
return r ? { regra: r.id, porque: r.porque } : null
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Uma variavel de ambiente da lista (`ORK_HITL_*`). */
|
|
246
|
+
export function avaliarAmbiente(a: Autoridade, nome: unknown): Achado | null {
|
|
247
|
+
if (typeof nome !== 'string') return null
|
|
248
|
+
const r = a.ambienteProibido.find(p => nome.startsWith(p.prefixo))
|
|
249
|
+
return r ? { regra: r.id, porque: r.porque } : null
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Separa um texto de shell em comandos e palavras: aspas respeitadas; `;`, `&`, `|`, `(`, `)`, crase e `$(` cortam. */
|
|
253
|
+
export function comandosDoTexto(textoDoShell: string): string[][] {
|
|
254
|
+
const comandos: string[][] = []
|
|
255
|
+
let atual: string[] = []
|
|
256
|
+
let palavra = ''
|
|
257
|
+
let temPalavra = false
|
|
258
|
+
let aspa: '"' | "'" | null = null
|
|
259
|
+
const fecharPalavra = () => { if (temPalavra) atual.push(palavra); palavra = ''; temPalavra = false }
|
|
260
|
+
const fecharComando = () => { fecharPalavra(); if (atual.length > 0) comandos.push(atual); atual = [] }
|
|
261
|
+
for (let i = 0; i < textoDoShell.length; i++) {
|
|
262
|
+
const c = textoDoShell[i] as string
|
|
263
|
+
if (aspa !== null) {
|
|
264
|
+
if (c === aspa) aspa = null
|
|
265
|
+
else palavra += c
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
if (c === '"' || c === "'") { aspa = c; temPalavra = true; continue }
|
|
269
|
+
if (c === '\\' && i + 1 < textoDoShell.length) { palavra += textoDoShell[++i]; temPalavra = true; continue }
|
|
270
|
+
if (c === ' ' || c === '\t') { fecharPalavra(); continue }
|
|
271
|
+
if (c === ';' || c === '&' || c === '|' || c === '\n' || c === '(' || c === ')' || c === '`' || c === '{' || c === '}') { fecharComando(); continue }
|
|
272
|
+
if (c === '$' && textoDoShell[i + 1] === '(') { fecharComando(); i++; continue }
|
|
273
|
+
palavra += c
|
|
274
|
+
temPalavra = true
|
|
275
|
+
}
|
|
276
|
+
fecharComando()
|
|
277
|
+
return comandos
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Codigo de interpretador (`node -e`, `python -c`): leitura solta, por palavras, so para pegar o obvio. */
|
|
281
|
+
function avaliarCodigoSolto(a: Autoridade, codigo: string): Achado | null {
|
|
282
|
+
const palavras = codigo.split(/[^A-Za-z0-9_.\/@-]+/).filter(Boolean)
|
|
283
|
+
for (let i = 0; i < palavras.length; i++) {
|
|
284
|
+
const base = nomeBase(palavras[i] as string)
|
|
285
|
+
for (const r of a.argvProibido) {
|
|
286
|
+
if (r.programa && r.programa === base) {
|
|
287
|
+
if (r.subcomando === undefined || palavras.slice(i + 1, i + 6).includes(r.subcomando)) return { regra: r.id, porque: r.porque }
|
|
288
|
+
}
|
|
289
|
+
if (r.sequencia && comecaCom(palavras.slice(i), r.sequencia)) {
|
|
290
|
+
if (r.opcao === undefined || palavras.includes(r.opcao)) return { regra: r.id, porque: r.porque }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return avaliarCaminho(a, codigo)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Um texto de shell: cada comando dele passa pela regra de argv. */
|
|
298
|
+
export function avaliarTexto(a: Autoridade, textoDoShell: string, profundidade = 0): Achado | null {
|
|
299
|
+
const direto = avaliarCaminho(a, textoDoShell) ?? citaAmbienteProibido(a, textoDoShell)
|
|
300
|
+
if (direto) return direto
|
|
301
|
+
for (const comando of comandosDoTexto(textoDoShell)) {
|
|
302
|
+
const achado = avaliarArgv(a, comando, undefined, profundidade + 1)
|
|
303
|
+
if (achado) return achado
|
|
304
|
+
}
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Um argv que um mod quer rodar (`$.process.run`, `$.process.spawn`), com o texto do `stdin` quando o comando e
|
|
310
|
+
* um shell que le o script da entrada. Devolve o primeiro achado ou `null`.
|
|
311
|
+
*/
|
|
312
|
+
export function avaliarArgv(a: Autoridade, argv: readonly unknown[], stdin?: unknown, profundidade = 0): Achado | null {
|
|
313
|
+
if (profundidade > MAX_PROFUNDIDADE) return { regra: 'profundidade', porque: 'comando aninhado demais para a guarda conferir' }
|
|
314
|
+
const brutos = argv.map(t => String(t))
|
|
315
|
+
for (const t of brutos) {
|
|
316
|
+
const achado = avaliarCaminho(a, t) ?? citaAmbienteProibido(a, t)
|
|
317
|
+
if (achado) return achado
|
|
318
|
+
}
|
|
319
|
+
if (typeof stdin === 'string') {
|
|
320
|
+
const noStdin = citaAmbienteProibido(a, stdin)
|
|
321
|
+
if (noStdin) return noStdin
|
|
322
|
+
}
|
|
323
|
+
const { tokens, scriptDoEnv, despejo: soEnv } = semEmbrulho(brutos)
|
|
324
|
+
if (scriptDoEnv !== undefined) return avaliarTexto(a, scriptDoEnv, profundidade)
|
|
325
|
+
if (soEnv || despejaOAmbiente(tokens)) return despejo(a)
|
|
326
|
+
if (tokens.length === 0) return null
|
|
327
|
+
const base = nomeBase(tokens[0] as string)
|
|
328
|
+
if (base === 'eval') return avaliarTexto(a, tokens.slice(1).join(' '), profundidade)
|
|
329
|
+
if (SHELLS.has(base)) {
|
|
330
|
+
const i = tokens.findIndex((t, j) => j > 0 && /^-[A-Za-z]*c[A-Za-z]*$/.test(t))
|
|
331
|
+
if (i > 0) return tokens[i + 1] === undefined ? null : avaliarTexto(a, tokens[i + 1] as string, profundidade)
|
|
332
|
+
const temScript = tokens.slice(1).some(t => !t.startsWith('-'))
|
|
333
|
+
if (!temScript && typeof stdin === 'string') return avaliarTexto(a, stdin, profundidade)
|
|
334
|
+
return null
|
|
335
|
+
}
|
|
336
|
+
const inline = INTERPRETADORES[base]
|
|
337
|
+
if (inline) {
|
|
338
|
+
const i = tokens.findIndex((t, j) => j > 0 && inline.includes(t))
|
|
339
|
+
if (i > 0 && tokens[i + 1] !== undefined) return avaliarCodigoSolto(a, tokens[i + 1] as string)
|
|
340
|
+
}
|
|
341
|
+
if (['npx', 'pnpx', 'bunx'].includes(base)) {
|
|
342
|
+
let i = 1
|
|
343
|
+
while (i < tokens.length && (tokens[i] as string).startsWith('-')) i++
|
|
344
|
+
const alvo = tokens[i] ?? ''
|
|
345
|
+
if (alvo === 'ork' || alvo.startsWith('@orkastery/')) return avaliarOrk(a, tokens.slice(i + 1))
|
|
346
|
+
return avaliarArgv(a, tokens.slice(i), undefined, profundidade + 1)
|
|
347
|
+
}
|
|
348
|
+
if (['npm', 'pnpm', 'yarn', 'bun'].includes(base)) {
|
|
349
|
+
// `npm exec`, `pnpm dlx`, `yarn run` e tambem `yarn <bin>` e `pnpm <bin>`, que rodam o binario sem subcomando, e
|
|
350
|
+
// opcao com valor no meio (`yarn --cwd x ork ...`): cada posicao depois do gerenciador e tentada como comando.
|
|
351
|
+
for (let i = 1; i < tokens.length && i <= 12; i++) {
|
|
352
|
+
const alvo = tokens[i] as string
|
|
353
|
+
if (alvo.startsWith('-')) continue
|
|
354
|
+
const achado = alvo === 'ork' || alvo.startsWith('@orkastery/') ? avaliarOrk(a, tokens.slice(i + 1))
|
|
355
|
+
: avaliarArgv(a, tokens.slice(i), undefined, profundidade + 1)
|
|
356
|
+
if (achado) return achado
|
|
357
|
+
}
|
|
358
|
+
return null
|
|
359
|
+
}
|
|
360
|
+
const programa = avaliarPrograma(a, tokens)
|
|
361
|
+
if (programa) return programa
|
|
362
|
+
const resto = restoDoOrk(tokens)
|
|
363
|
+
return resto === null ? null : avaliarOrk(a, resto)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
type Glob = { exato: string } | { prefixo: string }
|
|
367
|
+
|
|
368
|
+
function glob(padrao: string): Glob {
|
|
369
|
+
if (padrao === '*') return { prefixo: '' }
|
|
370
|
+
return padrao.endsWith('.*') ? { prefixo: padrao.slice(0, -1) } : { exato: padrao }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** O padrao de um `on(...)` (`*`, `fs.*`, `fs.read`) alcanca o evento ou o padrao da lista? */
|
|
374
|
+
export function sobrepoe(padraoDoMod: string, padraoDaLista: string): boolean {
|
|
375
|
+
if (padraoDoMod.startsWith('!')) return false
|
|
376
|
+
if (padraoDoMod.startsWith('telemetry.') || padraoDaLista.startsWith('telemetry.')) return padraoDoMod === padraoDaLista
|
|
377
|
+
const m = glob(padraoDoMod)
|
|
378
|
+
const l = glob(padraoDaLista)
|
|
379
|
+
if ('exato' in m && 'exato' in l) return m.exato === l.exato
|
|
380
|
+
if ('exato' in m) return m.exato.startsWith((l as { prefixo: string }).prefixo)
|
|
381
|
+
if ('exato' in l) return l.exato.startsWith(m.prefixo)
|
|
382
|
+
return m.prefixo.startsWith(l.prefixo) || l.prefixo.startsWith(m.prefixo)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Ganchos de um mod (os padroes do `on`): proibidos (sem componente) ou reservados a ponte. */
|
|
386
|
+
export function avaliarEventos(a: Autoridade, eventos: readonly string[], ehPonte = false): Achado | null {
|
|
387
|
+
for (const evento of eventos) {
|
|
388
|
+
const proibido = a.ganchosProibidos.find(g => g.componente === undefined && sobrepoe(evento, g.evento))
|
|
389
|
+
if (proibido) return { regra: proibido.id, porque: proibido.porque }
|
|
390
|
+
if (ehPonte) continue
|
|
391
|
+
const reservado = a.reservadosAPonte.find(r => sobrepoe(evento, r.evento))
|
|
392
|
+
if (reservado) return { regra: reservado.id, porque: `gancho reservado a ponte: ${reservado.porque}` }
|
|
393
|
+
}
|
|
394
|
+
return null
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** O que o motor escaneou de um modulo no `plugin.register`: ganchos, chamadas e variaveis lidas. */
|
|
398
|
+
export function avaliarUses(a: Autoridade, uses: { events?: readonly string[]; calls?: readonly string[]; env?: { reads?: readonly string[] } },
|
|
399
|
+
ehPonte = false): Achado | null {
|
|
400
|
+
const eventos = avaliarEventos(a, uses.events ?? [], ehPonte)
|
|
401
|
+
if (eventos) return eventos
|
|
402
|
+
for (const c of a.chamadasProibidas) {
|
|
403
|
+
if (c.tipo === undefined && (uses.calls ?? []).includes(c.chamada)) return { regra: c.id, porque: c.porque }
|
|
404
|
+
}
|
|
405
|
+
for (const nome of uses.env?.reads ?? []) {
|
|
406
|
+
const achado = avaliarAmbiente(a, nome)
|
|
407
|
+
if (achado) return achado
|
|
408
|
+
}
|
|
409
|
+
return null
|
|
410
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// O registro do que um mod mostrou ao dono (RM-059, M5 do dono; D8 da thread). O Telegram nao deve
|
|
2
|
+
// repetir o que o dono ja viu no terminal: a ponte guarda o que foi mostrado num formato versionado, no
|
|
3
|
+
// proprio `$.store`, para o nucleo deduplicar depois. Um item por mod, chave e texto. Modulo puro.
|
|
4
|
+
import type { OrkCanal, OrkMostrado, OrkRegistroDeMostrados } from '../types'
|
|
5
|
+
|
|
6
|
+
export const FORMATO_DO_MOSTRADO = 'ork.mod-mostrado/v1'
|
|
7
|
+
export const CHAVE_NO_STORE = 'mostrados'
|
|
8
|
+
export const TETO_DE_MOSTRADOS = 200
|
|
9
|
+
export const TETO_DO_TEXTO_MOSTRADO = 500
|
|
10
|
+
export const CANAIS: readonly OrkCanal[] = ['faixa', 'painel', 'notificacao', 'toast', 'status', 'transcript']
|
|
11
|
+
const NOME_DE_MOD = /^[a-z0-9][a-z0-9-]{0,63}$/
|
|
12
|
+
const ID_DE_THREAD = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/
|
|
13
|
+
|
|
14
|
+
function vazio(): OrkRegistroDeMostrados {
|
|
15
|
+
return { formato: FORMATO_DO_MOSTRADO, itens: [] }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** O registro guardado, ou um vazio se o que estava no store nao e deste formato. */
|
|
19
|
+
export function lerRegistro(guardado: unknown): OrkRegistroDeMostrados {
|
|
20
|
+
const r = guardado as Partial<OrkRegistroDeMostrados> | null | undefined
|
|
21
|
+
if (!r || r.formato !== FORMATO_DO_MOSTRADO || !Array.isArray(r.itens)) return vazio()
|
|
22
|
+
return { formato: FORMATO_DO_MOSTRADO, itens: r.itens.filter(i => i && typeof i.mod === 'string' && typeof i.texto === 'string') }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Confere o que o mod diz ter mostrado. */
|
|
26
|
+
export function conferirMostrado(m: unknown): OrkMostrado {
|
|
27
|
+
const x = (m ?? {}) as Record<string, unknown>
|
|
28
|
+
if (typeof x.mod !== 'string' || !NOME_DE_MOD.test(x.mod)) throw new Error('ork-ponte: nome de mod invalido')
|
|
29
|
+
if (typeof x.canal !== 'string' || !CANAIS.includes(x.canal as OrkCanal)) throw new Error(`ork-ponte: canal desconhecido: ${String(x.canal)}`)
|
|
30
|
+
if (typeof x.texto !== 'string' || x.texto.trim() === '') throw new Error('ork-ponte: texto mostrado vazio')
|
|
31
|
+
if (x.chave !== undefined && (typeof x.chave !== 'string' || x.chave.length > 200)) throw new Error('ork-ponte: chave invalida')
|
|
32
|
+
if (x.thread !== undefined && (typeof x.thread !== 'string' || !ID_DE_THREAD.test(x.thread))) throw new Error('ork-ponte: id de thread invalido')
|
|
33
|
+
return {
|
|
34
|
+
mod: x.mod, canal: x.canal as OrkCanal, texto: x.texto,
|
|
35
|
+
...(x.chave !== undefined ? { chave: x.chave as string } : {}),
|
|
36
|
+
...(x.thread !== undefined ? { thread: x.thread as string } : {}),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Acrescenta o que foi mostrado: o mesmo mod, chave e texto so ganham `ultimaVez` e mais uma vez; o resto
|
|
42
|
+
* entra no fim. Acima do teto saem os vistos ha mais tempo.
|
|
43
|
+
*/
|
|
44
|
+
export function registrarMostrado(guardado: unknown, m: OrkMostrado, agoraIso: string): OrkRegistroDeMostrados {
|
|
45
|
+
const r = lerRegistro(guardado)
|
|
46
|
+
const texto = m.texto.replace(/\s+/g, ' ').trim().slice(0, TETO_DO_TEXTO_MOSTRADO)
|
|
47
|
+
const chave = m.chave ?? null
|
|
48
|
+
const mesmo = (i: OrkRegistroDeMostrados['itens'][number]) => i.mod === m.mod && i.chave === chave && i.texto === texto
|
|
49
|
+
const anterior = r.itens.find(mesmo)
|
|
50
|
+
const resto = r.itens.filter(i => !mesmo(i))
|
|
51
|
+
const item = anterior
|
|
52
|
+
? { ...anterior, canal: m.canal, ultimaVez: agoraIso, vezes: anterior.vezes + 1 }
|
|
53
|
+
: { mod: m.mod, canal: m.canal, chave, thread: m.thread ?? null, texto, primeiraVez: agoraIso, ultimaVez: agoraIso, vezes: 1 }
|
|
54
|
+
return { formato: FORMATO_DO_MOSTRADO, itens: [...resto, item].slice(-TETO_DE_MOSTRADOS) }
|
|
55
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// A leitura do nucleo pela ponte (RM-059, T0.3; D3 e D10 da thread). Modulo puro: monta o pedido ao
|
|
2
|
+
// `ork_maestro`, a lista fechada de consultas do CLI e o cache por `stat`. As chamadas ao `$` ficam
|
|
3
|
+
// no register.ts. Dados so pelo nucleo: nada aqui le ou escreve estado em `.orkastery/`.
|
|
4
|
+
import type { OrkAssinatura, OrkConsulta, OrkPedidoDeLeitura, OrkPedidoDeRetrato, OrkRetratoGuardado, OrkSecao } from '../types'
|
|
5
|
+
|
|
6
|
+
export const SERVIDOR = 'orkastery'
|
|
7
|
+
export const FERRAMENTA_DO_RETRATO = 'ork_maestro'
|
|
8
|
+
/** Teto de cada consulta pelo CLI (D10): o mesmo teto total do retrato no nucleo. */
|
|
9
|
+
export const TETO_DO_CLI_MS = 10000
|
|
10
|
+
/** O cache vale enquanto os ledgers cobertos nao mudam, e nunca por mais que isto. */
|
|
11
|
+
export const TETO_DO_CACHE_MS = 60000
|
|
12
|
+
export const TETO_DE_ENTRADAS = 16
|
|
13
|
+
export const TETO_DE_LEDGERS = 50
|
|
14
|
+
/** As secoes do `ork.maestro-snapshot/v1` (core/src/maestro-contract.ts). */
|
|
15
|
+
export const SECOES: readonly OrkSecao[] = ['portfolio', 'demands', 'threads', 'sessions', 'blockers', 'leases',
|
|
16
|
+
'retries', 'hitl', 'ship', 'master', 'nextActions']
|
|
17
|
+
|
|
18
|
+
const ID_DE_THREAD = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/
|
|
19
|
+
|
|
20
|
+
/** A lista fechada de consultas de leitura (D10): o argv depois de `ork`, e se exige thread. */
|
|
21
|
+
export const CONSULTAS: Readonly<Record<OrkConsulta, { argv: readonly string[]; exigeThread: boolean }>> = {
|
|
22
|
+
'maestro': { argv: ['maestro', '--json'], exigeThread: false },
|
|
23
|
+
'thread.status': { argv: ['thread', 'status', '<thread>', '--json'], exigeThread: true },
|
|
24
|
+
'thread.list': { argv: ['thread', 'list', '--json'], exigeThread: false },
|
|
25
|
+
'conducao.status': { argv: ['conducao', 'status', '<thread>', '--json'], exigeThread: true },
|
|
26
|
+
'retry.plan': { argv: ['retry', 'plan', '<thread>', '--json'], exigeThread: true },
|
|
27
|
+
'retry.list': { argv: ['retry', 'list', '--json'], exigeThread: false },
|
|
28
|
+
'pulse': { argv: ['pulse', '--json'], exigeThread: false },
|
|
29
|
+
'orquestracao.status': { argv: ['orquestracao', 'status', '--json'], exigeThread: false },
|
|
30
|
+
'sessions.hitl': { argv: ['sessions', 'hitl', '--json'], exigeThread: false },
|
|
31
|
+
'roadmap.status': { argv: ['roadmap', 'status', '--json'], exigeThread: false },
|
|
32
|
+
'accounts.esgotamentos': { argv: ['accounts', 'esgotamentos', '--json'], exigeThread: false },
|
|
33
|
+
'decisao.placar': { argv: ['decisao', 'placar', '<thread>', '--json'], exigeThread: true },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ErroDeLeitura extends Error {}
|
|
37
|
+
|
|
38
|
+
function conferirThread(thread: unknown): string | undefined {
|
|
39
|
+
if (thread === undefined) return undefined
|
|
40
|
+
if (typeof thread !== 'string' || !ID_DE_THREAD.test(thread)) throw new ErroDeLeitura('ork-ponte: id de thread invalido')
|
|
41
|
+
return thread
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Confere o pedido de retrato: secao conhecida, offset inteiro e so junto da secao. */
|
|
45
|
+
export function conferirPedidoDeRetrato(p: OrkPedidoDeRetrato | undefined): OrkPedidoDeRetrato {
|
|
46
|
+
const pedido = p ?? {}
|
|
47
|
+
const thread = conferirThread(pedido.thread)
|
|
48
|
+
if (pedido.secao !== undefined && !SECOES.includes(pedido.secao)) throw new ErroDeLeitura(`ork-ponte: secao desconhecida: ${String(pedido.secao)}`)
|
|
49
|
+
if (pedido.offset !== undefined) {
|
|
50
|
+
if (pedido.secao === undefined) throw new ErroDeLeitura('ork-ponte: offset so vale com secao')
|
|
51
|
+
if (!Number.isInteger(pedido.offset) || pedido.offset < 0 || pedido.offset > 100000) throw new ErroDeLeitura('ork-ponte: offset fora da faixa')
|
|
52
|
+
}
|
|
53
|
+
return { ...(thread ? { thread } : {}), ...(pedido.secao ? { secao: pedido.secao, offset: pedido.offset ?? 0 } : {}) }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Os argumentos do `ork_maestro`: secao e offset andam juntos, como o nucleo exige. */
|
|
57
|
+
export function argsDoMaestro(p: OrkPedidoDeRetrato): Record<string, unknown> {
|
|
58
|
+
const a: Record<string, unknown> = {}
|
|
59
|
+
if (p.thread) a.threadId = p.thread
|
|
60
|
+
if (p.secao) { a.section = p.secao; a.offset = p.offset ?? 0 }
|
|
61
|
+
return a
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** O argv completo de uma consulta da lista fechada; qualquer outra coisa e recusada. */
|
|
65
|
+
export function argvDaConsulta(p: OrkPedidoDeLeitura & { secao?: OrkSecao; offset?: number }): string[] {
|
|
66
|
+
const modelo = Object.prototype.hasOwnProperty.call(CONSULTAS, p.consulta) ? CONSULTAS[p.consulta] : undefined
|
|
67
|
+
if (!modelo) throw new ErroDeLeitura(`ork-ponte: consulta fora da lista fechada: ${String(p.consulta)}`)
|
|
68
|
+
const thread = conferirThread(p.thread)
|
|
69
|
+
if (modelo.exigeThread && !thread) throw new ErroDeLeitura(`ork-ponte: a consulta ${p.consulta} exige thread`)
|
|
70
|
+
const argv = ['ork', ...modelo.argv.map(a => (a === '<thread>' ? (thread as string) : a))]
|
|
71
|
+
if (p.consulta === 'maestro') {
|
|
72
|
+
const r = conferirPedidoDeRetrato({ thread, secao: p.secao, offset: p.offset })
|
|
73
|
+
if (r.thread) argv.push('--thread', r.thread)
|
|
74
|
+
if (r.secao) argv.push('--section', r.secao, '--offset', String(r.offset ?? 0))
|
|
75
|
+
}
|
|
76
|
+
return argv
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** O JSON que o `ork_maestro` devolve no primeiro bloco de texto; recusa resultado com erro. */
|
|
80
|
+
export function interpretarMcp(r: { content?: ReadonlyArray<{ type: string; text?: string }>; isError?: boolean }): unknown {
|
|
81
|
+
if (r.isError) throw new ErroDeLeitura('ork-ponte: o ork_maestro respondeu com erro')
|
|
82
|
+
const texto = (r.content ?? []).find(c => c.type === 'text' && typeof c.text === 'string')?.text
|
|
83
|
+
if (texto === undefined) throw new ErroDeLeitura('ork-ponte: o ork_maestro nao devolveu texto')
|
|
84
|
+
return JSON.parse(texto)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** O JSON que o CLI imprime; saida diferente de zero vira erro com o fim do stderr. */
|
|
88
|
+
export function interpretarCli(argv: readonly string[], r: { exitCode: number; stdout: string; stderr: string }): unknown {
|
|
89
|
+
if (r.exitCode !== 0) {
|
|
90
|
+
const fim = r.stderr.trim().split('\n').slice(-2).join(' ').slice(0, 300)
|
|
91
|
+
throw new ErroDeLeitura(`ork-ponte: ${argv.slice(0, 3).join(' ')} saiu ${r.exitCode}${fim ? `: ${fim}` : ''}`)
|
|
92
|
+
}
|
|
93
|
+
return JSON.parse(r.stdout)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type Assinatura = OrkAssinatura
|
|
97
|
+
export type EntradaDoCache = OrkRetratoGuardado
|
|
98
|
+
|
|
99
|
+
export function chaveDoPedido(p: OrkPedidoDeRetrato): string {
|
|
100
|
+
return JSON.stringify([p.thread ?? null, p.secao ?? null, p.offset ?? null])
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pai(dir: string): string {
|
|
104
|
+
const sem = dir.replace(/\/+$/, '')
|
|
105
|
+
const i = sem.lastIndexOf('/')
|
|
106
|
+
return i <= 0 ? '/' : sem.slice(0, i)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function juntar(pasta: string, nome: string): string {
|
|
110
|
+
return pasta.replace(/\/+$/, '') + '/' + nome
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Os ledgers que o retrato cobre: o da thread pedida, ou os das threads da secao `threads` e a pasta delas. */
|
|
114
|
+
export function caminhosCobertos(raizDoEstado: string | null, p: OrkPedidoDeRetrato, retrato: unknown): string[] {
|
|
115
|
+
if (!raizDoEstado) return []
|
|
116
|
+
const base = juntar(raizDoEstado, '.orkastery/threads')
|
|
117
|
+
if (p.thread) return [juntar(base, `${p.thread}/ledger.jsonl`)]
|
|
118
|
+
const itens = (retrato as { sections?: { threads?: { items?: Array<{ id?: unknown }> } } } | null)?.sections?.threads?.items ?? []
|
|
119
|
+
const ids = [...new Set(itens.map(i => i.id).filter((id): id is string => typeof id === 'string' && ID_DE_THREAD.test(id)))]
|
|
120
|
+
return [base, ...ids.slice(0, TETO_DE_LEDGERS).map(id => juntar(base, `${id}/ledger.jsonl`))]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** O cache serve so com a mesma assinatura de `stat` e dentro do teto de idade. */
|
|
124
|
+
export function cacheValido(e: EntradaDoCache | undefined, atual: Assinatura, agora: number, teto = TETO_DO_CACHE_MS): boolean {
|
|
125
|
+
if (!e || e.caminhos.length === 0) return false
|
|
126
|
+
const idade = agora - e.lidoEm
|
|
127
|
+
if (!(idade >= 0 && idade < teto)) return false
|
|
128
|
+
return JSON.stringify(e.assinatura) === JSON.stringify(atual)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Guarda a entrada e corta as mais velhas acima do teto de entradas. */
|
|
132
|
+
export function guardar(cache: Readonly<Record<string, EntradaDoCache>> | undefined, chave: string, e: EntradaDoCache,
|
|
133
|
+
teto = TETO_DE_ENTRADAS): Record<string, EntradaDoCache> {
|
|
134
|
+
const todas = Object.entries({ ...(cache ?? {}), [chave]: e }).sort((a, b) => b[1].lidoEm - a[1].lidoEm)
|
|
135
|
+
return Object.fromEntries(todas.slice(0, teto))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Acha a raiz do estado subindo da raiz da sessao ate a pasta com `orkastery.yaml` e `.orkastery/`
|
|
140
|
+
* ao lado (a worktree da thread fica dentro do checkout principal). `existe` vem do register.ts.
|
|
141
|
+
*/
|
|
142
|
+
export async function subirAteOEstado(raizDaSessao: string, existe: (caminho: string) => Promise<boolean>, niveis = 8): Promise<string | null> {
|
|
143
|
+
let dir = raizDaSessao.replace(/\/+$/, '') || '/'
|
|
144
|
+
for (let i = 0; i <= niveis; i++) {
|
|
145
|
+
if (await existe(juntar(dir, 'orkastery.yaml')) && await existe(juntar(dir, '.orkastery'))) return dir
|
|
146
|
+
const acima = pai(dir)
|
|
147
|
+
if (acima === dir) break
|
|
148
|
+
dir = acima
|
|
149
|
+
}
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A raiz do checkout principal pelo `git rev-parse --git-common-dir`, quando ele termina em `/.git`. */
|
|
154
|
+
export function raizPeloGit(saida: string): string | null {
|
|
155
|
+
const linha = saida.trim().split('\n')[0] ?? ''
|
|
156
|
+
return /^\/.*\/\.git\/?$/.test(linha) ? linha.replace(/\/\.git\/?$/, '') : null
|
|
157
|
+
}
|