@orkastery/cli 0.5.3 → 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/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/hermes/skills/orkastery-devmaster/SKILL.md +1 -1
- package/adapters/openclaw/README.md +2 -1
- package/adapters/openclaw/openclaw.plugin.json +1 -1
- package/adapters/openclaw/package.json +1 -1
- 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/dist/adapters/claude-bg.js +77 -13
- package/dist/adapters/codex.js +10 -1
- package/dist/agents-md.js +24 -15
- package/dist/auditrun.js +17 -2
- package/dist/board.js +4 -3
- package/dist/branch-de-estado.js +23 -1
- package/dist/catalogo.js +33 -0
- package/dist/ci.js +13 -7
- package/dist/claims-conferir.js +342 -0
- package/dist/claims.js +3 -1
- package/dist/company-brain-client.js +36 -1
- 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 +22 -8
- package/dist/doctor-pulse-cron.js +182 -27
- package/dist/doctor-rede.js +18 -17
- package/dist/doctor.js +115 -111
- package/dist/entrega-pr.js +22 -21
- package/dist/escopo-escrita.js +10 -1
- package/dist/estado-thread.js +23 -1
- package/dist/experiencia-instalacao.js +45 -25
- package/dist/fabrica-batida.js +310 -0
- package/dist/fabrica-estado.js +38 -8
- package/dist/fabrica-publicar.js +3 -1
- package/dist/fila-pesada.js +444 -0
- package/dist/fix.js +3 -2
- package/dist/forja.js +1 -1
- package/dist/gates.js +3 -28
- package/dist/handoff.js +10 -6
- package/dist/hitl-local-receipt.js +2 -0
- package/dist/hitl-resumo.js +29 -3
- package/dist/hitl-tempo-parado.js +4 -4
- package/dist/hitl.js +29 -33
- package/dist/horario.js +14 -7
- package/dist/hosts.js +52 -1
- package/dist/impedimento.js +108 -14
- package/dist/index.js +623 -521
- package/dist/indice.js +7 -1
- package/dist/init.js +43 -47
- package/dist/instabilidade.js +144 -0
- package/dist/integracoes-locais.js +2 -0
- package/dist/intelligence-graph-cli.js +92 -21
- package/dist/intelligence-graph-contexto.js +56 -17
- 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-query.js +66 -19
- package/dist/ledger.js +15 -1
- 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/manifest.js +129 -3
- package/dist/maquina.js +23 -4
- package/dist/master-digest.js +6 -1
- package/dist/master-nota.js +4 -6
- package/dist/master.js +21 -24
- package/dist/mcp-artifacts.js +11 -2
- package/dist/mcp-git.js +23 -6
- package/dist/mcp-grafo.js +7 -4
- package/dist/mcp-server.js +8 -2
- package/dist/mcp-ship.js +3 -1
- package/dist/mcp-verify.js +6 -1
- package/dist/modos.js +3 -6
- package/dist/network-roadmap.js +5 -2
- 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 +29 -2
- package/dist/onboarding.js +40 -21
- package/dist/orkmind.js +9 -2
- package/dist/orquestracao.js +13 -12
- package/dist/parado-no-condutor.js +34 -9
- package/dist/phase.js +94 -39
- package/dist/policies.js +4 -4
- package/dist/portfolio.js +4 -2
- package/dist/postura-local.js +234 -0
- package/dist/preflight.js +11 -10
- package/dist/procedencia.js +225 -0
- package/dist/projeto-alvo.js +127 -4
- package/dist/prompts.js +6 -2
- package/dist/pulse-delivery.js +13 -2
- package/dist/pulse.js +52 -19
- package/dist/ratelimit.js +4 -2
- package/dist/redacao-saida.js +63 -0
- package/dist/rede-status.js +3 -2
- package/dist/retry.js +232 -148
- package/dist/roadmap-reservas.js +20 -3
- package/dist/roadmap-status.js +129 -33
- package/dist/runtime-context.js +181 -3
- package/dist/runtimes.js +2 -1
- package/dist/session-events.js +7 -3
- package/dist/session-watcher-claude.js +7 -1
- package/dist/session-watcher.js +185 -33
- package/dist/sessoes-contas.js +1 -1
- package/dist/ship-publicar.js +800 -0
- package/dist/ship.js +84 -81
- package/dist/steal.js +77 -0
- package/dist/thread-close.js +3 -2
- package/dist/thread.js +153 -82
- 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 +20 -0
- package/dist/yaml.js +27 -8
- package/package.json +1 -1
- package/schemas/code-artifact-graph.v1.schema.json +2 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// O despacho da fase seguinte pela Sentinela (RM-059, fatia F6, T6.2). Módulo puro: quais passos do núcleo despachar
|
|
2
|
+
// ou retomar, uma vez por turno, o argv do render, do despacho e da retomada, e a leitura de cada resultado. A
|
|
3
|
+
// Sentinela só despacha pelo `ork phase run`, que toma a condução da thread sob a trava do kernel e é idempotente; a
|
|
4
|
+
// fase vem do núcleo (`ork conducao sentinela status`), nunca de uma regra daqui, e ela nunca assume a condução.
|
|
5
|
+
import type { SentinelaPasso } from '../types'
|
|
6
|
+
|
|
7
|
+
export const PRAZO_DO_RENDER_MS = 30000
|
|
8
|
+
/** O mesmo teto que o condutor externo dá ao `ork phase run`, com folga. */
|
|
9
|
+
export const PRAZO_DO_DESPACHO_MS = 200000
|
|
10
|
+
export const PRAZO_DA_RETOMADA_MS = 200000
|
|
11
|
+
/**
|
|
12
|
+
* A recusa que passa (outro condutor, projeto sem vaga, pedido aberto, falha) não queima o turno: a chave volta e o
|
|
13
|
+
* passo é tentado de novo depois disto. A batida e o adiamento são da Sentinela; quem diz se sai é sempre o núcleo.
|
|
14
|
+
*/
|
|
15
|
+
export const ADIAMENTO_MS = 3 * 60 * 1000
|
|
16
|
+
/** A mesma recusa de novo dobra o adiamento, até este teto: falha que não passa não vira despacho a cada 3 min. */
|
|
17
|
+
export const TETO_DO_ADIAMENTO_MS = 30 * 60 * 1000
|
|
18
|
+
|
|
19
|
+
/** O adiamento da n-ésima recusa seguida do mesmo turno: 3, 6, 12 e 24 min, depois 30. */
|
|
20
|
+
export function adiamentoDa(vez: number): number {
|
|
21
|
+
return Math.min(TETO_DO_ADIAMENTO_MS, ADIAMENTO_MS * 2 ** Math.max(0, Math.floor(vez) - 1))
|
|
22
|
+
}
|
|
23
|
+
/** A linha que o `ork prompt render` imprime quando usa o pedido guardado na criação da thread (T6.0). */
|
|
24
|
+
export const MARCA_DO_PEDIDO = '# pedido guardado na criacao da thread'
|
|
25
|
+
/** O teto do prompt que o despacho aceita (o mesmo do `ork_phase_run`). */
|
|
26
|
+
export const TETO_DO_PROMPT = 65536
|
|
27
|
+
export const CANAL = 'claude-code'
|
|
28
|
+
/** O `ork retry run` não lê `--canal`: o canal da retomada vai pelo ambiente, como o núcleo o declara (D3). */
|
|
29
|
+
export const AMBIENTE_DA_RETOMADA: Readonly<Record<string, string>> = { ORK_CANAL: CANAL }
|
|
30
|
+
const FASES: readonly string[] = ['GOAL', 'PLAN', 'GO', 'CHECK', 'SHIP', 'MASTER']
|
|
31
|
+
const ID_DE_THREAD = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/
|
|
32
|
+
const SESSAO = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/
|
|
33
|
+
/** Os estados de pedido HITL que ainda esperam o dono no retrato do Maestro. */
|
|
34
|
+
const PEDIDO_ABERTO: readonly string[] = ['aberto', 'delivery.uncertain']
|
|
35
|
+
|
|
36
|
+
type Saida = { exitCode: number; stdout: string; stderr: string }
|
|
37
|
+
|
|
38
|
+
function exigirThread(thread: unknown): string {
|
|
39
|
+
if (typeof thread !== 'string' || !ID_DE_THREAD.test(thread)) throw new Error('ork-sentinela: id de thread invalido')
|
|
40
|
+
return thread
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function exigirFase(fase: unknown): string {
|
|
44
|
+
if (typeof fase !== 'string' || !FASES.includes(fase)) throw new Error('ork-sentinela: fase fora do ciclo')
|
|
45
|
+
return fase
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ultimaLinha(texto: unknown): string {
|
|
49
|
+
return String(texto ?? '').trim().split('\n').filter(l => l.trim() !== '').slice(-1)[0]?.trim().slice(0, 200) ?? ''
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function primeiraLinha(texto: unknown): string {
|
|
53
|
+
return String(texto ?? '').trim().split('\n').find(l => l.trim() !== '')?.trim().slice(0, 200) ?? ''
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A chave de um despacho: a thread, a fase e o fim do turno que o pediu. O mesmo turno nunca sai duas vezes. */
|
|
57
|
+
export function chaveDoDespacho(p: SentinelaPasso): string | null {
|
|
58
|
+
return p.acao === 'despachar' && p.fase && p.desdeEm ? `${p.thread}|${p.fase}|${p.desdeEm}` : null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A chave de uma retomada: a thread e o bloqueio. O mesmo bloqueio vai ao `retry plan` uma vez. */
|
|
62
|
+
export function chaveDaRetomada(p: SentinelaPasso): string | null {
|
|
63
|
+
return p.acao === 'retomar' && p.bloqueio ? `${p.thread}|${p.bloqueio.motivo}|${p.bloqueio.em}` : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Os passos a despachar agora: #Auto, sem condução, com fase do ciclo e turno ainda não despachado. */
|
|
67
|
+
export function paraDespachar(passos: readonly SentinelaPasso[], feitos: readonly string[]): SentinelaPasso[] {
|
|
68
|
+
return passos.filter(p => {
|
|
69
|
+
const chave = chaveDoDespacho(p)
|
|
70
|
+
return chave !== null && !p.conduzida && p.modo === 'auto' && ID_DE_THREAD.test(p.thread) && FASES.includes(p.fase as string)
|
|
71
|
+
&& !feitos.includes(chave)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Os passos a levar ao `retry plan` agora: #Auto, sem condução e bloqueio ainda não consultado. */
|
|
76
|
+
export function paraRetomar(passos: readonly SentinelaPasso[], feitos: readonly string[]): SentinelaPasso[] {
|
|
77
|
+
return passos.filter(p => {
|
|
78
|
+
const chave = chaveDaRetomada(p)
|
|
79
|
+
return chave !== null && !p.conduzida && p.modo === 'auto' && ID_DE_THREAD.test(p.thread) && !feitos.includes(chave)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Ninguém conduz a thread: o `ork conducao status --json` (pela ponte) diz `conducao: null`. */
|
|
84
|
+
export function conducaoLivre(status: unknown): boolean {
|
|
85
|
+
return !!status && typeof status === 'object' && 'conducao' in status && (status as { conducao: unknown }).conducao === null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** O retrato da thread tem pedido ao dono ainda sem resposta (seção `hitl` do `ork.maestro-snapshot/v1`). */
|
|
89
|
+
export function pedidoAbertoNoRetrato(retrato: unknown, thread: string): boolean {
|
|
90
|
+
const itens = (retrato as { sections?: { hitl?: { items?: unknown } } } | null)?.sections?.hitl?.items
|
|
91
|
+
if (!Array.isArray(itens)) return false
|
|
92
|
+
return itens.some(bruto => {
|
|
93
|
+
const i = bruto as { status?: unknown; refs?: unknown; facts?: { target?: unknown } } | null
|
|
94
|
+
if (!i || !PEDIDO_ABERTO.includes(String(i.status)) || i.facts?.target === 'decidido') return false
|
|
95
|
+
return Array.isArray(i.refs) && i.refs.some(r => (r as { kind?: unknown; id?: unknown })?.kind === 'thread' && (r as { id?: unknown }).id === thread)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** O render do prompt da fase com o pedido guardado: sem `--pedido`, o núcleo usa o da criação da thread. */
|
|
100
|
+
export function argvDoRender(thread: string, fase: string): string[] {
|
|
101
|
+
return ['ork', 'prompt', 'render', exigirThread(thread), exigirFase(fase)]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** O prompt do render; sem a linha do pedido guardado ele seria o texto de exemplo, e não serve. */
|
|
105
|
+
export function promptDoRender(r: Saida): { prompt: string } | { motivo: string } {
|
|
106
|
+
if (r.exitCode !== 0) return { motivo: `o render do prompt saiu ${r.exitCode}${ultimaLinha(r.stderr) ? `: ${ultimaLinha(r.stderr)}` : ''}` }
|
|
107
|
+
if (!r.stdout.split('\n').some(l => l.startsWith(MARCA_DO_PEDIDO))) {
|
|
108
|
+
return { motivo: 'a thread não tem o pedido guardado na criação (ork thread new --pedido): o render cairia no texto de exemplo' }
|
|
109
|
+
}
|
|
110
|
+
const prompt = r.stdout.trimEnd()
|
|
111
|
+
if (prompt.length > TETO_DO_PROMPT) return { motivo: `o prompt tem ${prompt.length} caracteres, acima do teto de ${TETO_DO_PROMPT}` }
|
|
112
|
+
return { prompt }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** O despacho pelo núcleo, com o canal da Sentinela e a correlação que a marca no ledger (D3). */
|
|
116
|
+
export function argvDoDespacho(thread: string, fase: string, prompt: string, sessao: string): string[] {
|
|
117
|
+
if (typeof prompt !== 'string' || prompt.trim() === '') throw new Error('ork-sentinela: prompt vazio')
|
|
118
|
+
if (typeof sessao !== 'string' || !SESSAO.test(sessao)) throw new Error('ork-sentinela: id de sessao invalido')
|
|
119
|
+
return ['ork', 'phase', 'run', exigirThread(thread), exigirFase(fase), '--prompt', prompt, '--canal', CANAL, '--correlacao', `sentinela:${sessao}`]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type ResultadoDoDespacho = { resultado: 'despachada' | 'repetida' | 'ocupada' | 'falhou'; detalhe: string }
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* O que o `ork phase run` disse. Saída 3: outro condutor conduz a thread, ou o projeto está sem vaga. Saída 1 com
|
|
126
|
+
* `HITL ocupado`: outro despacho tomou a trava do kernel no mesmo instante. Pedido repetido: a sessão de quem já
|
|
127
|
+
* conduz atende. Nenhum desses abre sessão nova, e nenhum é repetido pela Sentinela.
|
|
128
|
+
*/
|
|
129
|
+
export function lerDespacho(r: Saida): ResultadoDoDespacho {
|
|
130
|
+
if (r.exitCode === 0 && /Pedido ja em andamento|Request already in progress/.test(r.stdout)) {
|
|
131
|
+
return { resultado: 'repetida', detalhe: primeiraLinha(r.stdout) }
|
|
132
|
+
}
|
|
133
|
+
if (r.exitCode === 0) {
|
|
134
|
+
const sessao = /sessionId\s*:\s*([0-9a-f-]{8,})/.exec(r.stdout)?.[1]
|
|
135
|
+
return { resultado: 'despachada', detalhe: sessao ? `sessão ${sessao.slice(0, 8)}` : primeiraLinha(r.stdout) }
|
|
136
|
+
}
|
|
137
|
+
if (r.exitCode === 3) return { resultado: 'ocupada', detalhe: primeiraLinha(r.stdout) || ultimaLinha(r.stderr) }
|
|
138
|
+
if (r.exitCode === 1 && /HITL ocupado/.test(r.stderr)) return { resultado: 'ocupada', detalhe: 'outro despacho tomou a vez no mesmo instante' }
|
|
139
|
+
return { resultado: 'falhou', detalhe: `o despacho saiu ${r.exitCode}${ultimaLinha(r.stderr) ? `: ${ultimaLinha(r.stderr)}` : ''}` }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** O plano do retry declara a retomada automática, sem bloqueio: só ela a Sentinela executa. */
|
|
143
|
+
export function retomadaAutomatica(plano: unknown): boolean {
|
|
144
|
+
const p = plano as { automatica?: unknown; bloqueio?: unknown } | null
|
|
145
|
+
return !!p && p.automatica === true && (p.bloqueio === null || p.bloqueio === undefined)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A retomada pelo núcleo, sem `--por`: no #Auto o retry automático não pede autorização. */
|
|
149
|
+
export function argvDaRetomada(thread: string): string[] {
|
|
150
|
+
return ['ork', 'retry', 'run', exigirThread(thread)]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function lerRetomada(r: Saida): { resultado: 'retomada' | 'recusada'; detalhe: string } {
|
|
154
|
+
return r.exitCode === 0 ? { resultado: 'retomada', detalhe: primeiraLinha(r.stdout) }
|
|
155
|
+
: { resultado: 'recusada', detalhe: `o retry saiu ${r.exitCode}${ultimaLinha(r.stderr || r.stdout) ? `: ${ultimaLinha(r.stderr || r.stdout)}` : ''}` }
|
|
156
|
+
}
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// A Sentinela do Condutor do Orkastery (RM-059, fatia F6; ficha M2 do plano dos mods). Na sessão do dono, mantém a
|
|
2
|
+
// presença dela nesta máquina pelo núcleo, para o condutor externo ceder o despacho (DD5), relê o passo do condutor
|
|
3
|
+
// de cada thread aberta quando um ledger muda e a cada batida, e despacha a fase seguinte das threads #Auto que o
|
|
4
|
+
// núcleo manda despachar, só pelo `ork phase run` e uma vez por turno (T6.2); e avisa o dono, por notificação e na
|
|
5
|
+
// faixa, do que só ele resolve, com o próximo passo do núcleo (T6.3), registrando pela ponte o que mostrou para o
|
|
6
|
+
// Telegram não repetir. Lê só pelo núcleo (o `ork` e o `$.ork` da ponte), nunca `.orkastery/` por conta própria, e
|
|
7
|
+
// nunca assume a condução de ninguém. A lógica fica nos módulos puros ao lado; aqui só os ganchos e as chamadas ao
|
|
8
|
+
// `$`, escritas como `x.noun.metodo(...)`, como o validador exige.
|
|
9
|
+
import type { EngineInterface, Register } from 'claude-code'
|
|
10
|
+
import type { SentinelaEntrada, SentinelaPasso, SentinelaPresenca, SentinelaResumo } from '../types'
|
|
11
|
+
import {
|
|
12
|
+
adiamentoDa, AMBIENTE_DA_RETOMADA, argvDaRetomada, argvDoDespacho, argvDoRender, chaveDaRetomada, chaveDoDespacho, conducaoLivre, lerDespacho,
|
|
13
|
+
lerRetomada, paraDespachar, paraRetomar, pedidoAbertoNoRetrato, PRAZO_DA_RETOMADA_MS, PRAZO_DO_DESPACHO_MS, PRAZO_DO_RENDER_MS,
|
|
14
|
+
promptDoRender, retomadaAutomatica,
|
|
15
|
+
} from './despacho'
|
|
16
|
+
import type { ResultadoDoDespacho } from './despacho'
|
|
17
|
+
import { avisoDoDespacho, avisosDoPulse, avisosNovos, marcaDoAviso, PERIODO_DO_PULSE_MS, TITULO } from './alertas'
|
|
18
|
+
import type { Aviso } from './alertas'
|
|
19
|
+
import {
|
|
20
|
+
acrescentar, argvDaBatida, argvDoDesligar, argvDoStatus, BATIDA_MS, entrada, ESPERA_DO_LEDGER_MS, guardarChave, lerBatida, lerStatus, MOD,
|
|
21
|
+
PRAZO_DA_BATIDA_MS, PRAZO_DO_STATUS_MS, sessaoValida, TETO_DO_DIARIO, textoDaFaixa, textoDoDiario, threadDoLedger,
|
|
22
|
+
} from './vigia'
|
|
23
|
+
|
|
24
|
+
const RESUMO_VAZIO: SentinelaResumo = { lidoEm: null, presenca: 'desconhecida', passos: [], erro: null }
|
|
25
|
+
const REF_DIARIO = { plugin: 'ork-sentinela', key: 'diario' } as const
|
|
26
|
+
const REF_PAUSADA = { plugin: 'ork-sentinela', key: 'pausada' } as const
|
|
27
|
+
const REF_RESUMO = { plugin: 'ork-sentinela', key: 'resumo' } as const
|
|
28
|
+
const REF_DESPACHADOS = { plugin: 'ork-sentinela', key: 'despachados' } as const
|
|
29
|
+
const REF_RETOMADOS = { plugin: 'ork-sentinela', key: 'retomados' } as const
|
|
30
|
+
const REF_AVISADOS = { plugin: 'ork-sentinela', key: 'avisados' } as const
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* O estado desta carga. Um despacho do host lê o `$.state` do momento em que começou, e a segunda gravação com
|
|
34
|
+
* `ifVersion` no mesmo despacho nunca acerta (o `update` do motor ficaria em laço): a fonte da verdade é esta memória
|
|
35
|
+
* do módulo, carregada do estado uma vez na ativação (depois de uma recarga, o que a carga anterior gravou), e cada
|
|
36
|
+
* mudança vai ao estado por `$.state.set` sem versão. Os despachos deste módulo rodam um de cada vez no mesmo laço de
|
|
37
|
+
* eventos, então a memória nunca diverge do que foi gravado por último.
|
|
38
|
+
*/
|
|
39
|
+
const memoria = {
|
|
40
|
+
diario: [] as SentinelaEntrada[], pausada: false, resumo: RESUMO_VAZIO, despachados: [] as string[], retomados: [] as string[],
|
|
41
|
+
avisados: [] as string[],
|
|
42
|
+
}
|
|
43
|
+
type Chave = keyof typeof memoria
|
|
44
|
+
/** O servidor do núcleo pode conectar depois do início da sessão: o papel é conferido de novo nestas esperas. */
|
|
45
|
+
const ESPERAS_DO_PAPEL_MS = [3000, 10000, 30000]
|
|
46
|
+
/** Depois delas, enquanto dormente, a cada 5 min. */
|
|
47
|
+
const ESPERA_LONGA_DO_PAPEL_MS = 300000
|
|
48
|
+
/** A primeira rodada sai logo depois da ativação, fora do início da sessão. */
|
|
49
|
+
const ESPERA_DA_PRIMEIRA_MS = 1000
|
|
50
|
+
/** Ledgers mudam o tempo todo com sessões vivas: entre duas rodadas pedidas por arquivo, pelo menos isto. */
|
|
51
|
+
const INTERVALO_MINIMO_MS = 15000
|
|
52
|
+
|
|
53
|
+
// O que o módulo guarda entre ganchos. Uma recarga do módulo zera tudo, e o `session.start` dispara de novo nela; o
|
|
54
|
+
// diário, a pausa e a última leitura ficam em `$.state`.
|
|
55
|
+
let ativa = false
|
|
56
|
+
let desistiu = false
|
|
57
|
+
/** O id desta sessão do Claude Code, relido a cada batida: um `/clear` continua o processo com outro. */
|
|
58
|
+
let sessao: string | null = null
|
|
59
|
+
let ciclo: { cancel: () => void } | undefined
|
|
60
|
+
let acordar: { cancel: () => void } | undefined
|
|
61
|
+
let rodando: Promise<void> | undefined
|
|
62
|
+
let pendente = false
|
|
63
|
+
let ultimaRodadaEm = 0
|
|
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
|
+
/** A falha de uma rodada em segundo plano que nem o diário registrou; o `/sentinela` a mostra. */
|
|
70
|
+
let falhaSemRegistro: string | null = null
|
|
71
|
+
/** Quando o pulse foi lido pela última vez nesta carga, e quantos avisos ele trouxe (a faixa os conta). */
|
|
72
|
+
let ultimoPulseEm = Number.NEGATIVE_INFINITY
|
|
73
|
+
let avisosAtuais = 0
|
|
74
|
+
let batendo: Promise<void> | undefined
|
|
75
|
+
/** O que a última batida disse da presença desta sessão: `outra` segura o despacho e os avisos. */
|
|
76
|
+
let ultimaBatida: SentinelaPresenca = 'desconhecida'
|
|
77
|
+
/**
|
|
78
|
+
* Os turnos e bloqueios adiados nesta carga, com até quando. Só o despacho feito (ou o pedido repetido de quem já
|
|
79
|
+
* conduz) e a retomada que o plano declara do dono ficam de vez; a recusa que passa (outro condutor, sem vaga, pedido
|
|
80
|
+
* aberto, falha) volta depois do adiamento, porque o fim do turno que a chave carrega não muda com ela.
|
|
81
|
+
*/
|
|
82
|
+
const adiados = new Map<string, { ate: number; vezes: number }>()
|
|
83
|
+
/** A sessão acabou (`session.end`): batida que chega depois disso desliga a presença de novo, em vez de refazê-la. */
|
|
84
|
+
let encerrada = false
|
|
85
|
+
|
|
86
|
+
function emEspera(agora: number): string[] {
|
|
87
|
+
return [...adiados].filter(([, a]) => a.ate > agora).map(([chave]) => chave)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Adia a chave pela vez seguida dela: a mesma recusa dobra a espera até o teto (3 min a 30 min). */
|
|
91
|
+
function adiarChave(chave: string, agora: number): number {
|
|
92
|
+
const vezes = (adiados.get(chave)?.vezes ?? 0) + 1
|
|
93
|
+
const espera = adiamentoDa(vezes)
|
|
94
|
+
adiados.set(chave, { ate: agora + espera, vezes })
|
|
95
|
+
return espera
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Tira do adiamento o que o núcleo não pede mais (o turno andou, o bloqueio saiu): o Map não cresce sem fim. */
|
|
99
|
+
function podarAdiados(vivas: ReadonlySet<string>): void {
|
|
100
|
+
for (const chave of [...adiados.keys()]) if (!vivas.has(chave)) adiados.delete(chave)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function mensagem(erro: unknown): string {
|
|
104
|
+
return (erro instanceof Error ? erro.message : String(erro)).replace(/\s+/g, ' ').trim().slice(0, 200)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Carrega a memória do estado: o que esta sessão (ou a carga anterior do módulo) já gravou. */
|
|
108
|
+
async function carregarMemoria($: EngineInterface): Promise<void> {
|
|
109
|
+
memoria.diario = (await $.state.get(REF_DIARIO)).value ?? []
|
|
110
|
+
memoria.pausada = (await $.state.get(REF_PAUSADA)).value ?? false
|
|
111
|
+
memoria.resumo = (await $.state.get(REF_RESUMO)).value ?? RESUMO_VAZIO
|
|
112
|
+
memoria.despachados = (await $.state.get(REF_DESPACHADOS)).value ?? []
|
|
113
|
+
memoria.retomados = (await $.state.get(REF_RETOMADOS)).value ?? []
|
|
114
|
+
memoria.avisados = (await $.state.get(REF_AVISADOS)).value ?? []
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Muda uma chave da memória e a grava no estado, sem versão: a memória é a fonte desta carga. */
|
|
118
|
+
async function mudar<K extends Chave>($: EngineInterface, chave: K, mudanca: (atual: (typeof memoria)[K]) => (typeof memoria)[K]): Promise<void> {
|
|
119
|
+
memoria[chave] = mudanca(memoria[chave])
|
|
120
|
+
if (chave === 'diario') await $.state.set(REF_DIARIO, memoria.diario)
|
|
121
|
+
else if (chave === 'pausada') await $.state.set(REF_PAUSADA, memoria.pausada)
|
|
122
|
+
else if (chave === 'resumo') await $.state.set(REF_RESUMO, memoria.resumo)
|
|
123
|
+
else if (chave === 'despachados') await $.state.set(REF_DESPACHADOS, memoria.despachados)
|
|
124
|
+
else if (chave === 'retomados') await $.state.set(REF_RETOMADOS, memoria.retomados)
|
|
125
|
+
else await $.state.set(REF_AVISADOS, memoria.avisados)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* O papel pela ponte. `null` só quando o `$.ork` não existe (a ponte não carregou: a chamada nem acontece); uma falha
|
|
130
|
+
* da própria ponte conta como dormente e o papel é conferido de novo depois.
|
|
131
|
+
*/
|
|
132
|
+
async function papelDaSessao($: EngineInterface): Promise<string | null> {
|
|
133
|
+
try {
|
|
134
|
+
return String(await $.ork.papel())
|
|
135
|
+
} catch (erro) {
|
|
136
|
+
return erro instanceof TypeError ? null : 'dormente'
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Uma linha no diário, sem repetir a anterior igual (a mesma falha a cada batida vira uma linha só). */
|
|
141
|
+
async function anotar($: EngineInterface, e: SentinelaEntrada): Promise<void> {
|
|
142
|
+
await mudar($, 'diario', d => {
|
|
143
|
+
const ultima = d[d.length - 1]
|
|
144
|
+
if (ultima && ultima.tipo === e.tipo && ultima.thread === e.thread && ultima.texto === e.texto) return d
|
|
145
|
+
return acrescentar(d, e, TETO_DO_DIARIO)
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Liga a Sentinela na sessão do dono: o `/sentinela`, a batida de 60 s e a primeira rodada. Falha deixa tudo desligado. */
|
|
150
|
+
async function ativar($: EngineInterface): Promise<void> {
|
|
151
|
+
if (ativa) return
|
|
152
|
+
ativa = true
|
|
153
|
+
try {
|
|
154
|
+
await carregarMemoria($)
|
|
155
|
+
await $.command.register({
|
|
156
|
+
name: 'sentinela', description: 'Sentinela do Condutor: o passo de cada thread e o diário do que ela despachou, cedeu e avisou; pausar ou retomar',
|
|
157
|
+
immediate: true,
|
|
158
|
+
})
|
|
159
|
+
} catch (erro) {
|
|
160
|
+
ativa = false
|
|
161
|
+
falhaSemRegistro = mensagem(erro)
|
|
162
|
+
$.ui.toast(`sentinela: a Sentinela do Condutor não ligou nesta sessão (${mensagem(erro)})`)
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
ciclo?.cancel()
|
|
166
|
+
ciclo = $.clock.every(BATIDA_MS, () => tique($))
|
|
167
|
+
$.clock.after(ESPERA_DA_PRIMEIRA_MS, () => tique($))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A batida tem relógio próprio, fora da rodada: um despacho lento (até 200 s) dentro de uma rodada não deixa a presença
|
|
172
|
+
* vencer (3 min), e o condutor externo não volta a conduzir no meio dela. Uma por vez; pausada, nenhuma.
|
|
173
|
+
*/
|
|
174
|
+
function pedirBatida($: EngineInterface): Promise<void> | undefined {
|
|
175
|
+
if (!ativa || memoria.pausada || batendo) return undefined
|
|
176
|
+
const esta = (async () => {
|
|
177
|
+
try {
|
|
178
|
+
ultimaBatida = await bater($)
|
|
179
|
+
// A pausa ou o fim da sessão chegou com a batida no ar: ela refez a presença, que sai de novo.
|
|
180
|
+
if (memoria.pausada || encerrada) {
|
|
181
|
+
await desligar($, memoria.pausada ? 'batida que chegou depois da pausa' : 'batida que chegou depois do fim da sessão')
|
|
182
|
+
ultimaBatida = 'nenhuma'
|
|
183
|
+
}
|
|
184
|
+
} catch (erro) {
|
|
185
|
+
falhaSemRegistro = mensagem(erro)
|
|
186
|
+
} finally {
|
|
187
|
+
batendo = undefined
|
|
188
|
+
}
|
|
189
|
+
})()
|
|
190
|
+
batendo = esta
|
|
191
|
+
return esta
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** O relógio da Sentinela: a batida e, depois dela, uma rodada de reserva. Depois de um `/clear` o processo segue. */
|
|
195
|
+
function tique($: EngineInterface): void {
|
|
196
|
+
encerrada = false
|
|
197
|
+
const batida = pedirBatida($)
|
|
198
|
+
if (batida) void batida.then(() => { void pedirRodada($) })
|
|
199
|
+
else void pedirRodada($)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Confere o papel: liga no dono, desiste na fase ou sem a ponte, e tenta de novo enquanto dormente. */
|
|
203
|
+
async function conferirPapel($: EngineInterface, tentativa: number): Promise<void> {
|
|
204
|
+
if (ativa || desistiu) return
|
|
205
|
+
const papel = await papelDaSessao($)
|
|
206
|
+
if (papel === 'dono') return ativar($)
|
|
207
|
+
if (papel === 'fase' || papel === null) {
|
|
208
|
+
desistiu = true
|
|
209
|
+
if (papel === null) $.ui.toast('sentinela: a ponte ork-ponte não carregou; a Sentinela do Condutor fica desligada nesta sessão')
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
const espera = ESPERAS_DO_PAPEL_MS[tentativa] ?? ESPERA_LONGA_DO_PAPEL_MS
|
|
213
|
+
$.clock.after(espera, () => { void conferirPapel($, tentativa + 1).catch(erro => { falhaSemRegistro = mensagem(erro) }) })
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Uma rodada por vez. Com uma em andamento, o pedido só marca que outra deve rodar logo depois e volta sem nada para
|
|
218
|
+
* esperar: quem pediu nunca espera a rodada de outro despacho, cuja espera contaria no orçamento do gancho. Devolve a
|
|
219
|
+
* rodada só a quem a começou.
|
|
220
|
+
*/
|
|
221
|
+
function pedirRodada($: EngineInterface): Promise<void> | undefined {
|
|
222
|
+
if (!ativa) return undefined
|
|
223
|
+
if (rodando) {
|
|
224
|
+
pendente = true
|
|
225
|
+
return undefined
|
|
226
|
+
}
|
|
227
|
+
const esta = (async () => {
|
|
228
|
+
try {
|
|
229
|
+
do {
|
|
230
|
+
pendente = false
|
|
231
|
+
await rodadaUmaVez($)
|
|
232
|
+
} while (pendente)
|
|
233
|
+
} catch (erro) {
|
|
234
|
+
// O `$` pode já não responder (a sessão fechou): o motivo fica numa variável do módulo, sem depender dele.
|
|
235
|
+
falhaSemRegistro = mensagem(erro)
|
|
236
|
+
} finally {
|
|
237
|
+
rodando = undefined
|
|
238
|
+
}
|
|
239
|
+
})()
|
|
240
|
+
rodando = esta
|
|
241
|
+
return esta
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Um ledger mudou: a rodada sai depois de juntar as linhas do mesmo evento, e nunca antes do intervalo mínimo. */
|
|
245
|
+
function agendar($: EngineInterface, agora: number): void {
|
|
246
|
+
if (acordar) return
|
|
247
|
+
const espera = Math.max(ESPERA_DO_LEDGER_MS, ultimaRodadaEm + INTERVALO_MINIMO_MS - agora)
|
|
248
|
+
acordar = $.clock.after(espera, () => { acordar = undefined; void pedirRodada($) })
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** A batida da presença desta sessão; falha vira linha do diário e presença desconhecida. */
|
|
252
|
+
async function bater($: EngineInterface): Promise<SentinelaPresenca> {
|
|
253
|
+
sessao = await $.session.id().catch(() => sessao)
|
|
254
|
+
if (!sessaoValida(sessao)) return 'desconhecida'
|
|
255
|
+
try {
|
|
256
|
+
return lerBatida(await $.process.run(argvDaBatida(sessao), { timeoutMs: PRAZO_DA_BATIDA_MS }))
|
|
257
|
+
} catch (erro) {
|
|
258
|
+
await anotar($, entrada(await $.clock.now(), 'erro', null, `a batida da presença falhou: ${mensagem(erro)}`))
|
|
259
|
+
return 'desconhecida'
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** A rodada: o status do núcleo, o despacho e os avisos (só com a presença desta sessão) e a linha da faixa. */
|
|
264
|
+
async function rodadaUmaVez($: EngineInterface): Promise<void> {
|
|
265
|
+
ultimaRodadaEm = await $.clock.now()
|
|
266
|
+
const pausa = memoria.pausada
|
|
267
|
+
let lido: ReturnType<typeof lerStatus>
|
|
268
|
+
try {
|
|
269
|
+
lido = lerStatus(await $.process.run(argvDoStatus(), { timeoutMs: PRAZO_DO_STATUS_MS }), sessao)
|
|
270
|
+
} catch (erro) {
|
|
271
|
+
const motivo = mensagem(erro)
|
|
272
|
+
await mudar($, 'resumo', r => ({ ...r, erro: motivo }))
|
|
273
|
+
await anotar($, entrada(await $.clock.now(), 'erro', null, `a leitura do núcleo falhou: ${motivo}`))
|
|
274
|
+
await publicarFaixa($)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
const presenca: SentinelaPresenca = ultimaBatida === 'outra' ? 'outra' : lido.presenca
|
|
278
|
+
await mudar($, 'resumo', () => ({ lidoEm: ultimaRodadaEm, presenca, passos: lido.passos, erro: null }))
|
|
279
|
+
// Uma Sentinela por máquina: só a que tem a presença conduz e avisa, e nunca pausada (aí o condutor externo avisa).
|
|
280
|
+
if (!pausa && presenca === 'minha') {
|
|
281
|
+
await conduzir($, lido.passos)
|
|
282
|
+
await lerAvisos($, lido.passos)
|
|
283
|
+
} else avisosAtuais = 0
|
|
284
|
+
await publicarFaixa($)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* T6.3: a cada 5 min, o pulse pela ponte vira aviso do que só o dono resolve, com o próximo passo do núcleo. Cada aviso
|
|
289
|
+
* sai uma vez por texto: notificação do sistema (ou toast, se o canal não escreveu), linha no diário e o registro do
|
|
290
|
+
* que foi mostrado na ponte, para o Telegram não repetir. Pulse que falha fica no diário e a rodada segue.
|
|
291
|
+
*/
|
|
292
|
+
async function lerAvisos($: EngineInterface, passos: readonly SentinelaPasso[]): Promise<void> {
|
|
293
|
+
const agora = await $.clock.now()
|
|
294
|
+
if (agora - ultimoPulseEm < PERIODO_DO_PULSE_MS) return
|
|
295
|
+
ultimoPulseEm = agora
|
|
296
|
+
let avisos: Aviso[]
|
|
297
|
+
try {
|
|
298
|
+
avisos = avisosDoPulse(await $.ork.ler({ consulta: 'pulse' }), passos)
|
|
299
|
+
} catch (erro) {
|
|
300
|
+
return anotar($, entrada(agora, 'erro', null, `os avisos do pulse ficaram indisponíveis nesta rodada (${mensagem(erro)})`))
|
|
301
|
+
}
|
|
302
|
+
avisosAtuais = avisos.length
|
|
303
|
+
for (const a of avisosNovos(avisos, memoria.avisados)) await avisar($, a)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Um aviso ao dono, uma vez por texto. */
|
|
307
|
+
async function avisar($: EngineInterface, a: Aviso): Promise<void> {
|
|
308
|
+
const marca = marcaDoAviso(a)
|
|
309
|
+
if (memoria.avisados.includes(marca)) return
|
|
310
|
+
await mudar($, 'avisados', l => guardarChave(l, marca))
|
|
311
|
+
const enviado = await $.ui.notify(a.texto, { title: TITULO }).then(r => r.isSent, () => false)
|
|
312
|
+
if (!enviado) $.ui.toast(`sentinela: ${a.texto}`)
|
|
313
|
+
await anotar($, entrada(await $.clock.now(), 'aviso', a.thread, a.texto))
|
|
314
|
+
try {
|
|
315
|
+
await $.ork.mostrado({ mod: MOD, canal: enviado ? 'notificacao' : 'toast', texto: a.texto, chave: a.chave, ...(a.thread ? { thread: a.thread } : {}) })
|
|
316
|
+
} catch (erro) {
|
|
317
|
+
await anotar($, entrada(await $.clock.now(), 'erro', a.thread, `o aviso não entrou no registro do mostrado da ponte (${mensagem(erro)})`))
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** T6.2: o despacho e a retomada que o núcleo manda, cada turno feito uma vez só e o recusado depois do adiamento. */
|
|
322
|
+
async function conduzir($: EngineInterface, passos: readonly SentinelaPasso[]): Promise<void> {
|
|
323
|
+
if (!sessaoValida(sessao)) return
|
|
324
|
+
podarAdiados(new Set(passos.flatMap(p => [chaveDoDespacho(p), chaveDaRetomada(p)]).filter((c): c is string => c !== null)))
|
|
325
|
+
const esperando = emEspera(await $.clock.now())
|
|
326
|
+
for (const p of paraDespachar(passos, [...memoria.despachados, ...esperando])) await despachar($, p, sessao)
|
|
327
|
+
for (const p of paraRetomar(passos, [...memoria.retomados, ...esperando])) await retomarThread($, p)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const FRASE_DO_RESULTADO: Record<ResultadoDoDespacho['resultado'], string> = {
|
|
331
|
+
despachada: 'despachada pelo núcleo', repetida: 'o mesmo pedido já estava em andamento; nada novo',
|
|
332
|
+
ocupada: 'outro condutor conduz agora; nada despachado', falhou: 'o despacho falhou',
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Um despacho: a chave do turno entra antes (uma recarga no meio nunca pede o mesmo turno de novo); a condução e o
|
|
337
|
+
* pedido aberto ao dono são relidos pelo núcleo logo antes; o prompt sai do render com o pedido guardado; e o
|
|
338
|
+
* `ork phase run` decide, com o lease e a trava do kernel, se a sessão nova abre. Só o despacho feito e o pedido
|
|
339
|
+
* repetido ficam de vez; o resto devolve a chave e espera o adiamento, e o diário diz quando volta.
|
|
340
|
+
*/
|
|
341
|
+
async function despachar($: EngineInterface, p: SentinelaPasso, sessaoDoDono: string): Promise<void> {
|
|
342
|
+
const chave = chaveDoDespacho(p) as string
|
|
343
|
+
const fase = p.fase as string
|
|
344
|
+
const anotarAqui = async (tipo: SentinelaEntrada['tipo'], texto: string) => anotar($, entrada(await $.clock.now(), tipo, p.thread, texto))
|
|
345
|
+
let volta = ''
|
|
346
|
+
const adiar = async () => {
|
|
347
|
+
await mudar($, 'despachados', d => d.filter(c => c !== chave))
|
|
348
|
+
volta = `; tenta de novo em ${Math.round(adiarChave(chave, await $.clock.now()) / 60000)} min`
|
|
349
|
+
}
|
|
350
|
+
await mudar($, 'despachados', d => guardarChave(d, chave))
|
|
351
|
+
try {
|
|
352
|
+
if (!conducaoLivre(await $.ork.ler({ consulta: 'conducao.status', thread: p.thread }))) {
|
|
353
|
+
await adiar()
|
|
354
|
+
return anotarAqui('cedeu', `${fase}: outro condutor já conduz a thread; nada despachado${volta}`)
|
|
355
|
+
}
|
|
356
|
+
if (pedidoAbertoNoRetrato((await $.ork.retrato({ thread: p.thread })).retrato, p.thread)) {
|
|
357
|
+
await adiar()
|
|
358
|
+
return anotarAqui('cedeu', `${fase}: há pedido aberto ao dono na thread; o despacho espera a resposta${volta}`)
|
|
359
|
+
}
|
|
360
|
+
} catch (erro) {
|
|
361
|
+
await adiar()
|
|
362
|
+
return anotarAqui('erro', `${fase}: a condução não foi relida (${mensagem(erro)})${volta}`)
|
|
363
|
+
}
|
|
364
|
+
let render: ReturnType<typeof promptDoRender>
|
|
365
|
+
try {
|
|
366
|
+
render = promptDoRender(await $.process.run(argvDoRender(p.thread, fase), { timeoutMs: PRAZO_DO_RENDER_MS }))
|
|
367
|
+
} catch (erro) {
|
|
368
|
+
await adiar()
|
|
369
|
+
return anotarAqui('erro', `${fase}: o render do prompt falhou (${mensagem(erro)})${volta}`)
|
|
370
|
+
}
|
|
371
|
+
if ('motivo' in render) {
|
|
372
|
+
await adiar()
|
|
373
|
+
await anotarAqui('erro', `${fase}: não despachada: ${render.motivo}`)
|
|
374
|
+
return avisar($, avisoDoDespacho(p, render.motivo))
|
|
375
|
+
}
|
|
376
|
+
let r: ResultadoDoDespacho
|
|
377
|
+
try {
|
|
378
|
+
r = lerDespacho(await $.process.run(argvDoDespacho(p.thread, fase, render.prompt, sessaoDoDono), { timeoutMs: PRAZO_DO_DESPACHO_MS }))
|
|
379
|
+
} catch (erro) {
|
|
380
|
+
r = { resultado: 'falhou', detalhe: mensagem(erro) }
|
|
381
|
+
}
|
|
382
|
+
const deVez = r.resultado === 'despachada' || r.resultado === 'repetida'
|
|
383
|
+
if (!deVez) await adiar()
|
|
384
|
+
const tipo = r.resultado === 'despachada' ? 'despacho' : r.resultado === 'falhou' ? 'erro' : 'cedeu'
|
|
385
|
+
await anotarAqui(tipo, `${fase}: ${FRASE_DO_RESULTADO[r.resultado]}${r.detalhe ? ` (${r.detalhe})` : ''}${deVez ? '' : volta}`)
|
|
386
|
+
if (r.resultado === 'falhou') await avisar($, avisoDoDespacho(p, r.detalhe))
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Uma retomada: a condução e o pedido aberto ao dono são relidos antes, como no despacho; o `ork retry plan` (pela
|
|
391
|
+
* ponte) decide, e o `ork retry run` só roda quando ele é automático. O plano que é do dono fica de vez para esse
|
|
392
|
+
* bloqueio; a recusa que passa espera o adiamento.
|
|
393
|
+
*/
|
|
394
|
+
async function retomarThread($: EngineInterface, p: SentinelaPasso): Promise<void> {
|
|
395
|
+
const chave = chaveDaRetomada(p) as string
|
|
396
|
+
const anotarAqui = async (texto: string) => anotar($, entrada(await $.clock.now(), 'retomada', p.thread, texto))
|
|
397
|
+
let volta = ''
|
|
398
|
+
const adiar = async () => {
|
|
399
|
+
await mudar($, 'retomados', d => d.filter(c => c !== chave))
|
|
400
|
+
volta = `; tenta de novo em ${Math.round(adiarChave(chave, await $.clock.now()) / 60000)} min`
|
|
401
|
+
}
|
|
402
|
+
await mudar($, 'retomados', d => guardarChave(d, chave))
|
|
403
|
+
let plano: unknown
|
|
404
|
+
try {
|
|
405
|
+
if (!conducaoLivre(await $.ork.ler({ consulta: 'conducao.status', thread: p.thread }))) {
|
|
406
|
+
await adiar()
|
|
407
|
+
return anotarAqui(`bloqueio ${p.bloqueio?.motivo}: outro condutor já conduz a thread${volta}`)
|
|
408
|
+
}
|
|
409
|
+
if (pedidoAbertoNoRetrato((await $.ork.retrato({ thread: p.thread })).retrato, p.thread)) {
|
|
410
|
+
await adiar()
|
|
411
|
+
return anotarAqui(`bloqueio ${p.bloqueio?.motivo}: há pedido aberto ao dono na thread; a retomada espera a resposta${volta}`)
|
|
412
|
+
}
|
|
413
|
+
plano = await $.ork.ler({ consulta: 'retry.plan', thread: p.thread })
|
|
414
|
+
} catch (erro) {
|
|
415
|
+
await adiar()
|
|
416
|
+
return anotarAqui(`a condução ou o retry plan não foram lidos (${mensagem(erro)})${volta}`)
|
|
417
|
+
}
|
|
418
|
+
if (!retomadaAutomatica(plano)) {
|
|
419
|
+
const { bloqueio } = (plano ?? {}) as { bloqueio?: unknown }
|
|
420
|
+
return anotarAqui(`bloqueio ${p.bloqueio?.motivo}: o retry plan não é automático (${String(bloqueio ?? 'sem plano')}); o passo é do dono`)
|
|
421
|
+
}
|
|
422
|
+
let r: ReturnType<typeof lerRetomada>
|
|
423
|
+
try {
|
|
424
|
+
r = lerRetomada(await $.process.run(argvDaRetomada(p.thread), { env: { ...AMBIENTE_DA_RETOMADA }, timeoutMs: PRAZO_DA_RETOMADA_MS }))
|
|
425
|
+
} catch (erro) {
|
|
426
|
+
r = { resultado: 'recusada', detalhe: mensagem(erro) }
|
|
427
|
+
}
|
|
428
|
+
if (r.resultado !== 'retomada') await adiar()
|
|
429
|
+
await anotarAqui(`bloqueio ${p.bloqueio?.motivo}: ${r.resultado === 'retomada' ? 'retomado pelo núcleo' : 'o núcleo não retomou'}${r.detalhe ? ` (${r.detalhe})` : ''}${r.resultado === 'retomada' ? '' : volta}`)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** A linha da faixa pela ponte (D7 do plano dos mods), só quando o texto muda. */
|
|
433
|
+
async function publicarFaixa($: EngineInterface): Promise<void> {
|
|
434
|
+
const texto = textoDaFaixa(memoria.resumo, memoria.pausada, await $.clock.now(), avisosAtuais)
|
|
435
|
+
if (texto === ultimaFaixa) return
|
|
436
|
+
try {
|
|
437
|
+
if (texto === undefined) await $.ork.faixa({ mod: MOD })
|
|
438
|
+
else await $.ork.faixa({ mod: MOD, texto })
|
|
439
|
+
ultimaFaixa = texto
|
|
440
|
+
} catch (erro) {
|
|
441
|
+
// A linha fica como estava; o motivo vai ao diário, e a próxima rodada tenta de novo.
|
|
442
|
+
await anotar($, entrada(await $.clock.now(), 'erro', null, `a linha da faixa não foi publicada: ${mensagem(erro)}`))
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Tira a presença desta sessão: o condutor externo volta a conduzir. */
|
|
447
|
+
async function desligar($: EngineInterface, motivo: string): Promise<void> {
|
|
448
|
+
if (!sessaoValida(sessao)) return
|
|
449
|
+
const r = await $.process.run(argvDoDesligar(sessao), { timeoutMs: PRAZO_DA_BATIDA_MS }).catch((erro: unknown) => erro)
|
|
450
|
+
if (r instanceof Error || (r as { exitCode?: number }).exitCode !== 0) {
|
|
451
|
+
await anotar($, entrada(await $.clock.now(), 'erro', null, `a presença não saiu (${motivo}); ela vence sozinha em 3 min`))
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function pausarSentinela($: EngineInterface): Promise<string> {
|
|
456
|
+
await mudar($, 'pausada', () => true)
|
|
457
|
+
await desligar($, 'pausa pelo dono')
|
|
458
|
+
ultimaBatida = 'nenhuma'
|
|
459
|
+
await anotar($, entrada(await $.clock.now(), 'pausa', null, 'pausada pelo /sentinela; o condutor externo conduz'))
|
|
460
|
+
await mudar($, 'resumo', r => ({ ...r, presenca: 'nenhuma' }))
|
|
461
|
+
await publicarFaixa($)
|
|
462
|
+
return 'Sentinela pausada: nada é despachado por ela, e o condutor externo volta a conduzir. Para voltar: /sentinela retomar.'
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function retomarSentinela($: EngineInterface): Promise<string> {
|
|
466
|
+
await mudar($, 'pausada', () => false)
|
|
467
|
+
await anotar($, entrada(await $.clock.now(), 'pausa', null, 'retomada pelo /sentinela'))
|
|
468
|
+
const batida = pedirBatida($)
|
|
469
|
+
if (batida) await batida
|
|
470
|
+
const propria = pedirRodada($)
|
|
471
|
+
if (propria) await propria
|
|
472
|
+
return 'Sentinela retomada: ela volta a conduzir nesta máquina quando a presença for confirmada pelo núcleo.'
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export const register: Register = on => {
|
|
476
|
+
on('session.start', async ($, e, next) => {
|
|
477
|
+
if (e.isInteractive) await conferirPapel($, 0)
|
|
478
|
+
return next(e)
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
// Um ledger de thread mudou (a ponte já vigia a pasta das threads e os ledgers recentes no papel dono): a rodada
|
|
482
|
+
// relê o passo do condutor pelo núcleo.
|
|
483
|
+
on('classic.FileChanged', async ($, e, next) => {
|
|
484
|
+
if (ativa && threadDoLedger(e.file_path) !== null) agendar($, await $.clock.now())
|
|
485
|
+
return next(e)
|
|
486
|
+
}).catch(($, e, next) => next(e))
|
|
487
|
+
|
|
488
|
+
// A sessão acabou: a presença sai na hora, para o condutor externo não esperar o prazo. Num `/clear` o processo
|
|
489
|
+
// segue com outro id, e a próxima batida toma a presença com ele.
|
|
490
|
+
on('session.end', async ($, e, next) => {
|
|
491
|
+
encerrada = true
|
|
492
|
+
if (ativa && !memoria.pausada) await desligar($, `fim da sessão (${e.reason})`)
|
|
493
|
+
return next(e)
|
|
494
|
+
}).catch(($, e, next) => next(e))
|
|
495
|
+
|
|
496
|
+
// `/sentinela` relê o núcleo e mostra o passo de cada thread e o diário; `pausar` e `retomar` mudam quem conduz.
|
|
497
|
+
on('command.run', { command: 'sentinela' }, async ($, e) => {
|
|
498
|
+
if (!ativa && !desistiu) await conferirPapel($, ESPERAS_DO_PAPEL_MS.length)
|
|
499
|
+
if (!ativa) return { text: 'A Sentinela do Condutor só roda na sessão do dono de um projeto do Orkastery.' }
|
|
500
|
+
const pedido = String(e.args ?? '').trim()
|
|
501
|
+
if (pedido === 'pausar') return { text: await pausarSentinela($) }
|
|
502
|
+
if (pedido === 'retomar') return { text: await retomarSentinela($) }
|
|
503
|
+
if (pedido !== '') return { text: 'Uso: /sentinela (o passo de cada thread e o diário), /sentinela pausar ou /sentinela retomar.' }
|
|
504
|
+
const propria = pedirRodada($)
|
|
505
|
+
if (propria) await propria
|
|
506
|
+
const texto = textoDoDiario(memoria.resumo, memoria.diario, memoria.pausada, await $.clock.now())
|
|
507
|
+
const andamento = propria ? '' : '\nUma rodada já estava em andamento: o próximo /sentinela mostra a leitura nova.'
|
|
508
|
+
const falha = falhaSemRegistro ? `\nÚltima falha em segundo plano: ${falhaSemRegistro}` : ''
|
|
509
|
+
return { text: texto + andamento + falha }
|
|
510
|
+
})
|
|
511
|
+
}
|