@orkastery/cli 0.5.1 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/adapters/claude-code/README.md +158 -0
  2. package/adapters/claude-code/agents/ork-master.md +2 -0
  3. package/adapters/claude-code/commands/master.md +8 -0
  4. package/adapters/claude-code/commands/ork.md +9 -0
  5. package/adapters/claude-code/mods/autoridade.json +56 -0
  6. package/adapters/claude-code/mods/ork-clima/.claude-plugin/plugin.json +14 -0
  7. package/adapters/claude-code/mods/ork-clima/hooks/contas.ts +283 -0
  8. package/adapters/claude-code/mods/ork-clima/hooks/hooks.json +3 -0
  9. package/adapters/claude-code/mods/ork-clima/hooks/previsao.ts +393 -0
  10. package/adapters/claude-code/mods/ork-clima/hooks/register.ts +205 -0
  11. package/adapters/claude-code/mods/ork-clima/orkastery-mod.json +5 -0
  12. package/adapters/claude-code/mods/ork-clima/types/index.d.ts +80 -0
  13. package/adapters/claude-code/mods/ork-freio/.claude-plugin/plugin.json +14 -0
  14. package/adapters/claude-code/mods/ork-freio/hooks/carga.ts +160 -0
  15. package/adapters/claude-code/mods/ork-freio/hooks/conferencia.ts +309 -0
  16. package/adapters/claude-code/mods/ork-freio/hooks/hooks.json +3 -0
  17. package/adapters/claude-code/mods/ork-freio/hooks/lote.ts +270 -0
  18. package/adapters/claude-code/mods/ork-freio/hooks/register.ts +285 -0
  19. package/adapters/claude-code/mods/ork-freio/orkastery-mod.json +5 -0
  20. package/adapters/claude-code/mods/ork-freio/types/index.d.ts +127 -0
  21. package/adapters/claude-code/mods/ork-mesa/.claude-plugin/plugin.json +14 -0
  22. package/adapters/claude-code/mods/ork-mesa/hooks/cartoes.tsx +246 -0
  23. package/adapters/claude-code/mods/ork-mesa/hooks/decidir.ts +110 -0
  24. package/adapters/claude-code/mods/ork-mesa/hooks/fila.ts +253 -0
  25. package/adapters/claude-code/mods/ork-mesa/hooks/hooks.json +3 -0
  26. package/adapters/claude-code/mods/ork-mesa/hooks/obvia.ts +98 -0
  27. package/adapters/claude-code/mods/ork-mesa/hooks/register.tsx +493 -0
  28. package/adapters/claude-code/mods/ork-mesa/orkastery-mod.json +5 -0
  29. package/adapters/claude-code/mods/ork-mesa/types/index.d.ts +131 -0
  30. package/adapters/claude-code/mods/ork-pista/.claude-plugin/plugin.json +14 -0
  31. package/adapters/claude-code/mods/ork-pista/hooks/acoes.ts +71 -0
  32. package/adapters/claude-code/mods/ork-pista/hooks/ci.ts +117 -0
  33. package/adapters/claude-code/mods/ork-pista/hooks/hooks.json +3 -0
  34. package/adapters/claude-code/mods/ork-pista/hooks/pista.tsx +356 -0
  35. package/adapters/claude-code/mods/ork-pista/hooks/register.tsx +439 -0
  36. package/adapters/claude-code/mods/ork-pista/orkastery-mod.json +5 -0
  37. package/adapters/claude-code/mods/ork-pista/types/index.d.ts +85 -0
  38. package/adapters/claude-code/mods/ork-ponte/.claude-plugin/plugin.json +13 -0
  39. package/adapters/claude-code/mods/ork-ponte/hooks/faixa.tsx +57 -0
  40. package/adapters/claude-code/mods/ork-ponte/hooks/guarda.ts +410 -0
  41. package/adapters/claude-code/mods/ork-ponte/hooks/hooks.json +3 -0
  42. package/adapters/claude-code/mods/ork-ponte/hooks/mostrado.ts +55 -0
  43. package/adapters/claude-code/mods/ork-ponte/hooks/nucleo.ts +157 -0
  44. package/adapters/claude-code/mods/ork-ponte/hooks/papel.ts +42 -0
  45. package/adapters/claude-code/mods/ork-ponte/hooks/register.ts +331 -0
  46. package/adapters/claude-code/mods/ork-ponte/hooks/vigia.ts +76 -0
  47. package/adapters/claude-code/mods/ork-ponte/orkastery-mod.json +5 -0
  48. package/adapters/claude-code/mods/ork-ponte/types/index.d.ts +178 -0
  49. package/adapters/claude-code/mods/ork-portao/.claude-plugin/plugin.json +14 -0
  50. package/adapters/claude-code/mods/ork-portao/hooks/hooks.json +3 -0
  51. package/adapters/claude-code/mods/ork-portao/hooks/lease.ts +201 -0
  52. package/adapters/claude-code/mods/ork-portao/hooks/plano.ts +304 -0
  53. package/adapters/claude-code/mods/ork-portao/hooks/register.ts +339 -0
  54. package/adapters/claude-code/mods/ork-portao/hooks/verify.ts +152 -0
  55. package/adapters/claude-code/mods/ork-portao/orkastery-mod.json +5 -0
  56. package/adapters/claude-code/mods/ork-portao/types/index.d.ts +60 -0
  57. package/adapters/claude-code/mods/ork-radar/.claude-plugin/plugin.json +14 -0
  58. package/adapters/claude-code/mods/ork-radar/hooks/colisao.ts +420 -0
  59. package/adapters/claude-code/mods/ork-radar/hooks/hooks.json +3 -0
  60. package/adapters/claude-code/mods/ork-radar/hooks/mapa.tsx +322 -0
  61. package/adapters/claude-code/mods/ork-radar/hooks/register.tsx +470 -0
  62. package/adapters/claude-code/mods/ork-radar/orkastery-mod.json +5 -0
  63. package/adapters/claude-code/mods/ork-radar/types/index.d.ts +72 -0
  64. package/adapters/claude-code/mods/ork-sentinela/.claude-plugin/plugin.json +14 -0
  65. package/adapters/claude-code/mods/ork-sentinela/hooks/alertas.ts +85 -0
  66. package/adapters/claude-code/mods/ork-sentinela/hooks/despacho.ts +156 -0
  67. package/adapters/claude-code/mods/ork-sentinela/hooks/hooks.json +3 -0
  68. package/adapters/claude-code/mods/ork-sentinela/hooks/register.ts +511 -0
  69. package/adapters/claude-code/mods/ork-sentinela/hooks/vigia.ts +186 -0
  70. package/adapters/claude-code/mods/ork-sentinela/orkastery-mod.json +5 -0
  71. package/adapters/claude-code/mods/ork-sentinela/types/index.d.ts +65 -0
  72. package/adapters/claude-code/mods/ork-torre/.claude-plugin/plugin.json +14 -0
  73. package/adapters/claude-code/mods/ork-torre/hooks/agenda.ts +60 -0
  74. package/adapters/claude-code/mods/ork-torre/hooks/detalhe.tsx +236 -0
  75. package/adapters/claude-code/mods/ork-torre/hooks/frases.ts +42 -0
  76. package/adapters/claude-code/mods/ork-torre/hooks/hooks.json +3 -0
  77. package/adapters/claude-code/mods/ork-torre/hooks/painel.tsx +344 -0
  78. package/adapters/claude-code/mods/ork-torre/hooks/rede.ts +97 -0
  79. package/adapters/claude-code/mods/ork-torre/hooks/register.tsx +437 -0
  80. package/adapters/claude-code/mods/ork-torre/orkastery-mod.json +5 -0
  81. package/adapters/claude-code/mods/ork-torre/types/index.d.ts +108 -0
  82. package/adapters/codex/skills/ork/SKILL.md +18 -3
  83. package/adapters/hermes/README.md +1 -0
  84. package/adapters/hermes/bin/ork-network-status.sh +10 -0
  85. package/adapters/hermes/hermes.plugin.json +1 -0
  86. package/adapters/hermes/hitl-ingress/__init__.py +2 -2
  87. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +7 -6
  88. package/adapters/openclaw/README.md +26 -5
  89. package/adapters/openclaw/dist/hitl-ingress.js +2 -2
  90. package/adapters/openclaw/dist/index.js +46 -11
  91. package/adapters/openclaw/openclaw.plugin.json +5 -1
  92. package/adapters/openclaw/package.json +2 -2
  93. package/adapters/openclaw/src/hitl-ingress.ts +2 -2
  94. package/adapters/openclaw/src/index.ts +49 -11
  95. package/assets/docs-en/markdownlint-cli2.jsonc +22 -0
  96. package/assets/docs-en/padroes/documentacao-de-produto.md +135 -0
  97. package/assets/docs-en/padroes/roadmap-de-produto.md +100 -0
  98. package/assets/docs-en/produto/README.md +14 -0
  99. package/assets/docs-en/produto/_modelo-feature.md +62 -0
  100. package/assets/docs-en/roadmap/README.md +14 -0
  101. package/assets/docs-en/roadmap/_modelo-item.md +75 -0
  102. package/assets/orkmind_bridge.py +117 -9
  103. package/dist/adapters/claude-bg.js +116 -18
  104. package/dist/adapters/codex.js +14 -4
  105. package/dist/agents-md.js +24 -14
  106. package/dist/auditrun.js +22 -3
  107. package/dist/board.js +20 -17
  108. package/dist/branch-de-estado.js +88 -10
  109. package/dist/busca-semantica.js +52 -12
  110. package/dist/canarios-rm057.js +149 -0
  111. package/dist/canarios.js +2 -0
  112. package/dist/catalogo.js +33 -0
  113. package/dist/ci.js +21 -8
  114. package/dist/claims-conferir.js +342 -0
  115. package/dist/claims.js +41 -1
  116. package/dist/company-brain-client.js +36 -1
  117. package/dist/company-brain-context.js +158 -22
  118. package/dist/company-brain-journal.js +2 -1
  119. package/dist/company-brain-migration.js +36 -1
  120. package/dist/conducao-sentinela.js +388 -0
  121. package/dist/conducao-texto.js +12 -18
  122. package/dist/conducao.js +122 -30
  123. package/dist/conhecimento.js +490 -0
  124. package/dist/docs.js +70 -17
  125. package/dist/doctor-pulse-cron.js +349 -0
  126. package/dist/doctor-rede.js +150 -0
  127. package/dist/doctor.js +195 -111
  128. package/dist/entrega-pr.js +42 -29
  129. package/dist/escopo-escrita.js +10 -1
  130. package/dist/esgotamentos.js +181 -0
  131. package/dist/estado-thread.js +23 -1
  132. package/dist/experiencia-instalacao.js +45 -25
  133. package/dist/experiencia.js +12 -2
  134. package/dist/fabrica-batida.js +310 -0
  135. package/dist/fabrica-estado.js +52 -14
  136. package/dist/fabrica-publicar.js +9 -1
  137. package/dist/fechamento.js +20 -66
  138. package/dist/fila-pesada.js +444 -0
  139. package/dist/fix.js +3 -2
  140. package/dist/forja.js +31 -7
  141. package/dist/gates.js +3 -24
  142. package/dist/handoff.js +10 -6
  143. package/dist/hitl-contract.js +59 -4
  144. package/dist/hitl-curto.js +11 -6
  145. package/dist/hitl-gates.js +5 -0
  146. package/dist/hitl-local-receipt.js +2 -0
  147. package/dist/hitl-lote.js +5 -2
  148. package/dist/hitl-resumo.js +38 -3
  149. package/dist/hitl-tempo-parado.js +201 -0
  150. package/dist/hitl-texto-livre.js +5 -5
  151. package/dist/hitl.js +29 -33
  152. package/dist/horario.js +41 -7
  153. package/dist/hosts.js +104 -11
  154. package/dist/impedimento.js +299 -0
  155. package/dist/index.js +961 -542
  156. package/dist/indice-vetorial.js +87 -10
  157. package/dist/indice.js +9 -1
  158. package/dist/init.js +56 -43
  159. package/dist/instabilidade.js +144 -0
  160. package/dist/integracoes-locais.js +4 -0
  161. package/dist/intelligence-graph-cli.js +170 -16
  162. package/dist/intelligence-graph-contexto.js +254 -0
  163. package/dist/intelligence-graph-contract.js +2 -1
  164. package/dist/intelligence-graph-extract-md.js +56 -4
  165. package/dist/intelligence-graph-extract-ts.js +71 -3
  166. package/dist/intelligence-graph-parsers.js +6 -4
  167. package/dist/intelligence-graph-query.js +66 -19
  168. package/dist/leases.js +664 -87
  169. package/dist/ledger-stats.js +15 -1
  170. package/dist/ledger.js +18 -1
  171. package/dist/licoes.js +1 -0
  172. package/dist/liveness.js +6 -3
  173. package/dist/locale-ajuda.js +761 -0
  174. package/dist/locale-gerados.js +160 -0
  175. package/dist/locale-master.js +99 -0
  176. package/dist/locale-onboarding.js +92 -0
  177. package/dist/locale-phase.js +151 -0
  178. package/dist/locale-pulse.js +211 -0
  179. package/dist/locale-retry.js +385 -0
  180. package/dist/locale-roadmap.js +102 -0
  181. package/dist/locale-ship.js +494 -0
  182. package/dist/locale-thread.js +277 -0
  183. package/dist/locale-verify.js +125 -0
  184. package/dist/locale.js +714 -0
  185. package/dist/maestro-runtime.js +4 -1
  186. package/dist/manifest.js +149 -4
  187. package/dist/maquina.js +23 -4
  188. package/dist/master-digest.js +6 -1
  189. package/dist/master-nota.js +18 -11
  190. package/dist/master.js +145 -31
  191. package/dist/mcp-artifacts.js +43 -3
  192. package/dist/mcp-git.js +42 -8
  193. package/dist/mcp-grafo-worker.js +5 -3
  194. package/dist/mcp-grafo.js +55 -6
  195. package/dist/mcp-server.js +22 -3
  196. package/dist/mcp-ship.js +3 -1
  197. package/dist/mcp-verify.js +6 -1
  198. package/dist/memoria.js +48 -6
  199. package/dist/modos.js +3 -6
  200. package/dist/network-roadmap.js +196 -18
  201. package/dist/noctum-arco.js +911 -0
  202. package/dist/noctum-capacidade.js +141 -0
  203. package/dist/noctum-envelope.js +810 -0
  204. package/dist/noctum-execucao.js +304 -0
  205. package/dist/noctum-fila.js +280 -0
  206. package/dist/ocupacao.js +40 -5
  207. package/dist/onboarding.js +40 -21
  208. package/dist/orkmind.js +99 -7
  209. package/dist/orquestracao.js +24 -21
  210. package/dist/parado-no-condutor.js +219 -38
  211. package/dist/phase.js +160 -52
  212. package/dist/policies.js +56 -5
  213. package/dist/portfolio.js +8 -3
  214. package/dist/postura-local.js +234 -0
  215. package/dist/preflight.js +11 -10
  216. package/dist/procedencia.js +225 -0
  217. package/dist/projeto-alvo.js +132 -5
  218. package/dist/prompts.js +40 -1
  219. package/dist/prova-ativacao.js +75 -6
  220. package/dist/pulse-delivery.js +47 -4
  221. package/dist/pulse.js +98 -19
  222. package/dist/ratelimit.js +24 -6
  223. package/dist/recall.js +3 -1
  224. package/dist/redacao-saida.js +63 -0
  225. package/dist/rede-adesao.js +399 -0
  226. package/dist/rede-forja.js +289 -0
  227. package/dist/rede-projetos.js +239 -0
  228. package/dist/rede-status.js +337 -0
  229. package/dist/rede.js +980 -0
  230. package/dist/retry.js +291 -140
  231. package/dist/roadmap-reservas.js +20 -3
  232. package/dist/roadmap-status.js +150 -34
  233. package/dist/runtime-context.js +181 -3
  234. package/dist/runtime-profiles.js +22 -1
  235. package/dist/runtimes.js +2 -1
  236. package/dist/saida-segura.js +58 -0
  237. package/dist/session-events.js +7 -3
  238. package/dist/session-watcher-claude.js +120 -24
  239. package/dist/session-watcher.js +185 -33
  240. package/dist/sessoes-contas.js +187 -0
  241. package/dist/sessoes-inventario.js +6 -18
  242. package/dist/sessoes.js +83 -2
  243. package/dist/ship-publicar.js +800 -0
  244. package/dist/ship.js +157 -88
  245. package/dist/steal.js +77 -0
  246. package/dist/thread-close.js +3 -2
  247. package/dist/thread.js +299 -49
  248. package/dist/util.js +8 -1
  249. package/dist/verify.js +141 -52
  250. package/dist/vida-da-thread.js +254 -0
  251. package/dist/worktree-local.js +165 -0
  252. package/dist/worktree.js +25 -2
  253. package/dist/yaml.js +27 -8
  254. package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
  255. package/package.json +7 -3
  256. package/references/definition-of-done.md +1 -1
  257. package/schemas/code-artifact-graph.v1.schema.json +2 -1
  258. package/skills/README.md +1 -1
  259. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  260. package/skills/phases/master-metrics/SKILL.md +8 -3
