@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,186 @@
|
|
|
1
|
+
// A leitura da condução pela Sentinela (RM-059, fatia F6, T6.1). Módulo puro: o argv do status e da batida, a
|
|
2
|
+
// leitura do contrato `ork.conducao-sentinela/v1`, a thread de um ledger que mudou, o diário, as chaves guardadas e
|
|
3
|
+
// a linha da faixa. As chamadas ao `$` ficam no register.ts.
|
|
4
|
+
import type { SentinelaEntrada, SentinelaPasso, SentinelaPresenca, SentinelaResumo } from '../types'
|
|
5
|
+
|
|
6
|
+
export const MOD = 'ork-sentinela'
|
|
7
|
+
export const CONTRATO = 'ork.conducao-sentinela/v1'
|
|
8
|
+
/** A batida da presença e a rodada de reserva: a mesma cadência do núcleo (`BATIDA_DA_SENTINELA_MS`). */
|
|
9
|
+
export const BATIDA_MS = 60000
|
|
10
|
+
/** Um ledger que muda acorda a rodada depois deste intervalo, para juntar as linhas do mesmo evento. */
|
|
11
|
+
export const ESPERA_DO_LEDGER_MS = 1500
|
|
12
|
+
/** O status lê todas as threads abertas: até 20 s, com folga sobre o que se mede (1 a 4 s). */
|
|
13
|
+
export const PRAZO_DO_STATUS_MS = 20000
|
|
14
|
+
export const PRAZO_DA_BATIDA_MS = 10000
|
|
15
|
+
export const TETO_DO_DIARIO = 50
|
|
16
|
+
export const TETO_DE_CHAVES = 200
|
|
17
|
+
export const ACOES: readonly string[] = ['despachar', 'retomar', 'avisar', 'nada']
|
|
18
|
+
export const FASES: readonly string[] = ['GOAL', 'PLAN', 'GO', 'CHECK', 'SHIP', 'MASTER']
|
|
19
|
+
const ID_DE_THREAD = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/
|
|
20
|
+
const SESSAO = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/
|
|
21
|
+
const LEDGER = /\/\.orkastery\/threads\/([a-zA-Z0-9][a-zA-Z0-9._-]{0,79})\/ledger\.jsonl$/
|
|
22
|
+
|
|
23
|
+
export function idDeThreadValido(t: unknown): t is string {
|
|
24
|
+
return typeof t === 'string' && ID_DE_THREAD.test(t)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sessaoValida(s: unknown): s is string {
|
|
28
|
+
return typeof s === 'string' && SESSAO.test(s)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function exigirSessao(sessao: unknown): string {
|
|
32
|
+
if (!sessaoValida(sessao)) throw new Error('ork-sentinela: id de sessao invalido')
|
|
33
|
+
return sessao
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** O status do núcleo: a presença desta máquina e o passo do condutor em cada thread aberta. */
|
|
37
|
+
export function argvDoStatus(): string[] {
|
|
38
|
+
return ['ork', 'conducao', 'sentinela', 'status', '--json']
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A batida da presença desta sessão. */
|
|
42
|
+
export function argvDaBatida(sessao: string): string[] {
|
|
43
|
+
return ['ork', 'conducao', 'sentinela', 'bater', '--sessao', exigirSessao(sessao), '--json']
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Tira a presença desta sessão (pausa e fim da sessão). */
|
|
47
|
+
export function argvDoDesligar(sessao: string): string[] {
|
|
48
|
+
return ['ork', 'conducao', 'sentinela', 'desligar', '--sessao', exigirSessao(sessao), '--json']
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Saida = { exitCode: number; stdout: string; stderr: string }
|
|
52
|
+
|
|
53
|
+
function ultimaLinha(texto: unknown): string {
|
|
54
|
+
return String(texto ?? '').trim().split('\n').filter(l => l.trim() !== '').slice(-1)[0]?.trim().slice(0, 200) ?? ''
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function texto(v: unknown): string | null {
|
|
58
|
+
return typeof v === 'string' ? v : null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Um passo do contrato, ou `null` quando ele não tem a forma: passo torto nunca vira despacho. */
|
|
62
|
+
export function lerPasso(bruto: unknown): SentinelaPasso | null {
|
|
63
|
+
const p = bruto as Record<string, unknown> | null
|
|
64
|
+
if (!p || typeof p !== 'object' || !idDeThreadValido(p.thread) || typeof p.acao !== 'string' || !ACOES.includes(p.acao)) return null
|
|
65
|
+
const fase = texto(p.fase)
|
|
66
|
+
if (p.acao === 'despachar' && (fase === null || !FASES.includes(fase))) return null
|
|
67
|
+
const b = p.bloqueio as { motivo?: unknown; em?: unknown } | null | undefined
|
|
68
|
+
return {
|
|
69
|
+
thread: p.thread, modo: texto(p.modo) ?? '', faseAtual: texto(p.faseAtual) ?? '', conduzida: p.conduzida === true,
|
|
70
|
+
acao: p.acao as SentinelaPasso['acao'], fase: fase && FASES.includes(fase) ? fase : null,
|
|
71
|
+
caso: texto(p.caso), proximoPasso: texto(p.proximoPasso), porque: texto(p.porque) ?? '', desdeEm: texto(p.desdeEm),
|
|
72
|
+
bloqueio: b && typeof b.motivo === 'string' && typeof b.em === 'string' ? { motivo: b.motivo, em: b.em } : null,
|
|
73
|
+
pedidoGuardado: p.pedidoGuardado === true,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A leitura do `status --json`: recusa o que não for o contrato; a presença diz de quem é, frente a esta sessão. */
|
|
78
|
+
export function lerStatus(r: Saida, sessao: string | null): { presenca: SentinelaPresenca; passos: SentinelaPasso[] } {
|
|
79
|
+
if (r.exitCode !== 0) throw new Error(`o status do nucleo saiu ${r.exitCode}${ultimaLinha(r.stderr) ? `: ${ultimaLinha(r.stderr)}` : ''}`)
|
|
80
|
+
let s: { contrato?: unknown; presenca?: { ativa?: unknown; sessao?: unknown } | null; threads?: unknown }
|
|
81
|
+
try { s = JSON.parse(r.stdout) } catch { throw new Error('o status do nucleo nao e JSON') }
|
|
82
|
+
if (!s || s.contrato !== CONTRATO || !Array.isArray(s.threads)) throw new Error(`o status do nucleo nao e o contrato ${CONTRATO}`)
|
|
83
|
+
const p = s.presenca
|
|
84
|
+
const presenca: SentinelaPresenca = !p || p.ativa !== true ? 'nenhuma' : p.sessao === sessao ? 'minha' : 'outra'
|
|
85
|
+
const passos = s.threads.map(lerPasso).filter((x): x is SentinelaPasso => x !== null)
|
|
86
|
+
return { presenca, passos }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A batida: `minha` com saída 0 e `ok`; `outra` com saída 3 (outra sessão viva nesta máquina); o resto é erro. */
|
|
90
|
+
export function lerBatida(r: Saida): SentinelaPresenca {
|
|
91
|
+
let ok: unknown
|
|
92
|
+
try { ok = (JSON.parse(r.stdout) as { ok?: unknown }).ok } catch { ok = undefined }
|
|
93
|
+
if (r.exitCode === 0 && ok === true) return 'minha'
|
|
94
|
+
if (r.exitCode === 3 && ok === false) return 'outra'
|
|
95
|
+
throw new Error(`a batida da presenca saiu ${r.exitCode}${ultimaLinha(r.stderr) ? `: ${ultimaLinha(r.stderr)}` : ''}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A thread de um ledger que mudou (`.../.orkastery/threads/<id>/ledger.jsonl`), ou `null`. */
|
|
99
|
+
export function threadDoLedger(caminho: unknown): string | null {
|
|
100
|
+
const m = typeof caminho === 'string' ? LEDGER.exec(caminho) : null
|
|
101
|
+
return m ? (m[1] as string) : null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Acrescenta no fim e corta os mais velhos acima do teto. */
|
|
105
|
+
export function acrescentar<T>(lista: readonly T[] | undefined, item: T, teto: number): T[] {
|
|
106
|
+
return [...(lista ?? []), item].slice(-teto)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Guarda uma chave sem repetir, com teto: as mais velhas saem primeiro. */
|
|
110
|
+
export function guardarChave(chaves: readonly string[] | undefined, chave: string, teto = TETO_DE_CHAVES): string[] {
|
|
111
|
+
const resto = (chaves ?? []).filter(c => c !== chave)
|
|
112
|
+
return [...resto, chave].slice(-teto)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function entrada(em: number, tipo: SentinelaEntrada['tipo'], thread: string | null, texto: string): SentinelaEntrada {
|
|
116
|
+
return { em, tipo, thread, texto: texto.replace(/\s+/g, ' ').trim().slice(0, 300) }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function minutosDesde(iso: string | null, agora: number): number | null {
|
|
120
|
+
const t = iso ? Date.parse(iso) : NaN
|
|
121
|
+
return Number.isFinite(t) ? Math.max(0, Math.floor((agora - t) / 60000)) : null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function contar(n: number, um: string, varios: string): string {
|
|
125
|
+
return `${n} ${n === 1 ? um : varios}`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Há quanto tempo, em degraus (mais de 15, 30 ou 45 min, depois horas inteiras): a linha da faixa não muda a cada
|
|
130
|
+
* minuto, e o registro do mostrado não ganha uma linha nova por minuto. Antes de 15 min, nada.
|
|
131
|
+
*/
|
|
132
|
+
export function haQuantoTempo(minutos: number): string {
|
|
133
|
+
if (minutos < 15) return ''
|
|
134
|
+
if (minutos < 60) return `há mais de ${Math.floor(minutos / 15) * 15} min`
|
|
135
|
+
return `há mais de ${Math.floor(minutos / 60)} h`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A linha da faixa: quantas threads andam, quantas estão paradas e há quanto tempo a mais antiga, quantas estão na vez
|
|
140
|
+
* da Sentinela e quantos avisos do pulse esperam o dono. Pausada, ou com outra sessão conduzindo, diz isso. Sem nada a
|
|
141
|
+
* dizer, a linha sai.
|
|
142
|
+
*/
|
|
143
|
+
export function textoDaFaixa(r: SentinelaResumo, pausada: boolean, agora: number, avisos = 0): string | undefined {
|
|
144
|
+
if (pausada) return 'pausada; o condutor externo conduz'
|
|
145
|
+
if (r.presenca === 'outra') return 'outra sessão conduz nesta máquina; esta só olha'
|
|
146
|
+
if (r.lidoEm === null) return r.erro ? `sem leitura do núcleo (${r.erro})` : undefined
|
|
147
|
+
const andando = r.passos.filter(p => p.conduzida).length
|
|
148
|
+
const paradas = r.passos.filter(p => !p.conduzida && p.acao === 'avisar')
|
|
149
|
+
const naVez = r.passos.filter(p => !p.conduzida && (p.acao === 'despachar' || p.acao === 'retomar')).length
|
|
150
|
+
if (andando === 0 && paradas.length === 0 && naVez === 0 && avisos === 0) return r.erro ? `sem leitura do núcleo (${r.erro})` : undefined
|
|
151
|
+
const partes = [contar(andando, 'thread andando', 'threads andando')]
|
|
152
|
+
if (paradas.length > 0) {
|
|
153
|
+
const ha = haQuantoTempo(Math.max(...paradas.map(p => minutosDesde(p.desdeEm, agora) ?? 0)))
|
|
154
|
+
partes.push(`${contar(paradas.length, 'parada', 'paradas')}${ha ? ` ${ha}` : ''}`)
|
|
155
|
+
}
|
|
156
|
+
if (naVez > 0) partes.push(`${naVez} na vez da Sentinela`)
|
|
157
|
+
if (avisos > 0) partes.push(contar(avisos, 'aviso para você', 'avisos para você'))
|
|
158
|
+
return partes.join(', ')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const ROTULO: Record<string, string> = { despachar: 'despachar', retomar: 'retomar', avisar: 'parada', nada: 'nada' }
|
|
162
|
+
|
|
163
|
+
/** O `/sentinela`: o estado, o passo de cada thread e o diário mais recente. */
|
|
164
|
+
export function textoDoDiario(r: SentinelaResumo, diario: readonly SentinelaEntrada[], pausada: boolean, agora: number): string {
|
|
165
|
+
const linhas: string[] = []
|
|
166
|
+
linhas.push(pausada ? 'Sentinela pausada: o condutor externo conduz. Para voltar: /sentinela retomar.'
|
|
167
|
+
: r.presenca === 'outra' ? 'Outra sessão conduz pela Sentinela nesta máquina; esta só olha.'
|
|
168
|
+
: r.presenca === 'minha' ? 'Sentinela conduzindo nesta máquina: o condutor externo cede o despacho e o retry.'
|
|
169
|
+
: 'Sentinela ligada, sem a presença confirmada pelo núcleo ainda.')
|
|
170
|
+
if (r.erro) linhas.push(`Última leitura do núcleo falhou: ${r.erro}`)
|
|
171
|
+
if (r.passos.length > 0) {
|
|
172
|
+
linhas.push('', 'Threads abertas:')
|
|
173
|
+
for (const p of r.passos) {
|
|
174
|
+
const acao = p.conduzida ? 'andando' : p.acao === 'despachar' ? `despachar ${p.fase}` : ROTULO[p.acao] ?? p.acao
|
|
175
|
+
const desde = !p.conduzida && p.acao === 'avisar' ? minutosDesde(p.desdeEm, agora) : null
|
|
176
|
+
linhas.push(` ${p.thread}: ${acao}${desde ? ` há ${desde} min` : ''}${p.proximoPasso && p.acao === 'avisar' ? `; ${p.proximoPasso}` : ''}`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const recentes = diario.slice(-10)
|
|
180
|
+
linhas.push('', recentes.length ? 'Diário (mais recente por último):' : 'Diário vazio: nada despachado nesta sessão.')
|
|
181
|
+
for (const e of recentes) {
|
|
182
|
+
const ha = Math.max(0, Math.floor((agora - e.em) / 60000))
|
|
183
|
+
linhas.push(` ${ha === 0 ? 'agora' : `há ${ha} min`} ${e.tipo}${e.thread ? ` ${e.thread}` : ''}: ${e.texto}`)
|
|
184
|
+
}
|
|
185
|
+
return linhas.join('\n')
|
|
186
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Contrato do estado da Sentinela do Condutor (`ork-sentinela`, RM-059, fatia F6). O motor exige que cada chave de
|
|
2
|
+
// `$.state` do mod esteja declarada aqui, em `PluginState`. A Sentinela não acrescenta noun ao `$`: lê o núcleo pelo
|
|
3
|
+
// `$.ork` da `ork-ponte` (que o plugin.json lista em `dependencies`) e pelo `ork`, e despacha só pelo `ork phase run`.
|
|
4
|
+
// Sem import nem referência.
|
|
5
|
+
|
|
6
|
+
/** O que o núcleo diz que o condutor faz numa thread (contrato `ork.conducao-sentinela/v1`). */
|
|
7
|
+
export type SentinelaAcao = 'despachar' | 'retomar' | 'avisar' | 'nada'
|
|
8
|
+
|
|
9
|
+
/** O passo do condutor numa thread aberta, como o `ork conducao sentinela status --json` o devolve. */
|
|
10
|
+
export type SentinelaPasso = {
|
|
11
|
+
thread: string
|
|
12
|
+
modo: string
|
|
13
|
+
faseAtual: string
|
|
14
|
+
conduzida: boolean
|
|
15
|
+
acao: SentinelaAcao
|
|
16
|
+
/** A fase do despacho, só com `acao: despachar`. */
|
|
17
|
+
fase: string | null
|
|
18
|
+
caso: string | null
|
|
19
|
+
proximoPasso: string | null
|
|
20
|
+
porque: string
|
|
21
|
+
/** O fim do turno (ou o bloqueio): a chave do despacho. */
|
|
22
|
+
desdeEm: string | null
|
|
23
|
+
bloqueio: { motivo: string; em: string } | null
|
|
24
|
+
pedidoGuardado: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** De quem é a presença da Sentinela nesta máquina, pela última batida. */
|
|
28
|
+
export type SentinelaPresenca = 'minha' | 'outra' | 'nenhuma' | 'desconhecida'
|
|
29
|
+
|
|
30
|
+
/** A última leitura do status do núcleo, para a faixa e o `/sentinela`. */
|
|
31
|
+
export type SentinelaResumo = {
|
|
32
|
+
/** Quando a leitura terminou, em ms desde a época; null antes da primeira. */
|
|
33
|
+
lidoEm: number | null
|
|
34
|
+
presenca: SentinelaPresenca
|
|
35
|
+
passos: SentinelaPasso[]
|
|
36
|
+
/** Por que a última leitura falhou, quando falhou. */
|
|
37
|
+
erro: string | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Uma linha do diário do `/sentinela`. */
|
|
41
|
+
export type SentinelaEntrada = {
|
|
42
|
+
em: number
|
|
43
|
+
tipo: 'despacho' | 'cedeu' | 'retomada' | 'aviso' | 'erro' | 'pausa'
|
|
44
|
+
thread: string | null
|
|
45
|
+
texto: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
declare module 'claude-code' {
|
|
49
|
+
interface PluginState {
|
|
50
|
+
'ork-sentinela': {
|
|
51
|
+
/** O diário do que a Sentinela fez, do mais velho ao mais novo, com teto. */
|
|
52
|
+
diario: SentinelaEntrada[]
|
|
53
|
+
/** O dono pausou a Sentinela pelo `/sentinela pausar`: nada é despachado e a presença sai. */
|
|
54
|
+
pausada: boolean
|
|
55
|
+
/** As chaves `thread|fase|fim do turno` já despachadas: o mesmo turno nunca sai duas vezes. */
|
|
56
|
+
despachados: string[]
|
|
57
|
+
/** As chaves `thread|bloqueio` já levadas ao `ork retry plan`. */
|
|
58
|
+
retomados: string[]
|
|
59
|
+
/** As chaves dos avisos já mostrados, com o texto: o mesmo aviso não se repete. */
|
|
60
|
+
avisados: string[]
|
|
61
|
+
/** A última leitura do status do núcleo. */
|
|
62
|
+
resumo: SentinelaResumo
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ork-torre",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Torre de Controle do Orkastery no Claude Code: a fábrica num painel, cada thread aberta numa linha com a fase, quem conduz, o bloqueio e a ação que o estado dela pede. Só lê; ação com efeito vai ao mod dono dela ou aparece como o comando do ork.",
|
|
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,60 @@
|
|
|
1
|
+
// Quando a Torre rele o nucleo (RM-059, Torre de Controle; D7 da thread). O ledger de uma thread ativa muda
|
|
2
|
+
// a cada batida do sensor, e cada retrato custa um passeio pelo servidor MCP do dono: a Torre junta os
|
|
3
|
+
// eventos de arquivo num lote so, rele cada thread no maximo a cada 10 s e confere a lista das abertas a
|
|
4
|
+
// cada minuto. Modulo puro: so dados entram e saem; os relogios ficam no register.tsx.
|
|
5
|
+
|
|
6
|
+
/** Espera entre o primeiro evento de arquivo e a leitura do lote. */
|
|
7
|
+
export const ESPERA_DO_LOTE_MS = 2000
|
|
8
|
+
/** Intervalo minimo entre duas leituras da mesma thread. */
|
|
9
|
+
export const INTERVALO_POR_THREAD_MS = 10000
|
|
10
|
+
/** Periodo do tique: confere a lista das abertas e o que ficou velho. */
|
|
11
|
+
export const PERIODO_DO_TIQUE_MS = 60000
|
|
12
|
+
/** Linha lida ha mais que isto volta a ser lida no tique, mesmo sem evento de arquivo. */
|
|
13
|
+
export const IDADE_MAXIMA_MS = 300000
|
|
14
|
+
/**
|
|
15
|
+
* Com o painel escondido so a faixa le o quadro: o intervalo por thread e a idade maxima se espacam, porque
|
|
16
|
+
* cada retrato roda a observacao nativa no servidor do dono (D16).
|
|
17
|
+
*/
|
|
18
|
+
export const INTERVALO_ESCONDIDO_MS = 60000
|
|
19
|
+
export const IDADE_MAXIMA_ESCONDIDO_MS = 900000
|
|
20
|
+
|
|
21
|
+
const ID_DE_THREAD = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/
|
|
22
|
+
|
|
23
|
+
export type MarcaDoArquivo = { tipo: 'thread'; thread: string } | { tipo: 'lista' }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* O que um evento de arquivo marca: o ledger de uma thread marca a thread; a pasta das threads marca a
|
|
27
|
+
* lista das abertas (thread nova ou fechada). Outro caminho nao marca nada.
|
|
28
|
+
*/
|
|
29
|
+
export function marcaDoArquivo(caminho: unknown): MarcaDoArquivo | null {
|
|
30
|
+
if (typeof caminho !== 'string') return null
|
|
31
|
+
const c = caminho.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
32
|
+
const m = /(?:^|\/)\.orkastery\/threads\/([^/]+)\/ledger\.jsonl$/.exec(c)
|
|
33
|
+
if (m) return ID_DE_THREAD.test(m[1] as string) ? { tipo: 'thread', thread: m[1] as string } : null
|
|
34
|
+
return /(?:^|\/)\.orkastery\/threads$/.test(c) ? { tipo: 'lista' } : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Das threads marcadas, as que podem ser lidas agora (fora do intervalo minimo) e as que ficam para depois,
|
|
39
|
+
* com o instante mais cedo em que a primeira adiada pode ser lida.
|
|
40
|
+
*/
|
|
41
|
+
export function escolher(marcadas: Iterable<string>, lidaEm: Readonly<Record<string, number>>, agora: number,
|
|
42
|
+
intervalo = INTERVALO_POR_THREAD_MS): { ler: string[]; adiadas: string[]; proximaEm: number | null } {
|
|
43
|
+
const ler: string[] = []
|
|
44
|
+
const adiadas: string[] = []
|
|
45
|
+
let proximaEm: number | null = null
|
|
46
|
+
for (const t of [...new Set(marcadas)].sort()) {
|
|
47
|
+
const ultima = lidaEm[t]
|
|
48
|
+
if (typeof ultima !== 'number' || agora - ultima >= intervalo) { ler.push(t); continue }
|
|
49
|
+
adiadas.push(t)
|
|
50
|
+
const quando = ultima + intervalo
|
|
51
|
+
if (proximaEm === null || quando < proximaEm) proximaEm = quando
|
|
52
|
+
}
|
|
53
|
+
return { ler, adiadas, proximaEm }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** No tique: as threads abertas que nunca foram lidas ou foram lidas ha mais que a idade maxima. */
|
|
57
|
+
export function vencidas(abertas: readonly string[], lidaEm: Readonly<Record<string, number>>, agora: number,
|
|
58
|
+
idade = IDADE_MAXIMA_MS): string[] {
|
|
59
|
+
return abertas.filter(t => typeof lidaEm[t] !== 'number' || agora - (lidaEm[t] as number) > idade)
|
|
60
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// O detalhe de uma thread na Torre de Controle (RM-059, fatia F1, T1.2; D4 da thread). Apertar a linha abre o
|
|
2
|
+
// detalhe: sessoes, bloqueio, pedidos, entrega e MASTER, os ultimos eventos e as claims com estado. Cada acao
|
|
3
|
+
// que o retrato declara disponivel vira um botao de leitura, que mostra o resultado aqui, ou, se tem efeito,
|
|
4
|
+
// o atalho para o mod dono dela (que aplica as proprias regras) ou o comando do `ork` em texto, para o dono
|
|
5
|
+
// rodar. Nenhuma acao com efeito roda pela Torre. Modulo puro: o `$` fica no register.tsx.
|
|
6
|
+
import type { TorreDetalhe, TorreLeitura, TorreLinha } from '../types'
|
|
7
|
+
import type { ItemDoPainel } from './painel'
|
|
8
|
+
import { acoesRelevantes, cabecalhoDaLinha, emPortugues, idade, LEITURAS, ROTULO_DA_ACAO } from './painel'
|
|
9
|
+
|
|
10
|
+
export type Dono = { plugin: string; comando: string; nome: string }
|
|
11
|
+
export type ClasseDaAcao =
|
|
12
|
+
| { tipo: 'leitura' }
|
|
13
|
+
| { tipo: 'efeito'; dono: Dono | null; comando: ((thread: string) => string) | null; nota: string | null }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* D4: a classe de cada operacao do retrato (o enum `operation` do nucleo), o mod dono da acao com efeito
|
|
17
|
+
* (o nome do comando sai das fichas do plano dos mods) e o comando do `ork` que o dono roda sem ele.
|
|
18
|
+
*/
|
|
19
|
+
export const CLASSE_DA_ACAO: Readonly<Record<string, ClasseDaAcao>> = {
|
|
20
|
+
'thread.status': { tipo: 'leitura' },
|
|
21
|
+
'thread.observe': { tipo: 'leitura' },
|
|
22
|
+
'git.status': { tipo: 'leitura' },
|
|
23
|
+
'hitl.decide': { tipo: 'efeito', dono: { plugin: 'ork-mesa', comando: 'mesa', nome: 'a Mesa do Dono' },
|
|
24
|
+
comando: () => 'ork sessions hitl', nota: 'a resposta vai pelo Telegram ou pelo diálogo nativo' },
|
|
25
|
+
'ship': { tipo: 'efeito', dono: { plugin: 'ork-pista', comando: 'pista', nome: 'a Pista de Entrega' },
|
|
26
|
+
comando: t => `ork ship ${t} --para main`, nota: 'troque main pela branch base do projeto, se for outra' },
|
|
27
|
+
'phase': { tipo: 'efeito', dono: { plugin: 'ork-sentinela', comando: 'sentinela', nome: 'a Sentinela do Condutor' },
|
|
28
|
+
comando: t => `ork conducao status ${t}`, nota: 'a fase seguinte sai pelo condutor' },
|
|
29
|
+
'retry': { tipo: 'efeito', dono: { plugin: 'ork-sentinela', comando: 'sentinela', nome: 'a Sentinela do Condutor' },
|
|
30
|
+
comando: t => `ork retry plan ${t}`, nota: null },
|
|
31
|
+
'master': { tipo: 'efeito', dono: { plugin: 'ork-ritual', comando: 'nota', nome: 'o Ritual do MASTER' },
|
|
32
|
+
comando: t => `ork master pedir ${t}`, nota: null },
|
|
33
|
+
'verify': { tipo: 'efeito', dono: null, comando: t => `ork verify ${t}`, nota: null },
|
|
34
|
+
'git.commit': { tipo: 'efeito', dono: null, comando: null, nota: 'o commit é da sessão de GO, pelo ork_git_commit' },
|
|
35
|
+
'creation': { tipo: 'efeito', dono: null, comando: null, nota: 'não é ação de uma thread' },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Os comandos dos mods donos, um por mod: o que a Torre procura na lista de comandos da sessao. */
|
|
39
|
+
export const COMANDOS_DOS_DONOS: readonly Dono[] = Object.values(CLASSE_DA_ACAO)
|
|
40
|
+
.flatMap(c => (c.tipo === 'efeito' && c.dono ? [c.dono] : []))
|
|
41
|
+
.filter((d, i, todos) => todos.findIndex(x => x.comando === d.comando) === i)
|
|
42
|
+
|
|
43
|
+
/** Dos comandos da sessao (`$.command.list()`), os dos mods donos, com o plugin que registrou cada um. */
|
|
44
|
+
export function donosCarregados(comandos: ReadonlyArray<{ name?: unknown; plugin?: unknown }>): Record<string, string> {
|
|
45
|
+
const saida: Record<string, string> = {}
|
|
46
|
+
for (const d of COMANDOS_DOS_DONOS) {
|
|
47
|
+
const achado = comandos.find(c => c.name === d.comando && typeof c.plugin === 'string')
|
|
48
|
+
if (achado) saida[d.comando] = achado.plugin as string
|
|
49
|
+
}
|
|
50
|
+
return saida
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type AcaoNoDetalhe =
|
|
54
|
+
| { operacao: string; rotulo: string; tipo: 'leitura' }
|
|
55
|
+
| { operacao: string; rotulo: string; tipo: 'mod'; dono: Dono }
|
|
56
|
+
| { operacao: string; rotulo: string; tipo: 'texto'; texto: string }
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Como cada acao que a Torre oferece aparece no detalhe (D4 e D15): leitura vira botao; efeito pedido pelo
|
|
60
|
+
* estado abre o mod dono so quando o comando dele esta carregado pelo plugin esperado; senao, o comando do
|
|
61
|
+
* `ork` (ou a nota) aparece em texto.
|
|
62
|
+
*/
|
|
63
|
+
export function acoesDoDetalhe(linha: TorreLinha, donos: Readonly<Record<string, string>>): AcaoNoDetalhe[] {
|
|
64
|
+
const saida: AcaoNoDetalhe[] = []
|
|
65
|
+
for (const { operacao } of acoesRelevantes(linha)) {
|
|
66
|
+
const classe = Object.prototype.hasOwnProperty.call(CLASSE_DA_ACAO, operacao) ? CLASSE_DA_ACAO[operacao] : undefined
|
|
67
|
+
if (!classe) continue
|
|
68
|
+
const rotulo = ROTULO_DA_ACAO[operacao] ?? operacao
|
|
69
|
+
if (classe.tipo === 'leitura') { saida.push({ operacao, rotulo, tipo: 'leitura' }); continue }
|
|
70
|
+
if (classe.dono && donos[classe.dono.comando] === classe.dono.plugin) { saida.push({ operacao, rotulo, tipo: 'mod', dono: classe.dono }); continue }
|
|
71
|
+
const comando = classe.comando ? classe.comando(linha.thread) : null
|
|
72
|
+
const texto = [comando ? `rode ${comando}` : null, classe.nota].filter((p): p is string => !!p).join('; ')
|
|
73
|
+
saida.push({ operacao, rotulo, tipo: 'texto', texto: `${rotulo}: ${texto}` })
|
|
74
|
+
}
|
|
75
|
+
return saida
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** O JSON do primeiro bloco de texto de uma resposta MCP; resposta com erro vira erro. */
|
|
79
|
+
export function jsonDoMcp(r: { content?: ReadonlyArray<{ type?: string; text?: string }>; isError?: boolean } | null | undefined): unknown {
|
|
80
|
+
if (!r || r.isError) throw new Error('o núcleo respondeu com erro')
|
|
81
|
+
const texto = (r.content ?? []).find(c => c.type === 'text' && typeof c.text === 'string')?.text
|
|
82
|
+
if (texto === undefined) throw new Error('o núcleo não devolveu texto')
|
|
83
|
+
return JSON.parse(texto)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const curto = (v: unknown, n = 8) => (typeof v === 'string' ? v.slice(0, n) : '?')
|
|
87
|
+
const cortar = (v: unknown, n: number) => {
|
|
88
|
+
const t = typeof v === 'string' ? v.replace(/\s+/g, ' ').trim() : ''
|
|
89
|
+
return t.length <= n ? t : `${t.slice(0, n - 1)}…`
|
|
90
|
+
}
|
|
91
|
+
function haQuanto(ts: unknown, agora: number): string {
|
|
92
|
+
const ms = typeof ts === 'string' ? Date.parse(ts) : NaN
|
|
93
|
+
return Number.isFinite(ms) ? `há ${idade(agora - ms)}` : 'sem hora'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Os ultimos eventos (`ork_phase_list`), do mais novo ao mais velho. */
|
|
97
|
+
export function linhasDosEventos(resultado: unknown, agora: number, teto = 6): string[] {
|
|
98
|
+
const eventos = (resultado as { phases?: { eventos?: unknown } } | null)?.phases?.eventos
|
|
99
|
+
if (!Array.isArray(eventos)) throw new Error('a lista de eventos veio sem eventos')
|
|
100
|
+
if (eventos.length === 0) return ['nenhum evento de fase registrado']
|
|
101
|
+
return eventos.slice(-teto).reverse().map(e => {
|
|
102
|
+
const x = (e ?? {}) as Record<string, unknown>
|
|
103
|
+
return [String(x.tipo ?? '?'), typeof x.fase === 'string' ? x.fase : null, typeof x.classificacao === 'string' ? x.classificacao : null,
|
|
104
|
+
typeof x.motivo === 'string' ? x.motivo : null, haQuanto(x.ts, agora)].filter(p => p !== null).join(' · ')
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** As claims com estado (`ork_claims_list`): a conta por estado e as que ainda nao foram verificadas. */
|
|
109
|
+
export function linhasDasClaims(resultado: unknown, teto = 5): string[] {
|
|
110
|
+
if (!Array.isArray(resultado)) throw new Error('a lista de claims veio em outro formato')
|
|
111
|
+
if (resultado.length === 0) return ['nenhuma claim registrada']
|
|
112
|
+
const conta = new Map<string, number>()
|
|
113
|
+
for (const c of resultado) conta.set(String(c?.estado ?? '?'), (conta.get(String(c?.estado ?? '?')) ?? 0) + 1)
|
|
114
|
+
const resumo = [...conta.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).map(([e, n]) => `${n} ${e}`).join(', ')
|
|
115
|
+
const abertas = resultado.filter(c => c?.estado !== 'verificado' && c?.estado !== 'retirada').slice(-teto)
|
|
116
|
+
return [`${resultado.length} claims: ${resumo}`, ...abertas.map(c => `${String(c?.id ?? '?')} ${String(c?.estado ?? '?')}: ${cortar(c?.alegacao, 90)}`)]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** O estado da thread (`ork thread status --json`, pela ponte). */
|
|
120
|
+
export function linhasDoEstado(resultado: unknown, agora: number): string[] {
|
|
121
|
+
const t = (resultado ?? {}) as Record<string, unknown>
|
|
122
|
+
if (typeof t.id !== 'string') throw new Error('o estado da thread veio sem id')
|
|
123
|
+
const sessoes = Array.isArray(t.sessoes) ? t.sessoes.length : 0
|
|
124
|
+
const claims = Array.isArray(t.claims) ? t.claims.length : 0
|
|
125
|
+
const saida = [`status ${String(t.status ?? '?')} · fase ${String(t.faseAtual ?? '?')} · ${sessoes} sessões · ${claims} claims · atualizada ${haQuanto(t.atualizadaEm, agora)}`]
|
|
126
|
+
const b = t.baseline as { commit?: unknown; comandos?: Array<{ nome?: unknown; ok?: unknown }> } | null | undefined
|
|
127
|
+
if (b && Array.isArray(b.comandos)) {
|
|
128
|
+
saida.push(`baseline em ${curto(b.commit)}: ${b.comandos.map(c => `${String(c.nome ?? '?')} ${c.ok === true ? 'passava' : 'falhava'}`).join(', ')}`)
|
|
129
|
+
} else saida.push('sem baseline gravada')
|
|
130
|
+
return saida
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A observacao da thread (`ork_observe`): os pedidos pendentes e cada sessao nativa. */
|
|
134
|
+
export function linhasDaObservacao(resultado: unknown): string[] {
|
|
135
|
+
const o = (resultado ?? {}) as { pendencias?: unknown; nativas?: unknown }
|
|
136
|
+
if (!Array.isArray(o.nativas)) throw new Error('a observação veio sem as sessões nativas')
|
|
137
|
+
const pendencias = Array.isArray(o.pendencias) ? o.pendencias.length : 0
|
|
138
|
+
const saida = [pendencias === 0 ? 'nenhum pedido pendente' : pendencias === 1 ? '1 pedido pendente' : `${pendencias} pedidos pendentes`]
|
|
139
|
+
for (const n of o.nativas as Array<Record<string, unknown>>) {
|
|
140
|
+
const sessao = (n.sessao ?? {}) as Record<string, unknown>
|
|
141
|
+
const estado = n.disponivel === true ? String(sessao.estado ?? sessao.state ?? sessao.status ?? 'confirmada') : 'não confirmada'
|
|
142
|
+
const turno = (n.turno ?? {}) as Record<string, unknown>
|
|
143
|
+
saida.push(`sessão ${curto(n.sessionId)} ${String(n.runtime ?? '?')}: ${estado}${typeof turno.conclusaoDeFase === 'string' ? ` · ${turno.conclusaoDeFase}` : ''}`)
|
|
144
|
+
}
|
|
145
|
+
return saida
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** O estado do git da thread (`ork_git_status`): a branch da thread e a de destino, com os HEADs. */
|
|
149
|
+
export function linhasDoGit(resultado: unknown): string[] {
|
|
150
|
+
const g = (resultado ?? {}) as { source?: { branch?: unknown; head?: unknown }; destination?: { branch?: unknown; head?: unknown } }
|
|
151
|
+
if (!g.source || !g.destination) throw new Error('o estado do git veio sem origem e destino')
|
|
152
|
+
return [`${String(g.source.branch ?? '?')} @ ${curto(g.source.head)}`,
|
|
153
|
+
`destino ${String(g.destination.branch ?? '?')} @ ${curto(g.destination.head)}${g.source.head === g.destination.head ? ' (iguais)' : ''}`]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function leituraFeita(linhas: string[], agora: number): TorreLeitura {
|
|
157
|
+
return { lidoEm: agora, linhas, erro: null }
|
|
158
|
+
}
|
|
159
|
+
export function leituraFalha(erro: string, agora: number): TorreLeitura {
|
|
160
|
+
return { lidoEm: agora, linhas: [], erro }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** As linhas de uma secao lida: lendo, o erro, ou o que veio. */
|
|
164
|
+
function linhasDaLeitura(l: TorreLeitura | null | undefined, agora: number): string[] {
|
|
165
|
+
if (!l) return [' lendo…']
|
|
166
|
+
if (l.erro) return [` não leu: ${l.erro}`]
|
|
167
|
+
return [...l.linhas.map(x => ` ${x}`), ` (lido há ${idade(agora - l.lidoEm)})`]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* O texto do detalhe, linha a linha, na ordem em que aparece, com o que cada linha mostra ao dono (D10).
|
|
172
|
+
* Thread sem leitura so diz que nao leu e por que: nada de quem conduz nem de acao (D6).
|
|
173
|
+
*/
|
|
174
|
+
export function linhasDoDetalhe(linha: TorreLinha, detalhe: TorreDetalhe | undefined, agora: number): ItemDoPainel[] {
|
|
175
|
+
const saida: ItemDoPainel[] = [{ texto: cabecalhoDaLinha(linha) }]
|
|
176
|
+
if (linha.estado === 'sem-leitura') {
|
|
177
|
+
saida.push({ texto: `não li o retrato: ${linha.lacunas.join('; ')}` })
|
|
178
|
+
} else {
|
|
179
|
+
saida.push({ texto: linha.conducao ?? 'ninguém conduz agora' })
|
|
180
|
+
if (linha.bloqueio) {
|
|
181
|
+
saida.push({ texto: `bloqueio: ${linha.bloqueio.frase} (${linha.bloqueio.motivo})`,
|
|
182
|
+
mostrado: { chave: `bloqueio:${linha.thread}:${linha.bloqueio.motivo}`, thread: linha.thread, texto: `${linha.thread}: ${linha.bloqueio.frase}` } })
|
|
183
|
+
}
|
|
184
|
+
for (const p of linha.pedidos) {
|
|
185
|
+
saida.push({ texto: `pedido ${p.id}: ${p.texto}`,
|
|
186
|
+
mostrado: { chave: `hitl:${p.id}`, thread: linha.thread, texto: `${linha.thread}: espera você: ${p.texto || p.id}` } })
|
|
187
|
+
const sub = [p.prazo ? `prazo ${p.prazo}` : null, p.recomendacao ? `recomendação: ${p.recomendacao}` : null].filter(Boolean)
|
|
188
|
+
if (sub.length) saida.push({ texto: ` ${sub.join(' · ')}` })
|
|
189
|
+
}
|
|
190
|
+
for (const s of linha.sessoes) {
|
|
191
|
+
saida.push({ texto: `sessão ${curto(s.id)} ${s.rotulo}: ${emPortugues(s.estado)}${s.conduz ? ' · conduz' : ''}${s.confirmada ? '' : ' · não confirmada'}` })
|
|
192
|
+
}
|
|
193
|
+
if (linha.entrega) saida.push({ texto: `entrega: ${emPortugues(linha.entrega)}` })
|
|
194
|
+
if (linha.master) saida.push({ texto: `MASTER: ${emPortugues(linha.master)}` })
|
|
195
|
+
if (linha.lacunas.length) saida.push({ texto: `lacuna: ${linha.lacunas.join('; ')}` })
|
|
196
|
+
}
|
|
197
|
+
for (const t of ['últimos eventos', ...linhasDaLeitura(detalhe?.eventos, agora), 'claims', ...linhasDaLeitura(detalhe?.claims, agora)]) saida.push({ texto: t })
|
|
198
|
+
for (const op of LEITURAS) {
|
|
199
|
+
const l = detalhe?.leituras?.[op]
|
|
200
|
+
if (l) for (const t of [ROTULO_DA_ACAO[op] ?? op, ...linhasDaLeitura(l, agora)]) saida.push({ texto: t })
|
|
201
|
+
}
|
|
202
|
+
return saida
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export type Gestos = {
|
|
206
|
+
voltar?: () => void
|
|
207
|
+
atualizar?: () => void
|
|
208
|
+
ler?: (operacao: string) => void
|
|
209
|
+
abrirMod?: (dono: Dono) => void
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** A arvore do detalhe: os botoes de voltar e atualizar, o texto, e uma linha por acao oferecida. */
|
|
213
|
+
export function desenharDetalhe(el: { Box: any; Text: any; Button: any }, linha: TorreLinha, detalhe: TorreDetalhe | undefined,
|
|
214
|
+
donos: Readonly<Record<string, string>>, opcoes: { agora: number } & Gestos) {
|
|
215
|
+
const { Box, Text, Button } = el
|
|
216
|
+
const acoes = acoesDoDetalhe(linha, donos)
|
|
217
|
+
const lida = linha.estado !== 'sem-leitura'
|
|
218
|
+
return (
|
|
219
|
+
<Box flexDirection="column">
|
|
220
|
+
<Box key="gestos" flexDirection="row">
|
|
221
|
+
<Button key="voltar" label="Voltar" onPress={() => opcoes.voltar?.()} />
|
|
222
|
+
<Button key="atualizar" label="Atualizar" onPress={() => opcoes.atualizar?.()} />
|
|
223
|
+
</Box>
|
|
224
|
+
{linhasDoDetalhe(linha, detalhe, opcoes.agora).map((t, i) => (
|
|
225
|
+
<Text key={`detalhe:${i}`} dimColor={i > 0} wrap="truncate">{t.texto}</Text>
|
|
226
|
+
))}
|
|
227
|
+
{acoes.length > 0 && <Text key="acoes-titulo" bold>ações</Text>}
|
|
228
|
+
{acoes.map(a => (a.tipo === 'leitura'
|
|
229
|
+
? <Button key={`acao:${a.operacao}`} label={a.rotulo} onPress={() => opcoes.ler?.(a.operacao)} />
|
|
230
|
+
: a.tipo === 'mod'
|
|
231
|
+
? <Button key={`acao:${a.operacao}`} label={`${a.rotulo}: abrir ${a.dono.nome}`} onPress={() => opcoes.abrirMod?.(a.dono)} />
|
|
232
|
+
: <Text key={`acao:${a.operacao}`} wrap="truncate">{a.texto}</Text>))}
|
|
233
|
+
{lida && acoes.length === 0 && <Text key="sem-acoes" dimColor>nenhuma ação pedida pelo estado desta thread</Text>}
|
|
234
|
+
</Box>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// A frase curta de cada motivo tipado de gate do nucleo (RM-059, Torre de Controle; D5 da thread). O painel
|
|
2
|
+
// mostra o bloqueio ao dono numa frase so, com acento. O teste do nucleo exige as mesmas chaves de
|
|
3
|
+
// `DESCRICAO_DO_MOTIVO` (core/src/gates.ts): motivo novo no nucleo reprova o teste ate ganhar frase. Modulo
|
|
4
|
+
// puro: so dados.
|
|
5
|
+
|
|
6
|
+
export const FRASE_DO_MOTIVO: Readonly<Record<string, string>> = {
|
|
7
|
+
'artifact.missing': 'falta um artefato que a fase exige',
|
|
8
|
+
'claims.failed': 'uma claim reprovou no HEAD',
|
|
9
|
+
'claims.unverifiable': 'há claim sem comando de verificação',
|
|
10
|
+
'policy.violation': 'uma policy de bloqueio foi violada',
|
|
11
|
+
'runtime.autoconferencia': 'o CHECK caiu no mesmo runtime do GO',
|
|
12
|
+
'verify.regression': 'um teste que passava na baseline caiu',
|
|
13
|
+
'verify.failed': 'a verificação falhou, sem baseline para comparar',
|
|
14
|
+
'verify.timeout': 'a verificação estourou o prazo',
|
|
15
|
+
'verify.sem-veredito': 'a verificação terminou sem veredito',
|
|
16
|
+
'ci.failed': 'o CI independente não está verde',
|
|
17
|
+
'runtime.unavailable': 'o runtime não está disponível',
|
|
18
|
+
'runtime.silencio': 'a sessão parou de dar sinal',
|
|
19
|
+
'runtime.rate-limited': 'bateu o limite de uso da assinatura',
|
|
20
|
+
'runtime.quota-exhausted': 'a cota da conta acabou',
|
|
21
|
+
'runtime.auth-missing': 'a conta do runtime precisa de login',
|
|
22
|
+
'runtime.model-unavailable': 'o modelo pedido não está disponível',
|
|
23
|
+
'runtime.profile-invalid': 'o perfil pedido não existe',
|
|
24
|
+
'runtime.workspace-untrusted': 'o runtime pede que você confie na pasta',
|
|
25
|
+
'runtime.consent-pending': 'o runtime pede que você aceite os termos',
|
|
26
|
+
'cost.violation': 'o despacho iria para um provider pago',
|
|
27
|
+
'tree.blocked': 'a árvore de destino do merge está ocupada',
|
|
28
|
+
'lease.busy': 'outra thread segura a região',
|
|
29
|
+
'lease.resume-unavailable': 'a retomada do lease não está disponível',
|
|
30
|
+
'conducao.em-andamento': 'outra condução já roda nesta thread',
|
|
31
|
+
'hitl.formato': 'o pedido ao dono saiu fora do formato',
|
|
32
|
+
'human.pending': 'espera a sua autorização',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Os motivos que so o dono resolve (D6): a linha fica em "espera voce", nao em "parada". */
|
|
36
|
+
export const MOTIVOS_DO_DONO: readonly string[] = ['human.pending', 'runtime.workspace-untrusted', 'runtime.consent-pending',
|
|
37
|
+
'runtime.auth-missing']
|
|
38
|
+
|
|
39
|
+
/** A frase do motivo; motivo que a tabela nao conhece aparece cru, nunca some. */
|
|
40
|
+
export function fraseDoMotivo(motivo: string): string {
|
|
41
|
+
return Object.prototype.hasOwnProperty.call(FRASE_DO_MOTIVO, motivo) ? FRASE_DO_MOTIVO[motivo] as string : motivo
|
|
42
|
+
}
|