@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,470 @@
|
|
|
1
|
+
// O Radar de Colisão do Orkastery (RM-059, fatia F3; ficha M10 do plano dos mods). Na sessão do dono, calcula sem
|
|
2
|
+
// worktree o merge de cada thread aberta contra a origin/main local e entre as threads, cruza os conflitos com os
|
|
3
|
+
// leases e desenha o mapa de calor num painel; a colisão nova vira uma linha na faixa pela ponte, e o que o dono
|
|
4
|
+
// viu entra no registro da ponte (M5 do dono). Lê o núcleo pela ponte (`$.ork`), nunca `.orkastery/` por conta
|
|
5
|
+
// própria; do git, só ls-remote e merge-tree com objetos temporários (D6 do dono). Só lê: nenhum caminho daqui
|
|
6
|
+
// age pelo dono. A lógica fica nos módulos puros ao lado; aqui só os ganchos e as chamadas ao `$`, sempre
|
|
7
|
+
// escritas como `x.noun.metodo(...)`, como o validador exige.
|
|
8
|
+
import { atom, read, update } from 'claude-code'
|
|
9
|
+
import type { EngineInterface, Register } from 'claude-code'
|
|
10
|
+
import type { RadarMapa, RadarThread } from '../types'
|
|
11
|
+
import {
|
|
12
|
+
AMBIENTE_DO_GIT, ambienteDoMerge, anterioresDoMapa, argvDeApagar, argvDoLsRemoteLocal, argvDoLsRemoteRemoto, argvDoMergeTree,
|
|
13
|
+
argvDoTemporario, baseDoMapa, branchCanonica, celulaDoPar, chaveDoPar, colisoesDoMapa, contar, gitdirDoArquivo, guardarNoCache, juntar,
|
|
14
|
+
LEITURAS_EM_PARALELO, lerLsRemote, lerMergeTree, objetosDoComum, ORCAMENTO_DO_MERGE_MS, paginaDosLeases, paresComColisaoNova,
|
|
15
|
+
paresDaRodada, PERIODO_DA_RODADA_MS, PRAZO_DO_MERGE_MS, PRAZO_DO_REMOTO_MS, PRAZO_LOCAL_MS, REF_DA_BASE, REF_REMOTA, refDoStatus,
|
|
16
|
+
resumoDoMapa, rotuloDe, temporarioValido, TETO_DE_PAGINAS_DE_LEASES, threadsDaLista,
|
|
17
|
+
} from './colisao'
|
|
18
|
+
import type { LeaseDePath, Par, Resultado, ThreadDaLista } from './colisao'
|
|
19
|
+
import { desenharPainel, linhasDoPainel, mostradosDoMapa, mostradosVisiveis, novasDepoisDaRodada, textoDaFaixa } from './mapa'
|
|
20
|
+
import type { ElementosDoPainel, RadarMostrado } from './mapa'
|
|
21
|
+
|
|
22
|
+
const MOD = 'ork-radar'
|
|
23
|
+
const PAINEL = 'radar'
|
|
24
|
+
const TITULO = 'Radar'
|
|
25
|
+
const MAPA_VAZIO: RadarMapa = {
|
|
26
|
+
threads: [], base: { ref: REF_DA_BASE, sha: null, remoto: null, situacao: 'nao-conferida', erroRemoto: null },
|
|
27
|
+
celulas: [], omitidas: 0, lacunas: [], lidoEm: null, erro: null,
|
|
28
|
+
}
|
|
29
|
+
const mapa = atom({ plugin: 'ork-radar', key: 'mapa' } as const, MAPA_VAZIO)
|
|
30
|
+
const novas = atom({ plugin: 'ork-radar', key: 'novas' } as const, [] as string[])
|
|
31
|
+
/** O servidor do núcleo pode conectar depois do início da sessão: o papel é conferido de novo nestas esperas. */
|
|
32
|
+
const ESPERAS_DO_PAPEL_MS = [3000, 10000, 30000]
|
|
33
|
+
/** Depois delas, enquanto dormente, a cada 5 min. */
|
|
34
|
+
const ESPERA_LONGA_DO_PAPEL_MS = 300000
|
|
35
|
+
/** Teto de cada leitura pela ponte, que já tem o próprio teto de 10 s no CLI. */
|
|
36
|
+
const PRAZO_DA_PONTE_MS = 20000
|
|
37
|
+
/** A primeira rodada sai logo depois da ativação, fora do início da sessão. */
|
|
38
|
+
const ESPERA_DA_PRIMEIRA_MS = 1000
|
|
39
|
+
/** O que o desenho anotou vai ao registro da ponte, fora do desenho, a cada minuto. */
|
|
40
|
+
const PERIODO_DO_REGISTRO_MS = 60000
|
|
41
|
+
|
|
42
|
+
// O que o módulo guarda entre ganchos e não desenha. Uma recarga do módulo zera tudo, e o `session.start`
|
|
43
|
+
// dispara de novo nela; o mapa, as colisões novas e a linha da faixa (na ponte) ficam em `$.state`.
|
|
44
|
+
let ativa = false
|
|
45
|
+
let desistiu = false
|
|
46
|
+
let ciclo: { cancel: () => void } | undefined
|
|
47
|
+
let registro: { cancel: () => void } | undefined
|
|
48
|
+
let rodando: Promise<void> | undefined
|
|
49
|
+
let pendente = false
|
|
50
|
+
/** O resultado de cada par de SHAs já calculado nesta carga (D12): par que não andou não roda de novo. */
|
|
51
|
+
const cache = new Map<string, Resultado>()
|
|
52
|
+
/** A branch de cada thread pelo `base.branch` do `thread.status` (null: vale a `ork/<slug>`), lida uma vez por carga. */
|
|
53
|
+
const branchDoStatus = new Map<string, string | null>()
|
|
54
|
+
/** O diretório de objetos do repositório, achado uma vez por carga. */
|
|
55
|
+
let objetos: string | undefined
|
|
56
|
+
/**
|
|
57
|
+
* O `lidoEm` do último mapa que uma rodada desta carga gravou. A leitura de `$.state` num despacho vê o momento em
|
|
58
|
+
* que ele começou, e não a gravação que outro despacho faz no meio: quem compara a versão do mapa visto com a atual
|
|
59
|
+
* usa esta variável. Undefined até a primeira rodada desta carga gravar.
|
|
60
|
+
*/
|
|
61
|
+
let versaoGravada: number | null | undefined
|
|
62
|
+
/** As colisões (par e arquivo) da rodada anterior; null até a primeira rodada desta carga ler o mapa guardado (D6). */
|
|
63
|
+
let anteriores: Set<string> | null = null
|
|
64
|
+
/**
|
|
65
|
+
* A última linha publicada na faixa. `null` é "não sei": uma carga nova não sabe o que a anterior deixou na faixa
|
|
66
|
+
* da ponte, então a primeira publicação sempre acontece, inclusive a que tira a linha.
|
|
67
|
+
*/
|
|
68
|
+
let ultimaFaixa: string | undefined | null = null
|
|
69
|
+
/**
|
|
70
|
+
* O que o desenho pôs na janela de rolagem e ainda não foi ao registro da ponte, com a versão do mapa desenhado
|
|
71
|
+
* (`lidoEm`): anotação de um desenho anterior à última rodada não vale, porque o texto da tela era outro.
|
|
72
|
+
*/
|
|
73
|
+
const vistosNoDesenho = new Map<string, { visto: RadarMostrado; versao: number | null }>()
|
|
74
|
+
/** O que já foi ao registro nesta carga, por chave e texto, com a chave do par; sai quando o conflito some do mapa. */
|
|
75
|
+
const jaMostrado = new Map<string, string>()
|
|
76
|
+
|
|
77
|
+
function mensagem(erro: unknown): string {
|
|
78
|
+
return (erro instanceof Error ? erro.message : String(erro)).replace(/\s+/g, ' ').trim().slice(0, 200)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function primeiraLinha(texto: string): string {
|
|
82
|
+
return String(texto ?? '').trim().split('\n')[0]?.trim().slice(0, 200) ?? ''
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A leitura com teto próprio: o relógio da sessão recusa a espera que passar dele. */
|
|
86
|
+
function comPrazo<T>($: EngineInterface, leitura: Promise<T>, ms = PRAZO_DA_PONTE_MS): Promise<T> {
|
|
87
|
+
return new Promise<T>((resolver, recusar) => {
|
|
88
|
+
const prazo = $.clock.after(ms, () => recusar(new Error(`a leitura passou de ${Math.round(ms / 1000)} s`)))
|
|
89
|
+
leitura.then(v => { prazo.cancel(); resolver(v) }, e => { prazo.cancel(); recusar(e) })
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* O papel pela ponte. `null` só quando o `$.ork` não existe (a ponte não carregou: a chamada nem acontece);
|
|
95
|
+
* uma falha da própria ponte conta como dormente e o papel é conferido de novo depois.
|
|
96
|
+
*/
|
|
97
|
+
async function papelDaSessao($: EngineInterface): Promise<string | null> {
|
|
98
|
+
try {
|
|
99
|
+
return String(await $.ork.papel())
|
|
100
|
+
} catch (erro) {
|
|
101
|
+
return erro instanceof TypeError ? null : 'dormente'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Liga o Radar na sessão do dono: o `/radar`, a rodada de 10 min, o registro do mostrado e a primeira rodada. */
|
|
106
|
+
async function ativar($: EngineInterface): Promise<void> {
|
|
107
|
+
if (ativa) return
|
|
108
|
+
ativa = true
|
|
109
|
+
await $.command.register({
|
|
110
|
+
name: 'radar', description: 'Radar de Colisão: onde cada thread aberta colide com a origin/main e com as outras, antes do SHIP',
|
|
111
|
+
immediate: true,
|
|
112
|
+
})
|
|
113
|
+
ciclo?.cancel()
|
|
114
|
+
ciclo = $.clock.every(PERIODO_DA_RODADA_MS, () => { void pedirRodada($) })
|
|
115
|
+
registro?.cancel()
|
|
116
|
+
registro = $.clock.every(PERIODO_DO_REGISTRO_MS, () => { void registrarMostrados($) })
|
|
117
|
+
$.clock.after(ESPERA_DA_PRIMEIRA_MS, () => { void pedirRodada($) })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Confere o papel: liga no dono, desiste na fase ou sem a ponte, e tenta de novo enquanto dormente. */
|
|
121
|
+
async function conferirPapel($: EngineInterface, tentativa: number): Promise<void> {
|
|
122
|
+
if (ativa || desistiu) return
|
|
123
|
+
const papel = await papelDaSessao($)
|
|
124
|
+
if (papel === 'dono') return ativar($)
|
|
125
|
+
if (papel === 'fase' || papel === null) {
|
|
126
|
+
desistiu = true
|
|
127
|
+
if (papel === null) $.ui.toast('radar: a ponte ork-ponte não carregou; o Radar de Colisão fica desligado nesta sessão')
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
const espera = ESPERAS_DO_PAPEL_MS[tentativa] ?? ESPERA_LONGA_DO_PAPEL_MS
|
|
131
|
+
$.clock.after(espera, () => { void conferirPapel($, tentativa + 1) })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Uma rodada por vez. Com uma rodada em andamento, o pedido só marca que outra deve rodar logo depois e volta sem
|
|
136
|
+
* nada para esperar: quem pediu (o `/radar`, o botão, o relógio) nunca espera a rodada de outro despacho, cuja
|
|
137
|
+
* espera contaria no orçamento do gancho do host. Devolve a rodada só a quem a começou.
|
|
138
|
+
*/
|
|
139
|
+
function pedirRodada($: EngineInterface): Promise<void> | undefined {
|
|
140
|
+
if (!ativa) return undefined
|
|
141
|
+
if (rodando) {
|
|
142
|
+
pendente = true
|
|
143
|
+
return undefined
|
|
144
|
+
}
|
|
145
|
+
const esta = (async () => {
|
|
146
|
+
try {
|
|
147
|
+
do {
|
|
148
|
+
pendente = false
|
|
149
|
+
await rodadaUmaVez($)
|
|
150
|
+
} while (pendente)
|
|
151
|
+
} finally {
|
|
152
|
+
rodando = undefined
|
|
153
|
+
}
|
|
154
|
+
})()
|
|
155
|
+
rodando = esta
|
|
156
|
+
return esta
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A rodada parou antes do mapa: o mapa anterior fica, com o motivo. */
|
|
160
|
+
async function gravarErro($: EngineInterface, erro: string): Promise<void> {
|
|
161
|
+
await update($, mapa, anterior => ({ ...anterior, erro }))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** O SHA da base e de cada ref pedida, pelo `ls-remote` no próprio repositório. */
|
|
165
|
+
async function lerShas($: EngineInterface, raiz: string, refs: readonly string[]): Promise<Record<string, string>> {
|
|
166
|
+
const r = await $.process.run(argvDoLsRemoteLocal(refs), { cwd: raiz, env: { ...AMBIENTE_DO_GIT }, timeoutMs: PRAZO_LOCAL_MS })
|
|
167
|
+
if (r.exitCode !== 0) throw new Error(primeiraLinha(r.stderr) || `o ls-remote saiu ${r.exitCode}`)
|
|
168
|
+
return lerLsRemote(r.stdout)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** O SHA da `main` do `origin` (D4); falha vira "não conferida" e a rodada segue. */
|
|
172
|
+
async function lerRemoto($: EngineInterface, raiz: string): Promise<{ sha: string | null; erro: string | null }> {
|
|
173
|
+
try {
|
|
174
|
+
const r = await $.process.run(argvDoLsRemoteRemoto(), { cwd: raiz, env: { ...AMBIENTE_DO_GIT }, timeoutMs: PRAZO_DO_REMOTO_MS })
|
|
175
|
+
if (r.exitCode !== 0) return { sha: null, erro: primeiraLinha(r.stderr) || `o ls-remote saiu ${r.exitCode}` }
|
|
176
|
+
const sha = lerLsRemote(r.stdout)[REF_REMOTA] ?? null
|
|
177
|
+
return { sha, erro: sha === null ? 'o remoto não listou a main' : null }
|
|
178
|
+
} catch (erro) {
|
|
179
|
+
return { sha: null, erro: mensagem(erro) }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* A branch canônica de cada thread, como o núcleo a decide (`branchDaWorktree`): o `base.branch` do `thread.status`
|
|
185
|
+
* quando começa com `ork/`, senão `ork/<slug>`. O status de cada thread é lido uma vez por carga, algumas por vez;
|
|
186
|
+
* leitura que falha usa a `ork/<slug>` nesta rodada, vira lacuna e é tentada de novo na seguinte.
|
|
187
|
+
*/
|
|
188
|
+
async function branchesDasThreads($: EngineInterface, threads: readonly ThreadDaLista[], lacunas: string[]): Promise<Map<string, string>> {
|
|
189
|
+
const faltam = threads.filter(t => !branchDoStatus.has(t.id))
|
|
190
|
+
for (let i = 0; i < faltam.length; i += LEITURAS_EM_PARALELO) {
|
|
191
|
+
await Promise.all(faltam.slice(i, i + LEITURAS_EM_PARALELO).map(async t => {
|
|
192
|
+
try {
|
|
193
|
+
branchDoStatus.set(t.id, refDoStatus(await comPrazo($, $.ork.ler({ consulta: 'thread.status', thread: t.id }))))
|
|
194
|
+
} catch (erro) {
|
|
195
|
+
lacunas.push(`status de ${t.id} não lido; vale a ork/${t.slug} nesta rodada (${mensagem(erro)})`)
|
|
196
|
+
}
|
|
197
|
+
}))
|
|
198
|
+
}
|
|
199
|
+
return new Map(threads.map(t => [t.id, branchCanonica(branchDoStatus.get(t.id) ?? null, t.slug)]))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Os leases `path:` ativos de todas as páginas da seção `leases` do retrato, até o teto de páginas (D1 e D10). */
|
|
203
|
+
async function lerLeases($: EngineInterface, lacunas: string[]): Promise<LeaseDePath[]> {
|
|
204
|
+
const leases: LeaseDePath[] = []
|
|
205
|
+
let offset = 0
|
|
206
|
+
try {
|
|
207
|
+
for (let pagina = 0; pagina < TETO_DE_PAGINAS_DE_LEASES; pagina++) {
|
|
208
|
+
const p = paginaDosLeases((await comPrazo($, $.ork.retrato({ secao: 'leases', offset }))).retrato)
|
|
209
|
+
leases.push(...p.leases)
|
|
210
|
+
if (p.lacuna) lacunas.push(p.lacuna)
|
|
211
|
+
if (p.proximo === null || p.proximo <= offset) return leases
|
|
212
|
+
offset = p.proximo
|
|
213
|
+
}
|
|
214
|
+
lacunas.push(`leases além de ${TETO_DE_PAGINAS_DE_LEASES} páginas não lidos`)
|
|
215
|
+
} catch (erro) {
|
|
216
|
+
lacunas.push(`leases não lidos (${mensagem(erro)})`)
|
|
217
|
+
}
|
|
218
|
+
return leases
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Os objetos do repositório pelo `.git` da raiz: pasta, ou arquivo de worktree vinculada com `commondir` (D2). */
|
|
222
|
+
async function acharObjetos($: EngineInterface, raiz: string): Promise<string> {
|
|
223
|
+
if (objetos !== undefined) return objetos
|
|
224
|
+
const ponto = juntar(raiz, '.git')
|
|
225
|
+
const st = await $.fs.stat(ponto)
|
|
226
|
+
if (st.kind === 'dir') objetos = juntar(ponto, 'objects')
|
|
227
|
+
else if (st.kind === 'file') {
|
|
228
|
+
const gitdir = gitdirDoArquivo(String(await $.fs.read(ponto)), raiz)
|
|
229
|
+
if (gitdir === null) throw new Error('o .git da raiz não aponta um gitdir')
|
|
230
|
+
const commondir = await $.fs.read(juntar(gitdir, 'commondir')).then(String, () => null)
|
|
231
|
+
objetos = objetosDoComum(gitdir, commondir)
|
|
232
|
+
} else throw new Error('a raiz da sessão não tem .git')
|
|
233
|
+
return objetos
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** O temporário da rodada pelo `mktemp -d`; recusa o que não tiver a forma do temporário do Radar (D5). */
|
|
237
|
+
async function criarTemporario($: EngineInterface): Promise<string> {
|
|
238
|
+
const r = await $.process.run(argvDoTemporario(), { timeoutMs: PRAZO_LOCAL_MS })
|
|
239
|
+
const temporario = r.exitCode === 0 ? temporarioValido(r.stdout) : null
|
|
240
|
+
if (temporario === null) throw new Error(`o mktemp devolveu ${JSON.stringify(primeiraLinha(r.stdout || r.stderr))}`)
|
|
241
|
+
return temporario
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Os pares que o cache não tinha: um `merge-tree` por par de SHAs, com objetos novos num temporário da rodada e
|
|
246
|
+
* os do repositório como alternativo (D2, D3 e D5), dentro do orçamento da rodada (D12). O temporário sai sempre.
|
|
247
|
+
*/
|
|
248
|
+
async function calcularPares($: EngineInterface, raiz: string, pares: readonly Par[], resultados: Map<string, Resultado>,
|
|
249
|
+
motivos: Map<string, string>, lacunas: string[]): Promise<void> {
|
|
250
|
+
const semCalculo = (motivo: string) => { for (const p of pares) motivos.set(chaveDoPar(p.shaA, p.shaB), motivo) }
|
|
251
|
+
let dirDosObjetos: string
|
|
252
|
+
try {
|
|
253
|
+
dirDosObjetos = await acharObjetos($, raiz)
|
|
254
|
+
} catch (erro) {
|
|
255
|
+
lacunas.push(`objetos do repositório não achados (${mensagem(erro)})`)
|
|
256
|
+
return semCalculo('não calculado: objetos do repositório não achados')
|
|
257
|
+
}
|
|
258
|
+
let temporario: string
|
|
259
|
+
try {
|
|
260
|
+
temporario = await criarTemporario($)
|
|
261
|
+
} catch (erro) {
|
|
262
|
+
lacunas.push(`temporário recusado (${mensagem(erro)})`)
|
|
263
|
+
return semCalculo('não calculado: temporário recusado')
|
|
264
|
+
}
|
|
265
|
+
const inicio = await $.clock.now()
|
|
266
|
+
try {
|
|
267
|
+
for (const p of pares) {
|
|
268
|
+
const chave = chaveDoPar(p.shaA, p.shaB)
|
|
269
|
+
if (resultados.has(chave)) continue
|
|
270
|
+
if ((await $.clock.now()) - inicio >= ORCAMENTO_DO_MERGE_MS) {
|
|
271
|
+
motivos.set(chave, `não calculado nesta rodada (orçamento de ${ORCAMENTO_DO_MERGE_MS / 1000} s)`)
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
let r: Resultado
|
|
275
|
+
try {
|
|
276
|
+
r = lerMergeTree(await $.process.run(argvDoMergeTree(p.shaA, p.shaB),
|
|
277
|
+
{ cwd: raiz, env: ambienteDoMerge(temporario, dirDosObjetos), timeoutMs: PRAZO_DO_MERGE_MS }))
|
|
278
|
+
} catch (erro) {
|
|
279
|
+
r = { estado: 'erro', erro: mensagem(erro) }
|
|
280
|
+
}
|
|
281
|
+
resultados.set(chave, r)
|
|
282
|
+
guardarNoCache(cache, chave, r)
|
|
283
|
+
}
|
|
284
|
+
} finally {
|
|
285
|
+
const apagado = await $.process.run(argvDeApagar(temporario), { timeoutMs: PRAZO_LOCAL_MS }).then(r => r.exitCode === 0, () => false)
|
|
286
|
+
if (!apagado) lacunas.push(`temporário ${temporario} não apagado`)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Uma rodada inteira: threads e leases pela ponte, SHAs pelo `ls-remote`, pares pelo `merge-tree`, mapa no estado. */
|
|
291
|
+
async function rodadaUmaVez($: EngineInterface): Promise<void> {
|
|
292
|
+
let raiz: string
|
|
293
|
+
try {
|
|
294
|
+
const contexto = await comPrazo($, $.ork.contexto())
|
|
295
|
+
if (contexto.papel !== 'dono') return
|
|
296
|
+
raiz = contexto.raiz
|
|
297
|
+
} catch (erro) {
|
|
298
|
+
return gravarErro($, `o contexto da ponte falhou (${mensagem(erro)})`)
|
|
299
|
+
}
|
|
300
|
+
const lacunas: string[] = []
|
|
301
|
+
let escolha: ReturnType<typeof threadsDaLista>
|
|
302
|
+
try {
|
|
303
|
+
escolha = threadsDaLista(await comPrazo($, $.ork.ler({ consulta: 'thread.list' })))
|
|
304
|
+
} catch (erro) {
|
|
305
|
+
return gravarErro($, `a lista das threads abertas falhou (${mensagem(erro)})`)
|
|
306
|
+
}
|
|
307
|
+
const leases = await lerLeases($, lacunas)
|
|
308
|
+
const refs = await branchesDasThreads($, escolha.threads, lacunas)
|
|
309
|
+
let shas: Record<string, string>
|
|
310
|
+
try {
|
|
311
|
+
shas = await lerShas($, raiz, [...new Set(refs.values())])
|
|
312
|
+
} catch (erro) {
|
|
313
|
+
return gravarErro($, `o ls-remote do repositório falhou (${mensagem(erro)})`)
|
|
314
|
+
}
|
|
315
|
+
const threads: RadarThread[] = escolha.threads.map((t, i) => {
|
|
316
|
+
const branch = refs.get(t.id) as string
|
|
317
|
+
const sha = shas[branch] ?? null
|
|
318
|
+
return { id: t.id, fase: t.fase, rotulo: rotuloDe(i), branch, sha, motivo: sha === null ? 'sem branch local' : null }
|
|
319
|
+
})
|
|
320
|
+
const shaDaBase = shas[REF_DA_BASE] ?? null
|
|
321
|
+
if (shaDaBase === null) lacunas.push(`sem ${REF_DA_BASE} local: os pares com a base ficaram de fora`)
|
|
322
|
+
const remoto = await lerRemoto($, raiz)
|
|
323
|
+
const pares = paresDaRodada(threads, shaDaBase)
|
|
324
|
+
const resultados = new Map<string, Resultado>()
|
|
325
|
+
const motivos = new Map<string, string>()
|
|
326
|
+
const calcular: Par[] = []
|
|
327
|
+
for (const p of pares) {
|
|
328
|
+
const chave = chaveDoPar(p.shaA, p.shaB)
|
|
329
|
+
const guardado = cache.get(chave)
|
|
330
|
+
if (p.shaA === p.shaB) resultados.set(chave, { estado: 'limpa' })
|
|
331
|
+
else if (guardado !== undefined) resultados.set(chave, guardado)
|
|
332
|
+
else calcular.push(p)
|
|
333
|
+
}
|
|
334
|
+
if (calcular.length > 0) await calcularPares($, raiz, calcular, resultados, motivos, lacunas)
|
|
335
|
+
const celulas = pares.map(p => {
|
|
336
|
+
const chave = chaveDoPar(p.shaA, p.shaB)
|
|
337
|
+
return celulaDoPar(p, resultados.get(chave) ?? null, leases, motivos.get(chave))
|
|
338
|
+
})
|
|
339
|
+
const novo: RadarMapa = {
|
|
340
|
+
threads, base: baseDoMapa(shaDaBase, remoto), celulas, omitidas: escolha.omitidas, lacunas, lidoEm: await $.clock.now(), erro: null,
|
|
341
|
+
}
|
|
342
|
+
// D6: nova é a colisão que não estava na rodada anterior desta sessão. Depois de uma recarga do módulo, a rodada
|
|
343
|
+
// anterior é a do mapa guardado em `$.state`, lido antes de o mapa novo ir por cima dele.
|
|
344
|
+
if (anteriores === null) anteriores = anterioresDoMapa(await read($, mapa))
|
|
345
|
+
await update($, mapa, () => novo)
|
|
346
|
+
versaoGravada = novo.lidoEm
|
|
347
|
+
const novosPares = paresComColisaoNova(anteriores, novo)
|
|
348
|
+
anteriores = new Set(colisoesDoMapa(novo))
|
|
349
|
+
await update($, novas, antigas => novasDepoisDaRodada(antigas, novosPares, novo))
|
|
350
|
+
await publicarFaixa($)
|
|
351
|
+
await registrarMostrados($)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** O painel do Radar está posto e à vista do dono. */
|
|
355
|
+
async function painelAVista($: EngineInterface): Promise<boolean> {
|
|
356
|
+
const paineis = await $.ui.panes().catch(() => [])
|
|
357
|
+
return paineis.some(p => p.id === PAINEL && p.isPlaced && p.isShown)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Uma falha que o dono precisa ver: entra nas lacunas do mapa, que o painel e o `/radar` mostram, uma vez só. */
|
|
361
|
+
async function anotarLacuna($: EngineInterface, lacuna: string): Promise<void> {
|
|
362
|
+
await update($, mapa, m => (m.lacunas.includes(lacuna) ? m : { ...m, lacunas: [...m.lacunas, lacuna] }))
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** A linha da faixa pela ponte (D6 e D7), só quando o texto muda; sem colisão nova não vista, a linha sai. */
|
|
366
|
+
async function publicarFaixa($: EngineInterface): Promise<void> {
|
|
367
|
+
const texto = textoDaFaixa(await read($, mapa), await read($, novas))
|
|
368
|
+
if (texto === ultimaFaixa) return
|
|
369
|
+
try {
|
|
370
|
+
if (texto === undefined) await $.ork.faixa({ mod: MOD })
|
|
371
|
+
else await $.ork.faixa({ mod: MOD, texto })
|
|
372
|
+
ultimaFaixa = texto
|
|
373
|
+
} catch (erro) {
|
|
374
|
+
// A linha fica como estava; o motivo vai às lacunas, e a próxima rodada tenta de novo.
|
|
375
|
+
await anotarLacuna($, `linha da faixa não publicada (${mensagem(erro)})`)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* O que o dono viu (conflito por conflito) deixa de ser novo, e a linha da faixa acompanha. Só vale para a versão
|
|
381
|
+
* do mapa que ele viu: se uma rodada gravou outra no meio, a colisão nova dela continua nova.
|
|
382
|
+
*/
|
|
383
|
+
async function marcarVistos($: EngineInterface, vistos: readonly RadarMostrado[], versao: number | null): Promise<void> {
|
|
384
|
+
if (versaoGravada === undefined || versao !== versaoGravada) return
|
|
385
|
+
const pares = new Set(vistos.map(v => v.chave.slice('radar:'.length)))
|
|
386
|
+
if (!(await read($, novas)).some(p => pares.has(p))) return
|
|
387
|
+
await update($, novas, atuais => atuais.filter(p => !pares.has(p)))
|
|
388
|
+
await publicarFaixa($)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Registra pela ponte o que o dono viu, uma vez por chave e texto enquanto o conflito existir no mapa. */
|
|
392
|
+
async function registrar($: EngineInterface, canal: 'painel' | 'transcript', vistos: readonly RadarMostrado[]): Promise<RadarMostrado[]> {
|
|
393
|
+
const falharam: RadarMostrado[] = []
|
|
394
|
+
for (const v of vistos) {
|
|
395
|
+
const chave = `${v.chave}|${v.texto}`
|
|
396
|
+
if (jaMostrado.has(chave)) continue
|
|
397
|
+
try {
|
|
398
|
+
await $.ork.mostrado({ mod: MOD, canal, chave: v.chave, thread: v.thread, texto: v.texto })
|
|
399
|
+
jaMostrado.set(chave, v.chave)
|
|
400
|
+
} catch {
|
|
401
|
+
falharam.push(v)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return falharam
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* O que o desenho pôs na janela de rolagem do painel posto e à vista vai ao registro da ponte (M5 do dono), fora
|
|
409
|
+
* do desenho: o Telegram não repete o que o dono já viu no terminal, e nada que não estava na tela entra.
|
|
410
|
+
*/
|
|
411
|
+
async function registrarMostrados($: EngineInterface): Promise<void> {
|
|
412
|
+
const m = await read($, mapa)
|
|
413
|
+
// O registro já feito sai quando o par deixa de estar em conflito (a chave do par, porque o texto pode ser o
|
|
414
|
+
// pedaço que coube na largura); a anotação de outra versão do mapa sai sempre, e o próximo desenho anota de novo.
|
|
415
|
+
const emConflito = new Set(mostradosDoMapa(m).map(v => v.chave))
|
|
416
|
+
for (const [chave, par] of [...jaMostrado]) if (!emConflito.has(par)) jaMostrado.delete(chave)
|
|
417
|
+
const versao = versaoGravada
|
|
418
|
+
for (const [chave, a] of [...vistosNoDesenho]) if (versao === undefined || a.versao !== versao) vistosNoDesenho.delete(chave)
|
|
419
|
+
if (vistosNoDesenho.size === 0 || versao === undefined) return
|
|
420
|
+
// Com o painel escondido, o que foi anotado é descartado: o próximo desenho à vista anota de novo.
|
|
421
|
+
if (!await painelAVista($)) { vistosNoDesenho.clear(); return }
|
|
422
|
+
const vistos = [...vistosNoDesenho.values()].map(a => a.visto)
|
|
423
|
+
vistosNoDesenho.clear()
|
|
424
|
+
for (const v of await registrar($, 'painel', vistos)) vistosNoDesenho.set(`${v.chave}|${v.texto}`, { visto: v, versao })
|
|
425
|
+
await marcarVistos($, vistos, versao)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** O desenho anota o que pôs na janela, com a versão do mapa desenhado; quem registra é o laço, fora do desenho. */
|
|
429
|
+
function anotarVistos(vistos: readonly RadarMostrado[], versao: number | null): void {
|
|
430
|
+
for (const v of vistos) vistosNoDesenho.set(`${v.chave}|${v.texto}`, { visto: v, versao })
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export const register: Register = on => {
|
|
434
|
+
on('session.start', async ($, e, next) => {
|
|
435
|
+
if (e.isInteractive) await conferirPapel($, 0)
|
|
436
|
+
return next(e)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
// `/radar` abre o painel, roda uma rodada agora e responde o resumo, que o dono lê ali mesmo: os conflitos do
|
|
440
|
+
// resumo entram no registro da ponte e deixam de ser novos. Com uma rodada de outro despacho em andamento, não a
|
|
441
|
+
// espera: responde o mapa que há e a rodada pedida roda logo depois. O painel não abre sozinho (D6). O comando só
|
|
442
|
+
// existe com o Radar ligado; a conferência do papel aqui só cobre o caso raro de o comando ter sobrado de outra carga.
|
|
443
|
+
on('command.run', { command: 'radar' }, async $ => {
|
|
444
|
+
if (!ativa && !desistiu) await conferirPapel($, ESPERAS_DO_PAPEL_MS.length)
|
|
445
|
+
if (!ativa) return { text: 'O Radar de Colisão só roda na sessão do dono de um projeto do Orkastery.' }
|
|
446
|
+
const aberto = await $.ui.open({ id: PAINEL, title: TITULO }).catch(() => null)
|
|
447
|
+
const propria = pedirRodada($)
|
|
448
|
+
if (propria) await propria
|
|
449
|
+
const m = await read($, mapa)
|
|
450
|
+
const vistos = mostradosDoMapa(m)
|
|
451
|
+
const falharam = await registrar($, 'transcript', vistos)
|
|
452
|
+
if (falharam.length > 0) {
|
|
453
|
+
await anotarLacuna($, `${contar(falharam.length, 'conflito não foi', 'conflitos não foram')} ao registro do mostrado da ponte`)
|
|
454
|
+
}
|
|
455
|
+
await marcarVistos($, vistos, m.lidoEm)
|
|
456
|
+
const andamento = propria ? '' : '\nUma rodada já estava em andamento: o painel mostra a conta nova quando ela terminar.'
|
|
457
|
+
const espera = aberto && !aberto.isPlaced ? `\nO painel espera lugar: ${aberto.reason}` : ''
|
|
458
|
+
return { text: resumoDoMapa(m) + andamento + espera }
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
// O painel: a matriz (Raster no terminal, tabela fora dele), a lista de conflitos e o botão Atualizar. O desenho
|
|
462
|
+
// nunca escreve: só anota o que pôs na janela de rolagem, cortado na largura do painel, para o registro do mostrado.
|
|
463
|
+
on('ui.render', { component: 'Pane', requestId: PAINEL }, async ($, e) => {
|
|
464
|
+
const el = $.ui.resolve(e) as unknown as ElementosDoPainel
|
|
465
|
+
const m = await read($, mapa)
|
|
466
|
+
const linhas = linhasDoPainel(m, await read($, novas), await $.clock.now())
|
|
467
|
+
anotarVistos(mostradosVisiveis(linhas, e.props.scroll, e.props.bodyColumns), m.lidoEm)
|
|
468
|
+
return desenharPainel(el, m, linhas, { terminal: e.surface === 'terminal', atualizar: () => { void pedirRodada($) } })
|
|
469
|
+
})
|
|
470
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Contrato do estado do Radar de Colisão (`ork-radar`, RM-059, fatia F3). O motor exige que cada chave de
|
|
2
|
+
// `$.state` do mod esteja declarada aqui, em `PluginState`. O Radar não acrescenta noun ao `$`: lê o núcleo
|
|
3
|
+
// pelo `$.ork` da `ork-ponte`, que o plugin.json lista em `dependencies`. Sem import nem referência.
|
|
4
|
+
|
|
5
|
+
/** Como um par ficou na última conta: limpo, em conflito, com erro do git ou ainda não calculado. */
|
|
6
|
+
export type RadarEstado = 'limpa' | 'conflito' | 'erro' | 'pendente'
|
|
7
|
+
|
|
8
|
+
/** Um lease `path:<glob>` ativo que cobre um arquivo em conflito, com a thread que o segura. */
|
|
9
|
+
export type RadarLease = { lease: string; thread: string }
|
|
10
|
+
|
|
11
|
+
/** Um arquivo em conflito num par e os leases que o cobrem. */
|
|
12
|
+
export type RadarArquivo = { caminho: string; leases: RadarLease[] }
|
|
13
|
+
|
|
14
|
+
/** Um par: uma thread contra a base (`@main`) ou duas threads, com o que o `merge-tree` disse dele. */
|
|
15
|
+
export type RadarCelula = {
|
|
16
|
+
/** Os dois lados em ordem, separados por `|`. */
|
|
17
|
+
par: string
|
|
18
|
+
a: string
|
|
19
|
+
b: string
|
|
20
|
+
estado: RadarEstado
|
|
21
|
+
arquivos: RadarArquivo[]
|
|
22
|
+
/** A primeira linha do erro do git, ou o motivo de o par não ter sido calculado. */
|
|
23
|
+
erro: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Uma thread aberta da rodada: o rótulo do mapa, a branch e o SHA dela, ou por que não tem. */
|
|
27
|
+
export type RadarThread = {
|
|
28
|
+
id: string
|
|
29
|
+
fase: string
|
|
30
|
+
/** O nome curto no mapa: de 1 a 9, depois a, b e c. */
|
|
31
|
+
rotulo: string
|
|
32
|
+
/** A ref da branch, como o `ls-remote` a lista. */
|
|
33
|
+
branch: string
|
|
34
|
+
sha: string | null
|
|
35
|
+
motivo: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A base da conta: a `origin/main` local e o que o remoto disse dela. */
|
|
39
|
+
export type RadarBase = {
|
|
40
|
+
ref: string
|
|
41
|
+
sha: string | null
|
|
42
|
+
remoto: string | null
|
|
43
|
+
/** `igual` ao remoto, `diferente` dele, ou `nao-conferida` quando o remoto não respondeu. */
|
|
44
|
+
situacao: 'igual' | 'diferente' | 'nao-conferida'
|
|
45
|
+
erroRemoto: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** O mapa de uma rodada. */
|
|
49
|
+
export type RadarMapa = {
|
|
50
|
+
threads: RadarThread[]
|
|
51
|
+
base: RadarBase
|
|
52
|
+
celulas: RadarCelula[]
|
|
53
|
+
/** Threads abertas que ficaram de fora pelo teto da rodada. */
|
|
54
|
+
omitidas: number
|
|
55
|
+
/** O que não veio inteiro: os leases, a branch de uma thread, o orçamento do `merge-tree`, o temporário. */
|
|
56
|
+
lacunas: string[]
|
|
57
|
+
/** Quando a rodada terminou, em ms desde a época; null antes da primeira. */
|
|
58
|
+
lidoEm: number | null
|
|
59
|
+
/** Por que a rodada não foi feita, quando não foi. */
|
|
60
|
+
erro: string | null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
declare module 'claude-code' {
|
|
64
|
+
interface PluginState {
|
|
65
|
+
'ork-radar': {
|
|
66
|
+
/** O mapa da última rodada. */
|
|
67
|
+
mapa: RadarMapa
|
|
68
|
+
/** Os pares com colisão nova que o dono ainda não viu no painel nem no `/radar` (D6). */
|
|
69
|
+
novas: string[]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ork-sentinela",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sentinela do Condutor do Orkastery no Claude Code: na sessão do dono, despacha a fase seguinte das threads #Auto só pelo núcleo, sem duplicar o condutor externo, que cede enquanto ela conduz; o que só o dono resolve vira aviso com o próximo passo do núcleo.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Orkastery contributors",
|
|
7
|
+
"url": "https://github.com/orkastery"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://orkastery.com/",
|
|
10
|
+
"repository": "https://github.com/orkastery/orkastery",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"types": "./types/index.d.ts",
|
|
13
|
+
"dependencies": ["ork-ponte"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Os avisos da Sentinela (RM-059, fatia F6, T6.3). Módulo puro: do `ork pulse --json` (pela ponte), o que só o dono
|
|
2
|
+
// resolve e o que está parado no condutor fora do que o condutor externo ou a própria Sentinela fazem sozinhos, cada um
|
|
3
|
+
// com o próximo passo que o próprio núcleo dá. Em #Auto, nunca o comando de pedir gate: o núcleo o recusa no modo sem
|
|
4
|
+
// pausa humana (D7). O que vira aviso, e uma vez só por texto, sai daqui; as chamadas ao `$` ficam no register.ts.
|
|
5
|
+
import type { SentinelaPasso } from '../types'
|
|
6
|
+
|
|
7
|
+
export const CONTRATO_DO_PULSE = 'ork.pulse/v1'
|
|
8
|
+
/** O pulse lê a fábrica inteira (e a forja): a cada 5 min, como resumo, e não a cada ledger que muda. */
|
|
9
|
+
export const PERIODO_DO_PULSE_MS = 5 * 60 * 1000
|
|
10
|
+
/** Numa rodada, no máximo estes avisos novos viram notificação; o resto fica para a seguinte. */
|
|
11
|
+
export const TETO_DE_AVISOS_POR_RODADA = 5
|
|
12
|
+
export const TITULO = 'Sentinela'
|
|
13
|
+
/** Os casos parados no condutor que alguém faz sozinho: o condutor externo publica e mergeia; a Sentinela despacha. */
|
|
14
|
+
export const CASOS_DE_OUTRO: readonly string[] = ['sem-push', 'pr-verde', 'fase-seguinte', 'check-sem-veredito']
|
|
15
|
+
/** Os motivos que o condutor externo também não leva ao dono: a política do núcleo já reexecuta sozinha. */
|
|
16
|
+
export const MOTIVOS_SEM_AVISO: readonly string[] = ['verify.timeout']
|
|
17
|
+
const TETO_DO_TEXTO = 300
|
|
18
|
+
const PEDIR_GATE = /`?ork gate request[^`;\n]*`?/g
|
|
19
|
+
|
|
20
|
+
export type Aviso = { chave: string; thread: string | null; texto: string }
|
|
21
|
+
|
|
22
|
+
function limpar(texto: string): string {
|
|
23
|
+
return texto.replace(/\s+/g, ' ').trim().slice(0, TETO_DO_TEXTO)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Em #Auto o núcleo recusa o pedido de gate: o comando sai do texto, e o aviso diz por quê (D7). */
|
|
27
|
+
export function semPedirGate(texto: string, modo: string | null): string {
|
|
28
|
+
if (modo !== 'auto' || !/ork gate request/.test(texto)) return texto
|
|
29
|
+
const sem = texto.replace(PEDIR_GATE, '').replace(/\s+([;,.])/g, '$1').replace(/[:;,]\s*$/, '').replace(/\s+/g, ' ').trim()
|
|
30
|
+
return `${sem}${sem ? ' ' : ''}(em #Auto o núcleo não abre gate; resolva pela ação da thread)`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Os avisos de um pulse: o que precisa do dono agora, com o comando de resposta do núcleo (ou a recomendação), e o
|
|
35
|
+
* que está parado no condutor fora dos casos que alguém faz sozinho, com o próximo passo do núcleo. Thread que alguém
|
|
36
|
+
* conduz agora não vira aviso (é trabalho em curso, como no condutor externo). Pulse fora do contrato é erro.
|
|
37
|
+
*/
|
|
38
|
+
export function avisosDoPulse(pulse: unknown, passos: readonly SentinelaPasso[]): Aviso[] {
|
|
39
|
+
const p = pulse as { contrato?: unknown; precisaDeHumanoAgora?: unknown; paradoNoCondutor?: unknown } | null
|
|
40
|
+
if (!p || typeof p !== 'object' || p.contrato !== CONTRATO_DO_PULSE) throw new Error(`o pulse nao e o contrato ${CONTRATO_DO_PULSE}`)
|
|
41
|
+
const modo = new Map(passos.map(x => [x.thread, x.modo]))
|
|
42
|
+
const conduzidas = new Set(passos.filter(x => x.conduzida).map(x => x.thread))
|
|
43
|
+
const avisos: Aviso[] = []
|
|
44
|
+
for (const bruto of Array.isArray(p.precisaDeHumanoAgora) ? p.precisaDeHumanoAgora : []) {
|
|
45
|
+
const i = (bruto ?? {}) as { thread?: unknown; motivo?: unknown; comandoResposta?: unknown; recomendacao?: unknown; pergunta?: unknown }
|
|
46
|
+
const thread = typeof i.thread === 'string' && i.thread ? i.thread : null
|
|
47
|
+
const motivo = typeof i.motivo === 'string' && i.motivo ? i.motivo : 'pendente'
|
|
48
|
+
if (MOTIVOS_SEM_AVISO.includes(motivo) || (thread && conduzidas.has(thread))) continue
|
|
49
|
+
const comando = semPedirGate(typeof i.comandoResposta === 'string' ? i.comandoResposta.trim() : '', thread ? modo.get(thread) ?? null : null)
|
|
50
|
+
const reserva = typeof i.recomendacao === 'string' && i.recomendacao.trim() ? i.recomendacao : typeof i.pergunta === 'string' ? i.pergunta : ''
|
|
51
|
+
const passo = comando || reserva || 'veja o ork pulse'
|
|
52
|
+
avisos.push({ chave: `dono:${thread ?? '-'}:${motivo}`, thread, texto: limpar(`${thread ?? 'fábrica'} (${motivo}): ${passo}`) })
|
|
53
|
+
}
|
|
54
|
+
for (const bruto of Array.isArray(p.paradoNoCondutor) ? p.paradoNoCondutor : []) {
|
|
55
|
+
const x = (bruto ?? {}) as { thread?: unknown; caso?: unknown; proximoPasso?: unknown }
|
|
56
|
+
if (typeof x.thread !== 'string' || !x.thread || typeof x.caso !== 'string' || CASOS_DE_OUTRO.includes(x.caso) || conduzidas.has(x.thread)) continue
|
|
57
|
+
const passo = semPedirGate(typeof x.proximoPasso === 'string' ? x.proximoPasso : '', modo.get(x.thread) ?? null)
|
|
58
|
+
avisos.push({ chave: `parado:${x.thread}:${x.caso}`, thread: x.thread, texto: limpar(`${x.thread} parado no condutor (${x.caso}): ${passo || 'veja o ork pulse'}`) })
|
|
59
|
+
}
|
|
60
|
+
return avisos
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A marca de um aviso no estado: a chave e o texto. Texto novo é aviso novo, como no condutor externo. */
|
|
64
|
+
export function marcaDoAviso(a: Aviso): string {
|
|
65
|
+
return `${a.chave}|${a.texto}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Os avisos que ainda não foram mostrados ao dono, até o teto da rodada. */
|
|
69
|
+
export function avisosNovos(avisos: readonly Aviso[], avisados: readonly string[], teto = TETO_DE_AVISOS_POR_RODADA): Aviso[] {
|
|
70
|
+
const vistos = new Set(avisados)
|
|
71
|
+
const saida: Aviso[] = []
|
|
72
|
+
for (const a of avisos) {
|
|
73
|
+
const marca = marcaDoAviso(a)
|
|
74
|
+
if (vistos.has(marca)) continue
|
|
75
|
+
vistos.add(marca)
|
|
76
|
+
saida.push(a)
|
|
77
|
+
if (saida.length >= teto) break
|
|
78
|
+
}
|
|
79
|
+
return saida
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** O aviso de um despacho que a Sentinela não conseguiu fazer: vai ao dono uma vez por turno. */
|
|
83
|
+
export function avisoDoDespacho(p: SentinelaPasso, motivo: string): Aviso {
|
|
84
|
+
return { chave: `despacho:${p.thread}:${p.fase}:${p.desdeEm}`, thread: p.thread, texto: limpar(`${p.thread}: a Sentinela não despachou o ${p.fase} (${motivo})`) }
|
|
85
|
+
}
|