@@ -0,0 +1,71 @@
1
+ // Os botoes da Pista (RM-059, fatia F7, T7.3; D6 da thread ork-rm059pista). Modulo puro: o botao que o nucleo declara
2
+ // para cada pista, o argv dos dois comandos que um botao chama e a leitura do resultado de cada um. Botao so existe
3
+ // quando o nucleo declara a acao com `disponivel`, e o argv sai sempre daqui, nunca do texto que veio no JSON: a Pista
4
+ // so publica pelo `ork ship publicar` (que so age em #Auto e #Maestro) e so registra pelo `ork ship registrar-pr`.
5
+ import type { PistaAcao, PistaLinha } from '../types'
6
+ import { exigirThread, jsonDaSaida, umaLinha } from './pista'
7
+ import type { SaidaDoProcesso } from './pista'
8
+
9
+ export type AcaoDoBotao = PistaAcao['acao']
10
+
11
+ /** O rotulo de cada botao no painel. */
12
+ export const ROTULO_DO_BOTAO: Readonly<Record<AcaoDoBotao, string>> = { publicar: 'Publicar', registrar: 'Registrar a entrega' }
13
+
14
+ /** O argv do `ork ship publicar`, sem flag de autorizacao: quem decide se o modo autoriza o push e o nucleo. */
15
+ export function argvDoPublicar(thread: string): string[] {
16
+ return ['ork', 'ship', 'publicar', exigirThread(thread), '--json']
17
+ }
18
+
19
+ /** O argv do `ork ship registrar-pr` da thread: o nucleo confere o merge na base e o CI antes de gravar o `ship_done`. */
20
+ export function argvDoRegistro(thread: string): string[] {
21
+ return ['ork', 'ship', 'registrar-pr', exigirThread(thread), '--json']
22
+ }
23
+
24
+ /**
25
+ * O botao da pista (D6): so `publicar`, no modo que ja autoriza o push, e `registrar`, e so quando o nucleo os declara
26
+ * disponiveis. Fora disso, nenhum botao: o proximo passo fica como texto, com o comando para o dono.
27
+ */
28
+ export function botaoDaPista(l: PistaLinha): AcaoDoBotao | null {
29
+ if (!l.proximo.disponivel) return null
30
+ if (l.proximo.acao === 'publicar') return l.publicavel ? 'publicar' : null
31
+ if (l.proximo.acao === 'registrar') return 'registrar'
32
+ return null
33
+ }
34
+
35
+ export type ResultadoDoBotao = { estado: 'feita' | 'recusada' | 'falhou'; texto: string }
36
+
37
+ const curto = (sha: unknown) => (typeof sha === 'string' && /^[0-9a-f]{7,40}$/.test(sha) ? sha.slice(0, 7) : '-')
38
+ const texto = (v: unknown, teto = 300) => (typeof v === 'string' ? umaLinha(v, teto) : '')
39
+
40
+ /** O resultado do `ork ship publicar --json` (`ork.ship-publicar/v1`), numa linha para o painel. */
41
+ export function lerPublicacao(saida: SaidaDoProcesso): ResultadoDoBotao {
42
+ const r = jsonDaSaida(saida, 'ork ship publicar') as Record<string, unknown> | null
43
+ if (!r || typeof r !== 'object' || r.contrato !== 'ork.ship-publicar/v1') {
44
+ return { estado: 'falhou', texto: 'a saída do ork ship publicar não é o contrato ork.ship-publicar/v1' }
45
+ }
46
+ const pr = (r.pr ?? null) as { numero?: unknown; criado?: unknown } | null
47
+ const numero = pr && Number.isSafeInteger(pr.numero) ? `PR #${pr.numero as number}` : 'sem PR'
48
+ if (r.acao === 'publicou') {
49
+ return { estado: 'feita', texto: `publicada no HEAD ${curto(r.head)}, push provado pelo ls-remote; ${numero} ${pr?.criado === true ? 'aberto agora' : 'reaproveitado'}` }
50
+ }
51
+ if (r.acao === 'ja-publicada') {
52
+ return { estado: 'feita', texto: `já estava publicada no HEAD ${curto(r.head)}; ${numero}${texto(r.correcao) ? `; ${texto(r.correcao)}` : ''}` }
53
+ }
54
+ if (r.acao === 'recusada') {
55
+ const correcao = texto(r.correcao, 400)
56
+ return { estado: 'recusada', texto: `recusada (${texto(r.motivo, 60) || 'sem motivo'}): ${texto(r.detalhe) || 'sem detalhe'}${correcao ? `; ${correcao}` : ''}` }
57
+ }
58
+ return { estado: 'falhou', texto: `o ork ship publicar respondeu ${texto(r.acao, 40) || 'sem ação'}` }
59
+ }
60
+
61
+ /** O resultado do `ork ship registrar-pr <thread> --json` (uma lista de entregas), numa linha para o painel. */
62
+ export function lerRegistro(saida: SaidaDoProcesso, thread: string): ResultadoDoBotao {
63
+ const lista = jsonDaSaida(saida, 'ork ship registrar-pr')
64
+ const item = Array.isArray(lista) ? (lista as Array<Record<string, unknown> | null>).find(x => x?.thread === thread) : undefined
65
+ if (!item) return { estado: 'falhou', texto: `o ork ship registrar-pr não respondeu pela thread ${thread}` }
66
+ const motivo = texto(item.motivo) || 'sem motivo'
67
+ if (item.acao === 'registrou') return { estado: 'feita', texto: `ship_done registrado: ${motivo}` }
68
+ if (item.acao === 'ja-registrada') return { estado: 'feita', texto: `a entrega já estava registrada: ${motivo}` }
69
+ if (item.acao === 'sem-merge' || item.acao === 'recusada') return { estado: 'recusada', texto: `${String(item.acao)}: ${motivo}` }
70
+ return { estado: 'falhou', texto: `o ork ship registrar-pr respondeu ${texto(item.acao, 40) || 'sem ação'}` }
71
+ }
@@ -0,0 +1,117 @@
1
+ // O CI de cada pista (RM-059, fatia F7, T7.4; D7 da thread ork-rm059pista). Modulo puro: o argv da consulta ao nucleo
2
+ // (`ork ci status --sha <sha> --json`, contrato `ork.ci-status/v1`), a leitura dela, as pistas que um evento do GitHub
3
+ // cita, as que o relogio de reserva consulta e a etapa de CI que o painel mostra com a leitura mais nova. O evento so
4
+ // dispara a consulta: o conteudo dele nunca vira estado, e quem diz o CI e o check do nucleo no sha exato.
5
+ import type { PistaCi, PistaEstadoDaEtapa, PistaLinha, PistaQuadro } from '../types'
6
+ import { jsonDaSaida, umaLinha } from './pista'
7
+ import type { SaidaDoProcesso } from './pista'
8
+
9
+ /** A reserva sem evento: a cada 2 min com o painel a vista, a cada 10 min escondido. */
10
+ export const INTERVALO_DO_CI_A_VISTA_MS = 2 * 60000
11
+ export const INTERVALO_DO_CI_ESCONDIDO_MS = 10 * 60000
12
+ const SHA = /^[0-9a-f]{40}$/
13
+ const ESTADOS: readonly PistaCi['estado'][] = ['success', 'pending', 'failure', 'missing', 'unavailable', 'disabled']
14
+
15
+ /** O argv da consulta do CI no sha exato do head do PR, nunca por ref nem prefixo. */
16
+ export function argvDoCi(sha: string): string[] {
17
+ if (typeof sha !== 'string' || !SHA.test(sha)) throw new Error('ork-pista: sha invalido para o ork ci status')
18
+ return ['ork', 'ci', 'status', '--sha', sha, '--json']
19
+ }
20
+
21
+ /**
22
+ * A saida do `ork ci status --sha <sha> --json`: o comando sai 0 so com o check verde, e o JSON vem nos dois casos.
23
+ * Leitura de outro sha nao vale para a pista.
24
+ */
25
+ export function lerCi(saida: SaidaDoProcesso, sha: string, agora: number, motivo: PistaCi['motivo']): PistaCi {
26
+ const r = jsonDaSaida(saida, 'ork ci status') as Record<string, unknown> | null
27
+ if (!r || typeof r !== 'object' || r.schema !== 'ork.ci-status/v1' || !ESTADOS.includes(r.state as PistaCi['estado'])) {
28
+ throw new Error('a saída do ork ci status não é o contrato ork.ci-status/v1')
29
+ }
30
+ if (r.sha !== sha) throw new Error(`o ork ci status respondeu pelo commit ${umaLinha(r.sha, 40)}, e não pelo head ${sha.slice(0, 7)}`)
31
+ const estado = r.state as PistaCi['estado']
32
+ const contexto = typeof r.context === 'string' && r.context ? umaLinha(r.context, 60) : 'ork-verify'
33
+ const curto = sha.slice(0, 7)
34
+ const texto = estado === 'success' ? `check ${contexto} verde no head ${curto}`
35
+ : estado === 'failure' ? `check ${contexto} vermelho no head ${curto}`
36
+ : estado === 'pending' ? `check ${contexto} em andamento no head ${curto}`
37
+ : estado === 'missing' ? `o check ${contexto} ainda não apareceu no head ${curto}`
38
+ : estado === 'disabled' ? 'CI não exigido pelo manifesto'
39
+ : `o CI não foi lido: ${umaLinha(r.detail, 160) || 'sem detalhe'}`
40
+ const url = typeof r.url === 'string' && /^https:\/\/\S+$/.test(r.url) ? umaLinha(r.url, 300) : null
41
+ return { sha, estado, texto, url, lidoEm: agora, motivo }
42
+ }
43
+
44
+ /** A pista com PR aberto cujo CI a leitura da pista ainda nao fechou (nem verde nem vermelho). */
45
+ export function precisaDoCi(l: PistaLinha): boolean {
46
+ const etapa = l.etapas.find(e => e.id === 'ci')
47
+ return l.pr?.estado === 'aberto' && SHA.test(l.pr.head) && (etapa?.estado === 'pendente' || etapa?.estado === 'nao-lida')
48
+ }
49
+
50
+ /** As pistas que o relogio de reserva consulta agora: as que precisam, sem leitura do head atual dentro do intervalo. */
51
+ export function vencidasDoCi(linhas: readonly PistaLinha[], ci: Readonly<Record<string, PistaCi>>, agora: number, intervalo: number): string[] {
52
+ return linhas.filter(l => {
53
+ if (!precisaDoCi(l)) return false
54
+ const lido = ci[l.thread]
55
+ return !lido || lido.sha !== l.pr?.head || agora - lido.lidoEm >= intervalo
56
+ }).map(l => l.thread)
57
+ }
58
+
59
+ /**
60
+ * As pistas que um evento do GitHub cita, so pelos campos que o servidor assere (fora de `untrustedKeys`): o PR (o
61
+ * numero, ou `dono/repo#N`), a branch ou o sha. Evento de outra fonte, ou que nao cita pista nenhuma, nao consulta nada.
62
+ */
63
+ export function pistasDoEvento(evento: unknown, linhas: readonly PistaLinha[]): string[] {
64
+ if (!evento || typeof evento !== 'object') return []
65
+ const e = evento as { source?: unknown; data?: unknown; untrustedKeys?: unknown }
66
+ if (e.source !== 'github' || !e.data || typeof e.data !== 'object') return []
67
+ const deTerceiros = new Set(Array.isArray(e.untrustedKeys) ? e.untrustedKeys.map(String) : [])
68
+ const numeros = new Set<number>()
69
+ const textos = new Set<string>()
70
+ for (const [chave, valor] of Object.entries(e.data as Record<string, unknown>)) {
71
+ if (deTerceiros.has(chave)) continue
72
+ if (typeof valor === 'number' && Number.isSafeInteger(valor) && valor > 0 && /^(pr|number|pull_request|pull)$/i.test(chave)) numeros.add(valor)
73
+ if (typeof valor !== 'string') continue
74
+ const pr = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+#(\d{1,9})$/.exec(valor.trim()) ?? (/^(pr|number|pull_request|pull)$/i.test(chave) ? /^#?(\d{1,9})$/.exec(valor.trim()) : null)
75
+ if (pr) numeros.add(Number(pr[1]))
76
+ textos.add(valor.trim())
77
+ }
78
+ return linhas.filter(l => (l.pr && numeros.has(l.pr.numero)) || textos.has(l.branch) || (!!l.head && textos.has(l.head)) ||
79
+ (!!l.pr && textos.has(l.pr.head))).map(l => l.thread)
80
+ }
81
+
82
+ const ETAPA_DO_CI: Readonly<Record<PistaCi['estado'], PistaEstadoDaEtapa>> = {
83
+ success: 'feita', failure: 'falhou', pending: 'pendente', missing: 'pendente', unavailable: 'nao-lida', disabled: 'nao-se-aplica',
84
+ }
85
+
86
+ /** A consulta leu o check no head: `unavailable` (a consulta falhou) e `disabled` (o CI nao e exigido) nao leram nada. */
87
+ function leuOCheck(lido: PistaCi): boolean {
88
+ return lido.estado !== 'unavailable' && lido.estado !== 'disabled'
89
+ }
90
+
91
+ /**
92
+ * A pista com a leitura do CI mais nova por cima da etapa da leitura da pista, so quando ela e do head do PR aberto. A
93
+ * consulta que nao leu nada nunca apaga o que o nucleo ja leu: a etapa fica como o nucleo disse, com a falha ao lado.
94
+ */
95
+ export function comCi(q: PistaQuadro, ci: Readonly<Record<string, PistaCi>>): PistaQuadro {
96
+ return {
97
+ ...q,
98
+ linhas: q.linhas.map(l => {
99
+ const lido = ci[l.thread]
100
+ if (!lido || l.pr?.estado !== 'aberto' || lido.sha !== l.pr.head) return l
101
+ if (!leuOCheck(lido)) {
102
+ return { ...l, etapas: l.etapas.map(e => (e.id === 'ci' ? { ...e, texto: `${e.texto} (consulta: ${lido.texto})` } : e)) }
103
+ }
104
+ const origem = lido.motivo === 'evento' ? ' (pelo evento do GitHub)' : ''
105
+ return { ...l, etapas: l.etapas.map(e => (e.id === 'ci' ? { id: 'ci', estado: ETAPA_DO_CI[lido.estado], texto: `${lido.texto}${origem}` } : e)) }
106
+ }),
107
+ }
108
+ }
109
+
110
+ /**
111
+ * A leitura nova diz do CI outra coisa que a etapa da pista: o CI fechou, ou mudou depois de uma rodada nova. O proximo
112
+ * passo do nucleo muda com ele, e a pista inteira e relida. A consulta que nao leu nada nao muda nada.
113
+ */
114
+ export function mudouOCi(l: PistaLinha, lido: PistaCi): boolean {
115
+ if (!leuOCheck(lido) || l.pr?.estado !== 'aberto' || lido.sha !== l.pr.head) return false
116
+ return l.etapas.find(e => e.id === 'ci')?.estado !== ETAPA_DO_CI[lido.estado]
117
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "modules": ["./register.tsx"]
3
+ }
@@ -0,0 +1,356 @@
1
+ // A pista de entrega (RM-059, fatia F7; ficha M7 do plano dos mods). Modulo puro: o argv da leitura do nucleo
2
+ // (`ork ship pista --json`, contrato `ork.ship-pista/v1`), a conferencia da saida campo a campo, a ordem das pistas, a
3
+ // linha da faixa, as linhas do painel com o que cada uma mostra ao dono e o desenho. Quem calcula as etapas e o
4
+ // proximo passo e o nucleo (D4 da thread ork-rm059pista); aqui so se le, se ordena e se desenha. O `$` fica no
5
+ // register.tsx, e o desenho recebe a tabela de elementos que a superficie resolveu.
6
+ import type { PistaAcao, PistaEstadoDaEtapa, PistaEtapa, PistaIdDaEtapa, PistaLinha, PistaMostrado, PistaPr, PistaProximo, PistaQuadro } from '../types'
7
+
8
+ export const ETAPAS: readonly PistaIdDaEtapa[] = ['check', 'verify', 'bundle', 'branch', 'pr', 'ci', 'conflito', 'entrega']
9
+ const ESTADOS: readonly PistaEstadoDaEtapa[] = ['feita', 'pendente', 'falhou', 'nao-lida', 'nao-se-aplica']
10
+ const ID_DE_THREAD = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/
11
+ const SHA = /^[0-9a-f]{40}$/
12
+ /** O nome curto de cada etapa no painel. */
13
+ export const NOME_DA_ETAPA: Readonly<Record<PistaIdDaEtapa, string>> = {
14
+ check: 'CHECK', verify: 'verify', bundle: 'bundle', branch: 'branch', pr: 'PR', ci: 'CI', conflito: 'conflito', entrega: 'entrega',
15
+ }
16
+ /** A marca de cada estado de etapa: o que falta e o que falhou saltam aos olhos. */
17
+ export const MARCA_DA_ETAPA: Readonly<Record<PistaEstadoDaEtapa, string>> = {
18
+ 'feita': '✓', 'pendente': '·', 'falhou': '✗', 'nao-lida': '?', 'nao-se-aplica': '-',
19
+ }
20
+ /** Teto de pistas no quadro: cada uma ocupa umas cinco linhas do painel. */
21
+ export const TETO_DE_PISTAS = 30
22
+ const TETO_DE_TEXTO = 300
23
+
24
+ export type SaidaDoProcesso = { exitCode: number; stdout: string; stderr: string }
25
+
26
+ /** Uma linha so, sem caractere de controle, com teto: texto que vem do nucleo e vai para a tela. */
27
+ export function umaLinha(v: unknown, teto = TETO_DE_TEXTO): string {
28
+ const s = typeof v === 'string' ? v : v instanceof Error ? v.message : String(v)
29
+ return Array.from(s.replace(/\p{Cc}/gu, ' ').replace(/\s+/g, ' ').trim()).slice(0, teto).join('')
30
+ }
31
+
32
+ export function exigirThread(thread: unknown): string {
33
+ if (typeof thread !== 'string' || !ID_DE_THREAD.test(thread)) throw new Error('ork-pista: id de thread invalido')
34
+ return thread
35
+ }
36
+
37
+ /** O argv da leitura da pista, de todas as threads ou de uma: o unico jeito de a Pista ler as etapas. */
38
+ export function argvDaPista(thread?: string): string[] {
39
+ return thread === undefined ? ['ork', 'ship', 'pista', '--json'] : ['ork', 'ship', 'pista', exigirThread(thread), '--json']
40
+ }
41
+
42
+ /** A saida de um comando `--json` do nucleo, ou o motivo de nao ter sido lida (o CLI sem o subcomando incluso). */
43
+ export function jsonDaSaida(saida: SaidaDoProcesso, comando: string): unknown {
44
+ if (saida.exitCode === 2 && /(subcomando desconhecido|uso: ork ship|usage: ork ship|comando desconhecido)/i.test(`${saida.stderr}\n${saida.stdout}`)) {
45
+ throw new Error(`o ork deste PATH não tem ${comando}: atualize o CLI do Orkastery`)
46
+ }
47
+ try {
48
+ return JSON.parse(saida.stdout)
49
+ } catch {
50
+ const motivo = umaLinha(saida.stderr || saida.stdout, 160) || `saída ${saida.exitCode}`
51
+ throw new Error(`${comando} não devolveu JSON (saída ${saida.exitCode}): ${motivo}`)
52
+ }
53
+ }
54
+
55
+ const texto = (v: unknown, teto = TETO_DE_TEXTO): string | null => (typeof v === 'string' && v.trim() !== '' ? umaLinha(v, teto) : null)
56
+
57
+ function etapaDe(bruto: unknown, id: PistaIdDaEtapa): PistaEtapa | null {
58
+ if (!bruto || typeof bruto !== 'object') return null
59
+ const e = bruto as Record<string, unknown>
60
+ if (e.id !== id || !ESTADOS.includes(e.estado as PistaEstadoDaEtapa)) return null
61
+ return { id, estado: e.estado as PistaEstadoDaEtapa, texto: texto(e.texto) ?? '' }
62
+ }
63
+
64
+ function prDe(bruto: unknown): PistaPr | null | undefined {
65
+ if (bruto === null) return null
66
+ if (!bruto || typeof bruto !== 'object') return undefined
67
+ const p = bruto as Record<string, unknown>
68
+ if (!Number.isSafeInteger(p.numero) || (p.numero as number) < 1 || !['aberto', 'mesclado', 'fechado'].includes(String(p.estado)) ||
69
+ typeof p.rascunho !== 'boolean' || typeof p.head !== 'string' || !SHA.test(p.head)) return undefined
70
+ const url = typeof p.url === 'string' && /^https:\/\/\S+$/.test(p.url) ? umaLinha(p.url, 300) : null
71
+ return { numero: p.numero as number, url, estado: p.estado as PistaPr['estado'], rascunho: p.rascunho, head: p.head }
72
+ }
73
+
74
+ function proximoDe(bruto: unknown): PistaProximo | null {
75
+ if (!bruto || typeof bruto !== 'object') return null
76
+ const p = bruto as Record<string, unknown>
77
+ const acao = p.acao === 'publicar' || p.acao === 'registrar' ? p.acao : p.acao === null ? null : undefined
78
+ if (acao === undefined || typeof p.disponivel !== 'boolean') return null
79
+ const t = texto(p.texto)
80
+ if (!t) return null
81
+ // So uma acao conhecida pode estar disponivel; o resto vira texto.
82
+ return { acao, disponivel: acao !== null && p.disponivel, texto: t, comando: texto(p.comando, 600) }
83
+ }
84
+
85
+ /** Uma pista do nucleo, conferida campo a campo; `null` quando ela nao tem a forma do contrato. */
86
+ export function linhaDaPista(bruto: unknown): PistaLinha | null {
87
+ if (!bruto || typeof bruto !== 'object') return null
88
+ const p = bruto as Record<string, unknown>
89
+ if (typeof p.thread !== 'string' || !ID_DE_THREAD.test(p.thread) || typeof p.publicavel !== 'boolean' || typeof p.conduzida !== 'boolean') return null
90
+ if (!Array.isArray(p.etapas) || p.etapas.length !== ETAPAS.length) return null
91
+ const etapas = ETAPAS.map((id, i) => etapaDe((p.etapas as unknown[])[i], id))
92
+ if (etapas.some(e => e === null)) return null
93
+ const pr = prDe(p.pr)
94
+ const proximo = proximoDe(p.proximo)
95
+ const head = typeof p.head === 'string' && SHA.test(p.head) ? p.head : null
96
+ if (pr === undefined || !proximo) return null
97
+ return {
98
+ thread: p.thread, nome: texto(p.nome, 120) ?? p.thread, tag: texto(p.tag, 20) ?? '', publicavel: p.publicavel, conduzida: p.conduzida,
99
+ branch: texto(p.branch, 200) ?? '', head, pr, etapas: etapas as PistaEtapa[], proximo,
100
+ }
101
+ }
102
+
103
+ /**
104
+ * A saida do `ork ship pista --json`, conferida: o contrato, uma pista por thread (no maximo `TETO_DE_PISTAS`) e os
105
+ * avisos do que nao foi lido. Saida que nao e o contrato lanca, e o quadro anterior fica.
106
+ */
107
+ export function lerPista(saida: SaidaDoProcesso, agora: number): PistaQuadro {
108
+ const bruto = jsonDaSaida(saida, 'ork ship pista') as Record<string, unknown> | null
109
+ if (!bruto || typeof bruto !== 'object' || bruto.contrato !== 'ork.ship-pista/v1' || !Array.isArray(bruto.pistas)) {
110
+ throw new Error('a saída do ork ship pista não é o contrato ork.ship-pista/v1')
111
+ }
112
+ const linhas = (bruto.pistas as unknown[]).map(linhaDaPista).filter((l): l is PistaLinha => l !== null)
113
+ const avisos: string[] = []
114
+ if (bruto.remotoLido === false) avisos.push(`o remoto ${texto(bruto.remoto, 60) ?? ''} não respondeu ao ls-remote`)
115
+ if (bruto.prsLidos === false) avisos.push(`os PRs não foram lidos: ${texto(bruto.prsNaoLidos, 200) ?? 'sem motivo'}`)
116
+ const descartadas = (bruto.pistas as unknown[]).length - linhas.length
117
+ if (descartadas > 0) avisos.push(`${descartadas} pista(s) fora do contrato ficaram de fora`)
118
+ if (linhas.length > TETO_DE_PISTAS) avisos.push(`${linhas.length - TETO_DE_PISTAS} pista(s) além do teto ficaram de fora`)
119
+ return { linhas: ordenarLinhas(linhas).slice(0, TETO_DE_PISTAS), lidoEm: agora, erro: null, avisos }
120
+ }
121
+
122
+ /**
123
+ * A assinatura da secao `ship` do retrato do nucleo (o retrato da ponte): muda quando uma entrega anda (`ship_done`,
124
+ * `ship_started`, `ship_blocked`), e entao a pista e relida antes da idade. `null` quando o retrato nao veio.
125
+ */
126
+ export function assinaturaDoRetrato(r: unknown): string | null {
127
+ const secao = (r as { retrato?: { sections?: { ship?: { items?: unknown } } } } | null)?.retrato?.sections?.ship
128
+ if (!secao || !Array.isArray(secao.items)) return null
129
+ return (secao.items as unknown[]).map(i => {
130
+ const x = (i ?? {}) as { id?: unknown; status?: unknown; facts?: { receiptAt?: unknown } }
131
+ return `${String(x.id)}|${String(x.status)}|${String(x.facts?.receiptAt ?? '')}`
132
+ }).sort().join(';')
133
+ }
134
+
135
+ /** O ledger de thread que mudou (`classic.FileChanged`), pelo caminho; `null` para outro arquivo. */
136
+ export function threadDoLedger(caminho: unknown): string | null {
137
+ if (typeof caminho !== 'string') return null
138
+ const m = /\/\.orkastery\/threads\/([a-zA-Z0-9][a-zA-Z0-9._-]{0,79})\/ledger\.jsonl$/.exec(caminho)
139
+ return m ? m[1] as string : null
140
+ }
141
+
142
+ /** O peso de uma pista na ordem: o que tem botao, o que falhou, o que anda, o que ja foi entregue. */
143
+ export function pesoDaPista(l: PistaLinha): number {
144
+ if (l.proximo.disponivel) return 0
145
+ if (l.proximo.acao === 'publicar') return 1
146
+ if (l.etapas.some(e => e.estado === 'falhou')) return 2
147
+ if (l.etapas.find(e => e.id === 'entrega')?.estado === 'feita') return 4
148
+ return 3
149
+ }
150
+
151
+ export function ordenarLinhas(linhas: readonly PistaLinha[]): PistaLinha[] {
152
+ return [...linhas].sort((a, b) => pesoDaPista(a) - pesoDaPista(b) || a.thread.localeCompare(b.thread))
153
+ }
154
+
155
+ /** A marca da pista no painel: `!` com botao, `?` pede o dono, `x` falhou, `>` anda, `-` entregue. */
156
+ export function marcaDaPista(l: PistaLinha): string {
157
+ return ['!', '?', 'x', '>', '-'][pesoDaPista(l)] as string
158
+ }
159
+
160
+ const plural = (n: number, um: string, varios: string) => `${n} ${n === 1 ? um : varios}`
161
+
162
+ /**
163
+ * A linha da faixa (D9): so o que pede o dono, sem repetir o que anda sozinho. Sem nada disso, a linha sai da
164
+ * faixa. A ponte ja escreve `pista: ` antes dela.
165
+ */
166
+ export function textoDaFaixa(q: PistaQuadro): string | undefined {
167
+ const prontas = q.linhas.filter(l => l.proximo.acao === 'publicar' && l.proximo.disponivel).length
168
+ const doDono = q.linhas.filter(l => l.proximo.acao === 'publicar' && !l.proximo.disponivel).length
169
+ const registrar = q.linhas.filter(l => l.proximo.acao === 'registrar' && l.proximo.disponivel).length
170
+ const vermelhas = q.linhas.filter(l => l.etapas.some(e => (e.id === 'ci' || e.id === 'conflito') && e.estado === 'falhou')).length
171
+ const partes = [
172
+ prontas ? plural(prontas, 'pronta para publicar', 'prontas para publicar') : null,
173
+ doDono ? plural(doDono, 'pede o seu push', 'pedem o seu push') : null,
174
+ registrar ? plural(registrar, 'para registrar', 'para registrar') : null,
175
+ vermelhas ? plural(vermelhas, 'com CI vermelho ou conflito', 'com CI vermelho ou conflito') : null,
176
+ ].filter((p): p is string => p !== null)
177
+ return partes.length ? `${partes.join(' · ')} · /pista` : undefined
178
+ }
179
+
180
+ /** Quanto tempo faz, em palavras curtas. */
181
+ export function haQuanto(desde: number | null, agora: number): string {
182
+ if (desde === null) return 'ainda não lida'
183
+ const s = Math.max(0, Math.round((agora - desde) / 1000))
184
+ if (s < 60) return `lida há ${s} s`
185
+ const min = Math.round(s / 60)
186
+ return min < 60 ? `lida há ${min} min` : `lida há ${Math.round(min / 60)} h`
187
+ }
188
+
189
+ /** Uma linha do painel, com o tipo que decide o elemento e o que ela mostra ao dono (para o registro da ponte). */
190
+ export type LinhaDoPainel = {
191
+ tipo: 'placar' | 'aviso' | 'vazio' | 'cabecalho' | 'etapas' | 'etapa' | 'proximo' | 'botao' | 'comando' | 'resultado'
192
+ texto: string
193
+ thread?: string
194
+ /** So na linha do botao: o comando do nucleo que ele chama (T7.3). */
195
+ acao?: 'publicar' | 'registrar'
196
+ mostrado?: PistaMostrado
197
+ }
198
+
199
+ /** O botao que o register.tsx decidiu para uma pista (o nucleo o declara disponivel), com o rotulo. */
200
+ export type BotaoDaPista = { acao: 'publicar' | 'registrar'; rotulo: string }
201
+
202
+ const linhaDasEtapas = (l: PistaLinha) => l.etapas.map(e => {
203
+ const nome = e.id === 'pr' && l.pr ? `PR #${l.pr.numero}` : NOME_DA_ETAPA[e.id]
204
+ return `${nome} ${MARCA_DA_ETAPA[e.estado]}`
205
+ }).join(' · ')
206
+
207
+ /** O texto do resultado do ultimo botao da thread, quando ha. */
208
+ export function textoDaAcao(a: PistaAcao | undefined): string | null {
209
+ if (!a) return null
210
+ const rotulo = a.acao === 'publicar' ? 'publicar' : 'registrar'
211
+ if (a.estado === 'rodando') return `${rotulo}: rodando no núcleo...`
212
+ return `${rotulo}: ${a.texto}`
213
+ }
214
+
215
+ /**
216
+ * As linhas do painel, na ordem do desenho. A pista em foco (`/pista <thread>`) vem primeiro. Cada pista: o
217
+ * cabecalho, as oito etapas numa linha, o texto de cada etapa que nao esta feita, o proximo passo do nucleo, o
218
+ * comando (para o dono, quando o modo pede o push dele) e o resultado do ultimo botao.
219
+ */
220
+ export function linhasDoPainel(q: PistaQuadro, opcoes: { agora: number; foco?: string | null; acoes?: Readonly<Record<string, PistaAcao>>;
221
+ botoes?: Readonly<Record<string, BotaoDaPista>> }): LinhaDoPainel[] {
222
+ const saida: LinhaDoPainel[] = []
223
+ const prontas = q.linhas.filter(l => l.proximo.disponivel).length
224
+ saida.push({ tipo: 'placar', texto: `Pista de Entrega · ${plural(q.linhas.length, 'entrega', 'entregas')}` +
225
+ `${prontas ? ` · ${plural(prontas, 'com botão', 'com botão')}` : ''} · ${haQuanto(q.lidoEm, opcoes.agora)}` })
226
+ if (q.erro) saida.push({ tipo: 'aviso', texto: `a última leitura falhou: ${q.erro}` })
227
+ for (const aviso of q.avisos) saida.push({ tipo: 'aviso', texto: aviso })
228
+ if (!q.linhas.length) {
229
+ saida.push({ tipo: 'vazio', texto: q.lidoEm === null ? 'lendo a pista do núcleo...'
230
+ : 'Nenhuma entrega na pista: a thread entra aqui depois do CHECK, com a branch publicada ou com PR.' })
231
+ return saida
232
+ }
233
+ const foco = opcoes.foco ?? null
234
+ const ordem = foco ? [...q.linhas.filter(l => l.thread === foco), ...q.linhas.filter(l => l.thread !== foco)] : q.linhas
235
+ for (const l of ordem) {
236
+ const emFoco = l.thread === foco ? ' (em foco)' : ''
237
+ saida.push({ tipo: 'cabecalho', thread: l.thread, texto: `${marcaDaPista(l)} ${l.thread} ${l.tag} ${l.branch}${emFoco}` })
238
+ saida.push({ tipo: 'etapas', thread: l.thread, texto: ` ${linhaDasEtapas(l)}` })
239
+ for (const e of l.etapas.filter(x => x.estado === 'pendente' || x.estado === 'falhou' || x.estado === 'nao-lida')) {
240
+ saida.push({ tipo: 'etapa', thread: l.thread, texto: ` ${NOME_DA_ETAPA[e.id]}: ${e.texto}` })
241
+ }
242
+ const proximo = ` próximo: ${l.proximo.texto}`
243
+ saida.push({ tipo: 'proximo', thread: l.thread, texto: proximo,
244
+ mostrado: { chave: `pista:${l.thread}`, thread: l.thread, texto: `${l.thread}: ${l.proximo.texto}` } })
245
+ // O botao so aparece com a acao declarada e sem outro clique da mesma thread rodando no nucleo.
246
+ const botao = opcoes.botoes?.[l.thread]
247
+ if (botao && opcoes.acoes?.[l.thread]?.estado !== 'rodando') saida.push({ tipo: 'botao', thread: l.thread, acao: botao.acao, texto: botao.rotulo })
248
+ if (l.proximo.comando && !l.proximo.disponivel) {
249
+ const quem = l.proximo.acao === 'publicar' ? 'no seu terminal' : 'comando do núcleo'
250
+ saida.push({ tipo: 'comando', thread: l.thread, texto: ` ${quem}: ${l.proximo.comando}` })
251
+ }
252
+ const acao = textoDaAcao(opcoes.acoes?.[l.thread])
253
+ if (acao) saida.push({ tipo: 'resultado', thread: l.thread, texto: ` ${acao}`,
254
+ mostrado: { chave: `pista:${l.thread}:acao`, thread: l.thread, texto: `${l.thread}: ${acao}` } })
255
+ }
256
+ return saida
257
+ }
258
+
259
+ /**
260
+ * As linhas da tela que um texto com `wrap="wrap"` ocupa em `colunas` celulas: quebra por palavra, e a palavra maior que
261
+ * a linha quebra no meio. A `folga` (uma coluna, por padrao) tira colunas da conta: com ela a estimativa sobra.
262
+ */
263
+ export function linhasDaTela(texto: string, colunas: number | undefined, folga = 1): number {
264
+ if (typeof colunas !== 'number' || !(colunas > folga)) return 1
265
+ const largura = colunas - folga
266
+ let linhas = 1
267
+ let usadas = 0
268
+ texto.split(' ').forEach((palavra, i) => {
269
+ let tamanho = Array.from(palavra).length
270
+ const espaco = i === 0 ? 0 : 1
271
+ if (usadas + espaco + tamanho <= largura) {
272
+ usadas += espaco + tamanho
273
+ return
274
+ }
275
+ if (usadas > 0) {
276
+ linhas++
277
+ usadas = 0
278
+ }
279
+ while (tamanho > largura) {
280
+ linhas++
281
+ tamanho -= largura
282
+ }
283
+ usadas = tamanho
284
+ })
285
+ return linhas
286
+ }
287
+
288
+ /**
289
+ * A altura de uma linha do painel na tela, sem e com a folga: `proximo` e `comando` quebram (`wrap="wrap"`); as outras
290
+ * truncam numa so.
291
+ */
292
+ function alturasNaTela(l: { tipo?: string; texto?: string }, colunas: number | undefined): [number, number] {
293
+ if (l.tipo !== 'proximo' && l.tipo !== 'comando') return [1, 1]
294
+ return [linhasDaTela(l.texto ?? '', colunas, 0), linhasDaTela(l.texto ?? '', colunas, 1)]
295
+ }
296
+
297
+ /**
298
+ * O que esta inteiro dentro da janela de rolagem do painel: so isso conta como mostrado ao dono (M5). A janela conta
299
+ * linhas da tela (`offset` e `bodyRows`), e a linha que quebra nas `colunas` do corpo ocupa mais de uma. Como a quebra
300
+ * do motor e estimada, cada linha tem o topo mais cedo (a conta sem folga) e o mais tarde (com folga): ela so conta com
301
+ * o mais cedo abaixo do alto da janela e o fim mais tarde acima do pe dela, nas duas bordas.
302
+ */
303
+ export function mostradosVisiveis(linhas: ReadonlyArray<{ tipo?: string; texto?: string; mostrado?: PistaMostrado }>,
304
+ janela: { offset: number; bodyRows: number } | undefined, colunas?: number): PistaMostrado[] {
305
+ if (!janela || !(janela.bodyRows > 0)) return []
306
+ const fim = janela.offset + janela.bodyRows
307
+ const vistos: PistaMostrado[] = []
308
+ let cedo = 0
309
+ let tarde = 0
310
+ for (const l of linhas) {
311
+ const [menor, maior] = alturasNaTela(l, colunas)
312
+ if (l.mostrado && cedo >= janela.offset && tarde + maior <= fim) vistos.push(l.mostrado)
313
+ cedo += menor
314
+ tarde += maior
315
+ }
316
+ return vistos
317
+ }
318
+
319
+ /** Os gestos do painel: atualizar a pista e, na T7.3, os botoes que o nucleo declara. */
320
+ export type Gestos = {
321
+ atualizar?: () => void
322
+ publicar?: (thread: string) => void
323
+ registrar?: (thread: string) => void
324
+ }
325
+
326
+ /**
327
+ * A arvore do painel com os elementos que a superficie resolveu (no register.tsx), uma linha da
328
+ * tela por elemento: so `Box`, `Text` e `Button`, no terminal e no desktop.
329
+ */
330
+ export function desenharPista(el: { Box: any; Text: any; Button: any }, q: PistaQuadro,
331
+ opcoes: { agora: number; foco?: string | null; acoes?: Readonly<Record<string, PistaAcao>>; botoes?: Readonly<Record<string, BotaoDaPista>> } & Gestos) {
332
+ const { Box, Text, Button } = el
333
+ return (
334
+ <Box flexDirection="column">
335
+ {linhasDoPainel(q, opcoes).map((l, i) => {
336
+ if (l.tipo === 'placar') {
337
+ return (
338
+ <Box key="placar" flexDirection="row">
339
+ <Text key="placar-texto" bold wrap="truncate">{l.texto}</Text>
340
+ <Button key="atualizar" label="Atualizar" onPress={() => opcoes.atualizar?.()} />
341
+ </Box>
342
+ )
343
+ }
344
+ if (l.tipo === 'cabecalho') return <Text key={`cabecalho:${l.thread}`} bold wrap="truncate">{l.texto}</Text>
345
+ if (l.tipo === 'botao' && l.thread && l.acao) {
346
+ const thread = l.thread
347
+ const apertar = l.acao === 'publicar' ? opcoes.publicar : opcoes.registrar
348
+ return <Button key={`${l.acao}:${thread}`} label={l.texto} onPress={() => apertar?.(thread)} />
349
+ }
350
+ if (l.tipo === 'comando') return <Text key={`comando:${l.thread}`} wrap="wrap">{l.texto}</Text>
351
+ if (l.tipo === 'proximo') return <Text key={`proximo:${l.thread}`} wrap="wrap">{l.texto}</Text>
352
+ return <Text key={`${l.tipo}:${l.thread ?? ''}:${i}`} dimColor wrap="truncate">{l.texto}</Text>
353
+ })}
354
+ </Box>
355
+ )
356
+ }