Ritual do MASTER do Orkastery no Claude Code: as entregas que esperam a sua nota, a prova de cada uma (o que foi entregue, o índice derivado do ledger e as…

English · Português

In one sentence:
orkconducts AI coding agents (Claude Code and Codex) through parallel six-phase threads, checks every claim against the repository before it counts, and only calls you when the decision is really yours.
@orkastery/cli, with changes per version · reviewed on 2026-09-27main at PR #74, run 37102089623, 2026-10-03)docs/produto/ and docs/roadmap/, checked against the code by ork docs verificardocs/ are in Brazilian Portuguese, and the CLI reference is also in English. The CLI prints Brazilian Portuguese too, except ork doctor, ork init, ork onboarding, ork experiencia show, ork thread (except thread new --from-finding), ork phase list, ork phase run, ork verify, ork ship, ork pulse, ork master, ork retry plan, ork retry run, ork roadmap (status and reservations), ork worktree, ork claims, ork decisao placar, ork accounts, ork network (except network roadmap), ork docs, ork adapter and the help, which print English when owner.language or your locale asks for en; so do the orkastery.yaml comments, the AGENTS.md and CLAUDE.md blocks, the ork docs init templates and the skills, checklists, commands and subagents that ork adapter install copies; in English, read the quickstart on orkastery.com and the glossary of what the CLI printsnpm install -g @orkastery/cli
ork demo # 30 seconds: a false claim rejected, the fixed one accepted; no account, no model
ork doctor # what holds on this machine right now; exits != 0 when blocked
ork init # writes the orkastery.yaml; run it at the root of a git repository with at least one commit
ork modos # the four conduction modes
ork docs init # product docs and roadmap in the standard, with lint
The smallest full cycle, in #Fast mode (a single GO phase), right after ork init. <thread> is the ID that ork thread new prints.
git add orkastery.yaml AGENTS.md && git commit -m "ork init"
ork thread new "add an exclamation mark to greet" --modo fast
ork phase run <thread> GO --prompt "<the request>" # the agent runtime works in the thread's worktree
ork verify <thread> # re-runs the claims and the manifest checks on the real HEAD
ork ship <thread> --para main --autorizar-push <you> # #Fast never pushes without your authorization
ork master <thread> --aceitar-omissao # closes the thread with the index derived from the ledger
phase run stops with runtime.workspace-untrusted, run claude once in the worktree, accept the trust prompt, then ork retry run <thread>. ork pulse shows the same instruction.ork claims add <thread> <file> --claim "<statement>" --verificar "<command>".ork takes that seat. It is a deterministic TypeScript CLI, with no server and no embedded LLM.ork brings you is short and organized, with alternatives and one recommendation, so you decide in seconds without rereading a wall of text. That lightness is a design goal, meant also for people with ADHD: it keeps you conducting instead of drowning.Without ork | With ork |
|---|---|
| The agent says the tests pass | ork verify re-runs the command on the real HEAD, and CI re-runs it again on a clean runner |
| You ask for status several times a day | An hourly summary says what is pending, urgent, blocking or critical |
| Every obvious decision becomes a question | The obvious one comes decided and reported; you change it if you want |
| The runtime account runs out and the roadmap stops | The same prompt continues on the next profile or on the fallback runtime |
| Two sessions step on the same file | One worktree per thread, and leases with a queue |
| The docs lie about the code | ork docs verificar fails the PR when a page drifts |
Three layers, and business rules live only in the middle one:
flowchart TB
subgraph L1["Layer 1 - where you conduct"]
A["Claude Code, Codex, Hermes, OpenClaw<br/>adapters with no business rules"]
end
subgraph L2["Layer 2 - the ork core"]
B["schedules, isolates, verifies, serializes, records<br/>deterministic TypeScript, no LLM"]
end
subgraph L3["Layer 3 - who writes the code"]
C["claude-bg and codex runtimes<br/>their own accounts, profile rotation"]
end
A -->|"thread new, phase run, gate, ship"| B
B -->|"assembled prompt, sha256 in the ledger"| C
C -.->|"commits, diffs, command output"| B
B -.->|"summary, question batch, delivery"| A
flowchart LR
G(["GOAL"]) --> P(["PLAN"]) --> GO(["GO"]) --> C(["CHECK"]) --> S(["SHIP"]) --> M(["MASTER"])
C -.->|"failed: GO-FIX with a typed spec"| GO
| Phase | What it delivers | What ork requires to pass |
|---|---|---|
| GOAL | Objective, done criteria, claims with a command | a claim without a command is refused (claims.unverifiable) |
| PLAN | Tasks, decisions, an executable verify per task | a plan without an executable verify does not become GO |
| GO | One atomic commit per task, in the thread's worktree | writing outside the worktree is blocked by a lease |
| CHECK | Verification against the baseline, plus independent CI | a failed claim becomes claims.failed or verify.regression |
| SHIP | Serialized merge with a proven push | only counts when git ls-remote matches the local SHA |
| MASTER | Typed postmortem and a conduction index | the index comes from the ledger; nobody types it |
You write the #TAG in the request itself. The mode changes where the cycle waits for you; verifying the delivery is the same in all four.
| #TAG | Pauses | Cycle | What it is for |
|---|---|---|---|
| #Classic | 3 | GOAL* / PLAN* / GO-CHECK* / SHIP-MASTER | The ork init default: delicate premises |
| #Maestro | 1 | GOAL-PLAN* / GO-CHECK-SHIP / MASTER | A clear, quick solution |
| #Auto | 0 | GOAL-PLAN-GO-CHECK-SHIP-MASTER | Docs, studies, configuration, audits |
| #Fast | 0 | GO | A small, clear request that takes minutes; the push asks for authorization |
* marks the block that pauses. Source of truth: ork modos.#Look and #Ork were retired on 2026-09-24 (RM-043); what was recorded with them stays readable forever.#Fast runs only GO, with a minimal proof: a focused test, a cheap claim or an absence declared in the ledger. It never authorizes a push on its own and never touches a public contract (RM-042).The mode loosens the PAUSE. The mode NEVER loosens the VERIFICATION.
claims.jsonl. There are even negative claims, checked by the command that would falsify them.ork verify --baseline records the world before GO.ork ci prepare exports the claims, and the GitHub ork-verify job re-runs them on the exact SHA.git ls-remote, never by the exit code of git push.ork retry policy. cost.violation never gets an automatic retry.ork decisao registrar keeps the why, how to change it and the cost of changing it now or later; you are informed.owner.timezone, stated once per message.claude-bg (default) and codex, resolved by name in core/src/runtimes.ts; ork setup picks runtime, model and effort per block of each mode.ork accounts add <id> --runtime R --dir D runs the CLI's own login; ork never reads, copies or migrates a credential.cost.violation. Details and responsibilities in SECURITY.md.memory: orkmind, memory goes to the tenant's database; without it, it degrades to files with a typed reason.ork docs verificar: fails a source, symbol, contract or command that no longer exists, a merge commit outside main, and incoherent status.ork docs sincronizar --escrever: writes only facts from the ledger and git (merge, phase) and regenerates tables and indexes.documentacao job runs markdownlint and ork docs verificar on every PR.ork adapter list # the hosts and where each one installs
ork adapter install claude-code --dry-run
| Host | What gets installed |
|---|---|
| claude-code | A plugin with the catalog skills, phase subagents, the /orkastery:ork entry point and hooks |
| codex | The $ork entry point and the conduction catalog as project-local skills |
| hermes | A routing skill, the HITL ingress plugin and thread-opening scripts |
| openclaw | An extension with the ork_* tools, each one a CLI call |
ork modos --do-pedido; the core validates it.| Capability | Status |
|---|---|
| Threads, six phases, four modes, ledger, worktrees, leases, board | works |
Claims, baseline, verify on the real HEAD, 22 typed reasons, GO-FIX | works |
| CI as an independent CHECK on the exact SHA | works; native protection of main depends on the GitHub plan (RM-012) |
ork ship with a proven push | works |
| Account rotation across profiles and runtimes | works; account state is still per project (RM-040) |
| Layered HITL, a–d batch, decisions made and reported | works since 2026-09-24 (RM-041); automatic hourly summary since 2026-09-25 (RM-045) |
| Documentation as code with parity in CI | works (RM-044) |
#Fast mode: one phase, minimal proof, no push on its own | works since 2026-09-27 (RM-042) |
| Roadmap item reservations across machines | works since 2026-09-27 (RM-047); thread state is still per machine |
| Stable local verify on a machine whose CPU the hypervisor steals | does not exist: on such a machine, the reliable proof is CI (RM-037) |
A runtime_reported measurement of the context window | does not exist: claude-bg does not expose context usage, and the gate says unavailable |
| Deterministic code graph | works in the CLI (ork grafo: index, incremental update, queries and thread context, with each edge's provenance), for npm installs since 0.5.3; the MCP tools and the hint in the phase request stay behind the grafo.mcp flag, off by default (RM-031) |
The full picture, item by item and with git evidence, is in the roadmap index.
| Measure | Value | How to check |
|---|---|---|
| Core tests | 2,733, zero failing (CI on 2026-10-03) | npm --prefix core run test:ci |
| Behavior canaries | 24, all green | ork eval --so-canarios |
| Skills corpus | 20 skills, 101 cases, 199 assertions | ork eval --so-skills |
| Product docs and roadmap | zero parity errors | ork docs verificar |
| Runtime dependencies | 9, pinned | core/package.json |
On a virtual machine whose CPU the hypervisor steals (the steal in sar), the local verify can overrun the tests' time windows. That is why the merge gate is the independent CI, and the fix is planned in RM-037.
The docs home is organized by what you want to do now. Every page under docs/ is in Brazilian Portuguese; the CLI reference and the orchestration experience guide also have English versions, and the English quickstart lives on orkastery.com.
| Section | Start with |
|---|---|
| Getting started | Quickstart in English, and the full quickstart in Portuguese: from zero to the first full cycle |
| Guides | Modes, verification, human attention, memory, audits |
| Reference | CLI (the source is ork --help) and contracts |
| Concepts | Overview and architecture |
| Product and roadmap | What exists and what comes next, checked against the code |
Many commands still print Brazilian Portuguese (ork doctor, ork init, ork onboarding, ork experiencia show, ork thread (except thread new --from-finding), ork phase list, ork phase run, ork verify, ork ship, ork pulse, ork master, ork retry plan, ork retry run, ork roadmap (status and reservations), ork worktree, ork claims, ork decisao placar, ork accounts, ork network (except network roadmap), ork docs, ork adapter and the help follow an English locale). These are the words a first run shows.
| The CLI prints | It means |
|---|---|
PRONTO / BLOQUEADO | ready / blocked: the ork doctor verdict |
correcao | the fix for the line above it |
Proximo passo | next step |
VERDADE SUSTENTADA / REPROVADO | the claims held / failed: the ork verify verdict |
AGUARDANDO AUTORIZACAO HUMANA | waiting for your authorization |
Precisa de humano agora | needs a human now: the first line of ork pulse |
Recomendação, opção | recommendation, option |
aceitas por omissao | accepted by default, because nobody scored them |
pendente, respondida | pending, answered |
Horários em <zone> | times are shown in that time zone |
03/10 06:30 is October 3. The commands that follow an English locale print 2026-10-03 06:30.modos (modes), --modo (mode; --mode also works), --para (target branch), --por (who records it), --autorizar-push (authorize the push), --conteudo (content), --verificar (the command that verifies), decisao registrar (record a decision), docs verificar (check the docs), experiencia show (conversation preferences).owner.language: en-US sets the language of the conversation in your host (Claude Code, Codex), of the CLI commands that follow the locale and of what ork init, ork docs init and ork adapter install write for people to read (the orkastery.yaml comments, the AGENTS.md and CLAUDE.md blocks, the docs templates and the skills, checklists, commands and subagents the adapter installs); set it when machines with different locales conduct the same project, or those committed blocks switch language on each run; owner.language: pt-BR keeps them in Portuguese even with LANG=en_US.| Term | Meaning |
|---|---|
| HITL | human in the loop: a question only you can answer, with alternatives and one recommendation |
| ledger | the append-only event log of a thread, in .orkastery/threads/<thread>/ledger.jsonl; ork phase list <thread> prints it |
| claim | a statement, the file it is about and the command that judges it |
| worktree | the separate git checkout of each thread, under .claude/worktrees/ |
| lease | a lock on a shared resource, such as the main tree during a merge, with a queue |
| pulse | the hourly summary of what waits for you: ork pulse |
| maestro | the conversational overview in a host (say orkastery maestro), and the onboarding stage about you |
| GOAL … MASTER | the six phases, in the thread table |
| #Classic, #Maestro, #Auto, #Fast | the conduction modes, in the modes table |
ork docs sincronizar.ork roadmap pegar), and two never take the same one.| Project | What it is |
|---|---|
| orkastery | This repository: the ork core, the adapters, the skills and the auditors |
| OrkMind | The memory layer: Postgres with pgvector, an ontology and tag search; optional |
| orkastery.com | The website and user docs |
| orkmind.com | The OrkMind site |
orkastery maestro in a host with the adapter installed: you get an overview with sources, gaps, threads, sessions, HITL and next actions. In the terminal: ork maestro --json.No metronome ever turned anyone into Bernstein. Mastery is not tempo: it is knowing, after each performance, what you did, what it cost and how well it went, and being better at the next one. That is the phase we call MASTER, and it is why the project is called Orkastery.
hooks/register.tsx 495 lines1// O Ritual do MASTER do Orkastery (RM-059, fatia F8; ficha M8 do plano dos mods). Na sessao do dono, um painel com as
2// entregas que esperam a nota do dono, lidas do nucleo pelo retrato da ponte (`$.ork.retrato`, secao `master`), e o
3// replay da prova de cada uma, com as claims pelo `ork_claims_list`. O botao "Dar a nota" so pede ao nucleo o dialogo
4// nativo (`ork_request_master_note`, com o `threadId` e nada mais), e a reserva pelo Telegram so pede o codigo
5// (`ork master pedir`), no clique do dono. Nunca le `.orkastery/` por conta propria e nunca da a nota pelo dono. A
6// logica fica nos modulos puros ao lado; aqui so os ganchos e as chamadas ao `$`, sempre escritas como
7// `x.noun.metodo(...)`, como o validador exige.
8import type { EngineInterface, Register } from 'claude-code'
9import type { RitualMostrado, RitualNota, RitualProva, RitualQuadro, RitualReserva } from '../types'
10import {
11 argsDaNota, argvDaReserva, lerReserva, notaDoErro, PRAZO_DA_NOTA_MS, PRAZO_DA_RESERVA_MS, resultadoDaNota, soltarAbrindo,
12} from './nota'
13import { desenharRitual, linhasDoPainel, mostradosVisiveis } from './replay'
14import {
15 argsDasClaims, avisosDasLacunas, claimsDaProva, entregasDaJanela, exigirThread, jsonDoMcp, paginaDoMaster, plural, QUADRO_VAZIO,
16 semNotaDada, SERVIDOR, TETO_DE_PAGINAS, textoDaFaixa, threadDoLedger, threadsSemLeitura, umaLinha,
17} from './ritual'
18
19const MOD = 'ork-ritual'
20const PAINEL = 'ritual'
21const TITULO = 'Ritual do MASTER'
22const QUADRO = { plugin: 'ork-ritual', key: 'quadro' } as const
23const FOCO = { plugin: 'ork-ritual', key: 'foco' } as const
24const PASSO = { plugin: 'ork-ritual', key: 'passo' } as const
25const PROVA = { plugin: 'ork-ritual', key: 'prova' } as const
26const NOTAS = { plugin: 'ork-ritual', key: 'notas' } as const
27const RESERVAS = { plugin: 'ork-ritual', key: 'reservas' } as const
28/** O tique: idade das entregas e registro do mostrado. */
29const PERIODO_DO_TIQUE_MS = 60000
30/** As entregas sao relidas por idade: a cada 5 min com o painel a vista, a cada 15 min escondido. */
31const IDADE_A_VISTA_MS = 5 * 60000
32const IDADE_ESCONDIDO_MS = 15 * 60000
33/**
34 * Entre duas leituras que o dono nao pediu (ledger que mudou, idade), no minimo isto com o painel a vista; escondido, a
35 * idade de 15 min. Cada leitura rele todas as paginas da secao `master` do retrato, e cada pagina le os ledgers de todas
36 * as threads: a faixa nao precisa de mais que isso, e a entrega anda em minutos, nao em segundos.
37 */
38const INTERVALO_A_VISTA_MS = 60000
39/** Os ledgers que mudam juntos entram numa leitura so. */
40const ESPERA_DO_LOTE_MS = 5000
41/** Teto de cada chamada ao nucleo (uma pagina do retrato ou as claims de uma thread). */
42const PRAZO_DA_LEITURA_MS = 20000
43/** O servidor do nucleo pode conectar depois do inicio da sessao: o papel e conferido de novo nestas esperas. */
44const ESPERAS_DO_PAPEL_MS = [3000, 10000, 30000]
45const ESPERA_LONGA_DO_PAPEL_MS = 300000
46
47// O que o modulo guarda entre ganchos. O estado em `$.state` (quadro, foco, passo e prova) e o que o desenho le; aqui
48// ficam a copia dele, sempre gravada inteira por `$.state.set` sem versao, uma gravacao por mudanca (licao da Sentinela
49// e da Pista), e o que nao se desenha. Uma recarga do modulo zera tudo, e o `session.start` dispara de novo nela.
50let ativa = false
51let desistiu = false
52let ciclo: { cancel: () => void } | undefined
53let lote: { cancel: () => void } | undefined
54let lendo: Promise<void> | undefined
55let pendente = false
56let tentadaEm = 0
57let quadro: RitualQuadro = QUADRO_VAZIO
58let foco: string | null = null
59let passo = 0
60let prova: RitualProva | null = null
61/** A leitura das claims em curso e de qual thread. */
62let lendoProva: Promise<void> | undefined
63let lendoProvaDe: string | null = null
64/** O "Dar a nota" e a reserva de cada entrega, e as threads com um dialogo ou um pedido rodando no nucleo agora. */
65let notas: Record<string, RitualNota> = {}
66let reservas: Record<string, RitualReserva> = {}
67const emDialogo = new Set<string>()
68const pedindo = new Set<string>()
69/** O que o desenho pos na janela de rolagem e ainda nao foi ao registro da ponte. */
70const vistosNoDesenho = new Map<string, RitualMostrado>()
71/** O que ja foi ao registro nesta sessao, por chave e texto. */
72const jaMostrado = new Set<string>()
73/** O ultimo texto publicado na faixa; null antes da primeira publicacao. */
74let ultimaFaixa: string | undefined | null = null
75/** A falha de um trabalho em segundo plano que nem o quadro conseguiu guardar. */
76let problemaSemAviso: string | null = null
77
78function mensagem(erro: unknown): string {
79 return umaLinha(erro, 200)
80}
81
82/** A leitura com teto proprio: o relogio da sessao recusa a espera que passar dele. */
83function comPrazo<T>($: EngineInterface, leitura: Promise<T>, ms = PRAZO_DA_LEITURA_MS): Promise<T> {
84 return new Promise<T>((resolver, recusar) => {
85 const prazo = $.clock.after(ms, () => recusar(new Error(`a leitura passou de ${Math.round(ms / 1000)} s`)))
86 leitura.then(v => { prazo.cancel(); resolver(v) }, e => { prazo.cancel(); recusar(e) })
87 })
88}
89
90/**
91 * Trabalho em segundo plano (o tique, o lote, os gestos do painel): a falha vai ao quadro; sem como gravar (a sessao
92 * acabou ou o estado recusou), ela fica guardada aqui e o proximo desenho a mostra. O `.catch` nao depende do `$`.
93 */
94function emSegundoPlano($: EngineInterface, trabalho: Promise<unknown>, oQue: string): void {
95 trabalho.catch(async erro => {
96 const texto = `${oQue} parou: ${mensagem(erro)}`
97 try {
98 await gravarQuadro($, { ...quadro, erro: texto })
99 } catch (erroDoAviso) {
100 problemaSemAviso = `${texto} (o aviso também falhou: ${mensagem(erroDoAviso)})`
101 }
102 })
103}
104
105async function gravarQuadro($: EngineInterface, novo: RitualQuadro): Promise<void> {
106 quadro = novo
107 await $.state.set(QUADRO, novo)
108}
109
110/**
111 * O papel pela ponte. `null` so quando o `$.ork` nao existe (a ponte nao carregou); uma falha da propria ponte conta
112 * como dormente e o papel e conferido de novo depois.
113 */
114async function papelDaSessao($: EngineInterface): Promise<string | null> {
115 try {
116 return String(await $.ork.papel())
117 } catch (erro) {
118 return erro instanceof TypeError ? null : 'dormente'
119 }
120}
121
122/** Liga o Ritual na sessao do dono: o `/nota`, o tique e a primeira leitura. */
123async function ativar($: EngineInterface): Promise<void> {
124 if (ativa) return
125 ativa = true
126 try {
127 await $.command.register({ name: 'nota', description: 'Abre o Ritual do MASTER: as entregas que esperam a sua nota, com a prova de cada uma', immediate: true })
128 } catch (erro) {
129 // Sem o /nota o painel nao abre: o Ritual volta a dormente e tenta de novo na espera longa.
130 ativa = false
131 $.clock.after(ESPERA_LONGA_DO_PAPEL_MS, () => { emSegundoPlano($, conferirPapel($, ESPERAS_DO_PAPEL_MS.length), 'a conferência do papel') })
132 throw erro
133 }
134 quadro = (await $.state.get(QUADRO)).value ?? QUADRO_VAZIO
135 foco = (await $.state.get(FOCO)).value ?? null
136 passo = (await $.state.get(PASSO)).value ?? 0
137 prova = (await $.state.get(PROVA)).value ?? null
138 // Depois de uma recarga do modulo, nenhum dialogo nem pedido esta rodando: o que ficou em curso volta a ter o botao.
139 notas = soltarAbrindo((await $.state.get(NOTAS)).value ?? {})
140 await $.state.set(NOTAS, notas)
141 const guardadas = (await $.state.get(RESERVAS)).value ?? {}
142 reservas = Object.fromEntries(Object.entries(guardadas).map(([t, r]) => [t, r.estado === 'pedindo'
143 ? { ...r, estado: 'erro' as const, texto: 'o pedido pelo Telegram foi interrompido por uma recarga; peça de novo' } : r]))
144 await $.state.set(RESERVAS, reservas)
145 ciclo?.cancel()
146 ciclo = $.clock.every(PERIODO_DO_TIQUE_MS, () => { emSegundoPlano($, tique($), 'o tique do Ritual') })
147 agendar($, 1)
148}
149
150/** Confere o papel: liga no dono, desiste na fase ou sem a ponte, e tenta de novo enquanto dormente. */
151async function conferirPapel($: EngineInterface, tentativa: number): Promise<void> {
152 if (ativa || desistiu) return
153 const papel = await papelDaSessao($)
154 if (papel === 'dono') return ativar($)
155 if (papel === 'fase' || papel === null) {
156 desistiu = true
157 if (papel === null) $.ui.toast('ritual: a ponte ork-ponte não carregou; o Ritual do MASTER fica desligado nesta sessão')
158 return
159 }
160 const espera = ESPERAS_DO_PAPEL_MS[tentativa] ?? ESPERA_LONGA_DO_PAPEL_MS
161 $.clock.after(espera, () => { emSegundoPlano($, conferirPapel($, tentativa + 1), 'a conferência do papel') })
162}
163
164/** Uma leitura daqui a pouco, uma por vez: os ledgers que mudam juntos entram nela. */
165function agendar($: EngineInterface, ms = ESPERA_DO_LOTE_MS): void {
166 if (lote) return
167 lote = $.clock.after(Math.max(1, ms), () => {
168 lote = undefined
169 emSegundoPlano($, atualizar($), 'a leitura das entregas')
170 })
171}
172
173/** O painel do Ritual esta posto e a vista do dono. */
174async function painelAVista($: EngineInterface): Promise<boolean> {
175 const paineis = await $.ui.panes().catch(() => [])
176 return paineis.some(p => p.id === PAINEL && p.isPlaced && p.isShown)
177}
178
179/**
180 * Uma leitura por vez; pedido que chega no meio volta a rodar logo depois. Fora do pedido do dono (`forcar`), a leitura
181 * respeita o intervalo: com o painel a vista, volta a ser agendada para quando ele vencer; escondido, espera o tique da
182 * idade.
183 */
184async function atualizar($: EngineInterface, forcar = false): Promise<void> {
185 if (!ativa) return
186 const agora = await $.clock.now()
187 if (!forcar && tentadaEm > 0) {
188 const aVista = await painelAVista($)
189 const espera = (aVista ? INTERVALO_A_VISTA_MS : IDADE_ESCONDIDO_MS) - (agora - tentadaEm)
190 if (espera > 0) {
191 if (aVista) agendar($, espera)
192 return
193 }
194 }
195 if (lendo) {
196 pendente = true
197 return lendo
198 }
199 lendo = (async () => {
200 try {
201 do {
202 pendente = false
203 await lerUmaVez($)
204 } while (pendente)
205 } finally {
206 lendo = undefined
207 }
208 })()
209 return lendo
210}
211
212/**
213 * As entregas pelo retrato da ponte, pagina a pagina da secao `master` (D2): a que espera a nota do dono, na janela de
214 * dias, sem a que teve a nota registrada nesta sessao (a pagina guardada pela ponte pode ainda lista-la). A falha fica
215 * no quadro, com a leitura anterior.
216 */
217async function lerUmaVez($: EngineInterface): Promise<void> {
218 tentadaEm = await $.clock.now()
219 const itens: unknown[] = []
220 const lacunas: string[] = []
221 const avisos: string[] = []
222 let offset = 0
223 try {
224 for (let pagina = 0; ; pagina++) {
225 if (pagina >= TETO_DE_PAGINAS) {
226 avisos.push(`a seção master passou de ${TETO_DE_PAGINAS} páginas; o resto ficou de fora`)
227 break
228 }
229 const p = paginaDoMaster(await comPrazo($, $.ork.retrato({ secao: 'master', offset })))
230 itens.push(...p.itens)
231 lacunas.push(...p.lacunas)
232 if (p.proxima === null || p.proxima <= offset) break
233 offset = p.proxima
234 }
235 } catch (erro) {
236 await gravarQuadro($, { ...quadro, erro: mensagem(erro) })
237 await publicarFaixa($)
238 return
239 }
240 const agora = await $.clock.now()
241 const { entregas, fora, alemDoTeto } = entregasDaJanela(itens, agora)
242 avisos.unshift(...avisosDasLacunas(lacunas))
243 if (alemDoTeto) avisos.push(`${plural(alemDoTeto, 'entrega além do teto do painel ficou', 'entregas além do teto do painel ficaram')} de fora (ork master)`)
244 await gravarQuadro($, { entregas: semNotaDada(entregas, notas), lidoEm: agora, erro: null, avisos, foraDaJanela: fora,
245 semLeitura: threadsSemLeitura(lacunas) })
246 await publicarFaixa($)
247 await registrarMostrados($)
248}
249
250/** As claims da thread em foco pelo `ork_claims_list`, uma leitura por vez; a da thread que saiu do foco e descartada. */
251async function lerProva($: EngineInterface, thread: string): Promise<void> {
252 if (lendoProva) {
253 if (lendoProvaDe === thread) return lendoProva
254 await lendoProva.catch(() => undefined)
255 }
256 lendoProvaDe = thread
257 lendoProva = (async () => {
258 let nova: RitualProva
259 try {
260 const { claims, retiradas } = claimsDaProva(jsonDoMcp(await comPrazo($, $.mcp.call(SERVIDOR, 'ork_claims_list', argsDasClaims(thread)))))
261 nova = { thread, claims, retiradas, lidoEm: await $.clock.now(), erro: null }
262 } catch (erro) {
263 const anterior = prova?.thread === thread ? prova : null
264 nova = { thread, claims: anterior?.claims ?? [], retiradas: anterior?.retiradas ?? 0, lidoEm: await $.clock.now(), erro: mensagem(erro) }
265 }
266 if (foco !== thread) return
267 prova = nova
268 passo = Math.min(passo, Math.max(0, nova.claims.length - 1))
269 await $.state.set(PROVA, prova)
270 await $.state.set(PASSO, passo)
271 })()
272 try {
273 await lendoProva
274 } finally {
275 lendoProva = undefined
276 lendoProvaDe = null
277 }
278}
279
280/** Abre o replay de uma thread: o foco e o passo agora; as claims, em segundo plano. */
281async function focar($: EngineInterface, thread: string): Promise<void> {
282 if (foco !== thread || prova?.thread !== thread) {
283 foco = thread
284 passo = 0
285 prova = null
286 await $.state.set(FOCO, foco)
287 await $.state.set(PASSO, passo)
288 await $.state.set(PROVA, prova)
289 }
290 emSegundoPlano($, lerProva($, thread), 'a leitura das claims')
291}
292
293/** Fecha o replay: volta a lista das entregas. */
294async function soltarFoco($: EngineInterface): Promise<void> {
295 if (foco === null) return
296 foco = null
297 passo = 0
298 prova = null
299 await $.state.set(FOCO, foco)
300 await $.state.set(PASSO, passo)
301 await $.state.set(PROVA, prova)
302}
303
304/** Anda uma claim para tras ou para frente no replay, dentro da lista lida. */
305async function andar($: EngineInterface, delta: number): Promise<void> {
306 if (!prova || prova.thread !== foco || !prova.claims.length) return
307 const novo = Math.min(Math.max(0, passo + delta), prova.claims.length - 1)
308 if (novo === passo) return
309 passo = novo
310 await $.state.set(PASSO, passo)
311}
312
313async function gravarNota($: EngineInterface, thread: string, n: RitualNota): Promise<void> {
314 notas = { ...notas, [thread]: n }
315 await $.state.set(NOTAS, notas)
316}
317
318async function gravarReserva($: EngineInterface, thread: string, r: RitualReserva): Promise<void> {
319 reservas = { ...reservas, [thread]: r }
320 await $.state.set(RESERVAS, reservas)
321}
322
323/**
324 * O "Dar a nota" (T8.3, DD2): um dialogo por thread, em segundo plano. A chamada leva exatamente o `threadId`; quem da a
325 * nota e o porque e o dono, no dialogo nativo do host, e quem grava e o nucleo. A entrega anotada sai da lista e da
326 * faixa assim que o nucleo responde, e a releitura logo depois confirma.
327 */
328async function darNota($: EngineInterface, thread: string): Promise<void> {
329 if (emDialogo.has(thread)) return
330 // Marcado antes de qualquer espera: dois toques ao mesmo tempo abrem um dialogo so.
331 emDialogo.add(thread)
332 try {
333 exigirThread(thread)
334 await gravarNota($, thread, { estado: 'abrindo', texto: '', em: await $.clock.now() })
335 let n: RitualNota
336 try {
337 n = resultadoDaNota(jsonDoMcp(await comPrazo($, $.mcp.call(SERVIDOR, 'ork_request_master_note', argsDaNota(thread)), PRAZO_DA_NOTA_MS)),
338 thread, await $.clock.now())
339 } catch (erro) {
340 n = notaDoErro(erro, await $.clock.now())
341 }
342 await gravarNota($, thread, n)
343 if (n.estado === 'registrada' || n.estado === 'repetida') {
344 await gravarQuadro($, { ...quadro, entregas: semNotaDada(quadro.entregas, notas) })
345 await publicarFaixa($)
346 }
347 } finally {
348 emDialogo.delete(thread)
349 }
350 await atualizar($, true)
351}
352
353/**
354 * A reserva pelo Telegram (T8.3, D7): so no clique do dono, o pedido de nota do nucleo (`ork master pedir`), que guarda
355 * o pedido e devolve o codigo. O mod nunca roda `ork master` com nota no argv.
356 */
357async function pedirTelegram($: EngineInterface, thread: string): Promise<void> {
358 if (pedindo.has(thread)) return
359 pedindo.add(thread)
360 try {
361 exigirThread(thread)
362 await gravarReserva($, thread, { estado: 'pedindo', texto: '', codigo: null, em: await $.clock.now() })
363 let r: RitualReserva
364 try {
365 r = lerReserva(await $.process.run(argvDaReserva(thread), { timeoutMs: PRAZO_DA_RESERVA_MS }), thread, await $.clock.now())
366 } catch (erro) {
367 r = { estado: 'erro', texto: `o pedido pelo Telegram falhou: ${mensagem(erro)}`, codigo: null, em: await $.clock.now() }
368 }
369 await gravarReserva($, thread, r)
370 } finally {
371 pedindo.delete(thread)
372 }
373 await registrarMostrados($)
374}
375
376/** O tique: a idade das entregas e o registro do que o painel mostrou. */
377async function tique($: EngineInterface): Promise<void> {
378 if (!ativa) return
379 const agora = await $.clock.now()
380 const aVista = await painelAVista($)
381 const idade = quadro.lidoEm === null ? Number.POSITIVE_INFINITY : agora - quadro.lidoEm
382 if (idade >= (aVista ? IDADE_A_VISTA_MS : IDADE_ESCONDIDO_MS)) await atualizar($)
383 await registrarMostrados($)
384}
385
386/** A linha da faixa pela ponte, so quando o texto muda; sem entrega que espere a nota, a linha sai. */
387async function publicarFaixa($: EngineInterface): Promise<void> {
388 const texto = textoDaFaixa(quadro)
389 if (texto === ultimaFaixa) return
390 try {
391 if (texto === undefined) await $.ork.faixa({ mod: MOD })
392 else await $.ork.faixa({ mod: MOD, texto })
393 ultimaFaixa = texto
394 } catch {
395 // A ponte recusou ou falhou: a proxima leitura tenta de novo.
396 }
397}
398
399/** O desenho anota o que pos na janela; quem registra e o tique ou a leitura, fora do desenho. */
400function anotarVistos(vistos: readonly RitualMostrado[]): void {
401 for (const m of vistos) vistosNoDesenho.set(`${m.chave}|${m.texto}`, m)
402}
403
404/**
405 * O que o desenho pos na janela de rolagem do painel posto e a vista vai ao registro da ponte (M5 do dono), uma vez por
406 * chave e texto enquanto o texto existir no painel: o Telegram nao repete o que o dono ja viu no terminal.
407 */
408async function registrarMostrados($: EngineInterface): Promise<void> {
409 const existentes = new Set(linhasDoPainel(quadro, { agora: 0, foco, prova, passo, notas, reservas })
410 .flatMap(l => (l.mostrado ? [`${l.mostrado.chave}|${l.mostrado.texto}`] : [])))
411 for (const vista of [...jaMostrado]) if (!existentes.has(vista)) jaMostrado.delete(vista)
412 for (const vista of [...vistosNoDesenho.keys()]) if (!existentes.has(vista)) vistosNoDesenho.delete(vista)
413 if (vistosNoDesenho.size === 0) return
414 // Com o painel escondido, o que foi anotado e descartado: o proximo desenho a vista anota de novo.
415 if (!await painelAVista($)) { vistosNoDesenho.clear(); return }
416 for (const [vista, m] of [...vistosNoDesenho]) {
417 vistosNoDesenho.delete(vista)
418 if (jaMostrado.has(vista)) continue
419 try {
420 await $.ork.mostrado({ mod: MOD, canal: 'painel', chave: m.chave, thread: m.thread, texto: m.texto })
421 jaMostrado.add(vista)
422 } catch {
423 // A ponte recusou ou falhou: volta para a proxima vez.
424 vistosNoDesenho.set(vista, m)
425 }
426 }
427}
428
429export const register: Register = on => {
430 on('session.start', async ($, e, next) => {
431 if (e.isInteractive) await conferirPapel($, 0)
432 return next(e)
433 })
434
435 // `/nota [thread]` abre o painel em qualquer largura e rele as entregas agora. Com uma thread (a Torre abre o Ritual
436 // assim), o painel abre no replay da prova dela. O comando so existe com o Ritual ligado; a conferencia do papel
437 // aqui so cobre o caso raro de o comando ter sobrado de uma carga anterior.
438 on('command.run', { command: 'nota' }, async ($, e) => {
439 if (!ativa && !desistiu) await conferirPapel($, ESPERAS_DO_PAPEL_MS.length)
440 if (!ativa) return { text: 'O Ritual do MASTER só abre na sessão do dono de um projeto do Orkastery.' }
441 const pedida = typeof e.args === 'string' ? e.args.trim() : ''
442 let thread: string | null = null
443 let aviso = ''
444 if (pedida) {
445 try {
446 thread = exigirThread(pedida)
447 } catch {
448 aviso = ` O id ${umaLinha(pedida, 80)} não é de thread; o painel abre na lista das entregas.`
449 }
450 }
451 if (thread) await focar($, thread)
452 else await soltarFoco($)
453 const aberto = await $.ui.open({ id: PAINEL, title: TITULO })
454 emSegundoPlano($, atualizar($, true), 'a leitura das entregas')
455 const onde = thread ? ` na prova da ${thread}` : ''
456 return { text: (aberto.isPlaced ? `Ritual do MASTER aberto${onde}.` : `Ritual do MASTER aberto${onde}, à espera de lugar: ${aberto.reason}`) + aviso }
457 })
458
459 // O ledger de uma thread mudou (entrega, MASTER, nota): a releitura entra no proximo lote. Os caminhos vigiados sao
460 // os que a ponte pede para o papel dono; o Ritual nao pede outros.
461 on('classic.FileChanged', async ($, e, next) => {
462 if (ativa && threadDoLedger(e.file_path) !== null) agendar($)
463 return next(e)
464 }).catch(($, e, next) => next(e))
465
466 // O painel: a lista das entregas ou o replay da prova. O desenho nunca escreve: so anota o que pos na janela de
467 // rolagem, para o registro do mostrado.
468 on('ui.render', { component: 'Pane', requestId: PAINEL }, async ($, e) => {
469 const { Box, Text, Button } = $.ui.resolve(e)
470 const q = (await $.state.get(QUADRO)).value ?? QUADRO_VAZIO
471 const emFoco = (await $.state.get(FOCO)).value ?? null
472 const p = (await $.state.get(PASSO)).value ?? 0
473 const pr = (await $.state.get(PROVA)).value ?? null
474 const ns = (await $.state.get(NOTAS)).value ?? {}
475 const rs = (await $.state.get(RESERVAS)).value ?? {}
476 const agora = await $.clock.now()
477 const comProblema: RitualQuadro = problemaSemAviso ? { ...q, avisos: [...q.avisos, problemaSemAviso] } : q
478 const linhas = linhasDoPainel(comProblema, { agora, foco: emFoco, prova: pr, passo: p, notas: ns, reservas: rs })
479 anotarVistos(mostradosVisiveis(linhas, e.props.scroll, e.props.bodyColumns))
480 return desenharRitual({ Box, Text, Button }, linhas, {
481 foco: emFoco,
482 atualizar: () => {
483 emSegundoPlano($, atualizar($, true), 'a releitura das entregas')
484 if (emFoco) emSegundoPlano($, lerProva($, emFoco), 'a releitura das claims')
485 },
486 verProva: thread => { emSegundoPlano($, focar($, thread), 'o Ver a prova') },
487 voltar: () => { emSegundoPlano($, soltarFoco($), 'o Voltar') },
488 anterior: () => { emSegundoPlano($, andar($, -1), 'o Anterior') },
489 proxima: () => { emSegundoPlano($, andar($, 1), 'o Próxima') },
490 darNota: thread => { emSegundoPlano($, darNota($, thread), 'o Dar a nota') },
491 telegram: thread => { emSegundoPlano($, pedirTelegram($, thread), 'o pedido pelo Telegram') },
492 })
493 })
494}
495hooks/nota.ts 89 lines1// O que o Ritual do MASTER pede ao nucleo para a nota (RM-059, fatia F8, T8.3), em funcoes puras. Sao dois caminhos, e
2// em nenhum o mod da a nota pelo dono:
3// - "Dar a nota" chama `ork_request_master_note` com exatamente `{ threadId }`: o nucleo abre o dialogo nativo do host,
4// o dono escolhe de 0 a 5 e escreve o porque, e o nucleo grava o MASTER com o recibo do ingresso local. Nenhum
5// argumento leva nota, porque ou texto.
6// - "Pelo Telegram" (a reserva, so no clique do dono, D7) roda `ork master pedir <thread> --formato json`: o nucleo guarda
7// o pedido de nota e devolve o codigo que o dono responde pelo Telegram. O pedido nao responde nada.
8// So importa tipos.
9import type { RitualNota, RitualReserva } from '../types'
10import { exigirThread, umaLinha } from './ritual'
11
12export const FERRAMENTA_DA_NOTA = 'ork_request_master_note'
13/**
14 * O dialogo do nucleo fecha em ate 300 s, a gravacao espera a trava da thread por ate 60 s e a resposta sai depois da
15 * soltura ao fechar (o git das reservas e o runtime); o botao espera tudo isso, com folga.
16 */
17export const PRAZO_DA_NOTA_MS = 430000
18/** O pedido pelo Telegram so grava o pedido de nota e devolve o codigo. */
19export const PRAZO_DA_RESERVA_MS = 60000
20
21/** Os argumentos do "Dar a nota": so o id da thread, nada que seja nota. */
22export function argsDaNota(thread: string): { threadId: string } {
23 return { threadId: exigirThread(thread) }
24}
25
26/** O argv da reserva pelo Telegram, montado so aqui: o pedido de nota do nucleo, nunca a nota. */
27export function argvDaReserva(thread: string): string[] {
28 return ['ork', 'master', 'pedir', exigirThread(thread), '--formato', 'json']
29}
30
31/** O que o cartao diz do "Dar a nota", a partir do que o nucleo devolveu (`{ ok, estado, repetida, score }`). */
32export function resultadoDaNota(resposta: unknown, thread: string, agora: number): RitualNota {
33 const r = (resposta && typeof resposta === 'object' ? resposta : {}) as { ok?: unknown; estado?: unknown; repetida?: unknown; score?: unknown }
34 const nota = typeof r.score === 'number' && Number.isInteger(r.score) ? `${r.score}/5` : 'a nota'
35 if (r.ok === true && r.estado === 'registrada') {
36 return r.repetida === true
37 ? { estado: 'repetida', texto: `a ${thread} já tinha nota humana ${nota} no núcleo`, em: agora }
38 : { estado: 'registrada', texto: `nota ${nota} da ${thread} registrada pelo diálogo nativo`, em: agora }
39 }
40 return { estado: 'pendente', texto: 'o diálogo fechou sem nota; a entrega segue esperando', em: agora }
41}
42
43/**
44 * O que o cartao diz quando o "Dar a nota" falhou: sem dialogo nativo (ou sem a ferramenta) a reserva aparece; a thread
45 * que seguiu ocupada depois da resposta volta a ter o botao, sem reserva.
46 */
47export function notaDoErro(erro: unknown, agora: number): RitualNota {
48 const motivo = umaLinha(erro)
49 if (/master\.nota\.trava-ocupada/.test(motivo)) {
50 return { estado: 'pendente', texto: 'a thread seguiu ocupada e a nota não foi gravada; dê de novo', em: agora }
51 }
52 if (/master\.nota\.sem-dialogo/.test(motivo)) {
53 return { estado: 'sem-dialogo', texto: 'o diálogo nativo não abriu: este cliente MCP não abre formulário; peça a nota pelo Telegram', em: agora }
54 }
55 if (/mcp\.tool\.unknown/.test(motivo)) {
56 return { estado: 'sem-dialogo', texto: 'o núcleo desta sessão não tem a ferramenta da nota; atualize o Orkastery ou peça a nota pelo Telegram', em: agora }
57 }
58 return { estado: 'erro', texto: `o Dar a nota falhou: ${motivo}; peça a nota pelo Telegram`, em: agora }
59}
60
61/** A reserva aparece quando o dialogo nao serviu (sem dialogo nativo ou erro do nucleo). */
62export function pedeReserva(nota: RitualNota | undefined): boolean {
63 return nota?.estado === 'sem-dialogo' || nota?.estado === 'erro'
64}
65
66/** Depois de uma recarga do modulo nenhum dialogo esta em curso: o que ficou `abrindo` volta a ter o botao. */
67export function soltarAbrindo(notas: Readonly<Record<string, RitualNota>>): Record<string, RitualNota> {
68 return Object.fromEntries(Object.entries(notas).map(([t, n]) => [t, n.estado === 'abrindo'
69 ? { estado: 'pendente' as const, texto: 'o diálogo ficou sem resposta (o Ritual recarregou); a entrega segue esperando', em: n.em } : n]))
70}
71
72/** A saida do `ork master pedir --formato json`: o codigo que o dono responde pelo Telegram, ou o motivo da falha. */
73export function lerReserva(saida: { exitCode: number; stdout: string; stderr: string }, thread: string, agora: number): RitualReserva {
74 if (saida.exitCode !== 0) {
75 const fim = umaLinha(saida.stderr.trim().split('\n').slice(-2).join(' ') || `saiu ${saida.exitCode}`)
76 return { estado: 'erro', texto: `o pedido pelo Telegram falhou: ${fim}`, codigo: null, em: agora }
77 }
78 let p: { contrato?: unknown; thread?: unknown; codigo?: unknown }
79 try {
80 p = JSON.parse(saida.stdout) as typeof p
81 } catch {
82 return { estado: 'erro', texto: 'o pedido pelo Telegram não devolveu o contrato ork.master-nota/v1', codigo: null, em: agora }
83 }
84 if (!p || p.contrato !== 'ork.master-nota/v1' || p.thread !== thread || typeof p.codigo !== 'string' || !/^[0-9A-Za-z]{3,8}$/.test(p.codigo)) {
85 return { estado: 'erro', texto: 'o pedido pelo Telegram não devolveu o contrato ork.master-nota/v1', codigo: null, em: agora }
86 }
87 return { estado: 'pronta', texto: `pelo Telegram, responda: ${p.codigo} <0 a 5> <porquê>`, codigo: p.codigo, em: agora }
88}
89hooks/replay.tsx 272 lines1// O painel do Ritual do MASTER (RM-059, fatia F8, T8.2): a lista das entregas que esperam a nota do dono e o replay da
2// prova de uma delas, uma claim por vez, como o replay-theater percorre edicoes. Modulo puro: monta as linhas e a
3// arvore com os elementos que a superficie resolveu; o `$` fica no register.tsx. Estado sempre com rotulo e glifo,
4// nunca so cor; "espera sua nota" e o ponto de HITL do cartao; numero vem com o comando que o prova (Partitura).
5import type { RitualEntrega, RitualMostrado, RitualNota, RitualProva, RitualQuadro, RitualReserva } from '../types'
6import { pedeReserva } from './nota'
7import { JANELA_DIAS, numero, plural } from './ritual'
8
9/** Os botoes de uma entrega, na linha de acoes do cartao. */
10export type BotaoDoCartao = 'prova' | 'nota' | 'telegram'
11
12/** Uma linha do painel: o tipo decide o elemento; `mostrado` e o que o registro da ponte guarda (M5). */
13export type LinhaDoPainel = {
14 tipo: 'placar' | 'aviso' | 'feita' | 'vazio' | 'cabecalho' | 'situacao' | 'indice' | 'postmortem' | 'acoes' | 'nota' | 'reserva'
15 | 'prova-placar' | 'claim' | 'alegacao' | 'comando' | 'navegacao'
16 texto: string
17 thread?: string
18 /** So na linha de acoes: os botoes que ela tem, na ordem. */
19 botoes?: BotaoDoCartao[]
20 mostrado?: RitualMostrado
21}
22
23/** Os rotulos dos botoes do cartao. */
24export const ROTULO_DO_BOTAO: Readonly<Record<BotaoDoCartao, string>> = { prova: 'Ver a prova', nota: 'Dar a nota', telegram: 'Pelo Telegram' }
25
26/** O glifo e o rotulo de cada estado de claim que o nucleo registra. */
27export const ESTADO_DA_CLAIM: Readonly<Record<string, { glifo: string; rotulo: string }>> = {
28 verificado: { glifo: '✓', rotulo: 'verificada' },
29 reprovado: { glifo: '×', rotulo: 'reprovada' },
30 pendente: { glifo: '·', rotulo: 'pendente' },
31 'nao-verificavel': { glifo: '?', rotulo: 'não verificável' },
32}
33
34/** Quanto tempo faz a leitura, em palavras curtas. */
35export function haQuanto(desde: number | null, agora: number): string {
36 if (desde === null) return 'ainda não lido'
37 const s = Math.max(0, Math.round((agora - desde) / 1000))
38 if (s < 60) return `lido há ${s} s`
39 const min = Math.round(s / 60)
40 return min < 60 ? `lido há ${min} min` : `lido há ${Math.round(min / 60)} h`
41}
42
43/** A situacao da nota na entrega, em uma linha. */
44export function situacaoDaEntrega(e: RitualEntrega): string {
45 if (e.status === 'recorded') return `aceita por omissão${e.score === null ? '' : ` com ${e.score}/5`}; a sua nota sobrescreve`
46 if (e.status === 'ratificacao-pendente') return 'proposta da fábrica esperando a sua nota'
47 return 'sem nota: sem a sua, o MASTER fecha por omissão'
48}
49
50/** O cartao de uma entrega: cabecalho, situacao, indice e POSTMORTEM. */
51export function linhasDaEntrega(e: RitualEntrega): LinhaDoPainel[] {
52 const quando = e.entregueEmLocal ?? e.entregueEm
53 const merge = e.merge ? ` · merge ${e.merge.slice(0, 8)}` : e.artefato ? ` · artefato ${e.artefato}` : ''
54 const indice = e.indice === null ? 'índice derivado do ledger: lacuna (ork master)'
55 : `índice derivado do ledger: ${numero(e.indice)} de 5 (${e.parcelas || 'sem desconto'}) · ork master`
56 const reprovacoes = plural(e.reprovacoes, 'reprovação tipada', 'reprovações tipadas')
57 return [
58 { tipo: 'cabecalho', thread: e.thread, texto: `● espera sua nota · ${e.thread} · entregue ${quando}${merge}` },
59 { tipo: 'situacao', thread: e.thread, texto: ` ${situacaoDaEntrega(e)}` },
60 { tipo: 'indice', thread: e.thread, texto: ` ${indice}`,
61 mostrado: { chave: `nota:${e.thread}`, thread: e.thread,
62 texto: `${e.thread}: entregue, espera sua nota${e.indice === null ? '' : ` (índice ${numero(e.indice)} de 5)`}` } },
63 // Na proposta, as classes sao as que a fabrica propos; a nota do dono grava as que o nucleo infere dos gates, como
64 // no terminal e no Telegram (D14).
65 { tipo: 'postmortem', thread: e.thread, texto: e.classes
66 ? ` POSTMORTEM${e.status === 'ratificacao-pendente' ? ' da proposta' : ''}: classes ${e.classes} · ${reprovacoes}`
67 : ` POSTMORTEM: nasce no fechamento · ${reprovacoes}` },
68 ]
69}
70
71/**
72 * A linha de acoes e o resultado do "Dar a nota" e da reserva de uma entrega (T8.3). O "Dar a nota" some com o dialogo
73 * aberto e com a nota ja registrada; a reserva pelo Telegram so aparece quando o dialogo nao serviu (D7).
74 */
75export function linhasDaNota(thread: string, opcoes: { comProva: boolean; nota?: RitualNota; reserva?: RitualReserva }): LinhaDoPainel[] {
76 const { nota, reserva } = opcoes
77 const botoes: BotaoDoCartao[] = []
78 if (opcoes.comProva) botoes.push('prova')
79 if (!nota || !['abrindo', 'registrada', 'repetida'].includes(nota.estado)) botoes.push('nota')
80 if (pedeReserva(nota) && reserva?.estado !== 'pedindo') botoes.push('telegram')
81 const saida: LinhaDoPainel[] = []
82 if (botoes.length) saida.push({ tipo: 'acoes', thread, botoes, texto: botoes.map(b => ROTULO_DO_BOTAO[b]).join(' | ') })
83 if (nota) saida.push({ tipo: 'nota', thread, texto: ` ${nota.estado === 'abrindo' ? 'diálogo da nota aberto no Claude Code: responda lá' : nota.texto}` })
84 if (reserva) {
85 saida.push({ tipo: 'reserva', thread, texto: ` ${reserva.estado === 'pedindo' ? 'pedindo o código ao núcleo...' : reserva.texto}`,
86 ...(reserva.estado === 'pronta' && reserva.codigo
87 ? { mostrado: { chave: `nota:${thread}`, thread, texto: `${thread}: pelo Telegram, responda ${reserva.codigo} <0 a 5> <porquê>` } } : {}) })
88 }
89 return saida
90}
91
92/** As notas desta sessao que ja sairam da lista (registradas ou ja dadas), as mais novas primeiro, no maximo tres. */
93export function notasFeitas(q: RitualQuadro, notas: Readonly<Record<string, RitualNota>>): LinhaDoPainel[] {
94 const naLista = new Set(q.entregas.map(e => e.thread))
95 return Object.entries(notas).filter(([t, n]) => (n.estado === 'registrada' || n.estado === 'repetida') && !naLista.has(t))
96 .sort((a, b) => b[1].em - a[1].em).slice(0, 3)
97 .map(([thread, n]) => ({ tipo: 'feita' as const, thread, texto: `✓ ${n.texto}` }))
98}
99
100/** As linhas do replay: a claim do passo, com estado, arquivo, alegacao e comandos; nunca a saida do comando (D6). */
101export function linhasDaProva(foco: string, prova: RitualProva | null, passo: number): LinhaDoPainel[] {
102 if (!prova || prova.thread !== foco || (prova.lidoEm === null && !prova.erro)) return [{ tipo: 'vazio', thread: foco, texto: 'lendo as claims no núcleo...' }]
103 const saida: LinhaDoPainel[] = []
104 if (prova.erro) saida.push({ tipo: 'aviso', thread: foco, texto: `as claims não vieram: ${prova.erro}` })
105 if (!prova.claims.length) {
106 if (!prova.erro) saida.push({ tipo: 'vazio', thread: foco, texto: `Nenhuma claim ativa registrada na ${foco}.` })
107 return saida
108 }
109 const i = Math.min(Math.max(0, passo), prova.claims.length - 1)
110 const c = prova.claims[i]!
111 const estado = ESTADO_DA_CLAIM[c.estado] ?? { glifo: '?', rotulo: c.estado }
112 const retiradas = prova.retiradas ? ` · ${plural(prova.retiradas, 'retirada fora', 'retiradas fora')}` : ''
113 saida.push({ tipo: 'prova-placar', thread: foco, texto: ` claim ${i + 1} de ${prova.claims.length}${retiradas} · ork claims list ${foco}` })
114 saida.push({ tipo: 'claim', thread: foco, texto: ` ${estado.glifo} ${c.id} · ${estado.rotulo}${c.arquivo ? ` · ${c.arquivo}` : ''}` })
115 if (c.alegacao) saida.push({ tipo: 'alegacao', thread: foco, texto: ` ${c.alegacao}` })
116 for (const comando of c.comandos) saida.push({ tipo: 'comando', thread: foco, texto: ` $ ${comando}` })
117 if (!c.comandos.length) saida.push({ tipo: 'comando', thread: foco, texto: ' sem comando registrado' })
118 saida.push({ tipo: 'navegacao', thread: foco, texto: `${i > 0 ? 'anterior' : ''}|${i < prova.claims.length - 1 ? 'proxima' : ''}` })
119 return saida
120}
121
122/**
123 * As linhas do painel, na ordem do desenho. Sem foco: o placar, os avisos e um cartao por entrega com "Ver a prova".
124 * Com foco (`/nota <thread>` ou "Ver a prova"): o placar do replay, o cartao da thread e a claim do passo.
125 */
126export function linhasDoPainel(q: RitualQuadro, opcoes: { agora: number; foco?: string | null; prova?: RitualProva | null; passo?: number;
127 notas?: Readonly<Record<string, RitualNota>>; reservas?: Readonly<Record<string, RitualReserva>> }): LinhaDoPainel[] {
128 const saida: LinhaDoPainel[] = []
129 const foco = opcoes.foco ?? null
130 const notas = opcoes.notas ?? {}
131 const reservas = opcoes.reservas ?? {}
132 const quantas = q.semLeitura && !q.entregas.length ? `o retrato não leu ${plural(q.semLeitura, 'thread', 'threads')}`
133 : `${q.semLeitura ? 'pelo menos ' : ''}${plural(q.entregas.length, 'entrega espera', 'entregas esperam')} a sua nota`
134 saida.push({ tipo: 'placar', texto: foco ? `Ritual do MASTER · prova da ${foco}` : `Ritual do MASTER · ${quantas} · ${haQuanto(q.lidoEm, opcoes.agora)}` })
135 if (q.erro) saida.push({ tipo: 'aviso', texto: `a última leitura falhou: ${q.erro}` })
136 for (const aviso of q.avisos) saida.push({ tipo: 'aviso', texto: aviso })
137 saida.push(...notasFeitas(q, notas))
138 if (foco) {
139 const e = q.entregas.find(x => x.thread === foco)
140 if (e) saida.push(...linhasDaEntrega(e), ...linhasDaNota(foco, { comProva: false, nota: notas[foco], reserva: reservas[foco] }))
141 else if (q.lidoEm !== null) saida.push({ tipo: 'aviso', thread: foco, texto: `a ${foco} não está entre as entregas dos últimos ${JANELA_DIAS} dias que esperam a sua nota` })
142 saida.push(...linhasDaProva(foco, opcoes.prova ?? null, opcoes.passo ?? 0))
143 return saida
144 }
145 if (q.foraDaJanela) {
146 saida.push({ tipo: 'aviso', texto: `${plural(q.foraDaJanela, 'entrega sem a sua nota ficou', 'entregas sem a sua nota ficaram')} fora da janela de ${JANELA_DIAS} dias (ork master)` })
147 }
148 if (!q.entregas.length) {
149 saida.push({ tipo: 'vazio', texto: q.lidoEm === null ? 'lendo as entregas no núcleo...'
150 : q.semLeitura ? `Nenhuma das entregas que o retrato leu espera a sua nota nos últimos ${JANELA_DIAS} dias; ork master lista as das threads que ele não leu.`
151 : `Nada espera a sua nota nos últimos ${JANELA_DIAS} dias.` })
152 return saida
153 }
154 for (const e of q.entregas) {
155 saida.push(...linhasDaEntrega(e), ...linhasDaNota(e.thread, { comProva: true, nota: notas[e.thread], reserva: reservas[e.thread] }))
156 }
157 return saida
158}
159
160/**
161 * As linhas da tela que um texto com `wrap="wrap"` ocupa em `colunas` celulas: quebra por palavra, e a palavra maior que
162 * a linha quebra no meio. A `folga` tira colunas da conta: com ela a estimativa sobra.
163 */
164export function linhasDaTela(texto: string, colunas: number | undefined, folga = 1): number {
165 if (typeof colunas !== 'number' || !(colunas > folga)) return 1
166 const largura = colunas - folga
167 let linhas = 1
168 let usadas = 0
169 texto.split(' ').forEach((palavra, i) => {
170 let tamanho = Array.from(palavra).length
171 const espaco = i === 0 ? 0 : 1
172 if (usadas + espaco + tamanho <= largura) {
173 usadas += espaco + tamanho
174 return
175 }
176 if (usadas > 0) {
177 linhas++
178 usadas = 0
179 }
180 while (tamanho > largura) {
181 linhas++
182 tamanho -= largura
183 }
184 usadas = tamanho
185 })
186 return linhas
187}
188
189/** A altura de uma linha do painel na tela, sem e com a folga: so a alegacao quebra; as outras truncam numa so. */
190function alturasNaTela(l: { tipo?: string; texto?: string }, colunas: number | undefined): [number, number] {
191 if (l.tipo !== 'alegacao') return [1, 1]
192 return [linhasDaTela(l.texto ?? '', colunas, 0), linhasDaTela(l.texto ?? '', colunas, 1)]
193}
194
195/**
196 * O que esta inteiro dentro da janela de rolagem do painel: so isso conta como mostrado ao dono (M5). A linha que quebra
197 * tem o topo mais cedo (sem folga) e o mais tarde (com folga): ela so conta com os dois dentro da janela.
198 */
199export function mostradosVisiveis(linhas: ReadonlyArray<{ tipo?: string; texto?: string; mostrado?: RitualMostrado }>,
200 janela: { offset: number; bodyRows: number } | undefined, colunas?: number): RitualMostrado[] {
201 if (!janela || !(janela.bodyRows > 0)) return []
202 const fim = janela.offset + janela.bodyRows
203 const vistos: RitualMostrado[] = []
204 let cedo = 0
205 let tarde = 0
206 for (const l of linhas) {
207 const [menor, maior] = alturasNaTela(l, colunas)
208 if (l.mostrado && cedo >= janela.offset && tarde + maior <= fim) vistos.push(l.mostrado)
209 cedo += menor
210 tarde += maior
211 }
212 return vistos
213}
214
215/** Os gestos do painel: reler, abrir e fechar o replay, andar entre as claims, pedir a nota e a reserva. */
216export type Gestos = {
217 atualizar?: () => void
218 verProva?: (thread: string) => void
219 voltar?: () => void
220 anterior?: () => void
221 proxima?: () => void
222 darNota?: (thread: string) => void
223 telegram?: (thread: string) => void
224}
225
226/**
227 * A arvore do painel com os elementos que a superficie resolveu (no register.tsx), uma linha da tela por elemento: so
228 * `Box`, `Text` e `Button`, no terminal e no desktop.
229 */
230export function desenharRitual(el: { Box: any; Text: any; Button: any }, linhas: readonly LinhaDoPainel[], opcoes: { foco?: string | null } & Gestos) {
231 const { Box, Text, Button } = el
232 return (
233 <Box flexDirection="column">
234 {linhas.map((l, i) => {
235 if (l.tipo === 'placar') {
236 return (
237 <Box key="placar" flexDirection="row">
238 <Text key="placar-texto" bold wrap="truncate">{l.texto}</Text>
239 {opcoes.foco ? <Button key="voltar" label="Voltar" onPress={() => opcoes.voltar?.()} /> : null}
240 <Button key="atualizar" label="Atualizar" onPress={() => opcoes.atualizar?.()} />
241 </Box>
242 )
243 }
244 if (l.tipo === 'cabecalho') return <Text key={`cabecalho:${l.thread}`} bold wrap="truncate">{l.texto}</Text>
245 if (l.tipo === 'acoes' && l.thread) {
246 const thread = l.thread
247 const gesto: Record<BotaoDoCartao, ((t: string) => void) | undefined> = { prova: opcoes.verProva, nota: opcoes.darNota, telegram: opcoes.telegram }
248 return (
249 <Box key={`acoes:${thread}`} flexDirection="row">
250 {(l.botoes ?? []).map(b => <Button key={`${b}:${thread}`} label={ROTULO_DO_BOTAO[b]} onPress={() => gesto[b]?.(thread)} />)}
251 </Box>
252 )
253 }
254 if (l.tipo === 'feita') return <Text key={`feita:${l.thread}`} wrap="truncate">{l.texto}</Text>
255 if (l.tipo === 'navegacao') {
256 const [anterior, proxima] = l.texto.split('|')
257 return (
258 <Box key="navegacao" flexDirection="row">
259 {anterior ? <Button key="anterior" label="Anterior" onPress={() => opcoes.anterior?.()} /> : null}
260 {proxima ? <Button key="proxima" label="Próxima" onPress={() => opcoes.proxima?.()} /> : null}
261 </Box>
262 )
263 }
264 if (l.tipo === 'claim') return <Text key="claim" bold wrap="truncate">{l.texto}</Text>
265 if (l.tipo === 'alegacao') return <Text key="alegacao" wrap="wrap">{l.texto}</Text>
266 if (l.tipo === 'aviso') return <Text key={`aviso:${i}`} color="warning" wrap="truncate">{l.texto}</Text>
267 return <Text key={`${l.tipo}:${l.thread ?? ''}:${i}`} dimColor={l.tipo !== 'comando'} wrap="truncate">{l.texto}</Text>
268 })}
269 </Box>
270 )
271}
272hooks/ritual.ts 219 lines1// O que o Ritual do MASTER le do nucleo (RM-059, fatia F8; ficha M8 do plano dos mods), em funcoes puras: as entregas
2// que esperam a nota do dono, saidas da secao `master` do retrato da ponte (D2 da thread ork-rm059ritual), a
3// linha da faixa e as claims do replay, saidas do `ork_claims_list`. Nada aqui le `.orkastery/` nem chama o `$`: as
4// chamadas ficam no register.tsx. So importa tipos.
5import type { RitualClaim, RitualEntrega, RitualNota, RitualQuadro } from '../types'
6
7/** D1: a entrega dos ultimos 7 dias sem nota humana (sem MASTER, com proposta ou aceita por omissao). */
8export const JANELA_DIAS = 7
9/** A secao `master` vem em paginas de ate 50 itens; mais que isso e retrato quebrado, nao fila. */
10export const TETO_DE_PAGINAS = 20
11/** O painel mostra no maximo isto; o resto vira aviso com a contagem. */
12export const TETO_DE_ENTREGAS = 40
13export const TETO_DE_TEXTO = 200
14export const SERVIDOR = 'orkastery'
15export const QUADRO_VAZIO: RitualQuadro = { entregas: [], lidoEm: null, erro: null, avisos: [], foraDaJanela: 0, semLeitura: 0 }
16
17const ID_DE_THREAD = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/
18const DIA_MS = 86400000
19
20/** Uma linha so, sem caractere de controle e com teto. */
21export function umaLinha(v: unknown, teto = TETO_DE_TEXTO): string {
22 const s = typeof v === 'string' ? v : v instanceof Error ? v.message : String(v)
23 return Array.from(s.replace(/\p{Cc}/gu, ' ').replace(/\s+/g, ' ').trim()).slice(0, teto).join('')
24}
25
26export function plural(n: number, um: string, varios: string): string {
27 return `${n} ${n === 1 ? um : varios}`
28}
29
30export function exigirThread(thread: unknown): string {
31 if (typeof thread !== 'string' || !ID_DE_THREAD.test(thread)) throw new Error('ork-ritual: id de thread invalido')
32 return thread
33}
34
35/** O numero com virgula, como o dono le: 3.75 vira "3,75". */
36export function numero(n: number): string {
37 return String(n).replace('.', ',')
38}
39
40/** O JSON do primeiro bloco de texto de uma resposta MCP; a resposta com erro vira erro com o motivo do nucleo. */
41export function jsonDoMcp(r: { content?: ReadonlyArray<{ type?: string; text?: string }>; isError?: boolean } | null | undefined): unknown {
42 const texto = (r?.content ?? []).find(c => c.type === 'text' && typeof c.text === 'string')?.text
43 if (!r || r.isError) {
44 let motivo = 'o núcleo respondeu com erro'
45 try {
46 const corpo = JSON.parse(texto ?? '') as { erro?: unknown }
47 if (typeof corpo.erro === 'string' && corpo.erro.trim()) motivo = umaLinha(corpo.erro)
48 } catch {
49 // Sem JSON no corpo do erro: fica o motivo generico.
50 }
51 throw new Error(motivo)
52 }
53 if (texto === undefined) throw new Error('o núcleo não devolveu texto')
54 return JSON.parse(texto)
55}
56
57/** Uma pagina da secao `master` do retrato: os itens, a proxima pagina e as lacunas. */
58export function paginaDoMaster(r: unknown): { itens: unknown[]; proxima: number | null; lacunas: string[] } {
59 const secao = (r as { retrato?: { sections?: { master?: unknown } } } | null)?.retrato?.sections?.master as
60 { items?: unknown; gaps?: unknown; coverage?: { nextOffset?: unknown } } | undefined
61 if (!secao || !Array.isArray(secao.items)) throw new Error('o retrato do núcleo não trouxe a seção master')
62 const proxima = secao.coverage?.nextOffset
63 return {
64 itens: secao.items as unknown[],
65 proxima: typeof proxima === 'number' && Number.isInteger(proxima) && proxima >= 0 ? proxima : null,
66 lacunas: Array.isArray(secao.gaps) ? (secao.gaps as unknown[]).filter((g): g is string => typeof g === 'string').map(g => umaLinha(g, 120)) : [],
67 }
68}
69
70const textoOuNulo = (v: unknown, teto = TETO_DE_TEXTO): string | null => (typeof v === 'string' && v.trim() ? umaLinha(v, teto) : null)
71const numeroOuNulo = (v: unknown): number | null => (typeof v === 'number' && Number.isFinite(v) ? v : null)
72
73/**
74 * A entrega de um item da secao `master`, quando ela espera a nota do dono: sem MASTER (`pending`), com proposta
75 * (`ratificacao-pendente`) ou aceita por omissao (`recorded` com regime `omissao`), entregue por `ship_done` ou por
76 * artefato. Nota humana registrada, item sem entrega ou fora do contrato devolvem null.
77 */
78export function entregaDoItem(item: unknown): RitualEntrega | null {
79 const i = (item ?? {}) as { id?: unknown; status?: unknown; facts?: Record<string, unknown> }
80 const m = typeof i.id === 'string' ? /^master:(.+)$/.exec(i.id) : null
81 const thread = m && ID_DE_THREAD.test(m[1] as string) ? m[1] as string : null
82 const f = i.facts ?? {}
83 if (!thread || (i.status !== 'pending' && i.status !== 'ratificacao-pendente' && i.status !== 'recorded')) return null
84 if (i.status === 'recorded' && f.regime !== 'omissao') return null
85 const entregueEm = typeof f.deliveredAt === 'string' && Number.isFinite(Date.parse(f.deliveredAt)) ? f.deliveredAt : null
86 if (!entregueEm) return null
87 return {
88 thread, status: i.status, regime: textoOuNulo(f.regime, 40), score: numeroOuNulo(f.score), indice: numeroOuNulo(f.index),
89 parcelas: textoOuNulo(f.indexDetail) ?? '', reprovacoes: numeroOuNulo(f.reprovals) ?? 0, classes: textoOuNulo(f.classes),
90 entregueEm, entregueEmLocal: textoOuNulo(f.deliveredAtLocal, 80),
91 merge: typeof f.deliveredSha === 'string' && /^[a-f0-9]{40}$/.test(f.deliveredSha) ? f.deliveredSha : null,
92 artefato: textoOuNulo(f.deliveredArtifact, 160),
93 }
94}
95
96/** As entregas da janela, a mais nova primeiro, com o teto do painel; e quantas ficaram fora da janela. */
97export function entregasDaJanela(itens: readonly unknown[], agora: number, dias = JANELA_DIAS): { entregas: RitualEntrega[]; fora: number; alemDoTeto: number } {
98 const todas = itens.map(entregaDoItem).filter((e): e is RitualEntrega => e !== null)
99 const desde = agora - dias * DIA_MS
100 const dentro = todas.filter(e => Date.parse(e.entregueEm) >= desde)
101 .sort((a, b) => Date.parse(b.entregueEm) - Date.parse(a.entregueEm) || (a.thread < b.thread ? -1 : 1))
102 return { entregas: dentro.slice(0, TETO_DE_ENTREGAS), fora: todas.length - dentro.length, alemDoTeto: Math.max(0, dentro.length - TETO_DE_ENTREGAS) }
103}
104
105/** A lacuna que nao falta nada: o corte da pagina por bytes (a proxima pagina traz o resto, e o Ritual le todas). */
106const SO_PAGINACAO = new Set(['maestro.page.byte_limit'])
107const OMITIDAS = /^maestro\.messages\.omitted:(\d+)$/
108const SEM_LEITURA = /^(?:ledger\.unavailable|master\.invalid):([a-zA-Z0-9][a-zA-Z0-9._-]{0,79})$/
109
110/**
111 * As lacunas da secao `master`, somadas de todas as paginas (cada pagina repete as da secao): as threads que o retrato
112 * nao leu (o ledger passou do teto de leitura do Maestro, ou o MASTER log nao vale), quantas lacunas o retrato omitiu
113 * alem das quatro que publica (na secao `master` elas sao por thread) e as outras. O corte da pagina por bytes nao
114 * falta nada.
115 */
116export function lacunasDoMaster(lacunas: readonly string[]): { semLeitura: string[]; omitidas: number; outras: string[] } {
117 const semLeitura: string[] = []
118 const outras: string[] = []
119 let omitidas = 0
120 for (const lacuna of new Set(lacunas)) {
121 if (SO_PAGINACAO.has(lacuna)) continue
122 const omitida = OMITIDAS.exec(lacuna)
123 if (omitida) {
124 omitidas = Math.max(omitidas, Number(omitida[1]))
125 continue
126 }
127 const thread = SEM_LEITURA.exec(lacuna)?.[1]
128 if (thread) {
129 if (!semLeitura.includes(thread)) semLeitura.push(thread)
130 continue
131 }
132 outras.push(lacuna)
133 }
134 return { semLeitura, omitidas, outras }
135}
136
137/** Quantas threads o retrato nao leu: as nomeadas nas lacunas e as omitidas. */
138export function threadsSemLeitura(lacunas: readonly string[]): number {
139 const { semLeitura, omitidas } = lacunasDoMaster(lacunas)
140 return semLeitura.length + omitidas
141}
142
143/** Os avisos da secao que veio incompleta, uma linha cada: as threads que o retrato nao leu e as outras lacunas. */
144export function avisosDasLacunas(lacunas: readonly string[]): string[] {
145 const { semLeitura, omitidas, outras } = lacunasDoMaster(lacunas)
146 const avisos: string[] = []
147 const sem = semLeitura.length + omitidas
148 if (sem) {
149 const nomes = semLeitura.slice(0, 3)
150 const resto = sem - nomes.length
151 const quais = nomes.length ? ` (${nomes.join(', ')}${resto > 0 ? ` e +${resto}` : ''})` : ''
152 avisos.push(`o retrato não leu ${plural(sem, 'thread', 'threads')}${quais}: a lista pode estar incompleta; ork master lista todas`)
153 }
154 if (outras.length) {
155 avisos.push(`o retrato do MASTER veio incompleto: ${plural(outras.length, 'lacuna', 'lacunas')} (${outras.slice(0, 3).join(', ')}${outras.length > 3 ? ', ...' : ''})`)
156 }
157 return avisos
158}
159
160/**
161 * A entrega cuja nota o nucleo registrou nesta sessao (ou que ja tinha nota humana) sai da lista na hora: a pagina que a
162 * ponte guarda por ate 60 s ainda pode lista-la. A entrega mais nova que a nota volta a esperar.
163 */
164export function semNotaDada(entregas: readonly RitualEntrega[], notas: Readonly<Record<string, RitualNota>>): RitualEntrega[] {
165 return entregas.filter(e => {
166 const n = notas[e.thread]
167 return !n || (n.estado !== 'registrada' && n.estado !== 'repetida') || n.em < Date.parse(e.entregueEm)
168 })
169}
170
171/**
172 * A linha da faixa: quantas entregas esperam a nota e quais; sem nenhuma (ou antes da primeira leitura), sem linha. Com
173 * thread que o retrato nao leu, a contagem e "pelo menos", e sem entrega visivel a linha aponta o `ork master`.
174 */
175export function textoDaFaixa(q: RitualQuadro): string | undefined {
176 if (q.lidoEm === null) return undefined
177 if (!q.entregas.length) {
178 return q.semLeitura ? `o retrato não leu ${plural(q.semLeitura, 'thread', 'threads')}: ork master lista as entregas que esperam a sua nota · /nota` : undefined
179 }
180 const nomes = q.entregas.slice(0, 3).map(e => e.thread)
181 const resto = q.entregas.length - nomes.length
182 return `${q.semLeitura ? 'pelo menos ' : ''}${plural(q.entregas.length, 'entrega espera', 'entregas esperam')} a sua nota: ${nomes.join(', ')}${resto > 0 ? ` e +${resto}` : ''} · /nota`
183}
184
185/** A thread do ledger que mudou (`classic.FileChanged`), quando o caminho e o ledger de uma thread. */
186export function threadDoLedger(caminho: unknown): string | null {
187 if (typeof caminho !== 'string') return null
188 const m = /\/\.orkastery\/threads\/([a-zA-Z0-9][a-zA-Z0-9._-]{0,79})\/ledger\.jsonl$/.exec(caminho)
189 return m ? m[1] as string : null
190}
191
192/** Os argumentos do `ork_claims_list`: so o id da thread. */
193export function argsDasClaims(thread: string): { threadId: string } {
194 return { threadId: exigirThread(thread) }
195}
196
197const numeroDoId = (id: string) => { const m = /^C(\d+)$/.exec(id); return m ? Number(m[1]) : Number.POSITIVE_INFINITY }
198
199/**
200 * As claims do `ork_claims_list` (o arquivo e so de acrescimo: a retirada e uma linha nova): o ultimo registro de cada
201 * id, as retiradas contadas a parte, na ordem dos ids. Registro fora do contrato fica de fora. Nunca a saida do comando:
202 * o nucleo nao a devolve aqui, e o replay nao a mostra (D6).
203 */
204export function claimsDaProva(resposta: unknown): { claims: RitualClaim[]; retiradas: number } {
205 if (!Array.isArray(resposta)) throw new Error('o ork_claims_list não devolveu a lista de claims')
206 const ultimas = new Map<string, RitualClaim>()
207 for (const bruto of resposta as unknown[]) {
208 const c = (bruto ?? {}) as Record<string, unknown>
209 if (typeof c.id !== 'string' || !/^C\d+$/.test(c.id) || typeof c.estado !== 'string') continue
210 ultimas.set(c.id, {
211 id: c.id, estado: umaLinha(c.estado, 40), arquivo: typeof c.arquivo === 'string' ? umaLinha(c.arquivo, 300) : '',
212 alegacao: typeof c.alegacao === 'string' ? umaLinha(c.alegacao, 1200) : '',
213 comandos: Array.isArray(c.verificar) ? (c.verificar as unknown[]).filter((v): v is string => typeof v === 'string').map(v => umaLinha(v, 600)) : [],
214 })
215 }
216 const todas = [...ultimas.values()].sort((a, b) => numeroDoId(a.id) - numeroDoId(b.id) || (a.id < b.id ? -1 : 1))
217 return { claims: todas.filter(c => c.estado !== 'retirada'), retiradas: todas.filter(c => c.estado === 'retirada').length }
218}
219types/index.d.ts 107 lines1// Contrato do estado do Ritual do MASTER (`ork-ritual`, RM-059, fatia F8; ficha M8 do plano dos mods). O motor exige
2// que cada chave de `$.state` do mod esteja declarada aqui, em `PluginState`. O Ritual nao acrescenta noun ao `$`: le as
3// entregas pelo retrato da `ork-ponte` (`$.ork.retrato`, secao `master`) e as claims pelo `ork_claims_list` do nucleo.
4// Nenhum campo guarda o porque do dono, e a nota so aparece como o texto do que o nucleo registrou: quem recebe e grava
5// a nota e o nucleo, pelo dialogo nativo do host. Sem import nem referencia.
6
7/** Uma entrega que espera a nota do dono, como a secao `master` do retrato do nucleo a da. */
8export type RitualEntrega = {
9 thread: string
10 /** `pending` (sem MASTER), `ratificacao-pendente` (proposta) ou `recorded` (so a aceita por omissao entra aqui). */
11 status: 'pending' | 'ratificacao-pendente' | 'recorded'
12 /** O regime do MASTER log: `omissao` na aceita por omissao; null sem MASTER log. */
13 regime: string | null
14 /** O score que a omissao gravou (o indice arredondado), quando ha. */
15 score: number | null
16 /** O indice derivado do ledger, de 0 a 5, e as parcelas que o descontaram. */
17 indice: number | null
18 parcelas: string
19 /** Os gates que barraram a thread (reprovacoes tipadas), fora os de ensaio. */
20 reprovacoes: number
21 /** As classes de falha do MASTER log (ou da proposta); null quando o POSTMORTEM ainda nao nasceu. */
22 classes: string | null
23 /** A entrega (`ship_done` ou artefato), em ISO e na hora local que o nucleo formatou, e o merge. */
24 entregueEm: string
25 entregueEmLocal: string | null
26 merge: string | null
27 /** O arquivo da entrega por artefato (a thread so de planejamento); ausente no quadro gravado antes deste campo. */
28 artefato?: string | null
29}
30
31/** A ultima leitura das entregas. */
32export type RitualQuadro = {
33 entregas: RitualEntrega[]
34 /** Quando foi lida (ms desde a epoca); null antes da primeira leitura. */
35 lidoEm: number | null
36 /** Por que a ultima leitura falhou, quando falhou (a anterior continua no quadro). */
37 erro: string | null
38 /** O que veio incompleto do retrato (lacunas da secao, paginas que sobraram). */
39 avisos: string[]
40 /** Quantas entregas sem nota humana ficaram fora da janela de dias. */
41 foraDaJanela: number
42 /**
43 * Quantas threads o retrato nao leu (o ledger passou do teto de leitura do Maestro, ou o MASTER log nao vale): com
44 * alguma, a contagem da faixa e "pelo menos". Ausente no quadro gravado antes deste campo.
45 */
46 semLeitura?: number
47}
48
49/** Uma claim no replay: o ultimo registro de cada id, como o `ork_claims_list` a da. Nunca a saida do comando. */
50export type RitualClaim = {
51 id: string
52 estado: string
53 arquivo: string
54 alegacao: string
55 comandos: string[]
56}
57
58/** A prova de uma thread no replay. */
59export type RitualProva = {
60 thread: string
61 /** As claims ativas, na ordem dos ids. */
62 claims: RitualClaim[]
63 retiradas: number
64 lidoEm: number | null
65 erro: string | null
66}
67
68/** O que o painel mostrou ao dono e o Telegram tambem diria: a chave junta o que e a mesma coisa (M5). */
69export type RitualMostrado = { chave: string; thread: string; texto: string }
70
71/**
72 * O "Dar a nota" de uma entrega (T8.3): o dialogo pedido ao nucleo e o que ele devolveu. So o estado e o texto do
73 * resultado; a nota e o porque que o dono deu ficam no nucleo, nunca aqui.
74 */
75export type RitualNota = {
76 estado: 'abrindo' | 'registrada' | 'repetida' | 'pendente' | 'sem-dialogo' | 'erro'
77 texto: string
78 em: number
79}
80
81/** A reserva pelo Telegram (T8.3): o pedido de nota do nucleo e o codigo que o dono responde por la. */
82export type RitualReserva = {
83 estado: 'pedindo' | 'pronta' | 'erro'
84 texto: string
85 codigo: string | null
86 em: number
87}
88
89declare module 'claude-code' {
90 interface PluginState {
91 'ork-ritual': {
92 /** A ultima leitura das entregas. */
93 quadro: RitualQuadro
94 /** A thread no replay da prova, pedida por "Ver a prova" ou por `/nota <thread>`. */
95 foco: string | null
96 /** A claim que o replay mostra, a partir de 0. */
97 passo: number
98 /** As claims da thread em foco. */
99 prova: RitualProva | null
100 /** O "Dar a nota" de cada entrega, pelo id da thread. */
101 notas: Record<string, RitualNota>
102 /** A reserva pelo Telegram de cada entrega, pelo id da thread. */
103 reservas: Record<string, RitualReserva>
104 }
105 }
106}
107