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